Binance,OKx: Implement fetching funding rates (#1239)

* adds basic groundwork for rates on binance

* more into rates on binance

* rm redudant redundancy, add payments

* mini commit before merging with testnet ability branch

* changes function signature and fixes resulting build

* gets billing data too

* funding rates package, features use, testnet reimpl

* new endpoint, refinements and tests

* cli fix, rpc impl, testing, payments

* fixups from looking at code

* typo fix

* niteroos

* merge fixes

* adds test, fixes cli issues

* woah nelly
This commit is contained in:
Scott
2023-07-26 14:25:43 +10:00
committed by GitHub
parent 2ad9304045
commit 471f4f21c4
39 changed files with 5785 additions and 3416 deletions

View File

@@ -10,8 +10,8 @@ import (
var (
// ErrNotSupported is an error for an unsupported asset type
ErrNotSupported = errors.New("unsupported asset type")
// ErrNotEnabled returned when a supported asset type is disabled
ErrNotEnabled = errors.New("asset type disabled")
// ErrNotEnabled is an error for an asset not enabled
ErrNotEnabled = errors.New("asset type not enabled")
)
// Item stores the asset type

View File

@@ -35,6 +35,9 @@ const (
cfuturesAPIURL = "https://dapi.binance.com"
ufuturesAPIURL = "https://fapi.binance.com"
testnetSpotURL = "https://testnet.binance.vision/api"
testnetFutures = "https://testnet.binancefuture.com"
// Public endpoints
exchangeInfo = "/api/v3/exchangeInfo"
orderBookDepth = "/api/v3/depth"
@@ -49,6 +52,9 @@ const (
perpExchangeInfo = "/fapi/v1/exchangeInfo"
historicalTrades = "/api/v3/historicalTrades"
// Margin endpoints
marginInterestHistory = "/sapi/v1/margin/interestHistory"
// Authenticated endpoints
newOrderTest = "/api/v3/order/test"
orderEndpoint = "/api/v3/order"
@@ -76,8 +82,8 @@ const (
defaultRecvWindow = 5 * time.Second
)
// GetInterestHistory gets interest history for currency/currencies provided
func (b *Binance) GetInterestHistory(ctx context.Context) (MarginInfoData, error) {
// GetUndocumentedInterestHistory gets interest history for currency/currencies provided
func (b *Binance) GetUndocumentedInterestHistory(ctx context.Context) (MarginInfoData, error) {
var resp MarginInfoData
if err := b.SendHTTPRequest(ctx, exchange.EdgeCase1, undocumentedInterestHistory, spotDefaultRate, &resp); err != nil {
return resp, err
@@ -212,6 +218,41 @@ func (b *Binance) GetHistoricalTrades(ctx context.Context, symbol string, limit
b.SendAPIKeyHTTPRequest(ctx, exchange.RestSpotSupplementary, path, spotDefaultRate, &resp)
}
// GetUserMarginInterestHistory returns margin interest history for the user
func (b *Binance) GetUserMarginInterestHistory(ctx context.Context, assetCurrency currency.Code, isolatedSymbol currency.Pair, startTime, endTime time.Time, currentPage, size int64, archived bool) (*UserMarginInterestHistoryResponse, error) {
params := url.Values{}
if !assetCurrency.IsEmpty() {
params.Set("asset", assetCurrency.String())
}
if !isolatedSymbol.IsEmpty() {
fPair, err := b.FormatSymbol(isolatedSymbol, asset.Margin)
if err != nil {
return nil, err
}
params.Set("isolatedSymbol", fPair)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if currentPage > 0 {
params.Set("current", strconv.FormatInt(currentPage, 10))
}
if size > 0 {
params.Set("size", strconv.FormatInt(size, 10))
}
if archived {
params.Set("archived", "true")
}
path := marginInterestHistory + "?" + params.Encode()
var resp UserMarginInterestHistoryResponse
return &resp, b.SendAPIKeyHTTPRequest(ctx, exchange.RestSpotSupplementary, path, spotDefaultRate, &resp)
}
// GetAggregatedTrades returns aggregated trade activity.
// If more than one hour of data is requested or asked limit is not supported by exchange
// then the trades are collected with multiple backend requests.

View File

@@ -11,6 +11,7 @@ import (
"testing"
"github.com/thrasher-corp/gocryptotrader/config"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
)
@@ -32,6 +33,20 @@ func TestMain(m *testing.M) {
binanceConfig.API.Credentials.Secret = apiSecret
b.SetDefaults()
b.Websocket = sharedtestvalues.NewTestWebsocket()
if useTestNet {
err = b.API.Endpoints.SetRunning(exchange.RestUSDTMargined.String(), testnetFutures)
if err != nil {
log.Fatal("Binance setup error", err)
}
err = b.API.Endpoints.SetRunning(exchange.RestCoinMargined.String(), testnetFutures)
if err != nil {
log.Fatal("Binance setup error", err)
}
err = b.API.Endpoints.SetRunning(exchange.RestSpot.String(), testnetSpotURL)
if err != nil {
log.Fatal("Binance setup error", err)
}
}
err = b.Setup(binanceConfig)
if err != nil {
log.Fatal("Binance setup error", err)

View File

@@ -21,6 +21,9 @@ const mockfile = "../../testdata/http_mock/binance/binance.json"
var mockTests = true
func TestMain(m *testing.M) {
if useTestNet {
log.Fatal("cannot use testnet with mock tests")
}
cfg := config.GetConfig()
err := cfg.LoadConfig("../../testdata/configtest.json", true)
if err != nil {

View File

@@ -15,6 +15,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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
@@ -26,6 +27,7 @@ const (
apiKey = ""
apiSecret = ""
canManipulateRealOrders = false
useTestNet = false
)
var (
@@ -622,9 +624,9 @@ func TestGetFuturesExchangeInfo(t *testing.T) {
}
}
func TestGetInterestHistory(t *testing.T) {
func TestGetUndocumentedInterestHistory(t *testing.T) {
t.Parallel()
_, err := b.GetInterestHistory(context.Background())
_, err := b.GetUndocumentedInterestHistory(context.Background())
if err != nil {
t.Error(err)
}
@@ -638,19 +640,6 @@ func TestGetCrossMarginInterestHistory(t *testing.T) {
}
}
func TestGetFundingRates(t *testing.T) {
t.Parallel()
_, err := b.FundingRates(context.Background(), currency.NewPair(currency.BTC, currency.USDT), "", time.Time{}, time.Time{})
if err != nil {
t.Error(err)
}
start, end := getTime()
_, err = b.FundingRates(context.Background(), currency.NewPair(currency.BTC, currency.USDT), "2", start, end)
if err != nil {
t.Error(err)
}
}
func TestGetFuturesOrderbook(t *testing.T) {
t.Parallel()
_, err := b.GetFuturesOrderbook(context.Background(), currency.NewPairWithDelimiter("BTCUSD", "PERP", "_"), 1000)
@@ -2817,3 +2806,135 @@ func TestUpdateOrderExecutionLimits(t *testing.T) {
}
}
}
func TestGetFundingRates(t *testing.T) {
t.Parallel()
s, e := getTime()
_, err := b.GetFundingRates(context.Background(), &fundingrate.RatesRequest{
Asset: asset.USDTMarginedFutures,
Pair: currency.NewPair(currency.BTC, currency.USDT),
StartDate: s,
EndDate: e,
IncludePayments: true,
IncludePredictedRate: true,
})
if !errors.Is(err, common.ErrFunctionNotSupported) {
t.Error(err)
}
_, err = b.GetFundingRates(context.Background(), &fundingrate.RatesRequest{
Asset: asset.USDTMarginedFutures,
Pair: currency.NewPair(currency.BTC, currency.USDT),
StartDate: s,
EndDate: e,
PaymentCurrency: currency.DOGE,
})
if !errors.Is(err, common.ErrFunctionNotSupported) {
t.Error(err)
}
r := &fundingrate.RatesRequest{
Asset: asset.USDTMarginedFutures,
Pair: currency.NewPair(currency.BTC, currency.USDT),
StartDate: s,
EndDate: e,
}
if sharedtestvalues.AreAPICredentialsSet(b) {
r.IncludePayments = true
}
_, err = b.GetFundingRates(context.Background(), r)
if err != nil {
t.Error(err)
}
r.Asset = asset.CoinMarginedFutures
r.Pair, err = currency.NewPairFromString("BTCUSD_PERP")
if err != nil {
t.Fatal(err)
}
_, err = b.GetFundingRates(context.Background(), r)
if err != nil {
t.Error(err)
}
}
func TestGetLatestFundingRate(t *testing.T) {
t.Parallel()
_, err := b.GetLatestFundingRate(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.GetLatestFundingRate(context.Background(), &fundingrate.LatestRateRequest{
Asset: asset.USDTMarginedFutures,
Pair: currency.NewPair(currency.BTC, currency.USDT),
})
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{
Asset: asset.CoinMarginedFutures,
Pair: cp,
})
if err != nil {
t.Error(err)
}
}
func TestIsPerpetualFutureCurrency(t *testing.T) {
t.Parallel()
is, err := b.IsPerpetualFutureCurrency(asset.Binary, currency.NewPair(currency.BTC, currency.USDT))
if err != nil {
t.Error(err)
}
if is {
t.Error("expected false")
}
is, err = b.IsPerpetualFutureCurrency(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.USDT))
if err != nil {
t.Error(err)
}
if is {
t.Error("expected false")
}
is, err = b.IsPerpetualFutureCurrency(asset.CoinMarginedFutures, currency.NewPair(currency.BTC, currency.PERP))
if err != nil {
t.Error(err)
}
if !is {
t.Error("expected true")
}
is, err = b.IsPerpetualFutureCurrency(asset.USDTMarginedFutures, currency.NewPair(currency.BTC, currency.USDT))
if err != nil {
t.Error(err)
}
if !is {
t.Error("expected true")
}
is, err = b.IsPerpetualFutureCurrency(asset.USDTMarginedFutures, currency.NewPair(currency.BTC, currency.PERP))
if err != nil {
t.Error(err)
}
if is {
t.Error("expected false")
}
}
func TestGetUserMarginInterestHistory(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
_, err := b.GetUserMarginInterestHistory(context.Background(), currency.USDT, currency.NewPair(currency.BTC, currency.USDT), time.Now().Add(-time.Hour*24), time.Now(), 1, 10, false)
if err != nil {
t.Error(err)
}
}

View File

@@ -5,6 +5,7 @@ import (
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
@@ -294,14 +295,14 @@ type AggregatedTrade struct {
// IndexMarkPrice stores data for index and mark prices
type IndexMarkPrice struct {
Symbol string `json:"symbol"`
Pair string `json:"pair"`
MarkPrice float64 `json:"markPrice,string"`
IndexPrice float64 `json:"indexPrice,string"`
EstimatedSettlePrice float64 `json:"estimatedSettlePrice,string"`
LastFundingRate string `json:"lastFundingRate"`
NextFundingTime int64 `json:"nextFundingTime"`
Time int64 `json:"time"`
Symbol string `json:"symbol"`
Pair string `json:"pair"`
MarkPrice convert.StringToFloat64 `json:"markPrice"`
IndexPrice convert.StringToFloat64 `json:"indexPrice"`
EstimatedSettlePrice convert.StringToFloat64 `json:"estimatedSettlePrice"`
LastFundingRate convert.StringToFloat64 `json:"lastFundingRate"`
NextFundingTime int64 `json:"nextFundingTime"`
Time int64 `json:"time"`
}
// CandleStick holds kline data
@@ -917,3 +918,22 @@ type update struct {
type job struct {
Pair currency.Pair
}
// UserMarginInterestHistoryResponse user margin interest history response
type UserMarginInterestHistoryResponse struct {
Rows []UserMarginInterestHistory `json:"rows"`
Total int64 `json:"total"`
}
// UserMarginInterestHistory user margin interest history row
type UserMarginInterestHistory struct {
TxID int64 `json:"txId"`
InterestAccruedTime binanceTime `json:"interestAccuredTime"` // typo in docs, cannot verify due to API restrictions
Asset string `json:"asset"`
RawAsset string `json:"rawAsset"`
Principal float64 `json:"principal,string"`
Interest float64 `json:"interest,string"`
InterestRate float64 `json:"interestRate,string"`
Type string `json:"type"`
IsolatedSymbol string `json:"isolatedSymbol"`
}

View File

@@ -388,7 +388,7 @@ func (b *Binance) UGetFundingHistory(ctx context.Context, symbol currency.Pair,
}
params.Set("symbol", symbolValue)
}
if limit > 0 && limit < 1000 {
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if !startTime.IsZero() && !endTime.IsZero() {
@@ -1098,27 +1098,6 @@ func (b *Binance) GetPerpMarkets(ctx context.Context) (PerpsExchangeInfo, error)
return resp, b.SendHTTPRequest(ctx, exchange.RestUSDTMargined, perpExchangeInfo, uFuturesDefaultRate, &resp)
}
// FundingRates gets funding rate history for perpetual contracts
func (b *Binance) FundingRates(ctx context.Context, symbol currency.Pair, limit string, startTime, endTime time.Time) ([]FundingRateData, error) {
var resp []FundingRateData
params := url.Values{}
symbolValue, err := b.FormatSymbol(symbol, asset.USDTMarginedFutures)
if err != nil {
return resp, err
}
params.Set("symbol", symbolValue)
if limit != "" {
params.Set("limit", limit)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
return resp, b.SendHTTPRequest(ctx, exchange.RestUSDTMargined, fundingRate+params.Encode(), uFuturesDefaultRate, &resp)
}
// FetchUSDTMarginExchangeLimits fetches USDT margined order execution limits
func (b *Binance) FetchUSDTMarginExchangeLimits(ctx context.Context) ([]order.MinMaxLevel, error) {
usdtFutures, err := b.UExchangeInfo(ctx)

View File

@@ -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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
@@ -162,6 +164,10 @@ func (b *Binance) SetDefaults() {
DateRanges: true,
Intervals: true,
},
FuturesCapabilities: exchange.FuturesCapabilities{
FundingRates: true,
FundingRateFrequency: kline.EightHour.Duration(),
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
@@ -2057,3 +2063,202 @@ 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) {
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)
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),
}
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(),
}
default:
return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported)
}
return &pairRate, nil
}
// 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) {
if r == nil {
return nil, fmt.Errorf("%w RatesRequest", common.ErrNilPointer)
}
if r.IncludePredictedRate {
return nil, fmt.Errorf("%w GetFundingRates IncludePredictedRate", common.ErrFunctionNotSupported)
}
if !r.PaymentCurrency.IsEmpty() {
return nil, fmt.Errorf("%w GetFundingRates PaymentCurrency", common.ErrFunctionNotSupported)
}
if err := common.StartEndTimeCheck(r.StartDate, r.EndDate); err != nil {
return nil, err
}
format, err := b.GetPairFormat(r.Asset, true)
if err != nil {
return nil, err
}
fPair := r.Pair.Format(format)
pairRate := fundingrate.Rates{
Exchange: b.Name,
Asset: r.Asset,
Pair: fPair,
StartDate: r.StartDate,
EndDate: r.EndDate,
}
switch r.Asset {
case asset.USDTMarginedFutures:
requestLimit := 1000
sd := r.StartDate
for {
var frh []FundingRateHistory
frh, err = b.UGetFundingHistory(ctx, fPair, int64(requestLimit), sd, r.EndDate)
if err != nil {
return nil, err
}
for j := range frh {
pairRate.FundingRates = append(pairRate.FundingRates, fundingrate.Rate{
Time: time.UnixMilli(frh[j].FundingTime),
Rate: decimal.NewFromFloat(frh[j].FundingRate),
})
}
if len(frh) < requestLimit {
break
}
sd = time.UnixMilli(frh[len(frh)-1].FundingTime)
}
var mp []UMarkPrice
mp, err = b.UGetMarkPrice(ctx, fPair)
if err != nil {
return nil, err
}
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),
}
pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime)
if r.IncludePayments {
var income []UAccountIncomeHistory
income, err = b.UAccountIncomeHistory(ctx, fPair, "FUNDING_FEE", int64(requestLimit), r.StartDate, r.EndDate)
if err != nil {
return nil, err
}
for j := range income {
for x := range pairRate.FundingRates {
tt := time.UnixMilli(income[j].Time)
tt = tt.Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency)
if !tt.Equal(pairRate.FundingRates[x].Time) {
continue
}
if pairRate.PaymentCurrency.IsEmpty() {
pairRate.PaymentCurrency = currency.NewCode(income[j].Asset)
}
pairRate.FundingRates[x].Payment = decimal.NewFromFloat(income[j].Income)
pairRate.PaymentSum = pairRate.PaymentSum.Add(pairRate.FundingRates[x].Payment)
break
}
}
}
case asset.CoinMarginedFutures:
requestLimit := 1000
sd := r.StartDate
for {
var frh []FundingRateHistory
frh, err = b.FuturesGetFundingHistory(ctx, fPair, int64(requestLimit), sd, r.EndDate)
if err != nil {
return nil, err
}
for j := range frh {
pairRate.FundingRates = append(pairRate.FundingRates, fundingrate.Rate{
Time: time.UnixMilli(frh[j].FundingTime),
Rate: decimal.NewFromFloat(frh[j].FundingRate),
})
}
if len(frh) < requestLimit {
break
}
sd = time.UnixMilli(frh[len(frh)-1].FundingTime)
}
var mp []IndexMarkPrice
mp, err = b.GetIndexAndMarkPrice(ctx, fPair.String(), "")
if err != nil {
return nil, err
}
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(),
}
pairRate.TimeOfNextRate = time.UnixMilli(mp[len(mp)-1].NextFundingTime)
if r.IncludePayments {
var income []FuturesIncomeHistoryData
income, err = b.FuturesIncomeHistory(ctx, fPair, "FUNDING_FEE", r.StartDate, r.EndDate, int64(requestLimit))
if err != nil {
return nil, err
}
for j := range income {
for x := range pairRate.FundingRates {
tt := time.UnixMilli(income[j].Timestamp)
tt = tt.Truncate(b.Features.Supports.FuturesCapabilities.FundingRateFrequency)
if !tt.Equal(pairRate.FundingRates[x].Time) {
continue
}
if pairRate.PaymentCurrency.IsEmpty() {
pairRate.PaymentCurrency = currency.NewCode(income[j].Asset)
}
pairRate.FundingRates[x].Payment = decimal.NewFromFloat(income[j].Income)
pairRate.PaymentSum = pairRate.PaymentSum.Add(pairRate.FundingRates[x].Payment)
break
}
}
}
default:
return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported)
}
return &pairRate, nil
}
// 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
}
}
if a == asset.USDTMarginedFutures {
if cp.Quote.Equal(currency.USDT) || cp.Quote.Equal(currency.BUSD) {
return true, nil
}
}
return false, nil
}

