From 70690d9a047e26833f27d0596eedc8ce2755fb38 Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 3 Nov 2023 10:01:32 +1000 Subject: [PATCH] futures: Implement GetLatestFundingRates across exchanges (#1339) * adds funding rate implementations and improvements * merge fixes x1 * lint * kucoin funding rates func make * migrate sync-manager to keys * some kucoin work * adds some kucoin wrapper funcs * ehhh, todo * kucoin position * start of orders * adds the kucoin tests yay * multiplier * nits, EWS includes order limits * NotYetImplemented, IsPerp improvements, cleaning * lint, test fix, huobi time * fixes issues, improves testing * fixes linters I WRECKED * local lint but remote lint, lint, lint, lint * fixes err * skip CI * lint * Supported rates, binance endpoints * fixes weird mocktest problems * no, CZ is invalid * fixes some new EWS test errors --- backtester/data/kline/live/live_test.go | 4 +- cmd/exchange_template/wrapper_file.tmpl | 7 + cmd/exchange_wrapper_issues/main.go | 6 +- .../exchange_wrapper_standards_test.go | 49 +- common/key/key.go | 6 + currency/code_types.go | 3 + currency/manager.go | 2 + engine/engine.go | 4 +- engine/helpers.go | 2 +- engine/helpers_test.go | 40 +- engine/order_manager.go | 2 +- engine/rpcserver.go | 29 +- engine/rpcserver_test.go | 38 +- engine/sync_manager.go | 154 +- engine/sync_manager_test.go | 44 +- engine/sync_manager_types.go | 20 +- engine/websocketroutine_manager.go | 4 + engine/websocketroutine_manager_test.go | 14 +- engine/websocketroutine_manager_types.go | 1 + exchanges/alphapoint/alphapoint_wrapper.go | 6 + exchanges/binance/binance_cfutures.go | 8 + exchanges/binance/binance_test.go | 48 +- exchanges/binance/binance_ufutures.go | 7 + exchanges/binance/binance_wrapper.go | 310 ++- exchanges/binance/ufutures_types.go | 10 + exchanges/binanceus/binanceus_wrapper.go | 6 + exchanges/bitfinex/bitfinex_wrapper.go | 24 +- exchanges/bitflyer/bitflyer_wrapper.go | 6 + exchanges/bithumb/bithumb_wrapper.go | 6 + exchanges/bitmex/bitmex.go | 20 +- exchanges/bitmex/bitmex_test.go | 59 + exchanges/bitmex/bitmex_wrapper.go | 101 +- exchanges/bitstamp/bitstamp_wrapper.go | 6 + exchanges/bittrex/bittrex_wrapper.go | 6 + exchanges/btcmarkets/btcmarkets_wrapper.go | 6 + exchanges/btse/btse_test.go | 58 + exchanges/btse/btse_wrapper.go | 72 + exchanges/bybit/bybit_test.go | 19 + exchanges/bybit/bybit_wrapper.go | 12 + exchanges/coinbasepro/coinbasepro_wrapper.go | 6 + exchanges/coinut/coinut_wrapper.go | 6 + exchanges/exchange.go | 14 +- exchanges/exchange_test.go | 14 +- exchanges/exchange_types.go | 17 +- exchanges/exmo/exmo_wrapper.go | 9 + exchanges/fundingrate/fundingrate_types.go | 9 +- exchanges/futures/contract.go | 3 + exchanges/futures/futures.go | 10 +- exchanges/futures/futures_test.go | 10 +- exchanges/futures/futures_types.go | 11 +- exchanges/gateio/gateio_test.go | 38 +- exchanges/gateio/gateio_wrapper.go | 105 + exchanges/gemini/gemini_wrapper.go | 6 + exchanges/hitbtc/hitbtc_wrapper.go | 6 + exchanges/huobi/cfutures_types.go | 10 +- exchanges/huobi/huobi_cfutures.go | 20 +- exchanges/huobi/huobi_test.go | 63 +- exchanges/huobi/huobi_wrapper.go | 103 + exchanges/interfaces.go | 5 +- exchanges/itbit/itbit_wrapper.go | 6 + exchanges/kraken/kraken_test.go | 62 + exchanges/kraken/kraken_wrapper.go | 101 +- exchanges/kucoin/kucoin_futures_types.go | 2 +- exchanges/kucoin/kucoin_test.go | 146 +- exchanges/kucoin/kucoin_wrapper.go | 400 +++- exchanges/lbank/lbank_wrapper.go | 6 + exchanges/okcoin/okcoin_wrapper.go | 6 + exchanges/okx/okx_test.go | 12 +- exchanges/okx/okx_wrapper.go | 58 +- exchanges/order/limits.go | 30 +- exchanges/order/limits_test.go | 16 +- exchanges/order/order_test.go | 8 +- exchanges/poloniex/poloniex_wrapper.go | 10 +- exchanges/protocol/features.go | 60 +- exchanges/sharedtestvalues/customex.go | 11 + exchanges/yobit/yobit_wrapper.go | 6 + exchanges/zb/zb_wrapper.go | 6 + testdata/http_mock/binance/binance.json | 1985 ++++++++++++++++- 78 files changed, 4088 insertions(+), 527 deletions(-) diff --git a/backtester/data/kline/live/live_test.go b/backtester/data/kline/live/live_test.go index 08920087..428e4fa5 100644 --- a/backtester/data/kline/live/live_test.go +++ b/backtester/data/kline/live/live_test.go @@ -73,9 +73,7 @@ func TestLoadTrades(t *testing.T) { ConfigFormat: pFormat, } var data *gctkline.Item - // start is 10 mins in the past to ensure there are some trades to pull from the exchange - start := time.Now().Add(-time.Minute * 10) - data, err = LoadData(context.Background(), start, exch, common.DataTrade, interval.Duration(), cp, currency.EMPTYPAIR, a, true) + data, err = LoadData(context.Background(), time.Now().Add(-interval.Duration()*60), exch, common.DataTrade, interval.Duration(), cp, currency.EMPTYPAIR, a, true) if err != nil { t.Fatal(err) } diff --git a/cmd/exchange_template/wrapper_file.tmpl b/cmd/exchange_template/wrapper_file.tmpl index a29585ab..20cd464c 100644 --- a/cmd/exchange_template/wrapper_file.tmpl +++ b/cmd/exchange_template/wrapper_file.tmpl @@ -14,6 +14,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -547,4 +548,10 @@ func ({{.Variable}} *{{.CapitalName}}) GetFuturesContractDetails(context.Context return nil, common.ErrNotYetImplemented } +// GetLatestFundingRates returns the latest funding rates data +func ({{.Variable}} *{{.CapitalName}}) GetLatestFundingRates(_ context.Context, _ *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrNotYetImplemented +} + + {{end}} diff --git a/cmd/exchange_wrapper_issues/main.go b/cmd/exchange_wrapper_issues/main.go index dc6bbdb3..c7fefc7c 100644 --- a/cmd/exchange_wrapper_issues/main.go +++ b/cmd/exchange_wrapper_issues/main.go @@ -515,14 +515,14 @@ func testWrappers(e exchange.IBotExchange, base *exchange.Base, config *Config) Response: jsonifyInterface([]interface{}{""}), }) - fundingRateRequest := &fundingrate.RatesRequest{ + fundingRateRequest := &fundingrate.HistoricalRatesRequest{ Asset: assetTypes[i], Pair: p, StartDate: time.Now().Add(-time.Hour), EndDate: time.Now(), } - var fundingRateResponse *fundingrate.Rates - fundingRateResponse, err = e.GetFundingRates(context.TODO(), fundingRateRequest) + var fundingRateResponse *fundingrate.HistoricalRates + fundingRateResponse, err = e.GetHistoricalFundingRates(context.TODO(), fundingRateRequest) msg = "" if err != nil { msg = err.Error() diff --git a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go index 995c40ec..d60abaa7 100644 --- a/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go +++ b/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/margin" @@ -110,6 +111,7 @@ func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.C // Add +1 to len to verify that exchanges can handle requests with unset pairs and assets assetPairs := make([]assetPair, 0, len(assets)+1) +assets: for j := range assets { var pairs currency.Pairs pairs, err = b.CurrencyPairs.GetPairs(assets[j], false) @@ -132,6 +134,12 @@ func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.C if err != nil { t.Fatalf("Cannot setup %v asset %v FormatExchangeCurrency %v", name, assets[j], err) } + for x := range unsupportedAssets { + if assets[j] == unsupportedAssets[x] { + // this asset cannot handle disrupt formatting + continue assets + } + } p, err = disruptFormatting(t, p) if err != nil { t.Fatalf("Cannot setup %v asset %v disruptFormatting %v", name, assets[j], err) @@ -281,6 +289,7 @@ var ( positionChangeRequestParam = reflect.TypeOf((**margin.PositionChangeRequest)(nil)).Elem() positionSummaryRequestParam = reflect.TypeOf((**futures.PositionSummaryRequest)(nil)).Elem() positionsRequestParam = reflect.TypeOf((**futures.PositionsRequest)(nil)).Elem() + latestRateRequest = reflect.TypeOf((**fundingrate.LatestRateRequest)(nil)).Elem() ) // generateMethodArg determines the argument type and returns a pre-made @@ -322,8 +331,8 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr case argGenerator.MethodInputType.AssignableTo(feeBuilderParam): input = reflect.ValueOf(&exchange.FeeBuilder{ FeeType: exchange.OfflineTradeFee, - Amount: 1337, - PurchasePrice: 1337, + Amount: 150, + PurchasePrice: 150, Pair: argGenerator.AssetParams.Pair, }) case argGenerator.MethodInputType.AssignableTo(currencyPairParam): @@ -425,7 +434,7 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr Side: order.Buy, Pair: argGenerator.AssetParams.Pair, AssetType: argGenerator.AssetParams.Asset, - Price: 1337, + Price: 150, Amount: 1, ClientID: "1337", ClientOrderID: "13371337", @@ -438,7 +447,7 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr Side: order.Buy, Pair: argGenerator.AssetParams.Pair, AssetType: argGenerator.AssetParams.Asset, - Price: 1337, + Price: 150, Amount: 1, ClientOrderID: "13371337", OrderID: "1337", @@ -482,8 +491,8 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr Pair: argGenerator.AssetParams.Pair, Asset: argGenerator.AssetParams.Asset, MarginType: margin.Isolated, - OriginalAllocatedMargin: 1337, - NewAllocatedMargin: 1338, + OriginalAllocatedMargin: 150, + NewAllocatedMargin: 151, }) case argGenerator.MethodInputType.AssignableTo(positionSummaryRequestParam): input = reflect.ValueOf(&futures.PositionSummaryRequest{ @@ -502,9 +511,15 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr case argGenerator.MethodInputType.AssignableTo(orderSideParam): input = reflect.ValueOf(order.Long) case argGenerator.MethodInputType.AssignableTo(int64Param): - input = reflect.ValueOf(1337) + input = reflect.ValueOf(150) case argGenerator.MethodInputType.AssignableTo(float64Param): - input = reflect.ValueOf(13.37) + input = reflect.ValueOf(150.0) + case argGenerator.MethodInputType.AssignableTo(latestRateRequest): + input = reflect.ValueOf(&fundingrate.LatestRateRequest{ + Asset: argGenerator.AssetParams.Asset, + Pair: argGenerator.AssetParams.Pair, + IncludePredictedRate: true, + }) default: input = reflect.Zero(argGenerator.MethodInputType) } @@ -533,10 +548,7 @@ var excludedMethodNames = map[string]struct{}{ "FlushWebsocketChannels": {}, // Unnecessary websocket test "UnsubscribeToWebsocketChannels": {}, // Unnecessary websocket test "SubscribeToWebsocketChannels": {}, // Unnecessary websocket test - "GetOrderExecutionLimits": {}, // Not widely supported/implemented feature "UpdateCurrencyStates": {}, // Not widely supported/implemented feature - "UpdateOrderExecutionLimits": {}, // Not widely supported/implemented feature - "CheckOrderExecutionLimits": {}, // Not widely supported/implemented feature "CanTradePair": {}, // Not widely supported/implemented feature "CanTrade": {}, // Not widely supported/implemented feature "CanWithdraw": {}, // Not widely supported/implemented feature @@ -548,7 +560,7 @@ var excludedMethodNames = map[string]struct{}{ "GetCollateralCurrencyForContract": {}, "GetCurrencyForRealisedPNL": {}, "GetFuturesPositions": {}, - "GetFundingRates": {}, + "GetHistoricalFundingRates": {}, "IsPerpetualFutureCurrency": {}, "GetMarginRatesHistory": {}, "CalculatePNL": {}, @@ -563,7 +575,6 @@ var excludedMethodNames = map[string]struct{}{ "GetLeverage": {}, "SetMarginType": {}, "ChangePositionMargin": {}, - "GetLatestFundingRate": {}, } // blockedCIExchanges are exchanges that are not able to be tested on CI @@ -572,6 +583,12 @@ var blockedCIExchanges = []string{ "bybit", // bybit API is banned from executing within the US where github Actions is ran } +// unsupportedAssets contains assets that cannot handle +// normal processing for testing. This is to be used very sparingly +var unsupportedAssets = []asset.Item{ + asset.Index, +} + var unsupportedExchangeNames = []string{ "testexch", "alphapoint", @@ -592,6 +609,7 @@ var cryptoChainPerExchange = map[string]string{ // acceptable errors do not throw test errors, see below for why var acceptableErrors = []error{ common.ErrFunctionNotSupported, // Shows API cannot perform function and developer has recognised this + common.ErrNotYetImplemented, // Shows API can perform function but developer has not implemented it yet asset.ErrNotSupported, // Shows that valid and invalid asset types are handled request.ErrAuthRequestFailed, // We must set authenticated requests properly in order to understand and better handle auth failures order.ErrUnsupportedOrderType, // Should be returned if an ordertype like ANY is requested and the implementation knows to throw this specific error @@ -605,6 +623,11 @@ var acceptableErrors = []error{ deposit.ErrAddressNotFound, // Is thrown when an address is not found due to the exchange requiring valid API keys futures.ErrNotFuturesAsset, // Is thrown when a futures function receives a non-futures asset currency.ErrSymbolStringEmpty, // Is thrown when a symbol string is empty for blank MatchSymbol func checks + futures.ErrNotPerpetualFuture, // Is thrown when a futures function receives a non-perpetual future + 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 } // warningErrors will t.Log(err) when thrown to diagnose things, but not necessarily suggest diff --git a/common/key/key.go b/common/key/key.go index 96d2210b..921fe59a 100644 --- a/common/key/key.go +++ b/common/key/key.go @@ -15,6 +15,12 @@ type ExchangePairAsset struct { Asset asset.Item } +// ExchangeAsset is a unique map key signature for exchange and asset +type ExchangeAsset struct { + Exchange string + Asset asset.Item +} + // PairAsset is a unique map key signature for currency pair and asset type PairAsset struct { Base *currency.Item diff --git a/currency/code_types.go b/currency/code_types.go index 7aec9871..db3d1679 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -230,6 +230,7 @@ var ( SSC = NewCode("SSC") SHOW = NewCode("SHOW") SPF = NewCode("SPF") + PF = NewCode("PF") SNC = NewCode("SNC") SWFTC = NewCode("SWFTC") TRA = NewCode("TRA") @@ -3018,6 +3019,8 @@ var ( SWAP = NewCode("SWAP") PI = NewCode("PI") FI = NewCode("FI") + USDM = NewCode("USDM") + USDTM = NewCode("USDTM") stables = Currencies{ USDT, diff --git a/currency/manager.go b/currency/manager.go index 22e68bd3..00a45835 100644 --- a/currency/manager.go +++ b/currency/manager.go @@ -17,6 +17,8 @@ var ( ErrAssetAlreadyEnabled = errors.New("asset already enabled") // ErrPairAlreadyEnabled returns when enabling a pair that is already enabled ErrPairAlreadyEnabled = errors.New("pair already enabled") + // ErrPairNotEnabled returns when looking for a pair that is not enabled + ErrPairNotEnabled = errors.New("pair not enabled") // ErrPairNotFound is returned when a currency pair is not found ErrPairNotFound = errors.New("pair not found") // ErrAssetIsNil is an error when the asset has not been populated by the diff --git a/engine/engine.go b/engine/engine.go index fcc221c2..d279afa8 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -36,7 +36,7 @@ type Engine struct { apiServer *apiServerManager CommunicationsManager *CommunicationManager connectionManager *connectionManager - currencyPairSyncer *syncManager + currencyPairSyncer *SyncManager DatabaseManager *DatabaseConnectionManager DepositAddressManager *DepositAddressManager eventManager *eventManager @@ -513,7 +513,7 @@ func (bot *Engine) Start() error { bot.Settings.SyncWorkersCount != config.DefaultSyncerWorkers { cfg.NumWorkers = bot.Settings.SyncWorkersCount } - if s, err := setupSyncManager( + if s, err := SetupSyncManager( &cfg, bot.ExchangeManager, &bot.Config.RemoteControl, diff --git a/engine/helpers.go b/engine/helpers.go index 6634b83d..4e915e37 100644 --- a/engine/helpers.go +++ b/engine/helpers.go @@ -225,7 +225,7 @@ func (bot *Engine) SetSubsystem(subSystemName string, enable bool) error { bot.Settings.SyncWorkersCount != config.DefaultSyncerWorkers { cfg.NumWorkers = bot.Settings.SyncWorkersCount } - bot.currencyPairSyncer, err = setupSyncManager( + bot.currencyPairSyncer, err = SetupSyncManager( &cfg, bot.ExchangeManager, &bot.Config.RemoteControl, diff --git a/engine/helpers_test.go b/engine/helpers_test.go index 590214bc..56c3dd57 100644 --- a/engine/helpers_test.go +++ b/engine/helpers_test.go @@ -9,13 +9,11 @@ import ( "crypto/x509/pkix" "encoding/pem" "errors" - "fmt" "math/big" "net" "os" "path/filepath" "strings" - "sync" "testing" "time" @@ -1378,33 +1376,23 @@ func TestNewExchangeByNameWithDefaults(t *testing.T) { if !errors.Is(err, ErrExchangeNotFound) { t.Fatalf("received: '%v' but expected: '%v'", err, ErrExchangeNotFound) } - - ch := make(chan error, len(exchange.Exchanges)) - wg := sync.WaitGroup{} for x := range exchange.Exchanges { - wg.Add(1) - go func(x int) { - defer wg.Done() - exch, err := NewExchangeByNameWithDefaults(context.Background(), exchange.Exchanges[x]) + name := exchange.Exchanges[x] + t.Run(name, func(t *testing.T) { + t.Parallel() + if isCITest() && common.StringDataContains(blockedCIExchanges, name) { + t.Skipf("skipping %s due to CI test restrictions", name) + } + if common.StringDataContains(unsupportedDefaultConfigExchanges, name) { + t.Skipf("skipping %s unsupported", name) + } + exch, err := NewExchangeByNameWithDefaults(context.Background(), name) if err != nil { - ch <- err - return + t.Error(err) } - - if !strings.EqualFold(exch.GetName(), exchange.Exchanges[x]) { - ch <- fmt.Errorf("received: '%v' but expected: '%v'", exch.GetName(), exchange.Exchanges[x]) + if !strings.EqualFold(exch.GetName(), name) { + t.Errorf("received: '%v' but expected: '%v'", exch.GetName(), name) } - }(x) - } - wg.Wait() - -outta: - for { - select { - case err := <-ch: - t.Error(err) - default: - break outta - } + }) } } diff --git a/engine/order_manager.go b/engine/order_manager.go index 8224a3d3..32ed6f15 100644 --- a/engine/order_manager.go +++ b/engine/order_manager.go @@ -807,7 +807,7 @@ func (m *OrderManager) processFuturesPositions(exch exchange.IBotExchange, posit if !isPerp { return nil } - frp, err := exch.GetFundingRates(context.TODO(), &fundingrate.RatesRequest{ + frp, err := exch.GetHistoricalFundingRates(context.TODO(), &fundingrate.HistoricalRatesRequest{ Asset: position.Asset, Pair: position.Pair, StartDate: position.Orders[0].Date, diff --git a/engine/rpcserver.go b/engine/rpcserver.go index d7f27c68..f2824c9e 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -4463,8 +4463,8 @@ func (s *RPCServer) GetFuturesPositionsSummary(ctx context.Context, r *gctrpc.Ge if !stats.AverageOpenPrice.IsZero() { positionStats.AverageOpenPrice = stats.AverageOpenPrice.String() } - if !stats.PositionPNL.IsZero() { - positionStats.RecentPnl = stats.PositionPNL.String() + if !stats.UnrealisedPNL.IsZero() { + positionStats.RecentPnl = stats.UnrealisedPNL.String() } if !stats.MaintenanceMarginFraction.IsZero() { positionStats.MarginFraction = stats.MaintenanceMarginFraction.String() @@ -4701,7 +4701,7 @@ func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRat return nil, fmt.Errorf("%w %v", errPairNotEnabled, cp) } - funding, err := exch.GetFundingRates(ctx, &fundingrate.RatesRequest{ + funding, err := exch.GetHistoricalFundingRates(ctx, &fundingrate.HistoricalRatesRequest{ Asset: a, Pair: cp, StartDate: start, @@ -4799,7 +4799,7 @@ func (s *RPCServer) GetLatestFundingRate(ctx context.Context, r *gctrpc.GetLates return nil, fmt.Errorf("%w %v", errPairNotEnabled, cp) } - funding, err := exch.GetLatestFundingRate(ctx, &fundingrate.LatestRateRequest{ + fundingRates, err := exch.GetLatestFundingRates(ctx, &fundingrate.LatestRateRequest{ Asset: a, Pair: cp, IncludePredictedRate: r.IncludePredicted, @@ -4807,27 +4807,30 @@ func (s *RPCServer) GetLatestFundingRate(ctx context.Context, r *gctrpc.GetLates if err != nil { return nil, err } + if len(fundingRates) != 1 { + return nil, fmt.Errorf("expected 1 funding rate, received %v", len(fundingRates)) + } var response gctrpc.GetLatestFundingRateResponse fundingData := &gctrpc.FundingData{ Exchange: r.Exchange, Asset: r.Asset, Pair: &gctrpc.CurrencyPair{ - Delimiter: funding.Pair.Delimiter, - Base: funding.Pair.Base.String(), - Quote: funding.Pair.Quote.String(), + Delimiter: fundingRates[0].Pair.Delimiter, + Base: fundingRates[0].Pair.Base.String(), + Quote: fundingRates[0].Pair.Quote.String(), }, LatestRate: &gctrpc.FundingRate{ - Date: funding.LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone), - Rate: funding.LatestRate.Rate.String(), + Date: fundingRates[0].LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone), + Rate: fundingRates[0].LatestRate.Rate.String(), }, } - if !funding.TimeOfNextRate.IsZero() { - fundingData.TimeOfNextRate = funding.TimeOfNextRate.Format(common.SimpleTimeFormatWithTimezone) + if !fundingRates[0].TimeOfNextRate.IsZero() { + fundingData.TimeOfNextRate = fundingRates[0].TimeOfNextRate.Format(common.SimpleTimeFormatWithTimezone) } if r.IncludePredicted { fundingData.UpcomingRate = &gctrpc.FundingRate{ - Date: funding.PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone), - Rate: funding.PredictedUpcomingRate.Rate.String(), + Date: fundingRates[0].PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone), + Rate: fundingRates[0].PredictedUpcomingRate.Rate.String(), } } response.Rate = fundingData diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index 522c66ee..3b2dd8ce 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -70,7 +70,7 @@ func (f fExchange) GetFuturesPositionSummary(context.Context, *futures.PositionS MarkPrice: leet, CurrentSize: leet, AverageOpenPrice: leet, - PositionPNL: leet, + UnrealisedPNL: leet, MaintenanceMarginFraction: leet, FreeCollateral: leet, TotalCollateral: leet, @@ -141,29 +141,31 @@ func (f fExchange) GetFuturesPositionOrders(_ context.Context, req *futures.Posi return resp, nil } -func (f fExchange) GetLatestFundingRate(_ context.Context, request *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) { +func (f fExchange) GetLatestFundingRates(_ context.Context, request *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { leet := decimal.NewFromInt(1337) - return &fundingrate.LatestRateResponse{ - Exchange: f.GetName(), - Asset: request.Asset, - Pair: request.Pair, - LatestRate: fundingrate.Rate{ - Time: time.Now(), - Rate: leet, - Payment: leet, + return []fundingrate.LatestRateResponse{ + { + Exchange: f.GetName(), + Asset: request.Asset, + Pair: request.Pair, + LatestRate: fundingrate.Rate{ + Time: time.Now(), + Rate: leet, + Payment: leet, + }, + PredictedUpcomingRate: fundingrate.Rate{ + Time: time.Now(), + Rate: leet, + Payment: leet, + }, + TimeOfNextRate: time.Now(), }, - PredictedUpcomingRate: fundingrate.Rate{ - Time: time.Now(), - Rate: leet, - Payment: leet, - }, - TimeOfNextRate: time.Now(), }, nil } -func (f fExchange) GetFundingRates(_ context.Context, request *fundingrate.RatesRequest) (*fundingrate.Rates, error) { +func (f fExchange) GetHistoricalFundingRates(_ context.Context, request *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { leet := decimal.NewFromInt(1337) - return &fundingrate.Rates{ + return &fundingrate.HistoricalRates{ Exchange: f.GetName(), Asset: request.Asset, Pair: request.Pair, diff --git a/engine/sync_manager.go b/engine/sync_manager.go index 66dd63e1..52dd075e 100644 --- a/engine/sync_manager.go +++ b/engine/sync_manager.go @@ -11,6 +11,7 @@ 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" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -39,8 +40,8 @@ var ( errCouldNotSyncNewData = errors.New("could not sync new data") ) -// setupSyncManager starts a new CurrencyPairSyncer -func setupSyncManager(c *config.SyncManagerConfig, exchangeManager iExchangeManager, remoteConfig *config.RemoteControlConfig, websocketRoutineManagerEnabled bool) (*syncManager, error) { +// SetupSyncManager creates a new CurrencyPairSyncer +func SetupSyncManager(c *config.SyncManagerConfig, exchangeManager iExchangeManager, remoteConfig *config.RemoteControlConfig, websocketRoutineManagerEnabled bool) (*SyncManager, error) { if c == nil { return nil, fmt.Errorf("%T %w", c, common.ErrNilPointer) } @@ -79,15 +80,15 @@ func setupSyncManager(c *config.SyncManagerConfig, exchangeManager iExchangeMana return nil, fmt.Errorf("%T %w", c.PairFormatDisplay, common.ErrNilPointer) } - s := &syncManager{ + s := &SyncManager{ config: *c, remoteConfig: remoteConfig, exchangeManager: exchangeManager, websocketRoutineManagerEnabled: websocketRoutineManagerEnabled, fiatDisplayCurrency: c.FiatDisplayCurrency, format: *c.PairFormatDisplay, - tickerBatchLastRequested: make(map[string]time.Time), - currencyPairs: make(map[currencyPairKey]*currencyPairSyncAgent), + tickerBatchLastRequested: make(map[key.ExchangeAsset]time.Time), + currencyPairs: make(map[key.ExchangePairAsset]*currencyPairSyncAgent), } log.Debugf(log.SyncMgr, @@ -102,12 +103,12 @@ func setupSyncManager(c *config.SyncManagerConfig, exchangeManager iExchangeMana } // IsRunning safely checks whether the subsystem is running -func (m *syncManager) IsRunning() bool { +func (m *SyncManager) IsRunning() bool { return m != nil && atomic.LoadInt32(&m.started) == 1 } // Start runs the subsystem -func (m *syncManager) Start() error { +func (m *SyncManager) Start() error { if m == nil { return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem) } @@ -175,10 +176,11 @@ func (m *syncManager) Start() error { continue } for i := range enabledPairs { - k := currencyPairKey{ - AssetType: assetTypes[y], - Exchange: exchangeName, - Pair: enabledPairs[i].Format(currency.PairFormat{Uppercase: true}), + k := key.ExchangePairAsset{ + Asset: assetTypes[y], + Exchange: exchangeName, + Base: enabledPairs[i].Base.Item, + Quote: enabledPairs[i].Quote.Item, } if e := m.get(k); e != nil { continue @@ -235,7 +237,7 @@ func (m *syncManager) Start() error { } // Stop shuts down the exchange currency pair syncer -func (m *syncManager) Stop() error { +func (m *SyncManager) Stop() error { if m == nil { return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem) } @@ -248,22 +250,23 @@ func (m *syncManager) Stop() error { return nil } -func (m *syncManager) get(k currencyPairKey) *currencyPairSyncAgent { +func (m *SyncManager) get(k key.ExchangePairAsset) *currencyPairSyncAgent { m.mux.Lock() defer m.mux.Unlock() return m.currencyPairs[k] } -func newCurrencyPairSyncAgent(k currencyPairKey) *currencyPairSyncAgent { +func newCurrencyPairSyncAgent(k key.ExchangePairAsset) *currencyPairSyncAgent { return ¤cyPairSyncAgent{ - currencyPairKey: k, - Created: time.Now(), - locks: make([]sync.Mutex, SyncItemTrade+1), - trackers: make([]*syncBase, SyncItemTrade+1), + Key: k, + Pair: currency.NewPair(k.Base.Currency(), k.Quote.Currency()), + Created: time.Now(), + locks: make([]sync.Mutex, SyncItemTrade+1), + trackers: make([]*syncBase, SyncItemTrade+1), } } -func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent { +func (m *SyncManager) add(k key.ExchangePairAsset, s syncBase) *currencyPairSyncAgent { m.mux.Lock() defer m.mux.Unlock() @@ -288,7 +291,9 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent if m.config.Verbose { log.Debugf(log.SyncMgr, "%s: Added ticker sync item %v: using websocket: %v using REST: %v", - c.Exchange, m.FormatCurrency(c.Pair).String(), c.trackers[SyncItemTicker].IsUsingWebsocket, + c.Key.Exchange, + m.FormatCurrency(c.Pair), + c.trackers[SyncItemTicker].IsUsingWebsocket, c.trackers[SyncItemTicker].IsUsingREST) } if atomic.LoadInt32(&m.initSyncCompleted) != 1 { @@ -301,7 +306,9 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent if m.config.Verbose { log.Debugf(log.SyncMgr, "%s: Added orderbook sync item %v: using websocket: %v using REST: %v", - c.Exchange, m.FormatCurrency(c.Pair).String(), c.trackers[SyncItemOrderbook].IsUsingWebsocket, + k.Exchange, + m.FormatCurrency(c.Pair), + c.trackers[SyncItemOrderbook].IsUsingWebsocket, c.trackers[SyncItemOrderbook].IsUsingREST) } if atomic.LoadInt32(&m.initSyncCompleted) != 1 { @@ -314,7 +321,9 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent if m.config.Verbose { log.Debugf(log.SyncMgr, "%s: Added trade sync item %v: using websocket: %v using REST: %v", - c.Exchange, m.FormatCurrency(c.Pair).String(), c.trackers[SyncItemTrade].IsUsingWebsocket, + k.Exchange, + m.FormatCurrency(c.Pair), + c.trackers[SyncItemTrade].IsUsingWebsocket, c.trackers[SyncItemTrade].IsUsingREST) } if atomic.LoadInt32(&m.initSyncCompleted) != 1 { @@ -324,7 +333,7 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent } if m.currencyPairs == nil { - m.currencyPairs = make(map[currencyPairKey]*currencyPairSyncAgent) + m.currencyPairs = make(map[key.ExchangePairAsset]*currencyPairSyncAgent) } m.currencyPairs[k] = c @@ -332,9 +341,9 @@ func (m *syncManager) add(k currencyPairKey, s syncBase) *currencyPairSyncAgent return c } -// WebsocketUpdate notifies the syncManager to change the last updated time for a exchange asset pair +// WebsocketUpdate notifies the SyncManager to change the last updated time for a exchange asset pair // And set IsUsingWebsocket to true. It should be used externally only from websocket updaters -func (m *syncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a asset.Item, syncType syncItemType, err error) error { +func (m *SyncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a asset.Item, syncType syncItemType, err error) error { if m == nil { return fmt.Errorf("exchange CurrencyPairSyncer %w", ErrNilSubsystem) } @@ -362,15 +371,22 @@ func (m *syncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a as return fmt.Errorf("%v %w", syncType, errUnknownSyncItem) } - k := currencyPairKey{ - AssetType: a, - Exchange: exchangeName, - Pair: p.Format(currency.PairFormat{Uppercase: true}), + k := key.ExchangePairAsset{ + Asset: a, + Exchange: exchangeName, + Base: p.Base.Item, + Quote: p.Quote.Item, } c, exists := m.currencyPairs[k] if !exists { - return fmt.Errorf("%w for %s %s %s %s", errCouldNotSyncNewData, k.Exchange, k.Pair, k.AssetType, syncType) + return fmt.Errorf("%w for %s %s %s %s %s", + errCouldNotSyncNewData, + k.Exchange, + k.Base, + k.Quote, + k.Asset, + syncType) } c.locks[syncType].Lock() @@ -387,9 +403,9 @@ func (m *syncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a as if m.config.LogSwitchProtocolEvents { log.Warnf(log.SyncMgr, "%s %s %s: %s Websocket re-enabled, switching from rest to websocket", - c.Exchange, + k.Exchange, m.FormatCurrency(c.Pair), - strings.ToUpper(c.AssetType.String()), + strings.ToUpper(k.Asset.String()), syncType, ) } @@ -398,8 +414,8 @@ func (m *syncManager) WebsocketUpdate(exchangeName string, p currency.Pair, a as return m.update(c, syncType, err) } -// update notifies the syncManager to change the last updated time for a exchange asset pair -func (m *syncManager) update(c *currencyPairSyncAgent, syncType syncItemType, err error) error { +// update notifies the SyncManager to change the last updated time for a exchange asset pair +func (m *SyncManager) update(c *currencyPairSyncAgent, syncType syncItemType, err error) error { if syncType < SyncItemTicker || syncType > SyncItemTrade { return fmt.Errorf("%v %w", syncType, errUnknownSyncItem) } @@ -416,7 +432,7 @@ func (m *syncManager) update(c *currencyPairSyncAgent, syncType syncItemType, er removedCounter++ if m.config.LogInitialSyncEvents { log.Debugf(log.SyncMgr, "%s %s sync complete %v [%d/%d].", - c.Exchange, + c.Key.Exchange, syncType, m.FormatCurrency(c.Pair), removedCounter, @@ -428,7 +444,7 @@ func (m *syncManager) update(c *currencyPairSyncAgent, syncType syncItemType, er return nil } -func (m *syncManager) worker() { +func (m *SyncManager) worker() { cleanup := func() { log.Debugln(log.SyncMgr, "Exchange CurrencyPairSyncer worker shutting down.") @@ -492,10 +508,11 @@ func (m *syncManager) worker() { return } - k := currencyPairKey{ - AssetType: assetTypes[y], - Exchange: exchangeName, - Pair: enabledPairs[i].Format(currency.PairFormat{Uppercase: true}), + k := key.ExchangePairAsset{ + Asset: assetTypes[y], + Exchange: exchangeName, + Base: enabledPairs[i].Base.Item, + Quote: enabledPairs[i].Quote.Item, } c := m.get(k) if c == nil { @@ -521,7 +538,7 @@ func (m *syncManager) worker() { } } -func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchange) { +func (m *SyncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchange) { if !c.locks[SyncItemTicker].TryLock() { return } @@ -541,9 +558,9 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan if m.config.LogSwitchProtocolEvents { log.Warnf(log.SyncMgr, "%s %s %s: No ticker update after %s, switching from websocket to rest", - c.Exchange, + c.Key.Exchange, m.FormatCurrency(c.Pair), - strings.ToUpper(c.AssetType.String()), + strings.ToUpper(c.Key.Asset.String()), m.config.TimeoutWebsocket, ) } @@ -555,9 +572,15 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan if e.SupportsRESTTickerBatchUpdates() { m.mux.Lock() - batchLastDone, ok := m.tickerBatchLastRequested[e.GetName()] + batchLastDone, ok := m.tickerBatchLastRequested[key.ExchangeAsset{ + Exchange: c.Key.Exchange, + Asset: c.Key.Asset, + }] if !ok { - m.tickerBatchLastRequested[exchangeName] = time.Time{} + m.tickerBatchLastRequested[key.ExchangeAsset{ + Exchange: c.Key.Exchange, + Asset: c.Key.Asset, + }] = time.Time{} } m.mux.Unlock() @@ -566,11 +589,14 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan if m.config.Verbose { log.Debugf(log.SyncMgr, "Initialising %s REST ticker batching", exchangeName) } - err = e.UpdateTickers(context.TODO(), c.AssetType) + err = e.UpdateTickers(context.TODO(), c.Key.Asset) if err == nil { - result, err = e.FetchTicker(context.TODO(), c.Pair, c.AssetType) + result, err = e.FetchTicker(context.TODO(), c.Pair, c.Key.Asset) } - m.tickerBatchLastRequested[exchangeName] = time.Now() + m.tickerBatchLastRequested[key.ExchangeAsset{ + Exchange: c.Key.Exchange, + Asset: c.Key.Asset, + }] = time.Now() m.mux.Unlock() } else { if m.config.Verbose { @@ -578,17 +604,17 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan } result, err = e.FetchTicker(context.TODO(), c.Pair, - c.AssetType) + c.Key.Asset) } } else { result, err = e.UpdateTicker(context.TODO(), c.Pair, - c.AssetType) + c.Key.Asset) } m.PrintTickerSummary(result, "REST", err) if err == nil { if m.remoteConfig.WebsocketRPC.Enabled { - relayWebsocketEvent(result, "ticker_update", c.AssetType.String(), exchangeName) + relayWebsocketEvent(result, "ticker_update", c.Key.Asset.String(), exchangeName) } } updateErr := m.update(c, SyncItemTicker, err) @@ -598,7 +624,7 @@ func (m *syncManager) syncTicker(c *currencyPairSyncAgent, e exchange.IBotExchan } } -func (m *syncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExchange) { +func (m *SyncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExchange) { if !c.locks[SyncItemOrderbook].TryLock() { return } @@ -622,9 +648,9 @@ func (m *syncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExc if m.config.LogSwitchProtocolEvents { log.Warnf(log.SyncMgr, "%s %s %s: No orderbook update after %s, switching from websocket to rest", - c.Exchange, - m.FormatCurrency(c.Pair).String(), - strings.ToUpper(c.AssetType.String()), + c.Key.Exchange, + m.FormatCurrency(c.Pair), + strings.ToUpper(c.Key.Asset.String()), m.config.TimeoutWebsocket, ) } @@ -633,11 +659,11 @@ func (m *syncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExc if s.IsUsingREST && time.Since(s.LastUpdated) > m.config.TimeoutREST { result, err := e.UpdateOrderbook(context.TODO(), c.Pair, - c.AssetType) + c.Key.Asset) m.PrintOrderbookSummary(result, "REST", err) if err == nil { if m.remoteConfig.WebsocketRPC.Enabled { - relayWebsocketEvent(result, "orderbook_update", c.AssetType.String(), e.GetName()) + relayWebsocketEvent(result, "orderbook_update", c.Key.Asset.String(), e.GetName()) } } updateErr := m.update(c, SyncItemOrderbook, err) @@ -647,7 +673,7 @@ func (m *syncManager) syncOrderbook(c *currencyPairSyncAgent, e exchange.IBotExc } } -func (m *syncManager) syncTrades(c *currencyPairSyncAgent) { +func (m *SyncManager) syncTrades(c *currencyPairSyncAgent) { if !c.locks[SyncItemTrade].TryLock() { return } @@ -703,7 +729,7 @@ func printConvertCurrencyFormat(origPrice float64, origCurrency, displayCurrency } // PrintTickerSummary outputs the ticker results -func (m *syncManager) PrintTickerSummary(result *ticker.Price, protocol string, err error) { +func (m *SyncManager) PrintTickerSummary(result *ticker.Price, protocol string, err error) { if m == nil || atomic.LoadInt32(&m.started) == 0 { return } @@ -777,11 +803,11 @@ func (m *syncManager) PrintTickerSummary(result *ticker.Price, protocol string, // FormatCurrency is a method that formats and returns a currency pair // based on the user currency display preferences -func (m *syncManager) FormatCurrency(p currency.Pair) currency.Pair { +func (m *SyncManager) FormatCurrency(cp currency.Pair) string { if m == nil || atomic.LoadInt32(&m.started) == 0 { - return p + return "" } - return p.Format(m.format) + return m.format.Format(cp) } const ( @@ -789,7 +815,7 @@ const ( ) // PrintOrderbookSummary outputs orderbook results -func (m *syncManager) PrintOrderbookSummary(result *orderbook.Base, protocol string, err error) { +func (m *SyncManager) PrintOrderbookSummary(result *orderbook.Base, protocol string, err error) { if m == nil || atomic.LoadInt32(&m.started) == 0 { return } @@ -863,7 +889,7 @@ func (m *syncManager) PrintOrderbookSummary(result *orderbook.Base, protocol str // WaitForInitialSync allows for a routine to wait for an initial sync to be // completed without exposing the underlying type. This needs to be called in a // separate routine. -func (m *syncManager) WaitForInitialSync() error { +func (m *SyncManager) WaitForInitialSync() error { if m == nil { return fmt.Errorf("sync manager %w", ErrNilSubsystem) } diff --git a/engine/sync_manager_test.go b/engine/sync_manager_test.go index adf49e76..950be921 100644 --- a/engine/sync_manager_test.go +++ b/engine/sync_manager_test.go @@ -6,6 +6,7 @@ import ( "testing" "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/exchanges/asset" @@ -15,42 +16,42 @@ import ( func TestSetupSyncManager(t *testing.T) { t.Parallel() - _, err := setupSyncManager(nil, nil, nil, false) + _, err := SetupSyncManager(nil, nil, nil, false) if !errors.Is(err, common.ErrNilPointer) { t.Errorf("error '%v', expected '%v'", err, common.ErrNilPointer) } - _, err = setupSyncManager(&config.SyncManagerConfig{}, nil, nil, false) + _, err = SetupSyncManager(&config.SyncManagerConfig{}, nil, nil, false) if !errors.Is(err, errNoSyncItemsEnabled) { t.Errorf("error '%v', expected '%v'", err, errNoSyncItemsEnabled) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, nil, nil, false) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, nil, nil, false) if !errors.Is(err, errNilExchangeManager) { t.Errorf("error '%v', expected '%v'", err, errNilExchangeManager) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, &ExchangeManager{}, nil, false) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, &ExchangeManager{}, nil, false) if !errors.Is(err, errNilConfig) { t.Errorf("error '%v', expected '%v'", err, errNilConfig) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, currency.ErrCurrencyCodeEmpty) { t.Errorf("error '%v', expected '%v'", err, currency.ErrCurrencyCodeEmpty) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.BTC}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.BTC}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, currency.ErrFiatDisplayCurrencyIsNotFiat) { t.Errorf("error '%v', expected '%v'", err, currency.ErrFiatDisplayCurrencyIsNotFiat) } - _, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + _, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, common.ErrNilPointer) { t.Errorf("error '%v', expected '%v'", err, common.ErrNilPointer) } - m, err := setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + m, err := SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -61,7 +62,7 @@ func TestSetupSyncManager(t *testing.T) { func TestSyncManagerStart(t *testing.T) { t.Parallel() - m, err := setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) + m, err := SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, &ExchangeManager{}, &config.RemoteControlConfig{}, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -96,7 +97,7 @@ func TestSyncManagerStart(t *testing.T) { func TestSyncManagerStop(t *testing.T) { t.Parallel() - var m *syncManager + var m *SyncManager err := m.Stop() if !errors.Is(err, ErrNilSubsystem) { t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) @@ -112,7 +113,7 @@ func TestSyncManagerStop(t *testing.T) { if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } - m, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) + m, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -150,7 +151,7 @@ func TestPrintConvertCurrencyFormat(t *testing.T) { func TestPrintTickerSummary(t *testing.T) { t.Parallel() - var m *syncManager + var m *SyncManager m.PrintTickerSummary(&ticker.Price{}, "REST", nil) em := NewExchangeManager() @@ -163,7 +164,7 @@ func TestPrintTickerSummary(t *testing.T) { if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } - m, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) + m, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -192,7 +193,7 @@ func TestPrintTickerSummary(t *testing.T) { func TestPrintOrderbookSummary(t *testing.T) { t.Parallel() - var m *syncManager + var m *SyncManager m.PrintOrderbookSummary(nil, "REST", nil) em := NewExchangeManager() @@ -205,7 +206,7 @@ func TestPrintOrderbookSummary(t *testing.T) { if !errors.Is(err, nil) { t.Fatalf("received: '%v' but expected: '%v'", err, nil) } - m, err = setupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) + m, err = SetupSyncManager(&config.SyncManagerConfig{SynchronizeTrades: true, SynchronizeContinuously: true, FiatDisplayCurrency: currency.USD, PairFormatDisplay: ¤cy.EMPTYFORMAT}, em, &config.RemoteControlConfig{}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -242,13 +243,13 @@ func TestRelayWebsocketEvent(t *testing.T) { } func TestWaitForInitialSync(t *testing.T) { - var m *syncManager + var m *SyncManager err := m.WaitForInitialSync() if !errors.Is(err, ErrNilSubsystem) { t.Fatalf("received %v, but expected: %v", err, ErrNilSubsystem) } - m = &syncManager{} + m = &SyncManager{} err = m.WaitForInitialSync() if !errors.Is(err, ErrSubSystemNotStarted) { t.Fatalf("received %v, but expected: %v", err, ErrSubSystemNotStarted) @@ -263,13 +264,13 @@ func TestWaitForInitialSync(t *testing.T) { func TestSyncManagerWebsocketUpdate(t *testing.T) { t.Parallel() - var m *syncManager + var m *SyncManager err := m.WebsocketUpdate("", currency.EMPTYPAIR, 1, 47, nil) if !errors.Is(err, ErrNilSubsystem) { t.Fatalf("received %v, but expected: %v", err, ErrNilSubsystem) } - m = &syncManager{} + m = &SyncManager{} err = m.WebsocketUpdate("", currency.EMPTYPAIR, 1, 47, nil) if !errors.Is(err, ErrSubSystemNotStarted) { t.Fatalf("received %v, but expected: %v", err, ErrSubSystemNotStarted) @@ -314,9 +315,8 @@ func TestSyncManagerWebsocketUpdate(t *testing.T) { t.Fatalf("received %v, but expected: %v", err, errCouldNotSyncNewData) } - m.add(currencyPairKey{ - AssetType: asset.Spot, - Pair: currency.EMPTYPAIR.Format(currency.PairFormat{Uppercase: true}), + m.add(key.ExchangePairAsset{ + Asset: asset.Spot, }, syncBase{}) m.initSyncWG.Add(3) // orderbook match diff --git a/engine/sync_manager_types.go b/engine/sync_manager_types.go index aed9509c..46807832 100644 --- a/engine/sync_manager_types.go +++ b/engine/sync_manager_types.go @@ -4,9 +4,9 @@ import ( "sync" "time" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" - "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) // syncBase stores information @@ -18,23 +18,17 @@ type syncBase struct { NumErrors int } -// currencyPairKey is the map key for the sync agents -type currencyPairKey struct { - Exchange string - AssetType asset.Item - Pair currency.Pair -} - // currencyPairSyncAgent stores the sync agent info type currencyPairSyncAgent struct { - currencyPairKey + Key key.ExchangePairAsset + Pair currency.Pair Created time.Time trackers []*syncBase locks []sync.Mutex } -// syncManager stores the exchange currency pair syncer object -type syncManager struct { +// SyncManager stores the exchange currency pair syncer object +type SyncManager struct { initSyncCompleted int32 initSyncStarted int32 started int32 @@ -47,8 +41,8 @@ type syncManager struct { initSyncWG sync.WaitGroup inService sync.WaitGroup - currencyPairs map[currencyPairKey]*currencyPairSyncAgent - tickerBatchLastRequested map[string]time.Time + currencyPairs map[key.ExchangePairAsset]*currencyPairSyncAgent + tickerBatchLastRequested map[key.ExchangeAsset]time.Time remoteConfig *config.RemoteControlConfig config config.SyncManagerConfig diff --git a/engine/websocketroutine_manager.go b/engine/websocketroutine_manager.go index 6494b5e1..bec886be 100644 --- a/engine/websocketroutine_manager.go +++ b/engine/websocketroutine_manager.go @@ -242,6 +242,10 @@ func (m *WebsocketRoutineManager) websocketDataHandler(exchName string, data int } m.syncer.PrintTickerSummary(&d[x], "websocket", err) } + case order.Detail, + ticker.Price, + orderbook.Depth: + return errUseAPointer case stream.KlineData: if m.verbose { log.Infof(log.WebsocketMgr, "%s websocket %s %s kline updated %+v", diff --git a/engine/websocketroutine_manager_test.go b/engine/websocketroutine_manager_test.go index 6428d47e..e082193b 100644 --- a/engine/websocketroutine_manager_test.go +++ b/engine/websocketroutine_manager_test.go @@ -26,17 +26,17 @@ func TestWebsocketRoutineManagerSetup(t *testing.T) { if !errors.Is(err, errNilCurrencyPairSyncer) { t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairSyncer) } - _, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, nil, false) + _, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, nil, false) if !errors.Is(err, errNilCurrencyConfig) { t.Errorf("error '%v', expected '%v'", err, errNilCurrencyConfig) } - _, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{}, true) + _, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, ¤cy.Config{}, true) if !errors.Is(err, errNilCurrencyPairFormat) { t.Errorf("error '%v', expected '%v'", err, errNilCurrencyPairFormat) } - m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) + m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -55,7 +55,7 @@ func TestWebsocketRoutineManagerStart(t *testing.T) { Uppercase: false, Delimiter: "-", }} - m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, cfg, true) + m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, cfg, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -75,7 +75,7 @@ func TestWebsocketRoutineManagerIsRunning(t *testing.T) { t.Error("expected false") } - m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) + m, err := setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -102,7 +102,7 @@ func TestWebsocketRoutineManagerStop(t *testing.T) { t.Errorf("error '%v', expected '%v'", err, ErrNilSubsystem) } - m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &syncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) + m, err = setupWebsocketRoutineManager(NewExchangeManager(), &OrderManager{}, &SyncManager{}, ¤cy.Config{CurrencyPairFormat: ¤cy.PairFormat{}}, false) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } @@ -146,7 +146,7 @@ func TestWebsocketRoutineManagerHandleData(t *testing.T) { Uppercase: false, Delimiter: "-", }} - m, err := setupWebsocketRoutineManager(em, om, &syncManager{}, cfg, true) + m, err := setupWebsocketRoutineManager(em, om, &SyncManager{}, cfg, true) if !errors.Is(err, nil) { t.Errorf("error '%v', expected '%v'", err, nil) } diff --git a/engine/websocketroutine_manager_types.go b/engine/websocketroutine_manager_types.go index 17780f26..0aa4236e 100644 --- a/engine/websocketroutine_manager_types.go +++ b/engine/websocketroutine_manager_types.go @@ -14,6 +14,7 @@ var ( errNilWebsocketDataHandlerFunction = errors.New("websocket data handler function is nil") errNilWebsocket = errors.New("websocket is nil") errRoutineManagerNotStarted = errors.New("websocket routine manager not started") + errUseAPointer = errors.New("could not process, pass to websocket routine manager as a pointer") ) const ( diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index 4cc7964a..604afc0b 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -14,6 +14,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -482,3 +483,8 @@ func (a *Alphapoint) GetHistoricCandlesExtended(_ context.Context, _ currency.Pa func (a *Alphapoint) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (a *Alphapoint) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/binance/binance_cfutures.go b/exchanges/binance/binance_cfutures.go index a346663e..8e919b3f 100644 --- a/exchanges/binance/binance_cfutures.go +++ b/exchanges/binance/binance_cfutures.go @@ -31,6 +31,7 @@ const ( cfuturesContinuousKline = "/dapi/v1/continuousKlines?" cfuturesIndexKline = "/dapi/v1/indexPriceKlines?" cfuturesMarkPriceKline = "/dapi/v1/markPriceKlines?" + cfuturesFundingRateInfo = "/dapi/v1/fundingInfo?" cfuturesMarkPrice = "/dapi/v1/premiumIndex?" cfuturesFundingRateHistory = "/dapi/v1/fundingRate?" cfuturesTickerPriceStats = "/dapi/v1/ticker/24hr?" @@ -236,6 +237,13 @@ func (b *Binance) GetIndexAndMarkPrice(ctx context.Context, symbol, pair string) return resp, b.SendHTTPRequest(ctx, exchange.RestCoinMargined, cfuturesMarkPrice+params.Encode(), cFuturesIndexMarkPriceRate, &resp) } +// GetFundingRateInfo returns extra details about funding rates +func (b *Binance) GetFundingRateInfo(ctx context.Context) ([]FundingRateInfoResponse, error) { + params := url.Values{} + var resp []FundingRateInfoResponse + return resp, b.SendHTTPRequest(ctx, exchange.RestCoinMargined, cfuturesFundingRateInfo+params.Encode(), uFuturesDefaultRate, &resp) +} + // GetFuturesKlineData gets futures kline data for CoinMarginedFutures, func (b *Binance) GetFuturesKlineData(ctx context.Context, symbol currency.Pair, interval string, limit int64, startTime, endTime time.Time) ([]FuturesCandleStick, error) { params := url.Values{} diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 8cc074b0..e4c53d99 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" @@ -2819,7 +2820,7 @@ func TestUpdateOrderExecutionLimits(t *testing.T) { func TestGetFundingRates(t *testing.T) { t.Parallel() s, e := getTime() - _, err := b.GetFundingRates(context.Background(), &fundingrate.RatesRequest{ + _, err := b.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ Asset: asset.USDTMarginedFutures, Pair: currency.NewPair(currency.BTC, currency.USDT), StartDate: s, @@ -2831,7 +2832,7 @@ func TestGetFundingRates(t *testing.T) { t.Error(err) } - _, err = b.GetFundingRates(context.Background(), &fundingrate.RatesRequest{ + _, err = b.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ Asset: asset.USDTMarginedFutures, Pair: currency.NewPair(currency.BTC, currency.USDT), StartDate: s, @@ -2842,7 +2843,7 @@ func TestGetFundingRates(t *testing.T) { t.Error(err) } - r := &fundingrate.RatesRequest{ + r := &fundingrate.HistoricalRatesRequest{ Asset: asset.USDTMarginedFutures, Pair: currency.NewPair(currency.BTC, currency.USDT), StartDate: s, @@ -2851,7 +2852,7 @@ func TestGetFundingRates(t *testing.T) { if sharedtestvalues.AreAPICredentialsSet(b) { r.IncludePayments = true } - _, err = b.GetFundingRates(context.Background(), r) + _, err = b.GetHistoricalFundingRates(context.Background(), r) if err != nil { t.Error(err) } @@ -2861,37 +2862,36 @@ func TestGetFundingRates(t *testing.T) { if err != nil { t.Fatal(err) } - _, err = b.GetFundingRates(context.Background(), r) + _, err = b.GetHistoricalFundingRates(context.Background(), r) if err != nil { t.Error(err) } } -func TestGetLatestFundingRate(t *testing.T) { +func TestGetLatestFundingRates(t *testing.T) { t.Parallel() - _, err := b.GetLatestFundingRate(context.Background(), &fundingrate.LatestRateRequest{ + cp := currency.NewPair(currency.BTC, currency.USDT) + _, err := b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ Asset: asset.USDTMarginedFutures, - Pair: currency.NewPair(currency.BTC, currency.USDT), + Pair: cp, IncludePredictedRate: true, }) if !errors.Is(err, common.ErrFunctionNotSupported) { t.Error(err) } - _, err = b.GetLatestFundingRate(context.Background(), &fundingrate.LatestRateRequest{ + err = b.CurrencyPairs.EnablePair(asset.USDTMarginedFutures, cp) + if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) { + t.Fatal(err) + } + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ Asset: asset.USDTMarginedFutures, - Pair: currency.NewPair(currency.BTC, currency.USDT), + Pair: cp, }) if err != nil { t.Error(err) } - - cp, err := currency.NewPairFromString("BTCUSD_PERP") - if err != nil { - t.Error(err) - } - _, err = b.GetLatestFundingRate(context.Background(), &fundingrate.LatestRateRequest{ + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ Asset: asset.CoinMarginedFutures, - Pair: cp, }) if err != nil { t.Error(err) @@ -3418,14 +3418,24 @@ func TestGetFuturesContractDetails(t *testing.T) { if !errors.Is(err, asset.ErrNotSupported) { t.Error(err) } - _, err = b.GetFuturesContractDetails(context.Background(), asset.USDTMarginedFutures) if !errors.Is(err, nil) { t.Error(err) } - _, err = b.GetFuturesContractDetails(context.Background(), asset.CoinMarginedFutures) if !errors.Is(err, nil) { t.Error(err) } } + +func TestGetFundingRateInfo(t *testing.T) { + t.Parallel() + _, err := b.GetFundingRateInfo(context.Background()) + assert.NoError(t, err) +} + +func TestUGetFundingRateInfo(t *testing.T) { + t.Parallel() + _, err := b.UGetFundingRateInfo(context.Background()) + assert.NoError(t, err) +} diff --git a/exchanges/binance/binance_ufutures.go b/exchanges/binance/binance_ufutures.go index 6643529a..9ff95dd2 100644 --- a/exchanges/binance/binance_ufutures.go +++ b/exchanges/binance/binance_ufutures.go @@ -30,6 +30,7 @@ const ( ufuturesKlineData = "/fapi/v1/klines?" ufuturesMarkPrice = "/fapi/v1/premiumIndex?" ufuturesFundingRateHistory = "/fapi/v1/fundingRate?" + ufuturesFundingRateInfo = "/fapi/v1/fundingInfo?" ufuturesTickerPriceStats = "/fapi/v1/ticker/24hr?" ufuturesSymbolPriceTicker = "/fapi/v1/ticker/price?" ufuturesSymbolOrderbook = "/fapi/v1/ticker/bookTicker?" @@ -379,6 +380,12 @@ func (b *Binance) UGetMarkPrice(ctx context.Context, symbol currency.Pair) ([]UM return resp, nil } +// UGetFundingRateInfo returns extra details about funding rates +func (b *Binance) UGetFundingRateInfo(ctx context.Context) ([]FundingRateInfoResponse, error) { + var resp []FundingRateInfoResponse + return resp, b.SendHTTPRequest(ctx, exchange.RestUSDTMargined, ufuturesFundingRateInfo, uFuturesDefaultRate, &resp) +} + // UGetFundingHistory gets funding history for USDTMarginedFutures func (b *Binance) UGetFundingHistory(ctx context.Context, symbol currency.Pair, limit int64, startTime, endTime time.Time) ([]FundingRateHistory, error) { var resp []FundingRateHistory diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index dedc56f2..c6577f19 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -149,6 +149,7 @@ func (b *Binance) SetDefaults() { MultiChainDeposits: true, MultiChainWithdrawals: true, HasAssetTypeAccountSegregation: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ TradeFetching: true, @@ -161,6 +162,7 @@ func (b *Binance) SetDefaults() { GetOrders: true, Subscribe: true, Unsubscribe: true, + FundingRateFetching: false, // supported but not implemented // TODO when multi-websocket support added }, WithdrawPermissions: exchange.AutoWithdrawCrypto | exchange.NoFiatWithdrawals, @@ -169,11 +171,17 @@ func (b *Binance) SetDefaults() { Intervals: true, }, FuturesCapabilities: exchange.FuturesCapabilities{ - Positions: true, - Leverage: true, - CollateralMode: true, - FundingRates: true, - FundingRateFrequency: kline.EightHour.Duration(), + Positions: true, + Leverage: true, + CollateralMode: true, + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.FourHour: true, + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.USDTMarginedFutures: true, + }, }, }, Enabled: exchange.FeaturesEnabled{ @@ -1985,7 +1993,7 @@ func (b *Binance) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) err = fmt.Errorf("%w %v", asset.ErrNotSupported, a) } if err != nil { - return fmt.Errorf("cannot update exchange execution limits: %v", err) + return fmt.Errorf("cannot update exchange execution limits: %w", err) } return b.LoadLimits(limits) } @@ -2072,57 +2080,148 @@ func (b *Binance) GetServerTime(ctx context.Context, ai asset.Item) (time.Time, return time.Time{}, fmt.Errorf("%s %w", ai, asset.ErrNotSupported) } -// GetLatestFundingRate returns the latest funding rate for a given asset and currency -func (b *Binance) GetLatestFundingRate(ctx context.Context, r *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) { +// GetLatestFundingRates returns the latest funding rates data +func (b *Binance) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { if r == nil { return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) } if r.IncludePredictedRate { return nil, fmt.Errorf("%w IncludePredictedRate", common.ErrFunctionNotSupported) } - format, err := b.GetPairFormat(r.Asset, true) - if err != nil { - return nil, err - } - fPair := r.Pair.Format(format) - pairRate := fundingrate.LatestRateResponse{ - Exchange: b.Name, - Asset: r.Asset, - Pair: fPair, - } - switch r.Asset { - case asset.USDTMarginedFutures: - var mp []UMarkPrice - mp, err = b.UGetMarkPrice(ctx, r.Pair) + fPair := r.Pair + var err error + if !fPair.IsEmpty() { + var format currency.PairFormat + format, err = b.GetPairFormat(r.Asset, true) if err != nil { return nil, err } - pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime) - pairRate.LatestRate = fundingrate.Rate{ - Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency), - Rate: decimal.NewFromFloat(mp[len(mp)-1].LastFundingRate), + fPair = r.Pair.Format(format) + } + + switch r.Asset { + case asset.USDTMarginedFutures: + var mp []UMarkPrice + var fri []FundingRateInfoResponse + fri, err = b.UGetFundingRateInfo(ctx) + if err != nil { + return nil, err } + + mp, err = b.UGetMarkPrice(ctx, fPair) + if err != nil { + return nil, err + } + resp := make([]fundingrate.LatestRateResponse, 0, len(mp)) + for i := range mp { + var cp currency.Pair + var isEnabled bool + cp, isEnabled, err = b.MatchSymbolCheckEnabled(mp[i].Symbol, r.Asset, true) + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + continue + } + var isPerp bool + isPerp, err = b.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + var fundingRateFrequency int64 + for x := range fri { + if fri[x].Symbol != mp[i].Symbol { + continue + } + fundingRateFrequency = fri[x].FundingIntervalHours + break + } + nft := time.UnixMilli(mp[i].NextFundingTime) + rate := fundingrate.LatestRateResponse{ + TimeChecked: time.Now(), + Exchange: b.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: time.UnixMilli(mp[i].Time).Truncate(time.Hour * time.Duration(fundingRateFrequency)), + Rate: decimal.NewFromFloat(mp[i].LastFundingRate), + }, + } + if nft.Year() == rate.TimeChecked.Year() { + rate.TimeOfNextRate = nft + } + resp = append(resp, rate) + } + if len(resp) == 0 { + return nil, fmt.Errorf("%w %v %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) + } + return resp, nil case asset.CoinMarginedFutures: var mp []IndexMarkPrice mp, err = b.GetIndexAndMarkPrice(ctx, fPair.String(), "") if err != nil { return nil, err } - pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime) - pairRate.LatestRate = fundingrate.Rate{ - Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency), - Rate: mp[len(mp)-1].LastFundingRate.Decimal(), + var fri []FundingRateInfoResponse + fri, err = b.GetFundingRateInfo(ctx) + if err != nil { + return nil, err } - default: - return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported) + + resp := make([]fundingrate.LatestRateResponse, 0, len(mp)) + for i := range mp { + var cp currency.Pair + cp, err = currency.NewPairFromString(mp[i].Symbol) + if err != nil { + return nil, err + } + var isPerp bool + isPerp, err = b.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + var fundingRateFrequency int64 + for x := range fri { + if fri[x].Symbol != mp[i].Symbol { + continue + } + fundingRateFrequency = fri[x].FundingIntervalHours + break + } + nft := time.UnixMilli(mp[i].NextFundingTime) + rate := fundingrate.LatestRateResponse{ + TimeChecked: time.Now(), + Exchange: b.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: time.UnixMilli(mp[i].Time).Truncate(time.Duration(fundingRateFrequency) * time.Hour), + Rate: mp[i].LastFundingRate.Decimal(), + }, + } + if nft.Year() == rate.TimeChecked.Year() { + rate.TimeOfNextRate = nft + } + resp = append(resp, rate) + } + if len(resp) == 0 { + return nil, fmt.Errorf("%w %v %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) + } + return resp, nil } - return &pairRate, nil + return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported) } -// GetFundingRates returns funding rates for a given asset and currency for a time period -func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) (*fundingrate.Rates, error) { +// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period +func (b *Binance) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { if r == nil { - return nil, fmt.Errorf("%w RatesRequest", common.ErrNilPointer) + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) } if r.IncludePredictedRate { return nil, fmt.Errorf("%w GetFundingRates IncludePredictedRate", common.ErrFunctionNotSupported) @@ -2138,7 +2237,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque return nil, err } fPair := r.Pair.Format(format) - pairRate := fundingrate.Rates{ + pairRate := fundingrate.HistoricalRates{ Exchange: b.Name, Asset: r.Asset, Pair: fPair, @@ -2149,6 +2248,20 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque case asset.USDTMarginedFutures: requestLimit := 1000 sd := r.StartDate + var fri []FundingRateInfoResponse + fri, err = b.UGetFundingRateInfo(ctx) + if err != nil { + return nil, err + } + var fundingRateFrequency int64 + fps := fPair.String() + for x := range fri { + if fri[x].Symbol != fps { + continue + } + fundingRateFrequency = fri[x].FundingIntervalHours + break + } for { var frh []FundingRateHistory frh, err = b.UGetFundingHistory(ctx, fPair, int64(requestLimit), sd, r.EndDate) @@ -2172,7 +2285,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque return nil, err } pairRate.LatestRate = fundingrate.Rate{ - Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency), + Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(time.Duration(fundingRateFrequency) * time.Hour), Rate: decimal.NewFromFloat(mp[len(mp)-1].LastFundingRate), } pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime) @@ -2185,7 +2298,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque for j := range income { for x := range pairRate.FundingRates { tt := time.UnixMilli(income[j].Time) - tt = tt.Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency) + tt = tt.Truncate(time.Duration(fundingRateFrequency) * time.Hour) if !tt.Equal(pairRate.FundingRates[x].Time) { continue } @@ -2201,6 +2314,20 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque case asset.CoinMarginedFutures: requestLimit := 1000 sd := r.StartDate + var fri []FundingRateInfoResponse + fri, err = b.GetFundingRateInfo(ctx) + if err != nil { + return nil, err + } + var fundingRateFrequency int64 + fps := fPair.String() + for x := range fri { + if fri[x].Symbol != fps { + continue + } + fundingRateFrequency = fri[x].FundingIntervalHours + break + } for { var frh []FundingRateHistory frh, err = b.FuturesGetFundingHistory(ctx, fPair, int64(requestLimit), sd, r.EndDate) @@ -2224,7 +2351,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque return nil, err } pairRate.LatestRate = fundingrate.Rate{ - Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency), + Time: time.UnixMilli(mp[len(mp)-1].Time).Truncate(time.Duration(fundingRateFrequency) * time.Hour), Rate: mp[len(mp)-1].LastFundingRate.Decimal(), } pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime) @@ -2237,7 +2364,7 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque for j := range income { for x := range pairRate.FundingRates { tt := time.UnixMilli(income[j].Timestamp) - tt = tt.Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency) + tt = tt.Truncate(time.Duration(fundingRateFrequency) * time.Hour) if !tt.Equal(pairRate.FundingRates[x].Time) { continue } @@ -2259,14 +2386,10 @@ func (b *Binance) GetFundingRates(ctx context.Context, r *fundingrate.RatesReque // IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future func (b *Binance) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bool, error) { if a == asset.CoinMarginedFutures { - if cp.Quote.Equal(currency.PERP) { - return true, nil - } + return cp.Quote.Equal(currency.PERP), nil } if a == asset.USDTMarginedFutures { - if cp.Quote.Equal(currency.USDT) || cp.Quote.Equal(currency.BUSD) { - return true, nil - } + return cp.Quote.Equal(currency.USDT) || cp.Quote.Equal(currency.BUSD), nil } return false, nil } @@ -2406,7 +2529,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po var leverage, maintenanceMargin, initialMargin, liquidationPrice, markPrice, positionSize, collateralTotal, collateralUsed, collateralAvailable, - pnl, openPrice, isolatedMargin float64 + unrealisedPNL, openPrice, isolatedMargin float64 for i := range ai.Positions { if ai.Positions[i].Symbol != fPair.String() { @@ -2469,7 +2592,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po } collateralTotal = collateralAsset.WalletBalance collateralAvailable = collateralAsset.AvailableBalance - pnl = collateralAsset.UnrealizedProfit + unrealisedPNL = collateralAsset.UnrealizedProfit c = currency.NewCode(collateralAsset.Asset) if marginType == margin.Multi { isolatedMargin = collateralAsset.CrossUnPnl @@ -2482,7 +2605,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po collateralTotal = ai.TotalWalletBalance collateralUsed = ai.TotalWalletBalance - ai.AvailableBalance collateralAvailable = ai.AvailableBalance - pnl = accountPosition.UnrealisedProfit + unrealisedPNL = accountPosition.UnrealisedProfit } var maintenanceMarginFraction decimal.Decimal @@ -2496,8 +2619,9 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po return nil, err } var relevantPosition *UPositionInformationV2 + fps := fPair.String() for i := range positionsInfo { - if positionsInfo[i].Symbol != fPair.String() { + if positionsInfo[i].Symbol != fps { continue } relevantPosition = &positionsInfo[i] @@ -2522,7 +2646,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po MarkPrice: decimal.NewFromFloat(markPrice), CurrentSize: decimal.NewFromFloat(positionSize), AverageOpenPrice: decimal.NewFromFloat(openPrice), - PositionPNL: decimal.NewFromFloat(pnl), + UnrealisedPNL: decimal.NewFromFloat(unrealisedPNL), MaintenanceMarginFraction: maintenanceMarginFraction, FreeCollateral: decimal.NewFromFloat(collateralAvailable), TotalCollateral: decimal.NewFromFloat(collateralTotal), @@ -2540,8 +2664,9 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po pnl, openPrice, isolatedMargin float64 var accountPosition *FuturesAccountInformationPosition + fps := fPair.String() for i := range ai.Positions { - if ai.Positions[i].Symbol != fPair.String() { + if ai.Positions[i].Symbol != fps { continue } accountPosition = &ai.Positions[i] @@ -2594,7 +2719,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po } var relevantPosition *FuturesPositionInformation for i := range positionsInfo { - if positionsInfo[i].Symbol != fPair.String() { + if positionsInfo[i].Symbol != fps { continue } relevantPosition = &positionsInfo[i] @@ -2642,7 +2767,7 @@ func (b *Binance) GetFuturesPositionSummary(ctx context.Context, req *futures.Po MarkPrice: decimal.NewFromFloat(markPrice), CurrentSize: decimal.NewFromFloat(positionSize), AverageOpenPrice: decimal.NewFromFloat(openPrice), - PositionPNL: decimal.NewFromFloat(pnl), + UnrealisedPNL: decimal.NewFromFloat(pnl), MaintenanceMarginFraction: mmf, FreeCollateral: decimal.NewFromFloat(collateralAvailable), TotalCollateral: tc, @@ -2873,18 +2998,31 @@ func (b *Binance) GetFuturesContractDetails(ctx context.Context, item asset.Item } switch item { case asset.USDTMarginedFutures: + fri, err := b.UGetFundingRateInfo(ctx) + if err != nil { + return nil, err + } + ei, err := b.UExchangeInfo(ctx) if err != nil { return nil, err } resp := make([]futures.Contract, 0, len(ei.Symbols)) for i := range ei.Symbols { + var fundingRateFloor, fundingRateCeil decimal.Decimal + for j := range fri { + if fri[j].Symbol != ei.Symbols[i].Symbol { + continue + } + fundingRateFloor = fri[j].AdjustedFundingRateFloor.Decimal() + fundingRateCeil = fri[j].AdjustedFundingRateCap.Decimal() + break + } var cp currency.Pair cp, err = currency.NewPairFromStrings(ei.Symbols[i].BaseAsset, ei.Symbols[i].Symbol[len(ei.Symbols[i].BaseAsset):]) if err != nil { return nil, err } - var ct futures.ContractType var ed time.Time if cp.Quote.Equal(currency.USDT) || cp.Quote.Equal(currency.BUSD) { @@ -2894,27 +3032,43 @@ func (b *Binance) GetFuturesContractDetails(ctx context.Context, item asset.Item ed = ei.Symbols[i].DeliveryDate.Time() } resp = append(resp, futures.Contract{ - Exchange: b.Name, - Name: cp, - Underlying: currency.NewPair(currency.NewCode(ei.Symbols[i].BaseAsset), currency.NewCode(ei.Symbols[i].QuoteAsset)), - Asset: item, - SettlementType: futures.Linear, - StartDate: ei.Symbols[i].OnboardDate.Time(), - EndDate: ed, - IsActive: ei.Symbols[i].Status == "TRADING", - Status: ei.Symbols[i].Status, - MarginCurrency: currency.NewCode(ei.Symbols[i].MarginAsset), - Type: ct, + Exchange: b.Name, + Name: cp, + Underlying: currency.NewPair(currency.NewCode(ei.Symbols[i].BaseAsset), currency.NewCode(ei.Symbols[i].QuoteAsset)), + Asset: item, + SettlementType: futures.Linear, + StartDate: ei.Symbols[i].OnboardDate.Time(), + EndDate: ed, + IsActive: ei.Symbols[i].Status == "TRADING", + Status: ei.Symbols[i].Status, + MarginCurrency: currency.NewCode(ei.Symbols[i].MarginAsset), + Type: ct, + FundingRateFloor: fundingRateFloor, + FundingRateCeiling: fundingRateCeil, }) } return resp, nil case asset.CoinMarginedFutures: + fri, err := b.GetFundingRateInfo(ctx) + if err != nil { + return nil, err + } ei, err := b.FuturesExchangeInfo(ctx) if err != nil { return nil, err } + resp := make([]futures.Contract, 0, len(ei.Symbols)) for i := range ei.Symbols { + var fundingRateFloor, fundingRateCeil decimal.Decimal + for j := range fri { + if fri[j].Symbol != ei.Symbols[i].Symbol { + continue + } + fundingRateFloor = fri[j].AdjustedFundingRateFloor.Decimal() + fundingRateCeil = fri[j].AdjustedFundingRateCap.Decimal() + break + } var cp currency.Pair cp, err = currency.NewPairFromString(ei.Symbols[i].Symbol) if err != nil { @@ -2930,16 +3084,18 @@ func (b *Binance) GetFuturesContractDetails(ctx context.Context, item asset.Item ed = ei.Symbols[i].DeliveryDate.Time() } resp = append(resp, futures.Contract{ - Exchange: b.Name, - Name: cp, - Underlying: currency.NewPair(currency.NewCode(ei.Symbols[i].BaseAsset), currency.NewCode(ei.Symbols[i].QuoteAsset)), - Asset: item, - StartDate: ei.Symbols[i].OnboardDate.Time(), - EndDate: ed, - IsActive: ei.Symbols[i].ContractStatus == "TRADING", - MarginCurrency: currency.NewCode(ei.Symbols[i].MarginAsset), - SettlementType: futures.Inverse, - Type: ct, + Exchange: b.Name, + Name: cp, + Underlying: currency.NewPair(currency.NewCode(ei.Symbols[i].BaseAsset), currency.NewCode(ei.Symbols[i].QuoteAsset)), + Asset: item, + StartDate: ei.Symbols[i].OnboardDate.Time(), + EndDate: ed, + IsActive: ei.Symbols[i].ContractStatus == "TRADING", + MarginCurrency: currency.NewCode(ei.Symbols[i].MarginAsset), + SettlementType: futures.Inverse, + Type: ct, + FundingRateFloor: fundingRateFloor, + FundingRateCeiling: fundingRateCeil, }) } return resp, nil diff --git a/exchanges/binance/ufutures_types.go b/exchanges/binance/ufutures_types.go index dbd66dda..aca55dbf 100644 --- a/exchanges/binance/ufutures_types.go +++ b/exchanges/binance/ufutures_types.go @@ -3,6 +3,7 @@ package binance import ( "time" + "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/currency" ) @@ -81,6 +82,15 @@ type UMarkPrice struct { Time int64 `json:"time"` } +// FundingRateInfoResponse stores funding rate info +type FundingRateInfoResponse struct { + Symbol string `json:"symbol"` + AdjustedFundingRateCap convert.StringToFloat64 `json:"adjustedFundingRateCap"` + AdjustedFundingRateFloor convert.StringToFloat64 `json:"adjustedFundingRateFloor"` + FundingIntervalHours int64 `json:"fundingIntervalHours"` + Disclaimer bool `json:"disclaimer"` +} + // FundingRateHistory stores funding rate history type FundingRateHistory struct { Symbol string `json:"symbol"` diff --git a/exchanges/binanceus/binanceus_wrapper.go b/exchanges/binanceus/binanceus_wrapper.go index fdd05cec..4dd25779 100644 --- a/exchanges/binanceus/binanceus_wrapper.go +++ b/exchanges/binanceus/binanceus_wrapper.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -992,3 +993,8 @@ func (bi *Binanceus) GetAvailableTransferChains(ctx context.Context, cryptocurre func (bi *Binanceus) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (bi *Binanceus) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 4c0aafb5..28a4d761 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -125,6 +126,7 @@ func (b *Bitfinex) SetDefaults() { MultiChainDeposits: true, MultiChainWithdrawals: true, MultiChainDepositRequiresChainSet: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ AccountBalance: true, @@ -150,6 +152,15 @@ func (b *Bitfinex) SetDefaults() { DateRanges: true, Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Margin: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -362,12 +373,9 @@ func (b *Bitfinex) UpdateTickers(ctx context.Context, a asset.Item) error { for key, val := range tickerNew { pair, enabled, err := b.MatchSymbolCheckEnabled(key[1:], a, true) - if err != nil { - if !errors.Is(err, currency.ErrPairNotFound) { - return err - } + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return err } - if !enabled { continue } @@ -1297,3 +1305,9 @@ func (b *Bitfinex) GetServerTime(_ context.Context, _ asset.Item) (time.Time, er func (b *Bitfinex) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bitfinex) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + // TODO: Add futures support for Bitfinex + return nil, common.ErrNotYetImplemented +} diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index 6908aa1a..e93f8c7e 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -504,3 +505,8 @@ func (b *Bitflyer) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair func (b *Bitflyer) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bitflyer) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index daccf5ee..1ea333f6 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -19,6 +19,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/currencystate" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -919,3 +920,8 @@ func (b *Bithumb) GetServerTime(_ context.Context, _ asset.Item) (time.Time, err func (b *Bithumb) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bithumb) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index 8ebe9f5a..a618b94b 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -232,11 +232,21 @@ func (b *Bitmex) GetAccountExecutionTradeHistory(ctx context.Context, params *Ge func (b *Bitmex) GetFullFundingHistory(ctx context.Context, symbol, count, filter, columns, start string, reverse bool, startTime, endTime time.Time) ([]Funding, error) { var fundingHistory []Funding params := url.Values{} - params.Set("symbol", symbol) - params.Set("count", count) - params.Set("filter", filter) - params.Set("columns", columns) - params.Set("start", start) + if symbol != "" { + params.Set("symbol", symbol) + } + if count != "" { + params.Set("count", count) + } + if filter != "" { + params.Set("filter", filter) + } + if columns != "" { + params.Set("columns", columns) + } + if !startTime.IsZero() { + params.Set("start", start) + } params.Set("reverse", "true") if !reverse { params.Set("reverse", "false") diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index fc4e7c63..5e5c928a 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" @@ -1256,3 +1257,61 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USDT), + IncludePredictedRate: true, + }) + if !errors.Is(err, common.ErrFunctionNotSupported) { + t.Error(err) + } + + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.BTC, currency.KLAY), + }) + if !errors.Is(err, futures.ErrNotPerpetualFuture) { + t.Error(err) + } + + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.PerpetualContract, + }) + if err != nil { + t.Error(err) + } + + cp, err := currency.NewPairFromString("ETHUSD") + if err != nil { + t.Error(err) + } + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.PerpetualContract, + Pair: cp, + }) + if err != nil { + t.Error(err) + } +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + isPerp, err := b.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.BTC, currency.USD)) + if err != nil { + t.Error(err) + } + if isPerp { + t.Error("expected false") + } + + isPerp, err = b.IsPerpetualFutureCurrency(asset.PerpetualContract, currency.NewPair(currency.BTC, currency.USD)) + if err != nil { + t.Error(err) + } + if !isPerp { + t.Error("expected true") + } +} diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 2e73a118..95819f57 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -123,6 +123,7 @@ func (b *Bitmex) SetDefaults() { CryptoWithdrawal: true, TradeFee: true, CryptoWithdrawalFee: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ TradeFetching: true, @@ -134,6 +135,16 @@ func (b *Bitmex) SetDefaults() { DeadMansSwitch: true, GetOrders: true, GetOrder: true, + FundingRateFetching: false, // supported but not implemented // TODO when multi-websocket support added + }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.PerpetualContract: true, + }, }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission | exchange.WithdrawCryptoWithEmail | @@ -402,12 +413,9 @@ instruments: pair, enabled, err = b.MatchSymbolCheckEnabled(tick[j].Symbol, a, false) } - if err != nil { - if !errors.Is(err, currency.ErrPairNotFound) { - return err - } + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return err } - if !enabled { continue } @@ -1241,3 +1249,86 @@ func (b *Bitmex) GetFuturesContractDetails(ctx context.Context, item asset.Item) } return resp, nil } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bitmex) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + + if r.IncludePredictedRate { + return nil, fmt.Errorf("%w IncludePredictedRate", common.ErrFunctionNotSupported) + } + + count := "1" + if r.Pair.IsEmpty() { + count = "500" + } else { + isPerp, err := b.IsPerpetualFutureCurrency(r.Asset, r.Pair) + if err != nil { + return nil, err + } + if !isPerp { + return nil, fmt.Errorf("%w %v %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) + } + } + + format, err := b.GetPairFormat(r.Asset, true) + if err != nil { + return nil, err + } + fPair := format.Format(r.Pair) + rates, err := b.GetFullFundingHistory(ctx, fPair, count, "", "", "", true, time.Time{}, time.Time{}) + if err != nil { + return nil, err + } + + resp := make([]fundingrate.LatestRateResponse, 0, len(rates)) + // Bitmex returns historical rates from this endpoint, we only want the latest + latestRateSymbol := make(map[string]bool) + for i := range rates { + if _, ok := latestRateSymbol[rates[i].Symbol]; ok { + continue + } + latestRateSymbol[rates[i].Symbol] = true + var nr time.Time + nr, err = time.Parse(time.RFC3339, rates[i].FundingInterval) + if err != nil { + return nil, err + } + var cp currency.Pair + var isEnabled bool + cp, isEnabled, err = b.MatchSymbolCheckEnabled(rates[i].Symbol, r.Asset, false) + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + continue + } + var isPerp bool + isPerp, err = b.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + resp = append(resp, fundingrate.LatestRateResponse{ + Exchange: b.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: rates[i].Timestamp, + Rate: decimal.NewFromFloat(rates[i].FundingRate), + }, + TimeOfNextRate: rates[i].Timestamp.Add(time.Duration(nr.Hour()) * time.Hour), + TimeChecked: time.Now(), + }) + } + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (b *Bitmex) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) { + return a == asset.PerpetualContract, nil +} diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 13863077..8e0ad553 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1029,3 +1030,8 @@ func (b *Bitstamp) GetServerTime(_ context.Context, _ asset.Item) (time.Time, er func (b *Bitstamp) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bitstamp) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index 3b2e65c7..36f7623a 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1095,3 +1096,8 @@ func (b *Bittrex) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, func (b *Bittrex) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *Bittrex) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 36e71ab9..c530edcb 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1224,3 +1225,8 @@ func convertToKlineCandle(candle *[6]string) (kline.Candle, error) { func (b *BTCMarkets) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (b *BTCMarkets) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/btse/btse_test.go b/exchanges/btse/btse_test.go index 76c8c9f5..726f0283 100644 --- a/exchanges/btse/btse_test.go +++ b/exchanges/btse/btse_test.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -968,3 +969,60 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USDT), + IncludePredictedRate: true, + }) + if !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + }) + if err != nil { + t.Error(err) + } + + _, err = b.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: testFUTURESPair, + }) + if err != nil { + t.Error(err) + } +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + isPerp, err := b.IsPerpetualFutureCurrency(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.USD)) + if err != nil { + t.Error(err) + } + if isPerp { + t.Error("expected false") + } + + isPerp, err = b.IsPerpetualFutureCurrency(asset.Futures, testFUTURESPair) + if err != nil { + t.Error(err) + } + if !isPerp { + t.Error("expected true") + } + + cp, err := currency.NewPairFromString(testSPOTPair) + if err != nil { + t.Error(err) + } + isPerp, err = b.IsPerpetualFutureCurrency(asset.Futures, cp) + if err != nil { + t.Error(err) + } + if isPerp { + t.Error("expected false") + } +} diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index cba211de..677374f8 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -114,6 +114,7 @@ func (b *BTSE) SetDefaults() { FiatDepositFee: true, FiatWithdrawalFee: true, CryptoWithdrawalFee: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ OrderbookFetching: true, @@ -128,6 +129,15 @@ func (b *BTSE) SetDefaults() { DateRanges: true, Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.OneHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Futures: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -1219,3 +1229,65 @@ func (b *BTSE) GetFuturesContractDetails(ctx context.Context, item asset.Item) ( } return resp, nil } + +// GetLatestFundingRates returns the latest funding rates data +func (b *BTSE) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + if r.IncludePredictedRate { + return nil, fmt.Errorf("%w IncludePredictedRate", common.ErrFunctionNotSupported) + } + + format, err := b.GetPairFormat(r.Asset, true) + if err != nil { + return nil, err + } + fPair := format.Format(r.Pair) + rates, err := b.GetMarketSummary(ctx, fPair, false) + if err != nil { + return nil, err + } + + resp := make([]fundingrate.LatestRateResponse, 0, len(rates)) + for i := range rates { + var cp currency.Pair + var isEnabled bool + cp, isEnabled, err = b.MatchSymbolCheckEnabled(rates[i].Symbol, r.Asset, true) + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + continue + } + var isPerp bool + isPerp, err = b.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + tt := time.Now().Truncate(time.Hour) + resp = append(resp, fundingrate.LatestRateResponse{ + Exchange: b.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: time.Now().Truncate(time.Hour), + Rate: decimal.NewFromFloat(rates[i].FundingRate), + }, + TimeOfNextRate: tt.Add(time.Hour), + TimeChecked: time.Now(), + }) + } + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (b *BTSE) IsPerpetualFutureCurrency(a asset.Item, p currency.Pair) (bool, error) { + return a == asset.Futures && p.Quote.Equal(currency.PFC), nil +} diff --git a/exchanges/bybit/bybit_test.go b/exchanges/bybit/bybit_test.go index ed8e68fb..a2cb7891 100644 --- a/exchanges/bybit/bybit_test.go +++ b/exchanges/bybit/bybit_test.go @@ -3584,3 +3584,22 @@ func TestGetContractLength(t *testing.T) { t.Error("expected semi annually") } } + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + enabled, err := b.GetEnabledPairs(asset.Futures) + if err != nil { + t.Fatal(err) + } + for x := range enabled { + isPerp, err := b.IsPerpetualFutureCurrency(asset.Futures, enabled[x]) + if err != nil { + t.Fatal(err) + } + if enabled[x].Quote.Equal(currency.PFC) && !isPerp { + t.Error("expected true") + } else if !enabled[x].Quote.Equal(currency.PFC) && isPerp { + t.Error("expected false") + } + } +} diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index bfff780c..01ee925b 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -2416,3 +2417,14 @@ func getContractLength(contractLength time.Duration) (futures.ContractType, erro } return ct, nil } + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (by *Bybit) IsPerpetualFutureCurrency(a asset.Item, p currency.Pair) (bool, error) { + return a == asset.Futures && p.Quote.Equal(currency.PFC), nil +} + +// GetLatestFundingRates returns the latest funding rates data +func (by *Bybit) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + // TODO: implement with v5 API upgrade + return nil, common.ErrNotYetImplemented +} diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 4e76dfa7..3aa525b5 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -15,6 +15,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -981,6 +982,11 @@ func (c *CoinbasePro) GetServerTime(ctx context.Context, _ asset.Item) (time.Tim return st.ISO, nil } +// GetLatestFundingRates returns the latest funding rates data +func (c *CoinbasePro) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} + // GetFuturesContractDetails returns all contracts from the exchange by asset type func (c *CoinbasePro) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 2f61aeed..228a1845 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1199,3 +1200,8 @@ func (c *COINUT) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, func (c *COINUT) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (c *COINUT) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/exchange.go b/exchanges/exchange.go index a2bf0f32..b617efb9 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -1672,11 +1672,6 @@ func (b *Base) GetFuturesPositionSummary(context.Context, *futures.PositionSumma return nil, common.ErrNotYetImplemented } -// GetFundingPaymentDetails returns funding payment details for a future for a specific time period -func (b *Base) GetFundingPaymentDetails(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) { - return nil, common.ErrNotYetImplemented -} - // GetFuturesPositions returns futures positions for all currencies func (b *Base) GetFuturesPositions(context.Context, *futures.PositionsRequest) ([]futures.PositionDetails, error) { return nil, common.ErrNotYetImplemented @@ -1687,13 +1682,8 @@ func (b *Base) GetFuturesPositionOrders(context.Context, *futures.PositionsReque return nil, common.ErrNotYetImplemented } -// GetLatestFundingRate returns the latest funding rate based on request data -func (b *Base) GetLatestFundingRate(context.Context, *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) { - return nil, common.ErrNotYetImplemented -} - -// GetFundingRates returns funding rates based on request data -func (b *Base) GetFundingRates(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) { +// GetHistoricalFundingRates returns historical funding rates for a future +func (b *Base) GetHistoricalFundingRates(context.Context, *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { return nil, common.ErrNotYetImplemented } diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index 6c772da0..23603fe6 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -2521,18 +2521,10 @@ func TestGetFuturesPositions(t *testing.T) { } } -func TestGetFundingPaymentDetails(t *testing.T) { +func TestGetHistoricalFundingRates(t *testing.T) { t.Parallel() var b Base - if _, err := b.GetFundingPaymentDetails(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { - t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented) - } -} - -func TestGetFundingRate(t *testing.T) { - t.Parallel() - var b Base - if _, err := b.GetLatestFundingRate(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { + if _, err := b.GetHistoricalFundingRates(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented) } } @@ -2540,7 +2532,7 @@ func TestGetFundingRate(t *testing.T) { func TestGetFundingRates(t *testing.T) { t.Parallel() var b Base - if _, err := b.GetFundingRates(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { + if _, err := b.GetHistoricalFundingRates(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) { t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented) } } diff --git a/exchanges/exchange_types.go b/exchanges/exchange_types.go index c5a1f748..f70cc538 100644 --- a/exchanges/exchange_types.go +++ b/exchanges/exchange_types.go @@ -177,14 +177,15 @@ type FeaturesSupported struct { // FuturesCapabilities stores the exchange's futures capabilities type FuturesCapabilities struct { - FundingRates bool - Positions bool - OrderManagerPositionTracking bool - Collateral bool - CollateralMode bool - Leverage bool - MaximumFundingRateHistory time.Duration - FundingRateFrequency time.Duration + FundingRates bool + MaximumFundingRateHistory time.Duration + SupportedFundingRateFrequencies map[kline.Interval]bool + Positions bool + OrderManagerPositionTracking bool + Collateral bool + CollateralMode bool + Leverage bool + FundingRateBatching map[asset.Item]bool } // MarginCapabilities stores the exchange's margin capabilities diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index dbed8c21..91d29ec7 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -206,6 +207,9 @@ func (e *EXMO) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error // UpdateTickers updates the ticker for all currency pairs of a given asset type func (e *EXMO) UpdateTickers(ctx context.Context, a asset.Item) error { + if !e.SupportsAsset(a) { + return fmt.Errorf("%w: %v", asset.ErrNotSupported, a) + } result, err := e.GetTicker(ctx) if err != nil { return err @@ -831,3 +835,8 @@ func (e *EXMO) GetAvailableTransferChains(ctx context.Context, cryptocurrency cu func (e *EXMO) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (e *EXMO) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/fundingrate/fundingrate_types.go b/exchanges/fundingrate/fundingrate_types.go index b8709e73..a8376deb 100644 --- a/exchanges/fundingrate/fundingrate_types.go +++ b/exchanges/fundingrate/fundingrate_types.go @@ -12,8 +12,8 @@ import ( // ErrFundingRateOutsideLimits is returned when a funding rate is outside the allowed date range var ErrFundingRateOutsideLimits = errors.New("funding rate outside limits") -// RatesRequest is used to request funding rate details for a position -type RatesRequest struct { +// HistoricalRatesRequest is used to request funding rate details for a position +type HistoricalRatesRequest struct { Asset asset.Item Pair currency.Pair // PaymentCurrency is an optional parameter depending on exchange API @@ -30,8 +30,8 @@ type RatesRequest struct { RespectHistoryLimits bool } -// Rates is used to return funding rate details for a position -type Rates struct { +// HistoricalRates is used to return funding rate details for a position +type HistoricalRates struct { Exchange string Asset asset.Item Pair currency.Pair @@ -60,6 +60,7 @@ type LatestRateResponse struct { LatestRate Rate PredictedUpcomingRate Rate TimeOfNextRate time.Time + TimeChecked time.Time } // Rate holds details for an individual funding rate diff --git a/exchanges/futures/contract.go b/exchanges/futures/contract.go index 8fbf5f49..8b890412 100644 --- a/exchanges/futures/contract.go +++ b/exchanges/futures/contract.go @@ -3,6 +3,7 @@ package futures import ( "time" + "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" @@ -26,6 +27,8 @@ type Contract struct { Multiplier float64 MaxLeverage float64 LatestRate fundingrate.Rate + FundingRateFloor decimal.Decimal + FundingRateCeiling decimal.Decimal } // ContractSettlementType holds the various style of contracts offered by futures exchanges diff --git a/exchanges/futures/futures.go b/exchanges/futures/futures.go index e8e71eef..76cb8cf3 100644 --- a/exchanges/futures/futures.go +++ b/exchanges/futures/futures.go @@ -134,7 +134,7 @@ func (c *PositionController) GetPositionsForExchange(exch string, item asset.Ite } // TrackFundingDetails applies funding rate details to a tracked position -func (c *PositionController) TrackFundingDetails(d *fundingrate.Rates) error { +func (c *PositionController) TrackFundingDetails(d *fundingrate.HistoricalRates) error { if c == nil { return fmt.Errorf("position controller %w", common.ErrNilPointer) } @@ -459,7 +459,7 @@ func (m *MultiPositionTracker) TrackNewOrder(d *order.Detail) error { } // TrackFundingDetails applies funding rate details to a tracked position -func (m *MultiPositionTracker) TrackFundingDetails(d *fundingrate.Rates) error { +func (m *MultiPositionTracker) TrackFundingDetails(d *fundingrate.HistoricalRates) error { if m == nil { return fmt.Errorf("multi-position tracker %w", common.ErrNilPointer) } @@ -579,7 +579,7 @@ func (p *PositionTracker) GetStats() *Position { if p.fundingRateDetails != nil { frs := make([]fundingrate.Rate, len(p.fundingRateDetails.FundingRates)) copy(frs, p.fundingRateDetails.FundingRates) - pos.FundingRates = fundingrate.Rates{ + pos.FundingRates = fundingrate.HistoricalRates{ Exchange: p.fundingRateDetails.Exchange, Asset: p.fundingRateDetails.Asset, Pair: p.fundingRateDetails.Pair, @@ -687,7 +687,7 @@ func (p *PositionTracker) GetLatestPNLSnapshot() (PNLResult, error) { } // TrackFundingDetails sets funding rates to a position -func (p *PositionTracker) TrackFundingDetails(d *fundingrate.Rates) error { +func (p *PositionTracker) TrackFundingDetails(d *fundingrate.HistoricalRates) error { if p == nil { return fmt.Errorf("position tracker %w", common.ErrNilPointer) } @@ -715,7 +715,7 @@ func (p *PositionTracker) TrackFundingDetails(d *fundingrate.Rates) error { return fmt.Errorf("%w for timeframe %v %v %v %v-%v", ErrNoPositionsFound, p.exchange, p.asset, p.contractPair, d.StartDate, d.EndDate) } if p.fundingRateDetails == nil { - p.fundingRateDetails = &fundingrate.Rates{ + p.fundingRateDetails = &fundingrate.HistoricalRates{ Exchange: d.Exchange, Asset: d.Asset, Pair: d.Pair, diff --git a/exchanges/futures/futures_test.go b/exchanges/futures/futures_test.go index 4a598c55..d595efbb 100644 --- a/exchanges/futures/futures_test.go +++ b/exchanges/futures/futures_test.go @@ -541,7 +541,7 @@ func TestGetStats(t *testing.T) { } p.exchange = testExchange - p.fundingRateDetails = &fundingrate.Rates{ + p.fundingRateDetails = &fundingrate.HistoricalRates{ FundingRates: []fundingrate.Rate{ {}, }, @@ -1274,7 +1274,7 @@ func TestPCTrackFundingDetails(t *testing.T) { } p := currency.NewPair(currency.BTC, currency.PERP) - rates := &fundingrate.Rates{ + rates := &fundingrate.HistoricalRates{ Asset: asset.Futures, Pair: p, } @@ -1341,7 +1341,7 @@ func TestMPTTrackFundingDetails(t *testing.T) { } cp := currency.NewPair(currency.BTC, currency.PERP) - rates := &fundingrate.Rates{ + rates := &fundingrate.HistoricalRates{ Asset: asset.Futures, Pair: cp, } @@ -1351,7 +1351,7 @@ func TestMPTTrackFundingDetails(t *testing.T) { } mpt.exchange = testExchange - rates = &fundingrate.Rates{ + rates = &fundingrate.HistoricalRates{ Exchange: testExchange, Asset: asset.Futures, Pair: cp, @@ -1410,7 +1410,7 @@ func TestPTTrackFundingDetails(t *testing.T) { } cp := currency.NewPair(currency.BTC, currency.PERP) - rates := &fundingrate.Rates{ + rates := &fundingrate.HistoricalRates{ Exchange: testExchange, Asset: asset.Futures, Pair: cp, diff --git a/exchanges/futures/futures_types.go b/exchanges/futures/futures_types.go index 291472d8..4f73e90d 100644 --- a/exchanges/futures/futures_types.go +++ b/exchanges/futures/futures_types.go @@ -156,7 +156,7 @@ type PositionTracker struct { shortPositions []order.Detail longPositions []order.Detail pnlHistory []PNLResult - fundingRateDetails *fundingrate.Rates + fundingRateDetails *fundingrate.HistoricalRates } // PositionTrackerSetup contains all required fields to @@ -264,7 +264,7 @@ type Position struct { CloseDate time.Time Orders []order.Detail PNLHistory []PNLResult - FundingRates fundingrate.Rates + FundingRates fundingrate.HistoricalRates } // PositionSummaryRequest is used to request a summary of an open position @@ -333,7 +333,8 @@ type PositionSummary struct { CollateralMode collateral.Mode // The currency in which the values are quoted against. Isn't always pair.Quote // eg BTC-USDC-230929's quote in GCT is 230929, but the currency should be USDC - Currency currency.Code + Currency currency.Code + StartDate time.Time AvailableEquity decimal.Decimal CashBalance decimal.Decimal @@ -345,6 +346,7 @@ type PositionSummary struct { NotionalLeverage decimal.Decimal TotalEquity decimal.Decimal StrategyEquity decimal.Decimal + MarginBalance decimal.Decimal IsolatedMargin decimal.Decimal NotionalSize decimal.Decimal @@ -359,7 +361,8 @@ type PositionSummary struct { ContractMultiplier decimal.Decimal ContractSettlementType ContractSettlementType AverageOpenPrice decimal.Decimal - PositionPNL decimal.Decimal + UnrealisedPNL decimal.Decimal + RealisedPNL decimal.Decimal MaintenanceMarginFraction decimal.Decimal FreeCollateral decimal.Decimal TotalCollateral decimal.Decimal diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 0db0ff43..33d4be1a 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -3154,7 +3155,17 @@ func getFirstTradablePairOfAssets() { if err != nil { log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Futures) } - futuresTradablePair = enabledPairs[len(enabledPairs)-1] + + if len(enabledPairs) == 0 { + var availPairs currency.Pairs + availPairs, err = g.GetAvailablePairs(asset.Futures) + if err != nil { + log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Futures) + } + futuresTradablePair = availPairs[len(availPairs)-1] + } else { + futuresTradablePair = enabledPairs[len(enabledPairs)-1] + } enabledPairs, err = g.GetEnabledPairs(asset.Options) if err != nil { log.Fatalf("GateIO %v, trying to get %v enabled pairs error", err, asset.Options) @@ -3419,3 +3430,28 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := g.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USDT), + IncludePredictedRate: true, + }) + if !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } + _, err = g.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.BTC, currency.USD), + }) + if err != nil { + t.Error(err) + } + _, err = g.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + }) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 973e5568..e0cc7dce 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -94,6 +94,8 @@ func (g *Gateio) SetDefaults() { CryptoWithdrawalFee: true, MultiChainDeposits: true, MultiChainWithdrawals: true, + PredictedFundingRate: true, + FundingRateFetching: true, }, WebsocketCapabilities: protocol.Features{ TickerFetching: true, @@ -112,6 +114,16 @@ func (g *Gateio) SetDefaults() { Kline: kline.ExchangeCapabilitiesSupported{ Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.FourHour: true, + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Futures: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -2155,3 +2167,96 @@ func (g *Gateio) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e return g.LoadLimits(limits) } + +// GetLatestFundingRates returns the latest funding rates data +func (g *Gateio) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + + if !r.Pair.IsEmpty() { + resp := make([]fundingrate.LatestRateResponse, 1) + fPair, err := g.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + var settle string + settle, err = g.getSettlementFromCurrency(fPair, true) + if err != nil { + return nil, err + } + contract, err := g.GetSingleContract(ctx, settle, fPair.String()) + if err != nil { + return nil, err + } + resp[0] = contractToFundingRate(g.Name, r.Asset, fPair, contract, r.IncludePredictedRate) + return resp, nil + } + + var resp []fundingrate.LatestRateResponse + settleCurrencies := []string{"btc", "usdt", "usd"} + pairs, err := g.GetEnabledPairs(asset.Futures) + if err != nil { + return nil, err + } + + for i := range settleCurrencies { + contracts, err := g.GetAllFutureContracts(ctx, settleCurrencies[i]) + if err != nil { + return nil, err + } + for j := range contracts { + p := strings.ToUpper(contracts[j].Name) + if !g.IsValidPairString(p) { + continue + } + cp, err := currency.NewPairFromString(p) + if err != nil { + return nil, err + } + if !pairs.Contains(cp, false) { + continue + } + var isPerp bool + isPerp, err = g.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + resp = append(resp, contractToFundingRate(g.Name, r.Asset, cp, &contracts[j], r.IncludePredictedRate)) + } + } + + return resp, nil +} + +func contractToFundingRate(name string, item asset.Item, fPair currency.Pair, contract *FuturesContract, includeUpcomingRate bool) fundingrate.LatestRateResponse { + resp := fundingrate.LatestRateResponse{ + Exchange: name, + Asset: item, + Pair: fPair, + LatestRate: fundingrate.Rate{ + Time: contract.FundingNextApply.Time().Add(-time.Duration(contract.FundingInterval) * time.Second), + Rate: contract.FundingRate.Decimal(), + }, + TimeOfNextRate: contract.FundingNextApply.Time(), + TimeChecked: time.Now(), + } + if includeUpcomingRate { + resp.PredictedUpcomingRate = fundingrate.Rate{ + Time: contract.FundingNextApply.Time(), + Rate: contract.FundingRateIndicative.Decimal(), + } + } + return resp +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (g *Gateio) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) { + return a == asset.Futures, nil +} diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index 897e55ce..c278c5eb 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -954,3 +955,8 @@ func (g *Gemini) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e } return g.LoadLimits(resp) } + +// GetLatestFundingRates returns the latest funding rates data +func (g *Gemini) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 300ed1b1..cdb52bd3 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -993,3 +994,8 @@ func (h *HitBTC) GetHistoricCandlesExtended(ctx context.Context, pair currency.P func (h *HitBTC) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (h *HitBTC) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/huobi/cfutures_types.go b/exchanges/huobi/cfutures_types.go index 42cd7858..d2d84d0c 100644 --- a/exchanges/huobi/cfutures_types.go +++ b/exchanges/huobi/cfutures_types.go @@ -543,6 +543,12 @@ type LiquidationOrdersData struct { } `json:"data"` } +// SwapFundingRatesResponse holds funding rates and data response +type SwapFundingRatesResponse struct { + Response + Data []FundingRatesData `json:"data"` +} + // FundingRatesData stores funding rates data type FundingRatesData struct { EstimatedRate float64 `json:"estimated_rate,string"` @@ -550,8 +556,8 @@ type FundingRatesData struct { ContractCode string `json:"contractCode"` Symbol string `json:"symbol"` FeeAsset string `json:"fee_asset"` - FundingTime string `json:"fundingTime"` - NextFundingTime string `json:"next_funding_time"` + FundingTime int64 `json:"fundingTime,string"` + NextFundingTime int64 `json:"next_funding_time,string"` } // HistoricalFundingRateData stores historical funding rates for perpetuals diff --git a/exchanges/huobi/huobi_cfutures.go b/exchanges/huobi/huobi_cfutures.go index f94cfb5e..0477c8a1 100644 --- a/exchanges/huobi/huobi_cfutures.go +++ b/exchanges/huobi/huobi_cfutures.go @@ -20,6 +20,7 @@ const ( // Coin Margined Swap (perpetual futures) endpoints huobiSwapMarkets = "/swap-api/v1/swap_contract_info" huobiSwapFunding = "/swap-api/v1/swap_funding_rate" + huobiSwapBatchFunding = "/swap-api/v1/swap_batch_funding_rate" huobiSwapIndexPriceInfo = "/swap-api/v1/swap_index" huobiSwapPriceLimitation = "/swap-api/v1/swap_price_limit" huobiSwapOpenInterestInfo = "/swap-api/v1/swap_open_interest" @@ -344,8 +345,8 @@ func (h *HUOBI) GetLiquidationOrders(ctx context.Context, contract currency.Pair return resp, h.SendHTTPRequest(ctx, exchange.RestFutures, path, &resp) } -// GetHistoricalFundingRates gets historical funding rates for perpetual futures -func (h *HUOBI) GetHistoricalFundingRates(ctx context.Context, code currency.Pair, pageSize, pageIndex int64) (HistoricalFundingRateData, error) { +// GetHistoricalFundingRatesForPair gets historical funding rates for perpetual futures +func (h *HUOBI) GetHistoricalFundingRatesForPair(ctx context.Context, code currency.Pair, pageSize, pageIndex int64) (HistoricalFundingRateData, error) { var resp HistoricalFundingRateData codeValue, err := h.FormatSymbol(code, asset.CoinMarginedFutures) if err != nil { @@ -982,15 +983,15 @@ func (h *HUOBI) GetSwapMarkets(ctx context.Context, contract currency.Pair) ([]S Data []SwapMarketsData `json:"data"` } var result response - err := h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapMarkets+vals.Encode(), &result) + err := h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapMarkets+"?"+vals.Encode(), &result) if result.ErrorMessage != "" { return nil, errors.New(result.ErrorMessage) } return result.Data, err } -// GetSwapFundingRates gets funding rates data -func (h *HUOBI) GetSwapFundingRates(ctx context.Context, contract currency.Pair) (FundingRatesData, error) { +// GetSwapFundingRate gets funding rate data for one currency +func (h *HUOBI) GetSwapFundingRate(ctx context.Context, contract currency.Pair) (FundingRatesData, error) { vals := url.Values{} codeValue, err := h.FormatSymbol(contract, asset.CoinMarginedFutures) if err != nil { @@ -1002,9 +1003,16 @@ func (h *HUOBI) GetSwapFundingRates(ctx context.Context, contract currency.Pair) Data FundingRatesData `json:"data"` } var result response - err = h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapFunding+vals.Encode(), &result) + err = h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapFunding+"?"+vals.Encode(), &result) if result.ErrorMessage != "" { return FundingRatesData{}, errors.New(result.ErrorMessage) } return result.Data, err } + +// GetSwapFundingRates gets funding rates data +func (h *HUOBI) GetSwapFundingRates(ctx context.Context) (SwapFundingRatesResponse, error) { + var result SwapFundingRatesResponse + err := h.SendHTTPRequest(ctx, exchange.RestFutures, huobiSwapBatchFunding, &result) + return result, err +} diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 94d217f5..0cb4716a 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -18,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -975,7 +976,7 @@ func TestGetHistoricalFundingRates(t *testing.T) { if err != nil { t.Error(err) } - _, err = h.GetHistoricalFundingRates(context.Background(), cp, 0, 0) + _, err = h.GetHistoricalFundingRatesForPair(context.Background(), cp, 0, 0) if err != nil { t.Error(err) } @@ -2780,3 +2781,63 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := h.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USD), + IncludePredictedRate: true, + }) + if !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } + + _, err = h.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.CoinMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USD), + IncludePredictedRate: true, + }) + if err != nil { + t.Error(err) + } + + err = h.CurrencyPairs.EnablePair(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.USD)) + if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) { + t.Fatal(err) + } + _, err = h.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.CoinMarginedFutures, + IncludePredictedRate: true, + }) + if err != nil { + t.Error(err) + } +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + is, err := h.IsPerpetualFutureCurrency(asset.Binary, currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error(err) + } + if is { + t.Error("expected false") + } + + is, err = h.IsPerpetualFutureCurrency(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error(err) + } + if !is { + t.Error("expected true") + } +} + +func TestGetSwapFundingRates(t *testing.T) { + t.Parallel() + _, err := h.GetSwapFundingRates(context.Background()) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 3e818d0c..11009875 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" @@ -17,6 +18,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -122,6 +124,8 @@ func (h *HUOBI) SetDefaults() { MultiChainDeposits: true, MultiChainWithdrawals: true, HasAssetTypeAccountSegregation: true, + FundingRateFetching: true, + PredictedFundingRate: true, }, WebsocketCapabilities: protocol.Features{ KlineFetching: true, @@ -135,12 +139,23 @@ func (h *HUOBI) SetDefaults() { GetOrder: true, GetOrders: true, TickerFetching: true, + FundingRateFetching: false, // supported but not implemented // TODO when multi-websocket support added + }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | exchange.NoFiatWithdrawals, Kline: kline.ExchangeCapabilitiesSupported{ Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.CoinMarginedFutures: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -2196,3 +2211,91 @@ func (h *HUOBI) GetFuturesContractDetails(ctx context.Context, item asset.Item) } return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item) } + +// GetLatestFundingRates returns the latest funding rates data +func (h *HUOBI) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if r.Asset != asset.CoinMarginedFutures { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + + var rates []FundingRatesData + if r.Pair.IsEmpty() { + batchRates, err := h.GetSwapFundingRates(ctx) + if err != nil { + return nil, err + } + rates = batchRates.Data + } else { + rateResp, err := h.GetSwapFundingRate(ctx, r.Pair) + if err != nil { + return nil, err + } + rates = append(rates, rateResp) + } + resp := make([]fundingrate.LatestRateResponse, 0, len(rates)) + for i := range rates { + if rates[i].ContractCode == "" { + // formatting to match documentation + rates[i].ContractCode = rates[i].Symbol + "-USD" + } + cp, isEnabled, err := h.MatchSymbolCheckEnabled(rates[i].ContractCode, r.Asset, true) + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + continue + } + var isPerp bool + isPerp, err = h.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + var ft, nft time.Time + nft = time.UnixMilli(rates[i].NextFundingTime) + ft = time.UnixMilli(rates[i].FundingTime) + var fri time.Duration + if len(h.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range h.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } + if rates[i].FundingTime == 0 { + ft = nft.Add(-fri) + } + if ft.After(time.Now()) { + ft = ft.Add(-fri) + nft = nft.Add(-fri) + } + rate := fundingrate.LatestRateResponse{ + Exchange: h.Name, + Asset: r.Asset, + Pair: r.Pair, + LatestRate: fundingrate.Rate{ + Time: ft, + Rate: decimal.NewFromFloat(rates[i].FundingRate), + }, + TimeOfNextRate: nft, + TimeChecked: time.Now(), + } + if r.IncludePredictedRate { + rate.PredictedUpcomingRate = fundingrate.Rate{ + Time: rate.TimeOfNextRate, + Rate: decimal.NewFromFloat(rates[i].EstimatedRate), + } + } + resp = append(resp, rate) + } + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (h *HUOBI) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) { + return a == asset.CoinMarginedFutures, nil +} diff --git a/exchanges/interfaces.go b/exchanges/interfaces.go index bbaf8241..48067c7e 100644 --- a/exchanges/interfaces.go +++ b/exchanges/interfaces.go @@ -161,8 +161,9 @@ type FuturesManagement interface { ScaleCollateral(ctx context.Context, calculator *futures.CollateralCalculator) (*collateral.ByCurrency, error) GetPositionSummary(context.Context, *futures.PositionSummaryRequest) (*futures.PositionSummary, error) CalculateTotalCollateral(context.Context, *futures.TotalCollateralCalculator) (*futures.TotalCollateralResponse, error) - GetFundingRates(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) - GetLatestFundingRate(context.Context, *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) + GetFuturesPositions(context.Context, *futures.PositionsRequest) ([]futures.PositionDetails, error) + GetHistoricalFundingRates(context.Context, *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) + GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) IsPerpetualFutureCurrency(asset.Item, currency.Pair) (bool, error) GetCollateralCurrencyForContract(asset.Item, currency.Pair) (currency.Code, asset.Item, error) diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index 253e54ed..fbcc7fc5 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -698,3 +699,8 @@ func (i *ItBit) GetHistoricCandlesExtended(_ context.Context, _ currency.Pair, _ func (i *ItBit) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (i *ItBit) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index 03a95528..b405813f 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -21,6 +21,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -2199,3 +2200,64 @@ func TestGetFuturesContractDetails(t *testing.T) { t.Error(err) } } + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := k.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.USDTMarginedFutures, + Pair: currency.NewPair(currency.BTC, currency.USD), + IncludePredictedRate: true, + }) + if !errors.Is(err, asset.ErrNotSupported) { + t.Error(err) + } + + _, err = k.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + }) + if !errors.Is(err, nil) { + t.Error(err) + } + + cp := currency.NewPair(currency.PF, currency.NewCode("XBTUSD")) + cp.Delimiter = "_" + err = k.CurrencyPairs.EnablePair(asset.Futures, cp) + if !errors.Is(err, nil) { + t.Fatal(err) + } + _, err = k.GetLatestFundingRates(context.Background(), &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: cp, + IncludePredictedRate: true, + }) + if err != nil { + t.Error(err) + } +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + is, err := k.IsPerpetualFutureCurrency(asset.Binary, currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error(err) + } + if is { + t.Error("expected false") + } + + is, err = k.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.BTC, currency.USDT)) + if err != nil { + t.Error(err) + } + if is { + t.Error("expected false") + } + + is, err = k.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.PF, currency.NewCode("XBTUSD"))) + if err != nil { + t.Error(err) + } + if !is { + t.Error("expected true") + } +} diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 6e1fc8ef..ec17c453 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -10,6 +10,7 @@ import ( "sync" "time" + "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" "github.com/thrasher-corp/gocryptotrader/config" @@ -18,6 +19,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -131,20 +133,23 @@ func (k *Kraken) SetDefaults() { MultiChainDeposits: true, MultiChainWithdrawals: true, HasAssetTypeAccountSegregation: true, + FundingRateFetching: true, + PredictedFundingRate: true, }, WebsocketCapabilities: protocol.Features{ - TickerFetching: true, - TradeFetching: true, - KlineFetching: true, - OrderbookFetching: true, - Subscribe: true, - Unsubscribe: true, - MessageCorrelation: true, - SubmitOrder: true, - CancelOrder: true, - CancelOrders: true, - GetOrders: true, - GetOrder: true, + TickerFetching: true, + TradeFetching: true, + KlineFetching: true, + OrderbookFetching: true, + Subscribe: true, + Unsubscribe: true, + MessageCorrelation: true, + SubmitOrder: true, + CancelOrder: true, + CancelOrders: true, + GetOrders: true, + GetOrder: true, + FundingRateFetching: false, // has capability but is not supported // TODO when multi-websocket support added }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | exchange.WithdrawCryptoWith2FA | @@ -154,6 +159,15 @@ func (k *Kraken) SetDefaults() { DateRanges: true, Intervals: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + FundingRates: true, + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.FourHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Futures: true, + }, + }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, @@ -1779,3 +1793,66 @@ func (k *Kraken) GetFuturesContractDetails(ctx context.Context, item asset.Item) } return resp, nil } + +// GetLatestFundingRates returns the latest funding rates data +func (k *Kraken) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) + } + if !r.Pair.IsEmpty() { + _, isEnabled, err := k.MatchSymbolCheckEnabled(r.Pair.String(), r.Asset, r.Pair.Delimiter != "") + if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + return nil, err + } + if !isEnabled { + return nil, fmt.Errorf("%w %v", currency.ErrPairNotEnabled, r.Pair) + } + } + + t, err := k.GetFuturesTickers(ctx) + if err != nil { + return nil, err + } + resp := make([]fundingrate.LatestRateResponse, 0, len(t.Tickers)) + for i := range t.Tickers { + pair, err := currency.NewPairFromString(t.Tickers[i].Symbol) + if err != nil { + return nil, err + } + if !r.Pair.IsEmpty() && !r.Pair.Equal(pair) { + continue + } + var isPerp bool + isPerp, err = k.IsPerpetualFutureCurrency(r.Asset, pair) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + rate := fundingrate.LatestRateResponse{ + Exchange: k.Name, + Asset: r.Asset, + Pair: pair, + LatestRate: fundingrate.Rate{ + Rate: decimal.NewFromFloat(t.Tickers[i].FundingRate), + }, + TimeChecked: time.Now(), + } + if r.IncludePredictedRate { + rate.PredictedUpcomingRate = fundingrate.Rate{ + Rate: decimal.NewFromFloat(t.Tickers[i].FundingRatePrediction), + } + } + resp = append(resp, rate) + } + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (k *Kraken) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bool, error) { + return cp.Base.Equal(currency.PF) && a == asset.Futures, nil +} diff --git a/exchanges/kucoin/kucoin_futures_types.go b/exchanges/kucoin/kucoin_futures_types.go index 5599e692..7d8304eb 100644 --- a/exchanges/kucoin/kucoin_futures_types.go +++ b/exchanges/kucoin/kucoin_futures_types.go @@ -251,7 +251,7 @@ type FuturesPosition struct { ADLRankingPercentile float64 `json:"delevPercentage"` OpeningTimestamp convert.ExchangeTime `json:"openingTimestamp"` CurrentTimestamp convert.ExchangeTime `json:"currentTimestamp"` - CurrentQty int64 `json:"currentQty"` + CurrentQty float64 `json:"currentQty"` CurrentCost float64 `json:"currentCost"` // Current position value CurrentComm float64 `json:"currentComm"` // Current commission UnrealisedCost float64 `json:"unrealisedCost"` diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index 2a27edad..a8ddc4cc 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -9,11 +9,14 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/margin" @@ -30,9 +33,6 @@ const ( apiSecret = "" passPhrase = "" canManipulateRealOrders = false - - assetNotEnabled = "asset %v not enabled" - spotAndMarginAssetNotEnabled = "neither spot nor margin asset is enabled" ) var ( @@ -2424,15 +2424,137 @@ func TestSeedLocalCache(t *testing.T) { func TestGetFuturesContractDetails(t *testing.T) { t.Parallel() _, err := ku.GetFuturesContractDetails(context.Background(), asset.Spot) - if !errors.Is(err, futures.ErrNotFuturesAsset) { - t.Error(err) - } + assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) _, err = ku.GetFuturesContractDetails(context.Background(), asset.USDTMarginedFutures) - if !errors.Is(err, asset.ErrNotSupported) { - t.Error(err) - } + assert.ErrorIs(t, err, asset.ErrNotSupported) _, err = ku.GetFuturesContractDetails(context.Background(), asset.Futures) - if !errors.Is(err, nil) { - t.Error(err) - } + assert.NoError(t, err) +} + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + _, err := ku.GetLatestFundingRates(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + + req := &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.BTC, currency.USD), + } + _, err = ku.GetLatestFundingRates(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrNotPerpetualFuture) + + req = &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.XBT, currency.USDTM), + } + resp, err := ku.GetLatestFundingRates(context.Background(), req) + assert.NoError(t, err) + assert.Len(t, resp, 1) + + req = &fundingrate.LatestRateRequest{ + Asset: asset.Futures, + Pair: currency.EMPTYPAIR, + } + resp, err = ku.GetLatestFundingRates(context.Background(), req) + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestIsPerpetualFutureCurrency(t *testing.T) { + t.Parallel() + is, err := ku.IsPerpetualFutureCurrency(asset.Spot, currency.EMPTYPAIR) + assert.NoError(t, err) + assert.False(t, is) + is, err = ku.IsPerpetualFutureCurrency(asset.Futures, currency.EMPTYPAIR) + assert.NoError(t, err) + assert.False(t, is) + is, err = ku.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.XBT, currency.EOS)) + assert.NoError(t, err) + assert.False(t, is) + is, err = ku.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.XBT, currency.USDTM)) + assert.NoError(t, err) + assert.True(t, is) + is, err = ku.IsPerpetualFutureCurrency(asset.Futures, currency.NewPair(currency.XBT, currency.USDM)) + assert.NoError(t, err) + assert.True(t, is) +} + +func TestChangePositionMargin(t *testing.T) { + t.Parallel() + _, err := ku.ChangePositionMargin(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + + req := &margin.PositionChangeRequest{} + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrNotFuturesAsset) + + req.Asset = asset.Futures + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + req.Pair = currency.NewPair(currency.XBT, currency.USDTM) + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.ErrorIs(t, err, margin.ErrMarginTypeUnsupported) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + req.MarginType = margin.Isolated + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.Error(t, err) + + req.NewAllocatedMargin = 1337 + _, err = ku.ChangePositionMargin(context.Background(), req) + assert.ErrorIs(t, err, nil) +} + +func TestGetFuturesPositionSummary(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesPositionSummary(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + + req := &futures.PositionSummaryRequest{} + _, err = ku.GetFuturesPositionSummary(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrNotPerpetualFuture) + + req.Asset = asset.Futures + _, err = ku.GetFuturesPositionSummary(context.Background(), req) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + req.Pair = currency.NewPair(currency.XBT, currency.USDTM) + _, err = ku.GetFuturesPositionSummary(context.Background(), req) + assert.ErrorIs(t, err, nil) +} + +func TestGetFuturesPositionOrders(t *testing.T) { + t.Parallel() + _, err := ku.GetFuturesPositionOrders(context.Background(), nil) + assert.ErrorIs(t, err, common.ErrNilPointer) + + req := &futures.PositionsRequest{} + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrNotPerpetualFuture) + + req.Asset = asset.Futures + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + req.Pairs = currency.Pairs{ + currency.NewPair(currency.XBT, currency.USDTM), + } + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, common.ErrDateUnset) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, ku, canManipulateRealOrders) + req.EndDate = time.Now() + req.StartDate = req.EndDate.Add(-time.Hour * 24 * 7) + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, nil) + + req.StartDate = req.EndDate.Add(-time.Hour * 24 * 30) + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, futures.ErrOrderHistoryTooLarge) + + req.RespectOrderHistoryLimits = true + _, err = ku.GetFuturesPositionOrders(context.Background(), req) + assert.ErrorIs(t, err, nil) } diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index 80be1b26..10e77712 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -16,6 +16,7 @@ import ( exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/collateral" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" @@ -119,6 +120,20 @@ func (ku *Kucoin) SetDefaults() { KlineFetching: true, GetOrder: true, }, + FuturesCapabilities: exchange.FuturesCapabilities{ + Positions: true, + Leverage: true, + CollateralMode: true, + FundingRates: true, + MaximumFundingRateHistory: kline.ThreeMonth.Duration(), + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, + FundingRateBatching: map[asset.Item]bool{ + asset.Futures: true, + }, + }, + MaximumOrderHistory: kline.OneDay.Duration() * 7, WithdrawPermissions: exchange.AutoWithdrawCrypto, }, Enabled: exchange.FeaturesEnabled{ @@ -890,11 +905,8 @@ func (ku *Kucoin) GetOrderInfo(ctx context.Context, orderID string, pair currenc if err != nil { return nil, err } - enabledPairs, err := ku.GetEnabledPairs(asset.Futures) - if err != nil { - return nil, err - } - nPair, err := enabledPairs.DeriveFrom(orderDetail.Symbol) + var nPair currency.Pair + nPair, err = ku.MatchSymbolWithAvailablePairs(orderDetail.Symbol, assetType, true) if err != nil { return nil, err } @@ -1066,22 +1078,19 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M if err != nil { return nil, err } - var enabledPairs currency.Pairs - enabledPairs, err = ku.GetEnabledPairs(asset.Futures) - if err != nil { - return nil, err - } for x := range futuresOrders.Items { if !futuresOrders.Items[x].IsActive { continue } - dPair, err := enabledPairs.DeriveFrom(futuresOrders.Items[x].Symbol) + var dPair currency.Pair + var isEnabled bool + dPair, isEnabled, err = ku.MatchSymbolCheckEnabled(futuresOrders.Items[x].Symbol, getOrdersRequest.AssetType, true) if err != nil { - if errors.Is(err, currency.ErrPairNotFound) { - continue - } return nil, err } + if !isEnabled { + continue + } for i := range getOrdersRequest.Pairs { if !getOrdersRequest.Pairs[i].Equal(dPair) { continue @@ -1125,8 +1134,6 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M if err != nil { return nil, err } - var enabledPairs currency.Pairs - enabledPairs, err = ku.GetEnabledPairs(asset.Futures) if err != nil { return nil, err } @@ -1134,13 +1141,15 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M if !spotOrders.Items[x].IsActive { continue } - dPair, err := enabledPairs.DeriveFrom(spotOrders.Items[x].Symbol) + var dPair currency.Pair + var isEnabled bool + dPair, isEnabled, err = ku.MatchSymbolCheckEnabled(spotOrders.Items[x].Symbol, getOrdersRequest.AssetType, true) if err != nil { - if errors.Is(err, currency.ErrPairNotFound) { - continue - } return nil, err } + if !isEnabled { + continue + } if len(getOrdersRequest.Pairs) > 0 && !getOrdersRequest.Pairs.Contains(dPair, true) { continue } @@ -1220,24 +1229,20 @@ func (ku *Kucoin) GetOrderHistory(ctx context.Context, getOrdersRequest *order.M } } } - var enabledPairs currency.Pairs - enabledPairs, err = ku.GetEnabledPairs(asset.Futures) - if err != nil { - return nil, err - } orders = make(order.FilteredOrders, 0, len(futuresOrders.Items)) for i := range orders { orderSide, err = order.StringToOrderSide(futuresOrders.Items[i].Side) if err != nil { return nil, err } - pair, err = enabledPairs.DeriveFrom(futuresOrders.Items[i].Symbol) + var isEnabled bool + pair, isEnabled, err = ku.MatchSymbolCheckEnabled(futuresOrders.Items[i].Symbol, getOrdersRequest.AssetType, true) if err != nil { - if errors.Is(err, currency.ErrPairNotFound) { - continue - } return nil, err } + if !isEnabled { + continue + } oType, err = order.StringToOrderType(futuresOrders.Items[i].OrderType) if err != nil { log.Errorf(log.ExchangeSys, "%s %v", ku.Name, err) @@ -1540,7 +1545,14 @@ func (ku *Kucoin) GetFuturesContractDetails(ctx context.Context, item asset.Item if contracts[i].IsInverse { contractSettlementType = futures.Inverse } - timeOfCurrentFundingRate := time.Now().Add((time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond) - time.Hour*8).Truncate(time.Hour).UTC() + var fri time.Duration + if len(ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } + timeOfCurrentFundingRate := time.Now().Add((time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond) - fri).Truncate(time.Hour).UTC() resp[i] = futures.Contract{ Exchange: ku.Name, Name: cp, @@ -1564,3 +1576,331 @@ func (ku *Kucoin) GetFuturesContractDetails(ctx context.Context, item asset.Item } return resp, nil } + +// GetLatestFundingRates returns the latest funding rates data +func (ku *Kucoin) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) + } + var fri time.Duration + if len(ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range ku.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } + if r.Pair.IsEmpty() { + contracts, err := ku.GetFuturesOpenContracts(ctx) + if err != nil { + return nil, err + } + if r.IncludePredictedRate { + log.Warnf(log.ExchangeSys, "%s predicted rate for all currencies requires an additional %v requests", ku.Name, len(contracts)) + } + timeChecked := time.Now() + resp := make([]fundingrate.LatestRateResponse, 0, len(contracts)) + for i := range contracts { + timeOfNextFundingRate := time.Now().Add(time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond).Truncate(time.Hour).UTC() + var cp currency.Pair + cp, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].Symbol[len(contracts[i].BaseCurrency):]) + if err != nil { + return nil, err + } + var isPerp bool + isPerp, err = ku.IsPerpetualFutureCurrency(r.Asset, cp) + if err != nil { + return nil, err + } + if !isPerp { + continue + } + + rate := fundingrate.LatestRateResponse{ + Exchange: ku.Name, + Asset: r.Asset, + Pair: cp, + LatestRate: fundingrate.Rate{ + Time: timeOfNextFundingRate.Add(-fri), + Rate: decimal.NewFromFloat(contracts[i].FundingFeeRate), + }, + TimeOfNextRate: timeOfNextFundingRate, + TimeChecked: timeChecked, + } + if r.IncludePredictedRate { + var fr *FuturesFundingRate + fr, err = ku.GetFuturesCurrentFundingRate(ctx, contracts[i].Symbol) + if err != nil { + return nil, err + } + rate.PredictedUpcomingRate = fundingrate.Rate{ + Time: timeOfNextFundingRate, + Rate: decimal.NewFromFloat(fr.PredictedValue), + } + } + resp = append(resp, rate) + } + return resp, nil + } + resp := make([]fundingrate.LatestRateResponse, 1) + is, err := ku.IsPerpetualFutureCurrency(r.Asset, r.Pair) + if err != nil { + return nil, err + } + if !is { + return nil, fmt.Errorf("%w %s %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair) + } + fPair, err := ku.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + var fr *FuturesFundingRate + fr, err = ku.GetFuturesCurrentFundingRate(ctx, fPair.String()) + if err != nil { + return nil, err + } + rate := fundingrate.LatestRateResponse{ + Exchange: ku.Name, + Asset: r.Asset, + Pair: r.Pair, + LatestRate: fundingrate.Rate{ + Time: fr.TimePoint.Time(), + Rate: decimal.NewFromFloat(fr.Value), + }, + TimeOfNextRate: fr.TimePoint.Time().Add(fri).Truncate(time.Hour).UTC(), + TimeChecked: time.Now(), + } + if r.IncludePredictedRate { + rate.PredictedUpcomingRate = fundingrate.Rate{ + Time: rate.TimeOfNextRate, + Rate: decimal.NewFromFloat(fr.PredictedValue), + } + } + resp[0] = rate + return resp, nil +} + +// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future +func (ku *Kucoin) IsPerpetualFutureCurrency(a asset.Item, cp currency.Pair) (bool, error) { + return a == asset.Futures && (cp.Quote.Equal(currency.USDTM) || cp.Quote.Equal(currency.USDM)), nil +} + +// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period +func (ku *Kucoin) GetHistoricalFundingRates(_ context.Context, _ *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { + return nil, common.ErrFunctionNotSupported +} + +// GetLeverage gets the account's initial leverage for the asset type and pair +func (ku *Kucoin) GetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ order.Side) (float64, error) { + return -1, fmt.Errorf("%w leverage is set during order placement, view orders to view leverage", common.ErrFunctionNotSupported) +} + +// SetLeverage sets the account's initial leverage for the asset type and pair +func (ku *Kucoin) SetLeverage(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type, _ float64, _ order.Side) error { + return fmt.Errorf("%w leverage is set during order placement", common.ErrFunctionNotSupported) +} + +// SetMarginType sets the default margin type for when opening a new position +func (ku *Kucoin) SetMarginType(_ context.Context, _ asset.Item, _ currency.Pair, _ margin.Type) error { + return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported) +} + +// SetCollateralMode sets the collateral type for your account +func (ku *Kucoin) SetCollateralMode(_ context.Context, _ asset.Item, _ collateral.Mode) error { + return fmt.Errorf("%w must be set via website", common.ErrFunctionNotSupported) +} + +// GetCollateralMode returns the collateral type for your account +func (ku *Kucoin) GetCollateralMode(_ context.Context, _ asset.Item) (collateral.Mode, error) { + return collateral.UnknownMode, fmt.Errorf("%w only via website", common.ErrFunctionNotSupported) +} + +// ChangePositionMargin will modify a position/currencies margin parameters +func (ku *Kucoin) ChangePositionMargin(ctx context.Context, r *margin.PositionChangeRequest) (*margin.PositionChangeResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", futures.ErrNotFuturesAsset, r.Asset) + } + if r.Pair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + if r.MarginType != margin.Isolated { + return nil, fmt.Errorf("%w %v", margin.ErrMarginTypeUnsupported, r.MarginType) + } + fPair, err := ku.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + + resp, err := ku.AddMargin(ctx, fPair.String(), fmt.Sprintf("%s%v%v", r.Pair, r.NewAllocatedMargin, time.Now().Unix()), r.NewAllocatedMargin) + if err != nil { + return nil, err + } + if resp == nil { + return nil, fmt.Errorf("%s - %s", ku.Name, "no response received") + } + return &margin.PositionChangeResponse{ + Exchange: ku.Name, + Pair: r.Pair, + Asset: r.Asset, + AllocatedMargin: resp.PosMargin, + MarginType: r.MarginType, + }, nil +} + +// GetFuturesPositionSummary returns position summary details for an active position +func (ku *Kucoin) GetFuturesPositionSummary(ctx context.Context, r *futures.PositionSummaryRequest) (*futures.PositionSummary, error) { + if r == nil { + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset) + } + if r.Pair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + fPair, err := ku.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + pos, err := ku.GetFuturesPosition(ctx, fPair.String()) + if err != nil { + return nil, err + } + marginType := margin.Isolated + if pos.CrossMode { + marginType = margin.Multi + } + contracts, err := ku.GetFuturesContractDetails(ctx, r.Asset) + if err != nil { + return nil, err + } + var multiplier, contractSize float64 + var settlementType futures.ContractSettlementType + for i := range contracts { + if !contracts[i].Name.Equal(fPair) { + continue + } + multiplier = contracts[i].Multiplier + contractSize = multiplier * pos.CurrentQty + settlementType = contracts[i].SettlementType + } + + ao, err := ku.GetFuturesAccountOverview(ctx, fPair.String()) + if err != nil { + return nil, err + } + + return &futures.PositionSummary{ + Pair: r.Pair, + Asset: r.Asset, + MarginType: marginType, + CollateralMode: collateral.MultiMode, + Currency: currency.NewCode(pos.SettleCurrency), + StartDate: pos.OpeningTimestamp.Time(), + AvailableEquity: decimal.NewFromFloat(ao.AccountEquity), + MarginBalance: decimal.NewFromFloat(ao.MarginBalance), + NotionalSize: decimal.NewFromFloat(pos.MarkValue), + Leverage: decimal.NewFromFloat(pos.RealLeverage), + MaintenanceMarginRequirement: decimal.NewFromFloat(pos.MaintMarginReq), + InitialMarginRequirement: decimal.NewFromFloat(pos.PosInit), + EstimatedLiquidationPrice: decimal.NewFromFloat(pos.LiquidationPrice), + CollateralUsed: decimal.NewFromFloat(pos.PosCost), + MarkPrice: decimal.NewFromFloat(pos.MarkPrice), + CurrentSize: decimal.NewFromFloat(pos.CurrentQty), + ContractSize: decimal.NewFromFloat(contractSize), + ContractMultiplier: decimal.NewFromFloat(multiplier), + ContractSettlementType: settlementType, + AverageOpenPrice: decimal.NewFromFloat(pos.AvgEntryPrice), + UnrealisedPNL: decimal.NewFromFloat(pos.UnrealisedPnl), + RealisedPNL: decimal.NewFromFloat(pos.RealisedPnl), + MaintenanceMarginFraction: decimal.NewFromFloat(pos.MaintMarginReq), + FreeCollateral: decimal.NewFromFloat(ao.AvailableBalance), + TotalCollateral: decimal.NewFromFloat(ao.AccountEquity), + FrozenBalance: decimal.NewFromFloat(ao.FrozenFunds), + }, nil +} + +// GetFuturesPositionOrders returns the orders for futures positions +func (ku *Kucoin) GetFuturesPositionOrders(ctx context.Context, r *futures.PositionsRequest) ([]futures.PositionResponse, error) { + if r == nil { + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) + } + if r.Asset != asset.Futures { + return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset) + } + if len(r.Pairs) == 0 { + return nil, currency.ErrCurrencyPairEmpty + } + err := common.StartEndTimeCheck(r.StartDate, r.EndDate) + if err != nil { + return nil, err + } + if !r.EndDate.IsZero() && r.EndDate.Sub(r.StartDate) > ku.Features.Supports.MaximumOrderHistory { + if r.RespectOrderHistoryLimits { + r.StartDate = time.Now().Add(-ku.Features.Supports.MaximumOrderHistory) + } else { + return nil, fmt.Errorf("%w max lookup %v", futures.ErrOrderHistoryTooLarge, time.Now().Add(-ku.Features.Supports.MaximumOrderHistory)) + } + } + contracts, err := ku.GetFuturesContractDetails(ctx, r.Asset) + if err != nil { + return nil, err + } + resp := make([]futures.PositionResponse, len(r.Pairs)) + for x := range r.Pairs { + var multiplier float64 + fPair, err := ku.FormatExchangeCurrency(r.Pairs[x], r.Asset) + if err != nil { + return nil, err + } + for i := range contracts { + if !contracts[i].Name.Equal(fPair) { + continue + } + multiplier = contracts[i].Multiplier + } + + positionOrders, err := ku.GetFuturesOrders(ctx, "", fPair.String(), "", "", r.StartDate, r.EndDate) + if err != nil { + return nil, err + } + resp[x].Orders = make([]order.Detail, len(positionOrders.Items)) + for y := range positionOrders.Items { + side, err := order.StringToOrderSide(positionOrders.Items[y].Side) + if err != nil { + return nil, err + } + oType, err := order.StringToOrderType(positionOrders.Items[y].OrderType) + if err != nil { + return nil, fmt.Errorf("asset type: %v err: %w", r.Asset, err) + } + oStatus, err := order.StringToOrderStatus(positionOrders.Items[y].Status) + if err != nil { + return nil, fmt.Errorf("asset type: %v err: %w", r.Asset, err) + } + resp[x].Orders[y] = order.Detail{ + Leverage: positionOrders.Items[y].Leverage, + Price: positionOrders.Items[y].Price, + Amount: positionOrders.Items[y].Size * multiplier, + ContractAmount: positionOrders.Items[y].Size, + ExecutedAmount: positionOrders.Items[y].FilledSize, + RemainingAmount: positionOrders.Items[y].Size - positionOrders.Items[y].FilledSize, + CostAsset: currency.NewCode(positionOrders.Items[y].SettleCurrency), + Exchange: ku.Name, + OrderID: positionOrders.Items[y].ID, + ClientOrderID: positionOrders.Items[y].ClientOid, + Type: oType, + Side: side, + Status: oStatus, + AssetType: asset.Futures, + Date: positionOrders.Items[y].CreatedAt.Time(), + CloseTime: positionOrders.Items[y].EndAt.Time(), + LastUpdated: positionOrders.Items[y].UpdatedAt.Time(), + Pair: fPair, + } + } + } + return resp, nil +} diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index e607becc..aae7e58f 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1025,3 +1026,8 @@ func (l *Lbank) GetStatus(status int64) order.Status { func (l *Lbank) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (l *Lbank) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/okcoin/okcoin_wrapper.go b/exchanges/okcoin/okcoin_wrapper.go index 3eb1474f..e8152dee 100644 --- a/exchanges/okcoin/okcoin_wrapper.go +++ b/exchanges/okcoin/okcoin_wrapper.go @@ -15,6 +15,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1261,3 +1262,8 @@ func (o *Okcoin) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e func (o *Okcoin) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (o *Okcoin) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index 6e9f452d..9fe7e19d 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -3290,7 +3290,7 @@ func TestGetLatestFundingRate(t *testing.T) { if err != nil { t.Error(err) } - _, err = ok.GetLatestFundingRate(contextGenerate(), &fundingrate.LatestRateRequest{ + _, err = ok.GetLatestFundingRates(contextGenerate(), &fundingrate.LatestRateRequest{ Asset: asset.PerpetualSwap, Pair: cp, IncludePredictedRate: true, @@ -3300,13 +3300,13 @@ func TestGetLatestFundingRate(t *testing.T) { } } -func TestGetFundingRates(t *testing.T) { +func TestGetHistoricalFundingRates(t *testing.T) { t.Parallel() cp, err := currency.NewPairFromString("BTC-USD-SWAP") if err != nil { t.Error(err) } - r := &fundingrate.RatesRequest{ + r := &fundingrate.HistoricalRatesRequest{ Asset: asset.PerpetualSwap, Pair: cp, PaymentCurrency: currency.USDT, @@ -3317,19 +3317,19 @@ func TestGetFundingRates(t *testing.T) { if sharedtestvalues.AreAPICredentialsSet(ok) { r.IncludePayments = true } - _, err = ok.GetFundingRates(contextGenerate(), r) + _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) if err != nil { t.Error(err) } r.StartDate = time.Now().Add(-time.Hour * 24 * 120) - _, err = ok.GetFundingRates(contextGenerate(), r) + _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) if !errors.Is(err, fundingrate.ErrFundingRateOutsideLimits) { t.Error(err) } r.RespectHistoryLimits = true - _, err = ok.GetFundingRates(contextGenerate(), r) + _, err = ok.GetHistoricalFundingRates(contextGenerate(), r) if err != nil { t.Error(err) } diff --git a/exchanges/okx/okx_wrapper.go b/exchanges/okx/okx_wrapper.go index da66dad4..97cc5579 100644 --- a/exchanges/okx/okx_wrapper.go +++ b/exchanges/okx/okx_wrapper.go @@ -111,6 +111,8 @@ func (ok *Okx) SetDefaults() { DepositHistory: true, WithdrawalHistory: true, ModifyOrder: true, + FundingRateFetching: true, + PredictedFundingRate: true, }, WebsocketCapabilities: protocol.Features{ TickerFetching: true, @@ -135,7 +137,9 @@ func (ok *Okx) SetDefaults() { CollateralMode: true, FundingRates: true, MaximumFundingRateHistory: kline.ThreeMonth.Duration(), - FundingRateFrequency: kline.EightHour.Duration(), + SupportedFundingRateFrequencies: map[kline.Interval]bool{ + kline.EightHour: true, + }, }, }, Enabled: exchange.FeaturesEnabled{ @@ -1546,7 +1550,7 @@ func (ok *Okx) getInstrumentsForOptions(ctx context.Context) ([]Instrument, erro // getInstrumentsForAsset returns the instruments for an asset type func (ok *Okx) getInstrumentsForAsset(ctx context.Context, a asset.Item) ([]Instrument, error) { if !ok.SupportsAsset(a) { - return nil, fmt.Errorf("asset type of %s is not supported by %s", a, ok.Name) + return nil, fmt.Errorf("%w: %v", asset.ErrNotSupported, a) } var instType string @@ -1568,43 +1572,58 @@ func (ok *Okx) getInstrumentsForAsset(ctx context.Context, a asset.Item) ([]Inst }) } -// GetLatestFundingRate returns the latest funding rate for a given asset and currency -func (ok *Okx) GetLatestFundingRate(ctx context.Context, r *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) { +// GetLatestFundingRates returns the latest funding rates data +func (ok *Okx) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { if r == nil { return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) } + if r.Asset != asset.PerpetualSwap { + return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset) + } + if r.Pair.IsEmpty() { + return nil, fmt.Errorf("%w, pair required", currency.ErrCurrencyPairEmpty) + } format, err := ok.GetPairFormat(r.Asset, true) if err != nil { return nil, err } fPair := r.Pair.Format(format) pairRate := fundingrate.LatestRateResponse{ - Exchange: ok.Name, - Asset: r.Asset, - Pair: fPair, + TimeChecked: time.Now(), + Exchange: ok.Name, + Asset: r.Asset, + Pair: fPair, } fr, err := ok.GetSingleFundingRate(ctx, fPair.String()) if err != nil { return nil, err } + var fri time.Duration + if len(ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } pairRate.LatestRate = fundingrate.Rate{ - Time: fr.FundingTime.Time(), + // okx funding rate is settlement time, not when it started + Time: fr.FundingTime.Time().Add(-fri), Rate: fr.FundingRate.Decimal(), } if r.IncludePredictedRate { pairRate.TimeOfNextRate = fr.NextFundingTime.Time() pairRate.PredictedUpcomingRate = fundingrate.Rate{ - Time: fr.NextFundingTime.Time(), + Time: fr.NextFundingTime.Time().Add(-fri), Rate: fr.NextFundingRate.Decimal(), } } - return &pairRate, nil + return []fundingrate.LatestRateResponse{pairRate}, nil } -// GetFundingRates returns funding rates for a given asset and currency for a time period -func (ok *Okx) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) (*fundingrate.Rates, error) { +// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period +func (ok *Okx) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { if r == nil { - return nil, fmt.Errorf("%w RatesRequest", common.ErrNilPointer) + return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer) } requestLimit := 100 sd := r.StartDate @@ -1625,7 +1644,7 @@ func (ok *Okx) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) return nil, err } fPair := r.Pair.Format(format) - pairRate := fundingrate.Rates{ + pairRate := fundingrate.HistoricalRates{ Exchange: ok.Name, Asset: r.Asset, Pair: fPair, @@ -1693,6 +1712,13 @@ func (ok *Okx) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) if sd.Equal(r.EndDate) || sd.After(r.EndDate) { break } + var fri time.Duration + if len(ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { + // can infer funding rate interval from the only funding rate frequency defined + for k := range ok.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { + fri = k.Duration() + } + } var billDetails []BillsDetailResponse billDetails, err = billDetailsFunc(ctx, &BillsDetailQueryParameter{ InstrumentType: ok.GetInstrumentTypeFromAssetItem(r.Asset), @@ -1706,7 +1732,7 @@ func (ok *Okx) GetFundingRates(ctx context.Context, r *fundingrate.RatesRequest) return nil, err } for i := range billDetails { - if index, okay := mti[billDetails[i].Timestamp.Time().Truncate(ok.Features.Supports.FuturesCapabilities.FundingRateFrequency).Unix()]; okay { + if index, okay := mti[billDetails[i].Timestamp.Time().Truncate(fri).Unix()]; okay { pairRate.FundingRates[index].Payment = billDetails[i].ProfitAndLoss.Decimal() continue } @@ -1945,7 +1971,7 @@ func (ok *Okx) GetFuturesPositionSummary(ctx context.Context, req *futures.Posit ContractMultiplier: decimal.NewFromFloat(multiplier), ContractSettlementType: contractSettlementType, AverageOpenPrice: positionSummary.AveragePrice.Decimal(), - PositionPNL: positionSummary.UPNL.Decimal(), + UnrealisedPNL: positionSummary.UPNL.Decimal(), MaintenanceMarginFraction: positionSummary.MarginRatio.Decimal(), FreeCollateral: freeCollateral, TotalCollateral: totalCollateral, diff --git a/exchanges/order/limits.go b/exchanges/order/limits.go index 4a182510..ae6ef8d0 100644 --- a/exchanges/order/limits.go +++ b/exchanges/order/limits.go @@ -42,17 +42,19 @@ var ( // ErrMarketAmountExceedsStep is when the amount is not divisible by its // step for a market order ErrMarketAmountExceedsStep = errors.New("market order amount exceeds step limit") + // ErrCannotValidateAsset is thrown when the asset is not loaded + ErrCannotValidateAsset = errors.New("cannot check limit, asset not loaded") + // ErrCannotValidateBaseCurrency is thrown when the base currency is not loaded + ErrCannotValidateBaseCurrency = errors.New("cannot check limit, base currency not loaded") + // ErrCannotValidateQuoteCurrency is thrown when the quote currency is not loaded + ErrCannotValidateQuoteCurrency = errors.New("cannot check limit, quote currency not loaded") - 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") - errExchangeLimitAsset = errors.New("exchange limits not found for asset") - 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") + 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 @@ -171,7 +173,7 @@ func (e *ExecutionLimits) GetOrderExecutionLimits(a asset.Item, cp currency.Pair m1, ok := e.m[a] if !ok { - return MinMaxLevel{}, fmt.Errorf("%w %v", errExchangeLimitAsset, a) + return MinMaxLevel{}, fmt.Errorf("%w %v", ErrCannotValidateAsset, a) } m2, ok := m1[cp.Base.Item] @@ -200,17 +202,17 @@ func (e *ExecutionLimits) CheckOrderExecutionLimits(a asset.Item, cp currency.Pa m1, ok := e.m[a] if !ok { - return errCannotValidateAsset + return ErrCannotValidateAsset } m2, ok := m1[cp.Base.Item] if !ok { - return errCannotValidateBaseCurrency + return ErrCannotValidateBaseCurrency } limit, ok := m2[cp.Quote.Item] if !ok { - return errCannotValidateQuoteCurrency + return ErrCannotValidateQuoteCurrency } err := limit.Conforms(price, amount, orderType) diff --git a/exchanges/order/limits_test.go b/exchanges/order/limits_test.go index b743b27b..072c9c5a 100644 --- a/exchanges/order/limits_test.go +++ b/exchanges/order/limits_test.go @@ -166,8 +166,8 @@ func TestGetOrderExecutionLimits(t *testing.T) { } _, err = e.GetOrderExecutionLimits(asset.Futures, ltcusd) - if !errors.Is(err, errExchangeLimitAsset) { - t.Fatalf("expected error %v but received %v", errExchangeLimitAsset, err) + if !errors.Is(err, ErrCannotValidateAsset) { + t.Fatalf("expected error %v but received %v", ErrCannotValidateAsset, err) } _, err = e.GetOrderExecutionLimits(asset.Spot, ltcusd) @@ -218,18 +218,18 @@ func TestCheckLimit(t *testing.T) { } err = e.CheckOrderExecutionLimits(asset.Futures, ltcusd, 1337, 1337, Limit) - if !errors.Is(err, errCannotValidateAsset) { - t.Fatalf("expected error %v but received %v", errCannotValidateAsset, err) + if !errors.Is(err, ErrCannotValidateAsset) { + t.Fatalf("expected error %v but received %v", ErrCannotValidateAsset, err) } err = e.CheckOrderExecutionLimits(asset.Spot, ltcusd, 1337, 1337, Limit) - if !errors.Is(err, errCannotValidateBaseCurrency) { - t.Fatalf("expected error %v but received %v", errCannotValidateBaseCurrency, err) + if !errors.Is(err, ErrCannotValidateBaseCurrency) { + t.Fatalf("expected error %v but received %v", ErrCannotValidateBaseCurrency, err) } err = e.CheckOrderExecutionLimits(asset.Spot, btcltc, 1337, 1337, Limit) - if !errors.Is(err, errCannotValidateQuoteCurrency) { - t.Fatalf("expected error %v but received %v", errCannotValidateQuoteCurrency, err) + if !errors.Is(err, ErrCannotValidateQuoteCurrency) { + t.Fatalf("expected error %v but received %v", ErrCannotValidateQuoteCurrency, err) } err = e.CheckOrderExecutionLimits(asset.Spot, btcusd, 1337, 9, Limit) diff --git a/exchanges/order/order_test.go b/exchanges/order/order_test.go index c533e272..544ae9c2 100644 --- a/exchanges/order/order_test.go +++ b/exchanges/order/order_test.go @@ -1417,7 +1417,7 @@ func TestMatchFilter(t *testing.T) { if err != nil { t.Fatal(err) } - filters := map[int]Filter{ + filters := map[int]*Filter{ 0: {}, 1: {Exchange: "Binance"}, 2: {InternalOrderID: id}, @@ -1460,13 +1460,13 @@ func TestMatchFilter(t *testing.T) { // empty filter tests emptyFilter := filters[0] for _, o := range orders { - if !o.MatchFilter(&emptyFilter) { + if !o.MatchFilter(emptyFilter) { t.Error("empty filter should match everything") } } tests := map[int]struct { - f Filter + f *Filter o Detail expectedResult bool }{ @@ -1515,7 +1515,7 @@ func TestMatchFilter(t *testing.T) { tt := tt t.Run(fmt.Sprintf("%v", num), func(t *testing.T) { t.Parallel() - if tt.o.MatchFilter(&tt.f) != tt.expectedResult { + if tt.o.MatchFilter(tt.f) != tt.expectedResult { t.Errorf("tests[%v] failed", num) } }) diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index ee0efc0c..01f60734 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1133,5 +1134,12 @@ func (p *Poloniex) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, // GetFuturesContractDetails returns all contracts from the exchange by asset type func (p *Poloniex) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { - return nil, common.ErrFunctionNotSupported + // TODO: implement with API upgrade + return nil, common.ErrNotYetImplemented +} + +// GetLatestFundingRates returns the latest funding rates data +func (p *Poloniex) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + // TODO: implement with API upgrade + return nil, common.ErrNotYetImplemented } diff --git a/exchanges/protocol/features.go b/exchanges/protocol/features.go index eaa0f231..2d12a992 100644 --- a/exchanges/protocol/features.go +++ b/exchanges/protocol/features.go @@ -3,35 +3,37 @@ package protocol // Features holds all variables for the exchanges supported features // for a protocol (e.g REST or Websocket) type Features struct { - TickerBatching bool `json:"tickerBatching,omitempty"` - AutoPairUpdates bool `json:"autoPairUpdates,omitempty"` - AccountBalance bool `json:"accountBalance,omitempty"` - CryptoDeposit bool `json:"cryptoDeposit,omitempty"` - CryptoWithdrawal bool `json:"cryptoWithdrawal,omitempty"` - FiatWithdraw bool `json:"fiatWithdraw,omitempty"` - GetOrder bool `json:"getOrder,omitempty"` - GetOrders bool `json:"getOrders,omitempty"` - CancelOrders bool `json:"cancelOrders,omitempty"` - CancelOrder bool `json:"cancelOrder,omitempty"` - SubmitOrder bool `json:"submitOrder,omitempty"` - SubmitOrders bool `json:"submitOrders,omitempty"` - ModifyOrder bool `json:"modifyOrder,omitempty"` - DepositHistory bool `json:"depositHistory,omitempty"` - WithdrawalHistory bool `json:"withdrawalHistory,omitempty"` - TradeHistory bool `json:"tradeHistory,omitempty"` - UserTradeHistory bool `json:"userTradeHistory,omitempty"` - TradeFee bool `json:"tradeFee,omitempty"` - FiatDepositFee bool `json:"fiatDepositFee,omitempty"` - FiatWithdrawalFee bool `json:"fiatWithdrawalFee,omitempty"` - CryptoDepositFee bool `json:"cryptoDepositFee,omitempty"` - CryptoWithdrawalFee bool `json:"cryptoWithdrawalFee,omitempty"` - TickerFetching bool `json:"tickerFetching,omitempty"` - KlineFetching bool `json:"klineFetching,omitempty"` - TradeFetching bool `json:"tradeFetching,omitempty"` - OrderbookFetching bool `json:"orderbookFetching,omitempty"` - AccountInfo bool `json:"accountInfo,omitempty"` - FiatDeposit bool `json:"fiatDeposit,omitempty"` - DeadMansSwitch bool `json:"deadMansSwitch,omitempty"` + TickerBatching bool `json:"tickerBatching,omitempty"` + AutoPairUpdates bool `json:"autoPairUpdates,omitempty"` + AccountBalance bool `json:"accountBalance,omitempty"` + CryptoDeposit bool `json:"cryptoDeposit,omitempty"` + CryptoWithdrawal bool `json:"cryptoWithdrawal,omitempty"` + FiatWithdraw bool `json:"fiatWithdraw,omitempty"` + GetOrder bool `json:"getOrder,omitempty"` + GetOrders bool `json:"getOrders,omitempty"` + CancelOrders bool `json:"cancelOrders,omitempty"` + CancelOrder bool `json:"cancelOrder,omitempty"` + SubmitOrder bool `json:"submitOrder,omitempty"` + SubmitOrders bool `json:"submitOrders,omitempty"` + ModifyOrder bool `json:"modifyOrder,omitempty"` + DepositHistory bool `json:"depositHistory,omitempty"` + WithdrawalHistory bool `json:"withdrawalHistory,omitempty"` + TradeHistory bool `json:"tradeHistory,omitempty"` + UserTradeHistory bool `json:"userTradeHistory,omitempty"` + TradeFee bool `json:"tradeFee,omitempty"` + FiatDepositFee bool `json:"fiatDepositFee,omitempty"` + FiatWithdrawalFee bool `json:"fiatWithdrawalFee,omitempty"` + CryptoDepositFee bool `json:"cryptoDepositFee,omitempty"` + CryptoWithdrawalFee bool `json:"cryptoWithdrawalFee,omitempty"` + TickerFetching bool `json:"tickerFetching,omitempty"` + KlineFetching bool `json:"klineFetching,omitempty"` + TradeFetching bool `json:"tradeFetching,omitempty"` + OrderbookFetching bool `json:"orderbookFetching,omitempty"` + AccountInfo bool `json:"accountInfo,omitempty"` + FiatDeposit bool `json:"fiatDeposit,omitempty"` + DeadMansSwitch bool `json:"deadMansSwitch,omitempty"` + FundingRateFetching bool `json:"fundingRateFetching"` + PredictedFundingRate bool `json:"predictedFundingRate,omitempty"` // FullPayloadSubscribe flushes and changes full subscription on websocket // connection by subscribing with full default stream channel list FullPayloadSubscribe bool `json:"fullPayloadSubscribe,omitempty"` diff --git a/exchanges/sharedtestvalues/customex.go b/exchanges/sharedtestvalues/customex.go index d55571ce..776c233c 100644 --- a/exchanges/sharedtestvalues/customex.go +++ b/exchanges/sharedtestvalues/customex.go @@ -12,6 +12,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -360,6 +361,16 @@ func (c *CustomEx) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) e return nil } +// GetHistoricalFundingRates returns funding rates for a given asset and currency for a time period +func (c *CustomEx) GetHistoricalFundingRates(_ context.Context, _ *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) { + return nil, nil +} + +// GetLatestFundingRates returns the latest funding rates data +func (c *CustomEx) GetLatestFundingRates(_ context.Context, _ *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, nil +} + // GetFuturesContractDetails returns all contracts from the exchange by asset type func (c *CustomEx) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index 8a11c53a..7ec55d87 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -17,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -766,3 +767,8 @@ func (y *Yobit) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, err func (y *Yobit) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (y *Yobit) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index 0cebb6c1..d3a923ac 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" + "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -1152,3 +1153,8 @@ func (z *ZB) GetAvailableTransferChains(ctx context.Context, cryptocurrency curr func (z *ZB) GetFuturesContractDetails(context.Context, asset.Item) ([]futures.Contract, error) { return nil, common.ErrFunctionNotSupported } + +// GetLatestFundingRates returns the latest funding rates data +func (z *ZB) GetLatestFundingRates(context.Context, *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + return nil, common.ErrFunctionNotSupported +} diff --git a/testdata/http_mock/binance/binance.json b/testdata/http_mock/binance/binance.json index fd48e28f..12451766 100644 --- a/testdata/http_mock/binance/binance.json +++ b/testdata/http_mock/binance/binance.json @@ -262367,9 +262367,19 @@ } ] }, + "/dapi/v1/fundingInfo": { + "GET": [ + { + "data": null, + "queryString": "", + "bodyParams": "", + "headers": {} + } + ] + }, "/dapi/v1/fundingRate": { "GET": [ - { + { "data": [ { "fundingRate": "0.00010000", @@ -262397,7 +262407,7 @@ "symbol": "BTCUSD_PERP" } ], - "queryString": "endTime=1580515200000&limit=1000&startTime=1577836800000&symbol=BTCUSD_PERP", + "queryString": "endTime=1580515200000\u0026limit=1000\u0026startTime=1577836800000\u0026symbol=BTCUSD_PERP", "bodyParams": "", "headers": {} }, @@ -263394,6 +263404,695 @@ "queryString": "symbol=BTCUSD_PERP", "bodyParams": "", "headers": {} + }, + { + "data": [ + { + "estimatedSettlePrice": "5.98305943", + "indexPrice": "5.99608046", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "6.10235122", + "nextFundingTime": 0, + "pair": "LINKUSD", + "symbol": "LINKUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.10673771", + "indexPrice": "4.12005486", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "4.12235138", + "nextFundingTime": 0, + "pair": "DOTUSD", + "symbol": "DOTUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "3.10597019", + "indexPrice": "3.11371069", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004207", + "markPrice": "3.11300000", + "nextFundingTime": 1694419200000, + "pair": "FILUSD", + "symbol": "FILUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.49424516", + "indexPrice": "0.49509765", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "0.50212503", + "nextFundingTime": 0, + "pair": "XRPUSD", + "symbol": "XRPUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.14866513", + "indexPrice": "0.14904614", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00000660", + "markPrice": "0.14890000", + "nextFundingTime": 1694419200000, + "pair": "GMTUSD", + "symbol": "GMTUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.67023180", + "indexPrice": "0.67136966", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00026801", + "markPrice": "0.66920000", + "nextFundingTime": 1694419200000, + "pair": "XTZUSD", + "symbol": "XTZUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "189.76387568", + "indexPrice": "190.27248939", + "interestRate": "0.00010000", + "lastFundingRate": "0.00001624", + "markPrice": "190.22000000", + "nextFundingTime": 1694419200000, + "pair": "BCHUSD", + "symbol": "BCHUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "189.76387568", + "indexPrice": "190.27248939", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "189.47489159", + "nextFundingTime": 0, + "pair": "BCHUSD", + "symbol": "BCHUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.49424516", + "indexPrice": "0.49509765", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "0.49654169", + "nextFundingTime": 0, + "pair": "XRPUSD", + "symbol": "XRPUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "5.98305943", + "indexPrice": "5.99608046", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00000394", + "markPrice": "5.99171788", + "nextFundingTime": 1694419200000, + "pair": "LINKUSD", + "symbol": "LINKUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "25709.96183509", + "indexPrice": "25735.28694227", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004887", + "markPrice": "25720.70740797", + "nextFundingTime": 1694419200000, + "pair": "BTCUSD", + "symbol": "BTCUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.49424516", + "indexPrice": "0.49509765", + "interestRate": "0.00010000", + "lastFundingRate": "0.00003679", + "markPrice": "0.49492225", + "nextFundingTime": 1694419200000, + "pair": "XRPUSD", + "symbol": "XRPUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "5.98305943", + "indexPrice": "5.99608046", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "6.01514011", + "nextFundingTime": 0, + "pair": "LINKUSD", + "symbol": "LINKUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1.53426890", + "indexPrice": "1.53619621", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00020772", + "markPrice": "1.53431719", + "nextFundingTime": 1694419200000, + "pair": "RUNEUSD", + "symbol": "RUNEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "25709.96183509", + "indexPrice": "25735.28694227", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "26092.24351908", + "nextFundingTime": 0, + "pair": "BTCUSD", + "symbol": "BTCUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.05675151", + "indexPrice": "0.05689040", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.05688260", + "nextFundingTime": 1694419200000, + "pair": "CHZUSD", + "symbol": "CHZUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "211.67251057", + "indexPrice": "211.60154982", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "210.38483368", + "nextFundingTime": 0, + "pair": "BNBUSD", + "symbol": "BNBUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0", + "indexPrice": "0.01482385", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.01473491", + "nextFundingTime": 1694419200000, + "pair": "GALAUSD", + "symbol": "GALAUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.07828221", + "indexPrice": "0.07836312", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.07831000", + "nextFundingTime": 1694419200000, + "pair": "TRXUSD", + "symbol": "TRXUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.09169686", + "indexPrice": "0.09192646", + "interestRate": "0.00010000", + "lastFundingRate": "0.00007248", + "markPrice": "0.09181226", + "nextFundingTime": 1694419200000, + "pair": "ALGOUSD", + "symbol": "ALGOUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "211.67251057", + "indexPrice": "211.60154982", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "206.85230590", + "nextFundingTime": 0, + "pair": "BNBUSD", + "symbol": "BNBUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.06110966", + "indexPrice": "0.06126664", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.06125000", + "nextFundingTime": 1694419200000, + "pair": "DOGEUSD", + "symbol": "DOGEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "9.40639668", + "indexPrice": "9.42874590", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004538", + "markPrice": "9.41973781", + "nextFundingTime": 1694419200000, + "pair": "AVAXUSD", + "symbol": "AVAXUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.29313952", + "indexPrice": "0.29371279", + "interestRate": "0.00010000", + "lastFundingRate": "0.00008715", + "markPrice": "0.29352266", + "nextFundingTime": 1694419200000, + "pair": "SANDUSD", + "symbol": "SANDUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.51907941", + "indexPrice": "0.52034250", + "interestRate": "0.00010000", + "lastFundingRate": "0.00007855", + "markPrice": "0.52000000", + "nextFundingTime": 1694419200000, + "pair": "MATICUSD", + "symbol": "MATICUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.55917817", + "indexPrice": "0.56026570", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.56000000", + "nextFundingTime": 1694419200000, + "pair": "EOSUSD", + "symbol": "EOSUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "61.08722760", + "indexPrice": "61.21094078", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "61.81475791", + "nextFundingTime": 0, + "pair": "LTCUSD", + "symbol": "LTCUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "6.62118246", + "indexPrice": "6.63098588", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00012913", + "markPrice": "6.62200000", + "nextFundingTime": 1694419200000, + "pair": "ATOMUSD", + "symbol": "ATOMUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "25709.96183509", + "indexPrice": "25735.28694227", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "25737.81185242", + "nextFundingTime": 0, + "pair": "BTCUSD", + "symbol": "BTCUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.59839140", + "indexPrice": "0.60008085", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.59980785", + "nextFundingTime": 1694419200000, + "pair": "THETAUSD", + "symbol": "THETAUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "61.08722760", + "indexPrice": "61.21094078", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "61.22534124", + "nextFundingTime": 0, + "pair": "LTCUSD", + "symbol": "LTCUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0", + "indexPrice": "0.01555103", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.01549787", + "nextFundingTime": 1694419200000, + "pair": "ZILUSD", + "symbol": "ZILUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1.25837753", + "indexPrice": "1.26388236", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00003978", + "markPrice": "1.26341023", + "nextFundingTime": 1694419200000, + "pair": "OPUSD", + "symbol": "OPUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.13324903", + "indexPrice": "0.13388503", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00011125", + "markPrice": "0.13378063", + "nextFundingTime": 1694419200000, + "pair": "XLMUSD", + "symbol": "XLMUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0", + "indexPrice": "0.01517998", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.01517132", + "nextFundingTime": 1694419200000, + "pair": "VETUSD", + "symbol": "VETUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.24783468", + "indexPrice": "0.24801487", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "0.24844096", + "nextFundingTime": 0, + "pair": "ADAUSD", + "symbol": "ADAUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "15.03458674", + "indexPrice": "15.04121219", + "interestRate": "0.00010000", + "lastFundingRate": "0.00001197", + "markPrice": "15.03200000", + "nextFundingTime": 1694419200000, + "pair": "ETCUSD", + "symbol": "ETCUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.21483460", + "indexPrice": "4.22193353", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "4.21825776", + "nextFundingTime": 1694419200000, + "pair": "UNIUSD", + "symbol": "UNIUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "23.34740865", + "indexPrice": "23.38262705", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00013508", + "markPrice": "23.36900000", + "nextFundingTime": 1694419200000, + "pair": "EGLDUSD", + "symbol": "EGLDUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "142.62518042", + "indexPrice": "143.16242243", + "interestRate": "0.00010000", + "lastFundingRate": "0.00009462", + "markPrice": "143.12000000", + "nextFundingTime": 1694419200000, + "pair": "XMRUSD", + "symbol": "XMRUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "61.08722760", + "indexPrice": "61.21094078", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00005797", + "markPrice": "61.18000000", + "nextFundingTime": 1694419200000, + "pair": "LTCUSD", + "symbol": "LTCUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.27873276", + "indexPrice": "0.27952235", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00007630", + "markPrice": "0.27940000", + "nextFundingTime": 1694419200000, + "pair": "MANAUSD", + "symbol": "MANAUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.19218924", + "indexPrice": "0.19249999", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004105", + "markPrice": "0.19240000", + "nextFundingTime": 1694419200000, + "pair": "FTMUSD", + "symbol": "FTMUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.16099506", + "indexPrice": "0.16151141", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.16150000", + "nextFundingTime": 1694419200000, + "pair": "ICXUSD", + "symbol": "ICXUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.10673771", + "indexPrice": "4.12005486", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00010056", + "markPrice": "4.11700000", + "nextFundingTime": 1694419200000, + "pair": "DOTUSD", + "symbol": "DOTUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "53.69550437", + "indexPrice": "53.78489019", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00002744", + "markPrice": "53.69982355", + "nextFundingTime": 1694419200000, + "pair": "AAVEUSD", + "symbol": "AAVEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1610.47089411", + "indexPrice": "1612.50073389", + "interestRate": "0.00010000", + "lastFundingRate": "0.00004075", + "markPrice": "1611.64669108", + "nextFundingTime": 1694419200000, + "pair": "ETHUSD", + "symbol": "ETHUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1.21414412", + "indexPrice": "1.20942310", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "1.20939523", + "nextFundingTime": 1694419200000, + "pair": "APEUSD", + "symbol": "APEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "189.76387568", + "indexPrice": "190.27248939", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "186.54308604", + "nextFundingTime": 0, + "pair": "BCHUSD", + "symbol": "BCHUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1610.47089411", + "indexPrice": "1612.50073389", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "1630.17757997", + "nextFundingTime": 0, + "pair": "ETHUSD", + "symbol": "ETHUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.24783468", + "indexPrice": "0.24801487", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.24790000", + "nextFundingTime": 1694419200000, + "pair": "ADAUSD", + "symbol": "ADAUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "211.67251057", + "indexPrice": "211.60154982", + "interestRate": "0", + "lastFundingRate": "0", + "markPrice": "211.67497257", + "nextFundingTime": 1694419200000, + "pair": "BNBUSD", + "symbol": "BNBUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "18.27181500", + "indexPrice": "18.35939604", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00051634", + "markPrice": "18.33565485", + "nextFundingTime": 1694419200000, + "pair": "SOLUSD", + "symbol": "SOLUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.52198053", + "indexPrice": "0.52291416", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00022361", + "markPrice": "0.52231323", + "nextFundingTime": 1694419200000, + "pair": "KNCUSD", + "symbol": "KNCUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.24783468", + "indexPrice": "0.24801487", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "0.24992013", + "nextFundingTime": 0, + "pair": "ADAUSD", + "symbol": "ADAUSD_231229", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "7.41016438", + "indexPrice": "7.42285423", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "7.41697383", + "nextFundingTime": 1694419200000, + "pair": "ENSUSD", + "symbol": "ENSUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1610.47089411", + "indexPrice": "1612.50073389", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "1613.47385775", + "nextFundingTime": 0, + "pair": "ETHUSD", + "symbol": "ETHUSD_230929", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "0.03827183", + "indexPrice": "0.03839887", + "interestRate": "0.00010000", + "lastFundingRate": "0.00010000", + "markPrice": "0.03838000", + "nextFundingTime": 1694419200000, + "pair": "ROSEUSD", + "symbol": "ROSEUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "5.11847228", + "indexPrice": "5.12624707", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00025858", + "markPrice": "5.12474383", + "nextFundingTime": 1694419200000, + "pair": "APTUSD", + "symbol": "APTUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.34382753", + "indexPrice": "4.36395226", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00052514", + "markPrice": "4.35735921", + "nextFundingTime": 1694419200000, + "pair": "AXSUSD", + "symbol": "AXSUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "1.13703668", + "indexPrice": "1.13905831", + "interestRate": "0.00010000", + "lastFundingRate": "-0.00021479", + "markPrice": "1.13800000", + "nextFundingTime": 1694419200000, + "pair": "NEARUSD", + "symbol": "NEARUSD_PERP", + "time": 1694401588000 + }, + { + "estimatedSettlePrice": "4.10673771", + "indexPrice": "4.12005486", + "interestRate": "", + "lastFundingRate": "", + "markPrice": "4.08623194", + "nextFundingTime": 0, + "pair": "DOTUSD", + "symbol": "DOTUSD_231229", + "time": 1694401588000 + } + ], + "queryString": "", + "bodyParams": "", + "headers": {} } ] }, @@ -299843,6 +300542,1221 @@ } ] }, + "/fapi/v1/fundingInfo": { + "GET": [ + { + "data": [ + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "BLZUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GTCUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "LPTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "UNFIUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "TRBUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "PERPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "HIFIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ARKUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "IMXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FRONTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "FLMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GLMRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ENJUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BICOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "OGNUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BNTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "STMXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FOOTBALLUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DEFIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BTCDOMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BLUEBIRDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "QNTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ALGOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RNDRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FLOWUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1000XECUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FXSUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "WOOUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ARUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ASTRUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BATUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LRCUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ENSUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SEIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CVXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RVNUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ANTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "JASMYUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "AUDIOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SSVUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ICXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "TUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1000FLOKIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LUNA2USDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IOTXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "PENDLEUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BANDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ONEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MAGICUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CKBUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SKLUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GALUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DGBUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ACHUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ZENUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "UMAUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "API3USDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "EDUUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "NMRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "JOEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LQTYUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "XVSUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RADUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HFTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RDNTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "NKNUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "OXTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "DODOXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LINAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MAVUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BNXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "XVGUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "CYBERUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "YGGUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ARKMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SPELLUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ARPAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CTKUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HOOKUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ALICEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "TRUUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "AGLDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BELUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "COMBOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "TLMUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BAKEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LEVERUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ATAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KEYUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IDEXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MDTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "REEFUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "LITUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "PHBUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "AMBUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "STORJUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HBARUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ICPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GRTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "EGLDUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "STXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RUNEUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "THETAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "INJUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SNXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KAVAUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CHZUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ZECUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IOTAUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CRVUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KLAYUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MINAUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1000LUNCUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "COMPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DASHUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GMXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ROSEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1INCHUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ZILUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "1000PEPEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SFPUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "XEMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "QTUMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "AGIXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CELOUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "OCEANUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "FETUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ANKRUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "GMTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "YFIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HOTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "WAVESUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BALUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "BLURUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KSMUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SXPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ZRXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "SUSHIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ONTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "IOSTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "TOMOUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "KNCUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RSRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "STGUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CTSIUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CELRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "MTLUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "C98USDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RLCUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "CHRUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "HIGHUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "ALPHAUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "OMGUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DENTUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "COTIUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "RENUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DUSKUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "PEOPLEUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 8, + "symbol": "DARUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "STRAXUSDT" + }, + { + "adjustedFundingRateCap": "0.03000000", + "adjustedFundingRateFloor": "-0.03000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "LOOMUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "BIGTIMEUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "BONDUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "ORBSUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "STPTUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "WAXPUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "BSVUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "RIFUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "POLYXUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "GASUSDT" + }, + { + "adjustedFundingRateCap": "0.02000000", + "adjustedFundingRateFloor": "-0.02000000", + "disclaimer": false, + "fundingIntervalHours": 4, + "symbol": "POWRUSDT" + } + ], + "queryString": "", + "bodyParams": "", + "headers": {} + } + ] + }, "/fapi/v1/fundingRate": { "GET": [ { @@ -299869,30 +301783,6 @@ "bodyParams": "", "headers": {} }, - { - "data": [ - { - "fundingRate": "0.00010000", - "fundingTime": 1602979200006, - "symbol": "BTCUSDT" - } - ], - "queryString": "limit=1\u0026symbol=BTCUSDT", - "bodyParams": "", - "headers": {} - }, - { - "data": [ - { - "fundingRate": "0.00010000", - "fundingTime": 1602979200006, - "symbol": "LTCUSDT" - } - ], - "queryString": "endTime=1580515200000\u0026limit=1\u0026startTime=1577836800000\u0026symbol=LTCUSDT", - "bodyParams": "", - "headers": {} - }, { "data": [ { @@ -300882,17 +302772,40 @@ "queryString": "endTime=1580515200000\u0026limit=1000\u0026startTime=1577836800000\u0026symbol=BTCUSDT", "bodyParams": "", "headers": {} - } - ] - }, - "/fapi/v1/historicalTrades": { - "GET": [ + }, { - "data": { - "code": -2014, - "msg": "API-key format invalid." - }, - "queryString": "limit=5\u0026symbol=BTCUSDT", + "data": [ + { + "fundingRate": "0.00010000", + "fundingTime": 1698220800000, + "symbol": "LTCUSDT" + } + ], + "queryString": "endTime=1698719354537\u0026limit=1\u0026startTime=1698200954537\u0026symbol=LTCUSDT", + "bodyParams": "", + "headers": {} + }, + { + "data": [ + { + "fundingRate": "0.00010000", + "fundingTime": 1698710400001, + "symbol": "BTCUSDT" + } + ], + "queryString": "limit=1\u0026symbol=BTCUSDT", + "bodyParams": "", + "headers": {} + }, + { + "data": [ + { + "fundingRate": "0.00010000", + "fundingTime": 1602979200006, + "symbol": "LTCUSDT" + } + ], + "queryString": "endTime=1580515200000\u0026limit=1\u0026startTime=1577836800000\u0026symbol=LTCUSDT", "bodyParams": "", "headers": {} }