Merge pull request #1678 from c9s/c9s/xalign/withdraw-detection

FEATURE: [xalign] add withdraw detection
This commit is contained in:
c9s 2024-07-31 18:08:32 +08:00 committed by GitHub
commit eb317da21a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 187 additions and 40 deletions

View File

@ -8,8 +8,9 @@ import (
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
) )
//go:generate stringer -type=TransferType
// 1 for internal transfer, 0 for external transfer // 1 for internal transfer, 0 for external transfer
//
//go:generate stringer -type=TransferType
type TransferType int type TransferType int
const ( const (
@ -33,7 +34,7 @@ type WithdrawRecord struct {
TxID string `json:"txId"` TxID string `json:"txId"`
} }
//go:generate stringer -type=WithdrawStatus //go:generate stringer -type=WithdrawStatus -trimprefix=WithdrawStatus
type WithdrawStatus int type WithdrawStatus int
// WithdrawStatus: 0(0:Email Sent,1:Cancelled 2:Awaiting Approval 3:Rejected 4:Processing 5:Failure 6:Completed) // WithdrawStatus: 0(0:Email Sent,1:Cancelled 2:Awaiting Approval 3:Rejected 4:Processing 5:Failure 6:Completed)

View File

@ -212,6 +212,12 @@ func (g *GetWithdrawHistoryRequest) GetSlugsMap() (map[string]string, error) {
return slugs, nil return slugs, nil
} }
// GetPath returns the request path of the API
func (g *GetWithdrawHistoryRequest) GetPath() string {
return "/sapi/v1/capital/withdraw/history"
}
// Do generates the request object and send the request object to the API endpoint
func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]WithdrawRecord, error) { func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]WithdrawRecord, error) {
// empty params for GET operation // empty params for GET operation
@ -221,7 +227,9 @@ func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]WithdrawRecord, e
return nil, err return nil, err
} }
apiURL := "/sapi/v1/capital/withdraw/history" var apiURL string
apiURL = g.GetPath()
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil { if err != nil {
@ -234,8 +242,32 @@ func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]WithdrawRecord, e
} }
var apiResponse []WithdrawRecord var apiResponse []WithdrawRecord
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err type responseUnmarshaler interface {
Unmarshal(data []byte) error
}
if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok {
if err := unmarshaler.Unmarshal(response.Body); err != nil {
return nil, err
}
} else {
// The line below checks the content type, however, some API server might not send the correct content type header,
// Hence, this is commented for backward compatibility
// response.IsJSON()
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
}
type responseValidator interface {
Validate() error
}
if validator, ok := interface{}(&apiResponse).(responseValidator); ok {
if err := validator.Validate(); err != nil {
return nil, err
}
} }
return apiResponse, nil return apiResponse, nil
} }

View File

