From 881bab2d5a370f83879d0b8c74b2fbc4cb483966 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Thu, 25 Mar 2021 15:47:15 +1100 Subject: [PATCH] Exchanges: Add in exchange defined tolerance settings (#647) * Exchanges: Add in exchange defined tolerance settings to conform to min max amounts/price/notional etc (Initial) * Add to tests fix linter * Binance: Implement CMF and usdtMarginFutures fetching of currency information, addr nits * binance: Add in test for tolerance set up * exchanges: add in more tolerance settings and add tests * nits: addr * fix linter issue * RPCServer: Use ordermanager instead of going direct to exchange * Nits: Addr * nits: glorious addr phase one * nits: glorious nits phase 2 * exchange: move tolerance -> limits in order package add wrapper function, split binance functions to asset files * nits: Addr thrasher + also include locking of limits struct when we update via syncer later on * nits: mdc addr * nits: glorious nits * limits: unexport mutex * limit: revert maths optim. and fix spelling * limit: Add decimal package * limit: don't check price on market order * Orders: Add order execution checks on fake orders so as to always conform to tight specifications even in simulation * binance: handle case where spot is not enabled but margin is * backtester: add in amount conforming to back tested events to simulate realistic orders * rm ln * order limit: return amount when limit is nil and conformToAmount is requested * nits: glorious nits + friends * backtester/orders: fix tests * nits: glorious nits * nits: glorious nits * RMLINE * nits: more glorious nits! * nits: pooosh * binance: fix margin logic * nits: Add warning, settings log and report item for exchange order execution limits * backtester: add specific warnings in report output * backtest: Adjust warnings --- backtester/backtest/backtest.go | 23 +- backtester/config/config.go | 1 + backtester/config/config_types.go | 3 + backtester/config/configbuilder/main.go | 5 + .../dca-api-candles-multiple-currencies.strat | 6 +- ...-api-candles-simultaneous-processing.strat | 6 +- .../config/examples/dca-api-candles.strat | 3 +- .../config/examples/dca-api-trades.strat | 3 +- .../config/examples/dca-candles-live.strat | 3 +- .../config/examples/dca-csv-candles.strat | 3 +- .../config/examples/dca-csv-trades.strat | 3 +- .../examples/dca-database-candles.strat | 3 +- .../config/examples/rsi-api-candles.strat | 6 +- backtester/eventhandlers/exchange/exchange.go | 29 +- .../eventhandlers/exchange/exchange_test.go | 66 +-- .../eventhandlers/exchange/exchange_types.go | 5 +- backtester/report/tpl.gohtml | 27 ++ cmd/exchange_wrapper_coverage/main.go | 57 +-- cmd/exchange_wrapper_issues/main.go | 20 +- .../wrapperconfig.json | 3 +- engine/orders.go | 35 +- engine/rpcserver.go | 7 +- exchanges/binance/binance.go | 60 +++ exchanges/binance/binance_cfutures.go | 44 ++ exchanges/binance/binance_test.go | 48 +++ exchanges/binance/binance_types.go | 4 +- exchanges/binance/binance_ufutures.go | 44 ++ exchanges/binance/binance_wrapper.go | 42 ++ exchanges/binance/cfutures_types.go | 2 + exchanges/exchange.go | 5 + exchanges/exchange_types.go | 2 + exchanges/interfaces.go | 5 +- exchanges/kraken/kraken_wrapper.go | 3 + exchanges/order/limits.go | 381 ++++++++++++++++++ exchanges/order/limits_test.go | 319 +++++++++++++++ exchanges/order/order_test.go | 8 +- exchanges/order/order_types.go | 2 +- exchanges/order/orders.go | 9 +- go.mod | 1 + go.sum | 2 + 40 files changed, 1193 insertions(+), 105 deletions(-) create mode 100644 exchanges/order/limits.go create mode 100644 exchanges/order/limits_test.go diff --git a/backtester/backtest/backtest.go b/backtester/backtest/backtest.go index 66e26ea3..fc6399fa 100644 --- a/backtester/backtest/backtest.go +++ b/backtester/backtest/backtest.go @@ -69,9 +69,8 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine. if bot == nil { return nil, errNilBot } - bt := New() - var e exchange.Exchange + bt := New() bt.Datas = &data.HandlerPerCurrency{} bt.EventQueue = &eventholder.Holder{} reports := &report.Data{ @@ -86,7 +85,7 @@ func NewFromConfig(cfg *config.Config, templatePath, output string, bot *engine. return nil, err } - e, err = bt.setupExchangeSettings(cfg) + e, err := bt.setupExchangeSettings(cfg) if err != nil { return nil, err } @@ -280,6 +279,22 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange MaximumTotal: cfg.CurrencySettings[i].SellSide.MaximumTotal, } sellRule.Validate() + + limits, err := exch.GetOrderExecutionLimits(a, pair) + if err != nil { + return resp, err + } + + if limits != nil { + if !cfg.CurrencySettings[i].CanUseExchangeLimits { + log.Warnf(log.BackTester, "exchange %s order execution limits supported but disabled for %s %s, results may not work when in production", + cfg.CurrencySettings[i].ExchangeName, + pair, + a) + cfg.CurrencySettings[i].ShowExchangeOrderLimitWarning = true + } + } + resp.CurrencySettings = append(resp.CurrencySettings, exchange.Settings{ ExchangeName: cfg.CurrencySettings[i].ExchangeName, InitialFunds: cfg.CurrencySettings[i].InitialFunds, @@ -298,6 +313,8 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (exchange.Exchange MaximumLeverageRate: cfg.CurrencySettings[i].Leverage.MaximumLeverageRate, MaximumOrdersWithLeverageRatio: cfg.CurrencySettings[i].Leverage.MaximumOrdersWithLeverageRatio, }, + Limits: limits, + CanUseExchangeLimits: cfg.CurrencySettings[i].CanUseExchangeLimits, }) } diff --git a/backtester/config/config.go b/backtester/config/config.go index 3942ea63..ed41f70d 100644 --- a/backtester/config/config.go +++ b/backtester/config/config.go @@ -65,6 +65,7 @@ func (c *Config) PrintSetting() { log.Infof(log.BackTester, "Buy rules: %+v", c.CurrencySettings[i].BuySide) log.Infof(log.BackTester, "Sell rules: %+v", c.CurrencySettings[i].SellSide) log.Infof(log.BackTester, "Leverage rules: %+v", c.CurrencySettings[i].Leverage) + log.Infof(log.BackTester, "Can use exchange defined order execution limits: %+v", c.CurrencySettings[i].CanUseExchangeLimits) } log.Info(log.BackTester, "-------------------------------------------------------------") log.Info(log.BackTester, "------------------Portfolio Settings-------------------------") diff --git a/backtester/config/config_types.go b/backtester/config/config_types.go index fb7de5cb..04c394e5 100644 --- a/backtester/config/config_types.go +++ b/backtester/config/config_types.go @@ -104,6 +104,9 @@ type CurrencySettings struct { TakerFee float64 `json:"taker-fee-override"` MaximumHoldingsRatio float64 `json:"maximum-holdings-ratio"` + + CanUseExchangeLimits bool `json:"use-exchange-order-limits"` + ShowExchangeOrderLimitWarning bool `json:"-"` } // APIData defines all fields to configure API based data diff --git a/backtester/config/configbuilder/main.go b/backtester/config/configbuilder/main.go index 1ee51006..618ca84d 100644 --- a/backtester/config/configbuilder/main.go +++ b/backtester/config/configbuilder/main.go @@ -588,6 +588,11 @@ func addCurrencySetting(reader *bufio.Reader) (*config.CurrencySettings, error) return nil, err } } + fmt.Println("Will the in-sample data amounts conform to current exchange defined order execution limits? i.e. If amount is 1337.001345 and the step size is 0.01 order amount will be re-adjusted to 1337. y/n") + yn = quickParse(reader) + if yn == y || yn == yes { + setting.CanUseExchangeLimits = true + } fmt.Println("Do you wish to include slippage? y/n") yn = quickParse(reader) if yn == y || yn == yes { diff --git a/backtester/config/examples/dca-api-candles-multiple-currencies.strat b/backtester/config/examples/dca-api-candles-multiple-currencies.strat index 53b34d18..1c70ccbc 100644 --- a/backtester/config/examples/dca-api-candles-multiple-currencies.strat +++ b/backtester/config/examples/dca-api-candles-multiple-currencies.strat @@ -32,7 +32,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false }, { "exchange-name": "binance", @@ -59,7 +60,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false } ], "data-settings": { diff --git a/backtester/config/examples/dca-api-candles-simultaneous-processing.strat b/backtester/config/examples/dca-api-candles-simultaneous-processing.strat index 43f548a7..ee763d66 100644 --- a/backtester/config/examples/dca-api-candles-simultaneous-processing.strat +++ b/backtester/config/examples/dca-api-candles-simultaneous-processing.strat @@ -32,7 +32,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false }, { "exchange-name": "binance", @@ -59,7 +60,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false } ], "data-settings": { diff --git a/backtester/config/examples/dca-api-candles.strat b/backtester/config/examples/dca-api-candles.strat index 5b975fe2..b5728d10 100644 --- a/backtester/config/examples/dca-api-candles.strat +++ b/backtester/config/examples/dca-api-candles.strat @@ -32,7 +32,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false } ], "data-settings": { diff --git a/backtester/config/examples/dca-api-trades.strat b/backtester/config/examples/dca-api-trades.strat index 12e0e977..83053cae 100644 --- a/backtester/config/examples/dca-api-trades.strat +++ b/backtester/config/examples/dca-api-trades.strat @@ -32,7 +32,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false } ], "data-settings": { diff --git a/backtester/config/examples/dca-candles-live.strat b/backtester/config/examples/dca-candles-live.strat index 14e4f74e..870727db 100644 --- a/backtester/config/examples/dca-candles-live.strat +++ b/backtester/config/examples/dca-candles-live.strat @@ -32,7 +32,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": true } ], "data-settings": { diff --git a/backtester/config/examples/dca-csv-candles.strat b/backtester/config/examples/dca-csv-candles.strat index cafe3ec1..8bff4f2d 100644 --- a/backtester/config/examples/dca-csv-candles.strat +++ b/backtester/config/examples/dca-csv-candles.strat @@ -32,7 +32,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false } ], "data-settings": { diff --git a/backtester/config/examples/dca-csv-trades.strat b/backtester/config/examples/dca-csv-trades.strat index 23cccca1..1f019916 100644 --- a/backtester/config/examples/dca-csv-trades.strat +++ b/backtester/config/examples/dca-csv-trades.strat @@ -32,7 +32,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false } ], "data-settings": { diff --git a/backtester/config/examples/dca-database-candles.strat b/backtester/config/examples/dca-database-candles.strat index befc91f9..05a29487 100644 --- a/backtester/config/examples/dca-database-candles.strat +++ b/backtester/config/examples/dca-database-candles.strat @@ -32,7 +32,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false } ], "data-settings": { diff --git a/backtester/config/examples/rsi-api-candles.strat b/backtester/config/examples/rsi-api-candles.strat index 515dc90b..4ab686c4 100644 --- a/backtester/config/examples/rsi-api-candles.strat +++ b/backtester/config/examples/rsi-api-candles.strat @@ -36,7 +36,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false }, { "exchange-name": "binance", @@ -63,7 +64,8 @@ "max-slippage-percent": 0, "maker-fee-override": 0.001, "taker-fee-override": 0.002, - "maximum-holdings-ratio": 0 + "maximum-holdings-ratio": 0, + "use-exchange-order-limits": false } ], "data-settings": { diff --git a/backtester/eventhandlers/exchange/exchange.go b/backtester/eventhandlers/exchange/exchange.go index 3302afe3..f0904596 100644 --- a/backtester/eventhandlers/exchange/exchange.go +++ b/backtester/eventhandlers/exchange/exchange.go @@ -90,11 +90,30 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, bot *engine.En f.AppendReason(fmt.Sprintf("Order size shrunk from %v to %v to remain within portfolio limits", amount, reducedAmount)) } - var orderID string - orderID, err = e.placeOrder(adjustedPrice, reducedAmount, cs.UseRealOrders, f, bot) + var limitReducedAmount float64 + if cs.CanUseExchangeLimits { + // Conforms the amount to the exchange order defined step amount + // reducing it when needed + limitReducedAmount = cs.Limits.ConformToAmount(reducedAmount) + if limitReducedAmount != reducedAmount { + f.AppendReason(fmt.Sprintf("Order size shrunk from %v to %v to remain within exchange step amount limits", + reducedAmount, + limitReducedAmount)) + } + } else { + limitReducedAmount = reducedAmount + } + + orderID, err := e.placeOrder(adjustedPrice, limitReducedAmount, cs.UseRealOrders, cs.CanUseExchangeLimits, f, bot) if err != nil { + if f.GetDirection() == gctorder.Buy { + f.SetDirection(common.CouldNotBuy) + } else if f.GetDirection() == gctorder.Sell { + f.SetDirection(common.CouldNotSell) + } return f, err } + ords, _ := bot.OrderManager.GetOrdersSnapshot("") for i := range ords { if ords[i].ID != orderID { @@ -105,7 +124,7 @@ func (e *Exchange) ExecuteOrder(o order.Event, data data.Handler, bot *engine.En ords[i].CloseTime = o.GetTime() f.Order = &ords[i] f.PurchasePrice = ords[i].Price - f.Total = (f.PurchasePrice * reducedAmount) + f.ExchangeFee + f.Total = (f.PurchasePrice * limitReducedAmount) + f.ExchangeFee } if f.Order == nil { @@ -124,7 +143,7 @@ func reduceAmountToFitPortfolioLimit(adjustedPrice, amount, sizedPortfolioTotal return amount } -func (e *Exchange) placeOrder(price, amount float64, useRealOrders bool, f *fill.Fill, bot *engine.Engine) (string, error) { +func (e *Exchange) placeOrder(price, amount float64, useRealOrders, useExchangeLimits bool, f *fill.Fill, bot *engine.Engine) (string, error) { if f == nil { return "", common.ErrNilEvent } @@ -164,7 +183,7 @@ func (e *Exchange) placeOrder(price, amount float64, useRealOrders bool, f *fill Cost: price, FullyMatched: true, } - resp, err := bot.OrderManager.SubmitFakeOrder(o, submitResponse) + resp, err := bot.OrderManager.SubmitFakeOrder(o, submitResponse, useExchangeLimits) if resp != nil { orderID = resp.OrderID } diff --git a/backtester/eventhandlers/exchange/exchange_test.go b/backtester/eventhandlers/exchange/exchange_test.go index e32a3b25..97ed4d98 100644 --- a/backtester/eventhandlers/exchange/exchange_test.go +++ b/backtester/eventhandlers/exchange/exchange_test.go @@ -148,30 +148,30 @@ func TestPlaceOrder(t *testing.T) { t.Error(err) } e := Exchange{} - _, err = e.placeOrder(1, 1, false, nil, nil) + _, err = e.placeOrder(1, 1, false, true, nil, nil) if !errors.Is(err, common.ErrNilEvent) { t.Errorf("expected: %v, received %v", common.ErrNilEvent, err) } f := &fill.Fill{} - _, err = e.placeOrder(1, 1, false, f, bot) + _, err = e.placeOrder(1, 1, false, true, f, bot) if err != nil && err.Error() != "order exchange name must be specified" { t.Error(err) } f.Exchange = testExchange - _, err = e.placeOrder(1, 1, false, f, bot) - if err != nil && err.Error() != "order pair is empty" { - t.Error(err) + _, err = e.placeOrder(1, 1, false, true, f, bot) + if !errors.Is(err, gctorder.ErrPairIsEmpty) { + t.Errorf("expected: %v, received %v", gctorder.ErrPairIsEmpty, err) } f.CurrencyPair = currency.NewPair(currency.BTC, currency.USDT) f.AssetType = asset.Spot f.Direction = gctorder.Buy - _, err = e.placeOrder(1, 1, false, f, bot) + _, err = e.placeOrder(1, 1, false, true, f, bot) if err != nil { t.Error(err) } - _, err = e.placeOrder(1, 1, true, f, bot) + _, err = e.placeOrder(1, 1, true, true, f, bot) if err != nil && !strings.Contains(err.Error(), "unset/default API keys") { t.Error(err) } @@ -179,8 +179,36 @@ func TestPlaceOrder(t *testing.T) { func TestExecuteOrder(t *testing.T) { t.Parallel() + bot, err := engine.NewFromSettings(&engine.Settings{ + ConfigFile: filepath.Join("..", "..", "..", "testdata", "configtest.json"), + EnableDryRun: true, + }, nil) + if err != nil { + t.Fatal(err) + } + + err = bot.OrderManager.Start(bot) + if err != nil { + t.Error(err) + } + err = bot.LoadExchange(testExchange, false, nil) + if err != nil { + t.Error(err) + } + b := bot.GetExchangeByName(testExchange) + p := currency.NewPair(currency.BTC, currency.USDT) a := asset.Spot + _, err = b.FetchOrderbook(p, a) + if err != nil { + t.Fatal(err) + } + + limits, err := b.GetOrderExecutionLimits(a, p) + if err != nil { + t.Fatal(err) + } + cs := Settings{ ExchangeName: testExchange, UseRealOrders: false, @@ -195,6 +223,7 @@ func TestExecuteOrder(t *testing.T) { Leverage: config.Leverage{}, MinimumSlippageRate: 0, MaximumSlippageRate: 1, + Limits: limits, } e := Exchange{ CurrencySettings: []Settings{cs}, @@ -209,30 +238,10 @@ func TestExecuteOrder(t *testing.T) { o := &order.Order{ Base: ev, Direction: gctorder.Buy, - Amount: 1, + Amount: 10, Funds: 1337, } - bot, err := engine.NewFromSettings(&engine.Settings{ - ConfigFile: filepath.Join("..", "..", "..", "testdata", "configtest.json"), - EnableDryRun: true, - }, nil) - if err != nil { - t.Fatal(err) - } - err = bot.OrderManager.Start(bot) - if err != nil { - t.Error(err) - } - err = bot.LoadExchange(testExchange, false, nil) - if err != nil { - t.Error(err) - } - b := bot.GetExchangeByName(testExchange) - _, err = b.FetchOrderbook(p, a) - if err != nil { - t.Fatal(err) - } d := &kline.DataFromKline{ Item: gctkline.Item{ Exchange: "", @@ -260,6 +269,7 @@ func TestExecuteOrder(t *testing.T) { } cs.UseRealOrders = true + cs.CanUseExchangeLimits = true o.Direction = gctorder.Sell e.CurrencySettings = []Settings{cs} _, err = e.ExecuteOrder(o, d, bot) diff --git a/backtester/eventhandlers/exchange/exchange_types.go b/backtester/eventhandlers/exchange/exchange_types.go index ed51eb3b..8c7c90a8 100644 --- a/backtester/eventhandlers/exchange/exchange_types.go +++ b/backtester/eventhandlers/exchange/exchange_types.go @@ -10,11 +10,11 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) var ( errDataMayBeIncorrect = errors.New("data may be incorrect") - errExchangeUnset = errors.New("exchange unset") ) // ExecutionHandler interface dictates what functions are required to submit an order @@ -51,4 +51,7 @@ type Settings struct { MinimumSlippageRate float64 MaximumSlippageRate float64 + + Limits *gctorder.Limits + CanUseExchangeLimits bool } diff --git a/backtester/report/tpl.gohtml b/backtester/report/tpl.gohtml index 60e4d127..24bb093f 100644 --- a/backtester/report/tpl.gohtml +++ b/backtester/report/tpl.gohtml @@ -254,6 +254,33 @@ +
+

Warnings

+
+
+ + + + + + + + + + {{ range .Config.CurrencySettings}} + {{if .ShowExchangeOrderLimitWarning}} + + + + + + + + {{end}} + {{end}} + +
Exchange NameAssetCurrency BaseCurrency QuoteWarning
{{.ExchangeName}}{{.Asset}}{{.Base}}{{.Quote}}order execution limits supported but disabled, results may not work when in production
+
diff --git a/cmd/exchange_wrapper_coverage/main.go b/cmd/exchange_wrapper_coverage/main.go index c8d631e5..729124e5 100644 --- a/cmd/exchange_wrapper_coverage/main.go +++ b/cmd/exchange_wrapper_coverage/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "log" "math/rand" "sync" @@ -83,133 +84,137 @@ func testWrappers(e exchange.IBotExchange) []string { var funcs []string _, err := e.FetchTicker(p, assetType) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "FetchTicker") } _, err = e.UpdateTicker(p, assetType) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "UpdateTicker") } _, err = e.FetchOrderbook(p, assetType) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "FetchOrderbook") } _, err = e.UpdateOrderbook(p, assetType) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "UpdateOrderbook") } _, err = e.FetchTradablePairs(asset.Spot) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "FetchTradablePairs") } err = e.UpdateTradablePairs(false) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "UpdateTradablePairs") } _, err = e.FetchAccountInfo(assetType) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetAccountInfo") } _, err = e.GetRecentTrades(p, assetType) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetRecentTrades") } _, err = e.GetHistoricTrades(p, assetType, time.Time{}, time.Time{}) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetHistoricTrades") } _, err = e.GetFundingHistory() - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetFundingHistory") } _, err = e.SubmitOrder(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "SubmitOrder") } _, err = e.ModifyOrder(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "ModifyOrder") } err = e.CancelOrder(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "CancelOrder") } _, err = e.CancelBatchOrders(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "CancelBatchOrders") } _, err = e.CancelAllOrders(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "CancelAllOrders") } _, err = e.GetOrderInfo("1", p, assetType) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetOrderInfo") } _, err = e.GetOrderHistory(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetOrderHistory") } _, err = e.GetActiveOrders(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetActiveOrders") } _, err = e.GetDepositAddress(currency.BTC, "") - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetDepositAddress") } _, err = e.WithdrawCryptocurrencyFunds(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "WithdrawCryptocurrencyFunds") } _, err = e.WithdrawFiatFunds(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "WithdrawFiatFunds") } _, err = e.WithdrawFiatFundsToInternationalBank(nil) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "WithdrawFiatFundsToInternationalBank") } _, err = e.GetHistoricCandles(currency.Pair{}, asset.Spot, time.Unix(0, 0), time.Unix(0, 0), kline.OneDay) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetHistoricCandles") } _, err = e.GetHistoricCandlesExtended(currency.Pair{}, asset.Spot, time.Unix(0, 0), time.Unix(0, 0), kline.OneDay) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetHistoricCandlesExtended") } _, err = e.UpdateAccountInfo(assetType) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "UpdateAccountInfo") } _, err = e.GetFeeByType(&exchange.FeeBuilder{}) - if err == common.ErrNotYetImplemented { + if errors.Is(err, common.ErrNotYetImplemented) { funcs = append(funcs, "GetFeeByType") } + err = e.UpdateOrderExecutionLimits(asset.DownsideProfitContract) + if errors.Is(err, common.ErrNotYetImplemented) { + funcs = append(funcs, "UpdateOrderExecutionLimits") + } return funcs } diff --git a/cmd/exchange_wrapper_issues/main.go b/cmd/exchange_wrapper_issues/main.go index 6e6238fc..d9551e83 100644 --- a/cmd/exchange_wrapper_issues/main.go +++ b/cmd/exchange_wrapper_issues/main.go @@ -420,14 +420,14 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) }) var getHistoricTradesResponse []trade.Data - getHistoricTradesResponse, err = e.GetHistoricTrades(p, assetTypes[i], time.Now().Add(-time.Hour*24), time.Now()) + getHistoricTradesResponse, err = e.GetHistoricTrades(p, assetTypes[i], time.Now().Add(-time.Hour), time.Now()) msg = "" if err != nil { msg = err.Error() responseContainer.ErrorCount++ } responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ - SentParams: jsonifyInterface([]interface{}{p, assetTypes[i], time.Now().Add(-time.Hour * 24), time.Now()}), + SentParams: jsonifyInterface([]interface{}{p, assetTypes[i], time.Now().Add(-time.Hour), time.Now()}), Function: "GetHistoricTrades", Error: msg, Response: jsonifyInterface([]interface{}{getHistoricTradesResponse}), @@ -448,7 +448,7 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) }) var getHistoricCandlesResponse kline.Item - startTime, endTime := time.Now().AddDate(0, -1, 0), time.Now() + startTime, endTime := time.Now().AddDate(0, 0, -1), time.Now() getHistoricCandlesResponse, err = e.GetHistoricCandles(p, assetTypes[i], startTime, endTime, kline.OneDay) msg = "" if err != nil { @@ -475,6 +475,20 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) Response: getHisotirCandlesExtendedResponse, SentParams: jsonifyInterface([]interface{}{p, assetTypes[i], startTime, endTime, kline.OneDay}), }) + + err = e.UpdateOrderExecutionLimits(assetTypes[i]) + msg = "" + if err != nil { + msg = err.Error() + responseContainer.ErrorCount++ + } + + responseContainer.EndpointResponses = append(responseContainer.EndpointResponses, EndpointResponse{ + SentParams: jsonifyInterface([]interface{}{assetTypes[i]}), + Function: "UpdateOrderExecutionLimits", + Error: msg, + Response: jsonifyInterface([]interface{}{""}), + }) } var fetchAccountInfoResponse account.Holdings diff --git a/cmd/exchange_wrapper_issues/wrapperconfig.json b/cmd/exchange_wrapper_issues/wrapperconfig.json index b77b1b86..d73e905e 100644 --- a/cmd/exchange_wrapper_issues/wrapperconfig.json +++ b/cmd/exchange_wrapper_issues/wrapperconfig.json @@ -4,7 +4,8 @@ "orderType": "LIMIT", "amount": 1333333337, "price": 1333333337, - "orderID": "" + "orderID": "", + "assetType": "" }, "withdrawWalletAddress": "", "bankAccount": { diff --git a/engine/orders.go b/engine/orders.go index 12b5c2b8..2a80c3e8 100644 --- a/engine/orders.go +++ b/engine/orders.go @@ -324,7 +324,7 @@ func (o *orderManager) validate(newOrder *order.Submit) error { } if err := newOrder.Validate(); err != nil { - return err + return fmt.Errorf("order manager: %w", err) } if o.cfg.EnforceLimitConfig { @@ -359,8 +359,21 @@ func (o *orderManager) Submit(newOrder *order.Submit) (*orderSubmitResponse, err if exch == nil { return nil, ErrExchangeNotFound } - var result order.SubmitResponse - result, err = exch.SubmitOrder(newOrder) + + // Checks for exchange min max limits for order amounts before order + // execution can occur + err = exch.CheckOrderExecutionLimits(newOrder.AssetType, + newOrder.Pair, + newOrder.Price, + newOrder.Amount, + newOrder.Type) + if err != nil { + return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w", + newOrder.Exchange, + err) + } + + result, err := exch.SubmitOrder(newOrder) if err != nil { return nil, err } @@ -370,7 +383,7 @@ func (o *orderManager) Submit(newOrder *order.Submit) (*orderSubmitResponse, err // SubmitFakeOrder runs through the same process as order submission // but does not touch live endpoints -func (o *orderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder order.SubmitResponse) (*orderSubmitResponse, error) { +func (o *orderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder order.SubmitResponse, checkExchangeLimits bool) (*orderSubmitResponse, error) { err := o.validate(newOrder) if err != nil { return nil, err @@ -380,6 +393,20 @@ func (o *orderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder or return nil, ErrExchangeNotFound } + if checkExchangeLimits { + // Checks for exchange min max limits for order amounts before order + // execution can occur + err = exch.CheckOrderExecutionLimits(newOrder.AssetType, + newOrder.Pair, + newOrder.Price, + newOrder.Amount, + newOrder.Type) + if err != nil { + return nil, fmt.Errorf("order manager: exchange %s unable to place order: %w", + newOrder.Exchange, + err) + } + } return o.processSubmittedOrder(newOrder, resultingOrder) } diff --git a/engine/rpcserver.go b/engine/rpcserver.go index de17629a..218cfc16 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -986,11 +986,6 @@ func (s *RPCServer) GetOrder(_ context.Context, r *gctrpc.GetOrderRequest) (*gct // SubmitOrder submits an order specified by exchange, currency pair and asset // type func (s *RPCServer) SubmitOrder(_ context.Context, r *gctrpc.SubmitOrderRequest) (*gctrpc.SubmitOrderResponse, error) { - exch := s.GetExchangeByName(r.Exchange) - if exch == nil { - return nil, errExchangeNotLoaded - } - p, err := currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote) if err != nil { return nil, err @@ -1012,7 +1007,7 @@ func (s *RPCServer) SubmitOrder(_ context.Context, r *gctrpc.SubmitOrderRequest) AssetType: a, } - resp, err := exch.SubmitOrder(submission) + resp, err := s.OrderManager.Submit(submission) if err != nil { return &gctrpc.SubmitOrderResponse{}, err } diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 24319300..5fcfa572 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -940,3 +941,62 @@ func (b *Binance) MaintainWsAuthStreamKey() error { HTTPRecording: b.HTTPRecording, }) } + +// FetchSpotExchangeLimits fetches spot order execution limits +func (b *Binance) FetchSpotExchangeLimits() ([]order.MinMaxLevel, error) { + var limits []order.MinMaxLevel + spot, err := b.GetExchangeInfo() + if err != nil { + return nil, err + } + + for x := range spot.Symbols { + var cp currency.Pair + cp, err = currency.NewPairFromStrings(spot.Symbols[x].BaseAsset, + spot.Symbols[x].QuoteAsset) + if err != nil { + return nil, err + } + var assets []asset.Item + for y := range spot.Symbols[x].Permissions { + switch spot.Symbols[x].Permissions[y] { + case "SPOT": + assets = append(assets, asset.Spot) + case "MARGIN": + assets = append(assets, asset.Margin) + case "LEVERAGED": // leveraged tokens not available for spot trading + default: + return nil, fmt.Errorf("unhandled asset type for exchange limits loading %s", + spot.Symbols[x].Permissions[y]) + } + } + + for z := range assets { + if len(spot.Symbols[x].Filters) < 8 { + continue + } + + limits = append(limits, order.MinMaxLevel{ + Pair: cp, + Asset: assets[z], + MinPrice: spot.Symbols[x].Filters[0].MinPrice, + MaxPrice: spot.Symbols[x].Filters[0].MaxPrice, + StepPrice: 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, + StepAmount: 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, + MarketStepSize: spot.Symbols[x].Filters[5].StepSize, + MaxTotalOrders: spot.Symbols[x].Filters[6].MaxNumOrders, + MaxAlgoOrders: spot.Symbols[x].Filters[7].MaxNumAlgoOrders, + }) + } + } + return limits, nil +} diff --git a/exchanges/binance/binance_cfutures.go b/exchanges/binance/binance_cfutures.go index 63fa6266..881b6624 100644 --- a/exchanges/binance/binance_cfutures.go +++ b/exchanges/binance/binance_cfutures.go @@ -7,12 +7,14 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) @@ -1446,3 +1448,45 @@ func (b *Binance) FuturesPositionsADLEstimate(symbol currency.Pair) ([]ADLEstima } return resp, b.SendAuthHTTPRequest(exchange.RestCoinMargined, http.MethodGet, cfuturesADLQuantile, params, cFuturesAccountInformationRate, &resp) } + +// FetchCoinMarginExchangeLimits fetches coin margined order execution limits +func (b *Binance) FetchCoinMarginExchangeLimits() ([]order.MinMaxLevel, error) { + var limits []order.MinMaxLevel + coinFutures, err := b.FuturesExchangeInfo() + if err != nil { + return nil, err + } + + for x := range coinFutures.Symbols { + symbol := strings.Split(coinFutures.Symbols[x].Symbol, currency.UnderscoreDelimiter) + var cp currency.Pair + cp, err = currency.NewPairFromStrings(symbol[0], symbol[1]) + if err != nil { + return nil, err + } + + if len(coinFutures.Symbols[x].Filters) < 6 { + continue + } + + limits = append(limits, order.MinMaxLevel{ + Pair: cp, + Asset: asset.CoinMarginedFutures, + MinPrice: coinFutures.Symbols[x].Filters[0].MinPrice, + MaxPrice: coinFutures.Symbols[x].Filters[0].MaxPrice, + StepPrice: coinFutures.Symbols[x].Filters[0].TickSize, + MaxAmount: coinFutures.Symbols[x].Filters[1].MaxQty, + MinAmount: coinFutures.Symbols[x].Filters[1].MinQty, + StepAmount: coinFutures.Symbols[x].Filters[1].StepSize, + MarketMinQty: coinFutures.Symbols[x].Filters[2].MinQty, + MarketMaxQty: coinFutures.Symbols[x].Filters[2].MaxQty, + MarketStepSize: coinFutures.Symbols[x].Filters[2].StepSize, + MaxTotalOrders: coinFutures.Symbols[x].Filters[3].Limit, + MaxAlgoOrders: coinFutures.Symbols[x].Filters[4].Limit, + MultiplierUp: coinFutures.Symbols[x].Filters[5].MultiplierUp, + MultiplierDown: coinFutures.Symbols[x].Filters[5].MultiplierDown, + MultiplierDecimal: coinFutures.Symbols[x].Filters[5].MultiplierDecimal, + }) + } + return limits, nil +} diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index f49cd912..4cba2fd2 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -2,6 +2,7 @@ package binance import ( "encoding/json" + "errors" "testing" "time" @@ -2482,3 +2483,50 @@ func TestUFuturesHistoricalTrades(t *testing.T) { t.Error(err) } } + +func TestSetExchangeOrderExecutionLimits(t *testing.T) { + t.Parallel() + err := b.UpdateOrderExecutionLimits(asset.Spot) + if err != nil { + t.Fatal(err) + } + + err = b.UpdateOrderExecutionLimits(asset.CoinMarginedFutures) + if err != nil { + t.Fatal(err) + } + + err = b.UpdateOrderExecutionLimits(asset.USDTMarginedFutures) + if err != nil { + t.Fatal(err) + } + + err = b.UpdateOrderExecutionLimits(asset.Binary) + if err == nil { + t.Fatal("expected unhandled case") + } + + cmfCP, err := currency.NewPairFromStrings("BTCUSD", "PERP") + if err != nil { + t.Fatal(err) + } + + limit, err := b.GetOrderExecutionLimits(asset.CoinMarginedFutures, cmfCP) + if err != nil { + t.Fatal(err) + } + + if limit == nil { + t.Fatal("exchange limit should be loaded") + } + + err = limit.Conforms(0.000001, 0.1, order.Limit) + if !errors.Is(err, order.ErrAmountBelowMin) { + t.Fatalf("expected %v, but receieved %v", order.ErrAmountBelowMin, err) + } + + err = limit.Conforms(0.01, 1, order.Limit) + if !errors.Is(err, order.ErrPriceBelowMin) { + t.Fatalf("expected %v, but receieved %v", order.ErrPriceBelowMin, err) + } +} diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go index e1ae2526..5bb2b579 100644 --- a/exchanges/binance/binance_types.go +++ b/exchanges/binance/binance_types.go @@ -51,7 +51,7 @@ type ExchangeInfo struct { TickSize float64 `json:"tickSize,string"` MultiplierUp float64 `json:"multiplierUp,string"` MultiplierDown float64 `json:"multiplierDown,string"` - AvgPriceMins int64 `json:"avgPriceMins"` + AvgPriceMinutes int64 `json:"avgPriceMins"` MinQty float64 `json:"minQty,string"` MaxQty float64 `json:"maxQty,string"` StepSize float64 `json:"stepSize,string"` @@ -60,7 +60,9 @@ type ExchangeInfo struct { Limit int64 `json:"limit"` MaxNumAlgoOrders int64 `json:"maxNumAlgoOrders"` MaxNumIcebergOrders int64 `json:"maxNumIcebergOrders"` + MaxNumOrders int64 `json:"maxNumOrders"` } `json:"filters"` + Permissions []string `json:"permissions"` } `json:"symbols"` } diff --git a/exchanges/binance/binance_ufutures.go b/exchanges/binance/binance_ufutures.go index 2c155fed..52382f97 100644 --- a/exchanges/binance/binance_ufutures.go +++ b/exchanges/binance/binance_ufutures.go @@ -13,6 +13,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) const ( @@ -1114,3 +1115,46 @@ func (b *Binance) GetFundingRates(symbol currency.Pair, limit string, startTime, } return resp, b.SendHTTPRequest(exchange.RestUSDTMargined, fundingRate+params.Encode(), uFuturesDefaultRate, &resp) } + +// FetchUSDTMarginExchangeLimits fetches USDT margined order execution limits +func (b *Binance) FetchUSDTMarginExchangeLimits() ([]order.MinMaxLevel, error) { + var limits []order.MinMaxLevel + usdtFutures, err := b.UExchangeInfo() + if err != nil { + return nil, err + } + + for x := range usdtFutures.Symbols { + var cp currency.Pair + cp, err = currency.NewPairFromStrings(usdtFutures.Symbols[x].BaseAsset, + usdtFutures.Symbols[x].QuoteAsset) + if err != nil { + return nil, err + } + + if len(usdtFutures.Symbols[x].Filters) < 7 { + continue + } + + limits = append(limits, order.MinMaxLevel{ + Pair: cp, + Asset: asset.USDTMarginedFutures, + MinPrice: usdtFutures.Symbols[x].Filters[0].MinPrice, + MaxPrice: usdtFutures.Symbols[x].Filters[0].MaxPrice, + StepPrice: usdtFutures.Symbols[x].Filters[0].TickSize, + MaxAmount: usdtFutures.Symbols[x].Filters[1].MaxQty, + MinAmount: usdtFutures.Symbols[x].Filters[1].MinQty, + StepAmount: usdtFutures.Symbols[x].Filters[1].StepSize, + MarketMinQty: usdtFutures.Symbols[x].Filters[2].MinQty, + MarketMaxQty: usdtFutures.Symbols[x].Filters[2].MaxQty, + MarketStepSize: usdtFutures.Symbols[x].Filters[2].StepSize, + MaxTotalOrders: usdtFutures.Symbols[x].Filters[3].Limit, + MaxAlgoOrders: usdtFutures.Symbols[x].Filters[4].Limit, + MinNotional: usdtFutures.Symbols[x].Filters[5].Notional, + MultiplierUp: usdtFutures.Symbols[x].Filters[6].MultiplierUp, + MultiplierDown: usdtFutures.Symbols[x].Filters[6].MultiplierDown, + MultiplierDecimal: usdtFutures.Symbols[x].Filters[6].MultiplierDecimal, + }) + } + return limits, nil +} diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index ef29795f..6523d06c 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -302,6 +302,19 @@ func (b *Binance) Run() { } } + a := b.GetAssetTypes() + for x := range a { + if err = b.CurrencyPairs.IsAssetEnabled(a[x]); err == nil { + err = b.UpdateOrderExecutionLimits(a[x]) + if err != nil { + log.Errorf(log.ExchangeSys, + "Could not set %s exchange exchange limits: %v", + b.Name, + err) + } + } + } + if !b.GetEnabledFeatures().AutoPairUpdates && !forceUpdate { return } @@ -913,6 +926,9 @@ func (b *Binance) CancelBatchOrders(o []order.Cancel) (order.CancelBatchResponse // CancelAllOrders cancels all orders associated with a currency pair func (b *Binance) CancelAllOrders(req *order.Cancel) (order.CancelAllResponse, error) { + if err := req.Validate(); err != nil { + return order.CancelAllResponse{}, err + } var cancelAllOrdersResponse order.CancelAllResponse cancelAllOrdersResponse.Status = make(map[string]string) switch req.AssetType { @@ -1556,3 +1572,29 @@ func compatibleOrderVars(side, status, orderType string) OrderVars { } return resp } + +// UpdateOrderExecutionLimits sets exchange executions for a required asset type +func (b *Binance) UpdateOrderExecutionLimits(a asset.Item) error { + var limits []order.MinMaxLevel + var err error + switch a { + case asset.Spot: + limits, err = b.FetchSpotExchangeLimits() + case asset.USDTMarginedFutures: + limits, err = b.FetchUSDTMarginExchangeLimits() + case asset.CoinMarginedFutures: + limits, err = b.FetchCoinMarginExchangeLimits() + case asset.Margin: + if err = b.CurrencyPairs.IsAssetEnabled(asset.Spot); err != nil { + limits, err = b.FetchSpotExchangeLimits() + } else { + return nil + } + default: + err = fmt.Errorf("unhandled asset type %s", a) + } + if err != nil { + return fmt.Errorf("cannot update exchange execution limits: %v", err) + } + return b.LoadLimits(limits) +} diff --git a/exchanges/binance/cfutures_types.go b/exchanges/binance/cfutures_types.go index fc1ca259..1467d7b4 100644 --- a/exchanges/binance/cfutures_types.go +++ b/exchanges/binance/cfutures_types.go @@ -556,6 +556,7 @@ type UFuturesExchangeInfo struct { MultiplierDown float64 `json:"multiplierDown,string"` MultiplierUp float64 `json:"multiplierUp,string"` MultiplierDecimal float64 `json:"multiplierDecimal,string"` + Notional float64 `json:"notional,string"` } `json:"filters"` OrderTypes []string `json:"orderTypes"` TimeInForce []string `json:"timeInForce"` @@ -579,6 +580,7 @@ type CExchangeInfo struct { MinPrice float64 `json:"minPrice,string"` MaxPrice float64 `json:"maxPrice,string"` StepSize float64 `json:"stepSize,string"` + TickSize float64 `json:"tickSize,string"` MaxQty float64 `json:"maxQty,string"` MinQty float64 `json:"minQty,string"` Limit int64 `json:"limit"` diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 8241bf72..2eb78946 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -1261,3 +1261,8 @@ func (u URL) String() string { return "" } } + +// UpdateOrderExecutionLimits updates order execution limits this is overridable +func (e *Base) UpdateOrderExecutionLimits(a asset.Item) error { + return common.ErrNotYetImplemented +} diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index 4da9cdcd..153b6a36 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -7,6 +7,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" @@ -222,6 +223,7 @@ type Base struct { Config *config.ExchangeConfig settingsMutex sync.RWMutex OrderbookVerificationBypass bool + order.ExecutionLimits } // url lookup consts diff --git a/exchanges/interfaces.go b/exchanges/interfaces.go index 63420662..39b196d2 100644 --- a/exchanges/interfaces.go +++ b/exchanges/interfaces.go @@ -75,7 +75,6 @@ type IBotExchange interface { GetHistoricCandlesExtended(p currency.Pair, a asset.Item, timeStart, timeEnd time.Time, interval kline.Interval) (kline.Item, error) DisableRateLimiter() error EnableRateLimiter() error - // Websocket specific wrapper functionality // GetWebsocket returns a pointer to the websocket GetWebsocket() (*stream.Websocket, error) @@ -87,4 +86,8 @@ type IBotExchange interface { // pair,asset, url/proxy or subscription change FlushWebsocketChannels() error AuthenticateWebsocket() error + // Exchange order related execution limits + GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (*order.Limits, error) + CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType order.Type) error + UpdateOrderExecutionLimits(a asset.Item) error } diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 20d0dd50..9fb0e8da 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -792,6 +792,9 @@ func (k *Kraken) CancelBatchOrders(orders []order.Cancel) (order.CancelBatchResp // CancelAllOrders cancels all orders associated with a currency pair func (k *Kraken) CancelAllOrders(req *order.Cancel) (order.CancelAllResponse, error) { + if err := req.Validate(); err != nil { + return order.CancelAllResponse{}, err + } cancelAllOrdersResponse := order.CancelAllResponse{ Status: make(map[string]string), } diff --git a/exchanges/order/limits.go b/exchanges/order/limits.go new file mode 100644 index 00000000..29429d3a --- /dev/null +++ b/exchanges/order/limits.go @@ -0,0 +1,381 @@ +package order + +import ( + "errors" + "fmt" + "sync" + + "github.com/shopspring/decimal" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +var ( + // ErrExchangeLimitNotLoaded defines if an exchange does not have minmax + // values + ErrExchangeLimitNotLoaded = errors.New("exchange limits not loaded") + // ErrPriceBelowMin is when the price is lower than the minimum price + // limit accepted by the exchange + ErrPriceBelowMin = errors.New("price below minimum limit") + // ErrPriceExceedsMax is when the price is higher than the maximum price + // limit accepted by the exchange + ErrPriceExceedsMax = errors.New("price exceeds maximum limit") + // ErrPriceExceedsStep is when the price is not divisible by its step + ErrPriceExceedsStep = errors.New("price exceeds step limit") + // ErrAmountBelowMin is when the amount is lower than the minimum amount + // limit accepted by the exchange + ErrAmountBelowMin = errors.New("amount below minimum limit") + // ErrAmountExceedsMax is when the amount is higher than the maximum amount + // limit accepted by the exchange + ErrAmountExceedsMax = errors.New("amount exceeds maximum limit") + // ErrAmountExceedsStep is when the amount is not divisible by its step + ErrAmountExceedsStep = errors.New("amount exceeds step limit") + // ErrNotionalValue is when the notional value does not exceed currency pair + // requirements + ErrNotionalValue = errors.New("total notional value is under minimum limit") + // ErrMarketAmountBelowMin is when the amount is lower than the minimum + // amount limit accepted by the exchange for a market order + ErrMarketAmountBelowMin = errors.New("market order amount below minimum limit") + // ErrMarketAmountExceedsMax is when the amount is higher than the maximum + // amount limit accepted by the exchange for a market order + ErrMarketAmountExceedsMax = errors.New("market order amount exceeds maximum limit") + // ErrMarketAmountExceedsStep is when the amount is not divisible by its + // step for a market order + ErrMarketAmountExceedsStep = errors.New("market order amount exceeds step limit") + + errCannotValidateAsset = errors.New("cannot check limit, asset not loaded") + errCannotValidateBaseCurrency = errors.New("cannot check limit, base currency not loaded") + errCannotValidateQuoteCurrency = errors.New("cannot check limit, quote currency not loaded") + errExchangeLimitAsset = errors.New("exchange limits not found for asset") + errExchangeLimitBase = errors.New("exchange limits not found for base currency") + errExchangeLimitQuote = errors.New("exchange limits not found for quote currency") + errCannotLoadLimit = errors.New("cannot load limit, levels not supplied") + errInvalidPriceLevels = errors.New("invalid price levels, cannot load limits") + errInvalidAmountLevels = errors.New("invalid amount levels, cannot load limits") +) + +// ExecutionLimits defines minimum and maximum values in relation to +// order size, order pricing, total notional values, total maximum orders etc +// for execution on an exchange. +type ExecutionLimits struct { + m map[asset.Item]map[currency.Code]map[currency.Code]*Limits + mtx sync.RWMutex +} + +// MinMaxLevel defines the minimum and maximum parameters for a currency pair +// for outbound exchange execution +type MinMaxLevel struct { + Pair currency.Pair + Asset asset.Item + MinPrice float64 + MaxPrice float64 + StepPrice float64 + MultiplierUp float64 + MultiplierDown float64 + MultiplierDecimal float64 + AveragePriceMinutes int64 + MinAmount float64 + MaxAmount float64 + StepAmount float64 + MinNotional float64 + MaxIcebergParts int64 + MarketMinQty float64 + MarketMaxQty float64 + MarketStepSize float64 + MaxTotalOrders int64 + MaxAlgoOrders int64 +} + +// LoadLimits loads all limits levels into memory +func (e *ExecutionLimits) LoadLimits(levels []MinMaxLevel) error { + if len(levels) == 0 { + return errCannotLoadLimit + } + e.mtx.Lock() + defer e.mtx.Unlock() + if e.m == nil { + e.m = make(map[asset.Item]map[currency.Code]map[currency.Code]*Limits) + } + + for x := range levels { + m1, ok := e.m[levels[x].Asset] + if !ok { + m1 = make(map[currency.Code]map[currency.Code]*Limits) + e.m[levels[x].Asset] = m1 + } + + m2, ok := m1[levels[x].Pair.Base] + if !ok { + m2 = make(map[currency.Code]*Limits) + m1[levels[x].Pair.Base] = m2 + } + + limit, ok := m2[levels[x].Pair.Quote] + if !ok { + limit = new(Limits) + m2[levels[x].Pair.Quote] = limit + } + + if levels[x].MinPrice > levels[x].MaxPrice { + return fmt.Errorf("%w for %s %s supplied min: %f max: %f", + errInvalidPriceLevels, + levels[x].Asset, + levels[x].Pair, + levels[x].MinPrice, + levels[x].MaxPrice) + } + + if levels[x].MinAmount > levels[x].MaxAmount { + return fmt.Errorf("%w for %s %s supplied min: %f max: %f", + errInvalidAmountLevels, + levels[x].Asset, + levels[x].Pair, + levels[x].MinAmount, + levels[x].MaxAmount) + } + limit.m.Lock() + limit.minPrice = levels[x].MinPrice + limit.maxPrice = levels[x].MaxPrice + limit.stepIncrementSizePrice = levels[x].StepPrice + limit.minAmount = levels[x].MinAmount + limit.maxAmount = levels[x].MaxAmount + limit.stepIncrementSizeAmount = levels[x].StepAmount + limit.minNotional = levels[x].MinNotional + limit.multiplierUp = levels[x].MultiplierUp + limit.multiplierDown = levels[x].MultiplierDown + limit.averagePriceMinutes = levels[x].AveragePriceMinutes + limit.maxIcebergParts = levels[x].MaxIcebergParts + limit.marketMinQty = levels[x].MarketMinQty + limit.marketMaxQty = levels[x].MarketMaxQty + limit.marketStepIncrementSize = levels[x].MarketStepSize + limit.maxTotalOrders = levels[x].MaxTotalOrders + limit.maxAlgoOrders = levels[x].MaxAlgoOrders + limit.m.Unlock() + } + return nil +} + +// GetOrderExecutionLimits returns the exchange limit parameters for a currency +func (e *ExecutionLimits) GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (*Limits, error) { + e.mtx.RLock() + defer e.mtx.RUnlock() + + if e.m == nil { + return nil, ErrExchangeLimitNotLoaded + } + + m1, ok := e.m[a] + if !ok { + return nil, errExchangeLimitAsset + } + + m2, ok := m1[cp.Base] + if !ok { + return nil, errExchangeLimitBase + } + + limit, ok := m2[cp.Quote] + if !ok { + return nil, errExchangeLimitQuote + } + + return limit, nil +} + +// CheckOrderExecutionLimits checks to see if the price and amount conforms with +// exchange level order execution limits +func (e *ExecutionLimits) CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType Type) error { + e.mtx.RLock() + defer e.mtx.RUnlock() + + if e.m == nil { + // No exchange limits loaded so we can nil this + return nil + } + + m1, ok := e.m[a] + if !ok { + return errCannotValidateAsset + } + + m2, ok := m1[cp.Base] + if !ok { + return errCannotValidateBaseCurrency + } + + limit, ok := m2[cp.Quote] + if !ok { + return errCannotValidateQuoteCurrency + } + + err := limit.Conforms(price, amount, orderType) + if err != nil { + return fmt.Errorf("%w for %s %s", err, a, cp) + } + + return nil +} + +// Limits defines total limit values for an associated currency to be checked +// before execution on an exchange +type Limits struct { + minPrice float64 + maxPrice float64 + stepIncrementSizePrice float64 + minAmount float64 + maxAmount float64 + stepIncrementSizeAmount float64 + minNotional float64 + multiplierUp float64 + multiplierDown float64 + averagePriceMinutes int64 + maxIcebergParts int64 + marketMinQty float64 + marketMaxQty float64 + marketStepIncrementSize float64 + maxTotalOrders int64 + maxAlgoOrders int64 + m sync.RWMutex +} + +// Conforms checks outbound parameters +func (l *Limits) Conforms(price, amount float64, orderType Type) error { + if l == nil { + // For when we return a nil pointer we can assume there's nothing to + // check + return nil + } + + l.m.RLock() + defer l.m.RUnlock() + if l.minAmount != 0 && amount < l.minAmount { + return fmt.Errorf("%w min: %.8f supplied %.8f", + ErrAmountBelowMin, + l.minAmount, + amount) + } + if l.maxAmount != 0 && amount > l.maxAmount { + return fmt.Errorf("%w min: %.8f supplied %.8f", + ErrAmountExceedsMax, + l.maxAmount, + amount) + } + if l.stepIncrementSizeAmount != 0 { + dAmount := decimal.NewFromFloat(amount) + dMinAmount := decimal.NewFromFloat(l.minAmount) + dStep := decimal.NewFromFloat(l.stepIncrementSizeAmount) + if !dAmount.Sub(dMinAmount).Mod(dStep).IsZero() { + return fmt.Errorf("%w stepSize: %.8f supplied %.8f", + ErrAmountExceedsStep, + l.stepIncrementSizeAmount, + amount) + } + } + + // Multiplier checking not done due to the fact we need coherence with the + // last average price (TODO) + // l.multiplierUp will be used to determine how far our price can go up + // l.multiplierDown will be used to determine how far our price can go down + // l.averagePriceMinutes will be used to determine mean over this period + + // Max iceberg parts checking not done as we do not have that + // functionality yet (TODO) + // l.maxIcebergParts // How many components in an iceberg order + + // Max total orders not done due to order manager limitations (TODO) + // l.maxTotalOrders + + // Max algo orders not done due to order manager limitations (TODO) + // l.maxAlgoOrders + + // If order type is Market we do not need to do price checks + if orderType != Market { + if l.minPrice != 0 && price < l.minPrice { + return fmt.Errorf("%w min: %.8f supplied %.8f", + ErrPriceBelowMin, + l.minPrice, + price) + } + if l.maxPrice != 0 && price > l.maxPrice { + return fmt.Errorf("%w max: %.8f supplied %.8f", + ErrPriceExceedsMax, + l.maxPrice, + price) + } + if l.minNotional != 0 && (amount*price) < l.minNotional { + return fmt.Errorf("%w minimum notional: %.8f value of order %.8f", + ErrNotionalValue, + l.minNotional, + amount*price) + } + if l.stepIncrementSizePrice != 0 { + dPrice := decimal.NewFromFloat(price) + dMinPrice := decimal.NewFromFloat(l.minPrice) + dStep := decimal.NewFromFloat(l.stepIncrementSizePrice) + if !dPrice.Sub(dMinPrice).Mod(dStep).IsZero() { + return fmt.Errorf("%w stepSize: %.8f supplied %.8f", + ErrPriceExceedsStep, + l.stepIncrementSizePrice, + price) + } + } + return nil + } + + if l.marketMinQty != 0 && + l.minAmount < l.marketMinQty && + amount < l.marketMinQty { + return fmt.Errorf("%w min: %.8f supplied %.8f", + ErrMarketAmountBelowMin, + l.marketMinQty, + amount) + } + if l.marketMaxQty != 0 && + l.maxAmount > l.marketMaxQty && + amount > l.marketMaxQty { + return fmt.Errorf("%w max: %.8f supplied %.8f", + ErrMarketAmountExceedsMax, + l.marketMaxQty, + amount) + } + if l.marketStepIncrementSize != 0 && l.stepIncrementSizeAmount != l.marketStepIncrementSize { + dAmount := decimal.NewFromFloat(amount) + dMinMAmount := decimal.NewFromFloat(l.marketMinQty) + dStep := decimal.NewFromFloat(l.marketStepIncrementSize) + if !dAmount.Sub(dMinMAmount).Mod(dStep).IsZero() { + return fmt.Errorf("%w stepSize: %.8f supplied %.8f", + ErrMarketAmountExceedsStep, + l.marketStepIncrementSize, + amount) + } + } + return nil +} + +// ConformToAmount (POC) conforms amount to its amount interval +func (l *Limits) ConformToAmount(amount float64) float64 { + if l == nil { + // For when we return a nil pointer we can assume there's nothing to + // check + return amount + } + l.m.Lock() + defer l.m.Unlock() + if l.stepIncrementSizeAmount == 0 || amount == l.stepIncrementSizeAmount { + return amount + } + + if amount < l.stepIncrementSizeAmount { + return 0 + } + + // Convert floats to decimal types + dAmount := decimal.NewFromFloat(amount) + dStep := decimal.NewFromFloat(l.stepIncrementSizeAmount) + // derive modulus + mod := dAmount.Mod(dStep) + // subtract modulus to get the floor + rVal := dAmount.Sub(mod) + fVal, _ := rVal.Float64() + return fVal +} diff --git a/exchanges/order/limits_test.go b/exchanges/order/limits_test.go new file mode 100644 index 00000000..8f6a8313 --- /dev/null +++ b/exchanges/order/limits_test.go @@ -0,0 +1,319 @@ +package order + +import ( + "errors" + "testing" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" +) + +var btcusd = currency.NewPair(currency.BTC, currency.USD) +var ltcusd = currency.NewPair(currency.LTC, currency.USD) +var btcltc = currency.NewPair(currency.BTC, currency.LTC) + +func TestLoadLimits(t *testing.T) { + t.Parallel() + e := ExecutionLimits{} + err := e.LoadLimits(nil) + if !errors.Is(err, errCannotLoadLimit) { + t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err) + } + + newLimits := []MinMaxLevel{ + { + Pair: btcusd, + Asset: asset.Spot, + MinPrice: 100000, + MaxPrice: 1000000, + MinAmount: 1, + MaxAmount: 10, + }, + } + + err = e.LoadLimits(newLimits) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", nil, err) + } + + badLimit := []MinMaxLevel{ + { + Pair: btcusd, + Asset: asset.Spot, + MinPrice: 2, + MaxPrice: 1, + MinAmount: 1, + MaxAmount: 10, + }, + } + + err = e.LoadLimits(badLimit) + if !errors.Is(err, errInvalidPriceLevels) { + t.Fatalf("expected error %v but received %v", errInvalidPriceLevels, err) + } + + badLimit = []MinMaxLevel{ + { + Pair: btcusd, + Asset: asset.Spot, + MinPrice: 1, + MaxPrice: 2, + MinAmount: 10, + MaxAmount: 9, + }, + } + + err = e.LoadLimits(badLimit) + if !errors.Is(err, errInvalidAmountLevels) { + t.Fatalf("expected error %v but received %v", errInvalidPriceLevels, err) + } + + goodLimit := []MinMaxLevel{ + { + Pair: btcusd, + Asset: asset.Spot, + }, + } + + err = e.LoadLimits(goodLimit) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", nil, err) + } +} + +func TestGetOrderExecutionLimits(t *testing.T) { + t.Parallel() + e := ExecutionLimits{} + _, err := e.GetOrderExecutionLimits(asset.Spot, btcusd) + if !errors.Is(err, ErrExchangeLimitNotLoaded) { + t.Fatalf("expected error %v but received %v", ErrExchangeLimitNotLoaded, err) + } + + newLimits := []MinMaxLevel{ + { + Pair: btcusd, + Asset: asset.Spot, + MinPrice: 100000, + MaxPrice: 1000000, + MinAmount: 1, + MaxAmount: 10, + }, + } + + err = e.LoadLimits(newLimits) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err) + } + + _, err = e.GetOrderExecutionLimits(asset.Futures, ltcusd) + if !errors.Is(err, errExchangeLimitAsset) { + t.Fatalf("expected error %v but received %v", errExchangeLimitAsset, err) + } + + _, err = e.GetOrderExecutionLimits(asset.Spot, ltcusd) + if !errors.Is(err, errExchangeLimitBase) { + t.Fatalf("expected error %v but received %v", errExchangeLimitBase, err) + } + + _, err = e.GetOrderExecutionLimits(asset.Spot, btcltc) + if !errors.Is(err, errExchangeLimitQuote) { + t.Fatalf("expected error %v but received %v", errExchangeLimitQuote, err) + } + + tt, err := e.GetOrderExecutionLimits(asset.Spot, btcusd) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", nil, err) + } + + if tt.maxAmount != newLimits[0].MaxAmount || + tt.minAmount != newLimits[0].MinAmount || + tt.maxPrice != newLimits[0].MaxPrice || + tt.minPrice != newLimits[0].MinPrice { + t.Fatal("unexpected values") + } +} + +func TestCheckLimit(t *testing.T) { + t.Parallel() + e := ExecutionLimits{} + err := e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 1337, Limit) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", nil, err) + } + + newLimits := []MinMaxLevel{ + { + Pair: btcusd, + Asset: asset.Spot, + MinPrice: 100000, + MaxPrice: 1000000, + MinAmount: 1, + MaxAmount: 10, + }, + } + + err = e.LoadLimits(newLimits) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", errCannotLoadLimit, err) + } + + err = e.CheckOrderExecutionLimits(asset.Futures, ltcusd, 1337, 1337, Limit) + if !errors.Is(err, errCannotValidateAsset) { + t.Fatalf("expected error %v but received %v", errCannotValidateAsset, err) + } + + err = e.CheckOrderExecutionLimits(asset.Spot, ltcusd, 1337, 1337, Limit) + if !errors.Is(err, errCannotValidateBaseCurrency) { + t.Fatalf("expected error %v but received %v", errCannotValidateBaseCurrency, err) + } + + err = e.CheckOrderExecutionLimits(asset.Spot, btcltc, 1337, 1337, Limit) + if !errors.Is(err, errCannotValidateQuoteCurrency) { + t.Fatalf("expected error %v but received %v", errCannotValidateQuoteCurrency, err) + } + + err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 9, Limit) + if !errors.Is(err, ErrPriceBelowMin) { + t.Fatalf("expected error %v but received %v", ErrPriceBelowMin, err) + } + + err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1000001, 9, Limit) + if !errors.Is(err, ErrPriceExceedsMax) { + t.Fatalf("expected error %v but received %v", ErrPriceExceedsMax, err) + } + + err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, .5, Limit) + if !errors.Is(err, ErrAmountBelowMin) { + t.Fatalf("expected error %v but received %v", ErrAmountBelowMin, err) + } + + err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 11, Limit) + if !errors.Is(err, ErrAmountExceedsMax) { + t.Fatalf("expected error %v but received %v", ErrAmountExceedsMax, err) + } + + err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Limit) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", nil, err) + } + + err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Market) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", nil, err) + } +} + +func TestConforms(t *testing.T) { + t.Parallel() + var tt *Limits + err := tt.Conforms(0, 0, Limit) + if err != nil { + t.Fatal(err) + } + + tt = &Limits{ + minNotional: 100, + } + + err = tt.Conforms(1, 1, Limit) + if !errors.Is(err, ErrNotionalValue) { + t.Fatalf("expected error %v but received %v", ErrNotionalValue, err) + } + + err = tt.Conforms(200, .5, Limit) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", nil, err) + } + + tt.stepIncrementSizePrice = 0.001 + err = tt.Conforms(200.0001, .5, Limit) + if !errors.Is(err, ErrPriceExceedsStep) { + t.Fatalf("expected error %v but received %v", ErrPriceExceedsStep, err) + } + err = tt.Conforms(200.004, .5, Limit) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", nil, err) + } + + tt.stepIncrementSizeAmount = 0.001 + err = tt.Conforms(200, .0002, Limit) + if !errors.Is(err, ErrAmountExceedsStep) { + t.Fatalf("expected error %v but received %v", ErrAmountExceedsStep, err) + } + err = tt.Conforms(200000, .003, Limit) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received %v", nil, err) + } + + tt.minAmount = 1 + tt.maxAmount = 10 + tt.marketMinQty = 1.1 + tt.marketMaxQty = 9.9 + + err = tt.Conforms(200000, 1, Market) + if !errors.Is(err, ErrMarketAmountBelowMin) { + t.Fatalf("expected error %v but received: %v", ErrMarketAmountBelowMin, err) + } + + err = tt.Conforms(200000, 10, Market) + if !errors.Is(err, ErrMarketAmountExceedsMax) { + t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsMax, err) + } + + tt.marketStepIncrementSize = 10 + err = tt.Conforms(200000, 9.1, Market) + if !errors.Is(err, ErrMarketAmountExceedsStep) { + t.Fatalf("expected error %v but received: %v", ErrMarketAmountExceedsStep, err) + } + tt.marketStepIncrementSize = 1 + err = tt.Conforms(200000, 9.1, Market) + if !errors.Is(err, nil) { + t.Fatalf("expected error %v but received: %v", nil, err) + } +} + +func TestConformToAmount(t *testing.T) { + t.Parallel() + var tt *Limits + if tt.ConformToAmount(1.001) != 1.001 { + t.Fatal("value should not be changed") + } + + tt = &Limits{} + val := tt.ConformToAmount(1) + if val != 1 { // If there is no step amount set this should not change + // the inputted amount + t.Fatal("unexpected amount") + } + + tt.stepIncrementSizeAmount = 0.001 + val = tt.ConformToAmount(1.001) + if val != 1.001 { + t.Error("unexpected amount", val) + } + + val = tt.ConformToAmount(0.0001) + if val != 0 { + t.Error("unexpected amount", val) + } + + val = tt.ConformToAmount(0.7777) + if val != 0.777 { + t.Error("unexpected amount", val) + } + + tt.stepIncrementSizeAmount = 100 + val = tt.ConformToAmount(100) + if val != 100 { + t.Fatal("unexpected amount", val) + } + + val = tt.ConformToAmount(200) + if val != 200 { + t.Fatal("unexpected amount", val) + } + val = tt.ConformToAmount(150) + if val != 100 { + t.Fatal("unexpected amount", val) + } +} diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 9862abd6..e1847fc1 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -100,12 +100,8 @@ func TestValidate(t *testing.T) { } for x := range tester { - if err := tester[x].Submit.Validate(tester[x].ValidOpts); err != tester[x].ExpectedErr { - if err != nil && tester[x].ExpectedErr != nil { - if err.Error() == tester[x].ExpectedErr.Error() { - continue - } - } + err := tester[x].Submit.Validate(tester[x].ValidOpts) + if !errors.Is(err, tester[x].ExpectedErr) { t.Errorf("Unexpected result. Got: %v, want: %v", err, tester[x].ExpectedErr) } } diff --git a/exchanges/order/order_types.go b/exchanges/order/order_types.go index e2738bd7..5bc44077 100644 --- a/exchanges/order/order_types.go +++ b/exchanges/order/order_types.go @@ -18,7 +18,7 @@ var ( ErrAssetNotSet = errors.New("order asset type is not set") ErrSideIsInvalid = errors.New("order side is invalid") ErrTypeIsInvalid = errors.New("order type is invalid") - ErrAmountIsInvalid = errors.New("order amount is invalid") + ErrAmountIsInvalid = errors.New("order amount is equal or less than zero") ErrPriceMustBeSetIfLimitOrder = errors.New("order price must be set if limit order type is desired") ErrOrderIDNotSet = errors.New("order id or client order id is not set") ) diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index 07fc4be3..dc6676ed 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -38,25 +38,20 @@ func (s *Submit) Validate(opt ...validate.Checker) error { } if s.Amount <= 0 { - return ErrAmountIsInvalid + return fmt.Errorf("submit validation error %w, suppled: %.8f", ErrAmountIsInvalid, s.Amount) } if s.Type == Limit && s.Price <= 0 { return ErrPriceMustBeSetIfLimitOrder } - var errs common.Errors for _, o := range opt { err := o.Check() if err != nil { - errs = append(errs, err) + return err } } - if errs != nil { - return errs - } - return nil } diff --git a/go.mod b/go.mod index 44ded2ea..5cb7fb42 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/pelletier/go-toml v1.8.0 // indirect github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.3.0 + github.com/shopspring/decimal v1.2.0 github.com/spf13/afero v1.3.4 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/viper v1.7.1 diff --git a/go.sum b/go.sum index ed5e0a6d..c8e4ae15 100644 --- a/go.sum +++ b/go.sum @@ -301,6 +301,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=