#![allow(clippy::indexing_slicing)]
use crate::error::PricingError;
use crate::model::decimal::{d_div, d_mul, d_powd, d_sub};
use crate::model::types::{OptionStyle, OptionType, Side};
use crate::pricing::payoff::{Payoff, PayoffInfo};
use crate::pricing::utils::*;
use crate::{d2f, f2d};
use positive::Positive;
use rust_decimal::Decimal;
use std::num::NonZeroUsize;
use tracing::instrument;
#[cfg(test)]
use positive::pos_or_panic;
type BinomialTreeResult = Result<(Vec<Vec<Decimal>>, Vec<Vec<Decimal>>), PricingError>;
#[derive(Debug, Clone)]
pub struct BinomialPricingParams<'a> {
pub asset: Positive,
pub volatility: Positive,
pub int_rate: Decimal,
pub strike: Positive,
pub expiry: Positive,
pub no_steps: NonZeroUsize,
pub option_type: &'a OptionType,
pub option_style: &'a OptionStyle,
pub side: &'a Side,
}
#[instrument(skip(params), fields(
strike = %params.strike,
asset = %params.asset,
steps = params.no_steps.get(),
style = ?*params.option_style,
side = ?*params.side,
))]
pub fn price_binomial(params: BinomialPricingParams) -> Result<Decimal, PricingError> {
let mut info = PayoffInfo {
spot: params.asset,
strike: params.strike,
style: *params.option_style,
side: *params.side,
spot_prices: None,
spot_min: None,
spot_max: None,
};
if params.expiry == Decimal::ZERO {
let intrinsic_value = f2d!(params.option_type.payoff(&info));
return Ok(intrinsic_value);
}
if params.volatility == Decimal::ZERO {
return price_deterministic(¶ms);
}
let no_steps_raw = params.no_steps.get();
let dt = (params.expiry / Positive::new(no_steps_raw as f64)?).to_dec();
let u = calculate_up_factor(params.volatility, dt)?;
let d = calculate_down_factor(params.volatility, dt)?;
if u == d {
return price_deterministic(¶ms);
}
let p = calculate_probability(params.int_rate, dt, d, u)?;
let discount_factor = calculate_discount_factor(params.int_rate, dt)?;
let mut prices: Vec<Decimal> = (0..=no_steps_raw)
.map(|i| calculate_option_price(params.clone(), u, d, i))
.collect::<Result<Vec<_>, _>>()?;
let half_dt = d_div(dt, Decimal::TWO, "pricing::binomial::half_dt")?;
for step in (0..no_steps_raw).rev() {
for i in 0..=step {
let price_up = *prices
.get(i + 1)
.ok_or(PricingError::BinomialNodeMissing { node: "price_up" })?;
let price_down = *prices
.get(i)
.ok_or(PricingError::BinomialNodeMissing { node: "price_down" })?;
let option_value = option_node_value(p, price_up, price_down, discount_factor)?;
let slot = prices
.get_mut(i)
.ok_or(PricingError::BinomialNodeMissing { node: "price_slot" })?;
match params.option_type {
OptionType::American => {
info.spot = lattice_spot(params.asset, u, d, i, step)?;
let intrinsic_value = f2d!(params.option_type.payoff(&info));
*slot = option_value.max(intrinsic_value);
}
OptionType::Bermuda { exercise_dates } => {
let time_at_step = d_mul(
dt,
Decimal::from(step as u64),
"pricing::binomial::time_at_step",
)?;
let mut is_exercise_date = false;
for exercise in exercise_dates {
let gap = d_sub(
time_at_step,
exercise.to_dec(),
"pricing::binomial::exercise_gap",
)?;
if gap.abs() < half_dt {
is_exercise_date = true;
break;
}
}
if is_exercise_date {
let spot = lattice_spot(params.asset, u, d, i, step)?;
let slot_value = option_value;
info.spot = spot;
let intrinsic_value = f2d!(params.option_type.payoff(&info));
let slot = prices
.get_mut(i)
.ok_or(PricingError::BinomialNodeMissing { node: "price_slot" })?;
*slot = slot_value.max(intrinsic_value);
} else {
*slot = option_value;
}
}
OptionType::European => {
*slot = option_value;
}
_ => {
return Err(PricingError::other(
"OptionType not supported for binomial pricing",
));
}
}
}
}
prices
.first()
.copied()
.ok_or(PricingError::BinomialNodeMissing { node: "root" })
}
fn price_deterministic(params: &BinomialPricingParams) -> Result<Decimal, PricingError> {
let exercise_value = |time: Positive| -> Result<Decimal, PricingError> {
calculate_discounted_payoff(BinomialPricingParams {
expiry: time,
side: &Side::Long,
..params.clone()
})
};
let expiry_value = exercise_value(params.expiry)?;
let best = match params.option_type {
OptionType::American => {
let immediate = exercise_value(Positive::ZERO)?;
expiry_value.max(immediate)
}
OptionType::Bermuda { exercise_dates } => {
let mut best = expiry_value;
for date in exercise_dates {
if *date > params.expiry {
continue;
}
best = best.max(exercise_value(*date)?);
}
best
}
_ => expiry_value,
};
Ok(match params.side {
Side::Long => best,
Side::Short => -best,
})
}
fn lattice_spot(
asset: Positive,
u: Decimal,
d: Decimal,
i: usize,
step: usize,
) -> Result<Positive, PricingError> {
let down_steps = step
.checked_sub(i)
.ok_or(PricingError::BinomialNodeMissing { node: "down_steps" })?;
let up_power = d_powd(
u,
Decimal::from(i as u64),
"pricing::binomial::lattice_spot::up",
)?;
let down_power = d_powd(
d,
Decimal::from(down_steps as u64),
"pricing::binomial::lattice_spot::down",
)?;
let spot = d_mul(
d_mul(
asset.to_dec(),
up_power,
"pricing::binomial::lattice_spot::spot_up",
)?,
down_power,
"pricing::binomial::lattice_spot::spot",
)?;
Ok(Positive::new_decimal(spot)?)
}
pub fn generate_binomial_tree(params: &BinomialPricingParams) -> BinomialTreeResult {
let mut info = PayoffInfo {
spot: params.asset,
strike: params.strike,
style: *params.option_style,
side: *params.side,
spot_prices: None,
spot_min: None,
spot_max: None,
};
let no_steps_raw = params.no_steps.get();
let dt = (params.expiry / f2d!(no_steps_raw as f64)).to_dec();
let up_factor = calculate_up_factor(params.volatility, dt)?;
let down_factor = calculate_down_factor(params.volatility, dt)?;
let probability = calculate_probability(params.int_rate, dt, down_factor, up_factor)?;
let discount_factor = calculate_discount_factor(params.int_rate, dt)?;
let mut asset_tree = vec![vec![Decimal::ZERO; no_steps_raw + 1]; no_steps_raw + 1];
let mut option_tree = vec![vec![Decimal::ZERO; no_steps_raw + 1]; no_steps_raw + 1];
for (step, step_vec) in asset_tree.iter_mut().enumerate() {
for (node, node_val) in step_vec.iter_mut().enumerate().take(step + 1) {
let up_steps = step
.checked_sub(node)
.ok_or(PricingError::BinomialNodeMissing { node: "up_steps" })?;
let up_power = d_powd(
up_factor,
Decimal::from(up_steps as u64),
"pricing::binomial::tree::up_power",
)?;
let down_power = d_powd(
down_factor,
Decimal::from(node as u64),
"pricing::binomial::tree::down_power",
)?;
*node_val = d_mul(
d_mul(up_power, down_power, "pricing::binomial::tree::factor")?,
params.asset.to_dec(),
"pricing::binomial::tree::asset_price",
)?;
}
}
let terminal_assets = asset_tree
.get(no_steps_raw)
.ok_or(PricingError::BinomialNodeMissing {
node: "terminal_step",
})?
.clone();
let terminal_options =
option_tree
.get_mut(no_steps_raw)
.ok_or(PricingError::BinomialNodeMissing {
node: "terminal_step",
})?;
for (node, node_val) in terminal_assets.iter().enumerate().take(no_steps_raw + 1) {
info.spot = Positive::new_decimal(*node_val)?;
let slot = terminal_options
.get_mut(node)
.ok_or(PricingError::BinomialNodeMissing {
node: "terminal_node",
})?;
*slot = f2d!(params.option_type.payoff(&info));
}
let half_dt = d_div(dt, Decimal::TWO, "pricing::binomial::tree::half_dt")?;
for step in (0..no_steps_raw).rev() {
let step_assets = asset_tree
.get(step)
.ok_or(PricingError::BinomialNodeMissing { node: "asset_step" })?
.clone();
let (current_step_arr, next_step_arr) = option_tree.split_at_mut(step + 1);
let current = current_step_arr
.get_mut(step)
.ok_or(PricingError::BinomialNodeMissing {
node: "option_step",
})?;
for (node_idx, node_val) in current.iter_mut().enumerate().take(step + 1) {
let node_value =
option_node_value_wrapper(probability, next_step_arr, node_idx, discount_factor)?;
let node_asset = || -> Result<Positive, PricingError> {
let raw = step_assets
.get(node_idx)
.ok_or(PricingError::BinomialNodeMissing { node: "asset_node" })?;
Ok(Positive::new_decimal(*raw)?)
};
match params.option_type {
OptionType::European => {
*node_val = node_value;
}
OptionType::American => {
if (step == 0) & (node_idx == 0) {
*node_val = node_value;
} else {
info.spot = node_asset()?;
let intrinsic_value = params.option_type.payoff(&info);
let dec_node_val = d2f!(node_value);
*node_val = f2d!(intrinsic_value.max(dec_node_val));
}
}
OptionType::Bermuda { exercise_dates } => {
let time_at_step = d_mul(
dt,
Decimal::from(step as u64),
"pricing::binomial::tree::time_at_step",
)?;
let mut is_exercise_date = false;
for exercise in exercise_dates {
let gap = d_sub(
time_at_step,
exercise.to_dec(),
"pricing::binomial::tree::exercise_gap",
)?;
if gap.abs() < half_dt {
is_exercise_date = true;
break;
}
}
if is_exercise_date && !((step == 0) & (node_idx == 0)) {
info.spot = node_asset()?;
let intrinsic_value = params.option_type.payoff(&info);
let dec_node_val = d2f!(node_value);
*node_val = f2d!(intrinsic_value.max(dec_node_val));
} else {
*node_val = node_value;
}
}
_ => {
return Err(PricingError::other(
"OptionType not supported for binomial tree generation",
));
}
}
}
}
Ok((asset_tree, option_tree))
}
#[cfg(test)]
mod tests_price_binomial {
use super::*;
use crate::assert_decimal_eq;
use crate::model::types::OptionType;
use rust_decimal::MathematicalOps;
use rust_decimal_macros::dec;
const EPSILON: Decimal = dec!(1e-6);
#[test]
fn test_european_call_option() {
let params = BinomialPricingParams {
asset: Positive::HUNDRED,
strike: Positive::HUNDRED,
int_rate: dec!(0.05),
volatility: pos_or_panic!(0.2),
expiry: Positive::ONE,
no_steps: crate::nz!(3),
option_type: &OptionType::European,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
assert_decimal_eq!(price, dec!(11.0438708), EPSILON);
}
#[test]
fn test_european_put_option() {
let params = BinomialPricingParams {
asset: Positive::HUNDRED,
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: Positive::HUNDRED,
expiry: Positive::ONE,
no_steps: crate::nz!(1000),
option_type: &OptionType::European,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
assert_decimal_eq!(price, dec!(5.571526), EPSILON);
}
#[test]
fn test_european_put_option_extended() {
let params = BinomialPricingParams {
asset: pos_or_panic!(50.0),
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: pos_or_panic!(52.0),
expiry: Positive::ONE,
no_steps: crate::nz!(1),
option_type: &OptionType::European,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
assert_decimal_eq!(price, dec!(4.446415), EPSILON);
}
#[test]
fn test_short_option() {
let params = BinomialPricingParams {
asset: Positive::HUNDRED,
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: Positive::HUNDRED,
expiry: Positive::ONE,
no_steps: crate::nz!(1000),
option_type: &OptionType::European,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let long_price = price_binomial(params.clone()).unwrap();
let short_price = price_binomial(BinomialPricingParams {
side: &Side::Short,
..params
})
.unwrap();
assert_decimal_eq!(long_price, -short_price, EPSILON);
}
#[test]
fn test_zero_volatility() {
let asset = Positive::HUNDRED;
let strike = Positive::HUNDRED;
let int_rate = dec!(0.05);
let expiry = Positive::ONE;
let params = BinomialPricingParams {
asset,
volatility: Positive::ZERO,
int_rate,
strike,
expiry,
no_steps: crate::nz!(1000),
option_type: &OptionType::European,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
let exact_price = (asset * (int_rate * expiry).exp() - strike).max(Positive::ZERO)
* (-int_rate * expiry).exp();
assert_decimal_eq!(price, exact_price, EPSILON);
}
#[test]
fn test_deep_in_the_money() {
let params = BinomialPricingParams {
asset: pos_or_panic!(150.0),
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: Positive::HUNDRED,
expiry: Positive::ONE,
no_steps: crate::nz!(1000),
option_type: &OptionType::European,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
assert!(price > dec!(50.0));
}
#[test]
fn test_deep_out_of_the_money() {
let params = BinomialPricingParams {
asset: pos_or_panic!(50.0),
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: Positive::HUNDRED,
expiry: Positive::ONE,
no_steps: crate::nz!(1000),
option_type: &OptionType::European,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
assert!(price < Decimal::ONE);
}
#[test]
fn test_zero_time_to_expiry() {
let params = BinomialPricingParams {
asset: Positive::HUNDRED,
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: Positive::HUNDRED,
expiry: Positive::ZERO,
no_steps: crate::nz!(1000),
option_type: &OptionType::European,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
assert_decimal_eq!(price, Decimal::ZERO, EPSILON);
}
}
#[cfg(test)]
mod tests_generate_binomial_tree {
use super::*;
use crate::assert_decimal_eq;
use crate::model::types::OptionType;
use rust_decimal_macros::dec;
const EPSILON: Decimal = dec!(1e-5);
#[test]
fn test_binomial_tree_basic() {
let params = BinomialPricingParams {
asset: Positive::HUNDRED,
strike: Positive::HUNDRED,
int_rate: dec!(0.05),
volatility: pos_or_panic!(0.2),
expiry: Positive::ONE,
no_steps: crate::nz!(3),
option_type: &OptionType::European,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let (asset_tree, option_tree) = generate_binomial_tree(¶ms).unwrap();
assert_eq!(asset_tree[0][0], dec!(100.0));
assert_decimal_eq!(asset_tree[1][0], dec!(112.2400899), EPSILON);
assert_decimal_eq!(asset_tree[3][1], dec!(112.2400899), EPSILON);
assert_decimal_eq!(option_tree[0][0], dec!(11.0438708), EPSILON);
assert_decimal_eq!(option_tree[1][0], dec!(17.713887), EPSILON);
assert_decimal_eq!(option_tree[1][1], dec!(3.500653), EPSILON);
assert_decimal_eq!(option_tree[2][0], dec!(27.631232), EPSILON);
assert_decimal_eq!(option_tree[2][1], dec!(6.5458625), EPSILON);
assert_decimal_eq!(option_tree[2][2], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[3][0], dec!(41.398244), EPSILON);
assert_decimal_eq!(option_tree[3][1], dec!(12.240089), EPSILON);
assert_decimal_eq!(option_tree[3][2], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[3][3], Decimal::ZERO, EPSILON);
}
#[test]
fn test_binomial_tree_put_option() {
let params = BinomialPricingParams {
asset: Positive::HUNDRED,
strike: Positive::HUNDRED,
int_rate: dec!(0.05),
volatility: pos_or_panic!(0.2),
expiry: Positive::ONE,
no_steps: crate::nz!(3),
option_type: &OptionType::European,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let (_, option_tree) = generate_binomial_tree(¶ms).unwrap();
assert_decimal_eq!(option_tree[3][0], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[3][1], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[3][2], dec!(10.905274), EPSILON);
assert_decimal_eq!(option_tree[3][3], dec!(29.277764), EPSILON);
}
#[test]
fn test_binomial_tree_call_option_check() {
let params = BinomialPricingParams {
asset: pos_or_panic!(30.0),
strike: pos_or_panic!(30.0),
expiry: Positive::ONE,
int_rate: dec!(0.05),
volatility: pos_or_panic!(0.17),
no_steps: crate::nz!(1),
option_type: &OptionType::European,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let (asset_tree, option_tree) = generate_binomial_tree(¶ms).unwrap();
assert_eq!(asset_tree.len(), 2);
assert_decimal_eq!(asset_tree[0][0], dec!(30.0), EPSILON);
assert_decimal_eq!(asset_tree[1][0], dec!(35.559145), EPSILON);
assert_decimal_eq!(asset_tree[1][1], dec!(25.309944), EPSILON);
assert_decimal_eq!(option_tree[0][0], dec!(3.213401), EPSILON);
assert_decimal_eq!(option_tree[1][0], dec!(5.559145), EPSILON);
assert_decimal_eq!(option_tree[1][1], Decimal::ZERO, EPSILON);
let params = BinomialPricingParams {
asset: pos_or_panic!(30.0),
strike: pos_or_panic!(30.0),
expiry: Positive::ONE,
int_rate: dec!(0.05),
volatility: pos_or_panic!(0.17),
no_steps: crate::nz!(2),
option_type: &OptionType::European,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let (asset_tree, option_tree) = generate_binomial_tree(¶ms).unwrap();
assert_eq!(asset_tree.len(), 3);
assert_decimal_eq!(asset_tree[0][0], dec!(30.0), EPSILON);
assert_decimal_eq!(asset_tree[1][0], dec!(33.831947), EPSILON);
assert_decimal_eq!(asset_tree[1][1], dec!(26.602075), EPSILON);
assert_decimal_eq!(asset_tree[2][0], dec!(38.153354), EPSILON);
assert_decimal_eq!(asset_tree[2][1], dec!(30.0), EPSILON);
assert_decimal_eq!(asset_tree[2][2], dec!(23.589013), EPSILON);
assert_decimal_eq!(option_tree[0][0], dec!(2.564481), EPSILON);
assert_decimal_eq!(option_tree[1][0], dec!(4.572649), EPSILON);
assert_decimal_eq!(option_tree[1][1], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[2][0], dec!(8.153354), EPSILON);
assert_decimal_eq!(option_tree[2][1], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[2][2], Decimal::ZERO, EPSILON);
}
#[test]
fn test_binomial_tree_put_option_check() {
let params = BinomialPricingParams {
asset: Positive::HUNDRED,
strike: pos_or_panic!(110.0),
expiry: pos_or_panic!(3.0), int_rate: dec!(0.05),
volatility: pos_or_panic!(0.09531018), no_steps: crate::nz!(3),
option_type: &OptionType::European,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let (asset_tree, option_tree) = generate_binomial_tree(¶ms).unwrap();
assert_eq!(asset_tree.len(), 4);
assert_decimal_eq!(asset_tree[0][0], dec!(100.0), EPSILON);
assert_decimal_eq!(asset_tree[1][0], dec!(110.0), EPSILON);
assert_decimal_eq!(asset_tree[1][1], dec!(90.909090), EPSILON);
assert_decimal_eq!(asset_tree[2][0], dec!(121.0), EPSILON);
assert_decimal_eq!(asset_tree[2][1], dec!(100.0), EPSILON);
assert_decimal_eq!(asset_tree[2][2], dec!(82.644628), EPSILON);
assert_decimal_eq!(asset_tree[3][0], dec!(133.1), EPSILON);
assert_decimal_eq!(asset_tree[3][1], dec!(110.0), EPSILON);
assert_decimal_eq!(asset_tree[3][2], dec!(90.909090), EPSILON);
assert_decimal_eq!(asset_tree[3][3], dec!(75.131480), EPSILON);
assert_decimal_eq!(option_tree[0][0], dec!(2.890941), EPSILON);
assert_decimal_eq!(option_tree[1][0], dec!(1.125426), EPSILON);
assert_decimal_eq!(option_tree[1][1], dec!(8.623025), EPSILON);
assert_decimal_eq!(option_tree[2][0], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[2][1], dec!(4.635236), EPSILON);
assert_decimal_eq!(option_tree[2][2], dec!(21.990608), EPSILON);
assert_decimal_eq!(option_tree[3][0], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[3][1], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[3][2], dec!(19.090909), EPSILON);
assert_decimal_eq!(option_tree[3][3], dec!(34.868519), EPSILON);
}
#[test]
fn test_binomial_tree_european_put_option() {
let params = BinomialPricingParams {
asset: pos_or_panic!(50.0),
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: pos_or_panic!(52.0),
expiry: Positive::TWO,
no_steps: crate::nz!(2),
option_type: &OptionType::European,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let (asset_tree, option_tree) = generate_binomial_tree(¶ms).unwrap();
assert_decimal_eq!(asset_tree[0][0], dec!(50.0), EPSILON);
assert_decimal_eq!(asset_tree[1][0], dec!(61.070137), EPSILON);
assert_decimal_eq!(asset_tree[1][1], dec!(40.936537), EPSILON);
assert_decimal_eq!(asset_tree[2][0], dec!(74.591234), EPSILON);
assert_decimal_eq!(asset_tree[2][1], dec!(50.0), EPSILON);
assert_decimal_eq!(asset_tree[2][2], dec!(33.516002), EPSILON);
assert_decimal_eq!(option_tree[0][0], dec!(3.8687179), EPSILON);
assert_decimal_eq!(option_tree[1][0], dec!(0.8038018), EPSILON);
assert_decimal_eq!(option_tree[1][1], dec!(8.5273923), EPSILON);
assert_decimal_eq!(option_tree[2][0], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[2][1], dec!(2.0), EPSILON);
assert_decimal_eq!(option_tree[2][2], dec!(18.483997), EPSILON);
}
#[test]
fn test_binomial_tree_american_put_option() {
let params = BinomialPricingParams {
asset: pos_or_panic!(50.0),
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: pos_or_panic!(52.0),
expiry: Positive::TWO,
no_steps: crate::nz!(2),
option_type: &OptionType::American,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let (asset_tree, option_tree) = generate_binomial_tree(¶ms).unwrap();
assert_decimal_eq!(asset_tree[0][0], dec!(50.0), EPSILON);
assert_decimal_eq!(asset_tree[1][0], dec!(61.070137), EPSILON);
assert_decimal_eq!(asset_tree[1][1], dec!(40.936537), EPSILON);
assert_decimal_eq!(asset_tree[2][0], dec!(74.591234), EPSILON);
assert_decimal_eq!(asset_tree[2][1], dec!(50.0), EPSILON);
assert_decimal_eq!(asset_tree[2][2], dec!(33.516002), EPSILON);
assert_decimal_eq!(option_tree[2][0], Decimal::ZERO, EPSILON);
assert_decimal_eq!(option_tree[2][1], dec!(2.0), EPSILON);
assert_decimal_eq!(option_tree[2][2], dec!(18.483997), EPSILON);
assert_decimal_eq!(option_tree[1][0], dec!(0.803801), EPSILON);
assert_decimal_eq!(option_tree[1][1], params.strike - asset_tree[1][1], EPSILON);
assert_decimal_eq!(option_tree[0][0], dec!(4.887966), EPSILON);
}
}
#[cfg(test)]
mod tests_bermuda_option {
use super::*;
use crate::assert_decimal_eq;
use crate::model::types::OptionType;
use rust_decimal_macros::dec;
const EPSILON: Decimal = dec!(1e-4);
#[test]
fn test_bermuda_price_between_european_and_american() {
let european_params = BinomialPricingParams {
asset: pos_or_panic!(50.0),
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: pos_or_panic!(52.0),
expiry: Positive::ONE,
no_steps: crate::nz!(100),
option_type: &OptionType::European,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let american_params = BinomialPricingParams {
option_type: &OptionType::American,
..european_params.clone()
};
let bermuda_type = OptionType::Bermuda {
exercise_dates: vec![pos_or_panic!(0.25), pos_or_panic!(0.5), pos_or_panic!(0.75)],
};
let bermuda_params = BinomialPricingParams {
option_type: &bermuda_type,
..european_params.clone()
};
let european_price = price_binomial(european_params).unwrap();
let american_price = price_binomial(american_params).unwrap();
let bermuda_price = price_binomial(bermuda_params).unwrap();
assert!(
european_price <= bermuda_price,
"European {} should be <= Bermuda {}",
european_price,
bermuda_price
);
assert!(
bermuda_price <= american_price,
"Bermuda {} should be <= American {}",
bermuda_price,
american_price
);
}
#[test]
fn test_bermuda_single_exercise_date() {
let bermuda_type = OptionType::Bermuda {
exercise_dates: vec![pos_or_panic!(0.5)],
};
let params = BinomialPricingParams {
asset: Positive::HUNDRED,
volatility: pos_or_panic!(0.3),
int_rate: dec!(0.05),
strike: pos_or_panic!(105.0),
expiry: Positive::ONE,
no_steps: crate::nz!(50),
option_type: &bermuda_type,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
assert!(
price > Decimal::ZERO,
"Bermuda put price should be positive"
);
}
#[test]
fn test_bermuda_many_exercise_dates_approaches_american() {
let european_params = BinomialPricingParams {
asset: pos_or_panic!(50.0),
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: pos_or_panic!(52.0),
expiry: Positive::ONE,
no_steps: crate::nz!(52),
option_type: &OptionType::European,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let american_params = BinomialPricingParams {
option_type: &OptionType::American,
..european_params.clone()
};
let exercise_dates: Vec<Positive> = (1..=52)
.map(|i| pos_or_panic!(f64::from(i) / 52.0))
.collect();
let bermuda_type = OptionType::Bermuda { exercise_dates };
let bermuda_params = BinomialPricingParams {
option_type: &bermuda_type,
..european_params.clone()
};
let american_price = price_binomial(american_params).unwrap();
let bermuda_price = price_binomial(bermuda_params).unwrap();
let diff = (american_price - bermuda_price).abs();
assert!(
diff < dec!(0.5),
"Bermuda with 52 exercise dates should be close to American: diff = {}",
diff
);
}
#[test]
fn test_bermuda_no_exercise_dates_equals_european() {
let european_params = BinomialPricingParams {
asset: Positive::HUNDRED,
volatility: pos_or_panic!(0.2),
int_rate: dec!(0.05),
strike: Positive::HUNDRED,
expiry: Positive::ONE,
no_steps: crate::nz!(50),
option_type: &OptionType::European,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let bermuda_type = OptionType::Bermuda {
exercise_dates: vec![],
};
let bermuda_params = BinomialPricingParams {
option_type: &bermuda_type,
..european_params.clone()
};
let european_price = price_binomial(european_params).unwrap();
let bermuda_price = price_binomial(bermuda_params).unwrap();
assert_decimal_eq!(european_price, bermuda_price, EPSILON);
}
#[test]
fn test_bermuda_call_option() {
let bermuda_type = OptionType::Bermuda {
exercise_dates: vec![pos_or_panic!(0.25), pos_or_panic!(0.5), pos_or_panic!(0.75)],
};
let params = BinomialPricingParams {
asset: Positive::HUNDRED,
volatility: pos_or_panic!(0.25),
int_rate: dec!(0.05),
strike: pos_or_panic!(95.0),
expiry: Positive::ONE,
no_steps: crate::nz!(100),
option_type: &bermuda_type,
option_style: &OptionStyle::Call,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
assert!(
price > dec!(5.0),
"ITM Bermuda call should have value > intrinsic"
);
}
}
#[cfg(test)]
mod tests_zero_volatility_early_exercise {
use super::*;
use crate::assert_decimal_eq;
use crate::model::types::OptionType;
use rust_decimal_macros::dec;
const ANALYTIC_EPSILON: Decimal = dec!(1e-12);
fn zero_vol_params<'a>(
asset: Positive,
strike: Positive,
int_rate: Decimal,
option_type: &'a OptionType,
option_style: &'a OptionStyle,
) -> BinomialPricingParams<'a> {
BinomialPricingParams {
asset,
volatility: Positive::ZERO,
int_rate,
strike,
expiry: Positive::ONE,
no_steps: crate::nz!(100),
option_type,
option_style,
side: &Side::Long,
}
}
#[test]
fn test_price_binomial_zero_volatility_american_put_itm_exercises_immediately() {
let params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&OptionType::American,
&OptionStyle::Put,
);
let european = price_binomial(BinomialPricingParams {
option_type: &OptionType::European,
..params.clone()
})
.unwrap();
let american = price_binomial(params).unwrap();
assert_eq!(american, dec!(10));
assert_decimal_eq!(european, dec!(5.122942450071406), ANALYTIC_EPSILON);
assert!(
american > european,
"American {american} must carry an early-exercise premium over European {european}"
);
}
#[test]
fn test_price_binomial_zero_volatility_short_is_the_negative_of_long() {
let long_params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&OptionType::American,
&OptionStyle::Put,
);
let short = price_binomial(BinomialPricingParams {
side: &Side::Short,
..long_params.clone()
})
.unwrap();
let long = price_binomial(long_params).unwrap();
assert_eq!(long, dec!(10));
assert_eq!(short, dec!(-10));
}
#[test]
fn test_price_binomial_zero_volatility_short_symmetry_across_exercise_styles() {
let dates = vec![pos_or_panic!(0.5)];
let bermuda = OptionType::Bermuda {
exercise_dates: dates,
};
for option_type in [&OptionType::American, &bermuda, &OptionType::European] {
for option_style in [&OptionStyle::Put, &OptionStyle::Call] {
let long_params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
option_type,
option_style,
);
let short = price_binomial(BinomialPricingParams {
side: &Side::Short,
..long_params.clone()
})
.unwrap();
let long = price_binomial(long_params).unwrap();
assert_eq!(
long, -short,
"{option_type:?} {option_style:?} broke long == -short: {long} against {short}"
);
assert!(
long >= Decimal::ZERO,
"{option_type:?} {option_style:?} priced a long holding at {long}"
);
}
}
}
#[test]
fn test_price_binomial_zero_volatility_american_call_itm_holds_to_expiry() {
let params = zero_vol_params(
Positive::HUNDRED + pos_or_panic!(10.0),
Positive::HUNDRED,
dec!(0.05),
&OptionType::American,
&OptionStyle::Call,
);
let european = price_binomial(BinomialPricingParams {
option_type: &OptionType::European,
..params.clone()
})
.unwrap();
let american = price_binomial(params).unwrap();
assert_eq!(american, european);
assert_decimal_eq!(american, dec!(14.877057549928594), ANALYTIC_EPSILON);
assert!(american > dec!(10), "holding must beat the intrinsic 10");
}
#[test]
fn test_price_binomial_zero_volatility_american_call_negative_rate_exercises_immediately() {
let params = zero_vol_params(
Positive::HUNDRED + pos_or_panic!(10.0),
Positive::HUNDRED,
dec!(-0.05),
&OptionType::American,
&OptionStyle::Call,
);
let european = price_binomial(BinomialPricingParams {
option_type: &OptionType::European,
..params.clone()
})
.unwrap();
let american = price_binomial(params).unwrap();
assert_eq!(american, dec!(10));
assert_decimal_eq!(european, dec!(4.872890362397602), ANALYTIC_EPSILON);
}
#[test]
fn test_price_binomial_zero_volatility_american_put_negative_rate_holds_to_expiry() {
let params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(-0.02),
&OptionType::American,
&OptionStyle::Put,
);
let european = price_binomial(BinomialPricingParams {
option_type: &OptionType::European,
..params.clone()
})
.unwrap();
let american = price_binomial(params).unwrap();
assert_eq!(american, european);
assert_decimal_eq!(american, dec!(12.020134002675576), ANALYTIC_EPSILON);
assert!(american > dec!(10), "holding must beat the intrinsic 10");
}
#[test]
fn test_price_binomial_zero_volatility_european_put_keeps_the_discounted_forward_payoff() {
let params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&OptionType::European,
&OptionStyle::Put,
);
let price = price_binomial(params).unwrap();
assert_decimal_eq!(price, dec!(5.122942450071406), ANALYTIC_EPSILON);
}
#[test]
fn test_price_binomial_zero_volatility_bermuda_takes_the_best_scheduled_date() {
let bermuda_type = OptionType::Bermuda {
exercise_dates: vec![pos_or_panic!(0.25), pos_or_panic!(0.5), pos_or_panic!(0.75)],
};
let params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&bermuda_type,
&OptionStyle::Put,
);
let european = price_binomial(BinomialPricingParams {
option_type: &OptionType::European,
..params.clone()
})
.unwrap();
let american = price_binomial(BinomialPricingParams {
option_type: &OptionType::American,
..params.clone()
})
.unwrap();
let bermuda = price_binomial(params).unwrap();
assert_decimal_eq!(bermuda, dec!(8.757780049388145), ANALYTIC_EPSILON);
assert!(
european < bermuda,
"European {european} < Bermuda {bermuda}"
);
assert!(
bermuda < american,
"Bermuda {bermuda} < American {american}"
);
}
#[test]
fn test_price_binomial_zero_volatility_bermuda_later_only_schedule_is_worth_less() {
let early_type = OptionType::Bermuda {
exercise_dates: vec![pos_or_panic!(0.25), pos_or_panic!(0.5)],
};
let late_type = OptionType::Bermuda {
exercise_dates: vec![pos_or_panic!(0.5)],
};
let params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&late_type,
&OptionStyle::Put,
);
let early = price_binomial(BinomialPricingParams {
option_type: &early_type,
..params.clone()
})
.unwrap();
let late = price_binomial(params).unwrap();
assert_decimal_eq!(late, dec!(7.530991202833263), ANALYTIC_EPSILON);
assert_decimal_eq!(early, dec!(8.757780049388145), ANALYTIC_EPSILON);
assert!(late < early, "later-only schedule {late} < {early}");
}
#[test]
fn test_price_binomial_zero_volatility_bermuda_empty_schedule_equals_european() {
let bermuda_type = OptionType::Bermuda {
exercise_dates: vec![],
};
let params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&bermuda_type,
&OptionStyle::Put,
);
let european = price_binomial(BinomialPricingParams {
option_type: &OptionType::European,
..params.clone()
})
.unwrap();
let bermuda = price_binomial(params).unwrap();
assert_eq!(bermuda, european);
}
#[test]
fn test_price_binomial_zero_volatility_bermuda_schedule_after_expiry_equals_european() {
let bermuda_type = OptionType::Bermuda {
exercise_dates: vec![Positive::TWO],
};
let params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&bermuda_type,
&OptionStyle::Put,
);
let european = price_binomial(BinomialPricingParams {
option_type: &OptionType::European,
..params.clone()
})
.unwrap();
let bermuda = price_binomial(params).unwrap();
assert_eq!(bermuda, european);
}
#[test]
fn test_price_binomial_zero_volatility_bermuda_exercisable_now_equals_american() {
let bermuda_type = OptionType::Bermuda {
exercise_dates: vec![Positive::ZERO],
};
let params = zero_vol_params(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&bermuda_type,
&OptionStyle::Put,
);
let american = price_binomial(BinomialPricingParams {
option_type: &OptionType::American,
..params.clone()
})
.unwrap();
let bermuda = price_binomial(params).unwrap();
assert_eq!(bermuda, american);
assert_eq!(bermuda, dec!(10));
}
#[test]
fn test_price_binomial_collapsed_lattice_american_put_exercises_immediately() {
let params = BinomialPricingParams {
asset: pos_or_panic!(90.0),
volatility: Positive::new_decimal(dec!(1e-28)).unwrap(),
int_rate: dec!(0.05),
strike: Positive::HUNDRED,
expiry: Positive::ONE,
no_steps: crate::nz!(10),
option_type: &OptionType::American,
option_style: &OptionStyle::Put,
side: &Side::Long,
};
let price = price_binomial(params).unwrap();
assert_eq!(price, dec!(10));
}
}
#[cfg(test)]
mod tests_zero_volatility_continuity {
use super::*;
use crate::assert_decimal_eq;
use crate::model::types::OptionType;
use rust_decimal_macros::dec;
const CONTINUITY_EPSILON: Decimal = dec!(1e-12);
const LIMIT_VOLATILITY: Decimal = dec!(0.005);
fn assert_zero_volatility_is_the_limit(
asset: Positive,
strike: Positive,
int_rate: Decimal,
option_type: &OptionType,
option_style: &OptionStyle,
) {
let base = BinomialPricingParams {
asset,
volatility: Positive::ZERO,
int_rate,
strike,
expiry: Positive::ONE,
no_steps: crate::nz!(200),
option_type,
option_style,
side: &Side::Long,
};
let at_zero = price_binomial(base.clone()).unwrap();
let near_zero = price_binomial(BinomialPricingParams {
volatility: Positive::new_decimal(LIMIT_VOLATILITY).unwrap(),
..base
})
.unwrap();
assert_decimal_eq!(at_zero, near_zero, CONTINUITY_EPSILON);
}
#[test]
fn test_price_binomial_zero_volatility_american_put_matches_the_lattice_limit() {
assert_zero_volatility_is_the_limit(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&OptionType::American,
&OptionStyle::Put,
);
}
#[test]
fn test_price_binomial_zero_volatility_american_call_matches_the_lattice_limit() {
assert_zero_volatility_is_the_limit(
Positive::HUNDRED + pos_or_panic!(10.0),
Positive::HUNDRED,
dec!(0.05),
&OptionType::American,
&OptionStyle::Call,
);
}
#[test]
fn test_price_binomial_zero_volatility_american_call_negative_rate_matches_the_lattice_limit() {
assert_zero_volatility_is_the_limit(
Positive::HUNDRED + pos_or_panic!(10.0),
Positive::HUNDRED,
dec!(-0.05),
&OptionType::American,
&OptionStyle::Call,
);
}
#[test]
fn test_price_binomial_zero_volatility_bermuda_matches_the_lattice_limit() {
let bermuda_type = OptionType::Bermuda {
exercise_dates: vec![pos_or_panic!(0.25), pos_or_panic!(0.5), pos_or_panic!(0.75)],
};
assert_zero_volatility_is_the_limit(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&bermuda_type,
&OptionStyle::Put,
);
}
#[test]
fn test_price_binomial_zero_volatility_european_matches_the_lattice_limit() {
assert_zero_volatility_is_the_limit(
pos_or_panic!(90.0),
Positive::HUNDRED,
dec!(0.05),
&OptionType::European,
&OptionStyle::Put,
);
}
}