@ -1,4 +1,4 @@
// Code generated by "stringer -type=WithdrawStatus"; DO NOT EDIT. // Code generated by "stringer -type=WithdrawStatus -trimprefix=WithdrawStatus"; DO NOT EDIT.
package binanceapi package binanceapi
@ -17,9 +17,9 @@ func _() {
_ = x[WithdrawStatusCompleted-6] _ = x[WithdrawStatusCompleted-6]
} }
const _WithdrawStatus_name = "WithdrawStatusEmailSentWithdrawStatusCancelledWithdrawStatusAwaitingApprovalWithdrawStatusRejectedWithdrawStatusProcessingWithdrawStatusFailureWithdrawStatusCompleted" const _WithdrawStatus_name = "EmailSentCancelledAwaitingApprovalRejectedProcessingFailureCompleted"
var _WithdrawStatus_index = [...]uint8{0, 23, 46, 76, 98, 122, 143, 166} var _WithdrawStatus_index = [...]uint8{0, 9, 18, 34, 42, 52, 59, 68}
func (i WithdrawStatus) String() string { func (i WithdrawStatus) String() string {
if i < 0 || i >= WithdrawStatus(len(_WithdrawStatus_index)-1) { if i < 0 || i >= WithdrawStatus(len(_WithdrawStatus_index)-1) {

View File

@ -9,10 +9,32 @@ import (
"github.com/adshao/go-binance/v2/futures" "github.com/adshao/go-binance/v2/futures"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/exchange/binance/binanceapi"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
func toGlobalWithdrawStatus(status binanceapi.WithdrawStatus) (types.WithdrawStatus, error) {
switch status {
case binanceapi.WithdrawStatusEmailSent:
return types.WithdrawStatusSent, nil
case binanceapi.WithdrawStatusCancelled:
return types.WithdrawStatusCancelled, nil
case binanceapi.WithdrawStatusAwaitingApproval:
return types.WithdrawStatusAwaitingApproval, nil
case binanceapi.WithdrawStatusRejected:
return types.WithdrawStatusRejected, nil
case binanceapi.WithdrawStatusProcessing:
return types.WithdrawStatusProcessing, nil
case binanceapi.WithdrawStatusFailure:
return types.WithdrawStatusFailed, nil
case binanceapi.WithdrawStatusCompleted:
return types.WithdrawStatusCompleted, nil
default:
return types.WithdrawStatusUnknown, fmt.Errorf("unable to convert the withdraw status: %s", status)
}
}
func toGlobalMarket(symbol binance.Symbol) types.Market { func toGlobalMarket(symbol binance.Symbol) types.Market {
market := types.Market{ market := types.Market{
Exchange: types.ExchangeBinance, Exchange: types.ExchangeBinance,

View File

@ -571,6 +571,11 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since
return nil, err return nil, err
} }
status, err := toGlobalWithdrawStatus(d.Status)
if err != nil {
return nil, err
}
withdraws = append(withdraws, types.Withdraw{ withdraws = append(withdraws, types.Withdraw{
Exchange: types.ExchangeBinance, Exchange: types.ExchangeBinance,
ApplyTime: types.Time(applyTime), ApplyTime: types.Time(applyTime),
@ -581,7 +586,8 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since
TransactionFee: d.TransactionFee, TransactionFee: d.TransactionFee,
WithdrawOrderID: d.WithdrawOrderID, WithdrawOrderID: d.WithdrawOrderID,
Network: d.Network, Network: d.Network,
Status: d.Status.String(), Status: status,
OriginalStatus: fmt.Sprintf("%s (%d)", d.Status.String(), int(d.Status)),
}) })
} }

View File

@ -340,3 +340,27 @@ func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) {
UpdateTime: types.Time(time.Unix(0, u.UpdateTime*int64(time.Millisecond))), UpdateTime: types.Time(time.Unix(0, u.UpdateTime*int64(time.Millisecond))),
}, nil }, nil
} }
func convertWithdrawStatus(state max.WithdrawState) types.WithdrawStatus {
switch state {
case max.WithdrawStateSent, max.WithdrawStateSubmitting, max.WithdrawStatePending, "accepted", "approved":
return types.WithdrawStatusSent
case max.WithdrawStateProcessing, "delisted_processing", "kgi_manually_processing", "kgi_manually_confirmed", "sygna_verifying":
return types.WithdrawStatusProcessing
case max.WithdrawStateFailed, "kgi_possible_failed", "rejected", "suspect", "retryable":
return types.WithdrawStatusFailed
case max.WithdrawStateCanceled:
return types.WithdrawStatusCancelled
case "confirmed":
// make it compatible with binance
return types.WithdrawStatusCompleted
default:
return types.WithdrawStatus(state)
}
}

View File

@ -867,23 +867,7 @@ func (e *Exchange) QueryWithdrawHistory(
} }
// we can convert this later // we can convert this later
status := d.State status := convertWithdrawStatus(d.State)
switch d.State {
case "confirmed":
status = "completed" // make it compatible with binance
case "submitting", "submitted", "accepted",
"rejected", "suspect", "approved", "delisted_processing",
"processing", "retryable", "sent", "canceled",
"failed", "pending",
"kgi_manually_processing", "kgi_manually_confirmed", "kgi_possible_failed",
"sygna_verifying":
default:
status = d.State
}
txIDs[d.TxID] = struct{}{} txIDs[d.TxID] = struct{}{}
withdraw := types.Withdraw{ withdraw := types.Withdraw{
@ -891,14 +875,16 @@ func (e *Exchange) QueryWithdrawHistory(
ApplyTime: types.Time(d.CreatedAt), ApplyTime: types.Time(d.CreatedAt),
Asset: toGlobalCurrency(d.Currency), Asset: toGlobalCurrency(d.Currency),
Amount: d.Amount, Amount: d.Amount,
Address: "", Address: d.Address,
AddressTag: "", AddressTag: "",
TransactionID: d.TxID, TransactionID: d.TxID,
TransactionFee: d.Fee, TransactionFee: d.Fee,
TransactionFeeCurrency: d.FeeCurrency, TransactionFeeCurrency: d.FeeCurrency,
Network: d.NetworkProtocol,
// WithdrawOrderID: d.WithdrawOrderID, // WithdrawOrderID: d.WithdrawOrderID,
// Network: d.Network, // Network: d.Network,
Status: status, Status: status,
OriginalStatus: string(d.State),
} }
allWithdraws = append(allWithdraws, withdraw) allWithdraws = append(allWithdraws, withdraw)
} }

