From bed03dbd17fba6eba6b194ad2684fbb894f15762 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 31 Jan 2022 01:42:21 +0800 Subject: [PATCH] schedule: refactor and improve schedule strategy with QuantityOrAmount struct --- config/schedule-ethusdt.yaml | 23 +++++++++--- pkg/bbgo/moving_average_settings.go | 6 ++-- pkg/bbgo/quantity_amount.go | 19 ++++++++-- pkg/strategy/schedule/strategy.go | 56 +++++++++++++---------------- 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/config/schedule-ethusdt.yaml b/config/schedule-ethusdt.yaml index faaa6275c..3b0a2fc48 100644 --- a/config/schedule-ethusdt.yaml +++ b/config/schedule-ethusdt.yaml @@ -1,4 +1,15 @@ --- +# time godotenv -f .env.local -- go run ./cmd/bbgo backtest --exchange binance --base-asset-baseline --config config/schedule-ethusdt.yaml -v +backtest: + startTime: "2021-08-01" + endTime: "2021-08-07" + symbols: + - ETHUSDT + account: + balances: + ETH: 0.0 + USDT: 20_000.0 + riskControls: # This is the session-based risk controller, which let you configure different risk controller by session. sessionBased: @@ -18,16 +29,17 @@ riskControls: exchangeStrategies: -- on: max +- on: binance schedule: # trigger schedule per hour # valid intervals are: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d interval: 1h - + symbol: ETHUSDT side: buy - quantity: 0.01 - + # quantity: 0.01 + amount: 11.0 + # execute order only when the closed price is below the moving average line. # you can open the app to adjust your parameters here. # the interval here could be different from the triggering interval. @@ -35,3 +47,6 @@ exchangeStrategies: type: EWMA interval: 1h window: 99 + # quantity: 0.05 + amount: 11.0 + diff --git a/pkg/bbgo/moving_average_settings.go b/pkg/bbgo/moving_average_settings.go index 87091dd0c..d6d4dadd4 100644 --- a/pkg/bbgo/moving_average_settings.go +++ b/pkg/bbgo/moving_average_settings.go @@ -2,7 +2,7 @@ package bbgo import ( "fmt" - "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" ) @@ -12,8 +12,8 @@ type MovingAverageSettings struct { Window int `json:"window"` Side *types.SideType `json:"side"` - Quantity *fixedpoint.Value `json:"quantity"` - Amount *fixedpoint.Value `json:"amount"` + + QuantityOrAmount } func (settings MovingAverageSettings) IntervalWindow() types.IntervalWindow { diff --git a/pkg/bbgo/quantity_amount.go b/pkg/bbgo/quantity_amount.go index c72e245d2..4940684b0 100644 --- a/pkg/bbgo/quantity_amount.go +++ b/pkg/bbgo/quantity_amount.go @@ -1,6 +1,10 @@ package bbgo -import "github.com/c9s/bbgo/pkg/fixedpoint" +import ( + "errors" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) // QuantityOrAmount is a setting structure used for quantity/amount settings // You can embed this struct into your strategy to share the setting methods @@ -10,7 +14,18 @@ type QuantityOrAmount struct { Quantity fixedpoint.Value `json:"quantity"` // Amount is the order quote amount for your buy/sell order. - Amount fixedpoint.Value `json:"amount"` + Amount fixedpoint.Value `json:"amount,omitempty"` +} + +func (qa *QuantityOrAmount) IsSet() bool { + return qa.Quantity > 0 || qa.Amount > 0 +} + +func (qa *QuantityOrAmount) Validate() error { + if qa.Quantity == 0 && qa.Amount == 0 { + return errors.New("either quantity or amount can not be empty") + } + return nil } // CalculateQuantity calculates the equivalent quantity of the given price when amount is set diff --git a/pkg/strategy/schedule/strategy.go b/pkg/strategy/schedule/strategy.go index 64513715e..b13e7bdb6 100644 --- a/pkg/strategy/schedule/strategy.go +++ b/pkg/strategy/schedule/strategy.go @@ -2,10 +2,12 @@ package schedule import ( "context" - "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/types" ) @@ -16,7 +18,6 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } - type Strategy struct { Market types.Market @@ -33,12 +34,9 @@ type Strategy struct { Symbol string `json:"symbol"` // Side is the order side type, which can be buy or sell - Side types.SideType `json:"side"` + Side types.SideType `json:"side,omitempty"` - // Quantity is the quantity of the submit order - Quantity fixedpoint.Value `json:"quantity,omitempty"` - - Amount fixedpoint.Value `json:"amount,omitempty"` + bbgo.QuantityOrAmount BelowMovingAverage *bbgo.MovingAverageSettings `json:"belowMovingAverage,omitempty"` @@ -60,8 +58,8 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { } func (s *Strategy) Validate() error { - if s.Quantity == 0 && s.Amount == 0 { - return errors.New("either quantity or amount can not be empty") + if err := s.QuantityOrAmount.Validate(); err != nil { + return err } return nil @@ -99,9 +97,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } closePrice := fixedpoint.NewFromFloat(kline.Close) - quantity := s.Quantity - amount := s.Amount - + quantity := s.QuantityOrAmount.CalculateQuantity(closePrice) side := s.Side if s.BelowMovingAverage != nil || s.AboveMovingAverage != nil { @@ -116,12 +112,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } // override the default quantity or amount - if s.BelowMovingAverage.Quantity != nil { - quantity = *s.BelowMovingAverage.Quantity - } else if s.BelowMovingAverage.Amount != nil { - amount = *s.BelowMovingAverage.Amount + if s.BelowMovingAverage.QuantityOrAmount.IsSet() { + quantity = s.BelowMovingAverage.QuantityOrAmount.CalculateQuantity(closePrice) } - } } else if aboveMA != nil && closePrice.Float64() > aboveMA.Last() { match = true @@ -130,11 +123,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se side = *s.AboveMovingAverage.Side } - // override the default quantity or amount - if s.AboveMovingAverage.Quantity != nil { - quantity = *s.AboveMovingAverage.Quantity - } else if s.AboveMovingAverage.Amount != nil { - amount = *s.AboveMovingAverage.Amount + if s.AboveMovingAverage.QuantityOrAmount.IsSet() { + quantity = s.AboveMovingAverage.QuantityOrAmount.CalculateQuantity(closePrice) } } } @@ -145,11 +135,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } - // convert amount to quantity if amount is given - if amount > 0 { - quantity = amount.Div(closePrice) - } - // calculate quote quantity for balance checking quoteQuantity := quantity.Mul(closePrice) @@ -158,35 +143,42 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se case types.SideTypeBuy: quoteBalance, ok := session.Account.Balance(s.Market.QuoteCurrency) if !ok { + log.Errorf("can not place scheduled %s order, quote balance %s is empty", s.Symbol, s.Market.QuoteCurrency) return } + if quoteBalance.Available < quoteQuantity { - s.Notifiability.Notify("Quote balance %s is not enough: %f < %f", s.Market.QuoteCurrency, quoteBalance.Available.Float64(), quoteQuantity.Float64()) + s.Notifiability.Notify("Can not place scheduled %s order: quote balance %s is not enough: %f < %f", s.Symbol, s.Market.QuoteCurrency, quoteBalance.Available.Float64(), quoteQuantity.Float64()) + log.Errorf("can not place scheduled %s order: quote balance %s is not enough: %f < %f", s.Symbol, s.Market.QuoteCurrency, quoteBalance.Available.Float64(), quoteQuantity.Float64()) return } case types.SideTypeSell: baseBalance, ok := session.Account.Balance(s.Market.BaseCurrency) if !ok { + log.Errorf("can not place scheduled %s order, base balance %s is empty", s.Symbol, s.Market.BaseCurrency) return } + if baseBalance.Available < quantity { - s.Notifiability.Notify("Base balance %s is not enough: %f < %f", s.Market.QuoteCurrency, baseBalance.Available.Float64(), quantity.Float64()) + s.Notifiability.Notify("Can not place scheduled %s order: base balance %s is not enough: %f < %f", s.Symbol, s.Market.QuoteCurrency, baseBalance.Available.Float64(), quantity.Float64()) + log.Errorf("can not place scheduled %s order: base balance %s is not enough: %f < %f", s.Symbol, s.Market.QuoteCurrency, baseBalance.Available.Float64(), quantity.Float64()) return } } - s.Notifiability.Notify("Submitting scheduled order %s quantity %f at price %f", s.Symbol, quantity.Float64(), closePrice.Float64()) + s.Notifiability.Notify("Submitting scheduled %s order with quantity %f at price %f", s.Symbol, quantity.Float64(), closePrice.Float64()) _, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, Side: side, Type: types.OrderTypeMarket, Quantity: quantity.Float64(), + Market: s.Market, }) - if err != nil { - log.WithError(err).Error("submit order error") + s.Notifiability.Notify("Can not place scheduled %s order: submit error %s", s.Symbol, err.Error()) + log.WithError(err).Errorf("can not place scheduled %s order error", s.Symbol) } })