grid2: add ClearDuplicatedPriceOpenOrders option

This commit is contained in:
c9s 2023-03-10 00:46:12 +08:00
parent b672889602
commit ccf567fdab
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
2 changed files with 142 additions and 23 deletions

View File

@ -6,6 +6,7 @@ import (
"math" "math"
"sort" "sort"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@ -134,6 +135,8 @@ type Strategy struct {
ClearOpenOrdersIfMismatch bool `json:"clearOpenOrdersIfMismatch"` ClearOpenOrdersIfMismatch bool `json:"clearOpenOrdersIfMismatch"`
ClearDuplicatedPriceOpenOrders bool `json:"clearDuplicatedPriceOpenOrders"`
// UseCancelAllOrdersApiWhenClose uses a different API to cancel all the orders on the market when closing a grid // UseCancelAllOrdersApiWhenClose uses a different API to cancel all the orders on the market when closing a grid
UseCancelAllOrdersApiWhenClose bool `json:"useCancelAllOrdersApiWhenClose"` UseCancelAllOrdersApiWhenClose bool `json:"useCancelAllOrdersApiWhenClose"`
@ -1158,15 +1161,35 @@ func sortOrdersByPriceAscending(orders []types.Order) []types.Order {
} }
func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice fixedpoint.Value) { func (s *Strategy) debugGridOrders(submitOrders []types.SubmitOrder, lastPrice fixedpoint.Value) {
s.logger.Infof("GRID ORDERS: [") var sb strings.Builder
sb.WriteString("GRID ORDERS [")
for i, order := range submitOrders { for i, order := range submitOrders {
if i > 0 && lastPrice.Compare(order.Price) >= 0 && lastPrice.Compare(submitOrders[i-1].Price) <= 0 { if i > 0 && lastPrice.Compare(order.Price) >= 0 && lastPrice.Compare(submitOrders[i-1].Price) <= 0 {
s.logger.Infof(" - LAST PRICE: %f", lastPrice.Float64()) sb.WriteString(fmt.Sprintf(" - LAST PRICE: %f", lastPrice.Float64()))
} }
s.logger.Info(" - ", order.String()) sb.WriteString(" - " + order.String())
} }
s.logger.Infof("] END OF GRID ORDERS") sb.WriteString("] END OF GRID ORDERS")
s.logger.Infof(sb.String())
}
func (s *Strategy) debugOrders(desc string, orders []types.Order) {
var sb strings.Builder
if desc == "" {
desc = "ORDERS"
}
sb.WriteString(desc + " [")
for i, order := range orders {
sb.WriteString(fmt.Sprintf(" - %d) %s", i, order.String()))
}
sb.WriteString("]")
s.logger.Infof(sb.String())
} }
func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoint.Value) ([]types.SubmitOrder, error) { func (s *Strategy) generateGridOrders(totalQuote, totalBase, lastPrice fixedpoint.Value) ([]types.SubmitOrder, error) {
@ -1884,6 +1907,13 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
} }
} }
} }
if s.ClearDuplicatedPriceOpenOrders {
s.logger.Infof("clearDuplicatedPriceOpenOrders is set, finding duplicated open orders...")
if err := s.cancelDuplicatedPriceOpenOrders(ctx, session); err != nil {
s.logger.WithError(err).Errorf("cancelDuplicatedPriceOpenOrders error")
}
}
}) })
// if TriggerPrice is zero, that means we need to open the grid when start up // if TriggerPrice is zero, that means we need to open the grid when start up
@ -2020,11 +2050,55 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha
return false, nil return false, nil
} }
func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (fixedpoint.Value, int) { func (s *Strategy) cancelDuplicatedPriceOpenOrders(ctx context.Context, session *bbgo.ExchangeSession) error {
prec := market.VolumePrecision openOrders, err := session.Exchange.QueryOpenOrders(ctx, s.Symbol)
if c == market.QuoteCurrency { if err != nil {
prec = market.PricePrecision return err
} }
return v.Round(prec, fixedpoint.Up), prec if len(openOrders) == 0 {
return nil
}
dupOrders := s.findDuplicatedPriceOpenOrders(openOrders)
if len(dupOrders) > 0 {
s.debugOrders("DUPLICATED ORDERS", dupOrders)
return session.Exchange.CancelOrders(ctx, dupOrders...)
}
s.logger.Infof("no duplicated order found")
return nil
}
func (s *Strategy) findDuplicatedPriceOpenOrders(openOrders []types.Order) (dupOrders []types.Order) {
orderBook := bbgo.NewActiveOrderBook(s.Symbol)
for _, openOrder := range openOrders {
existingOrder := orderBook.Lookup(func(o types.Order) bool {
return o.Price.Compare(openOrder.Price) == 0
})
if existingOrder != nil {
// found duplicated order
// compare creation time and remove the latest created order
// if the creation time equals, then we can just cancel one of them
s.debugOrders(
fmt.Sprintf("found duplicated order at price %s, comparing orders", openOrder.Price.String()),
[]types.Order{*existingOrder, openOrder})
dupOrder := *existingOrder
if openOrder.CreationTime.After(existingOrder.CreationTime.Time()) {
dupOrder = openOrder
} else if openOrder.CreationTime.Before(existingOrder.CreationTime.Time()) {
// override the existing order and take the existing order as a duplicated one
orderBook.Add(openOrder)
}
dupOrders = append(dupOrders, dupOrder)
} else {
orderBook.Add(openOrder)
}
}
return dupOrders
} }

