use crate::errors::QlResult;
use crate::exercise::ExerciseType;
use crate::math::array::Array;
use crate::math::comparison::close_enough;
use crate::methods::lattices::lattice::Lattice;
use crate::shared::{Shared, SharedMut};
use crate::types::{Real, Size, Time};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CouponAdjustment {
Pre,
Post,
}
pub struct DiscretizedAssetBase {
time: Time,
latest_pre_adjustment: Time,
latest_post_adjustment: Time,
values: Array,
method: Option<Shared<dyn Lattice>>,
}
impl Default for DiscretizedAssetBase {
fn default() -> Self {
DiscretizedAssetBase {
time: 0.0,
latest_pre_adjustment: Real::MAX,
latest_post_adjustment: Real::MAX,
values: Array::new(),
method: None,
}
}
}
pub trait DiscretizedAsset {
fn base(&self) -> &DiscretizedAssetBase;
fn base_mut(&mut self) -> &mut DiscretizedAssetBase;
fn as_asset_mut(&mut self) -> &mut dyn DiscretizedAsset;
fn reset(&mut self, size: Size) -> QlResult<()>;
fn mandatory_times(&self) -> Vec<Time>;
fn pre_adjust_values_impl(&mut self) -> QlResult<()> {
Ok(())
}
fn post_adjust_values_impl(&mut self) -> QlResult<()> {
Ok(())
}
fn time(&self) -> Time {
self.base().time
}
fn set_time(&mut self, time: Time) {
self.base_mut().time = time;
}
fn values(&self) -> &Array {
&self.base().values
}
fn values_mut(&mut self) -> &mut Array {
&mut self.base_mut().values
}
fn method(&self) -> Option<&Shared<dyn Lattice>> {
self.base().method.as_ref()
}
fn initialize(&mut self, method: Shared<dyn Lattice>, t: Time) -> QlResult<()> {
self.base_mut().method = Some(Shared::clone(&method));
method.initialize(self.as_asset_mut(), t)
}
fn rollback(&mut self, to: Time) -> QlResult<()> {
let method = self.require_method()?;
method.rollback(self.as_asset_mut(), to)
}
fn partial_rollback(&mut self, to: Time) -> QlResult<()> {
let method = self.require_method()?;
method.partial_rollback(self.as_asset_mut(), to)
}
fn present_value(&mut self) -> QlResult<Real> {
let method = self.require_method()?;
method.present_value(self.as_asset_mut())
}
fn pre_adjust_values(&mut self) -> QlResult<()> {
if !close_enough(self.time(), self.base().latest_pre_adjustment) {
self.pre_adjust_values_impl()?;
self.base_mut().latest_pre_adjustment = self.base().time;
}
Ok(())
}
fn post_adjust_values(&mut self) -> QlResult<()> {
if !close_enough(self.time(), self.base().latest_post_adjustment) {
self.post_adjust_values_impl()?;
self.base_mut().latest_post_adjustment = self.base().time;
}
Ok(())
}
fn adjust_values(&mut self) -> QlResult<()> {
self.pre_adjust_values()?;
self.post_adjust_values()
}
fn is_on_time(&self, t: Time) -> bool {
let grid = self
.method()
.expect("asset is not initialized on any method")
.time_grid();
let times = grid.times();
let mut best = 0usize;
for i in 1..times.len() {
if (times[i] - t).abs() < (times[best] - t).abs() {
best = i;
}
}
close_enough(times[best], self.time())
}
fn require_method(&self) -> QlResult<Shared<dyn Lattice>> {
match self.method() {
Some(m) => Ok(Shared::clone(m)),
None => crate::fail!("asset is not initialized on any method"),
}
}
}
#[derive(Default)]
pub struct DiscretizedDiscountBond {
base: DiscretizedAssetBase,
}
impl DiscretizedDiscountBond {
pub fn new() -> Self {
Self::default()
}
}
impl DiscretizedAsset for DiscretizedDiscountBond {
fn base(&self) -> &DiscretizedAssetBase {
&self.base
}
fn base_mut(&mut self) -> &mut DiscretizedAssetBase {
&mut self.base
}
fn as_asset_mut(&mut self) -> &mut dyn DiscretizedAsset {
self
}
fn reset(&mut self, size: Size) -> QlResult<()> {
self.base.values = Array::filled(size, 1.0);
Ok(())
}
fn mandatory_times(&self) -> Vec<Time> {
Vec::new()
}
}
pub struct DiscretizedOption {
base: DiscretizedAssetBase,
underlying: SharedMut<dyn DiscretizedAsset>,
exercise_type: ExerciseType,
exercise_times: Vec<Time>,
}
impl DiscretizedOption {
pub fn new(
underlying: SharedMut<dyn DiscretizedAsset>,
exercise_type: ExerciseType,
exercise_times: Vec<Time>,
) -> Self {
DiscretizedOption {
base: DiscretizedAssetBase::default(),
underlying,
exercise_type,
exercise_times,
}
}
pub fn underlying(&self) -> &SharedMut<dyn DiscretizedAsset> {
&self.underlying
}
fn apply_exercise_condition(&mut self) {
let underlying = self.underlying.borrow();
for i in 0..self.base.values.size() {
self.base.values[i] = self.base.values[i].max(underlying.values()[i]);
}
}
}
impl DiscretizedAsset for DiscretizedOption {
fn base(&self) -> &DiscretizedAssetBase {
&self.base
}
fn base_mut(&mut self) -> &mut DiscretizedAssetBase {
&mut self.base
}
fn as_asset_mut(&mut self) -> &mut dyn DiscretizedAsset {
self
}
fn reset(&mut self, size: Size) -> QlResult<()> {
let same = match (self.method(), self.underlying.borrow().method()) {
(Some(a), Some(b)) => Shared::ptr_eq(a, b),
_ => false,
};
crate::require!(
same,
"option and underlying were initialized on different methods"
);
self.base.values = Array::filled(size, 0.0);
self.adjust_values()
}
fn mandatory_times(&self) -> Vec<Time> {
let mut times = self.underlying.borrow().mandatory_times();
if let Some(start) = self.exercise_times.iter().position(|&t| t >= 0.0) {
times.extend_from_slice(&self.exercise_times[start..]);
}
times
}
fn post_adjust_values_impl(&mut self) -> QlResult<()> {
let underlying = SharedMut::clone(&self.underlying);
let t = self.time();
underlying.borrow_mut().partial_rollback(t)?;
underlying.borrow_mut().pre_adjust_values()?;
match self.exercise_type {
ExerciseType::American => {
if self.base.time >= self.exercise_times[0]
&& self.base.time <= self.exercise_times[1]
{
self.apply_exercise_condition();
}
}
ExerciseType::Bermudan | ExerciseType::European => {
for i in 0..self.exercise_times.len() {
let et = self.exercise_times[i];
if et >= 0.0 && self.is_on_time(et) {
self.apply_exercise_condition();
}
}
}
}
underlying.borrow_mut().post_adjust_values()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::timegrid::TimeGrid;
use crate::shared::{shared, shared_mut};
use std::cell::{Cell, RefCell};
struct FlatLattice {
grid: TimeGrid,
discount: Real,
log: RefCell<Vec<Time>>,
}
impl FlatLattice {
fn new(end: Time, steps: Size, discount: Real) -> Self {
FlatLattice {
grid: TimeGrid::new(end, steps).unwrap(),
discount,
log: RefCell::new(Vec::new()),
}
}
fn index_of(&self, t: Time) -> Size {
let times = self.grid.times();
let mut best = 0;
for i in 1..times.len() {
if (times[i] - t).abs() < (times[best] - t).abs() {
best = i;
}
}
best
}
}
impl Lattice for FlatLattice {
fn time_grid(&self) -> &TimeGrid {
&self.grid
}
fn initialize(&self, asset: &mut dyn DiscretizedAsset, time: Time) -> QlResult<()> {
asset.set_time(time);
asset.reset(1)
}
fn partial_rollback(&self, asset: &mut dyn DiscretizedAsset, to: Time) -> QlResult<()> {
let target = self.index_of(to);
let mut i = self.index_of(asset.time());
while i > target {
i -= 1;
asset.set_time(self.grid[i]);
for v in asset.values_mut().iter_mut() {
*v *= self.discount;
}
self.log.borrow_mut().push(self.grid[i]);
if i > target {
asset.adjust_values()?;
}
}
Ok(())
}
fn rollback(&self, asset: &mut dyn DiscretizedAsset, to: Time) -> QlResult<()> {
self.partial_rollback(asset, to)?;
asset.adjust_values()
}
fn present_value(&self, asset: &mut dyn DiscretizedAsset) -> QlResult<Real> {
self.rollback(asset, self.grid[0])?;
Ok(asset.values()[0])
}
fn grid(&self, _time: Time) -> QlResult<Array> {
Ok(Array::filled(1, 0.0))
}
}
#[derive(Default)]
struct CountingAsset {
base: DiscretizedAssetBase,
pre_calls: Cell<u32>,
}
impl DiscretizedAsset for CountingAsset {
fn base(&self) -> &DiscretizedAssetBase {
&self.base
}
fn base_mut(&mut self) -> &mut DiscretizedAssetBase {
&mut self.base
}
fn as_asset_mut(&mut self) -> &mut dyn DiscretizedAsset {
self
}
fn reset(&mut self, size: Size) -> QlResult<()> {
self.base.values = Array::filled(size, 0.0);
Ok(())
}
fn mandatory_times(&self) -> Vec<Time> {
Vec::new()
}
fn pre_adjust_values_impl(&mut self) -> QlResult<()> {
self.pre_calls.set(self.pre_calls.get() + 1);
Ok(())
}
}
#[test]
fn discount_bond_resets_to_ones() {
let mut bond = DiscretizedDiscountBond::new();
bond.reset(3).unwrap();
assert_eq!(bond.values().to_vec(), vec![1.0, 1.0, 1.0]);
assert!(bond.mandatory_times().is_empty());
}
#[test]
fn discount_bond_present_value_is_product_of_step_discounts() {
let lattice: Shared<dyn Lattice> = shared(FlatLattice::new(1.0, 4, 0.9));
let mut bond = DiscretizedDiscountBond::new();
bond.initialize(Shared::clone(&lattice), 1.0).unwrap();
assert_eq!(bond.values().to_vec(), vec![1.0]);
let pv = bond.present_value().unwrap();
assert!((pv - 0.9_f64.powi(4)).abs() < 1e-12, "pv = {pv}");
}
#[test]
fn pre_adjust_guard_fires_impl_once_per_time() {
let mut asset = CountingAsset::default();
asset.pre_adjust_values().unwrap();
asset.pre_adjust_values().unwrap();
assert_eq!(asset.pre_calls.get(), 1, "same time must dedup");
asset.set_time(1.0);
asset.pre_adjust_values().unwrap();
assert_eq!(asset.pre_calls.get(), 2, "new time re-arms");
}
#[test]
fn is_on_time_matches_grid_node() {
let lattice: Shared<dyn Lattice> = shared(FlatLattice::new(1.0, 4, 0.9));
let mut asset = CountingAsset::default();
asset.initialize(Shared::clone(&lattice), 1.0).unwrap();
asset.set_time(0.5);
assert!(asset.is_on_time(0.5));
assert!(!asset.is_on_time(0.75));
}
#[test]
fn apply_exercise_condition_is_elementwise_max() {
let bond = shared_mut(DiscretizedDiscountBond::new());
*bond.borrow_mut().values_mut() = Array::from([1.0, 5.0, 2.0]);
let underlying: SharedMut<dyn DiscretizedAsset> = bond;
let mut option = DiscretizedOption::new(
SharedMut::clone(&underlying),
ExerciseType::Bermudan,
vec![],
);
*option.values_mut() = Array::from([3.0, 4.0, 2.0]);
option.apply_exercise_condition();
assert_eq!(option.values().to_vec(), vec![3.0, 5.0, 2.0]);
}
#[test]
fn option_post_adjust_rolls_underlying_back_before_exercising() {
let lattice: Shared<dyn Lattice> = shared(FlatLattice::new(1.0, 4, 0.9));
let bond = shared_mut(DiscretizedDiscountBond::new());
bond.borrow_mut()
.initialize(Shared::clone(&lattice), 1.0)
.unwrap();
let underlying: SharedMut<dyn DiscretizedAsset> = bond;
let mut option = DiscretizedOption::new(
SharedMut::clone(&underlying),
ExerciseType::Bermudan,
vec![0.5],
);
option.initialize(Shared::clone(&lattice), 1.0).unwrap();
assert_eq!(option.values().to_vec(), vec![0.0]);
option.rollback(0.5).unwrap();
let expected = 0.9_f64.powi(2);
assert!(
(option.values()[0] - expected).abs() < 1e-12,
"option = {}",
option.values()[0]
);
assert!((underlying.borrow().time() - 0.5).abs() < 1e-12);
assert!((underlying.borrow().values()[0] - expected).abs() < 1e-12);
}
}