schedule: refactor and improve schedule strategy with QuantityOrAmount struct

This commit is contained in:
c9s 2022-01-31 01:42:21 +08:00
parent 11bbdb16a0
commit bed03dbd17
4 changed files with 63 additions and 41 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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)
}
})