use crate::errors::QlResult;
use crate::fail;
use crate::instruments::Bond;
use crate::shared::Shared;
use crate::time::date::Date;
use crate::types::Real;
pub trait Claim {
fn amount(&self, default_date: &Date, notional: Real, recovery_rate: Real) -> QlResult<Real>;
fn as_any(&self) -> Option<&dyn std::any::Any> {
None
}
}
#[derive(Debug, Clone, Copy)]
pub struct FaceValueClaim;
impl Claim for FaceValueClaim {
fn amount(&self, _default_date: &Date, notional: Real, recovery_rate: Real) -> QlResult<Real> {
Ok(notional * (1.0 - recovery_rate))
}
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
}
#[derive(Clone)]
pub struct FaceValueAccrualClaim {
reference_security: Shared<Bond>,
}
impl FaceValueAccrualClaim {
#[must_use]
pub fn new(reference_security: Shared<Bond>) -> FaceValueAccrualClaim {
FaceValueAccrualClaim { reference_security }
}
}
impl Claim for FaceValueAccrualClaim {
fn amount(&self, default_date: &Date, notional: Real, recovery_rate: Real) -> QlResult<Real> {
let reference_notional = self.reference_security.notional(Some(*default_date))?;
if reference_notional == 0.0 {
fail!("the reference security has no notional at {default_date}");
}
let accrual = self
.reference_security
.accrued_amount(Some(*default_date))?
/ reference_notional;
Ok(notional * (1.0 - recovery_rate - accrual))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cashflow::{CashFlow, Leg};
use crate::cashflows::FixedRateCoupon;
use crate::settings::Settings;
use crate::shared::shared;
use crate::time::calendars::nullcalendar::NullCalendar;
use crate::time::date::Month;
use crate::time::daycounters::actual360::Actual360;
fn a_date() -> Date {
Date::new(15, Month::June, 2026)
}
#[test]
fn face_value_claim_pays_the_non_recovered_notional() {
let claim = FaceValueClaim;
assert_eq!(claim.amount(&a_date(), 100.0, 0.5).unwrap(), 50.0);
assert_eq!(
claim.amount(&a_date(), 1_000_000.0, 0.25).unwrap(),
750_000.0
);
}
#[test]
fn face_value_claim_at_the_recovery_extremes() {
let claim = FaceValueClaim;
assert_eq!(claim.amount(&a_date(), 100.0, 0.0).unwrap(), 100.0);
assert_eq!(claim.amount(&a_date(), 100.0, 1.0).unwrap(), 0.0);
}
#[test]
fn face_value_claim_ignores_the_default_date() {
let claim = FaceValueClaim;
let early = Date::new(1, Month::January, 2026);
let late = Date::new(31, Month::December, 2030);
assert_eq!(
claim.amount(&early, 100.0, 0.5).unwrap(),
claim.amount(&late, 100.0, 0.5).unwrap()
);
}
#[test]
fn claim_is_dyn_compatible() {
let claim: Shared<dyn Claim> = shared(FaceValueClaim);
assert_eq!(claim.amount(&a_date(), 100.0, 0.5).unwrap(), 50.0);
}
fn issue_date() -> Date {
Date::new(7, Month::July, 2026)
}
fn par_bond() -> Shared<Bond> {
let settings = shared(Settings::new());
settings.set_evaluation_date(issue_date());
let day_counter = Actual360::new();
let coupons: Leg = vec![
shared(FixedRateCoupon::from_rate(
Date::new(7, Month::July, 2027),
100.0,
0.05,
day_counter.clone(),
issue_date(),
Date::new(7, Month::July, 2027),
None,
None,
None,
)) as Shared<dyn CashFlow>,
shared(FixedRateCoupon::from_rate(
Date::new(7, Month::July, 2028),
100.0,
0.05,
day_counter,
Date::new(7, Month::July, 2027),
Date::new(7, Month::July, 2028),
None,
None,
None,
)) as Shared<dyn CashFlow>,
];
shared(
Bond::from_coupons(
2,
NullCalendar::new(),
Some(Date::new(1, Month::July, 2026)),
coupons,
settings,
)
.unwrap(),
)
}
#[test]
fn face_value_accrual_claim_surrenders_the_reference_accrual() {
let claim = FaceValueAccrualClaim::new(par_bond());
let mid_first_period = Date::new(7, Month::January, 2027);
let amount = claim.amount(&mid_first_period, 100.0, 0.4).unwrap();
assert!(
(amount - 57.444_444_444_444_44).abs() < 1.0e-12,
"the accrual claim paid {amount} rather than 57.44444444444444"
);
}
#[test]
fn face_value_accrual_claim_pays_under_the_face_value_claim() {
let accrual_claim = FaceValueAccrualClaim::new(par_bond());
let face_value_claim = FaceValueClaim;
let mid_first_period = Date::new(7, Month::January, 2027);
let accrual_amount = accrual_claim.amount(&mid_first_period, 100.0, 0.4).unwrap();
let face_value_amount = face_value_claim
.amount(&mid_first_period, 100.0, 0.4)
.unwrap();
assert_eq!(face_value_amount, 60.0);
assert!(
accrual_amount < face_value_amount,
"the accrual claim paid {accrual_amount}, not under the face value {face_value_amount}"
);
}
#[test]
fn face_value_accrual_claim_fails_on_a_redeemed_reference() {
let claim = FaceValueAccrualClaim::new(par_bond());
let past_redemption = Date::new(8, Month::July, 2028);
assert!(claim.amount(&past_redemption, 100.0, 0.4).is_err());
}
#[test]
fn face_value_accrual_claim_declines_the_downcast_seam() {
let claim = FaceValueAccrualClaim::new(par_bond());
assert!(claim.as_any().is_none());
}
}