package eval_test import ( "math" "testing" "gitea.d-ma.be/mathias/jepa-fx-risk/internal/eval" ) // ── VaRBreachRate golden tests ────────────────────────────────────────────── // // VaR_99_t = predVol[t] × z99 (parametric 99% normal VaR) // breach_t = actualVol[t] > VaR_99_t // breachRate = mean(breach_t) // kupiecP = Kupiec POF p-value (chi²(1) test, H0: breach rate = 1%) func TestVaRBreachRate_ZeroBreaches(t *testing.T) { // 0.02 < 0.01×2.326=0.02326 → no breaches pred := []float64{0.01, 0.01, 0.01} act := []float64{0.02, 0.02, 0.02} rate, _ := eval.VaRBreachRate(pred, act, 2.326) if rate != 0 { t.Fatalf("want rate=0, got %.4f", rate) } } func TestVaRBreachRate_AllBreach(t *testing.T) { // 0.03 > 0.02326 → all breach pred := []float64{0.01, 0.01} act := []float64{0.03, 0.03} rate, _ := eval.VaRBreachRate(pred, act, 2.326) if math.Abs(rate-1.0) > 1e-9 { t.Fatalf("want rate=1.0, got %.4f", rate) } } func TestVaRBreachRate_Golden(t *testing.T) { // n=10, 2 breaches at indices 0 and 2 → rate=0.2 // Kupiec: p_hat=0.2 vs p0=0.01 → strongly reject H0 (p < 0.05) pred := make([]float64, 10) act := make([]float64, 10) for i := range pred { pred[i] = 0.01 act[i] = 0.01 // no breach: 0.01 < 0.02326 } act[0] = 0.03 // breach act[2] = 0.03 // breach rate, kupiecP := eval.VaRBreachRate(pred, act, 2.326) if math.Abs(rate-0.2) > 1e-9 { t.Fatalf("breach rate: want 0.2, got %.4f", rate) } if kupiecP > 0.05 { t.Fatalf("kupiec p-value: want <0.05 (strong reject H0), got %.4f", kupiecP) } } func TestVaRBreachRate_PerfectCalibration(t *testing.T) { // n=100, exactly 1 breach → p_hat=0.01=p0 → LR=0 → kupiecP≈1.0 n := 100 pred := make([]float64, n) act := make([]float64, n) for i := range pred { pred[i] = 0.01 act[i] = 0.015 // < 0.02326, no breach } act[0] = 0.025 // > 0.02326, breach rate, kupiecP := eval.VaRBreachRate(pred, act, 2.326) if math.Abs(rate-0.01) > 1e-9 { t.Fatalf("breach rate: want 0.01, got %.4f", rate) } if kupiecP < 0.9 { t.Fatalf("kupiec p-value: want ≈1.0 (well calibrated), got %.4f", kupiecP) } } func TestVaRBreachRate_EmptyInput(t *testing.T) { rate, kupiecP := eval.VaRBreachRate(nil, nil, 2.326) if rate != 0 || kupiecP != 1 { t.Fatalf("empty: want (0,1), got (%.4f,%.4f)", rate, kupiecP) } } func TestVaRBreachRate_LenMismatch(t *testing.T) { rate, kupiecP := eval.VaRBreachRate([]float64{0.01}, []float64{0.01, 0.02}, 2.326) if rate != 0 || kupiecP != 1 { t.Fatalf("mismatch: want (0,1), got (%.4f,%.4f)", rate, kupiecP) } } func TestVaRBreachRate_Z99Default(t *testing.T) { // z99=2.326 is the canonical value; test that boundary case works // VaR = 0.01 × 2.326 = 0.02326 // actual = 0.02326 → NOT a breach (strict >) pred := []float64{0.01} act := []float64{0.02326} rate, _ := eval.VaRBreachRate(pred, act, 2.326) if rate != 0 { t.Fatalf("boundary: exactly at VaR is not a breach; want rate=0, got %.4f", rate) } } // ── LinearProbePredict ────────────────────────────────────────────────────── func TestLinearProbePredict_PerfectLinear(t *testing.T) { // y = x; predictions should match targets closely n := 20 trainEmb := make([][]float64, n) trainY := make([]float64, n) testEmb := make([][]float64, 5) testY := []float64{5, 10, 15, 20, 25} for i := range trainEmb { trainEmb[i] = []float64{float64(i)} trainY[i] = float64(i) } for i := range testEmb { testEmb[i] = []float64{testY[i]} } preds := eval.LinearProbePredict(trainEmb, trainY, testEmb, 1e-3) if len(preds) != len(testEmb) { t.Fatalf("len: want %d, got %d", len(testEmb), len(preds)) } for i, p := range preds { if math.Abs(p-testY[i]) > 1.0 { t.Fatalf("pred[%d]: want ≈%.1f, got %.4f", i, testY[i], p) } } } func TestLinearProbePredict_EmptyTrain(t *testing.T) { preds := eval.LinearProbePredict(nil, nil, [][]float64{{1.0}}, 1e-3) if len(preds) != 0 { t.Fatalf("empty train: want nil/empty preds, got len=%d", len(preds)) } }