FTX: Funding rates, payments & stats + order manager tracking (#976)

* Adds basic PoC for calculating/retrieving position data

* A very unfortunate day of miscalculations

* Adds position summary and funding rate details to RPC

* Offline funding rate calculations

* More helpers, more stats, refining data, automated retrieval

* Adds new rpc server commands and attempts some organisation

* lower string, lower stress

* Adds ordermanager config. Fleshes outcli. Tracks positions automatically

* Adds new separation for funding payments/rates

* Combines funding rates and payments

* Fun test coverage

* ALL THE TESTS... I hope

* Fixes

* polishes ftx tests. improves perp check. Loops rates

* Final touches before nit attax

* buff 💪

* Stops NotYetImplemented spam with one simple trick!

* Some lovely little niteroos

* linteroo

* Clarifies a couple of errors to help narrow likely end user problems

* Fixes asset type bug, fixes closed position order return, fixes unset status bug

* Fixes order manager handling when no rates are available yet

* Continues on no funding rates instead. Removes err

* Don't show predicted rate if the time is zero

* Addresses scenario with no funding rate payments

* Bug fixes and commentary before updating maps to use *currency.Item

* Adds a pair key type

* Polishes pKey, fixes map order bug

* key is not a property in the event someone changes the base/quote

* Adds improvements to order processing...Breaks it all

* Shakes up the design of things by removing a function

* Fixes issues with order manager positions. Limits update range

* Fixes build issues. Identification of bad tests.

* Merges and fixes features from master and this branch

* buff linter 💪

* re-gen

* proto regen

* Addresses some nits. But not all of them.

* Fixes issue where funding rates weren't returned 🎉

* completes transition futures tracking to map[*currency.Item]map[*currency.Item]

* who did that? not me

* removes redundant check on account of being redundant and unnecessary

* so buf

* addresses nits: duplications, startTime, loops, go tidy, typos

* fixes minor mistakes

* fixes 🍣 🐻 changes to int64
This commit is contained in:
Scott
2022-08-23 12:16:50 +10:00
committed by GitHub
parent e93ee83563
commit 46cadd6f15
50 changed files with 9249 additions and 3730 deletions

View File

@@ -70,6 +70,7 @@ var (
errCurrencyNotSpecified = errors.New("a currency must be specified")
errCurrencyPairInvalid = errors.New("currency provided is not found in the available pairs list")
errNoTrades = errors.New("no trades returned from supplied params")
errUnexpectedResponseSize = errors.New("unexpected slice size")
errNilRequestData = errors.New("nil request data received, cannot continue")
errNoAccountInformation = errors.New("account information does not exist")
errShutdownNotAllowed = errors.New("shutting down this bot instance is not allowed via gRPC, please enable by command line flag --grpcshutdown or config.json field grpcAllowBotShutdown")
@@ -998,10 +999,10 @@ func (s *RPCServer) GetOrders(ctx context.Context, r *gctrpc.GetOrdersRequest) (
Trades: trades,
}
if !resp[x].Date.IsZero() {
o.CreationTime = s.unixTimestamp(resp[x].Date)
o.CreationTime = resp[x].Date.Format(common.SimpleTimeFormatWithTimezone)
}
if !resp[x].LastUpdated.IsZero() {
o.UpdateTime = s.unixTimestamp(resp[x].LastUpdated)
o.UpdateTime = resp[x].LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
orders[x] = o
}
@@ -1087,10 +1088,10 @@ func (s *RPCServer) GetManagedOrders(_ context.Context, r *gctrpc.GetOrdersReque
Trades: trades,
}
if !resp[x].Date.IsZero() {
o.CreationTime = s.unixTimestamp(resp[x].Date)
o.CreationTime = resp[x].Date.Format(common.SimpleTimeFormatWithTimezone)
}
if !resp[x].LastUpdated.IsZero() {
o.UpdateTime = s.unixTimestamp(resp[x].LastUpdated)
o.UpdateTime = resp[x].LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
orders[x] = o
}
@@ -1152,12 +1153,12 @@ func (s *RPCServer) GetOrder(ctx context.Context, r *gctrpc.GetOrderRequest) (*g
}
}
var creationTime, updateTime int64
var creationTime, updateTime string
if !result.Date.IsZero() {
creationTime = s.unixTimestamp(result.Date)
creationTime = result.Date.Format(common.SimpleTimeFormatWithTimezone)
}
if !result.LastUpdated.IsZero() {
updateTime = s.unixTimestamp(result.LastUpdated)
updateTime = result.LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
return &gctrpc.OrderDetails{
@@ -4179,8 +4180,171 @@ func (s *RPCServer) CurrencyStateTradingPair(_ context.Context, r *gctrpc.Curren
ai)
}
func (s *RPCServer) buildFuturePosition(position *order.Position, getFundingPayments, includeFundingRates, includeOrders, includePredictedRate bool) *gctrpc.FuturePosition {
response := &gctrpc.FuturePosition{
Exchange: position.Exchange,
Asset: position.Asset.String(),
Pair: &gctrpc.CurrencyPair{
Delimiter: position.Pair.Delimiter,
Base: position.Pair.Base.String(),
Quote: position.Pair.Quote.String(),
},
Status: position.Status.String(),
OpeningDate: position.OpeningDate.Format(common.SimpleTimeFormatWithTimezone),
OpeningDirection: position.OpeningDirection.String(),
OpeningPrice: position.OpeningPrice.String(),
OpeningSize: position.OpeningSize.String(),
CurrentDirection: position.LatestDirection.String(),
CurrentPrice: position.LatestPrice.String(),
CurrentSize: position.LatestSize.String(),
UnrealisedPnl: position.UnrealisedPNL.String(),
RealisedPnl: position.RealisedPNL.String(),
OrderCount: int64(len(position.Orders)),
}
if getFundingPayments {
var sum decimal.Decimal
fundingData := &gctrpc.FundingData{}
for i := range position.FundingRates.FundingRates {
if includeFundingRates {
fundingData.Rates = append(fundingData.Rates, &gctrpc.FundingRate{
Date: position.FundingRates.FundingRates[i].Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: position.FundingRates.FundingRates[i].Rate.String(),
Payment: position.FundingRates.FundingRates[i].Payment.String(),
})
}
sum = sum.Add(position.FundingRates.FundingRates[i].Payment)
}
fundingData.PaymentSum = sum.String()
response.FundingData = fundingData
if includePredictedRate && !position.FundingRates.PredictedUpcomingRate.Time.IsZero() {
fundingData.UpcomingRate = &gctrpc.FundingRate{
Date: position.FundingRates.PredictedUpcomingRate.Time.Format(common.SimpleTimeFormatWithTimezone),
Rate: position.FundingRates.PredictedUpcomingRate.Rate.String(),
}
}
}
if includeOrders {
for i := range position.Orders {
od := &gctrpc.OrderDetails{
Exchange: position.Orders[i].Exchange,
Id: position.Orders[i].OrderID,
ClientOrderId: position.Orders[i].ClientOrderID,
BaseCurrency: position.Orders[i].Pair.Base.String(),
QuoteCurrency: position.Orders[i].Pair.Quote.String(),
AssetType: position.Orders[i].AssetType.String(),
OrderSide: position.Orders[i].Side.String(),
OrderType: position.Orders[i].Type.String(),
CreationTime: position.Orders[i].Date.Format(common.SimpleTimeFormatWithTimezone),
Status: position.Orders[i].Status.String(),
Price: position.Orders[i].Price,
Amount: position.Orders[i].Cost,
OpenVolume: position.Orders[i].RemainingAmount,
Fee: position.Orders[i].Fee,
Cost: position.Orders[i].Cost,
}
if !position.Orders[i].LastUpdated.IsZero() {
od.UpdateTime = position.Orders[i].LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
for j := range position.Orders[i].Trades {
od.Trades = append(od.Trades, &gctrpc.TradeHistory{
CreationTime: position.Orders[i].Trades[j].Timestamp.Unix(),
Id: position.Orders[i].Trades[j].TID,
Price: position.Orders[i].Trades[j].Price,
Amount: position.Orders[i].Trades[j].Amount,
Exchange: position.Orders[i].Trades[j].Exchange,
AssetType: position.Orders[i].AssetType.String(),
OrderSide: position.Orders[i].Trades[j].Side.String(),
Fee: position.Orders[i].Trades[j].Fee,
Total: position.Orders[i].Trades[j].Total,
})
}
response.Orders = append(response.Orders, od)
}
}
return response
}
// GetManagedPosition returns an open positions from the order manager, no calling any API endpoints to return this information
func (s *RPCServer) GetManagedPosition(_ context.Context, r *gctrpc.GetManagedPositionRequest) (*gctrpc.GetManagedPositionsResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetManagedPositionRequest", common.ErrNilPointer)
}
if err := order.CheckFundingRatePrerequisites(r.GetFundingPayments, r.IncludePredictedRate, r.GetFundingPayments); err != nil {
return nil, err
}
if r.Pair == nil {
return nil, fmt.Errorf("%w request pair", common.ErrNilPointer)
}
var (
exch exchange.IBotExchange
ai asset.Item
cp currency.Pair
err error
)
exch, err = s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
if !exch.IsEnabled() {
return nil, fmt.Errorf("%w '%v'", errExchangeDisabled, exch.GetName())
}
ai, err = asset.New(r.Asset)
if err != nil {
return nil, err
}
if !ai.IsFutures() {
return nil, fmt.Errorf("%w '%v'", order.ErrNotFuturesAsset, ai)
}
cp, err = currency.NewPairFromStrings(r.Pair.Base, r.Pair.Quote)
if err != nil {
return nil, err
}
err = checkParams(r.Exchange, exch, ai, cp)
if err != nil {
return nil, err
}
position, err := s.OrderManager.GetOpenFuturesPosition(r.Exchange, ai, cp)
if err != nil {
return nil, err
}
return &gctrpc.GetManagedPositionsResponse{Positions: []*gctrpc.FuturePosition{
s.buildFuturePosition(position, r.GetFundingPayments, r.IncludeFullFundingRates, r.IncludeFullOrderData, r.IncludePredictedRate),
}}, nil
}
// GetAllManagedPositions returns all open positions from the order manager, no calling any API endpoints to return this information
func (s *RPCServer) GetAllManagedPositions(_ context.Context, r *gctrpc.GetAllManagedPositionsRequest) (*gctrpc.GetManagedPositionsResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetAllManagedPositions", common.ErrNilPointer)
}
if err := order.CheckFundingRatePrerequisites(r.GetFundingPayments, r.IncludePredictedRate, r.GetFundingPayments); err != nil {
return nil, err
}
positions, err := s.OrderManager.GetAllOpenFuturesPositions()
if err != nil {
return nil, err
}
sort.Slice(positions, func(i, j int) bool {
return positions[i].OpeningDate.Before(positions[j].OpeningDate)
})
response := make([]*gctrpc.FuturePosition, len(positions))
for i := range positions {
response[i] = s.buildFuturePosition(&positions[i], r.GetFundingPayments, r.IncludeFullFundingRates, r.IncludeFullOrderData, r.IncludePredictedRate)
}
return &gctrpc.GetManagedPositionsResponse{Positions: response}, nil
}
// GetFuturesPositions returns pnl positions for an exchange asset pair
func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuturesPositionsRequest) (*gctrpc.GetFuturesPositionsResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetFuturesPositions", common.ErrNilPointer)
}
if err := order.CheckFundingRatePrerequisites(r.GetFundingPayments, r.IncludePredictedRate, r.GetFundingPayments); err != nil {
return nil, err
}
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
@@ -4225,25 +4389,29 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
if err != nil {
return nil, err
}
var subErr string
var subAccount string
if creds.SubAccount != "" {
subErr = "for subaccount: " + creds.SubAccount
subAccount = "for subaccount: " + creds.SubAccount
}
orders, err := exch.GetFuturesPositions(ctx, ai, cp, start, end)
if err != nil {
return nil, fmt.Errorf("%w %v", err, subErr)
}
sort.Slice(orders, func(i, j int) bool {
return orders[i].Date.Before(orders[j].Date)
positionDetails, err := exch.GetFuturesPositions(ctx, &order.PositionsRequest{
Asset: ai,
Pairs: currency.Pairs{cp},
StartDate: start,
})
if err != nil {
return nil, fmt.Errorf("%w %v", err, subAccount)
}
if len(positionDetails) != 1 {
return nil, errUnexpectedResponseSize
}
if r.Overwrite {
err = s.OrderManager.ClearFuturesTracking(r.Exchange, ai, cp)
if err != nil {
return nil, fmt.Errorf("%w %v", err, subErr)
return nil, fmt.Errorf("cannot overwrite %w %v", err, subAccount)
}
}
for i := range orders {
_, err = s.OrderManager.UpsertOrder(&orders[i])
for i := range positionDetails[0].Orders {
err = s.OrderManager.orderStore.futuresPositionController.TrackNewOrder(&positionDetails[0].Orders[i])
if err != nil {
if !errors.Is(err, order.ErrPositionClosed) {
return nil, err
@@ -4252,13 +4420,17 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
}
pos, err := s.OrderManager.GetFuturesPositionsForExchange(r.Exchange, ai, cp)
if err != nil {
return nil, fmt.Errorf("%w %v", err, subErr)
return nil, fmt.Errorf("cannot GetFuturesPositionsForExchange %w %v", err, subAccount)
}
response := &gctrpc.GetFuturesPositionsResponse{
SubAccount: creds.SubAccount,
}
var totalRealisedPNL, totalUnrealisedPNL decimal.Decimal
for i := range pos {
if r.Status != "" && pos[i].Status.String() != strings.ToUpper(r.Status) {
continue
}
if r.PositionLimit > 0 && len(response.Positions) >= int(r.PositionLimit) {
break
}
@@ -4266,18 +4438,34 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
var tick *ticker.Price
tick, err = exch.FetchTicker(ctx, pos[i].Pair, pos[i].Asset)
if err != nil {
return nil, fmt.Errorf("%w when fetching ticker data for %v %v %v", err, pos[i].Exchange, pos[i].Asset, pos[i].Pair)
return nil, fmt.Errorf("%w when fetching ticker data for %v %v %v %v", err, pos[i].Exchange, pos[i].Asset, pos[i].Pair, subAccount)
}
pos[i].UnrealisedPNL, err = s.OrderManager.UpdateOpenPositionUnrealisedPNL(pos[i].Exchange, pos[i].Asset, pos[i].Pair, tick.Last, tick.LastUpdated)
if err != nil {
return nil, fmt.Errorf("%w when updating unrealised PNL for %v %v %v", err, pos[i].Exchange, pos[i].Asset, pos[i].Pair)
return nil, fmt.Errorf("%w when updating unrealised PNL for %v %v %v %v", err, pos[i].Exchange, pos[i].Asset, pos[i].Pair, subAccount)
}
pos[i].LatestPrice = decimal.NewFromFloat(tick.Last)
}
response.TotalOrders += int64(len(pos[i].Orders))
details := &gctrpc.FuturePosition{
Status: pos[i].Status.String(),
UnrealisedPnl: pos[i].UnrealisedPNL.String(),
RealisedPnl: pos[i].RealisedPNL.String(),
Exchange: pos[i].Exchange,
Asset: pos[i].Asset.String(),
Pair: &gctrpc.CurrencyPair{
Delimiter: pos[i].Pair.Delimiter,
Base: pos[i].Pair.Base.String(),
Quote: pos[i].Pair.Quote.String(),
},
Status: pos[i].Status.String(),
OpeningDate: pos[i].OpeningDate.Format(common.SimpleTimeFormatWithTimezone),
OpeningDirection: pos[i].OpeningDirection.String(),
OpeningPrice: pos[i].OpeningPrice.String(),
OpeningSize: pos[i].OpeningSize.String(),
CurrentDirection: pos[i].LatestDirection.String(),
CurrentPrice: pos[i].LatestPrice.String(),
CurrentSize: pos[i].LatestSize.String(),
UnrealisedPnl: pos[i].UnrealisedPNL.String(),
RealisedPnl: pos[i].RealisedPNL.String(),
OrderCount: int64(len(pos[i].Orders)),
}
if !pos[i].UnrealisedPNL.IsZero() {
details.UnrealisedPnl = pos[i].UnrealisedPNL.String()
@@ -4296,7 +4484,85 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
}
totalRealisedPNL = totalRealisedPNL.Add(pos[i].RealisedPNL)
totalUnrealisedPNL = totalUnrealisedPNL.Add(pos[i].UnrealisedPNL)
if !r.Verbose {
if r.GetPositionStats {
var stats *order.PositionSummary
stats, err = exch.GetPositionSummary(ctx, &order.PositionSummaryRequest{
Asset: pos[i].Asset,
Pair: pos[i].Pair,
})
if err != nil {
return nil, fmt.Errorf("cannot GetPositionSummary %w %v", err, subAccount)
}
details.PositionStats = &gctrpc.FuturesPositionStats{
MaintenanceMarginRequirement: stats.MaintenanceMarginRequirement.String(),
InitialMarginRequirement: stats.InitialMarginRequirement.String(),
CollateralUsed: stats.CollateralUsed.String(),
MarkPrice: stats.MarkPrice.String(),
CurrentSize: stats.CurrentSize.String(),
BreakEvenPrice: stats.BreakEvenPrice.String(),
AverageOpenPrice: stats.AverageOpenPrice.String(),
RecentPnl: stats.RecentPNL.String(),
MarginFraction: stats.MarginFraction.String(),
FreeCollateral: stats.FreeCollateral.String(),
TotalCollateral: stats.TotalCollateral.String(),
}
if !stats.EstimatedLiquidationPrice.IsZero() {
details.PositionStats.EstimatedLiquidationPrice = stats.EstimatedLiquidationPrice.String()
}
}
if r.GetFundingPayments {
var endDate = time.Now()
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{
Asset: pos[i].Asset,
Pairs: currency.Pairs{pos[i].Pair},
StartDate: pos[i].Orders[0].Date,
EndDate: endDate,
IncludePayments: r.GetFundingPayments,
IncludePredictedRate: r.IncludePredictedRate,
})
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(),
})
}
}
fundingRates := &gctrpc.FundingData{
Rates: funding,
PaymentSum: fundingDetails[0].PaymentSum.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)
}
}
if !r.IncludeFullOrderData {
response.Positions = append(response.Positions, details)
continue
}
@@ -4324,7 +4590,7 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
AssetType: pos[i].Orders[j].AssetType.String(),
OrderSide: pos[i].Orders[j].Side.String(),
OrderType: pos[i].Orders[j].Type.String(),
CreationTime: pos[i].Orders[j].Date.Unix(),
CreationTime: pos[i].Orders[j].Date.Format(common.SimpleTimeFormatWithTimezone),
Status: pos[i].Orders[j].Status.String(),
Price: pos[i].Orders[j].Price,
Amount: pos[i].Orders[j].Amount,
@@ -4333,7 +4599,7 @@ func (s *RPCServer) GetFuturesPositions(ctx context.Context, r *gctrpc.GetFuture
Trades: trades,
}
if pos[i].Orders[j].LastUpdated.After(pos[i].Orders[j].Date) {
od.UpdateTime = pos[i].Orders[j].LastUpdated.Unix()
od.UpdateTime = pos[i].Orders[j].LastUpdated.Format(common.SimpleTimeFormatWithTimezone)
}
details.Orders = append(details.Orders, od)
}
@@ -4352,6 +4618,107 @@ 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
func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRatesRequest) (*gctrpc.GetFundingRatesResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetFundingRateRequest", 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)
}
start := time.Now().AddDate(-1, 0, 0)
end := time.Now()
if r.StartDate != "" {
start, err = time.Parse(common.SimpleTimeFormat, r.StartDate)
if err != nil {
return nil, err
}
}
if r.EndDate != "" {
end, err = time.Parse(common.SimpleTimeFormat, r.EndDate)
if err != nil {
return nil, err
}
}
err = common.StartEndTimeCheck(start, end)
if err != nil && !errors.Is(err, common.ErrDateUnset) {
return nil, err
}
pairs, err := currency.NewPairsFromStrings(r.Pairs)
if err != nil {
return nil, err
}
for i := range pairs {
err = checkParams(r.Exchange, exch, a, pairs[i])
if err != nil {
return nil, err
}
}
funding, err := exch.GetFundingRates(ctx, &order.FundingRatesRequest{
Asset: a,
Pairs: pairs,
StartDate: start,
EndDate: end,
IncludePayments: r.IncludePayments,
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
}
response.FundingPayments = responses
return &response, nil
}
// GetCollateral returns the total collateral for an exchange's asset
// as exchanges can scale collateral and represent it in a singular currency,
// a user can opt to include a breakdown by currency