From c70317af2b1ac60e3014075ed49d3c8148cc2a16 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 23 Apr 2022 15:00:04 +0800 Subject: [PATCH] add autoborrow strategy --- config/autoborrow.yaml | 25 ++++ pkg/bbgo/session.go | 7 + pkg/exchange/binance/exchange.go | 2 + pkg/strategy/autoborrow/strategy.go | 201 ++++++++++++++++++++++++++++ pkg/types/margin.go | 2 +- 5 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 config/autoborrow.yaml create mode 100644 pkg/strategy/autoborrow/strategy.go diff --git a/config/autoborrow.yaml b/config/autoborrow.yaml new file mode 100644 index 000000000..42a32083a --- /dev/null +++ b/config/autoborrow.yaml @@ -0,0 +1,25 @@ +--- +exchangeStrategies: +- on: binance + autoborrow: + interval: 30m + autoRepayWhenDeposit: true + + # minMarginRatio for triggering auto borrow + # we trigger auto borrow only when the margin ratio is above the number + minMarginRatio: 1.5 + + # maxMarginRatio for stop auto-repay + # if the margin ratio is high enough, we don't have the urge to repay + maxMarginRatio: 10.0 + + assets: + - asset: ETH + low: 3.0 + maxQuantityPerBorrow: 1.0 + maxTotalBorrow: 10.0 + - asset: USDT + low: 1000.0 + maxQuantityPerBorrow: 100.0 + maxTotalBorrow: 10.0 + minMarginRatio: 1.3 diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index c06d78960..156d1e5f2 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -282,6 +282,13 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { return session } +func (session *ExchangeSession) GetAccount() (a *types.Account) { + session.accountMutex.Lock() + a = session.Account + session.accountMutex.Unlock() + return a +} + // UpdateAccount locks the account mutex and update the account object func (session *ExchangeSession) UpdateAccount(ctx context.Context) error { account, err := session.Exchange.QueryAccount(ctx) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index c4e0e237a..5b9a83240 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -249,6 +249,7 @@ func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fi req.IsolatedSymbol(e.IsolatedMarginSymbol) } + log.Infof("repaying margin asset %s amount %f", asset, amount.Float64()) resp, err := req.Do(ctx) log.Debugf("margin repayed %f %s, transaction id = %d", amount.Float64(), asset, resp.TranID) return err @@ -262,6 +263,7 @@ func (e *Exchange) BorrowMarginAsset(ctx context.Context, asset string, amount f req.IsolatedSymbol(e.IsolatedMarginSymbol) } + log.Infof("borrowing margin asset %s amount %f", asset, amount.Float64()) resp, err := req.Do(ctx) log.Debugf("margin borrowed %f %s, transaction id = %d", amount.Float64(), asset, resp.TranID) return err diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go new file mode 100644 index 000000000..52f9a8e69 --- /dev/null +++ b/pkg/strategy/autoborrow/strategy.go @@ -0,0 +1,201 @@ +package autoborrow + +import ( + "context" + "fmt" + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/exchange/binance" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "autoborrow" + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +/** +- on: binance + autoborrow: + interval: 30m + repayWhenDeposit: true + + # minMarginRatio for triggering auto borrow + minMarginRatio: 1.5 + assets: + - asset: ETH + low: 3.0 + maxQuantityPerBorrow: 1.0 + maxTotalBorrow: 10.0 + - asset: USDT + low: 1000.0 + maxQuantityPerBorrow: 100.0 + maxTotalBorrow: 10.0 +*/ + +type MarginAsset struct { + Asset string `json:"asset"` + Low fixedpoint.Value `json:"low"` + MaxTotalBorrow fixedpoint.Value `json:"maxTotalBorrow"` + MaxQuantityPerBorrow fixedpoint.Value `json:"maxQuantityPerBorrow"` + MinQuantityPerBorrow fixedpoint.Value `json:"minQuantityPerBorrow"` +} + +type Strategy struct { + Interval types.Interval `json:"interval"` + MinMarginRatio fixedpoint.Value `json:"minMarginRatio"` + MaxMarginRatio fixedpoint.Value `json:"maxMarginRatio"` + AutoRepayWhenDeposit bool `json:"autoRepayWhenDeposit"` + + Assets []MarginAsset `json:"assets"` + + ExchangeSession *bbgo.ExchangeSession + + marginBorrowRepay types.MarginBorrowRepay +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) +} + +func (s *Strategy) checkAndBorrow(ctx context.Context) { + if s.MinMarginRatio.IsZero() { + return + } + + // if margin ratio is too low, do not borrow + if s.ExchangeSession.Account.MarginRatio.Compare(s.MinMarginRatio) < 0 { + return + } + + balances := s.ExchangeSession.GetAccount().Balances() + for _, marginAsset := range s.Assets { + if marginAsset.Low.IsZero() { + log.Warnf("margin asset low balance is not set: %+v", marginAsset) + continue + } + + b, ok := balances[marginAsset.Asset] + if ok { + toBorrow := marginAsset.Low - b.Total() + if toBorrow.Sign() < 0 { + log.Debugf("no need to borrow asset %+v", marginAsset) + continue + } + + if !marginAsset.MaxQuantityPerBorrow.IsZero() { + toBorrow = fixedpoint.Min(toBorrow, marginAsset.MaxQuantityPerBorrow) + } + + if !marginAsset.MaxTotalBorrow.IsZero() { + toBorrow = fixedpoint.Min(toBorrow.Add(b.Borrowed), marginAsset.MaxTotalBorrow) + } + + s.marginBorrowRepay.BorrowMarginAsset(ctx, marginAsset.Asset, toBorrow) + } else { + // available balance is less than marginAsset.Low, we should trigger borrow + toBorrow := marginAsset.Low + + if !marginAsset.MaxQuantityPerBorrow.IsZero() { + toBorrow = fixedpoint.Min(toBorrow, marginAsset.MaxQuantityPerBorrow) + } + + s.marginBorrowRepay.BorrowMarginAsset(ctx, marginAsset.Asset, toBorrow) + } + } +} + +func (s *Strategy) run(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + + } + } +} + +func (s *Strategy) handleBalanceUpdate(balances types.BalanceMap) { + if s.MinMarginRatio.IsZero() { + return + } + + if s.ExchangeSession.GetAccount().MarginRatio.Compare(s.MinMarginRatio) > 0 { + return + } + + for _, b := range balances { + if b.Available.IsZero() && b.Borrowed.IsZero() { + continue + } + } +} + +func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateEvent) { + if s.MinMarginRatio.IsZero() { + return + } + + if s.ExchangeSession.Account.MarginRatio.Compare(s.MinMarginRatio) > 0 { + return + } + + delta := fixedpoint.MustNewFromString(event.Delta) + + // ignore outflow + if delta.Sign() < 0 { + return + } + + if b, ok := s.ExchangeSession.Account.Balance(event.Asset); ok { + if b.Available.IsZero() || b.Borrowed.IsZero() { + return + } + + if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), event.Asset, b.Available); err != nil { + log.WithError(err).Errorf("margin repay error") + } + } +} + +// This strategy simply spent all available quote currency to buy the symbol whenever kline gets closed +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + if s.MinMarginRatio.IsZero() { + log.Warnf("minMarginRatio is 0, you should configure this minimal margin ratio for controlling the liquidation risk") + } + + marginBorrowRepay, ok := session.Exchange.(types.MarginBorrowRepay) + if !ok { + return fmt.Errorf("exchange %s does not implement types.MarginBorrowRepay", session.ExchangeName) + } + + s.marginBorrowRepay = marginBorrowRepay + + if s.AutoRepayWhenDeposit { + binanceStream, ok := session.UserDataStream.(*binance.Stream) + if ok { + binanceStream.OnBalanceUpdateEvent(s.handleBinanceBalanceUpdateEvent) + } else { + session.UserDataStream.OnBalanceUpdate(s.handleBalanceUpdate) + } + } + + go s.run(ctx, s.Interval.Duration()) + return nil +} diff --git a/pkg/types/margin.go b/pkg/types/margin.go index fdaeb2ee0..10e24e9e9 100644 --- a/pkg/types/margin.go +++ b/pkg/types/margin.go @@ -49,12 +49,12 @@ type MarginExchange interface { UseMargin() UseIsolatedMargin(symbol string) GetMarginSettings() MarginSettings - // QueryMarginAccount(ctx context.Context) (*binance.MarginAccount, error) } type MarginBorrowRepay interface { RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error BorrowMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error + QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (amount fixedpoint.Value, err error) } type MarginSettings struct {