From 48349bc3bbeee30a4f116ab992ad93b7daec13c4 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Wed, 3 Jul 2024 16:07:23 +1000 Subject: [PATCH] protocol/order: adds additional fields for trading requirements (#1552) * Add in initial handling for quote/base currency deployment requirements * include client order ID checking * glorious: suggestions * spell and fix * linter/context/test * rework tests and order side specific requirements * linter * mend panic at the disco * mending more panics at the disco * anudda fix brudda * glorious: NITTTTTT BOOOOOMB * leftover things and stuff * whoops * tie in gateio * glorious: nit fixes life and everything. * thrasher: nits --------- Co-authored-by: Ryan O'Hara-Reid --- cmd/exchange_template/wrapper_file.tmpl | 2 +- engine/order_manager.go | 23 +++--- engine/order_manager_test.go | 36 ++++----- exchanges/alphapoint/alphapoint_wrapper.go | 2 +- exchanges/binance/binance_wrapper.go | 2 +- exchanges/binanceus/binanceus_wrapper.go | 2 +- exchanges/bitfinex/bitfinex_wrapper.go | 2 +- exchanges/bithumb/bithumb_wrapper.go | 2 +- exchanges/bitmex/bitmex_wrapper.go | 2 +- exchanges/bitstamp/bitstamp_wrapper.go | 2 +- exchanges/btcmarkets/btcmarkets_wrapper.go | 2 +- exchanges/btse/btse_wrapper.go | 2 +- exchanges/bybit/bybit_wrapper.go | 2 +- exchanges/coinbasepro/coinbasepro_wrapper.go | 2 +- exchanges/coinut/coinut_wrapper.go | 2 +- exchanges/deribit/deribit_wrapper.go | 2 +- exchanges/exchange.go | 8 ++ exchanges/exchange_test.go | 8 ++ exchanges/exchange_types.go | 7 +- exchanges/exmo/exmo_wrapper.go | 2 +- exchanges/gateio/gateio_wrapper.go | 17 ++++- exchanges/gemini/gemini_wrapper.go | 2 +- exchanges/hitbtc/hitbtc_wrapper.go | 2 +- exchanges/huobi/huobi_wrapper.go | 2 +- exchanges/interfaces.go | 5 ++ exchanges/kraken/kraken_wrapper.go | 2 +- exchanges/kucoin/kucoin_wrapper.go | 5 +- exchanges/lbank/lbank_wrapper.go | 2 +- exchanges/okcoin/okcoin_wrapper.go | 2 +- exchanges/okx/okx_wrapper.go | 2 +- exchanges/order/order_test.go | 79 ++++++++++++++++++-- exchanges/order/orders.go | 32 +++++--- exchanges/poloniex/poloniex_wrapper.go | 2 +- exchanges/protocol/features.go | 19 +++++ exchanges/yobit/yobit_wrapper.go | 2 +- 35 files changed, 210 insertions(+), 77 deletions(-) diff --git a/cmd/exchange_template/wrapper_file.tmpl b/cmd/exchange_template/wrapper_file.tmpl index 6e9397b5..68e1639c 100644 --- a/cmd/exchange_template/wrapper_file.tmpl +++ b/cmd/exchange_template/wrapper_file.tmpl @@ -358,7 +358,7 @@ func ({{.Variable}} *{{.CapitalName}}) GetServerTime(ctx context.Context, a asse // SubmitOrder submits a new order func ({{.Variable}} *{{.CapitalName}}) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate({{.Variable}}.GetTradingRequirements()); err != nil { return nil, err } // When an order has been submitted you can use this helpful constructor to diff --git a/engine/order_manager.go b/engine/order_manager.go index 32ed6f15..665e37b4 100644 --- a/engine/order_manager.go +++ b/engine/order_manager.go @@ -352,7 +352,7 @@ func (m *OrderManager) GetOrderInfo(ctx context.Context, exchangeName, orderID s } // validate ensures a submitted order is valid before adding to the manager -func (m *OrderManager) validate(newOrder *order.Submit) error { +func (m *OrderManager) validate(exch exchange.IBotExchange, newOrder *order.Submit) error { if newOrder == nil { return errNilOrder } @@ -361,7 +361,7 @@ func (m *OrderManager) validate(newOrder *order.Submit) error { return ErrExchangeNameIsEmpty } - if err := newOrder.Validate(); err != nil { + if err := newOrder.Validate(exch.GetTradingRequirements()); err != nil { return fmt.Errorf("order manager: %w", err) } @@ -466,12 +466,14 @@ func (m *OrderManager) Submit(ctx context.Context, newOrder *order.Submit) (*Ord if atomic.LoadInt32(&m.started) == 0 { return nil, fmt.Errorf("order manager %w", ErrSubSystemNotStarted) } - - err := m.validate(newOrder) + if newOrder == nil { + return nil, errNilOrder + } + exch, err := m.orderStore.exchangeManager.GetExchangeByName(newOrder.Exchange) if err != nil { return nil, err } - exch, err := m.orderStore.exchangeManager.GetExchangeByName(newOrder.Exchange) + err = m.validate(exch, newOrder) if err != nil { return nil, err } @@ -515,16 +517,17 @@ func (m *OrderManager) SubmitFakeOrder(newOrder *order.Submit, resultingOrder *o if atomic.LoadInt32(&m.started) == 0 { return nil, fmt.Errorf("order manager %w", ErrSubSystemNotStarted) } - - err := m.validate(newOrder) - if err != nil { - return nil, err + if newOrder == nil { + return nil, errNilOrder } exch, err := m.orderStore.exchangeManager.GetExchangeByName(newOrder.Exchange) if err != nil { return nil, err } - + err = m.validate(exch, newOrder) + if err != nil { + return nil, err + } if checkExchangeLimits { // Checks for exchange min max limits for order amounts before order // execution can occur diff --git a/engine/order_manager_test.go b/engine/order_manager_test.go index 96ef85ec..a0172fa2 100644 --- a/engine/order_manager_test.go +++ b/engine/order_manager_test.go @@ -11,6 +11,7 @@ import ( "github.com/gofrs/uuid" "github.com/shopspring/decimal" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/config" @@ -562,9 +563,7 @@ func TestCancelAllOrders(t *testing.T) { func TestSubmit(t *testing.T) { m := OrdersSetup(t) _, err := m.Submit(context.Background(), nil) - if err == nil { - t.Error("Expected error from nil order") - } + require.ErrorIs(t, err, errNilOrder) o := &order.Submit{Type: order.Market} _, err = m.Submit(context.Background(), o) @@ -1316,26 +1315,12 @@ func TestSubmitFakeOrder(t *testing.T) { o := &OrderManager{} resp := &order.SubmitResponse{} _, err := o.SubmitFakeOrder(nil, resp, false) - if !errors.Is(err, ErrSubSystemNotStarted) { - t.Errorf("received '%v', expected '%v'", err, ErrSubSystemNotStarted) - } + assert.ErrorIs(t, err, ErrSubSystemNotStarted) o.started = 1 _, err = o.SubmitFakeOrder(nil, resp, false) - if !errors.Is(err, errNilOrder) { - t.Errorf("received '%v', expected '%v'", err, errNilOrder) - } - ord := &order.Submit{} - _, err = o.SubmitFakeOrder(ord, resp, false) - if !errors.Is(err, ErrExchangeNameIsEmpty) { - t.Errorf("received '%v', expected '%v'", err, ErrExchangeNameIsEmpty) - } - ord.Exchange = testExchange - ord.AssetType = asset.Spot - ord.Pair = currency.NewPair(currency.BTC, currency.DOGE) - ord.Side = order.Buy - ord.Type = order.Market - ord.Amount = 1337 + assert.ErrorIs(t, err, errNilOrder) + em := NewExchangeManager() exch, err := em.NewExchangeByName(testExchange) if err != nil { @@ -1348,6 +1333,17 @@ func TestSubmitFakeOrder(t *testing.T) { } o.orderStore.exchangeManager = em + ord := &order.Submit{} + _, err = o.SubmitFakeOrder(ord, resp, false) + assert.ErrorIs(t, err, ErrExchangeNameIsEmpty) + + ord.Exchange = testExchange + ord.AssetType = asset.Spot + ord.Pair = currency.NewPair(currency.BTC, currency.DOGE) + ord.Side = order.Buy + ord.Type = order.Market + ord.Amount = 1337 + resp, err = ord.DeriveSubmitResponse("1234") if err != nil { t.Fatal(err) diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index 4c2090cc..a11a6203 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -274,7 +274,7 @@ func (a *Alphapoint) GetHistoricTrades(_ context.Context, _ currency.Pair, _ ass // SubmitOrder submits a new order and returns a true value when // successfully submitted func (a *Alphapoint) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(a.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 9ea79f2b..3272940f 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -882,7 +882,7 @@ func (b *Binance) GetHistoricTrades(ctx context.Context, p currency.Pair, a asse // SubmitOrder submits a new order func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(b.GetTradingRequirements()); err != nil { return nil, err } var orderID string diff --git a/exchanges/binanceus/binanceus_wrapper.go b/exchanges/binanceus/binanceus_wrapper.go index e6ae5b74..8314c22d 100644 --- a/exchanges/binanceus/binanceus_wrapper.go +++ b/exchanges/binanceus/binanceus_wrapper.go @@ -530,7 +530,7 @@ func (bi *Binanceus) SubmitOrder(ctx context.Context, s *order.Submit) (*order.S var submitOrderResponse order.SubmitResponse var timeInForce RequestParamsTimeForceType var sideType string - err := s.Validate() + err := s.Validate(bi.GetTradingRequirements()) if err != nil { return nil, err } diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 3d676dcc..8258de9d 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -608,7 +608,7 @@ allTrades: // SubmitOrder submits a new order func (b *Bitfinex) SubmitOrder(ctx context.Context, o *order.Submit) (*order.SubmitResponse, error) { - if err := o.Validate(); err != nil { + if err := o.Validate(b.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index 2b25583d..8b30aee0 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -450,7 +450,7 @@ func (b *Bithumb) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset. // SubmitOrder submits a new order // TODO: Fill this out to support limit orders func (b *Bithumb) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(b.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index b980463b..0d20a361 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -693,7 +693,7 @@ allTrades: // SubmitOrder submits a new order func (b *Bitmex) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(b.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 2ea7f3dc..5ce0156d 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -491,7 +491,7 @@ func (b *Bitstamp) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset // SubmitOrder submits a new order func (b *Bitstamp) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(b.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 5981caaf..ece27598 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -464,7 +464,7 @@ func (b *BTCMarkets) GetHistoricTrades(_ context.Context, _ currency.Pair, _ ass // SubmitOrder submits a new order func (b *BTCMarkets) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(b.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index f7f6ae40..27df6de1 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -496,7 +496,7 @@ func (b *BTSE) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Ite // SubmitOrder submits a new order func (b *BTSE) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(b.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index c7d312b3..66922e25 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -778,7 +778,7 @@ func orderTypeToString(oType order.Type) string { // SubmitOrder submits a new order func (by *Bybit) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - err := s.Validate() + err := s.Validate(by.GetTradingRequirements()) if err != nil { return nil, err } diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 1e548b3d..81a64ccf 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -440,7 +440,7 @@ func (c *CoinbasePro) GetHistoricTrades(_ context.Context, _ currency.Pair, _ as // SubmitOrder submits a new order func (c *CoinbasePro) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(c.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index e04fe79f..2a8fd264 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -494,7 +494,7 @@ func (c *COINUT) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.I // SubmitOrder submits a new order func (c *COINUT) SubmitOrder(ctx context.Context, o *order.Submit) (*order.SubmitResponse, error) { - err := o.Validate() + err := o.Validate(c.GetTradingRequirements()) if err != nil { return nil, err } diff --git a/exchanges/deribit/deribit_wrapper.go b/exchanges/deribit/deribit_wrapper.go index 742ba92d..abe7c67f 100644 --- a/exchanges/deribit/deribit_wrapper.go +++ b/exchanges/deribit/deribit_wrapper.go @@ -632,7 +632,7 @@ func (d *Deribit) GetHistoricTrades(ctx context.Context, p currency.Pair, assetT // SubmitOrder submits a new order func (d *Deribit) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - err := s.Validate() + err := s.Validate(d.GetTradingRequirements()) if err != nil { return nil, err } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index bb67b984..491246ff 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -1942,3 +1942,11 @@ func GetDefaultConfig(ctx context.Context, exch IBotExchange) (*config.Exchange, func (b *Base) GetCurrencyTradeURL(context.Context, asset.Item, currency.Pair) (string, error) { return "", common.ErrFunctionNotSupported } + +// GetTradingRequirements returns the exchange's trading requirements. +func (b *Base) GetTradingRequirements() protocol.TradingRequirements { + if b == nil { + return protocol.TradingRequirements{} + } + return b.Features.TradingRequirements +} diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index 76d2d4a5..a4ff629b 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -3062,3 +3062,11 @@ func TestGetCurrencyTradeURL(t *testing.T) { _, err := b.GetCurrencyTradeURL(context.Background(), asset.Spot, currency.NewPair(currency.BTC, currency.USDT)) require.ErrorIs(t, err, common.ErrFunctionNotSupported) } + +func TestGetTradingRequirements(t *testing.T) { + t.Parallel() + requirements := (*Base)(nil).GetTradingRequirements() + require.Empty(t, requirements) + requirements = (&Base{Features: Features{TradingRequirements: protocol.TradingRequirements{ClientOrderID: true}}}).GetTradingRequirements() + require.NotEmpty(t, requirements) +} diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index c94b29f6..1cf0f52a 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -150,9 +150,10 @@ type WithdrawalHistory struct { // Features stores the supported and enabled features // for the exchange type Features struct { - Supports FeaturesSupported - Enabled FeaturesEnabled - Subscriptions subscription.List + Supports FeaturesSupported + Enabled FeaturesEnabled + Subscriptions subscription.List + TradingRequirements protocol.TradingRequirements } // FeaturesEnabled stores the exchange enabled features diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index dbb8f723..611d387c 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -472,7 +472,7 @@ func (e *EXMO) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Ite // SubmitOrder submits a new order func (e *EXMO) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(e.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 0972091d..ad59fe38 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -55,6 +55,10 @@ func (g *Gateio) SetDefaults() { } g.Features = exchange.Features{ + TradingRequirements: protocol.TradingRequirements{ + SpotMarketOrderAmountPurchaseQuotationOnly: true, + SpotMarketOrderAmountSellBaseOnly: true, + }, Supports: exchange.FeaturesSupported{ REST: true, Websocket: true, @@ -984,7 +988,7 @@ func (g *Gateio) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.I // SubmitOrder submits a new order // TODO: support multiple order types (IOC) func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - err := s.Validate() + err := s.Validate(g.GetTradingRequirements()) if err != nil { return nil, err } @@ -1008,11 +1012,20 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi if err != nil { return nil, err } + + // When doing spot market orders when purchasing base currency, the + // quote currency amount is used. When selling the base currency the + // base currency amount is used. + tradingAmount := s.Amount + if tradingAmount == 0 && s.Type == order.Market { + tradingAmount = s.QuoteAmount + } + sOrder, err := g.PlaceSpotOrder(ctx, &CreateOrderRequestData{ Side: s.Side.Lower(), Type: s.Type.Lower(), Account: g.assetTypeToString(s.AssetType), - Amount: types.Number(s.Amount), + Amount: types.Number(tradingAmount), Price: types.Number(s.Price), CurrencyPair: s.Pair, Text: s.ClientOrderID, diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index ed9b0f8f..e97c9d93 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -498,7 +498,7 @@ allTrades: // SubmitOrder submits a new order func (g *Gemini) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(g.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 83030b89..3e1bfc69 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -460,7 +460,7 @@ allTrades: // SubmitOrder submits a new order func (h *HitBTC) SubmitOrder(ctx context.Context, o *order.Submit) (*order.SubmitResponse, error) { - err := o.Validate() + err := o.Validate(h.GetTradingRequirements()) if err != nil { return nil, err } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index a9c34628..3c100230 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -993,7 +993,7 @@ func (h *HUOBI) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.It // SubmitOrder submits a new order func (h *HUOBI) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(h.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/interfaces.go b/exchanges/interfaces.go index e76f9268..a152474b 100644 --- a/exchanges/interfaces.go +++ b/exchanges/interfaces.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/margin" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" @@ -34,8 +35,12 @@ type IBotExchange interface { Shutdown() error GetName() string SetEnabled(bool) + GetEnabledFeatures() FeaturesEnabled GetSupportedFeatures() FeaturesSupported + // GetTradingRequirements returns trading requirements for the exchange + GetTradingRequirements() protocol.TradingRequirements + FetchTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) UpdateTickers(ctx context.Context, a asset.Item) error diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 05bede1b..397d1a52 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -716,7 +716,7 @@ func (k *Kraken) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.I // SubmitOrder submits a new order func (k *Kraken) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - err := s.Validate() + err := s.Validate(k.GetTradingRequirements()) if err != nil { return nil, err } diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index 5e2899dd..d7cb0299 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -66,6 +66,9 @@ func (ku *Kucoin) SetDefaults() { log.Errorln(log.ExchangeSys, err) } ku.Features = exchange.Features{ + TradingRequirements: protocol.TradingRequirements{ + ClientOrderID: true, + }, Supports: exchange.FeaturesSupported{ REST: true, Websocket: true, @@ -673,7 +676,7 @@ func (ku *Kucoin) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset. // SubmitOrder submits a new order func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - err := s.Validate() + err := s.Validate(ku.GetTradingRequirements()) if err != nil { return nil, err } diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index fbafa42f..f83239c3 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -441,7 +441,7 @@ allTrades: // SubmitOrder submits a new order func (l *Lbank) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(l.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index a86b88f5..0eeff738 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -577,7 +577,7 @@ func (o *Okcoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi if !o.SupportsAsset(s.AssetType) { return nil, fmt.Errorf("%w, asset: %v", asset.ErrNotSupported, s.AssetType) } - err := s.Validate() + err := s.Validate(o.GetTradingRequirements()) if err != nil { return nil, err } diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index 9b1f80d1..d29b7285 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -712,7 +712,7 @@ allTrades: // SubmitOrder submits a new order func (ok *Okx) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(ok.GetTradingRequirements()); err != nil { return nil, err } if !ok.SupportsAsset(s.AssetType) { diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 31e5b8c7..31659b58 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -15,6 +15,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/validate" ) @@ -24,9 +25,12 @@ func TestSubmit_Validate(t *testing.T) { t.Parallel() testPair := currency.NewPair(currency.BTC, currency.LTC) tester := []struct { - ExpectedErr error - Submit *Submit - ValidOpts validate.Checker + ExpectedErr error + Submit *Submit + ValidOpts validate.Checker + HasToPurchaseWithQuoteAmountSet bool + HasToSellWithBaseAmountSet bool + RequiresID bool }{ { ExpectedErr: ErrSubmissionIsNil, @@ -178,13 +182,72 @@ func TestSubmit_Validate(t *testing.T) { }, ValidOpts: validate.Check(func() error { return nil }), }, // valid order! + { + ExpectedErr: ErrAmountMustBeSet, + Submit: &Submit{ + Exchange: "test", + Pair: testPair, + Side: Buy, + Type: Market, + Amount: 1, + AssetType: asset.Spot, + }, + HasToPurchaseWithQuoteAmountSet: true, + ValidOpts: validate.Check(func() error { return nil }), + }, + { + ExpectedErr: ErrAmountMustBeSet, + Submit: &Submit{ + Exchange: "test", + Pair: testPair, + Side: Sell, + Type: Market, + QuoteAmount: 1, + AssetType: asset.Spot, + }, + HasToSellWithBaseAmountSet: true, + ValidOpts: validate.Check(func() error { return nil }), + }, + { + ExpectedErr: ErrClientOrderIDMustBeSet, + Submit: &Submit{ + Exchange: "test", + Pair: testPair, + Side: Buy, + Type: Market, + Amount: 1, + AssetType: asset.Spot, + }, + RequiresID: true, + ValidOpts: validate.Check(func() error { return nil }), + }, + { + ExpectedErr: nil, + Submit: &Submit{ + Exchange: "test", + Pair: testPair, + Side: Buy, + Type: Market, + Amount: 1, + AssetType: asset.Spot, + ClientOrderID: "69420", + }, + RequiresID: true, + ValidOpts: validate.Check(func() error { return nil }), + }, } - for x := range tester { - err := tester[x].Submit.Validate(tester[x].ValidOpts) - if !errors.Is(err, tester[x].ExpectedErr) { - t.Fatalf("Unexpected result. %d Got: %v, want: %v", x+1, err, tester[x].ExpectedErr) - } + for x, tc := range tester { + t.Run(strconv.Itoa(x), func(t *testing.T) { + t.Parallel() + requirements := protocol.TradingRequirements{ + SpotMarketOrderAmountPurchaseQuotationOnly: tc.HasToPurchaseWithQuoteAmountSet, + SpotMarketOrderAmountSellBaseOnly: tc.HasToSellWithBaseAmountSet, + ClientOrderID: tc.RequiresID, + } + err := tc.Submit.Validate(requirements, tc.ValidOpts) + assert.ErrorIs(t, err, tc.ExpectedErr) + }) } } diff --git a/exchanges/order/orders.go b/exchanges/order/orders.go index ad23b387..93c36228 100644 --- a/exchanges/order/orders.go +++ b/exchanges/order/orders.go @@ -14,6 +14,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/validate" "github.com/thrasher-corp/gocryptotrader/log" "golang.org/x/text/cases" @@ -30,16 +31,17 @@ const ( notPlaced = InsufficientBalance | MarketUnavailable | Rejected ) +// Public error vars for order package var ( - // ErrUnableToPlaceOrder defines an error when an order submission has - // failed. - ErrUnableToPlaceOrder = errors.New("order not placed") - // ErrOrderNotFound is returned when no order is found - ErrOrderNotFound = errors.New("order not found") - - // ErrUnknownPriceType returned when price type is unknown - ErrUnknownPriceType = errors.New("unknown price type") + ErrUnableToPlaceOrder = errors.New("order not placed") + ErrOrderNotFound = errors.New("order not found") + ErrUnknownPriceType = errors.New("unknown price type") + ErrAmountMustBeSet = errors.New("amount must be set") + ErrClientOrderIDMustBeSet = errors.New("client order ID must be set") + ErrUnknownSubmissionAmountType = errors.New("unknown submission amount type") +) +var ( errTimeInForceConflict = errors.New("multiple time in force options applied") errUnrecognisedOrderType = errors.New("unrecognised order type") errUnrecognisedOrderStatus = errors.New("unrecognised order status") @@ -56,7 +58,7 @@ func IsValidOrderSubmissionSide(s Side) bool { } // Validate checks the supplied data and returns whether it's valid -func (s *Submit) Validate(opt ...validate.Checker) error { +func (s *Submit) Validate(requirements protocol.TradingRequirements, opt ...validate.Checker) error { if s == nil { return ErrSubmissionIsNil } @@ -105,6 +107,18 @@ func (s *Submit) Validate(opt ...validate.Checker) error { return ErrPriceMustBeSetIfLimitOrder } + if requirements.ClientOrderID && s.ClientOrderID == "" { + return fmt.Errorf("submit validation error %w, client order ID must be set to satisfy submission requirements", ErrClientOrderIDMustBeSet) + } + + if requirements.SpotMarketOrderAmountPurchaseQuotationOnly && s.QuoteAmount == 0 && s.Type == Market && s.AssetType == asset.Spot && s.Side.IsLong() { + return fmt.Errorf("submit validation error %w, quote amount to be sold must be set to 'QuoteAmount' field to satisfy trading requirements", ErrAmountMustBeSet) + } + + if requirements.SpotMarketOrderAmountSellBaseOnly && s.Amount == 0 && s.Type == Market && s.AssetType == asset.Spot && s.Side.IsShort() { + return fmt.Errorf("submit validation error %w, base amount being sold must be set to 'Amount' field to satisfy trading requirements", ErrAmountMustBeSet) + } + for _, o := range opt { err := o.Check() if err != nil { diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 5118ae63..b115e0b4 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -541,7 +541,7 @@ allTrades: // SubmitOrder submits a new order func (p *Poloniex) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(p.GetTradingRequirements()); err != nil { return nil, err } diff --git a/exchanges/protocol/features.go b/exchanges/protocol/features.go index 5e8cbb89..b3c0c666 100644 --- a/exchanges/protocol/features.go +++ b/exchanges/protocol/features.go @@ -50,3 +50,22 @@ type Features struct { // types instead of just being denoted as spot holdings. HasAssetTypeAccountSegregation bool `json:"hasAssetTypeAccountSegregation,omitempty"` } + +// TradingRequirements defines the requirements for trading on the exchange. +type TradingRequirements struct { + // SpotMarketOrderAmountPurchaseQuotationOnly requires the amount to be in + // quote currency or what is to be sold for when you purchase base currency. + // For example, long BTC-USD, the quotation amount is USD. + // NOTE: Due to an exchange's matching engine process, the base amount + // acquired may vary from what is intended due to price fluctuations and + // liquidity on the books. Care must be taken when implementing a market + // neutral strategy. + SpotMarketOrderAmountPurchaseQuotationOnly bool + // SpotMarketOrderAmountSellBaseOnly requires the amount to be in the + // base currency or what is intended to be purchased. For example, short + // BTC-USD, the base amount is BTC. + SpotMarketOrderAmountSellBaseOnly bool + // ClientOrderID is a unique identifier for the order that is generated by + // the client and is required for order submission. + ClientOrderID bool +} diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index 58447754..251c64b6 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -370,7 +370,7 @@ func (y *Yobit) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.It // SubmitOrder submits a new order // Yobit only supports limit orders func (y *Yobit) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { - if err := s.Validate(); err != nil { + if err := s.Validate(y.GetTradingRequirements()); err != nil { return nil, err }