From ddab6083d4c402a17732c618fdd1866bb2ca0326 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 10 May 2021 02:52:41 +0800 Subject: [PATCH] xmaker: support quantity scale --- pkg/bbgo/scale.go | 69 ++++++++++++++++++++++------- pkg/bbgo/scale_test.go | 14 ++++++ pkg/strategy/grid/strategy.go | 14 +++--- pkg/strategy/xmaker/strategy.go | 78 +++++++++++++++++++++++++++------ 4 files changed, 140 insertions(+), 35 deletions(-) diff --git a/pkg/bbgo/scale.go b/pkg/bbgo/scale.go index 3d014e659..5c0806d64 100644 --- a/pkg/bbgo/scale.go +++ b/pkg/bbgo/scale.go @@ -238,9 +238,48 @@ func (rule *SlideRule) Scale() (Scale, error) { return nil, errors.New("no any scale is defined") } + +// LayerScale defines the scale DSL for maker layers, e.g., +// +// quantityScale: +// byLayer: +// exp: +// domain: [1, 5] +// range: [0.01, 1.0] +// +// and +// +// quantityScale: +// byLayer: +// linear: +// domain: [1, 3] +// range: [0.01, 1.0] +type LayerScale struct { + LayerRule *SlideRule `json:"byLayer"` +} + +func (s *LayerScale) Scale(layer int) (quantity float64, err error) { + if s.LayerRule == nil { + err = errors.New("either price or volume scale is not defined") + return + } + + scale, err := s.LayerRule.Scale() + if err != nil { + return 0, err + } + + if err := scale.Solve(); err != nil { + return 0, err + } + + return scale.Call(float64(layer)), nil +} + + // PriceVolumeScale defines the scale DSL for strategy, e.g., // -// scaleQuantity: +// quantityScale: // byPrice: // exp: // domain: [10_000, 50_000] @@ -248,22 +287,22 @@ func (rule *SlideRule) Scale() (Scale, error) { // // and // -// scaleQuantity: +// quantityScale: // byVolume: // linear: // domain: [10_000, 50_000] // range: [0.01, 1.0] type PriceVolumeScale struct { - ByPrice *SlideRule `json:"byPrice"` - ByVolume *SlideRule `json:"byVolume"` + ByPriceRule *SlideRule `json:"byPrice"` + ByVolumeRule *SlideRule `json:"byVolume"` } -func (q *PriceVolumeScale) Scale(price float64, volume float64) (quantity float64, err error) { - if q.ByPrice != nil { - quantity, err = q.ScaleByPrice(price) +func (s *PriceVolumeScale) Scale(price float64, volume float64) (quantity float64, err error) { + if s.ByPriceRule != nil { + quantity, err = s.ScaleByPrice(price) return - } else if q.ByVolume != nil { - quantity, err = q.ScaleByVolume(volume) + } else if s.ByVolumeRule != nil { + quantity, err = s.ScaleByVolume(volume) } else { err = errors.New("either price or volume scale is not defined") } @@ -271,12 +310,12 @@ func (q *PriceVolumeScale) Scale(price float64, volume float64) (quantity float6 } // ScaleByPrice scale quantity by the given price -func (q *PriceVolumeScale) ScaleByPrice(price float64) (float64, error) { - if q.ByPrice == nil { +func (s *PriceVolumeScale) ScaleByPrice(price float64) (float64, error) { + if s.ByPriceRule == nil { return 0, errors.New("byPrice scale is not defined") } - scale, err := q.ByPrice.Scale() + scale, err := s.ByPriceRule.Scale() if err != nil { return 0, err } @@ -289,12 +328,12 @@ func (q *PriceVolumeScale) ScaleByPrice(price float64) (float64, error) { } // ScaleByVolume scale quantity by the given volume -func (q *PriceVolumeScale) ScaleByVolume(volume float64) (float64, error) { - if q.ByVolume == nil { +func (s *PriceVolumeScale) ScaleByVolume(volume float64) (float64, error) { + if s.ByVolumeRule == nil { return 0, errors.New("byVolume scale is not defined") } - scale, err := q.ByVolume.Scale() + scale, err := s.ByVolumeRule.Scale() if err != nil { return 0, err } diff --git a/pkg/bbgo/scale_test.go b/pkg/bbgo/scale_test.go index d52a35c95..dd068bc39 100644 --- a/pkg/bbgo/scale_test.go +++ b/pkg/bbgo/scale_test.go @@ -82,6 +82,20 @@ func TestLinearScale(t *testing.T) { } } +func TestLinearScale2(t *testing.T) { + scale := LinearScale{ + Domain: [2]float64{1, 3}, + Range: [2]float64{0.1, 0.4}, + } + + err := scale.Solve() + assert.NoError(t, err) + assert.Equal(t, "f(x) = 0.150000 * x + -0.050000", scale.String()) + assert.Equal(t, fixedpoint.NewFromFloat(0.1), fixedpoint.NewFromFloat(scale.Call(1))) + assert.Equal(t, fixedpoint.NewFromFloat(0.4), fixedpoint.NewFromFloat(scale.Call(3))) +} + + func TestQuadraticScale(t *testing.T) { // see https://www.desmos.com/calculator/vfqntrxzpr scale := QuadraticScale{ diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index e21b80004..461cd9007 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -75,8 +75,8 @@ type Strategy struct { // Quantity is the quantity you want to submit for each order. Quantity fixedpoint.Value `json:"quantity,omitempty"` - // ScaleQuantity helps user to define the quantity by price scale or volume scale - ScaleQuantity *bbgo.PriceVolumeScale `json:"scaleQuantity,omitempty"` + // QuantityScale helps user to define the quantity by price scale or volume scale + QuantityScale *bbgo.PriceVolumeScale `json:"quantityScale,omitempty"` // FixedAmount is used for fixed amount (dynamic quantity) if you don't want to use fixed quantity. FixedAmount fixedpoint.Value `json:"amount,omitempty" yaml:"amount"` @@ -122,7 +122,7 @@ func (s *Strategy) Validate() error { return fmt.Errorf("profit spread should bigger than 0") } - if s.Quantity == 0 && s.ScaleQuantity == nil { + if s.Quantity == 0 && s.QuantityScale == nil { return fmt.Errorf("quantity or scaleQuantity can not be zero") } @@ -175,8 +175,8 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type var quantity fixedpoint.Value if s.Quantity > 0 { quantity = s.Quantity - } else if s.ScaleQuantity != nil { - qf, err := s.ScaleQuantity.Scale(price.Float64(), 0) + } else if s.QuantityScale != nil { + qf, err := s.QuantityScale.Scale(price.Float64(), 0) if err != nil { return nil, err } @@ -268,8 +268,8 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types var quantity fixedpoint.Value if s.Quantity > 0 { quantity = s.Quantity - } else if s.ScaleQuantity != nil { - qf, err := s.ScaleQuantity.Scale(price.Float64(), 0) + } else if s.QuantityScale != nil { + qf, err := s.QuantityScale.Scale(price.Float64(), 0) if err != nil { return nil, err } diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 7e8cb1088..2ec7515fe 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/bbgo" @@ -51,11 +52,19 @@ type Strategy struct { HedgeInterval types.Duration `json:"hedgeInterval"` OrderCancelWaitTime types.Duration `json:"orderCancelWaitTime"` - Margin fixedpoint.Value `json:"margin"` - BidMargin fixedpoint.Value `json:"bidMargin"` - AskMargin fixedpoint.Value `json:"askMargin"` - Quantity fixedpoint.Value `json:"quantity"` - QuantityMultiplier fixedpoint.Value `json:"quantityMultiplier"` + Margin fixedpoint.Value `json:"margin"` + BidMargin fixedpoint.Value `json:"bidMargin"` + AskMargin fixedpoint.Value `json:"askMargin"` + + // Quantity is used for fixed quantity of the first layer + Quantity fixedpoint.Value `json:"quantity"` + + // QuantityMultiplier is the factor that multiplies the quantity of the previous layer + QuantityMultiplier fixedpoint.Value `json:"quantityMultiplier"` + + // QuantityScale helps user to define the quantity by layer scale + QuantityScale *bbgo.LayerScale `json:"quantityScale,omitempty"` + MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` DisableHedge bool `json:"disableHedge"` @@ -134,10 +143,8 @@ func (s *Strategy) updateQuote(ctx context.Context) { bestAskPrice := sourceBook.Asks[0].Price log.Infof("%s best bid price %f, best ask price: %f", s.Symbol, bestBidPrice.Float64(), bestAskPrice.Float64()) - bidQuantity := s.Quantity bidPrice := bestBidPrice.MulFloat64(1.0 - s.BidMargin.Float64()) - askQuantity := s.Quantity askPrice := bestAskPrice.MulFloat64(1.0 + s.AskMargin.Float64()) log.Infof("%s quote bid price: %f ask price: %f", s.Symbol, bidPrice.Float64(), askPrice.Float64()) @@ -206,9 +213,24 @@ func (s *Strategy) updateQuote(ctx context.Context) { return } + bidQuantity := s.Quantity + askQuantity := s.Quantity for i := 0; i < s.NumLayers; i++ { // for maker bid orders if !disableMakerBid { + if s.QuantityScale != nil { + qf, err := s.QuantityScale.Scale(i + 1) + if err != nil { + log.WithError(err).Errorf("quantityScale error") + return + } + + log.Infof("scaling quantity to %f by layer: %d", qf, i+1) + + // override the default bid quantity + bidQuantity = fixedpoint.NewFromFloat(qf) + } + if makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) { // if we bought, then we need to sell the base from the hedge session submitOrders = append(submitOrders, types.SubmitOrder{ @@ -227,12 +249,27 @@ func (s *Strategy) updateQuote(ctx context.Context) { makerQuota.Rollback() hedgeQuota.Rollback() } + bidPrice -= fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) - bidQuantity = bidQuantity.Mul(s.QuantityMultiplier) + + if s.QuantityMultiplier > 0 { + bidQuantity = bidQuantity.Mul(s.QuantityMultiplier) + } } // for maker ask orders if !disableMakerAsk { + if s.QuantityScale != nil { + qf, err := s.QuantityScale.Scale(i + 1) + if err != nil { + log.WithError(err).Errorf("quantityScale error") + return + } + + // override the default bid quantity + askQuantity = fixedpoint.NewFromFloat(qf) + } + if makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) { // if we bought, then we need to sell the base from the hedge session submitOrders = append(submitOrders, types.SubmitOrder{ @@ -251,7 +288,10 @@ func (s *Strategy) updateQuote(ctx context.Context) { hedgeQuota.Rollback() } askPrice += fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) - askQuantity = askQuantity.Mul(s.QuantityMultiplier) + + if s.QuantityMultiplier > 0 { + askQuantity = askQuantity.Mul(s.QuantityMultiplier) + } } } @@ -371,6 +411,22 @@ func (s *Strategy) handleTradeUpdate(trade types.Trade) { s.lastPrice = trade.Price } +func (s *Strategy) Validate() error { + if s.Quantity == 0 || s.QuantityScale == nil { + return errors.New("quantity or quantityScale can not be empty") + } + + if s.QuantityMultiplier != 0 && s.QuantityMultiplier < 0 { + return errors.New("quantityMultiplier can not be a negative number") + } + + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + return nil +} + func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { // configure default values if s.UpdateInterval == 0 { @@ -401,10 +457,6 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se } } - if s.Quantity == 0 { - s.Quantity = defaultQuantity - } - // configure sessions sourceSession, ok := sessions[s.SourceExchange] if !ok {