Volatility Surfaces
Volatility objects are built from quotes exactly like curves: a configuration lists quote identifiers, a builder resolves them and produces an interpolated object stored in the ConstructedElementStore. Source: src/volatility/.
Configurations
pub struct VolatilitySurfaceConfiguration {
market_index: MarketIndex, // required
volatility_type: VolatilityType, // default Black
smile_type: SmileType, // default Strike
quotes: Vec<String>, // expiry × strike pillars
}
pub struct VolatilityCubeConfiguration { /* same fields; quotes are expiry × tenor × strike */ }
VolatilitySurfaceConfiguration::new(market_index, volatility_type, smile_type, quotes)
VolatilityCubeConfiguration::new(market_index, volatility_type, smile_type, quotes)
{
"market_index": "SOFR",
"volatility_type": "Black",
"smile_type": "Strike",
"quotes": [
"CapletFloorlet_USD_SOFR_3M_6M_Absolute_0.035_Straddle_Black",
"CapletFloorlet_USD_SOFR_3M_6M_Absolute_0.045_Straddle_Black",
"CapletFloorlet_USD_SOFR_3M_1Y_Absolute_0.045_Straddle_Black"
]
}
| Enum | Variants | Meaning |
|---|---|---|
VolatilityType | Black, Normal | lognormal (Black-76) or Bachelier quoting |
SmileType | Strike, Delta, LogMoneyness | what the second axis (key) means |
Strike | Absolute(f64), Atm, Relative(f64) | resolve(forward) returns K, F, or F + spread |
Quote identifiers and axes
| Identifier | Axes |
|---|---|
CapletFloorlet_USD_SOFR_3M_6M_Absolute_0.045_Straddle_Black | index tenor 3M, expiry 6M, strike 0.045, strategy Straddle, vol type Black |
Swaption_CLP_ICP_1Y_2Y_Absolute_0.045_Black | expiry 1Y, swap tenor 2Y, strike 0.045 (optional PayFreq_RecvFreq segments before the strike) |
FxCall_USDCLP_6M_Absolute_950, FxPut_... | expiry, strike (FX surfaces) |
EquityCall_USD_AAPL_1Y_Absolute_150 | expiry, strike (equity surfaces) |
Caplet quotes populate a surface (expiry × strike); swaption quotes populate a cube (expiry × tenor × strike).
Builders
let surfaces: HashMap<MarketIndex, VolatilitySurfaceElement> =
VolatilitySurfaceBuilder::new(surface_specs).build("e_store, Level::Mid)?;
let cubes: HashMap<MarketIndex, VolatilityCubeElement> =
VolatilityCubeBuilder::new(cube_specs).build("e_store, Level::Mid)?;
Inside PricingContext::initialize() the same builders run after the curves (with_volatility_surface_configurations, with_volatility_cube_configurations). Each quote value becomes a DualFwd leaf, so option sensitivities are reported per volatility quote identifier.
Querying
InterpolatedVolatilitySurface<T> implements the VolatilitySurface trait:
| Method | Notes |
|---|---|
volatility_from_period(expiry: Period, key: f64) -> Result<T> | bilinear in (expiry year fraction, key), flat extrapolation |
volatility_from_date(date: Date, key: f64) -> Result<T> | converts the date to a period first |
volatility_type(), smile_type(), market_index(), reference_date() |
InterpolatedVolatilityCube<T> adds the tenor axis: volatility_from_period(expiry, tenor, key) (trilinear).
let elem = &surfaces[&MarketIndex::SOFR];
let vol = elem.surface().volatility_from_period(Period::from_str("9M")?, 0.0325)?;
println!("9M / 3.25% Black vol = {:.4}", vol.value());
examples/volatilitysurface (cargo run -p volatilitysurface) builds the SOFR caplet surface from examples/volatilitysurface/data and prints interpolated vols for a grid of expiry/strike points plus the volatility and smile types.
FX orientation
FX surfaces are stored for one pair direction. OrientedFxVolSurface::new(&element, inverted: bool) exposes volatility_from_date(expiry, strike) and, when inverted, maps the strike as \(K \to 1/K\) so the same surface serves both USDCLP and CLPUSD trades. FxOptionPricer chooses the orientation from the trade’s pair.
Volatility sources for models
Models and simulations do not take raw surfaces; they take a VolatilitySourceConfiguration:
pub enum VolatilitySourceConfiguration {
Constant { value: f64 },
Surface { market_index: MarketIndex, key: f64 },
Cube { market_index: MarketIndex, tenor: Period, key: f64 },
Calibrated(ModelCalibrationConfiguration),
}
pub struct ModelCalibrationConfiguration {
source: CalibrationSource, // Surface { market_index } | Cube { market_index }
quote_ids: Vec<String>, // instruments to fit
strike: Option<Strike>, // e.g. "Atm" overrides the quoted strike
alpha: f64, // mean reversion used while fitting
}
{ "Constant": { "value": 0.2 } }
{ "Surface": { "market_index": "SOFR", "key": 0.03 } }
{ "Cube": { "market_index": "ICP", "tenor": "1Y", "key": 0.045 } }
{ "Calibrated": { "source": { "Surface": { "market_index": "SOFR" } },
"quote_ids": ["CapletFloorlet_USD_SOFR_3M_1Y_Absolute_0.045_Straddle_Black"],
"strike": "Atm", "alpha": 0.1 } }
bootstrap_black_term_volatility(&config, &store, reference_date, day_counter) -> Result<PiecewiseConstantVolatility<f64>> reads the implied vol \(\sigma_i\) at each calibration quote and strips a piecewise-constant forward volatility so that \(\int_0^{T_i}\sigma(s)^2\,ds = \sigma_i^2 T_i\) at every pillar; negative forward variance is rejected as arbitrageable. PiecewiseConstantVolatility::new(schedule) requires a non-empty, strictly increasing (year_fraction, sigma) schedule and implements TimeDependentVolatility::vol(t).
Hull-White and LGM use the Calibrated variant to fit their short-rate sigma schedule instead; see Hull-White.