View File

@ -155,7 +155,23 @@ type WithdrawState string
const ( const (
WithdrawStateSubmitting WithdrawState = "submitting" WithdrawStateSubmitting WithdrawState = "submitting"
WithdrawStateSubmitted WithdrawState = "submitted"
WithdrawStateConfirmed WithdrawState = "confirmed" WithdrawStateConfirmed WithdrawState = "confirmed"
WithdrawStatePending WithdrawState = "pending"
WithdrawStateProcessing WithdrawState = "processing"
WithdrawStateCanceled WithdrawState = "canceled"
WithdrawStateFailed WithdrawState = "failed"
WithdrawStateSent WithdrawState = "sent"
WithdrawStateRejected WithdrawState = "rejected"
)
type WithdrawStatus string
const (
WithdrawStatusPending WithdrawStatus = "pending"
WithdrawStatusCancelled WithdrawStatus = "cancelled"
WithdrawStatusFailed WithdrawStatus = "failed"
WithdrawStatusOK WithdrawStatus = "ok"
) )
type Withdraw struct { type Withdraw struct {
@ -167,17 +183,22 @@ type Withdraw struct {
FeeCurrency string `json:"fee_currency"` FeeCurrency string `json:"fee_currency"`
TxID string `json:"txid"` TxID string `json:"txid"`
NetworkProtocol string `json:"network_protocol"`
Address string `json:"to_address"`
// State can be "submitting", "submitted", // State can be "submitting", "submitted",
// "rejected", "accepted", "suspect", "approved", "delisted_processing", // "rejected", "accepted", "suspect", "approved", "delisted_processing",
// "processing", "retryable", "sent", "canceled", // "processing", "retryable", "sent", "canceled",
// "failed", "pending", "confirmed", // "failed", "pending", "confirmed",
// "kgi_manually_processing", "kgi_manually_confirmed", "kgi_possible_failed", // "kgi_manually_processing", "kgi_manually_confirmed", "kgi_possible_failed",
// "sygna_verifying" // "sygna_verifying"
State string `json:"state"` State WithdrawState `json:"state"`
Confirmations int `json:"confirmations"`
CreatedAt types.MillisecondTimestamp `json:"created_at"` Status WithdrawStatus `json:"status,omitempty"`
UpdatedAt types.MillisecondTimestamp `json:"updated_at"`
Notes string `json:"notes"` CreatedAt types.MillisecondTimestamp `json:"created_at"`
UpdatedAt types.MillisecondTimestamp `json:"updated_at"`
Notes string `json:"notes"`
} }
//go:generate GetRequest -url "v2/withdrawals" -type GetWithdrawHistoryRequest -responseType []Withdraw //go:generate GetRequest -url "v2/withdrawals" -type GetWithdrawHistoryRequest -responseType []Withdraw

View File

@ -114,6 +114,34 @@ func (s *Strategy) aggregateBalances(
return totalBalances, sessionBalances return totalBalances, sessionBalances
} }
func (s *Strategy) detectActiveTransfers(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) (bool, error) {
var err2 error
until := time.Now()
since := until.Add(-time.Hour * 24)
for _, session := range sessions {
transferService, ok := session.Exchange.(types.ExchangeTransferHistoryService)
if !ok {
continue
}
withdraws, err := transferService.QueryWithdrawHistory(ctx, "", since, until)
if err != nil {
log.WithError(err).Errorf("unable to query withdraw history")
err2 = err
continue
}
for _, withdraw := range withdraws {
switch withdraw.Status {
case types.WithdrawStatusProcessing, types.WithdrawStatusSent, types.WithdrawStatusAwaitingApproval:
return true, nil
}
}
}
return false, err2
}
func (s *Strategy) selectSessionForCurrency( func (s *Strategy) selectSessionForCurrency(
ctx context.Context, sessions map[string]*bbgo.ExchangeSession, currency string, changeQuantity fixedpoint.Value, ctx context.Context, sessions map[string]*bbgo.ExchangeSession, currency string, changeQuantity fixedpoint.Value,
) (*bbgo.ExchangeSession, *types.SubmitOrder) { ) (*bbgo.ExchangeSession, *types.SubmitOrder) {
@ -391,6 +419,14 @@ func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.Exchange
} }
} }
foundActiveTransfer, err := s.detectActiveTransfers(ctx, sessions)
if err != nil {
log.WithError(err).Errorf("unable to check active transfers")
} else if foundActiveTransfer {
log.Warnf("found active transfer, skip balance align check")
return
}
totalBalances, sessionBalances := s.aggregateBalances(ctx, sessions) totalBalances, sessionBalances := s.aggregateBalances(ctx, sessions)
_ = sessionBalances _ = sessionBalances

