use std::cell::Cell;
use crate::ensure;
use crate::errors::QlResult;
use crate::handle::{AsObservable, Handle};
use crate::indexes::iborindex::IborIndex;
use crate::indexes::interestrateindex::InterestRateIndex;
use crate::models::shortrate::hullwhite::convexity_bias;
use crate::patterns::observable::{Observable, Observer, ResetThenNotify};
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut};
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::imm;
use crate::types::Real;
use super::{Quote, invalidator};
pub struct FuturesConvAdjustmentQuote {
day_counter: DayCounter,
futures_date: Date,
index_maturity_date: Date,
futures_quote: Handle<dyn Quote>,
volatility: Handle<dyn Quote>,
mean_reversion: Handle<dyn Quote>,
settings: Shared<Settings<Date>>,
cache: Shared<Cell<Option<Real>>>,
observable: Shared<Observable>,
_listener: SharedMut<ResetThenNotify>,
}
impl FuturesConvAdjustmentQuote {
pub fn new(
index: &IborIndex,
futures_date: Date,
futures_quote: Handle<dyn Quote>,
volatility: Handle<dyn Quote>,
mean_reversion: Handle<dyn Quote>,
settings: Shared<Settings<Date>>,
) -> QlResult<FuturesConvAdjustmentQuote> {
let index_maturity_date = index.maturity_date(futures_date)?;
let (cache, observable, listener) = invalidator();
let observer = listener.clone() as SharedMut<dyn Observer>;
futures_quote.register_observer(&observer);
volatility.register_observer(&observer);
mean_reversion.register_observer(&observer);
settings.register_eval_date_observer(&observer);
Ok(FuturesConvAdjustmentQuote {
day_counter: index.day_counter().clone(),
futures_date,
index_maturity_date,
futures_quote,
volatility,
mean_reversion,
settings,
cache,
observable,
_listener: listener,
})
}
pub fn from_imm_code(
index: &IborIndex,
imm_code: &str,
futures_quote: Handle<dyn Quote>,
volatility: Handle<dyn Quote>,
mean_reversion: Handle<dyn Quote>,
settings: Shared<Settings<Date>>,
) -> QlResult<FuturesConvAdjustmentQuote> {
let Some(evaluation_date) = settings.evaluation_date() else {
crate::fail!("no evaluation date set");
};
let futures_date = imm::date(imm_code, evaluation_date)?;
Self::new(
index,
futures_date,
futures_quote,
volatility,
mean_reversion,
settings,
)
}
pub fn futures_value(&self) -> QlResult<Real> {
self.futures_quote.current_link()?.value()
}
pub fn volatility(&self) -> QlResult<Real> {
self.volatility.current_link()?.value()
}
pub fn mean_reversion(&self) -> QlResult<Real> {
self.mean_reversion.current_link()?.value()
}
pub fn imm_date(&self) -> Date {
self.futures_date
}
}
impl AsObservable for FuturesConvAdjustmentQuote {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl Quote for FuturesConvAdjustmentQuote {
fn value(&self) -> QlResult<Real> {
if let Some(cached) = self.cache.get() {
return Ok(cached);
}
ensure!(self.is_valid(), "invalid FuturesConvAdjustmentQuote");
let Some(settlement_date) = self.settings.evaluation_date() else {
crate::fail!("no evaluation date set");
};
let start_time = self
.day_counter
.year_fraction(settlement_date, self.futures_date);
let index_maturity = self
.day_counter
.year_fraction(settlement_date, self.index_maturity_date);
let value = convexity_bias(
self.futures_value()?,
start_time,
index_maturity,
self.volatility()?,
self.mean_reversion()?,
)?;
self.cache.set(Some(value));
Ok(value)
}
fn is_valid(&self) -> bool {
[&self.futures_quote, &self.volatility, &self.mean_reversion]
.iter()
.all(|handle| handle.current_link().is_ok_and(|quote| quote.is_valid()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indexes::ibor::euribor::Euribor;
use crate::quotes::{SimpleQuote, make_quote_handle};
use crate::shared::shared;
use crate::time::date::Month;
use crate::time::imm;
fn fixture() -> (
Shared<Settings<Date>>,
Shared<IborIndex>,
Shared<SimpleQuote>,
FuturesConvAdjustmentQuote,
) {
let settings = shared(Settings::<Date>::new());
let today = Date::new(25, Month::September, 2019);
settings.set_evaluation_date(today);
let index = shared(Euribor::three_months(Handle::empty(), settings.clone()));
let futures_date = imm::next_date(today, true);
let volatility = shared(SimpleQuote::new(1.0));
let quote = FuturesConvAdjustmentQuote::new(
&index,
futures_date,
Handle::new(shared(SimpleQuote::new(95.419)) as Shared<dyn Quote>),
Handle::new(Shared::clone(&volatility) as Shared<dyn Quote>),
make_quote_handle(0.03).handle(),
settings.clone(),
)
.expect("the index resolves the maturity of an IMM date");
(settings, index, volatility, quote)
}
#[test]
fn futures_conv_adjustment_quote_reproduces_the_cpp_bias() {
let (_settings, index, _volatility, quote) = fixture();
let today = Date::new(25, Month::September, 2019);
assert_eq!(quote.imm_date(), Date::new(18, Month::December, 2019));
assert_eq!(
index.maturity_date(quote.imm_date()).unwrap(),
Date::new(18, Month::March, 2020)
);
let start_time = index.day_counter().year_fraction(today, quote.imm_date());
let index_maturity = index
.day_counter()
.year_fraction(today, index.maturity_date(quote.imm_date()).unwrap());
assert!((start_time - 0.233_333_333_333_333_34).abs() < 1.0e-15);
assert!((index_maturity - 0.486_111_111_111_111_1).abs() < 1.0e-15);
let value = quote.value().unwrap();
assert!(
(value - 0.085_125_076_735_947_71).abs() < 1.0e-12,
"bias at vol 1.0 is {value}"
);
}
#[test]
fn a_volatility_change_drops_the_cached_bias() {
let (_settings, _index, volatility, quote) = fixture();
assert!((quote.value().unwrap() - 0.085_125_076_735_947_71).abs() < 1.0e-12);
volatility.set_value(0.2);
let value = quote.value().unwrap();
assert!(
(value - 0.003_440_259_983_763_012).abs() < 1.0e-12,
"bias at vol 0.2 is {value}"
);
}
#[test]
fn an_evaluation_date_change_re_times_the_bias() {
let (settings, index, _volatility, quote) = fixture();
assert!((quote.value().unwrap() - 0.085_125_076_735_947_71).abs() < 1.0e-12);
let moved = Date::new(25, Month::October, 2019);
settings.set_evaluation_date(moved);
let day_counter = index.day_counter();
let expected = convexity_bias(
95.419,
day_counter.year_fraction(moved, quote.imm_date()),
day_counter.year_fraction(moved, index.maturity_date(quote.imm_date()).unwrap()),
1.0,
0.03,
)
.unwrap();
let value = quote.value().unwrap();
assert!(
(value - expected).abs() < 1.0e-15,
"bias at the moved evaluation date is {value}, expected {expected}"
);
assert!(
(value - 0.085_125_076_735_947_71).abs() > 1.0e-3,
"the moved evaluation date must change the bias"
);
}
#[test]
fn an_empty_handle_makes_the_quote_invalid() {
let settings = shared(Settings::<Date>::new());
let today = Date::new(25, Month::September, 2019);
settings.set_evaluation_date(today);
let index = shared(Euribor::three_months(Handle::empty(), settings.clone()));
let quote = FuturesConvAdjustmentQuote::new(
&index,
imm::next_date(today, true),
Handle::new(shared(SimpleQuote::new(95.419)) as Shared<dyn Quote>),
Handle::empty(),
make_quote_handle(0.03).handle(),
settings,
)
.expect("the index resolves the maturity of an IMM date");
assert!(!quote.is_valid());
assert!(quote.value().is_err());
}
#[test]
fn the_imm_code_constructor_resolves_the_futures_date() {
let (settings, index, _volatility, _quote) = fixture();
let quote = FuturesConvAdjustmentQuote::from_imm_code(
&index,
"Z9",
Handle::new(shared(SimpleQuote::new(95.419)) as Shared<dyn Quote>),
Handle::new(shared(SimpleQuote::new(1.0)) as Shared<dyn Quote>),
make_quote_handle(0.03).handle(),
settings,
)
.expect("Z9 is an IMM code");
assert_eq!(quote.imm_date(), Date::new(18, Month::December, 2019));
assert!((quote.value().unwrap() - 0.085_125_076_735_947_71).abs() < 1.0e-12);
}
}