use super::*;
struct SimpleLcg {
state: u64,
}
impl SimpleLcg {
fn new(seed: u64) -> Self {
Self { state: seed }
}
fn next_u64(&mut self) -> u64 {
self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
self.state
}
fn next_normal(&mut self) -> f64 {
let u1 = (self.next_u64() as f64) / (u64::MAX as f64);
let u2 = (self.next_u64() as f64) / (u64::MAX as f64);
let u1 = u1.max(1e-15);
(-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
}
fn normal_series(&mut self, n: usize, scale: f64) -> Vec<f64> {
(0..n).map(|_| self.next_normal() * scale).collect()
}
}
fn make_correlated_pair(n: usize, corr: f64, seed: u64) -> (Vec<f64>, Vec<f64>) {
let mut rng = SimpleLcg::new(seed);
let base: Vec<f64> = rng.normal_series(n, 0.01);
let noise_weight = (1.0 - corr * corr).sqrt();
let mut rng2 = SimpleLcg::new(seed + 9999);
let correlated: Vec<f64> = base
.iter()
.map(|&b| corr * b + noise_weight * rng2.next_normal() * 0.01)
.collect();
(base, correlated)
}
#[test]
fn mean_of_simple_series() {
assert!((mean(&[1.0, 2.0, 3.0]) - 2.0).abs() < 1e-10);
}
#[test]
fn mean_of_single_value() {
assert!((mean(&[5.0]) - 5.0).abs() < 1e-10);
}
#[test]
fn variance_of_simple_series() {
let data = [1.0, 2.0, 3.0];
let m = mean(&data);
assert!((variance(&data, m) - 1.0).abs() < 1e-10);
}
#[test]
fn variance_of_constant_is_zero() {
let data = [3.0, 3.0, 3.0, 3.0];
let m = mean(&data);
assert!(variance(&data, m) < 1e-20);
}
#[test]
fn distance_from_perfect_correlation_is_zero() {
let corr = vec![vec![1.0, 1.0], vec![1.0, 1.0]];
let dist = distance_matrix(&corr);
assert!(dist[0][1].abs() < 1e-10);
}
#[test]
fn distance_from_zero_correlation() {
let corr = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let dist = distance_matrix(&corr);
assert!((dist[0][1] - 0.5_f64.sqrt()).abs() < 1e-10);
}
#[test]
fn distance_from_negative_correlation() {
let corr = vec![vec![1.0, -1.0], vec![-1.0, 1.0]];
let dist = distance_matrix(&corr);
assert!((dist[0][1] - 1.0).abs() < 1e-10);
}
#[test]
fn distance_matrix_is_symmetric() {
let corr = vec![
vec![1.0, 0.5, -0.3],
vec![0.5, 1.0, 0.1],
vec![-0.3, 0.1, 1.0],
];
let dist = distance_matrix(&corr);
for (i, row_i) in dist.iter().enumerate() {
for (j, &val) in row_i.iter().enumerate() {
assert!(
(val - dist[j][i]).abs() < 1e-10,
"dist[{}][{}] != dist[{}][{}]",
i,
j,
j,
i
);
}
}
}
#[test]
fn correlation_of_identical_series_is_one() {
let returns = vec![0.01, -0.02, 0.015, -0.005, 0.02];
let series: Vec<&[f64]> = vec![&returns, &returns];
let means: Vec<f64> = series.iter().map(|s| mean(s)).collect();
let vars: Vec<f64> = series
.iter()
.enumerate()
.map(|(i, s)| variance(s, means[i]))
.collect();
let corr = correlation_matrix(&series, &means, &vars);
assert!((corr[0][1] - 1.0).abs() < 1e-10);
}
#[test]
fn correlation_diagonal_is_one() {
let mut rng = SimpleLcg::new(42);
let series_a = rng.normal_series(100, 0.01);
let series_b = rng.normal_series(100, 0.01);
let series: Vec<&[f64]> = vec![&series_a, &series_b];
let means: Vec<f64> = series.iter().map(|s| mean(s)).collect();
let vars: Vec<f64> = series
.iter()
.enumerate()
.map(|(i, s)| variance(s, means[i]))
.collect();
let corr = correlation_matrix(&series, &means, &vars);
assert!((corr[0][0] - 1.0).abs() < 1e-10);
assert!((corr[1][1] - 1.0).abs() < 1e-10);
}
#[test]
fn high_correlation_detected() {
let (base, correlated) = make_correlated_pair(252, 0.9, 42);
let series: Vec<&[f64]> = vec![&base, &correlated];
let means: Vec<f64> = series.iter().map(|s| mean(s)).collect();
let vars: Vec<f64> = series
.iter()
.enumerate()
.map(|(i, s)| variance(s, means[i]))
.collect();
let corr = correlation_matrix(&series, &means, &vars);
assert!(
corr[0][1] > 0.7,
"Expected high correlation, got {}",
corr[0][1]
);
}
#[test]
fn single_leg_dendrogram() {
let tree = build_dendrogram(&[vec![0.0]], 1);
assert!(matches!(tree, ClusterNode::Leaf(0)));
}
#[test]
fn two_legs_merge_into_branch() {
let dist = vec![vec![0.0, 0.5], vec![0.5, 0.0]];
let tree = build_dendrogram(&dist, 2);
match tree {
ClusterNode::Branch { .. } => {}
ClusterNode::Leaf(_) => panic!("Expected branch for 2 legs"),
}
}
#[test]
fn closest_pair_merged_first() {
let dist = vec![
vec![0.0, 0.1, 0.9],
vec![0.1, 0.0, 0.8],
vec![0.9, 0.8, 0.0],
];
let tree = build_dendrogram(&dist, 3);
match tree {
ClusterNode::Branch {
ref left,
ref right,
} => {
let left_leaves = match left.as_ref() {
ClusterNode::Branch {
left: ll,
right: lr,
} => {
let mut v = Vec::new();
if let ClusterNode::Leaf(i) = ll.as_ref() {
v.push(*i);
}
if let ClusterNode::Leaf(i) = lr.as_ref() {
v.push(*i);
}
v
}
ClusterNode::Leaf(i) => vec![*i],
};
let right_leaves = match right.as_ref() {
ClusterNode::Branch {
left: rl,
right: rr,
} => {
let mut v = Vec::new();
if let ClusterNode::Leaf(i) = rl.as_ref() {
v.push(*i);
}
if let ClusterNode::Leaf(i) = rr.as_ref() {
v.push(*i);
}
v
}
ClusterNode::Leaf(i) => vec![*i],
};
let has_pair = (left_leaves == vec![0, 1] && right_leaves == vec![2])
|| (left_leaves == vec![2] && right_leaves == vec![0, 1]);
assert!(
has_pair,
"Expected {{0,1}} and {{2}}, got {:?} and {:?}",
left_leaves, right_leaves
);
}
_ => panic!("Expected branch at root"),
}
}
#[test]
fn equal_variance_bisection_gives_equal_weight() {
let tree = ClusterNode::Branch {
left: Box::new(ClusterNode::Leaf(0)),
right: Box::new(ClusterNode::Leaf(1)),
};
let variances = [1.0, 1.0];
let mut weights = [0.0; 2];
recursive_bisect(&tree, &variances, 1.0, &mut weights);
assert!((weights[0] - 0.5).abs() < 1e-10);
assert!((weights[1] - 0.5).abs() < 1e-10);
}
#[test]
fn higher_variance_gets_less_weight() {
let tree = ClusterNode::Branch {
left: Box::new(ClusterNode::Leaf(0)),
right: Box::new(ClusterNode::Leaf(1)),
};
let variances = [1.0, 3.0]; let mut weights = [0.0; 2];
recursive_bisect(&tree, &variances, 1.0, &mut weights);
assert!(
weights[0] > weights[1],
"Lower variance leg should get more weight: {} vs {}",
weights[0],
weights[1]
);
assert!(((weights[0] + weights[1]) - 1.0).abs() < 1e-10);
}
#[test]
fn bisection_preserves_total_weight() {
let tree = ClusterNode::Branch {
left: Box::new(ClusterNode::Branch {
left: Box::new(ClusterNode::Leaf(0)),
right: Box::new(ClusterNode::Leaf(1)),
}),
right: Box::new(ClusterNode::Leaf(2)),
};
let variances = [1.0, 2.0, 1.5];
let mut weights = [0.0; 3];
recursive_bisect(&tree, &variances, 1.0, &mut weights);
let total: f64 = weights.iter().sum();
assert!(
(total - 1.0).abs() < 1e-10,
"Weights should sum to 1.0, got {}",
total
);
}
#[test]
fn decimal_from_f64_normal_value() {
assert_eq!(
decimal_from_f64(0.5),
Ok(rust_decimal::Decimal::new(500000, 6))
);
}
#[test]
fn decimal_from_f64_rejects_nan() {
assert!(decimal_from_f64(f64::NAN).is_err());
}
#[test]
fn decimal_from_f64_rejects_infinity() {
assert!(decimal_from_f64(f64::INFINITY).is_err());
}