use crate::Options;
use crate::error::PricingError;
use crate::error::decimal::DecimalError;
use crate::prelude::simulate_returns;
use num_traits::{FromPrimitive, ToPrimitive};
use rand::random;
use rust_decimal::{Decimal, MathematicalOps};
use rust_decimal_macros::dec;
#[derive(Clone)]
pub struct TelegraphProcess {
lambda_up: Decimal,
lambda_down: Decimal,
current_state: i8,
}
impl TelegraphProcess {
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()
};
if random::<f64>() < probability.to_f64().unwrap() {
self.current_state *= -1;
}
self.current_state
}
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(),
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(),
reason: "Sum of up durations must be non-zero".to_string(),
});
}
let lambda_up = Decimal::ONE / sum_down * Decimal::from_usize(down_durations.len()).unwrap();
let lambda_down = Decimal::ONE / sum_up * Decimal::from_usize(up_durations.len()).unwrap();
Ok((lambda_up, lambda_down))
}
pub fn telegraph(
option: &Options,
no_steps: usize,
lambda_up: Option<Decimal>,
lambda_down: Option<Decimal>,
) -> Result<Decimal, PricingError> {
let mut price = option.underlying_price;
let dt = option.time_to_expiration()?.to_dec() / Decimal::from_f64(no_steps as f64).unwrap();
let one_over_252 = Decimal::from_f64(1.0 / 252.0).unwrap();
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 {
let state = telegraph_process.next_state(dt);
let drift: Decimal = option.risk_free_rate - dec!(0.5) * option.implied_volatility.powi(2);
let volatility: Decimal =
option.implied_volatility.to_dec() * Decimal::from_f64(state as f64).unwrap();
let rh = Decimal::from_f64(dt.sqrt().unwrap().to_f64().unwrap() * random::<f64>()).unwrap();
let lhs = drift * dt + volatility;
let update = (lhs * rh).exp();
price *= update;
}
let payoff = option.payoff_at_price(&price)?;
let result = payoff * (-option.risk_free_rate * option.time_to_expiration()?).exp();
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_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, 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, 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, 100, None, None);
}
#[test]
fn test_telegraph_with_one_estimated_parameter() {
let option = create_mock_option();
let _price_up = telegraph(&option, 100, Some(dec!(0.5)), None);
let _price_down = telegraph(&option, 100, None, Some(dec!(0.5)));
}
#[test]
fn test_telegraph_different_no_steps() {
let option = create_mock_option();
let _price_100 = telegraph(&option, 100, Some(dec!(0.5)), Some(dec!(0.5)));
let _price_1000 = telegraph(&option, 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, 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, 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, 100, Some(dec!(0.5)), Some(dec!(0.5))).unwrap();
assert_eq!(
price,
option.payoff_at_price(&option.underlying_price).unwrap()
);
}
}