use crate::error::ChainError;
use positive::Positive;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::{Decimal, MathematicalOps};
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use utoipa::ToSchema;
#[derive(DebugPretty, DisplaySimple, Clone, ToSchema, Serialize, Deserialize)]
pub struct RNDParameters {
pub risk_free_rate: Decimal,
pub interpolation_points: usize,
pub derivative_tolerance: Positive,
}
impl Default for RNDParameters {
fn default() -> Self {
Self {
risk_free_rate: Decimal::ZERO,
interpolation_points: 100,
derivative_tolerance: Positive::ZERO,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RNDResult {
pub densities: BTreeMap<Positive, Decimal>,
pub statistics: RNDStatistics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RNDStatistics {
pub mean: Decimal,
pub variance: Positive,
pub skewness: Decimal,
pub kurtosis: Decimal,
pub volatility: Positive,
}
impl RNDStatistics {
pub fn new(densities: &BTreeMap<Positive, Decimal>) -> Self {
let mean = Self::calculate_mean(densities);
let variance = Self::calculate_variance(densities, mean);
let skewness = Self::calculate_skewness(densities, mean, variance);
let kurtosis = Self::calculate_kurtosis(densities, mean, variance);
Self {
mean,
variance,
skewness,
kurtosis,
volatility: variance.sqrt(),
}
}
fn calculate_mean(densities: &BTreeMap<Positive, Decimal>) -> Decimal {
let mut mean = Decimal::ZERO;
let mut total_density = Decimal::ZERO;
for (strike, density) in densities {
mean += strike.to_dec() * density;
total_density += density;
}
if !total_density.is_zero() {
mean / total_density
} else {
Decimal::ZERO
}
}
fn calculate_variance(densities: &BTreeMap<Positive, Decimal>, mean: Decimal) -> Positive {
let mut variance = Decimal::ZERO;
let mut total_density = Decimal::ZERO;
for (strike, density) in densities {
let strike_dec = strike.to_dec();
let diff = strike_dec - mean;
variance += diff * diff * density;
total_density += density;
}
if !total_density.is_zero() {
Positive::new_decimal(variance / total_density).unwrap_or_default()
} else {
Positive::ZERO
}
}
fn calculate_skewness(
densities: &BTreeMap<Positive, Decimal>,
mean: Decimal,
variance: Positive,
) -> Decimal {
if variance == Positive::ZERO {
return Decimal::ZERO;
}
let std_dev = variance.sqrt();
let mut skewness = Decimal::ZERO;
let mut total_density = Decimal::ZERO;
for (strike, density) in densities {
let strike_dec = strike.to_dec();
let normalized_diff = (strike_dec - mean) / std_dev;
skewness += normalized_diff * normalized_diff * normalized_diff * density;
total_density += density;
}
if !total_density.is_zero() {
skewness / total_density
} else {
Decimal::ZERO
}
}
fn calculate_kurtosis(
densities: &BTreeMap<Positive, Decimal>,
mean: Decimal,
variance: Positive,
) -> Decimal {
if variance == Positive::ZERO {
return Decimal::ZERO;
}
let variance_dec = variance.to_dec();
let std_dev = variance_dec.sqrt().unwrap();
let std_dev_4 = std_dev.powi(4);
let mut fourth_moment = Decimal::ZERO;
let mut total_density = Decimal::ZERO;
for (strike, density) in densities {
let diff = strike.to_dec() - mean;
let term = diff.powi(4); fourth_moment += term * density;
total_density += density;
}
if !total_density.is_zero() {
let normalized_fourth_moment = fourth_moment / total_density;
(normalized_fourth_moment / std_dev_4) - dec!(3.0)
} else {
Decimal::ZERO
}
}
}
impl RNDResult {
pub fn new(densities: BTreeMap<Positive, Decimal>) -> Self {
let statistics = RNDStatistics::new(&densities);
Self {
densities,
statistics,
}
}
}
pub trait RNDAnalysis {
fn calculate_rnd(&self, params: &RNDParameters) -> Result<RNDResult, ChainError>;
fn calculate_skew(&self) -> Result<Vec<(Positive, Decimal)>, ChainError>;
}
#[cfg(test)]
mod tests {
use super::*;
use positive::{Positive, pos_or_panic, spos};
use crate::chains::chain::OptionChain;
use rust_decimal_macros::dec;
fn create_test_option_chain() -> OptionChain {
let mut chain = OptionChain::new(
"TEST",
Positive::HUNDRED,
"2024-12-31".to_string(),
None,
None,
);
for strike in [80.0, 90.0, 95.0, 100.0, 105.0, 110.0, 120.0].iter() {
chain.add_option(
pos_or_panic!(*strike),
spos!(15.0),
spos!(15.5),
spos!(5.0),
spos!(5.5),
pos_or_panic!(0.2),
Some(dec!(-0.3)),
Some(dec!(-0.3)),
Some(dec!(0.3)),
spos!(100.0),
Some(50),
None,
);
}
chain
}
fn create_empty_chain() -> OptionChain {
OptionChain::new(
"TEST",
Positive::HUNDRED,
"2024-12-31".to_string(),
None,
None,
)
}
mod rnd_parameters_tests {
use super::*;
#[test]
fn test_default_parameters() {
let params = RNDParameters::default();
assert_eq!(params.risk_free_rate, Decimal::ZERO);
assert_eq!(params.interpolation_points, 100);
assert_eq!(params.derivative_tolerance, Positive::ZERO);
}
#[test]
fn test_custom_parameters() {
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 200,
derivative_tolerance: pos_or_panic!(0.001),
};
assert_eq!(params.risk_free_rate, dec!(0.05));
assert_eq!(params.interpolation_points, 200);
assert_eq!(params.derivative_tolerance, pos_or_panic!(0.001));
}
}
mod rnd_statistics_tests {
use super::*;
use crate::assert_decimal_eq;
fn create_test_densities() -> BTreeMap<Positive, Decimal> {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(90.0), dec!(0.2));
densities.insert(Positive::HUNDRED, dec!(0.5));
densities.insert(pos_or_panic!(110.0), dec!(0.3));
densities
}
#[test]
fn test_calculate_mean_normal_case() {
let densities = create_test_densities();
let stats = RNDStatistics::new(&densities);
assert_eq!(stats.mean, dec!(101));
}
#[test]
fn test_calculate_mean_empty_densities() {
let densities = BTreeMap::new();
let stats = RNDStatistics::new(&densities);
assert_eq!(stats.mean, Decimal::ZERO);
}
#[test]
fn test_calculate_variance_normal_case() {
let densities = create_test_densities();
let stats = RNDStatistics::new(&densities);
assert!(stats.variance > Positive::ZERO);
}
#[test]
fn test_calculate_variance_empty_densities() {
let densities = BTreeMap::new();
let stats = RNDStatistics::new(&densities);
assert_eq!(stats.variance, Positive::ZERO);
}
#[test]
fn test_calculate_skewness_normal_case() {
let densities = create_test_densities();
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.skewness.abs(), dec!(0.139941), dec!(0.00001));
}
#[test]
fn test_calculate_skewness_empty_densities() {
let densities = BTreeMap::new();
let stats = RNDStatistics::new(&densities);
assert_eq!(stats.skewness, Decimal::ZERO);
}
#[test]
fn test_calculate_kurtosis_normal_case() {
let densities = create_test_densities();
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.kurtosis.abs(), dec!(0.96043315), dec!(0.00001));
}
#[test]
fn test_calculate_kurtosis_empty_densities() {
let densities = BTreeMap::new();
let stats = RNDStatistics::new(&densities);
assert_eq!(stats.kurtosis, Decimal::ZERO);
}
#[test]
fn test_calculate_volatility_normal_case() {
let densities = create_test_densities();
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.volatility.to_dec(), dec!(7.0), dec!(0.00001));
}
#[test]
fn test_calculate_volatility_empty_densities() {
let densities = BTreeMap::new();
let stats = RNDStatistics::new(&densities);
assert_eq!(stats.volatility.to_dec(), Decimal::ZERO);
}
}
mod rnd_calculation_tests {
use super::*;
#[test]
fn test_calculate_rnd_normal_case() {
let chain = create_test_option_chain();
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: pos_or_panic!(0.001),
};
let result = chain.calculate_rnd(¶ms);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Failed to calculate valid densities")
);
}
#[test]
fn test_calculate_rnd_empty_chain() {
let chain = create_empty_chain();
let params = RNDParameters::default();
let result = chain.calculate_rnd(¶ms);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Derivative tolerance must be greater than zero")
);
}
#[test]
fn test_calculate_rnd_zero_tolerance() {
let chain = create_test_option_chain();
let params = RNDParameters {
derivative_tolerance: Positive::ZERO,
..Default::default()
};
let result = chain.calculate_rnd(¶ms);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Derivative tolerance must be greater than zero")
);
}
#[test]
fn test_calculate_rnd_high_risk_free_rate() {
let chain = create_test_option_chain();
let params = RNDParameters {
risk_free_rate: dec!(0.5), derivative_tolerance: pos_or_panic!(0.001),
..Default::default()
};
let result = chain.calculate_rnd(¶ms);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Failed to calculate valid densities")
);
}
}
mod skew_calculation_tests {
use super::*;
#[test]
fn test_calculate_skew_normal_case() {
let chain = create_test_option_chain();
let result = chain.calculate_skew();
assert!(result.is_ok());
let skew = result.unwrap();
assert!(!skew.is_empty());
for window in skew.windows(2) {
assert!(window[0].0 < window[1].0); }
}
#[test]
fn test_calculate_skew_empty_chain() {
let chain = create_empty_chain();
let result = chain.calculate_skew();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Cannot find ATM OptionData for empty option chain: TEST")
);
}
#[test]
fn test_calculate_skew_missing_implied_volatility() {
let mut chain = create_test_option_chain();
chain.add_option(
pos_or_panic!(115.0),
spos!(5.0),
spos!(5.5),
spos!(15.0),
spos!(15.5),
pos_or_panic!(0.2), Some(dec!(0.3)),
Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(100.0),
Some(50),
None,
);
let result = chain.calculate_skew();
assert!(result.is_ok()); }
}
mod helper_method_tests {
use super::*;
#[test]
fn test_get_call_price() {
let chain = create_test_option_chain();
let price = chain.get_call_price(Positive::HUNDRED);
assert!(price.is_some());
let price = chain.get_call_price(pos_or_panic!(99.0));
assert!(price.is_none());
}
#[test]
fn test_get_atm_implied_volatility() {
let chain = create_test_option_chain();
let vol = chain.get_atm_implied_volatility();
assert!(vol.is_ok());
let empty_chain = create_empty_chain();
let vol = empty_chain.get_atm_implied_volatility();
assert!(vol.is_err());
}
}
mod integration_tests {
use super::*;
#[test]
fn test_full_rnd_workflow() {
let chain = create_test_option_chain();
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: pos_or_panic!(0.001),
};
let rnd_result = chain.calculate_rnd(¶ms);
assert!(rnd_result.is_err());
assert!(
rnd_result
.unwrap_err()
.to_string()
.contains("Failed to calculate valid densities")
);
}
#[test]
fn test_extreme_market_conditions() {
let mut chain = OptionChain::new(
"TEST",
Positive::HUNDRED,
"2024-12-31".to_string(),
None,
None,
);
chain.add_option(
pos_or_panic!(50.0), spos!(50.0),
spos!(51.0),
spos!(0.1),
spos!(0.2),
pos_or_panic!(0.8), Some(dec!(-0.99)),
Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(10.0),
Some(5),
None,
);
chain.add_option(
pos_or_panic!(150.0), spos!(0.1),
spos!(0.2),
spos!(50.0),
spos!(51.0),
pos_or_panic!(0.8), Some(dec!(0.99)),
Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(10.0),
Some(5),
None,
);
let params = RNDParameters {
risk_free_rate: dec!(0.10), interpolation_points: 200,
derivative_tolerance: pos_or_panic!(0.001),
};
let rnd_result = chain.calculate_rnd(¶ms);
assert!(rnd_result.is_err());
assert!(
rnd_result
.unwrap_err()
.to_string()
.contains("Failed to calculate valid densities")
);
}
}
}
#[cfg(test)]
mod additional_tests {
use super::*;
mod rnd_statistics_extended_tests {
use super::*;
use positive::pos_or_panic;
use crate::assert_decimal_eq;
#[test]
fn test_asymmetric_distribution() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(90.0), dec!(0.1));
densities.insert(Positive::HUNDRED, dec!(0.7));
densities.insert(pos_or_panic!(110.0), dec!(0.2));
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.skewness.abs(), dec!(0.076839), dec!(0.00001));
}
#[test]
fn test_extreme_values_distribution() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(50.0), dec!(0.01));
densities.insert(Positive::HUNDRED, dec!(0.97));
densities.insert(pos_or_panic!(150.0), dec!(0.02));
let stats = RNDStatistics::new(&densities);
assert!(stats.variance > Positive::ZERO);
assert!(stats.kurtosis.abs() > dec!(5.0));
}
#[test]
fn test_uniform_distribution() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(90.0), dec!(0.2));
densities.insert(pos_or_panic!(95.0), dec!(0.2));
densities.insert(Positive::HUNDRED, dec!(0.2));
densities.insert(pos_or_panic!(105.0), dec!(0.2));
densities.insert(pos_or_panic!(110.0), dec!(0.2));
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.skewness.abs(), dec!(0.0), dec!(0.00001));
assert_decimal_eq!(stats.kurtosis, dec!(-1.2999999), dec!(0.00001));
}
#[test]
fn test_bimodal_distribution() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(80.0), dec!(0.3));
densities.insert(pos_or_panic!(90.0), dec!(0.1));
densities.insert(Positive::HUNDRED, dec!(0.1));
densities.insert(pos_or_panic!(110.0), dec!(0.1));
densities.insert(pos_or_panic!(120.0), dec!(0.4));
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.kurtosis, dec!(-1.69028), dec!(0.00001));
}
}
mod rnd_calculation_extended_tests {
use super::*;
use positive::{pos_or_panic, spos};
use crate::chains::chain::OptionChain;
fn create_test_option_chain() -> OptionChain {
let mut chain = OptionChain::new(
"TEST",
Positive::HUNDRED,
"2024-12-31".to_string(),
None,
None,
);
for strike in [80.0, 90.0, 95.0, 100.0, 105.0, 110.0, 120.0].iter() {
chain.add_option(
pos_or_panic!(*strike),
spos!(15.0),
spos!(15.5),
spos!(5.0),
spos!(5.5),
pos_or_panic!(0.2),
Some(dec!(-0.3)),
Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(100.0),
Some(50),
None,
);
}
chain
}
fn create_wide_spread_chain() -> OptionChain {
let mut chain = OptionChain::new(
"TEST",
Positive::HUNDRED,
"2024-12-31".to_string(),
None,
None,
);
for strike in [60.0, 80.0, 100.0, 120.0, 140.0].iter() {
chain.add_option(
pos_or_panic!(*strike),
spos!(15.0),
spos!(15.5),
spos!(5.0),
spos!(5.5),
pos_or_panic!(0.2),
Some(dec!(-0.3)),
Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(100.0),
Some(50),
None,
);
}
chain
}
fn create_high_vol_chain() -> OptionChain {
let mut chain = OptionChain::new(
"TEST",
Positive::HUNDRED,
"2024-12-31".to_string(),
None,
None,
);
for strike in [90.0, 95.0, 100.0, 105.0, 110.0].iter() {
chain.add_option(
pos_or_panic!(*strike),
spos!(15.0),
spos!(15.5),
spos!(5.0),
spos!(5.5),
pos_or_panic!(0.5), Some(dec!(-0.3)),
Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(100.0),
Some(50),
None,
);
}
chain
}
#[test]
fn test_calculate_rnd_wide_spread() {
let chain = create_wide_spread_chain();
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: pos_or_panic!(0.001),
};
let result = chain.calculate_rnd(¶ms);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Failed to calculate valid densities")
);
}
#[test]
fn test_calculate_rnd_high_volatility() {
let chain = create_high_vol_chain();
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: pos_or_panic!(0.001),
};
let result = chain.calculate_rnd(¶ms);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Failed to calculate valid densities")
);
}
#[test]
fn test_calculate_rnd_different_tolerances() {
let chain = create_test_option_chain();
let tolerances = [
pos_or_panic!(0.0001),
pos_or_panic!(0.001),
pos_or_panic!(0.01),
pos_or_panic!(0.1),
];
for tolerance in tolerances.iter() {
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: *tolerance,
};
let result = chain.calculate_rnd(¶ms);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Failed to calculate valid densities")
);
}
}
}
mod numerical_stability_tests {
use super::*;
use positive::{pos_or_panic, spos};
use crate::chains::chain::OptionChain;
#[test]
fn test_numerical_stability_small_values() {
let mut chain =
OptionChain::new("TEST", Positive::ONE, "2024-12-31".to_string(), None, None);
chain.add_option(
pos_or_panic!(0.9),
spos!(0.001),
spos!(0.002),
spos!(0.001),
spos!(0.002),
pos_or_panic!(0.1),
Some(dec!(-0.3)),
Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(100.0),
Some(50),
None,
);
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: pos_or_panic!(0.0001),
};
let result = chain.calculate_rnd(¶ms);
assert!(result.is_err());
}
#[test]
fn test_numerical_stability_large_values() {
let mut chain = OptionChain::new(
"TEST",
pos_or_panic!(10000.0),
"2024-12-31".to_string(),
None,
None,
);
chain.add_option(
pos_or_panic!(9900.0),
spos!(1000.0),
spos!(1001.0),
spos!(1000.0),
spos!(1001.0),
pos_or_panic!(0.1),
Some(dec!(-0.3)),
Some(dec!(0.3)),
Some(dec!(0.3)),
spos!(100.0),
Some(50),
None,
);
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: pos_or_panic!(0.0001),
};
let result = chain.calculate_rnd(¶ms);
assert!(result.is_err());
}
}
}
#[cfg(test)]
mod statistical_validation_tests {
use super::*;
use crate::assert_decimal_eq;
use rust_decimal_macros::dec;
mod moments_tests {
use super::*;
use num_traits::{FromPrimitive, ToPrimitive};
use positive::pos_or_panic;
use tracing::info;
#[test]
fn test_simple_mean() {
let mut densities = BTreeMap::new();
densities.insert(Positive::HUNDRED, dec!(0.5));
densities.insert(pos_or_panic!(200.0), dec!(0.5));
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.mean, dec!(150.0), dec!(0.00001));
}
#[test]
fn test_normal_distribution_step_by_step() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(80.0), dec!(0.1));
densities.insert(pos_or_panic!(90.0), dec!(0.2));
densities.insert(Positive::HUNDRED, dec!(0.4));
densities.insert(pos_or_panic!(110.0), dec!(0.2));
densities.insert(pos_or_panic!(120.0), dec!(0.1));
let mut mean = Decimal::ZERO;
let mut total = Decimal::ZERO;
for (x, p) in densities.iter() {
mean += x.to_dec() * *p;
total += *p;
}
mean /= total;
info!("Step-by-step mean: {}", mean);
let mut variance = Decimal::ZERO;
for (x, p) in densities.iter() {
let diff = x.to_dec() - mean;
variance += diff * diff * (*p);
}
variance /= total;
info!("Step-by-step variance: {}", variance);
let std_dev = Decimal::from_f64(variance.to_f64().unwrap().sqrt()).unwrap();
let std_dev_4 = std_dev.powi(4);
let mut kurtosis = Decimal::ZERO;
for (x, p) in densities.iter() {
let diff = x.to_dec() - mean;
kurtosis += (diff.powi(4) * (*p)) / std_dev_4;
}
kurtosis = (kurtosis / total) - dec!(3.0);
info!("Step-by-step kurtosis: {}", kurtosis);
let stats = RNDStatistics::new(&densities);
info!("Structure values:");
info!("Mean: {}", stats.mean);
info!("Variance: {}", stats.variance);
info!("Kurtosis: {}", stats.kurtosis);
assert_decimal_eq!(stats.kurtosis, kurtosis, dec!(0.00001));
}
#[test]
fn test_kurtosis_calculation_comparison() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(80.0), dec!(0.1));
densities.insert(pos_or_panic!(90.0), dec!(0.2));
densities.insert(Positive::HUNDRED, dec!(0.4));
densities.insert(pos_or_panic!(110.0), dec!(0.2));
densities.insert(pos_or_panic!(120.0), dec!(0.1));
info!("Manual Calculation:");
let mut mean = Decimal::ZERO;
let mut total = Decimal::ZERO;
for (x, p) in densities.iter() {
mean += x.to_dec() * *p;
total += *p;
}
mean /= total;
info!("Mean: {}", mean);
let mut variance_dec = Decimal::ZERO;
for (x, p) in densities.iter() {
let diff = x.to_dec() - mean;
variance_dec += diff * diff * (*p);
}
variance_dec /= total;
info!("Variance as Decimal: {}", variance_dec);
let std_dev_manual = Decimal::from_f64(variance_dec.to_f64().unwrap().sqrt()).unwrap();
info!("Std Dev (manual): {}", std_dev_manual);
let std_dev_4_manual = std_dev_manual.powi(4);
info!("Std Dev^4 (manual): {}", std_dev_4_manual);
let mut fourth_moment = Decimal::ZERO;
for (x, p) in densities.iter() {
let diff = x.to_dec() - mean;
let term = diff.powi(4);
fourth_moment += term * (*p);
}
fourth_moment /= total;
info!("Fourth Moment: {}", fourth_moment);
let kurtosis_manual = (fourth_moment / std_dev_4_manual) - dec!(3.0);
info!("Kurtosis (manual): {}", kurtosis_manual);
info!("\nStructure Calculation:");
let stats = RNDStatistics::new(&densities);
info!("Mean: {}", stats.mean);
info!("Variance: {}", stats.variance);
info!("Kurtosis: {}", stats.kurtosis);
assert_decimal_eq!(stats.mean, mean, dec!(0.00001));
assert_decimal_eq!(stats.variance.to_dec(), variance_dec, dec!(0.00001));
assert_decimal_eq!(stats.kurtosis, kurtosis_manual, dec!(0.00001));
}
#[test]
fn test_normal_distribution_detailed() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(80.0), dec!(0.1));
densities.insert(pos_or_panic!(90.0), dec!(0.2));
densities.insert(Positive::HUNDRED, dec!(0.4));
densities.insert(pos_or_panic!(110.0), dec!(0.2));
densities.insert(pos_or_panic!(120.0), dec!(0.1));
let mut mean = Decimal::ZERO;
let mut total = Decimal::ZERO;
for (x, p) in densities.iter() {
mean += x.to_dec() * *p;
total += *p;
}
mean /= total;
info!("Step 1 - Mean: {}", mean);
let mut variance = Decimal::ZERO;
for (x, p) in densities.iter() {
let diff = x.to_dec() - mean;
variance += diff * diff * (*p);
}
variance /= total;
info!("Step 2 - Variance: {}", variance);
let std_dev = Decimal::from_f64(variance.to_f64().unwrap().sqrt()).unwrap();
let std_dev_4 = std_dev * std_dev * std_dev * std_dev;
info!("Step 3a - Std Dev: {}", std_dev);
info!("Step 3b - Std Dev^4: {}", std_dev_4);
let mut fourth_moment = Decimal::ZERO;
for (x, p) in densities.iter() {
let diff = x.to_dec() - mean;
let term = diff * diff * diff * diff;
info!("x: {}, diff^4: {}", x, term);
fourth_moment += term * (*p);
}
fourth_moment /= total;
info!("Step 3c - Fourth Moment: {}", fourth_moment);
let kurtosis = (fourth_moment / std_dev_4) - dec!(3.0);
info!("Step 4 - Final Kurtosis: {}", kurtosis);
let stats = RNDStatistics::new(&densities);
info!("\nStructure values:");
info!("Mean: {}", stats.mean);
info!("Variance: {}", stats.variance);
info!("Kurtosis: {}", stats.kurtosis);
assert_decimal_eq!(mean, stats.mean, dec!(0.00001));
assert_decimal_eq!(variance, stats.variance.to_dec(), dec!(0.00001));
assert_decimal_eq!(kurtosis, stats.kurtosis, dec!(0.00001));
}
#[test]
fn test_simple_variance() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(90.0), dec!(0.5));
densities.insert(pos_or_panic!(110.0), dec!(0.5));
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.mean, dec!(100.0), dec!(0.00001));
assert_decimal_eq!(stats.variance.to_dec(), dec!(100.0), dec!(0.00001));
}
#[test]
fn test_discrete_uniform() {
let mut densities = BTreeMap::new();
for i in 1..=5 {
densities.insert(pos_or_panic!(i as f64), dec!(0.2));
}
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.mean, dec!(3.0), dec!(0.00001));
assert_decimal_eq!(stats.variance.to_dec(), dec!(2.0), dec!(0.00001));
assert_decimal_eq!(stats.skewness, dec!(0.0), dec!(0.00001));
}
#[test]
fn test_normalization() {
let mut densities = BTreeMap::new();
densities.insert(Positive::HUNDRED, dec!(2.0));
densities.insert(pos_or_panic!(200.0), dec!(3.0));
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.mean, dec!(160.0), dec!(0.00001));
}
#[test]
fn test_small_values() {
let mut densities = BTreeMap::new();
densities.insert(Positive::ONE, dec!(0.001));
densities.insert(Positive::TWO, dec!(0.002));
densities.insert(pos_or_panic!(3.0), dec!(0.001));
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.mean, dec!(2.0), dec!(0.00001));
assert!(stats.variance > Positive::ZERO);
}
#[test]
fn test_extreme_values() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(1000000.0), dec!(0.3));
densities.insert(pos_or_panic!(2000000.0), dec!(0.4));
densities.insert(pos_or_panic!(3000000.0), dec!(0.3));
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.mean, dec!(2000000.0), dec!(0.00001));
assert!(stats.variance > Positive::ZERO);
}
#[test]
fn test_gap_distribution() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(10.0), dec!(0.45));
densities.insert(pos_or_panic!(90.0), dec!(0.55));
let stats = RNDStatistics::new(&densities);
assert_decimal_eq!(stats.mean, dec!(54.0), dec!(0.00001));
assert!(stats.kurtosis < dec!(0.0));
}
#[test]
fn test_gap_distribution_detailed() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(10.0), dec!(0.45));
densities.insert(pos_or_panic!(90.0), dec!(0.55));
let mut mean = Decimal::ZERO;
let mut total = Decimal::ZERO;
for (x, p) in densities.iter() {
mean += x.to_dec() * *p;
total += *p;
}
mean /= total;
info!("Step 1 - Mean: {}", mean);
let mut variance = Decimal::ZERO;
for (x, p) in densities.iter() {
let diff = x.to_dec() - mean;
variance += diff * diff * (*p);
}
variance /= total;
info!("Step 2 - Variance: {}", variance);
let std_dev = Decimal::from_f64(variance.to_f64().unwrap().sqrt()).unwrap();
let std_dev_4 = std_dev.powi(4);
info!("Step 3a - Std Dev: {}", std_dev);
info!("Step 3b - Std Dev^4: {}", std_dev_4);
let mut fourth_moment = Decimal::ZERO;
for (x, p) in densities.iter() {
let diff = x.to_dec() - mean;
let term = diff.powi(4);
info!("x: {}, diff: {}, diff^4: {}, p: {}", x, diff, term, p);
fourth_moment += term * (*p);
}
fourth_moment /= total;
info!("Step 3c - Fourth Moment: {}", fourth_moment);
let kurtosis = (fourth_moment / std_dev_4) - dec!(3.0);
info!("Step 4 - Final Kurtosis: {}", kurtosis);
let stats = RNDStatistics::new(&densities);
info!("\nStructure values:");
info!("Mean: {}", stats.mean);
info!("Variance: {}", stats.variance);
info!("Kurtosis: {}", stats.kurtosis);
assert_decimal_eq!(mean, stats.mean, dec!(0.00001));
assert_decimal_eq!(variance, stats.variance.to_dec(), dec!(0.00001));
assert_decimal_eq!(kurtosis, stats.kurtosis, dec!(0.00001));
assert_decimal_eq!(kurtosis, dec!(-1.9595959595), dec!(0.00001));
}
#[test]
fn test_moment_properties() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(95.0), dec!(0.3));
densities.insert(Positive::HUNDRED, dec!(0.4));
densities.insert(pos_or_panic!(105.0), dec!(0.3));
let stats = RNDStatistics::new(&densities);
assert!(stats.variance > Positive::ZERO);
assert_decimal_eq!(stats.skewness.abs(), dec!(0.0), dec!(0.00001));
assert!(stats.kurtosis > dec!(-3.0));
}
}
mod validation_utils {
use super::*;
use positive::pos_or_panic;
fn calculate_raw_moment(densities: &BTreeMap<Positive, Decimal>, order: i32) -> Decimal {
let mut moment = Decimal::ZERO;
let mut total_density = Decimal::ZERO;
for (strike, density) in densities {
moment += strike.to_dec().powi(order as i64) * density;
total_density += density;
}
if !total_density.is_zero() {
moment / total_density
} else {
Decimal::ZERO
}
}
#[test]
fn test_raw_moments() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(90.0), dec!(0.2));
densities.insert(Positive::HUNDRED, dec!(0.6));
densities.insert(pos_or_panic!(110.0), dec!(0.2));
let mean = calculate_raw_moment(&densities, 1);
assert_decimal_eq!(mean, dec!(100.0), dec!(0.00001));
let second_moment = calculate_raw_moment(&densities, 2);
assert!(second_moment > mean.powi(2)); }
}
}
#[cfg(test)]
mod chain_test {
use crate::chains::chain::OptionChain;
use crate::chains::utils::{OptionChainBuildParams, OptionDataPriceParams};
use crate::chains::{RNDAnalysis, RNDParameters};
use crate::{ExpirationDate, assert_decimal_eq};
use positive::{Positive, pos_or_panic, spos};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use tracing::debug;
fn create_test_option_chain() -> OptionChain {
let option_chain_params = OptionChainBuildParams::new(
"SP500".to_string(),
None,
10,
spos!(1.0),
dec!(-0.2),
dec!(0.1),
pos_or_panic!(0.02),
2,
OptionDataPriceParams::new(
Some(Box::new(Positive::HUNDRED)),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(Decimal::ZERO),
spos!(0.05),
Some("SP500".to_string()),
),
pos_or_panic!(0.2),
);
OptionChain::build_chain(&option_chain_params).unwrap()
}
#[test]
fn test_chain_creation() {
let option_chain_params = OptionChainBuildParams::new(
"SP500".to_string(),
None,
10,
spos!(1.0),
dec!(-0.2),
dec!(0.1),
pos_or_panic!(0.02),
2,
OptionDataPriceParams::new(
Some(Box::new(Positive::HUNDRED)),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(Decimal::ZERO),
spos!(0.0),
Some("SP500".to_string()),
),
pos_or_panic!(0.2),
);
let chain = OptionChain::build_chain(&option_chain_params).unwrap();
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: pos_or_panic!(0.01),
};
let rnd_result = chain.calculate_rnd(¶ms).unwrap();
assert!(!rnd_result.densities.is_empty());
assert_decimal_eq!(rnd_result.statistics.mean, dec!(99.96667), dec!(0.001));
assert_decimal_eq!(rnd_result.statistics.skewness, dec!(0.04974), dec!(0.001));
assert_decimal_eq!(rnd_result.statistics.kurtosis, dec!(-0.8346), dec!(0.001));
assert_decimal_eq!(
rnd_result.statistics.variance.to_dec(),
dec!(20.4989),
dec!(0.001)
);
}
#[test]
fn test_rnd_calculation_debug() {
let option_chain_params = OptionChainBuildParams::new(
"SP500".to_string(),
None,
10,
spos!(1.0),
dec!(-0.2),
dec!(0.1),
pos_or_panic!(0.02),
2,
OptionDataPriceParams::new(
Some(Box::new(Positive::HUNDRED)),
Some(ExpirationDate::Days(pos_or_panic!(30.0))),
Some(Decimal::ZERO),
spos!(0.05),
Some("SP500".to_string()),
),
pos_or_panic!(0.2),
);
let chain = OptionChain::build_chain(&option_chain_params).unwrap();
let params = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: Positive::ONE, };
debug!("Initial option chain:");
for opt in &chain.options {
debug!("Strike: {}, Call Ask: {:?}", opt.strike_price, opt.call_ask);
}
let result = chain.calculate_rnd(¶ms);
match result {
Ok(rnd) => {
debug!("\nCalculated densities:");
for (k, d) in rnd.densities {
debug!("Strike: {}, Density: {}", k, d);
}
}
Err(e) => debug!("Error: {}", e),
}
}
#[test]
fn test_rnd_calculation_tolerance_comparison() {
let chain = create_test_option_chain();
let params_1 = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: Positive::ONE,
};
let params_2 = RNDParameters {
risk_free_rate: dec!(0.05),
interpolation_points: 100,
derivative_tolerance: pos_or_panic!(0.1),
};
debug!("Testing with h = 1.0:");
for opt in &chain.options {
let k = opt.strike_price;
debug!(
"Strike {}: Found neighbors: k-h={}, k+h={}",
k,
chain.get_call_price(k - Positive::ONE).is_some(),
chain.get_call_price(k + Positive::ONE).is_some()
);
assert!(
chain.get_call_price(k - Positive::ONE).is_some()
|| chain.get_call_price(k + Positive::ONE).is_some()
);
}
assert!(chain.calculate_rnd(¶ms_1).is_ok());
debug!("\nTesting with h = 0.1:");
for opt in &chain.options {
let k = opt.strike_price;
debug!(
"Strike {}: Found neighbors: k-h={}, k+h={}",
k,
chain.get_call_price(k - pos_or_panic!(0.1)).is_some(),
chain.get_call_price(k + pos_or_panic!(0.1)).is_some()
);
assert!(
chain.get_call_price(k - Positive::ONE).is_some()
|| chain.get_call_price(k + Positive::ONE).is_some()
);
}
assert!(chain.calculate_rnd(¶ms_2).is_ok());
}
}
#[cfg(test)]
mod rnd_coverage_tests {
use super::*;
use crate::chains::OptionChain;
use crate::chains::RNDAnalysis;
use crate::chains::RNDResult;
use positive::{pos_or_panic, spos};
use std::collections::BTreeMap;
#[test]
fn test_rnd_result_new() {
let mut densities = BTreeMap::new();
densities.insert(pos_or_panic!(90.0), dec!(0.2));
densities.insert(Positive::HUNDRED, dec!(0.6));
densities.insert(pos_or_panic!(110.0), dec!(0.2));
let result = RNDResult::new(densities);
assert_eq!(result.statistics.mean, dec!(100.0));
assert!(result.statistics.variance > Positive::ZERO);
assert!(result.statistics.volatility > Positive::ZERO);
}
#[test]
fn test_calculate_skew_with_custom_chain() {
let mut chain = OptionChain::new(
"TEST",
Positive::HUNDRED,
"2024-06-30".to_string(),
Some(dec!(0.05)),
spos!(0.0),
);
let strikes = [80.0, 90.0, 100.0, 110.0, 120.0];
let vols = [0.25, 0.20, 0.17, 0.20, 0.25];
for (i, strike) in strikes.iter().enumerate() {
chain.add_option(
pos_or_panic!(*strike),
spos!(10.0),
spos!(10.5),
spos!(10.0),
spos!(10.5),
pos_or_panic!(vols[i]),
None,
None,
None,
spos!(1000.0),
None,
None,
);
}
let result = chain.calculate_skew();
assert!(result.is_ok());
let skew = result.unwrap();
assert_eq!(skew.len(), 5);
let atm_index = skew.iter().position(|(k, _)| *k == Positive::ONE).unwrap();
let lower = skew[atm_index - 1].1;
let higher = skew[atm_index + 1].1;
assert!((lower.abs() - higher.abs()).abs() < dec!(0.05));
}
}