Bates stochastic volatility with jumps¶
BatesProcess combines Heston stochastic variance with independent,
constant-intensity lognormal jumps. BatesModel holds eight calibrated
parameters. BatesEngine prices European plain-vanilla calls and puts.
Parameters and limits¶
| Parameter | Meaning | Domain |
|---|---|---|
v0 |
Initial variance | Finite, positive |
kappa |
Variance mean-reversion speed | Finite, positive |
theta |
Long-run variance | Finite, positive |
sigma |
Volatility of variance | Finite, positive |
rho |
Spot/variance correlation | Finite, in [-1, 1] |
lambda |
Expected jumps per year | Finite, nonnegative |
nu |
Mean logarithmic jump multiplier | Finite |
delta |
Standard deviation of logarithmic jumps | Finite, nonnegative |
The spot drift is compensated by
lambda * (exp(nu + delta**2 / 2) - 1). Unrepresentable jump moments or
compensation are errors. Zero intensity gives the Heston limit; zero jump
volatility allows a deterministic jump multiplier. The Feller condition is
not mandatory.
This initial process is a pricing-only market carrier. Generic drift, diffusion and evolution are explicitly unsupported in Rust, and there is no Bates simulation facade. It is not accepted by the Heston path simulator. Deterministic-intensity and double-exponential models, finite-difference Bates pricing and additional evolution schemes remain out of scope.
The engine reuses the Heston characteristic function and Gauss-Laguerre
integration, adding the compensated lognormal-jump exponent in the two
Gatheral probability integrals. Integration orders are 1..192; Python
defaults to 144. The engine reports NPV only: requesting a missing
Greek returns an error, not zero. American and other non-European exercises
are not supported.
Price an option¶
"""Price a European Bates option and observe a live spot update."""
# itofin library
from itofin import Settings
from itofin.instruments import OptionType, VanillaOption
from itofin.models import BatesModel
from itofin.pricingengines import BatesEngine
from itofin.processes import BatesProcess
from itofin.quotes import SimpleQuote
from itofin.termstructures import FlatForward
from itofin.time import Date, DayCounter
today = Date(2, 10, 2026)
expiry = Date(2, 10, 2027)
settings = Settings()
settings.set_evaluation_date(today)
dc = DayCounter.actual365_fixed()
spot = SimpleQuote(100.0)
rate = SimpleQuote(0.03)
dividend = SimpleQuote(0.01)
process = BatesProcess(
spot,
FlatForward.from_quote(today, rate, dc),
FlatForward.from_quote(today, dividend, dc),
0.04, 1.5, 0.04, 0.3, -0.5, 0.7, -0.12, 0.18,
)
model = BatesModel(process)
engine = BatesEngine(model, integration_order=144)
option = VanillaOption(OptionType.Call, 100.0, expiry, settings)
option.set_bates_engine(engine)
print("Bates NPV:", option.npv())
spot.set_value(105.0)
print("After spot update:", option.npv())
print("Calibration parameter order:", model.params())
package main
import (
"errors"
"fmt"
"log"
itofin "github.com/benbenbang/libitofin/sdk/go"
)
func must[T any](value T, err error) T {
if err != nil {
panic(err)
}
return value
}
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() (err error) {
s, err := itofin.NewSession()
if err != nil {
return err
}
defer func() { err = errors.Join(err, s.Close()) }()
today := must(itofin.NewDate(2, 10, 2026))
expiry := must(today.AddDays(365))
settings := must(s.NewSettings())
if err = settings.SetEvaluationDate(today); err != nil {
return err
}
dc := must(s.Actual365Fixed())
spot := must(s.NewSimpleQuote(100))
rate := must(s.NewSimpleQuote(.03))
dividend := must(s.NewSimpleQuote(.01))
process := must(s.NewBatesProcess(itofin.BatesProcessConfig{
Spot: spot,
RiskFree: must(s.NewFlatForwardFromQuote(today, rate, dc)),
Dividend: must(s.NewFlatForwardFromQuote(today, dividend, dc)),
V0: .04, Kappa: 1.5, Theta: .04, Sigma: .3, Rho: -.5,
Lambda: .7, Nu: -.12, Delta: .18,
}))
model := must(s.NewBatesModel(process))
engine := must(s.NewBatesEngine(model, 144))
option := must(s.NewVanillaOption(itofin.Call, 100, expiry, settings))
if err = option.SetBatesEngine(engine); err != nil {
return err
}
fmt.Println("Bates NPV:", must(option.NPV()))
if err = spot.SetValue(105); err != nil {
return err
}
fmt.Println("After spot update:", must(option.NPV()))
fmt.Println("Calibration parameter order:", must(model.Params()))
return nil
}
//! Price a European Bates option and observe a live spot update.
use libitofin::errors::QlResult;
use libitofin::exercise::EuropeanExercise;
use libitofin::handle::Handle;
use libitofin::instrument::Instrument;
use libitofin::instruments::{PlainVanillaPayoff, VanillaOption};
use libitofin::interestrate::Compounding;
use libitofin::models::{BatesModel, CalibratedModelHolder};
use libitofin::option::OptionType;
use libitofin::pricingengines::vanilla::BatesEngine;
use libitofin::processes::BatesProcess;
use libitofin::quotes::{Quote, SimpleQuote};
use libitofin::settings::Settings;
use libitofin::shared::{Shared, shared, shared_mut};
use libitofin::termstructures::yields::FlatForward;
use libitofin::termstructures::yieldtermstructure::YieldTermStructure;
use libitofin::time::date::{Date, Month};
use libitofin::time::daycounters::actual365fixed::Actual365Fixed;
use libitofin::time::frequency::Frequency;
fn main() -> QlResult<()> {
let today = Date::new(2, Month::October, 2026);
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let spot = shared(SimpleQuote::new(100.0));
let curve = |rate| {
Handle::new(shared(FlatForward::with_rate(
today,
rate,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
};
let process = shared(BatesProcess::new(
curve(0.03),
curve(0.01),
Handle::new(spot.clone() as Shared<dyn Quote>),
0.04,
1.5,
0.04,
0.3,
-0.5,
0.7,
-0.12,
0.18,
)?);
let model = BatesModel::new(process)?;
let engine = shared_mut(BatesEngine::new(model.clone(), 144)?);
let mut option = VanillaOption::new(
shared(PlainVanillaPayoff::new(OptionType::Call, 100.0)),
shared(EuropeanExercise::new(today + 365)),
settings,
);
option.base_mut().set_pricing_engine(engine);
println!("Bates NPV: {}", option.npv()?);
spot.set_value(105.0);
println!("After spot update: {}", option.npv()?);
println!(
"Calibration parameter order: {:?}",
model.borrow().calibrated_model().params()
);
Ok(())
}
The process retains live spot and yield-curve inputs. The model retains the market, the engine retains the model and the option retains its engine. Updating a quote or model parameter invalidates the cached option value. Go objects must belong to the same session; closing the session ends access.
Inspect and calibrate¶
Process getters expose the eight parameters, current spot, initial
[spot, variance] state and date-to-time conversion. Python uses lambda_()
because lambda is a keyword; Go uses Lambda().
model.params() / model.Params() and set_params / SetParams use this
order, which differs from the process constructor:
Parameter replacement validates the entire array before mutation. Invalid lengths, nonfinite values and domain errors leave the current model unchanged.
Calibration uses existing HestonModelHelper instruments with a Bates
engine, existing optimization methods and EndCriteria. Constraint, weights
and fixed-parameter options follow the same contract as Heston calibration,
but the fixed mask has eight entries in the order above. Model getters
read fitted values after calibration. Calibration fits risk-neutral prices;
it does not infer physical return forecasts.
- Weights must be finite and nonnegative; omitted weights default to one.
- Nonfinite final residuals or costs are errors, not successful fits.
- Calibration is stateful, not transactional: a failed fit may retain trial
parameters and the installed helper engine. Only
set_paramsis atomic.
Numerical evidence¶
The independent oracle records QuantLib price and calibration provenance, quadrature orders and fixed tolerances. Exact zero-intensity cases use independent Heston reference prices because upstream Bates model constraints exclude zero intensity. Finite quadrature orders need not agree with one another; reference tests compare the same documented order.