diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 8ab60aeb3..6b6f1c852 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -911,19 +911,35 @@ func (e *Exchange) BatchQueryKLines(ctx context.Context, symbol string, interval return allKLines, nil } -func (e *Exchange) QueryLastFundingRate(ctx context.Context, symbol string) (fixedpoint.Value, error) { +type FundingRate struct { + FundingRate fixedpoint.Value + FundingTime time.Time + Time time.Time +} + +func (e *Exchange) QueryLastFundingRate(ctx context.Context, symbol string) (*FundingRate, error) { futuresClient := binance.NewFuturesClient(e.key, e.secret) rates, err := futuresClient.NewFundingRateService(). Symbol(symbol). Limit(1). Do(ctx) if err != nil { - return 0, err + return nil, err } if len(rates) == 0 { - return 0, errors.New("empty funding rate data") + return nil, errors.New("empty funding rate data") } - return fixedpoint.NewFromString(rates[0].FundingRate) + rate := rates[0] + fundingRate, err := fixedpoint.NewFromString(rate.FundingRate) + if err != nil { + return nil, err + } + + return &FundingRate{ + FundingRate: fundingRate, + FundingTime: time.Unix(0, rate.FundingTime*int64(time.Millisecond)), + Time: time.Unix(0, rate.Time*int64(time.Millisecond)), + }, nil } diff --git a/pkg/strategy/techsignal/strategy.go b/pkg/strategy/techsignal/strategy.go index 6fb586ad6..a3eb2400b 100644 --- a/pkg/strategy/techsignal/strategy.go +++ b/pkg/strategy/techsignal/strategy.go @@ -4,10 +4,12 @@ import ( "context" "errors" "fmt" + "github.com/c9s/bbgo/pkg/exchange/binance" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/leekchan/accounting" "github.com/sirupsen/logrus" "strings" + "time" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/types" @@ -31,6 +33,12 @@ type Strategy struct { Symbol string `json:"symbol"` Market types.Market `json:"-"` + FundingRate *struct { + High fixedpoint.Value `json:"high"` + Neutral fixedpoint.Value `json:"neutral"` + DiffThreshold fixedpoint.Value `json:"diffThreshold"` + } `json:"fundingRate"` + SupportDetection []struct { Interval types.Interval `json:"interval"` @@ -74,12 +82,70 @@ func (s *Strategy) Validate() error { return nil } +func (s *Strategy) listenToFundingRate(ctx context.Context, exchange *binance.Exchange) { + var previousFundingRate, fundingRate24HoursLow *binance.FundingRate + + fundingRateTicker := time.NewTicker(1 * time.Hour) + defer fundingRateTicker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-fundingRateTicker.C: + fundingRate, err := exchange.QueryLastFundingRate(ctx, s.Symbol) + if err != nil { + log.WithError(err).Error("can not query last funding rate") + continue + } + + if fundingRate.FundingRate >= s.FundingRate.High { + s.Notifiability.Notify("%s funding rate is too high! %s > %s", + s.Symbol, + fundingRate.FundingRate.Percentage(), + s.FundingRate.High.Percentage(), + ) + } + + if previousFundingRate != nil { + diff := fundingRate.FundingRate - previousFundingRate.FundingRate + if diff > fixedpoint.NewFromFloat(0.001*0.01) { + s.Notifiability.Notify("%s funding rate changed %s -> %s", + s.Symbol, + diff.Percentage(), + fundingRate.FundingRate.Percentage(), + ) + } + } + + previousFundingRate = fundingRate + if fundingRate24HoursLow != nil { + if fundingRate24HoursLow.Time.Before(time.Now().Add(24 * time.Hour)) { + fundingRate24HoursLow = fundingRate + } + if fundingRate.FundingRate < fundingRate24HoursLow.FundingRate { + fundingRate24HoursLow = fundingRate + } + } else { + fundingRate24HoursLow = fundingRate + } + } + } +} + func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { standardIndicatorSet, ok := session.StandardIndicatorSet(s.Symbol) if !ok { return fmt.Errorf("standardIndicatorSet is nil, symbol %s", s.Symbol) } + if s.FundingRate != nil { + if binanceExchange, ok := session.Exchange.(*binance.Exchange); ok { + go s.listenToFundingRate(ctx, binanceExchange) + } else { + log.Error("exchange does not support funding rate api") + } + } + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { // skip k-lines from other symbols if kline.Symbol != s.Symbol {