2023-08-05 11:03:14 +00:00
package deposit2transfer
import (
"context"
"errors"
"fmt"
2023-08-07 01:51:07 +00:00
"sort"
"sync"
"time"
2023-08-05 11:03:14 +00:00
"github.com/sirupsen/logrus"
2023-08-11 11:03:16 +00:00
"golang.org/x/time/rate"
2023-08-05 11:03:14 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
2024-05-20 10:05:21 +00:00
"github.com/c9s/bbgo/pkg/exchange/retry"
2023-08-07 01:51:07 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
2024-11-12 07:48:17 +00:00
"github.com/c9s/bbgo/pkg/livenote"
2023-08-05 11:03:14 +00:00
"github.com/c9s/bbgo/pkg/types"
)
2023-08-07 01:51:07 +00:00
type marginTransferService interface {
TransferMarginAccountAsset ( ctx context . Context , asset string , amount fixedpoint . Value , io types . TransferDirection ) error
}
2023-08-16 04:02:18 +00:00
type spotAccountQueryService interface {
QuerySpotAccount ( ctx context . Context ) ( * types . Account , error )
}
2023-08-05 11:03:14 +00:00
const ID = "deposit2transfer"
var log = logrus . WithField ( "strategy" , ID )
2023-08-07 01:51:07 +00:00
var errMarginTransferNotSupport = errors . New ( "exchange session does not support margin transfer" )
var errDepositHistoryNotSupport = errors . New ( "exchange session does not support deposit history query" )
2023-08-05 11:03:14 +00:00
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 { } )
}
2024-11-12 07:48:17 +00:00
type SlackAlert struct {
2024-11-12 07:49:29 +00:00
Channel string ` json:"channel" `
2024-11-12 07:48:17 +00:00
Mentions [ ] string ` json:"mentions" `
2024-11-12 08:09:51 +00:00
Pin bool ` json:"pin" `
2024-11-12 07:48:17 +00:00
}
2023-08-05 11:03:14 +00:00
type Strategy struct {
Environment * bbgo . Environment
2023-08-08 03:07:21 +00:00
Assets [ ] string ` json:"assets" `
2023-08-07 01:51:07 +00:00
2024-11-04 09:34:56 +00:00
Interval types . Duration ` json:"interval" `
TransferDelay types . Duration ` json:"transferDelay" `
2023-08-05 11:03:14 +00:00
2024-11-14 07:38:30 +00:00
IgnoreDust bool ` json:"ignoreDust" `
DustAmounts map [ string ] fixedpoint . Value ` json:"dustAmounts" `
2024-11-12 07:48:17 +00:00
SlackAlert * SlackAlert ` json:"slackAlert" `
2023-08-07 01:51:07 +00:00
marginTransferService marginTransferService
depositHistoryService types . ExchangeTransferService
2023-08-05 11:03:14 +00:00
2024-11-12 07:53:17 +00:00
session * bbgo . ExchangeSession
2023-08-07 01:51:07 +00:00
watchingDeposits map [ string ] types . Deposit
mu sync . Mutex
2023-08-09 07:54:28 +00:00
2024-02-15 03:43:59 +00:00
logger logrus . FieldLogger
2023-08-09 07:54:28 +00:00
lastAssetDepositTimes map [ string ] time . Time
2023-08-05 11:03:14 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) Subscribe ( session * bbgo . ExchangeSession ) { }
func ( s * Strategy ) Defaults ( ) error {
2023-08-08 04:38:23 +00:00
if s . Interval == 0 {
2024-11-12 07:53:17 +00:00
s . Interval = types . Duration ( 3 * time . Minute )
2023-08-05 11:03:14 +00:00
}
2024-11-04 09:34:56 +00:00
if s . TransferDelay == 0 {
s . TransferDelay = types . Duration ( 3 * time . Second )
}
2024-11-14 07:38:30 +00:00
if s . DustAmounts == nil {
s . DustAmounts = map [ string ] fixedpoint . Value {
"USDC" : fixedpoint . NewFromFloat ( 1.0 ) ,
"USDT" : fixedpoint . NewFromFloat ( 1.0 ) ,
"BTC" : fixedpoint . NewFromFloat ( 0.00001 ) ,
"ETH" : fixedpoint . NewFromFloat ( 0.00001 ) ,
}
}
2024-11-04 09:34:56 +00:00
return nil
}
func ( s * Strategy ) Initialize ( ) error {
2024-02-15 03:43:59 +00:00
if s . logger == nil {
s . logger = log . Dup ( )
}
2023-08-05 11:03:14 +00:00
return nil
}
func ( s * Strategy ) Validate ( ) error {
return nil
}
func ( s * Strategy ) InstanceID ( ) string {
2023-08-08 03:58:36 +00:00
return fmt . Sprintf ( "%s-%s" , ID , s . Assets )
2023-08-05 11:03:14 +00:00
}
func ( s * Strategy ) Run ( ctx context . Context , orderExecutor bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
2023-08-08 04:08:14 +00:00
s . session = session
2023-08-07 01:51:07 +00:00
s . watchingDeposits = make ( map [ string ] types . Deposit )
2023-08-09 07:54:28 +00:00
s . lastAssetDepositTimes = make ( map [ string ] time . Time )
2024-02-15 03:43:59 +00:00
s . logger = s . logger . WithField ( "exchange" , session . ExchangeName )
2023-08-07 01:51:07 +00:00
2023-08-05 11:03:14 +00:00
var ok bool
2023-08-07 01:51:07 +00:00
s . marginTransferService , ok = session . Exchange . ( marginTransferService )
if ! ok {
return errMarginTransferNotSupport
}
2023-08-05 11:03:14 +00:00
2023-08-07 01:51:07 +00:00
s . depositHistoryService , ok = session . Exchange . ( types . ExchangeTransferService )
if ! ok {
return errDepositHistoryNotSupport
}
2023-08-05 11:03:14 +00:00
2023-08-08 04:38:23 +00:00
session . UserDataStream . OnStart ( func ( ) {
go s . tickWatcher ( ctx , s . Interval . Duration ( ) )
} )
2023-08-08 04:23:17 +00:00
2023-08-07 01:51:07 +00:00
return nil
}
2023-08-05 11:03:14 +00:00
2024-11-14 07:38:30 +00:00
func ( s * Strategy ) isDust ( asset string , amount fixedpoint . Value ) bool {
if s . IgnoreDust {
if dustAmount , ok := s . DustAmounts [ asset ] ; ok {
return amount . Compare ( dustAmount ) <= 0
}
}
return false
}
2023-08-07 01:51:07 +00:00
func ( s * Strategy ) tickWatcher ( ctx context . Context , interval time . Duration ) {
ticker := time . NewTicker ( interval )
defer ticker . Stop ( )
2023-08-08 04:23:17 +00:00
s . checkDeposits ( ctx )
2023-08-08 04:38:23 +00:00
2023-08-07 01:51:07 +00:00
for {
select {
case <- ctx . Done ( ) :
return
case <- ticker . C :
2023-08-08 04:23:17 +00:00
s . checkDeposits ( ctx )
}
}
}
func ( s * Strategy ) checkDeposits ( ctx context . Context ) {
2024-11-12 07:48:17 +00:00
accountLimiter := rate . NewLimiter ( rate . Every ( 5 * time . Second ) , 1 )
2023-08-11 11:03:16 +00:00
2023-08-08 04:23:17 +00:00
for _ , asset := range s . Assets {
2024-02-15 03:43:59 +00:00
logger := s . logger . WithField ( "asset" , asset )
logger . Debugf ( "checking %s deposits..." , asset )
2023-08-08 04:08:14 +00:00
2023-08-08 04:23:17 +00:00
succeededDeposits , err := s . scanDepositHistory ( ctx , asset , 4 * time . Hour )
if err != nil {
2024-02-15 03:43:59 +00:00
logger . WithError ( err ) . Errorf ( "unable to scan deposit history" )
2023-08-08 04:23:17 +00:00
return
}
2023-08-08 03:07:21 +00:00
2023-08-08 04:23:17 +00:00
if len ( succeededDeposits ) == 0 {
2024-02-15 03:43:59 +00:00
logger . Debugf ( "no %s deposit found" , asset )
2023-08-08 04:38:23 +00:00
continue
2024-11-04 09:34:56 +00:00
} else {
logger . Infof ( "found %d %s deposits" , len ( succeededDeposits ) , asset )
}
if s . TransferDelay > 0 {
logger . Infof ( "delaying transfer for %s..." , s . TransferDelay . Duration ( ) )
time . Sleep ( s . TransferDelay . Duration ( ) )
2023-08-08 04:23:17 +00:00
}
2023-08-08 03:07:21 +00:00
2023-08-08 04:23:17 +00:00
for _ , d := range succeededDeposits {
2024-02-15 03:43:59 +00:00
logger . Infof ( "found succeeded %s deposit: %+v" , asset , d )
2023-08-08 04:23:17 +00:00
2023-08-11 11:03:16 +00:00
if err2 := accountLimiter . Wait ( ctx ) ; err2 != nil {
2024-02-15 03:43:59 +00:00
logger . WithError ( err2 ) . Errorf ( "rate limiter error" )
2023-08-11 11:03:16 +00:00
return
}
2023-08-16 04:02:18 +00:00
// we can't use the account from margin
amount := d . Amount
if service , ok := s . session . Exchange . ( spotAccountQueryService ) ; ok {
2024-05-20 10:05:21 +00:00
var account * types . Account
err = retry . GeneralBackoff ( ctx , func ( ) ( err error ) {
account , err = service . QuerySpotAccount ( ctx )
return err
} )
if err != nil || account == nil {
logger . WithError ( err ) . Errorf ( "unable to query spot account" )
2023-08-16 04:02:18 +00:00
continue
}
2024-11-04 09:34:56 +00:00
bal , ok := account . Balance ( d . Asset )
if ok {
2024-02-15 03:43:59 +00:00
logger . Infof ( "spot account balance %s: %+v" , d . Asset , bal )
2023-08-16 04:02:18 +00:00
amount = fixedpoint . Min ( bal . Available , amount )
} else {
2024-02-15 03:43:59 +00:00
logger . Errorf ( "unexpected error: %s balance not found" , d . Asset )
2023-08-16 04:02:18 +00:00
}
2024-11-04 09:34:56 +00:00
if amount . IsZero ( ) || amount . Sign ( ) < 0 {
2024-11-15 03:02:04 +00:00
bbgo . Notify ( "🔍 Found succeeded deposit %s %s, but the balance %s %s is insufficient, skip transferring" ,
2024-11-04 09:34:56 +00:00
d . Amount . String ( ) , d . Asset ,
bal . Available . String ( ) , bal . Currency )
continue
}
2023-08-11 11:03:16 +00:00
}
2024-11-15 03:02:04 +00:00
bbgo . Notify ( "🔍 Found succeeded deposit %s %s, transferring %s %s into the margin account" ,
2023-08-08 04:23:17 +00:00
d . Amount . String ( ) , d . Asset ,
amount . String ( ) , d . Asset )
2023-08-08 04:08:14 +00:00
2024-11-12 07:48:17 +00:00
if s . SlackAlert != nil {
bbgo . PostLiveNote ( & d ,
2024-11-12 07:49:29 +00:00
livenote . Channel ( s . SlackAlert . Channel ) ,
2024-11-15 03:02:04 +00:00
livenote . Comment ( fmt . Sprintf ( "🚥 Transferring deposit asset %s %s into the margin account" , amount . String ( ) , d . Asset ) ) ,
2024-11-12 07:48:17 +00:00
)
}
2024-05-20 10:05:21 +00:00
err2 := retry . GeneralBackoff ( ctx , func ( ) error {
return s . marginTransferService . TransferMarginAccountAsset ( ctx , d . Asset , amount , types . TransferIn )
} )
2024-11-14 08:16:02 +00:00
2024-05-20 10:05:21 +00:00
if err2 != nil {
2024-02-15 03:43:59 +00:00
logger . WithError ( err2 ) . Errorf ( "unable to transfer deposit asset into the margin account" )
2024-11-12 07:48:17 +00:00
if s . SlackAlert != nil {
bbgo . PostLiveNote ( & d ,
2024-11-12 07:49:29 +00:00
livenote . Channel ( s . SlackAlert . Channel ) ,
2024-11-15 03:02:04 +00:00
livenote . Comment ( fmt . Sprintf ( "❌ Margin account transfer error: %+v" , err2 ) ) ,
2024-11-14 08:16:02 +00:00
)
}
} else {
if s . SlackAlert != nil {
bbgo . PostLiveNote ( & d ,
livenote . Channel ( s . SlackAlert . Channel ) ,
2024-11-15 03:02:04 +00:00
livenote . Comment ( fmt . Sprintf ( "✅ %s %s transferred successfully" , amount . String ( ) , d . Asset ) ) ,
2024-11-12 07:48:17 +00:00
)
}
2023-08-07 01:51:07 +00:00
}
2023-08-05 11:03:14 +00:00
}
2023-08-07 01:51:07 +00:00
}
}
2023-08-05 11:03:14 +00:00
2024-11-12 07:48:17 +00:00
func ( s * Strategy ) addWatchingDeposit ( deposit types . Deposit ) {
s . watchingDeposits [ deposit . TransactionID ] = deposit
2024-11-14 14:38:57 +00:00
if lastTime , ok := s . lastAssetDepositTimes [ deposit . Asset ] ; ok {
s . lastAssetDepositTimes [ deposit . Asset ] = later ( deposit . Time . Time ( ) , lastTime )
} else {
s . lastAssetDepositTimes [ deposit . Asset ] = deposit . Time . Time ( )
}
2024-11-12 07:48:17 +00:00
if s . SlackAlert != nil {
bbgo . PostLiveNote ( & deposit ,
2024-11-12 07:49:29 +00:00
livenote . Channel ( s . SlackAlert . Channel ) ,
2024-11-12 08:09:51 +00:00
livenote . Pin ( s . SlackAlert . Pin ) ,
2024-11-12 07:48:17 +00:00
livenote . CompareObject ( true ) ,
livenote . OneTimeMention ( s . SlackAlert . Mentions ... ) ,
)
}
}
2023-08-08 03:07:21 +00:00
func ( s * Strategy ) scanDepositHistory ( ctx context . Context , asset string , duration time . Duration ) ( [ ] types . Deposit , error ) {
2024-02-15 03:43:59 +00:00
logger := s . logger . WithField ( "asset" , asset )
logger . Debugf ( "scanning %s deposit history..." , asset )
2023-08-08 03:07:21 +00:00
2023-08-07 01:51:07 +00:00
now := time . Now ( )
2023-08-08 03:07:21 +00:00
since := now . Add ( - duration )
2024-05-20 10:05:21 +00:00
var deposits [ ] types . Deposit
err := retry . GeneralBackoff ( ctx , func ( ) ( err error ) {
deposits , err = s . depositHistoryService . QueryDepositHistory ( ctx , asset , since , now )
return err
} )
2023-08-07 01:51:07 +00:00
if err != nil {
return nil , err
2023-08-05 11:03:14 +00:00
}
2023-08-09 07:54:28 +00:00
// sort the recent deposit records in ascending order
2023-08-07 01:51:07 +00:00
sort . Slice ( deposits , func ( i , j int ) bool {
return deposits [ i ] . Time . Time ( ) . Before ( deposits [ j ] . Time . Time ( ) )
} )
s . mu . Lock ( )
2023-08-08 04:38:23 +00:00
defer s . mu . Unlock ( )
2023-08-07 01:51:07 +00:00
2024-11-12 07:48:17 +00:00
// update the watching deposits
2023-08-07 01:51:07 +00:00
for _ , deposit := range deposits {
2024-02-15 03:43:59 +00:00
logger . Debugf ( "checking deposit: %+v" , deposit )
2023-08-08 04:08:14 +00:00
2023-08-08 03:07:21 +00:00
if deposit . Asset != asset {
continue
}
2024-11-14 07:38:30 +00:00
if s . isDust ( asset , deposit . Amount ) {
continue
}
2024-11-12 07:48:17 +00:00
// if the deposit record is already in the watch list, update it
2023-08-07 01:51:07 +00:00
if _ , ok := s . watchingDeposits [ deposit . TransactionID ] ; ok {
2024-11-12 07:48:17 +00:00
s . addWatchingDeposit ( deposit )
2023-08-07 01:51:07 +00:00
} else {
2024-11-12 07:48:17 +00:00
// if the deposit record is not in the watch list, we need to check the status
// here the deposit is outside the watching list
2023-08-07 01:51:07 +00:00
switch deposit . Status {
2023-08-08 04:38:23 +00:00
2023-08-07 01:51:07 +00:00
case types . DepositSuccess :
2024-11-12 07:48:17 +00:00
// if the deposit is in success status, we need to check if it's newer than the latest deposit time
// this usually happens when the deposit is credited to the account very quickly
2023-08-09 07:54:28 +00:00
if depositTime , ok := s . lastAssetDepositTimes [ asset ] ; ok {
// if it's newer than the latest deposit time, then we just add it the monitoring list
if deposit . Time . After ( depositTime ) {
2024-11-12 07:53:17 +00:00
logger . Infof ( "adding new succeedded deposit: %s" , deposit . TransactionID )
2024-11-12 07:48:17 +00:00
s . addWatchingDeposit ( deposit )
2024-11-13 04:24:37 +00:00
} else {
// ignore all initial deposits that are already in success status
logger . Infof ( "ignored expired succeedded deposit: %s %+v" , deposit . TransactionID , deposit )
2023-08-09 07:54:28 +00:00
}
} else {
2024-11-14 14:38:57 +00:00
// if the latest deposit time is not found, check if the deposit is older than 5 minutes
expiryTime := 5 * time . Minute
if deposit . Time . Before ( time . Now ( ) . Add ( - expiryTime ) ) {
logger . Infof ( "ignored expired (%s) succeedded deposit: %s %+v" , expiryTime , deposit . TransactionID , deposit )
} else {
s . addWatchingDeposit ( deposit )
}
2023-08-09 07:54:28 +00:00
}
2023-08-07 01:51:07 +00:00
case types . DepositCredited , types . DepositPending :
2024-02-15 03:43:59 +00:00
logger . Infof ( "adding pending deposit: %s" , deposit . TransactionID )
2024-11-12 07:48:17 +00:00
s . addWatchingDeposit ( deposit )
2023-08-07 01:51:07 +00:00
}
}
}
2023-08-05 11:03:14 +00:00
2023-08-07 01:51:07 +00:00
var succeededDeposits [ ] types . Deposit
2024-11-12 07:48:17 +00:00
2024-11-12 07:53:17 +00:00
// find and move out succeeded deposits
2023-08-08 04:38:59 +00:00
for _ , deposit := range s . watchingDeposits {
2024-11-12 07:48:17 +00:00
switch deposit . Status {
case types . DepositSuccess :
2024-02-15 03:43:59 +00:00
logger . Infof ( "found pending -> success deposit: %+v" , deposit )
2023-08-08 03:07:21 +00:00
current , required := deposit . GetCurrentConfirmation ( )
if required > 0 && deposit . UnlockConfirm > 0 && current < deposit . UnlockConfirm {
2024-02-15 03:43:59 +00:00
logger . Infof ( "deposit %s unlock confirm %d is not reached, current: %d, required: %d, skip this round" , deposit . TransactionID , deposit . UnlockConfirm , current , required )
2023-08-08 03:07:21 +00:00
continue
}
2023-08-07 01:51:07 +00:00
succeededDeposits = append ( succeededDeposits , deposit )
delete ( s . watchingDeposits , deposit . TransactionID )
}
2023-08-05 11:03:14 +00:00
}
2023-08-07 01:51:07 +00:00
return succeededDeposits , nil
2023-08-05 11:03:14 +00:00
}
2023-08-09 07:54:28 +00:00
func later ( a , b time . Time ) time . Time {
if a . After ( b ) {
return a
}
return b
}