Merge pull request #1445 from c9s/feature/xdepthmaker

IMPROVE: [xdepthmaker] add more improvements
This commit is contained in:
c9s 2023-12-11 22:16:32 +08:00 committed by GitHub
commit d960d4ff95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 216 additions and 47 deletions

View File

@ -52,7 +52,7 @@ jobs:
# auto-start: "false" # auto-start: "false"
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v4
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}

View File

@ -12,11 +12,11 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: 1.18 go-version: 1.21
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
version: v1.46.2 version: v1.54

View File

@ -392,10 +392,10 @@ func (b *ActiveOrderBook) add(order types.Order) {
if pendingOrder, ok := b.pendingOrderUpdates.Get(order.OrderID); ok { if pendingOrder, ok := b.pendingOrderUpdates.Get(order.OrderID); ok {
// if the pending order update time is newer than the adding order // if the pending order update time is newer than the adding order
// we should use the pending order rather than the adding order. // we should use the pending order rather than the adding order.
// if pending order is older, than we should add the new one, and drop the pending order // if the pending order is older, then we should add the new one, and drop the pending order
log.Infof("found pending order update") log.Debugf("found pending order update: %+v", pendingOrder)
if isNewerOrderUpdate(pendingOrder, order) { if isNewerOrderUpdate(pendingOrder, order) {
log.Infof("pending order update is newer") log.Debugf("pending order update is newer: %+v", pendingOrder)
order = pendingOrder order = pendingOrder
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/core" "github.com/c9s/bbgo/pkg/core"
"github.com/c9s/bbgo/pkg/exchange/retry"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/util"
@ -24,7 +25,7 @@ var defaultMargin = fixedpoint.NewFromFloat(0.003)
var Two = fixedpoint.NewFromInt(2) var Two = fixedpoint.NewFromInt(2)
const priceUpdateTimeout = 30 * time.Second const priceUpdateTimeout = 5 * time.Minute
const ID = "xdepthmaker" const ID = "xdepthmaker"
@ -237,7 +238,7 @@ type Strategy struct {
lastPrice fixedpoint.Value lastPrice fixedpoint.Value
stopC chan struct{} stopC, authedC chan struct{}
} }
func (s *Strategy) ID() string { func (s *Strategy) ID() string {
@ -365,15 +366,40 @@ func (s *Strategy) CrossRun(
go s.runTradeRecover(ctx) go s.runTradeRecover(ctx)
} }
s.authedC = make(chan struct{}, 2)
bindAuthSignal(ctx, s.makerSession.UserDataStream, s.authedC)
bindAuthSignal(ctx, s.hedgeSession.UserDataStream, s.authedC)
go func() { go func() {
log.Infof("waiting for user data stream to get authenticated")
select {
case <-ctx.Done():
return
case <-s.authedC:
}
select {
case <-ctx.Done():
return
case <-s.authedC:
}
log.Infof("user data stream authenticated, start placing orders...")
posTicker := time.NewTicker(util.MillisecondsJitter(s.HedgeInterval.Duration(), 200)) posTicker := time.NewTicker(util.MillisecondsJitter(s.HedgeInterval.Duration(), 200))
defer posTicker.Stop() defer posTicker.Stop()
fullReplenishTicker := time.NewTicker(util.MillisecondsJitter(s.FullReplenishInterval.Duration(), 200)) fullReplenishTicker := time.NewTicker(util.MillisecondsJitter(s.FullReplenishInterval.Duration(), 200))
defer fullReplenishTicker.Stop() defer fullReplenishTicker.Stop()
// clean up the previous open orders
if err := s.cleanUpOpenOrders(ctx); err != nil {
log.WithError(err).Errorf("error cleaning up open orders")
}
s.updateQuote(ctx, 0) s.updateQuote(ctx, 0)
lastOrderReplenishTime := time.Now()
for { for {
select { select {
@ -387,6 +413,7 @@ func (s *Strategy) CrossRun(
case <-fullReplenishTicker.C: case <-fullReplenishTicker.C:
s.updateQuote(ctx, 0) s.updateQuote(ctx, 0)
lastOrderReplenishTime = time.Now()
case sig, ok := <-s.pricingBook.C: case sig, ok := <-s.pricingBook.C:
// when any book change event happened // when any book change event happened
@ -394,6 +421,10 @@ func (s *Strategy) CrossRun(
return return
} }
if time.Since(lastOrderReplenishTime) < 10*time.Second {
continue
}
switch sig.Type { switch sig.Type {
case types.BookSignalSnapshot: case types.BookSignalSnapshot:
s.updateQuote(ctx, 0) s.updateQuote(ctx, 0)
@ -402,6 +433,8 @@ func (s *Strategy) CrossRun(
s.updateQuote(ctx, 5) s.updateQuote(ctx, 5)
} }
lastOrderReplenishTime = time.Now()
case <-posTicker.C: case <-posTicker.C:
// For positive position and positive covered position: // For positive position and positive covered position:
// uncover position = +5 - +3 (covered position) = 2 // uncover position = +5 - +3 (covered position) = 2
@ -598,31 +631,61 @@ func (s *Strategy) runTradeRecover(ctx context.Context) {
} }
} }
func (s *Strategy) generateMakerOrders(pricingBook *types.StreamOrderBook, maxLayer int) ([]types.SubmitOrder, error) { func (s *Strategy) generateMakerOrders(
bestBid, bestAsk, hasPrice := pricingBook.BestBidAndAsk() pricingBook *types.StreamOrderBook, maxLayer int, availableBase fixedpoint.Value, availableQuote fixedpoint.Value,
) ([]types.SubmitOrder, error) {
_, _, hasPrice := pricingBook.BestBidAndAsk()
if !hasPrice { if !hasPrice {
return nil, nil return nil, nil
} }
bestBidPrice := bestBid.Price
bestAskPrice := bestAsk.Price
lastMidPrice := bestBidPrice.Add(bestAskPrice).Div(Two)
_ = lastMidPrice
var submitOrders []types.SubmitOrder var submitOrders []types.SubmitOrder
var accumulatedBidQuantity = fixedpoint.Zero var accumulatedBidQuantity = fixedpoint.Zero
var accumulatedAskQuantity = fixedpoint.Zero var accumulatedAskQuantity = fixedpoint.Zero
var accumulatedBidQuoteQuantity = fixedpoint.Zero var accumulatedBidQuoteQuantity = fixedpoint.Zero
dupPricingBook := pricingBook.CopyDepth(0) // copy the pricing book because during the generation the book data could change
dupPricingBook := pricingBook.Copy()
log.Infof("dupPricingBook: \n\tbids: %+v \n\tasks: %+v",
dupPricingBook.SideBook(types.SideTypeBuy),
dupPricingBook.SideBook(types.SideTypeSell))
log.Infof("pricingBook: \n\tbids: %+v \n\tasks: %+v",
pricingBook.SideBook(types.SideTypeBuy),
pricingBook.SideBook(types.SideTypeSell))
if maxLayer == 0 || maxLayer > s.NumLayers { if maxLayer == 0 || maxLayer > s.NumLayers {
maxLayer = s.NumLayers maxLayer = s.NumLayers
} }
var availableBalances = map[types.SideType]fixedpoint.Value{
types.SideTypeBuy: availableQuote,
types.SideTypeSell: availableBase,
}
for _, side := range []types.SideType{types.SideTypeBuy, types.SideTypeSell} { for _, side := range []types.SideType{types.SideTypeBuy, types.SideTypeSell} {
sideBook := dupPricingBook.SideBook(side)
if sideBook.Len() == 0 {
log.Warnf("orderbook %s side is empty", side)
continue
}
availableSideBalance, ok := availableBalances[side]
if !ok {
log.Warnf("no available balance for side %s side", side)
continue
}
layerLoop:
for i := 1; i <= maxLayer; i++ { for i := 1; i <= maxLayer; i++ {
// simple break, we need to check the market minNotional and minQuantity later
if !availableSideBalance.Eq(fixedpoint.PosInf) {
if availableSideBalance.IsZero() || availableSideBalance.Sign() < 0 {
break layerLoop
}
}
requiredDepthFloat, err := s.DepthScale.Scale(i) requiredDepthFloat, err := s.DepthScale.Scale(i)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "depthScale scale error") return nil, errors.Wrapf(err, "depthScale scale error")
@ -631,7 +694,6 @@ func (s *Strategy) generateMakerOrders(pricingBook *types.StreamOrderBook, maxLa
// requiredDepth is the required depth in quote currency // requiredDepth is the required depth in quote currency
requiredDepth := fixedpoint.NewFromFloat(requiredDepthFloat) requiredDepth := fixedpoint.NewFromFloat(requiredDepthFloat)
sideBook := dupPricingBook.SideBook(side)
index := sideBook.IndexByQuoteVolumeDepth(requiredDepth) index := sideBook.IndexByQuoteVolumeDepth(requiredDepth)
pvs := types.PriceVolumeSlice{} pvs := types.PriceVolumeSlice{}
@ -641,7 +703,11 @@ func (s *Strategy) generateMakerOrders(pricingBook *types.StreamOrderBook, maxLa
pvs = sideBook[0 : index+1] pvs = sideBook[0 : index+1]
} }
log.Infof("required depth: %f, pvs: %+v", requiredDepth.Float64(), pvs) if len(pvs) == 0 {
continue
}
log.Infof("side: %s required depth: %f, pvs: %+v", side, requiredDepth.Float64(), pvs)
depthPrice, err := averageDepthPrice(pvs) depthPrice, err := averageDepthPrice(pvs)
if err != nil { if err != nil {
@ -678,12 +744,36 @@ func (s *Strategy) generateMakerOrders(pricingBook *types.StreamOrderBook, maxLa
accumulatedBidQuantity = accumulatedBidQuantity.Add(quantity) accumulatedBidQuantity = accumulatedBidQuantity.Add(quantity)
quoteQuantity := fixedpoint.Mul(quantity, depthPrice) quoteQuantity := fixedpoint.Mul(quantity, depthPrice)
quoteQuantity = quoteQuantity.Round(s.makerMarket.PricePrecision, fixedpoint.Up) quoteQuantity = quoteQuantity.Round(s.makerMarket.PricePrecision, fixedpoint.Up)
if !availableSideBalance.Eq(fixedpoint.PosInf) && availableSideBalance.Compare(quoteQuantity) <= 0 {
quoteQuantity = availableSideBalance
quantity = quoteQuantity.Div(depthPrice).Round(s.makerMarket.PricePrecision, fixedpoint.Down)
}
if quantity.Compare(s.makerMarket.MinQuantity) <= 0 || quoteQuantity.Compare(s.makerMarket.MinNotional) <= 0 {
break layerLoop
}
availableSideBalance = availableSideBalance.Sub(quoteQuantity)
accumulatedBidQuoteQuantity = accumulatedBidQuoteQuantity.Add(quoteQuantity) accumulatedBidQuoteQuantity = accumulatedBidQuoteQuantity.Add(quoteQuantity)
case types.SideTypeSell: case types.SideTypeSell:
quantity = quantity.Sub(accumulatedAskQuantity) quantity = quantity.Sub(accumulatedAskQuantity)
accumulatedAskQuantity = accumulatedAskQuantity.Add(quantity) quoteQuantity := quantity.Mul(depthPrice)
// balance check
if !availableSideBalance.Eq(fixedpoint.PosInf) && availableSideBalance.Compare(quantity) <= 0 {
break layerLoop
}
if quantity.Compare(s.makerMarket.MinQuantity) <= 0 || quoteQuantity.Compare(s.makerMarket.MinNotional) <= 0 {
break layerLoop
}
availableSideBalance = availableSideBalance.Sub(quantity)
accumulatedAskQuantity = accumulatedAskQuantity.Add(quantity)
} }
submitOrders = append(submitOrders, types.SubmitOrder{ submitOrders = append(submitOrders, types.SubmitOrder{
@ -747,20 +837,38 @@ func (s *Strategy) updateQuote(ctx context.Context, maxLayer int) {
bookLastUpdateTime := s.pricingBook.LastUpdateTime() bookLastUpdateTime := s.pricingBook.LastUpdateTime()
if _, err := s.bidPriceHeartBeat.Update(bestBid); err != nil { if _, err := s.bidPriceHeartBeat.Update(bestBid); err != nil {
log.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago", log.WithError(err).Warnf("quote update error, %s price not updating, order book last update: %s ago",
s.Symbol, s.Symbol,
time.Since(bookLastUpdateTime)) time.Since(bookLastUpdateTime))
return
} }
if _, err := s.askPriceHeartBeat.Update(bestAsk); err != nil { if _, err := s.askPriceHeartBeat.Update(bestAsk); err != nil {
log.WithError(err).Errorf("quote update error, %s price not updating, order book last update: %s ago", log.WithError(err).Warnf("quote update error, %s price not updating, order book last update: %s ago",
s.Symbol, s.Symbol,
time.Since(bookLastUpdateTime)) time.Since(bookLastUpdateTime))
}
balances, err := s.MakerOrderExecutor.Session().Exchange.QueryAccountBalances(ctx)
if err != nil {
log.WithError(err).Errorf("balance query error")
return return
} }
submitOrders, err := s.generateMakerOrders(s.pricingBook, maxLayer) log.Infof("balances: %+v", balances.NotZero())
quoteBalance, ok := balances[s.makerMarket.QuoteCurrency]
if !ok {
return
}
baseBalance, ok := balances[s.makerMarket.BaseCurrency]
if !ok {
return
}
log.Infof("quote balance: %s, base balance: %s", quoteBalance, baseBalance)
submitOrders, err := s.generateMakerOrders(s.pricingBook, maxLayer, baseBalance.Available, quoteBalance.Available)
if err != nil { if err != nil {
log.WithError(err).Errorf("generate order error") log.WithError(err).Errorf("generate order error")
return return
@ -780,6 +888,19 @@ func (s *Strategy) updateQuote(ctx context.Context, maxLayer int) {
s.orderStore.Add(createdOrders...) s.orderStore.Add(createdOrders...)
} }
func (s *Strategy) cleanUpOpenOrders(ctx context.Context) error {
openOrders, err := retry.QueryOpenOrdersUntilSuccessful(ctx, s.makerSession.Exchange, s.Symbol)
if err != nil {
return err
}
if err := s.makerSession.Exchange.CancelOrders(ctx, openOrders...); err != nil {
return err
}
return nil
}
func selectSessions2( func selectSessions2(
sessions map[string]*bbgo.ExchangeSession, n1, n2 string, sessions map[string]*bbgo.ExchangeSession, n1, n2 string,
) (s1, s2 *bbgo.ExchangeSession, err error) { ) (s1, s2 *bbgo.ExchangeSession, err error) {
@ -820,3 +941,14 @@ func min(a, b int) int {
return b return b
} }
func bindAuthSignal(ctx context.Context, stream types.Stream, c chan<- struct{}) {
stream.OnAuth(func() {
select {
case <-ctx.Done():
return
case c <- struct{}{}:
default:
}
})
}

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
. "github.com/c9s/bbgo/pkg/testing/testhelper" . "github.com/c9s/bbgo/pkg/testing/testhelper"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -44,7 +45,7 @@ func TestStrategy_generateMakerOrders(t *testing.T) {
} }
pricingBook := types.NewStreamBook("BTCUSDT") pricingBook := types.NewStreamBook("BTCUSDT")
pricingBook.OrderBook.Load(types.SliceOrderBook{ pricingBook.Load(types.SliceOrderBook{
Symbol: "BTCUSDT", Symbol: "BTCUSDT",
Bids: types.PriceVolumeSlice{ Bids: types.PriceVolumeSlice{
{Price: Number("25000.00"), Volume: Number("0.1")}, {Price: Number("25000.00"), Volume: Number("0.1")},
@ -61,7 +62,7 @@ func TestStrategy_generateMakerOrders(t *testing.T) {
Time: time.Now(), Time: time.Now(),
}) })
orders, err := s.generateMakerOrders(pricingBook, 0) orders, err := s.generateMakerOrders(pricingBook, 0, fixedpoint.PosInf, fixedpoint.PosInf)
assert.NoError(t, err) assert.NoError(t, err)
AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{
{Side: types.SideTypeBuy, Price: Number("25000"), Quantity: Number("0.04")}, // =~ $1000.00 {Side: types.SideTypeBuy, Price: Number("25000"), Quantity: Number("0.04")}, // =~ $1000.00

View File

@ -27,7 +27,8 @@ type MutexOrderBook struct {
sync.Mutex sync.Mutex
Symbol string Symbol string
OrderBook OrderBook
orderBook OrderBook
} }
func NewMutexOrderBook(symbol string) *MutexOrderBook { func NewMutexOrderBook(symbol string) *MutexOrderBook {
@ -39,20 +40,27 @@ func NewMutexOrderBook(symbol string) *MutexOrderBook {
return &MutexOrderBook{ return &MutexOrderBook{
Symbol: symbol, Symbol: symbol,
OrderBook: book, orderBook: book,
} }
} }
func (b *MutexOrderBook) IsValid() (ok bool, err error) { func (b *MutexOrderBook) IsValid() (ok bool, err error) {
b.Lock() b.Lock()
ok, err = b.OrderBook.IsValid() ok, err = b.orderBook.IsValid()
b.Unlock() b.Unlock()
return ok, err return ok, err
} }
func (b *MutexOrderBook) SideBook(sideType SideType) PriceVolumeSlice {
b.Lock()
sideBook := b.orderBook.SideBook(sideType)
b.Unlock()
return sideBook
}
func (b *MutexOrderBook) LastUpdateTime() time.Time { func (b *MutexOrderBook) LastUpdateTime() time.Time {
b.Lock() b.Lock()
t := b.OrderBook.LastUpdateTime() t := b.orderBook.LastUpdateTime()
b.Unlock() b.Unlock()
return t return t
} }
@ -60,8 +68,8 @@ func (b *MutexOrderBook) LastUpdateTime() time.Time {
func (b *MutexOrderBook) BestBidAndAsk() (bid, ask PriceVolume, ok bool) { func (b *MutexOrderBook) BestBidAndAsk() (bid, ask PriceVolume, ok bool) {
var ok1, ok2 bool var ok1, ok2 bool
b.Lock() b.Lock()
bid, ok1 = b.OrderBook.BestBid() bid, ok1 = b.orderBook.BestBid()
ask, ok2 = b.OrderBook.BestAsk() ask, ok2 = b.orderBook.BestAsk()
b.Unlock() b.Unlock()
ok = ok1 && ok2 ok = ok1 && ok2
return bid, ask, ok return bid, ask, ok
@ -69,48 +77,49 @@ func (b *MutexOrderBook) BestBidAndAsk() (bid, ask PriceVolume, ok bool) {
func (b *MutexOrderBook) BestBid() (pv PriceVolume, ok bool) { func (b *MutexOrderBook) BestBid() (pv PriceVolume, ok bool) {
b.Lock() b.Lock()
pv, ok = b.OrderBook.BestBid() pv, ok = b.orderBook.BestBid()
b.Unlock() b.Unlock()
return pv, ok return pv, ok
} }
func (b *MutexOrderBook) BestAsk() (pv PriceVolume, ok bool) { func (b *MutexOrderBook) BestAsk() (pv PriceVolume, ok bool) {
b.Lock() b.Lock()
pv, ok = b.OrderBook.BestAsk() pv, ok = b.orderBook.BestAsk()
b.Unlock() b.Unlock()
return pv, ok return pv, ok
} }
func (b *MutexOrderBook) Load(book SliceOrderBook) { func (b *MutexOrderBook) Load(book SliceOrderBook) {
b.Lock() b.Lock()
b.OrderBook.Load(book) b.orderBook.Load(book)
b.Unlock() b.Unlock()
} }
func (b *MutexOrderBook) Reset() { func (b *MutexOrderBook) Reset() {
b.Lock() b.Lock()
b.OrderBook.Reset() b.orderBook.Reset()
b.Unlock() b.Unlock()
} }
func (b *MutexOrderBook) CopyDepth(depth int) OrderBook { func (b *MutexOrderBook) CopyDepth(depth int) OrderBook {
b.Lock() b.Lock()
book := b.OrderBook.CopyDepth(depth) defer b.Unlock()
b.Unlock()
return book return b.orderBook.CopyDepth(depth)
} }
func (b *MutexOrderBook) Copy() OrderBook { func (b *MutexOrderBook) Copy() OrderBook {
b.Lock() b.Lock()
book := b.OrderBook.Copy() defer b.Unlock()
b.Unlock()
return book return b.orderBook.Copy()
} }
func (b *MutexOrderBook) Update(update SliceOrderBook) { func (b *MutexOrderBook) Update(update SliceOrderBook) {
b.Lock() b.Lock()
b.OrderBook.Update(update) defer b.Unlock()
b.Unlock()
b.orderBook.Update(update)
} }
type BookSignalType string type BookSignalType string

View File

@ -17,7 +17,7 @@ func (p PriceVolume) Equals(b PriceVolume) bool {
} }
func (p PriceVolume) String() string { func (p PriceVolume) String() string {
return fmt.Sprintf("PriceVolume{ price: %s, volume: %s }", p.Price.String(), p.Volume.String()) return fmt.Sprintf("PriceVolume{ Price: %s, Volume: %s }", p.Price.String(), p.Volume.String())
} }
type PriceVolumeSlice []PriceVolume type PriceVolumeSlice []PriceVolume

View File

@ -0,0 +1,27 @@
package types
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSliceOrderBook_CopyDepth(t *testing.T) {
b := &SliceOrderBook{
Bids: PriceVolumeSlice{
{Price: number(0.119), Volume: number(100.0)},
{Price: number(0.118), Volume: number(100.0)},
{Price: number(0.117), Volume: number(100.0)},
{Price: number(0.116), Volume: number(100.0)},
},
Asks: PriceVolumeSlice{
{Price: number(0.120), Volume: number(100.0)},
{Price: number(0.121), Volume: number(100.0)},
{Price: number(0.122), Volume: number(100.0)},
},
}
copied := b.CopyDepth(0)
assert.Equal(t, 3, len(copied.SideBook(SideTypeSell)))
assert.Equal(t, 4, len(copied.SideBook(SideTypeBuy)))
}