View File

@ -355,11 +355,10 @@ func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.InDelta(t, 0.099992, quantity.Float64(), 0.0001) assert.InDelta(t, 0.099992, quantity.Float64(), 0.0001)
}) })
} }
func newTestStrategy() *Strategy { func newTestMarket() types.Market {
market := types.Market{ return types.Market{
BaseCurrency: "BTC", BaseCurrency: "BTC",
QuoteCurrency: "USDT", QuoteCurrency: "USDT",
TickSize: number(0.01), TickSize: number(0.01),
@ -368,6 +367,36 @@ func newTestStrategy() *Strategy {
MinNotional: number(10.0), MinNotional: number(10.0),
MinQuantity: number(0.001), MinQuantity: number(0.001),
} }
}
var testOrderID = uint64(0)
func newTestOrder(price, quantity fixedpoint.Value, side types.SideType) types.Order {
market := newTestMarket()
testOrderID++
return types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT",
Side: side,
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
AveragePrice: fixedpoint.Zero,
StopPrice: fixedpoint.Zero,
Market: market,
TimeInForce: types.TimeInForceGTC,
},
Exchange: "binance",
GID: testOrderID,
OrderID: testOrderID,
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
}
}
func newTestStrategy() *Strategy {
market := newTestMarket()
s := &Strategy{ s := &Strategy{
logger: logrus.NewEntry(logrus.New()), logger: logrus.NewEntry(logrus.New()),
@ -500,6 +529,33 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) {
assert.Equal(t, "0.01", baseFee.String()) assert.Equal(t, "0.01", baseFee.String())
} }
func TestStrategy_findDuplicatedPriceOpenOrders(t *testing.T) {
t.Run("no duplicated open orders", func(t *testing.T) {
s := newTestStrategy()
s.grid = s.newGrid()
dupOrders := s.findDuplicatedPriceOpenOrders([]types.Order{
newTestOrder(number(1900.0), number(0.1), types.SideTypeSell),
newTestOrder(number(1800.0), number(0.1), types.SideTypeSell),
newTestOrder(number(1700.0), number(0.1), types.SideTypeSell),
})
assert.Empty(t, dupOrders)
assert.Len(t, dupOrders, 0)
})
t.Run("1 duplicated open order SELL", func(t *testing.T) {
s := newTestStrategy()
s.grid = s.newGrid()
dupOrders := s.findDuplicatedPriceOpenOrders([]types.Order{
newTestOrder(number(1900.0), number(0.1), types.SideTypeSell),
newTestOrder(number(1900.0), number(0.1), types.SideTypeSell),
newTestOrder(number(1800.0), number(0.1), types.SideTypeSell),
})
assert.Len(t, dupOrders, 1)
})
}
func TestStrategy_handleOrderFilled(t *testing.T) { func TestStrategy_handleOrderFilled(t *testing.T) {
ctx := context.Background() ctx := context.Background()
@ -971,17 +1027,6 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {
}) })
} }
func Test_roundUpMarketQuantity(t *testing.T) {
q := number("0.00000003")
assert.Equal(t, "0.00000003", q.String())
q3, prec := roundUpMarketQuantity(types.Market{
VolumePrecision: 8,
}, q, "BTC")
assert.Equal(t, "0.00000003", q3.String(), "rounding prec 8")
assert.Equal(t, 8, prec)
}
func Test_buildPinOrderMap(t *testing.T) { func Test_buildPinOrderMap(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
s := newTestStrategy() s := newTestStrategy()