use crate::error::DecimalError;
use crate::model::decimal::d_ln_f64;
use positive::Positive;
use rust_decimal::Decimal;
const TOLERANCE_F64: f64 = 1e-8;
#[allow(dead_code)]
#[must_use]
pub fn approx_equal(a: f64, b: f64) -> bool {
(a - b).abs() < TOLERANCE_F64
}
pub fn calculate_log_returns(close_prices: &[Positive]) -> Result<Vec<Decimal>, DecimalError> {
if close_prices.len() < 2 {
return Ok(Vec::new());
}
let windows = close_prices.windows(2);
let mut log_returns = Vec::with_capacity(windows.len());
for window in windows {
let [previous_price, current_price] = window else {
continue;
};
let ratio = current_price.checked_div(previous_price).map_err(|_| {
DecimalError::arithmetic_error(
"utils::calculate_log_returns",
"price ratio is zero or not representable",
)
})?;
log_returns.push(d_ln_f64(ratio.to_dec(), "utils::calculate_log_returns")?);
}
Ok(log_returns)
}
#[cfg(test)]
mod tests_approx_equal {
use super::*;
#[test]
fn test_approx_equal_exact_values() {
assert!(approx_equal(1.0, 1.0));
}
#[test]
fn test_approx_equal_within_tolerance() {
let a = 1.00000001;
let b = 1.0;
assert!(approx_equal(a, b));
}
#[test]
fn test_approx_equal_outside_tolerance() {
let a = 1.0001;
let b = 1.0;
assert!(!approx_equal(a, b));
}
#[test]
fn test_approx_equal_negative_values() {
let a = -1.00000001;
let b = -1.0;
assert!(approx_equal(a, b));
}
#[test]
fn test_approx_equal_large_values_within_tolerance() {
let a = 1000000.000000001;
let b = 1000000.0;
assert!(approx_equal(a, b));
}
#[test]
fn test_approx_equal_large_values_outside_tolerance() {
let a = 1000000.1;
let b = 1000000.0;
assert!(!approx_equal(a, b));
}
#[test]
fn test_approx_equal_zero() {
let a = 0.0;
let b = 0.0;
assert!(approx_equal(a, b));
}
#[test]
fn test_approx_equal_zero_with_small_value() {
let a = 0.000000001;
let b = 0.0;
assert!(approx_equal(a, b));
}
#[test]
fn test_approx_equal_zero_outside_tolerance() {
let a = 0.01;
let b = 0.0;
assert!(!approx_equal(a, b));
}
}
#[cfg(test)]
mod tests_log_returns {
use super::*;
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use positive::pos_or_panic;
#[test]
fn test_empty_input() {
let prices: Vec<Positive> = Vec::new();
let result = calculate_log_returns(&prices);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_single_input() {
let prices = vec![Positive::HUNDRED];
let result = calculate_log_returns(&prices);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_basic_calculation() {
let prices = vec![
Positive::HUNDRED,
pos_or_panic!(110.0),
pos_or_panic!(105.0),
];
let result = calculate_log_returns(&prices).unwrap();
assert_eq!(result.len(), 2);
assert_relative_eq!(
result[0].to_f64().unwrap_or(f64::NAN),
0.09531018,
epsilon = 0.00001
);
assert_relative_eq!(
result[1].to_f64().unwrap_or(f64::NAN),
-0.04652,
epsilon = 0.00001
);
}
#[test]
fn test_zero_price() {
let prices = vec![Positive::HUNDRED, Positive::ZERO, pos_or_panic!(105.0)];
assert!(calculate_log_returns(&prices).is_err());
}
#[test]
#[should_panic]
fn test_negative_price() {
let prices = vec![
Positive::HUNDRED,
pos_or_panic!(-50.0),
pos_or_panic!(105.0),
];
let _ = calculate_log_returns(&prices);
}
#[test]
fn test_realistic_stock_prices() {
let prices = vec![
pos_or_panic!(150.25),
pos_or_panic!(151.50),
pos_or_panic!(149.75),
pos_or_panic!(152.25),
pos_or_panic!(153.00),
];
let result = calculate_log_returns(&prices).unwrap();
assert_eq!(result.len(), 4);
assert_relative_eq!(
result[0].to_f64().unwrap_or(f64::NAN),
0.00829,
epsilon = 0.00001
);
assert_relative_eq!(
result[1].to_f64().unwrap_or(f64::NAN),
-0.01161,
epsilon = 0.00001
);
assert_relative_eq!(
result[2].to_f64().unwrap_or(f64::NAN),
0.01656,
epsilon = 0.00001
);
assert_relative_eq!(
result[3].to_f64().unwrap_or(f64::NAN),
0.00491,
epsilon = 0.00001
);
}
#[test]
fn test_large_price_movements() {
let prices = vec![
Positive::HUNDRED, pos_or_panic!(200.0), pos_or_panic!(50.0), pos_or_panic!(300.0), ];
let result = calculate_log_returns(&prices).unwrap();
assert_eq!(result.len(), 3);
assert_relative_eq!(
result[0].to_f64().unwrap_or(f64::NAN),
std::f64::consts::LN_2,
epsilon = 0.0001
);
assert_relative_eq!(
result[1].to_f64().unwrap_or(f64::NAN),
-1.3863,
epsilon = 0.0001
);
assert_relative_eq!(
result[2].to_f64().unwrap_or(f64::NAN),
1.7918,
epsilon = 0.0001
);
}
#[test]
fn test_no_change_prices() {
let prices = vec![Positive::HUNDRED, Positive::HUNDRED, Positive::HUNDRED];
let result = calculate_log_returns(&prices).unwrap();
assert_eq!(result.len(), 2);
assert_relative_eq!(
result[0].to_f64().unwrap_or(f64::NAN),
0.0,
epsilon = 0.00001
);
assert_relative_eq!(
result[1].to_f64().unwrap_or(f64::NAN),
0.0,
epsilon = 0.00001
);
}
#[test]
fn test_very_small_price_changes() {
let prices = vec![
pos_or_panic!(100.000),
pos_or_panic!(100.001),
pos_or_panic!(100.002),
];
let result = calculate_log_returns(&prices).unwrap();
assert_eq!(result.len(), 2);
assert!(result[0].to_f64().unwrap_or(f64::NAN) > 0.0);
assert!(result[0].to_f64().unwrap_or(f64::NAN) < 0.0001);
assert!(result[1].to_f64().unwrap_or(f64::NAN) > 0.0);
assert!(result[1].to_f64().unwrap_or(f64::NAN) < 0.0001);
}
}
#[cfg(test)]
mod tests_log_returns_f64 {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn test_log_returns_within_the_owner_bound() {
let prices: Vec<Positive> = (0..500u32)
.map(|i| {
let k = Decimal::from((i * 7919) % 1009) - dec!(504);
Positive::new_decimal(dec!(100) + k * dec!(0.06)).expect("positive price")
})
.collect();
let fast = calculate_log_returns(&prices).expect("log returns");
let mut worst = Decimal::ZERO;
for (window, value) in prices.windows(2).zip(&fast) {
let [previous, current] = window else {
continue;
};
let exact = crate::model::decimal::d_ln(current.to_dec() / previous.to_dec(), "test")
.expect("series ln");
let error = if exact.is_zero() {
value.abs()
} else {
(*value - exact).abs() / exact.abs()
};
assert!(
error <= dec!(0.000000000001),
"{previous} -> {current}: {error}"
);
worst = worst.max(error);
}
assert!(worst < dec!(0.00000000000001), "worst {worst}");
}
}