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.
 
 

130 lines
3.6 KiB

package backtest
import (
"math"
"sig-pub/pkg/trade"
"sig-pub/pkg/types/ta"
"sig-pub/pkg/utils/collect"
"sort"
)
// SharpeRatio computes an annualized Sharpe ratio based on closed trades.
// rfAnnual is the annual risk-free rate expressed as a decimal (e.g. 0.01 for 1%).
// Method:
// - For each closed trade, we compute a period return = trade.Pnl / initialCash.
// - Period lengths are derived from successive trade close timestamps (ms).
// - Excess returns = periodReturn - rfAnnual * periodYears.
// - Sharpe = mean(excess) / stddev(excess) * sqrt(periodsPerYear)
// This provides a reasonable approximation when equity snapshots are not available.
func (a *TradingPlanBacktester) sharpeRatio(rfAnnual float64) float64 {
if a.account.initialCash <= 0 {
return 0
}
n := len(a.account.closeTrades)
if n < 2 {
return 0
}
trades := make([]*trade.TradeOrder, 0, n)
for _, tid := range a.account.closeTrades {
if order, ok := a.account.orders[tid]; ok {
trades = append(trades, order)
}
}
collect.SortAsc(trades, func(trd *trade.TradeOrder) int64 { return trd.TradeId })
// 年化每笔收益方法:
// 对每笔平仓交易,先计算该笔收益相对于初始资金的返回率(profit / initialCash),
// 再根据该笔持仓时长(以毫秒为单位,使用 t.Ctime - t.EntryTime)将收益年化:
// annualizedReturn = return * (secsYear / dtSec)
// 然后计算超额收益 = annualizedReturn - rfAnnual
// 最后 Sharpe = mean(excess) / stddev(excess)
const secsYear = 365.0 * 24.0 * 3600.0
excess := make([]float64, 0, n)
for _, t := range trades {
ret := t.Profit / a.account.initialCash
// holding time in seconds
dtSec := float64(t.Ctime-t.EntryTime) / 1000.0
if dtSec <= 0 {
dtSec = 1.0
}
annualized := ret * (secsYear / dtSec)
excess = append(excess, annualized-rfAnnual)
}
if len(excess) <= 1 {
return 0
}
meanEx := ta.Avg(excess)
sd := stddev(excess)
if sd == 0 {
return 0
}
return meanEx / sd
}
// sharpeFromEquitySnapshots computes Sharpe based on equity time series snapshots.
// Method:
// - compute simple returns between consecutive snapshots: r_t = eq_t / eq_{t-1} - 1
// - compute average snapshot interval and derive periodsPerYear = secsYear / avgDt
// - rf per period = rfAnnual / periodsPerYear
// - excess = r_t - rf_per_period
// - Sharpe = mean(excess)/std(excess) * sqrt(periodsPerYear)
func sharpeFromEquitySnapshots(snapshots []*EquitySnapshot, rfAnnual float64) float64 {
if len(snapshots) < 2 {
return 0
}
// ensure sorted by timestamp
sort.Slice(snapshots, func(i, j int) bool { return snapshots[i].Ts < snapshots[j].Ts })
const secsYear = 365.0 * 24.0 * 3600.0
var returns []float64
var dts []float64
for i := 1; i < len(snapshots); i++ {
prev := snapshots[i-1].Equity
cur := snapshots[i].Equity
if prev <= 0 {
continue
}
returns = append(returns, cur/prev-1)
dt := float64(snapshots[i].Ts-snapshots[i-1].Ts) / 1000.0
if dt <= 0 {
dt = 1.0
}
dts = append(dts, dt)
}
if len(returns) <= 1 {
return 0
}
sum := 0.0
for _, d := range dts {
sum += d
}
avgDt := sum / float64(len(dts))
periodsPerYear := secsYear / avgDt
rfPeriod := rfAnnual / periodsPerYear
excess := make([]float64, len(returns))
for i := range returns {
excess[i] = returns[i] - rfPeriod
}
meanEx := ta.Avg(excess)
sd := stddev(excess)
if sd == 0 {
return 0
}
return meanEx / sd * math.Sqrt(periodsPerYear)
}
func stddev(x []float64) float64 {
if len(x) <= 1 {
return 0
}
m := ta.Avg(x)
s := 0.0
for _, v := range x {
d := v - m
s += d * d
}
// population or sample? use sample (n-1)
return math.Sqrt(s / float64(len(x)-1))
}