all: remove ftx

This commit is contained in:
c9s 2022-11-24 16:46:40 +08:00
parent 16b4702248
commit 170c3b8c41
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
51 changed files with 9 additions and 8165 deletions

View File

@ -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)",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "<api_key>", "sign": "<signature>", "time": <ts> }, "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": "<api_key>",
"sign": "<signature>",
"time": <ts>
},
"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
}
// <best_bid_price>:<best_bid_size>:<best_ask_price>:<best_ask_size>...
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
}

View File

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

View File

@ -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":

View File

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