generated from mathias/template-go-web
CD / Lint / Test / Vet (push) Successful in 5s
go.mod declared this module under gitea.d-ma.be, the pre-rename host (infra ADR-0004). That name still resolves in DNS but no longer serves -- HTTPS to it fails outright -- so the declared path pointed at nothing. It built anyway, because a Go module does not have to be fetchable from its own declared path to compile locally. That is exactly what made it durable rot: nothing failed, so nothing flagged it. What it did break is anyone trying to depend on this repo -- `go get git.d-ma.be/mathias/<repo>` fails on the path mismatch, and an importer would have to spell the dead host. Verified safe before changing: nothing in the estate requires this module (checked every go.mod across the Go repos) -- these are leaf binaries, so the rename breaks no importer. Mechanical: module path plus every internal import rewritten, templ regenerated where applicable. Verified after -- go build, go vet, go test and golangci-lint (against a Go 1.26.1 toolchain, matching CI) all pass, and no gitea.d-ma.be reference remains in any .go/.templ/.mod file. Found by an estate-wide scan for the old host, prompted by the same rot turning up in tapir internal/web/views.templ (tapir@4bafd5f).
139 lines
4.1 KiB
Go
139 lines
4.1 KiB
Go
package eval_test
|
||
|
||
import (
|
||
"math"
|
||
"testing"
|
||
|
||
"git.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))
|
||
}
|
||
}
|