View File

@ -149,6 +149,11 @@ type CustomIntervalProvider interface {
IsSupportedInterval(interval Interval) bool IsSupportedInterval(interval Interval) bool
} }
type ExchangeTransferHistoryService interface {
QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []Deposit, err error)
QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []Withdraw, err error)
}
type ExchangeTransferService interface { type ExchangeTransferService interface {
QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []Deposit, err error) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []Deposit, err error)
QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []Withdraw, err error) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []Withdraw, err error)

View File

@ -7,14 +7,28 @@ import (
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
) )
type WithdrawStatus string
const (
WithdrawStatusSent WithdrawStatus = "sent"
WithdrawStatusCancelled WithdrawStatus = "cancelled"
WithdrawStatusAwaitingApproval WithdrawStatus = "awaiting_approval"
WithdrawStatusRejected WithdrawStatus = "rejected"
WithdrawStatusProcessing WithdrawStatus = "processing"
WithdrawStatusFailed WithdrawStatus = "failed"
WithdrawStatusCompleted WithdrawStatus = "completed"
WithdrawStatusUnknown WithdrawStatus = "unknown"
)
type Withdraw struct { type Withdraw struct {
GID int64 `json:"gid" db:"gid"` GID int64 `json:"gid" db:"gid"`
Exchange ExchangeName `json:"exchange" db:"exchange"` Exchange ExchangeName `json:"exchange" db:"exchange"`
Asset string `json:"asset" db:"asset"` Asset string `json:"asset" db:"asset"`
Amount fixedpoint.Value `json:"amount" db:"amount"` Amount fixedpoint.Value `json:"amount" db:"amount"`
Address string `json:"address" db:"address"` Address string `json:"address" db:"address"`
AddressTag string `json:"addressTag"` AddressTag string `json:"addressTag"`
Status string `json:"status"` Status WithdrawStatus `json:"status"`
OriginalStatus string `json:"originalStatus"`
TransactionID string `json:"transactionID" db:"txn_id"` TransactionID string `json:"transactionID" db:"txn_id"`
TransactionFee fixedpoint.Value `json:"transactionFee" db:"txn_fee"` TransactionFee fixedpoint.Value `json:"transactionFee" db:"txn_fee"`