package backtest import ( "math" "testing" "time" ) func approxEqual(a, b, eps float64) bool { return math.Abs(a-b) <= eps } func TestSharpeFromEquitySnapshots(t *testing.T) { t.Run("insufficient snapshots", func(t *testing.T) { var snaps []*EquitySnapshot if v := sharpeFromEquitySnapshots(snaps, 0.01); v != 0 { t.Fatalf("expected 0 for empty snapshots, got %v", v) } snaps = append(snaps, &EquitySnapshot{Ts: time.Now().UnixMilli(), Equity: 100}) if v := sharpeFromEquitySnapshots(snaps, 0.01); v != 0 { t.Fatalf("expected 0 for single snapshot, got %v", v) } }) t.Run("zero volatility returns", func(t *testing.T) { // two identical returns -> sd == 0 -> expect 0 now := time.Now().UnixMilli() snaps := []*EquitySnapshot{ {Ts: now, Equity: 100}, {Ts: now + 86400*1000, Equity: 102}, // +2% {Ts: now + 2*86400*1000, Equity: 104.04}, // +2% } if v := sharpeFromEquitySnapshots(snaps, 0.01); v != 0 { t.Fatalf("expected 0 for zero-volatility returns, got %v", v) } }) t.Run("known case matches manual calc", func(t *testing.T) { // create three snapshots with returns r1=1%, r2=-0.5%, r3=2% now := time.Now().UnixMilli() snaps := []*EquitySnapshot{ {Ts: now, Equity: 100.0}, {Ts: now + 86400*1000, Equity: 101.0}, // +1% {Ts: now + 2*86400*1000, Equity: 100.495}, // -0.5% from 101 {Ts: now + 3*86400*1000, Equity: 102.5049}, // +2% from prev } got := sharpeFromEquitySnapshots(snaps, 0.01) // manual compute const secsYear = 365.0 * 24.0 * 3600.0 returns := []float64{0.01, -0.005, 0.02} dt := 86400.0 periodsPerYear := secsYear / dt rfPeriod := 0.01 / periodsPerYear excess := make([]float64, len(returns)) for i := range returns { excess[i] = returns[i] - rfPeriod } mean := 0.0 for _, v := range excess { mean += v } mean /= float64(len(excess)) s := 0.0 for _, v := range excess { d := v - mean s += d * d } sd := math.Sqrt(s / float64(len(excess)-1)) want := mean / sd * math.Sqrt(periodsPerYear) if !approxEqual(got, want, 1e-9) { t.Fatalf("sharpe mismatch got=%v want=%v", got, want) } }) }