#[inline]
pub(crate) fn welford_mean_m2(slice: &[f64]) -> (f64, f64) {
let mut mean = 0.0_f64;
let mut m2 = 0.0_f64;
let mut n = 0.0_f64;
for &x in slice {
n += 1.0;
let delta = x - mean;
mean += delta / n;
let delta2 = x - mean;
m2 += delta * delta2;
}
(mean, m2)
}
fn rankdata(values: &[f64]) -> Vec<f64> {
let n = values.len();
if n == 0 {
return vec![];
}
if values.iter().any(|v| v.is_nan()) {
return vec![f64::NAN; n];
}
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&a, &b| {
values[a]
.partial_cmp(&values[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut ranks = vec![0.0_f64; n];
let mut i = 0;
while i < n {
let mut j = i + 1;
while j < n && values[indices[j]] == values[indices[i]] {
j += 1;
}
let avg_rank = (i + 1 + j) as f64 / 2.0;
for &idx in &indices[i..j] {
ranks[idx] = avg_rank;
}
i = j;
}
ranks
}
fn pearson(x: &[f64], y: &[f64]) -> f64 {
let n = x.len() as f64;
if n < 2.0 {
return f64::NAN;
}
let mean_x = x.iter().sum::<f64>() / n;
let mean_y = y.iter().sum::<f64>() / n;
let mut cov = 0.0_f64;
let mut var_x = 0.0_f64;
let mut var_y = 0.0_f64;
for i in 0..x.len() {
let dx = x[i] - mean_x;
let dy = y[i] - mean_y;
cov += dx * dy;
var_x += dx * dx;
var_y += dy * dy;
}
if var_x == 0.0 || var_y == 0.0 {
return f64::NAN;
}
cov / (var_x * var_y).sqrt()
}
fn regularized_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
if x >= 1.0 {
return 1.0;
}
if x > (a + 1.0) / (a + b + 2.0) {
return 1.0 - regularized_incomplete_beta_cf(1.0 - x, b, a);
}
regularized_incomplete_beta_cf(x, a, b)
}
fn regularized_incomplete_beta_cf(x: f64, a: f64, b: f64) -> f64 {
let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
let front = (x.ln() * a + (1.0 - x).ln() * b - ln_beta).exp() / a;
front * betacf(a, b, x)
}
fn betacf(a: f64, b: f64, x: f64) -> f64 {
let tiny = 1e-30_f64;
let eps = 3e-14;
let max_iter = 200;
let qab = a + b;
let qap = a + 1.0;
let qam = a - 1.0;
let mut c = 1.0_f64;
let mut d = 1.0 - qab * x / qap;
if d.abs() < tiny {
d = tiny;
}
d = 1.0 / d;
let mut h = d;
for m in 1..=max_iter {
let em = m as f64;
let m2 = 2.0 * em;
let aa = em * (b - em) * x / ((qam + m2) * (a + m2));
d = 1.0 + aa * d;
if d.abs() < tiny {
d = tiny;
}
c = 1.0 + aa / c;
if c.abs() < tiny {
c = tiny;
}
d = 1.0 / d;
h *= d * c;
let aa = -(a + em) * (qab + em) * x / ((a + m2) * (qap + m2));
d = 1.0 + aa * d;
if d.abs() < tiny {
d = tiny;
}
c = 1.0 + aa / c;
if c.abs() < tiny {
c = tiny;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() <= eps {
return h;
}
}
h }
fn ln_gamma(x: f64) -> f64 {
#[allow(clippy::excessive_precision)]
let coefs = [
0.999_999_999_999_809_93,
676.520_368_121_885_1,
-1259.139_216_722_402_8,
771.323_428_777_653_08,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_572e-6,
1.505_632_735_149_311_6e-7,
];
if x < 0.5 {
let pi = std::f64::consts::PI;
return pi.ln() - (pi * x).sin().ln() - ln_gamma(1.0 - x);
}
let x = x - 1.0;
let mut sum = coefs[0];
for (i, &c) in coefs[1..].iter().enumerate() {
sum += c / (x + i as f64 + 1.0);
}
let t = x + 7.5; 0.5 * (2.0 * std::f64::consts::PI).ln() + (t.ln() * (x + 0.5)) - t + sum.ln()
}
fn t_distribution_two_tailed_p(t_stat: f64, df: f64) -> f64 {
if df <= 0.0 {
return f64::NAN;
}
let x = df / (df + t_stat * t_stat);
let p_one_tail = 0.5 * regularized_incomplete_beta(x, df / 2.0, 0.5);
2.0 * p_one_tail
}
fn standard_normal_cdf(x: f64) -> f64 {
if x.is_nan() {
return f64::NAN;
}
if x == f64::INFINITY {
return 1.0;
}
if x == f64::NEG_INFINITY {
return 0.0;
}
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let z = x.abs() / std::f64::consts::SQRT_2;
let t = 1.0 / (1.0 + 0.327_591_1 * z);
let y = 1.0
- (((((1.061_405_429 * t - 1.453_152_027) * t + 1.421_413_741) * t - 0.284_496_736) * t
+ 0.254_829_592)
* t
* (-z * z).exp());
0.5 * (1.0 + sign * y)
}
fn inverse_standard_normal_cdf(p: f64) -> f64 {
if p <= 0.0 {
return f64::NEG_INFINITY;
}
if p >= 1.0 {
return f64::INFINITY;
}
const A: [f64; 6] = [
-3.969_683_028_665_376e1,
2.209_460_984_245_205e2,
-2.759_285_104_469_687e2,
1.383_577_518_672_69e2,
-3.066_479_806_614_716e1,
2.506_628_277_459_239,
];
const B: [f64; 5] = [
-5.447_609_879_822_406e1,
1.615_858_368_580_409e2,
-1.556_989_798_598_866e2,
6.680_131_188_771_972e1,
-1.328_068_155_288_572e1,
];
const C: [f64; 6] = [
-7.784_894_002_430_293e-3,
-3.223_964_580_411_365e-1,
-2.400_758_277_161_838,
-2.549_732_539_343_734,
4.374_664_141_464_968,
2.938_163_982_698_783,
];
const D: [f64; 4] = [
7.784_695_709_041_462e-3,
3.224_671_290_700_398e-1,
2.445_134_137_142_996,
3.754_408_661_907_416,
];
const P_LOW: f64 = 0.024_25;
const P_HIGH: f64 = 1.0 - P_LOW;
if p < P_LOW {
let q = (-2.0 * p.ln()).sqrt();
return (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0);
}
if p <= P_HIGH {
let q = p - 0.5;
let r = q * q;
return (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
/ (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0);
}
let q = (-2.0 * (1.0 - p).ln()).sqrt();
-(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
}
pub fn spearman(x: &[f64], y: &[f64]) -> (f64, f64) {
let n = x.len();
if n != y.len() || n < 3 {
return (f64::NAN, f64::NAN);
}
let rank_x = rankdata(x);
let rank_y = rankdata(y);
let r = pearson(&rank_x, &rank_y);
if r.is_nan() {
return (f64::NAN, f64::NAN);
}
let r_clamped = r.clamp(-1.0, 1.0);
if (r_clamped.abs() - 1.0).abs() < 1e-15 {
return (r_clamped, 0.0);
}
let df = n as f64 - 2.0;
let t_stat = r_clamped * (df / (1.0 - r_clamped * r_clamped)).sqrt();
let p_value = t_distribution_two_tailed_p(t_stat, df);
(r_clamped, p_value)
}
pub fn deflated_sharpe(sharpe: f64, n_trials: usize, skewness: f64, kurtosis: f64) -> f64 {
if n_trials == 0 {
return f64::NAN;
}
if n_trials == 1 {
return sharpe;
}
if !sharpe.is_finite()
|| !skewness.is_finite()
|| !kurtosis.is_finite()
|| sharpe < 0.0
|| kurtosis <= 0.0
{
return f64::NAN;
}
let n = n_trials as f64;
let variance_term = 1.0 - skewness * sharpe + 0.25 * (kurtosis - 1.0) * sharpe * sharpe;
if !variance_term.is_finite() || variance_term <= 0.0 {
return f64::NAN;
}
let sharpe_se = (variance_term / (n - 1.0)).sqrt();
if !sharpe_se.is_finite() || sharpe_se <= 0.0 {
return f64::NAN;
}
let euler_gamma = 0.577_215_664_901_532_9_f64;
let p1 = (1.0 - 1.0 / n).clamp(f64::EPSILON, 1.0 - f64::EPSILON);
let p2 = (1.0 - 1.0 / (n * std::f64::consts::E)).clamp(f64::EPSILON, 1.0 - f64::EPSILON);
let expected_max_standard = (1.0 - euler_gamma) * inverse_standard_normal_cdf(p1)
+ euler_gamma * inverse_standard_normal_cdf(p2);
if !expected_max_standard.is_finite() {
return f64::NAN;
}
let expected_max_sharpe = sharpe_se * expected_max_standard;
standard_normal_cdf((sharpe - expected_max_sharpe) / sharpe_se)
}
pub fn quintile_spread(scores: &[f64], returns: &[f64], n_quantiles: usize) -> f64 {
let n = scores.len();
if n != returns.len() || n < n_quantiles || n_quantiles == 0 {
return f64::NAN;
}
if scores.iter().any(|v| v.is_nan()) || returns.iter().any(|v| v.is_nan()) {
return f64::NAN;
}
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&a, &b| {
scores[a]
.partial_cmp(&scores[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let group_size = n / n_quantiles;
if group_size == 0 {
return f64::NAN;
}
let bottom_mean: f64 = indices[..group_size]
.iter()
.map(|&i| returns[i])
.sum::<f64>()
/ group_size as f64;
let top_start = n - group_size;
let top_mean: f64 = indices[top_start..]
.iter()
.map(|&i| returns[i])
.sum::<f64>()
/ group_size as f64;
top_mean - bottom_mean
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rankdata_no_ties() {
let values = [3.0, 1.0, 2.0];
let ranks = rankdata(&values);
assert!((ranks[0] - 3.0).abs() < 1e-10);
assert!((ranks[1] - 1.0).abs() < 1e-10);
assert!((ranks[2] - 2.0).abs() < 1e-10);
}
#[test]
fn rankdata_with_ties() {
let values = [1.0, 2.0, 2.0, 4.0];
let ranks = rankdata(&values);
assert!((ranks[0] - 1.0).abs() < 1e-10);
assert!((ranks[1] - 2.5).abs() < 1e-10); assert!((ranks[2] - 2.5).abs() < 1e-10);
assert!((ranks[3] - 4.0).abs() < 1e-10);
}
#[test]
fn rankdata_empty() {
let ranks = rankdata(&[]);
assert!(ranks.is_empty());
}
#[test]
fn spearman_perfect_positive() {
let x: Vec<f64> = (0..50).map(|i| i as f64).collect();
let y: Vec<f64> = (0..50).map(|i| i as f64).collect();
let (r, p) = spearman(&x, &y);
assert!((r - 1.0).abs() < 1e-10, "expected r=1.0, got {r}");
assert!(p < 1e-10, "expected p≈0, got {p}");
}
#[test]
fn spearman_perfect_negative() {
let x: Vec<f64> = (0..50).map(|i| i as f64).collect();
let y: Vec<f64> = (0..50).rev().map(|i| i as f64).collect();
let (r, p) = spearman(&x, &y);
assert!((r - (-1.0)).abs() < 1e-10, "expected r=-1.0, got {r}");
assert!(p < 1e-10, "expected p≈0, got {p}");
}
#[test]
fn spearman_too_few() {
let x = [1.0, 2.0];
let y = [3.0, 4.0];
let (r, p) = spearman(&x, &y);
assert!(r.is_nan());
assert!(p.is_nan());
}
#[test]
fn spearman_unequal_length() {
let x = [1.0, 2.0, 3.0];
let y = [1.0, 2.0];
let (r, _) = spearman(&x, &y);
assert!(r.is_nan());
}
#[test]
fn quintile_spread_basic() {
let scores: Vec<f64> = (1..=10).map(|i| i as f64).collect();
let returns: Vec<f64> = (1..=10).map(|i| i as f64 * 0.01).collect();
let spread = quintile_spread(&scores, &returns, 5);
assert!(spread > 0.0, "expected positive spread, got {spread}");
}
#[test]
fn quintile_spread_zero_for_random() {
let scores: Vec<f64> = (1..=10).map(|i| i as f64).collect();
let returns: Vec<f64> = (1..=10).rev().map(|i| i as f64 * 0.01).collect();
let spread = quintile_spread(&scores, &returns, 5);
assert!(spread < 0.0, "expected negative spread, got {spread}");
}
#[test]
fn quintile_spread_invalid() {
let scores = [1.0, 2.0];
let returns = [0.01, 0.02];
let spread = quintile_spread(&scores, &returns, 5);
assert!(spread.is_nan());
}
#[test]
fn rankdata_nan_input_propagates() {
let ranks = rankdata(&[1.0, f64::NAN, 2.0, 3.0]);
assert_eq!(ranks.len(), 4);
for (i, r) in ranks.iter().enumerate() {
assert!(r.is_nan(), "rankdata[{i}] = {r}, expected NaN");
}
}
#[test]
fn rankdata_multiple_nan_propagates() {
let ranks = rankdata(&[f64::NAN, f64::NAN, f64::NAN]);
assert!(ranks.iter().all(|r| r.is_nan()));
}
#[test]
fn rankdata_positive_infinity_is_ordered_not_nan() {
let ranks = rankdata(&[1.0, 2.0, f64::INFINITY, 3.0]);
assert!(ranks.iter().all(|r| !r.is_nan()), "infinity ≠ NaN");
assert!(ranks[2] > ranks[0], "+Inf should rank above finite values");
assert!(ranks[2] > ranks[3], "+Inf should rank above finite values");
}
#[test]
fn spearman_nan_input_propagates_to_result() {
let x = [1.0, 2.0, 3.0, f64::NAN, 5.0];
let y = [1.0, 2.0, 3.0, 4.0, 5.0];
let (r, p) = spearman(&x, &y);
assert!(r.is_nan(), "spearman r on NaN input should be NaN, got {r}");
assert!(p.is_nan(), "spearman p on NaN input should be NaN, got {p}");
}
#[test]
fn spearman_nan_in_either_input_propagates() {
let finite = [1.0, 2.0, 3.0, 4.0, 5.0];
let with_nan = [1.0, 2.0, f64::NAN, 4.0, 5.0];
let (rx, _) = spearman(&with_nan, &finite);
let (ry, _) = spearman(&finite, &with_nan);
assert!(rx.is_nan(), "NaN in x must propagate");
assert!(ry.is_nan(), "NaN in y must propagate");
}
#[test]
fn quintile_spread_nan_score_propagates() {
let mut scores: Vec<f64> = (1..=10).map(|i| i as f64).collect();
scores[3] = f64::NAN;
let returns: Vec<f64> = (1..=10).map(|i| i as f64 * 0.01).collect();
let spread = quintile_spread(&scores, &returns, 5);
assert!(spread.is_nan(), "NaN score must propagate, got {spread}");
}
#[test]
fn quintile_spread_nan_return_propagates() {
let scores: Vec<f64> = (1..=10).map(|i| i as f64).collect();
let mut returns: Vec<f64> = (1..=10).map(|i| i as f64 * 0.01).collect();
returns[7] = f64::NAN;
let spread = quintile_spread(&scores, &returns, 5);
assert!(spread.is_nan(), "NaN return must propagate, got {spread}");
}
proptest::proptest! {
#[test]
fn prop_rankdata_nan_anywhere_produces_all_nan(
len in 1usize..50,
nan_idx in 0usize..50,
values in proptest::collection::vec(-1_000.0f64..1_000.0, 1..50),
) {
let mut xs = values;
let len = len.min(xs.len());
xs.truncate(len);
let nan_idx = nan_idx % xs.len();
xs[nan_idx] = f64::NAN;
let ranks = rankdata(&xs);
proptest::prop_assert_eq!(ranks.len(), xs.len());
for r in &ranks {
proptest::prop_assert!(r.is_nan());
}
}
#[test]
fn prop_rankdata_finite_input_produces_no_nan(
values in proptest::collection::vec(-1_000.0f64..1_000.0, 2..50),
) {
let ranks = rankdata(&values);
for (i, r) in ranks.iter().enumerate() {
proptest::prop_assert!(
!r.is_nan(),
"ranks[{}] = {} on finite input {:?}",
i, r, values
);
}
}
}
#[test]
fn ln_gamma_known_values() {
assert!(ln_gamma(1.0).abs() < 1e-10);
assert!(ln_gamma(2.0).abs() < 1e-10);
assert!((ln_gamma(5.0) - 24.0_f64.ln()).abs() < 1e-8);
}
#[test]
fn test_deflated_sharpe_basic() {
let dsr = deflated_sharpe(1.5, 20, 0.0, 3.0);
assert!(dsr.is_finite(), "expected finite DSR, got {dsr}");
assert!(
(0.0..=1.0).contains(&dsr),
"expected probability, got {dsr}"
);
}
#[test]
fn test_deflated_sharpe_single_trial() {
let sharpe = 1.25;
let dsr = deflated_sharpe(sharpe, 1, 0.0, 3.0);
assert_eq!(dsr, sharpe);
}
#[test]
fn test_deflated_sharpe_zero_skewness_kurtosis() {
let dsr = deflated_sharpe(1.0, 10, 0.0, 3.0);
assert!(
dsr.is_finite(),
"expected finite normal-case DSR, got {dsr}"
);
assert!(
(0.0..=1.0).contains(&dsr),
"expected probability, got {dsr}"
);
}
#[test]
fn test_deflated_sharpe_invalid_inputs() {
assert!(deflated_sharpe(1.0, 0, 0.0, 3.0).is_nan());
assert!(deflated_sharpe(-1.0, 10, 0.0, 3.0).is_nan());
assert!(deflated_sharpe(1.0, 10, 0.0, -3.0).is_nan());
assert!(deflated_sharpe(f64::INFINITY, 10, 0.0, 3.0).is_nan());
}
#[test]
fn test_deflated_sharpe_extreme_values() {
let large = deflated_sharpe(10.0, 1_000, 0.0, 3.0);
let small = deflated_sharpe(1.0e-12, 1_000, 0.0, 3.0);
assert!(
large.is_finite(),
"large Sharpe should stay finite, got {large}"
);
assert!(
small.is_finite(),
"small Sharpe should stay finite, got {small}"
);
assert!((0.0..=1.0).contains(&large));
assert!((0.0..=1.0).contains(&small));
}
proptest::proptest! {
#[test]
fn prop_deflated_sharpe_does_not_exceed_unadjusted_probability(
sharpe in 0.01f64..10.0,
n_trials in 2usize..1000,
skewness in -5.0f64..5.0,
kurtosis in 0.1f64..10.0,
) {
let dsr = deflated_sharpe(sharpe, n_trials, skewness, kurtosis);
if dsr.is_finite() {
proptest::prop_assert!((0.0..=1.0).contains(&dsr), "DSR must be in [0, 1], got {dsr}");
}
}
}
}