From a8fe20ae3ad6c16dcc846901ad644f7dc72fb5be Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 20 Jul 2022 19:49:56 +0900 Subject: [PATCH] fix: drift exit condition, trade_stats serialization in redis --- config/drift.yaml | 16 ++++-- pkg/strategy/drift/strategy.go | 92 +++++++++++++++++++++------------- pkg/types/trade_stats.go | 19 ++----- 3 files changed, 73 insertions(+), 54 deletions(-) diff --git a/config/drift.yaml b/config/drift.yaml index 406512b14..e3c249bc4 100644 --- a/config/drift.yaml +++ b/config/drift.yaml @@ -1,8 +1,14 @@ --- +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + sessions: binance: exchange: binance - futures: true + futures: false envVarPrefix: binance heikinAshi: false @@ -15,13 +21,15 @@ exchangeStrategies: # kline interval for indicators interval: 15m window: 2 - stoploss: 3% + stoploss: 2% source: close - predictOffset: 14 + predictOffset: 3 + # position avg +- takeProfitFactor * atr as take profit price + takeProfitFactor: 1 noStopPrice: true noTrailingStopLoss: false # stddev on high/low-source - hlVarianceMultiplier: 0.35 + hlVarianceMultiplier: 0.34 generateGraph: true graphPNLDeductFee: false diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index b9fb3fc1b..6e2b14b59 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -57,6 +57,7 @@ type Strategy struct { lock sync.RWMutex Source string `json:"source"` + TakeProfitFactor float64 `json:"takeProfitFactor"` StopLoss fixedpoint.Value `json:"stoploss"` CanvasPath string `json:"canvasPath"` PredictOffset int `json:"predictOffset"` @@ -72,7 +73,7 @@ type Strategy struct { // Whether to generate graph when shutdown GenerateGraph bool `json:"generateGraph"` - StopOrders map[uint64]types.SubmitOrder + StopOrders map[uint64]*types.SubmitOrder ExitMethods bbgo.ExitMethodSet `json:"exits"` Session *bbgo.ExchangeSession @@ -91,6 +92,7 @@ func (s *Strategy) Print(o *os.File) { hiyellow(f, "canvasPath: %s\n", s.CanvasPath) hiyellow(f, "source: %s\n", s.Source) hiyellow(f, "stoploss: %v\n", s.StopLoss) + hiyellow(f, "takeProfitFactor: %f\n", s.TakeProfitFactor) hiyellow(f, "predictOffset: %d\n", s.PredictOffset) hiyellow(f, "exits:\n %s\n", string(b)) hiyellow(f, "symbol: %s\n", s.Symbol) @@ -124,12 +126,23 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { s.ExitMethods.SetAndSubscribe(session, s) } -func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) { - // Cleanup pending StopOrders - s.StopOrders = make(map[uint64]types.SubmitOrder) - order := s.Position.NewMarketCloseOrder(fixedpoint.One) +func (s *Strategy) CurrentPosition() *types.Position { + return s.Position +} + +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { + order := s.Position.NewMarketCloseOrder(percentage) if order == nil { - return nil, false + return nil + } + if percentage.Compare(fixedpoint.One) == 0 { + // Cleanup pending StopOrders + s.StopOrders = make(map[uint64]*types.SubmitOrder) + } else { + // Should only have one stop order + for _, o := range s.StopOrders { + o.Quantity = o.Quantity.Mul(fixedpoint.One.Sub(percentage)) + } } order.Tag = "close" order.TimeInForce = "" @@ -146,14 +159,14 @@ func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) { } for { if s.Market.IsDustQuantity(order.Quantity, price) { - return nil, true + return nil } - createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) + _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) if err != nil { order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) continue } - return &createdOrders[0], true + return nil } } @@ -190,7 +203,7 @@ func (s *Strategy) SourceFuncGenerator() SourceFunc { } func (s *Strategy) BindStopLoss(ctx context.Context) { - s.StopOrders = make(map[uint64]types.SubmitOrder) + s.StopOrders = make(map[uint64]*types.SubmitOrder) s.Session.UserDataStream.OnOrderUpdate(func(order types.Order) { if len(s.StopOrders) == 0 { return @@ -222,7 +235,7 @@ func (s *Strategy) BindStopLoss(ctx context.Context) { } o.Quantity = baseBalance.Available } - if _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, o); err != nil { + if _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *o); err != nil { log.WithError(err).Errorf("cannot send stop order: %v", order) } } @@ -295,16 +308,16 @@ func (s *Strategy) InitTickerFunctions(ctx context.Context) { atr = s.atr.Last() avg = s.Position.AverageCost.Float64() stoploss = s.StopLoss.Float64() - exitShortCondition := (avg+atr/2 <= pricef || avg*(1.+stoploss) <= pricef) && - (!s.Position.IsClosed() && !s.Position.IsDust(price)) - exitLongCondition := (avg-atr/2 >= pricef || avg*(1.-stoploss) >= pricef) && - (!s.Position.IsClosed() && !s.Position.IsDust(price)) + exitShortCondition := (avg+atr/2 <= pricef || avg*(1.+stoploss) <= pricef || avg-atr*s.TakeProfitFactor >= pricef) && + (s.Position.IsShort() && !s.Position.IsDust(price)) + exitLongCondition := (avg-atr/2 >= pricef || avg*(1.-stoploss) >= pricef || avg+atr*s.TakeProfitFactor <= pricef) && + (!s.Position.IsLong() && !s.Position.IsDust(price)) if exitShortCondition || exitLongCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") return } - _, _ = s.ClosePosition(ctx) + _ = s.ClosePosition(ctx, fixedpoint.One) } }) @@ -360,11 +373,12 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty } f, err = os.Create(s.GraphPNLPath) if err != nil { - panic("open pnl") + log.WithError(err).Errorf("open pnl") + return } defer f.Close() if err := canvas.Render(chart.PNG, f); err != nil { - panic("render pnl") + log.WithError(err).Errorf("render pnl") } canvas = types.NewCanvas(s.InstanceID()) @@ -375,11 +389,12 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty } f, err = os.Create(s.GraphCumPNLPath) if err != nil { - panic("open cumpnl") + log.WithError(err).Errorf("open cumpnl") + return } defer f.Close() if err := canvas.Render(chart.PNG, f); err != nil { - panic("render cumpnl") + log.WithError(err).Errorf("render cumpnl") } } @@ -410,7 +425,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.OnEmergencyStop(func() { _ = s.GeneralOrderExecutor.GracefulCancel(ctx) - _, _ = s.ClosePosition(ctx) + _ = s.ClosePosition(ctx, fixedpoint.One) }) s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) @@ -544,16 +559,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se highf := math.Max(kline.High.Float64(), pricef) avg := s.Position.AverageCost.Float64() - exitShortCondition := (avg+atr/2 <= highf || avg*(1.+stoploss) <= highf) && - (!s.Position.IsClosed() && !s.Position.IsDust(price)) - exitLongCondition := (avg-atr/2 >= lowf || avg*(1.-stoploss) >= lowf) && - (!s.Position.IsClosed() && !s.Position.IsDust(price)) + exitShortCondition := (avg+atr/2 <= highf || avg*(1.+stoploss) <= highf || avg-atr*s.TakeProfitFactor >= lowf) && + (s.Position.IsShort() && !s.Position.IsDust(price)) + exitLongCondition := (avg-atr/2 >= lowf || avg*(1.-stoploss) >= lowf || avg+atr*s.TakeProfitFactor <= highf) && + (s.Position.IsLong() && !s.Position.IsDust(price)) if exitShortCondition || exitLongCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") return } - _, _ = s.ClosePosition(ctx) + _ = s.ClosePosition(ctx, fixedpoint.One) } return } @@ -578,19 +593,26 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.stdevHigh.Update(highdiff) avg := s.Position.AverageCost.Float64() + if !s.IsBackTesting() { + balances := s.Session.GetAccount().Balances() + log.Infof("source: %.4f, price: %.4f, driftPred: %.4f, drift: %.4f, drift[1]: %.4f, atr: %.4f, avg: %.4f", + sourcef, pricef, driftPred, drift[0], drift[1], atr, avg) + log.Infof("balances: [Base] %v [Quote] %v", balances[s.Market.BaseCurrency], balances[s.Market.QuoteCurrency]) + } + shortCondition := (driftPred <= 0 && drift[0] <= 0) longCondition := (driftPred >= 0 && drift[0] >= 0) - exitShortCondition := ((drift[1] < 0 && drift[0] >= 0) || avg+atr/2 <= highf || avg*(1.+stoploss) <= highf) && - (!s.Position.IsClosed() && !s.Position.IsDust(fixedpoint.Max(price, source))) && !longCondition - exitLongCondition := ((drift[1] > 0 && drift[0] < 0) || avg-atr/2 >= lowf || avg*(1.-stoploss) >= lowf) && - (!s.Position.IsClosed() && !s.Position.IsDust(fixedpoint.Min(price, source))) && !shortCondition + exitShortCondition := ((drift[1] < 0 && drift[0] >= 0) || avg+atr/2 <= highf || avg*(1.+stoploss) <= highf || avg-atr*s.TakeProfitFactor >= lowf) && + (s.Position.IsShort() && !s.Position.IsDust(fixedpoint.Max(price, source))) && !longCondition + exitLongCondition := ((drift[1] > 0 && drift[0] < 0) || avg-atr/2 >= lowf || avg*(1.-stoploss) >= lowf || avg+atr*s.TakeProfitFactor <= highf) && + (s.Position.IsLong() && !s.Position.IsDust(fixedpoint.Min(price, source))) && !shortCondition if exitShortCondition || exitLongCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") return } - _, _ = s.ClosePosition(ctx) + _ = s.ClosePosition(ctx, fixedpoint.One) } if shortCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { @@ -611,7 +633,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } // Cleanup pending StopOrders - s.StopOrders = make(map[uint64]types.SubmitOrder) + s.StopOrders = make(map[uint64]*types.SubmitOrder) quantity := baseBalance.Available stopPrice := fixedpoint.NewFromFloat(math.Min(sourcef+atr/2, sourcef*(1.+stoploss))) stopOrder := types.SubmitOrder{ @@ -642,7 +664,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.GeneralOrderExecutor.SubmitOrders(ctx, stopOrder) return } - s.StopOrders[createdOrders[0].OrderID] = stopOrder + s.StopOrders[createdOrders[0].OrderID] = &stopOrder } if longCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { @@ -663,7 +685,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } // Cleanup pending StopOrders - s.StopOrders = make(map[uint64]types.SubmitOrder) + s.StopOrders = make(map[uint64]*types.SubmitOrder) quantity := quoteBalance.Available.Div(source) stopPrice := fixedpoint.NewFromFloat(math.Max(sourcef-atr/2, sourcef*(1.-stoploss))) stopOrder := types.SubmitOrder{ @@ -695,7 +717,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.GeneralOrderExecutor.SubmitOrders(ctx, stopOrder) return } - s.StopOrders[createdOrders[0].OrderID] = stopOrder + s.StopOrders[createdOrders[0].OrderID] = &stopOrder } }) diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index afd4bba6c..7dfd3d1a1 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -1,7 +1,6 @@ package types import ( - "encoding/json" "time" "gopkg.in/yaml.v3" @@ -10,9 +9,9 @@ import ( ) type IntervalProfitCollector struct { - Interval Interval - Profits *Float64Slice - tmpTime time.Time + Interval Interval `json:"interval"` + Profits *Float64Slice `json:"profits"` + tmpTime time.Time `json:"tmpTime"` } func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector { @@ -92,16 +91,6 @@ func (s IntervalProfitCollector) MarshalYAML() (interface{}, error) { return result, nil } -func (s *IntervalProfitCollector) MarshalJSON() ([]byte, error) { - result := make(map[string]interface{}) - result["Sharpe Ratio"] = s.GetSharpe() - result["Omega Ratio"] = s.GetOmega() - result["Profitable Count"] = s.GetNumOfProfitableIntervals() - result["NonProfitable Count"] = s.GetNumOfNonProfitableIntervals() - return json.Marshal(result) - -} - // TODO: Add more stats from the reference: // See https://www.metatrader5.com/en/terminal/help/algotrading/testing_report type TradeStats struct { @@ -117,7 +106,7 @@ type TradeStats struct { MostLossTrade fixedpoint.Value `json:"mostLossTrade" yaml:"mostLossTrade"` ProfitFactor fixedpoint.Value `json:"profitFactor" yaml:"profitFactor"` TotalNetProfit fixedpoint.Value `json:"totalNetProfit" yaml:"totalNetProfit"` - IntervalProfits map[Interval]*IntervalProfitCollector `json:"intervalProfits,omitempty" yaml: "intervalProfits,omitempty"` + IntervalProfits map[Interval]*IntervalProfitCollector `jons:"intervalProfits,omitempty" yaml: "intervalProfits,omitempty"` } func NewTradeStats(symbol string) *TradeStats {