use crate::error::DecimalError;
use crate::model::Position;
use crate::model::decimal::{d_add, d_div, d_mul, d_sub, decimal_to_f64};
use crate::model::types::{OptionStyle, OptionType, Side};
use crate::model::{ExpirationDate, Options};
use chrono::{NaiveDateTime, TimeZone, Utc};
use positive::{Positive, PositiveError};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
#[inline]
fn pos_lit(value: Decimal) -> Positive {
Positive::new_decimal(value).unwrap_or(Positive::ZERO)
}
#[doc(hidden)]
#[must_use]
#[inline]
pub fn sub_floor_zero(lhs: Positive, rhs: &Decimal) -> Positive {
lhs.sub_or_none(rhs).unwrap_or(Positive::ZERO)
}
pub fn positive_f64_to_f64(vec: Vec<Positive>) -> Result<Vec<f64>, DecimalError> {
vec.into_iter()
.map(|value| decimal_to_f64(value.to_dec()))
.collect()
}
#[allow(dead_code)]
#[must_use]
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_lit(dec!(30.0))),
volatility,
quantity,
underlying_price,
dec!(0.05),
option_style,
pos_lit(dec!(0.01)),
None,
)
}
#[allow(dead_code)]
#[must_use]
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_lit(dec!(30.0))),
implied_volatility,
quantity,
contract_size: Positive::ONE,
underlying_price,
risk_free_rate: dec!(0.05),
option_style,
dividend_yield: pos_lit(dec!(0.01)),
exotic_params: None,
},
premium: pos_lit(dec!(5.0)),
date: Utc::now(),
open_fee: pos_lit(dec!(0.5)),
close_fee: pos_lit(dec!(0.5)),
epic: Some("Epic123".to_string()),
extra_fields: None,
}
}
#[must_use]
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_lit(dec!(0.01)),
None,
)
}
#[must_use]
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_lit(dec!(0.01)),
None,
)
}
#[must_use]
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_lit(dec!(30.0))),
pos_lit(dec!(0.2)),
Positive::ONE,
Positive::HUNDRED,
dec!(0.05),
option_style,
pos_lit(dec!(0.01)),
None,
)
}
#[must_use]
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_lit(dec!(30.0))),
pos_lit(dec!(0.2)),
Positive::ONE,
Positive::HUNDRED,
dec!(0.05),
option_style,
pos_lit(dec!(0.01)),
None,
)
}
pub fn mean_and_std(vec: Vec<Positive>) -> Result<(Positive, Positive), PositiveError> {
if vec.is_empty() {
return Err(PositiveError::ArithmeticError {
operation: "mean_and_std".to_string(),
reason: "an empty sample has no mean".to_string(),
});
}
let count = Decimal::from(vec.len());
let mut total = Positive::ZERO;
for value in &vec {
total = total.checked_add(value)?;
}
let mean = total.checked_div_dec(count)?;
let to_f64 = |value: Positive| {
decimal_to_f64(value.to_dec()).map_err(|e| PositiveError::ArithmeticError {
operation: "mean_and_std".to_string(),
reason: e.to_string(),
})
};
let mean_f64 = to_f64(mean)?;
let mut squared_deviations = Positive::ZERO;
for value in &vec {
let deviation = Positive::new((to_f64(*value)? - mean_f64).powi(2))?;
squared_deviations = squared_deviations.checked_add(&deviation)?;
}
let variance = squared_deviations.checked_div_dec(count)?;
let std = Positive::new(to_f64(variance)?.sqrt())?;
Ok((mean, std))
}
pub trait ToRound {
fn round(&self) -> Decimal;
fn round_to(&self, decimal_places: u32) -> Decimal;
}
pub fn generate_price_points(
min_price: Decimal,
max_price: Decimal,
num_points: usize,
) -> Result<Vec<Decimal>, DecimalError> {
let Some(intervals) = num_points.checked_sub(1).filter(|n| *n > 0) else {
return Err(DecimalError::invalid_value(
num_points as f64,
"a price grid needs at least two points",
));
};
let range = d_sub(
max_price,
min_price,
"model::utils::generate_price_points/range",
)?;
let step = d_div(
range,
Decimal::from(intervals),
"model::utils::generate_price_points/step",
)?;
let mut prices = Vec::new();
prices.try_reserve_exact(num_points).map_err(|_| {
DecimalError::invalid_value(
num_points as f64,
"a price grid this large cannot be allocated",
)
})?;
for i in 0..num_points {
let offset = d_mul(
step,
Decimal::from(i),
"model::utils::generate_price_points/offset",
)?;
prices.push(d_add(
min_price,
offset,
"model::utils::generate_price_points/price",
)?);
}
Ok(prices)
}
#[cfg(test)]
mod tests_generate_price_points {
use super::*;
#[test]
fn test_generate_price_points_spans_both_ends() {
let prices = generate_price_points(dec!(90), dec!(110), 5);
assert_eq!(
prices.ok(),
Some(vec![dec!(90), dec!(95), dec!(100), dec!(105), dec!(110)])
);
}
#[test]
fn test_generate_price_points_below_two_points_is_error() {
for num_points in [0, 1] {
assert!(matches!(
generate_price_points(dec!(90), dec!(110), num_points),
Err(DecimalError::InvalidValue { .. })
));
}
}
#[test]
fn test_generate_price_points_unallocatable_is_error() {
assert!(matches!(
generate_price_points(dec!(90), dec!(110), usize::MAX),
Err(DecimalError::InvalidValue { .. })
));
}
#[test]
fn test_generate_price_points_overflow_is_error() {
assert!(generate_price_points(Decimal::MIN, Decimal::MAX, 3).is_err());
}
}
#[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!(matches!(f64_vec, Ok(v) if v == 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!(matches!(f64_vec, Ok(v) if v == 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).expect("the sample is non-empty and finite");
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).expect("the sample is non-empty and finite");
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).expect("the sample is non-empty and finite");
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).expect("the sample is non-empty and finite");
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).expect("the sample is non-empty and finite");
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).expect("the sample is non-empty and finite");
assert_relative_eq!(mean.to_f64(), 138.875, epsilon = 0.001);
assert_relative_eq!(std.to_f64(), 209.392, epsilon = 0.001);
}
#[test]
fn test_empty_vector() {
let values: Vec<Positive> = vec![];
let error = mean_and_std(values).expect_err("an empty sample has no mean");
assert!(matches!(error, PositiveError::ArithmeticError { .. }));
}
#[test]
fn test_sum_overflow_returns_error() {
let values = vec![Positive::MAX, Positive::MAX];
let error = mean_and_std(values).expect_err("two maxima overflow the sum");
assert!(matches!(error, PositiveError::ArithmeticError { .. }));
}
#[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).expect("the sample is non-empty and finite");
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).expect("the sample is non-empty and finite");
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).expect("the sample is non-empty and finite");
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).expect("the sample is non-empty and finite");
assert_relative_eq!(mean.to_f64(), 0.13456786, epsilon = 0.00000001);
assert_relative_eq!(std.to_f64(), 0.00907213, epsilon = 0.00000001);
}
}