xdepthmaker: add tests to the generateMakerOrders

This commit is contained in:
c9s 2023-11-30 10:32:03 +08:00
parent 263c0883d1
commit 46b3a81b07
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
5 changed files with 282 additions and 7 deletions

67
config/xdepthmaker.yaml Normal file
View File

@ -0,0 +1,67 @@
---
notifications:
slack:
defaultChannel: "dev-bbgo"
errorChannel: "bbgo-error"
switches:
trade: true
orderUpdate: false
submitOrder: false
persistence:
json:
directory: var/data
redis:
host: 127.0.0.1
port: 6379
db: 0
logging:
trade: true
order: true
fields:
env: staging
sessions:
max:
exchange: max
envVarPrefix: max
binance:
exchange: binance
envVarPrefix: binance
crossExchangeStrategies:
- xdepthmaker:
symbol: "BTCUSDT"
makerExchange: max
hedgeExchange: binance
# disableHedge disables the hedge orders on the source exchange
# disableHedge: true
hedgeInterval: 10s
notifyTrade: true
margin: 0.004
askMargin: 0.4%
bidMargin: 0.4%
depthScale:
byLayer:
linear:
domain: [1, 30]
range: [50, 20_000]
# numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders
numLayers: 30
# pips is the fraction numbers between each order. for BTC, 1 pip is 0.1,
# 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and
# 18002.00
pips: 10
persistence:
type: redis

View File

