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 @@
+
+
+
+
+
+
+ | Exchange Name |
+ Asset |
+ Currency Base |
+ Currency Quote |
+ Warning |
+
+
+ {{ range .Config.CurrencySettings}}
+ {{if .ShowExchangeOrderLimitWarning}}
+
+ | {{.ExchangeName}} |
+ {{.Asset}} |
+ {{.Base}} |
+ {{.Quote}} |
+ order execution limits supported but disabled, results may not work when in production |
+
+ {{end}}
+ {{end}}
+
+
+
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=