diff --git a/pkg/exchange/ftx/convert.go b/pkg/exchange/ftx/convert.go index 72f58bab4..7dd4afb97 100644 --- a/pkg/exchange/ftx/convert.go +++ b/pkg/exchange/ftx/convert.go @@ -129,3 +129,19 @@ func toGlobalTrade(f fill) (types.Trade, error) { IsIsolated: false, }, nil } + +func toGlobalKLine(symbol string, interval types.Interval, h Candle) (types.KLine, error) { + return types.KLine{ + Exchange: types.ExchangeFTX.String(), + Symbol: symbol, + StartTime: h.StartTime.Time, + EndTime: h.StartTime.Add(interval.Duration()), + Interval: interval, + Open: h.Open, + Close: h.Close, + High: h.High, + Low: h.Low, + Volume: h.Volume, + Closed: true, + }, nil +} diff --git a/pkg/exchange/ftx/exchange.go b/pkg/exchange/ftx/exchange.go index d71f670c5..aef70ee0b 100644 --- a/pkg/exchange/ftx/exchange.go +++ b/pkg/exchange/ftx/exchange.go @@ -142,7 +142,51 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, } func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - panic("implement me") + var since, until time.Time + if options.StartTime != nil { + since = *options.StartTime + } + if options.EndTime != nil { + until = *options.EndTime + } else { + until = time.Now() + } + if since.After(until) { + return nil, fmt.Errorf("invalid query klines time range, since: %+v, until: %+v", since, until) + } + if !isIntervalSupportedInKLine(interval) { + return nil, fmt.Errorf("interval %s is not supported", interval.String()) + } + resp, err := e.newRest().HistoricalPrices(ctx, symbol, interval, int64(options.Limit), since, until) + if err != nil { + return nil, err + } + if !resp.Success { + return nil, fmt.Errorf("ftx returns failure") + } + + var kline []types.KLine + for _, r := range resp.Result { + globalKline, err := toGlobalKLine(symbol, interval, r) + if err != nil { + return nil, err + } + kline = append(kline, globalKline) + } + return kline, nil +} + +func isIntervalSupportedInKLine(interval types.Interval) bool { + _, ok := map[int]struct{}{ + 15: {}, + 60: {}, + 300: {}, + 900: {}, + 3600: {}, + 14400: {}, + 86400: {}, + }[interval.Minutes()*60] + return ok } func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { @@ -247,10 +291,6 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, return } -func (e *Exchange) QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []types.Withdraw, err error) { - panic("implement me") -} - func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { var createdOrders types.OrderSlice // TODO: currently only support limit and market order diff --git a/pkg/exchange/ftx/exchange_test.go b/pkg/exchange/ftx/exchange_test.go index ddb5442b1..a79d393d3 100644 --- a/pkg/exchange/ftx/exchange_test.go +++ b/pkg/exchange/ftx/exchange_test.go @@ -620,3 +620,19 @@ func TestExchange_QueryTrades(t *testing.T) { }, trades[0]) }) } + +func Test_isIntervalSupportedInKLine(t *testing.T) { + supportedIntervals := []types.Interval{ + types.Interval1m, + types.Interval5m, + types.Interval15m, + types.Interval1h, + types.Interval4h, + types.Interval1d, + } + for _, i := range supportedIntervals { + assert.True(t, isIntervalSupportedInKLine(i)) + } + assert.False(t, isIntervalSupportedInKLine(types.Interval30m)) + assert.False(t, isIntervalSupportedInKLine(types.Interval3d)) +} diff --git a/pkg/exchange/ftx/rest_market_request.go b/pkg/exchange/ftx/rest_market_request.go index 9ac0479c3..d117f1403 100644 --- a/pkg/exchange/ftx/rest_market_request.go +++ b/pkg/exchange/ftx/rest_market_request.go @@ -4,6 +4,10 @@ import ( "context" "encoding/json" "fmt" + "strconv" + "time" + + "github.com/c9s/bbgo/pkg/types" ) type marketRequest struct { @@ -27,3 +31,41 @@ func (r *marketRequest) Markets(ctx context.Context) (marketsResponse, error) { return m, nil } + +/* +supported resolutions: window length in seconds. options: 15, 60, 300, 900, 3600, 14400, 86400 +doc: https://docs.ftx.com/?javascript#get-historical-prices +*/ +func (r *marketRequest) HistoricalPrices(ctx context.Context, market string, interval types.Interval, limit int64, start, end time.Time) (HistoricalPricesResponse, error) { + q := map[string]string{ + "resolution": strconv.FormatInt(int64(interval.Minutes())*60, 10), + } + + if limit > 0 { + q["limit"] = strconv.FormatInt(limit, 10) + } + + if start != (time.Time{}) { + q["start_time"] = strconv.FormatInt(start.Unix(), 10) + } + + if end != (time.Time{}) { + q["end_time"] = strconv.FormatInt(end.Unix(), 10) + } + + resp, err := r. + Method("GET"). + Query(q). + ReferenceURL(fmt.Sprintf("api/markets/%s/candles", market)). + DoAuthenticatedRequest(ctx) + + if err != nil { + return HistoricalPricesResponse{}, err + } + + var h HistoricalPricesResponse + if err := json.Unmarshal(resp.Body, &h); err != nil { + return HistoricalPricesResponse{}, fmt.Errorf("failed to unmarshal historical prices response body to json: %w", err) + } + return h, nil +} diff --git a/pkg/exchange/ftx/rest_responses.go b/pkg/exchange/ftx/rest_responses.go index 3ad33adde..0732fb16a 100644 --- a/pkg/exchange/ftx/rest_responses.go +++ b/pkg/exchange/ftx/rest_responses.go @@ -198,6 +198,35 @@ type market struct { VolumeUsd24h float64 `json:"volumeUsd24h"` } +/* +{ + "success": true, + "result": [ + { + "close": 11055.25, + "high": 11089.0, + "low": 11043.5, + "open": 11059.25, + "startTime": "2019-06-24T17:15:00+00:00", + "volume": 464193.95725 + } + ] +} +*/ +type HistoricalPricesResponse struct { + Success bool `json:"success"` + Result []Candle `json:"result"` +} + +type Candle struct { + Close float64 `json:"close"` + High float64 `json:"high"` + Low float64 `json:"low"` + Open float64 `json:"open"` + StartTime datetime `json:"startTime"` + Volume float64 `json:"volume"` +} + type ordersHistoryResponse struct { Success bool `json:"success"` Result []order `json:"result"`