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