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
44 changes: 44 additions & 0 deletions crates/itofin-py/python/itofin/termstructures.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -424,3 +424,47 @@ class ConstantSwaptionVolatility(SwaptionVolatilityStructure):
"""Reads the volatility from the caller's quote; a later set_value
notifies the surface's observers."""
...

class SwaptionVolatilityMatrix(SwaptionVolatilityStructure):
"""An at-the-money volatility grid, bilinear over an option-tenor x
swap-tenor lattice.

Every grid is a row per option tenor and a column per swap tenor; shifts,
when given, must match that shape, and None means all-zero shifts. The grid
is at the money, so a query's strike is range-checked and then ignored.
flat_extrapolation clamps a query past the grid to the nearest edge or
corner vol instead of extending the boundary surface."""

def __init__(
self,
reference_date: Date,
calendar: Calendar,
business_day_convention: BusinessDayConvention,
option_tenors: list[Period],
swap_tenors: list[Period],
volatilities: list[list[float]],
day_counter: DayCounter,
volatility_type: VolatilityType,
shifts: list[list[float]] | None = None,
flat_extrapolation: bool = False,
) -> None:
"""Pins the reference date, so every query's option time runs from
reference_date rather than the evaluation date."""
...
@staticmethod
def moving(
calendar: Calendar,
business_day_convention: BusinessDayConvention,
option_tenors: list[Period],
swap_tenors: list[Period],
volatilities: list[list[SimpleQuote]],
day_counter: DayCounter,
volatility_type: VolatilityType,
settings: Settings,
shifts: list[list[float]] | None = None,
flat_extrapolation: bool = False,
) -> SwaptionVolatilityMatrix:
"""A grid whose reference date floats off the evaluation date, reading
each node from the caller's quote: a later set_value on any of them
rebuilds the interpolation and notifies the grid's observers."""
...
6 changes: 5 additions & 1 deletion crates/itofin-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ use settings::PySettings;
use swap::{PySwapType, PyVanillaSwap};
use swaption::{PyEuropeanExercise, PySettlementMethod, PySettlementType, PySwaption};
use swaptionengine::{PyBlackSwaptionEngine, PyCashAnnuityModel};
use swaptionvol::{PyConstantSwaptionVolatility, PySwaptionVolatilityStructure, PyVolatilityType};
use swaptionvol::{
PyConstantSwaptionVolatility, PySwaptionVolatilityMatrix, PySwaptionVolatilityStructure,
PyVolatilityType,
};
use time::{
PyBusinessDayConvention, PyCalendar, PyDate, PyDayCounter, PyFrequency, PyPeriod, PySchedule,
};
Expand Down Expand Up @@ -130,6 +133,7 @@ fn itofin(m: &Bound<'_, PyModule>) -> PyResult<()> {
termstructures.add_class::<PySwaptionVolatilityStructure>()?;
termstructures.add_class::<PyVolatilityType>()?;
termstructures.add_class::<PyConstantSwaptionVolatility>()?;
termstructures.add_class::<PySwaptionVolatilityMatrix>()?;

let processes = PyModule::new(py, "processes")?;
processes.add_class::<PyBlackScholesProcess>()?;
Expand Down
178 changes: 175 additions & 3 deletions crates/itofin-py/src/swaptionvol.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Facades for the swaption volatility stack: the [`PySwaptionVolatilityStructure`]
//! base, the [`PyVolatilityType`] flag and the constant surface
//! [`PyConstantSwaptionVolatility`].
//! base, the [`PyVolatilityType`] flag, the constant surface
//! [`PyConstantSwaptionVolatility`] and the at-the-money grid
//! [`PySwaptionVolatilityMatrix`].
//!
//! The base holds the erased `Handle<dyn SwaptionVolatilityStructure>` and
//! exposes the queries every concrete surface inherits; concrete surfaces
Expand All @@ -17,12 +18,17 @@

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;
use libitofin::quotes::Quote;
use libitofin::shared::{Shared, shared};
use libitofin::termstructures::volatility::{
ConstantSwaptionVolatility, SwaptionVolatilityStructure, VolatilityType,
ConstantSwaptionVolatility, SwaptionVolatilityMatrix, SwaptionVolatilityStructure,
VolatilityType,
};
use libitofin::time::period::Period;
use pyo3::prelude::*;

/// Python `SwaptionVolatilityStructure`: the shared base for every swaption
Expand Down Expand Up @@ -215,3 +221,169 @@ impl PyConstantSwaptionVolatility {
)
}
}

/// Python `SwaptionVolatilityMatrix`: the at-the-money volatility grid,
/// bilinear over an option-tenor x swap-tenor lattice
/// (`termstructures::volatility::SwaptionVolatilityMatrix`).
///
/// Extends [`PySwaptionVolatilityStructure`] and supplies only the
/// constructors. Every grid is a row per option tenor and a column per swap
/// tenor; `shifts`, when given, must match that shape, and `None` means
/// all-zero shifts. The grid is at the money, so a query's strike argument is
/// range-checked and then ignored. `flat_extrapolation` selects C++'s
/// `flatExtrapolation = true`, under which a query past the grid clamps to the
/// nearest edge or corner vol instead of extending the boundary surface.
#[pyclass(name = "SwaptionVolatilityMatrix", extends = PySwaptionVolatilityStructure, unsendable)]
pub struct PySwaptionVolatilityMatrix;

