package main import ( "math" "testing" ) func TestMeanStdDev(t *testing.T) { changes := []exchangeChange{ {name: "A", change: 0.1}, {name: "B", change: 0.2}, {name: "C", change: 0.3}, {name: "D", change: 0.4}, } mean, std := meanStdDev(changes) if math.Abs(mean-0.25) > 0.001 { t.Errorf("mean = %.4f, want 0.2500", mean) } if math.Abs(std-0.1118) > 0.01 { t.Errorf("std = %.4f, want ~0.1118", std) } } func TestMeanStdDevSingleValue(t *testing.T) { changes := []exchangeChange{ {name: "A", change: 0.1}, } mean, std := meanStdDev(changes) if mean != 0.1 { t.Errorf("mean = %.4f, want 0.1000", mean) } if std != 0 { t.Errorf("std = %.4f, want 0.0000", std) } } func TestMeanStdDevZeroValues(t *testing.T) { changes := []exchangeChange{ {name: "A", change: 0}, {name: "B", change: 0}, } mean, std := meanStdDev(changes) if mean != 0 { t.Errorf("mean = %.4f, want 0.0000", mean) } if std != 0 { t.Errorf("std = %.4f, want 0.0000", std) } } func TestMeanStdDevEmpty(t *testing.T) { mean, std := meanStdDev(nil) if mean != 0 || std != 0 { t.Errorf("expected 0,0 for empty input, got %.4f, %.4f", mean, std) } } func TestZScoreCalculation(t *testing.T) { // One exchange strongly diverging from the others // Three exchanges nearly flat, one moves 1.5% changes := []exchangeChange{ {name: "BG", change: 0.01}, {name: "HL", change: 0.01}, {name: "BN", change: 0.02}, {name: "OK", change: 1.50}, // anomalous! } _, std := meanStdDev(changes) maxAbs := 1.50 zScore := maxAbs / std if zScore < 2.0 { t.Errorf("z-score = %.2f, expected > 2.0 for divergent exchange", zScore) } t.Logf("Divergent exchange (1 of 4): z-score = %.2f (std = %.4f)", zScore, std) } func TestCoordinatedMovement(t *testing.T) { // All exchanges moving together = also a trend (consensus, not anomaly) changes := []exchangeChange{ {name: "BG", change: 0.05}, {name: "HL", change: 0.06}, {name: "BN", change: 0.04}, {name: "OK", change: 0.07}, } _, std := meanStdDev(changes) maxAbs := 0.07 zScore := maxAbs / std // Tight cluster → std is small, z-score is high → valid trend signal t.Logf("Co-movement: z-score = %.2f (std = %.4f) — high z-score + 4/4 agreement = trend", zScore, std) } func TestTrendDetectorConfigure(t *testing.T) { td := NewTrendDetector(nil) if td.anomalyMul != 3.0 { t.Errorf("default anomalyMul = %.1f, want 3.0", td.anomalyMul) } if td.confirmTicks != 3 { t.Errorf("default confirmTicks = %d, want 3", td.confirmTicks) } td.Configure(300, 2.5, 2, 30000) if td.anomalyMul != 2.5 { t.Errorf("anomalyMul after configure = %.1f, want 2.5", td.anomalyMul) } if td.confirmTicks != 2 { t.Errorf("confirmTicks after configure = %d, want 2", td.confirmTicks) } } func TestTrendDetectorEmptyConfigure(t *testing.T) { td := NewTrendDetector(nil) // Passing zeros should keep defaults td.Configure(0, 0, 0, 0) if td.anomalyMul != 3.0 { t.Errorf("anomalyMul = %.1f, expected default 3.0", td.anomalyMul) } }