ModelPricer
The pricing trait — the struct holds model parameters and each call carries its own (s, k, r, q, tau) query, so one model prices a whole grid.
ModelPricer
The single-underlying pricing surface. A plain f64 trait — not generic
over T: FloatExt.
pub trait ModelPricer {
fn price_call(&self, s: f64, k: f64, r: f64, q: f64, tau: f64) -> f64;
// default: put-call parity from price_call
fn price_put(&self, s: f64, k: f64, r: f64, q: f64, tau: f64) -> f64 {
let call = self.price_call(s, k, r, q, tau);
call - s * (-q * tau).exp() + k * (-r * tau).exp()
}
// default: dispatches to price_call / price_put
fn price_option(&self, s: f64, k: f64, r: f64, q: f64, tau: f64, option_type: OptionType) -> f64 {
match option_type {
OptionType::Call => self.price_call(s, k, r, q, tau),
OptionType::Put => self.price_put(s, k, r, q, tau),
}
}
}Model, then query
The struct holds model parameters only — BSMPricer holds a
volatility and a cost-of-carry convention, HestonPricer holds
. Nothing about the contract (spot,
strike, rate, dividend yield, maturity) is baked in; every price query
passes its own (s, k, r, q, tau).
That is what makes vectorized pricing across a strike/maturity grid —
vol-surface construction, calibration — cheap: one model instance, many
price_option calls, no &dyn, no Box<dyn>.
Half of the implementors also expose an inherent call_put(s, k, r, q, tau) -> (f64, f64) returning both legs from one evaluation, which is
what their trait methods project from.
Override price_put unless parity really holds
The default price_put recomposes the put from the call by European
put-call parity with a carry factor of exactly . Two families
must override it or the default is a silent mispricing:
- Non-standard cost of carry —
BSMPricer,AsianPricerandMerton1976Pricercarry at , which equals only when . - American exercise —
BjerksundStensland2002Pricer,SnellEnvelopePricerandFiniteDifferencePricerprice a put carrying an early-exercise premium the call does not, so European parity is not an approximation but the wrong model.
Exercise style is the implementor's choice, not the trait's: what
ModelPricer fixes is the query shape, not the exercise right.
Blanket implementations
Two blanket impls do most of the work:
impl<T: FourierModelExt> ModelPricer for T— any model with a characteristic function gets priced by Gil-Pelaez quadrature for free.impl<T: VanillaEuropeanCall + ?Sized> ModelSurface for T— any model that asserts it really is a European vanilla call getsvol_surface(s, r, q, strikes, maturities). AModelPricerthat is not one (a digital, an American put) has no method there at all.
ToModel bridges a calibration result to the concrete ModelPricer it
produces:
pub trait ToModel {
type Model: ModelPricer;
fn to_model(&self, r: f64, q: f64) -> Self::Model;
}Bonds and multi-asset payoffs
ModelPricer's query has one underlying and one strike, so two families
sit outside it deliberately:
-
Short-rate bonds (
Vasicek,Cir,HullWhite) implementShortRatePricerinstead — they price off a short rate and a tenor, not a spot and a strike. -
Multi-asset payoffs (
MargrabePricer,KirkSpreadPricer,McSpreadPricer, the basket and rainbow pricers) carry no pricing trait.MargrabePriceris an exchange option with no strike at all;KirkSpreadPricerstrikes against a spread of two forwards, so its query isspread_call_put(f1, f2, x, r, tau); the basket and rainbow pricers take N legs. A shared signature would need anOption<f64>strike and a variable-length underlying list — a dishonest shape for all of them.All eight follow the model/query split through inherent methods: each holds its volatilities and correlation and takes the spots, strike, rate, yields and maturity per call, and every Monte Carlo member returns an
McEstimate— the mean plus its standard error andci_95()— rather than a bare number.pricer_registry.rsrecords the state of each.KirkSpreadPriceris the one member whose methods carry aspread_prefix. Its query is fivef64s, exactly the arity of the(s, k, r, q, tau)the single-underlying pricers take, and it disagrees with them in four of the five positions — so under the plain names a misdirected call compiled and returned a finite, well-scaled, wrong price. The other seven are separated by their signatures already (GeometricBasketPricer::price_calltakesArrayView1legs,MargrabePricer::pricehas no strike), so they keep the plain names.
The path-dependent family (barriers, lookbacks, cliquets, autocallables, Bermudans, chooser, compound, variance/volatility swaps) is trait-less for the same reason: each carries its own contract schedule.
Greeks
A pricer's Greeks follow the same model/query split as its prices:
inherent methods taking the query, not a trait. BSMPricer,
HestonPricer, Merton1976Pricer, CashOrNothingPricer and
AssetOrNothingPricer each expose
greeks(s, k, r, q, tau, option_type) -> Greeks — one identical signature
across all five — alongside per-Greek accessors on the same shape. Greeks
is a plain f64 struct of nine named fields (delta, gamma, vega,
theta, rho, vanna, charm, volga, veta), NaN where the pricer
exposes nothing.
GreeksExt is not that interface. Its accessors take &self and
nothing else, so only an estimator that already owns its query can
implement it: the Monte Carlo Malliavin pair GbmMalliavinGreeks and
HestonMalliavinGreeks, whose greeks() override runs a single simulation
so the returned estimators share their paths — see the
greeks-pattern SKILL
for that single-pass protocol. It is reached via
stochastic_rs::traits::GreeksExt, not the prelude.
TimeExt — dates to tau
ModelPricer takes tau in years. When the contract is given as dates
instead, TimeExt::tau_or_from_dates() derives from either an
explicit tau or an (eval, expiration) pair under a day-count
convention, and tau_with_dcc(dcc) overrides the convention explicitly.
Convert at the call site, then price.
Instruments implement it; pricers do not. EuropeanOption and
DigitalOption own a maturity and resolve it; a pricer holds model state and
receives as a query argument, so it has no dates to resolve. The
design that retired PricerExt also suggested moving this trait's role into
the calendar module — that is dropped rather than pending. The date
arithmetic is already there (DayCountConvention::year_fraction, which both
derivations call); what TimeExt adds is the instrument-side question of
which maturity slot is populated, and that belongs on the instrument.
What replaced PricerExt
Before 3.0 a second trait, PricerExt: TimeExt, bundled the market data
and the strike into the pricer and exposed calculate_call_put() /
calculate_price() / implied_volatility() with no arguments. It is
removed: pricing a second strike meant constructing a second pricer,
which is exactly what made grid pricing expensive.
Every former implementor moved to ModelPricer, to ShortRatePricer, or
— for KirkSpreadPricer — to inherent methods carrying its own natural
query. calculate_call_put() becomes call_put(...),
calculate_price() becomes price_call(...), and the four pricers that
genuinely invert a price to a vol keep implied_volatility as an
inherent method. KirkSpreadPricer is the exception on naming as well:
its two-forward query took the same five f64s, so its methods are
spread_call_put / spread_call / spread_put.
For the per-pricer mapping, each type's own rustdoc names what its model
state is and what its query is; pricer_registry.rs in the crate's test
directory is the compile-checked inventory of which pricer carries which
trait, including the ones that deliberately carry none.
See also
- Calibrator and CalibrationResult
- Quant catalog — pricers, calibrators, vol surface, risk
DistributionExt
Closed-form pdf, cdf, characteristic function, and moments for every distribution — 18 of 19 closed-form, with five named unimplemented moments.
Seeding & RNG
The uniform `new(args, &seed)` constructor pattern, the `SeedExt` strategies (`Unseeded` / `Deterministic`), in-place reseeding, and dual-stream RNG.