diff --git a/pkg/cmd/deposit.go b/pkg/cmd/deposit.go new file mode 100644 index 000000000..45900ed0d --- /dev/null +++ b/pkg/cmd/deposit.go @@ -0,0 +1,97 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/types" +) + +func init() { + depositsCmd.Flags().String("session", "", "the exchange session name for querying balances") + depositsCmd.Flags().String("asset", "", "the trading pair, like btcusdt") + RootCmd.AddCommand(depositsCmd) +} + +// go run ./cmd/bbgo deposits --session=ftx --asset="BTC" +// This is a testing util and will query deposits in last 7 days. +var depositsCmd = &cobra.Command{ + Use: "deposits", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + configFile, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + + if len(configFile) == 0 { + return errors.New("--config option is required") + } + + // if config file exists, use the config loaded from the config file. + // otherwise, use a empty config object + var userConfig *bbgo.Config + if _, err := os.Stat(configFile); err == nil { + // load successfully + userConfig, err = bbgo.Load(configFile, false) + if err != nil { + return err + } + } else if os.IsNotExist(err) { + // config file doesn't exist + userConfig = &bbgo.Config{} + } else { + // other error + return err + } + + environ := bbgo.NewEnvironment() + + if err := environ.ConfigureExchangeSessions(userConfig); err != nil { + return err + } + + sessionName, err := cmd.Flags().GetString("session") + if err != nil { + return err + } + + session, ok := environ.Session(sessionName) + if !ok { + return fmt.Errorf("session %s not found", sessionName) + } + + asset, err := cmd.Flags().GetString("asset") + if err != nil { + return fmt.Errorf("can't get the asset from flags: %w", err) + } + if asset == "" { + return fmt.Errorf("asset is not found") + } + + until := time.Now() + since := until.Add(-7 * 24 * time.Hour) + exchange, ok := session.Exchange.(types.ExchangeTransferService) + if !ok { + return fmt.Errorf("exchange session %s does not implement transfer service", sessionName) + } + histories, err := exchange.QueryDepositHistory(ctx, asset, since, until) + if err != nil { + return err + } + + for _, h := range histories { + log.Infof("deposit history: %+v", h) + } + return nil + }, +} diff --git a/pkg/exchange/ftx/convert.go b/pkg/exchange/ftx/convert.go index f833f2652..d4b46e0c0 100644 --- a/pkg/exchange/ftx/convert.go +++ b/pkg/exchange/ftx/convert.go @@ -3,6 +3,9 @@ package ftx import ( "fmt" "strings" + "time" + + log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/datatype" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -73,3 +76,35 @@ func toGlobalOrder(r order) (types.Order, error) { return o, nil } + +func toGlobalDeposit(input depositHistory) (types.Deposit, error) { + s, err := toGlobalDepositStatus(input.Status) + if err != nil { + log.WithError(err).Warnf("assign empty string to the deposit status") + } + t := input.Time + if input.ConfirmedTime != (time.Time{}) { + t = input.ConfirmedTime + } + d := types.Deposit{ + GID: 0, + Exchange: types.ExchangeFTX, + Time: datatype.Time(t), + Amount: input.Size, + Asset: toGlobalCurrency(input.Coin), + TransactionID: input.TxID, + Status: s, + Address: input.Address.Address, + AddressTag: input.Address.Tag, + } + return d, nil +} + +func toGlobalDepositStatus(input string) (types.DepositStatus, error) { + // The document only list `confirmed` status + switch input { + case "confirmed", "complete": + return types.DepositSuccess, nil + } + return "", fmt.Errorf("unsupported status %s", input) +} diff --git a/pkg/exchange/ftx/exchange.go b/pkg/exchange/ftx/exchange.go index 6e6ea7ff6..d4bb18d58 100644 --- a/pkg/exchange/ftx/exchange.go +++ b/pkg/exchange/ftx/exchange.go @@ -149,7 +149,27 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type } func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) { - panic("implement me") + if err = verifySinceUntil(since, until); err != nil { + return nil, err + } + + resp, err := e.newRest().DepositHistory(ctx) + if err != nil { + return nil, err + } + sort.Slice(resp.Result, func(i, j int) bool { + return resp.Result[i].Time.Before(resp.Result[j].Time) + }) + for _, r := range resp.Result { + d, err := toGlobalDeposit(r) + if err != nil { + return nil, err + } + if d.Asset == asset && !since.After(d.Time.Time()) && !until.Before(d.Time.Time()) { + allDeposits = append(allDeposits, d) + } + } + return } func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []types.Withdraw, err error) { @@ -212,8 +232,8 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ // symbol, since and until are all optional. FTX can only query by order created time, not updated time. // FTX doesn't support lastOrderID, so we will query by the time range first, and filter by the lastOrderID. func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) { - if since.After(until) { - return nil, fmt.Errorf("since can't be after until") + if err := verifySinceUntil(since, until); err != nil { + return nil, err } if lastOrderID > 0 { logger.Warn("FTX doesn't support lastOrderID") @@ -281,3 +301,16 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { panic("implement me") } + +func verifySinceUntil(since, until time.Time) error { + if since.After(until) { + return fmt.Errorf("since can't be greater than until") + } + if since == (time.Time{}) { + return fmt.Errorf("since not found") + } + if until == (time.Time{}) { + return fmt.Errorf("until not found") + } + return nil +} diff --git a/pkg/exchange/ftx/exchange_test.go b/pkg/exchange/ftx/exchange_test.go index bdd02f4a0..f91b3df1a 100644 --- a/pkg/exchange/ftx/exchange_test.go +++ b/pkg/exchange/ftx/exchange_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/datatype" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -116,7 +117,7 @@ func TestExchange_QueryClosedOrders(t *testing.T) { serverURL, err := url.Parse(ts.URL) assert.NoError(t, err) ex.restEndpoint = serverURL - resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Time{}, time.Time{}, 100) + resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) assert.NoError(t, err) assert.Len(t, resp, 0) @@ -157,7 +158,7 @@ func TestExchange_QueryClosedOrders(t *testing.T) { serverURL, err := url.Parse(ts.URL) assert.NoError(t, err) ex.restEndpoint = serverURL - resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Time{}, time.Time{}, 100) + resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) assert.NoError(t, err) assert.Len(t, resp, 1) assert.Equal(t, "BTC-PERP", resp[0].Symbol) @@ -201,7 +202,7 @@ func TestExchange_QueryClosedOrders(t *testing.T) { serverURL, err := url.Parse(ts.URL) assert.NoError(t, err) ex.restEndpoint = serverURL - resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Time{}, time.Time{}, 100) + resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) assert.NoError(t, err) assert.Len(t, resp, 3) @@ -272,7 +273,7 @@ func TestExchange_QueryClosedOrders(t *testing.T) { serverURL, err := url.Parse(ts.URL) assert.NoError(t, err) ex.restEndpoint = serverURL - resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Time{}, time.Time{}, 100) + resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) assert.NoError(t, err) assert.Len(t, resp, 2) expectedOrderID := []uint64{123, 456} @@ -423,3 +424,63 @@ func TestExchange_QueryMarkets(t *testing.T) { TickSize: 1, }, resp["BTC/USD"]) } + +func TestExchange_QueryDepositHistory(t *testing.T) { + respJSON := ` +{ + "success": true, + "result": [ + { + "coin": "TUSD", + "confirmations": 64, + "confirmedTime": "2019-03-05T09:56:55.728933+00:00", + "fee": 0, + "id": 1, + "sentTime": "2019-03-05T09:56:55.735929+00:00", + "size": 99.0, + "status": "confirmed", + "time": "2019-03-05T09:56:55.728933+00:00", + "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1", + "address": {"address": "test-addr", "tag": "test-tag"} + } + ] +} +` + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, respJSON) + })) + defer ts.Close() + + ex := NewExchange("", "", "") + serverURL, err := url.Parse(ts.URL) + assert.NoError(t, err) + ex.restEndpoint = serverURL + + ctx := context.Background() + layout := "2006-01-02T15:04:05.999999Z07:00" + actualConfirmedTime, err := time.Parse(layout, "2019-03-05T09:56:55.728933+00:00") + assert.NoError(t, err) + dh, err := ex.QueryDepositHistory(ctx, "TUSD", actualConfirmedTime.Add(-1*time.Hour), actualConfirmedTime.Add(1*time.Hour)) + assert.NoError(t, err) + assert.Len(t, dh, 1) + assert.Equal(t, types.Deposit{ + Exchange: types.ExchangeFTX, + Time: datatype.Time(actualConfirmedTime), + Amount: 99.0, + Asset: "TUSD", + TransactionID: "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1", + Status: types.DepositSuccess, + Address: "test-addr", + AddressTag: "test-tag", + }, dh[0]) + + // not in the time range + dh, err = ex.QueryDepositHistory(ctx, "TUSD", actualConfirmedTime.Add(1*time.Hour), actualConfirmedTime.Add(2*time.Hour)) + assert.NoError(t, err) + assert.Len(t, dh, 0) + + // exclude by asset + dh, err = ex.QueryDepositHistory(ctx, "BTC", actualConfirmedTime.Add(-1*time.Hour), actualConfirmedTime.Add(1*time.Hour)) + assert.NoError(t, err) + assert.Len(t, dh, 0) +} diff --git a/pkg/exchange/ftx/rest.go b/pkg/exchange/ftx/rest.go index 7ac417ba1..5457254cd 100644 --- a/pkg/exchange/ftx/rest.go +++ b/pkg/exchange/ftx/rest.go @@ -19,7 +19,7 @@ import ( ) type restRequest struct { - *balanceRequest + *walletRequest *orderRequest *accountRequest *marketRequest @@ -46,7 +46,7 @@ func newRestRequest(c *http.Client, baseURL *url.URL) *restRequest { r.marketRequest = &marketRequest{restRequest: r} r.accountRequest = &accountRequest{restRequest: r} - r.balanceRequest = &balanceRequest{restRequest: r} + r.walletRequest = &walletRequest{restRequest: r} r.orderRequest = &orderRequest{restRequest: r} return r } diff --git a/pkg/exchange/ftx/rest_balance_request.go b/pkg/exchange/ftx/rest_balance_request.go deleted file mode 100644 index c8e07d737..000000000 --- a/pkg/exchange/ftx/rest_balance_request.go +++ /dev/null @@ -1,29 +0,0 @@ -package ftx - -import ( - "context" - "encoding/json" - "fmt" -) - -type balanceRequest struct { - *restRequest -} - -func (r *balanceRequest) Balances(ctx context.Context) (balances, error) { - resp, err := r. - Method("GET"). - ReferenceURL("api/wallet/balances"). - DoAuthenticatedRequest(ctx) - - if err != nil { - return balances{}, err - } - - var b balances - if err := json.Unmarshal(resp.Body, &b); err != nil { - return balances{}, fmt.Errorf("failed to unmarshal balance response body to json: %w", err) - } - - return b, nil -} diff --git a/pkg/exchange/ftx/rest_responses.go b/pkg/exchange/ftx/rest_responses.go index 02fc4e295..f929de9ce 100644 --- a/pkg/exchange/ftx/rest_responses.go +++ b/pkg/exchange/ftx/rest_responses.go @@ -200,3 +200,57 @@ type orderResponse struct { Result order `json:"result"` } + +/* +{ + "success": true, + "result": [ + { + "coin": "TUSD", + "confirmations": 64, + "confirmedTime": "2019-03-05T09:56:55.728933+00:00", + "fee": 0, + "id": 1, + "sentTime": "2019-03-05T09:56:55.735929+00:00", + "size": 99.0, + "status": "confirmed", + "time": "2019-03-05T09:56:55.728933+00:00", + "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1" + } + ] +} +*/ +type depositHistoryResponse struct { + Success bool `json:"success"` + Result []depositHistory `json:"result"` +} + +type depositHistory struct { + ID int64 `json:"id"` + Coin string `json:"coin"` + TxID string `json:"txid"` + Address address `json:"address"` + Confirmations int64 `json:"confirmations"` + ConfirmedTime time.Time `json:"confirmedTime"` + Fee float64 `json:"fee"` + SentTime time.Time `json:"sentTime"` + Size float64 `json:"size"` + Status string `json:"status"` + Time time.Time `json:"time"` + Notes string `json:"notes"` +} + +/** +{ + "address": "test123", + "tag": null, + "method": "ltc", + "coin": null +} +*/ +type address struct { + Address string `json:"address"` + Tag string `json:"tag"` + Method string `json:"method"` + Coin string `json:"coin"` +} diff --git a/pkg/exchange/ftx/rest_wallet_request.go b/pkg/exchange/ftx/rest_wallet_request.go new file mode 100644 index 000000000..6a7d699e3 --- /dev/null +++ b/pkg/exchange/ftx/rest_wallet_request.go @@ -0,0 +1,47 @@ +package ftx + +import ( + "context" + "encoding/json" + "fmt" +) + +type walletRequest struct { + *restRequest +} + +func (r *walletRequest) DepositHistory(ctx context.Context) (depositHistoryResponse, error) { + resp, err := r. + Method("GET"). + ReferenceURL("api/wallet/deposits"). + DoAuthenticatedRequest(ctx) + + if err != nil { + return depositHistoryResponse{}, err + } + + var d depositHistoryResponse + if err := json.Unmarshal(resp.Body, &d); err != nil { + return depositHistoryResponse{}, fmt.Errorf("failed to unmarshal deposit history response body to json: %w", err) + } + + return d, nil +} + +func (r *walletRequest) Balances(ctx context.Context) (balances, error) { + resp, err := r. + Method("GET"). + ReferenceURL("api/wallet/balances"). + DoAuthenticatedRequest(ctx) + + if err != nil { + return balances{}, err + } + + var b balances + if err := json.Unmarshal(resp.Body, &b); err != nil { + return balances{}, fmt.Errorf("failed to unmarshal balance response body to json: %w", err) + } + + return b, nil +} diff --git a/pkg/types/deposit.go b/pkg/types/deposit.go index 1f78c85e8..03a29724c 100644 --- a/pkg/types/deposit.go +++ b/pkg/types/deposit.go @@ -9,7 +9,7 @@ import ( type DepositStatus string const ( - DepositOther = DepositStatus("") + // EMPTY string means not supported DepositPending = DepositStatus("pending")