ftx:support deposit histories

This commit is contained in:
ycdesu 2021-03-21 20:17:41 +08:00
parent f84b3a5177
commit ab743f85c2
9 changed files with 337 additions and 39 deletions

97
pkg/cmd/deposit.go Normal file
View File

@ -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
},
}

View File

@ -3,6 +3,9 @@ package ftx
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/datatype" "github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
@ -73,3 +76,35 @@ func toGlobalOrder(r order) (types.Order, error) {
return o, nil 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)
}

View File

@ -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) { 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) { 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. // 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. // 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) { func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) {
if since.After(until) { if err := verifySinceUntil(since, until); err != nil {
return nil, fmt.Errorf("since can't be after until") return nil, err
} }
if lastOrderID > 0 { if lastOrderID > 0 {
logger.Warn("FTX doesn't support lastOrderID") 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) { func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
panic("implement me") 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
}

View File

@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/datatype"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -116,7 +117,7 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL 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.NoError(t, err)
assert.Len(t, resp, 0) assert.Len(t, resp, 0)
@ -157,7 +158,7 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL 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.NoError(t, err)
assert.Len(t, resp, 1) assert.Len(t, resp, 1)
assert.Equal(t, "BTC-PERP", resp[0].Symbol) assert.Equal(t, "BTC-PERP", resp[0].Symbol)
@ -201,7 +202,7 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL 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.NoError(t, err)
assert.Len(t, resp, 3) assert.Len(t, resp, 3)
@ -272,7 +273,7 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL 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.NoError(t, err)
assert.Len(t, resp, 2) assert.Len(t, resp, 2)
expectedOrderID := []uint64{123, 456} expectedOrderID := []uint64{123, 456}
@ -423,3 +424,63 @@ func TestExchange_QueryMarkets(t *testing.T) {
TickSize: 1, TickSize: 1,
}, resp["BTC/USD"]) }, 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)
}

View File

@ -19,7 +19,7 @@ import (
) )
type restRequest struct { type restRequest struct {
*balanceRequest *walletRequest
*orderRequest *orderRequest
*accountRequest *accountRequest
*marketRequest *marketRequest
@ -46,7 +46,7 @@ func newRestRequest(c *http.Client, baseURL *url.URL) *restRequest {
r.marketRequest = &marketRequest{restRequest: r} r.marketRequest = &marketRequest{restRequest: r}
r.accountRequest = &accountRequest{restRequest: r} r.accountRequest = &accountRequest{restRequest: r}
r.balanceRequest = &balanceRequest{restRequest: r} r.walletRequest = &walletRequest{restRequest: r}
r.orderRequest = &orderRequest{restRequest: r} r.orderRequest = &orderRequest{restRequest: r}
return r return r
} }

View File

@ -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
}

View File

@ -200,3 +200,57 @@ type orderResponse struct {
Result order `json:"result"` 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"`
}

View File

@ -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
}

View File

@ -9,7 +9,7 @@ import (
type DepositStatus string type DepositStatus string
const ( const (
DepositOther = DepositStatus("") // EMPTY string means not supported
DepositPending = DepositStatus("pending") DepositPending = DepositStatus("pending")