From d0e26c66e45c479fc6f53c88a198bbb84a43fa82 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Thu, 13 Jan 2022 19:35:05 +0800 Subject: [PATCH] strategy: add funding strategy --- config/funding.yaml | 36 +++++ pkg/cmd/builtin.go | 3 +- pkg/strategy/funding/strategy.go | 266 +++++++++++++++++++++++++++++++ 3 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 config/funding.yaml create mode 100644 pkg/strategy/funding/strategy.go diff --git a/config/funding.yaml b/config/funding.yaml new file mode 100644 index 000000000..0b9621d5f --- /dev/null +++ b/config/funding.yaml @@ -0,0 +1,36 @@ +--- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + + # if you want to route channel by symbol + symbolChannels: + "^BTC": "btc" + "^ETH": "eth" + + # object routing rules + routing: + trade: "$symbol" + order: "$symbol" + submitOrder: "$session" # not supported yet + pnL: "bbgo-pnl" + +sessions: + binance: + exchange: binance + envVarPrefix: binance + futures: true + +exchangeStrategies: +- on: binance + funding: + symbol: ETHUSDT + fundingRate: + high: 0.01% + supportDetection: + - interval: 5m + movingAverageType: EMA + movingAverageInterval: 1h + movingAverageWindow: 99 + minVolume: 8_000 diff --git a/pkg/cmd/builtin.go b/pkg/cmd/builtin.go index 8b0817d31..8af23e482 100644 --- a/pkg/cmd/builtin.go +++ b/pkg/cmd/builtin.go @@ -7,7 +7,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/emastop" _ "github.com/c9s/bbgo/pkg/strategy/etf" _ "github.com/c9s/bbgo/pkg/strategy/flashcrash" - _ "github.com/c9s/bbgo/pkg/strategy/xgap" + _ "github.com/c9s/bbgo/pkg/strategy/funding" _ "github.com/c9s/bbgo/pkg/strategy/grid" _ "github.com/c9s/bbgo/pkg/strategy/kline" _ "github.com/c9s/bbgo/pkg/strategy/pricealert" @@ -18,6 +18,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/swing" _ "github.com/c9s/bbgo/pkg/strategy/techsignal" _ "github.com/c9s/bbgo/pkg/strategy/xbalance" + _ "github.com/c9s/bbgo/pkg/strategy/xgap" _ "github.com/c9s/bbgo/pkg/strategy/xmaker" _ "github.com/c9s/bbgo/pkg/strategy/xnav" _ "github.com/c9s/bbgo/pkg/strategy/xpuremaker" diff --git a/pkg/strategy/funding/strategy.go b/pkg/strategy/funding/strategy.go new file mode 100644 index 000000000..752e36094 --- /dev/null +++ b/pkg/strategy/funding/strategy.go @@ -0,0 +1,266 @@ +package funding + +import ( + "context" + "errors" + "fmt" + "math" + "strings" + "time" + + "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "funding" + +var log = logrus.WithField("strategy", ID) + +func init() { + // Register the pointer of the strategy struct, + // so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON) + // Note: built-in strategies need to imported manually in the bbgo cmd package. + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type Strategy struct { + *bbgo.Notifiability + + // These fields will be filled from the config file (it translates YAML to JSON) + Symbol string `json:"symbol"` + Market types.Market `json:"-"` + Quantity fixedpoint.Value `json:"quantity,omitempty"` + MaxPosition fixedpoint.Value `json:"maxPosition"` + + 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"` + + // MovingAverageType is the moving average indicator type that we want to use, + // it could be SMA or EWMA + MovingAverageType string `json:"movingAverageType"` + + // MovingAverageInterval is the interval of k-lines for the moving average indicator to calculate, + // it could be "1m", "5m", "1h" and so on. note that, the moving averages are calculated from + // the k-line data we subscribed + MovingAverageInterval types.Interval `json:"movingAverageInterval"` + + // MovingAverageWindow is the number of the window size of the moving average indicator. + // The number of k-lines in the window. generally used window sizes are 7, 25 and 99 in the TradingView. + MovingAverageWindow int `json:"movingAverageWindow"` + + MinVolume fixedpoint.Value `json:"minVolume"` + + MinQuoteVolume fixedpoint.Value `json:"minQuoteVolume"` + } `json:"supportDetection"` +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) + for _, detection := range s.SupportDetection { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: string(detection.Interval), + }) + + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ + Interval: string(detection.MovingAverageInterval), + }) + } +} + +func (s *Strategy) Validate() error { + if len(s.Symbol) == 0 { + return errors.New("symbol is required") + } + + return nil +} + +func (s *Strategy) listenToFundingRate(ctx context.Context, exchange *binance.Exchange) { + var previousIndex, fundingRate24HoursLowIndex *types.PremiumIndex + + fundingRateTicker := time.NewTicker(1 * time.Hour) + defer fundingRateTicker.Stop() + for { + select { + + case <-ctx.Done(): + return + + case <-fundingRateTicker.C: + index, err := exchange.QueryPremiumIndex(ctx, s.Symbol) + if err != nil { + log.WithError(err).Error("can not query last funding rate") + continue + } + + fundingRate := index.LastFundingRate + + if fundingRate >= s.FundingRate.High { + s.Notifiability.Notify("%s funding rate %s is too high! threshold %s", + s.Symbol, + fundingRate.Percentage(), + s.FundingRate.High.Percentage(), + ) + } else { + if previousIndex != nil { + if s.FundingRate.DiffThreshold == 0 { + // 0.6% + s.FundingRate.DiffThreshold = fixedpoint.NewFromFloat(0.006 * 0.01) + } + + diff := fundingRate - previousIndex.LastFundingRate + if diff.Abs() > s.FundingRate.DiffThreshold { + s.Notifiability.Notify("%s funding rate changed %s, current funding rate %s", + s.Symbol, + diff.SignedPercentage(), + fundingRate.Percentage(), + ) + } + } + } + + previousIndex = index + if fundingRate24HoursLowIndex != nil { + if fundingRate24HoursLowIndex.Time.Before(time.Now().Add(24 * time.Hour)) { + fundingRate24HoursLowIndex = index + } + if fundingRate < fundingRate24HoursLowIndex.LastFundingRate { + fundingRate24HoursLowIndex = index + } + } else { + fundingRate24HoursLowIndex = index + } + } + } +} + +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) + } + + binanceExchange, ok := session.Exchange.(*binance.Exchange) + if !ok { + log.Error("exchange does not support funding rate api") + } + binanceExchange.UseFutures() + //if s.FundingRate != nil { + // go s.listenToFundingRate(ctx, binanceExchange) + //} + + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + // skip k-lines from other symbols + if kline.Symbol != s.Symbol { + return + } + log.Infof(s.Symbol) + for _, detection := range s.SupportDetection { + if kline.Interval != detection.Interval { + continue + } + + closePriceF := kline.GetClose() + closePrice := fixedpoint.NewFromFloat(closePriceF) + + var ma types.Float64Indicator + + switch strings.ToLower(detection.MovingAverageType) { + case "sma": + ma = standardIndicatorSet.SMA(types.IntervalWindow{ + Interval: detection.MovingAverageInterval, + Window: detection.MovingAverageWindow, + }) + case "ema", "ewma": + ma = standardIndicatorSet.EWMA(types.IntervalWindow{ + Interval: detection.MovingAverageInterval, + Window: detection.MovingAverageWindow, + }) + default: + ma = standardIndicatorSet.EWMA(types.IntervalWindow{ + Interval: detection.MovingAverageInterval, + Window: detection.MovingAverageWindow, + }) + } + + var lastMA = ma.Last() + + // skip if the closed price is under the moving average + if closePrice.Float64() < lastMA { + log.Infof("skip %s support closed price %f > last ma %f", s.Symbol, closePrice.Float64(), lastMA) + return + } + + premiumIndex, err := binanceExchange.QueryPremiumIndex(ctx, s.Symbol) + if err != nil { + log.Error("exchange does not support funding rate api") + } + + fundingRate := premiumIndex.LastFundingRate + + if fundingRate >= s.FundingRate.High { + s.Notifiability.Notify("%s funding rate %s is too high! threshold %s", + s.Symbol, + fundingRate.Percentage(), + s.FundingRate.High.Percentage(), + ) + } else { + return + } + + prettyBaseVolume := s.Market.BaseCurrencyFormatter() + prettyQuoteVolume := s.Market.QuoteCurrencyFormatter() + + if detection.MinVolume > 0 && kline.Volume > detection.MinVolume.Float64() { + s.Notifiability.Notify("Detected %s %s resistance base volume %s > min base volume %s, quote volume %s", + s.Symbol, detection.Interval.String(), + prettyBaseVolume.FormatMoney(math.Round(kline.Volume)), + prettyBaseVolume.FormatMoney(math.Round(detection.MinVolume.Float64())), + prettyQuoteVolume.FormatMoney(math.Round(kline.QuoteVolume)), + ) + s.Notifiability.Notify(kline) + + baseBalance, ok := session.Account.Balance(s.Market.BaseCurrency) + if !ok { + return + } + + if baseBalance.Available > 0 && baseBalance.Total() < s.MaxPosition { + log.Infof("start to short position, selling futures..") + _, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + Symbol: kline.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Quantity: s.Quantity.Float64(), + }) + if err != nil { + log.WithError(err).Error("submit order error") + } + } + } else if detection.MinQuoteVolume > 0 && kline.QuoteVolume > detection.MinQuoteVolume.Float64() { + s.Notifiability.Notify("Detected %s %s resistance quote volume %s > min quote volume %s, base volume %s", + s.Symbol, detection.Interval.String(), + prettyQuoteVolume.FormatMoney(math.Round(kline.QuoteVolume)), + prettyQuoteVolume.FormatMoney(math.Round(detection.MinQuoteVolume.Float64())), + prettyBaseVolume.FormatMoney(math.Round(kline.Volume)), + ) + s.Notifiability.Notify(kline) + } + } + }) + return nil +}