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
This commit is contained in:
Scott
2023-11-03 10:01:32 +10:00
committed by GitHub
parent f9437dbd08
commit 70690d9a04
78 changed files with 4088 additions and 527 deletions

View File

@@ -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")

View File

@@ -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")
}
}

View File

@@ -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
}