@ -202,6 +202,9 @@ type Strategy struct {
// QuantityScale helps user to define the quantity by layer scale // QuantityScale helps user to define the quantity by layer scale
QuantityScale *bbgo.LayerScale `json:"quantityScale,omitempty"` QuantityScale *bbgo.LayerScale `json:"quantityScale,omitempty"`
// DepthScale helps user to define the depth by layer scale
DepthScale *bbgo.LayerScale `json:"depthScale,omitempty"`
// MaxExposurePosition defines the unhedged quantity of stop // MaxExposurePosition defines the unhedged quantity of stop
MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"` MaxExposurePosition fixedpoint.Value `json:"maxExposurePosition"`
@ -266,8 +269,8 @@ func (s *Strategy) Validate() error {
return errors.New("maker exchange is not configured") return errors.New("maker exchange is not configured")
} }
if s.Quantity.IsZero() || s.QuantityScale == nil { if s.DepthScale == nil {
return errors.New("quantity or quantityScale can not be empty") return errors.New("depthScale can not be empty")
} }
if len(s.Symbol) == 0 { if len(s.Symbol) == 0 {
@ -576,7 +579,106 @@ func (s *Strategy) runTradeRecover(ctx context.Context) {
} }
} }
func (s *Strategy) updateQuote(ctx context.Context, orderExecutionRouter bbgo.OrderExecutionRouter) { func (s *Strategy) generateMakerOrders(pricingBook *types.StreamOrderBook) ([]types.SubmitOrder, error) {
bestBid, bestAsk, hasPrice := pricingBook.BestBidAndAsk()
if !hasPrice {
return nil, nil
}
bestBidPrice := bestBid.Price
bestAskPrice := bestAsk.Price
log.Infof("%s book ticker: best ask / best bid = %v / %v", s.Symbol, bestAskPrice, bestBidPrice)
lastMidPrice := bestBidPrice.Add(bestAskPrice).Div(Two)
_ = lastMidPrice
var submitOrders []types.SubmitOrder
var accumulatedBidQuantity = fixedpoint.Zero
var accumulatedAskQuantity = fixedpoint.Zero
var accumulatedBidQuoteQuantity = fixedpoint.Zero
dupPricingBook := pricingBook.CopyDepth(0)
for _, side := range []types.SideType{types.SideTypeBuy, types.SideTypeSell} {
for i := 1; i <= s.NumLayers; i++ {
requiredDepthFloat, err := s.DepthScale.Scale(i)
if err != nil {
return nil, errors.Wrapf(err, "depthScale scale error")
}
// requiredDepth is the required depth in quote currency
requiredDepth := fixedpoint.NewFromFloat(requiredDepthFloat)
sideBook := dupPricingBook.SideBook(side)
index := sideBook.IndexByQuoteVolumeDepth(requiredDepth)
pvs := types.PriceVolumeSlice{}
if index == -1 {
pvs = sideBook[:]
} else {
pvs = sideBook[0 : index+1]
}
log.Infof("required depth: %f, pvs: %+v", requiredDepth.Float64(), pvs)
depthPrice, err := averageDepthPrice(pvs)
if err != nil {
log.WithError(err).Errorf("error aggregating depth price")
continue
}
switch side {
case types.SideTypeBuy:
if s.BidMargin.Sign() > 0 {
depthPrice = depthPrice.Mul(fixedpoint.One.Sub(s.BidMargin))
}
depthPrice = depthPrice.Round(s.makerMarket.PricePrecision+1, fixedpoint.Down)
case types.SideTypeSell:
if s.AskMargin.Sign() > 0 {
depthPrice = depthPrice.Mul(fixedpoint.One.Add(s.AskMargin))
}
depthPrice = depthPrice.Round(s.makerMarket.PricePrecision+1, fixedpoint.Up)
}
depthPrice = s.makerMarket.TruncatePrice(depthPrice)
quantity := requiredDepth.Div(depthPrice)
quantity = s.makerMarket.TruncateQuantity(quantity)
log.Infof("side: %s required depth: %f price: %f quantity: %f", side, requiredDepth.Float64(), depthPrice.Float64(), quantity.Float64())
switch side {
case types.SideTypeBuy:
quantity = quantity.Sub(accumulatedBidQuantity)
accumulatedBidQuantity = accumulatedBidQuantity.Add(quantity)
quoteQuantity := fixedpoint.Mul(quantity, depthPrice)
quoteQuantity = quoteQuantity.Round(s.makerMarket.PricePrecision, fixedpoint.Up)
accumulatedBidQuoteQuantity = accumulatedBidQuoteQuantity.Add(quoteQuantity)
case types.SideTypeSell:
quantity = quantity.Sub(accumulatedAskQuantity)
accumulatedAskQuantity = accumulatedAskQuantity.Add(quantity)
}
submitOrders = append(submitOrders, types.SubmitOrder{
Symbol: s.Symbol,
Type: types.OrderTypeLimitMaker,
Market: s.makerMarket,
Side: side,
Price: depthPrice,
Quantity: quantity,
})
}
}
return submitOrders, nil
}
func (s *Strategy) updateQuote(ctx context.Context) {
if err := s.MakerOrderExecutor.GracefulCancel(ctx); err != nil { if err := s.MakerOrderExecutor.GracefulCancel(ctx); err != nil {
log.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol) log.Warnf("there are some %s orders not canceled, skipping placing maker orders", s.Symbol)
s.MakerOrderExecutor.ActiveMakerOrders().Print() s.MakerOrderExecutor.ActiveMakerOrders().Print()
@ -844,3 +946,22 @@ func selectSessions2(
s2 = sessions[n2] s2 = sessions[n2]
return s1, s2, nil return s1, s2, nil
} }
func averageDepthPrice(pvs types.PriceVolumeSlice) (price fixedpoint.Value, err error) {
if len(pvs) == 0 {
return fixedpoint.Zero, fmt.Errorf("empty pv slice")
}
totalQuoteAmount := fixedpoint.Zero
totalQuantity := fixedpoint.Zero
for i := 0; i < len(pvs); i++ {
pv := pvs[i]
quoteAmount := fixedpoint.Mul(pv.Volume, pv.Price)
totalQuoteAmount = totalQuoteAmount.Add(quoteAmount)
totalQuantity = totalQuantity.Add(pv.Volume)
}
price = totalQuoteAmount.Div(totalQuantity)
return price, nil
}

View File

@ -0,0 +1,72 @@
package xdepthmaker
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/bbgo"
. "github.com/c9s/bbgo/pkg/testing/testhelper"
"github.com/c9s/bbgo/pkg/types"
)
func newTestBTCUSDTMarket() types.Market {
return types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
TickSize: Number(0.01),
StepSize: Number(0.000001),
PricePrecision: 2,
VolumePrecision: 8,
MinNotional: Number(8.0),
MinQuantity: Number(0.0003),
}
}
func TestStrategy_generateMakerOrders(t *testing.T) {
s := &Strategy{
Symbol: "BTCUSDT",
NumLayers: 3,
DepthScale: &bbgo.LayerScale{
LayerRule: &bbgo.SlideRule{
LinearScale: &bbgo.LinearScale{
Domain: [2]float64{1.0, 3.0},
Range: [2]float64{1000.0, 15000.0},
},
},
},
CrossExchangeMarketMakingStrategy: &CrossExchangeMarketMakingStrategy{
makerMarket: newTestBTCUSDTMarket(),
},
}
pricingBook := types.NewStreamBook("BTCUSDT")
pricingBook.OrderBook.Load(types.SliceOrderBook{
Symbol: "BTCUSDT",
Bids: types.PriceVolumeSlice{
{Price: Number("25000.00"), Volume: Number("0.1")},
{Price: Number("24900.00"), Volume: Number("0.2")},
{Price: Number("24800.00"), Volume: Number("0.3")},
{Price: Number("24700.00"), Volume: Number("0.4")},
},
Asks: types.PriceVolumeSlice{
{Price: Number("25100.00"), Volume: Number("0.1")},
{Price: Number("25200.00"), Volume: Number("0.2")},
{Price: Number("25300.00"), Volume: Number("0.3")},
{Price: Number("25400.00"), Volume: Number("0.4")},
},
Time: time.Now(),
})
orders, err := s.generateMakerOrders(pricingBook)
assert.NoError(t, err)
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
{Side: types.SideTypeBuy, Price: Number("25000"), Quantity: Number("0.04")}, // =~ $1000.00
{Side: types.SideTypeBuy, Price: Number("24866.66"), Quantity: Number("0.281715")}, // =~ $7005.3111219, accumulated amount =~ $1000.00 + $7005.3111219 = $8005.3111219
{Side: types.SideTypeBuy, Price: Number("24800"), Quantity: Number("0.283123")}, // =~ $7021.4504, accumulated amount =~ $1000.00 + $7005.3111219 + $7021.4504 = $8005.3111219 + $7021.4504 =~ $15026.7615219
{Side: types.SideTypeSell, Price: Number("25100"), Quantity: Number("0.03984")},
{Side: types.SideTypeSell, Price: Number("25233.33"), Quantity: Number("0.2772")},
{Side: types.SideTypeSell, Price: Number("25233.33"), Quantity: Number("0.277411")},
}, orders)
}

