stochastic-rs
Concepts

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 onlyBSMPricer holds a volatility and a cost-of-carry convention, HestonPricer holds (v0,κ,θ,σ,ρ)(v_0,\kappa,\theta,\sigma,\rho). 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 eqτe^{-q\tau}. Two families must override it or the default is a silent mispricing:

  • Non-standard cost of carryBSMPricer, AsianPricer and Merton1976Pricer carry at e(br)τe^{(b-r)\tau}, which equals eqτe^{-q\tau} only when b=rqb = r - q.
  • American exerciseBjerksundStensland2002Pricer, SnellEnvelopePricer and FiniteDifferencePricer price 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 gets vol_surface(s, r, q, strikes, maturities). A ModelPricer that 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) implement ShortRatePricer instead — 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. MargrabePricer is an exchange option with no strike at all; KirkSpreadPricer strikes against a spread of two forwards, so its query is spread_call_put(f1, f2, x, r, tau); the basket and rainbow pricers take N legs. A shared signature would need an Option<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 and ci_95() — rather than a bare number. pricer_registry.rs records the state of each.

    KirkSpreadPricer is the one member whose methods carry a spread_ prefix. Its query is five f64s, 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_call takes ArrayView1 legs, MargrabePricer::price has 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 τ\tau 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 τ\tau 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

On this page