View File

@@ -72,12 +72,13 @@ type UCompressedTradeData struct {
// UMarkPrice stores mark price data
type UMarkPrice struct {
Symbol string `json:"symbol"`
MarkPrice float64 `json:"markPrice,string"`
IndexPrice float64 `json:"indexPrice,string"`
LastFundingRate float64 `json:"lastFundingRate,string"`
NextFundingTime int64 `json:"nextFundingTime"`
Time int64 `json:"time"`
Symbol string `json:"symbol"`
MarkPrice float64 `json:"markPrice,string"`
IndexPrice float64 `json:"indexPrice,string"`
LastFundingRate float64 `json:"lastFundingRate,string"`
EstimatedSettlePrice float64 `json:"estimatedSettlePrice,string"`
NextFundingTime int64 `json:"nextFundingTime"`
Time int64 `json:"time"`
}
// FundingRateHistory stores funding rate history

View File

@@ -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/currencystate"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -1505,7 +1506,7 @@ func (b *Base) GetPositionSummary(context.Context, *order.PositionSummaryRequest
}
// GetFundingPaymentDetails returns funding payment details for a future for a specific time period
func (b *Base) GetFundingPaymentDetails(context.Context, *order.FundingRatesRequest) (*order.FundingRates, error) {
func (b *Base) GetFundingPaymentDetails(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) {
return nil, common.ErrNotYetImplemented
}
@@ -1514,8 +1515,13 @@ func (b *Base) GetFuturesPositions(context.Context, *order.PositionsRequest) ([]
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, *order.FundingRatesRequest) ([]order.FundingRates, error) {
func (b *Base) GetFundingRates(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error) {
return nil, common.ErrNotYetImplemented
}

View File

@@ -2494,7 +2494,7 @@ func TestSetFillsFeedStatus(t *testing.T) {
}
}
func TestGetFundingRateHistory(t *testing.T) {
func TestGetMarginRateHistory(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.GetMarginRatesHistory(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
@@ -2526,6 +2526,14 @@ func TestGetFundingPaymentDetails(t *testing.T) {
}
}
func TestGetFundingRate(t *testing.T) {
t.Parallel()
var b Base
if _, err := b.GetLatestFundingRate(context.Background(), nil); !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received: %v, expected: %v", err, common.ErrNotYetImplemented)
}
}
func TestGetFundingRates(t *testing.T) {
t.Parallel()
var b Base

View File

@@ -164,12 +164,27 @@ type FeaturesEnabled struct {
// FeaturesSupported stores the exchanges supported features
type FeaturesSupported struct {
REST bool
RESTCapabilities protocol.Features
Websocket bool
WebsocketCapabilities protocol.Features
WithdrawPermissions uint32
Kline kline.ExchangeCapabilitiesSupported
REST bool
RESTCapabilities protocol.Features
Websocket bool
WebsocketCapabilities protocol.Features
WithdrawPermissions uint32
Kline kline.ExchangeCapabilitiesSupported
MaximumOrderHistory time.Duration
FuturesCapabilities FuturesCapabilities
OfflineFuturesCapabilities FuturesCapabilities
}
// FuturesCapabilities stores the exchange's futures capabilities
type FuturesCapabilities struct {
FundingRates bool
MaximumFundingRateHistory time.Duration
FundingRateFrequency time.Duration
Positions bool
OrderManagerPositionTracking bool
Collateral bool
CollateralMode bool
Leverage bool
}
// Endpoints stores running url endpoints for exchanges

View File

@@ -0,0 +1,70 @@
package fundingrate
import (
"errors"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/currency"
"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")
// RatesRequest is used to request funding rate details for a position
type RatesRequest struct {
Asset asset.Item
Pair currency.Pair
// PaymentCurrency is an optional parameter depending on exchange API
// if you are paid in a currency that isn't easily inferred from the Pair,
// eg BTCUSD-PERP use this field
PaymentCurrency currency.Code
StartDate time.Time
EndDate time.Time
IncludePayments bool
IncludePredictedRate bool
// RespectHistoryLimits if an exchange has a limit on rate history lookup
// and your start date is beyond that time, this will set your start date
// to the maximum allowed date rather than give you errors
RespectHistoryLimits bool
}
// Rates is used to return funding rate details for a position
type Rates struct {
Exchange string
Asset asset.Item
Pair currency.Pair
StartDate time.Time
EndDate time.Time
LatestRate Rate
PredictedUpcomingRate Rate
FundingRates []Rate
PaymentSum decimal.Decimal
PaymentCurrency currency.Code
TimeOfNextRate time.Time
}
// LatestRateRequest is used to request the latest funding rate
type LatestRateRequest struct {
Asset asset.Item
Pair currency.Pair
IncludePredictedRate bool
}
// LatestRateResponse for when you just want the latest rate
type LatestRateResponse struct {
Exchange string
Asset asset.Item
Pair currency.Pair
LatestRate Rate
PredictedUpcomingRate Rate
TimeOfNextRate time.Time
}
// Rate holds details for an individual funding rate
type Rate struct {
Time time.Time
Rate decimal.Decimal
Payment decimal.Decimal
}

View File

@@ -11,6 +11,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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -144,7 +145,8 @@ type FuturesManagement interface {
ScaleCollateral(ctx context.Context, calculator *order.CollateralCalculator) (*order.CollateralByCurrency, error)
CalculateTotalCollateral(context.Context, *order.TotalCollateralCalculator) (*order.TotalCollateralResponse, error)
GetFuturesPositions(context.Context, *order.PositionsRequest) ([]order.PositionDetails, error)
GetFundingRates(context.Context, *order.FundingRatesRequest) ([]order.FundingRates, error)
GetFundingRates(context.Context, *fundingrate.RatesRequest) (*fundingrate.Rates, error)
GetLatestFundingRate(context.Context, *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error)
IsPerpetualFutureCurrency(asset.Item, currency.Pair) (bool, error)
GetCollateralCurrencyForContract(asset.Item, currency.Pair) (currency.Code, asset.Item, error)
GetMarginRatesHistory(context.Context, *margin.RateHistoryRequest) (*margin.RateHistoryResponse, error)

View File

@@ -10,12 +10,14 @@ import (
// RateHistoryRequest is used to request a funding rate
type RateHistoryRequest struct {
Exchange string
Asset asset.Item
Currency currency.Code
StartDate time.Time
EndDate time.Time
GetPredictedRate bool
Exchange string
Asset asset.Item
Currency currency.Code
Pair currency.Pair
StartDate time.Time
EndDate time.Time
GetPredictedRate bool
GetLendingPayments bool
GetBorrowRates bool
GetBorrowCosts bool

View File

@@ -3430,8 +3430,8 @@ func (ok *Okx) GetOpenInterest(ctx context.Context, instType, uly, instID string
return resp, ok.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestEPL, http.MethodGet, common.EncodeURLValues(publicOpenInterestValues, params), nil, &resp, false)
}
// GetFundingRate Retrieve funding rate.
func (ok *Okx) GetFundingRate(ctx context.Context, instrumentID string) (*FundingRateResponse, error) {
// GetSingleFundingRate returns the latest funding rate
func (ok *Okx) GetSingleFundingRate(ctx context.Context, instrumentID string) (*FundingRateResponse, error) {
params := url.Values{}
if instrumentID == "" {
return nil, errMissingInstrumentID
@@ -4244,6 +4244,9 @@ func (ok *Okx) SendHTTPRequest(ctx context.Context, ep exchange.URL, f request.E
path := endpoint + requestPath
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
if _, okay := ctx.Value(testNetVal).(bool); okay {
headers["x-simulated-trading"] = "1"
}
if authenticated {
var creds *account.Credentials
creds, err = ok.GetCredentials(ctx)

View File

@@ -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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
@@ -32,6 +33,7 @@ const (
apiSecret = ""
passphrase = ""
canManipulateRealOrders = false
useTestNet = false
)
var ok = &Okx{}
@@ -54,18 +56,36 @@ func TestMain(m *testing.M) {
exchCfg.API.AuthenticatedSupport = true
exchCfg.API.AuthenticatedWebsocketSupport = true
}
ok.Websocket = sharedtestvalues.NewTestWebsocket()
if !useTestNet {
ok.Websocket = sharedtestvalues.NewTestWebsocket()
}
err = ok.Setup(exchCfg)
if err != nil {
log.Fatal(err)
}
request.MaxRequestJobs = 200
ok.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
ok.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
setupWS()
if !useTestNet {
ok.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
ok.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
setupWS()
}
err = ok.UpdateTradablePairs(contextGenerate(), true)
if err != nil {
log.Fatal(err)
}
os.Exit(m.Run())
}
// contextGenerate sends an optional value to allow test requests
// named this way, so it shows up in auto-complete and reminds you to use it
func contextGenerate() context.Context {
ctx := context.Background()
if useTestNet {
ctx = context.WithValue(ctx, testNetKey("testnet"), useTestNet)
}
return ctx
}
func TestStart(t *testing.T) {
t.Parallel()
err := ok.Start(context.Background(), nil)
@@ -228,10 +248,10 @@ func TestGetOpenInterest(t *testing.T) {
}
}
func TestGetFundingRate(t *testing.T) {
func TestGetSingleFundingRate(t *testing.T) {
t.Parallel()
if _, err := ok.GetFundingRate(context.Background(), "BTC-USD-SWAP"); err != nil {
t.Error("okx GetFundingRate() error", err)
if _, err := ok.GetSingleFundingRate(context.Background(), "BTC-USD-SWAP"); err != nil {
t.Error("okx GetSingleFundingRate() error", err)
}
}
@@ -3203,6 +3223,80 @@ func TestInstrument(t *testing.T) {
}
}
func TestGetLatestFundingRate(t *testing.T) {
t.Parallel()
cp, err := currency.NewPairFromString("BTC-USD-SWAP")
if err != nil {
t.Error(err)
}
_, err = ok.GetLatestFundingRate(contextGenerate(), &fundingrate.LatestRateRequest{
Asset: asset.PerpetualSwap,
Pair: cp,
IncludePredictedRate: true,
})
if err != nil {
t.Error(err)
}
}
func TestGetFundingRates(t *testing.T) {
t.Parallel()
cp, err := currency.NewPairFromString("BTC-USD-SWAP")
if err != nil {
t.Error(err)
}
r := &fundingrate.RatesRequest{
Asset: asset.PerpetualSwap,
Pair: cp,
PaymentCurrency: currency.USDT,
StartDate: time.Now().Add(-time.Hour * 24 * 7),
EndDate: time.Now(),
IncludePredictedRate: true,
}
if sharedtestvalues.AreAPICredentialsSet(ok) {
r.IncludePayments = true
}
_, err = ok.GetFundingRates(contextGenerate(), r)
if err != nil {
t.Error(err)
}
r.StartDate = time.Now().Add(-time.Hour * 24 * 120)
_, err = ok.GetFundingRates(contextGenerate(), r)
if !errors.Is(err, fundingrate.ErrFundingRateOutsideLimits) {
t.Error(err)
}
r.RespectHistoryLimits = true
_, err = ok.GetFundingRates(contextGenerate(), r)
if err != nil {
t.Error(err)
}
}
func TestIsPerpetualFutureCurrency(t *testing.T) {
t.Parallel()
is, err := ok.IsPerpetualFutureCurrency(asset.Binary, currency.NewPair(currency.BTC, currency.USDT))
if err != nil {
t.Error(err)
}
if is {
t.Error("expected false")
}
cp, err := currency.NewPairFromString("BTC-USD-SWAP")
if err != nil {
t.Error(err)
}
is, err = ok.IsPerpetualFutureCurrency(asset.PerpetualSwap, cp)
if err != nil {
t.Error(err)
}
if !is {
t.Error("expected true")
}
}
func TestGetAssetsFromInstrumentTypeOrID(t *testing.T) {
t.Parallel()
_, err := ok.GetAssetsFromInstrumentTypeOrID("", "")

View File

@@ -160,22 +160,6 @@ func (a *OpenInterest) UnmarshalJSON(data []byte) error {
return nil
}
// UnmarshalJSON deserializes JSON, and timestamp information.
func (a *FundingRateResponse) UnmarshalJSON(data []byte) error {
type Alias FundingRateResponse
chil := &struct {
*Alias
FundingRate string `json:"fundingRate"`
}{
Alias: (*Alias)(a),
}
err := json.Unmarshal(data, chil)
if err != nil {
return err
}
return nil
}
// UnmarshalJSON deserializes JSON, and timestamp information.
func (a *LimitPriceResponse) UnmarshalJSON(data []byte) error {
type Alias LimitPriceResponse

View File

@@ -64,6 +64,13 @@ const (
operationLogin = "login"
)
// testNetKey this key is designed for using the testnet endpoints
// setting context.WithValue(ctx, testNetKey("testnet"), useTestNet)
// will ensure the appropriate headers are sent to OKx to use the testnet
type testNetKey string
var testNetVal = testNetKey("testnet")
// Market Data Endpoints
// TickerResponse represents the market data endpoint ticker detail
@@ -329,12 +336,13 @@ type OpenInterest struct {
// FundingRateResponse response data for the Funding Rate for an instruction type
type FundingRateResponse struct {
FundingRate okxNumericalValue `json:"fundingRate"`
FundingTime okxUnixMilliTime `json:"fundingTime"`
InstrumentID string `json:"instId"`
InstrumentType string `json:"instType"`
NextFundingRate okxNumericalValue `json:"nextFundingRate"`
NextFundingTime okxUnixMilliTime `json:"nextFundingTime"`
FundingRate convert.StringToFloat64 `json:"fundingRate"`
RealisedRate convert.StringToFloat64 `json:"realizedRate"`
FundingTime okxUnixMilliTime `json:"fundingTime"`
InstrumentID string `json:"instId"`
InstrumentType string `json:"instType"`
NextFundingRate convert.StringToFloat64 `json:"nextFundingRate"`
NextFundingTime okxUnixMilliTime `json:"nextFundingTime"`
}
// LimitPriceResponse hold an information for
@@ -1371,26 +1379,26 @@ type BillsDetailQueryParameter struct {
// BillsDetailResponse represents account bills information.
type BillsDetailResponse struct {
Balance string `json:"bal"`
BalanceChange string `json:"balChg"`
BillID string `json:"billId"`
Currency string `json:"ccy"`
ExecType string `json:"execType"` // Order flow type, Ttaker Mmaker
Fee string `json:"fee"` // Fee Negative number represents the user transaction fee charged by the platform. Positive number represents rebate.
From string `json:"from"` // The remitting account 6: FUNDING 18: Trading account When bill type is not transfer, the field returns "".
InstrumentID string `json:"instId"`
InstrumentType string `json:"instType"`
MarginMode string `json:"mgnMode"`
Notes string `json:"notes"` // notes When bill type is not transfer, the field returns "".
OrderID string `json:"ordId"`
ProfitAndLoss string `json:"pnl"`
PositionLevelBalance string `json:"posBal"`
PositionLevelBalanceChange string `json:"posBalChg"`
SubType string `json:"subType"`
Size string `json:"sz"`
To string `json:"to"`
Timestamp okxUnixMilliTime `json:"ts"`
Type string `json:"type"`
Balance okxNumericalValue `json:"bal"`
BalanceChange string `json:"balChg"`
BillID string `json:"billId"`
Currency string `json:"ccy"`
ExecType string `json:"execType"` // Order flow type, Ttaker Mmaker
Fee convert.StringToFloat64 `json:"fee"` // Fee Negative number represents the user transaction fee charged by the platform. Positive number represents rebate.
From string `json:"from"` // The remitting account 6: FUNDING 18: Trading account When bill type is not transfer, the field returns "".
InstrumentID string `json:"instId"`
InstrumentType asset.Item `json:"instType"`
MarginMode string `json:"mgnMode"`
Notes string `json:"notes"` // notes When bill type is not transfer, the field returns "".
OrderID string `json:"ordId"`
ProfitAndLoss convert.StringToFloat64 `json:"pnl"`
PositionLevelBalance convert.StringToFloat64 `json:"posBal"`
PositionLevelBalanceChange convert.StringToFloat64 `json:"posBalChg"`
SubType string `json:"subType"`
Size convert.StringToFloat64 `json:"sz"`
To string `json:"to"`
Timestamp okxUnixMilliTime `json:"ts"`
Type string `json:"type"`
}
// AccountConfigurationResponse represents account configuration response.

View File

@@ -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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
@@ -121,6 +122,11 @@ func (ok *Okx) SetDefaults() {
ModifyOrder: true,
},
WithdrawPermissions: exchange.AutoWithdrawCrypto,
FuturesCapabilities: exchange.FuturesCapabilities{
FundingRates: true,
MaximumFundingRateHistory: kline.ThreeMonth.Duration(),
FundingRateFrequency: kline.EightHour.Duration(),
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
@@ -1546,3 +1552,164 @@ func (ok *Okx) getInstrumentsForAsset(ctx context.Context, a asset.Item) ([]Inst
InstrumentType: instType,
})
}
// 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) {
if r == nil {
return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer)
}
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,
}
fr, err := ok.GetSingleFundingRate(ctx, fPair.String())
if err != nil {
return nil, err
}
pairRate.LatestRate = fundingrate.Rate{
Time: fr.FundingTime.Time(),
Rate: fr.FundingRate.Decimal(),
}
if r.IncludePredictedRate {
pairRate.TimeOfNextRate = fr.NextFundingTime.Time()
pairRate.PredictedUpcomingRate = fundingrate.Rate{
Time: fr.NextFundingTime.Time(),
Rate: fr.NextFundingRate.Decimal(),
}
}
return &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) {
if r == nil {
return nil, fmt.Errorf("%w RatesRequest", common.ErrNilPointer)
}
requestLimit := 100
sd := r.StartDate
maxLookback := time.Now().Add(-ok.Features.Supports.FuturesCapabilities.MaximumFundingRateHistory)
if r.StartDate.Before(maxLookback) {
if r.RespectHistoryLimits {
r.StartDate = maxLookback
} else {
return nil, fmt.Errorf("%w earliest date is %v", fundingrate.ErrFundingRateOutsideLimits, maxLookback)
}
if r.EndDate.Before(maxLookback) {
return nil, order.ErrGetFundingDataRequired
}
r.StartDate = maxLookback
}
format, err := ok.GetPairFormat(r.Asset, true)
if err != nil {
return nil, err
}
fPair := r.Pair.Format(format)
pairRate := fundingrate.Rates{
Exchange: ok.Name,
Asset: r.Asset,
Pair: fPair,
StartDate: r.StartDate,
EndDate: r.EndDate,
}
// map of time indexes, allowing for easy lookup of slice index from unix time data
mti := make(map[int64]int)
for {
if sd.Equal(r.EndDate) || sd.After(r.EndDate) {
break
}
var frh []FundingRateResponse
frh, err = ok.GetFundingRateHistory(ctx, fPair.String(), sd, r.EndDate, int64(requestLimit))
if err != nil {
return nil, err
}
if len(frh) == 0 {
break
}
for i := range frh {
if r.IncludePayments {
mti[frh[i].FundingTime.Time().Unix()] = i
}
pairRate.FundingRates = append(pairRate.FundingRates, fundingrate.Rate{
Time: frh[i].FundingTime.Time(),
Rate: frh[i].RealisedRate.Decimal(),
})
}
if len(frh) < requestLimit {
break
}
sd = frh[len(frh)-1].FundingTime.Time()
}
var fr *FundingRateResponse
fr, err = ok.GetSingleFundingRate(ctx, fPair.String())
if err != nil {
return nil, err
}
if fr == nil {
return nil, fmt.Errorf("%w GetSingleFundingRate", common.ErrNilPointer)
}
pairRate.LatestRate = fundingrate.Rate{
Time: fr.FundingTime.Time(),
Rate: fr.FundingRate.Decimal(),
}
pairRate.TimeOfNextRate = fr.NextFundingTime.Time()
if r.IncludePredictedRate {
pairRate.PredictedUpcomingRate = fundingrate.Rate{
Time: fr.NextFundingTime.Time(),
Rate: fr.NextFundingRate.Decimal(),
}
}
if r.IncludePayments {
pairRate.PaymentCurrency = r.Pair.Base
if !r.PaymentCurrency.IsEmpty() {
pairRate.PaymentCurrency = r.PaymentCurrency
}
sd = r.StartDate
billDetailsFunc := ok.GetBillsDetail3Months
if time.Since(r.StartDate) < kline.OneWeek.Duration() {
billDetailsFunc = ok.GetBillsDetailLast7Days
}
for {
if sd.Equal(r.EndDate) || sd.After(r.EndDate) {
break
}
var billDetails []BillsDetailResponse
billDetails, err = billDetailsFunc(ctx, &BillsDetailQueryParameter{
InstrumentType: ok.GetInstrumentTypeFromAssetItem(r.Asset),
Currency: pairRate.PaymentCurrency.String(),
BillType: 137,
BeginTime: sd,
EndTime: r.EndDate,
Limit: int64(requestLimit),
})
if err != nil {
return nil, err
}
for i := range billDetails {
if index, okay := mti[billDetails[i].Timestamp.Time().Truncate(ok.Features.Supports.FuturesCapabilities.FundingRateFrequency).Unix()]; okay {
pairRate.FundingRates[index].Payment = billDetails[i].ProfitAndLoss.Decimal()
continue
}
}
if len(billDetails) < requestLimit {
break
}
sd = billDetails[len(billDetails)-1].Timestamp.Time()
}
for i := range pairRate.FundingRates {
pairRate.PaymentSum = pairRate.PaymentSum.Add(pairRate.FundingRates[i].Payment)
}
}
return &pairRate, nil
}
// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future
func (ok *Okx) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) {
return a == asset.PerpetualSwap, nil
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
)
// SetupPositionController creates a position controller
@@ -126,7 +127,7 @@ func (c *PositionController) GetPositionsForExchange(exch string, item asset.Ite
}
// TrackFundingDetails applies funding rate details to a tracked position
func (c *PositionController) TrackFundingDetails(d *FundingRates) error {
func (c *PositionController) TrackFundingDetails(d *fundingrate.Rates) error {
if c == nil {
return fmt.Errorf("position controller %w", common.ErrNilPointer)
}
@@ -432,7 +433,7 @@ func (m *MultiPositionTracker) TrackNewOrder(d *Detail) error {
}
// TrackFundingDetails applies funding rate details to a tracked position
func (m *MultiPositionTracker) TrackFundingDetails(d *FundingRates) error {
func (m *MultiPositionTracker) TrackFundingDetails(d *fundingrate.Rates) error {
if m == nil {
return fmt.Errorf("multi-position tracker %w", common.ErrNilPointer)
}
@@ -550,9 +551,9 @@ func (p *PositionTracker) GetStats() *Position {
}
if p.fundingRateDetails != nil {
frs := make([]FundingRate, len(p.fundingRateDetails.FundingRates))
frs := make([]fundingrate.Rate, len(p.fundingRateDetails.FundingRates))
copy(frs, p.fundingRateDetails.FundingRates)
pos.FundingRates = FundingRates{
pos.FundingRates = fundingrate.Rates{
Exchange: p.fundingRateDetails.Exchange,
Asset: p.fundingRateDetails.Asset,
Pair: p.fundingRateDetails.Pair,
@@ -660,7 +661,7 @@ func (p *PositionTracker) GetLatestPNLSnapshot() (PNLResult, error) {
}
// TrackFundingDetails sets funding rates to a position
func (p *PositionTracker) TrackFundingDetails(d *FundingRates) error {
func (p *PositionTracker) TrackFundingDetails(d *fundingrate.Rates) error {
if p == nil {
return fmt.Errorf("position tracker %w", common.ErrNilPointer)
}
@@ -688,7 +689,7 @@ func (p *PositionTracker) TrackFundingDetails(d *FundingRates) 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 = &FundingRates{
p.fundingRateDetails = &fundingrate.Rates{
Exchange: d.Exchange,
Asset: d.Asset,
Pair: d.Pair,
@@ -699,7 +700,7 @@ func (p *PositionTracker) TrackFundingDetails(d *FundingRates) error {
PaymentSum: d.PaymentSum,
}
}
rates := make([]FundingRate, 0, len(d.FundingRates))
rates := make([]fundingrate.Rate, 0, len(d.FundingRates))
fundingRates:
for i := range d.FundingRates {
if d.FundingRates[i].Time.Before(p.openingDate) ||
@@ -823,12 +824,12 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
p.longPositions = append(p.longPositions, d.Copy())
}
}
var shortSide, longSide decimal.Decimal
var shortSideAmount, longSideAmount decimal.Decimal
for i := range p.shortPositions {
shortSide = shortSide.Add(decimal.NewFromFloat(p.shortPositions[i].Amount))
shortSideAmount = shortSideAmount.Add(decimal.NewFromFloat(p.shortPositions[i].Amount))
}
for i := range p.longPositions {
longSide = longSide.Add(decimal.NewFromFloat(p.longPositions[i].Amount))
longSideAmount = longSideAmount.Add(decimal.NewFromFloat(p.longPositions[i].Amount))
}
if isInitialOrder {
@@ -927,18 +928,18 @@ func (p *PositionTracker) TrackNewOrder(d *Detail, isInitialOrder bool) error {
p.unrealisedPNL = result.UnrealisedPNL
switch {
case longSide.GreaterThan(shortSide):
case longSideAmount.GreaterThan(shortSideAmount):
p.latestDirection = Long
case shortSide.GreaterThan(longSide):
case shortSideAmount.GreaterThan(longSideAmount):
p.latestDirection = Short
default:
p.latestDirection = ClosePosition
}
if p.latestDirection.IsLong() {
p.exposure = longSide.Sub(shortSide)
p.exposure = longSideAmount.Sub(shortSideAmount)
} else {
p.exposure = shortSide.Sub(longSide)
p.exposure = shortSideAmount.Sub(longSideAmount)
}
if p.exposure.Equal(decimal.Zero) {

View File

@@ -10,6 +10,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
)
const testExchange = "test"
@@ -538,8 +539,8 @@ func TestGetStats(t *testing.T) {
}
p.exchange = testExchange
p.fundingRateDetails = &FundingRates{
FundingRates: []FundingRate{
p.fundingRateDetails = &fundingrate.Rates{
FundingRates: []fundingrate.Rate{
{},
},
}
@@ -1264,7 +1265,7 @@ func TestPCTrackFundingDetails(t *testing.T) {
}
p := currency.NewPair(currency.BTC, currency.PERP)
rates := &FundingRates{
rates := &fundingrate.Rates{
Asset: asset.Futures,
Pair: p,
}
@@ -1296,7 +1297,7 @@ func TestPCTrackFundingDetails(t *testing.T) {
rates.StartDate = tn.Add(-time.Hour)
rates.EndDate = tn
rates.FundingRates = []FundingRate{
rates.FundingRates = []fundingrate.Rate{
{
Time: tn,
Rate: decimal.NewFromInt(1337),
@@ -1323,7 +1324,7 @@ func TestMPTTrackFundingDetails(t *testing.T) {
}
cp := currency.NewPair(currency.BTC, currency.PERP)
rates := &FundingRates{
rates := &fundingrate.Rates{
Asset: asset.Futures,
Pair: cp,
}
@@ -1333,7 +1334,7 @@ func TestMPTTrackFundingDetails(t *testing.T) {
}
mpt.exchange = testExchange
rates = &FundingRates{
rates = &fundingrate.Rates{
Exchange: testExchange,
Asset: asset.Futures,
Pair: cp,
@@ -1367,7 +1368,7 @@ func TestMPTTrackFundingDetails(t *testing.T) {
rates.StartDate = tn.Add(-time.Hour)
rates.EndDate = tn
rates.FundingRates = []FundingRate{
rates.FundingRates = []fundingrate.Rate{
{
Time: tn,
Rate: decimal.NewFromInt(1337),
@@ -1392,7 +1393,7 @@ func TestPTTrackFundingDetails(t *testing.T) {
}
cp := currency.NewPair(currency.BTC, currency.PERP)
rates := &FundingRates{
rates := &fundingrate.Rates{
Exchange: testExchange,
Asset: asset.Futures,
Pair: cp,
@@ -1431,7 +1432,7 @@ func TestPTTrackFundingDetails(t *testing.T) {
t.Errorf("received '%v' expected '%v", err, nil)
}
rates.FundingRates = []FundingRate{
rates.FundingRates = []fundingrate.Rate{
{
Time: rates.StartDate,
Rate: decimal.NewFromInt(1337),

View File

@@ -9,6 +9,7 @@ import (
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
)
var (
@@ -194,7 +195,7 @@ type PositionTracker struct {
shortPositions []Detail
longPositions []Detail
pnlHistory []PNLResult
fundingRateDetails *FundingRates
fundingRateDetails *fundingrate.Rates
}
// PositionTrackerSetup contains all required fields to
@@ -302,7 +303,7 @@ type Position struct {
CloseDate time.Time
Orders []Detail
PNLHistory []PNLResult
FundingRates FundingRates
FundingRates fundingrate.Rates
}
// PositionSummaryRequest is used to request a summary of an open position
@@ -343,36 +344,6 @@ type PositionSummary struct {
TotalCollateral decimal.Decimal
}
// FundingRatesRequest is used to request funding rate details for a position
type FundingRatesRequest struct {
Asset asset.Item
Pairs currency.Pairs
StartDate time.Time
EndDate time.Time
IncludePayments bool
IncludePredictedRate bool
}
// FundingRates is used to return funding rate details for a position
type FundingRates struct {
Exchange string
Asset asset.Item
Pair currency.Pair
StartDate time.Time
EndDate time.Time
LatestRate FundingRate
PredictedUpcomingRate FundingRate
FundingRates []FundingRate
PaymentSum decimal.Decimal
}
// FundingRate holds details for an individual funding rate
type FundingRate struct {
Time time.Time
Rate decimal.Decimal
Payment decimal.Decimal
}
// PositionDetails are used to track open positions
// in the order manager
type PositionDetails struct {