From 85403fe8017c2839acbc030c2998487edb21607c Mon Sep 17 00:00:00 2001 From: Scott Date: Tue, 26 Aug 2025 12:30:21 +1000 Subject: [PATCH] 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 * fix * Update exchanges/binance/binance_test.go HERE'S HOPING GITHUB FORMATS THIS CORRECTLY! Co-authored-by: Gareth Kirwan * update text * rn func, same line err gk4202000 --------- Co-authored-by: Adrian Gallagher Co-authored-by: Gareth Kirwan --- .../strategyexamples/dca-api-candles.strat | 4 +- backtester/data/data.go | 20 +- backtester/data/data_test.go | 7 +- backtester/data/data_types.go | 2 +- backtester/engine/backtest_test.go | 6 +- backtester/engine/setup.go | 25 +- backtester/eventhandlers/exchange/exchange.go | 4 +- .../eventhandlers/exchange/exchange_test.go | 8 +- .../eventhandlers/exchange/exchange_types.go | 4 +- .../eventhandlers/portfolio/portfolio.go | 35 +- .../eventhandlers/portfolio/portfolio_test.go | 55 +-- .../portfolio/portfolio_types.go | 2 +- .../eventhandlers/portfolio/risk/risk.go | 7 +- .../eventhandlers/portfolio/risk/risk_test.go | 37 +- .../portfolio/risk/risk_types.go | 2 +- backtester/eventhandlers/portfolio/setup.go | 11 +- .../statistics/fundingstatistics.go | 2 +- .../statistics/fundingstatistics_test.go | 21 +- .../eventhandlers/statistics/statistics.go | 32 +- .../statistics/statistics_test.go | 31 +- .../statistics/statistics_types.go | 2 +- backtester/report/chart.go | 4 +- backtester/report/chart_test.go | 25 +- backtester/report/report.go | 7 +- backtester/report/report_test.go | 32 +- .../exchange_wrapper_standards_test.go | 8 +- common/key/key.go | 87 ++--- common/key/key_test.go | 84 ++--- currency/manager.go | 2 +- engine/rpcserver.go | 2 +- engine/rpcserver_test.go | 7 +- engine/sync_manager.go | 34 +- engine/sync_manager_test.go | 4 +- engine/sync_manager_types.go | 4 +- exchange/order/limits/levels.go | 147 ++++++++ exchange/order/limits/levels_test.go | 164 ++++++++ exchange/order/limits/limits_types.go | 72 ++++ exchange/order/limits/store.go | 118 ++++++ exchange/order/limits/store_test.go | 225 +++++++++++ exchanges/binance/binance.go | 50 +-- exchanges/binance/binance_cfutures.go | 14 +- exchanges/binance/binance_test.go | 121 ++---- exchanges/binance/binance_ufutures.go | 15 +- exchanges/binance/binance_wrapper.go | 47 +-- exchanges/binanceus/binanceus_wrapper.go | 6 +- exchanges/bitfinex/bitfinex.go | 30 +- exchanges/bitfinex/bitfinex_test.go | 34 +- exchanges/bitfinex/bitfinex_wrapper.go | 9 +- exchanges/bithumb/bithumb.go | 14 +- exchanges/bithumb/bithumb_test.go | 22 +- exchanges/bithumb/bithumb_wrapper.go | 10 +- exchanges/bitmex/bitmex_wrapper.go | 14 +- exchanges/bitstamp/bitstamp_test.go | 50 +-- exchanges/bitstamp/bitstamp_wrapper.go | 11 +- exchanges/btcmarkets/btcmarkets_test.go | 21 +- exchanges/btcmarkets/btcmarkets_wrapper.go | 13 +- exchanges/btse/btse_test.go | 31 +- exchanges/btse/btse_wrapper.go | 27 +- exchanges/bybit/bybit.go | 19 +- exchanges/bybit/bybit_test.go | 45 +-- exchanges/bybit/bybit_wrapper.go | 28 +- exchanges/coinut/coinut_wrapper.go | 2 +- exchanges/deribit/deribit_test.go | 27 +- exchanges/deribit/deribit_wrapper.go | 27 +- exchanges/exchange.go | 37 +- exchanges/exchange_test.go | 69 ++-- exchanges/exchange_types.go | 2 - exchanges/futures/futures.go | 65 +--- exchanges/futures/futures_test.go | 77 +--- exchanges/futures/futures_types.go | 4 +- exchanges/gateio/gateio.go | 16 - exchanges/gateio/gateio_test.go | 57 +-- exchanges/gateio/gateio_types.go | 4 +- exchanges/gateio/gateio_wrapper.go | 137 +++++-- exchanges/gemini/gemini_test.go | 30 +- exchanges/gemini/gemini_wrapper.go | 13 +- exchanges/hitbtc/hitbtc_wrapper.go | 2 +- exchanges/huobi/huobi_wrapper.go | 38 +- exchanges/interfaces.go | 3 +- exchanges/kraken/kraken_test.go | 28 +- exchanges/kraken/kraken_wrapper.go | 21 +- exchanges/kucoin/kucoin.go | 65 ++-- exchanges/kucoin/kucoin_futures.go | 19 +- exchanges/kucoin/kucoin_test.go | 122 +++--- exchanges/kucoin/kucoin_wrapper.go | 32 +- exchanges/lbank/lbank.go | 5 +- exchanges/lbank/lbank_test.go | 5 +- exchanges/okx/okx.go | 67 ++-- exchanges/okx/okx_test.go | 124 +++--- exchanges/okx/okx_types.go | 7 +- exchanges/okx/okx_wrapper.go | 65 ++-- exchanges/order/limits.go | 356 ------------------ exchanges/order/limits_test.go | 313 --------------- exchanges/order/order_test.go | 37 +- exchanges/orderbook/depth.go | 4 +- exchanges/orderbook/depth_test.go | 2 +- exchanges/orderbook/orderbook.go | 16 +- exchanges/orderbook/orderbook_types.go | 4 +- exchanges/poloniex/poloniex_wrapper.go | 2 +- exchanges/sharedtestvalues/customex.go | 5 +- exchanges/ticker/ticker.go | 25 +- exchanges/ticker/ticker_test.go | 9 +- exchanges/ticker/ticker_types.go | 2 +- 103 files changed, 1751 insertions(+), 2168 deletions(-) create mode 100644 exchange/order/limits/levels.go create mode 100644 exchange/order/limits/levels_test.go create mode 100644 exchange/order/limits/limits_types.go create mode 100644 exchange/order/limits/store.go create mode 100644 exchange/order/limits/store_test.go delete mode 100644 exchanges/order/limits.go delete mode 100644 exchanges/order/limits_test.go diff --git a/backtester/config/strategyexamples/dca-api-candles.strat b/backtester/config/strategyexamples/dca-api-candles.strat index 83ba2774..013eac27 100644 --- a/backtester/config/strategyexamples/dca-api-candles.strat +++ b/backtester/config/strategyexamples/dca-api-candles.strat @@ -11,10 +11,10 @@ }, "currency-settings": [ { - "exchange-name": "bitfinex", + "exchange-name": "okx", "asset": "spot", "base": "BTC", - "quote": "USD", + "quote": "USDT", "spot-details": { "initial-quote-funds": "100000" }, diff --git a/backtester/data/data.go b/backtester/data/data.go index 41c575d3..bdc607b6 100644 --- a/backtester/data/data.go +++ b/backtester/data/data.go @@ -15,7 +15,7 @@ import ( // NewHandlerHolder returns a new HandlerHolder func NewHandlerHolder() *HandlerHolder { return &HandlerHolder{ - data: make(map[key.ExchangePairAsset]Handler), + data: make(map[key.ExchangeAssetPair]Handler), } } @@ -27,15 +27,10 @@ func (h *HandlerHolder) SetDataForCurrency(e string, a asset.Item, p currency.Pa h.m.Lock() defer h.m.Unlock() if h.data == nil { - h.data = make(map[key.ExchangePairAsset]Handler) + h.data = make(map[key.ExchangeAssetPair]Handler) } e = strings.ToLower(e) - h.data[key.ExchangePairAsset{ - Exchange: e, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }] = k + h.data[key.NewExchangeAssetPair(e, a, p)] = k return nil } @@ -66,12 +61,7 @@ func (h *HandlerHolder) GetDataForCurrency(ev common.Event) (Handler, error) { exch := ev.GetExchange() a := ev.GetAssetType() p := ev.Pair() - handler, ok := h.data[key.ExchangePairAsset{ - Exchange: exch, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }] + handler, ok := h.data[key.NewExchangeAssetPair(exch, a, p)] if !ok { return nil, fmt.Errorf("%s %s %s %w", exch, a, p, ErrHandlerNotFound) } @@ -85,7 +75,7 @@ func (h *HandlerHolder) Reset() error { } h.m.Lock() defer h.m.Unlock() - h.data = make(map[key.ExchangePairAsset]Handler) + h.data = make(map[key.ExchangeAssetPair]Handler) return nil } diff --git a/backtester/data/data_test.go b/backtester/data/data_test.go index ad5473cf..467cc7a0 100644 --- a/backtester/data/data_test.go +++ b/backtester/data/data_test.go @@ -40,12 +40,7 @@ func TestSetDataForCurrency(t *testing.T) { if d.data == nil { t.Error("expected not nil") } - if d.data[key.ExchangePairAsset{ - Exchange: exch, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }] != nil { + if d.data[key.NewExchangeAssetPair(exch, a, p)] != nil { t.Error("expected nil") } } diff --git a/backtester/data/data_types.go b/backtester/data/data_types.go index 921823b9..6d353012 100644 --- a/backtester/data/data_types.go +++ b/backtester/data/data_types.go @@ -29,7 +29,7 @@ var ( // HandlerHolder stores an event handler per exchange asset pair type HandlerHolder struct { m sync.Mutex - data map[key.ExchangePairAsset]Handler + data map[key.ExchangeAssetPair]Handler } // Holder interface dictates what a Data holder is expected to do diff --git a/backtester/engine/backtest_test.go b/backtester/engine/backtest_test.go index a8ba5b19..ac654cb0 100644 --- a/backtester/engine/backtest_test.go +++ b/backtester/engine/backtest_test.go @@ -68,7 +68,7 @@ func TestSetupFromConfig(t *testing.T) { err = bt.SetupFromConfig(cfg, "", "", false) assert.ErrorIs(t, err, base.ErrStrategyNotFound) - const testExchange = "bitfinex" + const testExchange = "okx" cfg.CurrencySettings = []config.CurrencySettings{ { @@ -424,7 +424,7 @@ func TestFullCycle(t *testing.T) { tt := time.Now() stats := &statistics.Statistic{} - stats.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) + stats.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*statistics.CurrencyPairStatistic) port, err := portfolio.Setup(&size.Size{ BuySide: exchange.MinMax{}, SellSide: exchange.MinMax{}, @@ -542,7 +542,7 @@ func TestFullCycleMulti(t *testing.T) { tt := time.Now() stats := &statistics.Statistic{} - stats.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) + stats.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*statistics.CurrencyPairStatistic) port, err := portfolio.Setup(&size.Size{ BuySide: exchange.MinMax{}, diff --git a/backtester/engine/setup.go b/backtester/engine/setup.go index 9dbfb9b0..a8a28527 100644 --- a/backtester/engine/setup.go +++ b/backtester/engine/setup.go @@ -36,11 +36,11 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" gctdatabase "github.com/thrasher-corp/gocryptotrader/database" "github.com/thrasher-corp/gocryptotrader/engine" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" gctexchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/currencystate" gctkline "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/log" ) @@ -194,7 +194,7 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str } portfolioRisk := &risk.Risk{ - CurrencySettings: make(map[key.ExchangePairAsset]*risk.CurrencySettings), + CurrencySettings: make(map[key.ExchangeAssetPair]*risk.CurrencySettings), } bt.Funding = funds @@ -234,7 +234,7 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str err) } if portfolioRisk.CurrencySettings == nil { - portfolioRisk.CurrencySettings = make(map[key.ExchangePairAsset]*risk.CurrencySettings) + portfolioRisk.CurrencySettings = make(map[key.ExchangeAssetPair]*risk.CurrencySettings) } var curr currency.Pair @@ -261,12 +261,7 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str portSet.MaximumOrdersWithLeverageRatio = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrdersWithLeverageRatio portSet.MaxLeverageRate = cfg.CurrencySettings[i].FuturesDetails.Leverage.MaximumOrderLeverageRate } - portfolioRisk.CurrencySettings[key.ExchangePairAsset{ - Exchange: cfg.CurrencySettings[i].ExchangeName, - Base: cfg.CurrencySettings[i].Base.Item, - Quote: cfg.CurrencySettings[i].Quote.Item, - Asset: a, - }] = portSet + portfolioRisk.CurrencySettings[key.NewExchangeAssetPair(cfg.CurrencySettings[i].ExchangeName, a, curr)] = portSet if cfg.CurrencySettings[i].MakerFee != nil && cfg.CurrencySettings[i].TakerFee != nil && cfg.CurrencySettings[i].MakerFee.GreaterThan(*cfg.CurrencySettings[i].TakerFee) { @@ -332,7 +327,7 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str return err } default: - return fmt.Errorf("%w: %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w: %q", asset.ErrNotSupported, a) } default: var bFunds, qFunds decimal.Decimal @@ -398,7 +393,7 @@ func (bt *BackTest) SetupFromConfig(cfg *config.Config, templatePath, output str StrategyNickname: cfg.Nickname, StrategyDescription: bt.Strategy.Description(), StrategyGoal: cfg.Goal, - ExchangeAssetPairStatistics: make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic), + ExchangeAssetPairStatistics: make(map[key.ExchangeAssetPair]*statistics.CurrencyPairStatistic), RiskFreeRate: cfg.StatisticSettings.RiskFreeRate, CandleInterval: cfg.DataSettings.Interval, FundManager: bt.Funding, @@ -584,12 +579,12 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (*exchange.Exchang MaximumTotal: cfg.CurrencySettings[i].SellSide.MaximumTotal, } - limits, err := exch.GetOrderExecutionLimits(a, pair) - if err != nil && !errors.Is(err, gctorder.ErrExchangeLimitNotLoaded) { + l, err := exch.GetOrderExecutionLimits(a, pair) + if err != nil && !errors.Is(err, limits.ErrOrderLimitNotFound) { return resp, err } - if limits != (gctorder.MinMaxLevel{}) { + if l != (limits.MinMaxLevel{}) { if !cfg.CurrencySettings[i].CanUseExchangeLimits { if realOrders { log.Warnf(common.Setup, "Exchange %s order execution limits enabled for %s %s due to using real orders", @@ -626,7 +621,7 @@ func (bt *BackTest) setupExchangeSettings(cfg *config.Config) (*exchange.Exchang BuySide: buyRule, SellSide: sellRule, Leverage: lev, - Limits: limits, + Limits: l, SkipCandleVolumeFitting: cfg.CurrencySettings[i].SkipCandleVolumeFitting, CanUseExchangeLimits: cfg.CurrencySettings[i].CanUseExchangeLimits, UseExchangePNLCalculation: cfg.CurrencySettings[i].UseExchangePNLCalculation, diff --git a/backtester/eventhandlers/exchange/exchange.go b/backtester/eventhandlers/exchange/exchange.go index 8582e30b..4d789a12 100644 --- a/backtester/eventhandlers/exchange/exchange.go +++ b/backtester/eventhandlers/exchange/exchange.go @@ -119,9 +119,7 @@ func (e *Exchange) ExecuteOrder(o order.Event, dh data.Handler, om *engine.Order } if cs.CanUseExchangeLimits || cs.UseRealOrders { - // Conforms the amount to the exchange order defined step amount - // reducing it when needed - adjustedAmount = cs.Limits.ConformToDecimalAmount(amount) + adjustedAmount = cs.Limits.FloorAmountToStepIncrementDecimal(amount) if !adjustedAmount.Equal(amount) && !adjustedAmount.IsZero() { f.AppendReasonf("Order size shrunk from %v to %v to remain within exchange step amount limits", amount, diff --git a/backtester/eventhandlers/exchange/exchange_test.go b/backtester/eventhandlers/exchange/exchange_test.go index e3cf6298..0078de66 100644 --- a/backtester/eventhandlers/exchange/exchange_test.go +++ b/backtester/eventhandlers/exchange/exchange_test.go @@ -28,7 +28,7 @@ import ( gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) -const testExchange = "binance" +const testExchange = "okx" type fakeFund struct{} @@ -205,6 +205,8 @@ func TestPlaceOrder(t *testing.T) { assert.ErrorIs(t, err, engine.ErrExchangeNameIsEmpty) f.Exchange = testExchange + require.NoError(t, exch.UpdateOrderExecutionLimits(t.Context(), asset.Spot), "UpdateOrderExecutionLimits must not error") + _, err = e.placeOrder(t.Context(), decimal.NewFromInt(1), decimal.NewFromInt(1), decimal.Zero, false, true, f, bot.OrderManager) assert.ErrorIs(t, err, gctorder.ErrPairIsEmpty) @@ -356,7 +358,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) { err = exch.UpdateOrderExecutionLimits(t.Context(), asset.Spot) require.NoError(t, err, "UpdateOrderExecutionLimits must not error") - limits, err := exch.GetOrderExecutionLimits(a, p) + l, err := exch.GetOrderExecutionLimits(a, p) require.NoError(t, err, "GetOrderExecutionLimits must not error") f := &btcmarkets.Exchange{} @@ -375,7 +377,7 @@ func TestExecuteOrderBuySellSizeLimit(t *testing.T) { MaximumSize: decimal.NewFromFloat(0.1), }, MaximumSlippageRate: decimal.NewFromInt(1), - Limits: limits, + Limits: l, } e := Exchange{ CurrencySettings: []Settings{cs}, diff --git a/backtester/eventhandlers/exchange/exchange_types.go b/backtester/eventhandlers/exchange/exchange_types.go index 76b689fd..8fc12d30 100644 --- a/backtester/eventhandlers/exchange/exchange_types.go +++ b/backtester/eventhandlers/exchange/exchange_types.go @@ -10,9 +10,9 @@ import ( "github.com/thrasher-corp/gocryptotrader/backtester/funding" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - gctorder "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) var ( @@ -57,8 +57,8 @@ type Settings struct { MinimumSlippageRate decimal.Decimal MaximumSlippageRate decimal.Decimal - Limits gctorder.MinMaxLevel CanUseExchangeLimits bool + Limits limits.MinMaxLevel SkipCandleVolumeFitting bool UseExchangePNLCalculation bool diff --git a/backtester/eventhandlers/portfolio/portfolio.go b/backtester/eventhandlers/portfolio/portfolio.go index c6218168..e6c011fc 100644 --- a/backtester/eventhandlers/portfolio/portfolio.go +++ b/backtester/eventhandlers/portfolio/portfolio.go @@ -56,12 +56,7 @@ func (p *Portfolio) OnSignal(ev signal.Event, exchangeSettings *exchange.Setting return o, errInvalidDirection } - lookup := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: ev.GetExchange(), - Base: ev.Pair().Base.Item, - Quote: ev.Pair().Quote.Item, - Asset: ev.GetAssetType(), - }] + lookup := p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(ev.GetExchange(), ev.GetAssetType(), ev.Pair())] if lookup == nil { return nil, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, @@ -240,12 +235,7 @@ func (p *Portfolio) OnFill(ev fill.Event, funds funding.IFundReleaser) (fill.Eve if ev == nil { return nil, common.ErrNilEvent } - lookup := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: ev.GetExchange(), - Base: ev.Pair().Base.Item, - Quote: ev.Pair().Quote.Item, - Asset: ev.GetAssetType(), - }] + lookup := p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(ev.GetExchange(), ev.GetAssetType(), ev.Pair())] if lookup == nil { return nil, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, ev.GetExchange(), ev.GetAssetType(), ev.Pair()) } @@ -310,12 +300,7 @@ func (p *Portfolio) addComplianceSnapshot(fillEvent fill.Event) error { // GetLatestOrderSnapshotForEvent gets orders related to the event func (p *Portfolio) GetLatestOrderSnapshotForEvent(ev common.Event) (compliance.Snapshot, error) { - eapSettings, ok := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: ev.GetExchange(), - Base: ev.Pair().Base.Item, - Quote: ev.Pair().Quote.Item, - Asset: ev.GetAssetType(), - }] + eapSettings, ok := p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(ev.GetExchange(), ev.GetAssetType(), ev.Pair())] if !ok { return compliance.Snapshot{}, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, ev.GetExchange(), ev.GetAssetType(), ev.Pair()) } @@ -347,12 +332,7 @@ func (p *Portfolio) GetLatestComplianceSnapshot(exchangeName string, a asset.Ite // getComplianceManager returns the order snapshots for a given exchange, asset, pair func (p *Portfolio) getComplianceManager(exchangeName string, a asset.Item, cp currency.Pair) (*compliance.Manager, error) { - lookup := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: exchangeName, - Base: cp.Base.Item, - Quote: cp.Quote.Item, - Asset: a, - }] + lookup := p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(exchangeName, a, cp)] if lookup == nil { return nil, fmt.Errorf("%w for %v %v %v could not retrieve compliance manager", errNoPortfolioSettings, exchangeName, a, cp) } @@ -615,12 +595,7 @@ func (p *Portfolio) getFuturesSettingsFromEvent(e common.Event) (*Settings, erro func (p *Portfolio) getSettings(exch string, item asset.Item, pair currency.Pair) (*Settings, error) { exch = strings.ToLower(exch) - settings, ok := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: exch, - Base: pair.Base.Item, - Quote: pair.Quote.Item, - Asset: item, - }] + settings, ok := p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(exch, item, pair)] if !ok { return nil, fmt.Errorf("%w for %v %v %v", errNoPortfolioSettings, exch, item, pair) } diff --git a/backtester/eventhandlers/portfolio/portfolio_test.go b/backtester/eventhandlers/portfolio/portfolio_test.go index 8f2d30b4..a6e13514 100644 --- a/backtester/eventhandlers/portfolio/portfolio_test.go +++ b/backtester/eventhandlers/portfolio/portfolio_test.go @@ -36,7 +36,7 @@ var leet = decimal.NewFromInt(1337) func TestReset(t *testing.T) { t.Parallel() p := &Portfolio{ - exchangeAssetPairPortfolioSettings: make(map[key.ExchangePairAsset]*Settings), + exchangeAssetPairPortfolioSettings: make(map[key.ExchangeAssetPair]*Settings), } err := p.Reset() assert.NoError(t, err) @@ -578,12 +578,7 @@ func TestGetSnapshotAtTime(t *testing.T) { assert.NoError(t, err) tt := time.Now() - s, ok := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: testExchange, - Base: cp.Base.Item, - Quote: cp.Quote.Item, - Asset: asset.Spot, - }] + s, ok := p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(testExchange, asset.Spot, cp)] if !ok { t.Fatal("couldn't get settings") } @@ -631,14 +626,8 @@ func TestGetLatestSnapshot(t *testing.T) { ff := &binance.Exchange{} ff.Name = testExchange err = p.SetCurrencySettingsMap(&exchange.Settings{Exchange: ff, Asset: asset.Spot, Pair: currency.NewPair(currency.XRP, currency.DOGE)}) - assert.NoError(t, err) - - s, ok := p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: testExchange, - Base: cp.Base.Item, - Quote: cp.Quote.Item, - Asset: asset.Spot, - }] + require.NoError(t, err, "SetCurrencySettingsMap must not error") + s, ok := p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(testExchange, asset.Spot, cp)] if !ok { t.Fatal("couldn't get settings") } @@ -740,13 +729,8 @@ func TestCalculatePNL(t *testing.T) { FuturesTracker: mpt, } - p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) - p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: testExchange, - Base: pair.Base.Item, - Quote: pair.Quote.Item, - Asset: ev.AssetType, - }] = s + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangeAssetPair]*Settings) + p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(testExchange, a, pair)] = s ev.Close = leet err = s.ComplianceManager.AddSnapshot(&compliance.Snapshot{ Timestamp: tt0, @@ -974,13 +958,8 @@ func TestGetLatestPNLForEvent(t *testing.T) { FuturesTracker: mpt, } - p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) - p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: testExchange, - Base: ev.Pair().Base.Item, - Quote: ev.Pair().Quote.Item, - Asset: asset.Futures, - }] = s + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangeAssetPair]*Settings) + p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(testExchange, asset.Futures, ev.Pair())] = s err = s.FuturesTracker.TrackNewOrder(&gctorder.Detail{ Exchange: ev.GetExchange(), AssetType: ev.AssetType, @@ -1274,13 +1253,8 @@ func TestCreateLiquidationOrdersForExchange(t *testing.T) { err = settings.FuturesTracker.TrackNewOrder(od) assert.NoError(t, err) - p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) - p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: testExchange, - Base: ev.Pair().Base.Item, - Quote: ev.Pair().Quote.Item, - Asset: asset.Spot, - }] = settings + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangeAssetPair]*Settings) + p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(testExchange, asset.Spot, ev.Pair())] = settings ev.Exchange = ff.Name ev.AssetType = asset.Futures @@ -1388,13 +1362,8 @@ func TestCheckLiquidationStatus(t *testing.T) { err = settings.FuturesTracker.TrackNewOrder(od) assert.NoError(t, err) - p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) - p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: testExchange, - Base: ev.Pair().Base.Item, - Quote: ev.Pair().Quote.Item, - Asset: asset.Futures, - }] = settings + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangeAssetPair]*Settings) + p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(testExchange, asset.Futures, ev.Pair())] = settings err = p.CheckLiquidationStatus(ev, collat, pnl) assert.NoError(t, err) } diff --git a/backtester/eventhandlers/portfolio/portfolio_types.go b/backtester/eventhandlers/portfolio/portfolio_types.go index 394c01a5..280951f0 100644 --- a/backtester/eventhandlers/portfolio/portfolio_types.go +++ b/backtester/eventhandlers/portfolio/portfolio_types.go @@ -45,7 +45,7 @@ type Portfolio struct { riskFreeRate decimal.Decimal sizeManager SizeHandler riskManager risk.Handler - exchangeAssetPairPortfolioSettings map[key.ExchangePairAsset]*Settings + exchangeAssetPairPortfolioSettings map[key.ExchangeAssetPair]*Settings } // Handler contains all functions expected to operate a portfolio manager diff --git a/backtester/eventhandlers/portfolio/risk/risk.go b/backtester/eventhandlers/portfolio/risk/risk.go index 60a05622..43cd3308 100644 --- a/backtester/eventhandlers/portfolio/risk/risk.go +++ b/backtester/eventhandlers/portfolio/risk/risk.go @@ -26,12 +26,7 @@ func (r *Risk) EvaluateOrder(o order.Event, latestHoldings []holdings.Holding, s e := o.GetExchange() a := o.GetAssetType() p := o.Pair().Format(currency.EMPTYFORMAT) - lookup, ok := r.CurrencySettings[key.ExchangePairAsset{ - Exchange: e, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }] + lookup, ok := r.CurrencySettings[key.NewExchangeAssetPair(e, a, p)] if !ok { return nil, fmt.Errorf("%v %v %v %w", e, a, p, errNoCurrencySettings) } diff --git a/backtester/eventhandlers/portfolio/risk/risk_test.go b/backtester/eventhandlers/portfolio/risk/risk_test.go index 16f4ecf4..d68269c9 100644 --- a/backtester/eventhandlers/portfolio/risk/risk_test.go +++ b/backtester/eventhandlers/portfolio/risk/risk_test.go @@ -68,16 +68,11 @@ func TestEvaluateOrder(t *testing.T) { }, } h := []holdings.Holding{} - r.CurrencySettings = make(map[key.ExchangePairAsset]*CurrencySettings) + r.CurrencySettings = make(map[key.ExchangeAssetPair]*CurrencySettings) _, err = r.EvaluateOrder(o, h, compliance.Snapshot{}) assert.ErrorIs(t, err, errNoCurrencySettings) - r.CurrencySettings[key.ExchangePairAsset{ - Exchange: e, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }] = &CurrencySettings{ + r.CurrencySettings[key.NewExchangeAssetPair(e, a, p)] = &CurrencySettings{ MaximumOrdersWithLeverageRatio: decimal.NewFromFloat(0.3), MaxLeverageRate: decimal.NewFromFloat(0.3), MaximumHoldingRatio: decimal.NewFromFloat(0.3), @@ -94,12 +89,7 @@ func TestEvaluateOrder(t *testing.T) { Pair: currency.NewPair(currency.DOGE, currency.USDT), }) o.Leverage = decimal.NewFromFloat(1.1) - r.CurrencySettings[key.ExchangePairAsset{ - Exchange: e, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }].MaximumHoldingRatio = decimal.Zero + r.CurrencySettings[key.NewExchangeAssetPair(e, a, p)].MaximumHoldingRatio = decimal.Zero _, err = r.EvaluateOrder(o, h, compliance.Snapshot{}) assert.ErrorIs(t, err, errLeverageNotAllowed) @@ -108,22 +98,12 @@ func TestEvaluateOrder(t *testing.T) { assert.ErrorIs(t, err, errCannotPlaceLeverageOrder) r.MaximumLeverage = decimal.NewFromInt(33) - r.CurrencySettings[key.ExchangePairAsset{ - Exchange: e, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }].MaxLeverageRate = decimal.NewFromInt(33) + r.CurrencySettings[key.NewExchangeAssetPair(e, a, p)].MaxLeverageRate = decimal.NewFromInt(33) _, err = r.EvaluateOrder(o, h, compliance.Snapshot{}) assert.NoError(t, err) r.MaximumLeverage = decimal.NewFromInt(33) - r.CurrencySettings[key.ExchangePairAsset{ - Exchange: e, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }].MaxLeverageRate = decimal.NewFromInt(33) + r.CurrencySettings[key.NewExchangeAssetPair(e, a, p)].MaxLeverageRate = decimal.NewFromInt(33) _, err = r.EvaluateOrder(o, h, compliance.Snapshot{ Orders: []compliance.SnapshotOrder{ @@ -137,12 +117,7 @@ func TestEvaluateOrder(t *testing.T) { assert.ErrorIs(t, err, errCannotPlaceLeverageOrder) h = append(h, holdings.Holding{Pair: p, BaseValue: decimal.NewFromInt(1337)}, holdings.Holding{Pair: p, BaseValue: decimal.NewFromFloat(1337.42)}) - r.CurrencySettings[key.ExchangePairAsset{ - Exchange: e, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }].MaximumHoldingRatio = decimal.NewFromFloat(0.1) + r.CurrencySettings[key.NewExchangeAssetPair(e, a, p)].MaximumHoldingRatio = decimal.NewFromFloat(0.1) _, err = r.EvaluateOrder(o, h, compliance.Snapshot{}) assert.NoError(t, err) diff --git a/backtester/eventhandlers/portfolio/risk/risk_types.go b/backtester/eventhandlers/portfolio/risk/risk_types.go index b1019a69..3ea0d2ff 100644 --- a/backtester/eventhandlers/portfolio/risk/risk_types.go +++ b/backtester/eventhandlers/portfolio/risk/risk_types.go @@ -23,7 +23,7 @@ type Handler interface { // Risk contains all currency settings in order to evaluate potential orders type Risk struct { - CurrencySettings map[key.ExchangePairAsset]*CurrencySettings + CurrencySettings map[key.ExchangeAssetPair]*CurrencySettings CanUseLeverage bool MaximumLeverage decimal.Decimal } diff --git a/backtester/eventhandlers/portfolio/setup.go b/backtester/eventhandlers/portfolio/setup.go index a40bc648..57c65b77 100644 --- a/backtester/eventhandlers/portfolio/setup.go +++ b/backtester/eventhandlers/portfolio/setup.go @@ -37,7 +37,7 @@ func (p *Portfolio) Reset() error { if p == nil { return gctcommon.ErrNilPointer } - p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangeAssetPair]*Settings) p.riskFreeRate = decimal.Zero p.sizeManager = nil p.riskManager = nil @@ -60,7 +60,7 @@ func (p *Portfolio) SetCurrencySettingsMap(setup *exchange.Settings) error { } if p.exchangeAssetPairPortfolioSettings == nil { - p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangePairAsset]*Settings) + p.exchangeAssetPairPortfolioSettings = make(map[key.ExchangeAssetPair]*Settings) } name := strings.ToLower(setup.Exchange.GetName()) @@ -98,11 +98,6 @@ func (p *Portfolio) SetCurrencySettingsMap(setup *exchange.Settings) error { } settings.FuturesTracker = tracker } - p.exchangeAssetPairPortfolioSettings[key.ExchangePairAsset{ - Exchange: name, - Base: setup.Pair.Base.Item, - Quote: setup.Pair.Quote.Item, - Asset: setup.Asset, - }] = settings + p.exchangeAssetPairPortfolioSettings[key.NewExchangeAssetPair(name, setup.Asset, setup.Pair)] = settings return nil } diff --git a/backtester/eventhandlers/statistics/fundingstatistics.go b/backtester/eventhandlers/statistics/fundingstatistics.go index 98a00975..04c8a5ba 100644 --- a/backtester/eventhandlers/statistics/fundingstatistics.go +++ b/backtester/eventhandlers/statistics/fundingstatistics.go @@ -15,7 +15,7 @@ import ( // CalculateFundingStatistics calculates funding statistics for total USD strategy results // along with individual funding item statistics -func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[key.ExchangePairAsset]*CurrencyPairStatistic, riskFreeRate decimal.Decimal, interval gctkline.Interval) (*FundingStatistics, error) { +func CalculateFundingStatistics(funds funding.IFundingManager, currStats map[key.ExchangeAssetPair]*CurrencyPairStatistic, riskFreeRate decimal.Decimal, interval gctkline.Interval) (*FundingStatistics, error) { if currStats == nil { return nil, gctcommon.ErrNilPointer } diff --git a/backtester/eventhandlers/statistics/fundingstatistics_test.go b/backtester/eventhandlers/statistics/fundingstatistics_test.go index bc843fac..f19595b3 100644 --- a/backtester/eventhandlers/statistics/fundingstatistics_test.go +++ b/backtester/eventhandlers/statistics/fundingstatistics_test.go @@ -7,6 +7,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/backtester/data" "github.com/thrasher-corp/gocryptotrader/backtester/data/kline" "github.com/thrasher-corp/gocryptotrader/backtester/funding" @@ -65,7 +66,7 @@ func TestCalculateFundingStatistics(t *testing.T) { err = f.AddUSDTrackingData(dfk) assert.ErrorIs(t, err, funding.ErrUSDTrackingDisabled) - cs := make(map[key.ExchangePairAsset]*CurrencyPairStatistic) + cs := make(map[key.ExchangeAssetPair]*CurrencyPairStatistic) _, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour) assert.NoError(t, err) @@ -79,14 +80,9 @@ func TestCalculateFundingStatistics(t *testing.T) { assert.NoError(t, err) err = f.AddUSDTrackingData(dfk) - assert.NoError(t, err) + require.NoError(t, err, "AddUSDTrackingData must not error") - cs[key.ExchangePairAsset{ - Exchange: "binance", - Base: currency.LTC.Item, - Quote: currency.USD.Item, - Asset: asset.Spot, - }] = &CurrencyPairStatistic{} + cs[key.NewExchangeAssetPair("binance", asset.Spot, currency.NewPair(currency.LTC, currency.USD))] = &CurrencyPairStatistic{} _, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour) assert.ErrorIs(t, err, errMissingSnapshots) @@ -94,14 +90,9 @@ func TestCalculateFundingStatistics(t *testing.T) { assert.NoError(t, err) err = f.CreateSnapshot(usdKline.Candles[1].Time) - assert.NoError(t, err) + require.NoError(t, err, "CreateSnapshot must not error") - cs[key.ExchangePairAsset{ - Exchange: "binance", - Base: currency.LTC.Item, - Quote: currency.USD.Item, - Asset: asset.Spot, - }] = &CurrencyPairStatistic{} + cs[key.NewExchangeAssetPair("binance", asset.Spot, currency.NewPair(currency.LTC, currency.USD))] = &CurrencyPairStatistic{} _, err = CalculateFundingStatistics(f, cs, decimal.Zero, gctkline.OneHour) assert.NoError(t, err) } diff --git a/backtester/eventhandlers/statistics/statistics.go b/backtester/eventhandlers/statistics/statistics.go index 76bd7108..cc88119c 100644 --- a/backtester/eventhandlers/statistics/statistics.go +++ b/backtester/eventhandlers/statistics/statistics.go @@ -33,7 +33,7 @@ func (s *Statistic) Reset() error { s.EndDate = time.Time{} s.CandleInterval = 0 s.RiskFreeRate = decimal.Zero - s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*CurrencyPairStatistic) s.CurrencyStatistics = nil s.TotalBuyOrders = 0 s.TotalLongOrders = 0 @@ -62,14 +62,9 @@ func (s *Statistic) SetEventForOffset(ev common.Event) error { a := ev.GetAssetType() p := ev.Pair() if s.ExchangeAssetPairStatistics == nil { - s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) - } - mapKey := key.ExchangePairAsset{ - Exchange: e, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, + s.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*CurrencyPairStatistic) } + mapKey := key.NewExchangeAssetPair(e, a, p) stats, ok := s.ExchangeAssetPairStatistics[mapKey] if !ok { stats = &CurrencyPairStatistic{ @@ -138,12 +133,7 @@ func (s *Statistic) AddHoldingsForTime(h *holdings.Holding) error { if s.ExchangeAssetPairStatistics == nil { return errExchangeAssetPairStatsUnset } - lookup := s.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: h.Exchange, - Base: h.Pair.Base.Item, - Quote: h.Pair.Quote.Item, - Asset: h.Asset, - }] + lookup := s.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(h.Exchange, h.Asset, h.Pair)] if lookup == nil { return fmt.Errorf("%w for %v %v %v to set holding event", errCurrencyStatisticsUnset, h.Exchange, h.Asset, h.Pair) } @@ -164,12 +154,7 @@ func (s *Statistic) AddPNLForTime(pnl *portfolio.PNLSummary) error { if s.ExchangeAssetPairStatistics == nil { return errExchangeAssetPairStatsUnset } - lookup := s.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: pnl.Exchange, - Base: pnl.Pair.Base.Item, - Quote: pnl.Pair.Quote.Item, - Asset: pnl.Asset, - }] + lookup := s.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(pnl.Exchange, pnl.Asset, pnl.Pair)] if lookup == nil { return fmt.Errorf("%w for %v %v %v to set pnl", errCurrencyStatisticsUnset, pnl.Exchange, pnl.Asset, pnl.Pair) } @@ -197,12 +182,7 @@ func (s *Statistic) AddComplianceSnapshotForTime(c *compliance.Snapshot, e commo exch := e.GetExchange() a := e.GetAssetType() p := e.Pair() - lookup := s.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: exch, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }] + lookup := s.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(exch, a, p)] if lookup == nil { return fmt.Errorf("%w for %v %v %v to set compliance snapshot", errCurrencyStatisticsUnset, exch, a, p) } diff --git a/backtester/eventhandlers/statistics/statistics_test.go b/backtester/eventhandlers/statistics/statistics_test.go index d5ab7e40..560ac96e 100644 --- a/backtester/eventhandlers/statistics/statistics_test.go +++ b/backtester/eventhandlers/statistics/statistics_test.go @@ -81,12 +81,7 @@ func TestAddDataEventForTime(t *testing.T) { if s.ExchangeAssetPairStatistics == nil { t.Error("expected not nil") } - if len(s.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: exch, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }].Events) != 1 { + if len(s.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(exch, a, p)].Events) != 1 { t.Error("expected 1 event") } } @@ -104,7 +99,7 @@ func TestAddSignalEventForTime(t *testing.T) { err = s.SetEventForOffset(&signal.Signal{}) assert.ErrorIs(t, err, common.ErrNilEvent) - s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*CurrencyPairStatistic) b := &event.Base{} err = s.SetEventForOffset(&signal.Signal{ Base: b, @@ -147,7 +142,7 @@ func TestAddExchangeEventForTime(t *testing.T) { err = s.SetEventForOffset(&order.Order{}) assert.ErrorIs(t, err, common.ErrNilEvent) - s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*CurrencyPairStatistic) b := &event.Base{} b.Exchange = exch @@ -191,7 +186,7 @@ func TestAddFillEventForTime(t *testing.T) { err = s.SetEventForOffset(&fill.Fill{}) assert.ErrorIs(t, err, common.ErrNilEvent) - s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*CurrencyPairStatistic) b := &event.Base{} err = s.SetEventForOffset(&fill.Fill{ Base: b, @@ -237,7 +232,7 @@ func TestAddHoldingsForTime(t *testing.T) { err := s.AddHoldingsForTime(&holdings.Holding{}) assert.ErrorIs(t, err, errExchangeAssetPairStatsUnset) - s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*CurrencyPairStatistic) err = s.AddHoldingsForTime(&holdings.Holding{}) assert.ErrorIs(t, err, errCurrencyStatisticsUnset) @@ -297,7 +292,7 @@ func TestAddComplianceSnapshotForTime(t *testing.T) { err = s.AddComplianceSnapshotForTime(&compliance.Snapshot{}, &fill.Fill{}) assert.ErrorIs(t, err, errExchangeAssetPairStatsUnset) - s.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*CurrencyPairStatistic) + s.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*CurrencyPairStatistic) b := &event.Base{} err = s.AddComplianceSnapshotForTime(&compliance.Snapshot{}, &fill.Fill{Base: b}) assert.ErrorIs(t, err, errCurrencyStatisticsUnset) @@ -673,18 +668,8 @@ func TestCalculateTheResults(t *testing.T) { err = s.SetEventForOffset(signal4) assert.NoError(t, err) - mapKey1 := key.ExchangePairAsset{ - Exchange: exch, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - } - mapKey2 := key.ExchangePairAsset{ - Exchange: exch, - Base: p2.Base.Item, - Quote: p2.Quote.Item, - Asset: a, - } + mapKey1 := key.NewExchangeAssetPair(exch, a, p) + mapKey2 := key.NewExchangeAssetPair(exch, a, p2) s.ExchangeAssetPairStatistics[mapKey1].Events[1].Holdings.QuoteInitialFunds = eleet s.ExchangeAssetPairStatistics[mapKey1].Events[1].Holdings.TotalValue = eleeet s.ExchangeAssetPairStatistics[mapKey2].Events[1].Holdings.QuoteInitialFunds = eleet diff --git a/backtester/eventhandlers/statistics/statistics_types.go b/backtester/eventhandlers/statistics/statistics_types.go index 0861673d..48491a31 100644 --- a/backtester/eventhandlers/statistics/statistics_types.go +++ b/backtester/eventhandlers/statistics/statistics_types.go @@ -43,7 +43,7 @@ type Statistic struct { EndDate time.Time `json:"end-date"` CandleInterval gctkline.Interval `json:"candle-interval"` RiskFreeRate decimal.Decimal `json:"risk-free-rate"` - ExchangeAssetPairStatistics map[key.ExchangePairAsset]*CurrencyPairStatistic `json:"-"` + ExchangeAssetPairStatistics map[key.ExchangeAssetPair]*CurrencyPairStatistic `json:"-"` CurrencyStatistics []*CurrencyPairStatistic `json:"currency-statistics"` TotalBuyOrders int64 `json:"total-buy-orders"` TotalLongOrders int64 `json:"total-long-orders"` diff --git a/backtester/report/chart.go b/backtester/report/chart.go index 98b64771..3b485e3f 100644 --- a/backtester/report/chart.go +++ b/backtester/report/chart.go @@ -91,7 +91,7 @@ func createHoldingsOverTimeChart(stats []statistics.FundingItemStatistics) (*Cha // createPNLCharts shows a running history of all realised and unrealised PNL values // over time -func createPNLCharts(items map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) (*Chart, error) { +func createPNLCharts(items map[key.ExchangeAssetPair]*statistics.CurrencyPairStatistic) (*Chart, error) { if items == nil { return nil, fmt.Errorf("%w missing currency pair statistics", gctcommon.ErrNilPointer) } @@ -131,7 +131,7 @@ func createPNLCharts(items map[key.ExchangePairAsset]*statistics.CurrencyPairSta // createFuturesSpotDiffChart highlights the difference in futures and spot prices // over time -func createFuturesSpotDiffChart(items map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) (*Chart, error) { +func createFuturesSpotDiffChart(items map[key.ExchangeAssetPair]*statistics.CurrencyPairStatistic) (*Chart, error) { if items == nil { return nil, fmt.Errorf("%w missing currency pair statistics", gctcommon.ErrNilPointer) } diff --git a/backtester/report/chart_test.go b/backtester/report/chart_test.go index be91103e..1309c417 100644 --- a/backtester/report/chart_test.go +++ b/backtester/report/chart_test.go @@ -102,13 +102,8 @@ func TestCreatePNLCharts(t *testing.T) { tt := time.Now() var d Data d.Statistics = &statistics.Statistic{} - d.Statistics.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: testExchange, - Base: currency.BTC.Item, - Quote: currency.USDT.Item, - Asset: asset.Spot, - }] = &statistics.CurrencyPairStatistic{ + d.Statistics.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*statistics.CurrencyPairStatistic) + d.Statistics.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(testExchange, asset.Spot, currency.NewBTCUSDT())] = &statistics.CurrencyPairStatistic{ Events: []statistics.DataAtOffset{ { PNL: &portfolio.PNLSummary{ @@ -161,13 +156,8 @@ func TestCreateFuturesSpotDiffChart(t *testing.T) { cp2 := currency.NewPair(currency.BTC, currency.DOGE) var d Data d.Statistics = &statistics.Statistic{} - d.Statistics.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: testExchange, - Base: currency.BTC.Item, - Quote: currency.USD.Item, - Asset: asset.Spot, - }] = &statistics.CurrencyPairStatistic{ + d.Statistics.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*statistics.CurrencyPairStatistic) + d.Statistics.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(testExchange, asset.Spot, currency.NewBTCUSD())] = &statistics.CurrencyPairStatistic{ Currency: cp, Events: []statistics.DataAtOffset{ { @@ -187,12 +177,7 @@ func TestCreateFuturesSpotDiffChart(t *testing.T) { }, }, } - d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: testExchange, - Base: currency.BTC.Item, - Quote: currency.DOGE.Item, - Asset: asset.Futures, - }] = &statistics.CurrencyPairStatistic{ + d.Statistics.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(testExchange, asset.Futures, currency.NewPair(currency.BTC, currency.DOGE))] = &statistics.CurrencyPairStatistic{ UnderlyingPair: cp, Currency: cp2, Events: []statistics.DataAtOffset{ diff --git a/backtester/report/report.go b/backtester/report/report.go index d302d876..b0495674 100644 --- a/backtester/report/report.go +++ b/backtester/report/report.go @@ -149,12 +149,7 @@ func (d *Data) enhanceCandles() error { Watermark: fmt.Sprintf("%s - %s - %s", cases.Title(language.English).String(lookup.Exchange), lookup.Asset.String(), lookup.Pair.Upper()), } - statsForCandles := d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: lookup.Exchange, - Base: lookup.Pair.Base.Item, - Quote: lookup.Pair.Quote.Item, - Asset: lookup.Asset, - }] + statsForCandles := d.Statistics.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(lookup.Exchange, lookup.Asset, lookup.Pair)] if statsForCandles == nil { continue } diff --git a/backtester/report/report_test.go b/backtester/report/report_test.go index 382aafd8..8dbcfcf4 100644 --- a/backtester/report/report_test.go +++ b/backtester/report/report_test.go @@ -235,7 +235,7 @@ func TestGenerateReport(t *testing.T) { }, StrategyName: "testStrat", RiskFreeRate: decimal.NewFromFloat(0.03), - ExchangeAssetPairStatistics: map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic{ + ExchangeAssetPairStatistics: map[key.ExchangeAssetPair]*statistics.CurrencyPairStatistic{ { Base: p.Base.Item, Quote: p.Quote.Item, @@ -334,13 +334,8 @@ func TestEnhanceCandles(t *testing.T) { err = d.enhanceCandles() assert.NoError(t, err) - d.Statistics.ExchangeAssetPairStatistics = make(map[key.ExchangePairAsset]*statistics.CurrencyPairStatistic) - d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: testExchange, - Base: currency.BTC.Item, - Quote: currency.USDT.Item, - Asset: asset.Spot, - }] = &statistics.CurrencyPairStatistic{} + d.Statistics.ExchangeAssetPairStatistics = make(map[key.ExchangeAssetPair]*statistics.CurrencyPairStatistic) + d.Statistics.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(testExchange, asset.Spot, currency.NewBTCUSDT())] = &statistics.CurrencyPairStatistic{} err = d.SetKlineData(&gctkline.Item{ Exchange: testExchange, @@ -392,12 +387,7 @@ func TestEnhanceCandles(t *testing.T) { err = d.enhanceCandles() assert.NoError(t, err) - d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: testExchange, - Base: currency.BTC.Item, - Quote: currency.USDT.Item, - Asset: asset.Spot, - }].FinalOrders = compliance.Snapshot{ + d.Statistics.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(testExchange, asset.Spot, currency.NewBTCUSDT())].FinalOrders = compliance.Snapshot{ Orders: []compliance.SnapshotOrder{ { ClosePrice: decimal.NewFromInt(1335), @@ -412,12 +402,7 @@ func TestEnhanceCandles(t *testing.T) { err = d.enhanceCandles() assert.NoError(t, err) - d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: testExchange, - Base: currency.BTC.Item, - Quote: currency.USDT.Item, - Asset: asset.Spot, - }].FinalOrders = compliance.Snapshot{ + d.Statistics.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(testExchange, asset.Spot, currency.NewBTCUSDT())].FinalOrders = compliance.Snapshot{ Orders: []compliance.SnapshotOrder{ { ClosePrice: decimal.NewFromInt(1335), @@ -435,12 +420,7 @@ func TestEnhanceCandles(t *testing.T) { err = d.enhanceCandles() assert.NoError(t, err) - d.Statistics.ExchangeAssetPairStatistics[key.ExchangePairAsset{ - Exchange: testExchange, - Base: currency.BTC.Item, - Quote: currency.USDT.Item, - Asset: asset.Spot, - }].FinalOrders = compliance.Snapshot{ + d.Statistics.ExchangeAssetPairStatistics[key.NewExchangeAssetPair(testExchange, asset.Spot, currency.NewBTCUSDT())].FinalOrders = compliance.Snapshot{ Orders: []compliance.SnapshotOrder{ { ClosePrice: decimal.NewFromInt(1335), diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index de532fa2..08b4a794 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/engine" + "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" @@ -644,10 +645,9 @@ var acceptableErrors = []error{ 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 - order.ErrExchangeLimitNotLoaded, // Is thrown when the limits aren't loaded for a particular exchange, asset, pair - order.ErrCannotValidateAsset, // Is thrown when attempting to get order limits from an asset that is not yet loaded - order.ErrCannotValidateBaseCurrency, // Is thrown when attempting to get order limits from an base currency that is not yet loaded - order.ErrCannotValidateQuoteCurrency, // Is thrown when attempting to get order limits from an quote currency that is not yet loaded + 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 account.ErrExchangeHoldingsNotFound, ticker.ErrTickerNotFound, orderbook.ErrOrderbookNotFound, diff --git a/common/key/key.go b/common/key/key.go index 5db6bb4f..b6f6f282 100644 --- a/common/key/key.go +++ b/common/key/key.go @@ -1,18 +1,48 @@ package key import ( - "strings" - "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -// ExchangePairAsset is a unique map key signature for exchange, currency pair and asset -type ExchangePairAsset struct { +// ExchangeAssetPair is a unique map key signature for exchange, currency pair and asset +type ExchangeAssetPair struct { Exchange string + Asset asset.Item Base *currency.Item Quote *currency.Item - Asset asset.Item +} + +// NewExchangeAssetPair is a helper function to expand a Pair into an ExchangeAssetPair +func NewExchangeAssetPair(exch string, a asset.Item, cp currency.Pair) ExchangeAssetPair { + return ExchangeAssetPair{ + Exchange: exch, + Base: cp.Base.Item, + Quote: cp.Quote.Item, + Asset: a, + } +} + +// Pair combines the base and quote into a pair +func (k ExchangeAssetPair) Pair() currency.Pair { + return currency.NewPair(k.Base.Currency(), k.Quote.Currency()) +} + +// MatchesExchangeAsset checks if the key matches the exchange and asset +func (k ExchangeAssetPair) MatchesExchangeAsset(exch string, item asset.Item) bool { + return k.Exchange == exch && k.Asset == item +} + +// MatchesPairAsset checks if the key matches the pair and asset +func (k ExchangeAssetPair) MatchesPairAsset(pair currency.Pair, item asset.Item) bool { + return k.Base == pair.Base.Item && + k.Quote == pair.Quote.Item && + k.Asset == item +} + +// MatchesExchange checks if the exchange matches +func (k ExchangeAssetPair) MatchesExchange(exch string) bool { + return k.Exchange == exch } // ExchangeAsset is a unique map key signature for exchange and asset @@ -28,50 +58,13 @@ type PairAsset struct { Asset asset.Item } +// Pair combines the base and quote into a pair +func (k PairAsset) Pair() currency.Pair { + return currency.NewPair(k.Base.Currency(), k.Quote.Currency()) +} + // SubAccountAsset is a unique map key signature for subaccount and asset type SubAccountAsset struct { SubAccount string Asset asset.Item } - -// Pair combines the base and quote into a pair -func (k *PairAsset) Pair() currency.Pair { - if k == nil || (k.Base == nil && k.Quote == nil) { - return currency.EMPTYPAIR - } - return currency.NewPair(k.Base.Currency(), k.Quote.Currency()) -} - -// Pair combines the base and quote into a pair -func (k *ExchangePairAsset) Pair() currency.Pair { - if k == nil || (k.Base == nil && k.Quote == nil) { - return currency.EMPTYPAIR - } - return currency.NewPair(k.Base.Currency(), k.Quote.Currency()) -} - -// MatchesExchangeAsset checks if the key matches the exchange and asset -func (k *ExchangePairAsset) MatchesExchangeAsset(exch string, item asset.Item) bool { - if k == nil { - return false - } - return strings.EqualFold(k.Exchange, exch) && k.Asset == item -} - -// MatchesPairAsset checks if the key matches the pair and asset -func (k *ExchangePairAsset) MatchesPairAsset(pair currency.Pair, item asset.Item) bool { - if k == nil { - return false - } - return k.Base == pair.Base.Item && - k.Quote == pair.Quote.Item && - k.Asset == item -} - -// MatchesExchange checks if the exchange matches -func (k *ExchangePairAsset) MatchesExchange(exch string) bool { - if k == nil { - return false - } - return strings.EqualFold(k.Exchange, exch) -} diff --git a/common/key/key_test.go b/common/key/key_test.go index f5363a16..de60c573 100644 --- a/common/key/key_test.go +++ b/common/key/key_test.go @@ -11,84 +11,55 @@ import ( func TestMatchesExchangeAsset(t *testing.T) { t.Parallel() cp := currency.NewBTCUSD() - k := ExchangePairAsset{ + k := ExchangeAssetPair{ Exchange: "test", Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot, } - if !k.MatchesExchangeAsset("test", asset.Spot) { - t.Error("expected true") - } - if k.MatchesExchangeAsset("TEST", asset.Futures) { - t.Error("expected false") - } - if k.MatchesExchangeAsset("test", asset.Futures) { - t.Error("expected false") - } - if !k.MatchesExchangeAsset("TEST", asset.Spot) { - t.Error("expected true") - } + assert.True(t, k.MatchesExchangeAsset("test", asset.Spot)) + assert.False(t, k.MatchesExchangeAsset("TEST", asset.Futures)) + assert.False(t, k.MatchesExchangeAsset("test", asset.Futures)) + assert.False(t, k.MatchesExchangeAsset("TEST", asset.Spot)) } func TestMatchesPairAsset(t *testing.T) { t.Parallel() cp := currency.NewBTCUSD() - k := ExchangePairAsset{ + k := ExchangeAssetPair{ Base: cp.Base.Item, Quote: cp.Quote.Item, Asset: asset.Spot, } - if !k.MatchesPairAsset(cp, asset.Spot) { - t.Error("expected true") - } - if k.MatchesPairAsset(cp, asset.Futures) { - t.Error("expected false") - } - if k.MatchesPairAsset(currency.EMPTYPAIR, asset.Futures) { - t.Error("expected false") - } - if k.MatchesPairAsset(currency.NewBTCUSDT(), asset.Spot) { - t.Error("expected false") - } + assert.True(t, k.MatchesPairAsset(cp, asset.Spot)) + assert.False(t, k.MatchesPairAsset(cp, asset.Futures)) + assert.False(t, k.MatchesPairAsset(currency.EMPTYPAIR, asset.Futures)) + assert.False(t, k.MatchesPairAsset(currency.NewBTCUSDT(), asset.Spot)) } func TestMatchesExchange(t *testing.T) { t.Parallel() - k := ExchangePairAsset{ + k := ExchangeAssetPair{ Exchange: "test", } - if !k.MatchesExchange("test") { - t.Error("expected true") - } - if !k.MatchesExchange("TEST") { - t.Error("expected true") - } - if k.MatchesExchange("tèst") { - t.Error("expected false") - } - if k.MatchesExchange("") { - t.Error("expected false") - } + assert.True(t, k.MatchesExchange("test")) + assert.False(t, k.MatchesExchange("TEST")) + assert.False(t, k.MatchesExchange("tèst")) + assert.False(t, k.MatchesExchange("")) } func TestExchangePairAsset_Pair(t *testing.T) { t.Parallel() cp := currency.NewBTCUSD() - k := ExchangePairAsset{ + k := ExchangeAssetPair{ Base: currency.BTC.Item, Quote: currency.USD.Item, Asset: asset.Spot, } assert.Equal(t, cp, k.Pair()) - cp = currency.NewPair(currency.BTC, currency.EMPTYCODE) k.Quote = currency.EMPTYCODE.Item assert.Equal(t, cp, k.Pair()) - - cp = currency.EMPTYPAIR - var epa *ExchangePairAsset - assert.Equal(t, cp, epa.Pair()) } func TestPairAsset_Pair(t *testing.T) { @@ -100,12 +71,25 @@ func TestPairAsset_Pair(t *testing.T) { Asset: asset.Spot, } assert.Equal(t, cp, k.Pair()) - cp = currency.NewPair(currency.BTC, currency.EMPTYCODE) k.Quote = currency.EMPTYCODE.Item assert.Equal(t, cp, k.Pair()) - - cp = currency.EMPTYPAIR - var pa *PairAsset - assert.Equal(t, cp, pa.Pair()) +} + +func TestNewExchangePairAssetKey(t *testing.T) { + t.Parallel() + e := "test" + a := asset.Spot + p := currency.NewBTCUSDT() + k := NewExchangeAssetPair(e, a, p) + assert.Equal(t, e, k.Exchange) + assert.Equal(t, p.Base.Item, k.Base) + assert.Equal(t, p.Quote.Item, k.Quote) + assert.Equal(t, a, k.Asset) + + e = "" + a = 0 + p = currency.EMPTYPAIR + k = NewExchangeAssetPair(e, a, p) + assert.Equal(t, a, k.Asset, "NewExchangeAssetPair should not alter an invalid asset") } diff --git a/currency/manager.go b/currency/manager.go index 6b31ec43..1dcdfe7c 100644 --- a/currency/manager.go +++ b/currency/manager.go @@ -453,7 +453,7 @@ func (p *PairsManager) getPairStoreRequiresLock(a asset.Item) (*PairStore, error pairStore, ok := p.Pairs[a] if !ok { - return nil, fmt.Errorf("%w %w %v", ErrAssetNotFound, asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %w %q", ErrAssetNotFound, asset.ErrNotSupported, a) } if pairStore == nil { diff --git a/engine/rpcserver.go b/engine/rpcserver.go index 50e5cbe2..b0e9525f 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -1918,7 +1918,7 @@ func (s *RPCServer) GetExchangePairs(_ context.Context, r *gctrpc.GetExchangePai return nil, err } if !assetTypes.Contains(a) { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index f3332b5f..823bf9a8 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -117,12 +117,7 @@ func (f fExchange) GetOpenInterest(_ context.Context, k ...key.PairAsset) ([]fut if len(k) > 0 { return []futures.OpenInterest{ { - Key: key.ExchangePairAsset{ - Exchange: f.GetName(), - Base: k[0].Base, - Quote: k[0].Quote, - Asset: k[0].Asset, - }, + Key: key.NewExchangeAssetPair(f.GetName(), k[0].Asset, k[0].Pair()), OpenInterest: 1337, }, }, nil diff --git a/engine/sync_manager.go b/engine/sync_manager.go index e6b1316e..4d0ff29f 100644 --- a/engine/sync_manager.go +++ b/engine/sync_manager.go @@ -89,7 +89,7 @@ func SetupSyncManager(c *config.SyncManagerConfig, exchangeManager iExchangeMana fiatDisplayCurrency: c.FiatDisplayCurrency, format: *c.PairFormatDisplay, tickerBatchLastRequested: make(map[key.ExchangeAsset]time.Time), - currencyPairs: make(map[key.ExchangePairAsset]*currencyPairSyncAgent), + currencyPairs: make(map[key.ExchangeAssetPair]*currencyPairSyncAgent), } log.Debugf(log.SyncMgr, @@ -177,12 +177,7 @@ func (m *SyncManager) Start() error { continue } for i := range enabledPairs { - k := key.ExchangePairAsset{ - Asset: assetTypes[y], - Exchange: exchangeName, - Base: enabledPairs[i].Base.Item, - Quote: enabledPairs[i].Quote.Item, - } + k := key.NewExchangeAssetPair(exchangeName, assetTypes[y], enabledPairs[i]) if e := m.get(k); e != nil { continue } @@ -251,14 +246,14 @@ func (m *SyncManager) Stop() error { return nil } -func (m *SyncManager) get(k key.ExchangePairAsset) *currencyPairSyncAgent { +func (m *SyncManager) get(k key.ExchangeAssetPair) *currencyPairSyncAgent { m.mux.Lock() defer m.mux.Unlock() return m.currencyPairs[k] } -func newCurrencyPairSyncAgent(k key.ExchangePairAsset) *currencyPairSyncAgent { +func newCurrencyPairSyncAgent(k key.ExchangeAssetPair) *currencyPairSyncAgent { return ¤cyPairSyncAgent{ Key: k, Pair: currency.NewPair(k.Base.Currency(), k.Quote.Currency()), @@ -268,7 +263,7 @@ func newCurrencyPairSyncAgent(k key.ExchangePairAsset) *currencyPairSyncAgent { } } -func (m *SyncManager) add(k key.ExchangePairAsset, s syncBase) *currencyPairSyncAgent { +func (m *SyncManager) add(k key.ExchangeAssetPair, s syncBase) *currencyPairSyncAgent { m.mux.Lock() defer m.mux.Unlock() @@ -335,7 +330,7 @@ func (m *SyncManager) add(k key.ExchangePairAsset, s syncBase) *currencyPairSync } if m.currencyPairs == nil { - m.currencyPairs = make(map[key.ExchangePairAsset]*currencyPairSyncAgent) + m.currencyPairs = make(map[key.ExchangeAssetPair]*currencyPairSyncAgent) } m.currencyPairs[k] = c @@ -373,16 +368,10 @@ func (m *SyncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a as return fmt.Errorf("%v %w", syncType, errUnknownSyncItem) } - k := key.ExchangePairAsset{ - Asset: a, - Exchange: exchangeName, - Base: p.Base.Item, - Quote: p.Quote.Item, - } - + k := key.NewExchangeAssetPair(exchangeName, a, p) c, exists := m.currencyPairs[k] if !exists { - return fmt.Errorf("%w for %s %s %s %s %s", + return fmt.Errorf("%w for %q %q %q %q %q", errCouldNotSyncNewData, k.Exchange, k.Base, @@ -507,12 +496,7 @@ func (m *SyncManager) worker() { return } - k := key.ExchangePairAsset{ - Asset: assetTypes[y], - Exchange: exchangeName, - Base: enabledPairs[i].Base.Item, - Quote: enabledPairs[i].Quote.Item, - } + k := key.NewExchangeAssetPair(exchangeName, assetTypes[y], enabledPairs[i]) c := m.get(k) if c == nil { c = m.add(k, syncBase{ diff --git a/engine/sync_manager_test.go b/engine/sync_manager_test.go index 086abbb5..06edb5cb 100644 --- a/engine/sync_manager_test.go +++ b/engine/sync_manager_test.go @@ -258,9 +258,7 @@ func TestSyncManagerWebsocketUpdate(t *testing.T) { err = m.WebsocketUpdate("", currency.EMPTYPAIR, asset.Spot, SyncItemOrderbook, nil) require.ErrorIs(t, err, errCouldNotSyncNewData) - m.add(key.ExchangePairAsset{ - Asset: asset.Spot, - }, syncBase{}) + m.add(key.NewExchangeAssetPair("", asset.Spot, currency.EMPTYPAIR), syncBase{}) m.initSyncWG.Add(3) // orderbook match err = m.WebsocketUpdate("", currency.EMPTYPAIR, asset.Spot, SyncItemOrderbook, errors.New("test")) diff --git a/engine/sync_manager_types.go b/engine/sync_manager_types.go index 46807832..bfd52f00 100644 --- a/engine/sync_manager_types.go +++ b/engine/sync_manager_types.go @@ -20,7 +20,7 @@ type syncBase struct { // currencyPairSyncAgent stores the sync agent info type currencyPairSyncAgent struct { - Key key.ExchangePairAsset + Key key.ExchangeAssetPair Pair currency.Pair Created time.Time trackers []*syncBase @@ -41,7 +41,7 @@ type SyncManager struct { initSyncWG sync.WaitGroup inService sync.WaitGroup - currencyPairs map[key.ExchangePairAsset]*currencyPairSyncAgent + currencyPairs map[key.ExchangeAssetPair]*currencyPairSyncAgent tickerBatchLastRequested map[key.ExchangeAsset]time.Time remoteConfig *config.RemoteControlConfig diff --git a/exchange/order/limits/levels.go b/exchange/order/limits/levels.go new file mode 100644 index 00000000..a6cfa399 --- /dev/null +++ b/exchange/order/limits/levels.go @@ -0,0 +1,147 @@ +package limits + +import ( + "fmt" + + "github.com/shopspring/decimal" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +// Validate ensures MinMaxLevel fields are valid +func (m *MinMaxLevel) Validate(price, amount float64, orderType order.Type) error { + // TODO: Verify Quote as well as Base amounts + if m == nil { + return nil + } + + if m.MinimumBaseAmount != 0 && amount < m.MinimumBaseAmount { + return fmt.Errorf("%w min: %.8f supplied %.8f", ErrAmountBelowMin, m.MinimumBaseAmount, amount) + } + if m.MaximumBaseAmount != 0 && amount > m.MaximumBaseAmount { + return fmt.Errorf("%w min: %.8f supplied %.8f", ErrAmountExceedsMax, m.MaximumBaseAmount, amount) + } + if m.AmountStepIncrementSize != 0 { + dAmount := decimal.NewFromFloat(amount) + dStep := decimal.NewFromFloat(m.AmountStepIncrementSize) + if !dAmount.Mod(dStep).IsZero() { + return fmt.Errorf("%w stepSize: %.8f supplied %.8f", ErrAmountExceedsStep, m.AmountStepIncrementSize, amount) + } + } + + /* + ContractMultiplier checking not done due to the fact we need coherence with the + last average price (TODO) + m.multiplierUp will be used to determine how far our price can go up + m.multiplierDown will be used to determine how far our price can go down + m.averagePriceMinutes will be used to determine mean over this period + + Max iceberg parts checking not done as we do not have that + functionality yet (TODO) + m.maxIcebergParts // How many components in an iceberg order + + Max total orders not done due to order manager limitations (TODO) + m.maxTotalOrders + + Max algo orders not done due to order manager limitations (TODO) + m.maxAlgoOrders + + If order type is Market we do not need to do price checks + */ + if orderType != order.Market { + if m.MinPrice != 0 && price < m.MinPrice { + return fmt.Errorf("%w min: %.8f supplied %.8f", ErrPriceBelowMin, m.MinPrice, price) + } + if m.MaxPrice != 0 && price > m.MaxPrice { + return fmt.Errorf("%w max: %.8f supplied %.8f", ErrPriceExceedsMax, m.MaxPrice, price) + } + if m.MinNotional != 0 && (amount*price) < m.MinNotional { + return fmt.Errorf("%w minimum notional: %.8f value of order %.8f", ErrNotionalValue, m.MinNotional, amount*price) + } + if m.PriceStepIncrementSize != 0 { + dPrice := decimal.NewFromFloat(price) + dMinPrice := decimal.NewFromFloat(m.MinPrice) + dStep := decimal.NewFromFloat(m.PriceStepIncrementSize) + if !dPrice.Sub(dMinPrice).Mod(dStep).IsZero() { + return fmt.Errorf("%w stepSize: %.8f supplied %.8f", ErrPriceExceedsStep, m.PriceStepIncrementSize, price) + } + } + return nil + } + + if m.MarketMinQty != 0 && m.MinimumBaseAmount < m.MarketMinQty && amount < m.MarketMinQty { + return fmt.Errorf("%w min: %.8f supplied %.8f", ErrMarketAmountBelowMin, m.MarketMinQty, amount) + } + if m.MarketMaxQty != 0 && m.MaximumBaseAmount > m.MarketMaxQty && amount > m.MarketMaxQty { + return fmt.Errorf("%w max: %.8f supplied %.8f", ErrMarketAmountExceedsMax, m.MarketMaxQty, amount) + } + if m.MarketStepIncrementSize != 0 && m.AmountStepIncrementSize != m.MarketStepIncrementSize { + dAmount := decimal.NewFromFloat(amount) + dMinMAmount := decimal.NewFromFloat(m.MarketMinQty) + dStep := decimal.NewFromFloat(m.MarketStepIncrementSize) + if !dAmount.Sub(dMinMAmount).Mod(dStep).IsZero() { + return fmt.Errorf("%w stepSize: %.8f supplied %.8f", ErrMarketAmountExceedsStep, m.MarketStepIncrementSize, amount) + } + } + return nil +} + +// FloorAmountToStepIncrementDecimal floors decimal amount to step increment +func (m *MinMaxLevel) FloorAmountToStepIncrementDecimal(amount decimal.Decimal) decimal.Decimal { + if m == nil { + return amount + } + + dStep := decimal.NewFromFloat(m.AmountStepIncrementSize) + if dStep.IsZero() || amount.Equal(dStep) { + return amount + } + + if amount.LessThan(dStep) { + return decimal.Zero + } + mod := amount.Mod(dStep) + // subtract to get the floor + return amount.Sub(mod) +} + +// FloorAmountToStepIncrement floors float amount to step increment +func (m *MinMaxLevel) FloorAmountToStepIncrement(amount float64) float64 { + if m == nil { + return amount + } + + if m.AmountStepIncrementSize == 0 || amount == m.AmountStepIncrementSize { + return amount + } + + if amount < m.AmountStepIncrementSize { + return 0 + } + + dAmount := decimal.NewFromFloat(amount) + dStep := decimal.NewFromFloat(m.AmountStepIncrementSize) + mod := dAmount.Mod(dStep) + // subtract to get the floor + return dAmount.Sub(mod).InexactFloat64() +} + +// FloorPriceToStepIncrement floors float price to step increment +func (m *MinMaxLevel) FloorPriceToStepIncrement(price float64) float64 { + if m == nil { + return price + } + + if m.PriceStepIncrementSize == 0 { + return price + } + + if price < m.PriceStepIncrementSize { + return 0 + } + + dPrice := decimal.NewFromFloat(price) + dStep := decimal.NewFromFloat(m.PriceStepIncrementSize) + mod := dPrice.Mod(dStep) + // subtract to get the floor + return dPrice.Sub(mod).InexactFloat64() +} diff --git a/exchange/order/limits/levels_test.go b/exchange/order/limits/levels_test.go new file mode 100644 index 00000000..cf372d22 --- /dev/null +++ b/exchange/order/limits/levels_test.go @@ -0,0 +1,164 @@ +package limits + +import ( + "testing" + + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +func TestConforms(t *testing.T) { + t.Parallel() + tt := &MinMaxLevel{} + err := tt.Validate(0, 0, order.Limit) + require.NoError(t, err) + + tt = &MinMaxLevel{ + MinNotional: 100, + } + err = tt.Validate(1, 1, order.Limit) + assert.ErrorIs(t, err, ErrNotionalValue) + + err = tt.Validate(200, .5, order.Limit) + assert.NoError(t, err) + + tt.PriceStepIncrementSize = 0.001 + err = tt.Validate(200.0001, .5, order.Limit) + assert.ErrorIs(t, err, ErrPriceExceedsStep) + + err = tt.Validate(200.004, .5, order.Limit) + assert.NoError(t, err) + + tt.AmountStepIncrementSize = 0.001 + err = tt.Validate(200, .0002, order.Limit) + assert.ErrorIs(t, err, ErrAmountExceedsStep) + err = tt.Validate(200000, .003, order.Limit) + assert.NoError(t, err) + + tt.MinimumBaseAmount = 1 + tt.MaximumBaseAmount = 10 + tt.MarketMinQty = 1.1 + tt.MarketMaxQty = 9.9 + + err = tt.Validate(200000, 1, order.Market) + assert.ErrorIs(t, err, ErrMarketAmountBelowMin) + + err = tt.Validate(200000, 10, order.Market) + assert.ErrorIs(t, err, ErrMarketAmountExceedsMax) + + tt.MarketStepIncrementSize = 10 + err = tt.Validate(200000, 9.1, order.Market) + assert.ErrorIs(t, err, ErrMarketAmountExceedsStep) + + tt.MarketStepIncrementSize = 1 + err = tt.Validate(200000, 9.1, order.Market) + assert.NoError(t, err) + + tt = &MinMaxLevel{ + MinimumBaseAmount: 0.1, + } + err = tt.Validate(0, 0, order.Market) + assert.ErrorIs(t, err, ErrAmountBelowMin) + + tt.MaximumBaseAmount = 0.5 + err = tt.Validate(0, 0.6, order.Market) + assert.ErrorIs(t, err, ErrAmountExceedsMax) + + tt.AmountStepIncrementSize = 0.1 + err = tt.Validate(0, 0.1337, order.Market) + assert.ErrorIs(t, err, ErrAmountExceedsStep) + + tt = nil + err = tt.Validate(0, 0, order.Limit) + assert.NoError(t, err) +} + +func TestConformToDecimalAmount(t *testing.T) { + t.Parallel() + tt := &MinMaxLevel{} + val := tt.FloorAmountToStepIncrementDecimal(decimal.NewFromFloat(1.001)) + assert.Equal(t, "1.001", val.String()) + + tt = &MinMaxLevel{} + val = tt.FloorAmountToStepIncrementDecimal(decimal.NewFromInt(1)) + assert.Equal(t, "1", val.String()) + + tt.AmountStepIncrementSize = 0.001 + val = tt.FloorAmountToStepIncrementDecimal(decimal.NewFromFloat(1.001)) + assert.Equal(t, "1.001", val.String()) + + val = tt.FloorAmountToStepIncrementDecimal(decimal.NewFromFloat(0.0001)) + assert.Equal(t, "0", val.String()) + + val = tt.FloorAmountToStepIncrementDecimal(decimal.NewFromFloat(0.7777)) + assert.Equal(t, "0.777", val.String()) + + tt.AmountStepIncrementSize = 100 + val = tt.FloorAmountToStepIncrementDecimal(decimal.NewFromInt(100)) + assert.Equal(t, "100", val.String()) + + val = tt.FloorAmountToStepIncrementDecimal(decimal.NewFromInt(200)) + assert.Equal(t, "200", val.String()) + + val = tt.FloorAmountToStepIncrementDecimal(decimal.NewFromInt(150)) + assert.Equal(t, "100", val.String()) + + tt = nil + val = tt.FloorAmountToStepIncrementDecimal(decimal.NewFromInt(150)) + assert.Equal(t, "150", val.String()) +} + +func TestConformToAmount(t *testing.T) { + t.Parallel() + tt := &MinMaxLevel{} + require.Equal(t, 1.001, tt.FloorAmountToStepIncrement(1.001)) + + tt = &MinMaxLevel{} + val := tt.FloorAmountToStepIncrement(1.0) + assert.Equal(t, 1.0, val) + + tt.AmountStepIncrementSize = 0.001 + val = tt.FloorAmountToStepIncrement(1.001) + assert.Equal(t, 1.001, val) + + val = tt.FloorAmountToStepIncrement(0.0001) + assert.Zero(t, val) + + val = tt.FloorAmountToStepIncrement(0.7777) + assert.Equal(t, 0.777, val) + + tt.AmountStepIncrementSize = 100 + val = tt.FloorAmountToStepIncrement(100) + assert.Equal(t, 100.0, val) + + val = tt.FloorAmountToStepIncrement(200) + assert.Equal(t, 200.0, val) + + val = tt.FloorAmountToStepIncrement(150) + assert.Equal(t, 100.0, val) + + tt = nil + val = tt.FloorAmountToStepIncrement(150) + assert.Equal(t, 150.0, val) +} + +func TestConformToPrice(t *testing.T) { + t.Parallel() + tt := &MinMaxLevel{} + resp := tt.FloorPriceToStepIncrement(1.0) + assert.Equal(t, 1.0, resp) + + tt.PriceStepIncrementSize = 1 + + resp = tt.FloorPriceToStepIncrement(1.5) + assert.Equal(t, 1.0, resp) + + resp = tt.FloorPriceToStepIncrement(0.5) + assert.Equal(t, 0.0, resp) + + tt = nil + resp = tt.FloorPriceToStepIncrement(1.0) + assert.Equal(t, 1.0, resp) +} diff --git a/exchange/order/limits/limits_types.go b/exchange/order/limits/limits_types.go new file mode 100644 index 00000000..8d2c9a31 --- /dev/null +++ b/exchange/order/limits/limits_types.go @@ -0,0 +1,72 @@ +package limits + +import ( + "errors" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common/key" +) + +// Public errors for order limits +var ( + ErrEmptyLevels = errors.New("cannot load limits, no levels supplied") + ErrOrderLimitNotFound = errors.New("order limit not found") + ErrExchangeLimitNotLoaded = errors.New("exchange limits not loaded") + ErrPriceBelowMin = errors.New("price below minimum limit") + ErrPriceExceedsMax = errors.New("price exceeds maximum limit") + ErrPriceExceedsStep = errors.New("price is not divisible by its step") + ErrAmountBelowMin = errors.New("amount below minimum limit") + ErrAmountExceedsMax = errors.New("amount exceeds maximum limit") + ErrAmountExceedsStep = errors.New("amount is not divisible by its step") + ErrNotionalValue = errors.New("total notional value is under minimum limit") + ErrMarketAmountBelowMin = errors.New("market order amount below minimum limit") + ErrMarketAmountExceedsMax = errors.New("market order amount exceeds maximum limit") + ErrMarketAmountExceedsStep = errors.New("amount is not divisible by its step for a market order") +) + +var ( + errExchangeNameEmpty = errors.New("exchange name is empty") + errAssetInvalid = errors.New("asset is invalid") + errPairNotSet = errors.New("currency pair is not set") + errInvalidPriceLevels = errors.New("invalid price levels, cannot load limits") + errInvalidAmountLevels = errors.New("invalid amount levels, cannot load limits") + errInvalidQuoteLevels = errors.New("invalid quote levels, cannot load limits") +) + +// store defines minimum and maximum values for order size, pricing, max orders for exchange order requirements +type store struct { + epaLimits map[key.ExchangeAssetPair]*MinMaxLevel + mtx sync.RWMutex +} + +var manager = store{ + epaLimits: make(map[key.ExchangeAssetPair]*MinMaxLevel), +} + +// MinMaxLevel defines the minimum and maximum parameters for a currency pair +// for outbound exchange execution +type MinMaxLevel struct { + UpdatedAt time.Time + Key key.ExchangeAssetPair + MinPrice float64 + MaxPrice float64 + PriceStepIncrementSize float64 + MultiplierUp float64 + MultiplierDown float64 + MultiplierDecimal float64 + AveragePriceMinutes int64 + MinimumBaseAmount float64 + MaximumBaseAmount float64 + MinimumQuoteAmount float64 + MaximumQuoteAmount float64 + AmountStepIncrementSize float64 + QuoteStepIncrementSize float64 + MinNotional float64 + MaxIcebergParts int64 + MarketMinQty float64 + MarketMaxQty float64 + MarketStepIncrementSize float64 + MaxTotalOrders int64 + MaxAlgoOrders int64 +} diff --git a/exchange/order/limits/store.go b/exchange/order/limits/store.go new file mode 100644 index 00000000..9ed8427e --- /dev/null +++ b/exchange/order/limits/store.go @@ -0,0 +1,118 @@ +package limits + +import ( + "fmt" + "time" + + "github.com/thrasher-corp/gocryptotrader/common/key" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +// Load loads all limits into private limit holder +func Load(levels []MinMaxLevel) error { + return manager.load(levels) +} + +// GetOrderExecutionLimits returns the order limit matching the key +func GetOrderExecutionLimits(k key.ExchangeAssetPair) (MinMaxLevel, error) { + return manager.getOrderExecutionLimits(k) +} + +// CheckOrderExecutionLimits is a convenience method to check if the price and amount conforms +// to the exchange order limits +func CheckOrderExecutionLimits(k key.ExchangeAssetPair, price, amount float64, orderType order.Type) error { + return manager.checkOrderExecutionLimits(k, price, amount, orderType) +} + +func (e *store) load(levels []MinMaxLevel) error { + if len(levels) == 0 { + return ErrEmptyLevels + } + e.mtx.Lock() + defer e.mtx.Unlock() + if e.epaLimits == nil { + e.epaLimits = make(map[key.ExchangeAssetPair]*MinMaxLevel) + } + + for x := range levels { + if levels[x].Key.Exchange == "" { + return fmt.Errorf("cannot load levels for %q %q: %w", levels[x].Key.Asset, levels[x].Key.Pair(), errExchangeNameEmpty) + } + if !levels[x].Key.Asset.IsValid() { + return fmt.Errorf("cannot load levels for %q %q: %w", levels[x].Key.Exchange, levels[x].Key.Pair(), errAssetInvalid) + } + if levels[x].Key.Pair().IsEmpty() { + return fmt.Errorf("cannot load levels for %q %q: %w", levels[x].Key.Exchange, levels[x].Key.Asset, errPairNotSet) + } + if levels[x].MinPrice > 0 && + levels[x].MaxPrice > 0 && + levels[x].MinPrice > levels[x].MaxPrice { + return fmt.Errorf("%w for %q %q %q supplied min: %f max: %f", + errInvalidPriceLevels, + levels[x].Key.Exchange, + levels[x].Key.Asset, + levels[x].Key.Pair(), + levels[x].MinPrice, + levels[x].MaxPrice) + } + + if levels[x].MinimumBaseAmount > 0 && + levels[x].MaximumBaseAmount > 0 && + levels[x].MinimumBaseAmount > levels[x].MaximumBaseAmount { + return fmt.Errorf("%w for %q %q %q supplied min: %f max: %f", + errInvalidAmountLevels, + levels[x].Key.Exchange, + levels[x].Key.Asset, + levels[x].Key.Pair(), + levels[x].MinimumBaseAmount, + levels[x].MaximumBaseAmount) + } + + if levels[x].MinimumQuoteAmount > 0 && + levels[x].MaximumQuoteAmount > 0 && + levels[x].MinimumQuoteAmount > levels[x].MaximumQuoteAmount { + return fmt.Errorf("%w for %q %q %q supplied min: %f max: %f", + errInvalidQuoteLevels, + levels[x].Key.Exchange, + levels[x].Key.Asset, + levels[x].Key.Pair(), + levels[x].MinimumQuoteAmount, + levels[x].MaximumQuoteAmount) + } + levels[x].UpdatedAt = time.Now() + e.epaLimits[levels[x].Key] = &levels[x] + } + return nil +} + +func (e *store) getOrderExecutionLimits(k key.ExchangeAssetPair) (MinMaxLevel, error) { + e.mtx.RLock() + defer e.mtx.RUnlock() + if e.epaLimits == nil { + return MinMaxLevel{}, ErrExchangeLimitNotLoaded + } + el, ok := e.epaLimits[k] + if !ok { + return MinMaxLevel{}, fmt.Errorf("%w for %q %q %q", ErrOrderLimitNotFound, k.Exchange, k.Asset, k.Pair()) + } + return *el, nil +} + +func (e *store) checkOrderExecutionLimits(k key.ExchangeAssetPair, price, amount float64, orderType order.Type) error { + e.mtx.RLock() + defer e.mtx.RUnlock() + if e.epaLimits == nil { + return ErrExchangeLimitNotLoaded + } + m1, ok := e.epaLimits[k] + if !ok { + return fmt.Errorf("%w for %q %q %q", ErrOrderLimitNotFound, k.Exchange, k.Asset, k.Pair()) + } + + err := m1.Validate(price, amount, orderType) + if err != nil { + return fmt.Errorf("%w for %q %q %q", err, k.Exchange, k.Asset, k.Pair()) + } + + return nil +} diff --git a/exchange/order/limits/store_test.go b/exchange/order/limits/store_test.go new file mode 100644 index 00000000..96ed56d3 --- /dev/null +++ b/exchange/order/limits/store_test.go @@ -0,0 +1,225 @@ +package limits + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/common/key" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" +) + +var happyKey = key.NewExchangeAssetPair("test", asset.Spot, currency.NewBTCUSDT()) + +func TestLoadLimits(t *testing.T) { + t.Parallel() + e := store{} + err := e.load(nil) + require.ErrorIs(t, err, ErrEmptyLevels) + + badKeyNoExchange := []MinMaxLevel{ + { + Key: key.NewExchangeAssetPair("", asset.Spot, currency.NewBTCUSDT()), + MinPrice: 100000, + MaxPrice: 1000000, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + err = e.load(badKeyNoExchange) + assert.ErrorIs(t, err, errExchangeNameEmpty) + + badKeyNoAsset := []MinMaxLevel{ + { + Key: key.NewExchangeAssetPair("hi", 0, currency.NewBTCUSDT()), + MinPrice: 100000, + MaxPrice: 1000000, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + err = e.load(badKeyNoAsset) + assert.ErrorIs(t, err, errAssetInvalid) + + badKeyNoPair := []MinMaxLevel{ + { + Key: key.NewExchangeAssetPair("hi", asset.Spot, currency.EMPTYPAIR), + MinPrice: 100000, + MaxPrice: 1000000, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + err = e.load(badKeyNoPair) + assert.ErrorIs(t, err, errPairNotSet) + + happyLimit := []MinMaxLevel{ + { + Key: happyKey, + MinPrice: 100000, + MaxPrice: 1000000, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + + err = e.load(happyLimit) + assert.NoError(t, err) + + badLimit := []MinMaxLevel{ + { + Key: happyKey, + MinPrice: 2, + MaxPrice: 1, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + err = e.load(badLimit) + assert.ErrorIs(t, err, errInvalidPriceLevels) + + badLimit = []MinMaxLevel{ + { + Key: happyKey, + MinPrice: 1, + MaxPrice: 2, + MinimumBaseAmount: 10, + MaximumBaseAmount: 9, + }, + } + err = e.load(badLimit) + assert.ErrorIs(t, err, errInvalidAmountLevels) + + badLimit = []MinMaxLevel{ + { + Key: happyKey, + MinimumQuoteAmount: 100, + MaximumQuoteAmount: 10, + }, + } + err = e.load(badLimit) + assert.ErrorIs(t, err, errInvalidQuoteLevels) +} + +func TestGetOrderExecutionLimits(t *testing.T) { + t.Parallel() + e := store{} + _, err := e.getOrderExecutionLimits(happyKey) + require.ErrorIs(t, err, ErrExchangeLimitNotLoaded) + + newLimits := []MinMaxLevel{ + { + Key: happyKey, + MinPrice: 100000, + MaxPrice: 1000000, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + err = e.load(newLimits) + require.NoError(t, err) + + _, err = e.getOrderExecutionLimits(key.NewExchangeAssetPair("hi", asset.Futures, currency.NewBTCUSDT())) + assert.ErrorIs(t, err, ErrOrderLimitNotFound) + + tt, err := e.getOrderExecutionLimits(happyKey) + require.NoError(t, err) + require.Equal(t, newLimits[0], tt) +} + +func TestCheckLimit(t *testing.T) { + t.Parallel() + e := store{} + err := e.checkOrderExecutionLimits(happyKey, 1337, 1337, order.Limit) + require.ErrorIs(t, err, ErrExchangeLimitNotLoaded) + + newLimits := []MinMaxLevel{ + { + Key: happyKey, + MinPrice: 100000, + MaxPrice: 1000000, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + err = e.load(newLimits) + assert.NoError(t, err) + + err = e.checkOrderExecutionLimits(key.NewExchangeAssetPair("test", asset.Futures, currency.NewBTCUSDT()), 1337, 1337, order.Limit) + assert.ErrorIs(t, err, ErrOrderLimitNotFound) + + err = e.checkOrderExecutionLimits(happyKey, 1337, 9, order.Limit) + assert.ErrorIs(t, err, ErrPriceBelowMin) + + err = e.checkOrderExecutionLimits(happyKey, 1000001, 9, order.Limit) + assert.ErrorIs(t, err, ErrPriceExceedsMax) + + err = e.checkOrderExecutionLimits(happyKey, 999999, .5, order.Limit) + assert.ErrorIs(t, err, ErrAmountBelowMin) + + err = e.checkOrderExecutionLimits(happyKey, 999999, 11, order.Limit) + assert.ErrorIs(t, err, ErrAmountExceedsMax) + + err = e.checkOrderExecutionLimits(happyKey, 999999, 7, order.Limit) + assert.NoError(t, err) + + err = e.checkOrderExecutionLimits(happyKey, 999999, 7, order.Market) + assert.NoError(t, err) +} + +// No parallel, working with global var +func TestPublicLoadLimits(t *testing.T) { + err := Load(nil) + assert.ErrorIs(t, err, ErrEmptyLevels) + + newLimits := []MinMaxLevel{ + { + Key: happyKey, + MinPrice: 100000, + MaxPrice: 1000000, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + err = Load(newLimits) + require.NoError(t, err) +} + +// No parallel, working with global var +func TestPublicGetOrderExecutionLimits(t *testing.T) { + newLimits := []MinMaxLevel{ + { + Key: happyKey, + MinPrice: 100000, + MaxPrice: 1000000, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + err := Load(newLimits) + require.NoError(t, err) + + resp, err := GetOrderExecutionLimits(happyKey) + assert.NoError(t, err) + assert.Equal(t, newLimits[0], resp) +} + +// No parallel, working with global var +func TestPublicCheckOrderExecutionLimits(t *testing.T) { + newLimits := []MinMaxLevel{ + { + Key: happyKey, + MinPrice: 100000, + MaxPrice: 1000000, + MinimumBaseAmount: 1, + MaximumBaseAmount: 10, + }, + } + err := Load(newLimits) + require.NoError(t, err) + + err = CheckOrderExecutionLimits(happyKey, 1, 1, order.Market) + assert.NoError(t, err) +} diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 1482122f..4d261b74 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -15,11 +15,12 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) @@ -1121,9 +1122,9 @@ func (e *Exchange) MaintainWsAuthStreamKey(ctx context.Context) error { } // FetchExchangeLimits fetches order execution limits filtered by asset -func (e *Exchange) FetchExchangeLimits(ctx context.Context, a asset.Item) ([]order.MinMaxLevel, error) { +func (e *Exchange) FetchExchangeLimits(ctx context.Context, a asset.Item) ([]limits.MinMaxLevel, error) { if a != asset.Spot && a != asset.Margin { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } resp, err := e.GetExchangeInfo(ctx) @@ -1133,7 +1134,7 @@ func (e *Exchange) FetchExchangeLimits(ctx context.Context, a asset.Item) ([]ord aUpper := strings.ToUpper(a.String()) - limits := make([]order.MinMaxLevel, 0, len(resp.Symbols)) + l := make([]limits.MinMaxLevel, 0, len(resp.Symbols)) for _, s := range resp.Symbols { var cp currency.Pair cp, err = currency.NewPairFromStrings(s.BaseAsset, s.QuoteAsset) @@ -1145,44 +1146,43 @@ func (e *Exchange) FetchExchangeLimits(ctx context.Context, a asset.Item) ([]ord if !slices.Contains(s.PermissionSets[i], aUpper) { continue } - l := order.MinMaxLevel{ - Pair: cp, - Asset: a, + mml := limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, cp), } for _, f := range s.Filters { // TODO: Unhandled filters: // maxPosition, trailingDelta, percentPriceBySide, maxNumAlgoOrders switch f.FilterType { case priceFilter: - l.MinPrice = f.MinPrice - l.MaxPrice = f.MaxPrice - l.PriceStepIncrementSize = f.TickSize + mml.MinPrice = f.MinPrice + mml.MaxPrice = f.MaxPrice + mml.PriceStepIncrementSize = f.TickSize case percentPriceFilter: - l.MultiplierUp = f.MultiplierUp - l.MultiplierDown = f.MultiplierDown - l.AveragePriceMinutes = f.AvgPriceMinutes + mml.MultiplierUp = f.MultiplierUp + mml.MultiplierDown = f.MultiplierDown + mml.AveragePriceMinutes = f.AvgPriceMinutes case lotSizeFilter: - l.MaximumBaseAmount = f.MaxQty - l.MinimumBaseAmount = f.MinQty - l.AmountStepIncrementSize = f.StepSize + mml.MaximumBaseAmount = f.MaxQty + mml.MinimumBaseAmount = f.MinQty + mml.AmountStepIncrementSize = f.StepSize case notionalFilter: - l.MinNotional = f.MinNotional + mml.MinNotional = f.MinNotional case icebergPartsFilter: - l.MaxIcebergParts = f.Limit + mml.MaxIcebergParts = f.Limit case marketLotSizeFilter: - l.MarketMinQty = f.MinQty - l.MarketMaxQty = f.MaxQty - l.MarketStepIncrementSize = f.StepSize + mml.MarketMinQty = f.MinQty + mml.MarketMaxQty = f.MaxQty + mml.MarketStepIncrementSize = f.StepSize case maxNumOrdersFilter: - l.MaxTotalOrders = f.MaxNumOrders - l.MaxAlgoOrders = f.MaxNumAlgoOrders + mml.MaxTotalOrders = f.MaxNumOrders + mml.MaxAlgoOrders = f.MaxNumAlgoOrders } } - limits = append(limits, l) + l = append(l, mml) break } } - return limits, nil + return l, nil } // CryptoLoanIncomeHistory returns crypto loan income history diff --git a/exchanges/binance/binance_cfutures.go b/exchanges/binance/binance_cfutures.go index 3a8b902c..a38dcf9e 100644 --- a/exchanges/binance/binance_cfutures.go +++ b/exchanges/binance/binance_cfutures.go @@ -11,12 +11,13 @@ import ( "strings" "time" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) @@ -1095,13 +1096,13 @@ func (e *Exchange) FuturesPositionsADLEstimate(ctx context.Context, symbol curre } // FetchCoinMarginExchangeLimits fetches coin margined order execution limits -func (e *Exchange) FetchCoinMarginExchangeLimits(ctx context.Context) ([]order.MinMaxLevel, error) { +func (e *Exchange) FetchCoinMarginExchangeLimits(ctx context.Context) ([]limits.MinMaxLevel, error) { coinFutures, err := e.FuturesExchangeInfo(ctx) if err != nil { return nil, err } - limits := make([]order.MinMaxLevel, 0, len(coinFutures.Symbols)) + l := make([]limits.MinMaxLevel, 0, len(coinFutures.Symbols)) for x := range coinFutures.Symbols { symbol := strings.Split(coinFutures.Symbols[x].Symbol, currency.UnderscoreDelimiter) var cp currency.Pair @@ -1114,9 +1115,8 @@ func (e *Exchange) FetchCoinMarginExchangeLimits(ctx context.Context) ([]order.M continue } - limits = append(limits, order.MinMaxLevel{ - Pair: cp, - Asset: asset.CoinMarginedFutures, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, asset.CoinMarginedFutures, cp), MinPrice: coinFutures.Symbols[x].Filters[0].MinPrice, MaxPrice: coinFutures.Symbols[x].Filters[0].MaxPrice, PriceStepIncrementSize: coinFutures.Symbols[x].Filters[0].TickSize, @@ -1133,5 +1133,5 @@ func (e *Exchange) FetchCoinMarginExchangeLimits(ctx context.Context) ([]order.M MultiplierDecimal: coinFutures.Symbols[x].Filters[5].MultiplierDecimal, }) } - return limits, nil + return l, nil } diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 924045e9..0f3b5305 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -2509,48 +2509,6 @@ func TestUFuturesHistoricalTrades(t *testing.T) { } } -func TestSetExchangeOrderExecutionLimits(t *testing.T) { - t.Parallel() - err := e.UpdateOrderExecutionLimits(t.Context(), asset.Spot) - if err != nil { - t.Fatal(err) - } - err = e.UpdateOrderExecutionLimits(t.Context(), asset.CoinMarginedFutures) - if err != nil { - t.Fatal(err) - } - - err = e.UpdateOrderExecutionLimits(t.Context(), asset.USDTMarginedFutures) - if err != nil { - t.Fatal(err) - } - - err = e.UpdateOrderExecutionLimits(t.Context(), asset.Binary) - if err == nil { - t.Fatal("expected unhandled case") - } - - cmfCP, err := currency.NewPairFromStrings("BTCUSD", "PERP") - if err != nil { - t.Fatal(err) - } - - limit, err := e.GetOrderExecutionLimits(asset.CoinMarginedFutures, cmfCP) - if err != nil { - t.Fatal(err) - } - - if limit == (order.MinMaxLevel{}) { - t.Fatal("exchange limit should be loaded") - } - - err = limit.Conforms(0.000001, 0.1, order.Limit) - require.ErrorIs(t, err, order.ErrAmountBelowMin) - - err = limit.Conforms(0.01, 1, order.Limit) - require.ErrorIs(t, err, order.ErrPriceBelowMin) -} - func TestWsOrderExecutionReport(t *testing.T) { t.Parallel() e := new(Exchange) //nolint:govet // Intentional shadow @@ -2750,13 +2708,13 @@ func TestFormatUSDTMarginedFuturesPair(t *testing.T) { func TestFetchExchangeLimits(t *testing.T) { t.Parallel() - limits, err := e.FetchExchangeLimits(t.Context(), asset.Spot) + l, err := e.FetchExchangeLimits(t.Context(), asset.Spot) assert.NoError(t, err, "FetchExchangeLimits should not error") - assert.NotEmpty(t, limits, "Should get some limits back") + assert.NotEmpty(t, l, "Should get some limits back") - limits, err = e.FetchExchangeLimits(t.Context(), asset.Margin) + l, err = e.FetchExchangeLimits(t.Context(), asset.Margin) assert.NoError(t, err, "FetchExchangeLimits should not error") - assert.NotEmpty(t, limits, "Should get some limits back") + assert.NotEmpty(t, l, "Should get some limits back") _, err = e.FetchExchangeLimits(t.Context(), asset.Futures) assert.ErrorIs(t, err, asset.ErrNotSupported, "FetchExchangeLimits should error on other asset types") @@ -2764,48 +2722,37 @@ func TestFetchExchangeLimits(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - spotEnabled, err := e.GetEnabledPairs(asset.Spot) - require.NoError(t, err, "GetEnabledPairs must not error") - - tests := map[asset.Item]currency.Pair{ - asset.Spot: spotEnabled[0], - asset.Margin: currency.NewPair(currency.ETH, currency.BTC), - } - for _, a := range []asset.Item{asset.CoinMarginedFutures, asset.USDTMarginedFutures} { - pairs, err := e.FetchTradablePairs(t.Context(), a) - require.NoErrorf(t, err, "FetchTradablePairs must not error for %s", a) - require.NotEmptyf(t, pairs, "Must get some pairs for %s", a) - tests[a] = pairs[0] - } - + testexch.UpdatePairsOnce(t, e) for _, a := range e.GetAssetTypes(false) { - err := e.UpdateOrderExecutionLimits(t.Context(), a) - require.NoError(t, err, "UpdateOrderExecutionLimits must not error") - - p := tests[a] - limits, err := e.GetOrderExecutionLimits(a, p) - require.NoErrorf(t, err, "GetOrderExecutionLimits must not error for %s pair %s", a, p) - assert.Positivef(t, limits.MinPrice, "MinPrice should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.MaxPrice, "MaxPrice should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.PriceStepIncrementSize, "PriceStepIncrementSize should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.MinimumBaseAmount, "MinimumBaseAmount should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.MaximumBaseAmount, "MaximumBaseAmount should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.AmountStepIncrementSize, "AmountStepIncrementSize should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.MarketMaxQty, "MarketMaxQty should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.MaxTotalOrders, "MaxTotalOrders should be positive for %s pair %s", a, p) - switch a { - case asset.Spot, asset.Margin: - assert.Positivef(t, limits.MaxIcebergParts, "MaxIcebergParts should be positive for %s pair %s", a, p) - case asset.USDTMarginedFutures: - assert.Positivef(t, limits.MinNotional, "MinNotional should be positive for %s pair %s", a, p) - fallthrough - case asset.CoinMarginedFutures: - assert.Positivef(t, limits.MultiplierUp, "MultiplierUp should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.MultiplierDown, "MultiplierDown should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.MarketMinQty, "MarketMinQty should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.MarketStepIncrementSize, "MarketStepIncrementSize should be positive for %s pair %s", a, p) - assert.Positivef(t, limits.MaxAlgoOrders, "MaxAlgoOrders should be positive for %s pair %s", a, p) - } + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(a, false) + 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.MinPrice, "MinPrice should be positive") + assert.Positive(t, l.MaxPrice, "MaxPrice should be positive") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + assert.Positive(t, l.MaximumBaseAmount, "MaximumBaseAmount should be positive") + assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") + assert.Positive(t, l.MarketMaxQty, "MarketMaxQty should be positive") + assert.Positive(t, l.MaxTotalOrders, "MaxTotalOrders should be positive") + switch a { + case asset.Spot, asset.Margin: + assert.Positive(t, l.MaxIcebergParts, "MaxIcebergParts should be positive") + case asset.USDTMarginedFutures: + assert.Positive(t, l.MinNotional, "MinNotional should be positive") + fallthrough + case asset.CoinMarginedFutures: + assert.Positive(t, l.MultiplierUp, "MultiplierUp should be positive") + assert.Positive(t, l.MultiplierDown, "MultiplierDown should be positive") + assert.Positive(t, l.MarketMinQty, "MarketMinQty should be positive") + assert.Positive(t, l.MarketStepIncrementSize, "MarketStepIncrementSize should be positive") + assert.Positive(t, l.MaxAlgoOrders, "MaxAlgoOrders should be positive") + } + }) } } diff --git a/exchanges/binance/binance_ufutures.go b/exchanges/binance/binance_ufutures.go index 2c223013..6401a393 100644 --- a/exchanges/binance/binance_ufutures.go +++ b/exchanges/binance/binance_ufutures.go @@ -10,12 +10,13 @@ import ( "strconv" "time" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/types" ) @@ -1002,13 +1003,12 @@ func (e *Exchange) GetPerpMarkets(ctx context.Context) (PerpsExchangeInfo, error } // FetchUSDTMarginExchangeLimits fetches USDT margined order execution limits -func (e *Exchange) FetchUSDTMarginExchangeLimits(ctx context.Context) ([]order.MinMaxLevel, error) { +func (e *Exchange) FetchUSDTMarginExchangeLimits(ctx context.Context) ([]limits.MinMaxLevel, error) { usdtFutures, err := e.UExchangeInfo(ctx) if err != nil { return nil, err } - - limits := make([]order.MinMaxLevel, 0, len(usdtFutures.Symbols)) + l := make([]limits.MinMaxLevel, 0, len(usdtFutures.Symbols)) for x := range usdtFutures.Symbols { var cp currency.Pair cp, err = currency.NewPairFromStrings(usdtFutures.Symbols[x].BaseAsset, @@ -1021,9 +1021,8 @@ func (e *Exchange) FetchUSDTMarginExchangeLimits(ctx context.Context) ([]order.M continue } - limits = append(limits, order.MinMaxLevel{ - Pair: cp, - Asset: asset.USDTMarginedFutures, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, asset.USDTMarginedFutures, cp), MinPrice: usdtFutures.Symbols[x].Filters[0].MinPrice, MaxPrice: usdtFutures.Symbols[x].Filters[0].MaxPrice, PriceStepIncrementSize: usdtFutures.Symbols[x].Filters[0].TickSize, @@ -1041,7 +1040,7 @@ func (e *Exchange) FetchUSDTMarginExchangeLimits(ctx context.Context) ([]order.M MultiplierDecimal: usdtFutures.Symbols[x].Filters[6].MultiplierDecimal, }) } - return limits, nil + return l, nil } // SetAssetsMode sets the current asset margin type, true for multi, false for single diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index d789e51d..d5863e1b 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -14,6 +14,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" "github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -246,7 +247,7 @@ func (e *Exchange) Setup(exch *config.Exchange) error { // FetchTradablePairs returns a list of the exchanges tradable pairs func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { if !e.SupportsAsset(a) { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } tradingStatus := "TRADING" var pairs []currency.Pair @@ -432,7 +433,7 @@ func (e *Exchange) UpdateTickers(ctx context.Context, a asset.Item) error { } } default: - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } return nil } @@ -507,7 +508,7 @@ func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, a asset.It } default: - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } return ticker.GetTicker(e.Name, p, a) } @@ -808,7 +809,7 @@ func (e *Exchange) GetHistoricTrades(ctx context.Context, p currency.Pair, a ass return nil, err } if a != asset.Spot { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } rFmt, err := e.GetPairFormat(a, true) if err != nil { @@ -1704,7 +1705,7 @@ func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a }) } default: - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } return req.ProcessResponse(timeSeries) } @@ -1785,7 +1786,7 @@ func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency }) } default: - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } return req.ProcessResponse(timeSeries) @@ -1836,24 +1837,24 @@ func compatibleOrderVars(side, status, orderType string) OrderVars { // UpdateOrderExecutionLimits sets exchange executions for a required asset type func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { - var limits []order.MinMaxLevel + var l []limits.MinMaxLevel var err error switch a { case asset.Spot: - limits, err = e.FetchExchangeLimits(ctx, asset.Spot) + l, err = e.FetchExchangeLimits(ctx, asset.Spot) case asset.USDTMarginedFutures: - limits, err = e.FetchUSDTMarginExchangeLimits(ctx) + l, err = e.FetchUSDTMarginExchangeLimits(ctx) case asset.CoinMarginedFutures: - limits, err = e.FetchCoinMarginExchangeLimits(ctx) + l, err = e.FetchCoinMarginExchangeLimits(ctx) case asset.Margin: - limits, err = e.FetchExchangeLimits(ctx, asset.Margin) + l, err = e.FetchExchangeLimits(ctx, asset.Margin) default: - err = fmt.Errorf("%w %v", asset.ErrNotSupported, a) + err = fmt.Errorf("%w %q", asset.ErrNotSupported, a) } if err != nil { return fmt.Errorf("cannot update exchange execution limits: %w", err) } - return e.LoadLimits(limits) + return limits.Load(l) } // GetAvailableTransferChains returns the available transfer blockchains for the specific cryptocurrency @@ -2252,7 +2253,7 @@ func (e *Exchange) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bo // SetCollateralMode sets the account's collateral mode for the asset type func (e *Exchange) SetCollateralMode(ctx context.Context, a asset.Item, collateralMode collateral.Mode) error { if a != asset.USDTMarginedFutures { - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } if collateralMode != collateral.MultiMode && collateralMode != collateral.SingleMode { return fmt.Errorf("%w %v", order.ErrCollateralInvalid, collateralMode) @@ -2263,7 +2264,7 @@ func (e *Exchange) SetCollateralMode(ctx context.Context, a asset.Item, collater // GetCollateralMode returns the account's collateral mode for the asset type func (e *Exchange) GetCollateralMode(ctx context.Context, a asset.Item) (collateral.Mode, error) { if a != asset.USDTMarginedFutures { - return collateral.UnknownMode, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return collateral.UnknownMode, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } isMulti, err := e.GetAssetsMode(ctx) if err != nil { @@ -2983,12 +2984,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f return nil, err } result[i] = futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: k[i].Base, - Quote: k[i].Quote, - Asset: k[i].Asset, - }, + Key: key.NewExchangeAssetPair(e.Name, k[i].Asset, k[i].Pair()), OpenInterest: oi.OpenInterest, } case asset.CoinMarginedFutures: @@ -2997,12 +2993,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f return nil, err } result[i] = futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: k[i].Base, - Quote: k[i].Quote, - Asset: k[i].Asset, - }, + Key: key.NewExchangeAssetPair(e.Name, k[i].Asset, k[i].Pair()), OpenInterest: oi.OpenInterest, } } @@ -3070,6 +3061,6 @@ func (e *Exchange) GetCurrencyTradeURL(ctx context.Context, a asset.Item, cp cur case asset.Margin: return tradeBaseURL + "trade/" + symbol + "?type=cross", nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/binanceus/binanceus_wrapper.go b/exchanges/binanceus/binanceus_wrapper.go index d7143f28..d805788c 100644 --- a/exchanges/binanceus/binanceus_wrapper.go +++ b/exchanges/binanceus/binanceus_wrapper.go @@ -196,7 +196,7 @@ func (e *Exchange) Setup(exch *config.Exchange) error { // FetchTradablePairs returns a list of the exchanges tradable pairs func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { if !e.SupportsAsset(a) { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } info, err := e.GetExchangeInfo(ctx) if err != nil { @@ -268,7 +268,7 @@ func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, a asset.It // UpdateTickers updates all currency pairs of a given asset type func (e *Exchange) UpdateTickers(ctx context.Context, a asset.Item) error { if a != asset.Spot { - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } tick, err := e.GetTickers(ctx) if err != nil { @@ -398,7 +398,7 @@ func (e *Exchange) GetAccountFundingHistory(_ context.Context) ([]exchange.Fundi // GetWithdrawalsHistory returns previous withdrawals data func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, a asset.Item) ([]exchange.WithdrawalHistory, error) { if a != asset.Spot { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } withdrawals, err := e.WithdrawalHistory(ctx, c, "", time.Time{}, time.Time{}, 0, 10000) if err != nil { diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index e03748ae..383f9ffd 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -19,8 +19,10 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/nonce" @@ -293,11 +295,11 @@ func (e *Exchange) GetV2MarginFunding(ctx context.Context, symbol, amount string } // GetV2FundingInfo gets funding info for margin pairs -func (e *Exchange) GetV2FundingInfo(ctx context.Context, key string) (MarginFundingDataV2, error) { +func (e *Exchange) GetV2FundingInfo(ctx context.Context, k string) (MarginFundingDataV2, error) { var resp []any var response MarginFundingDataV2 err := e.SendAuthenticatedHTTPRequestV2(ctx, exchange.RestSpot, http.MethodPost, - fmt.Sprintf(bitfinexV2FundingInfo, key), + fmt.Sprintf(bitfinexV2FundingInfo, k), nil, &resp, getAccountFees) @@ -454,9 +456,9 @@ func (e *Exchange) GetPairs(ctx context.Context, a asset.Item) ([]string, error) return nil, err } var pairs []string - for key := range funding { - symbol := key[1:] - if key[0] != 'f' || strings.Contains(symbol, ":") || len(symbol) > 6 { + for k := range funding { + symbol := k[1:] + if k[0] != 'f' || strings.Contains(symbol, ":") || len(symbol) > 6 { continue } pairs = append(pairs, symbol) @@ -490,7 +492,7 @@ func (e *Exchange) GetSiteListConfigData(ctx context.Context, set string) ([]str // GetSiteInfoConfigData returns site configuration data by pub:info:{AssetType} as a map // path should be bitfinexInfoPairs or bitfinexInfoPairsFuture??? // NOTE: See https://docs.bitfinex.com/reference/rest-public-conf -func (e *Exchange) GetSiteInfoConfigData(ctx context.Context, assetType asset.Item) ([]order.MinMaxLevel, error) { +func (e *Exchange) GetSiteInfoConfigData(ctx context.Context, assetType asset.Item) ([]limits.MinMaxLevel, error) { var path string switch assetType { case asset.Spot: @@ -500,7 +502,6 @@ func (e *Exchange) GetSiteInfoConfigData(ctx context.Context, assetType asset.It default: return nil, fmt.Errorf("invalid asset type for GetSiteInfoConfigData: %s", assetType) } - var resp [][][]any err := e.SendHTTPRequest(ctx, exchange.RestSpot, bitfinexAPIVersion2+path, &resp, status) if err != nil { @@ -510,7 +511,7 @@ func (e *Exchange) GetSiteInfoConfigData(ctx context.Context, assetType asset.It return nil, errors.New("response did not contain only one item") } data := resp[0] - pairs := make([]order.MinMaxLevel, 0, len(data)) + l := make([]limits.MinMaxLevel, 0, len(data)) for i := range data { if len(data[i]) != 2 { return nil, errors.New("response contained a tuple without exactly 2 items") @@ -542,14 +543,13 @@ func (e *Exchange) GetSiteInfoConfigData(ctx context.Context, assetType asset.It if err != nil { return nil, err } - pairs = append(pairs, order.MinMaxLevel{ - Asset: assetType, - Pair: pair, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, assetType, pair), MinimumBaseAmount: minOrder, MaximumBaseAmount: maxOrder, }) } - return pairs, nil + return l, nil } // GetDerivativeStatusInfo gets status data for the queried derivative @@ -992,7 +992,7 @@ func (e *Exchange) GetLiquidationFeed() error { // profit // Allowed time frames are 3h, 1w and 1M // Allowed symbols are trading pairs (e.g. tBTCUSD, tETHUSD and tGLOBAL:USD) -func (e *Exchange) GetLeaderboard(ctx context.Context, key, timeframe, symbol string, sort, limit int, start, end string) ([]LeaderboardEntry, error) { +func (e *Exchange) GetLeaderboard(ctx context.Context, k, timeframe, symbol string, sort, limit int, start, end string) ([]LeaderboardEntry, error) { validLeaderboardKey := func(input string) bool { switch input { case LeaderboardUnrealisedProfitPeriodDelta, @@ -1005,12 +1005,12 @@ func (e *Exchange) GetLeaderboard(ctx context.Context, key, timeframe, symbol st } } - if !validLeaderboardKey(key) { + if !validLeaderboardKey(k) { return nil, errors.New("invalid leaderboard key") } path := fmt.Sprintf("%s/%s:%s:%s/hist", bitfinexAPIVersion2+bitfinexLeaderboard, - key, + k, timeframe, symbol) vals := url.Values{} diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 462aba9a..06dc5e72 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -141,27 +141,21 @@ func TestUpdateTradablePairs(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - tests := map[asset.Item][]currency.Pair{ - asset.Spot: { - currency.NewPair(currency.ETH, currency.UST), - currency.NewPair(currency.BTC, currency.UST), - }, - } - for assetItem, pairs := range tests { - if err := e.UpdateOrderExecutionLimits(t.Context(), assetItem); err != nil { - t.Errorf("Error fetching %s pairs for test: %v", assetItem, err) - continue - } - for _, pair := range pairs { - limits, err := e.GetOrderExecutionLimits(assetItem, pair) - if err != nil { - t.Errorf("GetOrderExecutionLimits() error during TestExecutionLimits; Asset: %s Pair: %s Err: %v", assetItem, pair, err) - continue + for _, a := range e.GetAssetTypes(false) { + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + switch a { + case asset.Spot: + require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(a, false) + 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") + default: + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), a), common.ErrNotYetImplemented) } - if limits.MinimumBaseAmount == 0 { - t.Errorf("UpdateOrderExecutionLimits empty minimum base amount; Pair: %s Expected Limit: %v", pair, limits.MinimumBaseAmount) - } - } + }) } } diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 4c9b0400..31ba0f3a 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -14,6 +14,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" @@ -273,11 +274,11 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if a != asset.Spot { return common.ErrNotYetImplemented } - limits, err := e.GetSiteInfoConfigData(ctx, a) + l, err := e.GetSiteInfoConfigData(ctx, a) if err != nil { return err } - if err := e.LoadLimits(limits); err != nil { + if err := limits.Load(l); err != nil { return fmt.Errorf("%s Error loading exchange limits: %v", e.Name, err) } return nil @@ -489,7 +490,7 @@ func (e *Exchange) GetRecentTrades(ctx context.Context, p currency.Pair, assetTy // GetHistoricTrades returns historic trade data within the timeframe provided func (e *Exchange) GetHistoricTrades(ctx context.Context, p currency.Pair, a asset.Item, timestampStart, timestampEnd time.Time) ([]trade.Data, error) { if a == asset.MarginFunding { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } if err := common.StartEndTimeCheck(timestampStart, timestampEnd); err != nil { return nil, fmt.Errorf("invalid time range supplied. Start: %v End %v %w", timestampStart, timestampEnd, err) @@ -1201,6 +1202,6 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre case asset.Spot: return tradeBaseURL + "/t/" + symbol, nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/bithumb/bithumb.go b/exchanges/bithumb/bithumb.go index 81d2aa2e..59b8fc96 100644 --- a/exchanges/bithumb/bithumb.go +++ b/exchanges/bithumb/bithumb.go @@ -15,11 +15,12 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" ) @@ -699,21 +700,20 @@ func (e *Exchange) GetCandleStick(ctx context.Context, symbol, interval string) } // FetchExchangeLimits fetches spot order execution limits -func (e *Exchange) FetchExchangeLimits(ctx context.Context) ([]order.MinMaxLevel, error) { +func (e *Exchange) FetchExchangeLimits(ctx context.Context) ([]limits.MinMaxLevel, error) { ticks, err := e.GetAllTickers(ctx) if err != nil { return nil, err } - limits := make([]order.MinMaxLevel, 0, len(ticks)) + l := make([]limits.MinMaxLevel, 0, len(ticks)) for code, data := range ticks { - limits = append(limits, order.MinMaxLevel{ - Pair: currency.NewPair(currency.NewCode(code), currency.KRW), - Asset: asset.Spot, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, asset.Spot, currency.NewPair(currency.NewCode(code), currency.KRW)), MinimumBaseAmount: getAmountMinimum(data.ClosingPrice), }) } - return limits, nil + return l, nil } // getAmountMinimum derives the minimum amount based on current price. This diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index e041cf40..56b4cc4a 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -547,17 +547,17 @@ func TestGetHistoricTrades(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - err := e.UpdateOrderExecutionLimits(t.Context(), asset.Empty) - require.NoError(t, err, "UpdateOrderExecutionLimits must not error") - - limit, err := e.GetOrderExecutionLimits(asset.Spot, testPair) - require.NoError(t, err, "GetOrderExecutionLimits must not error") - - err = limit.Conforms(46241000, 0.00001, order.Limit) - assert.ErrorIs(t, err, order.ErrAmountBelowMin) - - err = limit.Conforms(46241000, 0.0001, order.Limit) - assert.NoError(t, err, "Conforms should not error") + for _, a := range e.GetAssetTypes(false) { + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(a, false) + 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") + }) + } } func TestGetAmountMinimum(t *testing.T) { diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index 5cdc3133..d88428eb 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -13,6 +13,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/convert" "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" @@ -782,12 +783,15 @@ func (e *Exchange) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair } // UpdateOrderExecutionLimits sets exchange executions for a required asset type -func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, _ asset.Item) error { - limits, err := e.FetchExchangeLimits(ctx) +func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { + if !e.CurrencyPairs.IsAssetSupported(a) { + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) + } + l, err := e.FetchExchangeLimits(ctx) if err != nil { return fmt.Errorf("cannot update exchange execution limits: %w", err) } - return e.LoadLimits(limits) + return limits.Load(l) } // UpdateCurrencyStates updates currency states for exchange diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 9b99dbea..01eae97a 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -1264,12 +1264,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } resp = append(resp, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: symbol.Base.Item, - Quote: symbol.Quote.Item, - Asset: a, - }, + Key: key.NewExchangeAssetPair(e.Name, a, symbol), OpenInterest: activeInstruments[i].OpenInterest, }) } @@ -1296,12 +1291,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f } resp := make([]futures.OpenInterest, 1) resp[0] = futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: k[0].Base, - Quote: k[0].Quote, - Asset: k[0].Asset, - }, + Key: key.NewExchangeAssetPair(e.Name, k[0].Asset, k[0].Pair()), OpenInterest: instrument[0].OpenInterest, } return resp, nil diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index 86adfed8..81ca525d 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -241,42 +241,24 @@ func TestUpdateTradablePairs(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - - type limitTest struct { - pair currency.Pair - step float64 - min float64 - } - - tests := map[asset.Item][]limitTest{ - asset.Spot: { - {currency.NewPair(currency.ETH, currency.USDT), 0.01, 20}, - {currency.NewBTCUSDT(), 0.01, 20}, - }, - } - for assetItem, limitTests := range tests { - if err := e.UpdateOrderExecutionLimits(t.Context(), assetItem); err != nil { - t.Errorf("Error fetching %s pairs for test: %v", assetItem, err) - } - for _, limitTest := range limitTests { - limits, err := e.GetOrderExecutionLimits(assetItem, limitTest.pair) - if err != nil { - t.Errorf("Bitstamp GetOrderExecutionLimits() error during TestExecutionLimits; Asset: %s Pair: %s Err: %v", assetItem, limitTest.pair, err) - continue - } - assert.NotEmpty(t, limits.Pair, "Pair should not be empty") - assert.Positive(t, limits.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") - assert.Positive(t, limits.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") - assert.Positive(t, limits.MinimumQuoteAmount, "MinimumQuoteAmount should be positive") + for _, a := range e.GetAssetTypes(false) { + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(a, false) + 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.PriceStepIncrementSize, "PriceStepIncrementSize should not be zero") + assert.NotEmpty(t, l.Key.Pair(), "Pair should not be empty") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") + assert.Positive(t, l.MinimumQuoteAmount, "MinimumQuoteAmount should be positive") if mockTests { - if got := limits.PriceStepIncrementSize; got != limitTest.step { - t.Errorf("Bitstamp UpdateOrderExecutionLimits wrong PriceStepIncrementSize; Asset: %s Pair: %s Expected: %v Got: %v", assetItem, limitTest.pair, limitTest.step, got) - } - if got := limits.MinimumQuoteAmount; got != limitTest.min { - t.Errorf("Bitstamp UpdateOrderExecutionLimits wrong MinAmount; Pair: %s Expected: %v Got: %v", limitTest.pair, limitTest.min, got) - } + assert.Equal(t, 0.01, l.PriceStepIncrementSize, "PriceStepIncrementSize should be 0.01") + assert.Equal(t, 20., l.MinimumQuoteAmount, "MinimumQuoteAmount should be 20") } - } + }) } } diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 562597b2..78c78a5a 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -10,8 +10,10 @@ import ( "time" "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/exchange/order/limits" "github.com/thrasher-corp/gocryptotrader/exchange/websocket" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/account" @@ -215,7 +217,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if err != nil { return err } - limits := make([]order.MinMaxLevel, 0, len(symbols)) + l := make([]limits.MinMaxLevel, 0, len(symbols)) for x, info := range symbols { if symbols[x].Trading != "Enabled" { continue @@ -224,15 +226,14 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if err != nil { return err } - limits = append(limits, order.MinMaxLevel{ - Asset: a, - Pair: pair, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, pair), PriceStepIncrementSize: math.Pow10(-info.CounterDecimals), AmountStepIncrementSize: math.Pow10(-info.BaseDecimals), MinimumQuoteAmount: info.MinimumOrder, }) } - if err := e.LoadLimits(limits); err != nil { + if err := limits.Load(l); err != nil { return fmt.Errorf("%s Error loading exchange limits: %v", e.Name, err) } return nil diff --git a/exchanges/btcmarkets/btcmarkets_test.go b/exchanges/btcmarkets/btcmarkets_test.go index 7df1ddd7..031929f9 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -912,17 +912,16 @@ func TestWrapperModifyOrder(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - err := e.UpdateOrderExecutionLimits(t.Context(), asset.Empty) - require.ErrorIs(t, err, asset.ErrNotSupported) - - err = e.UpdateOrderExecutionLimits(t.Context(), asset.Spot) - require.NoError(t, err) - - lim, err := e.ExecutionLimits.GetOrderExecutionLimits(asset.Spot, currency.NewPair(currency.BTC, currency.AUD)) - require.NoError(t, err) - - if lim == (order.MinMaxLevel{}) { - t.Fatal("expected value return") + for _, a := range e.GetAssetTypes(false) { + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(a, false) + 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") + }) } } diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 79506f9a..394cfc32 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -10,8 +10,10 @@ import ( "time" "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/exchange/order/limits" "github.com/thrasher-corp/gocryptotrader/exchange/websocket" "github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -177,7 +179,7 @@ func (e *Exchange) Setup(exch *config.Exchange) error { // FetchTradablePairs returns a list of the exchanges tradable pairs func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { if a != asset.Spot { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } markets, err := e.GetMarkets(ctx) if err != nil { @@ -992,7 +994,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) return err } - limits := make([]order.MinMaxLevel, len(markets)) + l := make([]limits.MinMaxLevel, len(markets)) for x := range markets { var pair currency.Pair pair, err = currency.NewPairFromStrings(markets[x].BaseAsset, markets[x].QuoteAsset) @@ -1000,16 +1002,15 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) return err } - limits[x] = order.MinMaxLevel{ - Pair: pair, - Asset: asset.Spot, + l[x] = limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, asset.Spot, pair), MinimumBaseAmount: markets[x].MinOrderAmount, MaximumBaseAmount: markets[x].MaxOrderAmount, AmountStepIncrementSize: math.Pow(10, -markets[x].AmountDecimals), PriceStepIncrementSize: math.Pow(10, -markets[x].PriceDecimals), } } - return e.LoadLimits(limits) + return limits.Load(l) } // GetFuturesContractDetails returns all contracts from the exchange by asset type diff --git a/exchanges/btse/btse_test.go b/exchanges/btse/btse_test.go index 244f6606..fc3b0953 100644 --- a/exchanges/btse/btse_test.go +++ b/exchanges/btse/btse_test.go @@ -571,27 +571,22 @@ func TestMatchType(t *testing.T) { assert.True(t, ret, "matchType should match") } -// TestUpdateOrderExecutionLimits exercises UpdateOrderExecutionLimits func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - testexch.UpdatePairsOnce(t, e) for _, a := range e.GetAssetTypes(false) { - err := e.UpdateOrderExecutionLimits(t.Context(), a) - require.NoErrorf(t, err, "UpdateOrderExecutionLimits must not error for %s", a) - - pairs, err := e.GetAvailablePairs(a) - require.NoErrorf(t, err, "GetAvailablePairs must not error for %s", a) - require.NotEmpty(t, pairs, "GetAvailablePairs must return some pairs") - - for _, p := range pairs { - limits, err := e.GetOrderExecutionLimits(a, p) - require.NoErrorf(t, err, "GetOrderExecutionLimits must not error for %s %s", a, p) - assert.Positivef(t, limits.MinimumBaseAmount, "MinimumBaseAmount should be positive for %s %s", a, p) - assert.Positivef(t, limits.MaximumBaseAmount, "MaximumBaseAmount should be positive for %s %s", a, p) - assert.Positivef(t, limits.AmountStepIncrementSize, "AmountStepIncrementSize should be positive for %s %s", a, p) - assert.Positivef(t, limits.MinPrice, "MinPrice should be positive for %s %s", a, p) - assert.Positivef(t, limits.PriceStepIncrementSize, "PriceStepIncrementSize should be positive for %s %s", a, p) - } + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + 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") + assert.Positive(t, l.MaximumBaseAmount, "MaximumBaseAmount should be positive") + assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") + assert.Positive(t, l.MinPrice, "MinPrice should be positive") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + }) } } diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 3e72593e..68f69ab2 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -14,6 +14,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" @@ -228,7 +229,7 @@ func (e *Exchange) UpdateTradablePairs(ctx context.Context, forceUpdate bool) er // UpdateTickers updates the ticker for all currency pairs of a given asset type func (e *Exchange) UpdateTickers(ctx context.Context, a asset.Item) error { if !e.SupportsAsset(a) { - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } tickers, err := e.GetMarketSummary(ctx, "", a == asset.Spot) if err != nil { @@ -265,7 +266,7 @@ func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, a asset.It return nil, currency.ErrCurrencyPairEmpty } if !e.SupportsAsset(a) { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } symbol, err := e.FormatSymbol(p, a) if err != nil { @@ -894,7 +895,7 @@ func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a switch a { case asset.Spot, asset.Futures: default: - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } req, err := e.GetKlineRequest(pair, a, interval, start, end, false) if err != nil { @@ -935,7 +936,7 @@ func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency switch a { case asset.Spot, asset.Futures: default: - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } req, err := e.GetKlineExtendedRequest(pair, a, interval, start, end) if err != nil { @@ -1222,16 +1223,15 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) return err } var errs error - limits := make([]order.MinMaxLevel, 0, len(summary)) + l := make([]limits.MinMaxLevel, 0, len(summary)) for _, marketInfo := range summary { p, err := marketInfo.Pair() if err != nil { errs = common.AppendError(err, fmt.Errorf("%s: %w", p, err)) continue } - limits = append(limits, order.MinMaxLevel{ - Pair: p, - Asset: a, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, p), MinimumBaseAmount: marketInfo.MinOrderSize, MaximumBaseAmount: marketInfo.MaxOrderSize, AmountStepIncrementSize: marketInfo.MinSizeIncrement, @@ -1239,7 +1239,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) PriceStepIncrementSize: marketInfo.MinPriceIncrement, }) } - if err = e.LoadLimits(limits); err != nil { + if err = limits.Load(l); err != nil { errs = common.AppendError(errs, err) } return errs @@ -1279,12 +1279,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } resp = append(resp, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: symbol.Base.Item, - Quote: symbol.Quote.Item, - Asset: asset.Futures, - }, + Key: key.NewExchangeAssetPair(e.Name, asset.Futures, symbol), OpenInterest: tickers[i].OpenInterest, }) } @@ -1304,6 +1299,6 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre case asset.Futures: return tradeBaseURL + tradeFutures + cp.Upper().String(), nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/bybit/bybit.go b/exchanges/bybit/bybit.go index 347a36c3..fa176e69 100644 --- a/exchanges/bybit/bybit.go +++ b/exchanges/bybit/bybit.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -489,7 +490,7 @@ func (e *Exchange) PlaceOrder(ctx context.Context, arg *PlaceOrderParams) (*Orde return nil, order.ErrTypeIsInvalid } if arg.OrderQuantity <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } switch arg.TriggerDirection { case 0, 1, 2: // 0: None, 1: triggered when market price rises to triggerPrice, 2: triggered when market price falls to triggerPrice @@ -698,7 +699,7 @@ func (e *Exchange) PlaceBatchOrder(ctx context.Context, arg *PlaceBatchOrderPara return nil, order.ErrTypeIsInvalid } if arg.Request[a].OrderQuantity <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } } var resp BatchOrdersList @@ -1854,7 +1855,7 @@ func (e *Exchange) WithdrawCurrency(ctx context.Context, arg *WithdrawalParam) ( return "", errMissingAddressInfo } if arg.Amount <= 0 { - return "", order.ErrAmountBelowMin + return "", limits.ErrAmountBelowMin } if arg.Timestamp == 0 { arg.Timestamp = time.Now().UnixMilli() @@ -2067,7 +2068,7 @@ func (e *Exchange) PurchaseLeverageToken(ctx context.Context, ltCoin currency.Co return nil, fmt.Errorf("%w, 'ltCoin' is required", currency.ErrCurrencyCodeEmpty) } if amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } arg := &struct { LTCoin string `json:"ltCoin"` @@ -2088,7 +2089,7 @@ func (e *Exchange) RedeemLeverageToken(ctx context.Context, ltCoin currency.Code return nil, fmt.Errorf("%w, 'ltCoin' is required", currency.ErrCurrencyCodeEmpty) } if quantity <= 0 { - return nil, fmt.Errorf("%w, quantity=%f", order.ErrAmountBelowMin, quantity) + return nil, fmt.Errorf("%w, quantity=%f", limits.ErrAmountBelowMin, quantity) } arg := &struct { LTCoin string `json:"ltCoin"` @@ -2219,7 +2220,7 @@ func (e *Exchange) Borrow(ctx context.Context, arg *LendArgument) (*BorrowRespon return nil, currency.ErrCurrencyCodeEmpty } if arg.AmountToBorrow <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } var resp *BorrowResponse return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/spot-cross-margin-trade/loan", nil, arg, &resp, spotCrossMarginTradeLoanEPL) @@ -2234,7 +2235,7 @@ func (e *Exchange) Repay(ctx context.Context, arg *LendArgument) (*RepayResponse return nil, currency.ErrCurrencyCodeEmpty } if arg.AmountToBorrow <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } var resp *RepayResponse return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/spot-cross-margin-trade/repay", nil, arg, &resp, spotCrossMarginTradeRepayEPL) @@ -2404,7 +2405,7 @@ func (e *Exchange) C2CDepositFunds(ctx context.Context, arg *C2CLendingFundsPara return nil, currency.ErrCurrencyCodeEmpty } if arg.Quantity <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } var resp *C2CLendingFundResponse return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/lending/purchase", nil, &arg, &resp, defaultEPL) @@ -2419,7 +2420,7 @@ func (e *Exchange) C2CRedeemFunds(ctx context.Context, arg *C2CLendingFundsParam return nil, currency.ErrCurrencyCodeEmpty } if arg.Quantity <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } var resp *C2CLendingFundResponse return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/lending/redeem", nil, &arg, &resp, defaultEPL) diff --git a/exchanges/bybit/bybit_test.go b/exchanges/bybit/bybit_test.go index dc5cae92..1537fd0e 100644 --- a/exchanges/bybit/bybit_test.go +++ b/exchanges/bybit/bybit_test.go @@ -19,6 +19,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "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" @@ -771,28 +772,16 @@ func TestGetDeliveryPrice(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - err := e.UpdateOrderExecutionLimits(t.Context(), asset.Futures) - assert.ErrorIs(t, err, asset.ErrNotSupported) - err = e.UpdateOrderExecutionLimits(t.Context(), asset.Options) - assert.NoError(t, err) - err = e.UpdateOrderExecutionLimits(t.Context(), asset.USDCMarginedFutures) - assert.NoError(t, err) - err = e.UpdateOrderExecutionLimits(t.Context(), asset.USDTMarginedFutures) - assert.NoError(t, err) - - err = e.UpdateOrderExecutionLimits(t.Context(), asset.Spot) - assert.NoError(t, err) - availablePairs, err := e.GetAvailablePairs(asset.Spot) - if err != nil { - t.Fatal("Bybit GetAvailablePairs() error", err) - } - for x := range availablePairs { - var limits order.MinMaxLevel - limits, err = e.GetOrderExecutionLimits(asset.Spot, availablePairs[x]) - require.NoError(t, err) - if limits == (order.MinMaxLevel{}) { - t.Fatal("Bybit GetOrderExecutionLimits() error cannot be nil") - } + for _, a := range e.GetAssetTypes(false) { + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + 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") + }) } } @@ -838,7 +827,7 @@ func TestPlaceOrder(t *testing.T) { Side: "buy", OrderType: "limit", }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.PlaceOrder(ctx, &PlaceOrderParams{ Category: "spot", @@ -2297,7 +2286,7 @@ func TestWithdrawCurrency(t *testing.T) { require.ErrorIs(t, err, errMissingAddressInfo) _, err = e.WithdrawCurrency(t.Context(), &WithdrawalParam{Coin: currency.LTC, Chain: "LTC", Address: "234234234"}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.WithdrawCurrency(t.Context(), &WithdrawalParam{Coin: currency.LTC, Chain: "LTC", Address: "234234234", Amount: 123}) if err != nil { @@ -2664,7 +2653,7 @@ func TestBorrow(t *testing.T) { assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.Borrow(t.Context(), &LendArgument{Coin: currency.BTC}) - assert.ErrorIs(t, err, order.ErrAmountBelowMin) + assert.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.Borrow(t.Context(), &LendArgument{Coin: currency.BTC, AmountToBorrow: 0.1}) if err != nil { @@ -2685,7 +2674,7 @@ func TestRepay(t *testing.T) { assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.Repay(t.Context(), &LendArgument{Coin: currency.BTC}) - assert.ErrorIs(t, err, order.ErrAmountBelowMin) + assert.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.Repay(t.Context(), &LendArgument{Coin: currency.BTC, AmountToBorrow: 0.1}) if err != nil { @@ -2806,7 +2795,7 @@ func TestC2CDepositFunds(t *testing.T) { assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.C2CDepositFunds(t.Context(), &C2CLendingFundsParams{Coin: currency.BTC}) - assert.ErrorIs(t, err, order.ErrAmountBelowMin) + assert.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.C2CDepositFunds(t.Context(), &C2CLendingFundsParams{Coin: currency.BTC, Quantity: 1232}) if err != nil { @@ -2827,7 +2816,7 @@ func TestC2CRedeemFunds(t *testing.T) { assert.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.C2CRedeemFunds(t.Context(), &C2CLendingFundsParams{Coin: currency.BTC}) - assert.ErrorIs(t, err, order.ErrAmountBelowMin) + assert.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.C2CRedeemFunds(t.Context(), &C2CLendingFundsParams{Coin: currency.BTC, Quantity: 1232}) if err != nil { diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index 2af0ea4f..ca99aad5 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -13,6 +13,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" "github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -434,7 +435,7 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren } } default: - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } pairs = make(currency.Pairs, 0, len(allPairs)) var filterSymbol string @@ -1675,7 +1676,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) default: return fmt.Errorf("%s %w", a, asset.ErrNotSupported) } - limits := make([]order.MinMaxLevel, 0, len(allInstrumentsInfo.List)) + l := make([]limits.MinMaxLevel, 0, len(allInstrumentsInfo.List)) for x := range allInstrumentsInfo.List { if allInstrumentsInfo.List[x].Status != "Trading" { continue @@ -1686,9 +1687,8 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) log.Warnf(log.ExchangeSys, "%s unable to load limits for %s %v, pair data missing", e.Name, a, symbol) continue } - limits = append(limits, order.MinMaxLevel{ - Asset: a, - Pair: pair, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, pair), MinimumBaseAmount: allInstrumentsInfo.List[x].LotSizeFilter.MinOrderQty.Float64(), MaximumBaseAmount: allInstrumentsInfo.List[x].LotSizeFilter.MaxOrderQty.Float64(), MinPrice: allInstrumentsInfo.List[x].PriceFilter.MinPrice.Float64(), @@ -1700,7 +1700,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) MaximumQuoteAmount: allInstrumentsInfo.List[x].LotSizeFilter.MaxOrderQty.Float64() * allInstrumentsInfo.List[x].PriceFilter.MaxPrice.Float64(), }) } - return e.LoadLimits(limits) + return limits.Load(l) } // SetLeverage sets the account's initial leverage for the asset type and pair @@ -2105,12 +2105,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } return []futures.OpenInterest{{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Asset: k[0].Asset, - Base: k[0].Base, - Quote: k[0].Quote, - }, + Key: key.NewExchangeAssetPair(e.Name, k[0].Asset, k[0].Pair()), OpenInterest: ticks.List[i].OpenInterest.Float64(), }}, nil } @@ -2141,12 +2136,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } resp = append(resp, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: pair.Base.Item, - Quote: pair.Quote.Item, - Asset: assets[i], - }, + Key: key.NewExchangeAssetPair(e.Name, assets[i], pair), OpenInterest: ticks.List[i].OpenInterest.Float64(), }) } @@ -2205,6 +2195,6 @@ func (e *Exchange) GetCurrencyTradeURL(ctx context.Context, a asset.Item, cp cur case asset.Options: return tradeBaseURL + "trade/option/usdc/" + cp.Base.Upper().String(), nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 2a0ef2ae..1af99202 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -305,7 +305,7 @@ func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, a asset.It return nil, currency.ErrCurrencyPairEmpty } if !e.SupportsAsset(a) { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } err := e.loadInstrumentsIfNotLoaded(ctx) if err != nil { diff --git a/exchanges/deribit/deribit_test.go b/exchanges/deribit/deribit_test.go index d8ab6685..594df380 100644 --- a/exchanges/deribit/deribit_test.go +++ b/exchanges/deribit/deribit_test.go @@ -3822,23 +3822,18 @@ func TestGetLatestFundingRates(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - err := e.UpdateOrderExecutionLimits(t.Context(), asset.Spot) - require.NoErrorf(t, err, "Error fetching %s pairs for test: %v", asset.Spot, err) - instrumentInfo, err := e.GetInstruments(t.Context(), currency.BTC, e.GetAssetKind(asset.Spot), false) - require.NoError(t, err) - require.NotEmpty(t, instrumentInfo, "instrument information must not be empty") - limits, err := e.GetOrderExecutionLimits(asset.Spot, spotTradablePair) - require.NoErrorf(t, err, "Asset: %s Pair: %s Err: %v", asset.Spot, spotTradablePair, err) - var instrumentDetail *InstrumentData - for a := range instrumentInfo { - if instrumentInfo[a].InstrumentName == spotTradablePair.String() { - instrumentDetail = instrumentInfo[a] - break - } + for _, a := range e.GetAssetTypes(false) { + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + 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") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + }) } - require.NotNil(t, instrumentDetail, "instrument required to be found") - require.Equalf(t, instrumentDetail.TickSize, limits.PriceStepIncrementSize, "Asset: %s Pair: %s Expected: %f Got: %f", asset.Spot, spotTradablePair, instrumentDetail.TickSize, limits.MinimumBaseAmount) - assert.Equalf(t, instrumentDetail.MinimumTradeAmount, limits.MinimumBaseAmount, "Pair: %s Expected: %f Got: %f", spotTradablePair, instrumentDetail.MinimumTradeAmount, limits.MinimumBaseAmount) } func TestGetLockedStatus(t *testing.T) { diff --git a/exchanges/deribit/deribit_wrapper.go b/exchanges/deribit/deribit_wrapper.go index 2355f5bb..28cc79ea 100644 --- a/exchanges/deribit/deribit_wrapper.go +++ b/exchanges/deribit/deribit_wrapper.go @@ -14,6 +14,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" "github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -1220,13 +1221,13 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if !e.SupportsAsset(a) { return fmt.Errorf("%s: %w - %v", e.Name, asset.ErrNotSupported, a) } - for _, x := range baseCurrencies { + for _, bc := range baseCurrencies { var instrumentsData []*InstrumentData var err error if e.Websocket.IsConnected() { - instrumentsData, err = e.WSRetrieveInstrumentsData(ctx, currency.NewCode(x), e.GetAssetKind(a), false) + instrumentsData, err = e.WSRetrieveInstrumentsData(ctx, currency.NewCode(bc), e.GetAssetKind(a), false) } else { - instrumentsData, err = e.GetInstruments(ctx, currency.NewCode(x), e.GetAssetKind(a), false) + instrumentsData, err = e.GetInstruments(ctx, currency.NewCode(bc), e.GetAssetKind(a), false) } if err != nil { return err @@ -1234,21 +1235,20 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) continue } - limits := make([]order.MinMaxLevel, len(instrumentsData)) - for x, inst := range instrumentsData { + l := make([]limits.MinMaxLevel, len(instrumentsData)) + for i, inst := range instrumentsData { var pair currency.Pair pair, err = currency.NewPairFromString(inst.InstrumentName) if err != nil { return err } - limits[x] = order.MinMaxLevel{ - Pair: pair, - Asset: a, + l[i] = limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, pair), PriceStepIncrementSize: inst.TickSize, MinimumBaseAmount: inst.MinimumTradeAmount, } } - err = e.LoadLimits(limits) + err = limits.Load(l) if err != nil { return err } @@ -1364,12 +1364,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f } for a := range oi { result = append(result, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: k[i].Base, - Quote: k[i].Quote, - Asset: k[i].Asset, - }, + Key: key.NewExchangeAssetPair(e.Name, k[i].Asset, k[i].Pair()), OpenInterest: oi[a].OpenInterest, }) break @@ -1413,7 +1408,7 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre case asset.OptionCombo: return tradeBaseURL + tradeOptionsCombo + cp.Base.Upper().String(), nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index e638a15d..d94b30b2 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -19,6 +19,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" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -51,11 +52,8 @@ const ( DefaultWebsocketOrderbookBufferLimit = 5 ) -// Public Errors -var ( - ErrExchangeNameIsEmpty = errors.New("exchange name is empty") - ErrSymbolCannotBeMatched = errors.New("symbol cannot be matched") -) +// ErrSymbolCannotBeMatched returned on symbol matching failure +var ErrSymbolCannotBeMatched = errors.New("symbol cannot be matched") var ( errEndpointStringNotFound = errors.New("endpoint string not found") @@ -1309,12 +1307,7 @@ func (b *Base) GetCachedOpenInterest(_ context.Context, k ...key.PairAsset) ([]f continue } resp = append(resp, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: b.Name, - Base: ticks[i].Pair.Base.Item, - Quote: ticks[i].Pair.Quote.Item, - Asset: ticks[i].AssetType, - }, + Key: key.NewExchangeAssetPair(b.Name, ticks[i].AssetType, ticks[i].Pair), OpenInterest: ticks[i].OpenInterest, }) } @@ -1330,12 +1323,7 @@ func (b *Base) GetCachedOpenInterest(_ context.Context, k ...key.PairAsset) ([]f return nil, err } resp[i] = futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: b.Name, - Base: t.Pair.Base.Item, - Quote: t.Pair.Quote.Item, - Asset: t.AssetType, - }, + Key: key.NewExchangeAssetPair(b.Name, t.AssetType, t.Pair), OpenInterest: t.OpenInterest, } } @@ -1947,6 +1935,21 @@ func (b *Base) GetCachedAccountInfo(ctx context.Context, assetType asset.Item) ( return account.GetHoldings(b.Name, creds, assetType) } +// GetOrderExecutionLimits returns a limit based on the exchange, asset and pair from storage +func (b *Base) GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (limits.MinMaxLevel, error) { + return limits.GetOrderExecutionLimits(key.NewExchangeAssetPair(b.Name, a, cp)) +} + +// CheckOrderExecutionLimits checks if the order execution limits are within the defined limits from storage +func (b *Base) CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, amount, price float64, orderType order.Type) error { + return limits.CheckOrderExecutionLimits( + key.NewExchangeAssetPair(b.Name, a, cp), + amount, + price, + orderType, + ) +} + // WebsocketSubmitOrder submits an order to the exchange via a websocket connection func (*Base) WebsocketSubmitOrder(context.Context, *order.Submit) (*order.SubmitResponse, error) { return nil, common.ErrFunctionNotSupported diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index 930b5e7b..9b40642a 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -13,6 +13,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" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -420,29 +421,18 @@ func TestSetCurrencyPairFormat(t *testing.T) { err = b.CurrencyPairs.Store(asset.Spot, ¤cy.PairStore{ ConfigFormat: ¤cy.PairFormat{Delimiter: "~"}, }) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err, "Store must not error") err = b.CurrencyPairs.Store(asset.Futures, ¤cy.PairStore{ ConfigFormat: ¤cy.PairFormat{Delimiter: ":)"}, }) - if err != nil { - t.Fatal(err) - } - err = b.SetCurrencyPairFormat() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err, "Store must not error") + require.NoError(t, b.SetCurrencyPairFormat(), "SetCurrencyPairFormat must not error") spot, err = b.GetPairFormat(asset.Spot, false) - if err != nil { - t.Fatal(err) - } - if spot.Delimiter != "~" { - t.Error("incorrect pair format delimiter") - } + require.NoError(t, err, "GetPairFormat must not error") + assert.Equal(t, "~", spot.Delimiter, "GetPairFormat should return a format with correct delimiter") f, err := b.GetPairFormat(asset.Futures, false) require.NoError(t, err, "GetPairFormat must not error") - assert.Equal(t, ":)", f.Delimiter, "Delimiter should be set correctly") + assert.Equal(t, ":)", f.Delimiter, "GetPairFormat should return a format with correct delimiter") } func TestLoadConfigPairs(t *testing.T) { @@ -2659,12 +2649,7 @@ type FakeBase struct{ Base } func (f *FakeBase) GetOpenInterest(context.Context, ...key.PairAsset) ([]futures.OpenInterest, error) { return []futures.OpenInterest{ { - Key: key.ExchangePairAsset{ - Exchange: f.Name, - Base: currency.BTC.Item, - Quote: currency.BONK.Item, - Asset: asset.Futures, - }, + Key: key.NewExchangeAssetPair(f.Name, asset.Futures, currency.NewPair(currency.BTC, currency.BONK)), OpenInterest: 1337, }, }, nil @@ -2841,6 +2826,44 @@ func TestSetConfigPairFormatFromExchange(t *testing.T) { assert.Equal(t, "🦥", b.Config.CurrencyPairs.Pairs[asset.Spot].RequestFormat.Delimiter, "RequestFormat should be correct and kinda lazy") } +func TestGetOrderExecutionLimits(t *testing.T) { + t.Parallel() + exch := Base{ + Name: "TESTNAME", + } + cp := currency.NewBTCUSDT() + k := key.NewExchangeAssetPair("TESTNAME", asset.Spread, cp) + l := limits.MinMaxLevel{ + Key: k, + MaxPrice: 1337, + } + err := limits.Load([]limits.MinMaxLevel{l}) + require.NoError(t, err, "Load must not error") + + _, err = exch.GetOrderExecutionLimits(asset.Spread, cp) + require.NoError(t, err) +} + +func TestCheckOrderExecutionLimits(t *testing.T) { + t.Parallel() + exch := Base{ + Name: "TESTNAME", + } + cp := currency.NewBTCUSDT() + k := key.NewExchangeAssetPair("TESTNAME", asset.Spread, cp) + l := limits.MinMaxLevel{ + Key: k, + MaxPrice: 1337, + } + err := limits.Load([]limits.MinMaxLevel{ + l, + }) + require.NoError(t, err, "Load must not error") + + err = exch.CheckOrderExecutionLimits(asset.Spread, cp, 1338.0, 1.0, order.Market) + require.NoError(t, err, "CheckOrderExecutionLimits must not error") +} + func TestWebsocketSubmitOrder(t *testing.T) { _, err := (&Base{}).WebsocketSubmitOrder(t.Context(), nil) require.ErrorIs(t, err, common.ErrFunctionNotSupported) diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index 40c5efbd..fec84050 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -11,7 +11,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/currencystate" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" @@ -255,7 +254,6 @@ type Base struct { // increasing potential update speed but decreasing confidence in orderbook // integrity. ValidateOrderbook bool - order.ExecutionLimits AssetWebsocketSupport *currencystate.States diff --git a/exchanges/futures/futures.go b/exchanges/futures/futures.go index a9d4da9e..3dab2cc3 100644 --- a/exchanges/futures/futures.go +++ b/exchanges/futures/futures.go @@ -21,7 +21,7 @@ import ( // to track futures orders func SetupPositionController() PositionController { return PositionController{ - multiPositionTrackers: make(map[key.ExchangePairAsset]*MultiPositionTracker), + multiPositionTrackers: make(map[key.ExchangeAssetPair]*MultiPositionTracker), } } @@ -42,12 +42,7 @@ func (c *PositionController) TrackNewOrder(d *order.Detail) error { } c.m.Lock() defer c.m.Unlock() - exchMap, ok := c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: d.Exchange, - Base: d.Pair.Base.Item, - Quote: d.Pair.Quote.Item, - Asset: d.AssetType, - }] + exchMap, ok := c.multiPositionTrackers[key.NewExchangeAssetPair(d.Exchange, d.AssetType, d.Pair)] if !ok { exchMap, err = SetupMultiPositionTracker(&MultiPositionTrackerSetup{ Exchange: d.Exchange, @@ -58,12 +53,7 @@ func (c *PositionController) TrackNewOrder(d *order.Detail) error { if err != nil { return err } - c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: d.Exchange, - Base: d.Pair.Base.Item, - Quote: d.Pair.Quote.Item, - Asset: d.AssetType, - }] = exchMap + c.multiPositionTrackers[key.NewExchangeAssetPair(d.Exchange, d.AssetType, d.Pair)] = exchMap } err = exchMap.TrackNewOrder(d) if err != nil { @@ -87,12 +77,7 @@ func (c *PositionController) SetCollateralCurrency(exch string, item asset.Item, c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: exch, - Base: pair.Base.Item, - Quote: pair.Quote.Item, - Asset: item, - }] + tracker := c.multiPositionTrackers[key.NewExchangeAssetPair(exch, item, pair)] if tracker == nil { return fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair) } @@ -120,12 +105,7 @@ func (c *PositionController) GetPositionsForExchange(exch string, item asset.Ite } c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: exch, - Base: pair.Base.Item, - Quote: pair.Quote.Item, - Asset: item, - }] + tracker := c.multiPositionTrackers[key.NewExchangeAssetPair(exch, item, pair)] if tracker == nil { return nil, fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair) } @@ -148,12 +128,7 @@ func (c *PositionController) TrackFundingDetails(d *fundingrate.HistoricalRates) } c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: d.Exchange, - Base: d.Pair.Base.Item, - Quote: d.Pair.Quote.Item, - Asset: d.Asset, - }] + tracker := c.multiPositionTrackers[key.NewExchangeAssetPair(d.Exchange, d.Asset, d.Pair)] if tracker == nil { return fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, d.Exchange, d.Asset, d.Pair) } @@ -188,12 +163,7 @@ func (c *PositionController) GetOpenPosition(exch string, item asset.Item, pair } c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: exch, - Base: pair.Base.Item, - Quote: pair.Quote.Item, - Asset: item, - }] + tracker := c.multiPositionTrackers[key.NewExchangeAssetPair(exch, item, pair)] if tracker == nil { return nil, fmt.Errorf("%w no open position for %v %v %v", ErrPositionNotFound, exch, item, pair) } @@ -245,12 +215,7 @@ func (c *PositionController) UpdateOpenPositionUnrealisedPNL(exch string, item a } c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: exch, - Base: pair.Base.Item, - Quote: pair.Quote.Item, - Asset: item, - }] + tracker := c.multiPositionTrackers[key.NewExchangeAssetPair(exch, item, pair)] if tracker == nil { return decimal.Zero, fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionNotFound) } @@ -342,12 +307,7 @@ func (c *PositionController) ClearPositionsForExchange(exch string, item asset.I c.m.Lock() defer c.m.Unlock() - tracker := c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: exch, - Base: pair.Base.Item, - Quote: pair.Quote.Item, - Asset: item, - }] + tracker := c.multiPositionTrackers[key.NewExchangeAssetPair(exch, item, pair)] if tracker == nil { return fmt.Errorf("%v %v %v %w", exch, item, pair, ErrPositionNotFound) } @@ -365,12 +325,7 @@ func (c *PositionController) ClearPositionsForExchange(exch string, item asset.I if err != nil { return err } - c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: exch, - Base: pair.Base.Item, - Quote: pair.Quote.Item, - Asset: item, - }] = newMPT + c.multiPositionTrackers[key.NewExchangeAssetPair(exch, item, pair)] = newMPT return nil } diff --git a/exchanges/futures/futures_test.go b/exchanges/futures/futures_test.go index 9c96bb74..d160b3fb 100644 --- a/exchanges/futures/futures_test.go +++ b/exchanges/futures/futures_test.go @@ -532,34 +532,19 @@ func TestGetPositionsForExchange(t *testing.T) { if len(pos) != 0 { t.Error("expected zero") } - c.multiPositionTrackers = make(map[key.ExchangePairAsset]*MultiPositionTracker) - c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: testExchange, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - }] = nil + c.multiPositionTrackers = make(map[key.ExchangeAssetPair]*MultiPositionTracker) + c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = nil _, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) - assert.ErrorIs(t, err, ErrPositionNotFound) + require.ErrorIs(t, err, ErrPositionNotFound, "GetPositionsForExchange must return ErrPositionNotFound") - c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: testExchange, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - }] = nil + c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = nil _, err = c.GetPositionsForExchange(testExchange, asset.Futures, p) assert.ErrorIs(t, err, ErrPositionNotFound) _, err = c.GetPositionsForExchange(testExchange, asset.Spot, p) assert.ErrorIs(t, err, ErrNotFuturesAsset) - c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: testExchange, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - }] = &MultiPositionTracker{ + c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = &MultiPositionTracker{ exchange: testExchange, } @@ -569,12 +554,7 @@ func TestGetPositionsForExchange(t *testing.T) { if len(pos) != 0 { t.Fatal("expected zero") } - c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: testExchange, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - }] = &MultiPositionTracker{ + c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = &MultiPositionTracker{ exchange: testExchange, positions: []*PositionTracker{ { @@ -606,19 +586,14 @@ func TestClearPositionsForExchange(t *testing.T) { err = c.ClearPositionsForExchange(testExchange, asset.Futures, p) assert.ErrorIs(t, err, ErrPositionNotFound) - c.multiPositionTrackers = make(map[key.ExchangePairAsset]*MultiPositionTracker) + c.multiPositionTrackers = make(map[key.ExchangeAssetPair]*MultiPositionTracker) err = c.ClearPositionsForExchange(testExchange, asset.Futures, p) assert.ErrorIs(t, err, ErrPositionNotFound) err = c.ClearPositionsForExchange(testExchange, asset.Spot, p) assert.ErrorIs(t, err, ErrNotFuturesAsset) - c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: testExchange, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - }] = &MultiPositionTracker{ + c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)] = &MultiPositionTracker{ exchange: testExchange, underlying: currency.DOGE, positions: []*PositionTracker{ @@ -628,14 +603,8 @@ func TestClearPositionsForExchange(t *testing.T) { }, } err = c.ClearPositionsForExchange(testExchange, asset.Futures, p) - assert.NoError(t, err) - - if len(c.multiPositionTrackers[key.ExchangePairAsset{ - Exchange: testExchange, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - }].positions) != 0 { + require.NoError(t, err, "ClearPositionsForExchange must not error") + if len(c.multiPositionTrackers[key.NewExchangeAssetPair(testExchange, asset.Futures, p)].positions) != 0 { t.Fatal("expected 0") } c = nil @@ -840,20 +809,14 @@ func TestSetCollateralCurrency(t *testing.T) { assert.ErrorIs(t, err, ErrNotFuturesAsset) p := currency.NewBTCUSDT() - pc.multiPositionTrackers = make(map[key.ExchangePairAsset]*MultiPositionTracker) + pc.multiPositionTrackers = make(map[key.ExchangeAssetPair]*MultiPositionTracker) err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) require.ErrorIs(t, err, ErrPositionNotFound) err = pc.SetCollateralCurrency("hi", asset.Futures, p, currency.DOGE) require.ErrorIs(t, err, ErrPositionNotFound) - mapKey := key.ExchangePairAsset{ - Exchange: "hi", - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - } - + mapKey := key.NewExchangeAssetPair("hi", asset.Futures, p) pc.multiPositionTrackers[mapKey] = &MultiPositionTracker{ exchange: "hi", asset: asset.Futures, @@ -904,13 +867,7 @@ func TestMPTUpdateOpenPositionUnrealisedPNL(t *testing.T) { }) require.NoError(t, err) - mapKey := key.ExchangePairAsset{ - Exchange: "hi", - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - } - + mapKey := key.NewExchangeAssetPair("hi", asset.Futures, p) result, err := pc.multiPositionTrackers[mapKey].UpdateOpenPositionUnrealisedPNL(1337, time.Now()) require.NoError(t, err) @@ -1125,13 +1082,7 @@ func TestPCTrackFundingDetails(t *testing.T) { }, } - mapKey := key.ExchangePairAsset{ - Exchange: testExchange, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - } - + mapKey := key.NewExchangeAssetPair(testExchange, asset.Futures, p) pc.multiPositionTrackers[mapKey].orderPositions["lol"].openingDate = tn.Add(-time.Hour) pc.multiPositionTrackers[mapKey].orderPositions["lol"].lastUpdated = tn err = pc.TrackFundingDetails(rates) diff --git a/exchanges/futures/futures_types.go b/exchanges/futures/futures_types.go index 770c53e6..2c751dcb 100644 --- a/exchanges/futures/futures_types.go +++ b/exchanges/futures/futures_types.go @@ -86,7 +86,7 @@ type TotalCollateralResponse struct { // the position controller and its all tracked happily type PositionController struct { m sync.Mutex - multiPositionTrackers map[key.ExchangePairAsset]*MultiPositionTracker + multiPositionTrackers map[key.ExchangeAssetPair]*MultiPositionTracker updated time.Time } @@ -201,7 +201,7 @@ type CollateralCalculator struct { // OpenInterest holds open interest data for an exchange pair asset type OpenInterest struct { - Key key.ExchangePairAsset + Key key.ExchangeAssetPair OpenInterest float64 } diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index 102a68f1..b70ebb70 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -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 diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 16df851c..ca2a8781 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -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") + } + }) } } diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index f14f2494..aeff59fa 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -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"` diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 2b3dc35a..4f3a524b 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -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) } } diff --git a/exchanges/gemini/gemini_test.go b/exchanges/gemini/gemini_test.go index 83d26419..3dd0fe3c 100644 --- a/exchanges/gemini/gemini_test.go +++ b/exchanges/gemini/gemini_test.go @@ -1258,26 +1258,18 @@ func TestGetSymbolDetails(t *testing.T) { } } -func TestSetExchangeOrderExecutionLimits(t *testing.T) { +func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - err := e.UpdateOrderExecutionLimits(t.Context(), asset.Spot) - if err != nil { - t.Fatal(err) - } - err = e.UpdateOrderExecutionLimits(t.Context(), asset.Futures) - assert.ErrorIs(t, err, asset.ErrNotSupported) - - availPairs, err := e.GetAvailablePairs(asset.Spot) - require.NoError(t, err) - for x := range availPairs { - var limit order.MinMaxLevel - limit, err = e.GetOrderExecutionLimits(asset.Spot, availPairs[x]) - if err != nil { - t.Fatal(err, availPairs[x]) - } - if limit == (order.MinMaxLevel{}) { - t.Fatal("exchange limit should be loaded") - } + for _, a := range e.GetAssetTypes(false) { + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(a, false) + 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") + }) } } diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index aa09bd19..dd50eb9c 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -11,8 +11,10 @@ import ( "time" "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/exchange/order/limits" "github.com/thrasher-corp/gocryptotrader/exchange/websocket" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/account" @@ -783,13 +785,13 @@ func (e *Exchange) GetFuturesContractDetails(context.Context, asset.Item) ([]fut // UpdateOrderExecutionLimits sets exchange executions for a required asset type func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { if a != asset.Spot { - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } details, err := e.GetSymbolDetails(ctx, "all") if err != nil { return fmt.Errorf("cannot update exchange execution limits: %w", err) } - resp := make([]order.MinMaxLevel, 0, len(details)) + resp := make([]limits.MinMaxLevel, 0, len(details)) for i := range details { status := strings.ToLower(details[i].Status) if status != "open" && status != "limit_only" { @@ -799,15 +801,14 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if err != nil { return err } - resp = append(resp, order.MinMaxLevel{ - Pair: cp, - Asset: a, + resp = append(resp, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, cp), AmountStepIncrementSize: details[i].TickSize, MinimumBaseAmount: details[i].MinOrderSize.Float64(), QuoteStepIncrementSize: details[i].QuoteIncrement, }) } - return e.LoadLimits(resp) + return limits.Load(resp) } // GetLatestFundingRates returns the latest funding rates data diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index eb01e8ad..8ee6066a 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -879,6 +879,6 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre case asset.Futures: return tradeBaseURL + tradeFutures + cp.Lower().String(), nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 464a8175..5ab68c75 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -238,7 +238,7 @@ func (e *Exchange) Setup(exch *config.Exchange) error { // FetchTradablePairs returns a list of the exchanges tradable pairs func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { if !e.SupportsAsset(a) { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } var pairs []currency.Pair @@ -458,7 +458,7 @@ func (e *Exchange) UpdateTickers(ctx context.Context, a asset.Item) error { } } default: - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } return errs } @@ -469,7 +469,7 @@ func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, a asset.It return nil, currency.ErrCurrencyPairEmpty } if !e.SupportsAsset(a) { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } switch a { case asset.Spot: @@ -820,7 +820,7 @@ func (e *Exchange) GetAccountFundingHistory(_ context.Context) ([]exchange.Fundi // GetWithdrawalsHistory returns previous withdrawals data func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, a asset.Item) ([]exchange.WithdrawalHistory, error) { if a != asset.Spot { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } withdrawals, err := e.SearchForExistedWithdrawsAndDeposits(ctx, c, "withdraw", "", 0, 500) if err != nil { @@ -2271,12 +2271,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f } return []futures.OpenInterest{ { - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: k[0].Asset, - }, + Key: key.NewExchangeAssetPair(e.Name, k[0].Asset, p), OpenInterest: data.Data[i].Amount, }, }, nil @@ -2297,12 +2292,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f } return []futures.OpenInterest{ { - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: k[0].Asset, - }, + Key: key.NewExchangeAssetPair(e.Name, k[0].Asset, p), OpenInterest: data.Data[i].Amount, }, }, nil @@ -2344,12 +2334,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } resp = append(resp, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }, + Key: key.NewExchangeAssetPair(e.Name, a, p), OpenInterest: allData[i].Amount, }) } @@ -2377,12 +2362,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } resp = append(resp, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }, + Key: key.NewExchangeAssetPair(e.Name, a, p), OpenInterest: data.Data[i].Amount, }) } @@ -2415,6 +2395,6 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre } return tradeBaseURL + tradeCoinMargined + cp.Base.Upper().String(), nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/interfaces.go b/exchanges/interfaces.go index efff3a08..584a6b4e 100644 --- a/exchanges/interfaces.go +++ b/exchanges/interfaces.go @@ -8,6 +8,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" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -84,7 +85,7 @@ type IBotExchange interface { FlushWebsocketChannels() error AuthenticateWebsocket(ctx context.Context) error CanUseAuthenticatedWebsocketEndpoints() bool - GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (order.MinMaxLevel, error) + GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (limits.MinMaxLevel, error) CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType order.Type) error UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error GetCredentials(ctx context.Context) (*account.Credentials, error) diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index c7bcaa7e..bdb71965 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -77,20 +77,24 @@ func TestWrapperGetServerTime(t *testing.T) { assert.WithinRange(t, st, time.Now().Add(-24*time.Hour), time.Now().Add(24*time.Hour), "ServerTime should be within a day of now") } -// TestUpdateOrderExecutionLimits exercises UpdateOrderExecutionLimits and GetOrderExecutionLimits func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - - err := e.UpdateOrderExecutionLimits(t.Context(), asset.Spot) - require.NoError(t, err, "UpdateOrderExecutionLimits must not error") - for _, p := range []currency.Pair{ - currency.NewPair(currency.ETH, currency.USDT), - currency.NewPair(currency.XBT, currency.USDT), - } { - limits, err := e.GetOrderExecutionLimits(asset.Spot, p) - require.NoErrorf(t, err, "%s GetOrderExecutionLimits must not error", p) - assert.Positivef(t, limits.PriceStepIncrementSize, "%s PriceStepIncrementSize should be positive", p) - assert.Positivef(t, limits.MinimumBaseAmount, "%s MinimumBaseAmount should be positive", p) + for _, a := range e.GetAssetTypes(false) { + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + switch a { + case asset.Futures: + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), a), common.ErrNotYetImplemented) + default: + require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(a, false) + 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") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + } + }) } } diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 8f114234..5c4f3089 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -14,6 +14,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" "github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -258,18 +259,17 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) return fmt.Errorf("%s failed to load %s pair execution limits. Err: %s", e.Name, a, err) } - limits := make([]order.MinMaxLevel, 0, len(pairInfo)) + l := make([]limits.MinMaxLevel, 0, len(pairInfo)) for pair, info := range pairInfo { - limits = append(limits, order.MinMaxLevel{ - Asset: a, - Pair: pair, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, pair), PriceStepIncrementSize: info.TickSize, MinimumBaseAmount: info.OrderMinimum, }) } - if err := e.LoadLimits(limits); err != nil { + if err := limits.Load(l); err != nil { return fmt.Errorf("%s Error loading %s exchange limits: %w", e.Name, a, err) } @@ -436,7 +436,7 @@ func (e *Exchange) UpdateTickers(ctx context.Context, a asset.Item) error { } } default: - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } return nil } @@ -1708,12 +1708,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, keys ...key.PairAsset) ( continue } resp = append(resp, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: asset.Futures, - }, + Key: key.NewExchangeAssetPair(e.Name, asset.Futures, p), OpenInterest: futuresTickersData.Tickers[i].OpenInterest, }) } @@ -1734,6 +1729,6 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre cp.Delimiter = currency.UnderscoreDelimiter return tradeFuturesURL + cp.Upper().String(), nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/kucoin/kucoin.go b/exchanges/kucoin/kucoin.go index df18af7a..3b4176e8 100644 --- a/exchanges/kucoin/kucoin.go +++ b/exchanges/kucoin/kucoin.go @@ -19,6 +19,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -313,7 +314,7 @@ func (e *Exchange) PostMarginBorrowOrder(ctx context.Context, arg *MarginBorrowP return nil, errTimeInForceRequired } if arg.Size <= 0 { - return nil, fmt.Errorf("%w , size = %f", order.ErrAmountBelowMin, arg.Size) + return nil, fmt.Errorf("%w , size = %f", limits.ErrAmountBelowMin, arg.Size) } var resp *BorrowAndRepaymentOrderResp return resp, e.SendAuthHTTPRequest(ctx, exchange.RestSpot, postMarginBorrowOrderEPL, http.MethodPost, "/v3/margin/borrow", arg, &resp) @@ -365,7 +366,7 @@ func (e *Exchange) PostRepayment(ctx context.Context, arg *RepayParam) (*BorrowA return nil, currency.ErrCurrencyCodeEmpty } if arg.Size <= 0 { - return nil, fmt.Errorf("%w , size = %f", order.ErrAmountBelowMin, arg.Size) + return nil, fmt.Errorf("%w , size = %f", limits.ErrAmountBelowMin, arg.Size) } var resp *BorrowAndRepaymentOrderResp return resp, e.SendAuthHTTPRequest(ctx, exchange.RestSpot, postMarginRepaymentEPL, http.MethodPost, "/v3/margin/repay", arg, &resp) @@ -509,10 +510,10 @@ func (a *PlaceHFParam) ValidatePlaceOrderParams() error { } a.Side = strings.ToLower(a.Side) if a.Price <= 0 { - return order.ErrPriceBelowMin + return limits.ErrPriceBelowMin } if a.Size <= 0 { - return order.ErrAmountBelowMin + return limits.ErrAmountBelowMin } return nil } @@ -646,7 +647,7 @@ func (e *Exchange) CancelSpecifiedNumberHFOrdersByOrderID(ctx context.Context, o return nil, currency.ErrSymbolStringEmpty } if cancelSize == 0 { - return nil, fmt.Errorf("%w, cancel size is required", order.ErrAmountBelowMin) + return nil, fmt.Errorf("%w, cancel size is required", limits.ErrAmountBelowMin) } params := url.Values{} params.Set("symbol", symbol) @@ -837,13 +838,13 @@ func (e *Exchange) HandlePostOrder(ctx context.Context, arg *SpotOrderParam, pat switch arg.OrderType { case order.Limit.Lower(), "": if arg.Price <= 0 { - return "", fmt.Errorf("%w, price =%.3f", order.ErrPriceBelowMin, arg.Price) + return "", fmt.Errorf("%w, price =%.3f", limits.ErrPriceBelowMin, arg.Price) } if arg.Size <= 0 { - return "", order.ErrAmountBelowMin + return "", limits.ErrAmountBelowMin } if arg.VisibleSize < 0 { - return "", fmt.Errorf("%w, visible size must be non-zero positive value", order.ErrAmountBelowMin) + return "", fmt.Errorf("%w, visible size must be non-zero positive value", limits.ErrAmountBelowMin) } case order.Market.Lower(): if arg.Size == 0 && arg.Funds == 0 { @@ -887,13 +888,13 @@ func (e *Exchange) SendPostMarginOrder(ctx context.Context, arg *MarginOrderPara switch arg.OrderType { case order.Limit.Lower(), "": if arg.Price <= 0 { - return nil, fmt.Errorf("%w, price=%.3f", order.ErrPriceBelowMin, arg.Price) + return nil, fmt.Errorf("%w, price=%.3f", limits.ErrPriceBelowMin, arg.Price) } if arg.Size <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if arg.VisibleSize < 0 { - return nil, fmt.Errorf("%w, visible size must be non-zero positive value", order.ErrAmountBelowMin) + return nil, fmt.Errorf("%w, visible size must be non-zero positive value", limits.ErrAmountBelowMin) } case order.Market.Lower(): sum := arg.Size + arg.Funds @@ -929,10 +930,10 @@ func (e *Exchange) PostBulkOrder(ctx context.Context, symbol string, orderList [ } orderList[i].Side = strings.ToLower(orderList[i].Side) if orderList[i].Price <= 0 { - return nil, order.ErrPriceBelowMin + return nil, limits.ErrPriceBelowMin } if orderList[i].Size <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } } arg := &struct { @@ -1089,7 +1090,7 @@ func (e *Exchange) PostStopOrder(ctx context.Context, clientOID, side, symbol, o if stop != "" { arg["stop"] = stop if stopPrice <= 0 { - return "", fmt.Errorf("%w, stopPrice is required", order.ErrPriceBelowMin) + return "", fmt.Errorf("%w, stopPrice is required", limits.ErrPriceBelowMin) } arg["stopPrice"] = strconv.FormatFloat(stopPrice, 'f', -1, 64) } @@ -1103,11 +1104,11 @@ func (e *Exchange) PostStopOrder(ctx context.Context, clientOID, side, symbol, o switch orderType { case order.Limit.Lower(), "": if price <= 0 { - return "", order.ErrPriceBelowMin + return "", limits.ErrPriceBelowMin } arg["price"] = strconv.FormatFloat(price, 'f', -1, 64) if size <= 0 { - return "", fmt.Errorf("%w, size is required", order.ErrAmountBelowMin) + return "", fmt.Errorf("%w, size is required", limits.ErrAmountBelowMin) } arg["size"] = strconv.FormatFloat(size, 'f', -1, 64) if timeInForce != "" { @@ -1265,16 +1266,16 @@ func (e *Exchange) PlaceOCOOrder(ctx context.Context, arg *OCOOrderParams) (stri } arg.Side = strings.ToLower(arg.Side) if arg.Price <= 0 { - return "", order.ErrPriceBelowMin + return "", limits.ErrPriceBelowMin } if arg.Size <= 0 { - return "", order.ErrAmountBelowMin + return "", limits.ErrAmountBelowMin } if arg.StopPrice <= 0 { - return "", fmt.Errorf("%w stop price = %f", order.ErrPriceBelowMin, arg.StopPrice) + return "", fmt.Errorf("%w stop price = %f", limits.ErrPriceBelowMin, arg.StopPrice) } if arg.LimitPrice <= 0 { - return "", fmt.Errorf("%w limit price = %f", order.ErrPriceBelowMin, arg.LimitPrice) + return "", fmt.Errorf("%w limit price = %f", limits.ErrPriceBelowMin, arg.LimitPrice) } if arg.ClientOrderID == "" { return "", order.ErrClientOrderIDMustBeSet @@ -1405,10 +1406,10 @@ func (e *Exchange) SendPlaceMarginHFOrder(ctx context.Context, arg *PlaceMarginH return nil, currency.ErrCurrencyPairEmpty } if arg.Price <= 0 { - return nil, order.ErrPriceBelowMin + return nil, limits.ErrPriceBelowMin } if arg.Size <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } var resp *MarginHFOrderResponse return resp, e.SendAuthHTTPRequest(ctx, exchange.RestSpot, placeMarginOrderEPL, http.MethodPost, path, arg, &resp) @@ -1927,7 +1928,7 @@ func (e *Exchange) GetUniversalTransfer(ctx context.Context, arg *UniversalTrans return "", order.ErrClientOrderIDMustBeSet } if arg.Amount <= 0 { - return "", order.ErrAmountBelowMin + return "", limits.ErrAmountBelowMin } if arg.FromAccountType == "" { return "", fmt.Errorf("%w, empty fromAccountType", errAccountTypeMissing) @@ -1951,7 +1952,7 @@ func (e *Exchange) TransferMainToSubAccount(ctx context.Context, ccy currency.Co return "", currency.ErrCurrencyCodeEmpty } if amount <= 0 { - return "", order.ErrAmountBelowMin + return "", limits.ErrAmountBelowMin } if direction == "" { return "", errTransferDirectionRequired @@ -1992,7 +1993,7 @@ func (e *Exchange) MakeInnerTransfer(ctx context.Context, amount float64, ccy cu return "", order.ErrClientOrderIDMustBeSet } if amount <= 0 { - return "", order.ErrAmountBelowMin + return "", limits.ErrAmountBelowMin } if paymentAccountType == "" { return "", fmt.Errorf("%w sending account type is required", errAccountTypeMissing) @@ -2029,7 +2030,7 @@ func (e *Exchange) TransferToMainOrTradeAccount(ctx context.Context, arg *FundTr return nil, common.ErrNilPointer } if arg.Amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if arg.Currency.IsEmpty() { return nil, currency.ErrCurrencyCodeEmpty @@ -2047,7 +2048,7 @@ func (e *Exchange) TransferToFuturesAccount(ctx context.Context, arg *FundTransf return nil, common.ErrNilPointer } if arg.Amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if arg.Currency.IsEmpty() { return nil, currency.ErrCurrencyCodeEmpty @@ -2227,7 +2228,7 @@ func (e *Exchange) ApplyWithdrawal(ctx context.Context, ccy currency.Code, addre return "", fmt.Errorf("%w, empty withdrawal address", errAddressRequired) } if amount <= 0 { - return "", order.ErrAmountBelowMin + return "", limits.ErrAmountBelowMin } arg := &struct { Currency currency.Code `json:"currency"` @@ -2311,7 +2312,7 @@ func (e *Exchange) MarginLendingSubscription(ctx context.Context, ccy currency.C return nil, currency.ErrCurrencyCodeEmpty } if size <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if interestRate <= 0 { return nil, errMissingInterestRate @@ -2335,7 +2336,7 @@ func (e *Exchange) Redemption(ctx context.Context, ccy currency.Code, size float return nil, currency.ErrCurrencyCodeEmpty } if size <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if purchaseOrderNo == "" { return nil, errMissingPurchaseOrderNumber @@ -2603,7 +2604,7 @@ func (e *Exchange) SubscribeToEarnFixedIncomeProduct(ctx context.Context, produc return nil, errProductIDMissing } if amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if accountType == "" { return nil, errAccountTypeMissing @@ -2626,7 +2627,7 @@ func (e *Exchange) RedeemByEarnHoldingID(ctx context.Context, orderID, fromAccou return nil, order.ErrOrderIDNotSet } if amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } params := url.Values{} params.Set("orderId", orderID) diff --git a/exchanges/kucoin/kucoin_futures.go b/exchanges/kucoin/kucoin_futures.go index 23fc6bdd..40494abb 100644 --- a/exchanges/kucoin/kucoin_futures.go +++ b/exchanges/kucoin/kucoin_futures.go @@ -13,6 +13,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -372,23 +373,23 @@ func (e *Exchange) FillFuturesPostOrderArgumentFilter(arg *FuturesOrderParam) er return errInvalidStopPriceType } if arg.StopPrice <= 0 { - return fmt.Errorf("%w, stopPrice is required", order.ErrPriceBelowMin) + return fmt.Errorf("%w, stopPrice is required", limits.ErrPriceBelowMin) } } switch arg.OrderType { case "limit", "": if arg.Price <= 0 { - return fmt.Errorf("%w %f", order.ErrPriceBelowMin, arg.Price) + return fmt.Errorf("%w %f", limits.ErrPriceBelowMin, arg.Price) } if arg.Size <= 0 { - return fmt.Errorf("%w, must be non-zero positive value", order.ErrAmountBelowMin) + return fmt.Errorf("%w, must be non-zero positive value", limits.ErrAmountBelowMin) } if arg.VisibleSize < 0 { - return fmt.Errorf("%w, visible size must be non-zero positive value", order.ErrAmountBelowMin) + return fmt.Errorf("%w, visible size must be non-zero positive value", limits.ErrAmountBelowMin) } case "market": if arg.Size <= 0 { - return fmt.Errorf("%w, market size must be > 0", order.ErrAmountBelowMin) + return fmt.Errorf("%w, market size must be > 0", limits.ErrAmountBelowMin) } default: return fmt.Errorf("%w, order type= %s", order.ErrTypeIsInvalid, arg.OrderType) @@ -632,7 +633,7 @@ func (e *Exchange) RemoveMarginManually(ctx context.Context, arg *WithdrawMargin return nil, currency.ErrSymbolStringEmpty } if arg.WithdrawAmount <= 0 { - return nil, fmt.Errorf("%w, withdrawAmount must be greater than 0", order.ErrAmountBelowMin) + return nil, fmt.Errorf("%w, withdrawAmount must be greater than 0", limits.ErrAmountBelowMin) } var resp *MarginRemovingResponse return resp, e.SendAuthHTTPRequest(ctx, exchange.RestSpot, removeMarginManuallyEPL, http.MethodPost, "/v1/margin/withdrawMargin", arg, &resp) @@ -767,7 +768,7 @@ func (e *Exchange) CreateFuturesSubAccountAPIKey(ctx context.Context, ipWhitelis // TransferFuturesFundsToMainAccount helps in transferring funds from futures to main/trade account func (e *Exchange) TransferFuturesFundsToMainAccount(ctx context.Context, amount float64, ccy currency.Code, recAccountType string) (*TransferRes, error) { if amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if ccy.IsEmpty() { return nil, currency.ErrCurrencyCodeEmpty @@ -786,7 +787,7 @@ func (e *Exchange) TransferFuturesFundsToMainAccount(ctx context.Context, amount // TransferFundsToFuturesAccount helps in transferring funds from payee account to futures account func (e *Exchange) TransferFundsToFuturesAccount(ctx context.Context, amount float64, ccy currency.Code, payAccountType string) error { if amount <= 0 { - return order.ErrAmountBelowMin + return limits.ErrAmountBelowMin } if ccy.IsEmpty() { return currency.ErrCurrencyCodeEmpty @@ -885,7 +886,7 @@ func (e *Exchange) GetMaximumOpenPositionSize(ctx context.Context, symbol string return nil, currency.ErrSymbolStringEmpty } if price <= 0 { - return nil, order.ErrPriceBelowMin + return nil, limits.ErrPriceBelowMin } if leverage <= 0 { return nil, fmt.Errorf("%w, leverage is required", errInvalidLeverage) diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index 69b5c9b7..aa425799 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -311,7 +312,7 @@ func TestPostBorrowOrder(t *testing.T) { TimeInForce: "FOK", Size: 0, }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.PostMarginBorrowOrder(t.Context(), &MarginBorrowParam{ @@ -342,7 +343,7 @@ func TestPostRepayment(t *testing.T) { _, err = e.PostRepayment(t.Context(), &RepayParam{Size: 0.05}) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.PostRepayment(t.Context(), &RepayParam{Currency: currency.ETH}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.PostRepayment(t.Context(), &RepayParam{ @@ -444,12 +445,12 @@ func TestPostOrder(t *testing.T) { Symbol: spotTradablePair, OrderType: "limit", Size: 0.1, }) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) _, err = e.PostOrder(t.Context(), &SpotOrderParam{ ClientOrderID: customID.String(), Symbol: spotTradablePair, Side: "buy", OrderType: "limit", Price: 234565, }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.PostOrder(t.Context(), &SpotOrderParam{ @@ -492,12 +493,12 @@ func TestPostOrderTest(t *testing.T) { Symbol: spotTradablePair, OrderType: "limit", Size: 0.1, }) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) _, err = e.PostOrderTest(t.Context(), &SpotOrderParam{ ClientOrderID: customID.String(), Symbol: spotTradablePair, Side: "buy", OrderType: "limit", Price: 234565, }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e) result, err := e.PostOrderTest(t.Context(), &SpotOrderParam{ @@ -546,21 +547,21 @@ func TestHandlePostOrder(t *testing.T) { Symbol: spotTradablePair, OrderType: "limit", Size: 0.1, }, "") - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) _, err = e.HandlePostOrder(t.Context(), &SpotOrderParam{ ClientOrderID: customID.String(), Side: "buy", Symbol: spotTradablePair, OrderType: "limit", Size: 0, Price: 1000, }, "") - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.HandlePostOrder(t.Context(), &SpotOrderParam{ ClientOrderID: customID.String(), Side: "buy", Symbol: spotTradablePair, OrderType: "limit", Size: .1, Price: 1000, VisibleSize: -1, }, "") - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.HandlePostOrder(t.Context(), &SpotOrderParam{ ClientOrderID: customID.String(), Symbol: spotTradablePair, Side: "buy", @@ -591,12 +592,12 @@ func TestPostMarginOrder(t *testing.T) { Symbol: marginTradablePair, OrderType: "limit", Size: 0.1, }) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) _, err = e.PostMarginOrder(t.Context(), &MarginOrderParam{ ClientOrderID: "5bd6e9286d99522a52e458de", Symbol: marginTradablePair, Side: "buy", OrderType: "limit", Price: 234565, }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) // default order type is limit and margin mode is cross @@ -643,12 +644,12 @@ func TestPostMarginOrderTest(t *testing.T) { Symbol: marginTradablePair, OrderType: "limit", Size: 0.1, }) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) _, err = e.PostMarginOrderTest(t.Context(), &MarginOrderParam{ ClientOrderID: "5bd6e9286d99522a52e458de", Symbol: marginTradablePair, Side: "buy", OrderType: "limit", Price: 234565, }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e) // default order type is limit and margin mode is cross @@ -693,11 +694,11 @@ func TestPostBulkOrder(t *testing.T) { arg.Side = "Sell" _, err = e.PostBulkOrder(t.Context(), spotTradablePair.String(), []OrderRequest{arg}) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.Price = 1000 _, err = e.PostBulkOrder(t.Context(), spotTradablePair.String(), []OrderRequest{arg}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) _, err = e.PostBulkOrder(t.Context(), spotTradablePair.String(), []OrderRequest{ @@ -1086,7 +1087,7 @@ func TestGetUniversalTransfer(t *testing.T) { arg.ClientSuppliedOrderID = "64ccc0f164781800010d8c09" _, err = e.GetUniversalTransfer(t.Context(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Amount = 1 _, err = e.GetUniversalTransfer(t.Context(), arg) @@ -1132,7 +1133,7 @@ func TestTransferMainToSubAccount(t *testing.T) { _, err = e.TransferMainToSubAccount(t.Context(), currency.BTC, 1, "", "OUT", "", "", "5caefba7d9575a0688f83c45") require.ErrorIs(t, err, order.ErrClientOrderIDMustBeSet) _, err = e.TransferMainToSubAccount(t.Context(), currency.BTC, 0, "62fcd1969474ea0001fd20e4", "OUT", "", "", "5caefba7d9575a0688f83c45") - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.TransferMainToSubAccount(t.Context(), currency.BTC, 1, "62fcd1969474ea0001fd20e4", "", "", "", "5caefba7d9575a0688f83c45") require.ErrorIs(t, err, errTransferDirectionRequired) _, err = e.TransferMainToSubAccount(t.Context(), currency.BTC, 1, "62fcd1969474ea0001fd20e4", "OUT", "", "", "") @@ -1151,7 +1152,7 @@ func TestMakeInnerTransfer(t *testing.T) { _, err = e.MakeInnerTransfer(t.Context(), 0, currency.USDT, "", "trade", "main", "1", "") require.ErrorIs(t, err, order.ErrClientOrderIDMustBeSet) _, err = e.MakeInnerTransfer(t.Context(), 0, currency.USDT, "62fcd1969474ea0001fd20e4", "", "main", "", "") - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.MakeInnerTransfer(t.Context(), 1, currency.USDT, "62fcd1969474ea0001fd20e4", "", "main", "", "") require.ErrorIs(t, err, errAccountTypeMissing) _, err = e.MakeInnerTransfer(t.Context(), 5, currency.USDT, "62fcd1969474ea0001fd20e4", "margin_hf", "", "", "") @@ -1168,7 +1169,7 @@ func TestTransferToMainOrTradeAccount(t *testing.T) { _, err := e.TransferToMainOrTradeAccount(t.Context(), &FundTransferFuturesParam{}) require.ErrorIs(t, err, common.ErrNilPointer) _, err = e.TransferToMainOrTradeAccount(t.Context(), &FundTransferFuturesParam{RecieveAccountType: "MAIN"}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.TransferToMainOrTradeAccount(t.Context(), &FundTransferFuturesParam{Amount: 1, RecieveAccountType: "MAIN"}) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) @@ -1187,7 +1188,7 @@ func TestTransferToFuturesAccount(t *testing.T) { _, err := e.TransferToFuturesAccount(t.Context(), &FundTransferToFuturesParam{}) require.ErrorIs(t, err, common.ErrNilPointer) _, err = e.TransferToFuturesAccount(t.Context(), &FundTransferToFuturesParam{PaymentAccountType: "Main"}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.TransferToFuturesAccount(t.Context(), &FundTransferToFuturesParam{PaymentAccountType: "Main", Amount: 12}) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) @@ -1314,7 +1315,7 @@ func TestApplyWithdrawal(t *testing.T) { _, err = e.ApplyWithdrawal(t.Context(), currency.ETH, "", "", "", "", "", false, 1) require.ErrorIs(t, err, errAddressRequired) _, err = e.ApplyWithdrawal(t.Context(), currency.ETH, "0x597873884BC3a6C10cB6Eb7C69172028Fa85B25A", "", "", "", "", false, 0) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.ApplyWithdrawal(t.Context(), currency.ETH, "0x597873884BC3a6C10cB6Eb7C69172028Fa85B25A", "", "", "", "", false, 1) @@ -1580,7 +1581,7 @@ func TestPostFuturesOrder(t *testing.T) { ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", Stop: "up", StopPriceType: "TP", TimeInForce: "", Size: 1, Price: 1000, StopPrice: 0, Leverage: 1, VisibleSize: 0, }) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.PostFuturesOrder(t.Context(), &FuturesOrderParam{ @@ -1595,9 +1596,9 @@ func TestPostFuturesOrder(t *testing.T) { ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", Leverage: 1, }) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) _, err = e.PostFuturesOrder(t.Context(), &FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", Price: 1000, Leverage: 1, VisibleSize: 0}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) result, err = e.PostFuturesOrder(t.Context(), &FuturesOrderParam{ ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", Size: 1, Price: 1000, Leverage: 1, VisibleSize: 0, @@ -1610,12 +1611,12 @@ func TestPostFuturesOrder(t *testing.T) { ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "market", Remark: "10", Leverage: 1, }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.PostFuturesOrder(t.Context(), &FuturesOrderParam{ ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "market", Remark: "10", Size: 1, Leverage: 1, VisibleSize: 0, }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) result, err = e.PostFuturesOrder(t.Context(), &FuturesOrderParam{ ClientOrderID: "5bd6e9286d99522a52e458de", @@ -1658,7 +1659,7 @@ func TestFillFuturesPostOrderArgumentFilter(t *testing.T) { ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", Stop: "up", StopPriceType: "TP", TimeInForce: "", Size: 1, Price: 1000, StopPrice: 0, Leverage: 1, VisibleSize: 0, }) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) err = e.FillFuturesPostOrderArgumentFilter(&FuturesOrderParam{ ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", @@ -1671,9 +1672,9 @@ func TestFillFuturesPostOrderArgumentFilter(t *testing.T) { ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", Leverage: 1, }) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) err = e.FillFuturesPostOrderArgumentFilter(&FuturesOrderParam{ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", Price: 1000, Leverage: 1, VisibleSize: 0}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) err = e.FillFuturesPostOrderArgumentFilter(&FuturesOrderParam{ ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "limit", Remark: "10", Size: 1, Price: 1000, Leverage: 1, VisibleSize: 0, @@ -1685,12 +1686,12 @@ func TestFillFuturesPostOrderArgumentFilter(t *testing.T) { ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "market", Remark: "10", Leverage: 1, }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) err = e.FillFuturesPostOrderArgumentFilter(&FuturesOrderParam{ ClientOrderID: "5bd6e9286d99522a52e458de", Side: "buy", Symbol: futuresTradablePair, OrderType: "market", Remark: "10", Size: 0, Leverage: 1, VisibleSize: 0, }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) err = e.FillFuturesPostOrderArgumentFilter(&FuturesOrderParam{ ClientOrderID: "5bd6e9286d99522a52e458de", @@ -2015,7 +2016,7 @@ func TestTransferFuturesFundsToMainAccount(t *testing.T) { assert.NoError(t, err) _, err = e.TransferFuturesFundsToMainAccount(t.Context(), 0, currency.USDT, "MAIN") - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.TransferFuturesFundsToMainAccount(t.Context(), 1, currency.EMPTYCODE, "MAIN") require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.TransferFuturesFundsToMainAccount(t.Context(), 1, currency.ETH, "") @@ -2030,7 +2031,7 @@ func TestTransferFuturesFundsToMainAccount(t *testing.T) { func TestTransferFundsToFuturesAccount(t *testing.T) { t.Parallel() err := e.TransferFundsToFuturesAccount(t.Context(), 0, currency.USDT, "MAIN") - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) err = e.TransferFundsToFuturesAccount(t.Context(), 1, currency.EMPTYCODE, "MAIN") require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) err = e.TransferFundsToFuturesAccount(t.Context(), 1, currency.USDT, "") @@ -3277,22 +3278,17 @@ func TestGetFuturesPositionOrders(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - err := e.UpdateOrderExecutionLimits(t.Context(), asset.Binary) - require.ErrorIs(t, err, asset.ErrNotSupported) - - assets := []asset.Item{asset.Spot, asset.Futures, asset.Margin} - for x := range assets { - err = e.UpdateOrderExecutionLimits(t.Context(), assets[x]) - assert.NoError(t, err) - - enabled, err := e.GetEnabledPairs(assets[x]) - assert.NoError(t, err) - - for y := range enabled { - lim, err := e.GetOrderExecutionLimits(assets[x], enabled[y]) - assert.NoErrorf(t, err, "%v %s %v", err, enabled[y], assets[x]) - assert.NotEmptyf(t, lim, "limit cannot be empty") - } + testexch.UpdatePairsOnce(t, e) + for _, a := range e.GetAssetTypes(false) { + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + 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.AmountStepIncrementSize, "AmountStepIncrementSize should not be zero") + }) } } @@ -3361,11 +3357,11 @@ func TestValidatePlaceOrderParams(t *testing.T) { require.ErrorIs(t, err, order.ErrSideIsInvalid) arg.Side = "Sell" err = arg.ValidatePlaceOrderParams() - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.Price = 323423423 arg.Size = 0 err = arg.ValidatePlaceOrderParams() - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Size = 1 err = arg.ValidatePlaceOrderParams() assert.NoError(t, err) @@ -3570,7 +3566,7 @@ func TestCancelSpecifiedNumberHFOrdersByOrderID(t *testing.T) { _, err = e.CancelSpecifiedNumberHFOrdersByOrderID(t.Context(), "1", "", 10.0) require.ErrorIs(t, err, currency.ErrSymbolStringEmpty) _, err = e.CancelSpecifiedNumberHFOrdersByOrderID(t.Context(), "1", spotTradablePair.String(), 0) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.CancelSpecifiedNumberHFOrdersByOrderID(t.Context(), "1", spotTradablePair.String(), 10.0) @@ -3699,19 +3695,19 @@ func TestPlaceOCOOrder(t *testing.T) { arg.Side = "Sell" _, err = e.PlaceOCOOrder(t.Context(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.Price = 1000 _, err = e.PlaceOCOOrder(t.Context(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Size = .1 _, err = e.PlaceOCOOrder(t.Context(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.StopPrice = .1 _, err = e.PlaceOCOOrder(t.Context(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.LimitPrice = .1 _, err = e.PlaceOCOOrder(t.Context(), arg) @@ -3829,11 +3825,11 @@ func TestSendPlaceMarginHFOrder(t *testing.T) { arg.Symbol = marginTradablePair _, err = e.SendPlaceMarginHFOrder(t.Context(), arg, "") - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.Price = 1000 _, err = e.SendPlaceMarginHFOrder(t.Context(), arg, "") - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) } func TestPlaceMarginHFOrder(t *testing.T) { @@ -3995,7 +3991,7 @@ func TestMarginLendingSubscription(t *testing.T) { _, err := e.MarginLendingSubscription(t.Context(), currency.EMPTYCODE, 1, 0.22) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.MarginLendingSubscription(t.Context(), currency.ETH, 0, 0.22) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.MarginLendingSubscription(t.Context(), currency.ETH, 1, 0) require.ErrorIs(t, err, errMissingInterestRate) @@ -4010,7 +4006,7 @@ func TestRedemption(t *testing.T) { _, err := e.Redemption(t.Context(), currency.EMPTYCODE, 1, "1245") require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.Redemption(t.Context(), currency.ETH, 0, "1245") - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.Redemption(t.Context(), currency.ETH, 1, "") require.ErrorIs(t, err, errMissingPurchaseOrderNumber) @@ -4122,7 +4118,7 @@ func TestGetMaximumOpenPositionSize(t *testing.T) { _, err := e.GetMaximumOpenPositionSize(t.Context(), "", 1, 1) require.ErrorIs(t, err, currency.ErrSymbolStringEmpty) _, err = e.GetMaximumOpenPositionSize(t.Context(), futuresTradablePair.String(), 0., 1) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) _, err = e.GetMaximumOpenPositionSize(t.Context(), futuresTradablePair.String(), 1, 0) require.ErrorIs(t, err, errInvalidLeverage) @@ -4145,7 +4141,7 @@ func TestSubscribeToEarnFixedIncomeProduct(t *testing.T) { _, err := e.SubscribeToEarnFixedIncomeProduct(t.Context(), "", "MAIN", 12.2) require.ErrorIs(t, err, errProductIDMissing) _, err = e.SubscribeToEarnFixedIncomeProduct(t.Context(), "1232412", "MAIN", 0) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.SubscribeToEarnFixedIncomeProduct(t.Context(), "1232412", "", 12.2) require.ErrorIs(t, err, errAccountTypeMissing) @@ -4160,7 +4156,7 @@ func TestRedeemByEarnHoldingID(t *testing.T) { _, err := e.RedeemByEarnHoldingID(t.Context(), "", SpotTradeType, "1", 1) require.ErrorIs(t, err, order.ErrOrderIDNotSet) _, err = e.RedeemByEarnHoldingID(t.Context(), "123231", "Main", "1", 0) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.RedeemByEarnHoldingID(t.Context(), "123231", SpotTradeType, "1", 1) diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index 903d1714..ae9b585f 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -13,6 +13,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" "github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -1843,7 +1844,7 @@ func (e *Exchange) GetServerTime(ctx context.Context, a asset.Item) (time.Time, case asset.Futures: return e.GetFuturesServerTime(ctx) default: - return time.Time{}, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return time.Time{}, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } @@ -2326,17 +2327,17 @@ func (e *Exchange) GetFuturesPositionOrders(ctx context.Context, r *futures.Posi // UpdateOrderExecutionLimits updates order execution limits func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { if !e.SupportsAsset(a) { - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } - var limits []order.MinMaxLevel + var l []limits.MinMaxLevel switch a { case asset.Spot, asset.Margin: symbols, err := e.GetSymbols(ctx, "") if err != nil { return err } - limits = make([]order.MinMaxLevel, 0, len(symbols)) + l = make([]limits.MinMaxLevel, 0, len(symbols)) for x := range symbols { if a == asset.Margin && !symbols[x].IsMarginEnabled { continue @@ -2348,9 +2349,8 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if !enabled { continue } - limits = append(limits, order.MinMaxLevel{ - Pair: pair, - Asset: a, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, pair), AmountStepIncrementSize: symbols[x].BaseIncrement, QuoteStepIncrementSize: symbols[x].QuoteIncrement, PriceStepIncrementSize: symbols[x].PriceIncrement, @@ -2365,7 +2365,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if err != nil { return err } - limits = make([]order.MinMaxLevel, 0, len(contract)) + l = make([]limits.MinMaxLevel, 0, len(contract)) for x := range contract { pair, enabled, err := e.MatchSymbolCheckEnabled(contract[x].Symbol, a, false) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { @@ -2374,9 +2374,8 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if !enabled { continue } - limits = append(limits, order.MinMaxLevel{ - Pair: pair, - Asset: a, + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, pair), AmountStepIncrementSize: contract[x].LotSize, QuoteStepIncrementSize: contract[x].TickSize, MaximumBaseAmount: contract[x].MaxOrderQty, @@ -2385,7 +2384,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) } } - return e.LoadLimits(limits) + return limits.Load(l) } // GetOpenInterest returns the open interest rate for a given asset pair @@ -2422,12 +2421,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } resp = append(resp, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: symbol.Base.Item, - Quote: symbol.Quote.Item, - Asset: asset.Futures, - }, + Key: key.NewExchangeAssetPair(e.Name, asset.Futures, symbol), OpenInterest: contracts[i].OpenInterest.Float64(), }) } @@ -2450,7 +2444,7 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre cp.Delimiter = "" return tradeBaseURL + tradeFutures + tradeSpot + cp.Upper().String(), nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/lbank/lbank.go b/exchanges/lbank/lbank.go index e5edfb86..260e192f 100644 --- a/exchanges/lbank/lbank.go +++ b/exchanges/lbank/lbank.go @@ -23,6 +23,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/request" @@ -190,10 +191,10 @@ func (e *Exchange) CreateOrder(ctx context.Context, pair, side string, amount, p return resp, order.ErrSideIsInvalid } if amount <= 0 { - return resp, order.ErrAmountBelowMin + return resp, limits.ErrAmountBelowMin } if price <= 0 { - return resp, order.ErrPriceBelowMin + return resp, limits.ErrPriceBelowMin } params := url.Values{} diff --git a/exchanges/lbank/lbank_test.go b/exchanges/lbank/lbank_test.go index be2aa042..dab9e589 100644 --- a/exchanges/lbank/lbank_test.go +++ b/exchanges/lbank/lbank_test.go @@ -21,6 +21,7 @@ import ( "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -126,9 +127,9 @@ func TestCreateOrder(t *testing.T) { _, err := e.CreateOrder(t.Context(), testPair.String(), "what", 1231, 12314) require.ErrorIs(t, err, order.ErrSideIsInvalid) _, err = e.CreateOrder(t.Context(), testPair.String(), order.Buy.String(), 0, 0) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.CreateOrder(t.Context(), testPair.String(), order.Sell.String(), 1231, 0) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) diff --git a/exchanges/okx/okx.go b/exchanges/okx/okx.go index e3abc1d1..bd3f8672 100644 --- a/exchanges/okx/okx.go +++ b/exchanges/okx/okx.go @@ -19,6 +19,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common/crypto" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" @@ -383,7 +384,7 @@ func (e *Exchange) PlaceAlgoOrder(ctx context.Context, arg *AlgoOrderParams) (*A return nil, order.ErrTypeIsInvalid } if arg.Size <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } var resp *AlgoOrder return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, placeAlgoOrderEPL, http.MethodPost, "trade/order-algo", arg, &resp, request.AuthenticatedRequest) @@ -396,7 +397,7 @@ func (e *Exchange) PlaceStopOrder(ctx context.Context, arg *AlgoOrderParams) (*A return nil, common.ErrEmptyParams } if arg.TakeProfitTriggerPrice <= 0 { - return nil, fmt.Errorf("%w, take profit trigger price is required", order.ErrPriceBelowMin) + return nil, fmt.Errorf("%w, take profit trigger price is required", limits.ErrPriceBelowMin) } if arg.TakeProfitTriggerPriceType == "" { return nil, order.ErrUnknownPriceType @@ -466,7 +467,7 @@ func (e *Exchange) PlaceTakeProfitStopLossOrder(ctx context.Context, arg *AlgoOr return nil, fmt.Errorf("%w for TPSL: %q", order.ErrTypeIsInvalid, arg.OrderType) } if arg.StopLossTriggerPrice <= 0 { - return nil, order.ErrPriceBelowMin + return nil, limits.ErrPriceBelowMin } switch arg.StopLossTriggerPriceType { case "", "last", "index", "mark": @@ -500,7 +501,7 @@ func (e *Exchange) PlaceTriggerAlgoOrder(ctx context.Context, arg *AlgoOrderPara return nil, fmt.Errorf("%w for Trigger: %q", order.ErrTypeIsInvalid, arg.OrderType) } if arg.TriggerPrice <= 0 { - return nil, fmt.Errorf("%w, trigger price must be greater than 0", order.ErrPriceBelowMin) + return nil, fmt.Errorf("%w, trigger price must be greater than 0", limits.ErrPriceBelowMin) } switch arg.TriggerPriceType { case "", "last", "index", "mark": @@ -798,7 +799,7 @@ func (e *Exchange) PreCheckOrder(ctx context.Context, arg *OrderPreCheckParams) return nil, order.ErrTypeIsInvalid } if arg.Size <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } var resp *OrderPreCheckResponse return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, orderPreCheckEPL, http.MethodPost, "trade/order-precheck", arg, &resp, request.AuthenticatedRequest) @@ -1129,7 +1130,7 @@ func (e *Exchange) FundingTransfer(ctx context.Context, arg *FundingTransferRequ return nil, common.ErrEmptyParams } if arg.Amount <= 0 { - return nil, fmt.Errorf("%w, funding amount must be greater than 0", order.ErrAmountBelowMin) + return nil, fmt.Errorf("%w, funding amount must be greater than 0", limits.ErrAmountBelowMin) } if arg.Currency.IsEmpty() { return nil, currency.ErrCurrencyCodeEmpty @@ -1198,7 +1199,7 @@ func (e *Exchange) GetLightningDeposits(ctx context.Context, ccy currency.Code, params := url.Values{} params.Set("ccy", ccy.String()) if amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } params.Set("amt", strconv.FormatFloat(amount, 'f', 0, 64)) if to == 6 || to == 18 { @@ -1261,7 +1262,7 @@ func (e *Exchange) Withdrawal(ctx context.Context, arg *WithdrawalInput) (*Withd case arg.Currency.IsEmpty(): return nil, currency.ErrCurrencyCodeEmpty case arg.Amount <= 0: - return nil, fmt.Errorf("%w, withdrawal amount required", order.ErrAmountBelowMin) + return nil, fmt.Errorf("%w, withdrawal amount required", limits.ErrAmountBelowMin) case arg.WithdrawalDestination == "": return nil, fmt.Errorf("%w, withdrawal destination required", errAddressRequired) case arg.ToAddress == "": @@ -1391,7 +1392,7 @@ func (e *Exchange) SavingsPurchaseOrRedemption(ctx context.Context, arg *Savings case arg.Currency.IsEmpty(): return nil, currency.ErrCurrencyCodeEmpty case arg.Amount <= 0: - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin case arg.ActionType != "purchase" && arg.ActionType != "redempt": return nil, fmt.Errorf("%w, side has to be either 'redempt' or 'purchase'", order.ErrSideIsInvalid) case arg.ActionType == "purchase" && (arg.Rate < 0.01 || arg.Rate > 3.65): @@ -1527,7 +1528,7 @@ func (e *Exchange) EstimateQuote(ctx context.Context, arg *EstimateQuoteRequestI return nil, order.ErrSideIsInvalid } if arg.RFQAmount <= 0 { - return nil, fmt.Errorf("%w, RFQ amount required", order.ErrAmountBelowMin) + return nil, fmt.Errorf("%w, RFQ amount required", limits.ErrAmountBelowMin) } if arg.RFQSzCurrency == "" { return nil, fmt.Errorf("%w, missing RFQ currency", currency.ErrCurrencyCodeEmpty) @@ -1554,7 +1555,7 @@ func (e *Exchange) ConvertTrade(ctx context.Context, arg *ConvertTradeInput) (*C return nil, order.ErrSideIsInvalid } if arg.Size <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if arg.SizeCurrency.IsEmpty() { return nil, currency.ErrCurrencyCodeEmpty @@ -1892,7 +1893,7 @@ func (e *Exchange) IncreaseDecreaseMargin(ctx context.Context, arg *IncreaseDecr return nil, fmt.Errorf("%w, missing valid 'type', 'add': add margin 'reduce': reduce margin are allowed", order.ErrTypeIsInvalid) } if arg.Amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } var resp *IncreaseDecreaseMargin return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, increaseOrDecreaseMarginEPL, http.MethodPost, "account/position/margin-balance", &arg, &resp, request.AuthenticatedRequest) @@ -2106,7 +2107,7 @@ func (e *Exchange) ManualBorrowAndRepayInQuickMarginMode(ctx context.Context, ar return nil, common.ErrEmptyParams } if arg.Amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if arg.LoanCcy.IsEmpty() { return nil, currency.ErrCurrencyCodeEmpty @@ -2181,7 +2182,7 @@ func (e *Exchange) VIPLoansBorrowAndRepay(ctx context.Context, arg *LoanBorrowAn return nil, order.ErrSideIsInvalid } if arg.Amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } var resp *LoanBorrowAndReplay return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, vipLoansBorrowAnsRepayEPL, http.MethodPost, "account/borrow-repay", &arg, &resp, request.AuthenticatedRequest) @@ -2330,7 +2331,7 @@ func (e *Exchange) GetFixedLoanBorrowQuote(ctx context.Context, borrowingCurrenc return nil, currency.ErrCurrencyCodeEmpty } if amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if maxRate <= 0 { return nil, errMaxRateRequired @@ -2371,7 +2372,7 @@ func (e *Exchange) PlaceFixedLoanBorrowingOrder(ctx context.Context, ccy currenc return nil, currency.ErrCurrencyCodeEmpty } if amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if maxRate <= 0 { return nil, errMaxRateRequired @@ -2509,7 +2510,7 @@ func (e *Exchange) ManualBorrowOrRepay(ctx context.Context, ccy currency.Code, s return nil, errLendingSideRequired } if amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } arg := &struct { Currency string `json:"ccy"` @@ -2573,7 +2574,7 @@ func (e *Exchange) SetRiskOffsetAmount(ctx context.Context, ccy currency.Code, c return nil, currency.ErrCurrencyCodeEmpty } if clientSpotInUseAmount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } arg := &struct { Currency string `json:"ccy"` @@ -2853,7 +2854,7 @@ func (e *Exchange) MasterAccountsManageTransfersBetweenSubaccounts(ctx context.C return nil, currency.ErrCurrencyCodeEmpty } if arg.Amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if arg.From == 0 { return nil, errInvalidSubaccount @@ -2946,10 +2947,10 @@ func (e *Exchange) PlaceGridAlgoOrder(ctx context.Context, arg *GridAlgoOrder) ( return nil, errMissingAlgoOrderType } if arg.MaxPrice <= 0 { - return nil, order.ErrPriceBelowMin + return nil, limits.ErrPriceBelowMin } if arg.MinPrice <= 0 { - return nil, order.ErrPriceBelowMin + return nil, limits.ErrPriceBelowMin } if arg.GridQuantity < 0 { return nil, errInvalidGridQuantity @@ -3053,7 +3054,7 @@ func (e *Exchange) ClosePositionForContractID(ctx context.Context, arg *ClosePos return nil, fmt.Errorf("%w 'size' is required", order.ErrAmountMustBeSet) } if !arg.MarketCloseAllPositions && arg.Price <= 0 { - return nil, order.ErrPriceBelowMin + return nil, limits.ErrPriceBelowMin } var resp *ClosePositionContractGridResponse return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, closePositionForForContractGridEPL, http.MethodPost, "tradingBot/grid/close-position", arg, &resp, request.AuthenticatedRequest) @@ -3277,10 +3278,10 @@ func (e *Exchange) ComputeMinInvestment(ctx context.Context, arg *ComputeInvestm return nil, errInvalidAlgoOrderType } if arg.MaxPrice <= 0 { - return nil, fmt.Errorf("%w, maxPrice = %f", order.ErrPriceBelowMin, arg.MaxPrice) + return nil, fmt.Errorf("%w, maxPrice = %f", limits.ErrPriceBelowMin, arg.MaxPrice) } if arg.MinPrice <= 0 { - return nil, fmt.Errorf("%w, minPrice = %f", order.ErrPriceBelowMin, arg.MaxPrice) + return nil, fmt.Errorf("%w, minPrice = %f", limits.ErrPriceBelowMin, arg.MaxPrice) } if arg.GridNumber == 0 { return nil, fmt.Errorf("%w, grid number is required", errInvalidGridQuantity) @@ -3300,7 +3301,7 @@ func (e *Exchange) ComputeMinInvestment(ctx context.Context, arg *ComputeInvestm } for x := range arg.InvestmentData { if arg.InvestmentData[x].Amount <= 0 { - return nil, fmt.Errorf("%w, investment amt = %f", order.ErrAmountBelowMin, arg.InvestmentData[x].Amount) + return nil, fmt.Errorf("%w, investment amt = %f", limits.ErrAmountBelowMin, arg.InvestmentData[x].Amount) } if arg.InvestmentData[x].Currency.IsEmpty() { return nil, currency.ErrCurrencyCodeEmpty @@ -3744,7 +3745,7 @@ func validateFirstCopySettings(arg *FirstCopySettings) error { return errCopyInstrumentIDTypeRequired } if arg.CopyTotalAmount <= 0 { - return fmt.Errorf("%w, copyTotalAmount value %f", order.ErrAmountBelowMin, arg.CopyTotalAmount) + return fmt.Errorf("%w, copyTotalAmount value %f", limits.ErrAmountBelowMin, arg.CopyTotalAmount) } if arg.SubPosCloseType == "" { return errSubPositionCloseTypeRequired @@ -4035,7 +4036,7 @@ func (e *Exchange) Purchase(ctx context.Context, arg *PurchaseRequestParam) (*Or return nil, fmt.Errorf("%w, currency information for investment is required", currency.ErrCurrencyCodeEmpty) } if arg.InvestData[x].Amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } } var resp *OrderIDResponse @@ -4136,7 +4137,7 @@ func (e *Exchange) GetProductInfo(ctx context.Context) (*ProductInfo, error) { // Only the assets in the funding account can be used func (e *Exchange) PurchaseETHStaking(ctx context.Context, amount float64) error { if amount <= 0 { - return order.ErrAmountBelowMin + return limits.ErrAmountBelowMin } var resp []string return e.SendHTTPRequest(ctx, exchange.RestSpot, purchaseETHStakingEPL, http.MethodPost, "finance/staking-defi/eth/purchase", map[string]string{"amt": strconv.FormatFloat(amount, 'f', -1, 64)}, &resp, request.AuthenticatedRequest) @@ -4145,7 +4146,7 @@ func (e *Exchange) PurchaseETHStaking(ctx context.Context, amount float64) error // RedeemETHStaking only the assets in the funding account can be used. If your BETH is in your trading account, you can make funding transfer first func (e *Exchange) RedeemETHStaking(ctx context.Context, amount float64) error { if amount <= 0 { - return order.ErrAmountBelowMin + return limits.ErrAmountBelowMin } var resp []string return e.SendHTTPRequest(ctx, exchange.RestSpot, redeemETHStakingEPL, http.MethodPost, "finance/staking-defi/eth/redeem", @@ -4622,10 +4623,10 @@ func (e *Exchange) validatePlaceSpreadOrderParam(arg *SpreadOrderParam) error { return fmt.Errorf("%w spread order type is required", order.ErrTypeIsInvalid) } if arg.Size <= 0 { - return order.ErrAmountBelowMin + return limits.ErrAmountBelowMin } if arg.Price <= 0 { - return order.ErrPriceBelowMin + return limits.ErrPriceBelowMin } arg.Side = strings.ToLower(arg.Side) switch arg.Side { @@ -5520,7 +5521,7 @@ func (e *Exchange) PlaceLendingOrder(ctx context.Context, arg *LendingOrderParam return nil, currency.ErrCurrencyCodeEmpty } if arg.Amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if arg.Rate <= 0 { return nil, errRateRequired @@ -5824,7 +5825,7 @@ func (e *Exchange) CreateWithdrawalOrder(ctx context.Context, ccy currency.Code, return nil, currency.ErrCurrencyCodeEmpty } if amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } if paymentMethod == "" { return nil, errPaymentMethodRequired diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 11ef2a28..f2765658 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -19,6 +19,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" @@ -747,7 +748,7 @@ func TestPlaceOrder(t *testing.T) { arg.OrderType = order.Limit.String() _, err = e.PlaceOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.AssetType = asset.Futures _, err = e.PlaceOrder(contextGenerate(), arg) @@ -755,7 +756,7 @@ func TestPlaceOrder(t *testing.T) { arg.PositionSide = "long" _, err = e.PlaceOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Amount = 1 arg.TargetCurrency = "abcd" @@ -829,7 +830,7 @@ func TestPlaceMultipleOrders(t *testing.T) { arg.OrderType = orderLimit _, err = e.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.AssetType = asset.Futures _, err = e.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) @@ -837,7 +838,7 @@ func TestPlaceMultipleOrders(t *testing.T) { arg.PositionSide = "long" _, err = e.PlaceMultipleOrders(contextGenerate(), []PlaceOrderRequestParam{arg}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.PlaceMultipleOrders(contextGenerate(), params) @@ -1057,7 +1058,7 @@ func TestPlaceAlgoOrder(t *testing.T) { arg.OrderType = "limit" _, err = e.PlaceAlgoOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) } func TestStopOrder(t *testing.T) { @@ -1069,7 +1070,7 @@ func TestStopOrder(t *testing.T) { } arg.OrderType = "conditional" _, err = e.PlaceStopOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.TakeProfitTriggerPrice = 123 _, err = e.PlaceStopOrder(contextGenerate(), arg) @@ -1095,7 +1096,7 @@ func TestStopOrder(t *testing.T) { arg.OrderType = "limit" _, err = e.PlaceStopOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) // Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder. sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) @@ -1184,7 +1185,7 @@ func TestPlaceTakeProfitStopLossOrder(t *testing.T) { _, err = e.PlaceTakeProfitStopLossOrder(contextGenerate(), &AlgoOrderParams{ReduceOnly: true}) require.ErrorIs(t, err, order.ErrTypeIsInvalid) _, err = e.PlaceTakeProfitStopLossOrder(contextGenerate(), &AlgoOrderParams{OrderType: "conditional"}) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) _, err = e.PlaceTakeProfitStopLossOrder(contextGenerate(), &AlgoOrderParams{ OrderType: "conditional", StopLossTriggerPrice: 1234, @@ -1242,7 +1243,7 @@ func TestPlaceChaseAlgoOrder(t *testing.T) { arg.Side = order.Sell.Lower() _, err = e.PlaceChaseAlgoOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) // Offline error handling unit tests for the base function PlaceAlgoOrder are already covered within unit test TestPlaceAlgoOrder. sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) @@ -1270,7 +1271,7 @@ func TestTriggerAlgoOrder(t *testing.T) { require.ErrorIs(t, err, order.ErrTypeIsInvalid) _, err = e.PlaceTriggerAlgoOrder(contextGenerate(), &AlgoOrderParams{AlgoClientOrderID: "1234", OrderType: "trigger"}) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) _, err = e.PlaceTriggerAlgoOrder(contextGenerate(), &AlgoOrderParams{AlgoClientOrderID: "1234", OrderType: "trigger", TriggerPrice: 123., TriggerPriceType: "abcd"}) require.ErrorIs(t, err, order.ErrUnknownPriceType) @@ -1759,7 +1760,7 @@ func TestFundingTransfer(t *testing.T) { _, err = e.FundingTransfer(contextGenerate(), &FundingTransferRequestInput{ BeneficiaryAccountType: "6", RemittingAccountType: "18", Currency: currency.BTC, }) - assert.ErrorIs(t, err, order.ErrAmountBelowMin) + assert.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.FundingTransfer(contextGenerate(), &FundingTransferRequestInput{ Amount: 12.000, BeneficiaryAccountType: "6", RemittingAccountType: "18", Currency: currency.EMPTYCODE, @@ -1811,7 +1812,7 @@ func TestGetLightningDeposits(t *testing.T) { _, err := e.GetLightningDeposits(contextGenerate(), currency.EMPTYCODE, 1.00, 0) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.GetLightningDeposits(contextGenerate(), currency.BTC, 0, 0) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e) result, err := e.GetLightningDeposits(contextGenerate(), currency.BTC, 1.00, 0) @@ -1845,7 +1846,7 @@ func TestWithdrawal(t *testing.T) { _, err = e.Withdrawal(contextGenerate(), &WithdrawalInput{Amount: 0.1, TransactionFee: 0.00005, Currency: currency.EMPTYCODE, WithdrawalDestination: "4", ToAddress: core.BitcoinDonationAddress}) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.Withdrawal(contextGenerate(), &WithdrawalInput{TransactionFee: 0.00005, Currency: currency.BTC, WithdrawalDestination: "4", ToAddress: core.BitcoinDonationAddress}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.Withdrawal(contextGenerate(), &WithdrawalInput{Amount: 0.1, TransactionFee: 0.00005, Currency: currency.BTC, ToAddress: core.BitcoinDonationAddress}) require.ErrorIs(t, err, errAddressRequired) _, err = e.Withdrawal(contextGenerate(), &WithdrawalInput{Amount: 0.1, TransactionFee: 0.00005, Currency: currency.BTC, WithdrawalDestination: "4"}) @@ -1942,7 +1943,7 @@ func TestSavingsPurchase(t *testing.T) { arg.Currency = currency.BTC _, err = e.SavingsPurchaseOrRedemption(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Amount = 123.4 _, err = e.SavingsPurchaseOrRedemption(contextGenerate(), arg) @@ -2077,7 +2078,7 @@ func TestEstimateQuote(t *testing.T) { require.ErrorIs(t, err, order.ErrSideIsInvalid) arg.Side = order.Sell.Lower() _, err = e.EstimateQuote(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.RFQAmount = 30 _, err = e.EstimateQuote(contextGenerate(), arg) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) @@ -2112,7 +2113,7 @@ func TestConvertTrade(t *testing.T) { arg.Side = order.Buy.Lower() _, err = e.ConvertTrade(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Size = 2 _, err = e.ConvertTrade(contextGenerate(), arg) @@ -2315,7 +2316,7 @@ func TestIncreaseDecreaseMargin(t *testing.T) { arg.MarginBalanceType = "reduce" _, err = e.IncreaseDecreaseMargin(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.IncreaseDecreaseMargin(contextGenerate(), &IncreaseDecreaseMarginInput{ @@ -2433,7 +2434,7 @@ func TestVIPLoansBorrowAndRepay(t *testing.T) { _, err = e.VIPLoansBorrowAndRepay(contextGenerate(), &LoanBorrowAndReplayInput{Currency: currency.BTC, Side: "", Amount: 12}) require.ErrorIs(t, err, order.ErrSideIsInvalid) _, err = e.VIPLoansBorrowAndRepay(contextGenerate(), &LoanBorrowAndReplayInput{Currency: currency.BTC, Side: "borrow", Amount: 0}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e) result, err := e.VIPLoansBorrowAndRepay(contextGenerate(), &LoanBorrowAndReplayInput{Currency: currency.BTC, Side: "borrow", Amount: 12}) @@ -2472,7 +2473,7 @@ func TestGetFixedLoanBorrowQuote(t *testing.T) { _, err = e.GetFixedLoanBorrowQuote(contextGenerate(), currency.EMPTYCODE, "normal", "30D", "123423423", 1, .4) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.GetFixedLoanBorrowQuote(contextGenerate(), currency.USDT, "normal", "30D", "", 0, .4) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.GetFixedLoanBorrowQuote(contextGenerate(), currency.USDT, "normal", "30D", "123423423", 1, 0) require.ErrorIs(t, err, errMaxRateRequired) _, err = e.GetFixedLoanBorrowQuote(contextGenerate(), currency.USDT, "normal", "", "123423423", 1, .4) @@ -2491,7 +2492,7 @@ func TestPlaceFixedLoanBorrowingOrder(t *testing.T) { _, err := e.PlaceFixedLoanBorrowingOrder(contextGenerate(), currency.EMPTYCODE, 1, .3, .2, "30D", false) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.PlaceFixedLoanBorrowingOrder(contextGenerate(), currency.USDT, 0, .3, .2, "30D", false) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.PlaceFixedLoanBorrowingOrder(contextGenerate(), currency.USDT, 1, 0, .2, "30D", false) require.ErrorIs(t, err, errMaxRateRequired) _, err = e.PlaceFixedLoanBorrowingOrder(contextGenerate(), currency.USDT, 1, .3, .2, "", false) @@ -2575,7 +2576,7 @@ func TestManualBorrowOrRepay(t *testing.T) { _, err = e.ManualBorrowOrRepay(contextGenerate(), currency.USDT, "", 1) require.ErrorIs(t, err, errLendingSideRequired) _, err = e.ManualBorrowOrRepay(contextGenerate(), currency.USDT, "borrow", 0) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.ManualBorrowOrRepay(contextGenerate(), currency.USDT, "borrow", 1) @@ -2635,7 +2636,7 @@ func TestSetRiskOffsetAmount(t *testing.T) { _, err := e.SetRiskOffsetAmount(contextGenerate(), currency.EMPTYCODE, 123) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.SetRiskOffsetAmount(contextGenerate(), currency.USDT, 0) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e) result, err := e.SetRiskOffsetAmount(contextGenerate(), currency.USDT, 123) @@ -2766,7 +2767,7 @@ func TestMasterAccountsManageTransfersBetweenSubaccounts(t *testing.T) { arg.Currency = currency.BTC _, err = e.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Amount = 1234 _, err = e.MasterAccountsManageTransfersBetweenSubaccounts(contextGenerate(), arg) @@ -2871,7 +2872,7 @@ func TestGetProductInfo(t *testing.T) { func TestPurcahseETHStaking(t *testing.T) { t.Parallel() err := e.PurchaseETHStaking(contextGenerate(), 0) - assert.ErrorIs(t, err, order.ErrAmountBelowMin) + assert.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) err = e.PurchaseETHStaking(contextGenerate(), 100) @@ -2882,7 +2883,7 @@ func TestPurcahseETHStaking(t *testing.T) { func TestRedeemETHStaking(t *testing.T) { t.Parallel() err := e.RedeemETHStaking(contextGenerate(), 0) - assert.ErrorIs(t, err, order.ErrAmountBelowMin) + assert.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) err = e.RedeemETHStaking(contextGenerate(), 100) @@ -2936,11 +2937,11 @@ func TestPlaceGridAlgoOrder(t *testing.T) { arg.AlgoOrdType = "contract_grid" _, err = e.PlaceGridAlgoOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.MaxPrice = 1000 _, err = e.PlaceGridAlgoOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.MinPrice = 1200 arg.GridQuantity = -1 @@ -3196,7 +3197,7 @@ func TestPurchase(t *testing.T) { _, err = e.Purchase(contextGenerate(), &PurchaseRequestParam{ProductID: "1234", Term: 2, InvestData: []PurchaseInvestDataItem{{Amount: 1}}}) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.Purchase(contextGenerate(), &PurchaseRequestParam{ProductID: "1234", Term: 2, InvestData: []PurchaseInvestDataItem{{Currency: currency.USDT}}}) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.Purchase(contextGenerate(), &PurchaseRequestParam{ @@ -3318,23 +3319,18 @@ func TestUpdateTradablePairs(t *testing.T) { func TestUpdateOrderExecutionLimits(t *testing.T) { t.Parallel() - testexch.UpdatePairsOnce(t, e) for _, a := range e.GetAssetTypes(false) { - err := e.UpdateOrderExecutionLimits(contextGenerate(), a) - if !assert.NoError(t, err) { - continue - } - - p, err := e.GetAvailablePairs(a) - require.NoErrorf(t, err, "GetAvailablePairs for asset %s must not error", a) - require.NotEmptyf(t, p, "GetAvailablePairs for asset %s must not return empty pairs", a) - - limits, err := e.GetOrderExecutionLimits(a, p[0]) - if assert.NoErrorf(t, err, "GetOrderExecutionLimits for asset %s and pair %s should not error", a, p[0]) { - require.Positivef(t, limits.PriceStepIncrementSize, "PriceStepIncrementSize must be positive for %s", p[0]) - require.Positivef(t, limits.MinimumBaseAmount, "MinimumBaseAmount must be positive for %s", p[0]) - } + t.Run(a.String(), func(t *testing.T) { + t.Parallel() + 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.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + }) } } @@ -3430,7 +3426,7 @@ func TestSubmitOrder(t *testing.T) { arg.AssetType = asset.Spot _, err = e.SubmitOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Amount = 1000000000 _, err = e.SubmitOrder(contextGenerate(), arg) @@ -3480,7 +3476,7 @@ func TestSubmitOrder(t *testing.T) { arg.TrackingMode = order.Percentage _, err = e.SubmitOrder(contextGenerate(), arg) - assert.ErrorIs(t, err, order.ErrAmountBelowMin) + assert.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.TrackingValue = .5 result, err = e.SubmitOrder(contextGenerate(), arg) @@ -3499,7 +3495,7 @@ func TestSubmitOrder(t *testing.T) { arg.Type = order.OCO _, err = e.SubmitOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.RiskManagementModes = order.RiskManagementModes{ TakeProfit: order.RiskManagement{ @@ -3689,11 +3685,11 @@ func TestModifyOrder(t *testing.T) { arg.Type = order.Trigger _, err = e.ModifyOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.Type = order.OCO _, err = e.ModifyOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) arg = &order.Modify{ @@ -3714,7 +3710,7 @@ func TestModifyOrder(t *testing.T) { arg.Type = order.Trigger _, err = e.ModifyOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.TriggerPrice = 12345678 _, err = e.ModifyOrder(contextGenerate(), arg) @@ -3723,7 +3719,7 @@ func TestModifyOrder(t *testing.T) { arg.Type = order.OCO _, err = e.ModifyOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.RiskManagementModes = order.RiskManagementModes{ TakeProfit: order.RiskManagement{Price: 12345677}, @@ -4502,7 +4498,7 @@ func TestManualBorrowAndRepayInQuickMarginMode(t *testing.T) { LoanCcy: currency.USDT, Side: "borrow", }) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.ManualBorrowAndRepayInQuickMarginMode(contextGenerate(), &BorrowAndRepay{ Amount: 1, InstrumentID: mainPair.String(), @@ -4707,7 +4703,7 @@ func TestPreCheckOrder(t *testing.T) { arg.OrderType = "limit" _, err = e.PreCheckOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.PreCheckOrder(contextGenerate(), &OrderPreCheckParams{ @@ -4766,7 +4762,7 @@ func TestClosePositionForContractID(t *testing.T) { _, err = e.ClosePositionForContractID(contextGenerate(), &ClosePositionParams{AlgoID: "448965992920907776", MarketCloseAllPositions: false}) require.ErrorIs(t, err, order.ErrAmountMustBeSet) _, err = e.ClosePositionForContractID(contextGenerate(), &ClosePositionParams{AlgoID: "448965992920907776", MarketCloseAllPositions: false, Size: 123}) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) result, err := e.ClosePositionForContractID(contextGenerate(), &ClosePositionParams{ @@ -4815,11 +4811,11 @@ func TestComputeMinInvestment(t *testing.T) { require.ErrorIs(t, err, errInvalidAlgoOrderType) arg.AlgoOrderType = "grid" _, err = e.ComputeMinInvestment(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.MaxPrice = 5000 _, err = e.ComputeMinInvestment(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.MinPrice = 5000 _, err = e.ComputeMinInvestment(contextGenerate(), arg) @@ -4842,7 +4838,7 @@ func TestComputeMinInvestment(t *testing.T) { arg.Leverage = 5 arg.InvestmentData = []InvestmentData{{Currency: currency.ETH}} _, err = e.ComputeMinInvestment(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.InvestmentData = []InvestmentData{{Amount: 0.01}} _, err = e.ComputeMinInvestment(contextGenerate(), arg) @@ -5187,7 +5183,7 @@ func TestAmendCopySettings(t *testing.T) { arg.CopyInstrumentIDType = "copy" _, err = e.SetFirstCopySettings(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.CopyTotalAmount = 500 _, err = e.SetFirstCopySettings(contextGenerate(), arg) @@ -5385,11 +5381,11 @@ func TestPlaceSpreadOrder(t *testing.T) { arg.OrderType = "limit" _, err = e.PlaceSpreadOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Size = 1 _, err = e.PlaceSpreadOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrPriceBelowMin) + require.ErrorIs(t, err, limits.ErrPriceBelowMin) arg.Price = 12345 _, err = e.PlaceSpreadOrder(contextGenerate(), arg) @@ -5684,7 +5680,7 @@ func TestPlaceLendingOrder(t *testing.T) { arg.Currency = currency.USDT _, err = e.PlaceLendingOrder(contextGenerate(), arg) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) arg.Amount = 1 _, err = e.PlaceLendingOrder(contextGenerate(), arg) @@ -5977,7 +5973,7 @@ func TestCreateWithdrawalOrder(t *testing.T) { _, err = e.CreateWithdrawalOrder(contextGenerate(), currency.EMPTYCODE, "1231312312", "SEPA", "194a6975e98246538faeb0fab0d502df", 1000) require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty) _, err = e.CreateWithdrawalOrder(contextGenerate(), currency.BTC, "1231312312", "SEPA", "194a6975e98246538faeb0fab0d502df", 0) - require.ErrorIs(t, err, order.ErrAmountBelowMin) + require.ErrorIs(t, err, limits.ErrAmountBelowMin) _, err = e.CreateWithdrawalOrder(contextGenerate(), currency.BTC, "1231312312", "", "194a6975e98246538faeb0fab0d502df", 1000) require.ErrorIs(t, err, errPaymentMethodRequired) _, err = e.CreateWithdrawalOrder(contextGenerate(), currency.BTC, "1231312312", "SEPA", "", 1000) @@ -6234,7 +6230,7 @@ func TestValidatePlaceOrderRequestParam(t *testing.T) { p.PositionSide = "long" require.ErrorIs(t, p.Validate(), order.ErrTypeIsInvalid) p.OrderType = order.Market.String() - require.ErrorIs(t, p.Validate(), order.ErrAmountBelowMin) + require.ErrorIs(t, p.Validate(), limits.ErrAmountBelowMin) p.Amount = 1 p.TargetCurrency = "moo cows" require.ErrorIs(t, p.Validate(), errCurrencyQuantityTypeRequired) @@ -6251,9 +6247,9 @@ func TestValidateSpreadOrderParam(t *testing.T) { p.SpreadID = spreadPair.String() require.ErrorIs(t, p.Validate(), order.ErrTypeIsInvalid) p.OrderType = order.Market.String() - require.ErrorIs(t, p.Validate(), order.ErrAmountBelowMin) + require.ErrorIs(t, p.Validate(), limits.ErrAmountBelowMin) p.Size = 1 - require.ErrorIs(t, p.Validate(), order.ErrPriceBelowMin) + require.ErrorIs(t, p.Validate(), limits.ErrPriceBelowMin) p.Price = 1 require.ErrorIs(t, p.Validate(), order.ErrSideIsInvalid) p.Side = order.Buy.String() diff --git a/exchanges/okx/okx_types.go b/exchanges/okx/okx_types.go index 85079cfb..c2fe843d 100644 --- a/exchanges/okx/okx_types.go +++ b/exchanges/okx/okx_types.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -813,7 +814,7 @@ func (arg *PlaceOrderRequestParam) Validate() error { return fmt.Errorf("%w: '%v'", order.ErrTypeIsInvalid, arg.OrderType) } if arg.Amount <= 0 { - return order.ErrAmountBelowMin + return limits.ErrAmountBelowMin } if !slices.Contains([]string{"", "base_ccy", "quote_ccy"}, arg.TargetCurrency) { return errCurrencyQuantityTypeRequired @@ -2974,10 +2975,10 @@ func (arg *SpreadOrderParam) Validate() error { return fmt.Errorf("%w spread order type is required", order.ErrTypeIsInvalid) } if arg.Size <= 0 { - return order.ErrAmountBelowMin + return limits.ErrAmountBelowMin } if arg.Price <= 0 { - return order.ErrPriceBelowMin + return limits.ErrPriceBelowMin } arg.Side = strings.ToLower(arg.Side) switch arg.Side { diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index 12859200..b852af0c 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -15,6 +15,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" @@ -311,16 +312,15 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if len(insts) == 0 { return common.ErrNoResponse } - limits := make([]order.MinMaxLevel, len(insts)) - for x := range insts { - limits[x] = order.MinMaxLevel{ - Pair: insts[x].InstrumentID, - Asset: a, - PriceStepIncrementSize: insts[x].TickSize.Float64(), - MinimumBaseAmount: insts[x].MinimumOrderSize.Float64(), + l := make([]limits.MinMaxLevel, len(insts)) + for i := range insts { + l[i] = limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, insts[i].InstrumentID), + PriceStepIncrementSize: insts[i].TickSize.Float64(), + MinimumBaseAmount: insts[i].MinimumOrderSize.Float64(), } } - return e.LoadLimits(limits) + return limits.Load(l) case asset.Spread: insts, err := e.GetPublicSpreads(ctx, "", "", "", "live") if err != nil { @@ -329,19 +329,18 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if len(insts) == 0 { return common.ErrNoResponse } - limits := make([]order.MinMaxLevel, len(insts)) - for x := range insts { - limits[x] = order.MinMaxLevel{ - Pair: insts[x].SpreadID, - Asset: a, - PriceStepIncrementSize: insts[x].MinSize.Float64(), - MinimumBaseAmount: insts[x].MinSize.Float64(), - QuoteStepIncrementSize: insts[x].TickSize.Float64(), + l := make([]limits.MinMaxLevel, len(insts)) + for i := range insts { + l[i] = limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, insts[i].SpreadID), + PriceStepIncrementSize: insts[i].MinSize.Float64(), + MinimumBaseAmount: insts[i].MinSize.Float64(), + QuoteStepIncrementSize: insts[i].TickSize.Float64(), } } - return e.LoadLimits(limits) + return limits.Load(l) default: - return fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } @@ -858,7 +857,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Sub return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, s.AssetType) } if s.Amount <= 0 { - return nil, order.ErrAmountBelowMin + return nil, limits.ErrAmountBelowMin } pairFormat, err := e.GetPairFormat(s.AssetType, true) if err != nil { @@ -987,7 +986,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Sub return nil, fmt.Errorf("%w, tracking mode unset", order.ErrUnknownTrackingMode) } if s.TrackingValue == 0 { - return nil, fmt.Errorf("%w, tracking value required", order.ErrAmountBelowMin) + return nil, fmt.Errorf("%w, tracking value required", limits.ErrAmountBelowMin) } result, err = e.PlaceChaseAlgoOrder(ctx, &AlgoOrderParams{ InstrumentID: pairString, @@ -1051,9 +1050,9 @@ func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Sub case orderOCO: switch { case s.RiskManagementModes.TakeProfit.Price <= 0: - return nil, fmt.Errorf("%w, take profit price is required", order.ErrPriceBelowMin) + return nil, fmt.Errorf("%w, take profit price is required", limits.ErrPriceBelowMin) case s.RiskManagementModes.StopLoss.Price <= 0: - return nil, fmt.Errorf("%w, stop loss price is required", order.ErrPriceBelowMin) + return nil, fmt.Errorf("%w, stop loss price is required", limits.ErrPriceBelowMin) } result, err = e.PlaceAlgoOrder(ctx, &AlgoOrderParams{ InstrumentID: pairString, @@ -1159,7 +1158,7 @@ func (e *Exchange) ModifyOrder(ctx context.Context, action *order.Modify) (*orde } case order.Trigger: if action.TriggerPrice == 0 { - return nil, fmt.Errorf("%w, trigger price required", order.ErrPriceBelowMin) + return nil, fmt.Errorf("%w, trigger price required", limits.ErrPriceBelowMin) } var postTriggerTPSLOrders []SubTPSLParams if action.RiskManagementModes.StopLoss.Price > 0 && action.RiskManagementModes.TakeProfit.Price > 0 { @@ -1194,10 +1193,10 @@ func (e *Exchange) ModifyOrder(ctx context.Context, action *order.Modify) (*orde switch { case action.RiskManagementModes.TakeProfit.Price <= 0 && action.RiskManagementModes.TakeProfit.LimitPrice <= 0: - return nil, fmt.Errorf("%w, either take profit trigger price or order price is required", order.ErrPriceBelowMin) + return nil, fmt.Errorf("%w, either take profit trigger price or order price is required", limits.ErrPriceBelowMin) case action.RiskManagementModes.StopLoss.Price <= 0 && action.RiskManagementModes.StopLoss.LimitPrice <= 0: - return nil, fmt.Errorf("%w, either stop loss trigger price or order price is required", order.ErrPriceBelowMin) + return nil, fmt.Errorf("%w, either stop loss trigger price or order price is required", limits.ErrPriceBelowMin) } _, err = e.AmendAlgoOrder(ctx, &AmendAlgoOrderParam{ InstrumentID: pairFormat.Format(action.Pair), @@ -2933,12 +2932,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } resp = append(resp, futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: v, - }, + Key: key.NewExchangeAssetPair(e.Name, v, p), OpenInterest: oid[j].OpenInterest.Float64(), }) } @@ -2985,12 +2979,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } resp[0] = futures.OpenInterest{ - Key: key.ExchangePairAsset{ - Exchange: e.Name, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: k[0].Asset, - }, + Key: key.NewExchangeAssetPair(e.Name, k[0].Asset, p), OpenInterest: oid[i].OpenInterest.Float64(), } } @@ -3047,6 +3036,6 @@ func (e *Exchange) GetCurrencyTradeURL(ctx context.Context, a asset.Item, cp cur } return baseURL + "trade-futures/" + strings.ToLower(insts[0].Underlying) + ct, nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/order/limits.go b/exchanges/order/limits.go deleted file mode 100644 index 6c543ba0..00000000 --- a/exchanges/order/limits.go +++ /dev/null @@ -1,356 +0,0 @@ -package order - -import ( - "errors" - "fmt" - "sync" - - "github.com/shopspring/decimal" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" -) - -// Public errors for order limits -var ( - ErrLoadLimitsFailed = errors.New("failed to load exchange limits") - ErrExchangeLimitNotLoaded = errors.New("exchange limits not loaded") - ErrPriceBelowMin = errors.New("price below minimum limit") - ErrPriceExceedsMax = errors.New("price exceeds maximum limit") - ErrPriceExceedsStep = errors.New("price exceeds step limit") // price is not divisible by its step - ErrAmountBelowMin = errors.New("amount below minimum limit") - ErrAmountExceedsMax = errors.New("amount exceeds maximum limit") - ErrAmountExceedsStep = errors.New("amount exceeds step limit") // amount is not divisible by its step - ErrNotionalValue = errors.New("total notional value is under minimum limit") - ErrMarketAmountBelowMin = errors.New("market order amount below minimum limit") - ErrMarketAmountExceedsMax = errors.New("market order amount exceeds maximum limit") - ErrMarketAmountExceedsStep = errors.New("market order amount exceeds step limit") // amount is not divisible by its step for a market order - ErrCannotValidateAsset = errors.New("cannot check limit, asset not loaded") - ErrCannotValidateBaseCurrency = errors.New("cannot check limit, base currency not loaded") - ErrCannotValidateQuoteCurrency = errors.New("cannot check limit, quote currency not loaded") -) - -var ( - errExchangeLimitBase = errors.New("exchange limits not found for base currency") - errExchangeLimitQuote = errors.New("exchange limits not found for quote currency") - errCannotLoadLimit = errors.New("cannot load limit, levels not supplied") - errInvalidPriceLevels = errors.New("invalid price levels, cannot load limits") - errInvalidAmountLevels = errors.New("invalid amount levels, cannot load limits") - errInvalidQuoteLevels = errors.New("invalid quote levels, cannot load limits") -) - -// ExecutionLimits defines minimum and maximum values in relation to -// order size, order pricing, total notional values, total maximum orders etc -// for execution on an exchange. -type ExecutionLimits struct { - m map[asset.Item]map[*currency.Item]map[*currency.Item]MinMaxLevel - mtx sync.RWMutex -} - -// MinMaxLevel defines the minimum and maximum parameters for a currency pair -// for outbound exchange execution -type MinMaxLevel struct { - Pair currency.Pair - Asset asset.Item - MinPrice float64 - MaxPrice float64 - PriceStepIncrementSize float64 - MultiplierUp float64 - MultiplierDown float64 - MultiplierDecimal float64 - AveragePriceMinutes int64 - MinimumBaseAmount float64 - MaximumBaseAmount float64 - MinimumQuoteAmount float64 - MaximumQuoteAmount float64 - AmountStepIncrementSize float64 - QuoteStepIncrementSize float64 - MinNotional float64 - MaxIcebergParts int64 - MarketMinQty float64 - MarketMaxQty float64 - MarketStepIncrementSize float64 - MaxTotalOrders int64 - MaxAlgoOrders int64 -} - -// LoadLimits loads all limits levels into memory -func (e *ExecutionLimits) LoadLimits(levels []MinMaxLevel) error { - if len(levels) == 0 { - return errCannotLoadLimit - } - e.mtx.Lock() - defer e.mtx.Unlock() - if e.m == nil { - e.m = make(map[asset.Item]map[*currency.Item]map[*currency.Item]MinMaxLevel) - } - - for x := range levels { - if !levels[x].Asset.IsValid() { - return fmt.Errorf("cannot load levels for %q: %w", levels[x].Asset, asset.ErrNotSupported) - } - m1, ok := e.m[levels[x].Asset] - if !ok { - m1 = make(map[*currency.Item]map[*currency.Item]MinMaxLevel) - e.m[levels[x].Asset] = m1 - } - - if levels[x].Pair.IsEmpty() { - return currency.ErrCurrencyPairEmpty - } - - m2, ok := m1[levels[x].Pair.Base.Item] - if !ok { - m2 = make(map[*currency.Item]MinMaxLevel) - m1[levels[x].Pair.Base.Item] = m2 - } - - if levels[x].MinPrice > 0 && - levels[x].MaxPrice > 0 && - levels[x].MinPrice > levels[x].MaxPrice { - return fmt.Errorf("%w for %s %s supplied min: %f max: %f", - errInvalidPriceLevels, - levels[x].Asset, - levels[x].Pair, - levels[x].MinPrice, - levels[x].MaxPrice) - } - - if levels[x].MinimumBaseAmount > 0 && - levels[x].MaximumBaseAmount > 0 && - levels[x].MinimumBaseAmount > levels[x].MaximumBaseAmount { - return fmt.Errorf("%w for %s %s supplied min: %f max: %f", - errInvalidAmountLevels, - levels[x].Asset, - levels[x].Pair, - levels[x].MinimumBaseAmount, - levels[x].MaximumBaseAmount) - } - - if levels[x].MinimumQuoteAmount > 0 && - levels[x].MaximumQuoteAmount > 0 && - levels[x].MinimumQuoteAmount > levels[x].MaximumQuoteAmount { - return fmt.Errorf("%w for %s %s supplied min: %f max: %f", - errInvalidQuoteLevels, - levels[x].Asset, - levels[x].Pair, - levels[x].MinimumQuoteAmount, - levels[x].MaximumQuoteAmount) - } - - m2[levels[x].Pair.Quote.Item] = levels[x] - } - return nil -} - -// GetOrderExecutionLimits returns the exchange limit parameters for a currency -func (e *ExecutionLimits) GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (MinMaxLevel, error) { - e.mtx.RLock() - defer e.mtx.RUnlock() - if e.m == nil { - return MinMaxLevel{}, ErrExchangeLimitNotLoaded - } - - m1, ok := e.m[a] - if !ok { - return MinMaxLevel{}, fmt.Errorf("%w %v", ErrCannotValidateAsset, a) - } - - m2, ok := m1[cp.Base.Item] - if !ok { - return MinMaxLevel{}, fmt.Errorf("%w %v", errExchangeLimitBase, cp.Base) - } - - limit, ok := m2[cp.Quote.Item] - if !ok { - return MinMaxLevel{}, fmt.Errorf("%w %v", errExchangeLimitQuote, cp.Quote) - } - - return limit, nil -} - -// CheckOrderExecutionLimits checks to see if the price and amount conforms with -// exchange level order execution limits -func (e *ExecutionLimits) CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType Type) error { - e.mtx.RLock() - defer e.mtx.RUnlock() - - if e.m == nil { - // No exchange limits loaded so we can nil this - return nil - } - - m1, ok := e.m[a] - if !ok { - return ErrCannotValidateAsset - } - - m2, ok := m1[cp.Base.Item] - if !ok { - return ErrCannotValidateBaseCurrency - } - - limit, ok := m2[cp.Quote.Item] - if !ok { - return ErrCannotValidateQuoteCurrency - } - - err := limit.Conforms(price, amount, orderType) - if err != nil { - return fmt.Errorf("%w for %s %s", err, a, cp) - } - - return nil -} - -// Conforms checks outbound parameters -func (m *MinMaxLevel) Conforms(price, amount float64, orderType Type) error { - // TODO: Update to take in account Quote amounts as well as Base amounts. - if m == nil { - return nil - } - - if m.MinimumBaseAmount != 0 && amount < m.MinimumBaseAmount { - return fmt.Errorf("%w min: %.8f supplied %.8f", - ErrAmountBelowMin, - m.MinimumBaseAmount, - amount) - } - if m.MaximumBaseAmount != 0 && amount > m.MaximumBaseAmount { - return fmt.Errorf("%w min: %.8f supplied %.8f", - ErrAmountExceedsMax, - m.MaximumBaseAmount, - amount) - } - if m.AmountStepIncrementSize != 0 { - dAmount := decimal.NewFromFloat(amount) - dStep := decimal.NewFromFloat(m.AmountStepIncrementSize) - if !dAmount.Mod(dStep).IsZero() { - return fmt.Errorf("%w stepSize: %.8f supplied %.8f", - ErrAmountExceedsStep, - m.AmountStepIncrementSize, - amount) - } - } - - // Multiplier checking not done due to the fact we need coherence with the - // last average price (TODO) - // m.multiplierUp will be used to determine how far our price can go up - // m.multiplierDown will be used to determine how far our price can go down - // m.averagePriceMinutes will be used to determine mean over this period - - // Max iceberg parts checking not done as we do not have that - // functionality yet (TODO) - // m.maxIcebergParts // How many components in an iceberg order - - // Max total orders not done due to order manager limitations (TODO) - // m.maxTotalOrders - - // Max algo orders not done due to order manager limitations (TODO) - // m.maxAlgoOrders - - // If order type is Market we do not need to do price checks - if orderType != Market { - if m.MinPrice != 0 && price < m.MinPrice { - return fmt.Errorf("%w min: %.8f supplied %.8f", - ErrPriceBelowMin, - m.MinPrice, - price) - } - if m.MaxPrice != 0 && price > m.MaxPrice { - return fmt.Errorf("%w max: %.8f supplied %.8f", - ErrPriceExceedsMax, - m.MaxPrice, - price) - } - if m.MinNotional != 0 && (amount*price) < m.MinNotional { - return fmt.Errorf("%w minimum notional: %.8f value of order %.8f", - ErrNotionalValue, - m.MinNotional, - amount*price) - } - if m.PriceStepIncrementSize != 0 { - dPrice := decimal.NewFromFloat(price) - dMinPrice := decimal.NewFromFloat(m.MinPrice) - dStep := decimal.NewFromFloat(m.PriceStepIncrementSize) - if !dPrice.Sub(dMinPrice).Mod(dStep).IsZero() { - return fmt.Errorf("%w stepSize: %.8f supplied %.8f", - ErrPriceExceedsStep, - m.PriceStepIncrementSize, - price) - } - } - return nil - } - - if m.MarketMinQty != 0 && - m.MinimumBaseAmount < m.MarketMinQty && - amount < m.MarketMinQty { - return fmt.Errorf("%w min: %.8f supplied %.8f", - ErrMarketAmountBelowMin, - m.MarketMinQty, - amount) - } - if m.MarketMaxQty != 0 && - m.MaximumBaseAmount > m.MarketMaxQty && - amount > m.MarketMaxQty { - return fmt.Errorf("%w max: %.8f supplied %.8f", - ErrMarketAmountExceedsMax, - m.MarketMaxQty, - amount) - } - if m.MarketStepIncrementSize != 0 && - m.AmountStepIncrementSize != m.MarketStepIncrementSize { - dAmount := decimal.NewFromFloat(amount) - dMinMAmount := decimal.NewFromFloat(m.MarketMinQty) - dStep := decimal.NewFromFloat(m.MarketStepIncrementSize) - if !dAmount.Sub(dMinMAmount).Mod(dStep).IsZero() { - return fmt.Errorf("%w stepSize: %.8f supplied %.8f", - ErrMarketAmountExceedsStep, - m.MarketStepIncrementSize, - amount) - } - } - return nil -} - -// ConformToDecimalAmount (POC) conforms amount to its amount interval -func (m *MinMaxLevel) ConformToDecimalAmount(amount decimal.Decimal) decimal.Decimal { - if m == nil { - return amount - } - - dStep := decimal.NewFromFloat(m.AmountStepIncrementSize) - if dStep.IsZero() || amount.Equal(dStep) { - return amount - } - - if amount.LessThan(dStep) { - return decimal.Zero - } - mod := amount.Mod(dStep) - // subtract modulus to get the floor - return amount.Sub(mod) -} - -// ConformToAmount (POC) conforms amount to its amount interval -func (m *MinMaxLevel) ConformToAmount(amount float64) float64 { - if m == nil { - return amount - } - - if m.AmountStepIncrementSize == 0 || amount == m.AmountStepIncrementSize { - return amount - } - - if amount < m.AmountStepIncrementSize { - return 0 - } - - // Convert floats to decimal types - dAmount := decimal.NewFromFloat(amount) - dStep := decimal.NewFromFloat(m.AmountStepIncrementSize) - // derive modulus - mod := dAmount.Mod(dStep) - // subtract modulus to get the floor - return dAmount.Sub(mod).InexactFloat64() -} diff --git a/exchanges/order/limits_test.go b/exchanges/order/limits_test.go deleted file mode 100644 index f2f0bef8..00000000 --- a/exchanges/order/limits_test.go +++ /dev/null @@ -1,313 +0,0 @@ -package order - -import ( - "testing" - - "github.com/shopspring/decimal" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" -) - -var ( - btcusd = currency.NewBTCUSD() - ltcusd = currency.NewPair(currency.LTC, currency.USD) - btcltc = currency.NewPair(currency.BTC, currency.LTC) -) - -func TestLoadLimits(t *testing.T) { - t.Parallel() - e := ExecutionLimits{} - err := e.LoadLimits(nil) - assert.ErrorIs(t, err, errCannotLoadLimit) - - invalidAsset := []MinMaxLevel{ - { - Pair: btcusd, - MinPrice: 100000, - MaxPrice: 1000000, - MinimumBaseAmount: 1, - MaximumBaseAmount: 10, - }, - } - err = e.LoadLimits(invalidAsset) - require.ErrorIs(t, err, asset.ErrNotSupported) - - invalidPairLoading := []MinMaxLevel{ - { - Asset: asset.Spot, - MinPrice: 100000, - MaxPrice: 1000000, - MinimumBaseAmount: 1, - MaximumBaseAmount: 10, - }, - } - - err = e.LoadLimits(invalidPairLoading) - assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) - - newLimits := []MinMaxLevel{ - { - Pair: btcusd, - Asset: asset.Spot, - MinPrice: 100000, - MaxPrice: 1000000, - MinimumBaseAmount: 1, - MaximumBaseAmount: 10, - }, - } - - err = e.LoadLimits(newLimits) - require.NoError(t, err) - - badLimit := []MinMaxLevel{ - { - Pair: btcusd, - Asset: asset.Spot, - MinPrice: 2, - MaxPrice: 1, - MinimumBaseAmount: 1, - MaximumBaseAmount: 10, - }, - } - - err = e.LoadLimits(badLimit) - require.ErrorIs(t, err, errInvalidPriceLevels) - - badLimit = []MinMaxLevel{ - { - Pair: btcusd, - Asset: asset.Spot, - MinPrice: 1, - MaxPrice: 2, - MinimumBaseAmount: 10, - MaximumBaseAmount: 9, - }, - } - - err = e.LoadLimits(badLimit) - require.ErrorIs(t, err, errInvalidAmountLevels) - - goodLimit := []MinMaxLevel{ - { - Pair: btcusd, - Asset: asset.Spot, - }, - } - - err = e.LoadLimits(goodLimit) - require.NoError(t, err) - - noCompare := []MinMaxLevel{ - { - Pair: btcusd, - Asset: asset.Spot, - MinimumBaseAmount: 10, - }, - } - - err = e.LoadLimits(noCompare) - require.NoError(t, err) - - noCompare = []MinMaxLevel{ - { - Pair: btcusd, - Asset: asset.Spot, - MinPrice: 10, - }, - } - - err = e.LoadLimits(noCompare) - assert.NoError(t, err) -} - -func TestGetOrderExecutionLimits(t *testing.T) { - t.Parallel() - e := ExecutionLimits{} - _, err := e.GetOrderExecutionLimits(asset.Spot, btcusd) - require.ErrorIs(t, err, ErrExchangeLimitNotLoaded) - - newLimits := []MinMaxLevel{ - { - Pair: btcusd, - Asset: asset.Spot, - MinPrice: 100000, - MaxPrice: 1000000, - MinimumBaseAmount: 1, - MaximumBaseAmount: 10, - }, - } - - err = e.LoadLimits(newLimits) - require.NoError(t, err) - - _, err = e.GetOrderExecutionLimits(asset.Futures, ltcusd) - require.ErrorIs(t, err, ErrCannotValidateAsset) - - _, err = e.GetOrderExecutionLimits(asset.Spot, ltcusd) - require.ErrorIs(t, err, errExchangeLimitBase) - - _, err = e.GetOrderExecutionLimits(asset.Spot, btcltc) - require.ErrorIs(t, err, errExchangeLimitQuote) - - tt, err := e.GetOrderExecutionLimits(asset.Spot, btcusd) - require.NoError(t, err) - assert.Equal(t, newLimits[0].MaximumBaseAmount, tt.MaximumBaseAmount) - assert.Equal(t, newLimits[0].MinimumBaseAmount, tt.MinimumBaseAmount) - assert.Equal(t, newLimits[0].MaxPrice, tt.MaxPrice) - assert.Equal(t, newLimits[0].MinPrice, tt.MinPrice) -} - -func TestCheckLimit(t *testing.T) { - t.Parallel() - e := ExecutionLimits{} - err := e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 1337, Limit) - require.NoError(t, err) - - newLimits := []MinMaxLevel{ - { - Pair: btcusd, - Asset: asset.Spot, - MinPrice: 100000, - MaxPrice: 1000000, - MinimumBaseAmount: 1, - MaximumBaseAmount: 10, - }, - } - - err = e.LoadLimits(newLimits) - require.NoError(t, err) - - err = e.CheckOrderExecutionLimits(asset.Futures, ltcusd, 1337, 1337, Limit) - require.ErrorIs(t, err, ErrCannotValidateAsset) - - err = e.CheckOrderExecutionLimits(asset.Spot, ltcusd, 1337, 1337, Limit) - require.ErrorIs(t, err, ErrCannotValidateBaseCurrency) - - err = e.CheckOrderExecutionLimits(asset.Spot, btcltc, 1337, 1337, Limit) - require.ErrorIs(t, err, ErrCannotValidateQuoteCurrency) - - err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 9, Limit) - require.ErrorIs(t, err, ErrPriceBelowMin) - - err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1000001, 9, Limit) - require.ErrorIs(t, err, ErrPriceExceedsMax) - - err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, .5, Limit) - require.ErrorIs(t, err, ErrAmountBelowMin) - - err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 11, Limit) - require.ErrorIs(t, err, ErrAmountExceedsMax) - - err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Limit) - require.NoError(t, err) - - err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 999999, 7, Market) - assert.NoError(t, err) -} - -func TestConforms(t *testing.T) { - t.Parallel() - var tt MinMaxLevel - err := tt.Conforms(0, 0, Limit) - require.NoError(t, err) - - tt = MinMaxLevel{ - MinNotional: 100, - } - - err = tt.Conforms(1, 1, Limit) - require.ErrorIs(t, err, ErrNotionalValue) - - err = tt.Conforms(200, .5, Limit) - require.NoError(t, err) - - tt.PriceStepIncrementSize = 0.001 - err = tt.Conforms(200.0001, .5, Limit) - require.ErrorIs(t, err, ErrPriceExceedsStep) - err = tt.Conforms(200.004, .5, Limit) - require.NoError(t, err) - - tt.AmountStepIncrementSize = 0.001 - err = tt.Conforms(200, .0002, Limit) - require.ErrorIs(t, err, ErrAmountExceedsStep) - err = tt.Conforms(200000, .003, Limit) - require.NoError(t, err) - - tt.MinimumBaseAmount = 1 - tt.MaximumBaseAmount = 10 - tt.MarketMinQty = 1.1 - tt.MarketMaxQty = 9.9 - - err = tt.Conforms(200000, 1, Market) - require.ErrorIs(t, err, ErrMarketAmountBelowMin) - - err = tt.Conforms(200000, 10, Market) - require.ErrorIs(t, err, ErrMarketAmountExceedsMax) - - tt.MarketStepIncrementSize = 10 - err = tt.Conforms(200000, 9.1, Market) - require.ErrorIs(t, err, ErrMarketAmountExceedsStep) - tt.MarketStepIncrementSize = 1 - err = tt.Conforms(200000, 9.1, Market) - assert.NoError(t, err) -} - -func TestConformToDecimalAmount(t *testing.T) { - t.Parallel() - var tt MinMaxLevel - require.True(t, tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001)).Equal(decimal.NewFromFloat(1.001))) - - tt = MinMaxLevel{} - val := tt.ConformToDecimalAmount(decimal.NewFromInt(1)) - assert.True(t, val.Equal(decimal.NewFromInt(1))) // If there is no step amount set, this should not change the inputted amount - - tt.AmountStepIncrementSize = 0.001 - val = tt.ConformToDecimalAmount(decimal.NewFromFloat(1.001)) - assert.True(t, val.Equal(decimal.NewFromFloat(1.001))) - - val = tt.ConformToDecimalAmount(decimal.NewFromFloat(0.0001)) - assert.True(t, val.IsZero()) - - val = tt.ConformToDecimalAmount(decimal.NewFromFloat(0.7777)) - assert.True(t, val.Equal(decimal.NewFromFloat(0.777))) - - tt.AmountStepIncrementSize = 100 - val = tt.ConformToDecimalAmount(decimal.NewFromInt(100)) - assert.True(t, val.Equal(decimal.NewFromInt(100))) - - val = tt.ConformToDecimalAmount(decimal.NewFromInt(200)) - assert.True(t, val.Equal(decimal.NewFromInt(200))) - val = tt.ConformToDecimalAmount(decimal.NewFromInt(150)) - assert.True(t, val.Equal(decimal.NewFromInt(100))) -} - -func TestConformToAmount(t *testing.T) { - t.Parallel() - var tt MinMaxLevel - require.Equal(t, 1.001, tt.ConformToAmount(1.001)) - - tt = MinMaxLevel{} - val := tt.ConformToAmount(1) - assert.Equal(t, 1.0, val, "ConformToAmount should return the same value with no step amount set") - - tt.AmountStepIncrementSize = 0.001 - val = tt.ConformToAmount(1.001) - assert.Equal(t, 1.001, val) - - val = tt.ConformToAmount(0.0001) - assert.Zero(t, val) - - val = tt.ConformToAmount(0.7777) - assert.Equal(t, 0.777, val) - - tt.AmountStepIncrementSize = 100 - val = tt.ConformToAmount(100) - assert.Equal(t, 100.0, val) - - val = tt.ConformToAmount(200) - require.Equal(t, 200.0, val) - val = tt.ConformToAmount(150) - assert.Equal(t, 100.0, val) -} diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index 994996eb..44859463 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -1569,24 +1569,25 @@ func TestGetOrdersRequest_Filter(t *testing.T) { request.AssetType = asset.Spot request.Type = AnyType request.Side = AnySide - + BTCUSD := currency.NewBTCUSD() + LTCUSD := currency.NewPair(currency.LTC, currency.USD) orders := []Detail{ - {OrderID: "0", Pair: btcusd, AssetType: asset.Spot, Type: Limit, Side: Buy}, - {OrderID: "1", Pair: btcusd, AssetType: asset.Spot, Type: Limit, Side: Sell}, - {OrderID: "2", Pair: btcusd, AssetType: asset.Spot, Type: Market, Side: Buy}, - {OrderID: "3", Pair: btcusd, AssetType: asset.Spot, Type: Market, Side: Sell}, - {OrderID: "4", Pair: btcusd, AssetType: asset.Futures, Type: Limit, Side: Buy}, - {OrderID: "5", Pair: btcusd, AssetType: asset.Futures, Type: Limit, Side: Sell}, - {OrderID: "6", Pair: btcusd, AssetType: asset.Futures, Type: Market, Side: Buy}, - {OrderID: "7", Pair: btcusd, AssetType: asset.Futures, Type: Market, Side: Sell}, - {OrderID: "8", Pair: btcltc, AssetType: asset.Spot, Type: Limit, Side: Buy}, - {OrderID: "9", Pair: btcltc, AssetType: asset.Spot, Type: Limit, Side: Sell}, - {OrderID: "10", Pair: btcltc, AssetType: asset.Spot, Type: Market, Side: Buy}, - {OrderID: "11", Pair: btcltc, AssetType: asset.Spot, Type: Market, Side: Sell}, - {OrderID: "12", Pair: btcltc, AssetType: asset.Futures, Type: Limit, Side: Buy}, - {OrderID: "13", Pair: btcltc, AssetType: asset.Futures, Type: Limit, Side: Sell}, - {OrderID: "14", Pair: btcltc, AssetType: asset.Futures, Type: Market, Side: Buy}, - {OrderID: "15", Pair: btcltc, AssetType: asset.Futures, Type: Market, Side: Sell}, + {OrderID: "0", Pair: BTCUSD, AssetType: asset.Spot, Type: Limit, Side: Buy}, + {OrderID: "1", Pair: BTCUSD, AssetType: asset.Spot, Type: Limit, Side: Sell}, + {OrderID: "2", Pair: BTCUSD, AssetType: asset.Spot, Type: Market, Side: Buy}, + {OrderID: "3", Pair: BTCUSD, AssetType: asset.Spot, Type: Market, Side: Sell}, + {OrderID: "4", Pair: BTCUSD, AssetType: asset.Futures, Type: Limit, Side: Buy}, + {OrderID: "5", Pair: BTCUSD, AssetType: asset.Futures, Type: Limit, Side: Sell}, + {OrderID: "6", Pair: BTCUSD, AssetType: asset.Futures, Type: Market, Side: Buy}, + {OrderID: "7", Pair: BTCUSD, AssetType: asset.Futures, Type: Market, Side: Sell}, + {OrderID: "8", Pair: LTCUSD, AssetType: asset.Spot, Type: Limit, Side: Buy}, + {OrderID: "9", Pair: LTCUSD, AssetType: asset.Spot, Type: Limit, Side: Sell}, + {OrderID: "10", Pair: LTCUSD, AssetType: asset.Spot, Type: Market, Side: Buy}, + {OrderID: "11", Pair: LTCUSD, AssetType: asset.Spot, Type: Market, Side: Sell}, + {OrderID: "12", Pair: LTCUSD, AssetType: asset.Futures, Type: Limit, Side: Buy}, + {OrderID: "13", Pair: LTCUSD, AssetType: asset.Futures, Type: Limit, Side: Sell}, + {OrderID: "14", Pair: LTCUSD, AssetType: asset.Futures, Type: Market, Side: Buy}, + {OrderID: "15", Pair: LTCUSD, AssetType: asset.Futures, Type: Market, Side: Sell}, } shinyAndClean := request.Filter("test", orders) @@ -1596,7 +1597,7 @@ func TestGetOrdersRequest_Filter(t *testing.T) { require.Equal(t, strconv.FormatInt(int64(x), 10), shinyAndClean[x].OrderID) } - request.Pairs = []currency.Pair{btcltc} + request.Pairs = []currency.Pair{LTCUSD} // Kicks off time error request.EndTime = time.Unix(1336, 0) diff --git a/exchanges/orderbook/depth.go b/exchanges/orderbook/depth.go index a96b6a55..5b784f85 100644 --- a/exchanges/orderbook/depth.go +++ b/exchanges/orderbook/depth.go @@ -666,8 +666,8 @@ func (d *Depth) Exchange() string { } // Key returns a combined key for the depth -func (d *Depth) Key() key.ExchangePairAsset { +func (d *Depth) Key() key.ExchangeAssetPair { d.m.RLock() defer d.m.RUnlock() - return key.ExchangePairAsset{Exchange: d.exchange, Base: d.pair.Base.Item, Quote: d.pair.Quote.Item, Asset: d.asset} + return key.NewExchangeAssetPair(d.exchange, d.asset, d.pair) } diff --git a/exchanges/orderbook/depth_test.go b/exchanges/orderbook/depth_test.go index 88890cdc..1fb481b5 100644 --- a/exchanges/orderbook/depth_test.go +++ b/exchanges/orderbook/depth_test.go @@ -764,6 +764,6 @@ func TestKey(t *testing.T) { depth.pair = currency.NewPair(currency.BTC, currency.WABI) depth.asset = asset.Spot require.Equal(t, - key.ExchangePairAsset{Exchange: depth.exchange, Base: depth.pair.Base.Item, Quote: depth.pair.Quote.Item, Asset: depth.asset}, + key.NewExchangeAssetPair(depth.exchange, depth.asset, depth.pair), depth.Key()) } diff --git a/exchanges/orderbook/orderbook.go b/exchanges/orderbook/orderbook.go index 8800df97..bc57161a 100644 --- a/exchanges/orderbook/orderbook.go +++ b/exchanges/orderbook/orderbook.go @@ -42,7 +42,7 @@ func SubscribeToExchangeOrderbooks(exchange string) (dispatch.Pipe, error) { // Update stores orderbook data func (s *store) Update(b *Book) error { s.m.RLock() - book, ok := s.orderbooks[key.ExchangePairAsset{Exchange: b.Exchange, Base: b.Pair.Base.Item, Quote: b.Pair.Quote.Item, Asset: b.Asset}] + book, ok := s.orderbooks[key.ExchangeAssetPair{Exchange: b.Exchange, Base: b.Pair.Base.Item, Quote: b.Pair.Quote.Item, Asset: b.Asset}] s.m.RUnlock() if !ok { var err error @@ -73,7 +73,7 @@ func (s *store) track(b *Book) (book, error) { depth := NewDepth(id) depth.AssignOptions(b) ob := book{RouterID: id, Depth: depth} - s.orderbooks[key.ExchangePairAsset{Exchange: b.Exchange, Base: b.Pair.Base.Item, Quote: b.Pair.Quote.Item, Asset: b.Asset}] = ob + s.orderbooks[key.ExchangeAssetPair{Exchange: b.Exchange, Base: b.Pair.Base.Item, Quote: b.Pair.Quote.Item, Asset: b.Asset}] = ob return ob, nil } @@ -90,7 +90,7 @@ func (s *store) DeployDepth(exchange string, p currency.Pair, a asset.Item) (*De } s.m.RLock() - ob, ok := s.orderbooks[key.ExchangePairAsset{Exchange: exchange, Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] + ob, ok := s.orderbooks[key.ExchangeAssetPair{Exchange: exchange, Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] s.m.RUnlock() var err error if !ok { @@ -102,10 +102,10 @@ func (s *store) DeployDepth(exchange string, p currency.Pair, a asset.Item) (*De // GetDepth returns the actual depth struct for potential subsystems and strategies to interact with func (s *store) GetDepth(exchange string, p currency.Pair, a asset.Item) (*Depth, error) { s.m.RLock() - ob, ok := s.orderbooks[key.ExchangePairAsset{Exchange: exchange, Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] + ob, ok := s.orderbooks[key.ExchangeAssetPair{Exchange: exchange, Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] s.m.RUnlock() if !ok { - return nil, fmt.Errorf("%w for %s %s %s", ErrOrderbookNotFound, exchange, p, a) + return nil, fmt.Errorf("%w for %q %q %q", ErrOrderbookNotFound, exchange, p, a) } return ob.Depth, nil } @@ -117,13 +117,13 @@ func (s *store) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Book, return nil, currency.ErrCurrencyPairEmpty } if !a.IsValid() { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } s.m.RLock() - ob, ok := s.orderbooks[key.ExchangePairAsset{Exchange: exchange, Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] + ob, ok := s.orderbooks[key.ExchangeAssetPair{Exchange: exchange, Base: p.Base.Item, Quote: p.Quote.Item, Asset: a}] s.m.RUnlock() if !ok { - return nil, fmt.Errorf("%w for %s %s %s", ErrOrderbookNotFound, exchange, p, a) + return nil, fmt.Errorf("%w for %q %q %q", ErrOrderbookNotFound, exchange, p, a) } return ob.Depth.Retrieve() } diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go index 6acf0794..b74de9d3 100644 --- a/exchanges/orderbook/orderbook_types.go +++ b/exchanges/orderbook/orderbook_types.go @@ -39,7 +39,7 @@ var ( ) var s = store{ - orderbooks: make(map[key.ExchangePairAsset]book), + orderbooks: make(map[key.ExchangeAssetPair]book), exchangeRouters: make(map[string]uuid.UUID), signalMux: dispatch.GetNewMux(nil), } @@ -51,7 +51,7 @@ type book struct { // store provides a centralised store for orderbooks type store struct { - orderbooks map[key.ExchangePairAsset]book + orderbooks map[key.ExchangeAssetPair]book exchangeRouters map[string]uuid.UUID signalMux *dispatch.Mux m sync.RWMutex diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 7772c292..17e1691b 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -1054,6 +1054,6 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre cp.Delimiter = "" return poloniexAPIURL + tradeFutures + cp.Upper().String(), nil default: - return "", fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } } diff --git a/exchanges/sharedtestvalues/customex.go b/exchanges/sharedtestvalues/customex.go index b442a126..a1028206 100644 --- a/exchanges/sharedtestvalues/customex.go +++ b/exchanges/sharedtestvalues/customex.go @@ -7,6 +7,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/common" "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" @@ -322,8 +323,8 @@ func (c *CustomEx) AuthenticateWebsocket(_ context.Context) error { } // GetOrderExecutionLimits is a mock method for CustomEx -func (c *CustomEx) GetOrderExecutionLimits(_ asset.Item, _ currency.Pair) (order.MinMaxLevel, error) { - return order.MinMaxLevel{}, nil +func (c *CustomEx) GetOrderExecutionLimits(_ asset.Item, _ currency.Pair) (limits.MinMaxLevel, error) { + return limits.MinMaxLevel{}, nil } // CheckOrderExecutionLimits is a mock method for CustomEx diff --git a/exchanges/ticker/ticker.go b/exchanges/ticker/ticker.go index b8c3dcef..d1d53bbe 100644 --- a/exchanges/ticker/ticker.go +++ b/exchanges/ticker/ticker.go @@ -28,7 +28,7 @@ var ( func init() { service = new(Service) - service.Tickers = make(map[key.ExchangePairAsset]*Ticker) + service.Tickers = make(map[key.ExchangeAssetPair]*Ticker) service.Exchange = make(map[string]uuid.UUID) service.mux = dispatch.GetNewMux(nil) } @@ -39,12 +39,7 @@ func SubscribeTicker(exchange string, p currency.Pair, a asset.Item) (dispatch.P exchange = strings.ToLower(exchange) service.mu.Lock() defer service.mu.Unlock() - tick, ok := service.Tickers[key.ExchangePairAsset{ - Exchange: exchange, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }] + tick, ok := service.Tickers[key.NewExchangeAssetPair(exchange, a, p)] if !ok { return dispatch.Pipe{}, fmt.Errorf("ticker item not found for %s %s %s", exchange, @@ -77,17 +72,12 @@ func GetTicker(exchange string, p currency.Pair, a asset.Item) (*Price, error) { return nil, currency.ErrCurrencyPairEmpty } if !a.IsValid() { - return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a) + return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } exchange = strings.ToLower(exchange) service.mu.Lock() defer service.mu.Unlock() - tick, ok := service.Tickers[key.ExchangePairAsset{ - Exchange: exchange, - Base: p.Base.Item, - Quote: p.Quote.Item, - Asset: a, - }] + tick, ok := service.Tickers[key.NewExchangeAssetPair(exchange, a, p)] if !ok { return nil, fmt.Errorf("%w %s %s %s", ErrTickerNotFound, exchange, p, a) } @@ -191,12 +181,7 @@ func ProcessTicker(p *Price) error { // update updates ticker price func (s *Service) update(p *Price) error { name := strings.ToLower(p.ExchangeName) - mapKey := key.ExchangePairAsset{ - Exchange: name, - Base: p.Pair.Base.Item, - Quote: p.Pair.Quote.Item, - Asset: p.AssetType, - } + mapKey := key.NewExchangeAssetPair(name, p.AssetType, p.Pair) s.mu.Lock() t, ok := service.Tickers[mapKey] if !ok || t == nil { diff --git a/exchanges/ticker/ticker_test.go b/exchanges/ticker/ticker_test.go index 08e384bc..0c87b2e7 100644 --- a/exchanges/ticker/ticker_test.go +++ b/exchanges/ticker/ticker_test.go @@ -445,7 +445,7 @@ func TestGetExchangeTickersPublic(t *testing.T) { func TestGetExchangeTickers(t *testing.T) { t.Parallel() s := Service{ - Tickers: make(map[key.ExchangePairAsset]*Ticker), + Tickers: make(map[key.ExchangeAssetPair]*Ticker), Exchange: make(map[string]uuid.UUID), } @@ -455,12 +455,7 @@ func TestGetExchangeTickers(t *testing.T) { _, err = s.getExchangeTickers("test") assert.ErrorIs(t, err, errExchangeNotFound) - s.Tickers[key.ExchangePairAsset{ - Exchange: "test", - Base: currency.XBT.Item, - Quote: currency.DOGE.Item, - Asset: asset.Futures, - }] = &Ticker{ + s.Tickers[key.NewExchangeAssetPair("test", asset.Spot, currency.NewPair(currency.XBT, currency.DOGE))] = &Ticker{ Price: Price{ Pair: currency.NewPair(currency.XBT, currency.DOGE), ExchangeName: "test", diff --git a/exchanges/ticker/ticker_types.go b/exchanges/ticker/ticker_types.go index 2acc0824..87cdddf3 100644 --- a/exchanges/ticker/ticker_types.go +++ b/exchanges/ticker/ticker_types.go @@ -25,7 +25,7 @@ var ( // Service holds ticker information for each individual exchange type Service struct { - Tickers map[key.ExchangePairAsset]*Ticker + Tickers map[key.ExchangeAssetPair]*Ticker Exchange map[string]uuid.UUID mux *dispatch.Mux mu sync.Mutex