Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: add more tradestats to SessionSymbolReport #1388

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
122 changes: 88 additions & 34 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 @@ -533,12 +531,9 @@ var BacktestCmd = &cobra.Command{
continue
}

tradeState := sessionTradeStats[session.Name][symbol]
profitFactor := tradeState.ProfitFactor
winningRatio := tradeState.WinningRatio
intervalProfits := tradeState.IntervalProfits[types.Interval1d]
tradeStats := sessionTradeStats[session.Name][symbol]

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 All @@ -549,8 +544,8 @@ var BacktestCmd = &cobra.Command{
summaryReport.TotalUnrealizedProfit = symbolReport.PnL.UnrealizedProfit
summaryReport.InitialEquityValue = summaryReport.InitialEquityValue.Add(symbolReport.InitialEquityValue())
summaryReport.FinalEquityValue = summaryReport.FinalEquityValue.Add(symbolReport.FinalEquityValue())
summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit)
summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss)
summaryReport.TotalGrossProfit = summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit)
summaryReport.TotalGrossLoss = summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss)

// write report to a file
if generatingReport {
Expand Down Expand Up @@ -603,14 +598,12 @@ var BacktestCmd = &cobra.Command{
},
}

func createSymbolReport(
userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade,
intervalProfit *types.IntervalProfitCollector,
profitFactor, winningRatio fixedpoint.Value,
) (
func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, tradeStats *types.TradeStats) (
*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 +613,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 +634,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 +727,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