mirror of
https://github.com/c9s/bbgo.git
synced 2024-09-20 08:11:08 +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"`
|
GrossLoss fixedpoint.Value `json:"grossLoss,omitempty"`
|
||||||
PRR fixedpoint.Value `json:"prr,omitempty"`
|
PRR fixedpoint.Value `json:"prr,omitempty"`
|
||||||
PercentProfitable fixedpoint.Value `json:"percentProfitable,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"`
|
MaxProfit fixedpoint.Value `json:"maxProfit,omitempty"`
|
||||||
MaxLoss fixedpoint.Value `json:"maxLoss,omitempty"`
|
MaxLoss fixedpoint.Value `json:"maxLoss,omitempty"`
|
||||||
AvgProfit fixedpoint.Value `json:"avgProfit,omitempty"`
|
AvgProfit fixedpoint.Value `json:"avgProfit,omitempty"`
|
||||||
|
@ -100,10 +102,12 @@ type SessionSymbolReport struct {
|
||||||
AnnualHistoricVolatility fixedpoint.Value `json:"annualHistoricVolatility,omitempty"`
|
AnnualHistoricVolatility fixedpoint.Value `json:"annualHistoricVolatility,omitempty"`
|
||||||
CAGR fixedpoint.Value `json:"cagr,omitempty"`
|
CAGR fixedpoint.Value `json:"cagr,omitempty"`
|
||||||
Calmar fixedpoint.Value `json:"calmar,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"`
|
Kelly fixedpoint.Value `json:"kelly,omitempty"`
|
||||||
OptimalF fixedpoint.Value `json:"optimalF,omitempty"`
|
OptimalF fixedpoint.Value `json:"optimalF,omitempty"`
|
||||||
StatN fixedpoint.Value `json:"statN,omitempty"`
|
StatN fixedpoint.Value `json:"statN,omitempty"`
|
||||||
StatNStdErr fixedpoint.Value `json:"statNStdErr,omitempty"`
|
StdErr fixedpoint.Value `json:"statNStdErr,omitempty"`
|
||||||
Sortino fixedpoint.Value `json:"sortinoRatio"`
|
Sortino fixedpoint.Value `json:"sortinoRatio"`
|
||||||
ProfitFactor fixedpoint.Value `json:"profitFactor"`
|
ProfitFactor fixedpoint.Value `json:"profitFactor"`
|
||||||
WinningRatio fixedpoint.Value `json:"winningRatio"`
|
WinningRatio fixedpoint.Value `json:"winningRatio"`
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -635,7 +634,10 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
||||||
initBalances := accountConfig.Balances.BalanceMap()
|
initBalances := accountConfig.Balances.BalanceMap()
|
||||||
finalBalances := session.GetAccount().Balances()
|
finalBalances := session.GetAccount().Balances()
|
||||||
maxProfit := n(intervalProfit.Profits.Max())
|
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))
|
roundTurnCount := n(float64(tradeStats.NumOfProfitTrade + tradeStats.NumOfLossTrade))
|
||||||
roundTurnLength := n(float64(intervalProfit.Profits.Length()))
|
roundTurnLength := n(float64(intervalProfit.Profits.Length()))
|
||||||
winningCount := n(float64(tradeStats.NumOfProfitTrade))
|
winningCount := n(float64(tradeStats.NumOfProfitTrade))
|
||||||
|
@ -650,7 +652,7 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
||||||
sortinoRatio := n(intervalProfit.GetSortino())
|
sortinoRatio := n(intervalProfit.GetSortino())
|
||||||
annVolHis := n(types.AnnualHistoricVolatility(intervalProfit.Profits))
|
annVolHis := n(types.AnnualHistoricVolatility(intervalProfit.Profits))
|
||||||
totalTimeInMarketSec, avgHoldSec := intervalProfit.GetTimeInMarket()
|
totalTimeInMarketSec, avgHoldSec := intervalProfit.GetTimeInMarket()
|
||||||
statn, stdErr := types.StatN(*intervalProfit.Profits)
|
statn, stdErr := types.StatN(intervalProfit.Profits)
|
||||||
symbolReport := backtest.SessionSymbolReport{
|
symbolReport := backtest.SessionSymbolReport{
|
||||||
Exchange: session.Exchange.Name(),
|
Exchange: session.Exchange.Name(),
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
|
@ -668,6 +670,8 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
||||||
WinningRatio: tradeStats.WinningRatio,
|
WinningRatio: tradeStats.WinningRatio,
|
||||||
PercentProfitable: winningPct,
|
PercentProfitable: winningPct,
|
||||||
ProfitFactor: tradeStats.ProfitFactor,
|
ProfitFactor: tradeStats.ProfitFactor,
|
||||||
|
MaxDrawdown: n(maxDrawdown),
|
||||||
|
AverageDrawdown: n(avgDrawdown),
|
||||||
MaxProfit: maxProfit,
|
MaxProfit: maxProfit,
|
||||||
MaxLoss: maxLoss,
|
MaxLoss: maxLoss,
|
||||||
MaxLossStreak: tradeStats.MaximumConsecutiveLosses,
|
MaxLossStreak: tradeStats.MaximumConsecutiveLosses,
|
||||||
|
@ -681,9 +685,9 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
||||||
PnL: report,
|
PnL: report,
|
||||||
PRR: types.PRR(tradeStats.GrossProfit, tradeStats.GrossLoss, winningCount, loosingCount),
|
PRR: types.PRR(tradeStats.GrossProfit, tradeStats.GrossLoss, winningCount, loosingCount),
|
||||||
Kelly: types.KellyCriterion(tradeStats.ProfitFactor, winningPct),
|
Kelly: types.KellyCriterion(tradeStats.ProfitFactor, winningPct),
|
||||||
OptimalF: types.OptimalF(*intervalProfit.Profits),
|
OptimalF: types.OptimalF(intervalProfit.Profits),
|
||||||
StatN: statn,
|
StatN: statn,
|
||||||
StatNStdErr: stdErr,
|
StdErr: stdErr,
|
||||||
Sharpe: sharpeRatio,
|
Sharpe: sharpeRatio,
|
||||||
Sortino: sortinoRatio,
|
Sortino: sortinoRatio,
|
||||||
}
|
}
|
||||||
|
@ -696,7 +700,9 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
||||||
), 0)
|
), 0)
|
||||||
|
|
||||||
symbolReport.CAGR = n(cagr)
|
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 {
|
for _, s := range session.Subscriptions {
|
||||||
symbolReport.Subscriptions = append(symbolReport.Subscriptions, s)
|
symbolReport.Subscriptions = append(symbolReport.Subscriptions, s)
|
||||||
|
@ -719,6 +725,7 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
||||||
func n(v float64) fixedpoint.Value {
|
func n(v float64) fixedpoint.Value {
|
||||||
return fixedpoint.NewFromFloat(v)
|
return fixedpoint.NewFromFloat(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func verify(userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time) error {
|
func verify(userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time) error {
|
||||||
for _, sourceExchange := range sourceExchanges {
|
for _, sourceExchange := range sourceExchanges {
|
||||||
err := backtestService.Verify(sourceExchange, userConfig.Backtest.Symbols, startTime, endTime)
|
err := backtestService.Verify(sourceExchange, userConfig.Backtest.Symbols, startTime, endTime)
|
||||||
|
|
|
@ -112,6 +112,18 @@ func (s Slice) Average() float64 {
|
||||||
return total / float64(len(s))
|
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) {
|
func (s Slice) Diff() (values Slice) {
|
||||||
for i, v := range s {
|
for i, v := range s {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
|
|
@ -31,11 +31,32 @@ func CAGR(initial, final float64, days int) float64 {
|
||||||
return math.Pow(x, y) - 1
|
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 {
|
func CalmarRatio(cagr, maxDrawdown float64) float64 {
|
||||||
return cagr / maxDrawdown
|
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.
|
// KellyCriterion the famous method for trade sizing.
|
||||||
func KellyCriterion(profitFactor, winP fixedpoint.Value) fixedpoint.Value {
|
func KellyCriterion(profitFactor, winP fixedpoint.Value) fixedpoint.Value {
|
||||||
return profitFactor.Mul(winP).Sub(fixedpoint.One.Sub(winP)).Div(profitFactor)
|
return profitFactor.Mul(winP).Sub(fixedpoint.One.Sub(winP)).Div(profitFactor)
|
||||||
|
@ -105,3 +126,26 @@ func NNZ(x, y float64) float64 {
|
||||||
}
|
}
|
||||||
return x
|
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)
|
f := OptimalF(roundturns)
|
||||||
assert.EqualValues(t, 0.45, f)
|
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?"
|
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 {
|
type ProfitReport struct {
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
Profit float64 `json:"profit"`
|
Profit float64 `json:"profit"`
|
||||||
|
@ -69,6 +33,41 @@ func (s ProfitReport) String() string {
|
||||||
return string(b)
|
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
|
// Determine average and total time spend in market
|
||||||
func (s *IntervalProfitCollector) GetTimeInMarket() (avgHoldSec, totalTimeInMarketSec int64) {
|
func (s *IntervalProfitCollector) GetTimeInMarket() (avgHoldSec, totalTimeInMarketSec int64) {
|
||||||
if s.Profits == nil {
|
if s.Profits == nil {
|
||||||
|
@ -116,7 +115,7 @@ func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) {
|
||||||
if s.Profits == nil {
|
if s.Profits == nil {
|
||||||
panic(ErrProfitArrEmpty)
|
panic(ErrProfitArrEmpty)
|
||||||
}
|
}
|
||||||
for _, v := range *s.Profits {
|
for _, v := range s.Profits {
|
||||||
if v > 1. {
|
if v > 1. {
|
||||||
profit += 1
|
profit += 1
|
||||||
}
|
}
|
||||||
|
@ -130,7 +129,7 @@ func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit in
|
||||||
if s.Profits == nil {
|
if s.Profits == nil {
|
||||||
panic(ErrProfitArrEmpty)
|
panic(ErrProfitArrEmpty)
|
||||||
}
|
}
|
||||||
for _, v := range *s.Profits {
|
for _, v := range s.Profits {
|
||||||
if v <= 1. {
|
if v <= 1. {
|
||||||
nonprofit += 1
|
nonprofit += 1
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user