From 027acfe3b5e2590e2c6b86d7bed3bbddd74effef Mon Sep 17 00:00:00 2001 From: Andy Cheng Date: Fri, 16 Jun 2023 18:06:47 +0800 Subject: [PATCH] feature/profitTracker: integrate profit report with profit tracker --- pkg/bbgo/order_executor_general.go | 15 ++- pkg/report/profit_report.go | 181 +++++++++++----------------- pkg/report/profit_tracker.go | 59 +++++---- pkg/strategy/supertrend/strategy.go | 53 ++++---- 4 files changed, 137 insertions(+), 171 deletions(-) diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index 3a7320fbe..9f818bb6d 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -164,7 +164,20 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) { } func (e *GeneralOrderExecutor) BindProfitTracker(profitTracker *report.ProfitTracker) { - profitTracker.Bind(e.tradeCollector, e.session) + e.session.Subscribe(types.KLineChannel, profitTracker.Market.Symbol, types.SubscribeOptions{Interval: profitTracker.Interval}) + + e.tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { + profitTracker.AddProfit(*profit) + }) + + e.tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { + profitTracker.AddTrade(trade) + }) + + // Rotate profitStats slice + e.session.MarketDataStream.OnKLineClosed(types.KLineWith(profitTracker.Market.Symbol, profitTracker.Interval, func(kline types.KLine) { + profitTracker.Rotate() + })) } func (e *GeneralOrderExecutor) Bind() { diff --git a/pkg/report/profit_report.go b/pkg/report/profit_report.go index 9d39b3278..c338a9921 100644 --- a/pkg/report/profit_report.go +++ b/pkg/report/profit_report.go @@ -2,7 +2,6 @@ package report import ( "fmt" - "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -12,134 +11,104 @@ import ( // AccumulatedProfitReport For accumulated profit report output type AccumulatedProfitReport struct { - // AccumulatedProfitMAWindow Accumulated profit SMA window, in number of trades - AccumulatedProfitMAWindow int `json:"accumulatedProfitMAWindow"` + // ProfitMAWindow Accumulated profit SMA window + ProfitMAWindow int `json:"ProfitMAWindow"` - // IntervalWindow interval window, in days - IntervalWindow int `json:"intervalWindow"` - - // NumberOfInterval How many intervals to output to TSV - NumberOfInterval int `json:"NumberOfInterval"` + // ShortTermProfitWindow The window to sum up the short-term profit + ShortTermProfitWindow int `json:"shortTermProfitWindow"` // TsvReportPath The path to output report to TsvReportPath string `json:"tsvReportPath"` - // AccumulatedDailyProfitWindow The window to sum up the daily profit, in days - AccumulatedDailyProfitWindow int `json:"accumulatedDailyProfitWindow"` + symbol string - Symbol string + types.IntervalWindow // Accumulated profit - accumulatedProfit fixedpoint.Value - accumulatedProfitPerDay floats.Slice - previousAccumulatedProfit fixedpoint.Value + accumulatedProfit fixedpoint.Value + accumulatedProfitPerInterval floats.Slice // Accumulated profit MA - accumulatedProfitMA *indicator.SMA - accumulatedProfitMAPerDay floats.Slice + profitMA *indicator.SMA + profitMAPerInterval floats.Slice - // Daily profit - dailyProfit floats.Slice + // Profit of each interval + ProfitPerInterval floats.Slice // Accumulated fee - accumulatedFee fixedpoint.Value - accumulatedFeePerDay floats.Slice + accumulatedFee fixedpoint.Value + accumulatedFeePerInterval floats.Slice // Win ratio - winRatioPerDay floats.Slice + winRatioPerInterval floats.Slice // Profit factor - profitFactorPerDay floats.Slice + profitFactorPerInterval floats.Slice // Trade number - dailyTrades floats.Slice - accumulatedTrades int - previousAccumulatedTrades int + accumulatedTrades int + accumulatedTradesPerInterval floats.Slice // Extra values - extraValues [][2]string + strategyParameters [][2]string } -func (r *AccumulatedProfitReport) Initialize(Symbol string, session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor, TradeStats *types.TradeStats) { - r.Symbol = Symbol +func (r *AccumulatedProfitReport) Initialize(symbol string, interval types.Interval, window int) { + r.symbol = symbol + r.Interval = interval + r.Window = window - if r.AccumulatedProfitMAWindow <= 0 { - r.AccumulatedProfitMAWindow = 60 + if r.ProfitMAWindow <= 0 { + r.ProfitMAWindow = 60 } - if r.IntervalWindow <= 0 { - r.IntervalWindow = 7 + + if r.Window <= 0 { + r.Window = 7 } - if r.AccumulatedDailyProfitWindow <= 0 { - r.AccumulatedDailyProfitWindow = 7 + + if r.ShortTermProfitWindow <= 0 { + r.ShortTermProfitWindow = 7 } - if r.NumberOfInterval <= 0 { - r.NumberOfInterval = 1 - } - r.accumulatedProfitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: types.Interval1d, Window: r.AccumulatedProfitMAWindow}} - session.Subscribe(types.KLineChannel, r.Symbol, types.SubscribeOptions{Interval: types.Interval1d}) - - // Record profit - orderExecutor.TradeCollector().OnProfit(func(trade types.Trade, profit *types.Profit) { - if profit == nil { - return - } - - r.RecordProfit(profit.Profit) - }) - - // Record trade - orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - r.RecordTrade(trade.Fee) - }) - - // Record daily status - session.MarketDataStream.OnKLineClosed(types.KLineWith(r.Symbol, types.Interval1d, func(kline types.KLine) { - r.DailyUpdate(TradeStats) - })) + r.profitMA = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: r.Interval, Window: r.ProfitMAWindow}} } -func (r *AccumulatedProfitReport) AddExtraValue(valueAndTitle [2]string) { - r.extraValues = append(r.extraValues, valueAndTitle) +func (r *AccumulatedProfitReport) AddStrategyParameter(title string, value string) { + r.strategyParameters = append(r.strategyParameters, [2]string{title, value}) } -func (r *AccumulatedProfitReport) RecordProfit(profit fixedpoint.Value) { - r.accumulatedProfit = r.accumulatedProfit.Add(profit) -} - -func (r *AccumulatedProfitReport) RecordTrade(fee fixedpoint.Value) { - r.accumulatedFee = r.accumulatedFee.Add(fee) +func (r *AccumulatedProfitReport) AddTrade(trade types.Trade) { + r.accumulatedFee = r.accumulatedFee.Add(trade.Fee) r.accumulatedTrades += 1 } -func (r *AccumulatedProfitReport) DailyUpdate(tradeStats *types.TradeStats) { - // Daily profit - r.dailyProfit.Update(r.accumulatedProfit.Sub(r.previousAccumulatedProfit).Float64()) - r.previousAccumulatedProfit = r.accumulatedProfit - +func (r *AccumulatedProfitReport) Rotate(ps *types.ProfitStats, ts *types.TradeStats) { // Accumulated profit - r.accumulatedProfitPerDay.Update(r.accumulatedProfit.Float64()) + r.accumulatedProfit.Add(ps.AccumulatedNetProfit) + r.accumulatedProfitPerInterval.Update(r.accumulatedProfit.Float64()) - // Accumulated profit MA - r.accumulatedProfitMA.Update(r.accumulatedProfit.Float64()) - r.accumulatedProfitMAPerDay.Update(r.accumulatedProfitMA.Last(0)) + // Profit of each interval + r.ProfitPerInterval.Update(ps.AccumulatedNetProfit.Float64()) + + // Profit MA + r.profitMA.Update(r.accumulatedProfit.Float64()) + r.profitMAPerInterval.Update(r.profitMA.Last(0)) // Accumulated Fee - r.accumulatedFeePerDay.Update(r.accumulatedFee.Float64()) + r.accumulatedFeePerInterval.Update(r.accumulatedFee.Float64()) + + // Trades + r.accumulatedTradesPerInterval.Update(float64(r.accumulatedTrades)) // Win ratio - r.winRatioPerDay.Update(tradeStats.WinningRatio.Float64()) + r.winRatioPerInterval.Update(ts.WinningRatio.Float64()) // Profit factor - r.profitFactorPerDay.Update(tradeStats.ProfitFactor.Float64()) - - // Daily trades - r.dailyTrades.Update(float64(r.accumulatedTrades - r.previousAccumulatedTrades)) - r.previousAccumulatedTrades = r.accumulatedTrades + r.profitFactorPerInterval.Update(ts.ProfitFactor.Float64()) } // Output Accumulated profit report to a TSV file -func (r *AccumulatedProfitReport) Output(symbol string) { +func (r *AccumulatedProfitReport) Output() { if r.TsvReportPath != "" { tsvwiter, err := tsv.AppendWriterFile(r.TsvReportPath) if err != nil { @@ -150,46 +119,34 @@ func (r *AccumulatedProfitReport) Output(symbol string) { titles := []string{ "#", "Symbol", - "accumulatedProfit", - "accumulatedProfitMA", - fmt.Sprintf("%dd profit", r.AccumulatedDailyProfitWindow), + "Total Net Profit", + fmt.Sprintf("Total Net Profit %sMA%d", r.Interval, r.Window), + fmt.Sprintf("%s %d Net Profit", r.Interval, r.ShortTermProfitWindow), "accumulatedFee", - "accumulatedNetProfit", "winRatio", "profitFactor", - "60D trades", + fmt.Sprintf("%s %d Trades", r.Interval, r.Window), } - for i := 0; i < len(r.extraValues); i++ { - titles = append(titles, r.extraValues[i][0]) + for i := 0; i < len(r.strategyParameters); i++ { + titles = append(titles, r.strategyParameters[i][0]) } _ = tsvwiter.Write(titles) // Output data row - for i := 0; i <= r.NumberOfInterval-1; i++ { - accumulatedProfit := r.accumulatedProfitPerDay.Index(r.IntervalWindow * i) - accumulatedProfitStr := fmt.Sprintf("%f", accumulatedProfit) - accumulatedProfitMA := r.accumulatedProfitMAPerDay.Index(r.IntervalWindow * i) - accumulatedProfitMAStr := fmt.Sprintf("%f", accumulatedProfitMA) - intervalAccumulatedProfit := r.dailyProfit.Tail(r.AccumulatedDailyProfitWindow+r.IntervalWindow*i).Sum() - r.dailyProfit.Tail(r.IntervalWindow*i).Sum() - intervalAccumulatedProfitStr := fmt.Sprintf("%f", intervalAccumulatedProfit) - accumulatedFee := fmt.Sprintf("%f", r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) - accumulatedNetProfit := fmt.Sprintf("%f", accumulatedProfit-r.accumulatedFeePerDay.Index(r.IntervalWindow*i)) - winRatio := fmt.Sprintf("%f", r.winRatioPerDay.Index(r.IntervalWindow*i)) - profitFactor := fmt.Sprintf("%f", r.profitFactorPerDay.Index(r.IntervalWindow*i)) - trades := r.dailyTrades.Tail(60+r.IntervalWindow*i).Sum() - r.dailyTrades.Tail(r.IntervalWindow*i).Sum() - tradesStr := fmt.Sprintf("%f", trades) + for i := 0; i <= r.Window-1; i++ { values := []string{ fmt.Sprintf("%d", i+1), - symbol, accumulatedProfitStr, - accumulatedProfitMAStr, - intervalAccumulatedProfitStr, - accumulatedFee, - accumulatedNetProfit, - winRatio, profitFactor, - tradesStr, + r.symbol, + fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)), + fmt.Sprintf("%f", r.profitMAPerInterval.Last(i)), + fmt.Sprintf("%f", r.accumulatedProfitPerInterval.Last(i)-r.accumulatedProfitPerInterval.Last(i+r.ShortTermProfitWindow)), + fmt.Sprintf("%f", r.accumulatedFeePerInterval.Last(i)), + fmt.Sprintf("%f", r.winRatioPerInterval.Last(i)), + fmt.Sprintf("%f", r.profitFactorPerInterval.Last(i)), + fmt.Sprintf("%f", r.accumulatedTradesPerInterval.Last(i)), } - for j := 0; j < len(r.extraValues); j++ { - values = append(values, r.extraValues[j][1]) + for j := 0; j < len(r.strategyParameters); j++ { + values = append(values, r.strategyParameters[j][1]) } _ = tsvwiter.Write(values) } diff --git a/pkg/report/profit_tracker.go b/pkg/report/profit_tracker.go index 86885034e..2bfad3338 100644 --- a/pkg/report/profit_tracker.go +++ b/pkg/report/profit_tracker.go @@ -1,64 +1,59 @@ package report import ( - "github.com/c9s/bbgo/pkg/bbgo" - "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) type ProfitTracker struct { types.IntervalWindow + // Accumulated profit report + AccumulatedProfitReport *AccumulatedProfitReport `json:"accumulatedProfitReport"` + + Market types.Market + ProfitStatsSlice []*types.ProfitStats CurrentProfitStats **types.ProfitStats - market types.Market + tradeStats *types.TradeStats } -// InitOld is for backward capability. ps is the ProfitStats of the strategy, market is the strategy market -func (p *ProfitTracker) InitOld(ps **types.ProfitStats, market types.Market) { - p.market = market +// InitOld is for backward capability. ps is the ProfitStats of the strategy, Market is the strategy Market +func (p *ProfitTracker) InitOld(market types.Market, ps **types.ProfitStats, ts *types.TradeStats) { + p.Market = market if *ps == nil { - *ps = types.NewProfitStats(p.market) + *ps = types.NewProfitStats(p.Market) } + p.tradeStats = ts + p.CurrentProfitStats = ps p.ProfitStatsSlice = append(p.ProfitStatsSlice, *ps) + + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.Initialize(p.Market.Symbol, p.Interval, p.Window) + } } -// Init initialize the tracker with the given market -func (p *ProfitTracker) Init(market types.Market) { - p.market = market - *p.CurrentProfitStats = types.NewProfitStats(p.market) - p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) -} - -func (p *ProfitTracker) Bind(tradeCollector *bbgo.TradeCollector, session *bbgo.ExchangeSession) { - session.Subscribe(types.KLineChannel, p.market.Symbol, types.SubscribeOptions{Interval: p.Interval}) - - tradeCollector.OnProfit(func(trade types.Trade, profit *types.Profit) { - p.AddProfit(*profit) - }) - - tradeCollector.OnTrade(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { - p.AddTrade(trade) - }) - - // Rotate profitStats slice - session.MarketDataStream.OnKLineClosed(types.KLineWith(p.market.Symbol, p.Interval, func(kline types.KLine) { - p.Rotate() - })) +// Init initialize the tracker with the given Market +func (p *ProfitTracker) Init(market types.Market, ts *types.TradeStats) { + ps := types.NewProfitStats(p.Market) + p.InitOld(market, &ps, ts) } // Rotate the tracker to make a new ProfitStats to record the profits func (p *ProfitTracker) Rotate() { - *p.CurrentProfitStats = types.NewProfitStats(p.market) + *p.CurrentProfitStats = types.NewProfitStats(p.Market) p.ProfitStatsSlice = append(p.ProfitStatsSlice, *p.CurrentProfitStats) // Truncate if len(p.ProfitStatsSlice) > p.Window { p.ProfitStatsSlice = p.ProfitStatsSlice[len(p.ProfitStatsSlice)-p.Window:] } + + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.Rotate(*p.CurrentProfitStats, p.tradeStats) + } } func (p *ProfitTracker) AddProfit(profit types.Profit) { @@ -67,4 +62,8 @@ func (p *ProfitTracker) AddProfit(profit types.Profit) { func (p *ProfitTracker) AddTrade(trade types.Trade) { (*p.CurrentProfitStats).AddTrade(trade) + + if p.AccumulatedProfitReport != nil { + p.AccumulatedProfitReport.AddTrade(trade) + } } diff --git a/pkg/strategy/supertrend/strategy.go b/pkg/strategy/supertrend/strategy.go index 57891b7a2..946dcb11c 100644 --- a/pkg/strategy/supertrend/strategy.go +++ b/pkg/strategy/supertrend/strategy.go @@ -39,8 +39,6 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` - ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` - // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` @@ -104,8 +102,7 @@ type Strategy struct { // StrategyController bbgo.StrategyController - // Accumulated profit report - AccumulatedProfitReport *report.AccumulatedProfitReport `json:"accumulatedProfitReport"` + ProfitTracker *report.ProfitTracker `json:"profitTracker" persistence:"profit_tracker"` } func (s *Strategy) ID() string { @@ -330,8 +327,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.TradeStats = types.NewTradeStats(s.Symbol) } - if s.ProfitTracker.CurrentProfitStats == nil { - s.ProfitTracker.InitOld(&s.ProfitStats, s.Market) + if s.ProfitTracker != nil { + if s.ProfitTracker.CurrentProfitStats == nil { + s.ProfitTracker.InitOld(s.Market, &s.ProfitStats, s.TradeStats) + } + + // Add strategy parameters to report + if s.ProfitTracker.AccumulatedProfitReport != nil { + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("window", fmt.Sprintf("%d", s.Window)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)) + s.ProfitTracker.AccumulatedProfitReport.AddStrategyParameter("stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)) + } } // Interval profit report @@ -361,25 +373,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // AccountValueCalculator s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency) - // Accumulated profit report - if bbgo.IsBackTesting { - if s.AccumulatedProfitReport == nil { - s.AccumulatedProfitReport = &report.AccumulatedProfitReport{} - } - s.AccumulatedProfitReport.Initialize(s.Symbol, session, s.orderExecutor, s.TradeStats) - - // Add strategy parameters to report - s.AccumulatedProfitReport.AddExtraValue([2]string{"window", fmt.Sprintf("%d", s.Window)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"multiplier", fmt.Sprintf("%f", s.SupertrendMultiplier)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"fastDEMA", fmt.Sprintf("%d", s.FastDEMAWindow)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"slowDEMA", fmt.Sprintf("%d", s.SlowDEMAWindow)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"takeProfitAtrMultiplier", fmt.Sprintf("%f", s.TakeProfitAtrMultiplier)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopLossByTriggeringK", fmt.Sprintf("%t", s.StopLossByTriggeringK)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedSupertrend", fmt.Sprintf("%t", s.StopByReversedSupertrend)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedDema", fmt.Sprintf("%t", s.StopByReversedDema)}) - s.AccumulatedProfitReport.AddExtraValue([2]string{"stopByReversedLinGre", fmt.Sprintf("%t", s.StopByReversedLinGre)}) - } - // For drawing profitSlice := floats.Slice{1., 1.} price, _ := session.LastPrice(s.Symbol) @@ -527,10 +520,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() - if bbgo.IsBackTesting { - // Output accumulated profit report - defer s.AccumulatedProfitReport.Output(s.Symbol) + // Output profit report + if s.ProfitTracker != nil { + if s.ProfitTracker.AccumulatedProfitReport != nil { + s.ProfitTracker.AccumulatedProfitReport.Output() + } + } + if bbgo.IsBackTesting { // Draw graph if s.DrawGraph { if err := s.Draw(&profitSlice, &cumProfitSlice); err != nil {