mod impliedvolatility;
mod oas;
mod sensitivities;
use std::any::Any;
use std::cell::Cell;
use crate::errors::QlResult;
use crate::event::event_has_occurred;
use crate::instrument::{Instrument, InstrumentBase};
use crate::instruments::{Bond, BondPrice, FixedRateBond, ZeroCouponBond};
use crate::pricingengine::{Arguments, Results};
use crate::pricingengines::bond::BlackCallableBondArguments;
use crate::settings::Settings;
use crate::shared::Shared;
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::calendar::Calendar;
use crate::time::calendars::nullcalendar::NullCalendar;
use crate::time::date::Date;
use crate::time::daycounter::DayCounter;
use crate::time::frequency::Frequency;
use crate::time::period::Period;
use crate::time::schedule::Schedule;
use crate::types::{Natural, Rate, Real};
use crate::{fail, require};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CallabilityType {
Call,
Put,
}
#[derive(Clone, Debug)]
pub struct Callability {
price: BondPrice,
call_type: CallabilityType,
date: Date,
}
impl Callability {
pub fn new(price: BondPrice, call_type: CallabilityType, date: Date) -> QlResult<Self> {
require!(
price.amount().is_finite() && price.amount() >= 0.0,
"callability price must be finite and nonnegative"
);
require!(date != Date::null(), "null callability date");
Ok(Self {
price,
call_type,
date,
})
}
pub fn price(&self) -> BondPrice {
self.price
}
pub fn call_type(&self) -> CallabilityType {
self.call_type
}
pub fn date(&self) -> Date {
self.date
}
}
pub type CallabilitySchedule = Vec<Callability>;
#[derive(Default)]
pub struct CallableBondArguments {
pub settlement_date: Option<Date>,
pub coupon_dates: Vec<Date>,
pub coupon_amounts: Vec<Real>,
pub face_amount: Real,
pub redemption: Real,
pub redemption_date: Option<Date>,
pub callability_prices: Vec<Real>,
pub callability_types: Vec<CallabilityType>,
pub callability_dates: Vec<Date>,
}
impl Arguments for CallableBondArguments {
fn validate(&self) -> QlResult<()> {
let Some(settlement) = self.settlement_date else {
fail!("null settlement date");
};
let Some(redemption_date) = self.redemption_date else {
fail!("null redemption date");
};
require!(
settlement != Date::null() && redemption_date != Date::null(),
"null bond date"
);
require!(settlement <= redemption_date, "settlement after redemption");
require!(
self.face_amount.is_finite() && self.face_amount > 0.0,
"face amount must be finite and positive"
);
require!(
self.redemption.is_finite() && self.redemption >= 0.0,
"redemption must be finite and nonnegative"
);
require!(
self.coupon_dates.len() == self.coupon_amounts.len(),
"coupon dates/amounts length mismatch"
);
require!(
self.callability_dates.len() == self.callability_prices.len()
&& self.callability_dates.len() == self.callability_types.len(),
"callability dates/prices/types length mismatch"
);
require!(
self.coupon_dates.windows(2).all(|pair| pair[0] <= pair[1]),
"coupon dates must be ordered"
);
for (date, amount) in self.coupon_dates.iter().zip(&self.coupon_amounts) {
require!(
*date != Date::null() && *date > settlement && *date <= redemption_date,
"coupon outside settlement/redemption interval"
);
require!(amount.is_finite(), "nonfinite coupon amount");
}
for (date, price) in self.callability_dates.iter().zip(&self.callability_prices) {
require!(
*date != Date::null() && *date > settlement && *date <= redemption_date,
"callability outside settlement/redemption interval"
);
require!(
price.is_finite() && *price >= 0.0,
"invalid callability price"
);
}
Ok(())
}
}
fn checked_callable_settlement_date(bond: &Bond, settings: &Settings<Date>) -> QlResult<Date> {
let mut date = settings
.evaluation_date()
.ok_or_else(|| crate::errors::QlError::new("evaluation date is unset", file!(), line!()))?;
require!(date != Date::null(), "null evaluation date");
let calendar = bond.calendar();
let mut remaining = bond.settlement_days();
while remaining > 0 || calendar.is_holiday(date) {
require!(
date < Date::max_date(),
"settlement exceeds supported date range"
);
date += 1;
if calendar.is_business_day(date) {
remaining = remaining.saturating_sub(1);
}
}
Ok(bond.issue_date().map_or(date, |issue| date.max(issue)))
}
pub struct CallableFixedRateBond {
bond: FixedRateBond,
put_call_schedule: CallabilitySchedule,
face_amount: Real,
settings: Shared<Settings<Date>>,
}
impl CallableFixedRateBond {
#[allow(clippy::too_many_arguments)]
pub fn new(
settlement_days: Natural,
face_amount: Real,
schedule: Schedule,
coupons: Vec<Rate>,
accrual_day_counter: DayCounter,
payment_convention: BusinessDayConvention,
redemption: Real,
issue_date: Option<Date>,
put_call_schedule: CallabilitySchedule,
settings: Shared<Settings<Date>>,
) -> QlResult<Self> {
Self::with_ex_coupon(
settlement_days,
face_amount,
schedule,
coupons,
accrual_day_counter,
payment_convention,
redemption,
issue_date,
put_call_schedule,
None,
NullCalendar::new(),
BusinessDayConvention::Unadjusted,
false,
settings,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_ex_coupon(
settlement_days: Natural,
face_amount: Real,
schedule: Schedule,
coupons: Vec<Rate>,
accrual_day_counter: DayCounter,
payment_convention: BusinessDayConvention,
redemption: Real,
issue_date: Option<Date>,
put_call_schedule: CallabilitySchedule,
ex_coupon_period: Option<Period>,
ex_coupon_calendar: Calendar,
ex_coupon_convention: BusinessDayConvention,
ex_coupon_end_of_month: bool,
settings: Shared<Settings<Date>>,
) -> QlResult<Self> {
require!(
schedule.len() >= 2,
"callable bond schedule requires at least two dates"
);
require!(
schedule.dates().iter().all(|date| *date != Date::null())
&& schedule.dates().windows(2).all(|pair| pair[0] < pair[1]),
"callable bond schedule dates must be nonnull and strictly increasing"
);
require!(
face_amount.is_finite() && face_amount > 0.0,
"face amount must be finite and positive"
);
require!(
redemption.is_finite() && redemption >= 0.0,
"redemption must be finite and nonnegative"
);
require!(
coupons.iter().all(|rate| rate.is_finite()),
"nonfinite coupon rate"
);
require!(
put_call_schedule
.iter()
.all(|call| call.date() <= schedule.end_date()),
"bond cannot mature before the last call/put date"
);
let bond = FixedRateBond::new(
settlement_days,
face_amount,
schedule,
coupons,
accrual_day_counter,
payment_convention,
redemption,
issue_date,
None,
ex_coupon_period,
ex_coupon_calendar,
ex_coupon_convention,
ex_coupon_end_of_month,
None,
Shared::clone(&settings),
)?;
Ok(Self {
bond,
put_call_schedule,
face_amount,
settings,
})
}
fn checked_settlement_date(&self) -> QlResult<Date> {
checked_callable_settlement_date(self.bond(), &self.settings)
}
pub fn callability(&self) -> &CallabilitySchedule {
&self.put_call_schedule
}
pub fn bond(&self) -> &Bond {
self.bond.bond()
}
pub fn settlement_value(&mut self) -> QlResult<Real> {
self.calculate()?;
self.bond.bond_mut().settlement_value()
}
pub fn dirty_price(&mut self) -> QlResult<Real> {
self.calculate()?;
let notional = self
.bond()
.notional(Some(self.checked_settlement_date()?))?;
if notional == 0.0 {
return Ok(0.0);
}
let price = (self.settlement_value()? / notional) * 100.0;
require!(price.is_finite(), "callable dirty price overflow");
Ok(price)
}
pub fn clean_price(&mut self) -> QlResult<Real> {
let price = self.dirty_price()?
- self
.bond()
.accrued_amount(Some(self.checked_settlement_date()?))?;
require!(price.is_finite(), "callable clean price overflow");
Ok(price)
}
}
impl Instrument for CallableFixedRateBond {
fn base(&self) -> &InstrumentBase {
self.bond.bond().base()
}
fn base_mut(&mut self) -> &mut InstrumentBase {
self.bond.bond_mut().base_mut()
}
fn is_expired(&self) -> QlResult<bool> {
self.bond.bond().is_expired()
}
fn setup_expired(&mut self) {
self.bond.bond_mut().setup_expired();
}
fn fetch_results(&mut self, results: &dyn Results) -> QlResult<()> {
self.bond.bond_mut().fetch_results(results)
}
fn setup_arguments(&self, arguments: &mut dyn Arguments) -> QlResult<()> {
if let Some(args) = (arguments as &mut dyn Any).downcast_mut::<BlackCallableBondArguments>()
{
self.setup_arguments(&mut args.bond)?;
args.cashflows = self.bond().cashflows().clone();
args.payment_day_counter = Some(self.bond.day_counter().clone());
args.frequency = self.bond.frequency();
args.callability_count = self.put_call_schedule.len();
return Ok(());
}
let Some(args) = (arguments as &mut dyn Any).downcast_mut::<CallableBondArguments>() else {
fail!("wrong argument type");
};
let settlement = self.checked_settlement_date()?;
let redemption =
self.bond().redemptions().first().ok_or_else(|| {
crate::errors::QlError::new("missing redemption", file!(), line!())
})?;
*args = CallableBondArguments {
settlement_date: Some(settlement),
face_amount: self.face_amount,
redemption: redemption.amount()?,
redemption_date: Some(redemption.date()),
..CallableBondArguments::default()
};
for flow in self.bond().cashflows() {
if flow.as_coupon().is_some()
&& !flow.has_occurred(&self.settings, Some(settlement), Some(false))?
&& !flow.trading_ex_coupon(&self.settings, Some(settlement))?
{
args.coupon_dates.push(flow.date());
args.coupon_amounts.push(flow.amount()?);
}
}
for callability in &self.put_call_schedule {
let call_date = callability.date();
if event_has_occurred(call_date, &self.settings, Some(settlement), Some(false))? {
continue;
}
let mut price = callability.price().amount();
if let BondPrice::Clean(_) = callability.price() {
for flow in self.bond().cashflows() {
if !event_has_occurred(
flow.date(),
&self.settings,
Some(call_date),
Some(false),
)? {
if let Some(coupon) = flow.as_coupon() {
let mut accrued = coupon.accrued_amount(call_date)?;
if coupon.trades_ex_coupon_on(call_date) {
accrued += flow.amount()?;
}
let notional = self.bond().notional(Some(call_date))?;
require!(
notional.is_finite() && notional > 0.0,
"invalid notional at call date"
);
price += accrued / notional * 100.0;
}
break;
}
}
}
args.callability_dates.push(call_date);
args.callability_types.push(callability.call_type());
args.callability_prices.push(price);
}
Ok(())
}
}
pub struct CallableZeroCouponBond {
bond: Bond,
put_call_schedule: CallabilitySchedule,
face_amount: Real,
day_counter: DayCounter,
settings: Shared<Settings<Date>>,
settings_flags: Cell<(bool, Option<bool>)>,
}
impl CallableZeroCouponBond {
#[allow(clippy::too_many_arguments)]
pub fn new(
settlement_days: Natural,
face_amount: Real,
calendar: Calendar,
maturity_date: Date,
day_counter: DayCounter,
payment_convention: BusinessDayConvention,
redemption: Real,
issue_date: Option<Date>,
put_call_schedule: CallabilitySchedule,
settings: Shared<Settings<Date>>,
) -> QlResult<Self> {
require!(
face_amount.is_finite() && face_amount > 0.0,
"face amount must be finite and positive"
);
require!(
redemption.is_finite() && redemption >= 0.0,
"redemption must be finite and nonnegative"
);
require!(maturity_date != Date::null(), "null maturity date");
require!(
settlement_days <= (Date::max_date() - Date::min_date()) as Natural,
"settlement lag exceeds supported date range"
);
let payment_date = Self::adjusted_date(&calendar, maturity_date, payment_convention)?;
if let Some(issue) = issue_date {
require!(
issue != Date::null() && issue <= maturity_date && issue <= payment_date,
"invalid issue date"
);
}
require!(
put_call_schedule
.iter()
.all(|call| call.date() <= maturity_date && call.date() <= payment_date),
"bond cannot redeem before the last call/put date"
);
let bond = ZeroCouponBond::new(
settlement_days,
calendar,
face_amount,
maturity_date,
payment_convention,
redemption,
issue_date,
Shared::clone(&settings),
)?
.into_bond();
Ok(Self {
bond,
put_call_schedule,
face_amount,
day_counter,
settings_flags: Cell::new(Self::flags(&settings)),
settings,
})
}
fn adjusted_date(
calendar: &Calendar,
date: Date,
convention: BusinessDayConvention,
) -> QlResult<Date> {
use BusinessDayConvention::*;
let roll = |mut candidate: Date, step| -> QlResult<Date> {
while calendar.is_holiday(candidate) {
require!(
if step > 0 {
candidate < Date::max_date()
} else {
candidate > Date::min_date()
},
"payment adjustment exceeds supported date range"
);
candidate += step;
}
Ok(candidate)
};
match convention {
Unadjusted => Ok(date),
Following => roll(date, 1),
Preceding => roll(date, -1),
ModifiedFollowing | HalfMonthModifiedFollowing => {
let next = roll(date, 1)?;
if next.month() != date.month()
|| (convention == HalfMonthModifiedFollowing
&& date.day_of_month() <= 15
&& next.day_of_month() > 15)
{
roll(date, -1)
} else {
Ok(next)
}
}
ModifiedPreceding => {
let previous = roll(date, -1)?;
if previous.month() != date.month() {
roll(date, 1)
} else {
Ok(previous)
}
}
Nearest => {
let (mut next, mut previous) = (date, date);
while calendar.is_holiday(next) && calendar.is_holiday(previous) {
require!(
next < Date::max_date() && previous > Date::min_date(),
"payment adjustment exceeds supported date range"
);
next += 1;
previous -= 1;
}
Ok(if calendar.is_holiday(next) {
previous
} else {
next
})
}
}
}
fn checked_settlement_date(&self) -> QlResult<Date> {
checked_callable_settlement_date(&self.bond, &self.settings)
}
fn flags(settings: &Settings<Date>) -> (bool, Option<bool>) {
(
settings.include_reference_date_events(),
settings.include_todays_cash_flows(),
)
}
pub fn callability(&self) -> &CallabilitySchedule {
&self.put_call_schedule
}
pub fn bond(&self) -> &Bond {
&self.bond
}
pub fn day_counter(&self) -> &DayCounter {
&self.day_counter
}
pub fn settlement_value(&mut self) -> QlResult<Real> {
self.calculate()?;
self.bond.settlement_value()
}
pub fn dirty_price(&mut self) -> QlResult<Real> {
self.calculate()?;
if self.is_expired()? {
return Ok(0.0);
}
let notional = self.bond.notional(Some(self.checked_settlement_date()?))?;
if notional == 0.0 {
return Ok(0.0);
}
let price = (self.settlement_value()? / notional) * 100.0;
require!(price.is_finite(), "callable dirty price overflow");
Ok(price)
}
pub fn clean_price(&mut self) -> QlResult<Real> {
self.dirty_price()
}
}
impl Instrument for CallableZeroCouponBond {
fn base(&self) -> &InstrumentBase {
let flags = Self::flags(&self.settings);
if self.settings_flags.replace(flags) != flags {
self.bond.base().invalidate_silently();
}
self.bond.base()
}
fn base_mut(&mut self) -> &mut InstrumentBase {
self.bond.base_mut()
}
fn is_expired(&self) -> QlResult<bool> {
require!(
self.settings
.evaluation_date()
.is_some_and(|date| date != Date::null()),
"evaluation date must be set and nonnull"
);
self.bond.is_expired()
}
fn setup_expired(&mut self) {
self.bond.setup_expired();
}
fn fetch_results(&mut self, results: &dyn Results) -> QlResult<()> {
self.bond.fetch_results(results)
}
fn setup_arguments(&self, arguments: &mut dyn Arguments) -> QlResult<()> {
if let Some(args) = (arguments as &mut dyn Any).downcast_mut::<BlackCallableBondArguments>()
{
self.setup_arguments(&mut args.bond)?;
args.cashflows = self.bond().cashflows().clone();
args.payment_day_counter = Some(self.day_counter.clone());
args.frequency = Frequency::Once;
args.callability_count = self.put_call_schedule.len();
return Ok(());
}
let Some(args) = (arguments as &mut dyn Any).downcast_mut::<CallableBondArguments>() else {
fail!("wrong argument type");
};
let settlement = self.checked_settlement_date()?;
let redemption =
self.bond.redemptions().first().ok_or_else(|| {
crate::errors::QlError::new("missing redemption", file!(), line!())
})?;
*args = CallableBondArguments {
settlement_date: Some(settlement),
face_amount: self.face_amount,
redemption: redemption.amount()?,
redemption_date: Some(redemption.date()),
..CallableBondArguments::default()
};
for callability in &self.put_call_schedule {
if event_has_occurred(
callability.date(),
&self.settings,
Some(settlement),
Some(false),
)? {
continue;
}
args.callability_dates.push(callability.date());
args.callability_types.push(callability.call_type());
args.callability_prices.push(callability.price().amount());
}
Ok(())
}
}