exchange/order/limits: Migrate to new package and integrate with exchanges (#1860)

* move limits, transition to key gen

* rollout NewExchangePairAssetKey everywhere

* test improvements

* self-review fixes

* ok, lets go

* fix merge issue

* slower value func,assertify,drop IsValidPairString

* remove binance reference for backtesting test

* Redundant nil checks removed due to redundancy

* Update order_test.go

* Move limits back into /exchanges/

* puts limits in a different box again

* SHAZBERT SPECIAL SUGGESTIONS

* Update gateio_wrapper.go

* fixes all build issues

* Many niteroos!

* something has gone awry

* bugfix

* gk's everywhere nits

* lint

* extra lint

* re-remove IsValidPairString

* lint fix

* standardise test

* revert some bads

* dupe rm

* another revert 360 mcgee

* un-in-revertify

* Update exchange/order/limits/levels_test.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* fix

* Update exchanges/binance/binance_test.go

HERE'S HOPING GITHUB FORMATS THIS CORRECTLY!

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* update text

* rn func, same line err gk4202000

---------

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
This commit is contained in:
Scott
2025-08-26 12:30:21 +10:00
committed by GitHub
parent fc0f262c42
commit 85403fe801
103 changed files with 1751 additions and 2168 deletions

View File

@@ -3582,22 +3582,6 @@ func (e *Exchange) InitiateFlashSwapOrderReview(ctx context.Context, arg FlashSw
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, flashOrderReviewEPL, http.MethodPost, gateioFlashSwapOrdersPreview, nil, &arg, &response)
}
// IsValidPairString returns true if the string represents a valid currency pair
func (e *Exchange) IsValidPairString(currencyPair string) bool {
if len(currencyPair) < 3 {
return false
}
pf, err := e.CurrencyPairs.GetFormat(asset.Spot, true)
if err != nil {
return false
}
if strings.Contains(currencyPair, pf.Delimiter) {
result := strings.Split(currencyPair, pf.Delimiter)
return len(result) >= 2
}
return false
}
// ********************************* Trading Fee calculation ********************************
// GetFee returns an estimate of fee based on type of transaction

View File

@@ -2473,46 +2473,23 @@ func TestUnlockSubAccount(t *testing.T) {
func TestUpdateOrderExecutionLimits(t *testing.T) {
t.Parallel()
testexch.UpdatePairsOnce(t, e)
err := e.UpdateOrderExecutionLimits(t.Context(), 1336)
require.ErrorIs(t, err, asset.ErrNotSupported)
err = e.UpdateOrderExecutionLimits(t.Context(), asset.Options)
require.ErrorIs(t, err, common.ErrNotYetImplemented)
err = e.UpdateOrderExecutionLimits(t.Context(), asset.Spot)
if err != nil {
t.Fatal(err)
}
avail, err := e.GetAvailablePairs(asset.Spot)
if err != nil {
t.Fatal(err)
}
for i := range avail {
mm, err := e.GetOrderExecutionLimits(asset.Spot, avail[i])
if err != nil {
t.Fatal(err)
}
if mm == (order.MinMaxLevel{}) {
t.Fatal("expected a value")
}
if mm.MinimumBaseAmount <= 0 {
t.Fatalf("MinimumBaseAmount expected 0 but received %v for %v", mm.MinimumBaseAmount, avail[i])
}
// 1INCH_TRY no minimum quote or base values are returned.
if mm.QuoteStepIncrementSize <= 0 {
t.Fatalf("QuoteStepIncrementSize expected 0 but received %v for %v", mm.QuoteStepIncrementSize, avail[i])
}
if mm.AmountStepIncrementSize <= 0 {
t.Fatalf("AmountStepIncrementSize expected 0 but received %v for %v", mm.AmountStepIncrementSize, avail[i])
}
for _, a := range e.GetAssetTypes(false) {
t.Run(a.String(), func(t *testing.T) {
t.Parallel()
switch a {
case asset.Options:
return // Options not supported
case asset.CrossMargin, asset.Margin:
require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), a), asset.ErrNotSupported)
default:
require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error")
pairs, err := e.CurrencyPairs.GetPairs(a, true)
require.NoError(t, err, "GetPairs must not error")
l, err := e.GetOrderExecutionLimits(a, pairs[0])
require.NoError(t, err, "GetOrderExecutionLimits must not error")
assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive")
}
})
}
}

