diff --git a/exchanges/fundingrate/fundingrate_types.go b/exchanges/fundingrate/fundingrate_types.go index a8376deb..79508ef4 100644 --- a/exchanges/fundingrate/fundingrate_types.go +++ b/exchanges/fundingrate/fundingrate_types.go @@ -9,8 +9,14 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/asset" ) -// ErrFundingRateOutsideLimits is returned when a funding rate is outside the allowed date range -var ErrFundingRateOutsideLimits = errors.New("funding rate outside limits") +var ( + // ErrFundingRateOutsideLimits is returned when a funding rate is outside the allowed date range + ErrFundingRateOutsideLimits = errors.New("funding rate outside limits") + // ErrPaymentCurrencyCannotBeEmpty is returned when a payment currency is not set + ErrPaymentCurrencyCannotBeEmpty = errors.New("payment currency cannot be empty") + // ErrNoFundingRatesFound is returned when no funding rates are found + ErrNoFundingRatesFound = errors.New("no funding rates found") +) // HistoricalRatesRequest is used to request funding rate details for a position type HistoricalRatesRequest struct { diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 33d4be1a..b1475f7e 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -11,6 +11,7 @@ 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" @@ -3455,3 +3456,85 @@ func TestGetLatestFundingRates(t *testing.T) { t.Error(err) } } + +func TestGetHistoricalFundingRates(t *testing.T) { + t.Parallel() + _, err := g.GetHistoricalFundingRates(context.Background(), nil) + if !errors.Is(err, common.ErrNilPointer) { + t.Fatalf("received: %v, expected: %v", err, common.ErrNilPointer) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{}) + if !errors.Is(err, asset.ErrNotSupported) { + t.Fatalf("received: %v, expected: %v", err, asset.ErrNotSupported) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + }) + if !errors.Is(err, currency.ErrCurrencyPairEmpty) { + t.Fatalf("received: %v, expected: %v", err, currency.ErrCurrencyPairEmpty) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + }) + if !errors.Is(err, fundingrate.ErrPaymentCurrencyCannotBeEmpty) { + t.Fatalf("received: %v, expected: %v", err, fundingrate.ErrPaymentCurrencyCannotBeEmpty) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + IncludePayments: true, + IncludePredictedRate: true, + }) + if !errors.Is(err, common.ErrNotYetImplemented) { + t.Fatalf("received: %v, expected: %v", err, common.ErrNotYetImplemented) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + IncludePredictedRate: true, + }) + if !errors.Is(err, common.ErrNotYetImplemented) { + t.Fatalf("received: %v, expected: %v", err, common.ErrNotYetImplemented) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + StartDate: time.Now().Add(time.Hour * 16), + EndDate: time.Now(), + }) + if !errors.Is(err, common.ErrStartAfterEnd) { + t.Fatalf("received: %v, expected: %v", err, common.ErrStartAfterEnd) + } + + _, err = g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + StartDate: time.Now().Add(-time.Hour * 8008), + EndDate: time.Now(), + }) + if !errors.Is(err, fundingrate.ErrFundingRateOutsideLimits) { + t.Fatalf("received: %v, expected: %v", err, fundingrate.ErrFundingRateOutsideLimits) + } + + history, err := g.GetHistoricalFundingRates(context.Background(), &fundingrate.HistoricalRatesRequest{ + Asset: asset.Futures, + Pair: currency.NewPair(currency.ENJ, currency.USDT), + PaymentCurrency: currency.USDT, + }) + if !errors.Is(err, nil) { + t.Fatalf("received: %v, expected: %v", err, nil) + } + + assert.NotEmpty(t, history, "should return values") +} diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index e0cc7dce..0531d213 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -11,6 +11,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" @@ -2168,6 +2169,88 @@ func (g *Gateio) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) e return g.LoadLimits(limits) } +// GetHistoricalFundingRates returns historical funding rates for a futures contract +func (g *Gateio) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, 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() { + return nil, currency.ErrCurrencyPairEmpty + } + + if !r.StartDate.IsZero() && !r.EndDate.IsZero() { + err := common.StartEndTimeCheck(r.StartDate, r.EndDate) + if err != nil { + return nil, err + } + } + + // NOTE: Opted to fail here as a misconfigured request will result in + // {"label":"CONTRACT_NOT_FOUND"} and rather not mutate request using + // quote currency as the settlement currency. + if r.PaymentCurrency.IsEmpty() { + return nil, fundingrate.ErrPaymentCurrencyCannotBeEmpty + } + + if r.IncludePayments { + return nil, fmt.Errorf("include payments %w", common.ErrNotYetImplemented) + } + + if r.IncludePredictedRate { + return nil, fmt.Errorf("include predicted rate %w", common.ErrNotYetImplemented) + } + + fPair, err := g.FormatExchangeCurrency(r.Pair, r.Asset) + if err != nil { + return nil, err + } + + records, err := g.GetFutureFundingRates(ctx, r.PaymentCurrency.String(), fPair, 1000) + if err != nil { + return nil, err + } + + if len(records) == 0 { + return nil, fundingrate.ErrNoFundingRatesFound + } + + if !r.StartDate.IsZero() && !r.RespectHistoryLimits && r.StartDate.Before(records[len(records)-1].Timestamp.Time()) { + return nil, fmt.Errorf("%w start date requested: %v last returned record: %v", fundingrate.ErrFundingRateOutsideLimits, r.StartDate, records[len(records)-1].Timestamp.Time()) + } + + fundingRates := make([]fundingrate.Rate, 0, len(records)) + for i := range records { + if (!r.EndDate.IsZero() && r.EndDate.Before(records[i].Timestamp.Time())) || + (!r.StartDate.IsZero() && r.StartDate.After(records[i].Timestamp.Time())) { + continue + } + + fundingRates = append(fundingRates, fundingrate.Rate{ + Rate: decimal.NewFromFloat(records[i].Rate.Float64()), + Time: records[i].Timestamp.Time(), + }) + } + + if len(fundingRates) == 0 { + return nil, fundingrate.ErrNoFundingRatesFound + } + + return &fundingrate.HistoricalRates{ + Exchange: g.Name, + Asset: r.Asset, + Pair: r.Pair, + FundingRates: fundingRates, + StartDate: fundingRates[len(fundingRates)-1].Time, + EndDate: fundingRates[0].Time, + LatestRate: fundingRates[0], + PaymentCurrency: r.PaymentCurrency, + }, nil +} + // GetLatestFundingRates returns the latest funding rates data func (g *Gateio) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { if r == nil {