Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 96 additions & 5 deletions crates/itofin-py/python/itofin/termstructures.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,12 @@ class OptionletVolatilityStructure:
def black_variance(
self, option_tenor: Period, strike: float, extrapolate: bool = False
) -> float: ...
def allows_extrapolation(self) -> bool: ...
def enable_extrapolation(self) -> None:
"""A stripped surface ends at its last optionlet fixing, so a cap whose
own last caplet fixes there queries the boundary."""
...
def disable_extrapolation(self) -> None: ...
def displacement(self) -> float:
"""The lognormal shift applied to forwards and strikes. This is what
BlackCapFloorEngine checks a caller-supplied displacement against."""
Expand All @@ -625,7 +631,7 @@ class ConstantOptionletVolatility(OptionletVolatilityStructure):

Both constructors pin the reference date, so every query's option time runs
from reference_date rather than the evaluation date. The moving (floating
reference date) forms are not exposed."""
reference date) forms are not exposed; tracked as #627."""

def __init__(
self,
Expand Down Expand Up @@ -661,10 +667,14 @@ class CapFloorTermVolSurface:
OptionletVolatilityStructure.

volatilities is a row per option tenor and a column per strike; both axes
must be strictly increasing. The moving (floating reference date)
constructors are not exposed - both forms here pin the reference date, so
every query's option time runs from reference_date rather than the
evaluation date."""
must be strictly increasing.

All four constructors are exposed. __init__ and with_quotes pin the
reference date, so every query's option time runs from reference_date rather
than the evaluation date. moving and moving_with_quotes float it
settlement_days off the evaluation date, and are what the optionlet
stripping pipeline runs on: StrippedOptionletAdapter reads its settlement
days back off this surface, and a pinned-reference surface has none."""

def __init__(
self,
Expand Down Expand Up @@ -693,6 +703,34 @@ class CapFloorTermVolSurface:
"""Reads each node from the caller's quote; a later set_value rebuilds
the interpolation and notifies the surface's observers."""
...
@staticmethod
def moving(
settlement_days: int,
calendar: Calendar,
business_day_convention: BusinessDayConvention,
option_tenors: list[Period],
strikes: list[float],
volatilities: list[list[float]],
day_counter: DayCounter,
settings: Settings,
) -> CapFloorTermVolSurface:
"""The reference date floats settlement_days business days off the
evaluation date. This is the form OptionletStripper1 and
StrippedOptionletAdapter need."""
...
@staticmethod
def moving_with_quotes(
settlement_days: int,
calendar: Calendar,
business_day_convention: BusinessDayConvention,
option_tenors: list[Period],
strikes: list[float],
volatilities: list[list[SimpleQuote]],
day_counter: DayCounter,
settings: Settings,
) -> CapFloorTermVolSurface:
"""The floating-reference surface over the caller's quotes."""
...
def volatility(
self, option_tenor: Period, strike: float, extrapolate: bool = False
) -> float: ...
Expand All @@ -705,3 +743,56 @@ class CapFloorTermVolSurface:
"""length is a year fraction off the reference date in the surface's own
day count."""
...

class OptionletStripper1:
"""Bootstraps caplet volatilities out of a market cap/floor term-volatility
surface.

Not itself a volatility surface: it produces a grid of caplet volatilities
that StrippedOptionletAdapter interpolates into one. Stripping is lazy and
cached, and re-runs only when a surface quote or the index changes.

term_vol_surface must come from CapFloorTermVolSurface.moving or
moving_with_quotes; a pinned-reference surface carries no settlement days
and fails the adapter. VolatilityType.Normal is deferred (#440/#577) and
fails at the strip, not at construction."""

def __init__(
self,
term_vol_surface: CapFloorTermVolSurface,
ibor_index: Euribor,
volatility_type: VolatilityType,
accuracy: float = 1e-6,
max_iter: int = 100,
displacement: float = 0.0,
discount: YieldTermStructure | None = None,
optionlet_frequency: Period | None = None,
) -> None:
"""discount None falls back to the index's own forwarding curve;
optionlet_frequency None uses the index tenor as the caplet step."""
...
def switch_strike(self) -> float:
"""The mean at-the-money caplet rate, which decides whether each strike
is stripped out of caps or out of floors. The first call strips."""
...
def atm_optionlet_rates(self) -> list[float]:
"""The at-the-money forward rate of each caplet, one per maturity."""
...

class StrippedOptionletAdapter(OptionletVolatilityStructure):
"""Serves a stripper's caplet volatility grid as an
OptionletVolatilityStructure: linear in strike within each maturity, then
linear across maturities.

This closes the cap/floor volatility loop - a BlackCapFloorEngine on this
surface reprices the caps the term volatilities were quoted on. The
reference date floats off the evaluation date carried by settings, advanced
by the term-volatility surface's settlement days. The surface ends at the
last caplet fixing, so pricing a cap that reaches it wants
enable_extrapolation()."""

def __init__(self, stripper: OptionletStripper1, settings: Settings) -> None:
"""Strips eagerly, to snapshot the strike domain and maximum date.
Raises ItofinError on a stripper whose term-volatility surface carries
no settlement days."""
...
88 changes: 82 additions & 6 deletions crates/itofin-py/src/capfloortermvol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@
//! `SabrSmileSection` precedent). A base can be introduced if a second one ever
//! lands.
//!
//! Deferred (visible): the two MOVING constructors (`moving` /
//! `moving_from_matrix`, whose reference date floats `settlement_days` off the
//! evaluation date) are not exposed. Both fixed-reference forms are, so a
//! caller can pin the reference date and still read the surface off live
//! quotes, which is the path the stripper consumes.
//! All four core constructors are exposed: a fixed reference date or one
//! floating `settlement_days` off the evaluation date, each over fixed
//! volatilities or over the caller's quotes. The moving pair is not optional
//! polish here - it is what the optionlet stripping pipeline runs on. The
//! adapter that serves the stripped surface reads its settlement days from this
//! surface (`strippedoptionletadapter.rs:87` through
//! `optionletstripper.rs:241`), and a fixed-reference term structure has none,
//! so a surface built by the fixed forms fails the adapter with `"settlement
//! days not provided for this instance"`.

use crate::PyQlError;
use crate::market::PySimpleQuote;
use crate::settings::PySettings;
use crate::time::{PyBusinessDayConvention, PyCalendar, PyDate, PyDayCounter, PyPeriod};
use libitofin::handle::Handle;
use libitofin::math::matrix::Matrix;
Expand Down Expand Up @@ -115,6 +120,78 @@ impl PyCapFloorTermVolSurface {
Ok(PyCapFloorTermVolSurface { inner })
}

/// A surface whose reference date floats `settlement_days` business days
/// off the evaluation date, over fixed volatilities.
///
/// This is the form the optionlet stripping pipeline needs: unlike the
/// pinned-reference constructors, it carries the settlement days
/// `StrippedOptionletAdapter` reads back off the stripper.
#[staticmethod]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (settlement_days, calendar, business_day_convention, option_tenors, strikes, volatilities, day_counter, settings))]
fn moving(
settlement_days: u32,
calendar: &PyCalendar,
business_day_convention: &PyBusinessDayConvention,
option_tenors: Vec<PyRef<'_, PyPeriod>>,
strikes: Vec<f64>,
volatilities: Vec<Vec<f64>>,
day_counter: &PyDayCounter,
settings: &PySettings,
) -> PyResult<Self> {
let volatilities = matrix_from_rows(&volatilities)?;
let inner = shared(
CapFloorTermVolSurface::moving_from_matrix(
settlement_days,
calendar.inner(),
business_day_convention.inner(),
tenors(&option_tenors),
strikes,
&volatilities,
day_counter.inner(),
settings.inner(),
)
.map_err(PyQlError::from)?,
);
Ok(PyCapFloorTermVolSurface { inner })
}

/// The same floating-reference surface reading each node from the caller's
/// quote.
#[staticmethod]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (settlement_days, calendar, business_day_convention, option_tenors, strikes, volatilities, day_counter, settings))]
fn moving_with_quotes(
settlement_days: u32,
calendar: &PyCalendar,
business_day_convention: &PyBusinessDayConvention,
option_tenors: Vec<PyRef<'_, PyPeriod>>,
strikes: Vec<f64>,
volatilities: Vec<Vec<PyRef<'_, PySimpleQuote>>>,
day_counter: &PyDayCounter,
settings: &PySettings,
) -> PyResult<Self> {
check_grid(&volatilities)?;
let volatilities: Vec<Vec<Handle<dyn Quote>>> = volatilities
.iter()
.map(|row| row.iter().map(|quote| quote.handle()).collect())
.collect();
let inner = shared(
CapFloorTermVolSurface::moving(
settlement_days,
calendar.inner(),
business_day_convention.inner(),
tenors(&option_tenors),
strikes,
volatilities,
day_counter.inner(),
settings.inner(),
)
.map_err(PyQlError::from)?,
);
Ok(PyCapFloorTermVolSurface { inner })
}

/// The cap volatility for a cap tenor and strike.
#[pyo3(signature = (option_tenor, strike, extrapolate = false))]
fn volatility(&self, option_tenor: &PyPeriod, strike: f64, extrapolate: bool) -> PyResult<f64> {
Expand Down Expand Up @@ -147,7 +224,6 @@ impl PyCapFloorTermVolSurface {
impl PyCapFloorTermVolSurface {
/// The wrapped core surface for the optionlet-stripper facade, which takes
/// the concrete type rather than a handle.
#[allow(dead_code)]
pub(crate) fn inner(&self) -> Shared<CapFloorTermVolSurface> {
Shared::clone(&self.inner)
}
Expand Down
7 changes: 6 additions & 1 deletion crates/itofin-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ use hullwhite::{PyEuribor, PyHullWhite, PySwaptionHelper};
use libitofin::errors::QlError;
use market::{PyBlackScholesProcess, PySimpleQuote};
use option::{PyOptionType, PyVanillaOption};
use optionletvol::{PyConstantOptionletVolatility, PyOptionletVolatilityStructure};
use optionletvol::{
PyConstantOptionletVolatility, PyOptionletStripper1, PyOptionletVolatilityStructure,
PyStrippedOptionletAdapter,
};
use pyo3::create_exception;
use pyo3::exceptions::PyException;
use pyo3::prelude::*;
Expand Down Expand Up @@ -155,6 +158,8 @@ fn itofin(m: &Bound<'_, PyModule>) -> PyResult<()> {
termstructures.add_class::<PyOptionletVolatilityStructure>()?;
termstructures.add_class::<PyConstantOptionletVolatility>()?;
termstructures.add_class::<PyCapFloorTermVolSurface>()?;
termstructures.add_class::<PyOptionletStripper1>()?;
termstructures.add_class::<PyStrippedOptionletAdapter>()?;

let processes = PyModule::new(py, "processes")?;
processes.add_class::<PyBlackScholesProcess>()?;
Expand Down
Loading
Loading