diff --git a/pkg/exchange/binance/binanceapi/get_withdraw_history_request.go b/pkg/exchange/binance/binanceapi/get_withdraw_history_request.go index 4e84dbc24..de1b88f0c 100644 --- a/pkg/exchange/binance/binanceapi/get_withdraw_history_request.go +++ b/pkg/exchange/binance/binanceapi/get_withdraw_history_request.go @@ -8,8 +8,9 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) -//go:generate stringer -type=TransferType // 1 for internal transfer, 0 for external transfer +// +//go:generate stringer -type=TransferType type TransferType int const ( @@ -33,7 +34,7 @@ type WithdrawRecord struct { TxID string `json:"txId"` } -//go:generate stringer -type=WithdrawStatus +//go:generate stringer -type=WithdrawStatus -trimprefix=WithdrawStatus type WithdrawStatus int // WithdrawStatus: 0(0:Email Sent,1:Cancelled 2:Awaiting Approval 3:Rejected 4:Processing 5:Failure 6:Completed) diff --git a/pkg/exchange/binance/binanceapi/get_withdraw_history_request_requestgen.go b/pkg/exchange/binance/binanceapi/get_withdraw_history_request_requestgen.go index 74717d3c4..eb40b3c73 100644 --- a/pkg/exchange/binance/binanceapi/get_withdraw_history_request_requestgen.go +++ b/pkg/exchange/binance/binanceapi/get_withdraw_history_request_requestgen.go @@ -212,6 +212,12 @@ func (g *GetWithdrawHistoryRequest) GetSlugsMap() (map[string]string, error) { 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) { // empty params for GET operation @@ -221,7 +227,9 @@ func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]WithdrawRecord, e 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) if err != nil { @@ -234,8 +242,32 @@ func (g *GetWithdrawHistoryRequest) Do(ctx context.Context) ([]WithdrawRecord, e } 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 } diff --git a/pkg/exchange/binance/binanceapi/withdrawstatus_string.go b/pkg/exchange/binance/binanceapi/withdrawstatus_string.go index 7c972b7fd..c3dd4f91c 100644 --- a/pkg/exchange/binance/binanceapi/withdrawstatus_string.go +++ b/pkg/exchange/binance/binanceapi/withdrawstatus_string.go @@ -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 @@ -17,9 +17,9 @@ func _() { _ = 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 { if i < 0 || i >= WithdrawStatus(len(_WithdrawStatus_index)-1) { diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index 824c20576..8f6d61922 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -9,10 +9,32 @@ import ( "github.com/adshao/go-binance/v2/futures" "github.com/pkg/errors" + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "github.com/c9s/bbgo/pkg/fixedpoint" "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 { market := types.Market{ Exchange: types.ExchangeBinance, diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 160253fdd..bc3e1001b 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -571,6 +571,11 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since return nil, err } + status, err := toGlobalWithdrawStatus(d.Status) + if err != nil { + return nil, err + } + withdraws = append(withdraws, types.Withdraw{ Exchange: types.ExchangeBinance, ApplyTime: types.Time(applyTime), @@ -581,7 +586,8 @@ func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since TransactionFee: d.TransactionFee, WithdrawOrderID: d.WithdrawOrderID, Network: d.Network, - Status: d.Status.String(), + Status: status, + OriginalStatus: fmt.Sprintf("%s (%d)", d.Status.String(), int(d.Status)), }) } diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index 28fa41788..6cf67d018 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -340,3 +340,27 @@ func convertWebSocketOrderUpdate(u max.OrderUpdate) (*types.Order, error) { UpdateTime: types.Time(time.Unix(0, u.UpdateTime*int64(time.Millisecond))), }, 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) + } +} diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 41f23d937..ce52d09da 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -867,23 +867,7 @@ func (e *Exchange) QueryWithdrawHistory( } // we can convert this later - status := 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 - - } + status := convertWithdrawStatus(d.State) txIDs[d.TxID] = struct{}{} withdraw := types.Withdraw{ @@ -891,14 +875,16 @@ func (e *Exchange) QueryWithdrawHistory( ApplyTime: types.Time(d.CreatedAt), Asset: toGlobalCurrency(d.Currency), Amount: d.Amount, - Address: "", + Address: d.Address, AddressTag: "", TransactionID: d.TxID, TransactionFee: d.Fee, TransactionFeeCurrency: d.FeeCurrency, + Network: d.NetworkProtocol, // WithdrawOrderID: d.WithdrawOrderID, // Network: d.Network, - Status: status, + Status: status, + OriginalStatus: string(d.State), } allWithdraws = append(allWithdraws, withdraw) } diff --git a/pkg/exchange/max/maxapi/account.go b/pkg/exchange/max/maxapi/account.go index 678de81f7..018c87b90 100644 --- a/pkg/exchange/max/maxapi/account.go +++ b/pkg/exchange/max/maxapi/account.go @@ -155,7 +155,23 @@ type WithdrawState string const ( WithdrawStateSubmitting WithdrawState = "submitting" + WithdrawStateSubmitted WithdrawState = "submitted" 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 { @@ -167,17 +183,22 @@ type Withdraw struct { FeeCurrency string `json:"fee_currency"` TxID string `json:"txid"` + NetworkProtocol string `json:"network_protocol"` + Address string `json:"to_address"` + // State can be "submitting", "submitted", // "rejected", "accepted", "suspect", "approved", "delisted_processing", // "processing", "retryable", "sent", "canceled", // "failed", "pending", "confirmed", // "kgi_manually_processing", "kgi_manually_confirmed", "kgi_possible_failed", // "sygna_verifying" - State string `json:"state"` - Confirmations int `json:"confirmations"` - CreatedAt types.MillisecondTimestamp `json:"created_at"` - UpdatedAt types.MillisecondTimestamp `json:"updated_at"` - Notes string `json:"notes"` + State WithdrawState `json:"state"` + + Status WithdrawStatus `json:"status,omitempty"` + + 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 diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 74fcba644..e94988e73 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -114,6 +114,34 @@ func (s *Strategy) aggregateBalances( 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( ctx context.Context, sessions map[string]*bbgo.ExchangeSession, currency string, changeQuantity fixedpoint.Value, ) (*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) _ = sessionBalances diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index bb5344600..d4364cad2 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -149,6 +149,11 @@ type CustomIntervalProvider interface { 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 { 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) diff --git a/pkg/types/withdraw.go b/pkg/types/withdraw.go index 18781341c..ef3e6d788 100644 --- a/pkg/types/withdraw.go +++ b/pkg/types/withdraw.go @@ -7,14 +7,28 @@ import ( "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 { - GID int64 `json:"gid" db:"gid"` - Exchange ExchangeName `json:"exchange" db:"exchange"` - Asset string `json:"asset" db:"asset"` - Amount fixedpoint.Value `json:"amount" db:"amount"` - Address string `json:"address" db:"address"` - AddressTag string `json:"addressTag"` - Status string `json:"status"` + GID int64 `json:"gid" db:"gid"` + Exchange ExchangeName `json:"exchange" db:"exchange"` + Asset string `json:"asset" db:"asset"` + Amount fixedpoint.Value `json:"amount" db:"amount"` + Address string `json:"address" db:"address"` + AddressTag string `json:"addressTag"` + Status WithdrawStatus `json:"status"` + OriginalStatus string `json:"originalStatus"` TransactionID string `json:"transactionID" db:"txn_id"` TransactionFee fixedpoint.Value `json:"transactionFee" db:"txn_fee"`