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 dev-version: pkg/version/dev.go
git commit $< -m "update dev build version" 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 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) [[ -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 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 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 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 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 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 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 * [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 orderupdate](bbgo_orderupdate.md) - Listen to order update events
* [bbgo pnl](bbgo_pnl.md) - pnl calculator * [bbgo pnl](bbgo_pnl.md) - pnl calculator
* [bbgo run](bbgo_run.md) - run strategies from config file * [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 sync](bbgo_sync.md) - sync trades and orders history
* [bbgo trades](bbgo_trades.md) - Query trading history * [bbgo trades](bbgo_trades.md) - Query trading history
* [bbgo tradeupdate](bbgo_tradeupdate.md) - Listen to trade update events * [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 userdatastream](bbgo_userdatastream.md) - Listen to session events (orderUpdate, tradeUpdate, balanceUpdate, balanceSnapshot)
* [bbgo version](bbgo_version.md) - show version name * [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 * [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 * [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 Show user account balances
``` ```
bbgo balances [flags] bbgo balances --session SESSION [flags]
``` ```
### Options ### Options
@ -40,4 +40,4 @@ bbgo balances [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [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 * [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 * [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 * [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 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 ### Options
@ -48,4 +48,4 @@ bbgo execute-order [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [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 * [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 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 ### Options
@ -41,4 +41,4 @@ bbgo list-orders [status] [flags]
* [bbgo](bbgo.md) - bbgo is a crypto trading bot * [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 * [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 * [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 * [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 * [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 * [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 ## 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] 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 * [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 * [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 * [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 * [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 * [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 * [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 * [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 ## Checklist
Exchange Interface - minimum requirement for trading Exchange Interface - the minimum requirement for spot trading
- [ ] QueryMarkets - [ ] QueryMarkets
- [ ] QueryTickers - [ ] QueryTickers
@ -17,11 +17,15 @@ Exchange Interface - minimum requirement for trading
- [ ] CancelOrders - [ ] CancelOrders
- [ ] NewStream - [ ] NewStream
Trading History Service Interface - used for syncing user trading data Trading History Service Interface - (optional) used for syncing user trading data
- [ ] QueryClosedOrders - [ ] QueryClosedOrders
- [ ] QueryTrades - [ ] QueryTrades
Order Query Service Interface - (optional) used for querying order status
- [ ] QueryOrder
Back-testing service - kline data is used for back-testing Back-testing service - kline data is used for back-testing
- [ ] QueryKLines - [ ] 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 ```shell
godotenv -f .env.local -- go run ./cmd/bbgo orderbook --config config/bbgo.yaml --session kucoin --symbol BTCUSDT 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 ```shell
godotenv -f .env.local -- go run ./cmd/bbgo --config config/bbgo.yaml userdatastream --session kucoin 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 ```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 ```shell
godotenv -f .env.local -- go run ./cmd/bbgo list-orders --session kucoin --symbol=BTCUSDT open godotenv -f .env.local -- go run ./cmd/bbgo balances --session $BBGO_SESSION
godotenv -f .env.local -- go run ./cmd/bbgo list-orders --session kucoin --symbol=BTCUSDT closed
``` ```
### Testing order cancel ### Test order submit
```shell ```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 ( import (
"context" "context"
"fmt" "fmt"
"os"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -19,41 +17,19 @@ func init() {
// go run ./cmd/bbgo balances --session=ftx // go run ./cmd/bbgo balances --session=ftx
var balancesCmd = &cobra.Command{ var balancesCmd = &cobra.Command{
Use: "balances", Use: "balances --session SESSION",
Short: "Show user account balances", Short: "Show user account balances",
SilenceUsage: true, SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background() 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") sessionName, err := cmd.Flags().GetString("session")
if err != nil { if err != nil {
return err return err
} }
// if config file exists, use the config loaded from the config file. if userConfig == nil {
// otherwise, use a empty config object return fmt.Errorf("user config is not loaded")
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
} }
environ := bbgo.NewEnvironment() environ := bbgo.NewEnvironment()

View File

@ -19,9 +19,58 @@ import (
"github.com/c9s/bbgo/pkg/types" "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 // go run ./cmd/bbgo list-orders [open|closed] --session=ftx --symbol=BTCUSDT
var listOrdersCmd = &cobra.Command{ 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", 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. // default is open which means we query open orders if you haven't provided args.
@ -39,22 +88,10 @@ var listOrdersCmd = &cobra.Command{
return fmt.Errorf("--config option is required") return fmt.Errorf("--config option is required")
} }
// if config file exists, use the config loaded from the config file. if userConfig == nil {
// otherwise, use a empty config object return errors.New("config file is required")
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
} }
environ := bbgo.NewEnvironment() environ := bbgo.NewEnvironment()
if err := environ.ConfigureExchangeSessions(userConfig); err != nil { if err := environ.ConfigureExchangeSessions(userConfig); err != nil {
@ -116,7 +153,7 @@ var listOrdersCmd = &cobra.Command{
} }
var executeOrderCmd = &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", Short: "execute buy/sell on the balance/position you have on specific symbol",
SilenceUsage: true, SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error { 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 // go run ./cmd/bbgo submit-order --session=ftx --symbol=BTCUSDT --side=buy --price=18000 --quantity=0.001
var submitOrderCmd = &cobra.Command{ var submitOrderCmd = &cobra.Command{
Use: "submit-order --session SESSION --symbol SYMBOL --side SIDE --quantity QUANTITY [--price PRICE]", 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, SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background() ctx := context.Background()
@ -345,6 +382,10 @@ func init() {
listOrdersCmd.Flags().String("session", "", "the exchange session name for sync") listOrdersCmd.Flags().String("session", "", "the exchange session name for sync")
listOrdersCmd.Flags().String("symbol", "", "the trading pair, like btcusdt") 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("session", "", "the exchange session name for sync")
submitOrderCmd.Flags().String("symbol", "", "the trading pair, like btcusdt") submitOrderCmd.Flags().String("symbol", "", "the trading pair, like btcusdt")
submitOrderCmd.Flags().String("side", "", "the trading side: buy or sell") 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") executeOrderCmd.Flags().Int("price-ticks", 0, "the number of price tick for the jump spread, default to 0")
RootCmd.AddCommand(listOrdersCmd) RootCmd.AddCommand(listOrdersCmd)
RootCmd.AddCommand(getOrderCmd)
RootCmd.AddCommand(submitOrderCmd) RootCmd.AddCommand(submitOrderCmd)
RootCmd.AddCommand(executeOrderCmd) RootCmd.AddCommand(executeOrderCmd)
} }

View File

@ -7,6 +7,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -36,6 +37,67 @@ func TrimLowerString(original string) string {
var errUnsupportedOrderStatus = fmt.Errorf("unsupported order status") 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) { func toGlobalOrder(r order) (types.Order, error) {
// In exchange/max/convert.go, it only parses these fields. // In exchange/max/convert.go, it only parses these fields.
timeInForce := types.TimeInForceGTC timeInForce := types.TimeInForceGTC
@ -125,24 +187,24 @@ func toGlobalDepositStatus(input string) (types.DepositStatus, error) {
return "", fmt.Errorf("unsupported status %s", input) 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{ return types.Trade{
ID: f.TradeId, ID: f.TradeId,
GID: 0,
OrderID: f.OrderId, OrderID: f.OrderId,
Exchange: types.ExchangeFTX, Exchange: types.ExchangeFTX,
Price: f.Price, Price: f.Price,
Quantity: f.Size, Quantity: f.Size,
QuoteQuantity: f.Price.Mul(f.Size), QuoteQuantity: f.Price.Mul(f.Size),
Symbol: toGlobalSymbol(f.Market), Symbol: toGlobalSymbol(f.Market),
Side: f.Side, Side: types.SideType(strings.ToUpper(string(f.Side))),
IsBuyer: f.Side == types.SideTypeBuy, IsBuyer: f.Side == ftxapi.SideBuy,
IsMaker: f.Liquidity == "maker", IsMaker: f.Liquidity == ftxapi.LiquidityMaker,
Time: types.Time(f.Time.Time), Time: types.Time(f.Time),
Fee: f.Fee, Fee: f.Fee,
FeeCurrency: f.FeeCurrency, FeeCurrency: f.FeeCurrency,
IsMargin: false, IsMargin: false,
IsIsolated: false, IsIsolated: false,
IsFutures: f.Future != "",
}, nil }, nil
} }
@ -169,17 +231,17 @@ const (
OrderTypeMarket OrderType = "market" OrderTypeMarket OrderType = "market"
) )
func toLocalOrderType(orderType types.OrderType) (OrderType, error) { func toLocalOrderType(orderType types.OrderType) (ftxapi.OrderType, error) {
switch orderType { switch orderType {
case types.OrderTypeLimitMaker: case types.OrderTypeLimitMaker:
return OrderTypeLimit, nil return ftxapi.OrderTypeLimit, nil
case types.OrderTypeLimit: case types.OrderTypeLimit:
return OrderTypeLimit, nil return ftxapi.OrderTypeLimit, nil
case types.OrderTypeMarket: case types.OrderTypeMarket:
return OrderTypeMarket, nil return ftxapi.OrderTypeMarket, nil
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -104,18 +105,18 @@ func Test_toGlobalSymbol(t *testing.T) {
func Test_toLocalOrderTypeWithLimitMaker(t *testing.T) { func Test_toLocalOrderTypeWithLimitMaker(t *testing.T) {
orderType, err := toLocalOrderType(types.OrderTypeLimitMaker) orderType, err := toLocalOrderType(types.OrderTypeLimitMaker)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, orderType, OrderTypeLimit) assert.Equal(t, ftxapi.OrderTypeLimit, orderType)
} }
func Test_toLocalOrderTypeWithLimit(t *testing.T) { func Test_toLocalOrderTypeWithLimit(t *testing.T) {
orderType, err := toLocalOrderType(types.OrderTypeLimit) orderType, err := toLocalOrderType(types.OrderTypeLimit)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, orderType, OrderTypeLimit) assert.Equal(t, ftxapi.OrderTypeLimit, orderType)
} }
func Test_toLocalOrderTypeWithMarket(t *testing.T) { func Test_toLocalOrderTypeWithMarket(t *testing.T) {
orderType, err := toLocalOrderType(types.OrderTypeMarket) orderType, err := toLocalOrderType(types.OrderTypeMarket)
assert.NoError(t, err) 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/http"
"net/url" "net/url"
"sort" "sort"
"strconv"
"strings" "strings"
"time" "time"
@ -14,6 +15,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "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 //go:generate go run generate_symbol_map.go
type Exchange struct { type Exchange struct {
client *ftxapi.RestClient
key, secret string key, secret string
subAccount string subAccount string
restEndpoint *url.URL restEndpoint *url.URL
@ -78,7 +82,10 @@ func NewExchange(key, secret string, subAccount string) *Exchange {
panic(err) panic(err)
} }
client := ftxapi.NewClient()
client.Auth(key, secret, subAccount)
return &Exchange{ return &Exchange{
client: client,
restEndpoint: u, restEndpoint: u,
key: key, key: key,
secret: secret, 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) { 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 { if err != nil {
return nil, err return nil, err
} }
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying markets failure")
}
markets := MarketMap{} markets := MarketMap{}
for _, m := range resp.Result { for _, m := range ftxMarkets {
symbol := toGlobalSymbol(m.Name) symbol := toGlobalSymbol(m.Name)
symbolMap[symbol] = 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) { 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 { if err != nil {
return nil, err return nil, err
} }
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying balances failure")
}
a := &types.Account{ a := &types.Account{
MakerCommission: resp.Result.MakerFee, MakerCommission: ftxAccount.MakerFee,
TakerCommission: resp.Result.TakerFee, TakerCommission: ftxAccount.TakerFee,
TotalAccountValue: resp.Result.TotalAccountValue, TotalAccountValue: ftxAccount.TotalAccountValue,
} }
balances, err := e.QueryAccountBalances(ctx) balances, err := e.QueryAccountBalances(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
a.UpdateBalances(balances)
a.UpdateBalances(balances)
return a, nil return a, nil
} }
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { 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 { if err != nil {
return nil, err return nil, err
} }
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying balances failure")
}
var balances = make(types.BalanceMap) var balances = make(types.BalanceMap)
for _, r := range resp.Result { for _, r := range ftxBalances {
balances[toGlobalCurrency(r.Coin)] = types.Balance{ currency := toGlobalCurrency(r.Coin)
Currency: toGlobalCurrency(r.Coin), balances[currency] = types.Balance{
Currency: currency,
Available: r.Free, Available: r.Free,
Locked: r.Total.Sub(r.Free), Locked: r.Total.Sub(r.Free),
} }
@ -340,76 +344,57 @@ func isIntervalSupportedInKLine(interval types.Interval) bool {
} }
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { 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{}) tradeIDs := make(map[uint64]struct{})
lastTradeID := options.LastTradeID lastTradeID := options.LastTradeID
var trades []types.Trade
symbol = strings.ToUpper(symbol)
for since.Before(until) { req := e.client.NewGetFillsRequest()
// DO not set limit to `1` since you will always get the same response. req.Market(toLocalSymbol(symbol))
resp, err := e.newRest().Fills(ctx, toLocalSymbol(symbol), since, until, limit, true)
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 { if err != nil {
return nil, err return nil, err
} }
if !resp.Success {
return nil, fmt.Errorf("ftx returns failure")
}
sort.Slice(resp.Result, func(i, j int) bool { sort.Slice(fills, func(i, j int) bool {
return resp.Result[i].TradeId < resp.Result[j].TradeId return fills[i].Time.Before(fills[j].Time)
}) })
for _, r := range resp.Result { var trades []types.Trade
// always update since to avoid infinite loop symbol = strings.ToUpper(symbol)
since = r.Time.Time for _, fill := range fills {
if _, ok := tradeIDs[fill.TradeId]; ok {
if _, ok := tradeIDs[r.TradeId]; ok {
continue continue
} }
if r.TradeId <= lastTradeID || r.Time.Before(since) || r.Time.After(until) || r.Market != toLocalSymbol(symbol) { if options.StartTime != nil && fill.Time.Before(*options.StartTime) {
continue continue
} }
tradeIDs[r.TradeId] = struct{}{}
lastTradeID = r.TradeId
t, err := toGlobalTrade(r) 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 { if err != nil {
return nil, err return nil, err
} }
trades = append(trades, t) trades = append(trades, t)
} }
if int64(len(resp.Result)) < limit {
return trades, nil
}
}
return trades, nil return trades, nil
} }
@ -458,27 +443,34 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
logrus.WithError(err).Error("type error") logrus.WithError(err).Error("type error")
} }
or, err := e.newRest().PlaceOrder(ctx, PlaceOrderPayload{ req := e.client.NewPlaceOrderRequest()
Market: toLocalSymbol(TrimUpperString(so.Symbol)), req.Market(toLocalSymbol(TrimUpperString(so.Symbol)))
Side: TrimLowerString(string(so.Side)), req.OrderType(orderType)
Price: so.Price, req.Side(ftxapi.Side(TrimLowerString(string(so.Side))))
Type: string(orderType), req.Size(so.Quantity)
Size: so.Quantity,
ReduceOnly: false,
IOC: so.TimeInForce == types.TimeInForceIOC,
PostOnly: so.Type == types.OrderTypeLimitMaker,
ClientID: newSpotClientOrderID(so.ClientOrderID),
})
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 { if err != nil {
return createdOrders, fmt.Errorf("failed to place order %+v: %w", so, err) return createdOrders, fmt.Errorf("failed to place order %+v: %w", so, err)
} }
if !or.Success { globalOrder, err := toGlobalOrderNew(*or)
return createdOrders, fmt.Errorf("ftx returns placing order failure")
}
globalOrder, err := toGlobalOrder(or.Result)
if err != nil { if err != nil {
return createdOrders, fmt.Errorf("failed to convert response to global order") 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 return createdOrders, nil
} }
func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) {
orderID, err := strconv.ParseInt(q.OrderID, 10, 64)
if err != nil {
return nil, err
}
req := e.client.NewGetOrderStatusRequest(uint64(orderID))
ftxOrder, err := req.Do(ctx)
if err != nil {
return nil, err
}
order, err := toGlobalOrderNew(*ftxOrder)
return &order, err
}
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
// TODO: invoke open trigger orders // TODO: invoke open trigger orders
resp, err := e.newRest().OpenOrders(ctx, toLocalSymbol(symbol))
req := e.client.NewGetOpenOrdersRequest(toLocalSymbol(symbol))
ftxOrders, err := req.Do(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying open orders failure") for _, ftxOrder := range ftxOrders {
} o, err := toGlobalOrderNew(ftxOrder)
for _, r := range resp.Result {
o, err := toGlobalOrder(r)
if err != nil { if err != nil {
return nil, err return orders, err
} }
orders = append(orders, o) orders = append(orders, o)
} }
return orders, nil return orders, nil
@ -510,75 +519,61 @@ 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. // 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. // 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) { 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) symbol = TrimUpperString(symbol)
limit := int64(100)
hasMoreData := true
s := since
var lastOrder order
for hasMoreData {
if err := requestLimit.Wait(ctx); err != nil { req := e.client.NewGetOrderHistoryRequest(toLocalSymbol(symbol))
logrus.WithError(err).Error("rate limit error")
if since != (time.Time{}) {
req.StartTime(since)
} else if until != (time.Time{}) {
req.EndTime(until)
} }
resp, err := e.newRest().OrdersHistory(ctx, toLocalSymbol(symbol), s, until, limit) ftxOrders, err := req.Do(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !resp.Success {
return nil, fmt.Errorf("ftx returns querying orders history failure")
}
sortByCreatedASC(resp.Result) sort.Slice(ftxOrders, func(i, j int) bool {
return ftxOrders[i].CreatedAt.Before(ftxOrders[j].CreatedAt)
})
for _, r := range resp.Result { for _, ftxOrder := range ftxOrders {
// There may be more than one orders at the same time, so also have to check the ID switch ftxOrder.Status {
if r.CreatedAt.Before(lastOrder.CreatedAt.Time) || r.ID == lastOrder.ID || r.Status != "closed" || r.ID < int64(lastOrderID) { case ftxapi.OrderStatusOpen, ftxapi.OrderStatusNew:
continue continue
} }
lastOrder = r
o, err := toGlobalOrder(r) o, err := toGlobalOrderNew(ftxOrder)
if err != nil { if err != nil {
return nil, err return orders, err
} }
orders = append(orders, o) 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)
}
return orders, nil 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 { func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {
for _, o := range orders { for _, o := range orders {
rest := e.newRest()
if err := requestLimit.Wait(ctx); err != nil { if err := requestLimit.Wait(ctx); err != nil {
logrus.WithError(err).Error("rate limit error") logrus.WithError(err).Error("rate limit error")
} }
if len(o.ClientOrderID) > 0 { 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 return err
} }
continue } else {
} req := e.client.NewCancelOrderRequest(strconv.FormatUint(o.OrderID, 10))
if _, err := rest.CancelOrderByOrderID(ctx, o.OrderID); err != nil { _, err := req.Do(ctx)
if err != nil {
return err return err
} }
} }
}
return nil return nil
} }

View File

@ -86,10 +86,11 @@ func TestExchange_QueryAccountBalances(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL ex.client.BaseURL = serverURL
resp, err := ex.QueryAccountBalances(context.Background()) resp, err := ex.QueryAccountBalances(context.Background())
assert.NoError(t, err) assert.NoError(t, err)
@ -99,10 +100,6 @@ func TestExchange_QueryAccountBalances(t *testing.T) {
expectedAvailable := fixedpoint.Must(fixedpoint.NewFromString("19.48085209")) expectedAvailable := fixedpoint.Must(fixedpoint.NewFromString("19.48085209"))
assert.Equal(t, expectedAvailable, b.Available) assert.Equal(t, expectedAvailable, b.Available)
assert.Equal(t, fixedpoint.Must(fixedpoint.NewFromString("1094.66405065")).Sub(expectedAvailable), b.Locked) 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) { func TestExchange_QueryOpenOrders(t *testing.T) {
@ -136,10 +133,12 @@ func TestExchange_QueryOpenOrders(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL ex.client.BaseURL = serverURL
resp, err := ex.QueryOpenOrders(context.Background(), "XRP-PREP") resp, err := ex.QueryOpenOrders(context.Background(), "XRP-PREP")
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, resp, 1) assert.Len(t, resp, 1)
@ -155,10 +154,11 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL ex.client.BaseURL = serverURL
resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100)
assert.NoError(t, err) assert.NoError(t, err)
@ -196,10 +196,11 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL ex.client.BaseURL = serverURL
resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, resp, 1) assert.Len(t, resp, 1)
@ -240,10 +241,11 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL ex.client.BaseURL = serverURL
resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100) resp, err := ex.QueryClosedOrders(context.Background(), "BTC-PERP", time.Now(), time.Now(), 100)
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, resp, 3) assert.Len(t, resp, 3)
@ -253,76 +255,6 @@ func TestExchange_QueryClosedOrders(t *testing.T) {
assert.Equal(t, expectedOrderID[i], o.OrderID) 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) { func TestExchange_QueryAccount(t *testing.T) {
@ -391,10 +323,11 @@ func TestExchange_QueryAccount(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL ex.client.BaseURL = serverURL
resp, err := ex.QueryAccount(context.Background()) resp, err := ex.QueryAccount(context.Background())
assert.NoError(t, err) assert.NoError(t, err)
@ -447,10 +380,12 @@ func TestExchange_QueryMarkets(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.client.BaseURL = serverURL
ex.restEndpoint = serverURL ex.restEndpoint = serverURL
resp, err := ex.QueryMarkets(context.Background()) resp, err := ex.QueryMarkets(context.Background())
assert.NoError(t, err) assert.NoError(t, err)
@ -494,9 +429,10 @@ func TestExchange_QueryDepositHistory(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.client.BaseURL = serverURL
ex.restEndpoint = serverURL ex.restEndpoint = serverURL
ctx := context.Background() ctx := context.Background()
@ -543,10 +479,10 @@ func TestExchange_QueryTrades(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL ex.client.BaseURL = serverURL
ctx := context.Background() ctx := context.Background()
actualConfirmedTime, err := parseDatetime("2021-02-23T09:29:08.534000+00:00") actualConfirmedTime, err := parseDatetime("2021-02-23T09:29:08.534000+00:00")
@ -619,10 +555,10 @@ func TestExchange_QueryTrades(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
ex := NewExchange("", "", "") ex := NewExchange("test-key", "test-secret", "")
serverURL, err := url.Parse(ts.URL) serverURL, err := url.Parse(ts.URL)
assert.NoError(t, err) assert.NoError(t, err)
ex.restEndpoint = serverURL ex.client.BaseURL = serverURL
ctx := context.Background() ctx := context.Background()
actualConfirmedTime, err := parseDatetime("2021-02-23T09:29:08.534000+00:00") 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 { type restRequest struct {
*walletRequest *walletRequest
*orderRequest *orderRequest
*accountRequest
*marketRequest *marketRequest
*fillsRequest
*transferRequest *transferRequest
key, secret string key, secret string
@ -88,9 +86,7 @@ func newRestRequest(c *http.Client, baseURL *url.URL) *restRequest {
p: make(map[string]interface{}), p: make(map[string]interface{}),
} }
r.fillsRequest = &fillsRequest{restRequest: r}
r.marketRequest = &marketRequest{restRequest: r} r.marketRequest = &marketRequest{restRequest: r}
r.accountRequest = &accountRequest{restRequest: r}
r.walletRequest = &walletRequest{restRequest: r} r.walletRequest = &walletRequest{restRequest: r}
r.orderRequest = &orderRequest{restRequest: r} r.orderRequest = &orderRequest{restRequest: r}
return r return r
@ -241,12 +237,12 @@ func (r *restRequest) sendRequest(req *http.Request) (*util.Response, error) {
type ErrorResponse struct { type ErrorResponse struct {
*util.Response *util.Response
IsSuccess bool `json:"Success"` IsSuccess bool `json:"success"`
ErrorString string `json:"error,omitempty"` ErrorString string `json:"error,omitempty"`
} }
func (r *ErrorResponse) Error() string { 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.Method,
r.Response.Request.URL.String(), r.Response.Request.URL.String(),
r.Response.StatusCode, 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 return
} }
globalOrder, err := toGlobalOrder(r.Data) globalOrder, err := toGlobalOrderNew(r.Data)
if err != nil { if err != nil {
logger.WithError(err).Errorf("failed to convert order update to global order") logger.WithError(err).Errorf("failed to convert order update to global order")
return return

View File

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

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -115,7 +116,7 @@ type optionalFields struct {
type orderUpdateResponse struct { type orderUpdateResponse struct {
mandatoryFields mandatoryFields
Data order `json:"data"` Data ftxapi.Order `json:"data"`
} }
func (r websocketResponse) toOrderUpdateResponse() (orderUpdateResponse, error) { func (r websocketResponse) toOrderUpdateResponse() (orderUpdateResponse, error) {
@ -133,7 +134,7 @@ func (r websocketResponse) toOrderUpdateResponse() (orderUpdateResponse, error)
type tradeUpdateResponse struct { type tradeUpdateResponse struct {
mandatoryFields mandatoryFields
Data fill `json:"data"` Data ftxapi.Fill `json:"data"`
} }
func (r websocketResponse) toTradeUpdateResponse() (tradeUpdateResponse, error) { 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) { func (r websocketResponse) toBookTickerResponse() (bookTickerResponse, error) {
if r.Channel != bookTickerChannel { if r.Channel != bookTickerChannel {
return bookTickerResponse{}, fmt.Errorf("type %s, channel %s: %w", r.Type, r.Channel, errUnsupportedConversion) 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{ return types.BookTicker{
// ex. BTC/USDT // ex. BTC/USDT
Symbol: toGlobalSymbol(strings.ToUpper(r.Market)), Symbol: toGlobalSymbol(strings.ToUpper(r.Market)),
//Time: r.Timestamp, // Time: r.Timestamp,
Buy: r.Bid, Buy: r.Bid,
BuySize: r.BidSize, BuySize: r.BidSize,
Sell: r.Ask, Sell: r.Ask,
SellSize: r.AskSize, SellSize: r.AskSize,
//Last: r.Last, // Last: r.Last,
}, nil }, nil
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/exchange/ftx/ftxapi"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -225,8 +226,8 @@ func Test_websocketResponse_toOrderUpdateResponse(t *testing.T) {
Channel: privateOrdersChannel, Channel: privateOrdersChannel,
Type: updateRespType, Type: updateRespType,
}, },
Data: order{ Data: ftxapi.Order{
ID: 12345, Id: 12345,
ClientId: "test-client-id", ClientId: "test-client-id",
Market: "SOL/USD", Market: "SOL/USD",
Type: "limit", Type: "limit",
@ -237,11 +238,10 @@ func Test_websocketResponse_toOrderUpdateResponse(t *testing.T) {
FilledSize: fixedpoint.Zero, FilledSize: fixedpoint.Zero,
RemainingSize: fixedpoint.Zero, RemainingSize: fixedpoint.Zero,
ReduceOnly: false, ReduceOnly: false,
Liquidation: false,
AvgFillPrice: fixedpoint.Zero, AvgFillPrice: fixedpoint.Zero,
PostOnly: false, PostOnly: false,
Ioc: 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: "", Future: "",
}, },
}, r) }, r)