use crate::error::ChainError;
use crate::model::Position;
use crate::model::types::{OptionStyle, OptionType, Side};
use crate::{ExpirationDate, Options};
use chrono::{NaiveDateTime, TimeZone, Utc};
use positive::{Positive, pos_or_panic};
use rust_decimal::{Decimal, MathematicalOps};
use rust_decimal_macros::dec;
use std::ops::Mul;
pub fn positive_f64_to_f64(vec: Vec<Positive>) -> Vec<f64> {
vec.into_iter().map(|pos_f64| pos_f64.to_f64()).collect()
}
#[allow(dead_code)]
pub fn create_sample_option(
option_style: OptionStyle,
side: Side,
underlying_price: Positive,
quantity: Positive,
strike_price: Positive,
volatility: Positive,
) -> Options {
Options::new(
OptionType::European,
side,
"AAPL".to_string(),
strike_price,
ExpirationDate::Days(pos_or_panic!(30.0)),
volatility,
quantity,
underlying_price,
dec!(0.05),
option_style,
pos_or_panic!(0.01),
None,
)
}
#[allow(dead_code)]
pub fn create_sample_position(
option_style: OptionStyle,
side: Side,
underlying_price: Positive,
quantity: Positive,
strike_price: Positive,
implied_volatility: Positive,
) -> Position {
Position {
option: Options {
option_type: OptionType::European,
side,
underlying_symbol: "AAPL".to_string(),
strike_price,
expiration_date: ExpirationDate::Days(pos_or_panic!(30.0)),
implied_volatility,
quantity,
underlying_price,
risk_free_rate: dec!(0.05),
option_style,
dividend_yield: pos_or_panic!(0.01),
exotic_params: None,
},
premium: pos_or_panic!(5.0),
date: Utc::now(),
open_fee: pos_or_panic!(0.5),
close_fee: pos_or_panic!(0.5),
epic: Some("Epic123".to_string()),
extra_fields: None,
}
}
pub fn create_sample_option_with_date(
option_style: OptionStyle,
side: Side,
underlying_price: Positive,
quantity: Positive,
strike_price: Positive,
volatility: Positive,
naive_date: NaiveDateTime,
) -> Options {
Options::new(
OptionType::European,
side,
"AAPL".to_string(),
strike_price,
ExpirationDate::DateTime(Utc.from_utc_datetime(&naive_date)),
volatility,
quantity,
underlying_price,
dec!(0.05),
option_style,
pos_or_panic!(0.01),
None,
)
}
pub fn create_sample_option_with_days(
option_style: OptionStyle,
side: Side,
underlying_price: Positive,
quantity: Positive,
strike_price: Positive,
volatility: Positive,
expiration_days: Positive,
) -> Options {
Options::new(
OptionType::European,
side,
"AAPL".to_string(),
strike_price,
ExpirationDate::Days(expiration_days),
volatility,
quantity,
underlying_price,
dec!(0.05),
option_style,
pos_or_panic!(0.01),
None,
)
}
pub fn create_sample_option_simplest(option_style: OptionStyle, side: Side) -> Options {
Options::new(
OptionType::European,
side,
"AAPL".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2),
Positive::ONE,
Positive::HUNDRED,
dec!(0.05),
option_style,
pos_or_panic!(0.01),
None,
)
}
pub fn create_sample_option_simplest_strike(
side: Side,
option_style: OptionStyle,
strike: Positive,
) -> Options {
Options::new(
OptionType::European,
side,
"AAPL".to_string(),
strike,
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2),
Positive::ONE,
Positive::HUNDRED,
dec!(0.05),
option_style,
pos_or_panic!(0.01),
None,
)
}
pub fn mean_and_std(vec: Vec<Positive>) -> (Positive, Positive) {
let mean = vec.iter().sum::<Positive>() / vec.len() as f64;
let variance = vec
.iter()
.map(|x| pos_or_panic!((x.to_f64() - mean.to_f64()).powi(2)))
.sum::<Positive>()
/ vec.len() as f64;
let std = variance.to_f64().sqrt();
(mean, pos_or_panic!(std))
}
pub trait ToRound {
fn round(&self) -> Decimal;
fn round_to(&self, decimal_places: u32) -> Decimal;
}
pub fn calculate_optimal_price_range(
underlying_price: Positive,
strike_price: Positive,
implied_volatility: Positive,
expiration_date: ExpirationDate,
) -> Result<(Positive, Positive), ChainError> {
let days_to_expiry = expiration_date.get_days()?;
let years_to_expiry = Decimal::from(days_to_expiry) / dec!(365.0);
let years_to_expiry_sqrt = years_to_expiry.sqrt().ok_or_else(|| {
ChainError::invalid_price_calculation(
"sqrt() failed to calculate for years_to_expiry value",
)
})?;
let confidence_interval = dec!(4.0);
let volatility_factor = implied_volatility * years_to_expiry_sqrt * confidence_interval;
let lower_bound = underlying_price * (dec!(1.0) - volatility_factor);
let upper_bound = underlying_price * (dec!(1.0) + volatility_factor);
let min_price = lower_bound.min(strike_price.mul(dec!(0.7)));
let max_price = upper_bound.max(strike_price.mul(dec!(1.3)));
let step = (max_price - min_price) / dec!(20.0);
let rounded_step = step.round_to_nice_number();
let min_price_rounded = (min_price / rounded_step).floor() * rounded_step;
let max_price_rounded = (max_price / rounded_step).ceiling() * rounded_step;
Ok((min_price_rounded, max_price_rounded))
}
pub fn generate_price_points(
min_price: Decimal,
max_price: Decimal,
num_points: usize,
) -> Vec<Decimal> {
let step = (max_price - min_price) / Decimal::from(num_points - 1);
let mut prices = Vec::with_capacity(num_points);
for i in 0..num_points {
let price = min_price + step * Decimal::from(i);
prices.push(price);
}
prices
}
#[cfg(test)]
mod tests_positive_f64_to_f64 {
use super::*;
#[test]
fn test_positive_f64_to_f64_non_empty() {
let positive_vec = vec![
Positive::new(10.0).unwrap(),
Positive::new(20.0).unwrap(),
Positive::new(30.0).unwrap(),
];
let f64_vec = positive_f64_to_f64(positive_vec);
assert_eq!(f64_vec, vec![10.0, 20.0, 30.0]);
}
#[test]
fn test_positive_f64_to_f64_single_element() {
let positive_vec = vec![Positive::new(42.0).unwrap()];
let f64_vec = positive_f64_to_f64(positive_vec);
assert_eq!(f64_vec, vec![42.0]);
}
#[test]
#[should_panic]
fn test_positive_f64_to_f64_invalid_positivef64() {
Positive::new(-10.0).unwrap();
}
}
#[cfg(test)]
mod tests_mean_and_std {
use super::*;
use approx::assert_relative_eq;
use positive::pos_or_panic;
#[test]
fn test_basic_mean_and_std() {
let values = vec![
Positive::TWO,
pos_or_panic!(4.0),
pos_or_panic!(4.0),
pos_or_panic!(4.0),
pos_or_panic!(5.0),
pos_or_panic!(5.0),
pos_or_panic!(7.0),
pos_or_panic!(9.0),
];
let (mean, std) = mean_and_std(values);
assert_relative_eq!(mean.to_f64(), 5.0, epsilon = 0.0001);
assert_relative_eq!(std.to_f64(), 2.0, epsilon = 0.0001);
}
#[test]
fn test_identical_values() {
let values = vec![
pos_or_panic!(5.0),
pos_or_panic!(5.0),
pos_or_panic!(5.0),
pos_or_panic!(5.0),
];
let (mean, std) = mean_and_std(values);
assert_relative_eq!(mean.to_f64(), 5.0, epsilon = 0.0001);
assert_relative_eq!(std.to_f64(), 0.0, epsilon = 0.0001);
}
#[test]
fn test_single_value() {
let values = vec![pos_or_panic!(3.0)];
let (mean, std) = mean_and_std(values);
assert_relative_eq!(mean.to_f64(), 3.0, epsilon = 0.0001);
assert_relative_eq!(std.to_f64(), 0.0, epsilon = 0.0001);
}
#[test]
fn test_small_numbers() {
let values = vec![pos_or_panic!(0.1), pos_or_panic!(0.2), pos_or_panic!(0.3)];
let (mean, std) = mean_and_std(values);
assert_relative_eq!(mean.to_f64(), 0.2, epsilon = 0.0001);
assert_relative_eq!(std.to_f64(), 0.08164966, epsilon = 0.0001);
}
#[test]
fn test_large_numbers() {
let values = vec![
pos_or_panic!(1000.0),
pos_or_panic!(2000.0),
pos_or_panic!(3000.0),
];
let (mean, std) = mean_and_std(values);
assert_relative_eq!(mean.to_f64(), 2000.0, epsilon = 0.0001);
assert_relative_eq!(std.to_f64(), 816.4966, epsilon = 0.1);
}
#[test]
fn test_mixed_range() {
let values = vec![
pos_or_panic!(0.5),
pos_or_panic!(5.0),
pos_or_panic!(50.0),
pos_or_panic!(500.0),
];
let (mean, std) = mean_and_std(values);
assert_relative_eq!(mean.to_f64(), 138.875, epsilon = 0.001);
assert_relative_eq!(std.to_f64(), 209.392, epsilon = 0.001);
}
#[test]
#[should_panic]
fn test_empty_vector() {
let values: Vec<Positive> = vec![];
let _ = mean_and_std(values);
}
#[test]
fn test_symmetric_distribution() {
let values = vec![
Positive::ONE,
Positive::TWO,
pos_or_panic!(3.0),
pos_or_panic!(4.0),
pos_or_panic!(5.0),
];
let (mean, std) = mean_and_std(values);
assert_relative_eq!(mean.to_f64(), 3.0, epsilon = 0.0001);
assert_relative_eq!(std.to_f64(), std::f64::consts::SQRT_2, epsilon = 0.0001);
}
#[test]
fn test_result_is_positive() {
let values = vec![Positive::ONE, Positive::TWO, pos_or_panic!(3.0)];
let (mean, std) = mean_and_std(values);
assert!(mean > Positive::ZERO);
assert!(std > Positive::ZERO);
}
#[test]
fn test_precision() {
let values = vec![
pos_or_panic!(1.23456789),
pos_or_panic!(2.34567890),
pos_or_panic!(3.45678901),
];
let (mean, std) = mean_and_std(values);
assert_relative_eq!(mean.to_f64(), 2.34567860, epsilon = 0.00000001);
assert_relative_eq!(std.to_f64(), 0.90721797, epsilon = 0.00000001);
}
#[test]
fn test_precision_bis() {
let values = vec![
pos_or_panic!(0.123456789),
pos_or_panic!(0.134567890),
pos_or_panic!(0.145678901),
];
let (mean, std) = mean_and_std(values);
assert_relative_eq!(mean.to_f64(), 0.13456786, epsilon = 0.00000001);
assert_relative_eq!(std.to_f64(), 0.00907213, epsilon = 0.00000001);
}
}
#[cfg(test)]
mod tests_model_utils {
use super::*;
use positive::pos_or_panic;
#[test]
fn test_calculate_optimal_price_range() {
let underlying_price = pos_or_panic!(100.0);
let strike_price = pos_or_panic!(90.0);
let implied_volatility = pos_or_panic!(0.20);
let expiration_date = ExpirationDate::Days(Positive::TWO);
let (min_price, max_price) = calculate_optimal_price_range(
underlying_price,
strike_price,
implied_volatility,
expiration_date,
)
.unwrap();
assert_eq!(min_price, pos_or_panic!(62.0));
assert_eq!(max_price, pos_or_panic!(118.0));
}
}