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

@@ -18,6 +18,7 @@ import (
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"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/order"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -785,9 +786,9 @@ func (m *OrderManager) processFuturesPositions(exch exchange.IBotExchange, posit
if !isPerp {
return nil
}
frp, err := exch.GetFundingRates(context.TODO(), &order.FundingRatesRequest{
frp, err := exch.GetFundingRates(context.TODO(), &fundingrate.RatesRequest{
Asset: position.Asset,
Pairs: currency.Pairs{position.Pair},
Pair: position.Pair,
StartDate: position.Orders[0].Date,
EndDate: time.Now(),
IncludePayments: true,
@@ -796,14 +797,8 @@ func (m *OrderManager) processFuturesPositions(exch exchange.IBotExchange, posit
if err != nil {
return err
}
for i := range frp {
err = m.orderStore.futuresPositionController.TrackFundingDetails(&frp[i])
if err != nil {
return err
}
}
return nil
return m.orderStore.futuresPositionController.TrackFundingDetails(frp)
}
func (m *OrderManager) processMatchingOrders(exch exchange.IBotExchange, orders []order.Detail, wg *sync.WaitGroup) {

View File

@@ -1654,7 +1654,7 @@ func TestProcessFuturesPositions(t *testing.T) {
position.Orders[0].AssetType = asset.Futures
position.Asset = asset.Futures
err = o.processFuturesPositions(fakeExchange, position)
if !errors.Is(err, common.ErrNotYetImplemented) {
t.Errorf("received '%v', expected '%v'", err, common.ErrNotYetImplemented)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
}

View File

@@ -34,6 +34,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/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
@@ -4546,10 +4547,10 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
if pos[i].Status == order.Closed {
endDate = pos[i].Orders[len(pos[i].Orders)-1].Date
}
var fundingDetails []order.FundingRates
fundingDetails, err = exch.GetFundingRates(ctx, &order.FundingRatesRequest{
var fundingDetails *fundingrate.Rates
fundingDetails, err = exch.GetFundingRates(ctx, &fundingrate.RatesRequest{
Asset: pos[i].Asset,
Pairs: currency.Pairs{pos[i].Pair},
Pair: pos[i].Pair,
StartDate: pos[i].Orders[0].Date,
EndDate: endDate,
IncludePayments: r.GetFundingPayments,
@@ -4558,39 +4559,33 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
if err != nil {
return nil, err
}
switch {
case len(fundingDetails) == 0:
case len(fundingDetails) == 1:
var funding []*gctrpc.FundingRate
if r.IncludeFullFundingRates {
for j := range fundingDetails[0].FundingRates {
funding = append(funding, &gctrpc.FundingRate{
Date: fundingDetails[0].FundingRates[j].Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: fundingDetails[0].FundingRates[j].Rate.String(),
Payment: fundingDetails[0].FundingRates[j].Payment.String(),
})
}
var funding []*gctrpc.FundingRate
if r.IncludeFullFundingRates {
for j := range fundingDetails.FundingRates {
funding = append(funding, &gctrpc.FundingRate{
Date: fundingDetails.FundingRates[j].Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: fundingDetails.FundingRates[j].Rate.String(),
Payment: fundingDetails.FundingRates[j].Payment.String(),
})
}
fundingRates := &gctrpc.FundingData{
Rates: funding,
PaymentSum: fundingDetails[0].PaymentSum.String(),
}
fundingRates := &gctrpc.FundingData{
Rates: funding,
PaymentSum: fundingDetails.PaymentSum.String(),
}
if r.IncludeFullFundingRates {
fundingRates.LatestRate = funding[len(fundingRates.Rates)-1]
}
if r.IncludePredictedRate && !fundingDetails.PredictedUpcomingRate.Time.IsZero() {
fundingRates.UpcomingRate = &gctrpc.FundingRate{
Date: fundingDetails.PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: fundingDetails.PredictedUpcomingRate.Rate.String(),
}
if r.IncludeFullFundingRates {
fundingRates.LatestRate = funding[len(fundingRates.Rates)-1]
}
if r.IncludePredictedRate && !fundingDetails[0].PredictedUpcomingRate.Time.IsZero() {
fundingRates.UpcomingRate = &gctrpc.FundingRate{
Date: fundingDetails[0].PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: fundingDetails[0].PredictedUpcomingRate.Rate.String(),
}
}
details.FundingData = fundingRates
err = s.OrderManager.orderStore.futuresPositionController.TrackFundingDetails(&fundingDetails[0])
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("%w expected 1 set of funding rates, got %d %v", errUnexpectedResponseSize, len(fundingDetails), subAccount)
}
details.FundingData = fundingRates
err = s.OrderManager.orderStore.futuresPositionController.TrackFundingDetails(fundingDetails)
if err != nil {
return nil, err
}
}
if !r.IncludeFullOrderData {
@@ -4649,7 +4644,7 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
return response, nil
}
// GetFundingRates returns the funding rates for a slice of pairs of an exchange, asset
// GetFundingRates returns the funding rates for an exchange, asset, pair
func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRatesRequest) (*gctrpc.GetFundingRatesResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetFundingRateRequest", common.ErrNilPointer)
@@ -4666,7 +4661,7 @@ func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRat
if !a.IsFutures() {
return nil, fmt.Errorf("%s %w", a, order.ErrNotFuturesAsset)
}
start := time.Now().AddDate(-1, 0, 0)
start := time.Now().AddDate(0, -1, 0)
end := time.Now()
if r.StartDate != "" {
start, err = time.Parse(common.SimpleTimeFormatWithTimezone, r.StartDate)
@@ -4684,69 +4679,140 @@ func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRat
if err != nil && !errors.Is(err, common.ErrDateUnset) {
return nil, err
}
pairs, err := currency.NewPairsFromStrings(r.Pairs)
pairs, err := exch.GetEnabledPairs(a)
if err != nil {
return nil, err
}
for i := range pairs {
err = checkParams(r.Exchange, exch, a, pairs[i])
if err != nil {
return nil, err
}
cp, err := pairs.DeriveFrom(r.Pair.Base + r.Pair.Quote)
if err != nil {
return nil, err
}
funding, err := exch.GetFundingRates(ctx, &order.FundingRatesRequest{
funding, err := exch.GetFundingRates(ctx, &fundingrate.RatesRequest{
Asset: a,
Pairs: pairs,
Pair: cp,
StartDate: start,
EndDate: end,
IncludePayments: r.IncludePayments,
IncludePredictedRate: r.IncludePredicted,
RespectHistoryLimits: r.RespectHistoryLimits,
PaymentCurrency: currency.NewCode(r.PaymentCurrency),
})
if err != nil {
return nil, err
}
var hasPayment bool
var response gctrpc.GetFundingRatesResponse
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(),
},
StartDate: start.Format(common.SimpleTimeFormatWithTimezone),
EndDate: end.Format(common.SimpleTimeFormatWithTimezone),
LatestRate: &gctrpc.FundingRate{
Date: funding.LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: funding.LatestRate.Rate.String(),
},
}
rates := make([]*gctrpc.FundingRate, len(funding.FundingRates))
for j := range funding.FundingRates {
rates[j] = &gctrpc.FundingRate{
Rate: funding.FundingRates[j].Rate.String(),
Date: funding.FundingRates[j].Time.Format(common.SimpleTimeFormatWithTimezone),
}
if r.IncludePayments {
if !funding.FundingRates[j].Payment.IsZero() {
hasPayment = true
}
rates[j].Payment = funding.FundingRates[j].Payment.String()
}
}
if r.IncludePayments {
fundingData.PaymentSum = funding.PaymentSum.String()
fundingData.PaymentCurrency = funding.PaymentCurrency.String()
if !hasPayment {
fundingData.PaymentMessage = "no payments found for payment currency " + funding.PaymentCurrency.String() +
" please ensure you have set the correct payment currency in the request"
}
}
if !funding.TimeOfNextRate.IsZero() {
fundingData.TimeOfNextRate = funding.TimeOfNextRate.Format(common.SimpleTimeFormatWithTimezone)
}
fundingData.Rates = rates
if r.IncludePredicted {
fundingData.UpcomingRate = &gctrpc.FundingRate{
Date: funding.PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: funding.PredictedUpcomingRate.Rate.String(),
}
}
response.Rates = fundingData
return &response, nil
}
// GetLatestFundingRate returns the latest funding rate for an exchange, asset, pair
func (s *RPCServer) GetLatestFundingRate(ctx context.Context, r *gctrpc.GetLatestFundingRateRequest) (*gctrpc.GetLatestFundingRateResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetLatestFundingRateRequest", common.ErrNilPointer)
}
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
a, err := asset.New(r.Asset)
if err != nil {
return nil, err
}
if !a.IsFutures() {
return nil, fmt.Errorf("%s %w", a, order.ErrNotFuturesAsset)
}
pairs, err := exch.GetEnabledPairs(a)
if err != nil {
return nil, err
}
cp, err := pairs.DeriveFrom(r.Pair.Base + r.Pair.Quote)
if err != nil {
return nil, err
}
funding, err := exch.GetLatestFundingRate(ctx, &fundingrate.LatestRateRequest{
Asset: a,
Pair: cp,
IncludePredictedRate: r.IncludePredicted,
})
if err != nil {
return nil, err
}
var response gctrpc.GetFundingRatesResponse
responses := make([]*gctrpc.FundingData, len(funding))
for i := range funding {
fundingData := &gctrpc.FundingData{
Exchange: r.Exchange,
Asset: r.Asset,
Pair: &gctrpc.CurrencyPair{
Delimiter: funding[i].Pair.Delimiter,
Base: funding[i].Pair.Base.String(),
Quote: funding[i].Pair.Quote.String(),
},
StartDate: start.Format(common.SimpleTimeFormatWithTimezone),
EndDate: end.Format(common.SimpleTimeFormatWithTimezone),
LatestRate: &gctrpc.FundingRate{
Date: funding[i].LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: funding[i].LatestRate.Rate.String(),
},
}
var rates []*gctrpc.FundingRate
for j := range funding[i].FundingRates {
rate := &gctrpc.FundingRate{
Rate: funding[i].FundingRates[j].Rate.String(),
Date: funding[i].FundingRates[j].Time.Format(common.SimpleTimeFormatWithTimezone),
}
if r.IncludePayments {
rate.Payment = funding[i].FundingRates[j].Payment.String()
}
rates = append(rates, rate)
}
if r.IncludePayments {
fundingData.PaymentSum = funding[i].PaymentSum.String()
}
fundingData.Rates = rates
if r.IncludePredicted {
fundingData.UpcomingRate = &gctrpc.FundingRate{
Date: funding[i].PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: funding[i].PredictedUpcomingRate.Rate.String(),
}
}
responses[i] = fundingData
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(),
},
LatestRate: &gctrpc.FundingRate{
Date: funding.LatestRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: funding.LatestRate.Rate.String(),
},
}
response.FundingPayments = responses
if !funding.TimeOfNextRate.IsZero() {
fundingData.TimeOfNextRate = funding.TimeOfNextRate.Format(common.SimpleTimeFormatWithTimezone)
}
if r.IncludePredicted {
fundingData.UpcomingRate = &gctrpc.FundingRate{
Date: funding.PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: funding.PredictedUpcomingRate.Rate.String(),
}
}
response.Rate = fundingData
return &response, nil
}

View File

@@ -29,6 +29,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
"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"
@@ -110,34 +111,52 @@ func (f fExchange) GetFuturesPositions(_ context.Context, req *order.PositionsRe
return resp, nil
}
func (f fExchange) GetFundingRates(_ context.Context, request *order.FundingRatesRequest) ([]order.FundingRates, error) {
func (f fExchange) GetLatestFundingRate(_ context.Context, request *fundingrate.LatestRateRequest) (*fundingrate.LatestRateResponse, error) {
leet := decimal.NewFromInt(1337)
return []order.FundingRates{
{
Exchange: f.GetName(),
Asset: request.Asset,
Pair: request.Pairs[0],
StartDate: request.StartDate,
EndDate: request.EndDate,
LatestRate: order.FundingRate{
Time: request.EndDate,
Rate: leet,
Payment: leet,
},
PredictedUpcomingRate: order.FundingRate{
Time: request.EndDate,
Rate: leet,
Payment: leet,
},
FundingRates: []order.FundingRate{
{
Time: request.EndDate,
Rate: leet,
Payment: leet,
},
},
PaymentSum: 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(),
}, nil
}
func (f fExchange) GetFundingRates(_ context.Context, request *fundingrate.RatesRequest) (*fundingrate.Rates, error) {
leet := decimal.NewFromInt(1337)
return &fundingrate.Rates{
Exchange: f.GetName(),
Asset: request.Asset,
Pair: request.Pair,
StartDate: request.StartDate,
EndDate: request.EndDate,
LatestRate: fundingrate.Rate{
Time: request.EndDate,
Rate: leet,
Payment: leet,
},
PredictedUpcomingRate: fundingrate.Rate{
Time: request.EndDate,
Rate: leet,
Payment: leet,
},
FundingRates: []fundingrate.Rate{
{
Time: request.EndDate,
Rate: leet,
Payment: leet,
},
},
PaymentSum: leet,
}, nil
}
@@ -2977,7 +2996,7 @@ func TestGetFundingRates(t *testing.T) {
request := &gctrpc.GetFundingRatesRequest{
Exchange: "",
Asset: "",
Pairs: nil,
Pair: nil,
StartDate: "",
EndDate: "",
IncludePredicted: false,
@@ -3000,7 +3019,11 @@ func TestGetFundingRates(t *testing.T) {
}
request.Asset = asset.Futures.String()
request.Pairs = []string{cp.String()}
request.Pair = &gctrpc.CurrencyPair{
Delimiter: cp.Delimiter,
Base: cp.Base.String(),
Quote: cp.Quote.String(),
}
request.IncludePredicted = true
request.IncludePayments = true
_, err = s.GetFundingRates(context.Background(), request)
@@ -3009,6 +3032,102 @@ func TestGetFundingRates(t *testing.T) {
}
}
func TestGetLatestFundingRate(t *testing.T) {
t.Parallel()
em := NewExchangeManager()
exch, err := em.NewExchangeByName("binance")
if err != nil {
t.Fatal(err)
}
exch.SetDefaults()
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-perp")
if err != nil {
t.Fatal(err)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Futures] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
RequestFormat: &currency.PairFormat{Delimiter: "-"},
ConfigFormat: &currency.PairFormat{Delimiter: "-"},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{Delimiter: "/"},
RequestFormat: &currency.PairFormat{Delimiter: "/"},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
err = em.Add(fakeExchange)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
var wg sync.WaitGroup
om, err := SetupOrderManager(em, &CommunicationManager{}, &wg, false, false, time.Hour)
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
om.started = 1
s := RPCServer{
Engine: &Engine{
ExchangeManager: em,
currencyStateManager: &CurrencyStateManager{
started: 1,
iExchangeManager: em,
},
OrderManager: om,
},
}
_, err = s.GetLatestFundingRate(context.Background(), nil)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received: '%v' but expected: '%v'", err, common.ErrNilPointer)
}
request := &gctrpc.GetLatestFundingRateRequest{
Exchange: "",
Asset: "",
Pair: nil,
IncludePredicted: false,
}
_, err = s.GetLatestFundingRate(context.Background(), request)
if !errors.Is(err, ErrExchangeNameIsEmpty) {
t.Errorf("received: '%v' but expected: '%v'", err, ErrExchangeNameIsEmpty)
}
request.Exchange = exch.GetName()
_, err = s.GetLatestFundingRate(context.Background(), request)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received: '%v' but expected: '%v'", err, asset.ErrNotSupported)
}
request.Asset = asset.Spot.String()
_, err = s.GetLatestFundingRate(context.Background(), request)
if !errors.Is(err, order.ErrNotFuturesAsset) {
t.Errorf("received: '%v' but expected: '%v'", err, order.ErrNotFuturesAsset)
}
request.Asset = asset.Futures.String()
request.Pair = &gctrpc.CurrencyPair{
Delimiter: cp.Delimiter,
Base: cp.Base.String(),
Quote: cp.Quote.String(),
}
request.IncludePredicted = true
_, err = s.GetLatestFundingRate(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestGetManagedPosition(t *testing.T) {
t.Parallel()
em := NewExchangeManager()