#[pymethods]
impl PySwaptionVolatilityMatrix {
/// A grid on a pinned reference date over fixed volatilities: every query's
/// option time runs from `reference_date`, not from the evaluation date.
#[new]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (reference_date, calendar, business_day_convention, option_tenors, swap_tenors, volatilities, day_counter, volatility_type, shifts = None, flat_extrapolation = false))]
fn new(
reference_date: &PyDate,
calendar: &PyCalendar,
business_day_convention: &PyBusinessDayConvention,
option_tenors: Vec<PyRef<'_, PyPeriod>>,
swap_tenors: Vec<PyRef<'_, PyPeriod>>,
volatilities: Vec<Vec<f64>>,
day_counter: &PyDayCounter,
volatility_type: PyVolatilityType,
shifts: Option<Vec<Vec<f64>>>,
flat_extrapolation: bool,
) -> PyResult<PyClassInitializer<Self>> {
let volatilities = matrix_from_rows(&volatilities, "volatility")?;
let shifts = match shifts {
Some(rows) => matrix_from_rows(&rows, "shift")?,
None => Matrix::new(),
};
let build = if flat_extrapolation {
SwaptionVolatilityMatrix::new_flat
} else {
SwaptionVolatilityMatrix::new
};
let surface = shared(
build(
reference_date.inner(),
calendar.inner(),
business_day_convention.inner(),
tenors(&option_tenors),
tenors(&swap_tenors),
&volatilities,
day_counter.inner(),
volatility_type.inner(),
&shifts,
)
.map_err(PyQlError::from)?,
) as Shared<dyn SwaptionVolatilityStructure>;
Ok(
PyClassInitializer::from(PySwaptionVolatilityStructure::from_handle(Handle::new(
surface,
)))
.add_subclass(PySwaptionVolatilityMatrix),
)
}

/// A grid whose reference date floats off `settings`' evaluation date (zero
/// settlement days), reading each node from the caller's quote: a later
/// `set_value` on any of them rebuilds the interpolation and notifies the
/// grid's observers.
#[staticmethod]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (calendar, business_day_convention, option_tenors, swap_tenors, volatilities, day_counter, volatility_type, settings, shifts = None, flat_extrapolation = false))]
fn moving(
py: Python<'_>,
calendar: &PyCalendar,
business_day_convention: &PyBusinessDayConvention,
option_tenors: Vec<PyRef<'_, PyPeriod>>,
swap_tenors: Vec<PyRef<'_, PyPeriod>>,
volatilities: Vec<Vec<PyRef<'_, PySimpleQuote>>>,
day_counter: &PyDayCounter,
volatility_type: PyVolatilityType,
settings: &PySettings,
shifts: Option<Vec<Vec<f64>>>,
flat_extrapolation: bool,
) -> PyResult<Py<Self>> {
check_grid(&volatilities, "volatility")?;
let volatilities: Vec<Vec<Handle<dyn Quote>>> = volatilities
.iter()
.map(|row| row.iter().map(|quote| quote.handle()).collect())
.collect();
let shifts = match shifts {
Some(rows) => {
check_grid(&rows, "shift")?;
rows
}
None => Vec::new(),
};
let build = if flat_extrapolation {
SwaptionVolatilityMatrix::moving_flat
} else {
SwaptionVolatilityMatrix::moving
};
let surface = shared(
build(
calendar.inner(),
business_day_convention.inner(),
tenors(&option_tenors),
tenors(&swap_tenors),
volatilities,
day_counter.inner(),
volatility_type.inner(),
shifts,
settings.inner(),
)
.map_err(PyQlError::from)?,
) as Shared<dyn SwaptionVolatilityStructure>;
Py::new(
py,
PyClassInitializer::from(PySwaptionVolatilityStructure::from_handle(Handle::new(
surface,
)))
.add_subclass(PySwaptionVolatilityMatrix),
)
}
}

/// The wrapped core periods, in order.
fn tenors(periods: &[PyRef<'_, PyPeriod>]) -> Vec<Period> {
periods.iter().map(|period| period.inner()).collect()
}

/// The shape check both grid constructors share, rejecting an empty or ragged
/// `list[list[...]]` before it reaches the core's dimension checks. Returns the
/// common row length.
fn check_grid<T>(rows: &[Vec<T>], label: &str) -> PyResult<usize> {
let Some(first) = rows.first() else {
return Err(crate::ItofinError::new_err(format!(
"swaption {label} grid must have at least one row"
)));
};
let columns = first.len();
if columns == 0 {
return Err(crate::ItofinError::new_err(format!(
"swaption {label} grid rows must have at least one column"
)));
}
if rows.iter().any(|row| row.len() != columns) {
return Err(crate::ItofinError::new_err(format!(
"swaption {label} grid rows must all have the same length"
)));
}
Ok(columns)
}

/// A `list[list[float]]` as a core [`Matrix`], one row per option tenor.
fn matrix_from_rows(rows: &[Vec<f64>], label: &str) -> PyResult<Matrix> {
let columns = check_grid(rows, label)?;
let mut matrix = Matrix::with_size(rows.len(), columns);
for (i, row) in rows.iter().enumerate() {
for (j, &value) in row.iter().enumerate() {
matrix[(i, j)] = value;
}
}
Ok(matrix)
}
Loading
Loading