use crate::normalization_lut::{cv_sigmoid_lut, sigmoid_lut, tanh_lut};
#[inline]
pub fn logistic_sigmoid(x: f64, center: f64, scale: f64) -> f64 {
1.0 / (1.0 + (-(x - center) * scale).exp())
}
#[inline]
pub fn normalize_epochs(epochs: usize, lookback: usize) -> f64 {
if lookback == 0 {
return 0.5; }
let density = epochs as f64 / lookback as f64;
sigmoid_lut(density)
}
#[inline]
pub fn normalize_excess(value: f64) -> f64 {
tanh_lut(value.abs() * 5.0)
}
#[inline]
pub fn normalize_cv(cv: f64) -> f64 {
let cv_effective = if cv.is_nan() { 0.0 } else { cv };
cv_sigmoid_lut(cv_effective)
}
#[inline]
pub fn normalize_drawdown(drawdown: f64) -> f64 {
drawdown.clamp(0.0, 1.0)
}
#[inline]
pub fn normalize_runup(runup: f64) -> f64 {
runup.clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_epochs_bounded() {
for epochs in 0..=100 {
for lookback in 1..=200 {
let result = normalize_epochs(epochs, lookback);
assert!(
result >= 0.0 && result <= 1.0,
"normalize_epochs({}, {}) = {} not in [0, 1]",
epochs,
lookback,
result
);
}
}
}
#[test]
fn test_normalize_epochs_monotonic() {
let lookback = 50;
let mut prev = normalize_epochs(0, lookback);
for epochs in 1..=lookback {
let curr = normalize_epochs(epochs, lookback);
assert!(
curr >= prev,
"normalize_epochs not monotonic: {} gave {}, {} gave {}",
epochs - 1,
prev,
epochs,
curr
);
prev = curr;
}
}
#[test]
fn test_normalize_excess_bounded() {
for &value in &[0.0, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 100.0] {
let result = normalize_excess(value);
assert!(
result >= 0.0 && result <= 1.0,
"normalize_excess({}) = {} not in [0, 1]",
value,
result
);
}
}
#[test]
fn test_normalize_cv_bounded() {
for &cv in &[0.0, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0] {
let result = normalize_cv(cv);
assert!(
result >= 0.0 && result <= 1.0,
"normalize_cv({}) = {} not in [0, 1]",
cv,
result
);
}
}
#[test]
fn test_normalize_cv_nan_handling() {
let nan_result = normalize_cv(f64::NAN);
assert!(nan_result.is_finite(), "NaN should map to finite value");
assert!(nan_result < 0.3, "NaN should map to low value");
}
#[test]
fn test_normalize_drawdown_clamped() {
assert_eq!(normalize_drawdown(-0.1), 0.0);
assert_eq!(normalize_drawdown(0.5), 0.5);
assert_eq!(normalize_drawdown(1.5), 1.0);
}
#[test]
fn test_normalize_runup_clamped() {
assert_eq!(normalize_runup(-0.1), 0.0);
assert_eq!(normalize_runup(0.5), 0.5);
assert_eq!(normalize_runup(1.5), 1.0);
}
#[test]
fn test_normalize_epochs_zero_lookback() {
assert_eq!(normalize_epochs(0, 0), 0.5);
assert_eq!(normalize_epochs(5, 0), 0.5);
}
#[test]
fn test_normalize_epochs_zero_epochs() {
let result = normalize_epochs(0, 100);
assert!(
result < 0.1,
"Zero epochs should map to low value, got {}",
result
);
assert!(result > 0.0, "Zero epochs should be distinguishable from 0");
}
#[test]
fn test_normalize_epochs_full_density() {
let result = normalize_epochs(100, 100);
assert!(
result > 0.5,
"All-epochs should map above midpoint, got {}",
result
);
assert!(result <= 1.0, "Must be bounded by 1.0");
}
#[test]
fn test_normalize_excess_zero() {
let result = normalize_excess(0.0);
assert!(
result.abs() < 0.01,
"Zero excess should map near 0, got {}",
result
);
}
#[test]
fn test_normalize_excess_negative_uses_abs() {
let pos = normalize_excess(0.1);
let neg = normalize_excess(-0.1);
assert_eq!(pos, neg, "normalize_excess should use absolute value");
}
#[test]
fn test_normalize_excess_large_saturates() {
let result = normalize_excess(100.0);
assert!(
result > 0.999,
"Large excess should saturate near 1.0, got {}",
result
);
}
#[test]
fn test_logistic_sigmoid_center() {
let result = logistic_sigmoid(0.5, 0.5, 4.0);
assert!(
(result - 0.5).abs() < 0.001,
"At center, sigmoid should be 0.5"
);
}
#[test]
fn test_logistic_sigmoid_extremes() {
let low = logistic_sigmoid(-10.0, 0.0, 1.0);
let high = logistic_sigmoid(10.0, 0.0, 1.0);
assert!(low < 0.001, "Far below center should be near 0");
assert!(high > 0.999, "Far above center should be near 1");
}
}