FTX: Margin lending/borrow rate history (#981)

* Adds lending rates/borrows to FTX and the command

* Movements, renames, rpc test

* Fleshing out rpc response

* Allows rpcserver to calculate offline (but not gctcli). Expands tests

* rn structs. add exchange_wrapper_issues support

* Adds a nice yearly rate

* Surprise yearly borrow rate!

* Rn+Mv to margin package. Fixes some serious whoopsies

* Adds average lend/borrow rates instead of sum

* rm oopsie whoopsie

* This is what the linter was having an issue with

* re-gen

* lintl

* niteroos
This commit is contained in:
Scott
2022-07-12 14:27:35 +10:00
committed by GitHub
parent e02053a2d6
commit bed9425a08
21 changed files with 3135 additions and 989 deletions

View File

@@ -35,6 +35,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
@@ -4361,7 +4362,7 @@ func (s *RPCServer) GetCollateral(ctx context.Context, r *gctrpc.GetCollateralRe
return nil, err
}
err = checkParams(r.Exchange, exch, a, currency.Pair{})
err = checkParams(r.Exchange, exch, a, currency.EMPTYPAIR)
if err != nil {
return nil, err
}
@@ -4749,3 +4750,182 @@ func (s *RPCServer) GetTechnicalAnalysis(ctx context.Context, r *gctrpc.GetTechn
return &gctrpc.GetTechnicalAnalysisResponse{Signals: signals}, nil
}
// GetMarginRatesHistory returns the margin lending or borrow rates for an exchange, asset, currency along with many customisable options
func (s *RPCServer) GetMarginRatesHistory(ctx context.Context, r *gctrpc.GetMarginRatesHistoryRequest) (*gctrpc.GetMarginRatesHistoryResponse, error) {
if r == nil {
return nil, fmt.Errorf("%w GetLendingRatesRequest", 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
}
err = checkParams(r.Exchange, exch, a, currency.EMPTYPAIR)
if err != nil {
return nil, err
}
c := currency.NewCode(r.Currency)
pairs, err := exch.GetEnabledPairs(a)
if err != nil {
return nil, err
}
if !pairs.ContainsCurrency(c) {
return nil, fmt.Errorf("%w '%v' in enabled pairs", currency.ErrCurrencyNotFound, r.Currency)
}
start := time.Now().AddDate(0, -1, 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 {
return nil, err
}
req := &margin.RateHistoryRequest{
Exchange: exch.GetName(),
Asset: a,
Currency: c,
StartDate: start,
EndDate: end,
GetPredictedRate: r.GetPredictedRate,
GetLendingPayments: r.GetLendingPayments,
GetBorrowRates: r.GetBorrowRates,
GetBorrowCosts: r.GetBorrowCosts,
CalculateOffline: r.CalculateOffline,
}
if req.CalculateOffline {
if r.TakerFeeRate == "" {
return nil, fmt.Errorf("%w for offline calculations", common.ErrCannotCalculateOffline)
}
req.TakeFeeRate, err = decimal.NewFromString(r.TakerFeeRate)
if err != nil {
return nil, err
}
if req.TakeFeeRate.LessThanOrEqual(decimal.Zero) {
return nil, fmt.Errorf("%w for offline calculations", common.ErrCannotCalculateOffline)
}
if len(r.Rates) == 0 {
return nil, fmt.Errorf("%w for offline calculations", common.ErrCannotCalculateOffline)
}
req.Rates = make([]margin.Rate, len(r.Rates))
for i := range r.Rates {
var offlineRate margin.Rate
offlineRate.Time, err = time.Parse(common.SimpleTimeFormat, r.Rates[i].Time)
if err != nil {
return nil, err
}
offlineRate.HourlyRate, err = decimal.NewFromString(r.Rates[i].HourlyRate)
if err != nil {
return nil, err
}
if r.Rates[i].BorrowCost != nil {
offlineRate.BorrowCost.Size, err = decimal.NewFromString(r.Rates[i].BorrowCost.Size)
if err != nil {
return nil, err
}
}
if r.Rates[i].LendingPayment != nil {
offlineRate.LendingPayment.Size, err = decimal.NewFromString(r.Rates[i].LendingPayment.Size)
if err != nil {
return nil, err
}
}
req.Rates[i] = offlineRate
}
}
lendingResp, err := exch.GetMarginRatesHistory(ctx, req)
if err != nil {
return nil, err
}
if len(lendingResp.Rates) == 0 {
return nil, order.ErrNoRates
}
resp := &gctrpc.GetMarginRatesHistoryResponse{
LatestRate: &gctrpc.MarginRate{
Time: lendingResp.Rates[len(lendingResp.Rates)-1].Time.Format(common.SimpleTimeFormatWithTimezone),
HourlyRate: lendingResp.Rates[len(lendingResp.Rates)-1].HourlyRate.String(),
YearlyRate: lendingResp.Rates[len(lendingResp.Rates)-1].YearlyRate.String(),
MarketBorrowSize: lendingResp.Rates[len(lendingResp.Rates)-1].MarketBorrowSize.String(),
},
TotalRates: int64(len(lendingResp.Rates)),
}
if r.GetBorrowRates {
resp.LatestRate.HourlyBorrowRate = lendingResp.Rates[len(lendingResp.Rates)-1].HourlyBorrowRate.String()
resp.LatestRate.YearlyBorrowRate = lendingResp.Rates[len(lendingResp.Rates)-1].YearlyBorrowRate.String()
}
if r.GetBorrowRates || r.GetLendingPayments {
resp.TakerFeeRate = lendingResp.TakerFeeRate.String()
}
if r.GetLendingPayments {
resp.SumLendingPayments = lendingResp.SumLendingPayments.String()
resp.AvgLendingSize = lendingResp.AverageLendingSize.String()
}
if r.GetBorrowCosts {
resp.SumBorrowCosts = lendingResp.SumBorrowCosts.String()
resp.AvgBorrowSize = lendingResp.AverageBorrowSize.String()
}
if r.GetPredictedRate {
resp.PredictedRate = &gctrpc.MarginRate{
Time: lendingResp.PredictedRate.Time.Format(common.SimpleTimeFormatWithTimezone),
HourlyRate: lendingResp.PredictedRate.HourlyRate.String(),
YearlyRate: lendingResp.PredictedRate.YearlyRate.String(),
}
if r.GetBorrowRates {
resp.PredictedRate.HourlyBorrowRate = lendingResp.PredictedRate.HourlyBorrowRate.String()
resp.PredictedRate.YearlyBorrowRate = lendingResp.PredictedRate.YearlyBorrowRate.String()
}
}
if r.IncludeAllRates {
resp.Rates = make([]*gctrpc.MarginRate, len(lendingResp.Rates))
for i := range lendingResp.Rates {
rate := &gctrpc.MarginRate{
Time: lendingResp.Rates[i].Time.Format(common.SimpleTimeFormatWithTimezone),
HourlyRate: lendingResp.Rates[i].HourlyRate.String(),
YearlyRate: lendingResp.Rates[i].YearlyRate.String(),
MarketBorrowSize: lendingResp.Rates[i].MarketBorrowSize.String(),
}
if r.GetBorrowRates {
rate.HourlyBorrowRate = lendingResp.Rates[i].HourlyBorrowRate.String()
rate.YearlyBorrowRate = lendingResp.Rates[i].YearlyBorrowRate.String()
}
if r.GetBorrowCosts {
rate.BorrowCost = &gctrpc.BorrowCost{
Cost: lendingResp.Rates[i].BorrowCost.Cost.String(),
Size: lendingResp.Rates[i].BorrowCost.Size.String(),
}
}
if r.GetLendingPayments {
rate.LendingPayment = &gctrpc.LendingPayment{
Payment: lendingResp.Rates[i].LendingPayment.Payment.String(),
Size: lendingResp.Rates[i].LendingPayment.Size.String(),
}
}
resp.Rates[i] = rate
}
}
return resp, nil
}

View File

@@ -30,6 +30,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
@@ -98,6 +99,37 @@ func (f fExchange) GetHistoricCandlesExtended(ctx context.Context, p currency.Pa
}, nil
}
func (f fExchange) GetMarginRatesHistory(context.Context, *margin.RateHistoryRequest) (*margin.RateHistoryResponse, error) {
leet := decimal.NewFromInt(1337)
rates := []margin.Rate{
{
Time: time.Now(),
MarketBorrowSize: leet,
HourlyRate: leet,
HourlyBorrowRate: leet,
LendingPayment: margin.LendingPayment{
Payment: leet,
Size: leet,
},
BorrowCost: margin.BorrowCost{
Cost: leet,
Size: leet,
},
},
}
resp := &margin.RateHistoryResponse{
Rates: rates,
SumBorrowCosts: leet,
AverageBorrowSize: leet,
SumLendingPayments: leet,
AverageLendingSize: leet,
PredictedRate: rates[0],
TakerFeeRate: leet,
}
return resp, nil
}
func (f fExchange) FetchTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
return &ticker.Price{
Last: 1337,
@@ -2619,3 +2651,141 @@ func TestGetTechnicalAnalysis(t *testing.T) {
t.Fatalf("received: '%v' but expected: '%v'", len(resp.Signals["RSI"].Signals), 33)
}
}
func TestGetMarginRatesHistory(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-usd")
if !errors.Is(err, nil) {
t.Fatalf("received '%v', expected '%v'", err, nil)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
s := RPCServer{
Engine: &Engine{
ExchangeManager: em,
currencyStateManager: &CurrencyStateManager{
started: 1, iExchangeManager: em,
},
},
}
_, err = s.GetMarginRatesHistory(context.Background(), nil)
if !errors.Is(err, common.ErrNilPointer) {
t.Errorf("received '%v' expected '%v'", err, common.ErrNilPointer)
}
request := &gctrpc.GetMarginRatesHistoryRequest{}
_, err = s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, ErrExchangeNameIsEmpty) {
t.Errorf("received '%v' expected '%v'", err, ErrExchangeNameIsEmpty)
}
request.Exchange = fakeExchangeName
_, err = s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("received '%v' expected '%v'", err, asset.ErrNotSupported)
}
request.Asset = asset.Spot.String()
_, err = s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, currency.ErrCurrencyNotFound) {
t.Errorf("received '%v' expected '%v'", err, currency.ErrCurrencyNotFound)
}
request.Currency = "usd"
_, err = s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
request.GetBorrowRates = true
request.GetLendingPayments = true
request.GetBorrowCosts = true
request.GetPredictedRate = true
request.IncludeAllRates = true
resp, err := s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
if len(resp.Rates) == 0 {
t.Errorf("received '%v' expected '%v'", len(resp.Rates), 1)
}
if resp.PredictedRate == nil {
t.Errorf("received '%v' expected '%v'", nil, "not nil")
}
if resp.TakerFeeRate != "1337" {
t.Errorf("received '%v' expected '%v'", resp.TakerFeeRate, "1337")
}
if resp.SumLendingPayments != "1337" {
t.Errorf("received '%v' expected '%v'", resp.SumLendingPayments, "1337")
}
if resp.AvgBorrowSize != "1337" {
t.Errorf("received '%v' expected '%v'", resp.AvgBorrowSize, "1337")
}
if resp.AvgLendingSize != "1337" {
t.Errorf("received '%v' expected '%v'", resp.AvgLendingSize, "1337")
}
if resp.SumBorrowCosts != "1337" {
t.Errorf("received '%v' expected '%v'", resp.SumBorrowCosts, "1337")
}
request.CalculateOffline = true
_, err = s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, common.ErrCannotCalculateOffline) {
t.Errorf("received '%v' expected '%v'", err, common.ErrCannotCalculateOffline)
}
request.TakerFeeRate = "-1337"
_, err = s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, common.ErrCannotCalculateOffline) {
t.Errorf("received '%v' expected '%v'", err, common.ErrCannotCalculateOffline)
}
request.TakerFeeRate = "1337"
_, err = s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, common.ErrCannotCalculateOffline) {
t.Errorf("received '%v' expected '%v'", err, common.ErrCannotCalculateOffline)
}
request.Rates = []*gctrpc.MarginRate{
{
Time: time.Now().Format(common.SimpleTimeFormat),
HourlyRate: "1337",
},
}
_, err = s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
request.Rates = []*gctrpc.MarginRate{
{
Time: time.Now().Format(common.SimpleTimeFormat),
HourlyRate: "1337",
LendingPayment: &gctrpc.LendingPayment{Size: "1337"},
BorrowCost: &gctrpc.BorrowCost{Size: "1337"},
},
}
_, err = s.GetMarginRatesHistory(context.Background(), request)
if !errors.Is(err, nil) {
t.Errorf("received '%v' expected '%v'", err, nil)
}
}