use crate::errors::QlResult;
use crate::handle::Handle;
use crate::option::OptionType;
use crate::patterns::observable::{AsObservable, Observable, Observer, ResetThenNotify};
use crate::pricingengines::blackformula::{bachelier_black_formula, black_formula};
use crate::shared::{Shared, SharedMut};
use crate::termstructures::volatility::{OptionletVolatilityStructure, VolatilityType};
use crate::time::date::Date;
use crate::types::{Rate, Real, Spread};
use crate::{fail, require};
use super::floatingratecoupon::FloatingRateCoupon;
pub trait FloatingRateCouponPricer: AsObservable {
fn initialize(&mut self, coupon: &FloatingRateCoupon);
fn swaplet_rate(&self) -> QlResult<Rate>;
fn swaplet_rate_for(&self, index_fixing: QlResult<Rate>) -> QlResult<Rate>;
fn caplet_rate(&self, effective_cap: Rate, forward: QlResult<Rate>) -> QlResult<Rate>;
fn floorlet_rate(&self, effective_floor: Rate, forward: QlResult<Rate>) -> QlResult<Rate>;
}
pub struct BlackIborCouponPricer {
gearing: Real,
spread: Spread,
is_in_arrears: bool,
index_fixing: Option<QlResult<Rate>>,
fixing_date: Option<Date>,
eval_date: Option<Date>,
caplet_vol: Handle<dyn OptionletVolatilityStructure>,
observable: Shared<Observable>,
forwarder: SharedMut<ResetThenNotify>,
}
impl Default for BlackIborCouponPricer {
fn default() -> Self {
let (observable, forwarder) = ResetThenNotify::forwarder();
BlackIborCouponPricer {
gearing: 1.0,
spread: 0.0,
is_in_arrears: false,
index_fixing: None,
fixing_date: None,
eval_date: None,
caplet_vol: Handle::empty(),
observable,
forwarder,
}
}
}
impl BlackIborCouponPricer {
pub fn new() -> Self {
Self::default()
}
pub fn with_vol(vol: Handle<dyn OptionletVolatilityStructure>) -> Self {
let mut pricer = Self::default();
pricer.set_caplet_volatility(vol);
pricer
}
pub fn set_caplet_volatility(&mut self, vol: Handle<dyn OptionletVolatilityStructure>) {
self.caplet_vol = vol;
let observer = self.forwarder.clone() as SharedMut<dyn Observer>;
self.caplet_vol.register_observer(&observer);
self.observable.notify_observers();
}
fn optionlet_rate(
&self,
option_type: OptionType,
eff_strike: Rate,
forward: Rate,
) -> QlResult<Rate> {
let Some(fixing_date) = self.fixing_date else {
fail!("pricer not initialized: no coupon captured");
};
let Some(eval_date) = self.eval_date else {
fail!("no evaluation date set: a caplet needs a reference date");
};
if fixing_date <= eval_date {
let (a, b) = match option_type {
OptionType::Call => (forward, eff_strike),
OptionType::Put => (eff_strike, forward),
};
return Ok((a - b).max(0.0));
}
require!(!self.caplet_vol.is_empty(), "missing optionlet volatility");
let surface = self.caplet_vol.current_link()?;
let std_dev = surface
.black_variance_date(fixing_date, eff_strike, false)?
.sqrt();
let shift = surface.displacement();
match surface.volatility_type() {
VolatilityType::ShiftedLognormal => {
black_formula(option_type, eff_strike, forward, std_dev, 1.0, shift)
}
VolatilityType::Normal => {
bachelier_black_formula(option_type, eff_strike, forward, std_dev, 1.0)
}
}
}
}
impl AsObservable for BlackIborCouponPricer {
fn observable(&self) -> &Observable {
&self.observable
}
}
impl FloatingRateCouponPricer for BlackIborCouponPricer {
fn initialize(&mut self, coupon: &FloatingRateCoupon) {
self.gearing = coupon.gearing();
self.spread = coupon.spread();
self.is_in_arrears = coupon.is_in_arrears();
self.index_fixing = Some(coupon.index_fixing());
self.fixing_date = Some(coupon.fixing_date());
self.eval_date = coupon.index().base().settings().evaluation_date();
}
fn swaplet_rate(&self) -> QlResult<Rate> {
let Some(index_fixing) = &self.index_fixing else {
fail!("pricer not initialized: no coupon captured");
};
self.swaplet_rate_for(index_fixing.clone())
}
fn swaplet_rate_for(&self, index_fixing: QlResult<Rate>) -> QlResult<Rate> {
require!(
!self.is_in_arrears,
"in-arrears convexity adjustment not ported: cap/floor slice"
);
Ok(self.gearing * index_fixing? + self.spread)
}
fn caplet_rate(&self, effective_cap: Rate, forward: QlResult<Rate>) -> QlResult<Rate> {
Ok(self.gearing * self.optionlet_rate(OptionType::Call, effective_cap, forward?)?)
}
fn floorlet_rate(&self, effective_floor: Rate, forward: QlResult<Rate>) -> QlResult<Rate> {
Ok(self.gearing * self.optionlet_rate(OptionType::Put, effective_floor, forward?)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::currency::Currency;
use crate::handle::Handle;
use crate::indexes::iborindex::IborIndex;
use crate::indexes::index::Index;
use crate::interestrate::Compounding;
use crate::settings::Settings;
use crate::shared::shared;
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendars::target::Target;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::timeunit::TimeUnit;
fn index_on(
settings: Shared<Settings<Date>>,
forwarding: Handle<dyn YieldTermStructure>,
) -> Shared<IborIndex> {
shared(IborIndex::new(
"foo".into(),
Period::new(6, TimeUnit::Months),
2,
Currency::eur(),
Target::new(),
BusinessDayConvention::Following,
false,
Actual360::new(),
forwarding,
settings,
))
}
fn flat_curve(reference: Date, rate: Rate) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
reference,
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
#[test]
fn a_determined_coupon_prices_the_intrinsic_optionlet() {
let today = Date::new(15, Month::June, 2026);
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let index = index_on(settings, Handle::empty());
let start = Date::new(3, Month::June, 2026);
let end = Date::new(3, Month::December, 2026);
let coupon = FloatingRateCoupon::new(
end,
100.0,
start,
end,
Some(2),
index.clone(),
1.0,
0.0,
None,
None,
None,
false,
None,
BusinessDayConvention::Preceding,
)
.unwrap();
index.add_fixing(coupon.fixing_date(), 0.05).unwrap();
let mut pricer = BlackIborCouponPricer::new();
pricer.initialize(&coupon);
let forward = || coupon.index_fixing();
assert!((pricer.caplet_rate(0.03, forward()).unwrap() - 0.02).abs() < 1e-15);
assert_eq!(pricer.caplet_rate(0.06, forward()).unwrap(), 0.0);
assert!((pricer.floorlet_rate(0.06, forward()).unwrap() - 0.01).abs() < 1e-15);
assert_eq!(pricer.floorlet_rate(0.03, forward()).unwrap(), 0.0);
}
#[test]
fn an_undetermined_coupon_without_a_surface_refuses() {
let today = Date::new(15, Month::June, 2026);
let settings = shared(Settings::<Date>::new());
settings.set_evaluation_date(today);
let index = index_on(settings, flat_curve(today, 0.03));
let start = Date::new(15, Month::June, 2027);
let end = Date::new(15, Month::December, 2027);
let coupon = FloatingRateCoupon::new(
end,
100.0,
start,
end,
Some(2),
index,
1.0,
0.0,
None,
None,
None,
false,
None,
BusinessDayConvention::Preceding,
)
.unwrap();
let mut pricer = BlackIborCouponPricer::new();
pricer.initialize(&coupon);
let err = pricer.caplet_rate(0.03, coupon.index_fixing()).unwrap_err();
assert!(err.message().contains("missing optionlet volatility"));
}
#[test]
fn an_uninitialized_pricer_refuses_the_optionlet() {
let pricer = BlackIborCouponPricer::new();
assert!(
pricer
.caplet_rate(0.03, Ok(0.05))
.unwrap_err()
.message()
.contains("not initialized")
);
}
}