mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
fix drawdown
This commit is contained in:
parent
0c2f4aef9d
commit
539e35d290
|
@ -87,6 +87,8 @@ type SessionSymbolReport struct {
|
|||
GrossLoss fixedpoint.Value `json:"grossLoss,omitempty"`
|
||||
PRR fixedpoint.Value `json:"prr,omitempty"`
|
||||
PercentProfitable fixedpoint.Value `json:"percentProfitable,omitempty"`
|
||||
MaxDrawdown fixedpoint.Value `json:"maxDrawdown,omitempty"`
|
||||
AverageDrawdown fixedpoint.Value `json:"avgDrawdown,omitempty"`
|
||||
MaxProfit fixedpoint.Value `json:"maxProfit,omitempty"`
|
||||
MaxLoss fixedpoint.Value `json:"maxLoss,omitempty"`
|
||||
AvgProfit fixedpoint.Value `json:"avgProfit,omitempty"`
|
||||
|
@ -100,10 +102,12 @@ type SessionSymbolReport struct {
|
|||
AnnualHistoricVolatility fixedpoint.Value `json:"annualHistoricVolatility,omitempty"`
|
||||
CAGR fixedpoint.Value `json:"cagr,omitempty"`
|
||||
Calmar fixedpoint.Value `json:"calmar,omitempty"`
|
||||
Sterling fixedpoint.Value `json:"sterling,omitempty"`
|
||||
Burke fixedpoint.Value `json:"burke,omitempty"`
|
||||
Kelly fixedpoint.Value `json:"kelly,omitempty"`
|
||||
OptimalF fixedpoint.Value `json:"optimalF,omitempty"`
|
||||
StatN fixedpoint.Value `json:"statN,omitempty"`
|
||||
StatNStdErr fixedpoint.Value `json:"statNStdErr,omitempty"`
|
||||
StdErr fixedpoint.Value `json:"statNStdErr,omitempty"`
|
||||
Sortino fixedpoint.Value `json:"sortinoRatio"`
|
||||
ProfitFactor fixedpoint.Value `json:"profitFactor"`
|
||||
WinningRatio fixedpoint.Value `json:"winningRatio"`
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -635,7 +634,10 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
|||
initBalances := accountConfig.Balances.BalanceMap()
|
||||
finalBalances := session.GetAccount().Balances()
|
||||
maxProfit := n(intervalProfit.Profits.Max())
|
||||
maxLoss := n(math.Abs(intervalProfit.Profits.Min()))
|
||||
maxLoss := n(intervalProfit.Profits.Min())
|
||||
drawdown := types.Drawdown(intervalProfit.Profits)
|
||||
maxDrawdown := drawdown.Max()
|
||||
avgDrawdown := drawdown.Average()
|
||||
roundTurnCount := n(float64(tradeStats.NumOfProfitTrade + tradeStats.NumOfLossTrade))
|
||||
roundTurnLength := n(float64(intervalProfit.Profits.Length()))
|
||||
winningCount := n(float64(tradeStats.NumOfProfitTrade))
|
||||
|
@ -650,7 +652,7 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
|||
sortinoRatio := n(intervalProfit.GetSortino())
|
||||
annVolHis := n(types.AnnualHistoricVolatility(intervalProfit.Profits))
|
||||
totalTimeInMarketSec, avgHoldSec := intervalProfit.GetTimeInMarket()
|
||||
statn, stdErr := types.StatN(*intervalProfit.Profits)
|
||||
statn, stdErr := types.StatN(intervalProfit.Profits)
|
||||
symbolReport := backtest.SessionSymbolReport{
|
||||
Exchange: session.Exchange.Name(),
|
||||
Symbol: symbol,
|
||||
|
@ -668,6 +670,8 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
|||
WinningRatio: tradeStats.WinningRatio,
|
||||
PercentProfitable: winningPct,
|
||||
ProfitFactor: tradeStats.ProfitFactor,
|
||||
MaxDrawdown: n(maxDrawdown),
|
||||
AverageDrawdown: n(avgDrawdown),
|
||||
MaxProfit: maxProfit,
|
||||
MaxLoss: maxLoss,
|
||||
MaxLossStreak: tradeStats.MaximumConsecutiveLosses,
|
||||
|
@ -681,9 +685,9 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
|||
PnL: report,
|
||||
PRR: types.PRR(tradeStats.GrossProfit, tradeStats.GrossLoss, winningCount, loosingCount),
|
||||
Kelly: types.KellyCriterion(tradeStats.ProfitFactor, winningPct),
|
||||
OptimalF: types.OptimalF(*intervalProfit.Profits),
|
||||
OptimalF: types.OptimalF(intervalProfit.Profits),
|
||||
StatN: statn,
|
||||
StatNStdErr: stdErr,
|
||||
StdErr: stdErr,
|
||||
Sharpe: sharpeRatio,
|
||||
Sortino: sortinoRatio,
|
||||
}
|
||||
|
@ -696,7 +700,9 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
|||
), 0)
|
||||
|
||||
symbolReport.CAGR = n(cagr)
|
||||
symbolReport.Calmar = n(types.CalmarRatio(cagr, tradeStats.MaxDrawdown))
|
||||
symbolReport.Calmar = n(types.CalmarRatio(cagr, maxDrawdown))
|
||||
symbolReport.Sterling = n(types.SterlingRatio(cagr, avgDrawdown))
|
||||
symbolReport.Burke = n(types.BurkeRatio(cagr, drawdown.AverageSquared()))
|
||||
|
||||
for _, s := range session.Subscriptions {
|
||||
symbolReport.Subscriptions = append(symbolReport.Subscriptions, s)
|
||||
|
@ -719,6 +725,7 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
|||
func n(v float64) fixedpoint.Value {
|
||||
return fixedpoint.NewFromFloat(v)
|
||||
}
|
||||
|
||||
func verify(userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time) error {
|
||||
for _, sourceExchange := range sourceExchanges {
|
||||
err := backtestService.Verify(sourceExchange, userConfig.Backtest.Symbols, startTime, endTime)
|
||||
|
|
|
@ -112,6 +112,18 @@ func (s Slice) Average() float64 {
|
|||
return total / float64(len(s))
|
||||
}
|
||||
|
||||
func (s Slice) AverageSquared() float64 {
|
||||
if len(s) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
for _, value := range s {
|
||||
total += math.Pow(value, 2)
|
||||
}
|
||||
return total / float64(len(s))
|
||||
}
|
||||
|
||||
func (s Slice) Diff() (values Slice) {
|
||||
for i, v := range s {
|
||||
if i == 0 {
|
||||
|
|
|
@ -31,11 +31,32 @@ func CAGR(initial, final float64, days int) float64 {
|
|||
return math.Pow(x, y) - 1
|
||||
}
|
||||
|
||||
// CalmarRatio relates the capaital growth rate to the maximum drawdown.
|
||||
// measures of risk-adjusted return based on drawdown risk
|
||||
|
||||
// calmar ratio - discounts expected excess return of a portfolio by the
|
||||
// worst expected maximum draw down for that portfolio
|
||||
// CR = E(re)/MD1 = (E(r) - rf) / MD1
|
||||
func CalmarRatio(cagr, maxDrawdown float64) float64 {
|
||||
return cagr / maxDrawdown
|
||||
}
|
||||
|
||||
// Sterling ratio
|
||||
// discounts the expected excess return of a portfolio by the average of the N worst
|
||||
// expected maximum drawdowns for that portfolio
|
||||
// CR = E(re) / (1/N)(sum MDi)
|
||||
func SterlingRatio(cagr, avgDrawdown float64) float64 {
|
||||
return cagr / avgDrawdown
|
||||
}
|
||||
|
||||
// Burke Ratio
|
||||
// similar to sterling, but less sensitive to outliers
|
||||
// discounts the expected excess return of a portfolio by the square root of the average
|
||||
// of the N worst expected maximum drawdowns for that portfolio
|
||||
// BR = E(re) / ((1/N)(sum MD^2))^0.5 ---> smoothing, can take roots, logs etc
|
||||
func BurkeRatio(cagr, avgDrawdownSquared float64) float64 {
|
||||
return cagr / math.Sqrt(avgDrawdownSquared)
|
||||
}
|
||||
|
||||
// KellyCriterion the famous method for trade sizing.
|
||||
func KellyCriterion(profitFactor, winP fixedpoint.Value) fixedpoint.Value {
|
||||
return profitFactor.Mul(winP).Sub(fixedpoint.One.Sub(winP)).Div(profitFactor)
|
||||
|
@ -105,3 +126,26 @@ func NNZ(x, y float64) float64 {
|
|||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// Compute the drawdown function associated to a portfolio equity curve,
|
||||
// also called the portfolio underwater equity curve.
|
||||
// Portfolio Optimization with Drawdown Constraints, Chekhlov et al., 2000
|
||||
// http://papers.ssrn.com/sol3/papers.cfm?abstract_id=223323
|
||||
func Drawdown(equityCurve floats.Slice) floats.Slice {
|
||||
// Initialize highWaterMark
|
||||
highWaterMark := math.Inf(-1)
|
||||
|
||||
// Create ddVector with the same length as equityCurve
|
||||
ddVector := make([]float64, len(equityCurve))
|
||||
|
||||
// Loop over all the values to compute the drawdown vector
|
||||
for i := 0; i < len(equityCurve); i++ {
|
||||
if equityCurve[i] > highWaterMark {
|
||||
highWaterMark = equityCurve[i]
|
||||
}
|
||||
|
||||
ddVector[i] = (highWaterMark - equityCurve[i]) / highWaterMark
|
||||
}
|
||||
|
||||
return ddVector
|
||||
}
|
||||
|
|
|
@ -42,3 +42,15 @@ func TestOptimalF(t *testing.T) {
|
|||
f := OptimalF(roundturns)
|
||||
assert.EqualValues(t, 0.45, f)
|
||||
}
|
||||
|
||||
func TestDrawdown(t *testing.T) {
|
||||
roundturns := floats.Slice{100, 50, 100}
|
||||
expected := []float64{.0, .5, .0}
|
||||
drawdown := Drawdown(roundturns)
|
||||
assert.EqualValues(t, 0.5, drawdown.Max())
|
||||
assert.EqualValues(t, 0.16666666666666666, drawdown.Average())
|
||||
assert.EqualValues(t, 0.08333333333333333, drawdown.AverageSquared())
|
||||
for i, v := range expected {
|
||||
assert.EqualValues(t, v, drawdown[i])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,42 +19,6 @@ const (
|
|||
ErrProfitArrEmpty = "profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?"
|
||||
)
|
||||
|
||||
type IntervalProfitCollector struct {
|
||||
Interval Interval `json:"interval"`
|
||||
Profits *floats.Slice `json:"profits"`
|
||||
TimeInMarket []time.Duration `json:"timeInMarket"`
|
||||
Timestamp *floats.Slice `json:"timestamp"`
|
||||
tmpTime time.Time `json:"tmpTime"`
|
||||
}
|
||||
|
||||
func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector {
|
||||
return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: &floats.Slice{1.}, Timestamp: &floats.Slice{float64(startTime.Unix())}}
|
||||
}
|
||||
|
||||
// Update the collector by every traded profit
|
||||
func (s *IntervalProfitCollector) Update(profit *Profit) {
|
||||
s.TimeInMarket = append(s.TimeInMarket, profit.TradedAt.Sub(profit.PositionOpenedAt))
|
||||
|
||||
if s.tmpTime.IsZero() {
|
||||
panic(ErrStartTimeNotValid)
|
||||
} else {
|
||||
duration := s.Interval.Duration()
|
||||
if profit.TradedAt.Before(s.tmpTime.Add(duration)) {
|
||||
(*s.Profits)[len(*s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64()
|
||||
} else {
|
||||
for {
|
||||
s.Profits.Update(1.)
|
||||
s.tmpTime = s.tmpTime.Add(duration)
|
||||
s.Timestamp.Update(float64(s.tmpTime.Unix()))
|
||||
if profit.TradedAt.Before(s.tmpTime.Add(duration)) {
|
||||
(*s.Profits)[len(*s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ProfitReport struct {
|
||||
StartTime time.Time `json:"startTime"`
|
||||
Profit float64 `json:"profit"`
|
||||
|
@ -69,6 +33,41 @@ func (s ProfitReport) String() string {
|
|||
return string(b)
|
||||
}
|
||||
|
||||
type IntervalProfitCollector struct {
|
||||
Interval Interval `json:"interval"`
|
||||
Profits floats.Slice `json:"profits"`
|
||||
TimeInMarket []time.Duration `json:"timeInMarket"`
|
||||
Timestamp floats.Slice `json:"timestamp"`
|
||||
tmpTime time.Time `json:"tmpTime"`
|
||||
}
|
||||
|
||||
func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector {
|
||||
return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: floats.Slice{1.}, Timestamp: floats.Slice{float64(startTime.Unix())}}
|
||||
}
|
||||
|
||||
// Update the collector by every traded profit
|
||||
func (s *IntervalProfitCollector) Update(profit *Profit) {
|
||||
if s.tmpTime.IsZero() {
|
||||
panic(ErrStartTimeNotValid)
|
||||
} else {
|
||||
s.TimeInMarket = append(s.TimeInMarket, profit.TradedAt.Sub(profit.PositionOpenedAt))
|
||||
duration := s.Interval.Duration()
|
||||
if profit.TradedAt.Before(s.tmpTime.Add(duration)) {
|
||||
(s.Profits)[len(s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64()
|
||||
} else {
|
||||
for {
|
||||
s.Profits.Update(1.)
|
||||
s.tmpTime = s.tmpTime.Add(duration)
|
||||
s.Timestamp.Update(float64(s.tmpTime.Unix()))
|
||||
if profit.TradedAt.Before(s.tmpTime.Add(duration)) {
|
||||
(s.Profits)[len(s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine average and total time spend in market
|
||||
func (s *IntervalProfitCollector) GetTimeInMarket() (avgHoldSec, totalTimeInMarketSec int64) {
|
||||
if s.Profits == nil {
|
||||
|
@ -116,7 +115,7 @@ func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) {
|
|||
if s.Profits == nil {
|
||||
panic(ErrProfitArrEmpty)
|
||||
}
|
||||
for _, v := range *s.Profits {
|
||||
for _, v := range s.Profits {
|
||||
if v > 1. {
|
||||
profit += 1
|
||||
}
|
||||
|
@ -130,7 +129,7 @@ func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit in
|
|||
if s.Profits == nil {
|
||||
panic(ErrProfitArrEmpty)
|
||||
}
|
||||
for _, v := range *s.Profits {
|
||||
for _, v := range s.Profits {
|
||||
if v <= 1. {
|
||||
nonprofit += 1
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user