You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
77 lines
2.1 KiB
77 lines
2.1 KiB
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) |
|
} |
|
}) |
|
}
|
|
|