Merge branch 'main' of github.com:c9s/bbgo into feature/302-record-assets-review

This commit is contained in:
TonyQ 2021-12-14 10:39:51 +08:00
commit 51e23b6a0c
48 changed files with 3079 additions and 286 deletions

1
.gitignore vendored
View File

@ -22,6 +22,7 @@
/.env.local
/.env.*.local
/.env.production
.DS_Store

View File

@ -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

View File

@ -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

View File

@ -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`

View 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
```

View 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")
}
}

View 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")
}
}

View 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
View 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")
}
}

View 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
View 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
},
}

View 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
},
}

View 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
},
}

View 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
},
}

View File

@ -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),

View File

@ -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{

View File

@ -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),

View File

@ -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)
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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"`

View File

@ -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)
}

View File

@ -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))
}

View File

@ -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 {

View File

@ -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)
}
}
}

View 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,
}
}

View 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
}

View File

@ -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)
}

View File

@ -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)
}

View 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(&currencyResponse); 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))
}

View 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)
}

View 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 volumethe 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"]], //[pricesize]
"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
}

View 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)
}

View 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
}

View 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)
}

View 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)
}

View File

@ -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"`

View File

@ -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)
})

View File

@ -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)

View File

@ -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

View File

@ -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,
})

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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,

View File

@ -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))
}

View File

@ -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) {