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 <ryan.oharareid@thrasher.io>
This commit is contained in:
Ryan O'Hara-Reid
2024-07-03 16:07:23 +10:00
committed by GitHub
parent b7a2f617d9
commit 48349bc3bb
35 changed files with 210 additions and 77 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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)
})
}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}