generated from mathias/template-go-web
Taskfile.yml line 46 had an unquoted Go-template `{{.VAR}}` inside a YAML
flow sequence (`cmds: [...]`) -- the literal braces broke YAML parsing
outright ("did not find expected ',' or ']'"), so `task check` (and thus
CI's push-triggered check job) failed before running a single command.
A manual `workflow_dispatch` re-run passed because the autoresearch
workflow never calls `task check` at all -- unrelated path, not an
env/secret difference as first suspected. Quoted the string.
Also fixed a staticcheck QF1002 in internal/eval/var.go: a boolean
switch comparing the same variable (n1) in every case is a tagged
switch in disguise -- converted to `switch n1 { case 0: ... case n: ...
}`.
108 lines
2.7 KiB
Go
108 lines
2.7 KiB
Go
package eval
|
||
|
||
import "math"
|
||
|
||
// VaRBreachRate computes the parametric 99% VaR breach rate and Kupiec POF p-value.
|
||
//
|
||
// VaR_99_t = predVol[t] × z99 (z99 = 2.326 for 99% normal VaR)
|
||
// breach_t = actualVol[t] > VaR_99_t (strict inequality)
|
||
// breachRate = fraction of breaches over all steps
|
||
// kupiecP = Kupiec POF p-value: P(chi²(1) > LR) where LR is the likelihood ratio
|
||
// testing H0: true breach probability = 1%. High p = well-calibrated.
|
||
//
|
||
// Returns (0, 1) for empty or mismatched input.
|
||
func VaRBreachRate(predVol, actualVol []float64, z99 float64) (breachRate, kupiecP float64) {
|
||
n := len(predVol)
|
||
if n == 0 || n != len(actualVol) {
|
||
return 0, 1
|
||
}
|
||
|
||
var n1 int
|
||
for i := 0; i < n; i++ {
|
||
if actualVol[i] > predVol[i]*z99 {
|
||
n1++
|
||
}
|
||
}
|
||
|
||
breachRate = float64(n1) / float64(n)
|
||
kupiecP = kupiecPOF(n, n1, 0.01)
|
||
return
|
||
}
|
||
|
||
// kupiecPOF returns the Kupiec Proportion-of-Failures p-value.
|
||
// H0: true breach probability = p0 (e.g. 0.01 for 99% VaR).
|
||
// Returns 1.0 for edge cases (n=0, p_hat=p0).
|
||
func kupiecPOF(n, n1 int, p0 float64) float64 {
|
||
if n == 0 {
|
||
return 1.0
|
||
}
|
||
n0 := n - n1
|
||
phat := float64(n1) / float64(n)
|
||
|
||
var lr float64
|
||
switch n1 {
|
||
case 0:
|
||
// 0 × ln(0/p0) = 0 by convention; only the n0 term contributes
|
||
lr = 2 * float64(n0) * math.Log((1-phat)/(1-p0))
|
||
case n:
|
||
// n0 term vanishes
|
||
lr = 2 * float64(n1) * math.Log(phat/p0)
|
||
default:
|
||
lr = 2 * (float64(n1)*math.Log(phat/p0) + float64(n0)*math.Log((1-phat)/(1-p0)))
|
||
}
|
||
|
||
if lr <= 0 {
|
||
return 1.0
|
||
}
|
||
// P(chi²(1) > LR) = erfc(sqrt(LR/2)) [chi²(1) = Z², Z~N(0,1)]
|
||
return math.Erfc(math.Sqrt(lr / 2))
|
||
}
|
||
|
||
// LinearProbePredict fits ridge regression on (trainEmb, trainY) and returns
|
||
// predictions for testEmb. Complements LinearProbeTrainTest when the caller
|
||
// needs the raw predictions (e.g. to compute VaR breach rate).
|
||
// Returns nil when trainEmb is empty.
|
||
func LinearProbePredict(trainEmb [][]float64, trainY []float64,
|
||
testEmb [][]float64, lambda float64) []float64 {
|
||
n := len(trainEmb)
|
||
if n == 0 || len(testEmb) == 0 {
|
||
return nil
|
||
}
|
||
d := len(trainEmb[0])
|
||
p := d + 1
|
||
|
||
A := make([][]float64, n)
|
||
for i, e := range trainEmb {
|
||
row := make([]float64, p)
|
||
copy(row, e)
|
||
row[d] = 1.0
|
||
A[i] = row
|
||
}
|
||
AtA := make([][]float64, p)
|
||
for i := range AtA {
|
||
AtA[i] = make([]float64, p)
|
||
}
|
||
Aty := make([]float64, p)
|
||
for i := 0; i < n; i++ {
|
||
for j := 0; j < p; j++ {
|
||
Aty[j] += A[i][j] * trainY[i]
|
||
for k := 0; k < p; k++ {
|
||
AtA[j][k] += A[i][j] * A[i][k]
|
||
}
|
||
}
|
||
}
|
||
for j := 0; j < p; j++ {
|
||
AtA[j][j] += lambda
|
||
}
|
||
w := solveCholesky(AtA, Aty)
|
||
|
||
preds := make([]float64, len(testEmb))
|
||
for i, e := range testEmb {
|
||
row := make([]float64, p)
|
||
copy(row, e)
|
||
row[d] = 1.0
|
||
preds[i] = dot(row, w)
|
||
}
|
||
return preds
|
||
}
|