use std::any::Any;
use std::fmt;
use crate::cashflow::Leg;
use crate::cashflows::CashFlows;
use crate::errors::QlResult;
use crate::instrument::{Instrument, InstrumentBase, InstrumentResults};
use crate::pricingengine::{Arguments, GenericEngine, Results};
use crate::settings::Settings;
use crate::shared::Shared;
use crate::time::date::Date;
use crate::types::{DiscountFactor, Real};
use crate::utilities::null::Null;
use crate::{fail, require};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SwapType {
Receiver = -1,
Payer = 1,
}
impl fmt::Display for SwapType {
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SwapType::Payer => out.write_str("Payer"),
SwapType::Receiver => out.write_str("Receiver"),
}
}
}
#[derive(Default)]
pub struct SwapArguments {
pub legs: Vec<Leg>,
pub payer: Vec<Real>,
}
impl Arguments for SwapArguments {
fn validate(&self) -> QlResult<()> {
require!(
self.legs.len() == self.payer.len(),
"number of legs and multipliers differ"
);
Ok(())
}
}
#[derive(Default)]
pub struct SwapResults {
pub instrument: InstrumentResults,
pub leg_npv: Vec<Real>,
pub leg_bps: Vec<Real>,
pub start_discounts: Vec<DiscountFactor>,
pub end_discounts: Vec<DiscountFactor>,
pub npv_date_discount: Option<DiscountFactor>,
}
impl Results for SwapResults {
fn reset(&mut self) {
self.instrument.reset();
self.leg_npv.clear();
self.leg_bps.clear();
self.start_discounts.clear();
self.end_discounts.clear();
self.npv_date_discount = None;
}
fn as_instrument_results(&self) -> Option<&InstrumentResults> {
Some(&self.instrument)
}
}
pub type SwapEngine = GenericEngine<SwapArguments, SwapResults>;
pub struct Swap {
base: InstrumentBase,
settings: Shared<Settings<Date>>,
legs: Vec<Leg>,
payer: Vec<Real>,
leg_npv: Vec<Option<Real>>,
leg_bps: Vec<Option<Real>>,
start_discounts: Vec<Option<DiscountFactor>>,
end_discounts: Vec<Option<DiscountFactor>>,
npv_date_discount: Option<DiscountFactor>,
}
impl Swap {
pub fn new(
legs: Vec<Leg>,
payer: Vec<bool>,
settings: Shared<Settings<Date>>,
) -> QlResult<Swap> {
require!(
payer.len() == legs.len(),
"size mismatch between payer ({}) and legs ({})",
payer.len(),
legs.len()
);
let n = legs.len();
let base = InstrumentBase::new();
settings.register_eval_date_observer(&base.observer());
let payer = payer.iter().map(|&p| if p { -1.0 } else { 1.0 }).collect();
let swap = Swap {
base,
settings,
legs,
payer,
leg_npv: vec![Some(0.0); n],
leg_bps: vec![Some(0.0); n],
start_discounts: vec![Some(0.0); n],
end_discounts: vec![Some(0.0); n],
npv_date_discount: Some(0.0),
};
for leg in &swap.legs {
for flow in leg {
swap.base.register_with(flow.observable());
}
}
Ok(swap)
}
pub fn two_leg(first_leg: Leg, second_leg: Leg, settings: Shared<Settings<Date>>) -> Swap {
Swap::new(vec![first_leg, second_leg], vec![true, false], settings)
.expect("two legs match two payer flags")
}
pub fn number_of_legs(&self) -> usize {
self.legs.len()
}
pub fn legs(&self) -> &[Leg] {
&self.legs
}
pub fn leg(&self, j: usize) -> QlResult<&Leg> {
require!(j < self.legs.len(), "leg #{j} doesn't exist!");
Ok(&self.legs[j])
}
pub fn payer(&self, j: usize) -> QlResult<bool> {
require!(j < self.legs.len(), "leg #{j} doesn't exist!");
Ok(self.payer[j] < 0.0)
}
pub fn start_date(&self) -> QlResult<Date> {
require!(!self.legs.is_empty(), "no legs given");
let mut date = CashFlows::start_date(&self.legs[0])?;
for leg in &self.legs[1..] {
date = date.min(CashFlows::start_date(leg)?);
}
Ok(date)
}
pub fn maturity_date(&self) -> QlResult<Date> {
require!(!self.legs.is_empty(), "no legs given");
let mut date = CashFlows::maturity_date(&self.legs[0])?;
for leg in &self.legs[1..] {
date = date.max(CashFlows::maturity_date(leg)?);
}
Ok(date)
}
pub fn leg_npv(&mut self, j: usize) -> QlResult<Real> {
require!(j < self.legs.len(), "leg #{j} doesn't exist!");
self.calculate()?;
let Some(value) = self.leg_npv[j] else {
fail!("result not available");
};
Ok(value)
}
pub fn leg_bps(&mut self, j: usize) -> QlResult<Real> {
require!(j < self.legs.len(), "leg #{j} doesn't exist!");
self.calculate()?;
let Some(value) = self.leg_bps[j] else {
fail!("result not available");
};
Ok(value)
}
pub fn start_discounts(&mut self, j: usize) -> QlResult<DiscountFactor> {
require!(j < self.legs.len(), "leg #{j} doesn't exist!");
self.calculate()?;
let Some(value) = self.start_discounts[j] else {
fail!("result not available");
};
Ok(value)
}
pub fn end_discounts(&mut self, j: usize) -> QlResult<DiscountFactor> {
require!(j < self.legs.len(), "leg #{j} doesn't exist!");
self.calculate()?;
let Some(value) = self.end_discounts[j] else {
fail!("result not available");
};
Ok(value)
}
pub fn npv_date_discount(&mut self) -> QlResult<DiscountFactor> {
self.calculate()?;
let Some(value) = self.npv_date_discount else {
fail!("result not available");
};
Ok(value)
}
pub fn deep_update(&mut self) {
self.base().observer().borrow_mut().update();
}
}
impl Instrument for Swap {
fn base(&self) -> &InstrumentBase {
&self.base
}
fn base_mut(&mut self) -> &mut InstrumentBase {
&mut self.base
}
fn is_expired(&self) -> QlResult<bool> {
for leg in &self.legs {
if !CashFlows::is_expired(leg, &self.settings, None, None)? {
return Ok(false);
}
}
Ok(true)
}
fn setup_arguments(&self, arguments: &mut dyn Arguments) -> QlResult<()> {
let Some(arguments) = (arguments as &mut dyn Any).downcast_mut::<SwapArguments>() else {
fail!("wrong argument type");
};
arguments.legs = self.legs.clone();
arguments.payer = self.payer.clone();
Ok(())
}
fn setup_expired(&mut self) {
let expired = InstrumentResults {
value: Some(0.0),
error_estimate: Some(0.0),
..InstrumentResults::default()
};
self.base_mut().store_results(&expired);
self.leg_npv.iter_mut().for_each(|v| *v = Some(0.0));
self.leg_bps.iter_mut().for_each(|v| *v = Some(0.0));
self.start_discounts.iter_mut().for_each(|v| *v = Some(0.0));
self.end_discounts.iter_mut().for_each(|v| *v = Some(0.0));
self.npv_date_discount = Some(0.0);
}
fn fetch_results(&mut self, results: &dyn Results) -> QlResult<()> {
let Some(results) = (results as &dyn Any).downcast_ref::<SwapResults>() else {
fail!("wrong result type");
};
self.base_mut().store_results(&results.instrument);
if results.leg_npv.is_empty() {
self.leg_npv.iter_mut().for_each(|v| *v = None);
} else {
require!(
results.leg_npv.len() == self.leg_npv.len(),
"wrong number of leg NPV returned"
);
self.leg_npv = results
.leg_npv
.iter()
.map(|&v| (!v.is_null()).then_some(v))
.collect();
}
if results.leg_bps.is_empty() {
self.leg_bps.iter_mut().for_each(|v| *v = None);
} else {
require!(
results.leg_bps.len() == self.leg_bps.len(),
"wrong number of leg BPS returned"
);
self.leg_bps = results
.leg_bps
.iter()
.map(|&v| (!v.is_null()).then_some(v))
.collect();
}
if results.start_discounts.is_empty() {
self.start_discounts.iter_mut().for_each(|v| *v = None);
} else {
require!(
results.start_discounts.len() == self.start_discounts.len(),
"wrong number of leg start discounts returned"
);
self.start_discounts = results
.start_discounts
.iter()
.map(|&v| (!v.is_null()).then_some(v))
.collect();
}
if results.end_discounts.is_empty() {
self.end_discounts.iter_mut().for_each(|v| *v = None);
} else {
require!(
results.end_discounts.len() == self.end_discounts.len(),
"wrong number of leg end discounts returned"
);
self.end_discounts = results
.end_discounts
.iter()
.map(|&v| (!v.is_null()).then_some(v))
.collect();
}
self.npv_date_discount = results.npv_date_discount.filter(|v| !v.is_null());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflow::CashFlow;
use crate::cashflows::SimpleCashFlow;
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::PricingEngine;
use crate::shared::{SharedMut, shared, shared_mut};
use crate::time::date::Month;
fn today() -> Date {
Date::new(7, Month::July, 2026)
}
fn settings_today() -> Shared<Settings<Date>> {
let settings = shared(Settings::new());
settings.set_evaluation_date(today());
settings
}
fn leg(amount: Real, date: Date) -> Leg {
vec![shared(SimpleCashFlow::new(amount, date).unwrap()) as Shared<dyn CashFlow>]
}
fn two_leg_swap() -> Swap {
Swap::two_leg(
leg(100.0, Date::new(7, Month::July, 2027)),
leg(100.0, Date::new(7, Month::July, 2028)),
settings_today(),
)
}
struct StubEngine {
base: SwapEngine,
npv: Real,
leg_npv: Vec<Real>,
leg_bps: Vec<Real>,
start_discounts: Vec<DiscountFactor>,
end_discounts: Vec<DiscountFactor>,
npv_date_discount: Option<DiscountFactor>,
}
impl AsObservable for StubEngine {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl PricingEngine for StubEngine {
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 results = self.base.results_mut();
results.instrument.value = Some(self.npv);
results.leg_npv = self.leg_npv.clone();
results.leg_bps = self.leg_bps.clone();
results.start_discounts = self.start_discounts.clone();
results.end_discounts = self.end_discounts.clone();
results.npv_date_discount = self.npv_date_discount;
Ok(())
}
}
fn engine(
leg_npv: Vec<Real>,
leg_bps: Vec<Real>,
start_discounts: Vec<DiscountFactor>,
end_discounts: Vec<DiscountFactor>,
npv_date_discount: Option<DiscountFactor>,
) -> SharedMut<StubEngine> {
shared_mut(StubEngine {
base: SwapEngine::new(SwapArguments::default(), SwapResults::default()),
npv: 2.0,
leg_npv,
leg_bps,
start_discounts,
end_discounts,
npv_date_discount,
})
}
#[test]
fn the_two_leg_ctor_pays_the_first_leg_and_receives_the_second() {
let swap = two_leg_swap();
assert_eq!(swap.number_of_legs(), 2);
assert!(swap.payer(0).unwrap(), "the first leg is paid");
assert!(!swap.payer(1).unwrap(), "the second leg is received");
}
#[test]
fn the_multi_leg_ctor_maps_payer_flags_to_multipliers() {
let swap = Swap::new(
vec![
leg(1.0, today() + 100),
leg(1.0, today() + 200),
leg(1.0, today() + 300),
],
vec![false, true, false],
settings_today(),
)
.unwrap();
assert_eq!(swap.number_of_legs(), 3);
assert!(!swap.payer(0).unwrap());
assert!(swap.payer(1).unwrap());
assert!(!swap.payer(2).unwrap());
}
#[test]
fn a_payer_legs_size_mismatch_is_rejected() {
let error = Swap::new(
vec![leg(1.0, today() + 100), leg(1.0, today() + 200)],
vec![true],
settings_today(),
)
.map(|_| ())
.unwrap_err();
assert_eq!(
error.message(),
"size mismatch between payer (1) and legs (2)"
);
}
#[test]
fn out_of_range_leg_indices_are_rejected() {
let mut swap = two_leg_swap();
assert_eq!(
swap.leg(2).map(|_| ()).unwrap_err().message(),
"leg #2 doesn't exist!"
);
assert_eq!(
swap.payer(2).unwrap_err().message(),
"leg #2 doesn't exist!"
);
assert_eq!(
swap.leg_npv(2).unwrap_err().message(),
"leg #2 doesn't exist!"
);
assert_eq!(
swap.start_discounts(2).unwrap_err().message(),
"leg #2 doesn't exist!"
);
}
#[test]
fn start_and_maturity_span_the_legs() {
let swap = two_leg_swap();
assert_eq!(swap.start_date().unwrap(), Date::new(7, Month::July, 2027));
assert_eq!(
swap.maturity_date().unwrap(),
Date::new(7, Month::July, 2028)
);
}
#[test]
fn is_expired_tracks_the_legs_flows() {
let future = two_leg_swap();
assert!(
!future.is_expired().unwrap(),
"both flows are in the future"
);
let settings = shared(Settings::new());
settings.set_evaluation_date(Date::new(8, Month::July, 2028));
let past = Swap::two_leg(
leg(100.0, Date::new(7, Month::July, 2027)),
leg(100.0, Date::new(7, Month::July, 2028)),
settings,
);
assert!(past.is_expired().unwrap(), "both flows have paid");
}
#[test]
fn the_accessors_read_the_engine_leg_results() {
let mut swap = two_leg_swap();
swap.base_mut().set_pricing_engine(engine(
vec![-98.0, 99.0],
vec![-1.0, 1.0],
vec![1.0, 1.0],
vec![0.95, 0.90],
Some(0.99),
));
assert_eq!(swap.npv().unwrap(), 2.0);
assert_eq!(swap.leg_npv(0).unwrap(), -98.0);
assert_eq!(swap.leg_npv(1).unwrap(), 99.0);
assert_eq!(swap.leg_bps(1).unwrap(), 1.0);
assert_eq!(swap.start_discounts(0).unwrap(), 1.0);
assert_eq!(swap.end_discounts(1).unwrap(), 0.90);
assert_eq!(swap.npv_date_discount().unwrap(), 0.99);
}
#[test]
fn a_null_sentinel_inside_leg_results_is_not_available() {
let mut swap = two_leg_swap();
swap.base_mut().set_pricing_engine(engine(
vec![Real::null(), 99.0],
vec![-1.0, 1.0],
vec![DiscountFactor::null(), 1.0],
vec![0.95, DiscountFactor::null()],
Some(DiscountFactor::null()),
));
let err = swap.leg_npv(0).unwrap_err();
assert!(err.message().contains("result not available"));
assert_eq!(swap.leg_npv(1).unwrap(), 99.0);
let err = swap.start_discounts(0).unwrap_err();
assert!(err.message().contains("result not available"));
assert_eq!(swap.start_discounts(1).unwrap(), 1.0);
assert_eq!(swap.end_discounts(0).unwrap(), 0.95);
assert!(swap.end_discounts(1).is_err());
assert!(swap.npv_date_discount().is_err());
assert_eq!(swap.leg_bps(0).unwrap(), -1.0);
}
#[test]
fn unprovided_leg_results_are_not_available() {
let mut swap = two_leg_swap();
swap.base_mut()
.set_pricing_engine(engine(vec![], vec![], vec![], vec![], None));
assert_eq!(swap.npv().unwrap(), 2.0);
assert_eq!(
swap.leg_npv(0).unwrap_err().message(),
"result not available"
);
assert_eq!(
swap.leg_bps(0).unwrap_err().message(),
"result not available"
);
assert_eq!(
swap.npv_date_discount().unwrap_err().message(),
"result not available"
);
}
#[test]
fn a_wrong_leg_result_count_is_rejected() {
let mut swap = two_leg_swap();
swap.base_mut().set_pricing_engine(engine(
vec![1.0],
vec![1.0, 1.0],
vec![1.0, 1.0],
vec![1.0, 1.0],
Some(1.0),
));
assert_eq!(
swap.leg_npv(0).unwrap_err().message(),
"wrong number of leg NPV returned"
);
}
#[test]
fn an_expired_swap_reports_zero() {
let settings = shared(Settings::new());
settings.set_evaluation_date(Date::new(8, Month::July, 2028));
let mut swap = Swap::two_leg(
leg(100.0, Date::new(7, Month::July, 2027)),
leg(100.0, Date::new(7, Month::July, 2028)),
settings,
);
assert_eq!(swap.npv().unwrap(), 0.0);
assert_eq!(swap.leg_npv(0).unwrap(), 0.0);
assert_eq!(swap.leg_bps(1).unwrap(), 0.0);
assert_eq!(swap.npv_date_discount().unwrap(), 0.0);
}
#[test]
fn the_arguments_reject_a_legs_payer_mismatch() {
let mut arguments = SwapArguments {
legs: vec![leg(1.0, today() + 100)],
payer: vec![-1.0, 1.0],
};
assert_eq!(
arguments.validate().unwrap_err().message(),
"number of legs and multipliers differ"
);
arguments.payer = vec![-1.0];
assert!(arguments.validate().is_ok());
}
#[test]
fn swap_type_displays_payer_and_receiver() {
assert_eq!(SwapType::Payer.to_string(), "Payer");
assert_eq!(SwapType::Receiver.to_string(), "Receiver");
}
}