xmaker: support quantity scale

This commit is contained in:
c9s 2021-05-10 02:52:41 +08:00
parent dde998aced
commit ddab6083d4
4 changed files with 140 additions and 35 deletions

View File

@ -238,9 +238,48 @@ func (rule *SlideRule) Scale() (Scale, error) {
return nil, errors.New("no any scale is defined") 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., // PriceVolumeScale defines the scale DSL for strategy, e.g.,
// //
// scaleQuantity: // quantityScale:
// byPrice: // byPrice:
// exp: // exp:
// domain: [10_000, 50_000] // domain: [10_000, 50_000]
@ -248,22 +287,22 @@ func (rule *SlideRule) Scale() (Scale, error) {
// //
// and // and
// //
// scaleQuantity: // quantityScale:
// byVolume: // byVolume:
// linear: // linear:
// domain: [10_000, 50_000] // domain: [10_000, 50_000]
// range: [0.01, 1.0] // range: [0.01, 1.0]
type PriceVolumeScale struct { type PriceVolumeScale struct {
ByPrice *SlideRule `json:"byPrice"` ByPriceRule *SlideRule `json:"byPrice"`
ByVolume *SlideRule `json:"byVolume"` ByVolumeRule *SlideRule `json:"byVolume"`
} }
func (q *PriceVolumeScale) Scale(price float64, volume float64) (quantity float64, err error) { func (s *PriceVolumeScale) Scale(price float64, volume float64) (quantity float64, err error) {
if q.ByPrice != nil { if s.ByPriceRule != nil {
quantity, err = q.ScaleByPrice(price) quantity, err = s.ScaleByPrice(price)
return return
} else if q.ByVolume != nil { } else if s.ByVolumeRule != nil {
quantity, err = q.ScaleByVolume(volume) quantity, err = s.ScaleByVolume(volume)
} else { } else {
err = errors.New("either price or volume scale is not defined") 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 // ScaleByPrice scale quantity by the given price
func (q *PriceVolumeScale) ScaleByPrice(price float64) (float64, error) { func (s *PriceVolumeScale) ScaleByPrice(price float64) (float64, error) {
if q.ByPrice == nil { if s.ByPriceRule == nil {
return 0, errors.New("byPrice scale is not defined") return 0, errors.New("byPrice scale is not defined")
} }
scale, err := q.ByPrice.Scale() scale, err := s.ByPriceRule.Scale()
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -289,12 +328,12 @@ func (q *PriceVolumeScale) ScaleByPrice(price float64) (float64, error) {
} }
// ScaleByVolume scale quantity by the given volume // ScaleByVolume scale quantity by the given volume
func (q *PriceVolumeScale) ScaleByVolume(volume float64) (float64, error) { func (s *PriceVolumeScale) ScaleByVolume(volume float64) (float64, error) {
if q.ByVolume == nil { if s.ByVolumeRule == nil {
return 0, errors.New("byVolume scale is not defined") return 0, errors.New("byVolume scale is not defined")
} }
scale, err := q.ByVolume.Scale() scale, err := s.ByVolumeRule.Scale()
if err != nil { if err != nil {
return 0, err return 0, err
} }

View File

@ -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) { func TestQuadraticScale(t *testing.T) {
// see https://www.desmos.com/calculator/vfqntrxzpr // see https://www.desmos.com/calculator/vfqntrxzpr
scale := QuadraticScale{ scale := QuadraticScale{

View File

@ -75,8 +75,8 @@ type Strategy struct {
// Quantity is the quantity you want to submit for each order. // Quantity is the quantity you want to submit for each order.
Quantity fixedpoint.Value `json:"quantity,omitempty"` Quantity fixedpoint.Value `json:"quantity,omitempty"`
// ScaleQuantity helps user to define the quantity by price scale or volume scale // QuantityScale helps user to define the quantity by price scale or volume scale
ScaleQuantity *bbgo.PriceVolumeScale `json:"scaleQuantity,omitempty"` QuantityScale *bbgo.PriceVolumeScale `json:"quantityScale,omitempty"`
// FixedAmount is used for fixed amount (dynamic quantity) if you don't want to use fixed quantity. // 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"` 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") 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") 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 var quantity fixedpoint.Value
if s.Quantity > 0 { if s.Quantity > 0 {
quantity = s.Quantity quantity = s.Quantity
} else if s.ScaleQuantity != nil { } else if s.QuantityScale != nil {
qf, err := s.ScaleQuantity.Scale(price.Float64(), 0) qf, err := s.QuantityScale.Scale(price.Float64(), 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -268,8 +268,8 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types
var quantity fixedpoint.Value var quantity fixedpoint.Value
if s.Quantity > 0 { if s.Quantity > 0 {
quantity = s.Quantity quantity = s.Quantity
} else if s.ScaleQuantity != nil { } else if s.QuantityScale != nil {
qf, err := s.ScaleQuantity.Scale(price.Float64(), 0) qf, err := s.QuantityScale.Scale(price.Float64(), 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -8,6 +8,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo"
@ -51,11 +52,19 @@ type Strategy struct {
HedgeInterval types.Duration `json:"hedgeInterval"` HedgeInterval types.Duration `json:"hedgeInterval"`
OrderCancelWaitTime types.Duration `json:"orderCancelWaitTime"` OrderCancelWaitTime types.Duration `json:"orderCancelWaitTime"`
Margin fixedpoint.Value `json:"margin"` Margin fixedpoint.Value `json:"margin"`
BidMargin fixedpoint.Value `json:"bidMargin"` BidMargin fixedpoint.Value `json:"bidMargin"`
AskMargin fixedpoint.Value `json:"askMargin"` AskMargin fixedpoint.Value `json:"askMargin"`
Quantity fixedpoint.Value `json:"quantity"`
QuantityMultiplier fixedpoint.Value `json:"quantityMultiplier"` // 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"` MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"`
DisableHedge bool `json:"disableHedge"` DisableHedge bool `json:"disableHedge"`
@ -134,10 +143,8 @@ func (s *Strategy) updateQuote(ctx context.Context) {
bestAskPrice := sourceBook.Asks[0].Price bestAskPrice := sourceBook.Asks[0].Price
log.Infof("%s best bid price %f, best ask price: %f", s.Symbol, bestBidPrice.Float64(), bestAskPrice.Float64()) 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()) bidPrice := bestBidPrice.MulFloat64(1.0 - s.BidMargin.Float64())
askQuantity := s.Quantity
askPrice := bestAskPrice.MulFloat64(1.0 + s.AskMargin.Float64()) askPrice := bestAskPrice.MulFloat64(1.0 + s.AskMargin.Float64())
log.Infof("%s quote bid price: %f ask price: %f", s.Symbol, bidPrice.Float64(), askPrice.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 return
} }
bidQuantity := s.Quantity
askQuantity := s.Quantity
for i := 0; i < s.NumLayers; i++ { for i := 0; i < s.NumLayers; i++ {
// for maker bid orders // for maker bid orders
if !disableMakerBid { 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 makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) {
// if we bought, then we need to sell the base from the hedge session // if we bought, then we need to sell the base from the hedge session
submitOrders = append(submitOrders, types.SubmitOrder{ submitOrders = append(submitOrders, types.SubmitOrder{
@ -227,12 +249,27 @@ func (s *Strategy) updateQuote(ctx context.Context) {
makerQuota.Rollback() makerQuota.Rollback()
hedgeQuota.Rollback() hedgeQuota.Rollback()
} }
bidPrice -= fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) 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 // for maker ask orders
if !disableMakerAsk { 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 makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) {
// if we bought, then we need to sell the base from the hedge session // if we bought, then we need to sell the base from the hedge session
submitOrders = append(submitOrders, types.SubmitOrder{ submitOrders = append(submitOrders, types.SubmitOrder{
@ -251,7 +288,10 @@ func (s *Strategy) updateQuote(ctx context.Context) {
hedgeQuota.Rollback() hedgeQuota.Rollback()
} }
askPrice += fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips)) 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 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 { func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error {
// configure default values // configure default values
if s.UpdateInterval == 0 { 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 // configure sessions
sourceSession, ok := sessions[s.SourceExchange] sourceSession, ok := sessions[s.SourceExchange]
if !ok { if !ok {