mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-21 22:43:52 +00:00
xdepthmaker: add tests to the generateMakerOrders
This commit is contained in:
parent
263c0883d1
commit
46b3a81b07
67
config/xdepthmaker.yaml
Normal file
67
config/xdepthmaker.yaml
Normal 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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
72
pkg/strategy/xdepthmaker/strategy_test.go
Normal file
72
pkg/strategy/xdepthmaker/strategy_test.go
Normal 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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user