first version
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
package backtest
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
TotalReturn float64 `json:"total_return"`
|
||||
CAGR float64 `json:"cagr"`
|
||||
AnnualizedVolatility float64 `json:"annualized_volatility"`
|
||||
SharpeRatio float64 `json:"sharpe_ratio"`
|
||||
SortinoRatio float64 `json:"sortino_ratio"`
|
||||
MaxDrawdown float64 `json:"max_drawdown"`
|
||||
CalmarRatio float64 `json:"calmar_ratio"`
|
||||
WinRate float64 `json:"win_rate"`
|
||||
AverageTradeReturn float64 `json:"average_trade_return"`
|
||||
MedianTradeReturn float64 `json:"median_trade_return"`
|
||||
ProfitFactor float64 `json:"profit_factor"`
|
||||
AverageSpreadBps float64 `json:"average_spread_bps"`
|
||||
AverageSlippageBps float64 `json:"average_slippage_bps"`
|
||||
NumberOfTrades int `json:"number_of_trades"`
|
||||
WorstOvernightGap float64 `json:"worst_overnight_gap"`
|
||||
VaR95 float64 `json:"var_95"`
|
||||
CVaR95 float64 `json:"cvar_95"`
|
||||
CapacityEstimate float64 `json:"capacity_estimate"`
|
||||
}
|
||||
|
||||
func ComputeMetrics(points []Point, trades []Trade) Metrics {
|
||||
if len(points) == 0 {
|
||||
return Metrics{}
|
||||
}
|
||||
start, _ := points[0].Equity.Float64()
|
||||
end, _ := points[len(points)-1].Equity.Float64()
|
||||
returns := make([]float64, 0, len(points)-1)
|
||||
for _, point := range points[1:] {
|
||||
r, _ := point.Return.Float64()
|
||||
returns = append(returns, r)
|
||||
}
|
||||
tradeReturns := make([]float64, 0, len(trades))
|
||||
spreads := make([]float64, 0, len(trades))
|
||||
slippages := make([]float64, 0, len(trades))
|
||||
profits := 0.0
|
||||
losses := 0.0
|
||||
wins := 0
|
||||
worstGap := 0.0
|
||||
capacity := 0.0
|
||||
for _, trade := range trades {
|
||||
r, _ := trade.Return.Float64()
|
||||
tradeReturns = append(tradeReturns, r)
|
||||
spread, _ := trade.SpreadBps.Float64()
|
||||
spreads = append(spreads, spread)
|
||||
slippage, _ := trade.SlippageBps.Float64()
|
||||
slippages = append(slippages, slippage)
|
||||
if r > 0 {
|
||||
wins++
|
||||
profits += r
|
||||
} else {
|
||||
losses += r
|
||||
}
|
||||
gap, _ := trade.OvernightGap.Float64()
|
||||
if gap < worstGap {
|
||||
worstGap = gap
|
||||
}
|
||||
tradeCapacity, _ := trade.CapacityRUB.Float64()
|
||||
if tradeCapacity > 0 && (capacity == 0 || tradeCapacity < capacity) {
|
||||
capacity = tradeCapacity
|
||||
}
|
||||
}
|
||||
totalReturn := 0.0
|
||||
if start > 0 {
|
||||
totalReturn = end/start - 1
|
||||
}
|
||||
vol := stddev(returns) * math.Sqrt(252)
|
||||
meanReturn := mean(returns)
|
||||
sharpe := 0.0
|
||||
if std := stddev(returns); std > 0 {
|
||||
sharpe = meanReturn / std * math.Sqrt(252)
|
||||
}
|
||||
sortino := 0.0
|
||||
if down := downsideStddev(returns); down > 0 {
|
||||
sortino = meanReturn / down * math.Sqrt(252)
|
||||
}
|
||||
tradingDays := math.Max(float64(len(returns)), 1)
|
||||
cagr := 0.0
|
||||
if start > 0 && end > 0 {
|
||||
cagr = math.Pow(end/start, 252/tradingDays) - 1
|
||||
}
|
||||
maxDD := maxDrawdown(points)
|
||||
calmar := 0.0
|
||||
if maxDD != 0 {
|
||||
calmar = cagr / math.Abs(maxDD)
|
||||
}
|
||||
pf := 0.0
|
||||
if losses != 0 {
|
||||
pf = profits / math.Abs(losses)
|
||||
}
|
||||
var95 := percentile(returns, 0.05)
|
||||
cvar95 := conditionalMean(returns, var95)
|
||||
return Metrics{
|
||||
TotalReturn: totalReturn,
|
||||
CAGR: cagr,
|
||||
AnnualizedVolatility: vol,
|
||||
SharpeRatio: sharpe,
|
||||
SortinoRatio: sortino,
|
||||
MaxDrawdown: maxDD,
|
||||
CalmarRatio: calmar,
|
||||
WinRate: ratio(wins, len(tradeReturns)),
|
||||
AverageTradeReturn: mean(tradeReturns),
|
||||
MedianTradeReturn: percentile(tradeReturns, 0.50),
|
||||
ProfitFactor: pf,
|
||||
AverageSpreadBps: mean(spreads),
|
||||
AverageSlippageBps: mean(slippages),
|
||||
NumberOfTrades: len(trades),
|
||||
WorstOvernightGap: worstGap,
|
||||
VaR95: var95,
|
||||
CVaR95: cvar95,
|
||||
CapacityEstimate: capacity,
|
||||
}
|
||||
}
|
||||
|
||||
func maxDrawdown(points []Point) float64 {
|
||||
peak := 0.0
|
||||
maxDD := 0.0
|
||||
for _, point := range points {
|
||||
e, _ := point.Equity.Float64()
|
||||
if e > peak {
|
||||
peak = e
|
||||
}
|
||||
if peak > 0 {
|
||||
dd := e/peak - 1
|
||||
if dd < maxDD {
|
||||
maxDD = dd
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxDD
|
||||
}
|
||||
|
||||
func mean(values []float64) float64 {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
sum := 0.0
|
||||
for _, value := range values {
|
||||
sum += value
|
||||
}
|
||||
return sum / float64(len(values))
|
||||
}
|
||||
|
||||
func stddev(values []float64) float64 {
|
||||
if len(values) < 2 {
|
||||
return 0
|
||||
}
|
||||
m := mean(values)
|
||||
sum := 0.0
|
||||
for _, value := range values {
|
||||
diff := value - m
|
||||
sum += diff * diff
|
||||
}
|
||||
return math.Sqrt(sum / float64(len(values)-1))
|
||||
}
|
||||
|
||||
func downsideStddev(values []float64) float64 {
|
||||
var downs []float64
|
||||
for _, value := range values {
|
||||
if value < 0 {
|
||||
downs = append(downs, value)
|
||||
}
|
||||
}
|
||||
return stddev(downs)
|
||||
}
|
||||
|
||||
func percentile(values []float64, q float64) float64 {
|
||||
if len(values) == 0 {
|
||||
return 0
|
||||
}
|
||||
cp := append([]float64(nil), values...)
|
||||
sort.Float64s(cp)
|
||||
pos := q * float64(len(cp)-1)
|
||||
lower := int(math.Floor(pos))
|
||||
upper := int(math.Ceil(pos))
|
||||
if lower == upper {
|
||||
return cp[lower]
|
||||
}
|
||||
weight := pos - float64(lower)
|
||||
return cp[lower]*(1-weight) + cp[upper]*weight
|
||||
}
|
||||
|
||||
func conditionalMean(values []float64, threshold float64) float64 {
|
||||
var selected []float64
|
||||
for _, value := range values {
|
||||
if value <= threshold {
|
||||
selected = append(selected, value)
|
||||
}
|
||||
}
|
||||
return mean(selected)
|
||||
}
|
||||
|
||||
func ratio(n, d int) float64 {
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(n) / float64(d)
|
||||
}
|
||||
|
||||
func point(date string, equity, ret string) Point {
|
||||
e, _ := decimal.NewFromString(equity)
|
||||
r, _ := decimal.NewFromString(ret)
|
||||
return Point{Date: date, Equity: e, Return: r}
|
||||
}
|
||||
Reference in New Issue
Block a user