mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-21 22:43:52 +00:00
Merge branch 'main' of github.com:c9s/bbgo into feature/302-record-assets-review
This commit is contained in:
commit
51e23b6a0c
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -22,6 +22,7 @@
|
|||
|
||||
/.env.local
|
||||
/.env.*.local
|
||||
/.env.production
|
||||
|
||||
.DS_Store
|
||||
|
||||
|
|
|
@ -606,6 +606,12 @@ for lorca
|
|||
make embed && go run -tags web ./cmd/bbgo-lorca
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
What's Position?
|
||||
|
||||
- Base Currency & Quote Currency <https://www.ig.com/au/glossary-trading-terms/base-currency-definition>
|
||||
- How to calculate average cost? <https://www.janushenderson.com/en-us/investor/planning/calculate-average-cost/>
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
---
|
||||
notifications:
|
||||
# object routing rules
|
||||
routing:
|
||||
trade: "$symbol"
|
||||
order: "$symbol"
|
||||
submitOrder: "$session" # not supported yet
|
||||
pnL: "bbgo-pnl"
|
||||
|
||||
sessions:
|
||||
binance:
|
||||
exchange: binance
|
||||
|
|
|
@ -26,17 +26,6 @@ TELEGRAM_BOT_AUTH_TOKEN=itsme55667788
|
|||
The alerting strategies use Telegram bot notification without further configuration. You can check the [pricealert
|
||||
yaml file](../../config/pricealert-tg.yaml) in the `config/` directory for example.
|
||||
|
||||
If you want the order submitting/filling notification, add the following to your `bbgo.yaml`:
|
||||
|
||||
```yaml
|
||||
notifications:
|
||||
routing:
|
||||
trade: "$symbol"
|
||||
order: "$symbol"
|
||||
submitOrder: "$session"
|
||||
pnL: "bbgo-pnl"
|
||||
```
|
||||
|
||||
Run your bbgo.
|
||||
|
||||
Open your Telegram app, search your bot `bbgo_bot_711222333`
|
||||
|
|
16
doc/development/kucoin-cli.md
Normal file
16
doc/development/kucoin-cli.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Kucoin command-line tool
|
||||
|
||||
```shell
|
||||
go run ./examples/kucoin accounts
|
||||
go run ./examples/kucoin subaccounts
|
||||
go run ./examples/kucoin symbols
|
||||
go run ./examples/kucoin tickers
|
||||
go run ./examples/kucoin tickers BTC-USDT
|
||||
go run ./examples/kucoin orderbook BTC-USDT 20
|
||||
go run ./examples/kucoin orderbook BTC-USDT 100
|
||||
|
||||
go run ./examples/kucoin orders place --symbol LTC-USDT --price 50 --size 1 --order-type limit --side buy
|
||||
go run ./examples/kucoin orders --symbol LTC-USDT --status active
|
||||
go run ./examples/kucoin orders --symbol LTC-USDT --status done
|
||||
go run ./examples/kucoin orders cancel --order-id 61b48b73b4de3e0001251382
|
||||
```
|
70
examples/kucoin-accounts/main.go
Normal file
70
examples/kucoin-accounts/main.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
|
||||
"github.com/joho/godotenv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().String("kucoin-api-key", "", "okex api key")
|
||||
rootCmd.PersistentFlags().String("kucoin-api-secret", "", "okex api secret")
|
||||
rootCmd.PersistentFlags().String("kucoin-api-passphrase", "", "okex api secret")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "kucoin-accounts",
|
||||
Short: "kucoin accounts",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
accounts, err := client.AccountService.ListAccounts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("accounts: %+v", accounts)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var client *kucoinapi.RestClient = nil
|
||||
|
||||
func main() {
|
||||
if _, err := os.Stat(".env.local"); err == nil {
|
||||
if err := godotenv.Load(".env.local"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
|
||||
log.WithError(err).Error("bind pflags error")
|
||||
}
|
||||
|
||||
client = kucoinapi.NewClient()
|
||||
|
||||
key, secret, passphrase := viper.GetString("kucoin-api-key"),
|
||||
viper.GetString("kucoin-api-secret"),
|
||||
viper.GetString("kucoin-api-passphrase")
|
||||
|
||||
if len(key) == 0 || len(secret) == 0 || len(passphrase) == 0 {
|
||||
log.Fatal("empty key, secret or passphrase")
|
||||
}
|
||||
|
||||
client.Auth(key, secret, passphrase)
|
||||
|
||||
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
|
||||
log.WithError(err).Error("cmd error")
|
||||
}
|
||||
}
|
70
examples/kucoin-subaccount/main.go
Normal file
70
examples/kucoin-subaccount/main.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
|
||||
"github.com/joho/godotenv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().String("kucoin-api-key", "", "okex api key")
|
||||
rootCmd.PersistentFlags().String("kucoin-api-secret", "", "okex api secret")
|
||||
rootCmd.PersistentFlags().String("kucoin-api-passphrase", "", "okex api secret")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "kucoin-subaccount",
|
||||
Short: "kucoin subaccount",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
subAccounts, err := client.AccountService.QuerySubAccounts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("subAccounts: %+v", subAccounts)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var client *kucoinapi.RestClient = nil
|
||||
|
||||
func main() {
|
||||
if _, err := os.Stat(".env.local"); err == nil {
|
||||
if err := godotenv.Load(".env.local"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
|
||||
log.WithError(err).Error("bind pflags error")
|
||||
}
|
||||
|
||||
client = kucoinapi.NewClient()
|
||||
|
||||
key, secret, passphrase := viper.GetString("kucoin-api-key"),
|
||||
viper.GetString("kucoin-api-secret"),
|
||||
viper.GetString("kucoin-api-passphrase")
|
||||
|
||||
if len(key) == 0 || len(secret) == 0 || len(passphrase) == 0 {
|
||||
log.Fatal("empty key, secret or passphrase")
|
||||
}
|
||||
|
||||
client.Auth(key, secret, passphrase)
|
||||
|
||||
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
|
||||
log.WithError(err).Error("cmd error")
|
||||
}
|
||||
}
|
34
examples/kucoin/accounts.go
Normal file
34
examples/kucoin/accounts.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var accountsCmd = &cobra.Command{
|
||||
Use: "accounts",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 0 {
|
||||
account, err := client.AccountService.GetAccount(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("account: %+v", account)
|
||||
return nil
|
||||
}
|
||||
|
||||
accounts, err := client.AccountService.ListAccounts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("accounts: %+v", accounts)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
70
examples/kucoin/main.go
Normal file
70
examples/kucoin/main.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
|
||||
"github.com/joho/godotenv"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().String("kucoin-api-key", "", "okex api key")
|
||||
rootCmd.PersistentFlags().String("kucoin-api-secret", "", "okex api secret")
|
||||
rootCmd.PersistentFlags().String("kucoin-api-passphrase", "", "okex api secret")
|
||||
|
||||
rootCmd.AddCommand(accountsCmd)
|
||||
rootCmd.AddCommand(subAccountsCmd)
|
||||
rootCmd.AddCommand(symbolsCmd)
|
||||
rootCmd.AddCommand(tickersCmd)
|
||||
rootCmd.AddCommand(orderbookCmd)
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "kucoin",
|
||||
Short: "kucoin",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var client *kucoinapi.RestClient = nil
|
||||
|
||||
func main() {
|
||||
if _, err := os.Stat(".env.local"); err == nil {
|
||||
if err := godotenv.Load(".env.local"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
|
||||
|
||||
if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil {
|
||||
log.WithError(err).Error("bind pflags error")
|
||||
}
|
||||
|
||||
client = kucoinapi.NewClient()
|
||||
|
||||
key, secret, passphrase := viper.GetString("kucoin-api-key"),
|
||||
viper.GetString("kucoin-api-secret"),
|
||||
viper.GetString("kucoin-api-passphrase")
|
||||
|
||||
if len(key) == 0 || len(secret) == 0 || len(passphrase) == 0 {
|
||||
log.Fatal("empty key, secret or passphrase")
|
||||
}
|
||||
|
||||
client.Auth(key, secret, passphrase)
|
||||
|
||||
if err := rootCmd.ExecuteContext(context.Background()); err != nil {
|
||||
log.WithError(err).Error("cmd error")
|
||||
}
|
||||
}
|
40
examples/kucoin/orderbook.go
Normal file
40
examples/kucoin/orderbook.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var orderbookCmd = &cobra.Command{
|
||||
Use: "orderbook",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var depth = 0
|
||||
if len(args) > 1 {
|
||||
v, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
depth = v
|
||||
}
|
||||
|
||||
orderBook, err := client.MarketDataService.GetOrderBook(args[0], depth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("orderBook: %+v", orderBook)
|
||||
return nil
|
||||
},
|
||||
}
|
176
examples/kucoin/orders.go
Normal file
176
examples/kucoin/orders.go
Normal file
|
@ -0,0 +1,176 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/kucoin/kucoinapi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ordersCmd.Flags().String("symbol", "", "symbol, BTC-USDT, LTC-USDT...etc")
|
||||
ordersCmd.Flags().String("status", "", "status, active or done")
|
||||
rootCmd.AddCommand(ordersCmd)
|
||||
|
||||
cancelOrderCmd.Flags().String("client-order-id", "", "client order id")
|
||||
cancelOrderCmd.Flags().String("order-id", "", "order id")
|
||||
ordersCmd.AddCommand(cancelOrderCmd)
|
||||
|
||||
placeOrderCmd.Flags().String("symbol", "", "symbol")
|
||||
placeOrderCmd.Flags().String("price", "", "price")
|
||||
placeOrderCmd.Flags().String("size", "", "size")
|
||||
placeOrderCmd.Flags().String("order-type", string(kucoinapi.OrderTypeLimit), "order type")
|
||||
placeOrderCmd.Flags().String("side", "", "buy or sell")
|
||||
ordersCmd.AddCommand(placeOrderCmd)
|
||||
}
|
||||
|
||||
|
||||
// go run ./examples/kucoin orders
|
||||
var ordersCmd = &cobra.Command{
|
||||
Use: "orders",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
req := client.TradeService.NewListOrdersRequest()
|
||||
|
||||
symbol, err := cmd.Flags().GetString("symbol")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(symbol) == 0 {
|
||||
return errors.New("--symbol option is required")
|
||||
}
|
||||
|
||||
req.Symbol(symbol)
|
||||
|
||||
|
||||
status, err := cmd.Flags().GetString("status")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(status) > 0 {
|
||||
req.Status(status)
|
||||
}
|
||||
|
||||
page, err := req.Do(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("page: %+v", page)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// usage:
|
||||
// go run ./examples/kucoin orders place --symbol LTC-USDT --price 50 --size 1 --order-type limit --side buy
|
||||
var placeOrderCmd = &cobra.Command{
|
||||
Use: "place",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
req := client.TradeService.NewPlaceOrderRequest()
|
||||
|
||||
orderType, err := cmd.Flags().GetString("order-type")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.OrderType(kucoinapi.OrderType(orderType))
|
||||
|
||||
|
||||
side, err := cmd.Flags().GetString("side")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Side(kucoinapi.SideType(side))
|
||||
|
||||
|
||||
symbol, err := cmd.Flags().GetString("symbol")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(symbol) == 0 {
|
||||
return errors.New("--symbol is required")
|
||||
}
|
||||
|
||||
req.Symbol(symbol)
|
||||
|
||||
switch kucoinapi.OrderType(orderType) {
|
||||
case kucoinapi.OrderTypeLimit:
|
||||
price, err := cmd.Flags().GetString("price")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Price(price)
|
||||
|
||||
case kucoinapi.OrderTypeMarket:
|
||||
|
||||
}
|
||||
|
||||
|
||||
size, err := cmd.Flags().GetString("size")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Size(size)
|
||||
|
||||
response, err := req.Do(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("place order response: %+v", response)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
// usage:
|
||||
var cancelOrderCmd = &cobra.Command{
|
||||
Use: "cancel",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
req := client.TradeService.NewCancelOrderRequest()
|
||||
|
||||
orderID, err := cmd.Flags().GetString("order-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientOrderID, err := cmd.Flags().GetString("client-order-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(orderID) > 0 {
|
||||
req.OrderID(orderID)
|
||||
} else if len(clientOrderID) > 0 {
|
||||
req.ClientOrderID(clientOrderID)
|
||||
} else {
|
||||
return errors.New("either order id or client order id is required")
|
||||
}
|
||||
|
||||
response, err := req.Do(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("cancel order response: %+v", response)
|
||||
return nil
|
||||
},
|
||||
}
|
24
examples/kucoin/subaccounts.go
Normal file
24
examples/kucoin/subaccounts.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var subAccountsCmd = &cobra.Command{
|
||||
Use: "subaccounts",
|
||||
Short: "subaccounts",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
subAccounts, err := client.AccountService.QuerySubAccounts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("subAccounts: %+v", subAccounts)
|
||||
return nil
|
||||
},
|
||||
}
|
24
examples/kucoin/symbols.go
Normal file
24
examples/kucoin/symbols.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var symbolsCmd = &cobra.Command{
|
||||
Use: "symbols",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
symbols, err := client.MarketDataService.ListSymbols(args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("symbols: %+v", symbols)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
35
examples/kucoin/tickers.go
Normal file
35
examples/kucoin/tickers.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var tickersCmd = &cobra.Command{
|
||||
Use: "tickers",
|
||||
|
||||
// SilenceUsage is an option to silence usage when an error occurs.
|
||||
SilenceUsage: true,
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
allTickers, err := client.MarketDataService.ListTickers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("allTickers: %+v", allTickers)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
ticker, err := client.MarketDataService.GetTicker(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("ticker: %+v", ticker)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package pnl
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
@ -33,8 +32,8 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
|
|||
|
||||
var currencyFees = map[string]float64{}
|
||||
|
||||
var position = bbgo.NewPositionFromMarket(c.Market)
|
||||
position.SetFeeRate(bbgo.ExchangeFee{
|
||||
var position = types.NewPositionFromMarket(c.Market)
|
||||
position.SetFeeRate(types.ExchangeFee{
|
||||
// binance vip 0 uses 0.075%
|
||||
MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
|
||||
TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
|
||||
|
|
|
@ -161,6 +161,10 @@ type ExchangeSession struct {
|
|||
IsolatedMargin bool `json:"isolatedMargin,omitempty" yaml:"isolatedMargin,omitempty"`
|
||||
IsolatedMarginSymbol string `json:"isolatedMarginSymbol,omitempty" yaml:"isolatedMarginSymbol,omitempty"`
|
||||
|
||||
Futures bool `json:"futures,omitempty" yaml:"futures"`
|
||||
IsolatedFutures bool `json:"isolatedFutures,omitempty" yaml:"isolatedFutures,omitempty"`
|
||||
IsolatedFuturesSymbol string `json:"isolatedFuturesSymbol,omitempty" yaml:"isolatedFuturesSymbol,omitempty"`
|
||||
|
||||
// ---------------------------
|
||||
// Runtime fields
|
||||
// ---------------------------
|
||||
|
@ -199,7 +203,7 @@ type ExchangeSession struct {
|
|||
// marketDataStores contains the market data store of each market
|
||||
marketDataStores map[string]*MarketDataStore
|
||||
|
||||
positions map[string]*Position
|
||||
positions map[string]*types.Position
|
||||
|
||||
// standard indicators of each market
|
||||
standardIndicatorSets map[string]*StandardIndicatorSet
|
||||
|
@ -236,7 +240,7 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession {
|
|||
markets: make(map[string]types.Market),
|
||||
startPrices: make(map[string]float64),
|
||||
lastPrices: make(map[string]float64),
|
||||
positions: make(map[string]*Position),
|
||||
positions: make(map[string]*types.Position),
|
||||
marketDataStores: make(map[string]*MarketDataStore),
|
||||
standardIndicatorSets: make(map[string]*StandardIndicatorSet),
|
||||
orderStores: make(map[string]*OrderStore),
|
||||
|
@ -388,7 +392,7 @@ func (session *ExchangeSession) initSymbol(ctx context.Context, environ *Environ
|
|||
session.Trades[symbol].Append(trade)
|
||||
})
|
||||
|
||||
position := &Position{
|
||||
position := &types.Position{
|
||||
Symbol: symbol,
|
||||
BaseCurrency: market.BaseCurrency,
|
||||
QuoteCurrency: market.QuoteCurrency,
|
||||
|
@ -475,7 +479,7 @@ func (session *ExchangeSession) StandardIndicatorSet(symbol string) (*StandardIn
|
|||
return set, ok
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) Position(symbol string) (pos *Position, ok bool) {
|
||||
func (session *ExchangeSession) Position(symbol string) (pos *types.Position, ok bool) {
|
||||
pos, ok = session.positions[symbol]
|
||||
if ok {
|
||||
return pos, ok
|
||||
|
@ -486,7 +490,7 @@ func (session *ExchangeSession) Position(symbol string) (pos *Position, ok bool)
|
|||
return nil, false
|
||||
}
|
||||
|
||||
pos = &Position{
|
||||
pos = &types.Position{
|
||||
Symbol: symbol,
|
||||
BaseCurrency: market.BaseCurrency,
|
||||
QuoteCurrency: market.QuoteCurrency,
|
||||
|
@ -496,7 +500,7 @@ func (session *ExchangeSession) Position(symbol string) (pos *Position, ok bool)
|
|||
return pos, ok
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) Positions() map[string]*Position {
|
||||
func (session *ExchangeSession) Positions() map[string]*types.Position {
|
||||
return session.positions
|
||||
}
|
||||
|
||||
|
@ -692,6 +696,19 @@ func InitExchangeSession(name string, session *ExchangeSession) error {
|
|||
}
|
||||
}
|
||||
|
||||
if session.Futures {
|
||||
futuresExchange, ok := exchange.(types.FuturesExchange)
|
||||
if !ok {
|
||||
return fmt.Errorf("exchange %s does not support futures", exchangeName)
|
||||
}
|
||||
|
||||
if session.IsolatedFutures {
|
||||
futuresExchange.UseIsolatedFutures(session.IsolatedFuturesSymbol)
|
||||
} else {
|
||||
futuresExchange.UseFutures()
|
||||
}
|
||||
}
|
||||
|
||||
session.Name = name
|
||||
session.Notifiability = Notifiability{
|
||||
SymbolChannelRouter: NewPatternChannelRouter(nil),
|
||||
|
@ -713,7 +730,7 @@ func InitExchangeSession(name string, session *ExchangeSession) error {
|
|||
session.lastPrices = make(map[string]float64)
|
||||
session.startPrices = make(map[string]float64)
|
||||
session.marketDataStores = make(map[string]*MarketDataStore)
|
||||
session.positions = make(map[string]*Position)
|
||||
session.positions = make(map[string]*types.Position)
|
||||
session.standardIndicatorSets = make(map[string]*StandardIndicatorSet)
|
||||
session.orderStores = make(map[string]*OrderStore)
|
||||
session.OrderExecutor = &ExchangeOrderExecutor{
|
||||
|
|
|
@ -16,15 +16,15 @@ type TradeCollector struct {
|
|||
|
||||
tradeStore *TradeStore
|
||||
tradeC chan types.Trade
|
||||
position *Position
|
||||
position *types.Position
|
||||
orderStore *OrderStore
|
||||
|
||||
tradeCallbacks []func(trade types.Trade)
|
||||
positionUpdateCallbacks []func(position *Position)
|
||||
positionUpdateCallbacks []func(position *types.Position)
|
||||
profitCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value)
|
||||
}
|
||||
|
||||
func NewTradeCollector(symbol string, position *Position, orderStore *OrderStore) *TradeCollector {
|
||||
func NewTradeCollector(symbol string, position *types.Position, orderStore *OrderStore) *TradeCollector {
|
||||
return &TradeCollector{
|
||||
Symbol: symbol,
|
||||
orderSig: sigchan.New(1),
|
||||
|
|
|
@ -17,11 +17,11 @@ func (c *TradeCollector) EmitTrade(trade types.Trade) {
|
|||
}
|
||||
}
|
||||
|
||||
func (c *TradeCollector) OnPositionUpdate(cb func(position *Position)) {
|
||||
func (c *TradeCollector) OnPositionUpdate(cb func(position *types.Position)) {
|
||||
c.positionUpdateCallbacks = append(c.positionUpdateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (c *TradeCollector) EmitPositionUpdate(position *Position) {
|
||||
func (c *TradeCollector) EmitPositionUpdate(position *types.Position) {
|
||||
for _, cb := range c.positionUpdateCallbacks {
|
||||
cb(position)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ type TwapExecution struct {
|
|||
|
||||
activeMakerOrders *LocalActiveOrderBook
|
||||
orderStore *OrderStore
|
||||
position *Position
|
||||
position *types.Position
|
||||
|
||||
executionCtx context.Context
|
||||
cancelExecution context.CancelFunc
|
||||
|
@ -444,7 +444,7 @@ func (e *TwapExecution) Run(parentCtx context.Context) error {
|
|||
|
||||
e.userDataStream = e.Session.Exchange.NewStream()
|
||||
e.userDataStream.OnTradeUpdate(e.handleTradeUpdate)
|
||||
e.position = &Position{
|
||||
e.position = &types.Position{
|
||||
Symbol: e.Symbol,
|
||||
BaseCurrency: e.market.BaseCurrency,
|
||||
QuoteCurrency: e.market.QuoteCurrency,
|
||||
|
|
|
@ -50,7 +50,6 @@ func toGlobalMarket(symbol binance.Symbol) types.Market {
|
|||
return market
|
||||
}
|
||||
|
||||
|
||||
func toGlobalIsolatedUserAsset(userAsset binance.IsolatedUserAsset) types.IsolatedUserAsset {
|
||||
return types.IsolatedUserAsset{
|
||||
Asset: userAsset.Asset,
|
||||
|
@ -137,10 +136,9 @@ func toGlobalTicker(stats *binance.PriceChangeStats) (*types.Ticker, error) {
|
|||
Buy: util.MustParseFloat(stats.BidPrice),
|
||||
Sell: util.MustParseFloat(stats.AskPrice),
|
||||
Time: time.Unix(0, stats.CloseTime*int64(time.Millisecond)),
|
||||
},nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) {
|
||||
switch orderType {
|
||||
|
||||
|
@ -163,6 +161,28 @@ func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) {
|
|||
return "", fmt.Errorf("can not convert to local order, order type %s not supported", orderType)
|
||||
}
|
||||
|
||||
func toLocalFuturesOrderType(orderType types.OrderType) (futures.OrderType, error) {
|
||||
switch orderType {
|
||||
|
||||
// case types.OrderTypeLimitMaker:
|
||||
// return futures.OrderTypeLimitMaker, nil //TODO
|
||||
|
||||
case types.OrderTypeLimit:
|
||||
return futures.OrderTypeLimit, nil
|
||||
|
||||
// case types.OrderTypeStopLimit:
|
||||
// return futures.OrderTypeStopLossLimit, nil //TODO
|
||||
|
||||
// case types.OrderTypeStopMarket:
|
||||
// return futures.OrderTypeStopLoss, nil //TODO
|
||||
|
||||
case types.OrderTypeMarket:
|
||||
return futures.OrderTypeMarket, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("can not convert to local order, order type %s not supported", orderType)
|
||||
}
|
||||
|
||||
func toGlobalOrders(binanceOrders []*binance.Order) (orders []types.Order, err error) {
|
||||
for _, binanceOrder := range binanceOrders {
|
||||
order, err := toGlobalOrder(binanceOrder, false)
|
||||
|
@ -176,6 +196,19 @@ func toGlobalOrders(binanceOrders []*binance.Order) (orders []types.Order, err e
|
|||
return orders, err
|
||||
}
|
||||
|
||||
func toGlobalFuturesOrders(futuresOrders []*futures.Order) (orders []types.Order, err error) {
|
||||
for _, futuresOrder := range futuresOrders {
|
||||
order, err := toGlobalFuturesOrder(futuresOrder, false)
|
||||
if err != nil {
|
||||
return orders, err
|
||||
}
|
||||
|
||||
orders = append(orders, *order)
|
||||
}
|
||||
|
||||
return orders, err
|
||||
}
|
||||
|
||||
func toGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, error) {
|
||||
return &types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
|
@ -199,11 +232,36 @@ func toGlobalOrder(binanceOrder *binance.Order, isMargin bool) (*types.Order, er
|
|||
}, nil
|
||||
}
|
||||
|
||||
func toGlobalFuturesOrder(futuresOrder *futures.Order, isMargin bool) (*types.Order, error) {
|
||||
return &types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
ClientOrderID: futuresOrder.ClientOrderID,
|
||||
Symbol: futuresOrder.Symbol,
|
||||
Side: toGlobalFuturesSideType(futuresOrder.Side),
|
||||
Type: toGlobalFuturesOrderType(futuresOrder.Type),
|
||||
ReduceOnly: futuresOrder.ReduceOnly,
|
||||
ClosePosition: futuresOrder.ClosePosition,
|
||||
Quantity: util.MustParseFloat(futuresOrder.OrigQuantity),
|
||||
Price: util.MustParseFloat(futuresOrder.Price),
|
||||
TimeInForce: string(futuresOrder.TimeInForce),
|
||||
},
|
||||
Exchange: types.ExchangeBinance,
|
||||
// IsWorking: futuresOrder.IsWorking,
|
||||
OrderID: uint64(futuresOrder.OrderID),
|
||||
Status: toGlobalFuturesOrderStatus(futuresOrder.Status),
|
||||
ExecutedQuantity: util.MustParseFloat(futuresOrder.ExecutedQuantity),
|
||||
CreationTime: types.Time(millisecondTime(futuresOrder.Time)),
|
||||
UpdateTime: types.Time(millisecondTime(futuresOrder.UpdateTime)),
|
||||
IsMargin: isMargin,
|
||||
// IsIsolated: futuresOrder.IsIsolated,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func millisecondTime(t int64) time.Time {
|
||||
return time.Unix(0, t*int64(time.Millisecond))
|
||||
}
|
||||
|
||||
func ToGlobalTrade(t binance.TradeV3, isMargin bool) (*types.Trade, error) {
|
||||
func toGlobalTrade(t binance.TradeV3, isMargin bool) (*types.Trade, error) {
|
||||
// skip trade ID that is the same. however this should not happen
|
||||
var side types.SideType
|
||||
if t.IsBuyer {
|
||||
|
@ -270,6 +328,20 @@ func toGlobalSideType(side binance.SideType) types.SideType {
|
|||
}
|
||||
}
|
||||
|
||||
func toGlobalFuturesSideType(side futures.SideType) types.SideType {
|
||||
switch side {
|
||||
case futures.SideTypeBuy:
|
||||
return types.SideTypeBuy
|
||||
|
||||
case futures.SideTypeSell:
|
||||
return types.SideTypeSell
|
||||
|
||||
default:
|
||||
log.Errorf("can not convert futures side type, unknown side type: %q", side)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func toGlobalOrderType(orderType binance.OrderType) types.OrderType {
|
||||
switch orderType {
|
||||
|
||||
|
@ -292,6 +364,27 @@ func toGlobalOrderType(orderType binance.OrderType) types.OrderType {
|
|||
}
|
||||
}
|
||||
|
||||
func toGlobalFuturesOrderType(orderType futures.OrderType) types.OrderType {
|
||||
switch orderType {
|
||||
// TODO
|
||||
case futures.OrderTypeLimit: // , futures.OrderTypeLimitMaker, futures.OrderTypeTakeProfitLimit:
|
||||
return types.OrderTypeLimit
|
||||
|
||||
case futures.OrderTypeMarket:
|
||||
return types.OrderTypeMarket
|
||||
// TODO
|
||||
// case futures.OrderTypeStopLossLimit:
|
||||
// return types.OrderTypeStopLimit
|
||||
// TODO
|
||||
// case futures.OrderTypeStopLoss:
|
||||
// return types.OrderTypeStopMarket
|
||||
|
||||
default:
|
||||
log.Errorf("unsupported order type: %v", orderType)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func toGlobalOrderStatus(orderStatus binance.OrderStatusType) types.OrderStatus {
|
||||
switch orderStatus {
|
||||
case binance.OrderStatusTypeNew:
|
||||
|
@ -313,10 +406,31 @@ func toGlobalOrderStatus(orderStatus binance.OrderStatusType) types.OrderStatus
|
|||
return types.OrderStatus(orderStatus)
|
||||
}
|
||||
|
||||
func toGlobalFuturesOrderStatus(orderStatus futures.OrderStatusType) types.OrderStatus {
|
||||
switch orderStatus {
|
||||
case futures.OrderStatusTypeNew:
|
||||
return types.OrderStatusNew
|
||||
|
||||
case futures.OrderStatusTypeRejected:
|
||||
return types.OrderStatusRejected
|
||||
|
||||
case futures.OrderStatusTypeCanceled:
|
||||
return types.OrderStatusCanceled
|
||||
|
||||
case futures.OrderStatusTypePartiallyFilled:
|
||||
return types.OrderStatusPartiallyFilled
|
||||
|
||||
case futures.OrderStatusTypeFilled:
|
||||
return types.OrderStatusFilled
|
||||
}
|
||||
|
||||
return types.OrderStatus(orderStatus)
|
||||
}
|
||||
|
||||
// ConvertTrades converts the binance v3 trade into the global trade type
|
||||
func ConvertTrades(remoteTrades []*binance.TradeV3) (trades []types.Trade, err error) {
|
||||
for _, t := range remoteTrades {
|
||||
trade, err := ToGlobalTrade(*t, false)
|
||||
trade, err := toGlobalTrade(*t, false)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "binance v3 trade parse error, trade: %+v", *t)
|
||||
}
|
||||
|
@ -364,4 +478,3 @@ func convertPremiumIndex(index *futures.PremiumIndex) (*types.PremiumIndex, erro
|
|||
Time: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package binance
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/adshao/go-binance/v2/futures"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
@ -14,7 +15,6 @@ import (
|
|||
"github.com/adshao/go-binance/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
|
@ -27,7 +27,6 @@ const BNB = "BNB"
|
|||
// 50 per 10 seconds = 5 per second
|
||||
var orderLimiter = rate.NewLimiter(5, 5)
|
||||
|
||||
|
||||
var log = logrus.WithFields(logrus.Fields{
|
||||
"exchange": "binance",
|
||||
})
|
||||
|
@ -35,6 +34,7 @@ var log = logrus.WithFields(logrus.Fields{
|
|||
func init() {
|
||||
_ = types.Exchange(&Exchange{})
|
||||
_ = types.MarginExchange(&Exchange{})
|
||||
_ = types.FuturesExchange(&Exchange{})
|
||||
|
||||
// FIXME: this is not effected since dotenv is loaded in the rootCmd, not in the init function
|
||||
if ok, _ := strconv.ParseBool(os.Getenv("DEBUG_BINANCE_STREAM")); ok {
|
||||
|
@ -46,20 +46,38 @@ type Exchange struct {
|
|||
types.MarginSettings
|
||||
types.FuturesSettings
|
||||
|
||||
key, secret string
|
||||
Client *binance.Client
|
||||
key, secret string
|
||||
Client *binance.Client // Spot & Margin
|
||||
futuresClient *futures.Client // USDT-M Futures
|
||||
// deliveryClient *delivery.Client // Coin-M Futures
|
||||
}
|
||||
|
||||
func New(key, secret string) *Exchange {
|
||||
var client = binance.NewClient(key, secret)
|
||||
client.HTTPClient = &http.Client{Timeout: 15 * time.Second}
|
||||
|
||||
_, _ = client.NewSetServerTimeService().Do(context.Background())
|
||||
return &Exchange{
|
||||
key: key,
|
||||
secret: secret,
|
||||
|
||||
Client: client,
|
||||
var futuresClient = binance.NewFuturesClient(key, secret)
|
||||
futuresClient.HTTPClient = &http.Client{Timeout: 15 * time.Second}
|
||||
_, _ = futuresClient.NewSetServerTimeService().Do(context.Background())
|
||||
|
||||
var err error
|
||||
_, err = client.NewSetServerTimeService().Do(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = futuresClient.NewSetServerTimeService().Do(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &Exchange{
|
||||
key: key,
|
||||
secret: secret,
|
||||
Client: client,
|
||||
futuresClient: futuresClient,
|
||||
// deliveryClient: deliveryClient,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,8 +170,9 @@ func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (float6
|
|||
}
|
||||
|
||||
func (e *Exchange) NewStream() types.Stream {
|
||||
stream := NewStream(e.Client)
|
||||
stream := NewStream(e.Client, e.futuresClient)
|
||||
stream.MarginSettings = e.MarginSettings
|
||||
stream.FuturesSettings = e.FuturesSettings
|
||||
return stream
|
||||
}
|
||||
|
||||
|
@ -180,7 +199,6 @@ func (e *Exchange) QueryIsolatedMarginAccount(ctx context.Context, symbols ...st
|
|||
return toGlobalIsolatedMarginAccount(account), nil
|
||||
}
|
||||
|
||||
|
||||
func (e *Exchange) Withdrawal(ctx context.Context, asset string, amount fixedpoint.Value, address string, options *types.WithdrawalOptions) error {
|
||||
req := e.Client.NewCreateWithdrawService()
|
||||
req.Coin(asset)
|
||||
|
@ -582,6 +600,95 @@ func (e *Exchange) submitMarginOrder(ctx context.Context, order types.SubmitOrde
|
|||
return createdOrder, err
|
||||
}
|
||||
|
||||
func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) {
|
||||
orderType, err := toLocalFuturesOrderType(order.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := e.futuresClient.NewCreateOrderService().
|
||||
Symbol(order.Symbol).
|
||||
Type(orderType).
|
||||
Side(futures.SideType(order.Side))
|
||||
|
||||
clientOrderID := newSpotClientOrderID(order.ClientOrderID)
|
||||
if len(clientOrderID) > 0 {
|
||||
req.NewClientOrderID(clientOrderID)
|
||||
}
|
||||
|
||||
// use response result format
|
||||
req.NewOrderResponseType(futures.NewOrderRespTypeRESULT)
|
||||
// if e.IsIsolatedFutures {
|
||||
// req.IsIsolated(e.IsIsolatedFutures)
|
||||
// }
|
||||
|
||||
if len(order.QuantityString) > 0 {
|
||||
req.Quantity(order.QuantityString)
|
||||
} else if order.Market.Symbol != "" {
|
||||
req.Quantity(order.Market.FormatQuantity(order.Quantity))
|
||||
} else {
|
||||
req.Quantity(strconv.FormatFloat(order.Quantity, 'f', 8, 64))
|
||||
}
|
||||
|
||||
// set price field for limit orders
|
||||
switch order.Type {
|
||||
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker:
|
||||
if len(order.PriceString) > 0 {
|
||||
req.Price(order.PriceString)
|
||||
} else if order.Market.Symbol != "" {
|
||||
req.Price(order.Market.FormatPrice(order.Price))
|
||||
}
|
||||
}
|
||||
|
||||
// set stop price
|
||||
switch order.Type {
|
||||
|
||||
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
|
||||
if len(order.StopPriceString) == 0 {
|
||||
return nil, fmt.Errorf("stop price string can not be empty")
|
||||
}
|
||||
|
||||
req.StopPrice(order.StopPriceString)
|
||||
}
|
||||
|
||||
// could be IOC or FOK
|
||||
if len(order.TimeInForce) > 0 {
|
||||
// TODO: check the TimeInForce value
|
||||
req.TimeInForce(futures.TimeInForceType(order.TimeInForce))
|
||||
} else {
|
||||
switch order.Type {
|
||||
case types.OrderTypeLimit, types.OrderTypeStopLimit:
|
||||
req.TimeInForce(futures.TimeInForceTypeGTC)
|
||||
}
|
||||
}
|
||||
|
||||
response, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("futures order creation response: %+v", response)
|
||||
|
||||
createdOrder, err := toGlobalFuturesOrder(&futures.Order{
|
||||
Symbol: response.Symbol,
|
||||
OrderID: response.OrderID,
|
||||
ClientOrderID: response.ClientOrderID,
|
||||
Price: response.Price,
|
||||
OrigQuantity: response.OrigQuantity,
|
||||
ExecutedQuantity: response.ExecutedQuantity,
|
||||
// CummulativeQuoteQuantity: response.CummulativeQuoteQuantity,
|
||||
Status: response.Status,
|
||||
TimeInForce: response.TimeInForce,
|
||||
Type: response.Type,
|
||||
Side: response.Side,
|
||||
// UpdateTime: response.TransactTime,
|
||||
// Time: response.TransactTime,
|
||||
// IsIsolated: response.IsIsolated,
|
||||
}, true)
|
||||
|
||||
return createdOrder, err
|
||||
}
|
||||
|
||||
// BBGO is a broker on Binance
|
||||
const spotBrokerID = "NSUYEBKM"
|
||||
|
||||
|
@ -700,13 +807,15 @@ func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder)
|
|||
|
||||
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) {
|
||||
for _, order := range orders {
|
||||
if err := orderLimiter.Wait(ctx) ; err != nil {
|
||||
if err := orderLimiter.Wait(ctx); err != nil {
|
||||
log.WithError(err).Errorf("order rate limiter wait error")
|
||||
}
|
||||
|
||||
var createdOrder *types.Order
|
||||
if e.IsMargin {
|
||||
createdOrder, err = e.submitMarginOrder(ctx, order)
|
||||
} else if e.IsFutures {
|
||||
createdOrder, err = e.submitFuturesOrder(ctx, order)
|
||||
} else {
|
||||
createdOrder, err = e.submitSpotOrder(ctx, order)
|
||||
}
|
||||
|
@ -847,7 +956,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
|||
}
|
||||
|
||||
for _, t := range remoteTrades {
|
||||
localTrade, err := ToGlobalTrade(*t, e.IsMargin)
|
||||
localTrade, err := toGlobalTrade(*t, e.IsMargin)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("can not convert binance trade: %+v", t)
|
||||
continue
|
||||
|
@ -932,4 +1041,4 @@ func getLaunchDate() (time.Time, error) {
|
|||
}
|
||||
|
||||
return time.Date(2017, time.July, 14, 0, 0, 0, 0, loc), nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/adshao/go-binance/v2/futures"
|
||||
"time"
|
||||
|
||||
"github.com/adshao/go-binance/v2"
|
||||
|
@ -292,15 +293,23 @@ func ParseEvent(message string) (interface{}, error) {
|
|||
|
||||
case "depthUpdate":
|
||||
return parseDepthEvent(val)
|
||||
|
||||
|
||||
case "markPriceUpdate":
|
||||
var event MarkPriceUpdateEvent
|
||||
err := json.Unmarshal([]byte(message), &event)
|
||||
return &event, err
|
||||
case "continuousKline":
|
||||
var event ContinuousKLineEvent
|
||||
err := json.Unmarshal([]byte(message), &event)
|
||||
return &event, err
|
||||
|
||||
// Binance futures data --------------
|
||||
case "continuousKline":
|
||||
var event ContinuousKLineEvent
|
||||
err := json.Unmarshal([]byte(message), &event)
|
||||
return &event, err
|
||||
|
||||
case "ORDER_TRADE_UPDATE":
|
||||
var event OrderTradeUpdateEvent
|
||||
err := json.Unmarshal([]byte(message), &event)
|
||||
return &event, err
|
||||
|
||||
default:
|
||||
id := val.GetInt("id")
|
||||
if id > 0 {
|
||||
|
@ -470,6 +479,37 @@ type KLine struct {
|
|||
Closed bool `json:"x"`
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
kline
|
||||
|
||||
{
|
||||
"e": "kline", // KLineEvent type
|
||||
"E": 123456789, // KLineEvent time
|
||||
"s": "BNBBTC", // Symbol
|
||||
"k": {
|
||||
"t": 123400000, // Kline start time
|
||||
"T": 123460000, // Kline close time
|
||||
"s": "BNBBTC", // Symbol
|
||||
"i": "1m", // Interval
|
||||
"f": 100, // First trade ID
|
||||
"L": 200, // Last trade ID
|
||||
"o": "0.0010", // Open price
|
||||
"c": "0.0020", // Close price
|
||||
"h": "0.0025", // High price
|
||||
"l": "0.0015", // Low price
|
||||
"v": "1000", // Base asset volume
|
||||
"n": 100, // Number of trades
|
||||
"x": false, // Is this kline closed?
|
||||
"q": "1.0000", // Quote asset volume
|
||||
"V": "500", // Taker buy base asset volume
|
||||
"Q": "0.500", // Taker buy quote asset volume
|
||||
"B": "123456" // Ignore
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
type KLineEvent struct {
|
||||
EventBase
|
||||
Symbol string `json:"s"`
|
||||
|
@ -497,18 +537,17 @@ func (k *KLine) KLine() types.KLine {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
type MarkPriceUpdateEvent struct {
|
||||
EventBase
|
||||
|
||||
Symbol string `json:"s"`
|
||||
Symbol string `json:"s"`
|
||||
|
||||
MarkPrice fixedpoint.Value `json:"p"`
|
||||
IndexPrice fixedpoint.Value `json:"i"`
|
||||
EstimatedPrice fixedpoint.Value `json:"P"`
|
||||
|
||||
FundingRate fixedpoint.Value `json:"r"`
|
||||
NextFundingTime int64 `json:"T"`
|
||||
MarkPrice fixedpoint.Value `json:"p"`
|
||||
IndexPrice fixedpoint.Value `json:"i"`
|
||||
EstimatedPrice fixedpoint.Value `json:"P"`
|
||||
|
||||
FundingRate fixedpoint.Value `json:"r"`
|
||||
NextFundingTime int64 `json:"T"`
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -558,36 +597,123 @@ type ContinuousKLineEvent struct {
|
|||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
// Similar to the ExecutionReportEvent's fields. But with totally different json key.
|
||||
// e.g., Stop price. So that, we can not merge them.
|
||||
type OrderTrade struct {
|
||||
Symbol string `json:"s"`
|
||||
ClientOrderID string `json:"c"`
|
||||
Side string `json:"S"`
|
||||
OrderType string `json:"o"`
|
||||
TimeInForce string `json:"f"`
|
||||
OriginalQuantity string `json:"q"`
|
||||
OriginalPrice string `json:"p"`
|
||||
|
||||
kline
|
||||
AveragePrice string `json:"ap"`
|
||||
StopPrice string `json:"sp"`
|
||||
CurrentExecutionType string `json:"x"`
|
||||
CurrentOrderStatus string `json:"X"`
|
||||
|
||||
{
|
||||
"e": "kline", // KLineEvent type
|
||||
"E": 123456789, // KLineEvent time
|
||||
"s": "BNBBTC", // Symbol
|
||||
"k": {
|
||||
"t": 123400000, // Kline start time
|
||||
"T": 123460000, // Kline close time
|
||||
"s": "BNBBTC", // Symbol
|
||||
"i": "1m", // Interval
|
||||
"f": 100, // First trade ID
|
||||
"L": 200, // Last trade ID
|
||||
"o": "0.0010", // Open price
|
||||
"c": "0.0020", // Close price
|
||||
"h": "0.0025", // High price
|
||||
"l": "0.0015", // Low price
|
||||
"v": "1000", // Base asset volume
|
||||
"n": 100, // Number of trades
|
||||
"x": false, // Is this kline closed?
|
||||
"q": "1.0000", // Quote asset volume
|
||||
"V": "500", // Taker buy base asset volume
|
||||
"Q": "0.500", // Taker buy quote asset volume
|
||||
"B": "123456" // Ignore
|
||||
}
|
||||
OrderId int64 `json:"i"`
|
||||
OrderLastFilledQuantity string `json:"l"`
|
||||
OrderFilledAccumulatedQuantity string `json:"z"`
|
||||
LastFilledPrice string `json:"L"`
|
||||
|
||||
CommissionAmount string `json:"n"`
|
||||
CommissionAsset string `json:"N"`
|
||||
|
||||
OrderTradeTime int64 `json:"T"`
|
||||
TradeId int64 `json:"t"`
|
||||
|
||||
BidsNotional string `json:"b"`
|
||||
AskNotional string `json:"a"`
|
||||
|
||||
IsMaker bool `json:"m"`
|
||||
IsReduceOnly bool ` json:"r"`
|
||||
|
||||
StopPriceWorkingType string `json:"wt"`
|
||||
OriginalOrderType string `json:"ot"`
|
||||
PositionSide string `json:"ps"`
|
||||
RealizedProfit string `json:"rp"`
|
||||
}
|
||||
|
||||
type OrderTradeUpdateEvent struct {
|
||||
EventBase
|
||||
Transaction int64 `json:"T"`
|
||||
OrderTrade OrderTrade `json:"o"`
|
||||
}
|
||||
|
||||
// {
|
||||
|
||||
// "e":"ORDER_TRADE_UPDATE", // Event Type
|
||||
// "E":1568879465651, // Event Time
|
||||
// "T":1568879465650, // Transaction Time
|
||||
// "o":{
|
||||
// "s":"BTCUSDT", // Symbol
|
||||
// "c":"TEST", // Client Order Id
|
||||
// // special client order id:
|
||||
// // starts with "autoclose-": liquidation order
|
||||
// // "adl_autoclose": ADL auto close order
|
||||
// "S":"SELL", // Side
|
||||
// "o":"TRAILING_STOP_MARKET", // Order Type
|
||||
// "f":"GTC", // Time in Force
|
||||
// "q":"0.001", // Original Quantity
|
||||
// "p":"0", // Original Price
|
||||
// "ap":"0", // Average Price
|
||||
// "sp":"7103.04", // Stop Price. Please ignore with TRAILING_STOP_MARKET order
|
||||
// "x":"NEW", // Execution Type
|
||||
// "X":"NEW", // Order Status
|
||||
// "i":8886774, // Order Id
|
||||
// "l":"0", // Order Last Filled Quantity
|
||||
// "z":"0", // Order Filled Accumulated Quantity
|
||||
// "L":"0", // Last Filled Price
|
||||
// "N":"USDT", // Commission Asset, will not push if no commission
|
||||
// "n":"0", // Commission, will not push if no commission
|
||||
// "T":1568879465651, // Order Trade Time
|
||||
// "t":0, // Trade Id
|
||||
// "b":"0", // Bids Notional
|
||||
// "a":"9.91", // Ask Notional
|
||||
// "m":false, // Is this trade the maker side?
|
||||
// "R":false, // Is this reduce only
|
||||
// "wt":"CONTRACT_PRICE", // Stop Price Working Type
|
||||
// "ot":"TRAILING_STOP_MARKET", // Original Order Type
|
||||
// "ps":"LONG", // Position Side
|
||||
// "cp":false, // If Close-All, pushed with conditional order
|
||||
// "AP":"7476.89", // Activation Price, only puhed with TRAILING_STOP_MARKET order
|
||||
// "cr":"5.0", // Callback Rate, only puhed with TRAILING_STOP_MARKET order
|
||||
// "rp":"0" // Realized Profit of the trade
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
func (e *OrderTradeUpdateEvent) OrderFutures() (*types.Order, error) {
|
||||
|
||||
switch e.OrderTrade.CurrentExecutionType {
|
||||
case "NEW", "CANCELED", "EXPIRED":
|
||||
case "CALCULATED - Liquidation Execution":
|
||||
case "TRADE": // For Order FILLED status. And the order has been completed.
|
||||
default:
|
||||
return nil, errors.New("execution report type is not for futures order")
|
||||
}
|
||||
|
||||
orderCreationTime := time.Unix(0, e.OrderTrade.OrderTradeTime*int64(time.Millisecond))
|
||||
return &types.Order{
|
||||
Exchange: types.ExchangeBinance,
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: e.OrderTrade.Symbol,
|
||||
ClientOrderID: e.OrderTrade.ClientOrderID,
|
||||
Side: toGlobalFuturesSideType(futures.SideType(e.OrderTrade.Side)),
|
||||
Type: toGlobalFuturesOrderType(futures.OrderType(e.OrderTrade.OrderType)),
|
||||
Quantity: util.MustParseFloat(e.OrderTrade.OriginalQuantity),
|
||||
Price: util.MustParseFloat(e.OrderTrade.OriginalPrice),
|
||||
TimeInForce: e.OrderTrade.TimeInForce,
|
||||
},
|
||||
OrderID: uint64(e.OrderTrade.OrderId),
|
||||
Status: toGlobalFuturesOrderStatus(futures.OrderStatusType(e.OrderTrade.CurrentOrderStatus)),
|
||||
ExecutedQuantity: util.MustParseFloat(e.OrderTrade.OrderFilledAccumulatedQuantity),
|
||||
CreationTime: types.Time(orderCreationTime),
|
||||
}, nil
|
||||
}
|
||||
|
||||
*/
|
||||
type EventBase struct {
|
||||
Event string `json:"e"` // event
|
||||
Time int64 `json:"E"`
|
||||
|
|
|
@ -12,6 +12,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/adshao/go-binance/v2"
|
||||
"github.com/adshao/go-binance/v2/futures"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -61,7 +63,9 @@ type Stream struct {
|
|||
types.FuturesSettings
|
||||
types.StandardStream
|
||||
|
||||
Client *binance.Client
|
||||
Client *binance.Client
|
||||
futuresClient *futures.Client
|
||||
|
||||
Conn *websocket.Conn
|
||||
ConnLock sync.Mutex
|
||||
|
||||
|
@ -76,23 +80,28 @@ type Stream struct {
|
|||
kLineClosedEventCallbacks []func(e *KLineEvent)
|
||||
|
||||
markPriceUpdateEventCallbacks []func(e *MarkPriceUpdateEvent)
|
||||
continuousKLineEventCallbacks []func(e *ContinuousKLineEvent)
|
||||
|
||||
continuousKLineEventCallbacks []func(e *ContinuousKLineEvent)
|
||||
continuousKLineClosedEventCallbacks []func(e *ContinuousKLineEvent)
|
||||
|
||||
balanceUpdateEventCallbacks []func(event *BalanceUpdateEvent)
|
||||
outboundAccountInfoEventCallbacks []func(event *OutboundAccountInfoEvent)
|
||||
outboundAccountPositionEventCallbacks []func(event *OutboundAccountPositionEvent)
|
||||
executionReportEventCallbacks []func(event *ExecutionReportEvent)
|
||||
|
||||
orderTradeUpdateEventCallbacks []func(e *OrderTradeUpdateEvent)
|
||||
|
||||
depthFrames map[string]*DepthFrame
|
||||
}
|
||||
|
||||
func NewStream(client *binance.Client) *Stream {
|
||||
func NewStream(client *binance.Client, futuresClient *futures.Client) *Stream {
|
||||
stream := &Stream{
|
||||
StandardStream: types.StandardStream{
|
||||
ReconnectC: make(chan struct{}, 1),
|
||||
},
|
||||
Client: client,
|
||||
depthFrames: make(map[string]*DepthFrame),
|
||||
Client: client,
|
||||
futuresClient: futuresClient,
|
||||
depthFrames: make(map[string]*DepthFrame),
|
||||
}
|
||||
|
||||
stream.OnDepthEvent(func(e *DepthEvent) {
|
||||
|
@ -207,6 +216,54 @@ func NewStream(client *binance.Client) *Stream {
|
|||
}
|
||||
})
|
||||
|
||||
stream.OnContinuousKLineEvent(func(e *ContinuousKLineEvent) {
|
||||
kline := e.KLine.KLine()
|
||||
if e.KLine.Closed {
|
||||
stream.EmitContinuousKLineClosedEvent(e)
|
||||
stream.EmitKLineClosed(kline)
|
||||
} else {
|
||||
stream.EmitKLine(kline)
|
||||
}
|
||||
})
|
||||
|
||||
stream.OnOrderTradeUpdateEvent(func(e *OrderTradeUpdateEvent) {
|
||||
switch e.OrderTrade.CurrentExecutionType {
|
||||
|
||||
case "NEW", "CANCELED", "EXPIRED":
|
||||
order, err := e.OrderFutures()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("order convert error")
|
||||
return
|
||||
}
|
||||
|
||||
stream.EmitOrderUpdate(*order)
|
||||
|
||||
case "TRADE":
|
||||
// TODO
|
||||
|
||||
// trade, err := e.Trade()
|
||||
// if err != nil {
|
||||
// log.WithError(err).Error("trade convert error")
|
||||
// return
|
||||
// }
|
||||
|
||||
// stream.EmitTradeUpdate(*trade)
|
||||
|
||||
// order, err := e.OrderFutures()
|
||||
// if err != nil {
|
||||
// log.WithError(err).Error("order convert error")
|
||||
// return
|
||||
// }
|
||||
|
||||
// Update Order with FILLED event
|
||||
// if order.Status == types.OrderStatusFilled {
|
||||
// stream.EmitOrderUpdate(*order)
|
||||
// }
|
||||
case "CALCULATED - Liquidation Execution":
|
||||
log.Infof("CALCULATED - Liquidation Execution not support yet.")
|
||||
}
|
||||
})
|
||||
|
||||
stream.OnDisconnect(func() {
|
||||
log.Infof("resetting depth snapshots...")
|
||||
for _, f := range stream.depthFrames {
|
||||
|
@ -246,9 +303,17 @@ func (s *Stream) SetPublicOnly() {
|
|||
func (s *Stream) dial(listenKey string) (*websocket.Conn, error) {
|
||||
var url string
|
||||
if s.publicOnly {
|
||||
url = "wss://stream.binance.com:9443/ws"
|
||||
if s.IsFutures {
|
||||
url = "wss://fstream.binance.com/ws/"
|
||||
} else {
|
||||
url = "wss://stream.binance.com:9443/ws"
|
||||
}
|
||||
} else {
|
||||
url = "wss://stream.binance.com:9443/ws/" + listenKey
|
||||
if s.IsFutures {
|
||||
url = "wss://fstream.binance.com/ws/" + listenKey
|
||||
} else {
|
||||
url = "wss://stream.binance.com:9443/ws/" + listenKey
|
||||
}
|
||||
}
|
||||
|
||||
conn, _, err := defaultDialer.Dial(url, nil)
|
||||
|
@ -278,7 +343,12 @@ func (s *Stream) fetchListenKey(ctx context.Context) (string, error) {
|
|||
log.Infof("margin mode is enabled, requesting margin user stream listen key...")
|
||||
req := s.Client.NewStartMarginUserStreamService()
|
||||
return req.Do(ctx)
|
||||
} else if s.IsFutures {
|
||||
log.Infof("futures mode is enabled, requesting futures user stream listen key...")
|
||||
req := s.futuresClient.NewStartUserStreamService()
|
||||
return req.Do(ctx)
|
||||
}
|
||||
log.Infof("spot mode is enabled, requesting margin user stream listen key...")
|
||||
|
||||
return s.Client.NewStartUserStreamService().Do(ctx)
|
||||
}
|
||||
|
@ -290,9 +360,11 @@ func (s *Stream) keepaliveListenKey(ctx context.Context, listenKey string) error
|
|||
req.Symbol(s.IsolatedMarginSymbol)
|
||||
return req.Do(ctx)
|
||||
}
|
||||
|
||||
req := s.Client.NewKeepaliveMarginUserStreamService().ListenKey(listenKey)
|
||||
return req.Do(ctx)
|
||||
} else if s.IsFutures {
|
||||
req := s.futuresClient.NewKeepaliveUserStreamService().ListenKey(listenKey)
|
||||
return req.Do(ctx)
|
||||
}
|
||||
|
||||
return s.Client.NewKeepaliveUserStreamService().ListenKey(listenKey).Do(ctx)
|
||||
|
@ -541,11 +613,15 @@ func (s *Stream) read(ctx context.Context) {
|
|||
|
||||
case *ExecutionReportEvent:
|
||||
s.EmitExecutionReportEvent(e)
|
||||
|
||||
|
||||
case *MarkPriceUpdateEvent:
|
||||
s.EmitMarkPriceUpdateEvent(e)
|
||||
case *ContinuousKLineEvent:
|
||||
s.EmitContinuousKLineEvent(e)
|
||||
|
||||
case *ContinuousKLineEvent:
|
||||
s.EmitContinuousKLineEvent(e)
|
||||
|
||||
case *OrderTradeUpdateEvent:
|
||||
s.EmitOrderTradeUpdateEvent(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -565,6 +641,9 @@ func (s *Stream) invalidateListenKey(ctx context.Context, listenKey string) (err
|
|||
err = req.Do(ctx)
|
||||
}
|
||||
|
||||
} else if s.IsFutures {
|
||||
req := s.futuresClient.NewCloseUserStreamService().ListenKey(listenKey)
|
||||
err = req.Do(ctx)
|
||||
} else {
|
||||
err = s.Client.NewCloseUserStreamService().ListenKey(listenKey).Do(ctx)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,16 @@ func (s *Stream) EmitContinuousKLineEvent(e *ContinuousKLineEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Stream) OnContinuousKLineClosedEvent(cb func(e *ContinuousKLineEvent)) {
|
||||
s.continuousKLineClosedEventCallbacks = append(s.continuousKLineClosedEventCallbacks, cb)
|
||||
}
|
||||
|
||||
func (s *Stream) EmitContinuousKLineClosedEvent(e *ContinuousKLineEvent) {
|
||||
for _, cb := range s.continuousKLineClosedEventCallbacks {
|
||||
cb(e)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Stream) OnBalanceUpdateEvent(cb func(event *BalanceUpdateEvent)) {
|
||||
s.balanceUpdateEventCallbacks = append(s.balanceUpdateEventCallbacks, cb)
|
||||
}
|
||||
|
@ -94,6 +104,16 @@ func (s *Stream) EmitExecutionReportEvent(event *ExecutionReportEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Stream) OnOrderTradeUpdateEvent(cb func(e *OrderTradeUpdateEvent)) {
|
||||
s.orderTradeUpdateEventCallbacks = append(s.orderTradeUpdateEventCallbacks, cb)
|
||||
}
|
||||
|
||||
func (s *Stream) EmitOrderTradeUpdateEvent(e *OrderTradeUpdateEvent) {
|
||||
for _, cb := range s.orderTradeUpdateEventCallbacks {
|
||||
cb(e)
|
||||
}
|
||||
}
|
||||
|
||||
type StreamEventHub interface {
|
||||
OnDepthEvent(cb func(e *DepthEvent))
|
||||
|
||||
|
@ -105,6 +125,8 @@ type StreamEventHub interface {
|
|||
|
||||
OnContinuousKLineEvent(cb func(e *ContinuousKLineEvent))
|
||||
|
||||
OnContinuousKLineClosedEvent(cb func(e *ContinuousKLineEvent))
|
||||
|
||||
OnBalanceUpdateEvent(cb func(event *BalanceUpdateEvent))
|
||||
|
||||
OnOutboundAccountInfoEvent(cb func(event *OutboundAccountInfoEvent))
|
||||
|
@ -112,4 +134,6 @@ type StreamEventHub interface {
|
|||
OnOutboundAccountPositionEvent(cb func(event *OutboundAccountPositionEvent))
|
||||
|
||||
OnExecutionReportEvent(cb func(event *ExecutionReportEvent))
|
||||
|
||||
OnOrderTradeUpdateEvent(cb func(e *OrderTradeUpdateEvent))
|
||||
}
|
||||
|
|
|
@ -177,6 +177,7 @@ func (r *restRequest) newAuthenticatedRequest(ctx context.Context) (*http.Reques
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ts := strconv.FormatInt(timestamp(), 10)
|
||||
p := fmt.Sprintf("%s%s%s", ts, r.m, u.Path)
|
||||
if len(r.q) > 0 {
|
||||
|
|
|
@ -170,7 +170,9 @@ func (s *Stream) pollKLines(ctx context.Context) {
|
|||
|
||||
if len(klines) > 0 {
|
||||
// handle mutiple klines, get the latest one
|
||||
s.EmitKLineClosed(klines[len(klines)-1])
|
||||
kline := klines[len(klines)-1]
|
||||
s.EmitKLine(kline)
|
||||
s.EmitKLineClosed(kline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
47
pkg/exchange/kucoin/exchange.go
Normal file
47
pkg/exchange/kucoin/exchange.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package kucoin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// OKB is the platform currency of OKEx, pre-allocate static string here
|
||||
const KCS = "KCS"
|
||||
|
||||
var log = logrus.WithFields(logrus.Fields{
|
||||
"exchange": "kucoin",
|
||||
})
|
||||
|
||||
type Exchange struct {
|
||||
key, secret, passphrase string
|
||||
}
|
||||
|
||||
func (e *Exchange) NewStream() types.Stream {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func New(key, secret, passphrase string) *Exchange {
|
||||
return &Exchange{
|
||||
key: key,
|
||||
secret: secret,
|
||||
passphrase: passphrase,
|
||||
}
|
||||
}
|
95
pkg/exchange/kucoin/kucoinapi/account.go
Normal file
95
pkg/exchange/kucoin/kucoinapi/account.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package kucoinapi
|
||||
|
||||
import "github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
|
||||
type AccountService struct {
|
||||
client *RestClient
|
||||
}
|
||||
|
||||
type SubAccount struct {
|
||||
UserID string `json:"userId"`
|
||||
Name string `json:"subName"`
|
||||
Type string `json:"type"`
|
||||
Remark string `json:"remarks"`
|
||||
}
|
||||
|
||||
func (s *AccountService) QuerySubAccounts() ([]SubAccount, error) {
|
||||
req, err := s.client.newAuthenticatedRequest("GET", "/api/v1/sub/user", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := s.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data []SubAccount `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ID string `json:"id"`
|
||||
Currency string `json:"currency"`
|
||||
Type string `json:"type"`
|
||||
Balance fixedpoint.Value `json:"balance"`
|
||||
Available fixedpoint.Value `json:"available"`
|
||||
Holds fixedpoint.Value `json:"holds"`
|
||||
}
|
||||
|
||||
func (s *AccountService) ListAccounts() ([]Account, error) {
|
||||
req, err := s.client.newAuthenticatedRequest("GET", "/api/v1/accounts", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := s.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data []Account `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
||||
|
||||
func (s *AccountService) GetAccount(accountID string) (*Account, error) {
|
||||
req, err := s.client.newAuthenticatedRequest("GET", "/api/v1/accounts/" + accountID, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := s.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data *Account `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// Code generated by "requestgen -type CancelAllOrderRequest"; DO NOT EDIT.
|
||||
|
||||
package kucoinapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (r *CancelAllOrderRequest) Symbol(symbol string) *CancelAllOrderRequest {
|
||||
r.symbol = &symbol
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CancelAllOrderRequest) TradeType(tradeType string) *CancelAllOrderRequest {
|
||||
r.tradeType = &tradeType
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CancelAllOrderRequest) GetParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
// check symbol field -> json key symbol
|
||||
if r.symbol != nil {
|
||||
symbol := *r.symbol
|
||||
|
||||
// assign parameter of symbol
|
||||
params["symbol"] = symbol
|
||||
}
|
||||
|
||||
// check tradeType field -> json key tradeType
|
||||
if r.tradeType != nil {
|
||||
tradeType := *r.tradeType
|
||||
|
||||
// assign parameter of tradeType
|
||||
params["tradeType"] = tradeType
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (r *CancelAllOrderRequest) GetParametersQuery() (url.Values, error) {
|
||||
query := url.Values{}
|
||||
|
||||
params, err := r.GetParameters()
|
||||
if err != nil {
|
||||
return query, err
|
||||
}
|
||||
|
||||
for k, v := range params {
|
||||
query.Add(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (r *CancelAllOrderRequest) GetParametersJSON() ([]byte, error) {
|
||||
params, err := r.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(params)
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// Code generated by "requestgen -type CancelOrderRequest"; DO NOT EDIT.
|
||||
|
||||
package kucoinapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (c *CancelOrderRequest) OrderID(orderID string) *CancelOrderRequest {
|
||||
c.orderID = &orderID
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CancelOrderRequest) ClientOrderID(clientOrderID string) *CancelOrderRequest {
|
||||
c.clientOrderID = &clientOrderID
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
// check orderID field -> json key orderID
|
||||
if c.orderID != nil {
|
||||
orderID := *c.orderID
|
||||
|
||||
// assign parameter of orderID
|
||||
params["orderID"] = orderID
|
||||
}
|
||||
|
||||
// check clientOrderID field -> json key clientOrderID
|
||||
if c.clientOrderID != nil {
|
||||
clientOrderID := *c.clientOrderID
|
||||
|
||||
// assign parameter of clientOrderID
|
||||
params["clientOrderID"] = clientOrderID
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) {
|
||||
params, err := c.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(params)
|
||||
}
|
394
pkg/exchange/kucoin/kucoinapi/client.go
Normal file
394
pkg/exchange/kucoin/kucoinapi/client.go
Normal file
|
@ -0,0 +1,394 @@
|
|||
package kucoinapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const defaultHTTPTimeout = time.Second * 15
|
||||
const RestBaseURL = "https://api.kucoin.com/api"
|
||||
const SandboxRestBaseURL = "https://openapi-sandbox.kucoin.com/api"
|
||||
|
||||
type TradeType string
|
||||
|
||||
const (
|
||||
TradeTypeSpot TradeType = "TRADE"
|
||||
TradeTypeMargin TradeType = "MARGIN"
|
||||
)
|
||||
|
||||
type SideType string
|
||||
|
||||
const (
|
||||
SideTypeBuy SideType = "buy"
|
||||
SideTypeSell SideType = "sell"
|
||||
)
|
||||
|
||||
type TimeInForceType string
|
||||
|
||||
const (
|
||||
// GTC Good Till Canceled orders remain open on the book until canceled. This is the default behavior if no policy is specified.
|
||||
TimeInForceGTC TimeInForceType = "GTC"
|
||||
|
||||
// GTT Good Till Time orders remain open on the book until canceled or the allotted cancelAfter is depleted on the matching engine. GTT orders are guaranteed to cancel before any other order is processed after the cancelAfter seconds placed in order book.
|
||||
TimeInForceGTT TimeInForceType = "GTT"
|
||||
|
||||
// FOK Fill Or Kill orders are rejected if the entire size cannot be matched.
|
||||
TimeInForceFOK TimeInForceType = "FOK"
|
||||
|
||||
// IOC Immediate Or Cancel orders instantly cancel the remaining size of the limit order instead of opening it on the book.
|
||||
TimeInForceIOC TimeInForceType = "IOC"
|
||||
)
|
||||
|
||||
type OrderType string
|
||||
|
||||
const (
|
||||
OrderTypeMarket OrderType = "market"
|
||||
OrderTypeLimit OrderType = "limit"
|
||||
)
|
||||
|
||||
type InstrumentType string
|
||||
|
||||
const (
|
||||
InstrumentTypeSpot InstrumentType = "SPOT"
|
||||
InstrumentTypeSwap InstrumentType = "SWAP"
|
||||
InstrumentTypeFutures InstrumentType = "FUTURES"
|
||||
InstrumentTypeOption InstrumentType = "OPTION"
|
||||
)
|
||||
|
||||
type OrderState string
|
||||
|
||||
const (
|
||||
OrderStateCanceled OrderState = "canceled"
|
||||
OrderStateLive OrderState = "live"
|
||||
OrderStatePartiallyFilled OrderState = "partially_filled"
|
||||
OrderStateFilled OrderState = "filled"
|
||||
)
|
||||
|
||||
type RestClient struct {
|
||||
BaseURL *url.URL
|
||||
|
||||
client *http.Client
|
||||
|
||||
Key, Secret, Passphrase string
|
||||
KeyVersion string
|
||||
|
||||
AccountService *AccountService
|
||||
MarketDataService *MarketDataService
|
||||
TradeService *TradeService
|
||||
}
|
||||
|
||||
func NewClient() *RestClient {
|
||||
u, err := url.Parse(RestBaseURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client := &RestClient{
|
||||
BaseURL: u,
|
||||
KeyVersion: "2",
|
||||
client: &http.Client{
|
||||
Timeout: defaultHTTPTimeout,
|
||||
},
|
||||
}
|
||||
|
||||
client.AccountService = &AccountService{client: client}
|
||||
client.MarketDataService = &MarketDataService{client: client}
|
||||
client.TradeService = &TradeService{client: client}
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *RestClient) Auth(key, secret, passphrase string) {
|
||||
c.Key = key
|
||||
c.Secret = secret
|
||||
c.Passphrase = passphrase
|
||||
}
|
||||
|
||||
// NewRequest create new API request. Relative url can be provided in refURL.
|
||||
func (c *RestClient) newRequest(method, refURL string, params url.Values, body []byte) (*http.Request, error) {
|
||||
rel, err := url.Parse(refURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if params != nil {
|
||||
rel.RawQuery = params.Encode()
|
||||
}
|
||||
|
||||
pathURL := c.BaseURL.ResolveReference(rel)
|
||||
return http.NewRequest(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) (*util.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 := util.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(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 := c.BaseURL.ResolveReference(rel)
|
||||
path := pathURL.Path
|
||||
if rel.RawQuery != "" {
|
||||
path += "?" + rel.RawQuery
|
||||
}
|
||||
|
||||
// set location to UTC so that it outputs "2020-12-08T09:08:57.715Z"
|
||||
t := time.Now().In(time.UTC)
|
||||
// timestamp := t.Format("2006-01-02T15:04:05.999Z07:00")
|
||||
timestamp := strconv.FormatInt(t.UnixMilli(), 10)
|
||||
|
||||
var body []byte
|
||||
|
||||
if payload != nil {
|
||||
switch v := payload.(type) {
|
||||
case string:
|
||||
body = []byte(v)
|
||||
|
||||
case []byte:
|
||||
body = v
|
||||
|
||||
default:
|
||||
body, err = json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signKey := timestamp + strings.ToUpper(method) + path + string(body)
|
||||
signature := sign(c.Secret, signKey)
|
||||
|
||||
req, err := http.NewRequest(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")
|
||||
req.Header.Add("KC-API-KEY", c.Key)
|
||||
req.Header.Add("KC-API-SIGN", signature)
|
||||
req.Header.Add("KC-API-TIMESTAMP", timestamp)
|
||||
req.Header.Add("KC-API-PASSPHRASE", sign(c.Secret, c.Passphrase))
|
||||
req.Header.Add("KC-API-KEY-VERSION", c.KeyVersion)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
type BalanceDetail struct {
|
||||
Currency string `json:"ccy"`
|
||||
Available fixedpoint.Value `json:"availEq"`
|
||||
CashBalance fixedpoint.Value `json:"cashBal"`
|
||||
OrderFrozen fixedpoint.Value `json:"ordFrozen"`
|
||||
Frozen fixedpoint.Value `json:"frozenBal"`
|
||||
Equity fixedpoint.Value `json:"eq"`
|
||||
EquityInUSD fixedpoint.Value `json:"eqUsd"`
|
||||
UpdateTime types.MillisecondTimestamp `json:"uTime"`
|
||||
UnrealizedProfitAndLoss fixedpoint.Value `json:"upl"`
|
||||
}
|
||||
|
||||
type AssetBalance struct {
|
||||
Currency string `json:"ccy"`
|
||||
Balance fixedpoint.Value `json:"bal"`
|
||||
Frozen fixedpoint.Value `json:"frozenBal,omitempty"`
|
||||
Available fixedpoint.Value `json:"availBal,omitempty"`
|
||||
}
|
||||
|
||||
type AssetBalanceList []AssetBalance
|
||||
|
||||
func (c *RestClient) AssetBalances() (AssetBalanceList, error) {
|
||||
req, err := c.newAuthenticatedRequest("GET", "/api/v5/asset/balances", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := c.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var balanceResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data AssetBalanceList `json:"data"`
|
||||
}
|
||||
if err := response.DecodeJSON(&balanceResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return balanceResponse.Data, nil
|
||||
}
|
||||
|
||||
type AssetCurrency struct {
|
||||
Currency string `json:"ccy"`
|
||||
Name string `json:"name"`
|
||||
Chain string `json:"chain"`
|
||||
CanDeposit bool `json:"canDep"`
|
||||
CanWithdraw bool `json:"canWd"`
|
||||
CanInternal bool `json:"canInternal"`
|
||||
MinWithdrawalFee fixedpoint.Value `json:"minFee"`
|
||||
MaxWithdrawalFee fixedpoint.Value `json:"maxFee"`
|
||||
MinWithdrawalThreshold fixedpoint.Value `json:"minWd"`
|
||||
}
|
||||
|
||||
func (c *RestClient) AssetCurrencies() ([]AssetCurrency, error) {
|
||||
req, err := c.newAuthenticatedRequest("GET", "/api/v5/asset/currencies", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := c.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var currencyResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data []AssetCurrency `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(¤cyResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return currencyResponse.Data, nil
|
||||
}
|
||||
|
||||
type MarketTicker struct {
|
||||
InstrumentType string `json:"instType"`
|
||||
InstrumentID string `json:"instId"`
|
||||
|
||||
// last traded price
|
||||
Last fixedpoint.Value `json:"last"`
|
||||
|
||||
// last traded size
|
||||
LastSize fixedpoint.Value `json:"lastSz"`
|
||||
|
||||
AskPrice fixedpoint.Value `json:"askPx"`
|
||||
AskSize fixedpoint.Value `json:"askSz"`
|
||||
|
||||
BidPrice fixedpoint.Value `json:"bidPx"`
|
||||
BidSize fixedpoint.Value `json:"bidSz"`
|
||||
|
||||
Open24H fixedpoint.Value `json:"open24h"`
|
||||
High24H fixedpoint.Value `json:"high24H"`
|
||||
Low24H fixedpoint.Value `json:"low24H"`
|
||||
Volume24H fixedpoint.Value `json:"vol24h"`
|
||||
VolumeCurrency24H fixedpoint.Value `json:"volCcy24h"`
|
||||
|
||||
// Millisecond timestamp
|
||||
Timestamp types.MillisecondTimestamp `json:"ts"`
|
||||
}
|
||||
|
||||
func (c *RestClient) MarketTicker(instId string) (*MarketTicker, error) {
|
||||
// SPOT, SWAP, FUTURES, OPTION
|
||||
var params = url.Values{}
|
||||
params.Add("instId", instId)
|
||||
|
||||
req, err := c.newRequest("GET", "/api/v5/market/ticker", params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := c.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tickerResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data []MarketTicker `json:"data"`
|
||||
}
|
||||
if err := response.DecodeJSON(&tickerResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(tickerResponse.Data) == 0 {
|
||||
return nil, fmt.Errorf("ticker of %s not found", instId)
|
||||
}
|
||||
|
||||
return &tickerResponse.Data[0], nil
|
||||
}
|
||||
|
||||
func (c *RestClient) MarketTickers(instType InstrumentType) ([]MarketTicker, error) {
|
||||
// SPOT, SWAP, FUTURES, OPTION
|
||||
var params = url.Values{}
|
||||
params.Add("instType", string(instType))
|
||||
|
||||
req, err := c.newRequest("GET", "/api/v5/market/tickers", params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := c.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tickerResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data []MarketTicker `json:"data"`
|
||||
}
|
||||
if err := response.DecodeJSON(&tickerResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tickerResponse.Data, nil
|
||||
}
|
||||
|
||||
func sign(secret, payload string) string {
|
||||
var sig = hmac.New(sha256.New, []byte(secret))
|
||||
_, err := sig.Write([]byte(payload))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(sig.Sum(nil))
|
||||
}
|
152
pkg/exchange/kucoin/kucoinapi/list_orders_request_accessors.go
Normal file
152
pkg/exchange/kucoin/kucoinapi/list_orders_request_accessors.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
// Code generated by "requestgen -type ListOrdersRequest"; DO NOT EDIT.
|
||||
|
||||
package kucoinapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (r *ListOrdersRequest) Status(status string) *ListOrdersRequest {
|
||||
r.status = &status
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) Symbol(symbol string) *ListOrdersRequest {
|
||||
r.symbol = &symbol
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) Side(side SideType) *ListOrdersRequest {
|
||||
r.side = &side
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) OrderType(orderType OrderType) *ListOrdersRequest {
|
||||
r.orderType = &orderType
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) TradeType(tradeType TradeType) *ListOrdersRequest {
|
||||
r.tradeType = &tradeType
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) StartAt(startAt time.Time) *ListOrdersRequest {
|
||||
r.startAt = &startAt
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) EndAt(endAt time.Time) *ListOrdersRequest {
|
||||
r.endAt = &endAt
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) GetParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
// check status field -> json key status
|
||||
if r.status != nil {
|
||||
status := *r.status
|
||||
|
||||
switch status {
|
||||
case "active", "done":
|
||||
params["status"] = status
|
||||
|
||||
default:
|
||||
return params, fmt.Errorf("status value %v is invalid", status)
|
||||
|
||||
}
|
||||
|
||||
// assign parameter of status
|
||||
params["status"] = status
|
||||
}
|
||||
|
||||
// check symbol field -> json key symbol
|
||||
if r.symbol != nil {
|
||||
symbol := *r.symbol
|
||||
|
||||
// assign parameter of symbol
|
||||
params["symbol"] = symbol
|
||||
}
|
||||
|
||||
// check side field -> json key side
|
||||
if r.side != nil {
|
||||
side := *r.side
|
||||
|
||||
switch side {
|
||||
case "buy", "sell":
|
||||
params["side"] = side
|
||||
|
||||
default:
|
||||
return params, fmt.Errorf("side value %v is invalid", side)
|
||||
|
||||
}
|
||||
|
||||
// assign parameter of side
|
||||
params["side"] = side
|
||||
}
|
||||
|
||||
// check orderType field -> json key type
|
||||
if r.orderType != nil {
|
||||
orderType := *r.orderType
|
||||
|
||||
// assign parameter of orderType
|
||||
params["type"] = orderType
|
||||
}
|
||||
|
||||
// check tradeType field -> json key tradeType
|
||||
if r.tradeType != nil {
|
||||
tradeType := *r.tradeType
|
||||
|
||||
// assign parameter of tradeType
|
||||
params["tradeType"] = tradeType
|
||||
}
|
||||
|
||||
// check startAt field -> json key startAt
|
||||
if r.startAt != nil {
|
||||
startAt := *r.startAt
|
||||
|
||||
// assign parameter of startAt
|
||||
// convert time.Time to milliseconds time
|
||||
params["startAt"] = strconv.FormatInt(startAt.UnixNano()/int64(time.Millisecond), 10)
|
||||
}
|
||||
|
||||
// check endAt field -> json key endAt
|
||||
if r.endAt != nil {
|
||||
endAt := *r.endAt
|
||||
|
||||
// assign parameter of endAt
|
||||
// convert time.Time to milliseconds time
|
||||
params["endAt"] = strconv.FormatInt(endAt.UnixNano()/int64(time.Millisecond), 10)
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) GetParametersQuery() (url.Values, error) {
|
||||
query := url.Values{}
|
||||
|
||||
params, err := r.GetParameters()
|
||||
if err != nil {
|
||||
return query, err
|
||||
}
|
||||
|
||||
for k, v := range params {
|
||||
query.Add(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) GetParametersJSON() ([]byte, error) {
|
||||
params, err := r.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(params)
|
||||
}
|
253
pkg/exchange/kucoin/kucoinapi/marketdata.go
Normal file
253
pkg/exchange/kucoin/kucoinapi/marketdata.go
Normal file
|
@ -0,0 +1,253 @@
|
|||
package kucoinapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type MarketDataService struct {
|
||||
client *RestClient
|
||||
}
|
||||
|
||||
type Symbol struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
BaseCurrency string `json:"baseCurrency"`
|
||||
QuoteCurrency string `json:"quoteCurrency"`
|
||||
FeeCurrency string `json:"feeCurrency"`
|
||||
Market string `json:"market"`
|
||||
BaseMinSize fixedpoint.Value `json:"baseMinSize"`
|
||||
QuoteMinSize fixedpoint.Value `json:"quoteMinSize"`
|
||||
BaseIncrement fixedpoint.Value `json:"baseIncrement"`
|
||||
QuoteIncrement fixedpoint.Value `json:"quoteIncrement"`
|
||||
PriceIncrement fixedpoint.Value `json:"priceIncrement"`
|
||||
PriceLimitRate fixedpoint.Value `json:"priceLimitRate"`
|
||||
IsMarginEnabled bool `json:"isMarginEnabled"`
|
||||
EnableTrading bool `json:"enableTrading"`
|
||||
}
|
||||
|
||||
func (s *MarketDataService) ListSymbols(market ...string) ([]Symbol, error) {
|
||||
var params = url.Values{}
|
||||
|
||||
if len(market) == 1 {
|
||||
params["market"] = []string{market[0]}
|
||||
} else if len(market) > 1 {
|
||||
return nil, errors.New("symbols api only supports one market parameter")
|
||||
}
|
||||
|
||||
req, err := s.client.newRequest("GET", "/api/v1/symbols", params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := s.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data []Symbol `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
||||
|
||||
/*
|
||||
//Get Ticker
|
||||
{
|
||||
"sequence": "1550467636704",
|
||||
"bestAsk": "0.03715004",
|
||||
"size": "0.17",
|
||||
"price": "0.03715005",
|
||||
"bestBidSize": "3.803",
|
||||
"bestBid": "0.03710768",
|
||||
"bestAskSize": "1.788",
|
||||
"time": 1550653727731
|
||||
}
|
||||
*/
|
||||
type Ticker struct {
|
||||
Sequence string `json:"sequence"`
|
||||
Size fixedpoint.Value `json:"size"`
|
||||
Price fixedpoint.Value `json:"price"`
|
||||
BestAsk fixedpoint.Value `json:"bestAsk"`
|
||||
BestBid fixedpoint.Value `json:"bestBid"`
|
||||
BestBidSize fixedpoint.Value `json:"bestBidSize"`
|
||||
Time types.MillisecondTimestamp `json:"time"`
|
||||
}
|
||||
|
||||
func (s *MarketDataService) GetTicker(symbol string) (*Ticker, error) {
|
||||
var params = url.Values{}
|
||||
params["symbol"] = []string{symbol}
|
||||
|
||||
req, err := s.client.newRequest("GET", "/api/v1/market/orderbook/level1", params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := s.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data *Ticker `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"time":1602832092060,
|
||||
"ticker":[
|
||||
{
|
||||
"symbol": "BTC-USDT", // symbol
|
||||
"symbolName":"BTC-USDT", // Name of trading pairs, it would change after renaming
|
||||
"buy": "11328.9", // bestAsk
|
||||
"sell": "11329", // bestBid
|
||||
"changeRate": "-0.0055", // 24h change rate
|
||||
"changePrice": "-63.6", // 24h change price
|
||||
"high": "11610", // 24h highest price
|
||||
"low": "11200", // 24h lowest price
|
||||
"vol": "2282.70993217", // 24h volume,the aggregated trading volume in BTC
|
||||
"volValue": "25984946.157790431", // 24h total, the trading volume in quote currency of last 24 hours
|
||||
"last": "11328.9", // last price
|
||||
"averagePrice": "11360.66065903", // 24h average transaction price yesterday
|
||||
"takerFeeRate": "0.001", // Basic Taker Fee
|
||||
"makerFeeRate": "0.001", // Basic Maker Fee
|
||||
"takerCoefficient": "1", // Taker Fee Coefficient
|
||||
"makerCoefficient": "1" // Maker Fee Coefficient
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
||||
|
||||
type Ticker24H struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"symbolName"`
|
||||
Buy fixedpoint.Value `json:"buy"`
|
||||
Sell fixedpoint.Value `json:"sell"`
|
||||
ChangeRate fixedpoint.Value `json:"changeRate"`
|
||||
ChangePrice fixedpoint.Value `json:"changePrice"`
|
||||
High fixedpoint.Value `json:"high"`
|
||||
Low fixedpoint.Value `json:"low"`
|
||||
Last fixedpoint.Value `json:"last"`
|
||||
AveragePrice fixedpoint.Value `json:"averagePrice"`
|
||||
Volume fixedpoint.Value `json:"vol"` // base volume
|
||||
VolumeValue fixedpoint.Value `json:"volValue"` // quote volume
|
||||
|
||||
TakerFeeRate fixedpoint.Value `json:"takerFeeRate"`
|
||||
MakerFeeRate fixedpoint.Value `json:"makerFeeRate"`
|
||||
|
||||
TakerCoefficient fixedpoint.Value `json:"takerCoefficient"`
|
||||
MakerCoefficient fixedpoint.Value `json:"makerCoefficient"`
|
||||
}
|
||||
|
||||
type AllTickers struct {
|
||||
Time types.MillisecondTimestamp `json:"time"`
|
||||
Ticker []Ticker24H `json:"ticker"`
|
||||
}
|
||||
|
||||
func (s *MarketDataService) ListTickers() (*AllTickers, error) {
|
||||
req, err := s.client.newRequest("GET", "/api/v1/market/allTickers", nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := s.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data *AllTickers `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"sequence": "3262786978",
|
||||
"time": 1550653727731,
|
||||
"bids": [["6500.12", "0.45054140"],
|
||||
["6500.11", "0.45054140"]], //[price,size]
|
||||
"asks": [["6500.16", "0.57753524"],
|
||||
["6500.15", "0.57753524"]]
|
||||
}
|
||||
*/
|
||||
type OrderBook struct {
|
||||
Sequence string `json:"sequence"`
|
||||
Time types.MillisecondTimestamp `json:"time"`
|
||||
Bids [][]fixedpoint.Value `json:"bids,omitempty"`
|
||||
Asks [][]fixedpoint.Value `json:"asks,omitempty"`
|
||||
}
|
||||
|
||||
func (s *MarketDataService) GetOrderBook(symbol string, depth int) (*OrderBook, error) {
|
||||
params := url.Values{}
|
||||
params["symbol"] = []string{symbol}
|
||||
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
switch depth {
|
||||
case 20, 100:
|
||||
refURL := "/api/v1/market/orderbook/level2_" + strconv.Itoa(depth)
|
||||
req, err = s.client.newRequest("GET", refURL, params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case 0:
|
||||
refURL := "/api/v3/market/orderbook/level2"
|
||||
req, err = s.client.newAuthenticatedRequest("GET", refURL, params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("depth %d is not supported, use 20, 100 or 0", depth)
|
||||
|
||||
}
|
||||
|
||||
response, err := s.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data *OrderBook `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
159
pkg/exchange/kucoin/kucoinapi/place_order_request_accessors.go
Normal file
159
pkg/exchange/kucoin/kucoinapi/place_order_request_accessors.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
// Code generated by "requestgen -type PlaceOrderRequest"; DO NOT EDIT.
|
||||
|
||||
package kucoinapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (r *PlaceOrderRequest) ClientOrderID(clientOrderID string) *PlaceOrderRequest {
|
||||
r.clientOrderID = &clientOrderID
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Symbol(symbol string) *PlaceOrderRequest {
|
||||
r.symbol = symbol
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Tag(tag string) *PlaceOrderRequest {
|
||||
r.tag = &tag
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Side(side SideType) *PlaceOrderRequest {
|
||||
r.side = side
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest {
|
||||
r.orderType = orderType
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Size(size string) *PlaceOrderRequest {
|
||||
r.size = size
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Price(price string) *PlaceOrderRequest {
|
||||
r.price = &price
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) TimeInForce(timeInForce TimeInForceType) *PlaceOrderRequest {
|
||||
r.timeInForce = &timeInForce
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
// check clientOrderID field -> json key clientOid
|
||||
if r.clientOrderID != nil {
|
||||
clientOrderID := *r.clientOrderID
|
||||
|
||||
if len(clientOrderID) == 0 {
|
||||
return params, fmt.Errorf("clientOid is required, empty string given")
|
||||
}
|
||||
|
||||
// assign parameter of clientOrderID
|
||||
params["clientOid"] = clientOrderID
|
||||
} else {
|
||||
|
||||
// assign default of clientOrderID
|
||||
clientOrderID := uuid.New().String()
|
||||
|
||||
// assign parameter of clientOrderID
|
||||
params["clientOid"] = clientOrderID
|
||||
}
|
||||
|
||||
// check symbol field -> json key symbol
|
||||
symbol := r.symbol
|
||||
|
||||
if len(symbol) == 0 {
|
||||
return params, fmt.Errorf("symbol is required, empty string given")
|
||||
}
|
||||
|
||||
// assign parameter of symbol
|
||||
params["symbol"] = symbol
|
||||
|
||||
// check tag field -> json key tag
|
||||
if r.tag != nil {
|
||||
tag := *r.tag
|
||||
|
||||
// assign parameter of tag
|
||||
params["tag"] = tag
|
||||
}
|
||||
|
||||
// check side field -> json key side
|
||||
side := r.side
|
||||
|
||||
// assign parameter of side
|
||||
params["side"] = side
|
||||
|
||||
// check orderType field -> json key ordType
|
||||
orderType := r.orderType
|
||||
|
||||
// assign parameter of orderType
|
||||
params["ordType"] = orderType
|
||||
|
||||
// check size field -> json key size
|
||||
size := r.size
|
||||
|
||||
if len(size) == 0 {
|
||||
return params, fmt.Errorf("size is required, empty string given")
|
||||
}
|
||||
|
||||
// assign parameter of size
|
||||
params["size"] = size
|
||||
|
||||
// check price field -> json key price
|
||||
if r.price != nil {
|
||||
price := *r.price
|
||||
|
||||
// assign parameter of price
|
||||
params["price"] = price
|
||||
}
|
||||
|
||||
// check timeInForce field -> json key timeInForce
|
||||
if r.timeInForce != nil {
|
||||
timeInForce := *r.timeInForce
|
||||
|
||||
if len(timeInForce) == 0 {
|
||||
return params, fmt.Errorf("timeInForce is required, empty string given")
|
||||
}
|
||||
|
||||
// assign parameter of timeInForce
|
||||
params["timeInForce"] = timeInForce
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) GetParametersQuery() (url.Values, error) {
|
||||
query := url.Values{}
|
||||
|
||||
params, err := r.GetParameters()
|
||||
if err != nil {
|
||||
return query, err
|
||||
}
|
||||
|
||||
for k, v := range params {
|
||||
query.Add(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) GetParametersJSON() ([]byte, error) {
|
||||
params, err := r.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(params)
|
||||
}
|
355
pkg/exchange/kucoin/kucoinapi/trade.go
Normal file
355
pkg/exchange/kucoin/kucoinapi/trade.go
Normal file
|
@ -0,0 +1,355 @@
|
|||
package kucoinapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type TradeService struct {
|
||||
client *RestClient
|
||||
}
|
||||
|
||||
type OrderResponse struct {
|
||||
OrderID string `json:"orderId"`
|
||||
}
|
||||
|
||||
func (c *TradeService) NewPlaceOrderRequest() *PlaceOrderRequest {
|
||||
return &PlaceOrderRequest{
|
||||
client: c.client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TradeService) NewBatchPlaceOrderRequest() *BatchPlaceOrderRequest {
|
||||
return &BatchPlaceOrderRequest{
|
||||
client: c.client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TradeService) NewCancelOrderRequest() *CancelOrderRequest {
|
||||
return &CancelOrderRequest{
|
||||
client: c.client,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TradeService) NewCancelAllOrderRequest() *CancelAllOrderRequest {
|
||||
return &CancelAllOrderRequest{
|
||||
client: c.client,
|
||||
}
|
||||
}
|
||||
|
||||
//go:generate requestgen -type ListOrdersRequest
|
||||
type ListOrdersRequest struct {
|
||||
client *RestClient
|
||||
|
||||
status *string `param:"status" validValues:"active,done"`
|
||||
|
||||
symbol *string `param:"symbol"`
|
||||
|
||||
side *SideType `param:"side" validValues:"buy,sell"`
|
||||
|
||||
orderType *OrderType `param:"type"`
|
||||
|
||||
tradeType *TradeType `param:"tradeType"`
|
||||
|
||||
startAt *time.Time `param:"startAt,milliseconds"`
|
||||
|
||||
endAt *time.Time `param:"endAt,milliseconds"`
|
||||
}
|
||||
|
||||
type Order struct {
|
||||
ID string `json:"id"`
|
||||
Symbol string `json:"symbol"`
|
||||
OperationType string `json:"opType"`
|
||||
Type string `json:"type"`
|
||||
Side string `json:"side"`
|
||||
Price fixedpoint.Value `json:"price"`
|
||||
Size fixedpoint.Value `json:"size"`
|
||||
Funds fixedpoint.Value `json:"funds"`
|
||||
DealFunds fixedpoint.Value `json:"dealFunds"`
|
||||
DealSize fixedpoint.Value `json:"dealSize"`
|
||||
Fee fixedpoint.Value `json:"fee"`
|
||||
FeeCurrency string `json:"feeCurrency"`
|
||||
StopType string `json:"stop"`
|
||||
StopTriggerred bool `json:"stopTriggered"`
|
||||
StopPrice fixedpoint.Value `json:"stopPrice"`
|
||||
TimeInForce TimeInForceType `json:"timeInForce"`
|
||||
PostOnly bool `json:"postOnly"`
|
||||
Hidden bool `json:"hidden"`
|
||||
Iceberg bool `json:"iceberg"`
|
||||
Channel string `json:"channel"`
|
||||
ClientOrderID string `json:"clientOid"`
|
||||
Remark string `json:"remark"`
|
||||
IsActive bool `json:"isActive"`
|
||||
CancelExist bool `json:"cancelExist"`
|
||||
CreatedAt types.MillisecondTimestamp `json:"createdAt"`
|
||||
}
|
||||
|
||||
type OrderListPage struct {
|
||||
CurrentPage int `json:"currentPage"`
|
||||
PageSize int `json:"pageSize"`
|
||||
TotalNumber int `json:"totalNum"`
|
||||
TotalPage int `json:"totalPage"`
|
||||
Items []Order `json:"items"`
|
||||
}
|
||||
|
||||
func (r *ListOrdersRequest) Do(ctx context.Context) (*OrderListPage, error) {
|
||||
params, err := r.GetParametersQuery()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !params.Has("tradeType") {
|
||||
params.Add("tradeType", "TRADE")
|
||||
}
|
||||
|
||||
req, err := r.client.newAuthenticatedRequest("GET", "/api/v1/orders", params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := r.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var orderResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data *OrderListPage `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&orderResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if orderResponse.Data == nil {
|
||||
return nil, errors.New("api error: [" + orderResponse.Code + "] " + orderResponse.Message)
|
||||
}
|
||||
|
||||
return orderResponse.Data, nil
|
||||
}
|
||||
|
||||
func (c *TradeService) NewListOrdersRequest() *ListOrdersRequest {
|
||||
return &ListOrdersRequest{client: c.client}
|
||||
}
|
||||
|
||||
//go:generate requestgen -type PlaceOrderRequest
|
||||
type PlaceOrderRequest struct {
|
||||
client *RestClient
|
||||
|
||||
// A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters.
|
||||
clientOrderID *string `param:"clientOid,required" defaultValuer:"uuid()"`
|
||||
|
||||
symbol string `param:"symbol,required"`
|
||||
|
||||
// A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 8 characters.
|
||||
tag *string `param:"tag"`
|
||||
|
||||
// "buy" or "sell"
|
||||
side SideType `param:"side"`
|
||||
|
||||
orderType OrderType `param:"ordType"`
|
||||
|
||||
// limit order parameters
|
||||
size string `param:"size,required"`
|
||||
|
||||
price *string `param:"price"`
|
||||
|
||||
timeInForce *TimeInForceType `param:"timeInForce,required"`
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) {
|
||||
payload, err := r.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := r.client.newAuthenticatedRequest("POST", "/api/v1/orders", nil, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := r.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var orderResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data *OrderResponse `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&orderResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if orderResponse.Data == nil {
|
||||
return nil, errors.New("api error: [" + orderResponse.Code + "] " + orderResponse.Message)
|
||||
}
|
||||
|
||||
return orderResponse.Data, nil
|
||||
}
|
||||
|
||||
//go:generate requestgen -type CancelOrderRequest
|
||||
type CancelOrderRequest struct {
|
||||
client *RestClient
|
||||
|
||||
orderID *string `param:"orderID"`
|
||||
clientOrderID *string `param:"clientOrderID"`
|
||||
}
|
||||
|
||||
type CancelOrderResponse struct {
|
||||
CancelledOrderIDs []string `json:"cancelledOrderIds,omitempty"`
|
||||
|
||||
// used when using client order id for canceling order
|
||||
CancelledOrderId string `json:"cancelledOrderId,omitempty"`
|
||||
ClientOrderID string `json:"clientOid,omitempty"`
|
||||
}
|
||||
|
||||
func (r *CancelOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) {
|
||||
if r.orderID == nil && r.clientOrderID == nil {
|
||||
return nil, errors.New("either orderID or clientOrderID is required for canceling order")
|
||||
}
|
||||
|
||||
var refURL string
|
||||
|
||||
if r.orderID != nil {
|
||||
refURL = "/api/v1/orders/" + *r.orderID
|
||||
} else if r.clientOrderID != nil {
|
||||
refURL = "/api/v1/order/client-order/" + *r.clientOrderID
|
||||
}
|
||||
|
||||
req, err := r.client.newAuthenticatedRequest("DELETE", refURL, nil, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := r.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data *CancelOrderResponse `json:"data"`
|
||||
}
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if apiResponse.Data == nil {
|
||||
return nil, errors.New("api error: [" + apiResponse.Code + "] " + apiResponse.Message)
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
||||
|
||||
//go:generate requestgen -type CancelAllOrderRequest
|
||||
type CancelAllOrderRequest struct {
|
||||
client *RestClient
|
||||
|
||||
symbol *string `param:"symbol"`
|
||||
tradeType *string `param:"tradeType"`
|
||||
}
|
||||
|
||||
func (r *CancelAllOrderRequest) Do(ctx context.Context) (*CancelOrderResponse, error) {
|
||||
params, err := r.GetParametersQuery()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := r.client.newAuthenticatedRequest("DELETE", "/api/v1/orders", params, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := r.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data *CancelOrderResponse `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if apiResponse.Data == nil {
|
||||
return nil, errors.New("api error: [" + apiResponse.Code + "] " + apiResponse.Message)
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
||||
|
||||
// Request via this endpoint to place 5 orders at the same time.
|
||||
// The order type must be a limit order of the same symbol.
|
||||
// The interface currently only supports spot trading
|
||||
type BatchPlaceOrderRequest struct {
|
||||
client *RestClient
|
||||
|
||||
symbol string
|
||||
reqs []*PlaceOrderRequest
|
||||
}
|
||||
|
||||
func (r *BatchPlaceOrderRequest) Symbol(symbol string) *BatchPlaceOrderRequest {
|
||||
r.symbol = symbol
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *BatchPlaceOrderRequest) Add(reqs ...*PlaceOrderRequest) *BatchPlaceOrderRequest {
|
||||
r.reqs = append(r.reqs, reqs...)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *BatchPlaceOrderRequest) Do(ctx context.Context) ([]OrderResponse, error) {
|
||||
var orderList []map[string]interface{}
|
||||
for _, req := range r.reqs {
|
||||
params, err := req.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orderList = append(orderList, params)
|
||||
}
|
||||
|
||||
var payload = map[string]interface{}{
|
||||
"symbol": r.symbol,
|
||||
"orderList": orderList,
|
||||
}
|
||||
|
||||
req, err := r.client.newAuthenticatedRequest("POST", "/api/v1/orders/multi", nil, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := r.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
Data []OrderResponse `json:"data"`
|
||||
}
|
||||
|
||||
if err := response.DecodeJSON(&apiResponse); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if apiResponse.Data == nil {
|
||||
return nil, errors.New("api error: [" + apiResponse.Code + "] " + apiResponse.Message)
|
||||
}
|
||||
|
||||
return apiResponse.Data, nil
|
||||
}
|
76
pkg/exchange/okex/okexapi/cancel_order_request_accessors.go
Normal file
76
pkg/exchange/okex/okexapi/cancel_order_request_accessors.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
// Code generated by "requestgen -type CancelOrderRequest"; DO NOT EDIT.
|
||||
|
||||
package okexapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (c *CancelOrderRequest) InstrumentID(instrumentID string) *CancelOrderRequest {
|
||||
c.instrumentID = instrumentID
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CancelOrderRequest) OrderID(orderID string) *CancelOrderRequest {
|
||||
c.orderID = &orderID
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CancelOrderRequest) ClientOrderID(clientOrderID string) *CancelOrderRequest {
|
||||
c.clientOrderID = &clientOrderID
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
// check instrumentID field -> json key instId
|
||||
instrumentID := c.instrumentID
|
||||
|
||||
// assign parameter of instrumentID
|
||||
params["instId"] = instrumentID
|
||||
|
||||
// check orderID field -> json key ordId
|
||||
if c.orderID != nil {
|
||||
orderID := *c.orderID
|
||||
|
||||
// assign parameter of orderID
|
||||
params["ordId"] = orderID
|
||||
}
|
||||
|
||||
// check clientOrderID field -> json key clOrdId
|
||||
if c.clientOrderID != nil {
|
||||
clientOrderID := *c.clientOrderID
|
||||
|
||||
// assign parameter of clientOrderID
|
||||
params["clOrdId"] = clientOrderID
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) {
|
||||
params, err := c.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(params)
|
||||
}
|
151
pkg/exchange/okex/okexapi/place_order_request_accessors.go
Normal file
151
pkg/exchange/okex/okexapi/place_order_request_accessors.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
// Code generated by "requestgen -type PlaceOrderRequest"; DO NOT EDIT.
|
||||
|
||||
package okexapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (p *PlaceOrderRequest) InstrumentID(instrumentID string) *PlaceOrderRequest {
|
||||
p.instrumentID = instrumentID
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) TradeMode(tradeMode string) *PlaceOrderRequest {
|
||||
p.tradeMode = tradeMode
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) ClientOrderID(clientOrderID string) *PlaceOrderRequest {
|
||||
p.clientOrderID = &clientOrderID
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) Tag(tag string) *PlaceOrderRequest {
|
||||
p.tag = &tag
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) Side(side SideType) *PlaceOrderRequest {
|
||||
p.side = side
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest {
|
||||
p.orderType = orderType
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) Quantity(quantity string) *PlaceOrderRequest {
|
||||
p.quantity = quantity
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) Price(price string) *PlaceOrderRequest {
|
||||
p.price = &price
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) {
|
||||
var params = map[string]interface{}{}
|
||||
|
||||
// check instrumentID field -> json key instId
|
||||
instrumentID := p.instrumentID
|
||||
|
||||
// assign parameter of instrumentID
|
||||
params["instId"] = instrumentID
|
||||
|
||||
// check tradeMode field -> json key tdMode
|
||||
tradeMode := p.tradeMode
|
||||
|
||||
switch tradeMode {
|
||||
case "cross", "isolated", "cash":
|
||||
params["tdMode"] = tradeMode
|
||||
|
||||
default:
|
||||
return params, fmt.Errorf("tdMode value %v is invalid", tradeMode)
|
||||
|
||||
}
|
||||
|
||||
// assign parameter of tradeMode
|
||||
params["tdMode"] = tradeMode
|
||||
|
||||
// check clientOrderID field -> json key clOrdId
|
||||
if p.clientOrderID != nil {
|
||||
clientOrderID := *p.clientOrderID
|
||||
|
||||
// assign parameter of clientOrderID
|
||||
params["clOrdId"] = clientOrderID
|
||||
}
|
||||
|
||||
// check tag field -> json key tag
|
||||
if p.tag != nil {
|
||||
tag := *p.tag
|
||||
|
||||
// assign parameter of tag
|
||||
params["tag"] = tag
|
||||
}
|
||||
|
||||
// check side field -> json key side
|
||||
side := p.side
|
||||
|
||||
switch side {
|
||||
case "buy", "sell":
|
||||
params["side"] = side
|
||||
|
||||
default:
|
||||
return params, fmt.Errorf("side value %v is invalid", side)
|
||||
|
||||
}
|
||||
|
||||
// assign parameter of side
|
||||
params["side"] = side
|
||||
|
||||
// check orderType field -> json key ordType
|
||||
orderType := p.orderType
|
||||
|
||||
// assign parameter of orderType
|
||||
params["ordType"] = orderType
|
||||
|
||||
// check quantity field -> json key sz
|
||||
quantity := p.quantity
|
||||
|
||||
// assign parameter of quantity
|
||||
params["sz"] = quantity
|
||||
|
||||
// check price field -> json key px
|
||||
if p.price != nil {
|
||||
price := *p.price
|
||||
|
||||
// assign parameter of price
|
||||
params["px"] = price
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) {
|
||||
params, err := p.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return json.Marshal(params)
|
||||
}
|
|
@ -64,96 +64,37 @@ func (c *TradeService) NewGetTransactionDetailsRequest() *GetTransactionDetailsR
|
|||
}
|
||||
}
|
||||
|
||||
//go:generate requestgen -type PlaceOrderRequest
|
||||
type PlaceOrderRequest struct {
|
||||
client *RestClient
|
||||
|
||||
instId string
|
||||
instrumentID string `param:"instId"`
|
||||
|
||||
// tdMode
|
||||
// margin mode: "cross", "isolated"
|
||||
// non-margin mode cash
|
||||
tdMode string
|
||||
tradeMode string `param:"tdMode" validValues:"cross,isolated,cash"`
|
||||
|
||||
// A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters.
|
||||
clientOrderID *string
|
||||
clientOrderID *string `param:"clOrdId"`
|
||||
|
||||
// A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 8 characters.
|
||||
tag *string
|
||||
tag *string `param:"tag"`
|
||||
|
||||
// "buy" or "sell"
|
||||
side SideType
|
||||
side SideType `param:"side" validValues:"buy,sell"`
|
||||
|
||||
ordType OrderType
|
||||
orderType OrderType `param:"ordType"`
|
||||
|
||||
// sz Quantity
|
||||
sz string
|
||||
quantity string `param:"sz"`
|
||||
|
||||
// price
|
||||
px *string
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) InstrumentID(instID string) *PlaceOrderRequest {
|
||||
r.instId = instID
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) TradeMode(mode string) *PlaceOrderRequest {
|
||||
r.tdMode = mode
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) ClientOrderID(clientOrderID string) *PlaceOrderRequest {
|
||||
r.clientOrderID = &clientOrderID
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Side(side SideType) *PlaceOrderRequest {
|
||||
r.side = side
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Quantity(quantity string) *PlaceOrderRequest {
|
||||
r.sz = quantity
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Price(price string) *PlaceOrderRequest {
|
||||
r.px = &price
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) OrderType(orderType OrderType) *PlaceOrderRequest {
|
||||
r.ordType = orderType
|
||||
return r
|
||||
price *string `param:"px"`
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Parameters() map[string]interface{} {
|
||||
payload := map[string]interface{}{}
|
||||
|
||||
payload["instId"] = r.instId
|
||||
|
||||
if r.tdMode == "" {
|
||||
payload["tdMode"] = "cash"
|
||||
} else {
|
||||
payload["tdMode"] = r.tdMode
|
||||
}
|
||||
|
||||
if r.clientOrderID != nil {
|
||||
payload["clOrdId"] = r.clientOrderID
|
||||
}
|
||||
|
||||
payload["side"] = r.side
|
||||
payload["ordType"] = r.ordType
|
||||
payload["sz"] = r.sz
|
||||
if r.px != nil {
|
||||
payload["px"] = r.px
|
||||
}
|
||||
|
||||
if r.tag != nil {
|
||||
payload["tag"] = r.tag
|
||||
}
|
||||
|
||||
return payload
|
||||
params, _ := r.GetParameters()
|
||||
return params
|
||||
}
|
||||
|
||||
func (r *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) {
|
||||
|
@ -184,47 +125,27 @@ func (r *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) {
|
|||
return &orderResponse.Data[0], nil
|
||||
}
|
||||
|
||||
//go:generate requestgen -type CancelOrderRequest
|
||||
type CancelOrderRequest struct {
|
||||
client *RestClient
|
||||
|
||||
instId string
|
||||
ordId *string
|
||||
clOrdId *string
|
||||
}
|
||||
|
||||
func (r *CancelOrderRequest) InstrumentID(instId string) *CancelOrderRequest {
|
||||
r.instId = instId
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CancelOrderRequest) OrderID(orderID string) *CancelOrderRequest {
|
||||
r.ordId = &orderID
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CancelOrderRequest) ClientOrderID(clientOrderID string) *CancelOrderRequest {
|
||||
r.clOrdId = &clientOrderID
|
||||
return r
|
||||
instrumentID string `param:"instId"`
|
||||
orderID *string `param:"ordId"`
|
||||
clientOrderID *string `param:"clOrdId"`
|
||||
}
|
||||
|
||||
func (r *CancelOrderRequest) Parameters() map[string]interface{} {
|
||||
var payload = map[string]interface{}{
|
||||
"instId": r.instId,
|
||||
}
|
||||
|
||||
if r.ordId != nil {
|
||||
payload["ordId"] = r.ordId
|
||||
} else if r.clOrdId != nil {
|
||||
payload["clOrdId"] = r.clOrdId
|
||||
}
|
||||
|
||||
payload, _ := r.GetParameters()
|
||||
return payload
|
||||
}
|
||||
|
||||
func (r *CancelOrderRequest) Do(ctx context.Context) ([]OrderResponse, error) {
|
||||
var payload = r.Parameters()
|
||||
payload, err := r.GetParameters()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.ordId == nil && r.clOrdId != nil {
|
||||
if r.clientOrderID == nil && r.orderID != nil {
|
||||
return nil, errors.New("either orderID or clientOrderID is required for canceling order")
|
||||
}
|
||||
|
||||
|
@ -369,7 +290,7 @@ type OrderDetails struct {
|
|||
Currency string `json:"ccy"`
|
||||
|
||||
// Leverage = from 0.01 to 125.
|
||||
//Only applicable to MARGIN/FUTURES/SWAP
|
||||
// Only applicable to MARGIN/FUTURES/SWAP
|
||||
Leverage fixedpoint.Value `json:"lever"`
|
||||
|
||||
RebateCurrency string `json:"rebateCcy"`
|
||||
|
|
|
@ -30,7 +30,7 @@ func init() {
|
|||
}
|
||||
|
||||
type State struct {
|
||||
Position *bbgo.Position `json:"position,omitempty"`
|
||||
Position *types.Position `json:"position,omitempty"`
|
||||
ProfitStats bbgo.ProfitStats `json:"profitStats,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -119,7 +119,7 @@ func (s *Strategy) LoadState() error {
|
|||
|
||||
// if position is nil, we need to allocate a new position for calculation
|
||||
if s.state.Position == nil {
|
||||
s.state.Position = bbgo.NewPositionFromMarket(s.market)
|
||||
s.state.Position = types.NewPositionFromMarket(s.market)
|
||||
}
|
||||
|
||||
// init profit states
|
||||
|
@ -297,7 +297,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
s.state.ProfitStats.AddTrade(trade)
|
||||
})
|
||||
|
||||
s.tradeCollector.OnPositionUpdate(func(position *bbgo.Position) {
|
||||
s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
|
||||
log.Infof("position changed: %s", s.state.Position)
|
||||
s.Notify(s.state.Position)
|
||||
})
|
||||
|
|
|
@ -31,7 +31,7 @@ type State struct {
|
|||
Orders []types.SubmitOrder `json:"orders,omitempty"`
|
||||
FilledBuyGrids map[fixedpoint.Value]struct{} `json:"filledBuyGrids"`
|
||||
FilledSellGrids map[fixedpoint.Value]struct{} `json:"filledSellGrids"`
|
||||
Position *bbgo.Position `json:"position,omitempty"`
|
||||
Position *types.Position `json:"position,omitempty"`
|
||||
|
||||
AccumulativeArbitrageProfit fixedpoint.Value `json:"accumulativeArbitrageProfit"`
|
||||
|
||||
|
@ -511,7 +511,7 @@ func (s *Strategy) LoadState() error {
|
|||
FilledBuyGrids: make(map[fixedpoint.Value]struct{}),
|
||||
FilledSellGrids: make(map[fixedpoint.Value]struct{}),
|
||||
ArbitrageOrders: make(map[uint64]types.Order),
|
||||
Position: bbgo.NewPositionFromMarket(s.Market),
|
||||
Position: types.NewPositionFromMarket(s.Market),
|
||||
}
|
||||
} else {
|
||||
s.state = &state
|
||||
|
@ -591,16 +591,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
})
|
||||
|
||||
/*
|
||||
if s.TradeService != nil {
|
||||
s.tradeCollector.OnTrade(func(trade types.Trade) {
|
||||
if err := s.TradeService.Mark(ctx, trade.ID, ID); err != nil {
|
||||
log.WithError(err).Error("trade mark error")
|
||||
}
|
||||
})
|
||||
}
|
||||
if s.TradeService != nil {
|
||||
s.tradeCollector.OnTrade(func(trade types.Trade) {
|
||||
if err := s.TradeService.Mark(ctx, trade.ID, ID); err != nil {
|
||||
log.WithError(err).Error("trade mark error")
|
||||
}
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
s.tradeCollector.OnPositionUpdate(func(position *bbgo.Position) {
|
||||
s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
|
||||
s.Notifiability.Notify(position)
|
||||
})
|
||||
s.tradeCollector.BindStream(session.UserDataStream)
|
||||
|
|
|
@ -27,7 +27,7 @@ func init() {
|
|||
}
|
||||
|
||||
type State struct {
|
||||
Position *bbgo.Position `json:"position,omitempty"`
|
||||
Position *types.Position `json:"position,omitempty"`
|
||||
}
|
||||
|
||||
type Target struct {
|
||||
|
@ -42,7 +42,7 @@ type PercentageTargetStop struct {
|
|||
}
|
||||
|
||||
// GenerateOrders generates the orders from the given targets
|
||||
func (stop *PercentageTargetStop) GenerateOrders(market types.Market, pos *bbgo.Position) []types.SubmitOrder {
|
||||
func (stop *PercentageTargetStop) GenerateOrders(market types.Market, pos *types.Position) []types.SubmitOrder {
|
||||
var price = pos.AverageCost
|
||||
var quantity = pos.Base
|
||||
|
||||
|
@ -62,12 +62,12 @@ func (stop *PercentageTargetStop) GenerateOrders(market types.Market, pos *bbgo.
|
|||
}
|
||||
|
||||
targetOrders = append(targetOrders, types.SubmitOrder{
|
||||
Symbol: market.Symbol,
|
||||
Market: market,
|
||||
Type: types.OrderTypeLimit,
|
||||
Side: types.SideTypeSell,
|
||||
Price: targetPrice,
|
||||
Quantity: targetQuantity,
|
||||
Symbol: market.Symbol,
|
||||
Market: market,
|
||||
Type: types.OrderTypeLimit,
|
||||
Side: types.SideTypeSell,
|
||||
Price: targetPrice,
|
||||
Quantity: targetQuantity,
|
||||
MarginSideEffect: target.MarginOrderSideEffect,
|
||||
TimeInForce: "GTC",
|
||||
})
|
||||
|
@ -178,7 +178,7 @@ func (s *Strategy) LoadState() error {
|
|||
}
|
||||
|
||||
if s.state.Position == nil {
|
||||
s.state.Position = bbgo.NewPositionFromMarket(s.Market)
|
||||
s.state.Position = types.NewPositionFromMarket(s.Market)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -41,7 +41,7 @@ func init() {
|
|||
type State struct {
|
||||
HedgePosition fixedpoint.Value `json:"hedgePosition"`
|
||||
CoveredPosition fixedpoint.Value `json:"coveredPosition,omitempty"`
|
||||
Position *bbgo.Position `json:"position,omitempty"`
|
||||
Position *types.Position `json:"position,omitempty"`
|
||||
ProfitStats ProfitStats `json:"profitStats,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -680,7 +680,7 @@ func (s *Strategy) LoadState() error {
|
|||
|
||||
// if position is nil, we need to allocate a new position for calculation
|
||||
if s.state.Position == nil {
|
||||
s.state.Position = bbgo.NewPositionFromMarket(s.makerMarket)
|
||||
s.state.Position = types.NewPositionFromMarket(s.makerMarket)
|
||||
}
|
||||
|
||||
s.state.ProfitStats.Symbol = s.makerMarket.Symbol
|
||||
|
@ -794,14 +794,14 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
|
|||
}
|
||||
|
||||
if s.makerSession.MakerFeeRate > 0 || s.makerSession.TakerFeeRate > 0 {
|
||||
s.state.Position.SetExchangeFeeRate(types.ExchangeName(s.MakerExchange), bbgo.ExchangeFee{
|
||||
s.state.Position.SetExchangeFeeRate(types.ExchangeName(s.MakerExchange), types.ExchangeFee{
|
||||
MakerFeeRate: s.makerSession.MakerFeeRate,
|
||||
TakerFeeRate: s.makerSession.TakerFeeRate,
|
||||
})
|
||||
}
|
||||
|
||||
if s.sourceSession.MakerFeeRate > 0 || s.sourceSession.TakerFeeRate > 0 {
|
||||
s.state.Position.SetExchangeFeeRate(types.ExchangeName(s.SourceExchange), bbgo.ExchangeFee{
|
||||
s.state.Position.SetExchangeFeeRate(types.ExchangeName(s.SourceExchange), types.ExchangeFee{
|
||||
MakerFeeRate: s.sourceSession.MakerFeeRate,
|
||||
TakerFeeRate: s.sourceSession.TakerFeeRate,
|
||||
})
|
||||
|
|
|
@ -109,6 +109,7 @@ func (m AssetMap) SlackAttachment() slack.Attachment {
|
|||
}
|
||||
|
||||
type BalanceMap map[string]Balance
|
||||
type PositionMap map[string]Position
|
||||
|
||||
func (m BalanceMap) String() string {
|
||||
var ss []string
|
||||
|
|
|
@ -4,11 +4,12 @@ import "github.com/c9s/bbgo/pkg/fixedpoint"
|
|||
|
||||
type FuturesExchange interface {
|
||||
UseFutures()
|
||||
UseIsolatedFutures(symbol string)
|
||||
GetFuturesSettings() FuturesSettings
|
||||
}
|
||||
|
||||
type FuturesSettings struct {
|
||||
IsFutures bool
|
||||
IsFutures bool
|
||||
IsIsolatedFutures bool
|
||||
IsolatedFuturesSymbol string
|
||||
}
|
||||
|
@ -25,10 +26,8 @@ func (s *FuturesSettings) UseIsolatedFutures(symbol string) {
|
|||
s.IsFutures = true
|
||||
s.IsIsolatedFutures = true
|
||||
s.IsolatedFuturesSymbol = symbol
|
||||
|
||||
}
|
||||
|
||||
|
||||
type MarginExchange interface {
|
||||
UseMargin()
|
||||
UseIsolatedMargin(symbol string)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package bbgo
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -6,7 +6,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
"github.com/slack-go/slack"
|
||||
)
|
||||
|
@ -21,7 +20,7 @@ type Position struct {
|
|||
BaseCurrency string `json:"baseCurrency"`
|
||||
QuoteCurrency string `json:"quoteCurrency"`
|
||||
|
||||
Market types.Market `json:"market"`
|
||||
Market Market `json:"market"`
|
||||
|
||||
Base fixedpoint.Value `json:"base"`
|
||||
Quote fixedpoint.Value `json:"quote"`
|
||||
|
@ -31,13 +30,29 @@ type Position struct {
|
|||
// This is used for calculating net profit
|
||||
ApproximateAverageCost fixedpoint.Value `json:"approximateAverageCost"`
|
||||
|
||||
FeeRate *ExchangeFee `json:"feeRate,omitempty"`
|
||||
ExchangeFeeRates map[types.ExchangeName]ExchangeFee `json:"exchangeFeeRates"`
|
||||
FeeRate *ExchangeFee `json:"feeRate,omitempty"`
|
||||
ExchangeFeeRates map[ExchangeName]ExchangeFee `json:"exchangeFeeRates"`
|
||||
|
||||
// Futures data fields
|
||||
Isolated bool `json:"isolated"`
|
||||
Leverage fixedpoint.Value `json:"leverage"`
|
||||
InitialMargin fixedpoint.Value `json:"initialMargin"`
|
||||
MaintMargin fixedpoint.Value `json:"maintMargin"`
|
||||
OpenOrderInitialMargin fixedpoint.Value `json:"openOrderInitialMargin"`
|
||||
PositionInitialMargin fixedpoint.Value `json:"positionInitialMargin"`
|
||||
UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"`
|
||||
EntryPrice fixedpoint.Value `json:"entryPrice"`
|
||||
MaxNotional fixedpoint.Value `json:"maxNotional"`
|
||||
PositionSide string `json:"positionSide"`
|
||||
PositionAmt fixedpoint.Value `json:"positionAmt"`
|
||||
Notional fixedpoint.Value `json:"notional"`
|
||||
IsolatedWallet fixedpoint.Value `json:"isolatedWallet"`
|
||||
UpdateTime int64 `json:"updateTime"`
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func NewPositionFromMarket(market types.Market) *Position {
|
||||
func NewPositionFromMarket(market Market) *Position {
|
||||
return &Position{
|
||||
Symbol: market.Symbol,
|
||||
BaseCurrency: market.BaseCurrency,
|
||||
|
@ -64,9 +79,9 @@ func (p *Position) SetFeeRate(exchangeFee ExchangeFee) {
|
|||
p.FeeRate = &exchangeFee
|
||||
}
|
||||
|
||||
func (p *Position) SetExchangeFeeRate(ex types.ExchangeName, exchangeFee ExchangeFee) {
|
||||
func (p *Position) SetExchangeFeeRate(ex ExchangeName, exchangeFee ExchangeFee) {
|
||||
if p.ExchangeFeeRates == nil {
|
||||
p.ExchangeFeeRates = make(map[types.ExchangeName]ExchangeFee)
|
||||
p.ExchangeFeeRates = make(map[ExchangeName]ExchangeFee)
|
||||
}
|
||||
|
||||
p.ExchangeFeeRates[ex] = exchangeFee
|
||||
|
@ -127,15 +142,15 @@ func (p *Position) String() string {
|
|||
)
|
||||
}
|
||||
|
||||
func (p *Position) BindStream(stream types.Stream) {
|
||||
stream.OnTradeUpdate(func(trade types.Trade) {
|
||||
func (p *Position) BindStream(stream Stream) {
|
||||
stream.OnTradeUpdate(func(trade Trade) {
|
||||
if p.Symbol == trade.Symbol {
|
||||
p.AddTrade(trade)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Position) AddTrades(trades []types.Trade) (fixedpoint.Value, fixedpoint.Value, bool) {
|
||||
func (p *Position) AddTrades(trades []Trade) (fixedpoint.Value, fixedpoint.Value, bool) {
|
||||
var totalProfitAmount, totalNetProfit fixedpoint.Value
|
||||
for _, trade := range trades {
|
||||
if profit, netProfit, madeProfit := p.AddTrade(trade); madeProfit {
|
||||
|
@ -147,7 +162,7 @@ func (p *Position) AddTrades(trades []types.Trade) (fixedpoint.Value, fixedpoint
|
|||
return totalProfitAmount, totalNetProfit, totalProfitAmount != 0
|
||||
}
|
||||
|
||||
func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) {
|
||||
func (p *Position) AddTrade(t Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) {
|
||||
price := fixedpoint.NewFromFloat(t.Price)
|
||||
quantity := fixedpoint.NewFromFloat(t.Quantity)
|
||||
quoteQuantity := fixedpoint.NewFromFloat(t.QuoteQuantity)
|
||||
|
@ -189,7 +204,7 @@ func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit f
|
|||
// Base < 0 means we're in short position
|
||||
switch t.Side {
|
||||
|
||||
case types.SideTypeBuy:
|
||||
case SideTypeBuy:
|
||||
if p.Base < 0 {
|
||||
// convert short position to long position
|
||||
if p.Base+quantity > 0 {
|
||||
|
@ -217,7 +232,7 @@ func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit f
|
|||
|
||||
return 0, 0, false
|
||||
|
||||
case types.SideTypeSell:
|
||||
case SideTypeSell:
|
||||
if p.Base > 0 {
|
||||
// convert long position to short position
|
||||
if p.Base-quantity < 0 {
|
|
@ -1,4 +1,4 @@
|
|||
package bbgo
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func TestPosition_ExchangeFeeRate_Short(t *testing.T) {
|
||||
|
@ -17,7 +16,7 @@ func TestPosition_ExchangeFeeRate_Short(t *testing.T) {
|
|||
}
|
||||
|
||||
feeRate := 0.075 * 0.01
|
||||
pos.SetExchangeFeeRate(types.ExchangeBinance, ExchangeFee{
|
||||
pos.SetExchangeFeeRate(ExchangeBinance, ExchangeFee{
|
||||
MakerFeeRate: fixedpoint.NewFromFloat(feeRate),
|
||||
TakerFeeRate: fixedpoint.NewFromFloat(feeRate),
|
||||
})
|
||||
|
@ -27,24 +26,24 @@ func TestPosition_ExchangeFeeRate_Short(t *testing.T) {
|
|||
fee := quoteQuantity * feeRate
|
||||
averageCost := (quoteQuantity - fee) / quantity
|
||||
bnbPrice := 570.0
|
||||
pos.AddTrade(types.Trade{
|
||||
Exchange: types.ExchangeBinance,
|
||||
pos.AddTrade(Trade{
|
||||
Exchange: ExchangeBinance,
|
||||
Price: 3000.0,
|
||||
Quantity: quantity,
|
||||
QuoteQuantity: quoteQuantity,
|
||||
Symbol: "BTCUSDT",
|
||||
Side: types.SideTypeSell,
|
||||
Side: SideTypeSell,
|
||||
Fee: fee / bnbPrice,
|
||||
FeeCurrency: "BNB",
|
||||
})
|
||||
|
||||
_, netProfit, madeProfit := pos.AddTrade(types.Trade{
|
||||
Exchange: types.ExchangeBinance,
|
||||
_, netProfit, madeProfit := pos.AddTrade(Trade{
|
||||
Exchange: ExchangeBinance,
|
||||
Price: 2000.0,
|
||||
Quantity: 10.0,
|
||||
QuoteQuantity: 2000.0 * 10.0,
|
||||
Symbol: "BTCUSDT",
|
||||
Side: types.SideTypeBuy,
|
||||
Side: SideTypeBuy,
|
||||
Fee: 2000.0 * 10.0 * feeRate / bnbPrice,
|
||||
FeeCurrency: "BNB",
|
||||
})
|
||||
|
@ -62,7 +61,7 @@ func TestPosition_ExchangeFeeRate_Long(t *testing.T) {
|
|||
}
|
||||
|
||||
feeRate := 0.075 * 0.01
|
||||
pos.SetExchangeFeeRate(types.ExchangeBinance, ExchangeFee{
|
||||
pos.SetExchangeFeeRate(ExchangeBinance, ExchangeFee{
|
||||
MakerFeeRate: fixedpoint.NewFromFloat(feeRate),
|
||||
TakerFeeRate: fixedpoint.NewFromFloat(feeRate),
|
||||
})
|
||||
|
@ -72,24 +71,24 @@ func TestPosition_ExchangeFeeRate_Long(t *testing.T) {
|
|||
fee := quoteQuantity * feeRate
|
||||
averageCost := (quoteQuantity + fee) / quantity
|
||||
bnbPrice := 570.0
|
||||
pos.AddTrade(types.Trade{
|
||||
Exchange: types.ExchangeBinance,
|
||||
pos.AddTrade(Trade{
|
||||
Exchange: ExchangeBinance,
|
||||
Price: 3000.0,
|
||||
Quantity: quantity,
|
||||
QuoteQuantity: quoteQuantity,
|
||||
Symbol: "BTCUSDT",
|
||||
Side: types.SideTypeBuy,
|
||||
Side: SideTypeBuy,
|
||||
Fee: fee / bnbPrice,
|
||||
FeeCurrency: "BNB",
|
||||
})
|
||||
|
||||
_, netProfit, madeProfit := pos.AddTrade(types.Trade{
|
||||
Exchange: types.ExchangeBinance,
|
||||
_, netProfit, madeProfit := pos.AddTrade(Trade{
|
||||
Exchange: ExchangeBinance,
|
||||
Price: 4000.0,
|
||||
Quantity: 10.0,
|
||||
QuoteQuantity: 4000.0 * 10.0,
|
||||
Symbol: "BTCUSDT",
|
||||
Side: types.SideTypeSell,
|
||||
Side: SideTypeSell,
|
||||
Fee: 4000.0 * 10.0 * feeRate / bnbPrice,
|
||||
FeeCurrency: "BNB",
|
||||
})
|
||||
|
@ -103,7 +102,7 @@ func TestPosition(t *testing.T) {
|
|||
var feeRate = 0.05 * 0.01
|
||||
var testcases = []struct {
|
||||
name string
|
||||
trades []types.Trade
|
||||
trades []Trade
|
||||
expectedAverageCost fixedpoint.Value
|
||||
expectedBase fixedpoint.Value
|
||||
expectedQuote fixedpoint.Value
|
||||
|
@ -111,9 +110,9 @@ func TestPosition(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
name: "base fee",
|
||||
trades: []types.Trade{
|
||||
trades: []Trade{
|
||||
{
|
||||
Side: types.SideTypeBuy,
|
||||
Side: SideTypeBuy,
|
||||
Price: 1000.0,
|
||||
Quantity: 0.01,
|
||||
QuoteQuantity: 1000.0 * 0.01,
|
||||
|
@ -128,9 +127,9 @@ func TestPosition(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "quote fee",
|
||||
trades: []types.Trade{
|
||||
trades: []Trade{
|
||||
{
|
||||
Side: types.SideTypeSell,
|
||||
Side: SideTypeSell,
|
||||
Price: 1000.0,
|
||||
Quantity: 0.01,
|
||||
QuoteQuantity: 1000.0 * 0.01,
|
||||
|
@ -145,15 +144,15 @@ func TestPosition(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "long",
|
||||
trades: []types.Trade{
|
||||
trades: []Trade{
|
||||
{
|
||||
Side: types.SideTypeBuy,
|
||||
Side: SideTypeBuy,
|
||||
Price: 1000.0,
|
||||
Quantity: 0.01,
|
||||
QuoteQuantity: 1000.0 * 0.01,
|
||||
},
|
||||
{
|
||||
Side: types.SideTypeBuy,
|
||||
Side: SideTypeBuy,
|
||||
Price: 2000.0,
|
||||
Quantity: 0.03,
|
||||
QuoteQuantity: 2000.0 * 0.03,
|
||||
|
@ -167,21 +166,21 @@ func TestPosition(t *testing.T) {
|
|||
|
||||
{
|
||||
name: "long and sell",
|
||||
trades: []types.Trade{
|
||||
trades: []Trade{
|
||||
{
|
||||
Side: types.SideTypeBuy,
|
||||
Side: SideTypeBuy,
|
||||
Price: 1000.0,
|
||||
Quantity: 0.01,
|
||||
QuoteQuantity: 1000.0 * 0.01,
|
||||
},
|
||||
{
|
||||
Side: types.SideTypeBuy,
|
||||
Side: SideTypeBuy,
|
||||
Price: 2000.0,
|
||||
Quantity: 0.03,
|
||||
QuoteQuantity: 2000.0 * 0.03,
|
||||
},
|
||||
{
|
||||
Side: types.SideTypeSell,
|
||||
Side: SideTypeSell,
|
||||
Price: 3000.0,
|
||||
Quantity: 0.01,
|
||||
QuoteQuantity: 3000.0 * 0.01,
|
||||
|
@ -195,21 +194,21 @@ func TestPosition(t *testing.T) {
|
|||
|
||||
{
|
||||
name: "long and sell to short",
|
||||
trades: []types.Trade{
|
||||
trades: []Trade{
|
||||
{
|
||||
Side: types.SideTypeBuy,
|
||||
Side: SideTypeBuy,
|
||||
Price: 1000.0,
|
||||
Quantity: 0.01,
|
||||
QuoteQuantity: 1000.0 * 0.01,
|
||||
},
|
||||
{
|
||||
Side: types.SideTypeBuy,
|
||||
Side: SideTypeBuy,
|
||||
Price: 2000.0,
|
||||
Quantity: 0.03,
|
||||
QuoteQuantity: 2000.0 * 0.03,
|
||||
},
|
||||
{
|
||||
Side: types.SideTypeSell,
|
||||
Side: SideTypeSell,
|
||||
Price: 3000.0,
|
||||
Quantity: 0.10,
|
||||
QuoteQuantity: 3000.0 * 0.10,
|
||||
|
@ -224,15 +223,15 @@ func TestPosition(t *testing.T) {
|
|||
|
||||
{
|
||||
name: "short",
|
||||
trades: []types.Trade{
|
||||
trades: []Trade{
|
||||
{
|
||||
Side: types.SideTypeSell,
|
||||
Side: SideTypeSell,
|
||||
Price: 2000.0,
|
||||
Quantity: 0.01,
|
||||
QuoteQuantity: 2000.0 * 0.01,
|
||||
},
|
||||
{
|
||||
Side: types.SideTypeSell,
|
||||
Side: SideTypeSell,
|
||||
Price: 3000.0,
|
||||
Quantity: 0.03,
|
||||
QuoteQuantity: 3000.0 * 0.03,
|
|
@ -114,6 +114,26 @@ func (stream *StandardStream) EmitBookSnapshot(book SliceOrderBook) {
|
|||
}
|
||||
}
|
||||
|
||||
func (stream *StandardStream) OnPositionUpdate(cb func(position PositionMap)) {
|
||||
stream.PositionUpdateCallbacks = append(stream.PositionUpdateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (stream *StandardStream) EmitPositionUpdate(position PositionMap) {
|
||||
for _, cb := range stream.PositionUpdateCallbacks {
|
||||
cb(position)
|
||||
}
|
||||
}
|
||||
|
||||
func (stream *StandardStream) OnPositionSnapshot(cb func(position PositionMap)) {
|
||||
stream.PositionSnapshotCallbacks = append(stream.PositionSnapshotCallbacks, cb)
|
||||
}
|
||||
|
||||
func (stream *StandardStream) EmitPositionSnapshot(position PositionMap) {
|
||||
for _, cb := range stream.PositionSnapshotCallbacks {
|
||||
cb(position)
|
||||
}
|
||||
}
|
||||
|
||||
type StandardStreamEventHub interface {
|
||||
OnStart(cb func())
|
||||
|
||||
|
@ -136,4 +156,8 @@ type StandardStreamEventHub interface {
|
|||
OnBookUpdate(cb func(book SliceOrderBook))
|
||||
|
||||
OnBookSnapshot(cb func(book SliceOrderBook))
|
||||
|
||||
OnPositionUpdate(cb func(position PositionMap))
|
||||
|
||||
OnPositionSnapshot(cb func(position PositionMap))
|
||||
}
|
||||
|
|
|
@ -51,6 +51,11 @@ type StandardStream struct {
|
|||
bookUpdateCallbacks []func(book SliceOrderBook)
|
||||
|
||||
bookSnapshotCallbacks []func(book SliceOrderBook)
|
||||
|
||||
// Futures
|
||||
PositionUpdateCallbacks []func(position PositionMap)
|
||||
|
||||
PositionSnapshotCallbacks []func(position PositionMap)
|
||||
}
|
||||
|
||||
func (stream *StandardStream) Subscribe(channel Channel, symbol string, options SubscribeOptions) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user