use std::any::Any;
use std::marker::PhantomData;
use crate::cashflow::Leg;
use crate::cashflows::CashFlows;
use crate::errors::QlResult;
use crate::exercise::ExerciseType;
use crate::handle::Handle;
use crate::instrument::{Instrument, InstrumentResults};
use crate::instruments::{
SettlementMethod, SettlementType, SwapType, SwaptionArguments, SwaptionEngine,
};
use crate::interestrate::{Compounding, InterestRate};
use crate::option::OptionType;
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::{Arguments, PricingEngine, Results};
use crate::pricingengines::DiscountingSwapEngine;
use crate::pricingengines::blackformula::{
bachelier_black_formula, bachelier_black_formula_forward_derivative,
bachelier_black_formula_std_dev_derivative, black_formula, black_formula_forward_derivative,
black_formula_std_dev_derivative,
};
use crate::quotes::Quote;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::termstructures::volatility::{
ConstantSwaptionVolatility, SwaptionVolatilityStructure, VolatilityType,
};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::nullcalendar::NullCalendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::types::Real;
use crate::{fail, require};
const BASIS_POINT: Real = 1.0e-4;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CashAnnuityModel {
SwapRate,
DiscountCurve,
}
pub trait BlackStyleSpec {
const VOL_TYPE: VolatilityType;
const VOL_REQUIREMENT: &'static str;
fn value(
option_type: OptionType,
strike: Real,
atm_forward: Real,
std_dev: Real,
annuity: Real,
displacement: Real,
) -> QlResult<Real>;
fn vega(
strike: Real,
atm_forward: Real,
std_dev: Real,
exercise_time: Real,
annuity: Real,
displacement: Real,
) -> QlResult<Real>;
fn delta(
option_type: OptionType,
strike: Real,
atm_forward: Real,
std_dev: Real,
annuity: Real,
displacement: Real,
) -> QlResult<Real>;
}
pub struct Black76Spec;
impl BlackStyleSpec for Black76Spec {
const VOL_TYPE: VolatilityType = VolatilityType::ShiftedLognormal;
const VOL_REQUIREMENT: &'static str =
"BlackSwaptionEngine requires (shifted) lognormal input volatility";
fn value(
option_type: OptionType,
strike: Real,
atm_forward: Real,
std_dev: Real,
annuity: Real,
displacement: Real,
) -> QlResult<Real> {
black_formula(
option_type,
strike,
atm_forward,
std_dev,
annuity,
displacement,
)
}
fn vega(
strike: Real,
atm_forward: Real,
std_dev: Real,
exercise_time: Real,
annuity: Real,
displacement: Real,
) -> QlResult<Real> {
Ok(exercise_time.sqrt()
* black_formula_std_dev_derivative(
strike,
atm_forward,
std_dev,
annuity,
displacement,
)?)
}
fn delta(
option_type: OptionType,
strike: Real,
atm_forward: Real,
std_dev: Real,
annuity: Real,
displacement: Real,
) -> QlResult<Real> {
black_formula_forward_derivative(
option_type,
strike,
atm_forward,
std_dev,
annuity,
displacement,
)
}
}
pub struct BachelierSpec;
impl BlackStyleSpec for BachelierSpec {
const VOL_TYPE: VolatilityType = VolatilityType::Normal;
const VOL_REQUIREMENT: &'static str =
"BachelierSwaptionEngine requires normal input volatility";
fn value(
option_type: OptionType,
strike: Real,
atm_forward: Real,
std_dev: Real,
annuity: Real,
_displacement: Real,
) -> QlResult<Real> {
bachelier_black_formula(option_type, strike, atm_forward, std_dev, annuity)
}
fn vega(
strike: Real,
atm_forward: Real,
std_dev: Real,
exercise_time: Real,
annuity: Real,
_displacement: Real,
) -> QlResult<Real> {
Ok(exercise_time.sqrt()
* bachelier_black_formula_std_dev_derivative(strike, atm_forward, std_dev, annuity)?)
}
fn delta(
option_type: OptionType,
strike: Real,
atm_forward: Real,
std_dev: Real,
annuity: Real,
_displacement: Real,
) -> QlResult<Real> {
bachelier_black_formula_forward_derivative(
option_type,
strike,
atm_forward,
std_dev,
annuity,
)
}
}
pub type BlackSwaptionEngine = BlackStyleSwaptionEngine<Black76Spec>;
pub type BachelierSwaptionEngine = BlackStyleSwaptionEngine<BachelierSpec>;
pub struct BlackStyleSwaptionEngine<S: BlackStyleSpec> {
base: SwaptionEngine,
discount_curve: Handle<dyn YieldTermStructure>,
vol: Handle<dyn SwaptionVolatilityStructure>,
model: CashAnnuityModel,
settings: Shared<Settings<Date>>,
_spec: PhantomData<S>,
}
impl<S: BlackStyleSpec> BlackStyleSwaptionEngine<S> {
pub fn new(
discount_curve: Handle<dyn YieldTermStructure>,
vol: Handle<dyn SwaptionVolatilityStructure>,
model: CashAnnuityModel,
settings: Shared<Settings<Date>>,
) -> BlackStyleSwaptionEngine<S> {
let base = SwaptionEngine::new(SwaptionArguments::default(), InstrumentResults::default());
discount_curve.register_observer(&base.observer());
vol.register_observer(&base.observer());
BlackStyleSwaptionEngine {
base,
discount_curve,
vol,
model,
settings,
_spec: PhantomData,
}
}
pub fn with_flat_vol(
discount_curve: Handle<dyn YieldTermStructure>,
vol: Handle<dyn Quote>,
day_counter: DayCounter,
displacement: Real,
model: CashAnnuityModel,
settings: Shared<Settings<Date>>,
) -> BlackStyleSwaptionEngine<S> {
let surface = ConstantSwaptionVolatility::moving_with_quote(
0,
NullCalendar::new(),
BusinessDayConvention::Following,
vol,
day_counter,
S::VOL_TYPE,
displacement,
Shared::clone(&settings),
);
let vol = Handle::new(shared(surface) as Shared<dyn SwaptionVolatilityStructure>);
BlackStyleSwaptionEngine::<S>::new(discount_curve, vol, model, settings)
}
pub fn discount_curve(&self) -> &Handle<dyn YieldTermStructure> {
&self.discount_curve
}
}
impl<S: BlackStyleSpec> AsObservable for BlackStyleSwaptionEngine<S> {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl<S: BlackStyleSpec> PricingEngine for BlackStyleSwaptionEngine<S> {
fn arguments_mut(&mut self) -> &mut dyn Arguments {
self.base.arguments_mut()
}
fn results(&self) -> &dyn Results {
self.base.results()
}
fn reset(&mut self) {
self.base.reset();
}
fn calculate(&mut self) -> QlResult<()> {
let (exercise, swap, settlement_type, settlement_method) = {
let arguments = self.base.arguments();
let Some(exercise) = arguments.exercise.as_ref() else {
fail!("exercise not set");
};
let Some(swap) = arguments.swap.as_ref() else {
fail!("swap not set");
};
(
Shared::clone(exercise),
SharedMut::clone(swap),
arguments.settlement_type,
arguments.settlement_method,
)
};
require!(
exercise.exercise_type() == ExerciseType::European,
"not a European option"
);
let exercise_date = exercise.dates()[0];
let discount = self.discount_curve.current_link()?;
let vol = self.vol.current_link()?;
require!(
vol.volatility_type() == S::VOL_TYPE,
"{}",
S::VOL_REQUIREMENT
);
let discounting_engine = shared_mut(DiscountingSwapEngine::new(
self.discount_curve.clone(),
Some(false),
None,
None,
Shared::clone(&self.settings),
)) as SharedMut<dyn PricingEngine>;
let (
valuation_date,
atm_forward,
fixed_rate,
swap_type,
fixed_leg_bps,
floating_leg_bps,
spread,
fixed_leg,
first_accrual_start,
first_day_count,
fixed_frequency,
floating_front,
floating_back,
) = {
let mut swap_ref = swap.borrow_mut();
let (first_accrual_start, first_day_count) = {
let leg = swap_ref.fixed_leg();
let Some(first) = leg.first() else {
fail!("swap has no fixed leg");
};
let Some(coupon) = first.as_coupon() else {
fail!("first fixed cash flow is not a coupon");
};
(coupon.accrual_start_date(), coupon.day_counter())
};
require!(
first_accrual_start >= exercise_date,
"swap start ({first_accrual_start}) before exercise date ({exercise_date}) \
not supported in Black swaption engine"
);
let fixed_rate = swap_ref.fixed_rate();
let spread = swap_ref.spread();
let swap_type = swap_ref.swap_type();
let fixed_leg: Leg = swap_ref.fixed_leg().to_vec();
let fixed_frequency = if swap_ref.fixed_schedule().has_tenor() {
swap_ref.fixed_schedule().tenor().frequency()
} else {
Frequency::Annual
};
let (floating_front, floating_back) = {
let dates = swap_ref.floating_schedule().dates();
(dates[0], dates[dates.len() - 1])
};
swap_ref
.base_mut()
.set_pricing_engine_silent(discounting_engine);
let valuation_date = swap_ref.valuation_date()?;
let atm_forward = swap_ref.fair_rate()?;
let fixed_leg_bps = swap_ref.fixed_leg_bps()?;
let floating_leg_bps = swap_ref.floating_leg_bps()?;
(
valuation_date,
atm_forward,
fixed_rate,
swap_type,
fixed_leg_bps,
floating_leg_bps,
spread,
fixed_leg,
first_accrual_start,
first_day_count,
fixed_frequency,
floating_front,
floating_back,
)
};
let mut strike = fixed_rate;
let mut atm_forward = atm_forward;
let spread_correction = if spread != 0.0 {
let correction = spread * (floating_leg_bps / fixed_leg_bps).abs();
strike -= correction;
atm_forward -= correction;
correction
} else {
0.0
};
let annuity = match (settlement_type, settlement_method) {
(SettlementType::Physical, _)
| (SettlementType::Cash, SettlementMethod::CollateralizedCashPrice) => {
fixed_leg_bps.abs() / BASIS_POINT
}
(SettlementType::Cash, SettlementMethod::ParYieldCurve) => {
let discount_date = match self.model {
CashAnnuityModel::DiscountCurve => first_accrual_start,
CashAnnuityModel::SwapRate => valuation_date,
};
let yield_rate = InterestRate::new(
atm_forward,
first_day_count,
Compounding::Compounded,
fixed_frequency,
)?;
let fixed_leg_cash_bps = CashFlows::bps_at_yield(
&fixed_leg,
&yield_rate,
&self.settings,
Some(false),
Some(discount_date),
None,
)?;
(fixed_leg_cash_bps / BASIS_POINT).abs()
* discount.discount_date(discount_date, false)?
}
_ => fail!("invalid (settlementType, settlementMethod) pair"),
};
let swap_length = vol
.swap_length(floating_front, floating_back)?
.max(1.0 / 12.0);
let variance = vol.black_variance(exercise_date, swap_length, strike, false)?;
let displacement = if vol.volatility_type() == VolatilityType::ShiftedLognormal {
vol.shift(exercise_date, swap_length, false)?
} else {
0.0
};
let std_dev = variance.sqrt();
let option_type = if swap_type == SwapType::Payer {
OptionType::Call
} else {
OptionType::Put
};
let value = S::value(
option_type,
strike,
atm_forward,
std_dev,
annuity,
displacement,
)?;
let exercise_time = vol.time_from_reference(exercise_date)?;
let vega = S::vega(
strike,
atm_forward,
std_dev,
exercise_time,
annuity,
displacement,
)?;
let delta = S::delta(
option_type,
strike,
atm_forward,
std_dev,
annuity,
displacement,
)?;
let implied_volatility = std_dev / exercise_time.sqrt();
let forward_price = value / discount.discount_date(exercise_date, false)?;
drop(vol);
drop(discount);
let results = self.base.results_mut();
results.value = Some(value);
results.error_estimate = None;
results.valuation_date = Some(valuation_date);
let extras = &mut results.additional_results;
extras.insert(
"spreadCorrection".into(),
shared(spread_correction) as Shared<dyn Any>,
);
extras.insert("strike".into(), shared(strike) as Shared<dyn Any>);
extras.insert("atmForward".into(), shared(atm_forward) as Shared<dyn Any>);
extras.insert("annuity".into(), shared(annuity) as Shared<dyn Any>);
extras.insert("swapLength".into(), shared(swap_length) as Shared<dyn Any>);
extras.insert("stdDev".into(), shared(std_dev) as Shared<dyn Any>);
extras.insert("vega".into(), shared(vega) as Shared<dyn Any>);
extras.insert("delta".into(), shared(delta) as Shared<dyn Any>);
extras.insert(
"timeToExpiry".into(),
shared(exercise_time) as Shared<dyn Any>,
);
extras.insert(
"impliedVolatility".into(),
shared(implied_volatility) as Shared<dyn Any>,
);
extras.insert(
"forwardPrice".into(),
shared(forward_price) as Shared<dyn Any>,
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exercise::EuropeanExercise;
use crate::indexes::IborIndex;
use crate::indexes::ibor::{Eonia, Euribor};
use crate::instrument::Instrument;
use crate::instruments::{
FixedVsFloatingSwap, MakeOis, MakeVanillaSwap, Swaption, VanillaSwap,
};
use crate::quotes::{SimpleQuote, make_quote_handle};
use crate::shared::shared;
use crate::termstructures::yields::{FlatForward, ZeroSpreadedTermStructure};
use crate::time::calendar::Calendar;
use crate::time::calendars::target::Target;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;
use crate::time::daycounters::thirty360::{Convention, Thirty360};
use crate::time::period::Period;
use crate::time::schedule::MakeSchedule;
use crate::time::timeunit::TimeUnit;
use crate::types::{Integer, Rate, Spread, Volatility};
const SETTLEMENT_DAYS: Integer = 2;
struct Vars {
settings: Shared<Settings<Date>>,
calendar: Calendar,
curve: Handle<dyn YieldTermStructure>,
index: Shared<IborIndex>,
today: Date,
settlement: Date,
}
impl Vars {
fn new(today: Date, using_at_par: bool) -> Vars {
let settings = shared(Settings::new());
settings.set_evaluation_date(today);
settings.set_using_at_par_coupons(using_at_par);
let calendar = Target::new();
let settlement = calendar.advance(
today,
SETTLEMENT_DAYS,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
let curve: Handle<dyn YieldTermStructure> = Handle::new(shared(FlatForward::with_rate(
settlement,
0.05,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
))
as Shared<dyn YieldTermStructure>);
let index = shared(Euribor::six_months(curve.clone(), Shared::clone(&settings)));
Vars {
settings,
calendar,
curve,
index,
today,
settlement,
}
}
fn fixed_day_count() -> DayCounter {
Thirty360::with_convention(Convention::BondBasis)
}
fn years(&self, from: Date, n: Integer) -> Date {
self.calendar.advance(
from,
n,
TimeUnit::Years,
BusinessDayConvention::Following,
false,
)
}
fn spot(&self, from: Date) -> Date {
self.calendar.advance(
from,
SETTLEMENT_DAYS,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
)
}
fn make_swaption(
&self,
swap: FixedVsFloatingSwap,
exercise_date: Date,
volatility: Volatility,
settlement_type: SettlementType,
settlement_method: SettlementMethod,
) -> Swaption {
let engine = shared_mut(BlackSwaptionEngine::with_flat_vol(
self.curve.clone(),
make_quote_handle(volatility).handle(),
Actual365Fixed::new(),
0.0,
CashAnnuityModel::SwapRate,
Shared::clone(&self.settings),
)) as SharedMut<dyn PricingEngine>;
let mut swaption = Swaption::new(
shared_mut(swap),
shared(EuropeanExercise::new(exercise_date))
as Shared<dyn crate::exercise::Exercise>,
settlement_type,
settlement_method,
Shared::clone(&self.settings),
);
swaption.base_mut().set_pricing_engine(engine);
swaption
}
fn make_vanilla(
&self,
start: Date,
length: Integer,
fixed_rate: Rate,
spread: Spread,
swap_type: SwapType,
) -> VanillaSwap {
let maturity = start + Period::new(length, TimeUnit::Years);
let schedule = |frequency| {
MakeSchedule::new()
.from(start)
.to(maturity)
.with_frequency(frequency)
.with_calendar(self.calendar.clone())
.with_convention(BusinessDayConvention::ModifiedFollowing)
.with_termination_date_convention(BusinessDayConvention::ModifiedFollowing)
.forwards()
.end_of_month(false)
.build()
};
VanillaSwap::new(
swap_type,
1.0,
schedule(Frequency::Annual),
fixed_rate,
Thirty360::with_convention(Convention::BondBasis),
schedule(Frequency::Semiannual),
Shared::clone(&self.index),
spread,
Actual360::new(),
None,
Shared::clone(&self.settings),
)
.unwrap()
}
fn leg_bps_ratio(
&self,
start: Date,
length: Integer,
fixed_rate: Rate,
spread: Spread,
swap_type: SwapType,
) -> Real {
let mut swap = self.make_vanilla(start, length, fixed_rate, spread, swap_type);
let engine = shared_mut(DiscountingSwapEngine::new(
self.curve.clone(),
Some(false),
None,
None,
Shared::clone(&self.settings),
)) as SharedMut<dyn PricingEngine>;
swap.base_mut().set_pricing_engine(engine);
let base = swap.fixed_vs_floating_mut();
base.floating_leg_bps().unwrap() / base.fixed_leg_bps().unwrap()
}
fn cash_settled_ratio(
&self,
start: Date,
maturity: Date,
exercise_date: Date,
strike: Rate,
fixed_convention: BusinessDayConvention,
fixed_day_count: DayCounter,
) -> (Real, Real) {
let build = || {
let fixed_schedule = MakeSchedule::new()
.from(start)
.to(maturity)
.with_frequency(Frequency::Annual)
.with_calendar(self.calendar.clone())
.with_convention(fixed_convention)
.with_termination_date_convention(fixed_convention)
.forwards()
.end_of_month(true)
.build();
let float_schedule = MakeSchedule::new()
.from(start)
.to(maturity)
.with_frequency(Frequency::Semiannual)
.with_calendar(self.calendar.clone())
.with_convention(BusinessDayConvention::ModifiedFollowing)
.with_termination_date_convention(BusinessDayConvention::ModifiedFollowing)
.forwards()
.end_of_month(false)
.build();
VanillaSwap::new(
SwapType::Receiver,
1_000_000.0,
fixed_schedule,
strike,
fixed_day_count.clone(),
float_schedule,
Shared::clone(&self.index),
0.0,
Actual360::new(),
None,
Shared::clone(&self.settings),
)
.unwrap()
.into_fixed_vs_floating()
};
let mut swap = build();
let swap_engine = shared_mut(DiscountingSwapEngine::new(
self.curve.clone(),
Some(false),
None,
None,
Shared::clone(&self.settings),
)) as SharedMut<dyn PricingEngine>;
swap.base_mut().set_pricing_engine(swap_engine);
let fair_rate = swap.fair_rate().unwrap();
let fixed_leg_bps = swap.fixed_leg_bps().unwrap();
let signed_bps = if swap.swap_type() == SwapType::Payer {
-fixed_leg_bps
} else {
fixed_leg_bps
};
let annuity = signed_bps / BASIS_POINT;
let curve: Shared<dyn YieldTermStructure> = shared(FlatForward::with_rate(
self.settlement,
fair_rate,
fixed_day_count.clone(),
Compounding::Compounded,
Frequency::Annual,
))
as Shared<dyn YieldTermStructure>;
let mut cash_annuity = 0.0;
for cash_flow in swap.fixed_leg() {
cash_annuity += cash_flow.amount().unwrap() / strike
* curve.discount_date(cash_flow.date(), false).unwrap();
}
let value_p = self
.make_swaption(
build(),
exercise_date,
0.20,
SettlementType::Physical,
SettlementMethod::PhysicalOTC,
)
.npv()
.unwrap();
let value_c = self
.make_swaption(
build(),
exercise_date,
0.20,
SettlementType::Cash,
SettlementMethod::ParYieldCurve,
)
.npv()
.unwrap();
(value_c / value_p, cash_annuity / annuity)
}
}
const EXERCISES: [Integer; 3] = [1, 5, 10];
const LENGTHS: [Integer; 4] = [1, 5, 10, 20];
#[test]
fn npv_is_monotone_in_the_strike() {
let strikes = [0.03, 0.04, 0.05, 0.06, 0.07];
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
for exercise in EXERCISES {
let exercise_date = vars.years(vars.today, exercise);
let start_date = vars.spot(exercise_date);
for length in LENGTHS {
for swap_type in [SwapType::Payer, SwapType::Receiver] {
for (settlement_type, settlement_method) in [
(SettlementType::Physical, SettlementMethod::PhysicalOTC),
(SettlementType::Cash, SettlementMethod::ParYieldCurve),
] {
let values: Vec<Real> = strikes
.iter()
.map(|&strike| {
let swap = vars
.make_vanilla(start_date, length, strike, 0.0, swap_type)
.into_fixed_vs_floating();
vars.make_swaption(
swap,
exercise_date,
0.20,
settlement_type,
settlement_method,
)
.npv()
.unwrap()
})
.collect();
for pair in values.windows(2) {
match swap_type {
SwapType::Payer => assert!(
pair[0] >= pair[1],
"payer NPV rose with strike ({exercise}y/{length}y): {pair:?}"
),
SwapType::Receiver => assert!(
pair[0] <= pair[1],
"receiver NPV fell with strike ({exercise}y/{length}y): {pair:?}"
),
}
}
}
}
}
}
}
#[test]
fn npv_is_monotone_in_the_spread() {
let spreads = [-0.002, -0.001, 0.0, 0.001, 0.002];
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
for exercise in EXERCISES {
let exercise_date = vars.years(vars.today, exercise);
let start_date = vars.spot(exercise_date);
for length in LENGTHS {
for swap_type in [SwapType::Payer, SwapType::Receiver] {
for (settlement_type, settlement_method) in [
(SettlementType::Physical, SettlementMethod::PhysicalOTC),
(SettlementType::Cash, SettlementMethod::ParYieldCurve),
] {
let values: Vec<Real> = spreads
.iter()
.map(|&spread| {
let swap = vars
.make_vanilla(start_date, length, 0.06, spread, swap_type)
.into_fixed_vs_floating();
vars.make_swaption(
swap,
exercise_date,
0.20,
settlement_type,
settlement_method,
)
.npv()
.unwrap()
})
.collect();
for pair in values.windows(2) {
match swap_type {
SwapType::Payer => assert!(
pair[0] <= pair[1],
"payer NPV fell with spread ({exercise}y/{length}y): {pair:?}"
),
SwapType::Receiver => assert!(
pair[0] >= pair[1],
"receiver NPV rose with spread ({exercise}y/{length}y): {pair:?}"
),
}
}
}
}
}
}
}
#[test]
fn a_spread_swaption_equals_its_zero_spread_equivalent() {
let spreads = [-0.002, -0.001, 0.0, 0.001, 0.002];
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
for exercise in EXERCISES {
let exercise_date = vars.years(vars.today, exercise);
let start_date = vars.spot(exercise_date);
for length in LENGTHS {
for swap_type in [SwapType::Payer, SwapType::Receiver] {
for spread in spreads {
let correction = spread
* vars.leg_bps_ratio(start_date, length, 0.06, spread, swap_type);
for (settlement_type, settlement_method) in [
(SettlementType::Physical, SettlementMethod::PhysicalOTC),
(SettlementType::Cash, SettlementMethod::ParYieldCurve),
] {
let original = vars
.make_vanilla(start_date, length, 0.06, spread, swap_type)
.into_fixed_vs_floating();
let equivalent = vars
.make_vanilla(start_date, length, 0.06 + correction, 0.0, swap_type)
.into_fixed_vs_floating();
let original_npv = vars
.make_swaption(
original,
exercise_date,
0.20,
settlement_type,
settlement_method,
)
.npv()
.unwrap();
let equivalent_npv = vars
.make_swaption(
equivalent,
exercise_date,
0.20,
settlement_type,
settlement_method,
)
.npv()
.unwrap();
assert!(
(original_npv - equivalent_npv).abs() <= 1.0e-6,
"spread treatment ({exercise}y/{length}y spread {spread}): \
{original_npv} vs {equivalent_npv}"
);
}
}
}
}
}
}
#[test]
fn cached_value_reproduces_the_par_and_indexed_arms() {
for (using_at_par, expected) in [(true, 0.036418158579), (false, 0.036421429684)] {
let vars = Vars::new(Date::new(13, Month::March, 2002), using_at_par);
let exercise_date = vars.years(vars.settlement, 5);
let start_date = vars.spot(exercise_date);
let swap = MakeVanillaSwap::new(
Period::new(10, TimeUnit::Years),
Shared::clone(&vars.index),
Some(0.06),
Period::new(0, TimeUnit::Days),
Shared::clone(&vars.settings),
)
.with_effective_date(start_date)
.with_fixed_leg_tenor(Period::new(1, TimeUnit::Years))
.with_fixed_leg_day_count(Vars::fixed_day_count())
.build()
.unwrap()
.into_fixed_vs_floating();
let mut swaption = vars.make_swaption(
swap,
exercise_date,
0.20,
SettlementType::Physical,
SettlementMethod::PhysicalOTC,
);
let npv = swaption.npv().unwrap();
assert!(
(npv - expected).abs() <= 1.0e-12,
"par={using_at_par}: npv {npv} vs cached {expected} (error {})",
(npv - expected).abs()
);
}
}
#[test]
fn black_engine_leaves_the_swaption_calculated_after_npv() {
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
let exercise_date = vars.years(vars.today, 1);
let start_date = vars.spot(exercise_date);
let swap = MakeVanillaSwap::new(
Period::new(1, TimeUnit::Years),
Shared::clone(&vars.index),
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&vars.settings),
)
.with_effective_date(start_date)
.with_fixed_leg_tenor(Period::new(1, TimeUnit::Years))
.with_fixed_leg_day_count(Vars::fixed_day_count())
.build()
.unwrap()
.into_fixed_vs_floating();
let mut swaption = vars.make_swaption(
swap,
exercise_date,
0.12,
SettlementType::Physical,
SettlementMethod::PhysicalOTC,
);
assert!(!swaption.base().is_calculated());
swaption.npv().unwrap();
assert!(
swaption.base().is_calculated(),
"the Black engine must leave the swaption calculated after NPV"
);
}
#[test]
fn a_normal_volatility_surface_is_refused() {
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
let exercise_date = vars.years(vars.today, 1);
let start_date = vars.spot(exercise_date);
let swap = MakeVanillaSwap::new(
Period::new(1, TimeUnit::Years),
Shared::clone(&vars.index),
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&vars.settings),
)
.with_effective_date(start_date)
.with_fixed_leg_tenor(Period::new(1, TimeUnit::Years))
.with_fixed_leg_day_count(Vars::fixed_day_count())
.build()
.unwrap()
.into_fixed_vs_floating();
let normal_surface = ConstantSwaptionVolatility::moving(
0,
NullCalendar::new(),
BusinessDayConvention::Following,
0.12,
Actual365Fixed::new(),
VolatilityType::Normal,
0.0,
Shared::clone(&vars.settings),
);
let vol: Handle<dyn SwaptionVolatilityStructure> =
Handle::new(shared(normal_surface) as Shared<dyn SwaptionVolatilityStructure>);
let engine = shared_mut(BlackSwaptionEngine::new(
vars.curve.clone(),
vol,
CashAnnuityModel::SwapRate,
Shared::clone(&vars.settings),
)) as SharedMut<dyn PricingEngine>;
let mut swaption = Swaption::new(
shared_mut(swap),
shared(EuropeanExercise::new(exercise_date)) as Shared<dyn crate::exercise::Exercise>,
SettlementType::Physical,
SettlementMethod::PhysicalOTC,
Shared::clone(&vars.settings),
);
swaption.base_mut().set_pricing_engine(engine);
let error = swaption.npv().unwrap_err();
assert!(
error.to_string().contains("lognormal input volatility"),
"expected the lognormal-input refusal, got: {error}"
);
}
#[test]
fn cached_value_reproduces_the_ois_swaption_arm() {
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
let spreaded: Handle<dyn YieldTermStructure> = Handle::new(shared(
ZeroSpreadedTermStructure::new(vars.curve.clone(), make_quote_handle(-0.01).handle()),
)
as Shared<dyn YieldTermStructure>);
let ois_index = shared(Eonia::new(spreaded, Shared::clone(&vars.settings)));
let exercise_date = vars.years(vars.settlement, 5);
let start_date = vars.spot(exercise_date);
let ois_swap = MakeOis::new(
Period::new(10, TimeUnit::Years),
ois_index,
Some(0.06),
Period::new(0, TimeUnit::Days),
Shared::clone(&vars.settings),
)
.with_effective_date(start_date)
.with_fixed_leg_day_count(Vars::fixed_day_count())
.build()
.unwrap()
.into_fixed_vs_floating();
let mut swaption = vars.make_swaption(
ois_swap,
exercise_date,
0.20,
SettlementType::Physical,
SettlementMethod::PhysicalOTC,
);
let expected = 0.014101075767;
let npv = swaption.npv().unwrap();
assert!(
(npv - expected).abs() <= 1.0e-12,
"OIS swaption npv {npv} vs cached {expected} (error {})",
(npv - expected).abs()
);
}
#[test]
fn analytical_vega_matches_a_finite_difference() {
let strikes = [0.03, 0.04, 0.05, 0.06, 0.07];
let vols = [0.01, 0.20, 0.30, 0.70, 0.90];
let shift = 1.0e-8;
let cases = [
(
SwapType::Receiver,
SettlementType::Physical,
SettlementMethod::PhysicalOTC,
),
(
SwapType::Payer,
SettlementType::Cash,
SettlementMethod::ParYieldCurve,
),
];
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
for exercise in EXERCISES {
let exercise_date = vars.years(vars.today, exercise);
let start_date = vars.spot(exercise_date);
for length in LENGTHS {
for strike in strikes {
for (swap_type, settlement_type, settlement_method) in cases {
let priced = |volatility: Volatility| {
let swap = vars
.make_vanilla(start_date, length, strike, 0.0, swap_type)
.into_fixed_vs_floating();
vars.make_swaption(
swap,
exercise_date,
volatility,
settlement_type,
settlement_method,
)
};
for vol in vols {
let mut swaption = priced(vol);
let swaption_npv = swaption.npv().unwrap();
let numerical = (priced(vol + shift).npv().unwrap()
- priced(vol - shift).npv().unwrap())
/ (200.0 * shift);
if numerical / swaption_npv > 1.0e-7 {
let analytical = swaption.result::<Real>("vega").unwrap() / 100.0;
let discrepancy = (analytical - numerical).abs() / numerical;
assert!(
discrepancy <= 0.015,
"vega {exercise}y/{length}y strike {strike} vol {vol}: \
analytical {analytical} vs numerical {numerical} \
(discrepancy {discrepancy})"
);
}
}
}
}
}
}
}
#[test]
fn a_lognormal_volatility_surface_is_refused_by_the_bachelier_engine() {
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
let exercise_date = vars.years(vars.today, 1);
let start_date = vars.spot(exercise_date);
let swap = MakeVanillaSwap::new(
Period::new(1, TimeUnit::Years),
Shared::clone(&vars.index),
Some(0.03),
Period::new(0, TimeUnit::Days),
Shared::clone(&vars.settings),
)
.with_effective_date(start_date)
.with_fixed_leg_tenor(Period::new(1, TimeUnit::Years))
.with_fixed_leg_day_count(Vars::fixed_day_count())
.build()
.unwrap()
.into_fixed_vs_floating();
let lognormal_surface = ConstantSwaptionVolatility::moving(
0,
NullCalendar::new(),
BusinessDayConvention::Following,
0.12,
Actual365Fixed::new(),
VolatilityType::ShiftedLognormal,
0.0,
Shared::clone(&vars.settings),
);
let vol: Handle<dyn SwaptionVolatilityStructure> =
Handle::new(shared(lognormal_surface) as Shared<dyn SwaptionVolatilityStructure>);
let engine = shared_mut(BachelierSwaptionEngine::new(
vars.curve.clone(),
vol,
CashAnnuityModel::SwapRate,
Shared::clone(&vars.settings),
)) as SharedMut<dyn PricingEngine>;
let mut swaption = Swaption::new(
shared_mut(swap),
shared(EuropeanExercise::new(exercise_date)) as Shared<dyn crate::exercise::Exercise>,
SettlementType::Physical,
SettlementMethod::PhysicalOTC,
Shared::clone(&vars.settings),
);
swaption.base_mut().set_pricing_engine(engine);
let error = swaption.npv().unwrap_err();
assert!(
error
.to_string()
.contains("requires normal input volatility"),
"expected the normal-input refusal, got: {error}"
);
}
fn check_swaption_delta(
use_bachelier_vol: bool,
make_engine: impl Fn(
&Handle<dyn YieldTermStructure>,
Handle<dyn Quote>,
&Shared<Settings<Date>>,
) -> SharedMut<dyn PricingEngine>,
) {
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
let today = vars.today;
let calendar = vars.calendar.clone();
let settings = Shared::clone(&vars.settings);
let bump = 1.0e-4;
let epsilon = 1.0e-10;
let projection_rate = 0.01;
let proj_quote = shared(SimpleQuote::new(projection_rate));
let proj_handle: Handle<dyn Quote> =
Handle::new(Shared::clone(&proj_quote) as Shared<dyn Quote>);
let projection: Handle<dyn YieldTermStructure> = Handle::new(shared(FlatForward::new(
today,
proj_handle,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
))
as Shared<dyn YieldTermStructure>);
let discount: Handle<dyn YieldTermStructure> = Handle::new(shared(FlatForward::with_rate(
today,
0.0085,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
))
as Shared<dyn YieldTermStructure>);
let index = shared(Euribor::six_months(
projection.clone(),
Shared::clone(&settings),
));
let strikes = [0.03, 0.04, 0.05, 0.06, 0.07];
let vols = [0.0, 0.10, 0.20, 0.30, 0.70, 0.90];
let cases = [
(
SwapType::Receiver,
SettlementType::Physical,
SettlementMethod::PhysicalOTC,
),
(
SwapType::Payer,
SettlementType::Cash,
SettlementMethod::CollateralizedCashPrice,
),
];
let make_underlying = |start: Date,
length: Integer,
strike: Rate,
swap_type: SwapType|
-> FixedVsFloatingSwap {
let maturity = start + Period::new(length, TimeUnit::Years);
let schedule = |frequency| {
MakeSchedule::new()
.from(start)
.to(maturity)
.with_frequency(frequency)
.with_calendar(calendar.clone())
.with_convention(BusinessDayConvention::ModifiedFollowing)
.with_termination_date_convention(BusinessDayConvention::ModifiedFollowing)
.forwards()
.end_of_month(false)
.build()
};
VanillaSwap::new(
swap_type,
1.0,
schedule(Frequency::Annual),
strike,
Thirty360::with_convention(Convention::BondBasis),
schedule(Frequency::Semiannual),
Shared::clone(&index),
0.0,
Actual360::new(),
None,
Shared::clone(&settings),
)
.unwrap()
.into_fixed_vs_floating()
};
for vol in vols {
for exercise in EXERCISES {
let exercise_date = calendar.advance(
today,
exercise,
TimeUnit::Years,
BusinessDayConvention::Following,
false,
);
let start_date = calendar.advance(
exercise_date,
SETTLEMENT_DAYS,
TimeUnit::Days,
BusinessDayConvention::Following,
false,
);
for length in LENGTHS {
for strike in strikes {
for (swap_type, settlement_type, settlement_method) in cases {
let volatility = if use_bachelier_vol { vol / 100.0 } else { vol };
let engine = make_engine(
&discount,
make_quote_handle(volatility).handle(),
&settings,
);
proj_quote.set_value(projection_rate);
let swap =
shared_mut(make_underlying(start_date, length, strike, swap_type));
let swap_engine = shared_mut(DiscountingSwapEngine::new(
discount.clone(),
Some(false),
None,
None,
Shared::clone(&settings),
))
as SharedMut<dyn PricingEngine>;
swap.borrow_mut().base_mut().set_pricing_engine(swap_engine);
let fair_rate = swap.borrow_mut().fair_rate().unwrap();
let mut swaption = Swaption::new(
SharedMut::clone(&swap),
shared(EuropeanExercise::new(exercise_date))
as Shared<dyn crate::exercise::Exercise>,
settlement_type,
settlement_method,
Shared::clone(&settings),
);
swaption.base_mut().set_pricing_engine(engine);
let value = swaption.npv().unwrap();
let delta = swaption.result::<Real>("delta").unwrap() * bump;
proj_quote.set_value(projection_rate + bump);
let bumped_fair_rate = swap.borrow_mut().fair_rate().unwrap();
let bumped_value = swaption.npv().unwrap();
let bumped_delta = swaption.result::<Real>("delta").unwrap() * bump;
let delta_bump = bumped_fair_rate - fair_rate;
let approx_delta = (bumped_value - value) / delta_bump * bump;
let lower = delta.min(bumped_delta) - epsilon;
let upper = delta.max(bumped_delta) + epsilon;
assert!(
lower < approx_delta && approx_delta < upper,
"swaption delta {exercise}y/{length}y strike {strike} vol \
{volatility} {swap_type:?}/{settlement_type:?}: analytical bracket \
[{lower}, {upper}] does not contain finite difference {approx_delta}"
);
}
}
}
}
}
}
#[test]
fn swaption_delta_in_the_black_model() {
check_swaption_delta(false, |discount, vol, settings| {
shared_mut(BlackSwaptionEngine::with_flat_vol(
discount.clone(),
vol,
Actual365Fixed::new(),
0.0,
CashAnnuityModel::DiscountCurve,
Shared::clone(settings),
)) as SharedMut<dyn PricingEngine>
});
}
#[test]
fn swaption_delta_in_the_bachelier_model() {
check_swaption_delta(true, |discount, vol, settings| {
shared_mut(BachelierSwaptionEngine::with_flat_vol(
discount.clone(),
vol,
Actual365Fixed::new(),
0.0,
CashAnnuityModel::DiscountCurve,
Shared::clone(settings),
)) as SharedMut<dyn PricingEngine>
});
}
#[test]
fn cash_settled_annuity_ratio_matches_the_hand_summed_annuity() {
let vars = Vars::new(Date::new(13, Month::March, 2002), true);
let strike = 0.05;
let conventions = [
(
"u360",
BusinessDayConvention::Unadjusted,
Thirty360::with_convention(Convention::BondBasis),
),
(
"u365",
BusinessDayConvention::Unadjusted,
Actual365Fixed::new(),
),
(
"a360",
BusinessDayConvention::ModifiedFollowing,
Thirty360::with_convention(Convention::BondBasis),
),
(
"a365",
BusinessDayConvention::ModifiedFollowing,
Actual365Fixed::new(),
),
];
for exercise in EXERCISES {
let exercise_date = vars.years(vars.today, exercise);
let start_date = vars.spot(exercise_date);
for length in LENGTHS {
let maturity = vars.calendar.advance(
start_date,
length,
TimeUnit::Years,
BusinessDayConvention::ModifiedFollowing,
false,
);
for (label, fixed_convention, fixed_day_count) in &conventions {
let (npv_ratio, annuity_ratio) = vars.cash_settled_ratio(
start_date,
maturity,
exercise_date,
strike,
*fixed_convention,
fixed_day_count.clone(),
);
let difference = (annuity_ratio - npv_ratio).abs();
assert!(
difference <= 1.0e-10,
"cash-settled annuity ratio {exercise}y/{length}y {label}: \
npv ratio {npv_ratio} vs annuity ratio {annuity_ratio} \
(difference {difference})"
);
}
}
}
}
}