Skip to content

Commit e3cd09d

Browse files
go-docklyc9s
authored andcommitted
add more trade stats
1 parent 95fd69d commit e3cd09d

File tree

6 files changed

+410
-76
lines changed

6 files changed

+410
-76
lines changed

pkg/backtest/report.go

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,21 +67,49 @@ func ReadSummaryReport(filename string) (*SummaryReport, error) {
6767
// SessionSymbolReport is the report per exchange session
6868
// trades are merged, collected and re-calculated
6969
type SessionSymbolReport struct {
70-
Exchange types.ExchangeName `json:"exchange"`
71-
Symbol string `json:"symbol,omitempty"`
72-
Intervals []types.Interval `json:"intervals,omitempty"`
73-
Subscriptions []types.Subscription `json:"subscriptions"`
74-
Market types.Market `json:"market"`
75-
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
76-
StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
77-
PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"`
78-
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
79-
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
80-
Manifests Manifests `json:"manifests,omitempty"`
81-
Sharpe fixedpoint.Value `json:"sharpeRatio"`
82-
Sortino fixedpoint.Value `json:"sortinoRatio"`
83-
ProfitFactor fixedpoint.Value `json:"profitFactor"`
84-
WinningRatio fixedpoint.Value `json:"winningRatio"`
70+
Exchange types.ExchangeName `json:"exchange"`
71+
Symbol string `json:"symbol,omitempty"`
72+
Intervals []types.Interval `json:"intervals,omitempty"`
73+
Subscriptions []types.Subscription `json:"subscriptions"`
74+
Market types.Market `json:"market"`
75+
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
76+
StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
77+
PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"`
78+
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
79+
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
80+
Manifests Manifests `json:"manifests,omitempty"`
81+
TradeCount fixedpoint.Value `json:"tradeCount,omitempty"`
82+
RoundTurnCount fixedpoint.Value `json:"roundTurnCount,omitempty"`
83+
TotalNetProfit fixedpoint.Value `json:"totalNetProfit,omitempty"`
84+
AvgNetProfit fixedpoint.Value `json:"avgNetProfit,omitempty"`
85+
GrossProfit fixedpoint.Value `json:"grossProfit,omitempty"`
86+
GrossLoss fixedpoint.Value `json:"grossLoss,omitempty"`
87+
PRR fixedpoint.Value `json:"prr,omitempty"`
88+
PercentProfitable fixedpoint.Value `json:"percentProfitable,omitempty"`
89+
MaxDrawdown fixedpoint.Value `json:"maxDrawdown,omitempty"`
90+
AverageDrawdown fixedpoint.Value `json:"avgDrawdown,omitempty"`
91+
MaxProfit fixedpoint.Value `json:"maxProfit,omitempty"`
92+
MaxLoss fixedpoint.Value `json:"maxLoss,omitempty"`
93+
AvgProfit fixedpoint.Value `json:"avgProfit,omitempty"`
94+
AvgLoss fixedpoint.Value `json:"avgLoss,omitempty"`
95+
TotalTimeInMarketSec int64 `json:"totalTimeInMarketSec,omitempty"`
96+
AvgHoldSec int64 `json:"avgHoldSec,omitempty"`
97+
WinningCount int `json:"winningCount,omitempty"`
98+
LosingCount int `json:"losingCount,omitempty"`
99+
MaxLossStreak int `json:"maxLossStreak,omitempty"`
100+
Sharpe fixedpoint.Value `json:"sharpeRatio"`
101+
AnnualHistoricVolatility fixedpoint.Value `json:"annualHistoricVolatility,omitempty"`
102+
CAGR fixedpoint.Value `json:"cagr,omitempty"`
103+
Calmar fixedpoint.Value `json:"calmar,omitempty"`
104+
Sterling fixedpoint.Value `json:"sterling,omitempty"`
105+
Burke fixedpoint.Value `json:"burke,omitempty"`
106+
Kelly fixedpoint.Value `json:"kelly,omitempty"`
107+
OptimalF fixedpoint.Value `json:"optimalF,omitempty"`
108+
StatN fixedpoint.Value `json:"statN,omitempty"`
109+
StdErr fixedpoint.Value `json:"statNStdErr,omitempty"`
110+
Sortino fixedpoint.Value `json:"sortinoRatio"`
111+
ProfitFactor fixedpoint.Value `json:"profitFactor"`
112+
WinningRatio fixedpoint.Value `json:"winningRatio"`
85113
}
86114

87115
func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value {

pkg/cmd/backtest.go

Lines changed: 99 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@ import (
1212

1313
"github.com/fatih/color"
1414
"github.com/google/uuid"
15-
16-
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
17-
"github.com/c9s/bbgo/pkg/core"
18-
"github.com/c9s/bbgo/pkg/data/tsv"
19-
"github.com/c9s/bbgo/pkg/util"
20-
2115
"github.com/pkg/errors"
2216
log "github.com/sirupsen/logrus"
2317
"github.com/spf13/cobra"
@@ -26,10 +20,14 @@ import (
2620
"github.com/c9s/bbgo/pkg/accounting/pnl"
2721
"github.com/c9s/bbgo/pkg/backtest"
2822
"github.com/c9s/bbgo/pkg/bbgo"
23+
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
24+
"github.com/c9s/bbgo/pkg/core"
25+
"github.com/c9s/bbgo/pkg/data/tsv"
2926
"github.com/c9s/bbgo/pkg/exchange"
3027
"github.com/c9s/bbgo/pkg/fixedpoint"
3128
"github.com/c9s/bbgo/pkg/service"
3229
"github.com/c9s/bbgo/pkg/types"
30+
"github.com/c9s/bbgo/pkg/util"
3331
)
3432

3533
func init() {
@@ -550,12 +548,11 @@ var BacktestCmd = &cobra.Command{
550548
continue
551549
}
552550

553-
tradeState := sessionTradeStats[session.Name][symbol]
554-
profitFactor := tradeState.ProfitFactor
555-
winningRatio := tradeState.WinningRatio
556-
intervalProfits := tradeState.IntervalProfits[types.Interval1d]
557-
558-
symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), intervalProfits, profitFactor, winningRatio)
551+
// profitFactor := tradeState.ProfitFactor
552+
// winningRatio := tradeState.WinningRatio
553+
// intervalProfits := tradeState.IntervalProfits[types.Interval1d]
554+
tradeStats := sessionTradeStats[session.Name][symbol]
555+
symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), tradeStats)
559556
if err != nil {
560557
return err
561558
}
@@ -566,8 +563,8 @@ var BacktestCmd = &cobra.Command{
566563
summaryReport.TotalUnrealizedProfit = symbolReport.PnL.UnrealizedProfit
567564
summaryReport.InitialEquityValue = summaryReport.InitialEquityValue.Add(symbolReport.InitialEquityValue())
568565
summaryReport.FinalEquityValue = summaryReport.FinalEquityValue.Add(symbolReport.FinalEquityValue())
569-
summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit)
570-
summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss)
566+
summaryReport.TotalGrossProfit = summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit)
567+
summaryReport.TotalGrossLoss = summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss)
571568

