Files
gocryptotrader/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go
Copilot fe6f762ab8 exchange_wrapper_standards: Add order execution limit errors to acceptableErrors list (#2096)
* Initial plan

* Add order execution limit errors to acceptableErrors list

Add all order execution limit validation errors from the limits package
to the acceptableErrors list. This fixes intermittent test failures when
exchanges have different minimum/maximum limits for price and amount that
don't match the hardcoded test values.

The test uses hardcoded values (150) for price and amount, but exchanges
like Binance have varying limits (e.g., BTCUSDPERP has a minimum price of
1000). By treating these limit validation errors as acceptable, the test
properly validates that exchanges correctly enforce their limits without
causing false test failures.

Fixes the CheckOrderExecutionLimits failure for Binance coinmarginedfutures
BTCUSDPERP where the hardcoded price of 150 is below the minimum limit of
1000.

Co-authored-by: thrasher- <4685270+thrasher-@users.noreply.github.com>

* Remove self-explanatory comments from limit errors

Remove comments that don't add information beyond the error names
for the newly added order execution limit errors, as requested in
code review feedback.

Co-authored-by: thrasher- <4685270+thrasher-@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thrasher- <4685270+thrasher-@users.noreply.github.com>
2025-10-31 17:26:44 +11:00

791 lines
31 KiB
Go

package exchangewrapperstandards
import (
"context"
"errors"
"os"
"reflect"
"slices"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch"
"github.com/thrasher-corp/gocryptotrader/engine"
"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"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/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
func TestMain(m *testing.M) {
// only run testing suite for one CI/CD environment
if skipAdditionalWrapperCITests() {
return
}
os.Exit(m.Run())
}
// singleExchangeOverride enter an exchange name to only test that exchange
var singleExchangeOverride = ""
func TestAllExchangeWrappers(t *testing.T) {
t.Parallel()
cfg := config.GetConfig()
err := cfg.LoadConfig("../../testdata/configtest.json", true)
require.NoError(t, err, "LoadConfig must not error")
err = dispatch.EnsureRunning(dispatch.DefaultMaxWorkers, dispatch.DefaultJobsLimit)
require.NoError(t, err, "dispatch.EnsureRunning must not error")
for i := range cfg.Exchanges {
name := strings.ToLower(cfg.Exchanges[i].Name)
t.Run(name+" wrapper tests", func(t *testing.T) {
t.Parallel()
if slices.Contains(unsupportedExchangeNames, name) {
t.Skipf("skipping unsupported exchange %v", name)
}
if singleExchangeOverride != "" && name != singleExchangeOverride {
t.Skip("skipping ", name, " due to override")
}
ctx := t.Context()
if isCITest() && slices.Contains(blockedCIExchanges, name) {
// rather than skipping tests where execution is blocked, provide an expired
// context, so no executions can take place
var cancelFn context.CancelFunc
ctx, cancelFn = context.WithTimeout(ctx, 0)
cancelFn()
}
exch, assetPairs := setupExchange(ctx, t, name, cfg)
executeExchangeWrapperTests(ctx, t, exch, assetPairs)
})
}
}
func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.Config) (exchange.IBotExchange, []assetPair) {
t.Helper()
em := engine.NewExchangeManager()
exch, err := em.NewExchangeByName(name)
if err != nil {
t.Fatalf("Cannot setup %v NewExchangeByName %v", name, err)
}
var exchCfg *config.Exchange
exchCfg, err = cfg.GetExchangeConfig(name)
if err != nil {
t.Fatalf("Cannot setup %v GetExchangeConfig %v", name, err)
}
exch.SetDefaults()
exchCfg.API.AuthenticatedSupport = true
exchCfg.API.Credentials = getExchangeCredentials(name)
err = exch.Setup(exchCfg)
if err != nil {
t.Fatalf("Cannot setup %v exchange Setup %v", name, err)
}
err = exch.UpdateTradablePairs(ctx)
require.Truef(t, errors.Is(err, context.DeadlineExceeded) || err == nil, "Exchange %s UpdateTradablePairs must not error: %s", name, err)
b := exch.GetBase()
assets := b.CurrencyPairs.GetAssetTypes(false)
require.NotEmptyf(t, assets, "Exchange %s must have assets", name)
for _, a := range assets {
require.NoErrorf(t, b.CurrencyPairs.SetAssetEnabled(a, true), "Exchange %s SetAssetEnabled must not error for asset %s: %s", name, a, err)
}
// Add +1 to len to verify that exchanges can handle requests with unset pairs and assets
assetPairs := make([]assetPair, 0, len(assets)+1)
assets:
for j := range assets {
var pairs currency.Pairs
pairs, err = b.CurrencyPairs.GetPairs(assets[j], false)
if err != nil {
t.Fatalf("Cannot setup %v asset %v GetPairs %v", name, assets[j], err)
}
var p currency.Pair
p, err = getPairFromPairs(t, pairs)
if err != nil {
if errors.Is(err, currency.ErrCurrencyPairsEmpty) {
continue
}
t.Fatalf("Cannot setup %v asset %v getPairFromPairs %v", name, assets[j], err)
}
err = b.CurrencyPairs.EnablePair(assets[j], p)
require.Truef(t, errors.Is(err, currency.ErrPairAlreadyEnabled) || err == nil, "Exchange %s EnablePair must not error for %s", name, p)
p, err = b.FormatExchangeCurrency(p, assets[j])
if err != nil {
t.Fatalf("Cannot setup %v asset %v FormatExchangeCurrency %v", name, assets[j], err)
}
for x := range unsupportedAssets {
if assets[j] == unsupportedAssets[x] {
// this asset cannot handle disrupt formatting
continue assets
}
}
p, err = disruptFormatting(t, p)
if err != nil {
t.Fatalf("Cannot setup %v asset %v disruptFormatting %v", name, assets[j], err)
}
assetPairs = append(assetPairs, assetPair{
Pair: p,
Asset: assets[j],
})
}
assetPairs = append(assetPairs, assetPair{})
return exch, assetPairs
}
// isUnacceptableError sentences errs to 10 years dungeon if unacceptable
func isUnacceptableError(t *testing.T, err error) error {
t.Helper()
for i := range acceptableErrors {
if errors.Is(err, acceptableErrors[i]) {
return nil
}
}
for i := range warningErrors {
if errors.Is(err, warningErrors[i]) {
t.Log(err)
return nil
}
}
return err
}
var validWrapperParams = []reflect.Type{
assetParam,
orderSubmitParam,
orderModifyParam,
orderCancelParam,
orderCancelsParam,
pairKeySliceParam,
getOrdersRequestParam,
latestRateRequest,
}
type testCtxKey string
func executeExchangeWrapperTests(ctx context.Context, t *testing.T, exch exchange.IBotExchange, assetParams []assetPair) {
t.Helper()
iExchange := reflect.TypeOf(&exch).Elem()
actualExchange := reflect.ValueOf(exch)
for x := range iExchange.NumMethod() {
methodName := iExchange.Method(x).Name
if _, ok := excludedMethodNames[methodName]; ok {
continue
}
method := actualExchange.MethodByName(methodName)
var assetLen int
for y := range method.Type().NumIn() {
input := method.Type().In(y)
if slices.ContainsFunc(validWrapperParams, func(t reflect.Type) bool {
return input.AssignableTo(t)
}) {
assetLen = len(assetParams)
break
}
}
tt := time.Now()
e := time.Date(tt.Year(), tt.Month(), tt.Day()-1, 0, 0, 0, 0, time.UTC)
s := e.Add(-time.Hour * 24 * 2)
if methodName == "GetHistoricTrades" {
// limit trade history
e = time.Now()
s = e.Add(-time.Minute * 3)
}
for y := range assetLen {
ap := assetParams[y]
t.Run(methodName+"-"+ap.Asset.String()+"-"+ap.Pair.String(), func(t *testing.T) {
t.Parallel()
// Create a new context for each test run to avoid race conditions
ctx := context.WithValue(ctx, testCtxKey("test"), t.Name()) //nolint:govet // Intentional shadow
inputs := make([]reflect.Value, method.Type().NumIn())
argGenerator := &MethodArgumentGenerator{
Exchange: exch,
AssetParams: ap,
MethodName: methodName,
Start: s,
End: e,
}
for z := range method.Type().NumIn() {
argGenerator.MethodInputType = method.Type().In(z)
generatedArg := generateMethodArg(ctx, t, argGenerator)
inputs[z] = *generatedArg
}
CallExchangeMethod(t, method, inputs, methodName, exch)
})
}
}
}
// CallExchangeMethod will call an exchange's method using generated arguments
// and determine if the error is friendly
func CallExchangeMethod(t *testing.T, methodToCall reflect.Value, methodValues []reflect.Value, methodName string, exch exchange.IBotExchange) {
t.Helper()
outputs := methodToCall.Call(methodValues)
for i := range outputs {
outputInterface := outputs[i].Interface()
err, ok := outputInterface.(error)
if !ok {
continue
}
if isUnacceptableError(t, err) != nil {
literalInputs := make([]any, len(methodValues))
for j := range methodValues {
switch {
case methodValues[j].Type().Implements(contextParam):
// Errorf will use reflection on ctx and cause a race, so we need to replace it
literalInputs[j] = "<context>"
case methodValues[j].Kind() == reflect.Ptr:
// dereference pointers just to add a bit more clarity
literalInputs[j] = methodValues[j].Elem().Interface()
default:
literalInputs[j] = methodValues[j].Interface()
}
}
t.Errorf("%v Func '%v' Error: '%v'. Inputs: %v.", exch.GetName(), methodName, err, literalInputs)
}
break
}
}
// MethodArgumentGenerator is used to create arguments for
// an IBotExchange method
type MethodArgumentGenerator struct {
Exchange exchange.IBotExchange
AssetParams assetPair
MethodInputType reflect.Type
MethodName string
Start time.Time
End time.Time
StartTimeSet bool
argNum int64
}
var (
currencyPairParam = reflect.TypeOf((*currency.Pair)(nil)).Elem()
klineParam = reflect.TypeOf((*kline.Interval)(nil)).Elem()
contextParam = reflect.TypeOf((*context.Context)(nil)).Elem()
timeParam = reflect.TypeOf((*time.Time)(nil)).Elem()
codeParam = reflect.TypeOf((*currency.Code)(nil)).Elem()
currencyPairsParam = reflect.TypeOf((*currency.Pairs)(nil)).Elem()
withdrawRequestParam = reflect.TypeOf((**withdraw.Request)(nil)).Elem()
stringParam = reflect.TypeOf((*string)(nil)).Elem()
feeBuilderParam = reflect.TypeOf((**exchange.FeeBuilder)(nil)).Elem()
credentialsParam = reflect.TypeOf((**accounts.Credentials)(nil)).Elem()
orderSideParam = reflect.TypeOf((*order.Side)(nil)).Elem()
collateralModeParam = reflect.TypeOf((*collateral.Mode)(nil)).Elem()
marginTypeParam = reflect.TypeOf((*margin.Type)(nil)).Elem()
int64Param = reflect.TypeOf((*int64)(nil)).Elem()
float64Param = reflect.TypeOf((*float64)(nil)).Elem()
// types with asset in params
assetParam = reflect.TypeOf((*asset.Item)(nil)).Elem()
orderSubmitParam = reflect.TypeOf((**order.Submit)(nil)).Elem()
orderModifyParam = reflect.TypeOf((**order.Modify)(nil)).Elem()
orderCancelParam = reflect.TypeOf((**order.Cancel)(nil)).Elem()
orderCancelsParam = reflect.TypeOf((*[]order.Cancel)(nil)).Elem()
getOrdersRequestParam = reflect.TypeOf((**order.MultiOrderRequest)(nil)).Elem()
positionChangeRequestParam = reflect.TypeOf((**margin.PositionChangeRequest)(nil)).Elem()
positionSummaryRequestParam = reflect.TypeOf((**futures.PositionSummaryRequest)(nil)).Elem()
positionsRequestParam = reflect.TypeOf((**futures.PositionsRequest)(nil)).Elem()
latestRateRequest = reflect.TypeOf((**fundingrate.LatestRateRequest)(nil)).Elem()
pairKeySliceParam = reflect.TypeOf((*[]key.PairAsset)(nil)).Elem()
)
// generateMethodArg determines the argument type and returns a pre-made
// response, else an empty version of the type
func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodArgumentGenerator) *reflect.Value {
t.Helper()
exchName := strings.ToLower(argGenerator.Exchange.GetName())
var input reflect.Value
switch {
case argGenerator.MethodInputType.AssignableTo(stringParam):
switch argGenerator.MethodName {
case "GetDepositAddress":
if argGenerator.argNum == 2 {
// account type
input = reflect.ValueOf("trading")
} else {
// Crypto Chain
input = reflect.ValueOf(cryptoChainPerExchange[exchName])
}
case "MatchSymbolWithAvailablePairs", "MatchSymbolCheckEnabled":
input = reflect.ValueOf(argGenerator.AssetParams.Pair.Base.Lower().String() + argGenerator.AssetParams.Pair.Quote.Lower().String())
default:
// OrderID
input = reflect.ValueOf("1337")
}
case argGenerator.MethodInputType.AssignableTo(pairKeySliceParam):
input = reflect.ValueOf(key.PairAsset{
Base: argGenerator.AssetParams.Pair.Base.Item,
Quote: argGenerator.AssetParams.Pair.Quote.Item,
Asset: argGenerator.AssetParams.Asset,
})
case argGenerator.MethodInputType.AssignableTo(credentialsParam):
input = reflect.ValueOf(&accounts.Credentials{
Key: "test",
Secret: "test",
ClientID: "test",
PEMKey: "test",
SubAccount: "test",
OneTimePassword: "test",
})
case argGenerator.MethodInputType.Implements(contextParam):
// Need to deploy a context.Context value as nil value is not checked throughout codebase
input = reflect.ValueOf(ctx)
case argGenerator.MethodInputType.AssignableTo(feeBuilderParam):
input = reflect.ValueOf(&exchange.FeeBuilder{
FeeType: exchange.OfflineTradeFee,
Amount: 150,
PurchasePrice: 150,
Pair: argGenerator.AssetParams.Pair,
})
case argGenerator.MethodInputType.AssignableTo(currencyPairParam):
input = reflect.ValueOf(argGenerator.AssetParams.Pair)
case argGenerator.MethodInputType.AssignableTo(assetParam):
input = reflect.ValueOf(argGenerator.AssetParams.Asset)
case argGenerator.MethodInputType.AssignableTo(klineParam):
input = reflect.ValueOf(kline.OneDay)
case argGenerator.MethodInputType.AssignableTo(codeParam):
if argGenerator.MethodName == "GetAvailableTransferChains" {
input = reflect.ValueOf(currency.ETH)
} else {
input = reflect.ValueOf(argGenerator.AssetParams.Pair.Base)
}
case argGenerator.MethodInputType.AssignableTo(timeParam):
if !argGenerator.StartTimeSet {
input = reflect.ValueOf(argGenerator.Start)
argGenerator.StartTimeSet = true
} else {
input = reflect.ValueOf(argGenerator.End)
}
case argGenerator.MethodInputType.AssignableTo(currencyPairsParam):
b := argGenerator.Exchange.GetBase()
if argGenerator.AssetParams.Asset != asset.Empty {
input = reflect.ValueOf(b.CurrencyPairs.Pairs[argGenerator.AssetParams.Asset].Available)
} else {
input = reflect.ValueOf(currency.Pairs{
argGenerator.AssetParams.Pair,
})
}
case argGenerator.MethodInputType.AssignableTo(withdrawRequestParam):
req := &withdraw.Request{
Exchange: exchName,
Description: "1337",
Amount: 1,
ClientOrderID: "1337",
WalletID: "7331",
}
if argGenerator.MethodName == "WithdrawCryptocurrencyFunds" {
req.Type = withdraw.Crypto
switch {
case !isFiat(t, argGenerator.AssetParams.Pair.Base.Item.Lower):
req.Currency = argGenerator.AssetParams.Pair.Base
case !isFiat(t, argGenerator.AssetParams.Pair.Quote.Item.Lower):
req.Currency = argGenerator.AssetParams.Pair.Quote
default:
req.Currency = currency.ETH
}
req.Crypto = withdraw.CryptoRequest{
Address: "1337",
AddressTag: "1337",
Chain: cryptoChainPerExchange[exchName],
}
} else {
req.Type = withdraw.Fiat
b := argGenerator.Exchange.GetBase()
if len(b.Config.BaseCurrencies) > 0 {
req.Currency = b.Config.BaseCurrencies[0]
} else {
req.Currency = currency.USD
}
req.Fiat = withdraw.FiatRequest{
Bank: banking.Account{
Enabled: true,
ID: "1337",
BankName: "1337",
BankAddress: "1337",
BankPostalCode: "1337",
BankPostalCity: "1337",
BankCountry: "1337",
AccountName: "1337",
AccountNumber: "1337",
SWIFTCode: "1337",
IBAN: "1337",
BSBNumber: "1337",
BankCode: 1337,
SupportedCurrencies: req.Currency.String(),
SupportedExchanges: exchName,
},
IsExpressWire: false,
RequiresIntermediaryBank: false,
IntermediaryBankAccountNumber: 1338,
IntermediaryBankName: "1338",
IntermediaryBankAddress: "1338",
IntermediaryBankCity: "1338",
IntermediaryBankCountry: "1338",
IntermediaryBankPostalCode: "1338",
IntermediarySwiftCode: "1338",
IntermediaryBankCode: 1338,
IntermediaryIBAN: "1338",
WireCurrency: "1338",
}
}
input = reflect.ValueOf(req)
case argGenerator.MethodInputType.AssignableTo(orderSubmitParam):
input = reflect.ValueOf(&order.Submit{
Exchange: exchName,
Type: order.Limit,
Side: order.Buy,
Pair: argGenerator.AssetParams.Pair,
AssetType: argGenerator.AssetParams.Asset,
Price: 150,
Amount: 1,
ClientID: "1337",
ClientOrderID: "13371337",
TimeInForce: order.ImmediateOrCancel,
Leverage: 1,
})
case argGenerator.MethodInputType.AssignableTo(orderModifyParam):
input = reflect.ValueOf(&order.Modify{
Exchange: exchName,
Type: order.Limit,
Side: order.Buy,
Pair: argGenerator.AssetParams.Pair,
AssetType: argGenerator.AssetParams.Asset,
Price: 150,
Amount: 1,
ClientOrderID: "13371337",
OrderID: "1337",
TimeInForce: order.ImmediateOrCancel,
})
case argGenerator.MethodInputType.AssignableTo(orderCancelParam):
input = reflect.ValueOf(&order.Cancel{
Exchange: exchName,
Type: order.Limit,
Side: order.Buy,
Pair: argGenerator.AssetParams.Pair,
AssetType: argGenerator.AssetParams.Asset,
OrderID: "1337",
})
case argGenerator.MethodInputType.AssignableTo(orderCancelsParam):
input = reflect.ValueOf([]order.Cancel{
{
Exchange: exchName,
Type: order.Market,
Side: order.Buy,
Pair: argGenerator.AssetParams.Pair,
AssetType: argGenerator.AssetParams.Asset,
OrderID: "1337",
},
})
case argGenerator.MethodInputType.AssignableTo(getOrdersRequestParam):
input = reflect.ValueOf(&order.MultiOrderRequest{
Type: order.AnyType,
Side: order.AnySide,
FromOrderID: "1337",
AssetType: argGenerator.AssetParams.Asset,
Pairs: currency.Pairs{argGenerator.AssetParams.Pair},
})
case argGenerator.MethodInputType.AssignableTo(marginTypeParam):
input = reflect.ValueOf(margin.Isolated)
case argGenerator.MethodInputType.AssignableTo(collateralModeParam):
input = reflect.ValueOf(collateral.SingleMode)
case argGenerator.MethodInputType.AssignableTo(positionChangeRequestParam):
input = reflect.ValueOf(&margin.PositionChangeRequest{
Exchange: argGenerator.Exchange.GetName(),
Pair: argGenerator.AssetParams.Pair,
Asset: argGenerator.AssetParams.Asset,
MarginType: margin.Isolated,
OriginalAllocatedMargin: 150,
NewAllocatedMargin: 151,
})
case argGenerator.MethodInputType.AssignableTo(positionSummaryRequestParam):
input = reflect.ValueOf(&futures.PositionSummaryRequest{
Asset: argGenerator.AssetParams.Asset,
Pair: argGenerator.AssetParams.Pair,
Direction: order.Buy,
})
case argGenerator.MethodInputType.AssignableTo(positionsRequestParam):
input = reflect.ValueOf(&futures.PositionsRequest{
Asset: argGenerator.AssetParams.Asset,
Pairs: currency.Pairs{argGenerator.AssetParams.Pair},
StartDate: argGenerator.Start,
EndDate: argGenerator.End,
RespectOrderHistoryLimits: true,
})
case argGenerator.MethodInputType.AssignableTo(orderSideParam):
input = reflect.ValueOf(order.Long)
case argGenerator.MethodInputType.AssignableTo(int64Param):
input = reflect.ValueOf(150)
case argGenerator.MethodInputType.AssignableTo(float64Param):
input = reflect.ValueOf(150.0)
case argGenerator.MethodInputType.AssignableTo(latestRateRequest):
input = reflect.ValueOf(&fundingrate.LatestRateRequest{
Asset: argGenerator.AssetParams.Asset,
Pair: argGenerator.AssetParams.Pair,
IncludePredictedRate: true,
})
default:
input = reflect.Zero(argGenerator.MethodInputType)
}
argGenerator.argNum++
return &input
}
// assetPair holds a currency pair associated with an asset
type assetPair struct {
Pair currency.Pair
Asset asset.Item
}
// excludedMethodNames represent the functions that are not
// currently tested under this suite due to irrelevance
// or not worth checking yet
var excludedMethodNames = map[string]struct{}{
"Setup": {}, // Is run via test setup
"Start": {}, // Is run via test setup
"SetDefaults": {}, // Is run via test setup
"UpdateTradablePairs": {}, // Is run via test setup
"GetDefaultConfig": {}, // Is run via test setup
"FetchTradablePairs": {}, // Is run via test setup
"AuthenticateWebsocket": {}, // Unnecessary websocket test
"FlushWebsocketChannels": {}, // Unnecessary websocket test
"UnsubscribeToWebsocketChannels": {}, // Unnecessary websocket test
"SubscribeToWebsocketChannels": {}, // Unnecessary websocket test
"UpdateCurrencyStates": {}, // Not widely supported/implemented feature
"CanTradePair": {}, // Not widely supported/implemented feature
"CanTrade": {}, // Not widely supported/implemented feature
"CanWithdraw": {}, // Not widely supported/implemented feature
"CanDeposit": {}, // Not widely supported/implemented feature
"GetCurrencyStateSnapshot": {}, // Not widely supported/implemented feature
"SetHTTPClientUserAgent": {}, // standard base implementation
"SetClientProxyAddress": {}, // standard base implementation
// Not widely supported/implemented futures endpoints
"GetCollateralCurrencyForContract": {},
"GetCurrencyForRealisedPNL": {},
"GetFuturesPositions": {},
"GetHistoricalFundingRates": {},
"IsPerpetualFutureCurrency": {},
"GetMarginRatesHistory": {},
"CalculatePNL": {},
"CalculateTotalCollateral": {},
"ScaleCollateral": {},
"GetPositionSummary": {},
"GetFuturesPositionSummary": {},
"GetFuturesPositionOrders": {},
"SetCollateralMode": {},
"GetCollateralMode": {},
"SetLeverage": {},
"GetLeverage": {},
"SetMarginType": {},
"ChangePositionMargin": {},
}
// blockedCIExchanges are exchanges that are not able to be tested on CI
var blockedCIExchanges = []string{
"binance", // binance API is banned from executing within the US where github Actions is ran
"bybit", // bybit API is banned from executing within the US where github Actions is ran
}
// unsupportedAssets contains assets that cannot handle
// normal processing for testing. This is to be used very sparingly
var unsupportedAssets = []asset.Item{
asset.Index,
}
var unsupportedExchangeNames = []string{
"testexch",
"bitflyer", // Bitflyer has many "ErrNotYetImplemented, which is true, but not what we care to test for here
"btse", // TODO rm once timeout issues resolved
"poloniex", // outdated API // TODO rm once updated
}
// cryptoChainPerExchange holds the deposit address chain per exchange
var cryptoChainPerExchange = map[string]string{
"binanceus": "ERC20",
"bybit": "ERC20",
"gateio": "ERC20",
}
// acceptable errors do not throw test errors, see below for why
var acceptableErrors = []error{
common.ErrFunctionNotSupported, // Shows API cannot perform function and developer has recognised this
common.ErrNotYetImplemented, // Shows API can perform function but developer has not implemented it yet
asset.ErrNotSupported, // Shows that valid and invalid asset types are handled
request.ErrAuthRequestFailed, // We must set authenticated requests properly in order to understand and better handle auth failures
order.ErrUnsupportedOrderType, // Should be returned if an ordertype like ANY is requested and the implementation knows to throw this specific error
currency.ErrCurrencyPairEmpty, // Demonstrates handling of EMPTYPAIR scenario and returns the correct error
currency.ErrCurrencyNotSupported, // Ensures a standard error is used for when a particular currency/pair is not supported by an exchange
currency.ErrCurrencyNotFound, // Semi-randomly selected currency pairs may not be found at an endpoint, so long as this is returned it is okay
asset.ErrNotEnabled, // Allows distinction when checking for supported versus enabled
request.ErrRateLimiterAlreadyEnabled, // If the rate limiter is already enabled, it is not an error
context.DeadlineExceeded, // If the context deadline is exceeded, it is not an error as only blockedCIExchanges use expired contexts by design
order.ErrPairIsEmpty, // Is thrown when the empty pair and asset scenario for an order submission is sent in the Validate() function
deposit.ErrAddressNotFound, // Is thrown when an address is not found due to the exchange requiring valid API keys
futures.ErrNotFuturesAsset, // Is thrown when a futures function receives a non-futures asset
currency.ErrSymbolStringEmpty, // Is thrown when a symbol string is empty for blank MatchSymbol func checks
futures.ErrNotPerpetualFuture, // Is thrown when a futures function receives a non-perpetual future
limits.ErrExchangeLimitNotLoaded, // Is thrown when the limits aren't loaded for a particular exchange, asset, pair
limits.ErrOrderLimitNotFound, // Is thrown when the order limit isn't found for a particular exchange, asset, pair
limits.ErrEmptyLevels, // Is thrown if limits are not provided for the asset
limits.ErrPriceBelowMin,
limits.ErrPriceExceedsMax,
limits.ErrPriceExceedsStep,
limits.ErrAmountBelowMin,
limits.ErrAmountExceedsMax,
limits.ErrAmountExceedsStep,
limits.ErrNotionalValue,
limits.ErrMarketAmountBelowMin,
limits.ErrMarketAmountExceedsMax,
limits.ErrMarketAmountExceedsStep,
accounts.ErrNoBalances,
accounts.ErrNoSubAccounts,
ticker.ErrTickerNotFound,
orderbook.ErrOrderbookNotFound,
websocket.ErrNotConnected,
}
// warningErrors will t.Log(err) when thrown to diagnose things, but not necessarily suggest
// that the implementation is in error
var warningErrors = []error{
kline.ErrNoTimeSeriesDataToConvert, // No data returned for a candle isn't worth failing the test suite over necessarily
}
// getPairFromPairs prioritises more normal pairs for an increased
// likelihood of returning data from API endpoints
func getPairFromPairs(t *testing.T, p currency.Pairs) (currency.Pair, error) {
t.Helper()
pFmt, err := p.GetFormatting()
if err != nil {
return currency.Pair{}, err
}
goodEth := currency.NewPair(currency.ETH, currency.USDT).Format(pFmt)
if p.Contains(goodEth, true) {
return goodEth, nil
}
for i := range p {
if p[i].Base.Equal(currency.ETH) {
return p[i], nil
}
}
goodBtc := currency.NewBTCUSDT().Format(pFmt)
if p.Contains(goodBtc, true) {
return goodBtc, nil
}
for i := range p {
if p[i].Base.Equal(currency.BTC) {
return p[i], nil
}
}
return p.GetRandomPair()
}
// isFiat helps determine fiat currency without using currency.storage
func isFiat(t *testing.T, c string) bool {
t.Helper()
fiats := []string{
currency.USD.Item.Lower,
currency.AUD.Item.Lower,
currency.EUR.Item.Lower,
currency.CAD.Item.Lower,
currency.TRY.Item.Lower,
currency.UAH.Item.Lower,
currency.RUB.Item.Lower,
currency.RUR.Item.Lower,
currency.JPY.Item.Lower,
currency.HKD.Item.Lower,
currency.SGD.Item.Lower,
currency.ZUSD.Item.Lower,
currency.ZEUR.Item.Lower,
currency.ZCAD.Item.Lower,
currency.ZJPY.Item.Lower,
}
return slices.Contains(fiats, c)
}
// disruptFormatting adds in an unused delimiter and strange casing features to
// ensure format currency pair is used throughout the code base.
func disruptFormatting(t *testing.T, p currency.Pair) (currency.Pair, error) {
t.Helper()
if p.Base.IsEmpty() {
return currency.EMPTYPAIR, errors.New("cannot disrupt formatting as base is not populated")
}
// NOTE: Quote can be empty for margin funding
return currency.Pair{
Base: p.Base.Upper(),
Quote: p.Quote.Lower(),
Delimiter: "-TEST-DELIM-",
}, nil
}
func getExchangeCredentials(exchangeName string) config.APICredentialsConfig {
var resp config.APICredentialsConfig
switch exchangeName {
case "lbank":
// these are just random keys, they are not usable
resp.Key = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3R2vuz3cpQUbCX0TgYZL
TiLSxUXdrvVEIyoqQyxNf+9fmHLEBrsO1s1msIKvWg24gdbLWXQ6NBCygO8OvZpm
+lfXD4MRv/0PxxIAkaD6Iplhv+qbae8nJkYQOpDJF3bPC9LCKfchCnRpZoGqkHgS
GqOBU13UDZ8BM1SaOLVBzcmE/iJCLPQPORNSzfLSb8TC+woe0AcaDmF9KjIzXPd0
Slacp1ZgZ+yIi1B5/akwxu6sGzHov6weXj/v9K8nUhL9+oPMd8FNzZ+z3viHY0fm
yWiHBywwlh4LgzrjGTUdUk9msjSr2rwjTdCp268A8ECC1fChvhdJfO3lYVj8ltDb
OQIDAQAB`
resp.Secret = `MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdHa+7PdylBRsJ
fROBhktOItLFRd2u9UQjKipDLE1/71+YcsQGuw7WzWawgq9aDbiB1stZdDo0ELKA
7w69mmb6V9cPgxG//Q/HEgCRoPoimWG/6ptp7ycmRhA6kMkXds8L0sIp9yEKdGlm
gaqQeBIao4FTXdQNnwEzVJo4tUHNyYT+IkIs9A85E1LN8tJvxML7Ch7QBxoOYX0q
MjNc93RKVpynVmBn7IiLUHn9qTDG7qwbMei/rB5eP+/0rydSEv36g8x3wU3Nn7Pe
+IdjR+bJaIcHLDCWHguDOuMZNR1ST2ayNKvavCNN0KnbrwDwQILV8KG+F0l87eVh
WPyW0Ns5AgMBAAECggEAdBs7hJmWO7yzlsbrsC7BajUU8eue3VkCv2hLqtwfkdcz
HkzdLB+bSiWvD25//0yHHv6X5tAGJALEiLl+xwbFnhzz27xaXLLYTxLf45hg4Dwk
PO9HTlf6+bj+mpIeVcjYLYAs3nZbDi9UjTP3SUcTUpOavBjf2YstyTNai/55oEF/
x+ulzP/OISVhKrk5iiSKgjB4KyFpQnBWyluTmnlNS17/T/k6FkECQFgNpzbUmHTH
Yq+s0I9fGXMMvsNnnoJjX6ALe9fkMjY6ijeA45plDeBZp+5J8uGOKV+/iTCNzm5o
wrQKPz335+tTZgsDdKLUFA9Rwmkcpn4PShOtnR6aZQKBgQD9tzFlomqt/mSWbHAV
Gfjog9snlvgEWBIUjfP5Ow79rbz0cGcL3GAexwKK1dwNmMHDx+fu4uVAIf0dM5aT
xfdp/I4OTkxOFcIupu+L4gmz1vY32pFLPQYbp+9oOAMy4thUFb5o/Dsq2g65e2BC
+gNALEWxPuhNYbI7c0cu5Y7AJwKBgQDfG1ovhNlETJO+oli25csayRwgm/qll4fH
sOnYospQiJ3ka0WjPT6NY8m2anWDp7+/guIwq+xXVF6wQNxNZc+6/MgNJo2R3XG5
FKPH5FYgI52Zv6VN1AUhdfInDpKQXQ8vWO6HV+/uJmHeZK2+D6nycN4dL2h7ElK/
sCthmNtFnwKBgQDCGdaGpLzspAScOBV/b0FH0Shmn07bM+2RIBCYiaAsXzCB6URM
hKpcoW/Ge1pAZK9IcrVzws4URGx6XK9EGl3wDbE4LJqf2nGWc0wsPh+iIEB59pLV
drgnjFDR8Jgx4+4QVho4A0/Ytr4xFLxOQSsfez9OHIxoNue+J7E7pY+SXQKBgBTT
0tl4x2eO1oQHV8zLKui3OX750K5AtRY5N7tXhxd5iXPXZ8rTXtGILT5wNcQylr3k
FAWDJy8H20cM5wP6qyfDjVFc9f5V89XZTWjNshSR/pZpw56+WjRDdHWc8KW1akN7
Q9kypl1PC/fc4jNJ9w2A59tFn7VNgpgOdB5KTL31AoGBAN3BIjKXzoOJnVGL3bja
SYC2m+JcRn/mVO7I5Hop8GDoWXPFAnPNx1YKSpRLM/EV+ukUJsOV/LTPb7BsXMsJ
IY9SZceJS6glsxt+blFxGEpypyv13xW+jeCrPjlxQX2TNbL0KwHqvm1zMnM9bss/
Rsd80LrBCVI8ctzrvYRFSugC`
default:
resp.Key = "realKey"
resp.Secret = "YXBpU2VjcmV0" // base64 encoded "apiSecret"
resp.ClientID = "realClientID"
}
return resp
}
func isCITest() bool {
return os.Getenv("CI") == "true"
}
func skipAdditionalWrapperCITests() bool {
return os.Getenv("SKIP_WRAPPER_CI_TESTS") == "true"
}