Skip to content

Commit

Permalink
add more trade stats
Browse files Browse the repository at this point in the history
  • Loading branch information
go-dockly authored and c9s committed Dec 14, 2023
1 parent 2c7e429 commit 2873394
Show file tree
Hide file tree
Showing 6 changed files with 396 additions and 66 deletions.
58 changes: 43 additions & 15 deletions pkg/backtest/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,49 @@ func ReadSummaryReport(filename string) (*SummaryReport, error) {
// SessionSymbolReport is the report per exchange session
// trades are merged, collected and re-calculated
type SessionSymbolReport struct {
Exchange types.ExchangeName `json:"exchange"`
Symbol string `json:"symbol,omitempty"`
Intervals []types.Interval `json:"intervals,omitempty"`
Subscriptions []types.Subscription `json:"subscriptions"`
Market types.Market `json:"market"`
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"`
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
Manifests Manifests `json:"manifests,omitempty"`
Sharpe fixedpoint.Value `json:"sharpeRatio"`
Sortino fixedpoint.Value `json:"sortinoRatio"`
ProfitFactor fixedpoint.Value `json:"profitFactor"`
WinningRatio fixedpoint.Value `json:"winningRatio"`
Exchange types.ExchangeName `json:"exchange"`
Symbol string `json:"symbol,omitempty"`
Intervals []types.Interval `json:"intervals,omitempty"`
Subscriptions []types.Subscription `json:"subscriptions"`
Market types.Market `json:"market"`
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"`
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
Manifests Manifests `json:"manifests,omitempty"`
TradeCount fixedpoint.Value `json:"tradeCount,omitempty"`
RoundTurnCount fixedpoint.Value `json:"roundTurnCount,omitempty"`
TotalNetProfit fixedpoint.Value `json:"totalNetProfit,omitempty"`
AvgNetProfit fixedpoint.Value `json:"avgNetProfit,omitempty"`
GrossProfit fixedpoint.Value `json:"grossProfit,omitempty"`
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"`
AvgLoss fixedpoint.Value `json:"avgLoss,omitempty"`
TotalTimeInMarketSec int64 `json:"totalTimeInMarketSec,omitempty"`
AvgHoldSec int64 `json:"avgHoldSec,omitempty"`
WinningCount int `json:"winningCount,omitempty"`
LosingCount int `json:"losingCount,omitempty"`
MaxLossStreak int `json:"maxLossStreak,omitempty"`
Sharpe fixedpoint.Value `json:"sharpeRatio"`
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"`
StdErr fixedpoint.Value `json:"statNStdErr,omitempty"`
Sortino fixedpoint.Value `json:"sortinoRatio"`
ProfitFactor fixedpoint.Value `json:"profitFactor"`
WinningRatio fixedpoint.Value `json:"winningRatio"`
}

func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value {
Expand Down
108 changes: 85 additions & 23 deletions pkg/cmd/backtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ import (

"github.com/fatih/color"
"github.com/google/uuid"

"github.com/c9s/bbgo/pkg/cmd/cmdutil"
"github.com/c9s/bbgo/pkg/core"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/util"

"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
Expand All @@ -26,10 +20,14 @@ import (
"github.com/c9s/bbgo/pkg/accounting/pnl"
"github.com/c9s/bbgo/pkg/backtest"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
"github.com/c9s/bbgo/pkg/core"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)

func init() {
Expand Down Expand Up @@ -528,6 +526,7 @@ var BacktestCmd = &cobra.Command{

for _, session := range environ.Sessions() {
for symbol, trades := range session.Trades {

if len(trades.Trades) == 0 {
log.Warnf("session has no %s trades", symbol)
continue
Expand All @@ -538,7 +537,7 @@ var BacktestCmd = &cobra.Command{
winningRatio := tradeState.WinningRatio
intervalProfits := tradeState.IntervalProfits[types.Interval1d]

symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), intervalProfits, profitFactor, winningRatio)
symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), tradeStats)
if err != nil {
return err
}
Expand Down Expand Up @@ -611,6 +610,8 @@ func createSymbolReport(
*backtest.SessionSymbolReport,
error,
) {
intervalProfit := tradeStats.IntervalProfits[types.Interval1d]

backtestExchange, ok := session.Exchange.(*backtest.Exchange)
if !ok {
return nil, fmt.Errorf("unexpected error, exchange instance is not a backtest exchange")
Expand All @@ -620,6 +621,11 @@ func createSymbolReport(
if !ok {
return nil, fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name())
}
tStart, tEnd := trades[0].Time, trades[len(trades)-1].Time

periodStart := tStart.Time()
periodEnd := tEnd.Time()
period := periodEnd.Sub(periodStart)

startPrice, ok := session.StartPrice(symbol)
if !ok {
Expand All @@ -636,29 +642,81 @@ func createSymbolReport(
Market: market,
}

sharpeRatio := fixedpoint.NewFromFloat(intervalProfit.GetSharpe())
sortinoRatio := fixedpoint.NewFromFloat(intervalProfit.GetSortino())

report := calculator.Calculate(symbol, trades, lastPrice)
accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String())
initBalances := accountConfig.Balances.BalanceMap()
finalBalances := session.GetAccount().Balances()
maxProfit := n(intervalProfit.Profits.Max())
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))
loosingCount := n(float64(tradeStats.NumOfLossTrade))
avgProfit := tradeStats.GrossProfit.Div(n(types.NNZ(float64(tradeStats.NumOfProfitTrade), 1)))
avgLoss := tradeStats.GrossLoss.Div(n(types.NNZ(float64(tradeStats.NumOfLossTrade), 1)))

winningPct := winningCount.Div(roundTurnCount)
// losingPct := fixedpoint.One.Sub(winningPct)

sharpeRatio := n(intervalProfit.GetSharpe())
sortinoRatio := n(intervalProfit.GetSortino())
annVolHis := n(types.AnnualHistoricVolatility(intervalProfit.Profits))
totalTimeInMarketSec, avgHoldSec := intervalProfit.GetTimeInMarket()
statn, stdErr := types.StatN(intervalProfit.Profits)
symbolReport := backtest.SessionSymbolReport{
Exchange: session.Exchange.Name(),
Symbol: symbol,
Market: market,
LastPrice: lastPrice,
StartPrice: startPrice,
PnL: report,
InitialBalances: initBalances,
FinalBalances: finalBalances,
// Manifests: manifests,
Sharpe: sharpeRatio,
Sortino: sortinoRatio,
ProfitFactor: profitFactor,
WinningRatio: winningRatio,
Exchange: session.Exchange.Name(),
Symbol: symbol,
Market: market,
LastPrice: lastPrice,
StartPrice: startPrice,
InitialBalances: initBalances,
FinalBalances: finalBalances,
TradeCount: fixedpoint.NewFromInt(int64(len(trades))),
GrossLoss: tradeStats.GrossLoss,
GrossProfit: tradeStats.GrossProfit,
WinningCount: tradeStats.NumOfProfitTrade,
LosingCount: tradeStats.NumOfLossTrade,
RoundTurnCount: roundTurnCount,
WinningRatio: tradeStats.WinningRatio,
PercentProfitable: winningPct,
ProfitFactor: tradeStats.ProfitFactor,
MaxDrawdown: n(maxDrawdown),
AverageDrawdown: n(avgDrawdown),
MaxProfit: maxProfit,
MaxLoss: maxLoss,
MaxLossStreak: tradeStats.MaximumConsecutiveLosses,
TotalTimeInMarketSec: totalTimeInMarketSec,
AvgHoldSec: avgHoldSec,
AvgProfit: avgProfit,
AvgLoss: avgLoss,
AvgNetProfit: tradeStats.TotalNetProfit.Div(roundTurnLength),
TotalNetProfit: tradeStats.TotalNetProfit,
AnnualHistoricVolatility: annVolHis,
PnL: report,
PRR: types.PRR(tradeStats.GrossProfit, tradeStats.GrossLoss, winningCount, loosingCount),
Kelly: types.KellyCriterion(tradeStats.ProfitFactor, winningPct),
OptimalF: types.OptimalF(intervalProfit.Profits),
StatN: statn,
StdErr: stdErr,
Sharpe: sharpeRatio,
Sortino: sortinoRatio,
}

cagr := types.NN(
types.CAGR(
symbolReport.InitialEquityValue().Float64(),
symbolReport.FinalEquityValue().Float64(),
int(period.Hours())/24,
), 0)

symbolReport.CAGR = n(cagr)
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)
}
Expand All @@ -677,6 +735,10 @@ func createSymbolReport(
return &symbolReport, nil
}

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,
Expand Down
12 changes: 12 additions & 0 deletions pkg/datatype/floats/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 2873394

Please sign in to comment.