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")
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user