View File

@ -32,7 +32,7 @@ type PriceSideQuantityAssert struct {
func AssertOrdersPriceSideQuantity( func AssertOrdersPriceSideQuantity(
t *testing.T, asserts []PriceSideQuantityAssert, orders []types.SubmitOrder, t *testing.T, asserts []PriceSideQuantityAssert, orders []types.SubmitOrder,
) { ) {
assert.Equalf(t, len(orders), len(asserts), "expecting %d orders", len(asserts)) assert.Equalf(t, len(asserts), len(orders), "expecting %d orders", len(asserts))
var assertPrices, orderPrices fixedpoint.Slice var assertPrices, orderPrices fixedpoint.Slice
var assertPricesFloat, orderPricesFloat []float64 var assertPricesFloat, orderPricesFloat []float64

View File

@ -38,7 +38,7 @@ func (slice PriceVolumeSlice) Trim() (pvs PriceVolumeSlice) {
} }
func (slice PriceVolumeSlice) CopyDepth(depth int) PriceVolumeSlice { func (slice PriceVolumeSlice) CopyDepth(depth int) PriceVolumeSlice {
if depth > len(slice) { if depth == 0 || depth > len(slice) {
return slice.Copy() return slice.Copy()
} }
@ -67,8 +67,23 @@ func (slice PriceVolumeSlice) First() (PriceVolume, bool) {
return PriceVolume{}, false return PriceVolume{}, false
} }
func (slice PriceVolumeSlice) IndexByQuoteVolumeDepth(requiredQuoteVolume fixedpoint.Value) int {
var totalQuoteVolume = fixedpoint.Zero
for x, pv := range slice {
// this should use float64 multiply
quoteVolume := fixedpoint.Mul(pv.Volume, pv.Price)
totalQuoteVolume = totalQuoteVolume.Add(quoteVolume)
if totalQuoteVolume.Compare(requiredQuoteVolume) >= 0 {
return x
}
}
// depth not enough
return -1
}
func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value) int { func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value) int {
var tv fixedpoint.Value = fixedpoint.Zero var tv = fixedpoint.Zero
for x, el := range slice { for x, el := range slice {
tv = tv.Add(el.Volume) tv = tv.Add(el.Volume)
if tv.Compare(requiredVolume) >= 0 { if tv.Compare(requiredVolume) >= 0 {
@ -76,7 +91,7 @@ func (slice PriceVolumeSlice) IndexByVolumeDepth(requiredVolume fixedpoint.Value
} }
} }
// not deep enough // depth not enough
return -1 return -1
} }