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
56 changes: 54 additions & 2 deletions crates/itofin-py/python/itofin/indexes.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# Hand-written stubs for itofin.indexes; sync manually with src/hullwhite.rs and src/helpers.rs (#517).
# Hand-written stubs for itofin.indexes; sync manually with src/hullwhite.rs, src/helpers.rs
# and src/swapindex.rs (#517).

from itofin import Settings
from itofin.termstructures import YieldTermStructure
from itofin.time import Date, Period
from itofin.time import (
BusinessDayConvention,
Calendar,
Date,
DayCounter,
Period,
)

class Euribor:
"""The Euribor IBOR index family."""
Expand All @@ -22,3 +29,48 @@ class Estr:

def __init__(self, curve: YieldTermStructure | None, settings: Settings) -> None: ...
def fixing(self, fixing_date: Date, forecast_todays_fixing: bool) -> float: ...

class SwapIndex:
"""The index whose fixing is the fair rate of an on-the-fly vanilla swap,
assembled from the index tenor, the forecasting Euribor index and the
fixed-leg conventions.

The currency is hard-coded to EUR: there is no Currency facade yet, and the
currency is inert for every ported consumer."""

def __init__(
self,
family_name: str,
tenor: Period,
settlement_days: int,
calendar: Calendar,
fixed_leg_tenor: Period,
fixed_leg_convention: BusinessDayConvention,
fixed_leg_day_counter: DayCounter,
ibor_index: Euribor,
settings: Settings,
) -> None:
"""Forecasts and discounts off the ibor index's forwarding curve."""
...
@staticmethod
def with_exogenous_discount(
family_name: str,
tenor: Period,
settlement_days: int,
calendar: Calendar,
fixed_leg_tenor: Period,
fixed_leg_convention: BusinessDayConvention,
fixed_leg_day_counter: DayCounter,
ibor_index: Euribor,
discount: YieldTermStructure,
settings: Settings,
) -> SwapIndex:
"""Forecasts off the ibor index's forwarding curve but discounts off the
separate discount curve."""
...
def fixing(self, fixing_date: Date, forecast_todays_fixing: bool = False) -> float:
"""The underlying swap's fair rate, the at-the-money forward the
volatility cubes read."""
...
def fixed_leg_tenor(self) -> Period: ...
def exogenous_discount(self) -> bool: ...
32 changes: 31 additions & 1 deletion crates/itofin-py/python/itofin/termstructures.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# src/helpers.rs and src/swaptionvol.rs (#517).

