pivotshort: add TradeStats

This commit is contained in:
c9s 2022-06-10 00:49:32 +08:00
parent b79e4f2fb8
commit 260857b5b1
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54

View File

@ -3,8 +3,10 @@ package pivotshort
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
"github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
@ -12,6 +14,39 @@ import (
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
type TradeStats struct {
WinningRatio fixedpoint.Value `json:"winningRatio" yaml:"winningRatio"`
NumOfLossTrade int `json:"numOfLossTrade" yaml:"numOfLossTrade"`
NumOfProfitTrade int `json:"numOfProfitTrade" yaml:"numOfProfitTrade"`
GrossProfit fixedpoint.Value `json:"grossProfit" yaml:"grossProfit"`
GrossLoss fixedpoint.Value `json:"grossLoss" yaml:"grossLoss"`
Profits []fixedpoint.Value `json:"profits" yaml:"profits"`
Losses []fixedpoint.Value `json:"losses" yaml:"losses"`
MostProfitableTrade fixedpoint.Value `json:"mostProfitableTrade" yaml:"mostProfitableTrade"`
MostLossTrade fixedpoint.Value `json:"mostLossTrade" yaml:"mostLossTrade"`
}
func (s *TradeStats) Add(pnl fixedpoint.Value) {
if pnl.Sign() > 0 {
s.NumOfProfitTrade++
s.Profits = append(s.Profits, pnl)
s.GrossProfit = s.GrossProfit.Add(pnl)
s.MostProfitableTrade = fixedpoint.Max(s.MostProfitableTrade, pnl)
} else {
s.NumOfLossTrade++
s.Losses = append(s.Losses, pnl)
s.GrossLoss = s.GrossLoss.Add(pnl)
s.MostLossTrade = fixedpoint.Min(s.MostLossTrade, pnl)
}
s.WinningRatio = fixedpoint.NewFromFloat(float64(s.NumOfProfitTrade) / float64(s.NumOfLossTrade))
}
func (s *TradeStats) String() string {
out, _ := yaml.Marshal(s)
return string(out)
}
const ID = "pivotshort" const ID = "pivotshort"
var log = logrus.WithField("strategy", ID) var log = logrus.WithField("strategy", ID)
@ -63,6 +98,7 @@ type Strategy struct {
// persistence fields // persistence fields
Position *types.Position `json:"position,omitempty" persistence:"position"` Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
TradeStats *TradeStats `persistence:"trade_stats"`
PivotLength int `json:"pivotLength"` PivotLength int `json:"pivotLength"`
@ -152,6 +188,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
s.tradeCollector.Process() s.tradeCollector.Process()
return err return err
} }
func (s *Strategy) InstanceID() string { func (s *Strategy) InstanceID() string {
return fmt.Sprintf("%s:%s", ID, s.Symbol) return fmt.Sprintf("%s:%s", ID, s.Symbol)
} }
@ -174,6 +211,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.ProfitStats = types.NewProfitStats(s.Market) s.ProfitStats = types.NewProfitStats(s.Market)
} }
if s.TradeStats == nil {
s.TradeStats = &TradeStats{}
}
instanceID := s.InstanceID() instanceID := s.InstanceID()
// Always update the position fields // Always update the position fields
@ -198,6 +239,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.ProfitStats.AddProfit(p) s.ProfitStats.AddProfit(p)
s.Notify(&s.ProfitStats) s.Notify(&s.ProfitStats)
s.TradeStats.Add(profit)
s.Environment.RecordPosition(s.Position, trade, &p) s.Environment.RecordPosition(s.Position, trade, &p)
} }
}) })
@ -242,10 +285,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
if isPositionOpened && s.Position.IsShort() { if isPositionOpened && s.Position.IsShort() {
// calculate return rate // calculate return rate
// TODO: apply quantity to this formula // TODO: apply quantity to this formula
roi := kline.Close.Sub(s.Position.AverageCost).Div(s.Position.AverageCost) roi := s.Position.AverageCost.Sub(kline.Close).Div(s.Position.AverageCost)
if roi.Compare(s.Exit.RoiStopLossPercentage) > 0 { if roi.Compare(s.Exit.RoiStopLossPercentage.Neg()) < 0 {
// SL // SL
s.Notify("%s ROI StopLoss triggered at price %f", s.Symbol, kline.Close.Float64()) s.Notify("%s ROI StopLoss triggered at price %f, ROI = %s", s.Symbol, kline.Close.Float64(), roi.Percentage())
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error") log.WithError(err).Errorf("graceful cancel order error")
} }
@ -255,8 +298,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
} }
return return
} else if roi.Compare(s.Exit.RoiTakeProfitPercentage.Neg()) < 0 { } else if roi.Compare(s.Exit.RoiTakeProfitPercentage) > 0 {
s.Notify("%s TakeProfit triggered at price %f", s.Symbol, kline.Close.Float64()) s.Notify("%s TakeProfit triggered at price %f, ROI take profit percentage by %s", s.Symbol, kline.Close.Float64(), roi.Percentage(), kline)
if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error") log.WithError(err).Errorf("graceful cancel order error")
} }
@ -336,6 +379,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
} }
}) })
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
log.Info(s.TradeStats.String())
wg.Done()
})
return nil return nil
} }