bbgo_origin/pkg/strategy/autoborrow/strategy.go

602 lines
15 KiB
Go
Raw Permalink Normal View History

2022-04-23 07:00:04 +00:00
package autoborrow
import (
"context"
"fmt"
"strings"
2022-04-23 07:00:04 +00:00
"time"
"github.com/sirupsen/logrus"
2022-04-25 10:12:08 +00:00
"github.com/slack-go/slack"
2022-04-23 07:00:04 +00:00
"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:
2022-04-23 07:00:04 +00:00
interval: 30m
repayWhenDeposit: true
# minMarginLevel for triggering auto borrow
minMarginLevel: 1.5
2022-04-23 07:00:04 +00:00
assets:
- asset: ETH
low: 3.0
maxQuantityPerBorrow: 1.0
maxTotalBorrow: 10.0
- asset: USDT
low: 1000.0
maxQuantityPerBorrow: 100.0
maxTotalBorrow: 10.0
2022-04-23 07:00:04 +00:00
*/
type MarginAlert struct {
CurrentMarginLevel fixedpoint.Value
MinimalMarginLevel fixedpoint.Value
SlackMentions []string
SessionName string
}
func (m *MarginAlert) SlackAttachment() slack.Attachment {
return slack.Attachment{
Color: "red",
Title: fmt.Sprintf("Margin Level Alert: %s session - current margin level %f < required margin level %f",
m.SessionName, m.CurrentMarginLevel.Float64(), m.MinimalMarginLevel.Float64()),
Text: strings.Join(m.SlackMentions, " "),
Fields: []slack.AttachmentField{
{
Title: "Session",
Value: m.SessionName,
Short: true,
},
{
Title: "Current Margin Level",
Value: m.CurrentMarginLevel.String(),
Short: true,
},
{
Title: "Minimal Margin Level",
Value: m.MinimalMarginLevel.String(),
Short: true,
},
},
// Footer: "",
// FooterIcon: "",
}
}
2022-04-23 07:00:04 +00:00
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"`
2023-07-24 10:05:32 +00:00
DebtRatio fixedpoint.Value `json:"debtRatio"`
2022-04-23 07:00:04 +00:00
}
type Strategy struct {
Interval types.Interval `json:"interval"`
MinMarginLevel fixedpoint.Value `json:"minMarginLevel"`
MaxMarginLevel fixedpoint.Value `json:"maxMarginLevel"`
2022-04-23 07:00:04 +00:00
AutoRepayWhenDeposit bool `json:"autoRepayWhenDeposit"`
MarginLevelAlertInterval types.Duration `json:"marginLevelAlertInterval"`
MarginLevelAlertMinMargin fixedpoint.Value `json:"marginLevelAlertMinMargin"`
MarginLevelAlertSlackMentions []string `json:"marginLevelAlertSlackMentions"`
2022-04-23 07:00:04 +00:00
Assets []MarginAsset `json:"assets"`
ExchangeSession *bbgo.ExchangeSession
marginBorrowRepay types.MarginBorrowRepayService
2022-04-23 07:00:04 +00:00
}
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) tryToRepayAnyDebt(ctx context.Context) {
log.Infof("trying to repay any debt...")
2022-06-01 17:27:04 +00:00
account, err := s.ExchangeSession.UpdateAccount(ctx)
if err != nil {
log.WithError(err).Errorf("can not update account")
return
}
minMarginLevel := s.MinMarginLevel
curMarginLevel := account.MarginLevel
balances := account.Balances()
for _, b := range balances {
if b.Borrowed.Sign() <= 0 {
continue
}
if b.Available.IsZero() {
continue
}
2022-07-26 03:49:04 +00:00
toRepay := fixedpoint.Min(b.Available, b.Debt())
if toRepay.IsZero() {
continue
}
bbgo.Notify(&MarginAction{
Exchange: s.ExchangeSession.ExchangeName,
Action: "Repay",
Asset: b.Currency,
Amount: toRepay,
MarginLevel: curMarginLevel,
MinMarginLevel: minMarginLevel,
})
log.Infof("repaying %f %s", toRepay.Float64(), b.Currency)
if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), b.Currency, toRepay); err != nil {
log.WithError(err).Errorf("margin repay error")
}
return
}
}
func (s *Strategy) reBalanceDebt(ctx context.Context) {
account, err := s.ExchangeSession.UpdateAccount(ctx)
if err != nil {
log.WithError(err).Errorf("can not update account")
return
}
minMarginLevel := s.MinMarginLevel
balances := account.Balances()
if len(balances) == 0 {
log.Warn("balance is empty, skip autoborrow")
return
}
for _, marginAsset := range s.Assets {
b, ok := balances[marginAsset.Asset]
if !ok {
continue
}
// debt / total
debt := b.Debt()
total := b.Total()
2023-07-24 10:05:32 +00:00
debtRatio := debt.Div(total)
2023-07-24 10:05:32 +00:00
if marginAsset.DebtRatio.IsZero() {
marginAsset.DebtRatio = fixedpoint.One
}
2023-07-18 03:08:22 +00:00
if total.Compare(marginAsset.Low) <= 0 {
2023-07-18 06:31:32 +00:00
log.Infof("%s total %f is less than margin asset low %f, skip early repay", marginAsset.Asset, total.Float64(), marginAsset.Low.Float64())
2022-08-25 03:05:31 +00:00
continue
}
log.Infof("checking debtRatio: session = %s asset = %s, debt = %f, total = %f, debtRatio = %f", s.ExchangeSession.Name, marginAsset.Asset, debt.Float64(), total.Float64(), debtRatio.Float64())
2022-08-19 08:05:59 +00:00
// if debt is greater than total, skip repay
2023-07-18 06:31:32 +00:00
if debt.Compare(total) > 0 {
log.Infof("%s debt %f is greater than total %f, skip early repay", marginAsset.Asset, debt.Float64(), total.Float64())
continue
}
2023-07-24 10:05:32 +00:00
// if debtRatio is lesser, means that we have more spot, we should try to repay as much as we can
if debtRatio.Compare(marginAsset.DebtRatio) > 0 {
log.Infof("%s debt ratio %f is greater than min debt ratio %f, skip", marginAsset.Asset, debtRatio.Float64(), marginAsset.DebtRatio.Float64())
continue
}
2023-07-24 10:05:32 +00:00
log.Infof("checking repayable balance: %+v", b)
toRepay := fixedpoint.Min(b.Borrowed, b.Available)
2022-08-25 03:05:31 +00:00
toRepay = toRepay.Sub(marginAsset.Low)
if toRepay.Sign() <= 0 {
2023-07-24 10:05:32 +00:00
log.Warnf("%s repay = %f, available = %f, borrowed = %f, can not repay",
marginAsset.Asset,
toRepay.Float64(),
b.Available.Float64(),
b.Borrowed.Float64())
continue
}
bbgo.Notify(&MarginAction{
Exchange: s.ExchangeSession.ExchangeName,
2023-07-24 10:05:32 +00:00
Action: fmt.Sprintf("Repay for Debt Ratio %f < Minimal Debt Ratio %f", debtRatio.Float64(), marginAsset.DebtRatio.Float64()),
Asset: b.Currency,
Amount: toRepay,
MarginLevel: account.MarginLevel,
MinMarginLevel: minMarginLevel,
})
if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), b.Currency, toRepay); err != nil {
log.WithError(err).Errorf("margin repay error")
}
if accountUpdate, err2 := s.ExchangeSession.UpdateAccount(ctx); err2 != nil {
log.WithError(err).Errorf("unable to update account")
} else {
account = accountUpdate
}
}
}
2022-04-23 07:00:04 +00:00
func (s *Strategy) checkAndBorrow(ctx context.Context) {
s.reBalanceDebt(ctx)
if s.MinMarginLevel.IsZero() {
2022-04-23 07:00:04 +00:00
return
}
2022-06-01 17:27:04 +00:00
account, err := s.ExchangeSession.UpdateAccount(ctx)
if err != nil {
log.WithError(err).Errorf("can not update account")
return
}
minMarginLevel := s.MinMarginLevel
curMarginLevel := account.MarginLevel
log.Infof("%s: current margin level: %s, margin ratio: %s, margin tolerance: %s",
s.ExchangeSession.Name,
account.MarginLevel.String(),
account.MarginRatio.String(),
account.MarginTolerance.String(),
)
2022-04-23 07:00:04 +00:00
// if margin ratio is too low, do not borrow
for maxTries := 5; account.MarginLevel.Compare(minMarginLevel) < 0 && maxTries > 0; maxTries-- {
log.Infof("current margin level %f < min margin level %f, skip autoborrow", account.MarginLevel.Float64(), minMarginLevel.Float64())
2023-07-14 05:22:42 +00:00
bbgo.Notify("Warning!!! %s Current Margin Level %f < Minimal Margin Level %f",
s.ExchangeSession.Name,
account.MarginLevel.Float64(),
minMarginLevel.Float64(),
account.Balances().Debts(),
)
s.tryToRepayAnyDebt(ctx)
select {
case <-ctx.Done():
return
case <-time.After(time.Second * 5):
}
// update account info after the repay
account, err = s.ExchangeSession.UpdateAccount(ctx)
if err != nil {
log.WithError(err).Errorf("can not update account")
return
}
2022-04-23 07:00:04 +00:00
}
2022-06-01 17:27:04 +00:00
balances := account.Balances()
if len(balances) == 0 {
log.Warn("balance is empty, skip autoborrow")
return
}
2022-08-19 08:05:59 +00:00
2022-04-23 07:00:04 +00:00
for _, marginAsset := range s.Assets {
2022-06-01 17:27:04 +00:00
changed := false
2022-04-23 07:00:04 +00:00
if marginAsset.Low.IsZero() {
log.Warnf("margin asset low balance is not set: %+v", marginAsset)
continue
}
b, ok := balances[marginAsset.Asset]
if ok {
2022-04-23 07:18:25 +00:00
toBorrow := marginAsset.Low.Sub(b.Total())
2022-04-23 07:00:04 +00:00
if toBorrow.Sign() < 0 {
2022-04-25 11:10:22 +00:00
log.Infof("balance %f > low %f. no need to borrow asset %+v",
b.Total().Float64(),
marginAsset.Low.Float64(),
marginAsset)
2022-04-23 07:00:04 +00:00
continue
}
if !marginAsset.MaxQuantityPerBorrow.IsZero() {
toBorrow = fixedpoint.Min(toBorrow, marginAsset.MaxQuantityPerBorrow)
}
if !marginAsset.MaxTotalBorrow.IsZero() {
2022-04-23 07:18:25 +00:00
// check if we over borrow
2023-06-01 04:27:39 +00:00
newTotalBorrow := toBorrow.Add(b.Borrowed)
if newTotalBorrow.Compare(marginAsset.MaxTotalBorrow) > 0 {
toBorrow = toBorrow.Sub(newTotalBorrow.Sub(marginAsset.MaxTotalBorrow))
2022-04-23 07:23:43 +00:00
if toBorrow.Sign() < 0 {
log.Warnf("margin asset %s is over borrowed, skip", marginAsset.Asset)
continue
}
2022-04-23 07:18:25 +00:00
}
2022-04-23 07:00:04 +00:00
}
2023-06-01 04:27:39 +00:00
maxBorrowable, err2 := s.marginBorrowRepay.QueryMarginAssetMaxBorrowable(ctx, marginAsset.Asset)
if err2 != nil {
log.WithError(err).Errorf("max borrowable query error")
continue
}
if toBorrow.Compare(maxBorrowable) > 0 {
bbgo.Notify("Trying to borrow %f %s, which is greater than the max borrowable amount %f, will adjust borrow amount to %f",
toBorrow.Float64(),
marginAsset.Asset,
maxBorrowable.Float64(),
maxBorrowable.Float64())
toBorrow = fixedpoint.Min(maxBorrowable, toBorrow)
}
2022-06-01 17:27:04 +00:00
if toBorrow.IsZero() {
continue
}
bbgo.Notify(&MarginAction{
Exchange: s.ExchangeSession.ExchangeName,
2022-04-25 10:12:08 +00:00
Action: "Borrow",
Asset: marginAsset.Asset,
Amount: toBorrow,
MarginLevel: account.MarginLevel,
MinMarginLevel: minMarginLevel,
2022-04-25 10:12:08 +00:00
})
log.Infof("sending borrow request %f %s", toBorrow.Float64(), marginAsset.Asset)
2022-06-01 17:27:04 +00:00
if err := s.marginBorrowRepay.BorrowMarginAsset(ctx, marginAsset.Asset, toBorrow); err != nil {
2022-06-01 17:47:55 +00:00
log.WithError(err).Errorf("borrow error")
2022-06-01 17:27:04 +00:00
continue
}
changed = true
2022-04-23 07:00:04 +00:00
} 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)
}
2022-06-01 17:27:04 +00:00
if toBorrow.IsZero() {
continue
}
bbgo.Notify(&MarginAction{
Exchange: s.ExchangeSession.ExchangeName,
2022-04-25 10:12:08 +00:00
Action: "Borrow",
Asset: marginAsset.Asset,
Amount: toBorrow,
MarginLevel: curMarginLevel,
MinMarginLevel: minMarginLevel,
2022-04-25 10:12:08 +00:00
})
log.Infof("sending borrow request %f %s", toBorrow.Float64(), marginAsset.Asset)
2022-06-01 17:27:04 +00:00
if err := s.marginBorrowRepay.BorrowMarginAsset(ctx, marginAsset.Asset, toBorrow); err != nil {
log.WithError(err).Errorf("borrow error")
continue
}
changed = true
}
// if debt is changed, we need to update account
if changed {
account, err = s.ExchangeSession.UpdateAccount(ctx)
if err != nil {
log.WithError(err).Errorf("can not update account")
return
}
2022-04-23 07:00:04 +00:00
}
}
}
func (s *Strategy) run(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
2022-04-25 09:45:16 +00:00
s.checkAndBorrow(ctx)
2022-04-23 07:00:04 +00:00
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
2022-04-25 09:45:16 +00:00
s.checkAndBorrow(ctx)
2022-04-23 07:00:04 +00:00
}
}
}
func (s *Strategy) handleBalanceUpdate(balances types.BalanceMap) {
if s.MinMarginLevel.IsZero() {
2022-04-23 07:00:04 +00:00
return
}
if s.ExchangeSession.GetAccount().MarginLevel.Compare(s.MinMarginLevel) > 0 {
2022-04-23 07:00:04 +00:00
return
}
for _, b := range balances {
if b.Available.IsZero() && b.Borrowed.IsZero() {
continue
}
}
}
func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateEvent) {
2023-06-01 04:13:51 +00:00
bbgo.Notify(event)
2022-07-11 08:20:45 +00:00
account := s.ExchangeSession.GetAccount()
2022-04-23 07:00:04 +00:00
delta := event.Delta
2022-04-23 07:00:04 +00:00
// ignore outflow
if delta.Sign() < 0 {
return
}
minMarginLevel := s.MinMarginLevel
curMarginLevel := account.MarginLevel
2022-04-25 10:12:08 +00:00
// margin repay/borrow also trigger this update event
if curMarginLevel.Compare(minMarginLevel) > 0 {
return
}
if b, ok := account.Balance(event.Asset); ok {
2023-06-13 06:21:16 +00:00
if b.Available.IsZero() {
2022-04-23 07:00:04 +00:00
return
}
2023-06-13 06:21:16 +00:00
debt := b.Debt()
if debt.IsZero() {
return
}
toRepay := fixedpoint.Min(debt, b.Available)
2022-07-11 08:22:21 +00:00
if toRepay.IsZero() {
return
}
bbgo.Notify(&MarginAction{
Exchange: s.ExchangeSession.ExchangeName,
Action: "Repay",
2022-04-25 10:12:08 +00:00
Asset: b.Currency,
Amount: toRepay,
MarginLevel: curMarginLevel,
MinMarginLevel: minMarginLevel,
2022-04-25 10:12:08 +00:00
})
2022-07-11 08:20:45 +00:00
2022-04-25 10:12:08 +00:00
if err := s.marginBorrowRepay.RepayMarginAsset(context.Background(), event.Asset, toRepay); err != nil {
2022-04-23 07:00:04 +00:00
log.WithError(err).Errorf("margin repay error")
}
}
}
2022-04-25 10:12:08 +00:00
type MarginAction struct {
2022-06-01 17:53:22 +00:00
Exchange types.ExchangeName `json:"exchange"`
Action string `json:"action"`
Asset string `json:"asset"`
Amount fixedpoint.Value `json:"amount"`
MarginLevel fixedpoint.Value `json:"marginLevel"`
MinMarginLevel fixedpoint.Value `json:"minMarginLevel"`
2022-04-25 10:12:08 +00:00
}
func (a *MarginAction) SlackAttachment() slack.Attachment {
return slack.Attachment{
Title: fmt.Sprintf("%s %s %s", a.Action, a.Amount, a.Asset),
Color: "warning",
2022-04-25 10:12:08 +00:00
Fields: []slack.AttachmentField{
{
Title: "Exchange",
Value: a.Exchange.String(),
Short: true,
},
2022-04-25 10:12:08 +00:00
{
Title: "Action",
Value: a.Action,
Short: true,
},
{
Title: "Asset",
Value: a.Asset,
Short: true,
},
{
Title: "Amount",
Value: a.Amount.String(),
Short: true,
},
{
2022-04-25 11:15:47 +00:00
Title: "Current Margin Level",
Value: a.MarginLevel.String(),
2022-04-25 10:12:08 +00:00
Short: true,
},
{
2022-04-25 11:15:47 +00:00
Title: "Min Margin Level",
Value: a.MinMarginLevel.String(),
2022-04-25 10:12:08 +00:00
Short: true,
},
},
}
}
2022-04-23 07:00:04 +00:00
// 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.MinMarginLevel.IsZero() {
log.Warnf("%s: minMarginLevel is 0, you should configure this minimal margin ratio for controlling the liquidation risk", session.Name)
2022-04-23 07:00:04 +00:00
}
2022-04-25 09:54:16 +00:00
s.ExchangeSession = session
marginBorrowRepay, ok := session.Exchange.(types.MarginBorrowRepayService)
2022-04-23 07:00:04 +00:00
if !ok {
return fmt.Errorf("exchange %s does not implement types.MarginBorrowRepayService", session.Name)
2022-04-23 07:00:04 +00:00
}
s.marginBorrowRepay = marginBorrowRepay
if s.AutoRepayWhenDeposit {
binanceStream, ok := session.UserDataStream.(*binance.Stream)
if ok {
binanceStream.OnBalanceUpdateEvent(s.handleBinanceBalanceUpdateEvent)
} else {
session.UserDataStream.OnBalanceUpdate(s.handleBalanceUpdate)
}
}
if !s.MarginLevelAlertMinMargin.IsZero() {
alertInterval := time.Minute * 5
if s.MarginLevelAlertInterval > 0 {
alertInterval = s.MarginLevelAlertInterval.Duration()
}
go s.marginAlertWorker(ctx, alertInterval)
}
2022-04-23 07:00:04 +00:00
go s.run(ctx, s.Interval.Duration())
return nil
}
func (s *Strategy) marginAlertWorker(ctx context.Context, alertInterval time.Duration) {
go func() {
ticker := time.NewTicker(alertInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
account := s.ExchangeSession.GetAccount()
if account.MarginLevel.Compare(s.MarginLevelAlertMinMargin) <= 0 {
bbgo.Notify(&MarginAlert{
CurrentMarginLevel: account.MarginLevel,
MinimalMarginLevel: s.MarginLevelAlertMinMargin,
SlackMentions: s.MarginLevelAlertSlackMentions,
SessionName: s.ExchangeSession.Name,
})
bbgo.Notify(account.Balances().Debts())
}
}
}
}()
}