mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 06:53:52 +00:00
all: remove ftx
This commit is contained in:
parent
16b4702248
commit
170c3b8c41
|
@ -19,7 +19,7 @@ func init() {
|
||||||
RootCmd.AddCommand(accountCmd)
|
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{
|
var accountCmd = &cobra.Command{
|
||||||
Use: "account [--session SESSION]",
|
Use: "account [--session SESSION]",
|
||||||
Short: "show user account details (ex: balance)",
|
Short: "show user account details (ex: balance)",
|
||||||
|
|
|
@ -15,7 +15,7 @@ func init() {
|
||||||
RootCmd.AddCommand(balancesCmd)
|
RootCmd.AddCommand(balancesCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// go run ./cmd/bbgo balances --session=ftx
|
// go run ./cmd/bbgo balances --session=binance
|
||||||
var balancesCmd = &cobra.Command{
|
var balancesCmd = &cobra.Command{
|
||||||
Use: "balances [--session SESSION]",
|
Use: "balances [--session SESSION]",
|
||||||
Short: "Show user account balances",
|
Short: "Show user account balances",
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"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{
|
var klineCmd = &cobra.Command{
|
||||||
Use: "kline",
|
Use: "kline",
|
||||||
Short: "connect to the kline market data streaming service of an exchange",
|
Short: "connect to the kline market data streaming service of an exchange",
|
||||||
|
|
|
@ -17,7 +17,7 @@ func init() {
|
||||||
RootCmd.AddCommand(marketCmd)
|
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{
|
var marketCmd = &cobra.Command{
|
||||||
Use: "market",
|
Use: "market",
|
||||||
Short: "List the symbols that the are available to be traded in the exchange",
|
Short: "List the symbols that the are available to be traded in the exchange",
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"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{
|
var orderbookCmd = &cobra.Command{
|
||||||
Use: "orderbook --session=[exchange_name] --symbol=[pair_name]",
|
Use: "orderbook --session=[exchange_name] --symbol=[pair_name]",
|
||||||
Short: "connect to the order book market data streaming service of an exchange",
|
Short: "connect to the order book market data streaming service of an exchange",
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"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{
|
var tradesCmd = &cobra.Command{
|
||||||
Use: "trades --session=[exchange_name] --symbol=[pair_name]",
|
Use: "trades --session=[exchange_name] --symbol=[pair_name]",
|
||||||
Short: "Query trading history",
|
Short: "Query trading history",
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// go run ./cmd/bbgo userdatastream --session=ftx
|
// go run ./cmd/bbgo userdatastream --session=binance
|
||||||
var userDataStreamCmd = &cobra.Command{
|
var userDataStreamCmd = &cobra.Command{
|
||||||
Use: "userdatastream",
|
Use: "userdatastream",
|
||||||
Short: "Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)",
|
Short: "Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)",
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/ftx"
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
@ -36,16 +31,3 @@ func inBaseAsset(balances types.BalanceMap, market types.Market, price fixedpoin
|
||||||
base := balances[market.BaseCurrency]
|
base := balances[market.BaseCurrency]
|
||||||
return quote.Total().Div(price).Add(base.Total())
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/binance"
|
"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/kucoin"
|
||||||
"github.com/c9s/bbgo/pkg/exchange/max"
|
"github.com/c9s/bbgo/pkg/exchange/max"
|
||||||
"github.com/c9s/bbgo/pkg/exchange/okex"
|
"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) {
|
func NewStandard(n types.ExchangeName, key, secret, passphrase, subAccount string) (types.Exchange, error) {
|
||||||
switch n {
|
switch n {
|
||||||
|
|
||||||
case types.ExchangeFTX:
|
|
||||||
return ftx.NewExchange(key, secret, subAccount), nil
|
|
||||||
|
|
||||||
case types.ExchangeBinance:
|
case types.ExchangeBinance:
|
||||||
return binance.New(key, secret), nil
|
return binance.New(key, secret), nil
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
)
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"))
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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",
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -26,13 +26,13 @@ func (n *ExchangeName) UnmarshalJSON(data []byte) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch s {
|
switch s {
|
||||||
case "max", "binance", "ftx", "okex":
|
case "max", "binance", "okex", "kucoin":
|
||||||
*n = ExchangeName(s)
|
*n = ExchangeName(s)
|
||||||
return nil
|
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 {
|
func (n ExchangeName) String() string {
|
||||||
|
@ -42,7 +42,6 @@ func (n ExchangeName) String() string {
|
||||||
const (
|
const (
|
||||||
ExchangeMax ExchangeName = "max"
|
ExchangeMax ExchangeName = "max"
|
||||||
ExchangeBinance ExchangeName = "binance"
|
ExchangeBinance ExchangeName = "binance"
|
||||||
ExchangeFTX ExchangeName = "ftx"
|
|
||||||
ExchangeOKEx ExchangeName = "okex"
|
ExchangeOKEx ExchangeName = "okex"
|
||||||
ExchangeKucoin ExchangeName = "kucoin"
|
ExchangeKucoin ExchangeName = "kucoin"
|
||||||
ExchangeBacktest ExchangeName = "backtest"
|
ExchangeBacktest ExchangeName = "backtest"
|
||||||
|
@ -51,7 +50,6 @@ const (
|
||||||
var SupportedExchanges = []ExchangeName{
|
var SupportedExchanges = []ExchangeName{
|
||||||
ExchangeMax,
|
ExchangeMax,
|
||||||
ExchangeBinance,
|
ExchangeBinance,
|
||||||
ExchangeFTX,
|
|
||||||
ExchangeOKEx,
|
ExchangeOKEx,
|
||||||
ExchangeKucoin,
|
ExchangeKucoin,
|
||||||
// note: we are not using "backtest"
|
// note: we are not using "backtest"
|
||||||
|
@ -63,8 +61,6 @@ func ValidExchangeName(a string) (ExchangeName, error) {
|
||||||
return ExchangeMax, nil
|
return ExchangeMax, nil
|
||||||
case "binance", "bn":
|
case "binance", "bn":
|
||||||
return ExchangeBinance, nil
|
return ExchangeBinance, nil
|
||||||
case "ftx":
|
|
||||||
return ExchangeFTX, nil
|
|
||||||
case "okex":
|
case "okex":
|
||||||
return ExchangeOKEx, nil
|
return ExchangeOKEx, nil
|
||||||
case "kucoin":
|
case "kucoin":
|
||||||
|
|
|
@ -8,8 +8,6 @@ func ExchangeFooterIcon(exName ExchangeName) string {
|
||||||
footerIcon = "https://bin.bnbstatic.com/static/images/common/favicon.ico"
|
footerIcon = "https://bin.bnbstatic.com/static/images/common/favicon.ico"
|
||||||
case ExchangeMax:
|
case ExchangeMax:
|
||||||
footerIcon = "https://max.maicoin.com/favicon-16x16.png"
|
footerIcon = "https://max.maicoin.com/favicon-16x16.png"
|
||||||
case ExchangeFTX:
|
|
||||||
footerIcon = "https://ftx.com/favicon.ico?v=2"
|
|
||||||
case ExchangeOKEx:
|
case ExchangeOKEx:
|
||||||
footerIcon = "https://static.okex.com/cdn/assets/imgs/MjAxODg/D91A7323087D31A588E0D2A379DD7747.png"
|
footerIcon = "https://static.okex.com/cdn/assets/imgs/MjAxODg/D91A7323087D31A588E0D2A379DD7747.png"
|
||||||
case ExchangeKucoin:
|
case ExchangeKucoin:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user