mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 17:13:51 +00:00
210 lines
5.2 KiB
Go
210 lines
5.2 KiB
Go
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.Sub(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() {
|
|
// check if we over borrow
|
|
if toBorrow.Add(b.Borrowed).Compare(marginAsset.MaxTotalBorrow) > 0 {
|
|
toBorrow = toBorrow.Sub( toBorrow.Add(b.Borrowed).Sub(marginAsset.MaxTotalBorrow) )
|
|
if toBorrow.Sign() < 0 {
|
|
log.Warnf("over borrowed, skip borrowing %+v", marginAsset)
|
|
continue
|
|
}
|
|
}
|
|
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
|
|
}
|