use crate::Options;
use crate::error::PricingError;
use crate::model::decimal::{d_div, d_mul, d_sub, finite_decimal};
use crate::pricing::utils::wiener_increment;
use num_traits::{FromPrimitive, ToPrimitive};
use positive::Positive;
use rust_decimal::{Decimal, MathematicalOps};
use std::num::NonZeroUsize;
use tracing::instrument;
#[instrument(skip(option), fields(
steps = steps.get(),
simulations = simulations.get(),
strike = %option.strike_price,
style = ?option.option_style,
side = ?option.side,
))]
pub fn monte_carlo_option_pricing(
option: &Options,
steps: NonZeroUsize, simulations: NonZeroUsize, ) -> Result<Decimal, PricingError> {
let steps_raw = steps.get();
let simulations_raw = simulations.get();
let dt = option.expiration_date.get_years()? / steps_raw as f64;
let mut payoff_sum = 0.0;
for _ in 0..simulations_raw {
let mut st = option.underlying_price.to_dec();
for _ in 0..steps_raw {
let w = wiener_increment(dt.to_dec())?;
st *=
Decimal::ONE + option.risk_free_rate * dt + option.implied_volatility.to_dec() * w;
}
let payoff_dec = d_sub(
st,
option.strike_price.to_dec(),
"pricing::monte_carlo::gbm::payoff",
)?
.max(Decimal::ZERO);
let payoff: f64 = payoff_dec.to_f64().ok_or_else(|| {
PricingError::non_finite("pricing::monte_carlo::gbm::payoff_cast", f64::NAN)
})?;
if !payoff.is_finite() {
return Err(PricingError::non_finite(
"pricing::monte_carlo::gbm::payoff",
payoff,
));
}
payoff_sum += payoff;
}
let rate_f64 = option.risk_free_rate.to_f64().ok_or_else(|| {
PricingError::non_finite("pricing::monte_carlo::rate_f64::cast", f64::NAN)
})?;
if !rate_f64.is_finite() {
return Err(PricingError::non_finite(
"pricing::monte_carlo::rate_f64",
rate_f64,
));
}
let years = option.expiration_date.get_years()?.to_f64();
if !years.is_finite() {
return Err(PricingError::non_finite(
"pricing::monte_carlo::years",
years,
));
}
let discount = (-rate_f64 * years).exp();
if !discount.is_finite() {
return Err(PricingError::non_finite(
"pricing::monte_carlo::discount",
discount,
));
}
let average_payoff = (payoff_sum / simulations_raw as f64) * discount;
if !average_payoff.is_finite() {
return Err(PricingError::non_finite(
"pricing::monte_carlo::average_payoff",
average_payoff,
));
}
finite_decimal(average_payoff).ok_or_else(|| {
PricingError::non_finite("pricing::monte_carlo::average_payoff::cast", average_payoff)
})
}
pub fn price_option_monte_carlo(
option: &Options,
final_prices: &[Positive],
) -> Result<Positive, PricingError> {
let num_simulations = final_prices.len();
if num_simulations == 0 {
return Ok(Positive::ZERO);
}
let effective_rate = option.risk_free_rate - option.dividend_yield;
let discount_factor = (-effective_rate * option.expiration_date.get_years()?).exp();
let total_payoff: Decimal = final_prices
.iter()
.map(|&final_price| {
option
.payoff_at_price(&final_price)
.unwrap_or(Decimal::ZERO)
})
.sum();
let n_dec = Decimal::from_usize(num_simulations).ok_or_else(|| {
PricingError::method_error(
"price_option_monte_carlo",
&format!("num_simulations not representable as Decimal: {num_simulations}"),
)
})?;
let mean_payoff = d_div(total_payoff, n_dec, "pricing::monte_carlo::mean")?;
let avg_payoff = d_mul(discount_factor, mean_payoff, "pricing::monte_carlo::price")?;
Ok(Positive::new_decimal(avg_payoff.abs()).unwrap_or(Positive::ZERO))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::ZERO;
use crate::model::types::{OptionStyle, OptionType, Side};
use crate::{ExpirationDate, assert_decimal_eq, f2du};
use positive::constants::DAYS_IN_A_YEAR;
use positive::{Positive, pos_or_panic};
use rust_decimal::MathematicalOps;
use rust_decimal_macros::dec;
fn create_test_option() -> Options {
Options {
option_type: OptionType::European,
side: Side::Long,
underlying_symbol: "TEST".to_string(),
strike_price: Positive::HUNDRED,
expiration_date: ExpirationDate::Days(DAYS_IN_A_YEAR), implied_volatility: pos_or_panic!(0.2),
quantity: Positive::ONE,
underlying_price: Positive::HUNDRED,
risk_free_rate: dec!(0.05),
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
exotic_params: None,
}
}
#[test]
fn test_monte_carlo_option_pricing_at_the_money() {
let option = create_test_option();
let price = monte_carlo_option_pricing(&option, crate::nz!(252), crate::nz!(1000)).unwrap();
let expected_price = dec!(9.100); assert_decimal_eq!(price, expected_price, dec!(5));
}
#[test]
fn test_monte_carlo_option_pricing_zero_volatility() {
let mut option = create_test_option();
option.implied_volatility = Positive::ZERO;
let price = monte_carlo_option_pricing(&option, crate::nz!(25), crate::nz!(100)).unwrap();
let expected_price = f64::max(
(option.underlying_price - option.strike_price * (-option.risk_free_rate).exp()).into(),
ZERO,
);
assert_decimal_eq!(price, f2du!(expected_price).unwrap(), dec!(0.1));
}
#[test]
fn test_monte_carlo_option_pricing_high_volatility() {
let mut option = create_test_option();
option.implied_volatility = pos_or_panic!(0.5);
let price = monte_carlo_option_pricing(&option, crate::nz!(252), crate::nz!(100)).unwrap();
assert!(price > dec!(10.0));
}
#[test]
fn test_monte_carlo_option_pricing_short_expiration() {
let mut option = create_test_option();
option.expiration_date = ExpirationDate::Days(pos_or_panic!(30.0)); let price = monte_carlo_option_pricing(&option, crate::nz!(30), crate::nz!(100)).unwrap();
assert!(price < dec!(5.0));
}
#[test]
fn test_monte_carlo_option_pricing_consistency() {
let option = create_test_option();
let _price1 =
monte_carlo_option_pricing(&option, crate::nz!(100), crate::nz!(100)).unwrap();
let _price2 =
monte_carlo_option_pricing(&option, crate::nz!(100), crate::nz!(100)).unwrap();
}
}
#[cfg(test)]
mod tests_price_option_monte_carlo {
use super::*;
use crate::chains::generator_positive;
use crate::model::utils::create_sample_option;
use crate::simulation::simulator::Simulator;
use crate::simulation::steps::{Step, Xstep, Ystep};
use crate::simulation::{WalkParams, WalkType, WalkTypeAble};
use crate::utils::TimeFrame;
use crate::utils::time::convert_time_frame;
#[cfg(feature = "static_export")]
use crate::visualization::Graph;
use crate::{ExpirationDate, OptionStyle, Side};
use positive::{Positive, assert_pos_relative_eq, pos_or_panic};
use rust_decimal_macros::dec;
#[test]
fn test_empty_prices_returns_zero() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
);
let empty_prices = &[];
let result = price_option_monte_carlo(&option, empty_prices);
assert!(result.is_ok());
assert_eq!(result.unwrap(), Positive::ZERO);
}
#[test]
fn test_call_option_pricing() {
let mut option = create_sample_option(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
);
option.risk_free_rate = dec!(0.05);
option.dividend_yield = pos_or_panic!(0.02);
option.expiration_date = ExpirationDate::Days(pos_or_panic!(365.0));
let prices = vec![
pos_or_panic!(110.0),
pos_or_panic!(90.0),
pos_or_panic!(105.0),
];
let result = price_option_monte_carlo(&option, &prices);
assert!(result.is_ok());
assert_pos_relative_eq!(
result.unwrap(),
pos_or_panic!(4.85222766),
pos_or_panic!(0.001)
);
}
#[test]
fn test_simulation() {
#[derive(Clone)]
struct TestWalker;
impl WalkTypeAble<Positive, Positive> for TestWalker {}
let walker = Box::new(TestWalker);
let initial_price = pos_or_panic!(1000.0);
let days = pos_or_panic!(365.0);
let volatility = pos_or_panic!(0.2);
let mut option = create_sample_option(
OptionStyle::Call,
Side::Long,
initial_price,
Positive::ONE,
initial_price,
volatility,
);
option.risk_free_rate = dec!(0.05);
option.dividend_yield = pos_or_panic!(0.02);
option.expiration_date = ExpirationDate::Days(days);
let init_step = Step {
x: Xstep::new(Positive::ONE, TimeFrame::Day, ExpirationDate::Days(days)),
y: Ystep::new(0, initial_price),
};
let dt = convert_time_frame(Positive::ONE, &TimeFrame::Day, &TimeFrame::Year);
let walk_params = WalkParams {
size: 365,
init_step,
walk_type: WalkType::Custom {
dt,
drift: dec!(0.02),
volatility,
vov: pos_or_panic!(0.01),
vol_speed: Default::default(),
vol_mean: pos_or_panic!(0.2),
},
walker,
};
let Ok(simulator) = Simulator::new(
"Test Simulator".to_string(),
100,
&walk_params,
generator_positive,
) else {
panic!("simulator setup failed");
};
#[cfg(feature = "static_export")]
simulator
.write_html("Draws/Simulation/simulator_test_montecarlo.html".as_ref())
.unwrap();
let get_last_positive_values = simulator.get_last_positive_values();
let result = price_option_monte_carlo(&option, &get_last_positive_values);
assert!(result.is_ok());
let bs = option.calculate_price_black_scholes().unwrap();
assert_pos_relative_eq!(
result.unwrap(),
Positive::new_decimal(bs).unwrap(),
pos_or_panic!(10.0)
);
}
}