from itofin import Settings
from itofin.indexes import Estr, Euribor
from itofin.indexes import Estr, Euribor, SwapIndex
from itofin.quotes import SimpleQuote
from itofin.time import (
BusinessDayConvention,
Expand Down Expand Up @@ -468,3 +468,33 @@ class SwaptionVolatilityMatrix(SwaptionVolatilityStructure):
each node from the caller's quote: a later set_value on any of them
rebuilds the interpolation and notifies the grid's observers."""
...

class InterpolatedSwaptionVolatilityCube(SwaptionVolatilityStructure):
"""A smile cube adding bilinearly-interpolated volatility spreads to an
at-the-money surface.

The inherited volatility query now takes a real strike: the cube reads the
at-the-money forward off its base swap indexes, the at-the-money volatility
off atm_vol, and adds the spread interpolated at strike - atm_strike.

vol_spreads is row-major over the (option tenor, swap tenor) nodes: row
i * len(swap_tenors) + j is the smile at (option_tenors[i], swap_tenors[j]),
holding one quote per entry of strike_spreads. A later set_value on any of
those quotes rebuilds the per-strike interpolators."""

def __init__(
self,
atm_vol: SwaptionVolatilityStructure,
option_tenors: list[Period],
swap_tenors: list[Period],
strike_spreads: list[float],
vol_spreads: list[list[SimpleQuote]],
swap_index_base: SwapIndex,
short_swap_index_base: SwapIndex,
settings: Settings,
vega_weighted_smile_fit: bool = False,
) -> None: ...
def atm_strike_from_tenor(self, option_tenor: Period, swap_tenor: Period) -> float:
"""The at-the-money strike for an option tenor and swap tenor: the fixing
of whichever base swap index the swap tenor selects."""
...
8 changes: 6 additions & 2 deletions crates/itofin-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod market;
mod option;
mod settings;
mod swap;
mod swapindex;
mod swaption;
mod swaptionengine;
mod swaptionvol;
Expand Down Expand Up @@ -41,11 +42,12 @@ use pyo3::prelude::*;
use pyo3::types::PyDict;
use settings::PySettings;
use swap::{PySwapType, PyVanillaSwap};
use swapindex::PySwapIndex;
use swaption::{PyEuropeanExercise, PySettlementMethod, PySettlementType, PySwaption};
use swaptionengine::{PyBlackSwaptionEngine, PyCashAnnuityModel};
use swaptionvol::{
PyConstantSwaptionVolatility, PySwaptionVolatilityMatrix, PySwaptionVolatilityStructure,
PyVolatilityType,
PyConstantSwaptionVolatility, PyInterpolatedSwaptionVolatilityCube, PySwaptionVolatilityMatrix,
PySwaptionVolatilityStructure, PyVolatilityType,
};
use time::{
PyBusinessDayConvention, PyCalendar, PyDate, PyDayCounter, PyFrequency, PyPeriod, PySchedule,
Expand Down Expand Up @@ -134,6 +136,7 @@ fn itofin(m: &Bound<'_, PyModule>) -> PyResult<()> {
termstructures.add_class::<PyVolatilityType>()?;
termstructures.add_class::<PyConstantSwaptionVolatility>()?;
termstructures.add_class::<PySwaptionVolatilityMatrix>()?;
termstructures.add_class::<PyInterpolatedSwaptionVolatilityCube>()?;

let processes = PyModule::new(py, "processes")?;
processes.add_class::<PyBlackScholesProcess>()?;
Expand All @@ -142,6 +145,7 @@ fn itofin(m: &Bound<'_, PyModule>) -> PyResult<()> {
let indexes = PyModule::new(py, "indexes")?;
indexes.add_class::<PyEuribor>()?;
indexes.add_class::<PyEstr>()?;
indexes.add_class::<PySwapIndex>()?;

let instruments = PyModule::new(py, "instruments")?;
instruments.add_class::<PyOptionType>()?;
Expand Down
136 changes: 136 additions & 0 deletions crates/itofin-py/src/swapindex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//! Facade for the swap-rate index [`PySwapIndex`] (`indexes::SwapIndex`), the
//! index whose fixing is a vanilla swap's fair rate.
//!
//! The swaption volatility cubes take two of these (a long and a short base) and
//! read the at-the-money forward off them, so this is the index the cube facades
//! stack on rather than one the Python user prices with directly.
//!
//! Deferred (visible): the currency is hard-coded to EUR. There is no
//! `PyCurrency` facade yet, and the index's currency is inert for every ported
//! consumer - `underlying_swap` never reads it, and the cube's at-the-money
//! forward is a fair rate, not an amount. A `PyCurrency` ticket carries the
//! general form; until then a non-EUR index is not expressible here. The
//! `clone` family (re-curving / re-tenoring) is deferred in the core itself.

use crate::PyQlError;
use crate::curve::PyYieldTermStructure;
use crate::hullwhite::PyEuribor;
use crate::settings::PySettings;
use crate::time::{PyBusinessDayConvention, PyCalendar, PyDate, PyDayCounter, PyPeriod};
use libitofin::currency::Currency;
use libitofin::indexes::{Index, SwapIndex};
use libitofin::shared::{Shared, shared};
use libitofin::types::Natural;
use pyo3::prelude::*;

/// Python `SwapIndex`: the index whose fixing is the fair rate of an on-the-fly
/// vanilla swap (`indexes::SwapIndex`).
///
/// The swap is assembled from the index tenor, the forecasting `Euribor` index
/// and the fixed-leg conventions, off the value date the fixing date implies.
/// [`new`](PySwapIndex::new) forecasts and discounts off the ibor index's
/// forwarding curve; [`with_exogenous_discount`](PySwapIndex::with_exogenous_discount)
/// discounts off a separate curve.
#[pyclass(name = "SwapIndex", unsendable)]
pub struct PySwapIndex {
inner: Shared<SwapIndex>,
}

#[pymethods]
impl PySwapIndex {
/// A swap index forecasting and discounting off `ibor_index`'s forwarding
/// curve, registering with that index so a relinked curve notifies
/// observers.
#[new]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (family_name, tenor, settlement_days, calendar, fixed_leg_tenor, fixed_leg_convention, fixed_leg_day_counter, ibor_index, settings))]
fn new(
family_name: String,
tenor: &PyPeriod,
settlement_days: Natural,
calendar: &PyCalendar,
fixed_leg_tenor: &PyPeriod,
fixed_leg_convention: &PyBusinessDayConvention,
fixed_leg_day_counter: &PyDayCounter,
ibor_index: &PyEuribor,
settings: &PySettings,
) -> Self {
PySwapIndex {
inner: shared(SwapIndex::new(
family_name,
tenor.inner(),
settlement_days,
Currency::eur(),
calendar.inner(),
fixed_leg_tenor.inner(),
fixed_leg_convention.inner(),
fixed_leg_day_counter.inner(),
ibor_index.inner(),
settings.inner(),
)),
}
}

/// A swap index forecasting off `ibor_index`'s forwarding curve but
/// discounting off the separate `discount` curve, registering with both.
#[staticmethod]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (family_name, tenor, settlement_days, calendar, fixed_leg_tenor, fixed_leg_convention, fixed_leg_day_counter, ibor_index, discount, settings))]
fn with_exogenous_discount(
family_name: String,
tenor: &PyPeriod,
settlement_days: Natural,
calendar: &PyCalendar,
fixed_leg_tenor: &PyPeriod,
fixed_leg_convention: &PyBusinessDayConvention,
fixed_leg_day_counter: &PyDayCounter,
ibor_index: &PyEuribor,
discount: &PyYieldTermStructure,
settings: &PySettings,
) -> Self {
PySwapIndex {
inner: shared(SwapIndex::with_exogenous_discount(
family_name,
tenor.inner(),
settlement_days,
Currency::eur(),
calendar.inner(),
fixed_leg_tenor.inner(),
fixed_leg_convention.inner(),
fixed_leg_day_counter.inner(),
ibor_index.inner(),
discount.handle(),
settings.inner(),
)),
}
}

/// The index's fixing for `fixing_date`: the fair rate of the underlying
/// swap, the number the volatility cubes read as the at-the-money forward.
/// Fallible: an empty forwarding handle, an unset evaluation date or an
/// invalid fixing date is an error.
#[pyo3(signature = (fixing_date, forecast_todays_fixing = false))]
fn fixing(&self, fixing_date: &PyDate, forecast_todays_fixing: bool) -> PyResult<f64> {
Ok(self
.inner
.fixing(fixing_date.inner(), forecast_todays_fixing)
.map_err(PyQlError::from)?)
}

/// The fixed-leg tenor.
fn fixed_leg_tenor(&self) -> PyPeriod {
PyPeriod::from_inner(self.inner.fixed_leg_tenor())
}

/// Whether the index discounts off a separate curve.
fn exogenous_discount(&self) -> bool {
self.inner.exogenous_discount()
}
}

impl PySwapIndex {
/// A clone of the inner index for the volatility cube facades.
pub(crate) fn inner(&self) -> Shared<SwapIndex> {
Shared::clone(&self.inner)
}
}
Loading
Loading