Merge pull request #461 from c9s/c9s/refactor-ftx

REFACTOR: re-implement ftxapi with requestgen and support query order status API
This commit is contained in:
Yo-An Lin 2022-03-03 10:44:50 +08:00 committed by GitHub
commit 83cc2d96e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 3073 additions and 463 deletions

View File

@ -131,7 +131,11 @@ pkg/version/dev.go: .FORCE
dev-version: pkg/version/dev.go
git commit $< -m "update dev build version"
version: pkg/version/version.go pkg/version/dev.go migrations
cmd-doc: .FORCE
go run ./cmd/update-doc
git add -v doc/commands
version: pkg/version/version.go pkg/version/dev.go migrations cmd-doc
git commit $< $(word 2,$^) -m "bump version to $(VERSION)" || true
[[ -e doc/release/$(VERSION).md ]] || (echo "file doc/release/$(VERSION).md does not exist" ; exit 1)
git add -v doc/release/$(VERSION).md && git commit doc/release/$(VERSION).md -m "add $(VERSION) release note" || true

View File

@ -39,6 +39,7 @@ bbgo [flags]
* [bbgo cancel-order](bbgo_cancel-order.md) - cancel orders
* [bbgo deposits](bbgo_deposits.md) - A testing utility that will query deposition history in last 7 days
* [bbgo execute-order](bbgo_execute-order.md) - execute buy/sell on the balance/position you have on specific symbol
* [bbgo get-order](bbgo_get-order.md) - Get order status
* [bbgo kline](bbgo_kline.md) - connect to the kline market data streaming service of an exchange
* [bbgo list-orders](bbgo_list-orders.md) - list user's open orders in exchange of a specific trading pair
* [bbgo market](bbgo_market.md) - List the symbols that the are available to be traded in the exchange
@ -46,7 +47,7 @@ bbgo [flags]
* [bbgo orderupdate](bbgo_orderupdate.md) - Listen to order update events
* [bbgo pnl](bbgo_pnl.md) - pnl calculator
* [bbgo run](bbgo_run.md) - run strategies from config file
* [bbgo submit-order](bbgo_submit-order.md) - submit limit order to the exchange
* [bbgo submit-order](bbgo_submit-order.md) - place limit order to the exchange
* [bbgo sync](bbgo_sync.md) - sync trades and orders history
* [bbgo trades](bbgo_trades.md) - Query trading history
* [bbgo tradeupdate](bbgo_tradeupdate.md) - Listen to trade update events
@ -54,4 +55,4 @@ bbgo [flags]
* [bbgo userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)
* [bbgo version](bbgo_version.md) - show version name
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -41,4 +41,4 @@ bbgo account [--session=[exchange_name]] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -48,4 +48,4 @@ bbgo backtest [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -3,7 +3,7 @@
Show user account balances
```
bbgo balances [flags]
bbgo balances --session SESSION [flags]
```
### Options
@ -40,4 +40,4 @@ bbgo balances [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -39,4 +39,4 @@ bbgo build [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -49,4 +49,4 @@ bbgo cancel-order [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -41,4 +41,4 @@ bbgo deposits [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -3,7 +3,7 @@
execute buy/sell on the balance/position you have on specific symbol
```
bbgo execute-order [flags]
bbgo execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quantity TOTAL_QUANTITY --slice-quantity SLICE_QUANTITY [flags]
```
### Options
@ -48,4 +48,4 @@ bbgo execute-order [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -0,0 +1,45 @@
## bbgo get-order
Get order status
```
bbgo get-order --session SESSION --order-id ORDER_ID [flags]
```
### Options
```
-h, --help help for get-order
--order-id string order id
--session string the exchange session name for sync
--symbol string the trading pair, like btcusdt
```
### Options inherited from parent commands
```
--binance-api-key string binance api key
--binance-api-secret string binance api secret
--config string config file (default "bbgo.yaml")
--debug debug mode
--dotenv string the dotenv file you want to load (default ".env.local")
--ftx-api-key string ftx api key
--ftx-api-secret string ftx api secret
--ftx-subaccount string subaccount name. Specify it if the credential is for subaccount.
--max-api-key string max api key
--max-api-secret string max api secret
--metrics enable prometheus metrics
--metrics-port string prometheus http server port (default "9090")
--no-dotenv disable built-in dotenv
--slack-channel string slack trading channel (default "dev-bbgo")
--slack-error-channel string slack error channel (default "bbgo-error")
--slack-token string slack token
--telegram-bot-auth-token string telegram auth token
--telegram-bot-token string telegram bot token from bot father
```
### SEE ALSO
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -42,4 +42,4 @@ bbgo kline [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -3,7 +3,7 @@
list user's open orders in exchange of a specific trading pair
```
bbgo list-orders [status] [flags]
bbgo list-orders open|closed --session SESSION --symbol SYMBOL [flags]
```
### Options
@ -41,4 +41,4 @@ bbgo list-orders [status] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -40,4 +40,4 @@ bbgo market [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -42,4 +42,4 @@ bbgo orderbook --session=[exchange_name] --symbol=[pair_name] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -40,4 +40,4 @@ bbgo orderupdate [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -43,4 +43,4 @@ bbgo pnl [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -49,4 +49,4 @@ bbgo run [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -1,6 +1,6 @@
## bbgo submit-order
submit limit order to the exchange
place limit order to the exchange
```
bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANTITY [--price PRICE] [flags]
@ -44,4 +44,4 @@ bbgo submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANT
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -42,4 +42,4 @@ bbgo sync --session=[exchange_name] --symbol=[pair_name] [--since=yyyy/mm/dd] [f
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -42,4 +42,4 @@ bbgo trades --session=[exchange_name] --symbol=[pair_name] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -40,4 +40,4 @@ bbgo tradeupdate --session=[exchange_name] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -42,4 +42,4 @@ bbgo transfer-history [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -40,4 +40,4 @@ bbgo userdatastream [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -39,4 +39,4 @@ bbgo version [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot
###### Auto generated by spf13/cobra on 22-Feb-2022
###### Auto generated by spf13/cobra on 3-Mar-2022

View File

@ -8,7 +8,7 @@ You should send multiple small pull request to implement them.
## Checklist
Exchange Interface - minimum requirement for trading
Exchange Interface - the minimum requirement for spot trading
- [ ] QueryMarkets
- [ ] QueryTickers
@ -17,11 +17,15 @@ Exchange Interface - minimum requirement for trading
- [ ] CancelOrders
- [ ] NewStream
Trading History Service Interface - used for syncing user trading data
Trading History Service Interface - (optional) used for syncing user trading data
- [ ] QueryClosedOrders
- [ ] QueryTrades
Order Query Service Interface - (optional) used for querying order status
- [ ] QueryOrder
Back-testing service - kline data is used for back-testing
- [ ] QueryKLines
@ -100,33 +104,58 @@ func NewExchangeStandard(n types.ExchangeName, key, secret, passphrase, subAccou
}
```
## Testing order book stream
## Test Market Data Stream
### Test order book stream
```shell
godotenv -f .env.local -- go run ./cmd/bbgo orderbook --config config/bbgo.yaml --session kucoin --symbol BTCUSDT
```
## Testing user data stream
## Test User Data Stream
```shell
godotenv -f .env.local -- go run ./cmd/bbgo --config config/bbgo.yaml userdatastream --session kucoin
```
### Testing order submit
## Test Restful Endpoints
You can choose the session name to set-up for testing:
```shell
godotenv -f .env.local -- go run ./cmd/bbgo submit-order --session=kucoin --symbol=BTCUSDT --side=buy --price=18000 --quantity=0.001
export BBGO_SESSION=ftx
export BBGO_SESSION=kucoin
export BBGO_SESSION=binance
```
### Testing open orders query
### Test user account balance
```shell
godotenv -f .env.local -- go run ./cmd/bbgo list-orders --session kucoin --symbol=BTCUSDT open
godotenv -f .env.local -- go run ./cmd/bbgo list-orders --session kucoin --symbol=BTCUSDT closed
godotenv -f .env.local -- go run ./cmd/bbgo balances --session $BBGO_SESSION
```
### Testing order cancel
### Test order submit
```shell
godotenv -f .env.local -- go run ./cmd/bbgo cancel-order --session kucoin --order-uuid 61c745c44592c200014abdcf
godotenv -f .env.local -- go run ./cmd/bbgo submit-order --session $BBGO_SESSION --symbol=BTCUSDT --side=buy --price=18000 --quantity=0.001
```
### Test open orders query
```shell
godotenv -f .env.local -- go run ./cmd/bbgo list-orders --session $BBGO_SESSION --symbol=BTCUSDT open
godotenv -f .env.local -- go run ./cmd/bbgo list-orders --session $BBGO_SESSION --symbol=BTCUSDT closed
```
### Test order status
```shell
godotenv -f .env.local -- go run ./cmd/bbgo get-order --session $BBGO_SESSION --order-id ORDER_ID
```
### Test order cancel
```shell
godotenv -f .env.local -- go run ./cmd/bbgo cancel-order --session $BBGO_SESSION --order-uuid 61c745c44592c200014abdcf
```

View File

@ -3,9 +3,7 @@ package cmd
import (
"context"
"fmt"
"os"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@ -19,41 +17,19 @@ func init() {
// go run ./cmd/bbgo balances --session=ftx
var balancesCmd = &cobra.Command{
Use: "balances",
Use: "balances --session SESSION",
Short: "Show user account balances",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
configFile, err := cmd.Flags().GetString("config")
if err != nil {
return err
}
if len(configFile) == 0 {
return errors.New("--config option is required")
}
sessionName, err := cmd.Flags().GetString("session")
if err != nil {
return err
}
// if config file exists, use the config loaded from the config file.
// otherwise, use a empty config object
var userConfig *bbgo.Config
if _, err := os.Stat(configFile); err == nil {
// load successfully
userConfig, err = bbgo.Load(configFile, false)
if err != nil {
return err
}
} else if os.IsNotExist(err) {
// config file doesn't exist
userConfig = &bbgo.Config{}
} else {
// other error
return err
if userConfig == nil {
return fmt.Errorf("user config is not loaded")
}
environ := bbgo.NewEnvironment()

View File

@ -19,11 +19,60 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
var getOrderCmd = &cobra.Command{
Use: "get-order --session SESSION --order-id ORDER_ID",
Short: "Get order status",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
if userConfig == nil {
return errors.New("config file is required")
}
environ := bbgo.NewEnvironment()
if err := environ.ConfigureExchangeSessions(userConfig); err != nil {
return err
}
sessionName, err := cmd.Flags().GetString("session")
if err != nil {
return err
}
session, ok := environ.Session(sessionName)
if !ok {
return fmt.Errorf("session %s not found", sessionName)
}
orderID, err := cmd.Flags().GetString("order-id")
if err != nil {
return fmt.Errorf("can't get the symbol from flags: %w", err)
}
service, ok := session.Exchange.(types.ExchangeOrderQueryService)
if !ok {
return fmt.Errorf("query order status is not supported for exchange %T, interface types.ExchangeOrderQueryService is not implemented", session.Exchange)
}
order, err := service.QueryOrder(ctx, types.OrderQuery{
OrderID: orderID,
})
if err != nil {
return err
}
log.Infof("%+v", order)
return nil
},
}
// go run ./cmd/bbgo list-orders [open|closed] --session=ftx --symbol=BTCUSDT
var listOrdersCmd = &cobra.Command{
Use: "list-orders [status]",
Use: "list-orders open|closed --session SESSION --symbol SYMBOL",
Short: "list user's open orders in exchange of a specific trading pair",
Args: cobra.OnlyValidArgs,
Args: cobra.OnlyValidArgs,
// default is open which means we query open orders if you haven't provided args.
ValidArgs: []string{"", "open", "closed"},
SilenceUsage: true,
@ -39,22 +88,10 @@ var listOrdersCmd = &cobra.Command{
return fmt.Errorf("--config option is required")
}
// if config file exists, use the config loaded from the config file.
// otherwise, use a empty config object
var userConfig *bbgo.Config
if _, err := os.Stat(configFile); err == nil {
// load successfully
userConfig, err = bbgo.Load(configFile, false)
if err != nil {
return err
}
} else if os.IsNotExist(err) {
// config file doesn't exist
userConfig = &bbgo.Config{}
} else {
// other error
return err
if userConfig == nil {
return errors.New("config file is required")
}
environ := bbgo.NewEnvironment()
if err := environ.ConfigureExchangeSessions(userConfig); err != nil {
@ -116,7 +153,7 @@ var listOrdersCmd = &cobra.Command{
}
var executeOrderCmd = &cobra.Command{
Use: "execute-order",
Use: "execute-order --session SESSION --symbol SYMBOL --side SIDE --target-quantity TOTAL_QUANTITY --slice-quantity SLICE_QUANTITY",
Short: "execute buy/sell on the balance/position you have on specific symbol",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
@ -265,7 +302,7 @@ var executeOrderCmd = &cobra.Command{
// go run ./cmd/bbgo submit-order --session=ftx --symbol=BTCUSDT --side=buy --price=18000 --quantity=0.001
var submitOrderCmd = &cobra.Command{
Use: "submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANTITY [--price PRICE]",
Short: "submit limit order to the exchange",
Short: "place limit order to the exchange",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
@ -345,6 +382,10 @@ func init() {
listOrdersCmd.Flags().String("session", "", "the exchange session name for sync")
listOrdersCmd.Flags().String("symbol", "", "the trading pair, like btcusdt")
getOrderCmd.Flags().String("session", "", "the exchange session name for sync")
getOrderCmd.Flags().String("symbol", "", "the trading pair, like btcusdt")
getOrderCmd.Flags().String("order-id", "", "order id")
submitOrderCmd.Flags().String("session", "", "the exchange session name for sync")
submitOrderCmd.Flags().String("symbol", "", "the trading pair, like btcusdt")
submitOrderCmd.Flags().String("side", "", "the trading side: buy or sell")
@ -362,6 +403,7 @@ func init() {
executeOrderCmd.Flags().Int("price-ticks", 0, "the number of price tick for the jump spread, default to 0")
RootCmd.AddCommand(listOrdersCmd)
RootCmd.AddCommand(getOrderCmd)
RootCmd.AddCommand(submitOrderCmd)
RootCmd.AddCommand(executeOrderCmd)
}

View File

@ -7,6 +7,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi"
"github.com/c9s/bbgo/pkg/types"
)
@ -36,6 +37,67 @@ func TrimLowerString(original string) string {
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
@ -54,10 +116,10 @@ func toGlobalOrder(r order) (types.Order, error) {
ClientOrderID: r.ClientId,
Symbol: toGlobalSymbol(r.Market),
Side: types.SideType(TrimUpperString(r.Side)),
Type: orderType,
Quantity: r.Size,
Price: r.Price,
TimeInForce: timeInForce,
Type: orderType,
Quantity: r.Size,
Price: r.Price,
TimeInForce: timeInForce,
},
Exchange: types.ExchangeFTX,
IsWorking: r.Status == "open",
@ -125,24 +187,24 @@ func toGlobalDepositStatus(input string) (types.DepositStatus, error) {
return "", fmt.Errorf("unsupported status %s", input)
}
func toGlobalTrade(f fill) (types.Trade, error) {
func toGlobalTrade(f ftxapi.Fill) (types.Trade, error) {
return types.Trade{
ID: f.TradeId,
GID: 0,
OrderID: f.OrderId,
Exchange: types.ExchangeFTX,
Price: f.Price,
Quantity: f.Size,
QuoteQuantity: f.Price.Mul(f.Size),
Symbol: toGlobalSymbol(f.Market),
Side: f.Side,
IsBuyer: f.Side == types.SideTypeBuy,
IsMaker: f.Liquidity == "maker",
Time: types.Time(f.Time.Time),
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
}
@ -169,17 +231,17 @@ const (
OrderTypeMarket OrderType = "market"
)
func toLocalOrderType(orderType types.OrderType) (OrderType, error) {
func toLocalOrderType(orderType types.OrderType) (ftxapi.OrderType, error) {
switch orderType {
case types.OrderTypeLimitMaker:
return OrderTypeLimit, nil
return ftxapi.OrderTypeLimit, nil
case types.OrderTypeLimit:
return OrderTypeLimit, nil
return ftxapi.OrderTypeLimit, nil
case types.OrderTypeMarket:
return OrderTypeMarket, nil
return ftxapi.OrderTypeMarket, nil
}

View File

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi"
"github.com/c9s/bbgo/pkg/types"
)
@ -104,18 +105,18 @@ func Test_toGlobalSymbol(t *testing.T) {
func Test_toLocalOrderTypeWithLimitMaker(t *testing.T) {
orderType, err := toLocalOrderType(types.OrderTypeLimitMaker)
assert.NoError(t, err)
assert.Equal(t, orderType, OrderTypeLimit)
assert.Equal(t, ftxapi.OrderTypeLimit, orderType)
}
func Test_toLocalOrderTypeWithLimit(t *testing.T) {
orderType, err := toLocalOrderType(types.OrderTypeLimit)
assert.NoError(t, err)
assert.Equal(t, orderType, OrderTypeLimit)
assert.Equal(t, ftxapi.OrderTypeLimit, orderType)
}
func Test_toLocalOrderTypeWithMarket(t *testing.T) {
orderType, err := toLocalOrderType(types.OrderTypeMarket)
assert.NoError(t, err)
assert.Equal(t, orderType, OrderTypeMarket)
assert.Equal(t, ftxapi.OrderTypeMarket, orderType)
}

View File

@ -6,6 +6,7 @@ import (
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
@ -14,6 +15,7 @@ import (
"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"
)
@ -31,6 +33,8 @@ var requestLimit = rate.NewLimiter(rate.Every(220*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
@ -78,7 +82,10 @@ func NewExchange(key, secret string, subAccount string) *Exchange {
panic(err)
}
client := ftxapi.NewClient()
client.Auth(key, secret, subAccount)
return &Exchange{
client: client,
restEndpoint: u,
key: key,
secret: secret,
@ -119,16 +126,14 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
}
func (e *Exchange) _queryMarkets(ctx context.Context) (MarketMap, error) {
resp, err := e.newRest().Markets(ctx)
req := e.client.NewGetMarketsRequest()
ftxMarkets, err := req.Do(ctx)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying markets failure")
}
markets := MarketMap{}
for _, m := range resp.Result {
for _, m := range ftxMarkets {
symbol := toGlobalSymbol(m.Name)
symbolMap[symbol] = m.Name
@ -164,41 +169,40 @@ func (e *Exchange) _queryMarkets(ctx context.Context) (MarketMap, error) {
}
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
resp, err := e.newRest().Account(ctx)
req := e.client.NewGetAccountRequest()
ftxAccount, err := req.Do(ctx)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying balances failure")
}
a := &types.Account{
MakerCommission: resp.Result.MakerFee,
TakerCommission: resp.Result.TakerFee,
TotalAccountValue: resp.Result.TotalAccountValue,
MakerCommission: ftxAccount.MakerFee,
TakerCommission: ftxAccount.TakerFee,
TotalAccountValue: ftxAccount.TotalAccountValue,
}
balances, err := e.QueryAccountBalances(ctx)
if err != nil {
return nil, err
}
a.UpdateBalances(balances)
a.UpdateBalances(balances)
return a, nil
}
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
resp, err := e.newRest().Balances(ctx)
balanceReq := e.client.NewGetBalancesRequest()
ftxBalances, err := balanceReq.Do(ctx)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying balances failure")
}
var balances = make(types.BalanceMap)
for _, r := range resp.Result {
balances[toGlobalCurrency(r.Coin)] = types.Balance{
Currency: toGlobalCurrency(r.Coin),
for _, r := range ftxBalances {
currency := toGlobalCurrency(r.Coin)
balances[currency] = types.Balance{
Currency: currency,
Available: r.Free,
Locked: r.Total.Sub(r.Free),
}
@ -340,74 +344,55 @@ func isIntervalSupportedInKLine(interval types.Interval) bool {
}
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) {
var since, until time.Time
if options.StartTime != nil {
since = *options.StartTime
}
if options.EndTime != nil {
until = *options.EndTime
} else {
until = time.Now()
}
if since.After(until) {
return nil, fmt.Errorf("invalid query trades time range, since: %+v, until: %+v", since, until)
}
if options.Limit == 1 {
// FTX doesn't provide pagination api, so we have to split the since/until time range into small slices, and paginate ourselves.
// If the limit is 1, we always get the same data from FTX.
return nil, fmt.Errorf("limit can't be 1 which can't be used in pagination")
}
limit := options.Limit
if limit == 0 {
limit = 200
}
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
}
for since.Before(until) {
// DO not set limit to `1` since you will always get the same response.
resp, err := e.newRest().Fills(ctx, toLocalSymbol(symbol), since, until, limit, true)
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
}
if !resp.Success {
return nil, fmt.Errorf("ftx returns failure")
}
sort.Slice(resp.Result, func(i, j int) bool {
return resp.Result[i].TradeId < resp.Result[j].TradeId
})
for _, r := range resp.Result {
// always update since to avoid infinite loop
since = r.Time.Time
if _, ok := tradeIDs[r.TradeId]; ok {
continue
}
if r.TradeId <= lastTradeID || r.Time.Before(since) || r.Time.After(until) || r.Market != toLocalSymbol(symbol) {
continue
}
tradeIDs[r.TradeId] = struct{}{}
lastTradeID = r.TradeId
t, err := toGlobalTrade(r)
if err != nil {
return nil, err
}
trades = append(trades, t)
}
if int64(len(resp.Result)) < limit {
return trades, nil
}
trades = append(trades, t)
}
return trades, nil
@ -458,27 +443,34 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
logrus.WithError(err).Error("type error")
}
or, err := e.newRest().PlaceOrder(ctx, PlaceOrderPayload{
Market: toLocalSymbol(TrimUpperString(so.Symbol)),
Side: TrimLowerString(string(so.Side)),
Price: so.Price,
Type: string(orderType),
Size: so.Quantity,
ReduceOnly: false,
IOC: so.TimeInForce == types.TimeInForceIOC,
PostOnly: so.Type == types.OrderTypeLimitMaker,
ClientID: newSpotClientOrderID(so.ClientOrderID),
})
req := e.client.NewPlaceOrderRequest()
req.Market(toLocalSymbol(TrimUpperString(so.Symbol)))
req.OrderType(orderType)
req.Side(ftxapi.Side(TrimLowerString(string(so.Side))))
req.Size(so.Quantity)
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 createdOrders, fmt.Errorf("failed to place order %+v: %w", so, err)
}
if !or.Success {
return createdOrders, fmt.Errorf("ftx returns placing order failure")
}
globalOrder, err := toGlobalOrder(or.Result)
globalOrder, err := toGlobalOrderNew(*or)
if err != nil {
return createdOrders, fmt.Errorf("failed to convert response to global order")
}
@ -488,20 +480,37 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
return createdOrders, nil
}
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
// TODO: invoke open trigger orders
resp, err := e.newRest().OpenOrders(ctx, toLocalSymbol(symbol))
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
}
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying open orders failure")
req := e.client.NewGetOrderStatusRequest(uint64(orderID))
ftxOrder, err := req.Do(ctx)
if err != nil {
return nil, err
}
for _, r := range resp.Result {
o, err := toGlobalOrder(r)
order, err := toGlobalOrderNew(*ftxOrder)
return &order, 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 nil, err
return orders, err
}
orders = append(orders, o)
}
return orders, nil
@ -510,73 +519,59 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
// 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) {
if until == (time.Time{}) {
until = time.Now()
}
if since.After(until) {
return nil, fmt.Errorf("invalid query closed orders time range, since: %+v, until: %+v", since, until)
}
symbol = TrimUpperString(symbol)
limit := int64(100)
hasMoreData := true
s := since
var lastOrder order
for hasMoreData {
if err := requestLimit.Wait(ctx); err != nil {
logrus.WithError(err).Error("rate limit error")
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
}
resp, err := e.newRest().OrdersHistory(ctx, toLocalSymbol(symbol), s, until, limit)
o, err := toGlobalOrderNew(ftxOrder)
if err != nil {
return nil, err
}
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying orders history failure")
return orders, err
}
sortByCreatedASC(resp.Result)
for _, r := range resp.Result {
// There may be more than one orders at the same time, so also have to check the ID
if r.CreatedAt.Before(lastOrder.CreatedAt.Time) || r.ID == lastOrder.ID || r.Status != "closed" || r.ID < int64(lastOrderID) {
continue
}
lastOrder = r
o, err := toGlobalOrder(r)
if err != nil {
return nil, err
}
orders = append(orders, o)
}
hasMoreData = resp.HasMoreData
// the start_time and end_time precision is second. There might be more than one orders within one second.
s = lastOrder.CreatedAt.Add(-1 * time.Second)
orders = append(orders, o)
}
return orders, nil
}
func sortByCreatedASC(orders []order) {
sort.Slice(orders, func(i, j int) bool {
return orders[i].CreatedAt.Before(orders[j].CreatedAt.Time)
})
}
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
for _, o := range orders {
rest := e.newRest()
if err := requestLimit.Wait(ctx); err != nil {
logrus.WithError(err).Error("rate limit error")
}
if len(o.ClientOrderID) > 0 {
if _, err := rest.CancelOrderByClientID(ctx, o.ClientOrderID); err != nil {
req := e.client.NewCancelOrderByClientOrderIdRequest(o.ClientOrderID)
_, err := req.Do(ctx)
if err != nil {
return err
}
} else {
req := e.client.NewCancelOrderRequest(strconv.FormatUint(o.OrderID, 10))
_, err := req.Do(ctx)
if err != nil {
return err
}
continue
}
if _, err := rest.CancelOrderByOrderID(ctx, o.OrderID); err != nil {
return err
}
}
return nil

View File

@ -86,10 +86,11 @@ func TestExchange_QueryAccountBalances(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
ex.client.BaseURL = serverURL
resp, err := ex.QueryAccountBalances(context.Background())
assert.NoError(t, err)
@ -99,10 +100,6 @@ func TestExchange_QueryAccountBalances(t *testing.T) {
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)
resp, err = ex.QueryAccountBalances(context.Background())
assert.EqualError(t, err, "ftx returns querying balances failure")
assert.Nil(t, resp)
}
func TestExchange_QueryOpenOrders(t *testing.T) {
@ -136,10 +133,12 @@ func TestExchange_QueryOpenOrders(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
ex.client.BaseURL = serverURL
resp, err := ex.QueryOpenOrders(context.Background(), "XRP-PREP")
assert.NoError(t, err)
assert.Len(t, resp, 1)
@ -155,10 +154,11 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
ex.client.BaseURL = serverURL
resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100)
assert.NoError(t, err)
@ -196,10 +196,11 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
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)
@ -240,10 +241,11 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
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)
@ -253,76 +255,6 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
assert.Equal(t, expectedOrderID[i], o.OrderID)
}
})
t.Run("receive duplicated orders", func(t *testing.T) {
successRespOne := `
{
"success": true,
"result": [
{
"status": "closed",
"createdAt": "2020-09-01T15:24:03.101197+00:00",
"id": 123
}
],
"hasMoreData": true
}
`
successRespTwo := `
{
"success": true,
"result": [
{
"clientId": "ignored-by-created-at",
"status": "closed",
"createdAt": "1999-09-01T15:24:03.101197+00:00",
"id": 999
},
{
"clientId": "ignored-by-duplicated-id",
"status": "closed",
"createdAt": "2020-09-02T15:24:03.101197+00:00",
"id": 123
},
{
"clientId": "ignored-duplicated-entry",
"status": "closed",
"createdAt": "2020-09-01T15:24:03.101197+00:00",
"id": 123
},
{
"status": "closed",
"createdAt": "2020-09-02T15:24:03.101197+00:00",
"id": 456
}
],
"hasMoreData": false
}
`
i := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if i == 0 {
i++
fmt.Fprintln(w, successRespOne)
return
}
fmt.Fprintln(w, successRespTwo)
}))
defer ts.Close()
ex := NewExchange("", "", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100)
assert.NoError(t, err)
assert.Len(t, resp, 2)
expectedOrderID := []uint64{123, 456}
for i, o := range resp {
assert.Equal(t, expectedOrderID[i], o.OrderID)
}
})
}
func TestExchange_QueryAccount(t *testing.T) {
@ -391,10 +323,11 @@ func TestExchange_QueryAccount(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
ex.client.BaseURL = serverURL
resp, err := ex.QueryAccount(context.Background())
assert.NoError(t, err)
@ -447,10 +380,12 @@ func TestExchange_QueryMarkets(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
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)
@ -494,9 +429,10 @@ func TestExchange_QueryDepositHistory(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
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()
@ -543,10 +479,10 @@ func TestExchange_QueryTrades(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
ex.client.BaseURL = serverURL
ctx := context.Background()
actualConfirmedTime, err := parseDatetime("2021-02-23T09:29:08.534000+00:00")
@ -619,10 +555,10 @@ func TestExchange_QueryTrades(t *testing.T) {
}))
defer ts.Close()
ex := NewExchange("", "", "")
ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err)
ex.restEndpoint = serverURL
ex.client.BaseURL = serverURL
ctx := context.Background()
actualConfirmedTime, err := parseDatetime("2021-02-23T09:29:08.534000+00:00")

View File

@ -0,0 +1,88 @@
package ftxapi
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result
//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result
import (
"github.com/c9s/requestgen"
"github.com/c9s/bbgo/pkg/fixedpoint"
)
type Position struct {
Cost fixedpoint.Value `json:"cost"`
EntryPrice fixedpoint.Value `json:"entryPrice"`
Future string `json:"future"`
InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"`
LongOrderSize fixedpoint.Value `json:"longOrderSize"`
MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"`
NetSize fixedpoint.Value `json:"netSize"`
OpenSize fixedpoint.Value `json:"openSize"`
ShortOrderSize fixedpoint.Value `json:"shortOrderSize"`
Side string `json:"side"`
Size fixedpoint.Value `json:"size"`
RealizedPnl fixedpoint.Value `json:"realizedPnl"`
UnrealizedPnl fixedpoint.Value `json:"unrealizedPnl"`
}
type Account struct {
BackstopProvider bool `json:"backstopProvider"`
Collateral fixedpoint.Value `json:"collateral"`
FreeCollateral fixedpoint.Value `json:"freeCollateral"`
Leverage fixedpoint.Value `json:"leverage"`
InitialMarginRequirement fixedpoint.Value `json:"initialMarginRequirement"`
MaintenanceMarginRequirement fixedpoint.Value `json:"maintenanceMarginRequirement"`
Liquidating bool `json:"liquidating"`
MakerFee fixedpoint.Value `json:"makerFee"`
MarginFraction fixedpoint.Value `json:"marginFraction"`
OpenMarginFraction fixedpoint.Value `json:"openMarginFraction"`
TakerFee fixedpoint.Value `json:"takerFee"`
TotalAccountValue fixedpoint.Value `json:"totalAccountValue"`
TotalPositionSize fixedpoint.Value `json:"totalPositionSize"`
Username string `json:"username"`
Positions []Position `json:"positions"`
}
//go:generate GetRequest -url "/api/account" -type GetAccountRequest -responseDataType .Account
type GetAccountRequest struct {
client requestgen.AuthenticatedAPIClient
}
func (c *RestClient) NewGetAccountRequest() *GetAccountRequest {
return &GetAccountRequest{
client: c,
}
}
//go:generate GetRequest -url "/api/positions" -type GetPositionsRequest -responseDataType []Position
type GetPositionsRequest struct {
client requestgen.AuthenticatedAPIClient
}
func (c *RestClient) NewGetPositionsRequest() *GetPositionsRequest {
return &GetPositionsRequest{
client: c,
}
}
type Balance struct {
Coin string `json:"coin"`
Free fixedpoint.Value `json:"free"`
SpotBorrow fixedpoint.Value `json:"spotBorrow"`
Total fixedpoint.Value `json:"total"`
UsdValue fixedpoint.Value `json:"usdValue"`
AvailableWithoutBorrow fixedpoint.Value `json:"availableWithoutBorrow"`
}
//go:generate GetRequest -url "/api/wallet/balances" -type GetBalancesRequest -responseDataType []Balance
type GetBalancesRequest struct {
client requestgen.AuthenticatedAPIClient
}
func (c *RestClient) NewGetBalancesRequest() *GetBalancesRequest {
return &GetBalancesRequest{
client: c,
}
}

View File

@ -0,0 +1,126 @@
// Code generated by "requestgen -method DELETE -url /api/orders -type CancelAllOrderRequest -responseType .APIResponse"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
)
func (c *CancelAllOrderRequest) Market(market string) *CancelAllOrderRequest {
c.market = &market
return c
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (c *CancelAllOrderRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (c *CancelAllOrderRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
// check market field -> json key market
if c.market != nil {
market := *c.market
// assign parameter of market
params["market"] = market
} else {
}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (c *CancelAllOrderRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := c.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (c *CancelAllOrderRequest) GetParametersJSON() ([]byte, error) {
params, err := c.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (c *CancelAllOrderRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (c *CancelAllOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (c *CancelAllOrderRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := c.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (c *CancelAllOrderRequest) Do(ctx context.Context) (*APIResponse, error) {
params, err := c.GetParameters()
if err != nil {
return nil, err
}
query := url.Values{}
apiURL := "/api/orders"
req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := c.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return &apiResponse, nil
}

View File

@ -0,0 +1,133 @@
// Code generated by "requestgen -method DELETE -url /api/orders/by_client_id/:clientOrderId -type CancelOrderByClientOrderIdRequest -responseType .APIResponse"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
)
func (c *CancelOrderByClientOrderIdRequest) ClientOrderId(clientOrderId string) *CancelOrderByClientOrderIdRequest {
c.clientOrderId = clientOrderId
return c
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (c *CancelOrderByClientOrderIdRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (c *CancelOrderByClientOrderIdRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (c *CancelOrderByClientOrderIdRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := c.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (c *CancelOrderByClientOrderIdRequest) GetParametersJSON() ([]byte, error) {
params, err := c.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (c *CancelOrderByClientOrderIdRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
// check clientOrderId field -> json key clientOrderId
clientOrderId := c.clientOrderId
// TEMPLATE check-required
if len(clientOrderId) == 0 {
return params, fmt.Errorf("clientOrderId is required, empty string given")
}
// END TEMPLATE check-required
// assign parameter of clientOrderId
params["clientOrderId"] = clientOrderId
return params, nil
}
func (c *CancelOrderByClientOrderIdRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (c *CancelOrderByClientOrderIdRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := c.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (c *CancelOrderByClientOrderIdRequest) Do(ctx context.Context) (*APIResponse, error) {
// no body params
var params interface{}
query := url.Values{}
apiURL := "/api/orders/by_client_id/:clientOrderId"
slugs, err := c.GetSlugsMap()
if err != nil {
return nil, err
}
apiURL = c.applySlugsToUrl(apiURL, slugs)
req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := c.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return &apiResponse, nil
}

View File

@ -0,0 +1,133 @@
// Code generated by "requestgen -method DELETE -url /api/orders/:orderID -type CancelOrderRequest -responseType .APIResponse"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
)
func (c *CancelOrderRequest) OrderID(orderID string) *CancelOrderRequest {
c.orderID = orderID
return c
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (c *CancelOrderRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (c *CancelOrderRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := c.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) {
params, err := c.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (c *CancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
// check orderID field -> json key orderID
orderID := c.orderID
// TEMPLATE check-required
if len(orderID) == 0 {
return params, fmt.Errorf("orderID is required, empty string given")
}
// END TEMPLATE check-required
// assign parameter of orderID
params["orderID"] = orderID
return params, nil
}
func (c *CancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (c *CancelOrderRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := c.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (c *CancelOrderRequest) Do(ctx context.Context) (*APIResponse, error) {
// no body params
var params interface{}
query := url.Values{}
apiURL := "/api/orders/:orderID"
slugs, err := c.GetSlugsMap()
if err != nil {
return nil, err
}
apiURL = c.applySlugsToUrl(apiURL, slugs)
req, err := c.client.NewAuthenticatedRequest(ctx, "DELETE", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := c.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
return &apiResponse, nil
}

View File

@ -0,0 +1,204 @@
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
c.Secret = secret
c.subAccount = subAccount
}
// NewRequest create new API request. Relative url can be provided in refURL.
func (c *RestClient) NewRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) {
rel, err := url.Parse(refURL)
if err != nil {
return nil, err
}
if params != nil {
rel.RawQuery = params.Encode()
}
body, err := castPayload(payload)
if err != nil {
return nil, err
}
pathURL := c.BaseURL.ResolveReference(rel)
return http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body))
}
// sendRequest sends the request to the API server and handle the response
func (c *RestClient) SendRequest(req *http.Request) (*requestgen.Response, error) {
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
// newResponse reads the response body and return a new Response object
response, err := requestgen.NewResponse(resp)
if err != nil {
return response, err
}
// Check error, if there is an error, return the ErrorResponse struct type
if response.IsError() {
return response, errors.New(string(response.Body))
}
return response, nil
}
// newAuthenticatedRequest creates new http request for authenticated routes.
func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) {
if len(c.Key) == 0 {
return nil, errors.New("empty api key")
}
if len(c.Secret) == 0 {
return nil, errors.New("empty api secret")
}
rel, err := url.Parse(refURL)
if err != nil {
return nil, err
}
if params != nil {
rel.RawQuery = params.Encode()
}
// pathURL is for sending request
pathURL := c.BaseURL.ResolveReference(rel)
// path here is used for auth header
path := pathURL.Path
if rel.RawQuery != "" {
path += "?" + rel.RawQuery
}
body, err := castPayload(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
// Build authentication headers
c.attachAuthHeaders(req, method, path, body)
return req, nil
}
func (c *RestClient) attachAuthHeaders(req *http.Request, method string, path string, body []byte) {
millisecondTs := time.Now().UnixNano() / int64(time.Millisecond)
ts := strconv.FormatInt(millisecondTs, 10)
p := ts + method + path + string(body)
signature := sign(c.Secret, p)
req.Header.Set("FTX-KEY", c.Key)
req.Header.Set("FTX-SIGN", signature)
req.Header.Set("FTX-TS", ts)
if c.subAccount != "" {
req.Header.Set("FTX-SUBACCOUNT", c.subAccount)
}
}
// sign uses sha256 to sign the payload with the given secret
func sign(secret, payload string) string {
var sig = hmac.New(sha256.New, []byte(secret))
_, err := sig.Write([]byte(payload))
if err != nil {
return ""
}
return hex.EncodeToString(sig.Sum(nil))
}
func castPayload(payload interface{}) ([]byte, error) {
if payload != nil {
switch v := payload.(type) {
case string:
return []byte(v), nil
case []byte:
return v, nil
default:
body, err := json.Marshal(v)
return body, err
}
}
return nil, nil
}

View File

@ -0,0 +1,99 @@
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: "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/USDT")
fills, err := req.Do(ctx)
assert.NoError(t, err)
assert.NotNil(t, fills)
t.Logf("fills: %+v", fills)
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, testCase.tt)
}
}

View File

@ -0,0 +1,42 @@
package ftxapi
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result
//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result
import (
"github.com/c9s/requestgen"
"github.com/c9s/bbgo/pkg/fixedpoint"
)
type Coin struct {
Bep2Asset *string `json:"bep2Asset"`
CanConvert bool `json:"canConvert"`
CanDeposit bool `json:"canDeposit"`
CanWithdraw bool `json:"canWithdraw"`
Collateral bool `json:"collateral"`
CollateralWeight fixedpoint.Value `json:"collateralWeight"`
CreditTo *string `json:"creditTo"`
Erc20Contract string `json:"erc20Contract"`
Fiat bool `json:"fiat"`
HasTag bool `json:"hasTag"`
Id string `json:"id"`
IsToken bool `json:"isToken"`
Methods []string `json:"methods"`
Name string `json:"name"`
SplMint string `json:"splMint"`
Trc20Contract string `json:"trc20Contract"`
UsdFungible bool `json:"usdFungible"`
}
//go:generate GetRequest -url "api/coins" -type GetCoinsRequest -responseDataType []Coin
type GetCoinsRequest struct {
client requestgen.AuthenticatedAPIClient
}
func (c *RestClient) NewGetCoinsRequest() *GetCoinsRequest {
return &GetCoinsRequest{
client: c,
}
}

View File

@ -0,0 +1,115 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/account -type GetAccountRequest -responseDataType .Account"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
)
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetAccountRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetAccountRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetAccountRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetAccountRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetAccountRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (g *GetAccountRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (g *GetAccountRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (g *GetAccountRequest) Do(ctx context.Context) (*Account, error) {
// no body params
var params interface{}
query := url.Values{}
apiURL := "/api/account"
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data Account
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return &data, nil
}

View File

@ -0,0 +1,115 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/wallet/balances -type GetBalancesRequest -responseDataType []Balance"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
)
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetBalancesRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetBalancesRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetBalancesRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetBalancesRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetBalancesRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (g *GetBalancesRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (g *GetBalancesRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (g *GetBalancesRequest) Do(ctx context.Context) ([]Balance, error) {
// no body params
var params interface{}
query := url.Values{}
apiURL := "/api/wallet/balances"
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data []Balance
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return data, nil
}

View File

@ -0,0 +1,115 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url api/coins -type GetCoinsRequest -responseDataType []Coin"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
)
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetCoinsRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetCoinsRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetCoinsRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetCoinsRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetCoinsRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (g *GetCoinsRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (g *GetCoinsRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (g *GetCoinsRequest) Do(ctx context.Context) ([]Coin, error) {
// no body params
var params interface{}
query := url.Values{}
apiURL := "api/coins"
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data []Coin
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return data, nil
}

View File

@ -0,0 +1,187 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/fills -type GetFillsRequest -responseDataType []Fill"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
"strconv"
"time"
)
func (g *GetFillsRequest) Market(market string) *GetFillsRequest {
g.market = &market
return g
}
func (g *GetFillsRequest) StartTime(startTime time.Time) *GetFillsRequest {
g.startTime = &startTime
return g
}
func (g *GetFillsRequest) EndTime(endTime time.Time) *GetFillsRequest {
g.endTime = &endTime
return g
}
func (g *GetFillsRequest) OrderID(orderID int) *GetFillsRequest {
g.orderID = &orderID
return g
}
func (g *GetFillsRequest) Order(order string) *GetFillsRequest {
g.order = &order
return g
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetFillsRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
// check market field -> json key market
if g.market != nil {
market := *g.market
// assign parameter of market
params["market"] = market
} else {
}
// check startTime field -> json key start_time
if g.startTime != nil {
startTime := *g.startTime
// assign parameter of startTime
// convert time.Time to seconds time stamp
params["start_time"] = strconv.FormatInt(startTime.Unix(), 10)
} else {
}
// check endTime field -> json key end_time
if g.endTime != nil {
endTime := *g.endTime
// assign parameter of endTime
// convert time.Time to seconds time stamp
params["end_time"] = strconv.FormatInt(endTime.Unix(), 10)
} else {
}
// check orderID field -> json key orderId
if g.orderID != nil {
orderID := *g.orderID
// assign parameter of orderID
params["orderId"] = orderID
} else {
}
// check order field -> json key order
if g.order != nil {
order := *g.order
// assign parameter of order
params["order"] = order
} else {
}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetFillsRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetFillsRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetFillsRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetFillsRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (g *GetFillsRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (g *GetFillsRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (g *GetFillsRequest) Do(ctx context.Context) ([]Fill, error) {
// no body params
var params interface{}
query, err := g.GetQueryParameters()
if err != nil {
return nil, err
}
apiURL := "/api/fills"
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data []Fill
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return data, nil
}

View File

@ -0,0 +1,131 @@
// 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"
"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 {
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) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (g *GetMarketRequest) Do(ctx context.Context) (*Market, error) {
// no body params
var params interface{}
query := url.Values{}
apiURL := "api/markets/:market"
slugs, err := g.GetSlugsMap()
if err != nil {
return nil, err
}
apiURL = g.applySlugsToUrl(apiURL, slugs)
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data Market
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return &data, nil
}

View File

@ -0,0 +1,115 @@
// 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"
"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 {
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) 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.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data []Market
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return data, nil
}

View File

@ -0,0 +1,128 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/orders -type GetOpenOrdersRequest -responseDataType []Order"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
)
func (g *GetOpenOrdersRequest) Market(market string) *GetOpenOrdersRequest {
g.market = market
return g
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetOpenOrdersRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
// check market field -> json key market
market := g.market
// assign parameter of market
params["market"] = market
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetOpenOrdersRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetOpenOrdersRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetOpenOrdersRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetOpenOrdersRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (g *GetOpenOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (g *GetOpenOrdersRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (g *GetOpenOrdersRequest) Do(ctx context.Context) ([]Order, error) {
// no body params
var params interface{}
query, err := g.GetQueryParameters()
if err != nil {
return nil, err
}
apiURL := "/api/orders"
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data []Order
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return data, nil
}

View File

@ -0,0 +1,158 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/orders/history -type GetOrderHistoryRequest -responseDataType []Order"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
"strconv"
"time"
)
func (g *GetOrderHistoryRequest) Market(market string) *GetOrderHistoryRequest {
g.market = market
return g
}
func (g *GetOrderHistoryRequest) StartTime(startTime time.Time) *GetOrderHistoryRequest {
g.startTime = &startTime
return g
}
func (g *GetOrderHistoryRequest) EndTime(endTime time.Time) *GetOrderHistoryRequest {
g.endTime = &endTime
return g
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetOrderHistoryRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
// check market field -> json key market
market := g.market
// assign parameter of market
params["market"] = market
// check startTime field -> json key start_time
if g.startTime != nil {
startTime := *g.startTime
// assign parameter of startTime
// convert time.Time to seconds time stamp
params["start_time"] = strconv.FormatInt(startTime.Unix(), 10)
} else {
}
// check endTime field -> json key end_time
if g.endTime != nil {
endTime := *g.endTime
// assign parameter of endTime
// convert time.Time to seconds time stamp
params["end_time"] = strconv.FormatInt(endTime.Unix(), 10)
} else {
}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetOrderHistoryRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetOrderHistoryRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetOrderHistoryRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetOrderHistoryRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (g *GetOrderHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (g *GetOrderHistoryRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (g *GetOrderHistoryRequest) Do(ctx context.Context) ([]Order, error) {
// no body params
var params interface{}
query, err := g.GetQueryParameters()
if err != nil {
return nil, err
}
apiURL := "/api/orders/history"
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data []Order
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return data, nil
}

View File

@ -0,0 +1,131 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/orders/:orderId -type GetOrderStatusRequest -responseDataType .Order"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
)
func (g *GetOrderStatusRequest) OrderID(orderID uint64) *GetOrderStatusRequest {
g.orderID = orderID
return g
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetOrderStatusRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetOrderStatusRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetOrderStatusRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetOrderStatusRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetOrderStatusRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
// check orderID field -> json key orderId
orderID := g.orderID
// assign parameter of orderID
params["orderId"] = orderID
return params, nil
}
func (g *GetOrderStatusRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (g *GetOrderStatusRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (g *GetOrderStatusRequest) Do(ctx context.Context) (*Order, error) {
// no body params
var params interface{}
query := url.Values{}
apiURL := "/api/orders/:orderId"
slugs, err := g.GetSlugsMap()
if err != nil {
return nil, err
}
apiURL = g.applySlugsToUrl(apiURL, slugs)
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data Order
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return &data, nil
}

View File

@ -0,0 +1,115 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /api/positions -type GetPositionsRequest -responseDataType []Position"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"net/url"
"regexp"
)
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (g *GetPositionsRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (g *GetPositionsRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (g *GetPositionsRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := g.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (g *GetPositionsRequest) GetParametersJSON() ([]byte, error) {
params, err := g.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (g *GetPositionsRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (g *GetPositionsRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (g *GetPositionsRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := g.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (g *GetPositionsRequest) Do(ctx context.Context) ([]Position, error) {
// no body params
var params interface{}
query := url.Values{}
apiURL := "/api/positions"
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data []Position
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return data, nil
}

View File

@ -0,0 +1,58 @@
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.AuthenticatedAPIClient
}
func (c *RestClient) NewGetMarketsRequest() *GetMarketsRequest {
return &GetMarketsRequest{
client: c,
}
}
//go:generate GetRequest -url "api/markets/:market" -type GetMarketRequest -responseDataType .Market
type GetMarketRequest struct {
client requestgen.AuthenticatedAPIClient
market string `param:"market,slug"`
}
func (c *RestClient) NewGetMarketRequest(market string) *GetMarketRequest {
return &GetMarketRequest{
client: c,
market: market,
}
}

View File

@ -0,0 +1,219 @@
// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Result -url /api/orders -type PlaceOrderRequest -responseDataType .Order"; DO NOT EDIT.
package ftxapi
import (
"context"
"encoding/json"
"fmt"
"github.com/c9s/bbgo/pkg/fixedpoint"
"net/url"
"regexp"
)
func (p *PlaceOrderRequest) Market(market string) *PlaceOrderRequest {
p.market = market
return p
}
func (p *PlaceOrderRequest) Side(side Side) *PlaceOrderRequest {
p.side = side
return p
}
func (p *PlaceOrderRequest) Price(price fixedpoint.Value) *PlaceOrderRequest {
p.price = price
return p
}
func (p *PlaceOrderRequest) Size(size fixedpoint.Value) *PlaceOrderRequest {
p.size = size
return p
}
func (p *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest {
p.orderType = orderType
return p
}
func (p *PlaceOrderRequest) Ioc(ioc bool) *PlaceOrderRequest {
p.ioc = &ioc
return p
}
func (p *PlaceOrderRequest) PostOnly(postOnly bool) *PlaceOrderRequest {
p.postOnly = &postOnly
return p
}
func (p *PlaceOrderRequest) ClientID(clientID string) *PlaceOrderRequest {
p.clientID = &clientID
return p
}
// GetQueryParameters builds and checks the query parameters and returns url.Values
func (p *PlaceOrderRequest) GetQueryParameters() (url.Values, error) {
var params = map[string]interface{}{}
query := url.Values{}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParameters builds and checks the parameters and return the result in a map object
func (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
// check market field -> json key market
market := p.market
// TEMPLATE check-required
if len(market) == 0 {
return params, fmt.Errorf("market is required, empty string given")
}
// END TEMPLATE check-required
// assign parameter of market
params["market"] = market
// check side field -> json key side
side := p.side
// TEMPLATE check-required
if len(side) == 0 {
return params, fmt.Errorf("side is required, empty string given")
}
// END TEMPLATE check-required
// assign parameter of side
params["side"] = side
// check price field -> json key price
price := p.price
// assign parameter of price
params["price"] = price
// check size field -> json key size
size := p.size
// assign parameter of size
params["size"] = size
// check orderType field -> json key type
orderType := p.orderType
// assign parameter of orderType
params["type"] = orderType
// check ioc field -> json key ioc
if p.ioc != nil {
ioc := *p.ioc
// assign parameter of ioc
params["ioc"] = ioc
} else {
}
// check postOnly field -> json key postOnly
if p.postOnly != nil {
postOnly := *p.postOnly
// assign parameter of postOnly
params["postOnly"] = postOnly
} else {
}
// check clientID field -> json key clientId
if p.clientID != nil {
clientID := *p.clientID
// assign parameter of clientID
params["clientId"] = clientID
} else {
}
return params, nil
}
// GetParametersQuery converts the parameters from GetParameters into the url.Values format
func (p *PlaceOrderRequest) GetParametersQuery() (url.Values, error) {
query := url.Values{}
params, err := p.GetParameters()
if err != nil {
return query, err
}
for k, v := range params {
query.Add(k, fmt.Sprintf("%v", v))
}
return query, nil
}
// GetParametersJSON converts the parameters from GetParameters into the JSON format
func (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) {
params, err := p.GetParameters()
if err != nil {
return nil, err
}
return json.Marshal(params)
}
// GetSlugParameters builds and checks the slug parameters and return the result in a map object
func (p *PlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) {
var params = map[string]interface{}{}
return params, nil
}
func (p *PlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string {
for k, v := range slugs {
needleRE := regexp.MustCompile(":" + k + "\\b")
url = needleRE.ReplaceAllString(url, v)
}
return url
}
func (p *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) {
slugs := map[string]string{}
params, err := p.GetSlugParameters()
if err != nil {
return slugs, nil
}
for k, v := range params {
slugs[k] = fmt.Sprintf("%v", v)
}
return slugs, nil
}
func (p *PlaceOrderRequest) Do(ctx context.Context) (*Order, error) {
params, err := p.GetParameters()
if err != nil {
return nil, err
}
query := url.Values{}
apiURL := "/api/orders"
req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params)
if err != nil {
return nil, err
}
response, err := p.client.SendRequest(req)
if err != nil {
return nil, err
}
var apiResponse APIResponse
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data Order
if err := json.Unmarshal(apiResponse.Result, &data); err != nil {
return nil, err
}
return &data, nil
}

View File

@ -0,0 +1,172 @@
package ftxapi
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result
//go:generate -command DeleteRequest requestgen -method DELETE -responseType .APIResponse -responseDataField Result
import (
"time"
"github.com/c9s/requestgen"
"github.com/c9s/bbgo/pkg/fixedpoint"
)
type Order struct {
CreatedAt time.Time `json:"createdAt"`
Future string `json:"future"`
Id int64 `json:"id"`
Market string `json:"market"`
Price fixedpoint.Value `json:"price"`
AvgFillPrice fixedpoint.Value `json:"avgFillPrice"`
Size fixedpoint.Value `json:"size"`
RemainingSize fixedpoint.Value `json:"remainingSize"`
FilledSize fixedpoint.Value `json:"filledSize"`
Side Side `json:"side"`
Status OrderStatus `json:"status"`
Type OrderType `json:"type"`
ReduceOnly bool `json:"reduceOnly"`
Ioc bool `json:"ioc"`
PostOnly bool `json:"postOnly"`
ClientId string `json:"clientId"`
}
//go:generate GetRequest -url "/api/orders" -type GetOpenOrdersRequest -responseDataType []Order
type GetOpenOrdersRequest struct {
client requestgen.AuthenticatedAPIClient
market string `param:"market,query"`
}
func (c *RestClient) NewGetOpenOrdersRequest(market string) *GetOpenOrdersRequest {
return &GetOpenOrdersRequest{
client: c,
market: market,
}
}
//go:generate GetRequest -url "/api/orders/history" -type GetOrderHistoryRequest -responseDataType []Order
type GetOrderHistoryRequest struct {
client requestgen.AuthenticatedAPIClient
market string `param:"market,query"`
startTime *time.Time `param:"start_time,seconds,query"`
endTime *time.Time `param:"end_time,seconds,query"`
}
func (c *RestClient) NewGetOrderHistoryRequest(market string) *GetOrderHistoryRequest {
return &GetOrderHistoryRequest{
client: c,
market: market,
}
}
//go:generate PostRequest -url "/api/orders" -type PlaceOrderRequest -responseDataType .Order
type PlaceOrderRequest struct {
client requestgen.AuthenticatedAPIClient
market string `param:"market,required"`
side Side `param:"side,required"`
price fixedpoint.Value `param:"price"`
size fixedpoint.Value `param:"size"`
orderType OrderType `param:"type"`
ioc *bool `param:"ioc"`
postOnly *bool `param:"postOnly"`
clientID *string `param:"clientId,optional"`
}
func (c *RestClient) NewPlaceOrderRequest() *PlaceOrderRequest {
return &PlaceOrderRequest{
client: c,
}
}
//go:generate requestgen -method DELETE -url "/api/orders/:orderID" -type CancelOrderRequest -responseType .APIResponse
type CancelOrderRequest struct {
client requestgen.AuthenticatedAPIClient
orderID string `param:"orderID,required,slug"`
}
func (c *RestClient) NewCancelOrderRequest(orderID string) *CancelOrderRequest {
return &CancelOrderRequest{
client: c,
orderID: orderID,
}
}
//go:generate requestgen -method DELETE -url "/api/orders" -type CancelAllOrderRequest -responseType .APIResponse
type CancelAllOrderRequest struct {
client requestgen.AuthenticatedAPIClient
market *string `param:"market"`
}
func (c *RestClient) NewCancelAllOrderRequest() *CancelAllOrderRequest {
return &CancelAllOrderRequest{
client: c,
}
}
//go:generate requestgen -method DELETE -url "/api/orders/by_client_id/:clientOrderId" -type CancelOrderByClientOrderIdRequest -responseType .APIResponse
type CancelOrderByClientOrderIdRequest struct {
client requestgen.AuthenticatedAPIClient
clientOrderId string `param:"clientOrderId,required,slug"`
}
func (c *RestClient) NewCancelOrderByClientOrderIdRequest(clientOrderId string) *CancelOrderByClientOrderIdRequest {
return &CancelOrderByClientOrderIdRequest{
client: c,
clientOrderId: clientOrderId,
}
}
type Fill struct {
// Id is fill ID
Id uint64 `json:"id"`
Future string `json:"future"`
Liquidity Liquidity `json:"liquidity"`
Market string `json:"market"`
BaseCurrency string `json:"baseCurrency"`
QuoteCurrency string `json:"quoteCurrency"`
OrderId uint64 `json:"orderId"`
TradeId uint64 `json:"tradeId"`
Price fixedpoint.Value `json:"price"`
Side Side `json:"side"`
Size fixedpoint.Value `json:"size"`
Time time.Time `json:"time"`
Type string `json:"type"` // always = "order"
Fee fixedpoint.Value `json:"fee"`
FeeCurrency string `json:"feeCurrency"`
FeeRate fixedpoint.Value `json:"feeRate"`
}
//go:generate GetRequest -url "/api/fills" -type GetFillsRequest -responseDataType []Fill
type GetFillsRequest struct {
client requestgen.AuthenticatedAPIClient
market *string `param:"market,query"`
startTime *time.Time `param:"start_time,seconds,query"`
endTime *time.Time `param:"end_time,seconds,query"`
orderID *int `param:"orderId,query"`
// order is the order of the returned records, asc or null
order *string `param:"order,query"`
}
func (c *RestClient) NewGetFillsRequest() *GetFillsRequest {
return &GetFillsRequest{
client: c,
}
}
//go:generate GetRequest -url "/api/orders/:orderId" -type GetOrderStatusRequest -responseDataType .Order
type GetOrderStatusRequest struct {
client requestgen.AuthenticatedAPIClient
orderID uint64 `param:"orderId,slug"`
}
func (c *RestClient) NewGetOrderStatusRequest(orderID uint64) *GetOrderStatusRequest {
return &GetOrderStatusRequest{
client: c,
orderID: orderID,
}
}

View File

@ -0,0 +1,35 @@
package ftxapi
type Liquidity string
const (
LiquidityTaker Liquidity = "taker"
LiquidityMaker Liquidity = "maker"
)
type Side string
const (
SideBuy Side = "buy"
SideSell Side = "sell"
)
type OrderType string
const (
OrderTypeLimit OrderType = "limit"
OrderTypeMarket OrderType = "market"
// trigger order types
OrderTypeStopLimit OrderType = "stop"
OrderTypeTrailingStop OrderType = "trailingStop"
OrderTypeTakeProfit OrderType = "takeProfit"
)
type OrderStatus string
const (
OrderStatusNew OrderStatus = "new"
OrderStatusOpen OrderStatus = "open"
OrderStatusClosed OrderStatus = "closed"
)

View File

@ -55,9 +55,7 @@ func (r *restRequest) Transfer(ctx context.Context, p TransferPayload) (transfer
type restRequest struct {
*walletRequest
*orderRequest
*accountRequest
*marketRequest
*fillsRequest
*transferRequest
key, secret string
@ -88,9 +86,7 @@ func newRestRequest(c *http.Client, baseURL *url.URL) *restRequest {
p: make(map[string]interface{}),
}
r.fillsRequest = &fillsRequest{restRequest: r}
r.marketRequest = &marketRequest{restRequest: r}
r.accountRequest = &accountRequest{restRequest: r}
r.walletRequest = &walletRequest{restRequest: r}
r.orderRequest = &orderRequest{restRequest: r}
return r
@ -241,12 +237,12 @@ func (r *restRequest) sendRequest(req *http.Request) (*util.Response, error) {
type ErrorResponse struct {
*util.Response
IsSuccess bool `json:"Success"`
IsSuccess bool `json:"success"`
ErrorString string `json:"error,omitempty"`
}
func (r *ErrorResponse) Error() string {
return fmt.Sprintf("%s %s %d, Success: %t, err: %s",
return fmt.Sprintf("%s %s %d, success: %t, err: %s",
r.Response.Request.Method,
r.Response.Request.URL.String(),
r.Response.StatusCode,

View File

@ -1,47 +0,0 @@
package ftx
import (
"context"
"encoding/json"
"fmt"
)
type accountRequest struct {
*restRequest
}
func (r *accountRequest) Account(ctx context.Context) (accountResponse, error) {
resp, err := r.
Method("GET").
ReferenceURL("api/account").
DoAuthenticatedRequest(ctx)
if err != nil {
return accountResponse{}, err
}
var a accountResponse
if err := json.Unmarshal(resp.Body, &a); err != nil {
return accountResponse{}, fmt.Errorf("failed to unmarshal account response body to json: %w", err)
}
return a, nil
}
func (r *accountRequest) Positions(ctx context.Context) (positionsResponse, error) {
resp, err := r.
Method("GET").
ReferenceURL("api/positions").
DoAuthenticatedRequest(ctx)
if err != nil {
return positionsResponse{}, err
}
var p positionsResponse
if err := json.Unmarshal(resp.Body, &p); err != nil {
return positionsResponse{}, fmt.Errorf("failed to unmarshal position response body to json: %w", err)
}
return p, nil
}

View File

@ -1,50 +0,0 @@
package ftx
import (
"context"
"encoding/json"
"fmt"
"strconv"
"time"
)
type fillsRequest struct {
*restRequest
}
func (r *fillsRequest) Fills(ctx context.Context, market string, since, until time.Time, limit int64, orderByASC bool) (fillsResponse, error) {
q := make(map[string]string)
if len(market) > 0 {
q["market"] = market
}
if since != (time.Time{}) {
q["start_time"] = strconv.FormatInt(since.Unix(), 10)
}
if until != (time.Time{}) {
q["end_time"] = strconv.FormatInt(until.Unix(), 10)
}
if limit > 0 {
q["limit"] = strconv.FormatInt(limit, 10)
}
// default is descending
if orderByASC {
q["order"] = "asc"
}
resp, err := r.
Method("GET").
ReferenceURL("api/fills").
Query(q).
DoAuthenticatedRequest(ctx)
if err != nil {
return fillsResponse{}, err
}
var f fillsResponse
if err := json.Unmarshal(resp.Body, &f); err != nil {
fmt.Println("??? => ", resp.Body)
return fillsResponse{}, fmt.Errorf("failed to unmarshal fills response body to json: %w", err)
}
return f, nil
}

View File

@ -123,7 +123,7 @@ func (h *messageHandler) handlePrivateOrders(response websocketResponse) {
return
}
globalOrder, err := toGlobalOrder(r.Data)
globalOrder, err := toGlobalOrderNew(r.Data)
if err != nil {
logger.WithError(err).Errorf("failed to convert order update to global order")
return

View File

@ -6,8 +6,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
func Test_messageHandler_handleMessage(t *testing.T) {
@ -93,9 +93,8 @@ func Test_messageHandler_handleMessage(t *testing.T) {
h.OnTradeUpdate(func(trade types.Trade) {
i++
assert.Equal(t, types.Trade{
GID: 0,
ID: 6276431,
OrderID: 323789,
ID: uint64(6276431),
OrderID: uint64(323789),
Exchange: types.ExchangeFTX,
Price: fixedpoint.NewFromFloat(2.723),
Quantity: fixedpoint.One,
@ -109,6 +108,7 @@ func Test_messageHandler_handleMessage(t *testing.T) {
FeeCurrency: "USD",
IsMargin: false,
IsIsolated: false,
IsFutures: true,
StrategyID: sql.NullString{},
PnL: sql.NullFloat64{},
}, trade)

View File

@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
@ -115,7 +116,7 @@ type optionalFields struct {
type orderUpdateResponse struct {
mandatoryFields
Data order `json:"data"`
Data ftxapi.Order `json:"data"`
}
func (r websocketResponse) toOrderUpdateResponse() (orderUpdateResponse, error) {
@ -133,7 +134,7 @@ func (r websocketResponse) toOrderUpdateResponse() (orderUpdateResponse, error)
type tradeUpdateResponse struct {
mandatoryFields
Data fill `json:"data"`
Data ftxapi.Fill `json:"data"`
}
func (r websocketResponse) toTradeUpdateResponse() (tradeUpdateResponse, error) {
@ -195,7 +196,7 @@ func (r websocketResponse) toErrResponse() errResponse {
}
}
//sample :{"bid": 49194.0, "ask": 49195.0, "bidSize": 0.0775, "askSize": 0.0247, "last": 49200.0, "time": 1640171788.9339821}
// 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)
@ -404,12 +405,12 @@ func toGlobalBookTicker(r bookTickerResponse) (types.BookTicker, error) {
return types.BookTicker{
// ex. BTC/USDT
Symbol: toGlobalSymbol(strings.ToUpper(r.Market)),
//Time: r.Timestamp,
// Time: r.Timestamp,
Buy: r.Bid,
BuySize: r.BidSize,
Sell: r.Ask,
SellSize: r.AskSize,
//Last: r.Last,
// Last: r.Last,
}, nil
}

View File

@ -9,6 +9,7 @@ import (
"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"
)
@ -225,8 +226,8 @@ func Test_websocketResponse_toOrderUpdateResponse(t *testing.T) {
Channel: privateOrdersChannel,
Type: updateRespType,
},
Data: order{
ID: 12345,
Data: ftxapi.Order{
Id: 12345,
ClientId: "test-client-id",
Market: "SOL/USD",
Type: "limit",
@ -237,11 +238,10 @@ func Test_websocketResponse_toOrderUpdateResponse(t *testing.T) {
FilledSize: fixedpoint.Zero,
RemainingSize: fixedpoint.Zero,
ReduceOnly: false,
Liquidation: false,
AvgFillPrice: fixedpoint.Zero,
PostOnly: false,
Ioc: false,
CreatedAt: datetime{Time: mustParseDatetime("2021-03-27T11:00:36.418674+00:00")},
CreatedAt: mustParseDatetime("2021-03-27T11:00:36.418674+00:00"),
Future: "",
},
}, r)