From 6464a8a7e0e5c9df07cd64b7341552e533f2f2d8 Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Fri, 12 May 2023 08:20:01 +0100 Subject: [PATCH] Binance: Fix exchange order limits not populating (#1187) * Binance: Fix Exchange Order Limits not populating The order of the filters cannot be trusted. New filters have been added after the 2nd filter, breaking all filter passing afterwards. This adds a basic test that the data is being populated, but more could be done on testing. We should have stricter typing on the filters, perhaps by unmarshalling into json.RawMessage initially and then into typed stuct fields based on the filterType. Finally we should spot both missing and unhandled filters, at least in tests. * Binance: Add TODO for unhandled filterTypes --- exchanges/binance/binance.go | 56 +++++++++++++-------- exchanges/binance/binance_test.go | 81 ++++++++++++++++++++++++++++++ exchanges/binance/binance_types.go | 79 +++++++++++++++++------------ 3 files changed, 163 insertions(+), 53 deletions(-) diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 0c04a7f9..bf010308 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -1196,30 +1196,42 @@ func (b *Binance) FetchSpotExchangeLimits(ctx context.Context) ([]order.MinMaxLe } for z := range assets { - if len(spot.Symbols[x].Filters) < 8 { - continue + l := order.MinMaxLevel{ + Pair: cp, + Asset: assets[z], } - limits = append(limits, order.MinMaxLevel{ - Pair: cp, - Asset: assets[z], - MinPrice: spot.Symbols[x].Filters[0].MinPrice, - MaxPrice: spot.Symbols[x].Filters[0].MaxPrice, - PriceStepIncrementSize: spot.Symbols[x].Filters[0].TickSize, - MultiplierUp: spot.Symbols[x].Filters[1].MultiplierUp, - MultiplierDown: spot.Symbols[x].Filters[1].MultiplierDown, - AveragePriceMinutes: spot.Symbols[x].Filters[1].AvgPriceMinutes, - MaxAmount: spot.Symbols[x].Filters[2].MaxQty, - MinAmount: spot.Symbols[x].Filters[2].MinQty, - AmountStepIncrementSize: spot.Symbols[x].Filters[2].StepSize, - MinNotional: spot.Symbols[x].Filters[3].MinNotional, - MaxIcebergParts: spot.Symbols[x].Filters[4].Limit, - MarketMinQty: spot.Symbols[x].Filters[5].MinQty, - MarketMaxQty: spot.Symbols[x].Filters[5].MaxQty, - MarketStepIncrementSize: spot.Symbols[x].Filters[5].StepSize, - MaxTotalOrders: spot.Symbols[x].Filters[6].MaxNumOrders, - MaxAlgoOrders: spot.Symbols[x].Filters[7].MaxNumAlgoOrders, - }) + for _, f := range spot.Symbols[x].Filters { + // TODO: Unhandled filters: + // maxPosition, trailingDelta, percentPriceBySide, maxNumAlgoOrders + switch f.FilterType { + case priceFilter: + l.MinPrice = f.MinPrice + l.MaxPrice = f.MaxPrice + l.PriceStepIncrementSize = f.TickSize + case percentPriceFilter: + l.MultiplierUp = f.MultiplierUp + l.MultiplierDown = f.MultiplierDown + l.AveragePriceMinutes = f.AvgPriceMinutes + case lotSizeFilter: + l.MaxAmount = f.MaxQty + l.MinAmount = f.MinQty + l.AmountStepIncrementSize = f.StepSize + case notionalFilter: + l.MinNotional = f.MinNotional + case icebergPartsFilter: + l.MaxIcebergParts = f.Limit + case marketLotSizeFilter: + l.MarketMinQty = f.MinQty + l.MarketMaxQty = f.MaxQty + l.MarketStepIncrementSize = f.StepSize + case maxNumOrdersFilter: + l.MaxTotalOrders = f.MaxNumOrders + l.MaxAlgoOrders = f.MaxNumAlgoOrders + } + } + + limits = append(limits, l) } } return limits, nil diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 1822e602..e33dca9f 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -2734,3 +2734,84 @@ func TestFetchSpotExchangeLimits(t *testing.T) { t.Error("expected a response") } } + +func TestUpdateOrderExecutionLimits(t *testing.T) { + t.Parallel() + + tests := map[asset.Item]currency.Pair{ + asset.Spot: currency.NewPair(currency.BTC, currency.USDT), + asset.Margin: currency.NewPair(currency.ETH, currency.BTC), + } + for _, a := range []asset.Item{asset.CoinMarginedFutures, asset.USDTMarginedFutures} { + pairs, err := b.FetchTradablePairs(context.Background(), a) + if err != nil { + t.Errorf("Error fetching dated %s pairs for test: %v", a, err) + } + tests[a] = pairs[0] + } + + for _, a := range b.GetAssetTypes(false) { + if err := b.UpdateOrderExecutionLimits(context.Background(), a); err != nil { + t.Error("Binance UpdateOrderExecutionLimits() error", err) + continue + } + + p := tests[a] + limits, err := b.GetOrderExecutionLimits(a, p) + if err != nil { + t.Errorf("Binance GetOrderExecutionLimits() error during TestUpdateOrderExecutionLimits; Asset: %s Pair: %s Err: %v", a, p, err) + continue + } + if limits.MinPrice == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MinPrice; Asset: %s, Pair: %s, Got: %v", a, p, limits.MinPrice) + } + if limits.MaxPrice == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MaxPrice; Asset: %s, Pair: %s, Got: %v", a, p, limits.MaxPrice) + } + if limits.PriceStepIncrementSize == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty PriceStepIncrementSize; Asset: %s, Pair: %s, Got: %v", a, p, limits.PriceStepIncrementSize) + } + if limits.MinAmount == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MinAmount; Asset: %s, Pair: %s, Got: %v", a, p, limits.MinAmount) + } + if limits.MaxAmount == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MaxAmount; Asset: %s, Pair: %s, Got: %v", a, p, limits.MaxAmount) + } + if limits.AmountStepIncrementSize == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty AmountStepIncrementSize; Asset: %s, Pair: %s, Got: %v", a, p, limits.AmountStepIncrementSize) + } + if a == asset.USDTMarginedFutures && limits.MinNotional == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MinNotional; Asset: %s, Pair: %s, Got: %v", a, p, limits.MinNotional) + } + if limits.MarketMaxQty == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MarketMaxQty; Asset: %s, Pair: %s, Got: %v", a, p, limits.MarketMaxQty) + } + if limits.MaxTotalOrders == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MaxTotalOrders; Asset: %s, Pair: %s, Got: %v", a, p, limits.MaxTotalOrders) + } + + if a == asset.Spot || a == asset.Margin { + if limits.MaxIcebergParts == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MaxIcebergParts; Asset: %s, Pair: %s, Got: %v", a, p, limits.MaxIcebergParts) + } + } + + if a == asset.CoinMarginedFutures || a == asset.USDTMarginedFutures { + if limits.MultiplierUp == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MultiplierUp; Asset: %s, Pair: %s, Got: %v", a, p, limits.MultiplierUp) + } + if limits.MultiplierDown == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MultiplierDown; Asset: %s, Pair: %s, Got: %v", a, p, limits.MultiplierDown) + } + if limits.MarketMinQty == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MarketMinQty; Asset: %s, Pair: %s, Got: %v", a, p, limits.MarketMinQty) + } + if limits.MarketStepIncrementSize == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MarketStepIncrementSize; Asset: %s, Pair: %s, Got: %v", a, p, limits.MarketStepIncrementSize) + } + if limits.MaxAlgoOrders == 0 { + t.Errorf("Binance UpdateOrderExecutionLimits empty MaxAlgoOrders; Asset: %s, Pair: %s, Got: %v", a, p, limits.MaxAlgoOrders) + } + } + } +} diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index a686e988..b742d7a1 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -22,6 +22,21 @@ const ( Completed ) +type filterType string + +const ( + priceFilter filterType = "PRICE_FILTER" + lotSizeFilter filterType = "LOT_SIZE" + icebergPartsFilter filterType = "ICEBERG_PARTS" + marketLotSizeFilter filterType = "MARKET_LOT_SIZE" + trailingDeltaFilter filterType = "TRAILING_DELTA" + percentPriceFilter filterType = "PERCENT_PRICE" + percentPriceBySizeFilter filterType = "PERCENT_PRICE_BY_SIDE" + notionalFilter filterType = "NOTIONAL" + maxNumOrdersFilter filterType = "MAX_NUM_ORDERS" + maxNumAlgoOrdersFilter filterType = "MAX_NUM_ALGO_ORDERS" +) + // ExchangeInfo holds the full exchange information type type ExchangeInfo struct { Code int `json:"code"` @@ -35,40 +50,42 @@ type ExchangeInfo struct { } `json:"rateLimits"` ExchangeFilters interface{} `json:"exchangeFilters"` Symbols []struct { - Symbol string `json:"symbol"` - Status string `json:"status"` - BaseAsset string `json:"baseAsset"` - BaseAssetPrecision int `json:"baseAssetPrecision"` - QuoteAsset string `json:"quoteAsset"` - QuotePrecision int `json:"quotePrecision"` - OrderTypes []string `json:"orderTypes"` - IcebergAllowed bool `json:"icebergAllowed"` - OCOAllowed bool `json:"ocoAllowed"` - QuoteOrderQtyMarketAllowed bool `json:"quoteOrderQtyMarketAllowed"` - IsSpotTradingAllowed bool `json:"isSpotTradingAllowed"` - IsMarginTradingAllowed bool `json:"isMarginTradingAllowed"` - Filters []struct { - FilterType string `json:"filterType"` - MinPrice float64 `json:"minPrice,string"` - MaxPrice float64 `json:"maxPrice,string"` - TickSize float64 `json:"tickSize,string"` - MultiplierUp float64 `json:"multiplierUp,string"` - MultiplierDown float64 `json:"multiplierDown,string"` - AvgPriceMinutes int64 `json:"avgPriceMins"` - MinQty float64 `json:"minQty,string"` - MaxQty float64 `json:"maxQty,string"` - StepSize float64 `json:"stepSize,string"` - MinNotional float64 `json:"minNotional,string"` - ApplyToMarket bool `json:"applyToMarket"` - Limit int64 `json:"limit"` - MaxNumAlgoOrders int64 `json:"maxNumAlgoOrders"` - MaxNumIcebergOrders int64 `json:"maxNumIcebergOrders"` - MaxNumOrders int64 `json:"maxNumOrders"` - } `json:"filters"` - Permissions []string `json:"permissions"` + Symbol string `json:"symbol"` + Status string `json:"status"` + BaseAsset string `json:"baseAsset"` + BaseAssetPrecision int `json:"baseAssetPrecision"` + QuoteAsset string `json:"quoteAsset"` + QuotePrecision int `json:"quotePrecision"` + OrderTypes []string `json:"orderTypes"` + IcebergAllowed bool `json:"icebergAllowed"` + OCOAllowed bool `json:"ocoAllowed"` + QuoteOrderQtyMarketAllowed bool `json:"quoteOrderQtyMarketAllowed"` + IsSpotTradingAllowed bool `json:"isSpotTradingAllowed"` + IsMarginTradingAllowed bool `json:"isMarginTradingAllowed"` + Filters []*filterData `json:"filters"` + Permissions []string `json:"permissions"` } `json:"symbols"` } +type filterData struct { + FilterType filterType `json:"filterType"` + MinPrice float64 `json:"minPrice,string"` + MaxPrice float64 `json:"maxPrice,string"` + TickSize float64 `json:"tickSize,string"` + MultiplierUp float64 `json:"multiplierUp,string"` + MultiplierDown float64 `json:"multiplierDown,string"` + AvgPriceMinutes int64 `json:"avgPriceMins"` + MinQty float64 `json:"minQty,string"` + MaxQty float64 `json:"maxQty,string"` + StepSize float64 `json:"stepSize,string"` + MinNotional float64 `json:"minNotional,string"` + ApplyToMarket bool `json:"applyToMarket"` + Limit int64 `json:"limit"` + MaxNumAlgoOrders int64 `json:"maxNumAlgoOrders"` + MaxNumIcebergOrders int64 `json:"maxNumIcebergOrders"` + MaxNumOrders int64 `json:"maxNumOrders"` +} + // CoinInfo stores information about all supported coins type CoinInfo struct { Coin string `json:"coin"`