572569
// write report to a file
573570
if generatingReport {
@@ -620,14 +617,21 @@ var BacktestCmd = &cobra.Command{
620617
},
621618
}
622619

620+
/*
623621
func createSymbolReport(
622+
624623
userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade,
625624
intervalProfit *types.IntervalProfitCollector,
626625
profitFactor, winningRatio fixedpoint.Value,
626+
627627
) (
628-
*backtest.SessionSymbolReport,
629-
error,
630-
) {
628+
*/
629+
func createSymbolReport(
630+
userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade,
631+
tradeStats *types.TradeStats,
632+
) (*backtest.SessionSymbolReport, error) {
633+
intervalProfit := tradeStats.IntervalProfits[types.Interval1d]
634+
631635
backtestExchange, ok := session.Exchange.(*backtest.Exchange)
632636
if !ok {
633637
return nil, fmt.Errorf("unexpected error, exchange instance is not a backtest exchange")
@@ -637,6 +641,11 @@ func createSymbolReport(
637641
if !ok {
638642
return nil, fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name())
639643
}
644+
tStart, tEnd := trades[0].Time, trades[len(trades)-1].Time
645+
646+
periodStart := tStart.Time()
647+
periodEnd := tEnd.Time()
648+
period := periodEnd.Sub(periodStart)
640649

641650
startPrice, ok := session.StartPrice(symbol)
642651
if !ok {
@@ -653,29 +662,81 @@ func createSymbolReport(
653662
Market: market,
654663
}
655664

656-
sharpeRatio := fixedpoint.NewFromFloat(intervalProfit.GetSharpe())
657-
sortinoRatio := fixedpoint.NewFromFloat(intervalProfit.GetSortino())
658-
659665
report := calculator.Calculate(symbol, trades, lastPrice)
660666
accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String())
661667
initBalances := accountConfig.Balances.BalanceMap()
662668
finalBalances := session.GetAccount().Balances()
669+
maxProfit := n(intervalProfit.Profits.Max())
670+
maxLoss := n(intervalProfit.Profits.Min())
671+
drawdown := types.Drawdown(intervalProfit.Profits)
672+
maxDrawdown := drawdown.Max()
673+
avgDrawdown := drawdown.Average()
674+
roundTurnCount := n(float64(tradeStats.NumOfProfitTrade + tradeStats.NumOfLossTrade))
675+
roundTurnLength := n(float64(intervalProfit.Profits.Length()))
676+
winningCount := n(float64(tradeStats.NumOfProfitTrade))
677+
loosingCount := n(float64(tradeStats.NumOfLossTrade))
678+
avgProfit := tradeStats.GrossProfit.Div(n(types.NNZ(float64(tradeStats.NumOfProfitTrade), 1)))
679+
avgLoss := tradeStats.GrossLoss.Div(n(types.NNZ(float64(tradeStats.NumOfLossTrade), 1)))
680+
681+
winningPct := winningCount.Div(roundTurnCount)
682+
// losingPct := fixedpoint.One.Sub(winningPct)
683+
684+
sharpeRatio := n(intervalProfit.GetSharpe())
685+
sortinoRatio := n(intervalProfit.GetSortino())
686+
annVolHis := n(types.AnnualHistoricVolatility(intervalProfit.Profits))
687+
totalTimeInMarketSec, avgHoldSec := intervalProfit.GetTimeInMarket()
688+
statn, stdErr := types.StatN(intervalProfit.Profits)
663689
symbolReport := backtest.SessionSymbolReport{
664-
Exchange: session.Exchange.Name(),
665-
Symbol: symbol,
666-
Market: market,
667-
LastPrice: lastPrice,
668-
StartPrice: startPrice,
669-
PnL: report,
670-
InitialBalances: initBalances,
671-
FinalBalances: finalBalances,
672-
// Manifests: manifests,
673-
Sharpe: sharpeRatio,
674-
Sortino: sortinoRatio,
675-
ProfitFactor: profitFactor,
676-
WinningRatio: winningRatio,
690+
Exchange: session.Exchange.Name(),
691+
Symbol: symbol,
692+
Market: market,
693+
LastPrice: lastPrice,
694+
StartPrice: startPrice,
695+
InitialBalances: initBalances,
696+
FinalBalances: finalBalances,
697+
TradeCount: fixedpoint.NewFromInt(int64(len(trades))),
698+
GrossLoss: tradeStats.GrossLoss,
699+
GrossProfit: tradeStats.GrossProfit,
700+
WinningCount: tradeStats.NumOfProfitTrade,
701+
LosingCount: tradeStats.NumOfLossTrade,
702+
RoundTurnCount: roundTurnCount,
703+
WinningRatio: tradeStats.WinningRatio,
704+
PercentProfitable: winningPct,
705+
ProfitFactor: tradeStats.ProfitFactor,
706+
MaxDrawdown: n(maxDrawdown),
707+
AverageDrawdown: n(avgDrawdown),
708+
MaxProfit: maxProfit,
709+
MaxLoss: maxLoss,
710+
MaxLossStreak: tradeStats.MaximumConsecutiveLosses,
711+
TotalTimeInMarketSec: totalTimeInMarketSec,
712+
AvgHoldSec: avgHoldSec,
713+
AvgProfit: avgProfit,
714+
AvgLoss: avgLoss,
715+
AvgNetProfit: tradeStats.TotalNetProfit.Div(roundTurnLength),
716+
TotalNetProfit: tradeStats.TotalNetProfit,
717+
AnnualHistoricVolatility: annVolHis,
718+
PnL: report,
719+
PRR: types.PRR(tradeStats.GrossProfit, tradeStats.GrossLoss, winningCount, loosingCount),
720+
Kelly: types.KellyCriterion(tradeStats.ProfitFactor, winningPct),
721+
OptimalF: types.OptimalF(intervalProfit.Profits),
722+
StatN: statn,
723+
StdErr: stdErr,
724+
Sharpe: sharpeRatio,
725+
Sortino: sortinoRatio,
677726
}
678727

728+
cagr := types.NN(
729+
types.CAGR(
730+
symbolReport.InitialEquityValue().Float64(),
731+
symbolReport.FinalEquityValue().Float64(),
732+
int(period.Hours())/24,
733+
), 0)
734+
735+
symbolReport.CAGR = n(cagr)
736+
symbolReport.Calmar = n(types.CalmarRatio(cagr, maxDrawdown))
737+
symbolReport.Sterling = n(types.SterlingRatio(cagr, avgDrawdown))
738+
symbolReport.Burke = n(types.BurkeRatio(cagr, drawdown.AverageSquared()))
739+
679740
for _, s := range session.Subscriptions {
680741
symbolReport.Subscriptions = append(symbolReport.Subscriptions, s)
681742
}
@@ -694,6 +755,10 @@ func createSymbolReport(
694755
return &symbolReport, nil
695756
}
696757

758+
func n(v float64) fixedpoint.Value {
759+
return fixedpoint.NewFromFloat(v)
760+
}
761+
697762
func verify(
698763
userConfig *bbgo.Config, backtestService *service.BacktestService,
699764
sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time,
@@ -704,6 +769,7 @@ func verify(
704769
return err
705770
}
706771
}
772+
707773
return nil
708774
}
709775

pkg/datatype/floats/slice.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,18 @@ func (s Slice) Average() float64 {
194194
return total / float64(len(s))
195195
}
196196

197+
func (s Slice) AverageSquared() float64 {
198+
if len(s) == 0 {
199+
return 0.0
200+
}
201+
202+
total := 0.0
203+
for _, value := range s {
204+
total += math.Pow(value, 2)
205+
}
206+
return total / float64(len(s))
207+
}
208+
197209
func (s Slice) Diff() (values Slice) {
198210
for i, v := range s {
199211
if i == 0 {

0 commit comments

Comments
 (0)