mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-21 22:43:52 +00:00
xmaker: support quantity scale
This commit is contained in:
parent
dde998aced
commit
ddab6083d4
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user