View File

@@ -641,7 +641,7 @@ type FuturesContract struct {
InDelisting bool `json:"in_delisting"`
RiskLimitBase string `json:"risk_limit_base"`
InterestRate string `json:"interest_rate"`
OrderPriceRound string `json:"order_price_round"`
OrderPriceRound types.Number `json:"order_price_round"`
OrderSizeMin int64 `json:"order_size_min"`
RefRebateRate string `json:"ref_rebate_rate"`
FundingInterval int64 `json:"funding_interval"`
@@ -845,7 +845,7 @@ type OptionContract struct {
Underlying string `json:"underlying"`
UnderlyingPrice types.Number `json:"underlying_price"`
Multiplier string `json:"multiplier"`
OrderPriceRound string `json:"order_price_round"`
OrderPriceRound types.Number `json:"order_price_round"`
MarkPriceRound string `json:"mark_price_round"`
MakerFeeRate string `json:"maker_fee_rate"`
TakerFeeRate string `json:"taker_fee_rate"`

View File

@@ -16,6 +16,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"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/account"
@@ -422,9 +423,6 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren
continue
}
p := strings.ToUpper(tradables[x].ID)
if !e.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
@@ -443,9 +441,6 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren
continue
}
p := strings.ToUpper(tradables[x].Base + currency.UnderscoreDelimiter + tradables[x].Quote)
if !e.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
@@ -468,9 +463,6 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren
continue
}
p := strings.ToUpper(contracts[i].Name)
if !e.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
@@ -489,9 +481,6 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren
continue
}
p := strings.ToUpper(contracts[i].Name)
if !e.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
@@ -511,9 +500,6 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren
return nil, err
}
for c := range contracts {
if !e.IsValidPairString(contracts[c].Name) {
continue
}
cp, err := currency.NewPairFromString(strings.ReplaceAll(contracts[c].Name, currency.DashDelimiter, currency.UnderscoreDelimiter))
if err != nil {
return nil, err
@@ -683,7 +669,7 @@ func (e *Exchange) UpdateOrderbookWithLimit(ctx context.Context, p currency.Pair
case asset.Options:
o, err = e.GetOptionsOrderbook(ctx, p, "", limit, true)
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
if err != nil {
return nil, err
@@ -1824,7 +1810,7 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, a asset.Item)
return nil, futures.ErrNotFuturesAsset
}
if !e.SupportsAsset(a) {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
settle, err := getSettlementCurrency(currency.EMPTYPAIR, a)
if err != nil {
@@ -1920,7 +1906,7 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, a asset.Item)
}
return resp, nil
}
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
// UpdateOrderExecutionLimits sets exchange executions for a required asset type
@@ -1929,7 +1915,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item)
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
}
var limits []order.MinMaxLevel
var l []limits.MinMaxLevel
switch a {
case asset.Spot:
pairsData, err := e.ListSpotCurrencyPairs(ctx)
@@ -1937,7 +1923,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item)
return err
}
limits = make([]order.MinMaxLevel, 0, len(pairsData))
l = make([]limits.MinMaxLevel, 0, len(pairsData))
for i := range pairsData {
if pairsData[i].TradeStatus == "untradable" {
continue
@@ -1954,21 +1940,110 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item)
minBaseAmount = math.Pow10(-int(pairsData[i].AmountPrecision))
}
limits = append(limits, order.MinMaxLevel{
Asset: a,
Pair: pair,
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, pair),
QuoteStepIncrementSize: math.Pow10(-int(pairsData[i].Precision)),
AmountStepIncrementSize: math.Pow10(-int(pairsData[i].AmountPrecision)),
MinimumBaseAmount: minBaseAmount,
MinimumQuoteAmount: pairsData[i].MinQuoteAmount.Float64(),
})
}
case asset.CoinMarginedFutures:
btcContracts, err := e.GetAllFutureContracts(ctx, currency.BTC)
if err != nil {
return err
}
l = make([]limits.MinMaxLevel, 0, len(btcContracts))
for x := range btcContracts {
p := strings.ToUpper(btcContracts[x].Name)
cp, err := currency.NewPairFromString(p)
if err != nil {
return err
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(btcContracts[x].OrderSizeMin),
MaximumBaseAmount: float64(btcContracts[x].OrderSizeMax),
PriceStepIncrementSize: btcContracts[x].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
})
}
case asset.USDTMarginedFutures:
usdtContracts, err := e.GetAllFutureContracts(ctx, currency.USDT)
if err != nil {
return err
}
l = make([]limits.MinMaxLevel, 0, len(usdtContracts))
for x := range usdtContracts {
p := strings.ToUpper(usdtContracts[x].Name)
cp, err := currency.NewPairFromString(p)
if err != nil {
return err
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(usdtContracts[x].OrderSizeMin),
MaximumBaseAmount: float64(usdtContracts[x].OrderSizeMax),
PriceStepIncrementSize: usdtContracts[x].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
})
}
case asset.DeliveryFutures:
btcContracts, err := e.GetAllDeliveryContracts(ctx, currency.BTC)
if err != nil {
return err
}
usdtContracts, err := e.GetAllDeliveryContracts(ctx, currency.USDT)
if err != nil {
return err
}
btcContracts = append(btcContracts, usdtContracts...)
l = make([]limits.MinMaxLevel, 0, len(btcContracts))
for x := range btcContracts {
p := strings.ToUpper(btcContracts[x].Name)
cp, err := currency.NewPairFromString(p)
if err != nil {
return err
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(btcContracts[x].OrderSizeMin),
MaximumBaseAmount: float64(btcContracts[x].OrderSizeMax),
PriceStepIncrementSize: btcContracts[x].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
})
}
case asset.Options:
underlyings, err := e.GetAllOptionsUnderlyings(ctx)
if err != nil {
return err
}
for x := range underlyings {
contracts, err := e.GetAllContractOfUnderlyingWithinExpiryDate(ctx, underlyings[x].Name, time.Time{})
if err != nil {
return err
}
l = make([]limits.MinMaxLevel, 0, len(contracts))
for c := range contracts {
cp, err := currency.NewPairFromString(strings.ReplaceAll(contracts[c].Name, currency.DashDelimiter, currency.UnderscoreDelimiter))
if err != nil {
return err
}
cp.Quote = currency.NewCode(strings.ReplaceAll(cp.Quote.String(), currency.UnderscoreDelimiter, currency.DashDelimiter))
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(contracts[c].OrderSizeMin),
MaximumBaseAmount: float64(contracts[c].OrderSizeMax),
PriceStepIncrementSize: contracts[c].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
})
}
}
default:
// TODO: Add in other assets
return fmt.Errorf("%s %w", a, common.ErrNotYetImplemented)
return fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
return e.LoadLimits(limits)
return limits.Load(l)
}
// GetHistoricalFundingRates returns historical funding rates for a futures contract
@@ -2091,11 +2166,7 @@ func (e *Exchange) GetLatestFundingRates(ctx context.Context, r *fundingrate.Lat
}
resp := make([]fundingrate.LatestRateResponse, 0, len(contracts))
for i := range contracts {
p := strings.ToUpper(contracts[i].Name)
if !e.IsValidPairString(p) {
continue
}
cp, err := currency.NewPairFromString(p)
cp, err := currency.NewPairFromString(contracts[i].Name)
if err != nil {
return nil, err
}
@@ -2183,7 +2254,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, keys ...key.PairAsset) (
}
}
resp = append(resp, futures.OpenInterest{
Key: key.ExchangePairAsset{
Key: key.ExchangeAssetPair{
Exchange: e.Name,
Base: p.Base.Item,
Quote: p.Quote.Item,
@@ -2313,7 +2384,7 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre
}
return tradeBaseURL + futuresPath + settle.String() + "/" + cp.Upper().String(), nil
default:
return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a)
return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
}