diff --git a/pkg/cmd/account.go b/pkg/cmd/account.go index 41ed9465d..18d5ff6f2 100644 --- a/pkg/cmd/account.go +++ b/pkg/cmd/account.go @@ -19,7 +19,7 @@ func init() { RootCmd.AddCommand(accountCmd) } -// go run ./cmd/bbgo account --session=ftx --config=config/bbgo.yaml +// go run ./cmd/bbgo account --session=binance --config=config/bbgo.yaml var accountCmd = &cobra.Command{ Use: "account [--session SESSION]", Short: "show user account details (ex: balance)", diff --git a/pkg/cmd/balances.go b/pkg/cmd/balances.go index 492b49d64..9298b409f 100644 --- a/pkg/cmd/balances.go +++ b/pkg/cmd/balances.go @@ -15,7 +15,7 @@ func init() { RootCmd.AddCommand(balancesCmd) } -// go run ./cmd/bbgo balances --session=ftx +// go run ./cmd/bbgo balances --session=binance var balancesCmd = &cobra.Command{ Use: "balances [--session SESSION]", Short: "Show user account balances", diff --git a/pkg/cmd/kline.go b/pkg/cmd/kline.go index b35abf05f..e51b822bb 100644 --- a/pkg/cmd/kline.go +++ b/pkg/cmd/kline.go @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// go run ./cmd/bbgo kline --exchange=ftx --symbol=BTCUSDT +// go run ./cmd/bbgo kline --exchange=binance --symbol=BTCUSDT var klineCmd = &cobra.Command{ Use: "kline", Short: "connect to the kline market data streaming service of an exchange", diff --git a/pkg/cmd/market.go b/pkg/cmd/market.go index 794f1bba6..1ff22a25c 100644 --- a/pkg/cmd/market.go +++ b/pkg/cmd/market.go @@ -17,7 +17,7 @@ func init() { RootCmd.AddCommand(marketCmd) } -// go run ./cmd/bbgo market --session=ftx --config=config/bbgo.yaml +// go run ./cmd/bbgo market --session=binance --config=config/bbgo.yaml var marketCmd = &cobra.Command{ Use: "market", Short: "List the symbols that the are available to be traded in the exchange", diff --git a/pkg/cmd/orderbook.go b/pkg/cmd/orderbook.go index bfe932344..9eba051d6 100644 --- a/pkg/cmd/orderbook.go +++ b/pkg/cmd/orderbook.go @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// go run ./cmd/bbgo orderbook --session=ftx --symbol=BTCUSDT +// go run ./cmd/bbgo orderbook --session=binance --symbol=BTCUSDT var orderbookCmd = &cobra.Command{ Use: "orderbook --session=[exchange_name] --symbol=[pair_name]", Short: "connect to the order book market data streaming service of an exchange", diff --git a/pkg/cmd/trades.go b/pkg/cmd/trades.go index 7b9308605..7472fe2a4 100644 --- a/pkg/cmd/trades.go +++ b/pkg/cmd/trades.go @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// go run ./cmd/bbgo trades --session=ftx --symbol="BTC/USD" +// go run ./cmd/bbgo trades --session=binance --symbol="BTC/USD" var tradesCmd = &cobra.Command{ Use: "trades --session=[exchange_name] --symbol=[pair_name]", Short: "Query trading history", diff --git a/pkg/cmd/userdatastream.go b/pkg/cmd/userdatastream.go index b53a1a631..29c5d9148 100644 --- a/pkg/cmd/userdatastream.go +++ b/pkg/cmd/userdatastream.go @@ -14,7 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -// go run ./cmd/bbgo userdatastream --session=ftx +// go run ./cmd/bbgo userdatastream --session=binance var userDataStreamCmd = &cobra.Command{ Use: "userdatastream", Short: "Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)", diff --git a/pkg/cmd/utils.go b/pkg/cmd/utils.go index dda83d2ff..eac567af2 100644 --- a/pkg/cmd/utils.go +++ b/pkg/cmd/utils.go @@ -1,14 +1,9 @@ package cmd import ( - "fmt" - - "github.com/spf13/viper" - log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "github.com/c9s/bbgo/pkg/exchange/ftx" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -36,16 +31,3 @@ func inBaseAsset(balances types.BalanceMap, market types.Market, price fixedpoin base := balances[market.BaseCurrency] return quote.Total().Div(price).Add(base.Total()) } - -func newExchange(session string) (types.Exchange, error) { - switch session { - case "ftx": - return ftx.NewExchange( - viper.GetString("ftx-api-key"), - viper.GetString("ftx-api-secret"), - viper.GetString("ftx-subaccount"), - ), nil - - } - return nil, fmt.Errorf("unsupported session %s", session) -} diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index d03f8654e..436b03b5b 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/c9s/bbgo/pkg/exchange/binance" - "github.com/c9s/bbgo/pkg/exchange/ftx" "github.com/c9s/bbgo/pkg/exchange/kucoin" "github.com/c9s/bbgo/pkg/exchange/max" "github.com/c9s/bbgo/pkg/exchange/okex" @@ -20,9 +19,6 @@ func NewPublic(exchangeName types.ExchangeName) (types.Exchange, error) { func NewStandard(n types.ExchangeName, key, secret, passphrase, subAccount string) (types.Exchange, error) { switch n { - case types.ExchangeFTX: - return ftx.NewExchange(key, secret, subAccount), nil - case types.ExchangeBinance: return binance.New(key, secret), nil diff --git a/pkg/exchange/ftx/convert.go b/pkg/exchange/ftx/convert.go deleted file mode 100644 index f40bc73bb..000000000 --- a/pkg/exchange/ftx/convert.go +++ /dev/null @@ -1,249 +0,0 @@ -package ftx - -import ( - "fmt" - "strings" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/types" -) - -func toGlobalCurrency(original string) string { - return TrimUpperString(original) -} - -func toGlobalSymbol(original string) string { - return strings.ReplaceAll(TrimUpperString(original), "/", "") -} - -func toLocalSymbol(original string) string { - if symbolMap[original] == "" { - return original - } - - return symbolMap[original] -} - -func TrimUpperString(original string) string { - return strings.ToUpper(strings.TrimSpace(original)) -} - -func TrimLowerString(original string) string { - return strings.ToLower(strings.TrimSpace(original)) -} - -var errUnsupportedOrderStatus = fmt.Errorf("unsupported order status") - -func toGlobalOrderNew(r ftxapi.Order) (types.Order, error) { - // In exchange/max/convert.go, it only parses these fields. - timeInForce := types.TimeInForceGTC - if r.Ioc { - timeInForce = types.TimeInForceIOC - } - - // order type definition: https://github.com/ftexchange/ftx/blob/master/rest/client.py#L122 - orderType := types.OrderType(TrimUpperString(string(r.Type))) - if orderType == types.OrderTypeLimit && r.PostOnly { - orderType = types.OrderTypeLimitMaker - } - - o := types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: r.ClientId, - Symbol: toGlobalSymbol(r.Market), - Side: types.SideType(TrimUpperString(string(r.Side))), - Type: orderType, - Quantity: r.Size, - Price: r.Price, - TimeInForce: timeInForce, - }, - Exchange: types.ExchangeFTX, - IsWorking: r.Status == ftxapi.OrderStatusOpen || r.Status == ftxapi.OrderStatusNew, - OrderID: uint64(r.Id), - Status: "", - ExecutedQuantity: r.FilledSize, - CreationTime: types.Time(r.CreatedAt), - UpdateTime: types.Time(r.CreatedAt), - } - - s, err := toGlobalOrderStatus(r, r.Status) - o.Status = s - return o, err -} - -func toGlobalOrderStatus(o ftxapi.Order, s ftxapi.OrderStatus) (types.OrderStatus, error) { - switch s { - case ftxapi.OrderStatusNew: - return types.OrderStatusNew, nil - - case ftxapi.OrderStatusOpen: - if !o.FilledSize.IsZero() { - return types.OrderStatusPartiallyFilled, nil - } else { - return types.OrderStatusNew, nil - } - case ftxapi.OrderStatusClosed: - // filled or canceled - if o.FilledSize == o.Size { - return types.OrderStatusFilled, nil - } else { - // can't distinguish it's canceled or rejected from order response, so always set to canceled - return types.OrderStatusCanceled, nil - } - } - - return "", fmt.Errorf("unsupported ftx order status %s: %w", s, errUnsupportedOrderStatus) -} - -func toGlobalOrder(r order) (types.Order, error) { - // In exchange/max/convert.go, it only parses these fields. - timeInForce := types.TimeInForceGTC - if r.Ioc { - timeInForce = types.TimeInForceIOC - } - - // order type definition: https://github.com/ftexchange/ftx/blob/master/rest/client.py#L122 - orderType := types.OrderType(TrimUpperString(r.Type)) - if orderType == types.OrderTypeLimit && r.PostOnly { - orderType = types.OrderTypeLimitMaker - } - - o := types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: r.ClientId, - Symbol: toGlobalSymbol(r.Market), - Side: types.SideType(TrimUpperString(r.Side)), - Type: orderType, - Quantity: r.Size, - Price: r.Price, - TimeInForce: timeInForce, - }, - Exchange: types.ExchangeFTX, - IsWorking: r.Status == "open", - OrderID: uint64(r.ID), - Status: "", - ExecutedQuantity: r.FilledSize, - CreationTime: types.Time(r.CreatedAt.Time), - UpdateTime: types.Time(r.CreatedAt.Time), - } - - // `new` (accepted but not processed yet), `open`, or `closed` (filled or cancelled) - switch r.Status { - case "new": - o.Status = types.OrderStatusNew - case "open": - if !o.ExecutedQuantity.IsZero() { - o.Status = types.OrderStatusPartiallyFilled - } else { - o.Status = types.OrderStatusNew - } - case "closed": - // filled or canceled - if o.Quantity == o.ExecutedQuantity { - o.Status = types.OrderStatusFilled - } else { - // can't distinguish it's canceled or rejected from order response, so always set to canceled - o.Status = types.OrderStatusCanceled - } - default: - return types.Order{}, fmt.Errorf("unsupported status %s: %w", r.Status, errUnsupportedOrderStatus) - } - - 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.Time{}) { - t = input.ConfirmedTime - } - d := types.Deposit{ - GID: 0, - Exchange: types.ExchangeFTX, - Time: types.Time(t.Time), - 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) -} - -func toGlobalTrade(f ftxapi.Fill) (types.Trade, error) { - return types.Trade{ - ID: f.TradeId, - OrderID: f.OrderId, - Exchange: types.ExchangeFTX, - Price: f.Price, - Quantity: f.Size, - QuoteQuantity: f.Price.Mul(f.Size), - Symbol: toGlobalSymbol(f.Market), - Side: types.SideType(strings.ToUpper(string(f.Side))), - IsBuyer: f.Side == ftxapi.SideBuy, - IsMaker: f.Liquidity == ftxapi.LiquidityMaker, - Time: types.Time(f.Time), - Fee: f.Fee, - FeeCurrency: f.FeeCurrency, - IsMargin: false, - IsIsolated: false, - IsFutures: f.Future != "", - }, nil -} - -func toGlobalKLine(symbol string, interval types.Interval, h Candle) (types.KLine, error) { - return types.KLine{ - Exchange: types.ExchangeFTX, - Symbol: toGlobalSymbol(symbol), - StartTime: types.Time(h.StartTime.Time), - EndTime: types.Time(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 -} - -type OrderType string - -const ( - OrderTypeLimit OrderType = "limit" - OrderTypeMarket OrderType = "market" -) - -func toLocalOrderType(orderType types.OrderType) (ftxapi.OrderType, error) { - switch orderType { - - case types.OrderTypeLimitMaker: - return ftxapi.OrderTypeLimit, nil - - case types.OrderTypeLimit: - return ftxapi.OrderTypeLimit, nil - - case types.OrderTypeMarket: - return ftxapi.OrderTypeMarket, nil - - } - - return "", fmt.Errorf("order type %s not supported", orderType) -} diff --git a/pkg/exchange/ftx/convert_test.go b/pkg/exchange/ftx/convert_test.go deleted file mode 100644 index 3a1ea7f1e..000000000 --- a/pkg/exchange/ftx/convert_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package ftx - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/types" -) - -func Test_toGlobalOrderFromOpenOrder(t *testing.T) { - input := ` -{ - "createdAt": "2019-03-05T09:56:55.728933+00:00", - "filledSize": 10, - "future": "XRP-PERP", - "id": 9596912, - "market": "XRP-PERP", - "price": 0.306525, - "avgFillPrice": 0.306526, - "remainingSize": 31421, - "side": "sell", - "size": 31431, - "status": "open", - "type": "limit", - "reduceOnly": false, - "ioc": false, - "postOnly": false, - "clientId": "client-id-123" -} -` - - var r order - assert.NoError(t, json.Unmarshal([]byte(input), &r)) - - o, err := toGlobalOrder(r) - assert.NoError(t, err) - assert.Equal(t, "client-id-123", o.ClientOrderID) - assert.Equal(t, "XRP-PERP", o.Symbol) - assert.Equal(t, types.SideTypeSell, o.Side) - assert.Equal(t, types.OrderTypeLimit, o.Type) - assert.Equal(t, "31431", o.Quantity.String()) - assert.Equal(t, "0.306525", o.Price.String()) - assert.Equal(t, types.TimeInForceGTC, o.TimeInForce) - assert.Equal(t, types.ExchangeFTX, o.Exchange) - assert.True(t, o.IsWorking) - assert.Equal(t, uint64(9596912), o.OrderID) - assert.Equal(t, types.OrderStatusPartiallyFilled, o.Status) - assert.Equal(t, "10", o.ExecutedQuantity.String()) -} - -func TestTrimLowerString(t *testing.T) { - type args struct { - original string - } - tests := []struct { - name string - args args - want string - }{ - { - name: "spaces", - args: args{ - original: " ", - }, - want: "", - }, - { - name: "uppercase", - args: args{ - original: " HELLO ", - }, - want: "hello", - }, - { - name: "lowercase", - args: args{ - original: " hello", - }, - want: "hello", - }, - { - name: "upper/lower cases", - args: args{ - original: " heLLo ", - }, - want: "hello", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := TrimLowerString(tt.args.original); got != tt.want { - t.Errorf("TrimLowerString() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_toGlobalSymbol(t *testing.T) { - assert.Equal(t, "BTCUSDT", toGlobalSymbol("BTC/USDT")) -} - -func Test_toLocalOrderTypeWithLimitMaker(t *testing.T) { - orderType, err := toLocalOrderType(types.OrderTypeLimitMaker) - assert.NoError(t, err) - assert.Equal(t, ftxapi.OrderTypeLimit, orderType) -} - -func Test_toLocalOrderTypeWithLimit(t *testing.T) { - orderType, err := toLocalOrderType(types.OrderTypeLimit) - assert.NoError(t, err) - assert.Equal(t, ftxapi.OrderTypeLimit, orderType) -} - -func Test_toLocalOrderTypeWithMarket(t *testing.T) { - orderType, err := toLocalOrderType(types.OrderTypeMarket) - assert.NoError(t, err) - assert.Equal(t, ftxapi.OrderTypeMarket, orderType) -} diff --git a/pkg/exchange/ftx/exchange.go b/pkg/exchange/ftx/exchange.go deleted file mode 100644 index d5b329491..000000000 --- a/pkg/exchange/ftx/exchange.go +++ /dev/null @@ -1,637 +0,0 @@ -package ftx - -import ( - "context" - "fmt" - "net/http" - "net/url" - "sort" - "strconv" - "strings" - "time" - - "golang.org/x/time/rate" - - "github.com/google/uuid" - "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -const ( - restEndpoint = "https://ftx.com" - defaultHTTPTimeout = 15 * time.Second -) - -var logger = logrus.WithField("exchange", "ftx") - -// POST https://ftx.com/api/orders 429, Success: false, err: Do not send more than 2 orders on this market per 200ms -var requestLimit = rate.NewLimiter(rate.Every(220*time.Millisecond), 2) - -var marketDataLimiter = rate.NewLimiter(rate.Every(500*time.Millisecond), 2) - -//go:generate go run generate_symbol_map.go - -type Exchange struct { - client *ftxapi.RestClient - - key, secret string - subAccount string - restEndpoint *url.URL - orderAmountReduceFactor fixedpoint.Value -} - -type MarketTicker struct { - Market types.Market - Price fixedpoint.Value - Ask fixedpoint.Value - Bid fixedpoint.Value - Last fixedpoint.Value -} - -type MarketMap map[string]MarketTicker - -// FTX does not have broker ID -const spotBrokerID = "BBGO" - -func newSpotClientOrderID(originalID string) (clientOrderID string) { - prefix := "x-" + spotBrokerID - prefixLen := len(prefix) - - if originalID != "" { - // try to keep the whole original client order ID if user specifies it. - if prefixLen+len(originalID) > 32 { - return originalID - } - - clientOrderID = prefix + originalID - return clientOrderID - } - - clientOrderID = uuid.New().String() - clientOrderID = prefix + clientOrderID - if len(clientOrderID) > 32 { - return clientOrderID[0:32] - } - - return clientOrderID -} - -func NewExchange(key, secret string, subAccount string) *Exchange { - u, err := url.Parse(restEndpoint) - if err != nil { - panic(err) - } - - client := ftxapi.NewClient() - client.Auth(key, secret, subAccount) - return &Exchange{ - client: client, - restEndpoint: u, - key: key, - // pragma: allowlist nextline secret - secret: secret, - subAccount: subAccount, - orderAmountReduceFactor: fixedpoint.One, - } -} - -func (e *Exchange) newRest() *restRequest { - r := newRestRequest(&http.Client{Timeout: defaultHTTPTimeout}, e.restEndpoint).Auth(e.key, e.secret) - if len(e.subAccount) > 0 { - r.SubAccount(e.subAccount) - } - return r -} - -func (e *Exchange) Name() types.ExchangeName { - return types.ExchangeFTX -} - -func (e *Exchange) PlatformFeeCurrency() string { - return toGlobalCurrency("FTT") -} - -func (e *Exchange) NewStream() types.Stream { - return NewStream(e.key, e.secret, e.subAccount, e) -} - -func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { - markets, err := e._queryMarkets(ctx) - if err != nil { - return nil, err - } - marketMap := types.MarketMap{} - for k, v := range markets { - marketMap[k] = v.Market - } - return marketMap, nil -} - -func (e *Exchange) _queryMarkets(ctx context.Context) (MarketMap, error) { - req := e.client.NewGetMarketsRequest() - ftxMarkets, err := req.Do(ctx) - if err != nil { - return nil, err - } - - markets := MarketMap{} - for _, m := range ftxMarkets { - symbol := toGlobalSymbol(m.Name) - symbolMap[symbol] = m.Name - - mkt2 := MarketTicker{ - Market: types.Market{ - Symbol: symbol, - LocalSymbol: m.Name, - // The max precision is length(DefaultPow). For example, currently fixedpoint.DefaultPow - // is 1e8, so the max precision will be 8. - PricePrecision: m.PriceIncrement.NumFractionalDigits(), - VolumePrecision: m.SizeIncrement.NumFractionalDigits(), - QuoteCurrency: toGlobalCurrency(m.QuoteCurrency), - BaseCurrency: toGlobalCurrency(m.BaseCurrency), - // FTX only limit your order by `MinProvideSize`, so I assign zero value to unsupported fields: - // MinNotional, MinAmount, MaxQuantity, MinPrice and MaxPrice. - MinNotional: fixedpoint.Zero, - MinAmount: fixedpoint.Zero, - MinQuantity: m.MinProvideSize, - MaxQuantity: fixedpoint.Zero, - StepSize: m.SizeIncrement, - MinPrice: fixedpoint.Zero, - MaxPrice: fixedpoint.Zero, - TickSize: m.PriceIncrement, - }, - Price: m.Price, - Bid: m.Bid, - Ask: m.Ask, - Last: m.Last, - } - markets[symbol] = mkt2 - } - return markets, nil -} - -func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { - - req := e.client.NewGetAccountRequest() - ftxAccount, err := req.Do(ctx) - if err != nil { - return nil, err - } - - a := &types.Account{ - TotalAccountValue: ftxAccount.TotalAccountValue, - } - - balances, err := e.QueryAccountBalances(ctx) - if err != nil { - return nil, err - } - - a.UpdateBalances(balances) - return a, nil -} - -func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { - balanceReq := e.client.NewGetBalancesRequest() - ftxBalances, err := balanceReq.Do(ctx) - if err != nil { - return nil, err - } - - var balances = make(types.BalanceMap) - for _, r := range ftxBalances { - currency := toGlobalCurrency(r.Coin) - balances[currency] = types.Balance{ - Currency: currency, - Available: r.Free, - Locked: r.Total.Sub(r.Free), - } - } - - return balances, nil -} - -// DefaultFeeRates returns the FTX Tier 1 fee -// See also https://help.ftx.com/hc/en-us/articles/360024479432-Fees -func (e *Exchange) DefaultFeeRates() types.ExchangeFee { - return types.ExchangeFee{ - MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.020), // 0.020% - TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.070), // 0.070% - } -} - -// SetModifyOrderAmountForFee protects the limit buy orders by reducing amount with taker fee. -// The amount is recalculated before submit: submit_amount = original_amount / (1 + taker_fee_rate) . -// This prevents balance exceeding error while closing position without spot margin enabled. -func (e *Exchange) SetModifyOrderAmountForFee(feeRate types.ExchangeFee) { - e.orderAmountReduceFactor = fixedpoint.One.Add(feeRate.TakerFeeRate) -} - -// resolution field in api -// window length in seconds. options: 15, 60, 300, 900, 3600, 14400, 86400, or any multiple of 86400 up to 30*86400 -var supportedIntervals = map[types.Interval]int{ - types.Interval1m: 1 * 60, - types.Interval5m: 5 * 60, - types.Interval15m: 15 * 60, - types.Interval1h: 60 * 60, - types.Interval4h: 60 * 60 * 4, - types.Interval1d: 60 * 60 * 24, - types.Interval3d: 60 * 60 * 24 * 3, -} - -func (e *Exchange) SupportedInterval() map[types.Interval]int { - return supportedIntervals -} - -func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { - return isIntervalSupportedInKLine(interval) -} - -func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - var klines []types.KLine - - // the fetch result is from newest to oldest - // currentEnd = until - // endTime := currentEnd.Add(interval.Duration()) - klines, err := e._queryKLines(ctx, symbol, interval, options) - if err != nil { - return nil, err - } - - klines = types.SortKLinesAscending(klines) - return klines, nil -} - -func (e *Exchange) _queryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { - if !isIntervalSupportedInKLine(interval) { - return nil, fmt.Errorf("interval %s is not supported", interval.String()) - } - - if err := marketDataLimiter.Wait(ctx); err != nil { - return nil, err - } - - // assign limit to a default value since ftx has the limit - if options.Limit == 0 { - options.Limit = 500 - } - - // if the time range exceed the ftx valid time range, we need to adjust the endTime - if options.StartTime != nil && options.EndTime != nil { - rangeDuration := options.EndTime.Sub(*options.StartTime) - estimatedCount := rangeDuration / interval.Duration() - - if options.Limit != 0 && uint64(estimatedCount) > uint64(options.Limit) { - endTime := options.StartTime.Add(interval.Duration() * time.Duration(options.Limit)) - options.EndTime = &endTime - } - } - - resp, err := e.newRest().marketRequest.HistoricalPrices(ctx, toLocalSymbol(symbol), interval, int64(options.Limit), options.StartTime, options.EndTime) - if err != nil { - return nil, err - } - if !resp.Success { - return nil, fmt.Errorf("ftx returns failure") - } - - var klines []types.KLine - for _, r := range resp.Result { - globalKline, err := toGlobalKLine(symbol, interval, r) - if err != nil { - return nil, err - } - klines = append(klines, globalKline) - } - - return klines, nil -} - -func isIntervalSupportedInKLine(interval types.Interval) bool { - _, ok := supportedIntervals[interval] - return ok -} - -func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { - tradeIDs := make(map[uint64]struct{}) - lastTradeID := options.LastTradeID - - req := e.client.NewGetFillsRequest() - req.Market(toLocalSymbol(symbol)) - - if options.StartTime != nil { - req.StartTime(*options.StartTime) - } else if options.EndTime != nil { - req.EndTime(*options.EndTime) - } - - req.Order("asc") - fills, err := req.Do(ctx) - if err != nil { - return nil, err - } - - sort.Slice(fills, func(i, j int) bool { - return fills[i].Time.Before(fills[j].Time) - }) - - var trades []types.Trade - symbol = strings.ToUpper(symbol) - for _, fill := range fills { - if _, ok := tradeIDs[fill.TradeId]; ok { - continue - } - - if options.StartTime != nil && fill.Time.Before(*options.StartTime) { - continue - } - - if options.EndTime != nil && fill.Time.After(*options.EndTime) { - continue - } - - if fill.TradeId <= lastTradeID { - continue - } - - tradeIDs[fill.TradeId] = struct{}{} - lastTradeID = fill.TradeId - - t, err := toGlobalTrade(fill) - if err != nil { - return nil, err - } - trades = append(trades, t) - } - - return trades, nil -} - -func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, until time.Time) (allDeposits []types.Deposit, err error) { - if until == (time.Time{}) { - until = time.Now() - } - if since.After(until) { - return nil, fmt.Errorf("invalid query deposit history time range, since: %+v, until: %+v", since, until) - } - asset = TrimUpperString(asset) - - resp, err := e.newRest().DepositHistory(ctx, since, until, 0) - if err != nil { - return nil, err - } - if !resp.Success { - return nil, fmt.Errorf("ftx returns failure") - } - sort.Slice(resp.Result, func(i, j int) bool { - return resp.Result[i].Time.Before(resp.Result[j].Time.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) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { - // TODO: currently only support limit and market order - // TODO: support time in force - so := order - if err := requestLimit.Wait(ctx); err != nil { - logrus.WithError(err).Error("rate limit error") - } - - orderType, err := toLocalOrderType(so.Type) - if err != nil { - logrus.WithError(err).Error("type error") - } - - submitQuantity := so.Quantity - switch orderType { - case ftxapi.OrderTypeLimit, ftxapi.OrderTypeStopLimit: - submitQuantity = so.Quantity.Div(e.orderAmountReduceFactor) - } - - req := e.client.NewPlaceOrderRequest() - req.Market(toLocalSymbol(TrimUpperString(so.Symbol))) - req.OrderType(orderType) - req.Side(ftxapi.Side(TrimLowerString(string(so.Side)))) - req.Size(submitQuantity) - - switch so.Type { - case types.OrderTypeLimit, types.OrderTypeLimitMaker: - req.Price(so.Price) - - } - - if so.Type == types.OrderTypeLimitMaker { - req.PostOnly(true) - } - - if so.TimeInForce == types.TimeInForceIOC { - req.Ioc(true) - } - - req.ClientID(newSpotClientOrderID(so.ClientOrderID)) - - or, err := req.Do(ctx) - if err != nil { - return nil, fmt.Errorf("failed to place order %+v: %w", so, err) - } - - globalOrder, err := toGlobalOrderNew(*or) - return &globalOrder, err -} - -func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { - orderID, err := strconv.ParseInt(q.OrderID, 10, 64) - if err != nil { - return nil, err - } - - req := e.client.NewGetOrderStatusRequest(uint64(orderID)) - ftxOrder, err := req.Do(ctx) - if err != nil { - return nil, err - } - - o, err := toGlobalOrderNew(*ftxOrder) - if err != nil { - return nil, err - } - - return &o, err -} - -func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { - // TODO: invoke open trigger orders - - req := e.client.NewGetOpenOrdersRequest(toLocalSymbol(symbol)) - ftxOrders, err := req.Do(ctx) - if err != nil { - return nil, err - } - - for _, ftxOrder := range ftxOrders { - o, err := toGlobalOrderNew(ftxOrder) - if err != nil { - return orders, err - } - - orders = append(orders, o) - } - return orders, nil -} - -// 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) { - symbol = TrimUpperString(symbol) - - req := e.client.NewGetOrderHistoryRequest(toLocalSymbol(symbol)) - - if since != (time.Time{}) { - req.StartTime(since) - } else if until != (time.Time{}) { - req.EndTime(until) - } - - ftxOrders, err := req.Do(ctx) - if err != nil { - return nil, err - } - - sort.Slice(ftxOrders, func(i, j int) bool { - return ftxOrders[i].CreatedAt.Before(ftxOrders[j].CreatedAt) - }) - - for _, ftxOrder := range ftxOrders { - switch ftxOrder.Status { - case ftxapi.OrderStatusOpen, ftxapi.OrderStatusNew: - continue - } - - o, err := toGlobalOrderNew(ftxOrder) - if err != nil { - return orders, err - } - - orders = append(orders, o) - } - return orders, nil -} - -func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error { - for _, o := range orders { - if err := requestLimit.Wait(ctx); err != nil { - logrus.WithError(err).Error("rate limit error") - } - - var resp *ftxapi.APIResponse - var err error - if len(o.ClientOrderID) > 0 { - req := e.client.NewCancelOrderByClientOrderIdRequest(o.ClientOrderID) - resp, err = req.Do(ctx) - } else { - req := e.client.NewCancelOrderRequest(strconv.FormatUint(o.OrderID, 10)) - resp, err = req.Do(ctx) - } - - if err != nil { - return err - } - - if !resp.Success { - return fmt.Errorf("cancel order failed: %s", resp.Result) - } - } - return nil -} - -func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { - ticketMap, err := e.QueryTickers(ctx, symbol) - if err != nil { - return nil, err - } - - if ticker, ok := ticketMap[symbol]; ok { - return &ticker, nil - } - return nil, fmt.Errorf("ticker %s not found", symbol) -} - -func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { - var tickers = make(map[string]types.Ticker) - - markets, err := e._queryMarkets(ctx) - if err != nil { - return nil, err - } - - m := make(map[string]struct{}) - for _, s := range symbol { - m[toGlobalSymbol(s)] = struct{}{} - } - - rest := e.newRest() - for k, v := range markets { - - // if we provide symbol as condition then we only query the gieven symbol , - // or we should query "ALL" symbol in the market. - if _, ok := m[toGlobalSymbol(k)]; len(symbol) != 0 && !ok { - continue - } - - if err := requestLimit.Wait(ctx); err != nil { - logrus.WithError(err).Errorf("order rate limiter wait error") - } - - // ctx context.Context, market string, interval types.Interval, limit int64, start, end time.Time - now := time.Now() - since := now.Add(time.Duration(-1) * time.Hour) - until := now - prices, err := rest.marketRequest.HistoricalPrices(ctx, v.Market.LocalSymbol, types.Interval1h, 1, &since, &until) - if err != nil || !prices.Success || len(prices.Result) == 0 { - continue - } - - lastCandle := prices.Result[0] - tickers[toGlobalSymbol(k)] = types.Ticker{ - Time: lastCandle.StartTime.Time, - Volume: lastCandle.Volume, - Last: v.Last, - Open: lastCandle.Open, - High: lastCandle.High, - Low: lastCandle.Low, - Buy: v.Bid, - Sell: v.Ask, - } - } - - return tickers, nil -} - -func (e *Exchange) Transfer(ctx context.Context, coin string, size float64, destination string) (string, error) { - payload := TransferPayload{ - Coin: coin, - Size: size, - Source: e.subAccount, - Destination: destination, - } - resp, err := e.newRest().Transfer(ctx, payload) - if err != nil { - return "", err - } - if !resp.Success { - return "", fmt.Errorf("ftx returns transfer failure") - } - return resp.Result.String(), nil -} diff --git a/pkg/exchange/ftx/exchange_test.go b/pkg/exchange/ftx/exchange_test.go deleted file mode 100644 index 1f4f46ebd..000000000 --- a/pkg/exchange/ftx/exchange_test.go +++ /dev/null @@ -1,612 +0,0 @@ -package ftx - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -func integrationTestConfigured() (key, secret string, ok bool) { - var hasKey, hasSecret bool - key, hasKey = os.LookupEnv("FTX_API_KEY") - secret, hasSecret = os.LookupEnv("FTX_API_SECRET") - ok = hasKey && hasSecret && os.Getenv("TEST_FTX") == "1" - return key, secret, ok -} - -func TestExchange_IOCOrder(t *testing.T) { - key, secret, ok := integrationTestConfigured() - if !ok { - t.SkipNow() - return - } - - ex := NewExchange(key, secret, "") - createdOrder, err := ex.SubmitOrder(context.Background(), types.SubmitOrder{ - Symbol: "LTCUSDT", - Side: types.SideTypeBuy, - Type: types.OrderTypeLimitMaker, - Quantity: fixedpoint.NewFromFloat(1.0), - Price: fixedpoint.NewFromFloat(50.0), - Market: types.Market{ - Symbol: "LTCUSDT", - LocalSymbol: "LTC/USDT", - PricePrecision: 3, - VolumePrecision: 2, - QuoteCurrency: "USDT", - BaseCurrency: "LTC", - MinQuantity: fixedpoint.NewFromFloat(0.01), - StepSize: fixedpoint.NewFromFloat(0.01), - TickSize: fixedpoint.NewFromFloat(0.01), - }, - TimeInForce: "IOC", - }) - assert.NoError(t, err) - assert.NotEmpty(t, createdOrder) - t.Logf("created orders: %+v", createdOrder) -} - -func TestExchange_QueryAccountBalances(t *testing.T) { - successResp := ` -{ - "result": [ - { - "availableWithoutBorrow": 19.47458865, - "coin": "USD", - "free": 19.48085209, - "spotBorrow": 0.0, - "total": 1094.66405065, - "usdValue": 1094.664050651561 - } - ], - "success": true -} -` - failureResp := `{"result":[],"success":false}` - i := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if i == 0 { - fmt.Fprintln(w, successResp) - i++ - return - } - fmt.Fprintln(w, failureResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryAccountBalances(context.Background()) - assert.NoError(t, err) - - assert.Len(t, resp, 1) - b, ok := resp["USD"] - assert.True(t, ok) - expectedAvailable := fixedpoint.Must(fixedpoint.NewFromString("19.48085209")) - assert.Equal(t, expectedAvailable, b.Available) - assert.Equal(t, fixedpoint.Must(fixedpoint.NewFromString("1094.66405065")).Sub(expectedAvailable), b.Locked) -} - -func TestExchange_QueryOpenOrders(t *testing.T) { - successResp := ` -{ - "success": true, - "result": [ - { - "createdAt": "2019-03-05T09:56:55.728933+00:00", - "filledSize": 10, - "future": "XRP-PERP", - "id": 9596912, - "market": "XRP-PERP", - "price": 0.306525, - "avgFillPrice": 0.306526, - "remainingSize": 31421, - "side": "sell", - "size": 31431, - "status": "open", - "type": "limit", - "reduceOnly": false, - "ioc": false, - "postOnly": false, - "clientId": null - } - ] -} -` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, successResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryOpenOrders(context.Background(), "XRP-PREP") - assert.NoError(t, err) - assert.Len(t, resp, 1) - assert.Equal(t, "XRP-PERP", resp[0].Symbol) -} - -func TestExchange_QueryClosedOrders(t *testing.T) { - t.Run("no closed orders", func(t *testing.T) { - successResp := `{"success": true, "result": []}` - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, successResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) - assert.NoError(t, err) - - assert.Len(t, resp, 0) - }) - t.Run("one closed order", func(t *testing.T) { - successResp := ` -{ - "success": true, - "result": [ - { - "avgFillPrice": 10135.25, - "clientId": null, - "createdAt": "2019-06-27T15:24:03.101197+00:00", - "filledSize": 0.001, - "future": "BTC-PERP", - "id": 257132591, - "ioc": false, - "market": "BTC-PERP", - "postOnly": false, - "price": 10135.25, - "reduceOnly": false, - "remainingSize": 0.0, - "side": "buy", - "size": 0.001, - "status": "closed", - "type": "limit" - } - ], - "hasMoreData": false -} -` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, successResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - 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) - }) - - t.Run("sort the order", func(t *testing.T) { - successResp := ` -{ - "success": true, - "result": [ - { - "status": "closed", - "createdAt": "2020-09-01T15:24:03.101197+00:00", - "id": 789 - }, - { - "status": "closed", - "createdAt": "2019-03-27T15:24:03.101197+00:00", - "id": 123 - }, - { - "status": "closed", - "createdAt": "2019-06-27T15:24:03.101197+00:00", - "id": 456 - }, - { - "status": "new", - "createdAt": "2019-06-27T15:24:03.101197+00:00", - "id": 999 - } - ], - "hasMoreData": false -} -` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, successResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) - assert.NoError(t, err) - assert.Len(t, resp, 3) - - expectedOrderID := []uint64{123, 456, 789} - for i, o := range resp { - assert.Equal(t, expectedOrderID[i], o.OrderID) - } - }) -} - -func TestExchange_QueryAccount(t *testing.T) { - balanceResp := ` -{ - "result": [ - { - "availableWithoutBorrow": 19.47458865, - "coin": "USD", - "free": 19.48085209, - "spotBorrow": 0.0, - "total": 1094.66405065, - "usdValue": 1094.664050651561 - } - ], - "success": true -} -` - - accountInfoResp := ` -{ - "success": true, - "result": { - "backstopProvider": true, - "collateral": 3568181.02691129, - "freeCollateral": 1786071.456884368, - "initialMarginRequirement": 0.12222384240257728, - "leverage": 10, - "liquidating": false, - "maintenanceMarginRequirement": 0.07177992558058484, - "makerFee": 0.0002, - "marginFraction": 0.5588433331419503, - "openMarginFraction": 0.2447194090423075, - "takerFee": 0.0005, - "totalAccountValue": 3568180.98341129, - "totalPositionSize": 6384939.6992, - "username": "user@domain.com", - "positions": [ - { - "cost": -31.7906, - "entryPrice": 138.22, - "future": "ETH-PERP", - "initialMarginRequirement": 0.1, - "longOrderSize": 1744.55, - "maintenanceMarginRequirement": 0.04, - "netSize": -0.23, - "openSize": 1744.32, - "realizedPnl": 3.39441714, - "shortOrderSize": 1732.09, - "side": "sell", - "size": 0.23, - "unrealizedPnl": 0 - } - ] - } -} -` - returnBalance := false - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if returnBalance { - fmt.Fprintln(w, balanceResp) - return - } - returnBalance = true - fmt.Fprintln(w, accountInfoResp) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - resp, err := ex.QueryAccount(context.Background()) - assert.NoError(t, err) - - b, ok := resp.Balance("USD") - assert.True(t, ok) - expected := types.Balance{ - Currency: "USD", - Available: fixedpoint.MustNewFromString("19.48085209"), - Locked: fixedpoint.MustNewFromString("1094.66405065"), - } - expected.Locked = expected.Locked.Sub(expected.Available) - assert.Equal(t, expected, b) -} - -func TestExchange_QueryMarkets(t *testing.T) { - respJSON := `{ -"success": true, -"result": [ - { - "name": "BTC/USD", - "enabled": true, - "postOnly": false, - "priceIncrement": 1.0, - "sizeIncrement": 0.0001, - "minProvideSize": 0.001, - "last": 59039.0, - "bid": 59038.0, - "ask": 59040.0, - "price": 59039.0, - "type": "spot", - "baseCurrency": "BTC", - "quoteCurrency": "USD", - "underlying": null, - "restricted": false, - "highLeverageFeeExempt": true, - "change1h": 0.0015777151969599294, - "change24h": 0.05475756601279165, - "changeBod": -0.0035107262814994852, - "quoteVolume24h": 316493675.5463, - "volumeUsd24h": 316493675.5463 - } -] -}` - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, respJSON) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - ex.restEndpoint = serverURL - - resp, err := ex.QueryMarkets(context.Background()) - assert.NoError(t, err) - - assert.Len(t, resp, 1) - assert.Equal(t, types.Market{ - Symbol: "BTCUSD", - LocalSymbol: "BTC/USD", - PricePrecision: 0, - VolumePrecision: 4, - QuoteCurrency: "USD", - BaseCurrency: "BTC", - MinQuantity: fixedpoint.NewFromFloat(0.001), - StepSize: fixedpoint.NewFromFloat(0.0001), - TickSize: fixedpoint.NewFromInt(1), - }, resp["BTCUSD"]) -} - -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("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - 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: types.Time(actualConfirmedTime), - Amount: fixedpoint.NewFromInt(99), - 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) -} - -func TestExchange_QueryTrades(t *testing.T) { - t.Run("empty response", func(t *testing.T) { - respJSON := ` -{ - "success": true, - "result": [] -} -` - var f fillsResponse - assert.NoError(t, json.Unmarshal([]byte(respJSON), &f)) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, respJSON) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - ctx := context.Background() - actualConfirmedTime, err := parseDatetime("2021-02-23T09:29:08.534000+00:00") - assert.NoError(t, err) - - since := actualConfirmedTime.Add(-1 * time.Hour) - until := actualConfirmedTime.Add(1 * time.Hour) - - // ignore unavailable market - trades, err := ex.QueryTrades(ctx, "TSLA/USD", &types.TradeQueryOptions{ - StartTime: &since, - EndTime: &until, - Limit: 0, - LastTradeID: 0, - }) - assert.NoError(t, err) - assert.Len(t, trades, 0) - }) - - t.Run("duplicated response", func(t *testing.T) { - respJSON := ` -{ - "success": true, - "result": [{ - "id": 123, - "market": "TSLA/USD", - "future": null, - "baseCurrency": "TSLA", - "quoteCurrency": "USD", - "type": "order", - "side": "sell", - "price": 672.5, - "size": 1.0, - "orderId": 456, - "time": "2021-02-23T09:29:08.534000+00:00", - "tradeId": 789, - "feeRate": -5e-6, - "fee": -0.0033625, - "feeCurrency": "USD", - "liquidity": "maker" -}, { - "id": 123, - "market": "TSLA/USD", - "future": null, - "baseCurrency": "TSLA", - "quoteCurrency": "USD", - "type": "order", - "side": "sell", - "price": 672.5, - "size": 1.0, - "orderId": 456, - "time": "2021-02-23T09:29:08.534000+00:00", - "tradeId": 789, - "feeRate": -5e-6, - "fee": -0.0033625, - "feeCurrency": "USD", - "liquidity": "maker" -}] -} -` - var f fillsResponse - assert.NoError(t, json.Unmarshal([]byte(respJSON), &f)) - i := 0 - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if i == 0 { - fmt.Fprintln(w, respJSON) - return - } - fmt.Fprintln(w, `{"success":true, "result":[]}`) - })) - defer ts.Close() - - ex := NewExchange("test-key", "test-secret", "") - serverURL, err := url.Parse(ts.URL) - assert.NoError(t, err) - ex.client.BaseURL = serverURL - - ctx := context.Background() - actualConfirmedTime, err := parseDatetime("2021-02-23T09:29:08.534000+00:00") - assert.NoError(t, err) - - since := actualConfirmedTime.Add(-1 * time.Hour) - until := actualConfirmedTime.Add(1 * time.Hour) - - // ignore unavailable market - trades, err := ex.QueryTrades(ctx, "TSLA/USD", &types.TradeQueryOptions{ - StartTime: &since, - EndTime: &until, - Limit: 0, - LastTradeID: 0, - }) - assert.NoError(t, err) - assert.Len(t, trades, 1) - assert.Equal(t, types.Trade{ - ID: 789, - OrderID: 456, - Exchange: types.ExchangeFTX, - Price: fixedpoint.NewFromFloat(672.5), - Quantity: fixedpoint.One, - QuoteQuantity: fixedpoint.NewFromFloat(672.5 * 1.0), - Symbol: "TSLAUSD", - Side: types.SideTypeSell, - IsBuyer: false, - IsMaker: true, - Time: types.Time(actualConfirmedTime), - Fee: fixedpoint.NewFromFloat(-0.0033625), - FeeCurrency: "USD", - IsMargin: false, - IsIsolated: false, - StrategyID: sql.NullString{}, - PnL: sql.NullFloat64{}, - }, trades[0]) - }) -} - -func Test_isIntervalSupportedInKLine(t *testing.T) { - supportedIntervals := []types.Interval{ - types.Interval1m, - types.Interval5m, - types.Interval15m, - types.Interval1h, - types.Interval1d, - } - for _, i := range supportedIntervals { - assert.True(t, isIntervalSupportedInKLine(i)) - } - assert.False(t, isIntervalSupportedInKLine(types.Interval30m)) - assert.False(t, isIntervalSupportedInKLine(types.Interval2h)) - assert.True(t, isIntervalSupportedInKLine(types.Interval3d)) -} diff --git a/pkg/exchange/ftx/ftxapi/account.go b/pkg/exchange/ftx/ftxapi/account.go deleted file mode 100644 index f6309f272..000000000 --- a/pkg/exchange/ftx/ftxapi/account.go +++ /dev/null @@ -1,87 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "github.com/c9s/requestgen" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Position struct { - Cost fixedpoint.Value `json:"cost"` - EntryPrice fixedpoint.Value `json:"entryPrice"` - Future string `json:"future"` - InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"` - LongOrderSize fixedpoint.Value `json:"longOrderSize"` - MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"` - NetSize fixedpoint.Value `json:"netSize"` - OpenSize fixedpoint.Value `json:"openSize"` - ShortOrderSize fixedpoint.Value `json:"shortOrderSize"` - Side string `json:"side"` - Size fixedpoint.Value `json:"size"` - RealizedPnl fixedpoint.Value `json:"realizedPnl"` - UnrealizedPnl fixedpoint.Value `json:"unrealizedPnl"` -} - -type Account struct { - BackstopProvider bool `json:"backstopProvider"` - Collateral fixedpoint.Value `json:"collateral"` - FreeCollateral fixedpoint.Value `json:"freeCollateral"` - Leverage fixedpoint.Value `json:"leverage"` - InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"` - MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"` - Liquidating bool `json:"liquidating"` - MakerFee fixedpoint.Value `json:"makerFee"` - MarginFraction fixedpoint.Value `json:"marginFraction"` - OpenMarginFraction fixedpoint.Value `json:"openMarginFraction"` - TakerFee fixedpoint.Value `json:"takerFee"` - TotalAccountValue fixedpoint.Value `json:"totalAccountValue"` - TotalPositionSize fixedpoint.Value `json:"totalPositionSize"` - Username string `json:"username"` - Positions []Position `json:"positions"` -} - -//go:generate GetRequest -url "/api/account" -type GetAccountRequest -responseDataType .Account -type GetAccountRequest struct { - client requestgen.AuthenticatedAPIClient -} - -func (c *RestClient) NewGetAccountRequest() *GetAccountRequest { - return &GetAccountRequest{ - client: c, - } -} - -//go:generate GetRequest -url "/api/positions" -type GetPositionsRequest -responseDataType []Position -type GetPositionsRequest struct { - client requestgen.AuthenticatedAPIClient -} - -func (c *RestClient) NewGetPositionsRequest() *GetPositionsRequest { - return &GetPositionsRequest{ - client: c, - } -} - -type Balance struct { - Coin string `json:"coin"` - Free fixedpoint.Value `json:"free"` - SpotBorrow fixedpoint.Value `json:"spotBorrow"` - Total fixedpoint.Value `json:"total"` - UsdValue fixedpoint.Value `json:"usdValue"` - AvailableWithoutBorrow fixedpoint.Value `json:"availableWithoutBorrow"` -} - -//go:generate GetRequest -url "/api/wallet/balances" -type GetBalancesRequest -responseDataType []Balance -type GetBalancesRequest struct { - client requestgen.AuthenticatedAPIClient -} - -func (c *RestClient) NewGetBalancesRequest() *GetBalancesRequest { - return &GetBalancesRequest{ - client: c, - } -} diff --git a/pkg/exchange/ftx/ftxapi/cancel_all_order_request_requestgen.go b/pkg/exchange/ftx/ftxapi/cancel_all_order_request_requestgen.go deleted file mode 100644 index f47ea614c..000000000 --- a/pkg/exchange/ftx/ftxapi/cancel_all_order_request_requestgen.go +++ /dev/null @@ -1,126 +0,0 @@ -// Code generated by "requestgen -method DELETE -url /api/orders -type CancelAllOrderRequest -responseType .APIResponse"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (c *CancelAllOrderRequest) Market(market string) *CancelAllOrderRequest { - c.market = &market - return c -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (c *CancelAllOrderRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (c *CancelAllOrderRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - if c.market != nil { - market := *c.market - - // assign parameter of market - params["market"] = market - } else { - } - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (c *CancelAllOrderRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := c.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (c *CancelAllOrderRequest) GetParametersJSON() ([]byte, error) { - params, err := c.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (c *CancelAllOrderRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (c *CancelAllOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (c *CancelAllOrderRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := c.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (c *CancelAllOrderRequest) Do(ctx context.Context) (*APIResponse, error) { - - params, err := c.GetParameters() - if err != nil { - return nil, err - } - query := url.Values{} - - apiURL := "/api/orders" - - req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := c.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return &apiResponse, nil -} diff --git a/pkg/exchange/ftx/ftxapi/cancel_order_by_client_order_id_request_requestgen.go b/pkg/exchange/ftx/ftxapi/cancel_order_by_client_order_id_request_requestgen.go deleted file mode 100644 index 23cb4bab3..000000000 --- a/pkg/exchange/ftx/ftxapi/cancel_order_by_client_order_id_request_requestgen.go +++ /dev/null @@ -1,133 +0,0 @@ -// Code generated by "requestgen -method DELETE -url /api/orders/by_client_id/:clientOrderId -type CancelOrderByClientOrderIdRequest -responseType .APIResponse"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (c *CancelOrderByClientOrderIdRequest) ClientOrderId(clientOrderId string) *CancelOrderByClientOrderIdRequest { - c.clientOrderId = clientOrderId - return c -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (c *CancelOrderByClientOrderIdRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (c *CancelOrderByClientOrderIdRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (c *CancelOrderByClientOrderIdRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := c.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (c *CancelOrderByClientOrderIdRequest) GetParametersJSON() ([]byte, error) { - params, err := c.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (c *CancelOrderByClientOrderIdRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check clientOrderId field -> json key clientOrderId - clientOrderId := c.clientOrderId - - // TEMPLATE check-required - if len(clientOrderId) == 0 { - return params, fmt.Errorf("clientOrderId is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of clientOrderId - params["clientOrderId"] = clientOrderId - - return params, nil -} - -func (c *CancelOrderByClientOrderIdRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (c *CancelOrderByClientOrderIdRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := c.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (c *CancelOrderByClientOrderIdRequest) Do(ctx context.Context) (*APIResponse, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/orders/by_client_id/:clientOrderId" - slugs, err := c.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = c.applySlugsToUrl(apiURL, slugs) - - req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := c.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return &apiResponse, nil -} diff --git a/pkg/exchange/ftx/ftxapi/cancel_order_request_requestgen.go b/pkg/exchange/ftx/ftxapi/cancel_order_request_requestgen.go deleted file mode 100644 index 70684c1df..000000000 --- a/pkg/exchange/ftx/ftxapi/cancel_order_request_requestgen.go +++ /dev/null @@ -1,133 +0,0 @@ -// Code generated by "requestgen -method DELETE -url /api/orders/:orderID -type CancelOrderRequest -responseType .APIResponse"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (c *CancelOrderRequest) OrderID(orderID string) *CancelOrderRequest { - c.orderID = orderID - return c -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (c *CancelOrderRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (c *CancelOrderRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := c.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) { - params, err := c.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (c *CancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check orderID field -> json key orderID - orderID := c.orderID - - // TEMPLATE check-required - if len(orderID) == 0 { - return params, fmt.Errorf("orderID is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of orderID - params["orderID"] = orderID - - return params, nil -} - -func (c *CancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (c *CancelOrderRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := c.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (c *CancelOrderRequest) Do(ctx context.Context) (*APIResponse, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/orders/:orderID" - slugs, err := c.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = c.applySlugsToUrl(apiURL, slugs) - - req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := c.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - return &apiResponse, nil -} diff --git a/pkg/exchange/ftx/ftxapi/client.go b/pkg/exchange/ftx/ftxapi/client.go deleted file mode 100644 index 2437bd48f..000000000 --- a/pkg/exchange/ftx/ftxapi/client.go +++ /dev/null @@ -1,203 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/c9s/requestgen" - "github.com/pkg/errors" -) - -const defaultHTTPTimeout = time.Second * 15 -const RestBaseURL = "https://ftx.com/api" - -type APIResponse struct { - Success bool `json:"success"` - Result json.RawMessage `json:"result,omitempty"` - HasMoreData bool `json:"hasMoreData,omitempty"` -} - -type RestClient struct { - BaseURL *url.URL - - client *http.Client - - Key, Secret, subAccount string - - /* - AccountService *AccountService - MarketDataService *MarketDataService - TradeService *TradeService - BulletService *BulletService - */ -} - -func NewClient() *RestClient { - u, err := url.Parse(RestBaseURL) - if err != nil { - panic(err) - } - - client := &RestClient{ - BaseURL: u, - client: &http.Client{ - Timeout: defaultHTTPTimeout, - }, - } - - /* - client.AccountService = &AccountService{client: client} - client.MarketDataService = &MarketDataService{client: client} - client.TradeService = &TradeService{client: client} - client.BulletService = &BulletService{client: client} - */ - return client -} - -func (c *RestClient) Auth(key, secret, subAccount string) { - c.Key = key - // pragma: allowlist nextline secret - c.Secret = secret - c.subAccount = subAccount -} - -// NewRequest create new API request. Relative url can be provided in refURL. -func (c *RestClient) NewRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - body, err := castPayload(payload) - if err != nil { - return nil, err - } - - pathURL := c.BaseURL.ResolveReference(rel) - return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) -} - -// sendRequest sends the request to the API server and handle the response -func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) { - resp, err := c.client.Do(req) - if err != nil { - return nil, err - } - - // newResponse reads the response body and return a new Response object - response, err := requestgen.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - return response, errors.New(string(response.Body)) - } - - return response, nil -} - -// newAuthenticatedRequest creates new http request for authenticated routes. -func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { - if len(c.Key) == 0 { - return nil, errors.New("empty api key") - } - - if len(c.Secret) == 0 { - return nil, errors.New("empty api secret") - } - - rel, err := url.Parse(refURL) - if err != nil { - return nil, err - } - - if params != nil { - rel.RawQuery = params.Encode() - } - - // pathURL is for sending request - pathURL := c.BaseURL.ResolveReference(rel) - - // path here is used for auth header - path := pathURL.Path - if rel.RawQuery != "" { - path += "?" + rel.RawQuery - } - - body, err := castPayload(payload) - if err != nil { - return nil, err - } - - req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Accept", "application/json") - - // Build authentication headers - c.attachAuthHeaders(req, method, path, body) - return req, nil -} - -func (c *RestClient) attachAuthHeaders(req *http.Request, method string, path string, body []byte) { - millisecondTs := time.Now().UnixNano() / int64(time.Millisecond) - ts := strconv.FormatInt(millisecondTs, 10) - p := ts + method + path + string(body) - signature := sign(c.Secret, p) - req.Header.Set("FTX-KEY", c.Key) - req.Header.Set("FTX-SIGN", signature) - req.Header.Set("FTX-TS", ts) - if c.subAccount != "" { - req.Header.Set("FTX-SUBACCOUNT", c.subAccount) - } -} - -// sign uses sha256 to sign the payload with the given secret -func sign(secret, payload string) string { - var sig = hmac.New(sha256.New, []byte(secret)) - _, err := sig.Write([]byte(payload)) - if err != nil { - return "" - } - - return hex.EncodeToString(sig.Sum(nil)) -} - -func castPayload(payload interface{}) ([]byte, error) { - if payload != nil { - switch v := payload.(type) { - case string: - return []byte(v), nil - - case []byte: - return v, nil - - default: - body, err := json.Marshal(v) - return body, err - } - } - - return nil, nil -} diff --git a/pkg/exchange/ftx/ftxapi/client_test.go b/pkg/exchange/ftx/ftxapi/client_test.go deleted file mode 100644 index a73f59566..000000000 --- a/pkg/exchange/ftx/ftxapi/client_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package ftxapi - -import ( - "context" - "os" - "regexp" - "strconv" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -func maskSecret(s string) string { - re := regexp.MustCompile(`\b(\w{4})\w+\b`) - s = re.ReplaceAllString(s, "$1******") - return s -} - -func integrationTestConfigured(t *testing.T) (key, secret string, ok bool) { - var hasKey, hasSecret bool - key, hasKey = os.LookupEnv("FTX_API_KEY") - secret, hasSecret = os.LookupEnv("FTX_API_SECRET") - ok = hasKey && hasSecret && os.Getenv("TEST_FTX") == "1" - if ok { - t.Logf("ftx api integration test enabled, key = %s, secret = %s", maskSecret(key), maskSecret(secret)) - } - return key, secret, ok -} - -func TestClient_Requests(t *testing.T) { - key, secret, ok := integrationTestConfigured(t) - if !ok { - t.SkipNow() - return - } - - ctx, cancel := context.WithTimeout(context.TODO(), 15*time.Second) - defer cancel() - - client := NewClient() - client.Auth(key, secret, "") - - testCases := []struct { - name string - tt func(t *testing.T) - }{ - { - name: "GetMarketsRequest", - tt: func(t *testing.T) { - req := client.NewGetMarketsRequest() - markets, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, markets) - t.Logf("markets: %+v", markets) - }, - }, - { - name: "GetAccountRequest", - tt: func(t *testing.T) { - req := client.NewGetAccountRequest() - account, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, account) - t.Logf("account: %+v", account) - }, - }, - { - name: "PlaceOrderRequest", - tt: func(t *testing.T) { - req := client.NewPlaceOrderRequest() - req.PostOnly(true). - Size(fixedpoint.MustNewFromString("1.0")). - Price(fixedpoint.MustNewFromString("10.0")). - OrderType(OrderTypeLimit). - Side(SideBuy). - Market("LTC/USDT") - - createdOrder, err := req.Do(ctx) - if assert.NoError(t, err) { - assert.NotNil(t, createdOrder) - t.Logf("createdOrder: %+v", createdOrder) - - req2 := client.NewCancelOrderRequest(strconv.FormatInt(createdOrder.Id, 10)) - ret, err := req2.Do(ctx) - assert.NoError(t, err) - t.Logf("cancelOrder: %+v", ret) - assert.True(t, ret.Success) - } - }, - }, - { - name: "GetFillsRequest", - tt: func(t *testing.T) { - req := client.NewGetFillsRequest() - req.Market("CRO/USD") - fills, err := req.Do(ctx) - assert.NoError(t, err) - assert.NotNil(t, fills) - t.Logf("fills: %+v", fills) - }, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, testCase.tt) - } -} diff --git a/pkg/exchange/ftx/ftxapi/coin.go b/pkg/exchange/ftx/ftxapi/coin.go deleted file mode 100644 index ca8d81a55..000000000 --- a/pkg/exchange/ftx/ftxapi/coin.go +++ /dev/null @@ -1,42 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "github.com/c9s/requestgen" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Coin struct { - Bep2Asset *string `json:"bep2Asset"` - CanConvert bool `json:"canConvert"` - CanDeposit bool `json:"canDeposit"` - CanWithdraw bool `json:"canWithdraw"` - Collateral bool `json:"collateral"` - CollateralWeight fixedpoint.Value `json:"collateralWeight"` - CreditTo *string `json:"creditTo"` - Erc20Contract string `json:"erc20Contract"` - Fiat bool `json:"fiat"` - HasTag bool `json:"hasTag"` - Id string `json:"id"` - IsToken bool `json:"isToken"` - Methods []string `json:"methods"` - Name string `json:"name"` - SplMint string `json:"splMint"` - Trc20Contract string `json:"trc20Contract"` - UsdFungible bool `json:"usdFungible"` -} - -//go:generate GetRequest -url "api/coins" -type GetCoinsRequest -responseDataType []Coin -type GetCoinsRequest struct { - client requestgen.AuthenticatedAPIClient -} - -func (c *RestClient) NewGetCoinsRequest() *GetCoinsRequest { - return &GetCoinsRequest{ - client: c, - } -} diff --git a/pkg/exchange/ftx/ftxapi/get_account_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_account_request_requestgen.go deleted file mode 100644 index ef153bc72..000000000 --- a/pkg/exchange/ftx/ftxapi/get_account_request_requestgen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/account -type GetAccountRequest -responseDataType .Account"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetAccountRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetAccountRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetAccountRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetAccountRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetAccountRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetAccountRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetAccountRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetAccountRequest) Do(ctx context.Context) (*Account, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/account" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data Account - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return &data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_balances_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_balances_request_requestgen.go deleted file mode 100644 index e67a36299..000000000 --- a/pkg/exchange/ftx/ftxapi/get_balances_request_requestgen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/wallet/balances -type GetBalancesRequest -responseDataType []Balance"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetBalancesRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetBalancesRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetBalancesRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetBalancesRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetBalancesRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetBalancesRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetBalancesRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetBalancesRequest) Do(ctx context.Context) ([]Balance, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/wallet/balances" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Balance - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_coins_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_coins_request_requestgen.go deleted file mode 100644 index 3e5547c79..000000000 --- a/pkg/exchange/ftx/ftxapi/get_coins_request_requestgen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url api/coins -type GetCoinsRequest -responseDataType []Coin"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetCoinsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetCoinsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetCoinsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetCoinsRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetCoinsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetCoinsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetCoinsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetCoinsRequest) Do(ctx context.Context) ([]Coin, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "api/coins" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Coin - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_fills_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_fills_request_requestgen.go deleted file mode 100644 index 714817710..000000000 --- a/pkg/exchange/ftx/ftxapi/get_fills_request_requestgen.go +++ /dev/null @@ -1,187 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/fills -type GetFillsRequest -responseDataType []Fill"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" - "strconv" - "time" -) - -func (g *GetFillsRequest) Market(market string) *GetFillsRequest { - g.market = &market - return g -} - -func (g *GetFillsRequest) StartTime(startTime time.Time) *GetFillsRequest { - g.startTime = &startTime - return g -} - -func (g *GetFillsRequest) EndTime(endTime time.Time) *GetFillsRequest { - g.endTime = &endTime - return g -} - -func (g *GetFillsRequest) OrderID(orderID int) *GetFillsRequest { - g.orderID = &orderID - return g -} - -func (g *GetFillsRequest) Order(order string) *GetFillsRequest { - g.order = &order - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetFillsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check market field -> json key market - if g.market != nil { - market := *g.market - - // assign parameter of market - params["market"] = market - } else { - } - // check startTime field -> json key start_time - if g.startTime != nil { - startTime := *g.startTime - - // assign parameter of startTime - // convert time.Time to seconds time stamp - params["start_time"] = strconv.FormatInt(startTime.Unix(), 10) - } else { - } - // check endTime field -> json key end_time - if g.endTime != nil { - endTime := *g.endTime - - // assign parameter of endTime - // convert time.Time to seconds time stamp - params["end_time"] = strconv.FormatInt(endTime.Unix(), 10) - } else { - } - // check orderID field -> json key orderId - if g.orderID != nil { - orderID := *g.orderID - - // assign parameter of orderID - params["orderId"] = orderID - } else { - } - // check order field -> json key order - if g.order != nil { - order := *g.order - - // assign parameter of order - params["order"] = order - } else { - } - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetFillsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetFillsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetFillsRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetFillsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetFillsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetFillsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetFillsRequest) Do(ctx context.Context) ([]Fill, error) { - - // no body params - var params interface{} - query, err := g.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/api/fills" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Fill - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go deleted file mode 100644 index 72825a4c2..000000000 --- a/pkg/exchange/ftx/ftxapi/get_market_request_requestgen.go +++ /dev/null @@ -1,155 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url api/markets/:market -type GetMarketRequest -responseDataType .Market"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" -) - -func (g *GetMarketRequest) Market(market string) *GetMarketRequest { - g.market = market - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetMarketRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for _k, _v := range params { - query.Add(_k, fmt.Sprintf("%v", _v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetMarketRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetMarketRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for _k, _v := range params { - if g.isVarSlice(_v) { - g.iterateSlice(_v, func(it interface{}) { - query.Add(_k+"[]", fmt.Sprintf("%v", it)) - }) - } else { - query.Add(_k, fmt.Sprintf("%v", _v)) - } - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetMarketRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetMarketRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := g.market - - // assign parameter of market - params["market"] = market - - return params, nil -} - -func (g *GetMarketRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for _k, _v := range slugs { - needleRE := regexp.MustCompile(":" + _k + "\\b") - url = needleRE.ReplaceAllString(url, _v) - } - - return url -} - -func (g *GetMarketRequest) iterateSlice(slice interface{}, _f func(it interface{})) { - sliceValue := reflect.ValueOf(slice) - for _i := 0; _i < sliceValue.Len(); _i++ { - it := sliceValue.Index(_i).Interface() - _f(it) - } -} - -func (g *GetMarketRequest) isVarSlice(_v interface{}) bool { - rt := reflect.TypeOf(_v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (g *GetMarketRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for _k, _v := range params { - slugs[_k] = fmt.Sprintf("%v", _v) - } - - return slugs, nil -} - -func (g *GetMarketRequest) Do(ctx context.Context) (*Market, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "api/markets/:market" - slugs, err := g.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = g.applySlugsToUrl(apiURL, slugs) - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data Market - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return &data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go deleted file mode 100644 index db8e591bc..000000000 --- a/pkg/exchange/ftx/ftxapi/get_markets_request_requestgen.go +++ /dev/null @@ -1,139 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url api/markets -type GetMarketsRequest -responseDataType []Market"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "reflect" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetMarketsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for _k, _v := range params { - query.Add(_k, fmt.Sprintf("%v", _v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetMarketsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetMarketsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for _k, _v := range params { - if g.isVarSlice(_v) { - g.iterateSlice(_v, func(it interface{}) { - query.Add(_k+"[]", fmt.Sprintf("%v", it)) - }) - } else { - query.Add(_k, fmt.Sprintf("%v", _v)) - } - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetMarketsRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetMarketsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetMarketsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for _k, _v := range slugs { - needleRE := regexp.MustCompile(":" + _k + "\\b") - url = needleRE.ReplaceAllString(url, _v) - } - - return url -} - -func (g *GetMarketsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { - sliceValue := reflect.ValueOf(slice) - for _i := 0; _i < sliceValue.Len(); _i++ { - it := sliceValue.Index(_i).Interface() - _f(it) - } -} - -func (g *GetMarketsRequest) isVarSlice(_v interface{}) bool { - rt := reflect.TypeOf(_v) - switch rt.Kind() { - case reflect.Slice: - return true - } - return false -} - -func (g *GetMarketsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for _k, _v := range params { - slugs[_k] = fmt.Sprintf("%v", _v) - } - - return slugs, nil -} - -func (g *GetMarketsRequest) Do(ctx context.Context) ([]Market, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "api/markets" - - req, err := g.client.NewRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Market - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_open_orders_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_open_orders_request_requestgen.go deleted file mode 100644 index b36d0fede..000000000 --- a/pkg/exchange/ftx/ftxapi/get_open_orders_request_requestgen.go +++ /dev/null @@ -1,128 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/orders -type GetOpenOrdersRequest -responseDataType []Order"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (g *GetOpenOrdersRequest) Market(market string) *GetOpenOrdersRequest { - g.market = market - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetOpenOrdersRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := g.market - - // assign parameter of market - params["market"] = market - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetOpenOrdersRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetOpenOrdersRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetOpenOrdersRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetOpenOrdersRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetOpenOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetOpenOrdersRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetOpenOrdersRequest) Do(ctx context.Context) ([]Order, error) { - - // no body params - var params interface{} - query, err := g.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/api/orders" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Order - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_order_history_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_order_history_request_requestgen.go deleted file mode 100644 index e10a4da17..000000000 --- a/pkg/exchange/ftx/ftxapi/get_order_history_request_requestgen.go +++ /dev/null @@ -1,158 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/orders/history -type GetOrderHistoryRequest -responseDataType []Order"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" - "strconv" - "time" -) - -func (g *GetOrderHistoryRequest) Market(market string) *GetOrderHistoryRequest { - g.market = market - return g -} - -func (g *GetOrderHistoryRequest) StartTime(startTime time.Time) *GetOrderHistoryRequest { - g.startTime = &startTime - return g -} - -func (g *GetOrderHistoryRequest) EndTime(endTime time.Time) *GetOrderHistoryRequest { - g.endTime = &endTime - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetOrderHistoryRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := g.market - - // assign parameter of market - params["market"] = market - // check startTime field -> json key start_time - if g.startTime != nil { - startTime := *g.startTime - - // assign parameter of startTime - // convert time.Time to seconds time stamp - params["start_time"] = strconv.FormatInt(startTime.Unix(), 10) - } else { - } - // check endTime field -> json key end_time - if g.endTime != nil { - endTime := *g.endTime - - // assign parameter of endTime - // convert time.Time to seconds time stamp - params["end_time"] = strconv.FormatInt(endTime.Unix(), 10) - } else { - } - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetOrderHistoryRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetOrderHistoryRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetOrderHistoryRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetOrderHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetOrderHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetOrderHistoryRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetOrderHistoryRequest) Do(ctx context.Context) ([]Order, error) { - - // no body params - var params interface{} - query, err := g.GetQueryParameters() - if err != nil { - return nil, err - } - - apiURL := "/api/orders/history" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Order - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_order_status_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_order_status_request_requestgen.go deleted file mode 100644 index 6b613611a..000000000 --- a/pkg/exchange/ftx/ftxapi/get_order_status_request_requestgen.go +++ /dev/null @@ -1,131 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/orders/:orderId -type GetOrderStatusRequest -responseDataType .Order"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -func (g *GetOrderStatusRequest) OrderID(orderID uint64) *GetOrderStatusRequest { - g.orderID = orderID - return g -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetOrderStatusRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetOrderStatusRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetOrderStatusRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetOrderStatusRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetOrderStatusRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check orderID field -> json key orderId - orderID := g.orderID - - // assign parameter of orderID - params["orderId"] = orderID - - return params, nil -} - -func (g *GetOrderStatusRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetOrderStatusRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetOrderStatusRequest) Do(ctx context.Context) (*Order, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/orders/:orderId" - slugs, err := g.GetSlugsMap() - if err != nil { - return nil, err - } - - apiURL = g.applySlugsToUrl(apiURL, slugs) - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data Order - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return &data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/get_positions_request_requestgen.go b/pkg/exchange/ftx/ftxapi/get_positions_request_requestgen.go deleted file mode 100644 index d77811f6f..000000000 --- a/pkg/exchange/ftx/ftxapi/get_positions_request_requestgen.go +++ /dev/null @@ -1,115 +0,0 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/positions -type GetPositionsRequest -responseDataType []Position"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "regexp" -) - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetPositionsRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (g *GetPositionsRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (g *GetPositionsRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := g.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetPositionsRequest) GetParametersJSON() ([]byte, error) { - params, err := g.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetPositionsRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (g *GetPositionsRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (g *GetPositionsRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := g.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (g *GetPositionsRequest) Do(ctx context.Context) ([]Position, error) { - - // no body params - var params interface{} - query := url.Values{} - - apiURL := "/api/positions" - - req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := g.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data []Position - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/market.go b/pkg/exchange/ftx/ftxapi/market.go deleted file mode 100644 index 4cac2fee9..000000000 --- a/pkg/exchange/ftx/ftxapi/market.go +++ /dev/null @@ -1,59 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "github.com/c9s/requestgen" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Market struct { - Name string `json:"name"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - QuoteVolume24H fixedpoint.Value `json:"quoteVolume24h"` - Change1H fixedpoint.Value `json:"change1h"` - Change24H fixedpoint.Value `json:"change24h"` - ChangeBod fixedpoint.Value `json:"changeBod"` - VolumeUsd24H fixedpoint.Value `json:"volumeUsd24h"` - HighLeverageFeeExempt bool `json:"highLeverageFeeExempt"` - MinProvideSize fixedpoint.Value `json:"minProvideSize"` - Type string `json:"type"` - Underlying string `json:"underlying"` - Enabled bool `json:"enabled"` - Ask fixedpoint.Value `json:"ask"` - Bid fixedpoint.Value `json:"bid"` - Last fixedpoint.Value `json:"last"` - PostOnly bool `json:"postOnly"` - Price fixedpoint.Value `json:"price"` - PriceIncrement fixedpoint.Value `json:"priceIncrement"` - SizeIncrement fixedpoint.Value `json:"sizeIncrement"` - Restricted bool `json:"restricted"` -} - -//go:generate GetRequest -url "api/markets" -type GetMarketsRequest -responseDataType []Market -type GetMarketsRequest struct { - client requestgen.APIClient -} - -func (c *RestClient) NewGetMarketsRequest() *GetMarketsRequest { - return &GetMarketsRequest{ - client: c, - } -} - -//go:generate GetRequest -url "api/markets/:market" -type GetMarketRequest -responseDataType .Market -type GetMarketRequest struct { - client requestgen.AuthenticatedAPIClient - market string `param:"market,slug"` -} - -func (c *RestClient) NewGetMarketRequest(market string) *GetMarketRequest { - return &GetMarketRequest{ - client: c, - market: market, - } -} diff --git a/pkg/exchange/ftx/ftxapi/place_order_request_requestgen.go b/pkg/exchange/ftx/ftxapi/place_order_request_requestgen.go deleted file mode 100644 index 994011ca9..000000000 --- a/pkg/exchange/ftx/ftxapi/place_order_request_requestgen.go +++ /dev/null @@ -1,219 +0,0 @@ -// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Result -url /api/orders -type PlaceOrderRequest -responseDataType .Order"; DO NOT EDIT. - -package ftxapi - -import ( - "context" - "encoding/json" - "fmt" - "github.com/c9s/bbgo/pkg/fixedpoint" - "net/url" - "regexp" -) - -func (p *PlaceOrderRequest) Market(market string) *PlaceOrderRequest { - p.market = market - return p -} - -func (p *PlaceOrderRequest) Side(side Side) *PlaceOrderRequest { - p.side = side - return p -} - -func (p *PlaceOrderRequest) Price(price fixedpoint.Value) *PlaceOrderRequest { - p.price = price - return p -} - -func (p *PlaceOrderRequest) Size(size fixedpoint.Value) *PlaceOrderRequest { - p.size = size - return p -} - -func (p *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest { - p.orderType = orderType - return p -} - -func (p *PlaceOrderRequest) Ioc(ioc bool) *PlaceOrderRequest { - p.ioc = &ioc - return p -} - -func (p *PlaceOrderRequest) PostOnly(postOnly bool) *PlaceOrderRequest { - p.postOnly = &postOnly - return p -} - -func (p *PlaceOrderRequest) ClientID(clientID string) *PlaceOrderRequest { - p.clientID = &clientID - return p -} - -// GetQueryParameters builds and checks the query parameters and returns url.Values -func (p *PlaceOrderRequest) GetQueryParameters() (url.Values, error) { - var params = map[string]interface{}{} - - query := url.Values{} - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParameters builds and checks the parameters and return the result in a map object -func (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - // check market field -> json key market - market := p.market - - // TEMPLATE check-required - if len(market) == 0 { - return params, fmt.Errorf("market is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of market - params["market"] = market - // check side field -> json key side - side := p.side - - // TEMPLATE check-required - if len(side) == 0 { - return params, fmt.Errorf("side is required, empty string given") - } - // END TEMPLATE check-required - - // assign parameter of side - params["side"] = side - // check price field -> json key price - price := p.price - - // assign parameter of price - params["price"] = price - // check size field -> json key size - size := p.size - - // assign parameter of size - params["size"] = size - // check orderType field -> json key type - orderType := p.orderType - - // assign parameter of orderType - params["type"] = orderType - // check ioc field -> json key ioc - if p.ioc != nil { - ioc := *p.ioc - - // assign parameter of ioc - params["ioc"] = ioc - } else { - } - // check postOnly field -> json key postOnly - if p.postOnly != nil { - postOnly := *p.postOnly - - // assign parameter of postOnly - params["postOnly"] = postOnly - } else { - } - // check clientID field -> json key clientId - if p.clientID != nil { - clientID := *p.clientID - - // assign parameter of clientID - params["clientId"] = clientID - } else { - } - - return params, nil -} - -// GetParametersQuery converts the parameters from GetParameters into the url.Values format -func (p *PlaceOrderRequest) GetParametersQuery() (url.Values, error) { - query := url.Values{} - - params, err := p.GetParameters() - if err != nil { - return query, err - } - - for k, v := range params { - query.Add(k, fmt.Sprintf("%v", v)) - } - - return query, nil -} - -// GetParametersJSON converts the parameters from GetParameters into the JSON format -func (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) { - params, err := p.GetParameters() - if err != nil { - return nil, err - } - - return json.Marshal(params) -} - -// GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (p *PlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) { - var params = map[string]interface{}{} - - return params, nil -} - -func (p *PlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { - for k, v := range slugs { - needleRE := regexp.MustCompile(":" + k + "\\b") - url = needleRE.ReplaceAllString(url, v) - } - - return url -} - -func (p *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) { - slugs := map[string]string{} - params, err := p.GetSlugParameters() - if err != nil { - return slugs, nil - } - - for k, v := range params { - slugs[k] = fmt.Sprintf("%v", v) - } - - return slugs, nil -} - -func (p *PlaceOrderRequest) Do(ctx context.Context) (*Order, error) { - - params, err := p.GetParameters() - if err != nil { - return nil, err - } - query := url.Values{} - - apiURL := "/api/orders" - - req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) - if err != nil { - return nil, err - } - - response, err := p.client.SendRequest(req) - if err != nil { - return nil, err - } - - var apiResponse APIResponse - if err := response.DecodeJSON(&apiResponse); err != nil { - return nil, err - } - var data Order - if err := json.Unmarshal(apiResponse.Result, &data); err != nil { - return nil, err - } - return &data, nil -} diff --git a/pkg/exchange/ftx/ftxapi/trade.go b/pkg/exchange/ftx/ftxapi/trade.go deleted file mode 100644 index 323481d20..000000000 --- a/pkg/exchange/ftx/ftxapi/trade.go +++ /dev/null @@ -1,172 +0,0 @@ -package ftxapi - -//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result -//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result - -import ( - "time" - - "github.com/c9s/requestgen" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Order struct { - CreatedAt time.Time `json:"createdAt"` - Future string `json:"future"` - Id int64 `json:"id"` - Market string `json:"market"` - Price fixedpoint.Value `json:"price"` - AvgFillPrice fixedpoint.Value `json:"avgFillPrice"` - Size fixedpoint.Value `json:"size"` - RemainingSize fixedpoint.Value `json:"remainingSize"` - FilledSize fixedpoint.Value `json:"filledSize"` - Side Side `json:"side"` - Status OrderStatus `json:"status"` - Type OrderType `json:"type"` - ReduceOnly bool `json:"reduceOnly"` - Ioc bool `json:"ioc"` - PostOnly bool `json:"postOnly"` - ClientId string `json:"clientId"` -} - -//go:generate GetRequest -url "/api/orders" -type GetOpenOrdersRequest -responseDataType []Order -type GetOpenOrdersRequest struct { - client requestgen.AuthenticatedAPIClient - market string `param:"market,query"` -} - -func (c *RestClient) NewGetOpenOrdersRequest(market string) *GetOpenOrdersRequest { - return &GetOpenOrdersRequest{ - client: c, - market: market, - } -} - -//go:generate GetRequest -url "/api/orders/history" -type GetOrderHistoryRequest -responseDataType []Order -type GetOrderHistoryRequest struct { - client requestgen.AuthenticatedAPIClient - - market string `param:"market,query"` - - startTime *time.Time `param:"start_time,seconds,query"` - endTime *time.Time `param:"end_time,seconds,query"` -} - -func (c *RestClient) NewGetOrderHistoryRequest(market string) *GetOrderHistoryRequest { - return &GetOrderHistoryRequest{ - client: c, - market: market, - } -} - -//go:generate PostRequest -url "/api/orders" -type PlaceOrderRequest -responseDataType .Order -type PlaceOrderRequest struct { - client requestgen.AuthenticatedAPIClient - - market string `param:"market,required"` - side Side `param:"side,required"` - price fixedpoint.Value `param:"price"` - size fixedpoint.Value `param:"size"` - orderType OrderType `param:"type"` - ioc *bool `param:"ioc"` - postOnly *bool `param:"postOnly"` - clientID *string `param:"clientId,optional"` -} - -func (c *RestClient) NewPlaceOrderRequest() *PlaceOrderRequest { - return &PlaceOrderRequest{ - client: c, - } -} - -//go:generate requestgen -method DELETE -url "/api/orders/:orderID" -type CancelOrderRequest -responseType .APIResponse -type CancelOrderRequest struct { - client requestgen.AuthenticatedAPIClient - orderID string `param:"orderID,required,slug"` -} - -func (c *RestClient) NewCancelOrderRequest(orderID string) *CancelOrderRequest { - return &CancelOrderRequest{ - client: c, - orderID: orderID, - } -} - -//go:generate requestgen -method DELETE -url "/api/orders" -type CancelAllOrderRequest -responseType .APIResponse -type CancelAllOrderRequest struct { - client requestgen.AuthenticatedAPIClient - market *string `param:"market"` -} - -func (c *RestClient) NewCancelAllOrderRequest() *CancelAllOrderRequest { - return &CancelAllOrderRequest{ - client: c, - } -} - -//go:generate requestgen -method DELETE -url "/api/orders/by_client_id/:clientOrderId" -type CancelOrderByClientOrderIdRequest -responseType .APIResponse -type CancelOrderByClientOrderIdRequest struct { - client requestgen.AuthenticatedAPIClient - clientOrderId string `param:"clientOrderId,required,slug"` -} - -func (c *RestClient) NewCancelOrderByClientOrderIdRequest(clientOrderId string) *CancelOrderByClientOrderIdRequest { - return &CancelOrderByClientOrderIdRequest{ - client: c, - clientOrderId: clientOrderId, - } -} - -type Fill struct { - // Id is fill ID - Id uint64 `json:"id"` - Future string `json:"future"` - Liquidity Liquidity `json:"liquidity"` - Market string `json:"market"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - OrderId uint64 `json:"orderId"` - TradeId uint64 `json:"tradeId"` - Price fixedpoint.Value `json:"price"` - Side Side `json:"side"` - Size fixedpoint.Value `json:"size"` - Time time.Time `json:"time"` - Type string `json:"type"` // always = "order" - Fee fixedpoint.Value `json:"fee"` - FeeCurrency string `json:"feeCurrency"` - FeeRate fixedpoint.Value `json:"feeRate"` -} - -//go:generate GetRequest -url "/api/fills" -type GetFillsRequest -responseDataType []Fill -type GetFillsRequest struct { - client requestgen.AuthenticatedAPIClient - - market *string `param:"market,query"` - startTime *time.Time `param:"start_time,seconds,query"` - endTime *time.Time `param:"end_time,seconds,query"` - orderID *int `param:"orderId,query"` - - // order is the order of the returned records, asc or null - order *string `param:"order,query"` -} - -func (c *RestClient) NewGetFillsRequest() *GetFillsRequest { - return &GetFillsRequest{ - client: c, - } -} - -//go:generate GetRequest -url "/api/orders/:orderId" -type GetOrderStatusRequest -responseDataType .Order -type GetOrderStatusRequest struct { - client requestgen.AuthenticatedAPIClient - orderID uint64 `param:"orderId,slug"` -} - -func (c *RestClient) NewGetOrderStatusRequest(orderID uint64) *GetOrderStatusRequest { - return &GetOrderStatusRequest{ - client: c, - orderID: orderID, - } -} diff --git a/pkg/exchange/ftx/ftxapi/types.go b/pkg/exchange/ftx/ftxapi/types.go deleted file mode 100644 index fdfe6cd78..000000000 --- a/pkg/exchange/ftx/ftxapi/types.go +++ /dev/null @@ -1,35 +0,0 @@ -package ftxapi - -type Liquidity string - -const ( - LiquidityTaker Liquidity = "taker" - LiquidityMaker Liquidity = "maker" -) - -type Side string - -const ( - SideBuy Side = "buy" - SideSell Side = "sell" -) - -type OrderType string - -const ( - OrderTypeLimit OrderType = "limit" - OrderTypeMarket OrderType = "market" - - // trigger order types - OrderTypeStopLimit OrderType = "stop" - OrderTypeTrailingStop OrderType = "trailingStop" - OrderTypeTakeProfit OrderType = "takeProfit" -) - -type OrderStatus string - -const ( - OrderStatusNew OrderStatus = "new" - OrderStatusOpen OrderStatus = "open" - OrderStatusClosed OrderStatus = "closed" -) diff --git a/pkg/exchange/ftx/generate_symbol_map.go b/pkg/exchange/ftx/generate_symbol_map.go deleted file mode 100644 index b2c68072e..000000000 --- a/pkg/exchange/ftx/generate_symbol_map.go +++ /dev/null @@ -1,65 +0,0 @@ -//go:build ignore -// +build ignore - -package main - -import ( - "encoding/json" - "log" - "net/http" - "os" - "strings" - "text/template" -) - -var packageTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. -package ftx -var symbolMap = map[string]string{ -{{- range $k, $v := . }} - {{ printf "%q" $k }}: {{ printf "%q" $v }}, -{{- end }} -} -`)) - -type Market struct { - Name string `json:"name"` -} - -type ApiResponse struct { - Success bool `json:"success"` - - Result []Market `json:"result"` -} - -func main() { - var data = map[string]string{} - - const url = "https://ftx.com/api/markets" - - resp, err := http.Get(url) - if err != nil { - log.Fatal(err) - return - } - defer resp.Body.Close() - - r := &ApiResponse{} - json.NewDecoder(resp.Body).Decode(r) - - for _, m := range r.Result { - key := strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(m.Name)), "/", "") - data[key] = m.Name - } - - f, err := os.Create("symbols.go") - if err != nil { - log.Fatal(err) - } - - defer f.Close() - - err = packageTemplate.Execute(f, data) - if err != nil { - log.Fatal(err) - } -} diff --git a/pkg/exchange/ftx/orderbook_snapshot.json b/pkg/exchange/ftx/orderbook_snapshot.json deleted file mode 100644 index ca912fa49..000000000 --- a/pkg/exchange/ftx/orderbook_snapshot.json +++ /dev/null @@ -1,814 +0,0 @@ -{ - "channel": "orderbook", - "market": "BTC/USDT", - "type": "partial", - "data": { - "time": 1614520368.9313016, - "checksum": 2150525410, - "bids": [ - [ - 44555.0, - 3.3968 - ], - [ - 44554.0, - 0.0561 - ], - [ - 44548.0, - 0.1683 - ], - [ - 44542.0, - 0.1762 - ], - [ - 44540.0, - 0.0433 - ], - [ - 44539.0, - 4.1616 - ], - [ - 44534.0, - 0.0234 - ], - [ - 44533.0, - 33.1201 - ], - [ - 44532.0, - 8.2272 - ], - [ - 44531.0, - 0.3364 - ], - [ - 44530.0, - 0.0011 - ], - [ - 44527.0, - 0.0074 - ], - [ - 44526.0, - 0.0117 - ], - [ - 44525.0, - 0.4514 - ], - [ - 44520.0, - 0.001 - ], - [ - 44518.0, - 0.1054 - ], - [ - 44517.0, - 0.0077 - ], - [ - 44512.0, - 0.8512 - ], - [ - 44511.0, - 31.8569 - ], - [ - 44510.0, - 0.001 - ], - [ - 44507.0, - 0.0234 - ], - [ - 44506.0, - 0.382 - ], - [ - 44505.0, - 0.0468 - ], - [ - 44501.0, - 0.0082 - ], - [ - 44500.0, - 0.501 - ], - [ - 44498.0, - 0.001 - ], - [ - 44496.0, - 0.0269 - ], - [ - 44490.0, - 0.001 - ], - [ - 44480.0, - 0.001 - ], - [ - 44479.0, - 0.0306 - ], - [ - 44478.0, - 0.01 - ], - [ - 44477.0, - 0.302 - ], - [ - 44470.0, - 0.001 - ], - [ - 44469.0, - 0.0001 - ], - [ - 44460.0, - 0.001 - ], - [ - 44454.0, - 0.001 - ], - [ - 44450.0, - 0.0019 - ], - [ - 44448.0, - 0.0005 - ], - [ - 44440.0, - 0.001 - ], - [ - 44439.0, - 28.9321 - ], - [ - 44430.0, - 0.001 - ], - [ - 44420.0, - 0.001 - ], - [ - 44416.0, - 0.0001 - ], - [ - 44411.0, - 0.0984 - ], - [ - 44410.0, - 0.001 - ], - [ - 44409.0, - 0.001 - ], - [ - 44408.0, - 0.0004 - ], - [ - 44407.0, - 0.0002 - ], - [ - 44400.0, - 0.001 - ], - [ - 44397.0, - 0.0002 - ], - [ - 44391.0, - 0.0004 - ], - [ - 44390.0, - 0.001 - ], - [ - 44389.0, - 43.3904 - ], - [ - 44380.0, - 0.001 - ], - [ - 44376.0, - 0.0001 - ], - [ - 44375.0, - 0.0001 - ], - [ - 44372.0, - 0.0002 - ], - [ - 44370.0, - 0.0012 - ], - [ - 44365.0, - 0.001 - ], - [ - 44363.0, - 0.0004 - ], - [ - 44360.0, - 0.001 - ], - [ - 44354.0, - 54.0385 - ], - [ - 44350.0, - 0.0028 - ], - [ - 44346.0, - 0.0001 - ], - [ - 44340.0, - 0.0013 - ], - [ - 44338.0, - 0.0002 - ], - [ - 44336.0, - 39.6518 - ], - [ - 44333.0, - 0.0001 - ], - [ - 44330.0, - 0.001 - ], - [ - 44329.0, - 0.5014 - ], - [ - 44326.0, - 0.0002 - ], - [ - 44322.0, - 0.001 - ], - [ - 44321.0, - 0.001 - ], - [ - 44320.0, - 0.001 - ], - [ - 44314.0, - 0.0007 - ], - [ - 44310.0, - 0.001 - ], - [ - 44306.0, - 0.0001 - ], - [ - 44300.0, - 33.2836 - ], - [ - 44292.0, - 0.0035 - ], - [ - 44291.0, - 0.0004 - ], - [ - 44290.0, - 0.001 - ], - [ - 44287.0, - 39.717 - ], - [ - 44285.0, - 0.0439 - ], - [ - 44281.0, - 1.0294 - ], - [ - 44280.0, - 0.001 - ], - [ - 44277.0, - 0.001 - ], - [ - 44275.0, - 0.0165 - ], - [ - 44270.0, - 0.001 - ], - [ - 44268.0, - 48.31 - ], - [ - 44260.0, - 0.0011 - ], - [ - 44254.0, - 0.0003 - ], - [ - 44250.0, - 0.0031 - ], - [ - 44246.0, - 0.0002 - ], - [ - 44244.0, - 0.0001 - ], - [ - 44241.0, - 0.0009 - ], - [ - 44240.0, - 0.001 - ], - [ - 44233.0, - 0.001 - ], - [ - 44230.0, - 0.001 - ], - [ - 44224.0, - 0.0001 - ], - [ - 44222.0, - 0.0002 - ] - ], - "asks": [ - [ - 44574.0, - 0.4591 - ], - [ - 44579.0, - 0.15 - ], - [ - 44582.0, - 2.9122 - ], - [ - 44583.0, - 0.1683 - ], - [ - 44584.0, - 0.5 - ], - [ - 44588.0, - 0.0433 - ], - [ - 44590.0, - 8.6379 - ], - [ - 44593.0, - 0.405 - ], - [ - 44595.0, - 0.5988 - ], - [ - 44596.0, - 0.06 - ], - [ - 44605.0, - 0.6927 - ], - [ - 44606.0, - 0.3365 - ], - [ - 44616.0, - 0.1752 - ], - [ - 44617.0, - 0.0215 - ], - [ - 44620.0, - 0.008 - ], - [ - 44629.0, - 0.0078 - ], - [ - 44630.0, - 0.101 - ], - [ - 44631.0, - 0.246 - ], - [ - 44632.0, - 0.01 - ], - [ - 44635.0, - 0.2997 - ], - [ - 44636.0, - 26.777 - ], - [ - 44639.0, - 0.662 - ], - [ - 44642.0, - 0.0078 - ], - [ - 44650.0, - 0.0009 - ], - [ - 44651.0, - 0.0001 - ], - [ - 44652.0, - 0.0079 - ], - [ - 44653.0, - 0.0003 - ], - [ - 44654.0, - 0.354 - ], - [ - 44661.0, - 0.0306 - ], - [ - 44666.0, - 0.0002 - ], - [ - 44667.0, - 0.0009 - ], - [ - 44668.0, - 0.0234 - ], - [ - 44672.0, - 25.923 - ], - [ - 44673.0, - 0.1 - ], - [ - 44674.0, - 0.001 - ], - [ - 44675.0, - 0.0467 - ], - [ - 44678.0, - 0.1286 - ], - [ - 44680.0, - 0.0467 - ], - [ - 44684.0, - 0.0117 - ], - [ - 44687.0, - 0.0351 - ], - [ - 44689.0, - 0.1052 - ], - [ - 44693.0, - 0.0132 - ], - [ - 44699.0, - 0.0984 - ], - [ - 44700.0, - 0.671 - ], - [ - 44709.0, - 0.0007 - ], - [ - 44713.0, - 45.9031 - ], - [ - 44714.0, - 0.0001 - ], - [ - 44719.0, - 0.001 - ], - [ - 44727.0, - 0.0004 - ], - [ - 44728.0, - 0.0002 - ], - [ - 44735.0, - 0.0003 - ], - [ - 44744.0, - 64.7511 - ], - [ - 44750.0, - 0.0018 - ], - [ - 44763.0, - 0.001 - ], - [ - 44775.0, - 0.0006 - ], - [ - 44781.0, - 0.0001 - ], - [ - 44782.0, - 34.2206 - ], - [ - 44784.0, - 0.0001 - ], - [ - 44790.0, - 0.0002 - ], - [ - 44796.0, - 0.001 - ], - [ - 44799.0, - 0.0002 - ], - [ - 44800.0, - 0.0011 - ], - [ - 44806.0, - 0.0165 - ], - [ - 44807.0, - 0.001 - ], - [ - 44813.0, - 0.0001 - ], - [ - 44814.0, - 0.0003 - ], - [ - 44816.0, - 0.0002 - ], - [ - 44820.0, - 38.3495 - ], - [ - 44822.0, - 0.0026 - ], - [ - 44836.0, - 0.0001 - ], - [ - 44846.0, - 50.1127 - ], - [ - 44850.0, - 0.0018 - ], - [ - 44851.0, - 0.001 - ], - [ - 44859.0, - 0.0003 - ], - [ - 44867.0, - 66.5987 - ], - [ - 44876.0, - 1.0294 - ], - [ - 44885.0, - 0.0005 - ], - [ - 44888.0, - 0.0002 - ], - [ - 44889.0, - 0.0003 - ], - [ - 44895.0, - 0.001 - ], - [ - 44897.0, - 0.0443 - ], - [ - 44900.0, - 40.9965 - ], - [ - 44909.0, - 0.0008 - ], - [ - 44913.0, - 0.0001 - ], - [ - 44926.0, - 45.4838 - ], - [ - 44928.0, - 70.5138 - ], - [ - 44938.0, - 0.0005 - ], - [ - 44939.0, - 0.001 - ], - [ - 44949.0, - 0.0004 - ], - [ - 44950.0, - 0.0019 - ], - [ - 44959.0, - 0.0002 - ], - [ - 44962.0, - 0.0002 - ], - [ - 44979.0, - 0.0002 - ], - [ - 44982.0, - 68.1033 - ], - [ - 44983.0, - 0.001 - ], - [ - 44999.0, - 0.0003 - ], - [ - 45000.0, - 0.0273 - ], - [ - 45002.0, - 0.0002 - ], - [ - 45009.0, - 0.0003 - ], - [ - 45010.0, - 0.0003 - ] - ], - "action": "partial" - } -} diff --git a/pkg/exchange/ftx/orderbook_update.json b/pkg/exchange/ftx/orderbook_update.json deleted file mode 100644 index 51931ed80..000000000 --- a/pkg/exchange/ftx/orderbook_update.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "channel": "orderbook", - "market": "BTC/USDT", - "type": "update", - "data": { - "time": 1614737706.650016, - "checksum": 3976343467, - "bids": [ - [ - 48763.0, - 0.5001 - ] - ], - "asks": [ - [ - 48826.0, - 0.3385 - ], - [ - 48929.0, - 26.8713 - ] - ], - "action": "update" - } -} \ No newline at end of file diff --git a/pkg/exchange/ftx/rest.go b/pkg/exchange/ftx/rest.go deleted file mode 100644 index 18282551c..000000000 --- a/pkg/exchange/ftx/rest.go +++ /dev/null @@ -1,269 +0,0 @@ -package ftx - -import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strconv" - "time" - - "github.com/pkg/errors" - - "github.com/c9s/bbgo/pkg/util" -) - -type transferRequest struct { - *restRequest -} - -type TransferPayload struct { - Coin string - Size float64 - Source string - Destination string -} - -func (r *restRequest) Transfer(ctx context.Context, p TransferPayload) (transferResponse, error) { - resp, err := r. - Method("POST"). - ReferenceURL("api/subaccounts/transfer"). - Payloads(map[string]interface{}{ - "coin": p.Coin, - "size": p.Size, - "source": p.Source, - "destination": p.Destination, - }). - DoAuthenticatedRequest(ctx) - if err != nil { - return transferResponse{}, err - } - - var t transferResponse - if err := json.Unmarshal(resp.Body, &t); err != nil { - return transferResponse{}, fmt.Errorf("failed to unmarshal transfer response body to json: %w", err) - } - - return t, nil -} - -type restRequest struct { - *walletRequest - *marketRequest - *transferRequest - - key, secret string - // Optional sub-account name - sub string - - c *http.Client - baseURL *url.URL - refURL string - // http method, e.g., GET or POST - m string - - // query string - q map[string]string - - // payload - p map[string]interface{} - - // object id - id string -} - -func newRestRequest(c *http.Client, baseURL *url.URL) *restRequest { - r := &restRequest{ - c: c, - baseURL: baseURL, - q: make(map[string]string), - p: make(map[string]interface{}), - } - - r.marketRequest = &marketRequest{restRequest: r} - r.walletRequest = &walletRequest{restRequest: r} - return r -} - -func (r *restRequest) Auth(key, secret string) *restRequest { - r.key = key - // pragma: allowlist nextline secret - r.secret = secret - return r -} - -func (r *restRequest) SubAccount(subAccount string) *restRequest { - r.sub = subAccount - return r -} - -func (r *restRequest) Method(method string) *restRequest { - r.m = method - return r -} - -func (r *restRequest) ReferenceURL(refURL string) *restRequest { - r.refURL = refURL - return r -} - -func (r *restRequest) buildURL() (*url.URL, error) { - u := r.refURL - if len(r.id) > 0 { - u = u + "/" + r.id - } - refURL, err := url.Parse(u) - if err != nil { - return nil, err - } - - return r.baseURL.ResolveReference(refURL), nil -} - -func (r *restRequest) ID(id string) *restRequest { - r.id = id - return r -} - -func (r *restRequest) Payloads(payloads map[string]interface{}) *restRequest { - for k, v := range payloads { - r.p[k] = v - } - return r -} - -func (r *restRequest) Query(query map[string]string) *restRequest { - for k, v := range query { - r.q[k] = v - } - return r -} - -func (r *restRequest) DoAuthenticatedRequest(ctx context.Context) (*util.Response, error) { - req, err := r.newAuthenticatedRequest(ctx) - if err != nil { - return nil, err - } - - return r.sendRequest(req) -} - -func (r *restRequest) newAuthenticatedRequest(ctx context.Context) (*http.Request, error) { - u, err := r.buildURL() - if err != nil { - return nil, err - } - - var jsonPayload []byte - if len(r.p) > 0 { - var err2 error - jsonPayload, err2 = json.Marshal(r.p) - if err2 != nil { - return nil, fmt.Errorf("can't marshal payload map to json: %w", err2) - } - } - - req, err := http.NewRequestWithContext(ctx, r.m, u.String(), bytes.NewBuffer(jsonPayload)) - if err != nil { - return nil, err - } - - ts := strconv.FormatInt(timestamp(), 10) - p := fmt.Sprintf("%s%s%s", ts, r.m, u.Path) - if len(r.q) > 0 { - rq := u.Query() - for k, v := range r.q { - rq.Add(k, v) - } - req.URL.RawQuery = rq.Encode() - p += "?" + req.URL.RawQuery - } - if len(jsonPayload) > 0 { - p += string(jsonPayload) - } - signature := sign(r.secret, p) - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("FTX-KEY", r.key) - req.Header.Set("FTX-SIGN", signature) - req.Header.Set("FTX-TS", ts) - if r.sub != "" { - req.Header.Set("FTX-SUBACCOUNT", r.sub) - } - - return req, nil -} - -func sign(secret, body string) string { - mac := hmac.New(sha256.New, []byte(secret)) - mac.Write([]byte(body)) - return hex.EncodeToString(mac.Sum(nil)) -} - -func timestamp() int64 { - return time.Now().UnixNano() / int64(time.Millisecond) -} - -func (r *restRequest) sendRequest(req *http.Request) (*util.Response, error) { - resp, err := r.c.Do(req) - if err != nil { - return nil, err - } - - // newResponse reads the response body and return a new Response object - response, err := util.NewResponse(resp) - if err != nil { - return response, err - } - - // Check error, if there is an error, return the ErrorResponse struct type - if response.IsError() { - errorResponse, err := toErrorResponse(response) - if err != nil { - return response, err - } - return response, errorResponse - } - - return response, nil -} - -type ErrorResponse struct { - *util.Response - - IsSuccess bool `json:"success"` - ErrorString string `json:"error,omitempty"` -} - -func (r *ErrorResponse) Error() string { - return fmt.Sprintf("%s %s %d, success: %t, err: %s", - r.Response.Request.Method, - r.Response.Request.URL.String(), - r.Response.StatusCode, - r.IsSuccess, - r.ErrorString, - ) -} - -func toErrorResponse(response *util.Response) (*ErrorResponse, error) { - errorResponse := &ErrorResponse{Response: response} - - if response.IsJSON() { - var err = response.DecodeJSON(errorResponse) - if err != nil { - return nil, errors.Wrapf(err, "failed to decode json for response: %d %s", response.StatusCode, string(response.Body)) - } - - if errorResponse.IsSuccess { - return nil, fmt.Errorf("response.Success should be false") - } - return errorResponse, nil - } - - return errorResponse, fmt.Errorf("unexpected response content type %s", response.Header.Get("content-type")) -} diff --git a/pkg/exchange/ftx/rest_market_request.go b/pkg/exchange/ftx/rest_market_request.go deleted file mode 100644 index aeb41e17a..000000000 --- a/pkg/exchange/ftx/rest_market_request.go +++ /dev/null @@ -1,53 +0,0 @@ -package ftx - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "time" - - "github.com/c9s/bbgo/pkg/types" -) - -type marketRequest struct { - *restRequest -} - -/* -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 != nil { - q["start_time"] = strconv.FormatInt(start.Unix(), 10) - } - - if end != nil { - 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 deleted file mode 100644 index 15da5e606..000000000 --- a/pkg/exchange/ftx/rest_responses.go +++ /dev/null @@ -1,391 +0,0 @@ -package ftx - -import ( - "fmt" - "strings" - "time" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -// ex: 2019-03-05T09:56:55.728933+00:00 -const timeLayout = "2006-01-02T15:04:05.999999Z07:00" - -type datetime struct { - time.Time -} - -func parseDatetime(s string) (time.Time, error) { - return time.Parse(timeLayout, s) -} - -// used in unit test -func mustParseDatetime(s string) time.Time { - t, err := parseDatetime(s) - if err != nil { - panic(err) - } - return t -} - -func (d *datetime) UnmarshalJSON(b []byte) error { - // remove double quote from json string - s := strings.Trim(string(b), "\"") - if len(s) == 0 { - d.Time = time.Time{} - return nil - } - t, err := parseDatetime(s) - if err != nil { - return err - } - d.Time = t - return nil -} - -/* -{ - "success": true, - "result": { - "backstopProvider": true, - "collateral": 3568181.02691129, - "freeCollateral": 1786071.456884368, - "initialMarginRequirement": 0.12222384240257728, - "leverage": 10, - "liquidating": false, - "maintenanceMarginRequirement": 0.07177992558058484, - "makerFee": 0.0002, - "marginFraction": 0.5588433331419503, - "openMarginFraction": 0.2447194090423075, - "takerFee": 0.0005, - "totalAccountValue": 3568180.98341129, - "totalPositionSize": 6384939.6992, - "username": "user@domain.com", - "positions": [ - { - "cost": -31.7906, - "entryPrice": 138.22, - "future": "ETH-PERP", - "initialMarginRequirement": 0.1, - "longOrderSize": 1744.55, - "maintenanceMarginRequirement": 0.04, - "netSize": -0.23, - "openSize": 1744.32, - "realizedPnl": 3.39441714, - "shortOrderSize": 1732.09, - "side": "sell", - "size": 0.23, - "unrealizedPnl": 0 - } - ] - } -} -*/ -type accountResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result account `json:"result"` -} - -type account struct { - MakerFee fixedpoint.Value `json:"makerFee"` - TakerFee fixedpoint.Value `json:"takerFee"` - TotalAccountValue fixedpoint.Value `json:"totalAccountValue"` -} - -type positionsResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result []position `json:"result"` -} - -/* -{ - "cost": -31.7906, - "entryPrice": 138.22, - "estimatedLiquidationPrice": 152.1, - "future": "ETH-PERP", - "initialMarginRequirement": 0.1, - "longOrderSize": 1744.55, - "maintenanceMarginRequirement": 0.04, - "netSize": -0.23, - "openSize": 1744.32, - "realizedPnl": 3.39441714, - "shortOrderSize": 1732.09, - "side": "sell", - "size": 0.23, - "unrealizedPnl": 0, - "collateralUsed": 3.17906 -} -*/ -type position struct { - Cost fixedpoint.Value `json:"cost"` - EntryPrice fixedpoint.Value `json:"entryPrice"` - EstimatedLiquidationPrice fixedpoint.Value `json:"estimatedLiquidationPrice"` - Future string `json:"future"` - InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"` - LongOrderSize fixedpoint.Value `json:"longOrderSize"` - MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"` - NetSize fixedpoint.Value `json:"netSize"` - OpenSize fixedpoint.Value `json:"openSize"` - RealizedPnl fixedpoint.Value `json:"realizedPnl"` - ShortOrderSize fixedpoint.Value `json:"shortOrderSize"` - Side string `json:"Side"` - Size fixedpoint.Value `json:"size"` - UnrealizedPnl fixedpoint.Value `json:"unrealizedPnl"` - CollateralUsed fixedpoint.Value `json:"collateralUsed"` -} - -type balances struct { // nolint:golint,deadcode - Success bool `json:"success"` - - Result []struct { - Coin string `json:"coin"` - Free fixedpoint.Value `json:"free"` - Total fixedpoint.Value `json:"total"` - } `json:"result"` -} - -/* -[ - { - "name": "BTC/USD", - "enabled": true, - "postOnly": false, - "priceIncrement": 1.0, - "sizeIncrement": 0.0001, - "minProvideSize": 0.0001, - "last": 59039.0, - "bid": 59038.0, - "ask": 59040.0, - "price": 59039.0, - "type": "spot", - "baseCurrency": "BTC", - "quoteCurrency": "USD", - "underlying": null, - "restricted": false, - "highLeverageFeeExempt": true, - "change1h": 0.0015777151969599294, - "change24h": 0.05475756601279165, - "changeBod": -0.0035107262814994852, - "quoteVolume24h": 316493675.5463, - "volumeUsd24h": 316493675.5463 - } -] -*/ -type marketsResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result []market `json:"result"` -} - -type market struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` - PostOnly bool `json:"postOnly"` - PriceIncrement fixedpoint.Value `json:"priceIncrement"` - SizeIncrement fixedpoint.Value `json:"sizeIncrement"` - MinProvideSize fixedpoint.Value `json:"minProvideSize"` - Last fixedpoint.Value `json:"last"` - Bid fixedpoint.Value `json:"bid"` - Ask fixedpoint.Value `json:"ask"` - Price fixedpoint.Value `json:"price"` - Type string `json:"type"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - Underlying string `json:"underlying"` - Restricted bool `json:"restricted"` - HighLeverageFeeExempt bool `json:"highLeverageFeeExempt"` - Change1h fixedpoint.Value `json:"change1h"` - Change24h fixedpoint.Value `json:"change24h"` - ChangeBod fixedpoint.Value `json:"changeBod"` - QuoteVolume24h fixedpoint.Value `json:"quoteVolume24h"` - VolumeUsd24h fixedpoint.Value `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 fixedpoint.Value `json:"close"` - High fixedpoint.Value `json:"high"` - Low fixedpoint.Value `json:"low"` - Open fixedpoint.Value `json:"open"` - StartTime datetime `json:"startTime"` - Volume fixedpoint.Value `json:"volume"` -} - -type ordersHistoryResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result []order `json:"result"` - HasMoreData bool `json:"hasMoreData"` -} - -type ordersResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - - Result []order `json:"result"` -} - -type cancelOrderResponse struct { // nolint:golint,deadcode - Success bool `json:"success"` - Result string `json:"result"` -} - -type order struct { - CreatedAt datetime `json:"createdAt"` - FilledSize fixedpoint.Value `json:"filledSize"` - // Future field is not defined in the response format table but in the response example. - Future string `json:"future"` - ID int64 `json:"id"` - Market string `json:"market"` - Price fixedpoint.Value `json:"price"` - AvgFillPrice fixedpoint.Value `json:"avgFillPrice"` - RemainingSize fixedpoint.Value `json:"remainingSize"` - Side string `json:"side"` - Size fixedpoint.Value `json:"size"` - Status string `json:"status"` - Type string `json:"type"` - ReduceOnly bool `json:"reduceOnly"` - Ioc bool `json:"ioc"` - PostOnly bool `json:"postOnly"` - ClientId string `json:"clientId"` - Liquidation bool `json:"liquidation"` -} - -type orderResponse struct { - Success bool `json:"success"` - - 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 datetime `json:"confirmedTime"` - Fee fixedpoint.Value `json:"fee"` - SentTime datetime `json:"sentTime"` - Size fixedpoint.Value `json:"size"` - Status string `json:"status"` - Time datetime `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"` -} - -type fillsResponse struct { - Success bool `json:"success"` - Result []fill `json:"result"` -} - -/* -{ - "id": 123, - "market": "TSLA/USD", - "future": null, - "baseCurrency": "TSLA", - "quoteCurrency": "USD", - "type": "order", - "side": "sell", - "price": 672.5, - "size": 1.0, - "orderId": 456, - "time": "2021-02-23T09:29:08.534000+00:00", - "tradeId": 789, - "feeRate": -5e-6, - "fee": -0.0033625, - "feeCurrency": "USD", - "liquidity": "maker" -} -*/ -type fill struct { - ID int64 `json:"id"` - Market string `json:"market"` - Future string `json:"future"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - Type string `json:"type"` - Side types.SideType `json:"side"` - Price fixedpoint.Value `json:"price"` - Size fixedpoint.Value `json:"size"` - OrderId uint64 `json:"orderId"` - Time datetime `json:"time"` - TradeId uint64 `json:"tradeId"` - FeeRate fixedpoint.Value `json:"feeRate"` - Fee fixedpoint.Value `json:"fee"` - FeeCurrency string `json:"feeCurrency"` - Liquidity string `json:"liquidity"` -} - -type transferResponse struct { - Success bool `json:"success"` - Result transfer `json:"result"` -} - -type transfer struct { - Id uint `json:"id"` - Coin string `json:"coin"` - Size fixedpoint.Value `json:"size"` - Time string `json:"time"` - Notes string `json:"notes"` - Status string `json:"status"` -} - -func (t *transfer) String() string { - return fmt.Sprintf("%+v", *t) -} diff --git a/pkg/exchange/ftx/rest_test.go b/pkg/exchange/ftx/rest_test.go deleted file mode 100644 index ca1adaf3a..000000000 --- a/pkg/exchange/ftx/rest_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package ftx - -import ( - "bytes" - "io/ioutil" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/util" -) - -func Test_toErrorResponse(t *testing.T) { - r, err := util.NewResponse(&http.Response{ - Header: http.Header{}, - StatusCode: 200, - Body: ioutil.NopCloser(bytes.NewReader([]byte(`{"Success": true}`))), - }) - assert.NoError(t, err) - - _, err = toErrorResponse(r) - assert.EqualError(t, err, "unexpected response content type ") - r.Header.Set("content-type", "text/json") - - _, err = toErrorResponse(r) - assert.EqualError(t, err, "response.Success should be false") - - r.Body = []byte(`{"error":"Not logged in","Success":false}`) - errResp, err := toErrorResponse(r) - assert.NoError(t, err) - assert.False(t, errResp.IsSuccess) - assert.Equal(t, "Not logged in", errResp.ErrorString) -} diff --git a/pkg/exchange/ftx/rest_wallet_request.go b/pkg/exchange/ftx/rest_wallet_request.go deleted file mode 100644 index 039a32553..000000000 --- a/pkg/exchange/ftx/rest_wallet_request.go +++ /dev/null @@ -1,44 +0,0 @@ -package ftx - -import ( - "context" - "encoding/json" - "fmt" - "strconv" - "time" -) - -type walletRequest struct { - *restRequest -} - -func (r *walletRequest) DepositHistory(ctx context.Context, since time.Time, until time.Time, limit int) (depositHistoryResponse, error) { - q := make(map[string]string) - if limit > 0 { - q["limit"] = strconv.Itoa(limit) - } - - if since != (time.Time{}) { - q["start_time"] = strconv.FormatInt(since.Unix(), 10) - } - if until != (time.Time{}) { - q["end_time"] = strconv.FormatInt(until.Unix(), 10) - } - - resp, err := r. - Method("GET"). - ReferenceURL("api/wallet/deposits"). - Query(q). - 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 -} diff --git a/pkg/exchange/ftx/stream.go b/pkg/exchange/ftx/stream.go deleted file mode 100644 index 6a70a249d..000000000 --- a/pkg/exchange/ftx/stream.go +++ /dev/null @@ -1,259 +0,0 @@ -package ftx - -import ( - "context" - "fmt" - "time" - - "github.com/gorilla/websocket" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/net/websocketbase" - "github.com/c9s/bbgo/pkg/types" -) - -const endpoint = "wss://ftx.com/ws/" - -type Stream struct { - *types.StandardStream - - ws *websocketbase.WebsocketClientBase - exchange *Exchange - - key string - secret string - subAccount string - - // subscriptions are only accessed in single goroutine environment, so I don't use mutex to protect them - subscriptions []websocketRequest - klineSubscriptions []klineSubscription -} - -type klineSubscription struct { - symbol string - interval types.Interval -} - -func NewStream(key, secret string, subAccount string, e *Exchange) *Stream { - s := &Stream{ - exchange: e, - key: key, - // pragma: allowlist nextline secret - secret: secret, - subAccount: subAccount, - StandardStream: &types.StandardStream{}, - ws: websocketbase.NewWebsocketClientBase(endpoint, 3*time.Second), - } - - s.ws.OnMessage((&messageHandler{StandardStream: s.StandardStream}).handleMessage) - s.ws.OnConnected(func(conn *websocket.Conn) { - subs := []websocketRequest{newLoginRequest(s.key, s.secret, time.Now(), s.subAccount)} - subs = append(subs, s.subscriptions...) - for _, sub := range subs { - if err := conn.WriteJSON(sub); err != nil { - s.ws.EmitError(fmt.Errorf("failed to send subscription: %+v", sub)) - } - } - - s.EmitConnect() - }) - - return s -} - -func (s *Stream) Connect(ctx context.Context) error { - // If it's not public only, let's do the authentication. - if !s.PublicOnly { - s.subscribePrivateEvents() - } - - if err := s.ws.Connect(ctx); err != nil { - return err - } - s.EmitStart() - - go s.pollKLines(ctx) - go s.pollBalances(ctx) - - go func() { - // https://docs.ftx.com/?javascript#request-process - tk := time.NewTicker(15 * time.Second) - defer tk.Stop() - for { - select { - case <-ctx.Done(): - if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) { - logger.WithError(err).Errorf("context returned error") - } - - case <-tk.C: - if err := s.ws.Conn().WriteJSON(websocketRequest{ - Operation: ping, - }); err != nil { - logger.WithError(err).Warnf("failed to ping, try in next tick") - } - } - } - }() - return nil -} - -func (s *Stream) subscribePrivateEvents() { - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: privateOrdersChannel, - }) - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: privateTradesChannel, - }) -} - -func (s *Stream) addSubscription(request websocketRequest) { - s.subscriptions = append(s.subscriptions, request) -} - -func (s *Stream) Subscribe(channel types.Channel, symbol string, option types.SubscribeOptions) { - switch channel { - case types.BookChannel: - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: orderBookChannel, - Market: toLocalSymbol(TrimUpperString(symbol)), - }) - return - case types.BookTickerChannel: - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: bookTickerChannel, - Market: toLocalSymbol(TrimUpperString(symbol)), - }) - return - case types.KLineChannel: - // FTX does not support kline channel, do polling - interval := types.Interval(option.Interval) - ks := klineSubscription{symbol: symbol, interval: interval} - s.klineSubscriptions = append(s.klineSubscriptions, ks) - return - case types.MarketTradeChannel: - s.addSubscription(websocketRequest{ - Operation: subscribe, - Channel: marketTradeChannel, - Market: toLocalSymbol(TrimUpperString(symbol)), - }) - return - default: - panic("only support book/kline/trade channel now") - } -} - -func (s *Stream) pollBalances(ctx context.Context) { - ticker := time.NewTicker(15 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - - case <-ticker.C: - balances, err := s.exchange.QueryAccountBalances(ctx) - if err != nil { - log.WithError(err).Errorf("query balance error") - continue - } - s.EmitBalanceSnapshot(balances) - } - } -} - -func (s *Stream) pollKLines(ctx context.Context) { - lastClosed := make(map[string]map[types.Interval]time.Time, 0) - // get current kline candle - for _, sub := range s.klineSubscriptions { - klines := getLast2KLine(s.exchange, ctx, sub.symbol, sub.interval) - lastClosed[sub.symbol] = make(map[types.Interval]time.Time, 0) - if len(klines) > 0 { - // handle mutiple klines, get the latest one - if lastClosed[sub.symbol][sub.interval].Unix() < klines[0].StartTime.Unix() { - s.EmitKLine(klines[0]) - s.EmitKLineClosed(klines[0]) - lastClosed[sub.symbol][sub.interval] = klines[0].StartTime.Time() - } - - if len(klines) > 1 { - s.EmitKLine(klines[1]) - } - } - } - - // the highest resolution of kline is 1min - ticker := time.NewTicker(time.Second * 30) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) { - logger.WithError(err).Errorf("context returned error") - } - return - case <-ticker.C: - now := time.Now().Truncate(time.Minute) - for _, sub := range s.klineSubscriptions { - subTime := now.Truncate(sub.interval.Duration()) - if now != subTime { - // not in the checking time slot, check next subscription - continue - } - klines := getLast2KLine(s.exchange, ctx, sub.symbol, sub.interval) - - if len(klines) > 0 { - // handle mutiple klines, get the latest one - if lastClosed[sub.symbol][sub.interval].Unix() < klines[0].StartTime.Unix() { - s.EmitKLine(klines[0]) - s.EmitKLineClosed(klines[0]) - lastClosed[sub.symbol][sub.interval] = klines[0].StartTime.Time() - } - - if len(klines) > 1 { - s.EmitKLine(klines[1]) - } - } - } - } - } -} - -func getLast2KLine(e *Exchange, ctx context.Context, symbol string, interval types.Interval) []types.KLine { - // set since to more 30s ago to avoid getting no kline candle - since := time.Now().Add(time.Duration(interval.Minutes()*-3) * time.Minute) - klines, err := e.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{ - StartTime: &since, - Limit: 2, - }) - if err != nil { - logger.WithError(err).Errorf("failed to get kline data") - return klines - } - - return klines -} - -func getLastClosedKLine(e *Exchange, ctx context.Context, symbol string, interval types.Interval) []types.KLine { - // set since to more 30s ago to avoid getting no kline candle - klines := getLast2KLine(e, ctx, symbol, interval) - if len(klines) == 0 { - return []types.KLine{} - } - return []types.KLine{klines[0]} -} - -func (s *Stream) Close() error { - s.subscriptions = nil - if s.ws != nil { - return s.ws.Conn().Close() - } - return nil -} diff --git a/pkg/exchange/ftx/stream_message_handler.go b/pkg/exchange/ftx/stream_message_handler.go deleted file mode 100644 index 98744622c..000000000 --- a/pkg/exchange/ftx/stream_message_handler.go +++ /dev/null @@ -1,169 +0,0 @@ -package ftx - -import ( - "encoding/json" - - "github.com/c9s/bbgo/pkg/types" -) - -type messageHandler struct { - *types.StandardStream -} - -func (h *messageHandler) handleMessage(message []byte) { - var r websocketResponse - if err := json.Unmarshal(message, &r); err != nil { - logger.WithError(err).Errorf("failed to unmarshal resp: %s", string(message)) - return - } - - if r.Type == errRespType { - logger.Errorf("receives err: %+v", r) - return - } - - if r.Type == pongRespType { - return - } - - switch r.Channel { - case orderBookChannel: - h.handleOrderBook(r) - case bookTickerChannel: - h.handleBookTicker(r) - case marketTradeChannel: - h.handleMarketTrade(r) - case privateOrdersChannel: - h.handlePrivateOrders(r) - case privateTradesChannel: - h.handleTrades(r) - default: - logger.Warnf("unsupported message type: %+v", r.Type) - } -} - -// {"type": "subscribed", "channel": "orderbook", "market": "BTC/USDT"} -func (h messageHandler) handleSubscribedMessage(response websocketResponse) { - r, err := response.toSubscribedResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the subscribed message") - return - } - logger.Info(r) -} - -func (h *messageHandler) handleOrderBook(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - r, err := response.toPublicOrderBookResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the public orderbook") - return - } - - globalOrderBook, err := toGlobalOrderBook(r) - if err != nil { - logger.WithError(err).Errorf("failed to generate orderbook snapshot") - return - } - - switch r.Type { - case partialRespType: - if err := r.verifyChecksum(); err != nil { - logger.WithError(err).Errorf("invalid orderbook snapshot") - return - } - h.EmitBookSnapshot(globalOrderBook) - case updateRespType: - // emit updates, not the whole orderbook - h.EmitBookUpdate(globalOrderBook) - default: - logger.Errorf("unsupported order book data type %s", r.Type) - return - } -} - -func (h *messageHandler) handleMarketTrade(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - trades, err := response.toMarketTradeResponse() - if err != nil { - logger.WithError(err).Errorf("failed to generate market trade %v", response) - return - } - for _, trade := range trades { - h.EmitMarketTrade(trade) - } -} - -func (h *messageHandler) handleBookTicker(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - - r, err := response.toBookTickerResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the book ticker") - return - } - - globalBookTicker, err := toGlobalBookTicker(r) - if err != nil { - logger.WithError(err).Errorf("failed to generate book ticker") - return - } - - switch r.Type { - case updateRespType: - // emit updates, not the whole orderbook - h.EmitBookTickerUpdate(globalBookTicker) - default: - logger.Errorf("unsupported book ticker data type %s", r.Type) - return - } -} - -func (h *messageHandler) handlePrivateOrders(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - - r, err := response.toOrderUpdateResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the order update response") - return - } - - globalOrder, err := toGlobalOrderNew(r.Data) - if err != nil { - logger.WithError(err).Errorf("failed to convert order update to global order") - return - } - h.EmitOrderUpdate(globalOrder) -} - -func (h *messageHandler) handleTrades(response websocketResponse) { - if response.Type == subscribedRespType { - h.handleSubscribedMessage(response) - return - } - - r, err := response.toTradeUpdateResponse() - if err != nil { - logger.WithError(err).Errorf("failed to convert the trade update response") - return - } - - t, err := toGlobalTrade(r.Data) - if err != nil { - logger.WithError(err).Errorf("failed to convert trade update to global trade ") - return - } - h.EmitTradeUpdate(t) -} diff --git a/pkg/exchange/ftx/stream_message_handler_test.go b/pkg/exchange/ftx/stream_message_handler_test.go deleted file mode 100644 index 1f640211f..000000000 --- a/pkg/exchange/ftx/stream_message_handler_test.go +++ /dev/null @@ -1,119 +0,0 @@ -package ftx - -import ( - "database/sql" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -func Test_messageHandler_handleMessage(t *testing.T) { - t.Run("handle order update", func(t *testing.T) { - input := []byte(` -{ - "channel": "orders", - "type": "update", - "data": { - "id": 36379, - "clientId": null, - "market": "OXY-PERP", - "type": "limit", - "side": "sell", - "price": 2.7185, - "size": 1.0, - "status": "closed", - "filledSize": 1.0, - "remainingSize": 0.0, - "reduceOnly": false, - "liquidation": false, - "avgFillPrice": 2.7185, - "postOnly": false, - "ioc": false, - "createdAt": "2021-03-28T06:12:50.991447+00:00" - } -} -`) - - h := &messageHandler{StandardStream: &types.StandardStream{}} - i := 0 - h.OnOrderUpdate(func(order types.Order) { - i++ - assert.Equal(t, types.Order{ - SubmitOrder: types.SubmitOrder{ - ClientOrderID: "", - Symbol: "OXY-PERP", - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Quantity: fixedpoint.One, - Price: fixedpoint.NewFromFloat(2.7185), - TimeInForce: "GTC", - }, - Exchange: types.ExchangeFTX, - OrderID: 36379, - Status: types.OrderStatusFilled, - ExecutedQuantity: fixedpoint.One, - CreationTime: types.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")), - UpdateTime: types.Time(mustParseDatetime("2021-03-28T06:12:50.991447+00:00")), - }, order) - }) - h.handleMessage(input) - assert.Equal(t, 1, i) - }) - - t.Run("handle trade update", func(t *testing.T) { - input := []byte(` -{ - "channel": "fills", - "type": "update", - "data": { - "id": 23427, - "market": "OXY-PERP", - "future": "OXY-PERP", - "baseCurrency": null, - "quoteCurrency": null, - "type": "order", - "side": "buy", - "price": 2.723, - "size": 1.0, - "orderId": 323789, - "time": "2021-03-28T06:12:34.702926+00:00", - "tradeId": 6276431, - "feeRate": 0.00056525, - "fee": 0.00153917575, - "feeCurrency": "USD", - "liquidity": "taker" - } -} -`) - h := &messageHandler{StandardStream: &types.StandardStream{}} - i := 0 - h.OnTradeUpdate(func(trade types.Trade) { - i++ - assert.Equal(t, types.Trade{ - ID: uint64(6276431), - OrderID: uint64(323789), - Exchange: types.ExchangeFTX, - Price: fixedpoint.NewFromFloat(2.723), - Quantity: fixedpoint.One, - QuoteQuantity: fixedpoint.NewFromFloat(2.723 * 1.0), - Symbol: "OXY-PERP", - Side: types.SideTypeBuy, - IsBuyer: true, - IsMaker: false, - Time: types.Time(mustParseDatetime("2021-03-28T06:12:34.702926+00:00")), - Fee: fixedpoint.NewFromFloat(0.00153917575), - FeeCurrency: "USD", - IsMargin: false, - IsIsolated: false, - IsFutures: true, - StrategyID: sql.NullString{}, - PnL: sql.NullFloat64{}, - }, trade) - }) - h.handleMessage(input) - assert.Equal(t, 1, i) - }) -} diff --git a/pkg/exchange/ftx/symbols.go b/pkg/exchange/ftx/symbols.go deleted file mode 100644 index 33cb02296..000000000 --- a/pkg/exchange/ftx/symbols.go +++ /dev/null @@ -1,819 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -package ftx - -var symbolMap = map[string]string{ - "1INCH-0325": "1INCH-0325", - "1INCH-PERP": "1INCH-PERP", - "1INCHUSD": "1INCH/USD", - "AAPL-0325": "AAPL-0325", - "AAPLUSD": "AAPL/USD", - "AAVE-0325": "AAVE-0325", - "AAVE-PERP": "AAVE-PERP", - "AAVEUSD": "AAVE/USD", - "AAVEUSDT": "AAVE/USDT", - "ABNB-0325": "ABNB-0325", - "ABNBUSD": "ABNB/USD", - "ACB-0325": "ACB-0325", - "ACBUSD": "ACB/USD", - "ADA-0325": "ADA-0325", - "ADA-PERP": "ADA-PERP", - "ADABEARUSD": "ADABEAR/USD", - "ADABULLUSD": "ADABULL/USD", - "ADAHALFUSD": "ADAHALF/USD", - "ADAHEDGEUSD": "ADAHEDGE/USD", - "AGLD-PERP": "AGLD-PERP", - "AGLDUSD": "AGLD/USD", - "AKROUSD": "AKRO/USD", - "AKROUSDT": "AKRO/USDT", - "ALCX-PERP": "ALCX-PERP", - "ALCXUSD": "ALCX/USD", - "ALEPHUSD": "ALEPH/USD", - "ALGO-0325": "ALGO-0325", - "ALGO-PERP": "ALGO-PERP", - "ALGOBEARUSD": "ALGOBEAR/USD", - "ALGOBULLUSD": "ALGOBULL/USD", - "ALGOHALFUSD": "ALGOHALF/USD", - "ALGOHEDGEUSD": "ALGOHEDGE/USD", - "ALICE-PERP": "ALICE-PERP", - "ALICEUSD": "ALICE/USD", - "ALPHA-PERP": "ALPHA-PERP", - "ALPHAUSD": "ALPHA/USD", - "ALT-0325": "ALT-0325", - "ALT-PERP": "ALT-PERP", - "ALTBEARUSD": "ALTBEAR/USD", - "ALTBULLUSD": "ALTBULL/USD", - "ALTHALFUSD": "ALTHALF/USD", - "ALTHEDGEUSD": "ALTHEDGE/USD", - "AMC-0325": "AMC-0325", - "AMCUSD": "AMC/USD", - "AMD-0325": "AMD-0325", - "AMDUSD": "AMD/USD", - "AMPL-PERP": "AMPL-PERP", - "AMPLUSD": "AMPL/USD", - "AMPLUSDT": "AMPL/USDT", - "AMZN-0325": "AMZN-0325", - "AMZNUSD": "AMZN/USD", - "APHAUSD": "APHA/USD", - "AR-PERP": "AR-PERP", - "ARKK-0325": "ARKK-0325", - "ARKKUSD": "ARKK/USD", - "ASD-PERP": "ASD-PERP", - "ASDBEARUSD": "ASDBEAR/USD", - "ASDBEARUSDT": "ASDBEAR/USDT", - "ASDBULLUSD": "ASDBULL/USD", - "ASDBULLUSDT": "ASDBULL/USDT", - "ASDHALFUSD": "ASDHALF/USD", - "ASDHEDGEUSD": "ASDHEDGE/USD", - "ASDUSD": "ASD/USD", - "ATLAS-PERP": "ATLAS-PERP", - "ATLASUSD": "ATLAS/USD", - "ATOM-0325": "ATOM-0325", - "ATOM-PERP": "ATOM-PERP", - "ATOMBEARUSD": "ATOMBEAR/USD", - "ATOMBULLUSD": "ATOMBULL/USD", - "ATOMHALFUSD": "ATOMHALF/USD", - "ATOMHEDGEUSD": "ATOMHEDGE/USD", - "ATOMUSD": "ATOM/USD", - "ATOMUSDT": "ATOM/USDT", - "AUDIO-PERP": "AUDIO-PERP", - "AUDIOUSD": "AUDIO/USD", - "AUDIOUSDT": "AUDIO/USDT", - "AURYUSD": "AURY/USD", - "AVAX-0325": "AVAX-0325", - "AVAX-PERP": "AVAX-PERP", - "AVAXBTC": "AVAX/BTC", - "AVAXUSD": "AVAX/USD", - "AVAXUSDT": "AVAX/USDT", - "AXS-PERP": "AXS-PERP", - "AXSUSD": "AXS/USD", - "BABA-0325": "BABA-0325", - "BABAUSD": "BABA/USD", - "BADGER-PERP": "BADGER-PERP", - "BADGERUSD": "BADGER/USD", - "BAL-0325": "BAL-0325", - "BAL-PERP": "BAL-PERP", - "BALBEARUSD": "BALBEAR/USD", - "BALBEARUSDT": "BALBEAR/USDT", - "BALBULLUSD": "BALBULL/USD", - "BALBULLUSDT": "BALBULL/USDT", - "BALHALFUSD": "BALHALF/USD", - "BALHEDGEUSD": "BALHEDGE/USD", - "BALUSD": "BAL/USD", - "BALUSDT": "BAL/USDT", - "BAND-PERP": "BAND-PERP", - "BANDUSD": "BAND/USD", - "BAO-PERP": "BAO-PERP", - "BAOUSD": "BAO/USD", - "BARUSD": "BAR/USD", - "BAT-PERP": "BAT-PERP", - "BATUSD": "BAT/USD", - "BB-0325": "BB-0325", - "BBUSD": "BB/USD", - "BCH-0325": "BCH-0325", - "BCH-PERP": "BCH-PERP", - "BCHBEARUSD": "BCHBEAR/USD", - "BCHBEARUSDT": "BCHBEAR/USDT", - "BCHBTC": "BCH/BTC", - "BCHBULLUSD": "BCHBULL/USD", - "BCHBULLUSDT": "BCHBULL/USDT", - "BCHHALFUSD": "BCHHALF/USD", - "BCHHEDGEUSD": "BCHHEDGE/USD", - "BCHUSD": "BCH/USD", - "BCHUSDT": "BCH/USDT", - "BEARSHITUSD": "BEARSHIT/USD", - "BEARUSD": "BEAR/USD", - "BEARUSDT": "BEAR/USDT", - "BICOUSD": "BICO/USD", - "BILI-0325": "BILI-0325", - "BILIUSD": "BILI/USD", - "BIT-PERP": "BIT-PERP", - "BITO-0325": "BITO-0325", - "BITOUSD": "BITO/USD", - "BITUSD": "BIT/USD", - "BITW-0325": "BITW-0325", - "BITWUSD": "BITW/USD", - "BLTUSD": "BLT/USD", - "BNB-0325": "BNB-0325", - "BNB-PERP": "BNB-PERP", - "BNBBEARUSD": "BNBBEAR/USD", - "BNBBEARUSDT": "BNBBEAR/USDT", - "BNBBTC": "BNB/BTC", - "BNBBULLUSD": "BNBBULL/USD", - "BNBBULLUSDT": "BNBBULL/USDT", - "BNBHALFUSD": "BNBHALF/USD", - "BNBHEDGEUSD": "BNBHEDGE/USD", - "BNBUSD": "BNB/USD", - "BNBUSDT": "BNB/USDT", - "BNT-PERP": "BNT-PERP", - "BNTUSD": "BNT/USD", - "BNTX-0325": "BNTX-0325", - "BNTXUSD": "BNTX/USD", - "BOBA-PERP": "BOBA-PERP", - "BOBAUSD": "BOBA/USD", - "BOLSONARO2022": "BOLSONARO2022", - "BRZ-PERP": "BRZ-PERP", - "BRZUSD": "BRZ/USD", - "BRZUSDT": "BRZ/USDT", - "BSV-0325": "BSV-0325", - "BSV-PERP": "BSV-PERP", - "BSVBEARUSD": "BSVBEAR/USD", - "BSVBEARUSDT": "BSVBEAR/USDT", - "BSVBULLUSD": "BSVBULL/USD", - "BSVBULLUSDT": "BSVBULL/USDT", - "BSVHALFUSD": "BSVHALF/USD", - "BSVHEDGEUSD": "BSVHEDGE/USD", - "BTC-0325": "BTC-0325", - "BTC-0624": "BTC-0624", - "BTC-MOVE-0303": "BTC-MOVE-0303", - "BTC-MOVE-0304": "BTC-MOVE-0304", - "BTC-MOVE-2022Q1": "BTC-MOVE-2022Q1", - "BTC-MOVE-2022Q2": "BTC-MOVE-2022Q2", - "BTC-MOVE-2022Q3": "BTC-MOVE-2022Q3", - "BTC-MOVE-WK-0304": "BTC-MOVE-WK-0304", - "BTC-MOVE-WK-0311": "BTC-MOVE-WK-0311", - "BTC-MOVE-WK-0318": "BTC-MOVE-WK-0318", - "BTC-MOVE-WK-0325": "BTC-MOVE-WK-0325", - "BTC-PERP": "BTC-PERP", - "BTCBRZ": "BTC/BRZ", - "BTCEUR": "BTC/EUR", - "BTCTRYB": "BTC/TRYB", - "BTCUSD": "BTC/USD", - "BTCUSDT": "BTC/USDT", - "BTT-PERP": "BTT-PERP", - "BTTUSD": "BTT/USD", - "BULLSHITUSD": "BULLSHIT/USD", - "BULLUSD": "BULL/USD", - "BULLUSDT": "BULL/USDT", - "BVOLBTC": "BVOL/BTC", - "BVOLUSD": "BVOL/USD", - "BVOLUSDT": "BVOL/USDT", - "BYND-0325": "BYND-0325", - "BYNDUSD": "BYND/USD", - "C98-PERP": "C98-PERP", - "C98USD": "C98/USD", - "CADUSD": "CAD/USD", - "CAKE-PERP": "CAKE-PERP", - "CEL-0325": "CEL-0325", - "CEL-PERP": "CEL-PERP", - "CELBTC": "CEL/BTC", - "CELO-PERP": "CELO-PERP", - "CELUSD": "CEL/USD", - "CGC-0325": "CGC-0325", - "CGCUSD": "CGC/USD", - "CHR-PERP": "CHR-PERP", - "CHRUSD": "CHR/USD", - "CHZ-0325": "CHZ-0325", - "CHZ-PERP": "CHZ-PERP", - "CHZUSD": "CHZ/USD", - "CHZUSDT": "CHZ/USDT", - "CITYUSD": "CITY/USD", - "CLV-PERP": "CLV-PERP", - "CLVUSD": "CLV/USD", - "COINUSD": "COIN/USD", - "COMP-0325": "COMP-0325", - "COMP-PERP": "COMP-PERP", - "COMPBEARUSD": "COMPBEAR/USD", - "COMPBEARUSDT": "COMPBEAR/USDT", - "COMPBULLUSD": "COMPBULL/USD", - "COMPBULLUSDT": "COMPBULL/USDT", - "COMPHALFUSD": "COMPHALF/USD", - "COMPHEDGEUSD": "COMPHEDGE/USD", - "COMPUSD": "COMP/USD", - "COMPUSDT": "COMP/USDT", - "CONV-PERP": "CONV-PERP", - "CONVUSD": "CONV/USD", - "COPEUSD": "COPE/USD", - "CQTUSD": "CQT/USD", - "CREAM-PERP": "CREAM-PERP", - "CREAMUSD": "CREAM/USD", - "CREAMUSDT": "CREAM/USDT", - "CRO-PERP": "CRO-PERP", - "CRON-0325": "CRON-0325", - "CRONUSD": "CRON/USD", - "CROUSD": "CRO/USD", - "CRV-PERP": "CRV-PERP", - "CRVUSD": "CRV/USD", - "CUSDT-PERP": "CUSDT-PERP", - "CUSDTBEARUSD": "CUSDTBEAR/USD", - "CUSDTBEARUSDT": "CUSDTBEAR/USDT", - "CUSDTBULLUSD": "CUSDTBULL/USD", - "CUSDTBULLUSDT": "CUSDTBULL/USDT", - "CUSDTHALFUSD": "CUSDTHALF/USD", - "CUSDTHEDGEUSD": "CUSDTHEDGE/USD", - "CUSDTUSD": "CUSDT/USD", - "CUSDTUSDT": "CUSDT/USDT", - "CVC-PERP": "CVC-PERP", - "CVCUSD": "CVC/USD", - "DAIUSD": "DAI/USD", - "DAIUSDT": "DAI/USDT", - "DASH-PERP": "DASH-PERP", - "DAWN-PERP": "DAWN-PERP", - "DAWNUSD": "DAWN/USD", - "DEFI-0325": "DEFI-0325", - "DEFI-PERP": "DEFI-PERP", - "DEFIBEARUSD": "DEFIBEAR/USD", - "DEFIBEARUSDT": "DEFIBEAR/USDT", - "DEFIBULLUSD": "DEFIBULL/USD", - "DEFIBULLUSDT": "DEFIBULL/USDT", - "DEFIHALFUSD": "DEFIHALF/USD", - "DEFIHEDGEUSD": "DEFIHEDGE/USD", - "DENT-PERP": "DENT-PERP", - "DENTUSD": "DENT/USD", - "DFLUSD": "DFL/USD", - "DKNG-0325": "DKNG-0325", - "DKNGUSD": "DKNG/USD", - "DMGUSD": "DMG/USD", - "DMGUSDT": "DMG/USDT", - "DODO-PERP": "DODO-PERP", - "DODOUSD": "DODO/USD", - "DOGE-0325": "DOGE-0325", - "DOGE-PERP": "DOGE-PERP", - "DOGEBEAR2021USD": "DOGEBEAR2021/USD", - "DOGEBTC": "DOGE/BTC", - "DOGEBULLUSD": "DOGEBULL/USD", - "DOGEHALFUSD": "DOGEHALF/USD", - "DOGEHEDGEUSD": "DOGEHEDGE/USD", - "DOGEUSD": "DOGE/USD", - "DOGEUSDT": "DOGE/USDT", - "DOT-0325": "DOT-0325", - "DOT-PERP": "DOT-PERP", - "DOTBTC": "DOT/BTC", - "DOTUSD": "DOT/USD", - "DOTUSDT": "DOT/USDT", - "DRGN-0325": "DRGN-0325", - "DRGN-PERP": "DRGN-PERP", - "DRGNBEARUSD": "DRGNBEAR/USD", - "DRGNBULLUSD": "DRGNBULL/USD", - "DRGNHALFUSD": "DRGNHALF/USD", - "DRGNHEDGEUSD": "DRGNHEDGE/USD", - "DYDX-PERP": "DYDX-PERP", - "DYDXUSD": "DYDX/USD", - "EDEN-0325": "EDEN-0325", - "EDEN-PERP": "EDEN-PERP", - "EDENUSD": "EDEN/USD", - "EGLD-PERP": "EGLD-PERP", - "EMBUSD": "EMB/USD", - "ENJ-PERP": "ENJ-PERP", - "ENJUSD": "ENJ/USD", - "ENS-PERP": "ENS-PERP", - "ENSUSD": "ENS/USD", - "EOS-0325": "EOS-0325", - "EOS-PERP": "EOS-PERP", - "EOSBEARUSD": "EOSBEAR/USD", - "EOSBEARUSDT": "EOSBEAR/USDT", - "EOSBULLUSD": "EOSBULL/USD", - "EOSBULLUSDT": "EOSBULL/USDT", - "EOSHALFUSD": "EOSHALF/USD", - "EOSHEDGEUSD": "EOSHEDGE/USD", - "ETC-PERP": "ETC-PERP", - "ETCBEARUSD": "ETCBEAR/USD", - "ETCBULLUSD": "ETCBULL/USD", - "ETCHALFUSD": "ETCHALF/USD", - "ETCHEDGEUSD": "ETCHEDGE/USD", - "ETH-0325": "ETH-0325", - "ETH-0624": "ETH-0624", - "ETH-PERP": "ETH-PERP", - "ETHBEARUSD": "ETHBEAR/USD", - "ETHBEARUSDT": "ETHBEAR/USDT", - "ETHBRZ": "ETH/BRZ", - "ETHBTC": "ETH/BTC", - "ETHBULLUSD": "ETHBULL/USD", - "ETHBULLUSDT": "ETHBULL/USDT", - "ETHE-0325": "ETHE-0325", - "ETHEUR": "ETH/EUR", - "ETHEUSD": "ETHE/USD", - "ETHHALFUSD": "ETHHALF/USD", - "ETHHEDGEUSD": "ETHHEDGE/USD", - "ETHUSD": "ETH/USD", - "ETHUSDT": "ETH/USDT", - "EURTEUR": "EURT/EUR", - "EURTUSD": "EURT/USD", - "EURTUSDT": "EURT/USDT", - "EURUSD": "EUR/USD", - "EXCH-0325": "EXCH-0325", - "EXCH-PERP": "EXCH-PERP", - "EXCHBEARUSD": "EXCHBEAR/USD", - "EXCHBULLUSD": "EXCHBULL/USD", - "EXCHHALFUSD": "EXCHHALF/USD", - "EXCHHEDGEUSD": "EXCHHEDGE/USD", - "FB-0325": "FB-0325", - "FBUSD": "FB/USD", - "FIDA-PERP": "FIDA-PERP", - "FIDAUSD": "FIDA/USD", - "FIDAUSDT": "FIDA/USDT", - "FIL-0325": "FIL-0325", - "FIL-PERP": "FIL-PERP", - "FLM-PERP": "FLM-PERP", - "FLOW-PERP": "FLOW-PERP", - "FRONTUSD": "FRONT/USD", - "FRONTUSDT": "FRONT/USDT", - "FTM-PERP": "FTM-PERP", - "FTMUSD": "FTM/USD", - "FTT-PERP": "FTT-PERP", - "FTTBTC": "FTT/BTC", - "FTTUSD": "FTT/USD", - "FTTUSDT": "FTT/USDT", - "GALA-PERP": "GALA-PERP", - "GALAUSD": "GALA/USD", - "GALUSD": "GAL/USD", - "GARIUSD": "GARI/USD", - "GBPUSD": "GBP/USD", - "GBTC-0325": "GBTC-0325", - "GBTCUSD": "GBTC/USD", - "GDX-0325": "GDX-0325", - "GDXJ-0325": "GDXJ-0325", - "GDXJUSD": "GDXJ/USD", - "GDXUSD": "GDX/USD", - "GENEUSD": "GENE/USD", - "GLD-0325": "GLD-0325", - "GLDUSD": "GLD/USD", - "GLXYUSD": "GLXY/USD", - "GME-0325": "GME-0325", - "GMEUSD": "GME/USD", - "GODSUSD": "GODS/USD", - "GOGUSD": "GOG/USD", - "GOOGL-0325": "GOOGL-0325", - "GOOGLUSD": "GOOGL/USD", - "GRT-0325": "GRT-0325", - "GRT-PERP": "GRT-PERP", - "GRTBEARUSD": "GRTBEAR/USD", - "GRTBULLUSD": "GRTBULL/USD", - "GRTUSD": "GRT/USD", - "GTUSD": "GT/USD", - "HALFSHITUSD": "HALFSHIT/USD", - "HALFUSD": "HALF/USD", - "HBAR-PERP": "HBAR-PERP", - "HEDGESHITUSD": "HEDGESHIT/USD", - "HEDGEUSD": "HEDGE/USD", - "HGETUSD": "HGET/USD", - "HGETUSDT": "HGET/USDT", - "HMTUSD": "HMT/USD", - "HNT-PERP": "HNT-PERP", - "HNTUSD": "HNT/USD", - "HNTUSDT": "HNT/USDT", - "HOLY-PERP": "HOLY-PERP", - "HOLYUSD": "HOLY/USD", - "HOODUSD": "HOOD/USD", - "HOT-PERP": "HOT-PERP", - "HT-PERP": "HT-PERP", - "HTBEARUSD": "HTBEAR/USD", - "HTBULLUSD": "HTBULL/USD", - "HTHALFUSD": "HTHALF/USD", - "HTHEDGEUSD": "HTHEDGE/USD", - "HTUSD": "HT/USD", - "HUM-PERP": "HUM-PERP", - "HUMUSD": "HUM/USD", - "HXROUSD": "HXRO/USD", - "HXROUSDT": "HXRO/USDT", - "IBVOLBTC": "IBVOL/BTC", - "IBVOLUSD": "IBVOL/USD", - "IBVOLUSDT": "IBVOL/USDT", - "ICP-PERP": "ICP-PERP", - "ICX-PERP": "ICX-PERP", - "IMX-PERP": "IMX-PERP", - "IMXUSD": "IMX/USD", - "INTERUSD": "INTER/USD", - "IOTA-PERP": "IOTA-PERP", - "JETUSD": "JET/USD", - "JOEUSD": "JOE/USD", - "JSTUSD": "JST/USD", - "KAVA-PERP": "KAVA-PERP", - "KBTT-PERP": "KBTT-PERP", - "KBTTUSD": "KBTT/USD", - "KIN-PERP": "KIN-PERP", - "KINUSD": "KIN/USD", - "KNC-PERP": "KNC-PERP", - "KNCBEARUSD": "KNCBEAR/USD", - "KNCBEARUSDT": "KNCBEAR/USDT", - "KNCBULLUSD": "KNCBULL/USD", - "KNCBULLUSDT": "KNCBULL/USDT", - "KNCHALFUSD": "KNCHALF/USD", - "KNCHEDGEUSD": "KNCHEDGE/USD", - "KNCUSD": "KNC/USD", - "KNCUSDT": "KNC/USDT", - "KSHIB-PERP": "KSHIB-PERP", - "KSHIBUSD": "KSHIB/USD", - "KSM-PERP": "KSM-PERP", - "KSOS-PERP": "KSOS-PERP", - "KSOSUSD": "KSOS/USD", - "LEO-PERP": "LEO-PERP", - "LEOBEARUSD": "LEOBEAR/USD", - "LEOBULLUSD": "LEOBULL/USD", - "LEOHALFUSD": "LEOHALF/USD", - "LEOHEDGEUSD": "LEOHEDGE/USD", - "LEOUSD": "LEO/USD", - "LINA-PERP": "LINA-PERP", - "LINAUSD": "LINA/USD", - "LINK-0325": "LINK-0325", - "LINK-PERP": "LINK-PERP", - "LINKBEARUSD": "LINKBEAR/USD", - "LINKBEARUSDT": "LINKBEAR/USDT", - "LINKBTC": "LINK/BTC", - "LINKBULLUSD": "LINKBULL/USD", - "LINKBULLUSDT": "LINKBULL/USDT", - "LINKHALFUSD": "LINKHALF/USD", - "LINKHEDGEUSD": "LINKHEDGE/USD", - "LINKUSD": "LINK/USD", - "LINKUSDT": "LINK/USDT", - "LOOKS-PERP": "LOOKS-PERP", - "LOOKSUSD": "LOOKS/USD", - "LRC-PERP": "LRC-PERP", - "LRCUSD": "LRC/USD", - "LTC-0325": "LTC-0325", - "LTC-PERP": "LTC-PERP", - "LTCBEARUSD": "LTCBEAR/USD", - "LTCBEARUSDT": "LTCBEAR/USDT", - "LTCBTC": "LTC/BTC", - "LTCBULLUSD": "LTCBULL/USD", - "LTCBULLUSDT": "LTCBULL/USDT", - "LTCHALFUSD": "LTCHALF/USD", - "LTCHEDGEUSD": "LTCHEDGE/USD", - "LTCUSD": "LTC/USD", - "LTCUSDT": "LTC/USDT", - "LUAUSD": "LUA/USD", - "LUAUSDT": "LUA/USDT", - "LUNA-PERP": "LUNA-PERP", - "LUNAUSD": "LUNA/USD", - "LUNAUSDT": "LUNA/USDT", - "MANA-PERP": "MANA-PERP", - "MANAUSD": "MANA/USD", - "MAPS-PERP": "MAPS-PERP", - "MAPSUSD": "MAPS/USD", - "MAPSUSDT": "MAPS/USDT", - "MATHUSD": "MATH/USD", - "MATHUSDT": "MATH/USDT", - "MATIC-PERP": "MATIC-PERP", - "MATICBEAR2021USD": "MATICBEAR2021/USD", - "MATICBTC": "MATIC/BTC", - "MATICBULLUSD": "MATICBULL/USD", - "MATICHALFUSD": "MATICHALF/USD", - "MATICHEDGEUSD": "MATICHEDGE/USD", - "MATICUSD": "MATIC/USD", - "MBSUSD": "MBS/USD", - "MCB-PERP": "MCB-PERP", - "MCBUSD": "MCB/USD", - "MEDIA-PERP": "MEDIA-PERP", - "MEDIAUSD": "MEDIA/USD", - "MER-PERP": "MER-PERP", - "MERUSD": "MER/USD", - "MID-0325": "MID-0325", - "MID-PERP": "MID-PERP", - "MIDBEARUSD": "MIDBEAR/USD", - "MIDBULLUSD": "MIDBULL/USD", - "MIDHALFUSD": "MIDHALF/USD", - "MIDHEDGEUSD": "MIDHEDGE/USD", - "MINA-PERP": "MINA-PERP", - "MKR-PERP": "MKR-PERP", - "MKRBEARUSD": "MKRBEAR/USD", - "MKRBULLUSD": "MKRBULL/USD", - "MKRUSD": "MKR/USD", - "MKRUSDT": "MKR/USDT", - "MNGO-PERP": "MNGO-PERP", - "MNGOUSD": "MNGO/USD", - "MOBUSD": "MOB/USD", - "MOBUSDT": "MOB/USDT", - "MRNA-0325": "MRNA-0325", - "MRNAUSD": "MRNA/USD", - "MSOLUSD": "MSOL/USD", - "MSTR-0325": "MSTR-0325", - "MSTRUSD": "MSTR/USD", - "MTA-PERP": "MTA-PERP", - "MTAUSD": "MTA/USD", - "MTAUSDT": "MTA/USDT", - "MTL-PERP": "MTL-PERP", - "MTLUSD": "MTL/USD", - "MVDA10-PERP": "MVDA10-PERP", - "MVDA25-PERP": "MVDA25-PERP", - "NEAR-PERP": "NEAR-PERP", - "NEO-PERP": "NEO-PERP", - "NEXOUSD": "NEXO/USD", - "NFLX-0325": "NFLX-0325", - "NFLXUSD": "NFLX/USD", - "NIO-0325": "NIO-0325", - "NIOUSD": "NIO/USD", - "NOK-0325": "NOK-0325", - "NOKUSD": "NOK/USD", - "NVDA-0325": "NVDA-0325", - "NVDAUSD": "NVDA/USD", - "OKB-0325": "OKB-0325", - "OKB-PERP": "OKB-PERP", - "OKBBEARUSD": "OKBBEAR/USD", - "OKBBULLUSD": "OKBBULL/USD", - "OKBHALFUSD": "OKBHALF/USD", - "OKBHEDGEUSD": "OKBHEDGE/USD", - "OKBUSD": "OKB/USD", - "OMG-0325": "OMG-0325", - "OMG-PERP": "OMG-PERP", - "OMGUSD": "OMG/USD", - "ONE-PERP": "ONE-PERP", - "ONT-PERP": "ONT-PERP", - "ORBS-PERP": "ORBS-PERP", - "ORBSUSD": "ORBS/USD", - "OXY-PERP": "OXY-PERP", - "OXYUSD": "OXY/USD", - "OXYUSDT": "OXY/USDT", - "PAXG-PERP": "PAXG-PERP", - "PAXGBEARUSD": "PAXGBEAR/USD", - "PAXGBULLUSD": "PAXGBULL/USD", - "PAXGHALFUSD": "PAXGHALF/USD", - "PAXGHEDGEUSD": "PAXGHEDGE/USD", - "PAXGUSD": "PAXG/USD", - "PAXGUSDT": "PAXG/USDT", - "PENN-0325": "PENN-0325", - "PENNUSD": "PENN/USD", - "PEOPLE-PERP": "PEOPLE-PERP", - "PEOPLEUSD": "PEOPLE/USD", - "PERP-PERP": "PERP-PERP", - "PERPUSD": "PERP/USD", - "PFE-0325": "PFE-0325", - "PFEUSD": "PFE/USD", - "POLIS-PERP": "POLIS-PERP", - "POLISUSD": "POLIS/USD", - "PORTUSD": "PORT/USD", - "PRISMUSD": "PRISM/USD", - "PRIV-0325": "PRIV-0325", - "PRIV-PERP": "PRIV-PERP", - "PRIVBEARUSD": "PRIVBEAR/USD", - "PRIVBULLUSD": "PRIVBULL/USD", - "PRIVHALFUSD": "PRIVHALF/USD", - "PRIVHEDGEUSD": "PRIVHEDGE/USD", - "PROM-PERP": "PROM-PERP", - "PROMUSD": "PROM/USD", - "PSGUSD": "PSG/USD", - "PSYUSD": "PSY/USD", - "PTUUSD": "PTU/USD", - "PUNDIX-PERP": "PUNDIX-PERP", - "PUNDIXUSD": "PUNDIX/USD", - "PYPL-0325": "PYPL-0325", - "PYPLUSD": "PYPL/USD", - "QIUSD": "QI/USD", - "QTUM-PERP": "QTUM-PERP", - "RAMP-PERP": "RAMP-PERP", - "RAMPUSD": "RAMP/USD", - "RAY-PERP": "RAY-PERP", - "RAYUSD": "RAY/USD", - "REALUSD": "REAL/USD", - "REEF-0325": "REEF-0325", - "REEF-PERP": "REEF-PERP", - "REEFUSD": "REEF/USD", - "REN-PERP": "REN-PERP", - "RENUSD": "REN/USD", - "RNDR-PERP": "RNDR-PERP", - "RNDRUSD": "RNDR/USD", - "RON-PERP": "RON-PERP", - "ROOK-PERP": "ROOK-PERP", - "ROOKUSD": "ROOK/USD", - "ROOKUSDT": "ROOK/USDT", - "ROSE-PERP": "ROSE-PERP", - "RSR-PERP": "RSR-PERP", - "RSRUSD": "RSR/USD", - "RUNE-PERP": "RUNE-PERP", - "RUNEUSD": "RUNE/USD", - "RUNEUSDT": "RUNE/USDT", - "SAND-PERP": "SAND-PERP", - "SANDUSD": "SAND/USD", - "SC-PERP": "SC-PERP", - "SCRT-PERP": "SCRT-PERP", - "SECO-PERP": "SECO-PERP", - "SECOUSD": "SECO/USD", - "SHIB-PERP": "SHIB-PERP", - "SHIBUSD": "SHIB/USD", - "SHIT-0325": "SHIT-0325", - "SHIT-PERP": "SHIT-PERP", - "SKL-PERP": "SKL-PERP", - "SKLUSD": "SKL/USD", - "SLNDUSD": "SLND/USD", - "SLP-PERP": "SLP-PERP", - "SLPUSD": "SLP/USD", - "SLRSUSD": "SLRS/USD", - "SLV-0325": "SLV-0325", - "SLVUSD": "SLV/USD", - "SNX-PERP": "SNX-PERP", - "SNXUSD": "SNX/USD", - "SNYUSD": "SNY/USD", - "SOL-0325": "SOL-0325", - "SOL-PERP": "SOL-PERP", - "SOLBTC": "SOL/BTC", - "SOLUSD": "SOL/USD", - "SOLUSDT": "SOL/USDT", - "SOS-PERP": "SOS-PERP", - "SOSUSD": "SOS/USD", - "SPELL-PERP": "SPELL-PERP", - "SPELLUSD": "SPELL/USD", - "SPY-0325": "SPY-0325", - "SPYUSD": "SPY/USD", - "SQ-0325": "SQ-0325", - "SQUSD": "SQ/USD", - "SRM-PERP": "SRM-PERP", - "SRMUSD": "SRM/USD", - "SRMUSDT": "SRM/USDT", - "SRN-PERP": "SRN-PERP", - "STARSUSD": "STARS/USD", - "STEP-PERP": "STEP-PERP", - "STEPUSD": "STEP/USD", - "STETHUSD": "STETH/USD", - "STMX-PERP": "STMX-PERP", - "STMXUSD": "STMX/USD", - "STORJ-PERP": "STORJ-PERP", - "STORJUSD": "STORJ/USD", - "STSOLUSD": "STSOL/USD", - "STX-PERP": "STX-PERP", - "SUNUSD": "SUN/USD", - "SUSHI-0325": "SUSHI-0325", - "SUSHI-PERP": "SUSHI-PERP", - "SUSHIBEARUSD": "SUSHIBEAR/USD", - "SUSHIBTC": "SUSHI/BTC", - "SUSHIBULLUSD": "SUSHIBULL/USD", - "SUSHIUSD": "SUSHI/USD", - "SUSHIUSDT": "SUSHI/USDT", - "SXP-0325": "SXP-0325", - "SXP-PERP": "SXP-PERP", - "SXPBEARUSD": "SXPBEAR/USD", - "SXPBTC": "SXP/BTC", - "SXPBULLUSD": "SXPBULL/USD", - "SXPHALFUSD": "SXPHALF/USD", - "SXPHALFUSDT": "SXPHALF/USDT", - "SXPHEDGEUSD": "SXPHEDGE/USD", - "SXPUSD": "SXP/USD", - "SXPUSDT": "SXP/USDT", - "THETA-0325": "THETA-0325", - "THETA-PERP": "THETA-PERP", - "THETABEARUSD": "THETABEAR/USD", - "THETABULLUSD": "THETABULL/USD", - "THETAHALFUSD": "THETAHALF/USD", - "THETAHEDGEUSD": "THETAHEDGE/USD", - "TLM-PERP": "TLM-PERP", - "TLMUSD": "TLM/USD", - "TLRY-0325": "TLRY-0325", - "TLRYUSD": "TLRY/USD", - "TOMO-PERP": "TOMO-PERP", - "TOMOBEAR2021USD": "TOMOBEAR2021/USD", - "TOMOBULLUSD": "TOMOBULL/USD", - "TOMOHALFUSD": "TOMOHALF/USD", - "TOMOHEDGEUSD": "TOMOHEDGE/USD", - "TOMOUSD": "TOMO/USD", - "TOMOUSDT": "TOMO/USDT", - "TONCOIN-PERP": "TONCOIN-PERP", - "TONCOINUSD": "TONCOIN/USD", - "TRU-PERP": "TRU-PERP", - "TRUMP2024": "TRUMP2024", - "TRUUSD": "TRU/USD", - "TRUUSDT": "TRU/USDT", - "TRX-0325": "TRX-0325", - "TRX-PERP": "TRX-PERP", - "TRXBEARUSD": "TRXBEAR/USD", - "TRXBTC": "TRX/BTC", - "TRXBULLUSD": "TRXBULL/USD", - "TRXHALFUSD": "TRXHALF/USD", - "TRXHEDGEUSD": "TRXHEDGE/USD", - "TRXUSD": "TRX/USD", - "TRXUSDT": "TRX/USDT", - "TRYB-PERP": "TRYB-PERP", - "TRYBBEARUSD": "TRYBBEAR/USD", - "TRYBBULLUSD": "TRYBBULL/USD", - "TRYBHALFUSD": "TRYBHALF/USD", - "TRYBHEDGEUSD": "TRYBHEDGE/USD", - "TRYBUSD": "TRYB/USD", - "TSLA-0325": "TSLA-0325", - "TSLABTC": "TSLA/BTC", - "TSLADOGE": "TSLA/DOGE", - "TSLAUSD": "TSLA/USD", - "TSM-0325": "TSM-0325", - "TSMUSD": "TSM/USD", - "TULIP-PERP": "TULIP-PERP", - "TULIPUSD": "TULIP/USD", - "TWTR-0325": "TWTR-0325", - "TWTRUSD": "TWTR/USD", - "UBER-0325": "UBER-0325", - "UBERUSD": "UBER/USD", - "UBXTUSD": "UBXT/USD", - "UBXTUSDT": "UBXT/USDT", - "UMEEUSD": "UMEE/USD", - "UNI-0325": "UNI-0325", - "UNI-PERP": "UNI-PERP", - "UNIBTC": "UNI/BTC", - "UNISWAP-0325": "UNISWAP-0325", - "UNISWAP-PERP": "UNISWAP-PERP", - "UNISWAPBEARUSD": "UNISWAPBEAR/USD", - "UNISWAPBULLUSD": "UNISWAPBULL/USD", - "UNIUSD": "UNI/USD", - "UNIUSDT": "UNI/USDT", - "USDT-0325": "USDT-0325", - "USDT-PERP": "USDT-PERP", - "USDTBEARUSD": "USDTBEAR/USD", - "USDTBULLUSD": "USDTBULL/USD", - "USDTHALFUSD": "USDTHALF/USD", - "USDTHEDGEUSD": "USDTHEDGE/USD", - "USDTUSD": "USDT/USD", - "USO-0325": "USO-0325", - "USOUSD": "USO/USD", - "UST-PERP": "UST-PERP", - "USTUSD": "UST/USD", - "USTUSDT": "UST/USDT", - "VET-PERP": "VET-PERP", - "VETBEARUSD": "VETBEAR/USD", - "VETBEARUSDT": "VETBEAR/USDT", - "VETBULLUSD": "VETBULL/USD", - "VETBULLUSDT": "VETBULL/USDT", - "VETHEDGEUSD": "VETHEDGE/USD", - "VGXUSD": "VGX/USD", - "WAVES-0325": "WAVES-0325", - "WAVES-PERP": "WAVES-PERP", - "WAVESUSD": "WAVES/USD", - "WBTCBTC": "WBTC/BTC", - "WBTCUSD": "WBTC/USD", - "WNDRUSD": "WNDR/USD", - "WRXUSD": "WRX/USD", - "WRXUSDT": "WRX/USDT", - "WSB-0325": "WSB-0325", - "XAUT-0325": "XAUT-0325", - "XAUT-PERP": "XAUT-PERP", - "XAUTBEARUSD": "XAUTBEAR/USD", - "XAUTBULLUSD": "XAUTBULL/USD", - "XAUTHALFUSD": "XAUTHALF/USD", - "XAUTHEDGEUSD": "XAUTHEDGE/USD", - "XAUTUSD": "XAUT/USD", - "XAUTUSDT": "XAUT/USDT", - "XEM-PERP": "XEM-PERP", - "XLM-PERP": "XLM-PERP", - "XLMBEARUSD": "XLMBEAR/USD", - "XLMBULLUSD": "XLMBULL/USD", - "XMR-PERP": "XMR-PERP", - "XRP-0325": "XRP-0325", - "XRP-PERP": "XRP-PERP", - "XRPBEARUSD": "XRPBEAR/USD", - "XRPBEARUSDT": "XRPBEAR/USDT", - "XRPBTC": "XRP/BTC", - "XRPBULLUSD": "XRPBULL/USD", - "XRPBULLUSDT": "XRPBULL/USDT", - "XRPHALFUSD": "XRPHALF/USD", - "XRPHEDGEUSD": "XRPHEDGE/USD", - "XRPUSD": "XRP/USD", - "XRPUSDT": "XRP/USDT", - "XTZ-0325": "XTZ-0325", - "XTZ-PERP": "XTZ-PERP", - "XTZBEARUSD": "XTZBEAR/USD", - "XTZBEARUSDT": "XTZBEAR/USDT", - "XTZBULLUSD": "XTZBULL/USD", - "XTZBULLUSDT": "XTZBULL/USDT", - "XTZHALFUSD": "XTZHALF/USD", - "XTZHEDGEUSD": "XTZHEDGE/USD", - "YFI-0325": "YFI-0325", - "YFI-PERP": "YFI-PERP", - "YFIBTC": "YFI/BTC", - "YFII-PERP": "YFII-PERP", - "YFIIUSD": "YFII/USD", - "YFIUSD": "YFI/USD", - "YFIUSDT": "YFI/USDT", - "YGGUSD": "YGG/USD", - "ZEC-PERP": "ZEC-PERP", - "ZECBEARUSD": "ZECBEAR/USD", - "ZECBULLUSD": "ZECBULL/USD", - "ZIL-PERP": "ZIL-PERP", - "ZM-0325": "ZM-0325", - "ZMUSD": "ZM/USD", - "ZRX-PERP": "ZRX-PERP", - "ZRXUSD": "ZRX/USD", - "GMTUSD": "GMT/USD", - "GMT-PERP": "GMT-PERP", -} diff --git a/pkg/exchange/ftx/ticker_test.go b/pkg/exchange/ftx/ticker_test.go deleted file mode 100644 index 0bf019753..000000000 --- a/pkg/exchange/ftx/ticker_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package ftx - -import ( - "context" - "os" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestExchange_QueryTickers_AllSymbols(t *testing.T) { - key := os.Getenv("FTX_API_KEY") - secret := os.Getenv("FTX_API_SECRET") - subAccount := os.Getenv("FTX_SUBACCOUNT") - if len(key) == 0 && len(secret) == 0 { - t.Skip("api key/secret are not configured") - } - - e := NewExchange(key, secret, subAccount) - got, err := e.QueryTickers(context.Background()) - if assert.NoError(t, err) { - assert.True(t, len(got) > 1, "binance: attempting to get all symbol tickers, but get 1 or less") - } -} - -func TestExchange_QueryTickers_SomeSymbols(t *testing.T) { - key := os.Getenv("FTX_API_KEY") - secret := os.Getenv("FTX_API_SECRET") - subAccount := os.Getenv("FTX_SUBACCOUNT") - if len(key) == 0 && len(secret) == 0 { - t.Skip("api key/secret are not configured") - } - - e := NewExchange(key, secret, subAccount) - got, err := e.QueryTickers(context.Background(), "BTCUSDT", "ETHUSDT") - if assert.NoError(t, err) { - assert.Len(t, got, 2, "binance: attempting to get two symbols, but number of tickers do not match") - } -} - -func TestExchange_QueryTickers_SingleSymbol(t *testing.T) { - key := os.Getenv("FTX_API_KEY") - secret := os.Getenv("FTX_API_SECRET") - subAccount := os.Getenv("FTX_SUBACCOUNT") - if len(key) == 0 && len(secret) == 0 { - t.Skip("api key/secret are not configured") - } - - e := NewExchange(key, secret, subAccount) - got, err := e.QueryTickers(context.Background(), "BTCUSDT") - if assert.NoError(t, err) { - assert.Len(t, got, 1, "binance: attempting to get one symbol, but number of tickers do not match") - } -} diff --git a/pkg/exchange/ftx/websocket_messages.go b/pkg/exchange/ftx/websocket_messages.go deleted file mode 100644 index 2265e1de9..000000000 --- a/pkg/exchange/ftx/websocket_messages.go +++ /dev/null @@ -1,468 +0,0 @@ -package ftx - -import ( - "encoding/json" - "fmt" - "hash/crc32" - "math" - "strconv" - "strings" - "time" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -type operation string - -const ping operation = "ping" -const login operation = "login" -const subscribe operation = "subscribe" -const unsubscribe operation = "unsubscribe" - -type channel string - -const orderBookChannel channel = "orderbook" -const marketTradeChannel channel = "trades" -const bookTickerChannel channel = "ticker" -const privateOrdersChannel channel = "orders" -const privateTradesChannel channel = "fills" - -var errUnsupportedConversion = fmt.Errorf("unsupported conversion") - -/* -Private: - order update: `{'op': 'subscribe', 'channel': 'orders'}` - login: `{"args": { "key": "", "sign": "", "time": }, "op": "login" }` - -*/ -type websocketRequest struct { - Operation operation `json:"op"` - - // {'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'} - Channel channel `json:"channel,omitempty"` - Market string `json:"market,omitempty"` - - Login loginArgs `json:"args,omitempty"` -} - -/* -{ - "args": { - "key": "", - "sign": "", - "time": - }, - "op": "login" -} -*/ -type loginArgs struct { - Key string `json:"key"` - Signature string `json:"sign"` - Time int64 `json:"time"` - SubAccount string `json:"subaccount,omitempty"` -} - -func newLoginRequest(key, secret string, t time.Time, subaccount string) websocketRequest { - millis := t.UnixNano() / int64(time.Millisecond) - return websocketRequest{ - Operation: login, - Login: loginArgs{ - Key: key, - Signature: sign(secret, loginBody(millis)), - Time: millis, - SubAccount: subaccount, - }, - } -} - -func loginBody(millis int64) string { - return fmt.Sprintf("%dwebsocket_login", millis) -} - -type respType string - -const pongRespType respType = "pong" -const errRespType respType = "error" -const subscribedRespType respType = "subscribed" -const unsubscribedRespType respType = "unsubscribed" -const infoRespType respType = "info" -const partialRespType respType = "partial" -const updateRespType respType = "update" - -type websocketResponse struct { - mandatoryFields - - optionalFields -} - -type mandatoryFields struct { - Channel channel `json:"channel"` - - Type respType `json:"type"` -} - -type optionalFields struct { - Market string `json:"market"` - - // Example: {"type": "error", "code": 404, "msg": "No such market: BTCUSDT"} - Code int64 `json:"code"` - - Message string `json:"msg"` - - Data json.RawMessage `json:"data"` -} - -type orderUpdateResponse struct { - mandatoryFields - - Data ftxapi.Order `json:"data"` -} - -type trade struct { - Price fixedpoint.Value `json:"price"` - Size fixedpoint.Value `json:"size"` - Side string `json:"side"` - Liquidation bool `json:"liquidation"` - Time time.Time `json:"time"` -} -type tradeResponse struct { - mandatoryFields - Data []trade `json:"data"` -} - -func (r websocketResponse) toMarketTradeResponse() (t []types.Trade, err error) { - if r.Channel != marketTradeChannel { - return t, fmt.Errorf("type %s, channel %s: channel incorrect", r.Type, r.Channel) - } - var tds []trade - if err = json.Unmarshal(r.Data, &tds); err != nil { - return t, err - } - t = make([]types.Trade, len(tds)) - for i, td := range tds { - tt := &t[i] - tt.Exchange = types.ExchangeFTX - tt.Price = td.Price - tt.Quantity = td.Size - tt.QuoteQuantity = td.Size - tt.Symbol = r.Market - tt.Side = types.SideType(TrimUpperString(string(td.Side))) - tt.IsBuyer = true - tt.IsMaker = false - tt.Time = types.Time(td.Time) - } - return t, nil -} - -func (r websocketResponse) toOrderUpdateResponse() (orderUpdateResponse, error) { - if r.Channel != privateOrdersChannel { - return orderUpdateResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - var o orderUpdateResponse - if err := json.Unmarshal(r.Data, &o.Data); err != nil { - return orderUpdateResponse{}, err - } - o.mandatoryFields = r.mandatoryFields - return o, nil -} - -type tradeUpdateResponse struct { - mandatoryFields - - Data ftxapi.Fill `json:"data"` -} - -func (r websocketResponse) toTradeUpdateResponse() (tradeUpdateResponse, error) { - if r.Channel != privateTradesChannel { - return tradeUpdateResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - var t tradeUpdateResponse - if err := json.Unmarshal(r.Data, &t.Data); err != nil { - return tradeUpdateResponse{}, err - } - t.mandatoryFields = r.mandatoryFields - return t, nil -} - -/* - Private: - order: {"type": "subscribed", "channel": "orders"} - -Public - orderbook: {"type": "subscribed", "channel": "orderbook", "market": "BTC/USDT"} - -*/ -type subscribedResponse struct { - mandatoryFields - - Market string `json:"market"` -} - -func (s subscribedResponse) String() string { - return fmt.Sprintf("%s channel is subscribed", strings.TrimSpace(fmt.Sprintf("%s %s", s.Market, s.Channel))) -} - -// {"type": "subscribed", "channel": "orderbook", "market": "BTC/USDT"} -func (r websocketResponse) toSubscribedResponse() (subscribedResponse, error) { - if r.Type != subscribedRespType { - return subscribedResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - - return subscribedResponse{ - mandatoryFields: r.mandatoryFields, - Market: r.Market, - }, nil -} - -// {"type": "error", "code": 400, "msg": "Already logged in"} -type errResponse struct { - Code int64 `json:"code"` - Message string `json:"msg"` -} - -func (e errResponse) String() string { - return fmt.Sprintf("%d: %s", e.Code, e.Message) -} - -func (r websocketResponse) toErrResponse() errResponse { - return errResponse{ - Code: r.Code, - Message: r.Message, - } -} - -// sample :{"bid": 49194.0, "ask": 49195.0, "bidSize": 0.0775, "askSize": 0.0247, "last": 49200.0, "time": 1640171788.9339821} -func (r websocketResponse) toBookTickerResponse() (bookTickerResponse, error) { - if r.Channel != bookTickerChannel { - return bookTickerResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - - var o bookTickerResponse - if err := json.Unmarshal(r.Data, &o); err != nil { - return bookTickerResponse{}, err - } - - o.mandatoryFields = r.mandatoryFields - o.Market = r.Market - o.Timestamp = nanoToTime(o.Time) - - return o, nil -} - -func (r websocketResponse) toPublicOrderBookResponse() (orderBookResponse, error) { - if r.Channel != orderBookChannel { - return orderBookResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) - } - - var o orderBookResponse - if err := json.Unmarshal(r.Data, &o); err != nil { - return orderBookResponse{}, err - } - - o.mandatoryFields = r.mandatoryFields - o.Market = r.Market - o.Timestamp = nanoToTime(o.Time) - - return o, nil -} - -func nanoToTime(input float64) time.Time { - sec, dec := math.Modf(input) - return time.Unix(int64(sec), int64(dec*1e9)) -} - -type orderBookResponse struct { - mandatoryFields - - Market string `json:"market"` - - Action string `json:"action"` - - Time float64 `json:"time"` - - Timestamp time.Time - - Checksum uint32 `json:"checksum"` - - // best 100 orders. Ex. {[100,1], [50, 2]} - Bids [][]json.Number `json:"bids"` - - // best 100 orders. Ex. {[51, 1], [102, 3]} - Asks [][]json.Number `json:"asks"` -} - -type bookTickerResponse struct { - mandatoryFields - Market string `json:"market"` - Bid fixedpoint.Value `json:"bid"` - Ask fixedpoint.Value `json:"ask"` - BidSize fixedpoint.Value `json:"bidSize"` - AskSize fixedpoint.Value `json:"askSize"` - Last fixedpoint.Value `json:"last"` - Time float64 `json:"time"` - Timestamp time.Time -} - -// only 100 orders so we use linear search here -func (r *orderBookResponse) update(orderUpdates orderBookResponse) { - r.Checksum = orderUpdates.Checksum - r.updateBids(orderUpdates.Bids) - r.updateAsks(orderUpdates.Asks) -} - -func (r *orderBookResponse) updateAsks(asks [][]json.Number) { - higherPrice := func(dst, src float64) bool { - return dst < src - } - for _, o := range asks { - if remove := o[1] == "0"; remove { - r.Asks = removePrice(r.Asks, o[0]) - } else { - r.Asks = upsertPriceVolume(r.Asks, o, higherPrice) - } - } -} - -func (r *orderBookResponse) updateBids(bids [][]json.Number) { - lessPrice := func(dst, src float64) bool { - return dst > src - } - for _, o := range bids { - if remove := o[1] == "0"; remove { - r.Bids = removePrice(r.Bids, o[0]) - } else { - r.Bids = upsertPriceVolume(r.Bids, o, lessPrice) - } - } -} - -func upsertPriceVolume(dst [][]json.Number, src []json.Number, priceComparator func(dst float64, src float64) bool) [][]json.Number { - for i, pv := range dst { - dstPrice := pv[0] - srcPrice := src[0] - - // update volume - if dstPrice == srcPrice { - pv[1] = src[1] - return dst - } - - // The value must be a number which is verified by json.Unmarshal, so the err - // should never happen. - dstPriceNum, err := strconv.ParseFloat(string(dstPrice), 64) - if err != nil { - logger.WithError(err).Errorf("unexpected price %s", dstPrice) - continue - } - srcPriceNum, err := strconv.ParseFloat(string(srcPrice), 64) - if err != nil { - logger.WithError(err).Errorf("unexpected price updates %s", srcPrice) - continue - } - - if !priceComparator(dstPriceNum, srcPriceNum) { - return insertAt(dst, i, src) - } - } - - return append(dst, src) -} - -func insertAt(dst [][]json.Number, id int, pv []json.Number) (result [][]json.Number) { - result = append(result, dst[:id]...) - result = append(result, pv) - result = append(result, dst[id:]...) - return -} - -func removePrice(dst [][]json.Number, price json.Number) [][]json.Number { - for i, pv := range dst { - if pv[0] == price { - return append(dst[:i], dst[i+1:]...) - } - } - - return dst -} - -func (r orderBookResponse) verifyChecksum() error { - if crc32Val := crc32.ChecksumIEEE([]byte(checksumString(r.Bids, r.Asks))); crc32Val != r.Checksum { - return fmt.Errorf("expected checksum %d, actual checksum %d: %w", r.Checksum, crc32Val, errUnmatchedChecksum) - } - return nil -} - -// :::... -func checksumString(bids, asks [][]json.Number) string { - sb := strings.Builder{} - appendNumber := func(pv []json.Number) { - if sb.Len() != 0 { - sb.WriteString(":") - } - sb.WriteString(string(pv[0])) - sb.WriteString(":") - sb.WriteString(string(pv[1])) - } - - bidsLen := len(bids) - asksLen := len(asks) - for i := 0; i < bidsLen || i < asksLen; i++ { - if i < bidsLen { - appendNumber(bids[i]) - } - if i < asksLen { - appendNumber(asks[i]) - } - } - return sb.String() -} - -var errUnmatchedChecksum = fmt.Errorf("unmatched checksum") - -func toGlobalOrderBook(r orderBookResponse) (types.SliceOrderBook, error) { - bids, err := toPriceVolumeSlice(r.Bids) - if err != nil { - return types.SliceOrderBook{}, fmt.Errorf("can't convert bids to priceVolumeSlice: %w", err) - } - asks, err := toPriceVolumeSlice(r.Asks) - if err != nil { - return types.SliceOrderBook{}, fmt.Errorf("can't convert asks to priceVolumeSlice: %w", err) - } - return types.SliceOrderBook{ - // ex. BTC/USDT - Symbol: toGlobalSymbol(strings.ToUpper(r.Market)), - Bids: bids, - Asks: asks, - }, nil -} - -func toGlobalBookTicker(r bookTickerResponse) (types.BookTicker, error) { - return types.BookTicker{ - // ex. BTC/USDT - Symbol: toGlobalSymbol(strings.ToUpper(r.Market)), - // Time: r.Timestamp, - Buy: r.Bid, - BuySize: r.BidSize, - Sell: r.Ask, - SellSize: r.AskSize, - // Last: r.Last, - }, nil -} - -func toPriceVolumeSlice(orders [][]json.Number) (types.PriceVolumeSlice, error) { - var pv types.PriceVolumeSlice - for _, o := range orders { - p, err := fixedpoint.NewFromString(string(o[0])) - if err != nil { - return nil, fmt.Errorf("can't convert price %+v to fixedpoint: %w", o[0], err) - } - v, err := fixedpoint.NewFromString(string(o[1])) - if err != nil { - return nil, fmt.Errorf("can't convert volume %+v to fixedpoint: %w", o[0], err) - } - pv = append(pv, types.PriceVolume{Price: p, Volume: v}) - } - return pv, nil -} diff --git a/pkg/exchange/ftx/websocket_messages_test.go b/pkg/exchange/ftx/websocket_messages_test.go deleted file mode 100644 index 5a9635e5a..000000000 --- a/pkg/exchange/ftx/websocket_messages_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package ftx - -import ( - "encoding/json" - "io/ioutil" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi" - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -func Test_rawResponse_toSubscribedResp(t *testing.T) { - input := `{"type": "subscribed", "channel": "orderbook", "market": "BTC/USDT"}` - var m websocketResponse - assert.NoError(t, json.Unmarshal([]byte(input), &m)) - r, err := m.toSubscribedResponse() - assert.NoError(t, err) - assert.Equal(t, subscribedResponse{ - mandatoryFields: mandatoryFields{ - Channel: orderBookChannel, - Type: subscribedRespType, - }, - Market: "BTC/USDT", - }, r) -} - -func Test_websocketResponse_toPublicOrderBookResponse(t *testing.T) { - f, err := ioutil.ReadFile("./orderbook_snapshot.json") - assert.NoError(t, err) - var m websocketResponse - assert.NoError(t, json.Unmarshal(f, &m)) - r, err := m.toPublicOrderBookResponse() - assert.NoError(t, err) - assert.Equal(t, partialRespType, r.Type) - assert.Equal(t, orderBookChannel, r.Channel) - assert.Equal(t, "BTC/USDT", r.Market) - assert.Equal(t, int64(1614520368), r.Timestamp.Unix()) - assert.Equal(t, uint32(2150525410), r.Checksum) - assert.Len(t, r.Bids, 100) - assert.Equal(t, []json.Number{"44555.0", "3.3968"}, r.Bids[0]) - assert.Equal(t, []json.Number{"44554.0", "0.0561"}, r.Bids[1]) - assert.Len(t, r.Asks, 100) - assert.Equal(t, []json.Number{"44574.0", "0.4591"}, r.Asks[0]) - assert.Equal(t, []json.Number{"44579.0", "0.15"}, r.Asks[1]) -} - -func Test_orderBookResponse_toGlobalOrderBook(t *testing.T) { - f, err := ioutil.ReadFile("./orderbook_snapshot.json") - assert.NoError(t, err) - var m websocketResponse - assert.NoError(t, json.Unmarshal(f, &m)) - r, err := m.toPublicOrderBookResponse() - assert.NoError(t, err) - - b, err := toGlobalOrderBook(r) - assert.NoError(t, err) - assert.Equal(t, "BTCUSDT", b.Symbol) - isValid, err := b.IsValid() - assert.True(t, isValid) - assert.NoError(t, err) - - assert.Len(t, b.Bids, 100) - assert.Equal(t, types.PriceVolume{ - Price: fixedpoint.MustNewFromString("44555.0"), - Volume: fixedpoint.MustNewFromString("3.3968"), - }, b.Bids[0]) - assert.Equal(t, types.PriceVolume{ - Price: fixedpoint.MustNewFromString("44222.0"), - Volume: fixedpoint.MustNewFromString("0.0002"), - }, b.Bids[99]) - - assert.Len(t, b.Asks, 100) - assert.Equal(t, types.PriceVolume{ - Price: fixedpoint.MustNewFromString("44574.0"), - Volume: fixedpoint.MustNewFromString("0.4591"), - }, b.Asks[0]) - assert.Equal(t, types.PriceVolume{ - Price: fixedpoint.MustNewFromString("45010.0"), - Volume: fixedpoint.MustNewFromString("0.0003"), - }, b.Asks[99]) - -} - -func Test_checksumString(t *testing.T) { - type args struct { - bids [][]json.Number - asks [][]json.Number - } - tests := []struct { - name string - args args - want string - }{ - { - name: "more bids", - args: args{ - bids: [][]json.Number{{"5000.5", "10"}, {"4995.0", "5"}}, - asks: [][]json.Number{{"5001.0", "6"}}, - }, - want: "5000.5:10:5001.0:6:4995.0:5", - }, - { - name: "lengths of bids and asks are the same", - args: args{ - bids: [][]json.Number{{"5000.5", "10"}, {"4995.0", "5"}}, - asks: [][]json.Number{{"5001.0", "6"}, {"5002.0", "7"}}, - }, - want: "5000.5:10:5001.0:6:4995.0:5:5002.0:7", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := checksumString(tt.args.bids, tt.args.asks); got != tt.want { - t.Errorf("checksumString() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_orderBookResponse_verifyChecksum(t *testing.T) { - for _, file := range []string{"./orderbook_snapshot.json"} { - f, err := ioutil.ReadFile(file) - assert.NoError(t, err) - var m websocketResponse - assert.NoError(t, json.Unmarshal(f, &m)) - r, err := m.toPublicOrderBookResponse() - assert.NoError(t, err) - assert.NoError(t, r.verifyChecksum(), "filename: "+file) - } -} - -func Test_removePrice(t *testing.T) { - pairs := [][]json.Number{{"123.99", "2.0"}, {"2234.12", "3.1"}} - assert.Equal(t, pairs, removePrice(pairs, "99333")) - - pairs = removePrice(pairs, "2234.12") - assert.Equal(t, [][]json.Number{{"123.99", "2.0"}}, pairs) - assert.Equal(t, [][]json.Number{}, removePrice(pairs, "123.99")) -} - -func Test_orderBookResponse_update(t *testing.T) { - ob := &orderBookResponse{Bids: nil, Asks: nil} - - ob.update(orderBookResponse{ - Bids: [][]json.Number{{"1.0", "0"}, {"10.0", "1"}, {"11.0", "1"}}, - Asks: [][]json.Number{{"1.0", "1"}}, - }) - assert.Equal(t, [][]json.Number{{"11.0", "1"}, {"10.0", "1"}}, ob.Bids) - assert.Equal(t, [][]json.Number{{"1.0", "1"}}, ob.Asks) - ob.update(orderBookResponse{ - Bids: [][]json.Number{{"9.0", "1"}, {"12.0", "1"}, {"10.5", "1"}}, - Asks: [][]json.Number{{"1.0", "0"}}, - }) - assert.Equal(t, [][]json.Number{{"12.0", "1"}, {"11.0", "1"}, {"10.5", "1"}, {"10.0", "1"}, {"9.0", "1"}}, ob.Bids) - assert.Equal(t, [][]json.Number{}, ob.Asks) - - // remove them - ob.update(orderBookResponse{ - Bids: [][]json.Number{{"9.0", "0"}, {"12.0", "0"}, {"10.5", "0"}}, - Asks: [][]json.Number{{"9.0", "1"}, {"12.0", "1"}, {"10.5", "1"}}, - }) - assert.Equal(t, [][]json.Number{{"11.0", "1"}, {"10.0", "1"}}, ob.Bids) - assert.Equal(t, [][]json.Number{{"9.0", "1"}, {"10.5", "1"}, {"12.0", "1"}}, ob.Asks) -} - -func Test_insertAt(t *testing.T) { - r := insertAt([][]json.Number{{"1.2", "2"}, {"1.4", "2"}}, 1, []json.Number{"1.3", "2"}) - assert.Equal(t, [][]json.Number{{"1.2", "2"}, {"1.3", "2"}, {"1.4", "2"}}, r) - - r = insertAt([][]json.Number{{"1.2", "2"}, {"1.4", "2"}}, 0, []json.Number{"1.1", "2"}) - assert.Equal(t, [][]json.Number{{"1.1", "2"}, {"1.2", "2"}, {"1.4", "2"}}, r) - - r = insertAt([][]json.Number{{"1.2", "2"}, {"1.4", "2"}}, 2, []json.Number{"1.5", "2"}) - assert.Equal(t, [][]json.Number{{"1.2", "2"}, {"1.4", "2"}, {"1.5", "2"}}, r) -} - -func Test_newLoginRequest(t *testing.T) { - // From API doc: https://docs.ftx.com/?javascript#authentication-2 - r := newLoginRequest("", "Y2QTHI23f23f23jfjas23f23To0RfUwX3H42fvN-", time.Unix(0, 1557246346499*int64(time.Millisecond)), "") - // pragma: allowlist nextline secret - expectedSignature := "d10b5a67a1a941ae9463a60b285ae845cdeac1b11edc7da9977bef0228b96de9" - assert.Equal(t, expectedSignature, r.Login.Signature) - jsonStr, err := json.Marshal(r) - assert.NoError(t, err) - assert.True(t, strings.Contains(string(jsonStr), expectedSignature)) -} - -func Test_websocketResponse_toOrderUpdateResponse(t *testing.T) { - input := []byte(` -{ - "channel": "orders", - "type": "update", - "data": { - "id": 12345, - "clientId": "test-client-id", - "market": "SOL/USD", - "type": "limit", - "side": "buy", - "price": 0.5, - "size": 100.0, - "status": "closed", - "filledSize": 0.0, - "remainingSize": 0.0, - "reduceOnly": false, - "liquidation": false, - "avgFillPrice": null, - "postOnly": false, - "ioc": false, - "createdAt": "2021-03-27T11:00:36.418674+00:00" - } -} -`) - - var raw websocketResponse - assert.NoError(t, json.Unmarshal(input, &raw)) - - r, err := raw.toOrderUpdateResponse() - assert.NoError(t, err) - - assert.Equal(t, orderUpdateResponse{ - mandatoryFields: mandatoryFields{ - Channel: privateOrdersChannel, - Type: updateRespType, - }, - Data: ftxapi.Order{ - Id: 12345, - ClientId: "test-client-id", - Market: "SOL/USD", - Type: "limit", - Side: "buy", - Price: fixedpoint.NewFromFloat(0.5), - Size: fixedpoint.NewFromInt(100), - Status: "closed", - FilledSize: fixedpoint.Zero, - RemainingSize: fixedpoint.Zero, - ReduceOnly: false, - AvgFillPrice: fixedpoint.Zero, - PostOnly: false, - Ioc: false, - CreatedAt: mustParseDatetime("2021-03-27T11:00:36.418674+00:00"), - Future: "", - }, - }, r) -} diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index cb6708f94..bae287c79 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -26,13 +26,13 @@ func (n *ExchangeName) UnmarshalJSON(data []byte) error { } switch s { - case "max", "binance", "ftx", "okex": + case "max", "binance", "okex", "kucoin": *n = ExchangeName(s) return nil } - return fmt.Errorf("unknown or unsupported exchange name: %s, valid names are: max, binance, ftx", s) + return fmt.Errorf("unknown or unsupported exchange name: %s, valid names are: max, binance, okex, kucoin", s) } func (n ExchangeName) String() string { @@ -42,7 +42,6 @@ func (n ExchangeName) String() string { const ( ExchangeMax ExchangeName = "max" ExchangeBinance ExchangeName = "binance" - ExchangeFTX ExchangeName = "ftx" ExchangeOKEx ExchangeName = "okex" ExchangeKucoin ExchangeName = "kucoin" ExchangeBacktest ExchangeName = "backtest" @@ -51,7 +50,6 @@ const ( var SupportedExchanges = []ExchangeName{ ExchangeMax, ExchangeBinance, - ExchangeFTX, ExchangeOKEx, ExchangeKucoin, // note: we are not using "backtest" @@ -63,8 +61,6 @@ func ValidExchangeName(a string) (ExchangeName, error) { return ExchangeMax, nil case "binance", "bn": return ExchangeBinance, nil - case "ftx": - return ExchangeFTX, nil case "okex": return ExchangeOKEx, nil case "kucoin": diff --git a/pkg/types/exchange_icon.go b/pkg/types/exchange_icon.go index a85624a13..7c4df85b0 100644 --- a/pkg/types/exchange_icon.go +++ b/pkg/types/exchange_icon.go @@ -8,8 +8,6 @@ func ExchangeFooterIcon(exName ExchangeName) string { footerIcon = "https://bin.bnbstatic.com/static/images/common/favicon.ico" case ExchangeMax: footerIcon = "https://max.maicoin.com/favicon-16x16.png" - case ExchangeFTX: - footerIcon = "https://ftx.com/favicon.ico?v=2" case ExchangeOKEx: footerIcon = "https://static.okex.com/cdn/assets/imgs/MjAxODg/D91A7323087D31A588E0D2A379DD7747.png" case ExchangeKucoin: