#![allow(clippy::indexing_slicing)]
use crate::Options;
use crate::error::PricingError;
use crate::error::decimal::DecimalError;
use crate::model::decimal::{d_mul, finite_decimal};
use crate::prelude::simulate_returns;
use num_traits::{FromPrimitive, ToPrimitive};
use rand::random;
use rust_decimal::{Decimal, MathematicalOps};
use rust_decimal_macros::dec;
use std::num::NonZeroUsize;
use tracing::warn;
#[derive(Debug, Clone)]
pub struct TelegraphProcess {
lambda_up: Decimal,
lambda_down: Decimal,
current_state: i8,
}
impl TelegraphProcess {
#[must_use]
pub fn new(lambda_up: Decimal, lambda_down: Decimal) -> Self {
let initial_state = if random::<f64>() < 0.5 { 1 } else { -1 };
TelegraphProcess {
lambda_up,
lambda_down,
current_state: initial_state,
}
}
pub fn next_state(&mut self, dt: Decimal) -> i8 {
let lambda = if self.current_state == 1 {
self.lambda_down
} else {
self.lambda_up
};
let lambda_dt = -lambda * dt;
let probability = if lambda_dt < dec!(-11.7) {
Decimal::ONE
} else {
Decimal::ONE - lambda_dt.exp()
};
let p_f64 = probability.to_f64().unwrap_or_else(|| {
warn!(
probability = %probability,
"telegraph::next_state: probability.to_f64() returned None; treating as 0.0"
);
0.0
});
if random::<f64>() < p_f64 {
self.current_state *= -1;
}
self.current_state
}
#[must_use]
pub fn get_current_state(&self) -> i8 {
self.current_state
}
}
pub(crate) fn estimate_telegraph_parameters(
returns: &[Decimal],
threshold: Decimal,
) -> Result<(Decimal, Decimal), DecimalError> {
let mut current_state = if returns[0] > threshold {
Decimal::ONE
} else {
Decimal::NEGATIVE_ONE
};
let mut current_duration = Decimal::ONE;
let mut up_durations = Vec::new();
let mut down_durations = Vec::new();
for &ret in returns.iter().skip(1) {
let new_state = if ret > threshold {
Decimal::ONE
} else {
Decimal::NEGATIVE_ONE
};
if new_state == current_state {
current_duration += Decimal::ONE;
} else {
if current_state == Decimal::ONE {
up_durations.push(current_duration);
} else {
down_durations.push(current_duration);
}
current_state = new_state;
current_duration = Decimal::ONE;
}
}
if current_state == Decimal::ONE {
up_durations.push(current_duration);
} else {
down_durations.push(current_duration);
}
if down_durations.is_empty() {
return Err(DecimalError::InvalidValue {
value: 0.0,
reason: "No transitions from state +1 to -1 found. All returns are above threshold."
.to_string(),
});
}
if up_durations.is_empty() {
return Err(DecimalError::InvalidValue {
value: 0.0,
reason: "No transitions from state -1 to +1 found. All returns are below threshold."
.to_string(),
});
}
let sum_down = down_durations.iter().sum::<Decimal>();
let sum_up = up_durations.iter().sum::<Decimal>();
if sum_down == Decimal::ZERO {
return Err(DecimalError::InvalidValue {
value: sum_down.to_f64().unwrap_or(0.0),
reason: "Sum of down durations must be non-zero".to_string(),
});
}
if sum_up == Decimal::ZERO {
return Err(DecimalError::InvalidValue {
value: sum_up.to_f64().unwrap_or(0.0),
reason: "Sum of up durations must be non-zero".to_string(),
});
}
let down_len = Decimal::from_usize(down_durations.len()).ok_or_else(|| {
DecimalError::invalid_value(
down_durations.len() as f64,
"down_durations length not representable as Decimal",
)
})?;
let up_len = Decimal::from_usize(up_durations.len()).ok_or_else(|| {
DecimalError::invalid_value(
up_durations.len() as f64,
"up_durations length not representable as Decimal",
)
})?;
let lambda_up = Decimal::ONE / sum_down * down_len;
let lambda_down = Decimal::ONE / sum_up * up_len;
Ok((lambda_up, lambda_down))
}
pub fn telegraph(
option: &Options,
no_steps: NonZeroUsize,
lambda_up: Option<Decimal>,
lambda_down: Option<Decimal>,
) -> Result<Decimal, PricingError> {
let no_steps_raw = no_steps.get();
let mut price = option.underlying_price;
let no_steps_dec = Decimal::from_usize(no_steps_raw).ok_or_else(|| {
PricingError::method_error("telegraph", &format!("invalid no_steps: {no_steps_raw}"))
})?;
let dt = option.time_to_expiration()?.to_dec() / no_steps_dec;
let one_over_252 = finite_decimal(1.0 / 252.0)
.ok_or_else(|| PricingError::non_finite("pricing::telegraph::one_over_252", 1.0 / 252.0))?;
let (lambda_up_temp, lambda_down_temp) = match (lambda_up, lambda_down) {
(None, None) => {
let returns =
simulate_returns(Decimal::ZERO, option.implied_volatility, 100, one_over_252)?;
estimate_telegraph_parameters(&returns, Decimal::ZERO)?
}
(Some(l_up), None) => {
let returns =
simulate_returns(Decimal::ZERO, option.implied_volatility, 100, one_over_252)?;
let (_, l_down) = estimate_telegraph_parameters(&returns, Decimal::ZERO)?;
(l_up, l_down)
}
(None, Some(l_down)) => {
let returns =
simulate_returns(Decimal::ZERO, option.implied_volatility, 100, one_over_252)?;
let (l_up, _) = estimate_telegraph_parameters(&returns, Decimal::ZERO)?;
(l_up, l_down)
}
(Some(l_up), Some(l_down)) => (l_up, l_down),
};
let telegraph_process = TelegraphProcess::new(lambda_up_temp, lambda_down_temp);
let tp = telegraph_process;
let mut telegraph_process = tp.clone();
for _ in 0..no_steps_raw {
let state = telegraph_process.next_state(dt);
let drift: Decimal = option.risk_free_rate - dec!(0.5) * option.implied_volatility.powi(2);
let state_f64 = state as f64;
let state_dec = finite_decimal(state_f64)
.ok_or_else(|| PricingError::non_finite("pricing::telegraph::state_dec", state_f64))?;
let volatility: Decimal = option.implied_volatility.to_dec() * state_dec;
let sqrt_dt = dt
.sqrt()
.ok_or_else(|| PricingError::method_error("telegraph", "non-finite dt sqrt"))?;
let sqrt_dt_f64 = sqrt_dt.to_f64().ok_or_else(|| {
PricingError::method_error("telegraph", "sqrt(dt) not representable as f64")
})?;
let rh_f64 = sqrt_dt_f64 * random::<f64>();
let rh = finite_decimal(rh_f64)
.ok_or_else(|| PricingError::non_finite("pricing::telegraph::rh", rh_f64))?;
let lhs = drift * dt + volatility;
let update = (lhs * rh).exp();
price *= update;
}
let payoff = option.payoff_at_price(&price)?;
let discount_exponent = d_mul(
-option.risk_free_rate,
option.time_to_expiration()?.to_dec(),
"pricing::telegraph::discount_exponent",
)?;
let discount = discount_exponent.exp();
let result = d_mul(payoff, discount, "pricing::telegraph::price")?;
Ok(result)
}
#[cfg(test)]
mod tests_telegraph_process_basis {
use super::*;
use positive::{Positive, pos_or_panic};
use crate::model::types::{OptionStyle, OptionType, Side};
use rust_decimal_macros::dec;
#[test]
fn test_telegraph_process_new() {
let tp = TelegraphProcess::new(dec!(0.5), dec!(0.3));
assert_eq!(tp.lambda_up, dec!(0.5));
assert_eq!(tp.lambda_down, dec!(0.3));
assert!(tp.current_state == 1 || tp.current_state == -1);
}
#[test]
fn test_telegraph_process_next_state() {
let mut tp = TelegraphProcess::new(Decimal::ONE, Decimal::ONE);
let _initial_state = tp.get_current_state();
let new_state = tp.next_state(dec!(0.1));
assert!(new_state == 1 || new_state == -1);
}
#[test]
fn test_next_state_empirical_flip_rate_matches_poisson() {
let lambda_f = 0.5_f64;
let dt_f = 0.01_f64;
let mut tp = TelegraphProcess::new(dec!(0.5), dec!(0.5));
let n = 100_000_u64;
let mut prev = tp.get_current_state();
let mut flips: u64 = 0;
for _ in 0..n {
let next = tp.next_state(dec!(0.01));
if next != prev {
flips += 1;
}
prev = next;
}
let empirical = flips as f64 / n as f64;
let expected = 1.0 - (-lambda_f * dt_f).exp();
let std_err = (expected * (1.0 - expected) / n as f64).sqrt();
assert!(
(empirical - expected).abs() < 5.0 * std_err,
"empirical flip rate {empirical} differs from expected {expected} by more than 5σ ({})",
5.0 * std_err
);
assert!(empirical < 0.05, "flip rate suspiciously high: {empirical}");
}
#[test]
fn test_telegraph_process_get_current_state() {
let tp = TelegraphProcess::new(dec!(0.5), dec!(0.5));
let state = tp.get_current_state();
assert!(state == 1 || state == -1);
}
#[test]
fn test_estimate_telegraph_parameters() {
let returns = vec![
dec!(-0.01),
dec!(0.02),
dec!(0.01),
dec!(-0.02),
dec!(0.03),
dec!(-0.01),
dec!(0.01),
dec!(-0.03),
];
let threshold = dec!(0.01);
let (lambda_up, lambda_down) = estimate_telegraph_parameters(&returns, threshold).unwrap();
assert!(lambda_up > Decimal::ZERO);
assert!(lambda_down > Decimal::ZERO);
}
#[test]
fn test_telegraph() {
let option = Options {
option_type: OptionType::European,
side: Side::Long,
underlying_price: Positive::HUNDRED,
strike_price: Positive::ONE,
risk_free_rate: dec!(0.05),
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
implied_volatility: pos_or_panic!(0.2),
underlying_symbol: "".to_string(),
expiration_date: Default::default(),
quantity: Positive::ONE,
exotic_params: None,
};
let _price = telegraph(&option, crate::nz!(1000), Some(dec!(0.7)), Some(dec!(0.5)));
}
}
#[cfg(test)]
mod tests_telegraph_process_extended {
use super::*;
use positive::{Positive, pos_or_panic};
use crate::model::types::{OptionStyle, OptionType, Side};
use rust_decimal_macros::dec;
fn create_mock_option() -> Options {
Options {
option_type: OptionType::European,
side: Side::Long,
underlying_price: Positive::HUNDRED,
strike_price: Positive::HUNDRED,
risk_free_rate: dec!(0.05),
option_style: OptionStyle::Call,
dividend_yield: Positive::ZERO,
implied_volatility: pos_or_panic!(0.2),
underlying_symbol: "".to_string(),
expiration_date: Default::default(),
quantity: Positive::ZERO,
exotic_params: None,
}
}
#[test]
fn test_telegraph_process_new() {
let tp = TelegraphProcess::new(dec!(0.5), dec!(0.3));
assert_eq!(tp.lambda_up, dec!(0.5));
assert_eq!(tp.lambda_down, dec!(0.3));
assert!(tp.get_current_state() == 1 || tp.get_current_state() == -1);
}
#[test]
fn test_telegraph_process_next_state() {
let mut tp = TelegraphProcess::new(dec!(1000.0), dec!(1000.0)); let initial_state = tp.get_current_state();
let new_state = tp.next_state(dec!(0.1));
assert_ne!(initial_state, new_state);
}
#[test]
fn test_telegraph_process_get_current_state() {
let tp = TelegraphProcess::new(dec!(0.5), dec!(0.5));
let state = tp.get_current_state();
assert!(state == 1 || state == -1);
}
#[test]
fn test_estimate_telegraph_parameters() {
let returns = vec![
dec!(-0.01),
dec!(0.02),
dec!(0.01),
dec!(-0.02),
dec!(0.03),
dec!(-0.01),
dec!(0.01),
dec!(-0.03),
];
let threshold = Decimal::ZERO;
let result = estimate_telegraph_parameters(&returns, threshold);
assert!(result.is_ok());
let (lambda_up, lambda_down) = result.unwrap();
assert!(lambda_up > Decimal::ZERO);
assert!(lambda_down > Decimal::ZERO);
}
#[test]
fn test_estimate_telegraph_parameters_all_positive() {
let returns = vec![
dec!(0.01),
dec!(0.02),
dec!(0.01),
dec!(0.02),
dec!(0.03),
dec!(0.01),
dec!(0.01),
dec!(0.03),
];
let threshold = Decimal::ZERO;
assert!(estimate_telegraph_parameters(&returns, threshold).is_err());
}
#[test]
fn test_estimate_telegraph_parameters_all_negative() {
let returns = vec![
dec!(-0.01),
dec!(-0.02),
dec!(-0.01),
dec!(-0.02),
dec!(-0.03),
dec!(-0.01),
dec!(-0.01),
dec!(-0.03),
];
let threshold = dec!(0.01);
assert!(estimate_telegraph_parameters(&returns, threshold).is_err());
}
#[test]
fn test_telegraph_with_provided_parameters() {
let option = create_mock_option();
let _price = telegraph(&option, crate::nz!(100), Some(dec!(0.5)), Some(dec!(0.5)));
}
#[test]
fn test_telegraph_with_estimated_parameters() {
let option = create_mock_option();
let _price = telegraph(&option, crate::nz!(100), None, None);
}
#[test]
fn test_telegraph_with_one_estimated_parameter() {
let option = create_mock_option();
let _price_up = telegraph(&option, crate::nz!(100), Some(dec!(0.5)), None);
let _price_down = telegraph(&option, crate::nz!(100), None, Some(dec!(0.5)));
}
#[test]
fn test_telegraph_different_no_steps() {
let option = create_mock_option();
let _price_100 = telegraph(&option, crate::nz!(100), Some(dec!(0.5)), Some(dec!(0.5)));
let _price_1000 = telegraph(&option, crate::nz!(1000), Some(dec!(0.5)), Some(dec!(0.5)));
}
#[test]
fn test_telegraph_zero_volatility() {
let mut option = create_mock_option();
option.implied_volatility = Positive::ZERO;
let _price = telegraph(&option, crate::nz!(100), Some(dec!(0.5)), Some(dec!(0.5)));
}
#[test]
fn test_telegraph_zero_risk_free_rate() {
let mut option = create_mock_option();
option.risk_free_rate = Decimal::ZERO;
let _price = telegraph(&option, crate::nz!(100), Some(dec!(0.5)), Some(dec!(0.5)));
}
#[test]
fn test_telegraph_zero_time_to_expiration() {
let option = create_mock_option();
let price = telegraph(&option, crate::nz!(100), Some(dec!(0.5)), Some(dec!(0.5))).unwrap();
assert_eq!(
price,
option.payoff_at_price(&option.underlying_price).unwrap()
);
}
}