mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #27 from c9s/feature/maker-strategy
strategy: add exponential quantity maker strategy
This commit is contained in:
commit
27aa65aa13
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
imports:
|
||||
- github.com/c9s/bbgo/pkg/strategy/buyandhold
|
||||
- github.com/c9s/bbgo/pkg/strategy/xpuremaker
|
||||
|
||||
notifications:
|
||||
slack:
|
||||
defaultChannel: "bbgo"
|
||||
|
@ -39,3 +41,11 @@ exchangeStrategies:
|
|||
interval: "1m"
|
||||
baseQuantity: 0.01
|
||||
minDropPercentage: -0.02
|
||||
- on: max
|
||||
xpuremaker:
|
||||
symbol: MAXUSDT
|
||||
numOrders: 2
|
||||
side: both
|
||||
behindVolume: 1000.0
|
||||
priceTick: 0.01
|
||||
baseQuantity: 100.0
|
||||
|
|
42
config/xpuremaker.yaml
Normal file
42
config/xpuremaker.yaml
Normal file
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
notifications:
|
||||
slack:
|
||||
defaultChannel: "bbgo"
|
||||
errorChannel: "bbgo-error"
|
||||
|
||||
reportTrades:
|
||||
channelBySymbol:
|
||||
"btcusdt": "bbgo-btcusdt"
|
||||
"ethusdt": "bbgo-ethusdt"
|
||||
"bnbusdt": "bbgo-bnbusdt"
|
||||
"sxpusdt": "bbgo-sxpusdt"
|
||||
"maxusdt": "max-maxusdt"
|
||||
|
||||
reportPnL:
|
||||
- averageCostBySymbols:
|
||||
- "BTCUSDT"
|
||||
- "BNBUSDT"
|
||||
of: binance
|
||||
when:
|
||||
- "@daily"
|
||||
- "@hourly"
|
||||
|
||||
sessions:
|
||||
max:
|
||||
exchange: max
|
||||
keyVar: MAX_API_KEY
|
||||
secretVar: MAX_API_SECRET
|
||||
binance:
|
||||
exchange: binance
|
||||
keyVar: BINANCE_API_KEY
|
||||
secretVar: BINANCE_API_SECRET
|
||||
|
||||
exchangeStrategies:
|
||||
- on: max
|
||||
xpuremaker:
|
||||
symbol: MAXUSDT
|
||||
numOrders: 2
|
||||
side: both
|
||||
behindVolume: 1000.0
|
||||
priceTick: 0.001
|
||||
baseQuantity: 100.0
|
|
@ -216,7 +216,7 @@ func generateOrders(symbol, side string, price, priceTick, baseVolume fixedpoint
|
|||
|
||||
orders = append(orders, maxapi.Order{
|
||||
Side: side,
|
||||
OrderType: string(maxapi.OrderTypeLimit),
|
||||
OrderType: maxapi.OrderTypeLimit,
|
||||
Market: symbol,
|
||||
Price: util.FormatFloat(price.Float64(), 3),
|
||||
Volume: util.FormatFloat(volume, 2),
|
||||
|
|
2
go.mod
2
go.mod
|
@ -4,7 +4,7 @@ go 1.13
|
|||
|
||||
require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
github.com/adshao/go-binance v0.0.0-20200604145522-bf563a35f17f
|
||||
github.com/adshao/go-binance v0.0.0-20201015231210-37cee298310e
|
||||
github.com/c9s/goose v0.0.0-20200415105707-8da682162a5b
|
||||
github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.7
|
||||
|
|
2
go.sum
2
go.sum
|
@ -19,6 +19,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q
|
|||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/adshao/go-binance v0.0.0-20200604145522-bf563a35f17f h1:lVxx5HSt/imprfR8v577N3gCQmKmRgkGNz30FlHISO4=
|
||||
github.com/adshao/go-binance v0.0.0-20200604145522-bf563a35f17f/go.mod h1:XlIpE7brbCEQxp6VRouG/ZgjLjygQWE1xnc1DtQNp6I=
|
||||
github.com/adshao/go-binance v0.0.0-20201015231210-37cee298310e h1:+fOwsQnvjCIVXuiVbyfuzNbubHvUrx1saeRa9pd7Df8=
|
||||
github.com/adshao/go-binance v0.0.0-20201015231210-37cee298310e/go.mod h1:XlIpE7brbCEQxp6VRouG/ZgjLjygQWE1xnc1DtQNp6I=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
|
||||
|
|
|
@ -73,7 +73,7 @@ func (reporter *TradeReporter) Report(trade types.Trade) {
|
|||
|
||||
var text = util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade)
|
||||
if err := reporter.notifier.NotifyTo(channel, text, trade); err != nil {
|
||||
log.WithError(err).Error("notifier error")
|
||||
log.WithError(err).Errorf("notifier error, channel=%s", channel)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
package bbgo
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
package bbgo
|
||||
|
69
pkg/bbgo/order_execution.go
Normal file
69
pkg/bbgo/order_execution.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type ExchangeOrderExecutionRouter struct {
|
||||
Notifiability
|
||||
|
||||
sessions map[string]*ExchangeSession
|
||||
}
|
||||
|
||||
func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, session string, orders ...types.SubmitOrder) ([]types.Order, error) {
|
||||
es, ok := e.sessions[session]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("exchange session %s not found", session)
|
||||
}
|
||||
|
||||
var formattedOrders []types.SubmitOrder
|
||||
for _, order := range orders {
|
||||
market, ok := es.Market(order.Symbol)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("market is not defined: %s", order.Symbol)
|
||||
}
|
||||
|
||||
order.Market = market
|
||||
order.PriceString = market.FormatPrice(order.Price)
|
||||
order.QuantityString = market.FormatVolume(order.Quantity)
|
||||
formattedOrders = append(formattedOrders, order)
|
||||
}
|
||||
|
||||
// e.Notify(":memo: Submitting order to %s %s %s %s with quantity: %s", session, order.Symbol, order.Type, order.Side, order.QuantityString, order)
|
||||
|
||||
return es.Exchange.SubmitOrders(ctx, formattedOrders...)
|
||||
}
|
||||
|
||||
// ExchangeOrderExecutor is an order executor wrapper for single exchange instance.
|
||||
type ExchangeOrderExecutor struct {
|
||||
Notifiability
|
||||
|
||||
session *ExchangeSession
|
||||
}
|
||||
|
||||
func (e *ExchangeOrderExecutor) Session() *ExchangeSession {
|
||||
return e.session
|
||||
}
|
||||
|
||||
func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) ([]types.Order, error) {
|
||||
var formattedOrders []types.SubmitOrder
|
||||
for _, order := range orders {
|
||||
market, ok := e.session.Market(order.Symbol)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("market is not defined: %s", order.Symbol)
|
||||
}
|
||||
|
||||
order.Market = market
|
||||
order.PriceString = market.FormatPrice(order.Price)
|
||||
order.QuantityString = market.FormatVolume(order.Quantity)
|
||||
formattedOrders = append(formattedOrders, order)
|
||||
|
||||
// e.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order)
|
||||
}
|
||||
|
||||
return e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
|
||||
}
|
|
@ -126,7 +126,9 @@ func (p *OrderProcessor) Submit(ctx context.Context, order types.SubmitOrder) er
|
|||
order.QuantityString = market.FormatVolume(quantity)
|
||||
*/
|
||||
|
||||
return p.Exchange.SubmitOrder(ctx, order)
|
||||
createdOrders, err := p.Exchange.SubmitOrders(ctx, order)
|
||||
_ = createdOrders
|
||||
return err
|
||||
}
|
||||
|
||||
func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount float64) float64 {
|
||||
|
|
|
@ -3,7 +3,6 @@ package bbgo
|
|||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/robfig/cron/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
|
@ -26,11 +25,11 @@ func PersistentFlags(flags *flag.FlagSet) {
|
|||
|
||||
// SingleExchangeStrategy represents the single Exchange strategy
|
||||
type SingleExchangeStrategy interface {
|
||||
Run(ctx context.Context, orderExecutor types.OrderExecutor, session *ExchangeSession) error
|
||||
Run(ctx context.Context, orderExecutor OrderExecutor, session *ExchangeSession) error
|
||||
}
|
||||
|
||||
type CrossExchangeStrategy interface {
|
||||
Run(ctx context.Context, orderExecutionRouter types.OrderExecutionRouter, sessions map[string]*ExchangeSession) error
|
||||
Run(ctx context.Context, orderExecutionRouter OrderExecutionRouter, sessions map[string]*ExchangeSession) error
|
||||
}
|
||||
|
||||
type PnLReporter interface {
|
||||
|
@ -81,7 +80,7 @@ func (reporter *AverageCostPnLReporter) Of(sessions ...string) *AverageCostPnLRe
|
|||
}
|
||||
|
||||
func (reporter *AverageCostPnLReporter) When(specs ...string) *AverageCostPnLReporter {
|
||||
for _,spec := range specs {
|
||||
for _, spec := range specs {
|
||||
_, err := reporter.cron.AddJob(spec, reporter)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -152,15 +151,16 @@ func (trader *Trader) Run(ctx context.Context) error {
|
|||
|
||||
// load and run session strategies
|
||||
for sessionName, strategies := range trader.exchangeStrategies {
|
||||
session := trader.environment.sessions[sessionName]
|
||||
// we can move this to the exchange session,
|
||||
// that way we can mount the notification on the exchange with DSL
|
||||
orderExecutor := &ExchangeOrderExecutor{
|
||||
Notifiability: trader.Notifiability,
|
||||
Exchange: nil,
|
||||
session: session,
|
||||
}
|
||||
|
||||
for _, strategy := range strategies {
|
||||
err := strategy.Run(ctx, orderExecutor, trader.environment.sessions[sessionName])
|
||||
err := strategy.Run(ctx, orderExecutor, session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -319,36 +319,12 @@ func (trader *Trader) SubmitOrder(ctx context.Context, order types.SubmitOrder)
|
|||
}
|
||||
}
|
||||
|
||||
type ExchangeOrderExecutionRouter struct {
|
||||
Notifiability
|
||||
|
||||
sessions map[string]*ExchangeSession
|
||||
type OrderExecutor interface {
|
||||
Session() *ExchangeSession
|
||||
SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders []types.Order, err error)
|
||||
}
|
||||
|
||||
func (e *ExchangeOrderExecutionRouter) SubmitOrderTo(ctx context.Context, session string, order types.SubmitOrder) error {
|
||||
es, ok := e.sessions[session]
|
||||
if !ok {
|
||||
return errors.Errorf("exchange session %s not found", session)
|
||||
}
|
||||
|
||||
e.Notify(":memo: Submitting order to %s %s %s %s with quantity: %s", session, order.Symbol, order.Type, order.Side, order.QuantityString, order)
|
||||
|
||||
order.PriceString = order.Market.FormatVolume(order.Price)
|
||||
order.QuantityString = order.Market.FormatVolume(order.Quantity)
|
||||
return es.Exchange.SubmitOrder(ctx, order)
|
||||
}
|
||||
|
||||
// ExchangeOrderExecutor is an order executor wrapper for single exchange instance.
|
||||
type ExchangeOrderExecutor struct {
|
||||
Notifiability
|
||||
|
||||
Exchange types.Exchange
|
||||
}
|
||||
|
||||
func (e *ExchangeOrderExecutor) SubmitOrder(ctx context.Context, order types.SubmitOrder) error {
|
||||
e.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order)
|
||||
|
||||
order.PriceString = order.Market.FormatVolume(order.Price)
|
||||
order.QuantityString = order.Market.FormatVolume(order.Quantity)
|
||||
return e.Exchange.SubmitOrder(ctx, order)
|
||||
type OrderExecutionRouter interface {
|
||||
// SubmitOrderTo submit order to a specific exchange session
|
||||
SubmitOrdersTo(ctx context.Context, session string, orders ...types.SubmitOrder) (createdOrders []types.Order, err error)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
// import built-in strategies
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/xpuremaker"
|
||||
)
|
||||
|
||||
var errSlackTokenUndefined = errors.New("slack token is not defined.")
|
||||
|
|
151
pkg/exchange/binance/convert.go
Normal file
151
pkg/exchange/binance/convert.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package binance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/adshao/go-binance"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
)
|
||||
|
||||
func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) {
|
||||
switch orderType {
|
||||
case types.OrderTypeLimit:
|
||||
return binance.OrderTypeLimit, nil
|
||||
|
||||
case types.OrderTypeStopLimit:
|
||||
return binance.OrderTypeStopLossLimit, nil
|
||||
|
||||
case types.OrderTypeStopMarket:
|
||||
return binance.OrderTypeStopLoss, nil
|
||||
|
||||
case types.OrderTypeMarket:
|
||||
return binance.OrderTypeMarket, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("order type %s not supported", orderType)
|
||||
}
|
||||
|
||||
func toGlobalOrder(binanceOrder *binance.Order) (*types.Order, error) {
|
||||
return &types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
ClientOrderID: binanceOrder.ClientOrderID,
|
||||
Symbol: binanceOrder.Symbol,
|
||||
Side: toGlobalSideType(binanceOrder.Side),
|
||||
Type: toGlobalOrderType(binanceOrder.Type),
|
||||
Quantity: util.MustParseFloat(binanceOrder.OrigQuantity),
|
||||
Price: util.MustParseFloat(binanceOrder.Price),
|
||||
TimeInForce: string(binanceOrder.TimeInForce),
|
||||
},
|
||||
OrderID: uint64(binanceOrder.OrderID),
|
||||
Status: toGlobalOrderStatus(binanceOrder.Status),
|
||||
ExecutedQuantity: util.MustParseFloat(binanceOrder.ExecutedQuantity),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGlobalTrade(t binance.TradeV3) (*types.Trade, error) {
|
||||
// skip trade ID that is the same. however this should not happen
|
||||
var side types.SideType
|
||||
if t.IsBuyer {
|
||||
side = types.SideTypeBuy
|
||||
} else {
|
||||
side = types.SideTypeSell
|
||||
}
|
||||
|
||||
// trade time
|
||||
mts := time.Unix(0, t.Time*int64(time.Millisecond))
|
||||
|
||||
price, err := strconv.ParseFloat(t.Price, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quantity, err := strconv.ParseFloat(t.Quantity, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quoteQuantity, err := strconv.ParseFloat(t.QuoteQuantity, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fee, err := strconv.ParseFloat(t.Commission, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.Trade{
|
||||
ID: t.ID,
|
||||
Price: price,
|
||||
Symbol: t.Symbol,
|
||||
Exchange: "binance",
|
||||
Quantity: quantity,
|
||||
Side: side,
|
||||
IsBuyer: t.IsBuyer,
|
||||
IsMaker: t.IsMaker,
|
||||
Fee: fee,
|
||||
FeeCurrency: t.CommissionAsset,
|
||||
QuoteQuantity: quoteQuantity,
|
||||
Time: mts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGlobalSideType(side binance.SideType) types.SideType {
|
||||
switch side {
|
||||
case binance.SideTypeBuy:
|
||||
return types.SideTypeBuy
|
||||
|
||||
case binance.SideTypeSell:
|
||||
return types.SideTypeSell
|
||||
|
||||
default:
|
||||
log.Errorf("unknown side type: %v", side)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func toGlobalOrderType(orderType binance.OrderType) types.OrderType {
|
||||
switch orderType {
|
||||
|
||||
case binance.OrderTypeLimit:
|
||||
return types.OrderTypeLimit
|
||||
|
||||
case binance.OrderTypeMarket:
|
||||
return types.OrderTypeMarket
|
||||
|
||||
case binance.OrderTypeStopLossLimit:
|
||||
return types.OrderTypeStopLimit
|
||||
|
||||
case binance.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:
|
||||
return types.OrderStatusNew
|
||||
|
||||
case binance.OrderStatusTypeRejected:
|
||||
return types.OrderStatusRejected
|
||||
|
||||
case binance.OrderStatusTypeCanceled:
|
||||
return types.OrderStatusCanceled
|
||||
|
||||
case binance.OrderStatusTypePartiallyFilled:
|
||||
return types.OrderStatusPartiallyFilled
|
||||
|
||||
case binance.OrderStatusTypeFilled:
|
||||
return types.OrderStatusFilled
|
||||
}
|
||||
|
||||
return types.OrderStatus(orderStatus)
|
||||
}
|
|
@ -3,10 +3,11 @@ package binance
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/adshao/go-binance"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
|
@ -55,7 +56,7 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
|
|||
BaseCurrency: symbol.BaseAsset,
|
||||
}
|
||||
|
||||
if f := symbol.MinNotionalFilter() ; f != nil {
|
||||
if f := symbol.MinNotionalFilter(); f != nil {
|
||||
market.MinNotional = util.MustParseFloat(f.MinNotional)
|
||||
market.MinAmount = util.MustParseFloat(f.MinNotional)
|
||||
}
|
||||
|
@ -65,14 +66,14 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
|
|||
// minQty defines the minimum quantity/icebergQty allowed.
|
||||
// maxQty defines the maximum quantity/icebergQty allowed.
|
||||
// stepSize defines the intervals that a quantity/icebergQty can be increased/decreased by.
|
||||
if f := symbol.LotSizeFilter() ; f != nil {
|
||||
if f := symbol.LotSizeFilter(); f != nil {
|
||||
market.MinLot = util.MustParseFloat(f.MinQuantity)
|
||||
market.MinQuantity = util.MustParseFloat(f.MinQuantity)
|
||||
market.MaxQuantity = util.MustParseFloat(f.MaxQuantity)
|
||||
// market.StepSize = util.MustParseFloat(f.StepSize)
|
||||
}
|
||||
|
||||
if f := symbol.PriceFilter() ; f != nil {
|
||||
if f := symbol.PriceFilter(); f != nil {
|
||||
market.MaxPrice = util.MustParseFloat(f.MaxPrice)
|
||||
market.MinPrice = util.MustParseFloat(f.MinPrice)
|
||||
market.TickSize = util.MustParseFloat(f.TickSize)
|
||||
|
@ -268,53 +269,115 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
|
|||
return a, nil
|
||||
}
|
||||
|
||||
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) error {
|
||||
/*
|
||||
limit order example
|
||||
|
||||
order, err := Client.NewCreateOrderService().
|
||||
Symbol(Symbol).
|
||||
Side(side).
|
||||
Type(binance.OrderTypeLimit).
|
||||
TimeInForce(binance.TimeInForceTypeGTC).
|
||||
Quantity(volumeString).
|
||||
Price(priceString).
|
||||
Do(ctx)
|
||||
*/
|
||||
|
||||
orderType, err := toLocalOrderType(order.Type)
|
||||
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
|
||||
remoteOrders, err := e.Client.NewListOpenOrdersService().Symbol(symbol).Do(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return orders, err
|
||||
}
|
||||
|
||||
req := e.Client.NewCreateOrderService().
|
||||
Symbol(order.Symbol).
|
||||
Side(binance.SideType(order.Side)).
|
||||
Type(orderType).
|
||||
Quantity(order.QuantityString)
|
||||
for _, binanceOrder := range remoteOrders {
|
||||
order, err := toGlobalOrder(binanceOrder)
|
||||
if err != nil {
|
||||
return orders, err
|
||||
}
|
||||
|
||||
if len(order.PriceString) > 0 {
|
||||
req.Price(order.PriceString)
|
||||
}
|
||||
if len(order.TimeInForce) > 0 {
|
||||
req.TimeInForce(order.TimeInForce)
|
||||
orders = append(orders, *order)
|
||||
}
|
||||
|
||||
retOrder, err := req.Do(ctx)
|
||||
log.Infof("order created: %+v", retOrder)
|
||||
return err
|
||||
return orders, err
|
||||
}
|
||||
|
||||
func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) {
|
||||
switch orderType {
|
||||
case types.OrderTypeLimit:
|
||||
return binance.OrderTypeLimit, nil
|
||||
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err2 error) {
|
||||
for _, o := range orders {
|
||||
var req = e.Client.NewCancelOrderService()
|
||||
|
||||
case types.OrderTypeMarket:
|
||||
return binance.OrderTypeMarket, nil
|
||||
// Mandatory
|
||||
req.Symbol(o.Symbol)
|
||||
|
||||
if o.OrderID > 0 {
|
||||
req.OrderID(int64(o.OrderID))
|
||||
} else if len(o.ClientOrderID) > 0 {
|
||||
req.NewClientOrderID(o.ClientOrderID)
|
||||
}
|
||||
|
||||
_, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("order cancel error")
|
||||
err2 = err
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("order type %s not supported", orderType)
|
||||
return err2
|
||||
}
|
||||
|
||||
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders []types.Order, err error) {
|
||||
for _, order := range orders {
|
||||
orderType, err := toLocalOrderType(order.Type)
|
||||
if err != nil {
|
||||
return createdOrders, err
|
||||
}
|
||||
|
||||
clientOrderID := uuid.New().String()
|
||||
if len(order.ClientOrderID) > 0 {
|
||||
clientOrderID = order.ClientOrderID
|
||||
}
|
||||
|
||||
req := e.Client.NewCreateOrderService().
|
||||
Symbol(order.Symbol).
|
||||
Side(binance.SideType(order.Side)).
|
||||
NewClientOrderID(clientOrderID).
|
||||
Type(orderType)
|
||||
|
||||
req.Quantity(order.QuantityString)
|
||||
|
||||
if len(order.PriceString) > 0 {
|
||||
req.Price(order.PriceString)
|
||||
}
|
||||
|
||||
if len(order.TimeInForce) > 0 {
|
||||
// TODO: check the TimeInForce value
|
||||
req.TimeInForce(binance.TimeInForceType(order.TimeInForce))
|
||||
}
|
||||
|
||||
response, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
return createdOrders, err
|
||||
}
|
||||
|
||||
log.Infof("order creation response: %+v", response)
|
||||
|
||||
retOrder := binance.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,
|
||||
// StopPrice:
|
||||
// IcebergQuantity:
|
||||
Time: response.TransactTime,
|
||||
// UpdateTime:
|
||||
// IsWorking: ,
|
||||
}
|
||||
|
||||
createdOrder, err := toGlobalOrder(&retOrder)
|
||||
if err != nil {
|
||||
return createdOrders, err
|
||||
}
|
||||
|
||||
if createdOrder == nil {
|
||||
return createdOrders, errors.New("nil converted order")
|
||||
}
|
||||
|
||||
createdOrders = append(createdOrders, *createdOrder)
|
||||
}
|
||||
|
||||
return createdOrders, err
|
||||
}
|
||||
|
||||
func (e *Exchange) QueryKLines(ctx context.Context, symbol, interval string, options types.KLineQueryOptions) ([]types.KLine, error) {
|
||||
|
@ -393,7 +456,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
|||
}
|
||||
|
||||
for _, t := range remoteTrades {
|
||||
localTrade, err := convertRemoteTrade(*t)
|
||||
localTrade, err := toGlobalTrade(*t)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("can not convert binance trade: %+v", t)
|
||||
continue
|
||||
|
@ -406,54 +469,6 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
|||
return trades, nil
|
||||
}
|
||||
|
||||
func convertRemoteTrade(t binance.TradeV3) (*types.Trade, error) {
|
||||
// skip trade ID that is the same. however this should not happen
|
||||
var side string
|
||||
if t.IsBuyer {
|
||||
side = "BUY"
|
||||
} else {
|
||||
side = "SELL"
|
||||
}
|
||||
|
||||
// trade time
|
||||
mts := time.Unix(0, t.Time*int64(time.Millisecond))
|
||||
|
||||
price, err := strconv.ParseFloat(t.Price, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quantity, err := strconv.ParseFloat(t.Quantity, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quoteQuantity, err := strconv.ParseFloat(t.QuoteQuantity, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fee, err := strconv.ParseFloat(t.Commission, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.Trade{
|
||||
ID: t.ID,
|
||||
Price: price,
|
||||
Symbol: t.Symbol,
|
||||
Exchange: "binance",
|
||||
Quantity: quantity,
|
||||
Side: side,
|
||||
IsBuyer: t.IsBuyer,
|
||||
IsMaker: t.IsMaker,
|
||||
Fee: fee,
|
||||
FeeCurrency: t.CommissionAsset,
|
||||
QuoteQuantity: quoteQuantity,
|
||||
Time: mts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *Exchange) BatchQueryKLines(ctx context.Context, symbol, interval string, startTime, endTime time.Time) ([]types.KLine, error) {
|
||||
var allKLines []types.KLine
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/adshao/go-binance"
|
||||
"github.com/valyala/fastjson"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
|
@ -74,7 +75,7 @@ type ExecutionReportEvent struct {
|
|||
CurrentExecutionType string `json:"x"`
|
||||
CurrentOrderStatus string `json:"X"`
|
||||
|
||||
OrderID int `json:"i"`
|
||||
OrderID uint64 `json:"i"`
|
||||
|
||||
TradeID int64 `json:"t"`
|
||||
TransactionTime int64 `json:"T"`
|
||||
|
@ -84,7 +85,34 @@ type ExecutionReportEvent struct {
|
|||
LastExecutedPrice string `json:"L"`
|
||||
LastQuoteAssetTransactedQuantity string `json:"Y"`
|
||||
|
||||
OrderCreationTime int `json:"O"`
|
||||
OrderCreationTime int64 `json:"O"`
|
||||
}
|
||||
|
||||
func (e *ExecutionReportEvent) Order() (*types.Order, error) {
|
||||
|
||||
switch e.CurrentExecutionType {
|
||||
case "NEW", "CANCELED", "REJECTED", "EXPIRED":
|
||||
case "REPLACED":
|
||||
default:
|
||||
return nil, errors.New("execution report type is not for order")
|
||||
}
|
||||
|
||||
orderCreationTime := time.Unix(0, e.OrderCreationTime*int64(time.Millisecond))
|
||||
return &types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
Symbol: e.Symbol,
|
||||
ClientOrderID: e.ClientOrderID,
|
||||
Side: toGlobalSideType(binance.SideType(e.Side)),
|
||||
Type: toGlobalOrderType(binance.OrderType(e.OrderType)),
|
||||
Quantity: util.MustParseFloat(e.OrderQuantity),
|
||||
Price: util.MustParseFloat(e.OrderPrice),
|
||||
TimeInForce: e.TimeInForce,
|
||||
},
|
||||
OrderID: e.OrderID,
|
||||
Status: toGlobalOrderStatus(binance.OrderStatusType(e.CurrentOrderStatus)),
|
||||
ExecutedQuantity: util.MustParseFloat(e.CumulativeFilledQuantity),
|
||||
CreationTime: orderCreationTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *ExecutionReportEvent) Trade() (*types.Trade, error) {
|
||||
|
@ -92,14 +120,14 @@ func (e *ExecutionReportEvent) Trade() (*types.Trade, error) {
|
|||
return nil, errors.New("execution report is not a trade")
|
||||
}
|
||||
|
||||
tt := time.Unix(0, e.TransactionTime/1000000)
|
||||
tt := time.Unix(0, e.TransactionTime*int64(time.Millisecond))
|
||||
return &types.Trade{
|
||||
ID: e.TradeID,
|
||||
Symbol: e.Symbol,
|
||||
Side: toGlobalSideType(binance.SideType(e.Side)),
|
||||
Price: util.MustParseFloat(e.LastExecutedPrice),
|
||||
Quantity: util.MustParseFloat(e.LastExecutedQuantity),
|
||||
QuoteQuantity: util.MustParseFloat(e.LastQuoteAssetTransactedQuantity),
|
||||
Side: e.Side,
|
||||
IsBuyer: e.Side == "BUY",
|
||||
IsMaker: e.IsMaker,
|
||||
Time: tt,
|
||||
|
|
61
pkg/exchange/binance/parse_test.go
Normal file
61
pkg/exchange/binance/parse_test.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package binance
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var jsCommentTrimmer = regexp.MustCompile("(?m)//.*$")
|
||||
|
||||
func TestParseOrderUpdate(t *testing.T) {
|
||||
payload := `{
|
||||
"e": "executionReport", // Event type
|
||||
"E": 1499405658658, // Event time
|
||||
"s": "ETHBTC", // Symbol
|
||||
"c": "mUvoqJxFIILMdfAW5iGSOW", // Client order ID
|
||||
"S": "BUY", // Side
|
||||
"o": "LIMIT", // Order type
|
||||
"f": "GTC", // Time in force
|
||||
"q": "1.00000000", // Order quantity
|
||||
"p": "0.10264410", // Order price
|
||||
"P": "0.00000000", // Stop price
|
||||
"F": "0.00000000", // Iceberg quantity
|
||||
"g": -1, // OrderListId
|
||||
"C": null, // Original client order ID; This is the ID of the order being canceled
|
||||
"x": "NEW", // Current execution type
|
||||
"X": "NEW", // Current order status
|
||||
"r": "NONE", // Order reject reason; will be an error code.
|
||||
"i": 4293153, // Order ID
|
||||
"l": "0.00000000", // Last executed quantity
|
||||
"z": "0.00000000", // Cumulative filled quantity
|
||||
"L": "0.00000000", // Last executed price
|
||||
"n": "0", // Commission amount
|
||||
"N": null, // Commission asset
|
||||
"T": 1499405658657, // Transaction time
|
||||
"t": -1, // Trade ID
|
||||
"I": 8641984, // Ignore
|
||||
"w": true, // Is the order on the book?
|
||||
"m": false, // Is this trade the maker side?
|
||||
"M": false, // Ignore
|
||||
"O": 1499405658657, // Order creation time
|
||||
"Z": "0.00000000", // Cumulative quote asset transacted quantity
|
||||
"Y": "0.00000000", // Last quote asset transacted quantity (i.e. lastPrice * lastQty)
|
||||
"Q": "0.00000000" // Quote Order Qty
|
||||
}`
|
||||
|
||||
payload = jsCommentTrimmer.ReplaceAllLiteralString(payload, "")
|
||||
|
||||
event, err := ParseEvent(payload)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, event)
|
||||
|
||||
executionReport, ok := event.(*ExecutionReportEvent)
|
||||
assert.True(t, ok)
|
||||
assert.NotNil(t, executionReport)
|
||||
|
||||
orderUpdate, err := executionReport.Order()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, orderUpdate)
|
||||
}
|
218
pkg/exchange/max/convert.go
Normal file
218
pkg/exchange/max/convert.go
Normal file
|
@ -0,0 +1,218 @@
|
|||
package max
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/max/maxapi"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
)
|
||||
|
||||
func toGlobalCurrency(currency string) string {
|
||||
return strings.ToUpper(currency)
|
||||
}
|
||||
|
||||
func toLocalCurrency(currency string) string {
|
||||
return strings.ToLower(currency)
|
||||
}
|
||||
|
||||
func toLocalSymbol(symbol string) string {
|
||||
return strings.ToLower(symbol)
|
||||
}
|
||||
|
||||
func toGlobalSymbol(symbol string) string {
|
||||
return strings.ToUpper(symbol)
|
||||
}
|
||||
|
||||
func toLocalSideType(side types.SideType) string {
|
||||
return strings.ToLower(string(side))
|
||||
}
|
||||
|
||||
func toGlobalSideType(v string) types.SideType {
|
||||
switch strings.ToLower(v) {
|
||||
case "bid", "buy":
|
||||
return types.SideTypeBuy
|
||||
|
||||
case "ask", "sell":
|
||||
return types.SideTypeSell
|
||||
|
||||
case "self-trade":
|
||||
return types.SideTypeSelf
|
||||
|
||||
}
|
||||
|
||||
return types.SideType(v)
|
||||
}
|
||||
|
||||
func toGlobalOrderStatus(orderStatus max.OrderState, executedVolume, remainingVolume fixedpoint.Value) types.OrderStatus {
|
||||
|
||||
switch orderStatus {
|
||||
|
||||
case max.OrderStateCancel:
|
||||
return types.OrderStatusCanceled
|
||||
|
||||
case max.OrderStateFinalizing, max.OrderStateDone:
|
||||
if executedVolume > 0 && remainingVolume > 0 {
|
||||
return types.OrderStatusPartiallyFilled
|
||||
} else if remainingVolume == 0 {
|
||||
return types.OrderStatusFilled
|
||||
}
|
||||
|
||||
return types.OrderStatusFilled
|
||||
|
||||
case max.OrderStateWait:
|
||||
if executedVolume > 0 && remainingVolume > 0 {
|
||||
return types.OrderStatusPartiallyFilled
|
||||
}
|
||||
|
||||
return types.OrderStatusNew
|
||||
|
||||
case max.OrderStateConvert:
|
||||
if executedVolume > 0 && remainingVolume > 0 {
|
||||
return types.OrderStatusPartiallyFilled
|
||||
}
|
||||
|
||||
return types.OrderStatusNew
|
||||
|
||||
case max.OrderStateFailed:
|
||||
return types.OrderStatusRejected
|
||||
|
||||
}
|
||||
|
||||
logger.Errorf("unknown order status: %v", orderStatus)
|
||||
return types.OrderStatus(orderStatus)
|
||||
}
|
||||
|
||||
func toGlobalOrderType(orderType max.OrderType) types.OrderType {
|
||||
switch orderType {
|
||||
case max.OrderTypeLimit:
|
||||
return types.OrderTypeLimit
|
||||
|
||||
case max.OrderTypeMarket:
|
||||
return types.OrderTypeMarket
|
||||
|
||||
case max.OrderTypeStopLimit:
|
||||
return types.OrderTypeStopLimit
|
||||
|
||||
case max.OrderTypeStopMarket:
|
||||
return types.OrderTypeStopMarket
|
||||
|
||||
}
|
||||
|
||||
logger.Errorf("unknown order type: %v", orderType)
|
||||
return types.OrderType(orderType)
|
||||
}
|
||||
|
||||
func toLocalOrderType(orderType types.OrderType) (max.OrderType, error) {
|
||||
switch orderType {
|
||||
|
||||
case types.OrderTypeStopLimit:
|
||||
return max.OrderTypeStopLimit, nil
|
||||
|
||||
case types.OrderTypeStopMarket:
|
||||
return max.OrderTypeStopMarket, nil
|
||||
|
||||
case types.OrderTypeLimit:
|
||||
return max.OrderTypeLimit, nil
|
||||
|
||||
case types.OrderTypeMarket:
|
||||
return max.OrderTypeMarket, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("order type %s not supported", orderType)
|
||||
}
|
||||
|
||||
func toGlobalOrder(maxOrder max.Order) (*types.Order, error) {
|
||||
executedVolume, err := fixedpoint.NewFromString(maxOrder.ExecutedVolume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remainingVolume, err := fixedpoint.NewFromString(maxOrder.RemainingVolume)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.Order{
|
||||
SubmitOrder: types.SubmitOrder{
|
||||
ClientOrderID: maxOrder.ClientOID,
|
||||
Symbol: toGlobalSymbol(maxOrder.Market),
|
||||
Side: toGlobalSideType(maxOrder.Side),
|
||||
Type: toGlobalOrderType(maxOrder.OrderType),
|
||||
Quantity: util.MustParseFloat(maxOrder.Volume),
|
||||
Price: util.MustParseFloat(maxOrder.Price),
|
||||
TimeInForce: "GTC", // MAX only supports GTC
|
||||
},
|
||||
OrderID: maxOrder.ID,
|
||||
Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume),
|
||||
ExecutedQuantity: executedVolume.Float64(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGlobalTrade(t max.Trade) (*types.Trade, error) {
|
||||
// skip trade ID that is the same. however this should not happen
|
||||
var side = toGlobalSideType(t.Side)
|
||||
|
||||
// trade time
|
||||
mts := time.Unix(0, t.CreatedAtMilliSeconds*int64(time.Millisecond))
|
||||
|
||||
price, err := strconv.ParseFloat(t.Price, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quantity, err := strconv.ParseFloat(t.Volume, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quoteQuantity, err := strconv.ParseFloat(t.Funds, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fee, err := strconv.ParseFloat(t.Fee, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.Trade{
|
||||
ID: int64(t.ID),
|
||||
Price: price,
|
||||
Symbol: toGlobalSymbol(t.Market),
|
||||
Exchange: "max",
|
||||
Quantity: quantity,
|
||||
Side: side,
|
||||
IsBuyer: t.IsBuyer(),
|
||||
IsMaker: t.IsMaker(),
|
||||
Fee: fee,
|
||||
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
|
||||
QuoteQuantity: quoteQuantity,
|
||||
Time: mts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toGlobalDepositStatus(a string) types.DepositStatus {
|
||||
switch a {
|
||||
case "submitting", "submitted", "checking":
|
||||
return types.DepositPending
|
||||
|
||||
case "accepted":
|
||||
return types.DepositSuccess
|
||||
|
||||
case "rejected":
|
||||
return types.DepositRejected
|
||||
|
||||
case "canceled":
|
||||
return types.DepositCancelled
|
||||
|
||||
case "suspect", "refunded":
|
||||
|
||||
}
|
||||
|
||||
return types.DepositStatus(a)
|
||||
}
|
|
@ -2,11 +2,9 @@ package max
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
|
@ -46,8 +44,10 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
|
|||
|
||||
markets := types.MarketMap{}
|
||||
for _, m := range remoteMarkets {
|
||||
symbol := toGlobalSymbol(m.ID)
|
||||
|
||||
market := types.Market{
|
||||
Symbol: toGlobalSymbol(m.ID),
|
||||
Symbol: symbol,
|
||||
PricePrecision: m.QuoteUnitPrecision,
|
||||
VolumePrecision: m.BaseUnitPrecision,
|
||||
QuoteCurrency: toGlobalCurrency(m.QuoteUnit),
|
||||
|
@ -62,7 +62,7 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
|
|||
TickSize: 0.001,
|
||||
}
|
||||
|
||||
markets[m.ID] = market
|
||||
markets[symbol] = market
|
||||
}
|
||||
|
||||
return markets, nil
|
||||
|
@ -72,26 +72,86 @@ func (e *Exchange) NewStream() types.Stream {
|
|||
return NewStream(e.key, e.secret)
|
||||
}
|
||||
|
||||
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) error {
|
||||
orderType, err := toLocalOrderType(order.Type)
|
||||
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
|
||||
maxOrders, err := e.client.OrderService.Open(toLocalSymbol(symbol), maxapi.QueryOrderOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
return orders, err
|
||||
}
|
||||
|
||||
req := e.client.OrderService.NewCreateOrderRequest().
|
||||
Market(toLocalSymbol(order.Symbol)).
|
||||
OrderType(string(orderType)).
|
||||
Side(toLocalSideType(order.Side)).
|
||||
Volume(order.QuantityString).
|
||||
Price(order.PriceString)
|
||||
for _, maxOrder := range maxOrders {
|
||||
order, err := toGlobalOrder(maxOrder)
|
||||
if err != nil {
|
||||
return orders, err
|
||||
}
|
||||
|
||||
retOrder, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
orders = append(orders, *order)
|
||||
}
|
||||
|
||||
logger.Infof("order created: %+v", retOrder)
|
||||
return err
|
||||
return orders, err
|
||||
}
|
||||
|
||||
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err2 error) {
|
||||
for _, o := range orders {
|
||||
var req = e.client.OrderService.NewOrderCancelRequest()
|
||||
if o.OrderID > 0 {
|
||||
req.ID(o.OrderID)
|
||||
} else if len(o.ClientOrderID) > 0 {
|
||||
req.ClientOrderID(o.ClientOrderID)
|
||||
} else {
|
||||
return errors.Errorf("order id or client order id is not defined, order=%+v", o)
|
||||
}
|
||||
|
||||
if err := req.Do(ctx); err != nil {
|
||||
log.WithError(err).Errorf("order cancel error")
|
||||
err2 = err
|
||||
}
|
||||
}
|
||||
|
||||
return err2
|
||||
}
|
||||
|
||||
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders []types.Order, err error) {
|
||||
for _, order := range orders {
|
||||
orderType, err := toLocalOrderType(order.Type)
|
||||
if err != nil {
|
||||
return createdOrders, err
|
||||
}
|
||||
|
||||
req := e.client.OrderService.NewCreateOrderRequest().
|
||||
Market(toLocalSymbol(order.Symbol)).
|
||||
OrderType(string(orderType)).
|
||||
Side(toLocalSideType(order.Side)).
|
||||
Volume(order.QuantityString)
|
||||
|
||||
if len(order.ClientOrderID) > 0 {
|
||||
req.ClientOrderID(order.ClientOrderID)
|
||||
} else {
|
||||
clientOrderID := uuid.New().String()
|
||||
req.ClientOrderID(clientOrderID)
|
||||
}
|
||||
|
||||
if len(order.PriceString) > 0 {
|
||||
req.Price(order.PriceString)
|
||||
}
|
||||
|
||||
retOrder, err := req.Do(ctx)
|
||||
if err != nil {
|
||||
return createdOrders, err
|
||||
}
|
||||
if retOrder == nil {
|
||||
return createdOrders, errors.New("returned nil order")
|
||||
}
|
||||
|
||||
logger.Infof("order created: %+v", retOrder)
|
||||
createdOrder, err := toGlobalOrder(*retOrder)
|
||||
if err != nil {
|
||||
return createdOrders, err
|
||||
}
|
||||
|
||||
createdOrders = append(createdOrders, *createdOrder)
|
||||
}
|
||||
|
||||
return createdOrders, err
|
||||
}
|
||||
|
||||
// PlatformFeeCurrency
|
||||
|
@ -230,7 +290,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since,
|
|||
Address: "", // not supported
|
||||
AddressTag: "", // not supported
|
||||
TransactionID: d.TxID,
|
||||
Status: convertDepositState(d.State),
|
||||
Status: toGlobalDepositStatus(d.State),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -240,27 +300,6 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since,
|
|||
return allDeposits, err
|
||||
}
|
||||
|
||||
func convertDepositState(a string) types.DepositStatus {
|
||||
switch a {
|
||||
case "submitting", "submitted", "checking":
|
||||
return types.DepositPending
|
||||
|
||||
case "accepted":
|
||||
return types.DepositSuccess
|
||||
|
||||
case "rejected":
|
||||
return types.DepositRejected
|
||||
|
||||
case "canceled":
|
||||
return types.DepositCancelled
|
||||
|
||||
case "suspect", "refunded":
|
||||
|
||||
}
|
||||
|
||||
return types.DepositStatus(a)
|
||||
}
|
||||
|
||||
func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
|
||||
accounts, err := e.client.AccountService.Accounts()
|
||||
if err != nil {
|
||||
|
@ -301,7 +340,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
|||
}
|
||||
|
||||
for _, t := range remoteTrades {
|
||||
localTrade, err := convertRemoteTrade(t)
|
||||
localTrade, err := toGlobalTrade(t)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("can not convert trade: %+v", t)
|
||||
continue
|
||||
|
@ -356,94 +395,3 @@ func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (float6
|
|||
|
||||
return (util.MustParseFloat(ticker.Sell) + util.MustParseFloat(ticker.Buy)) / 2, nil
|
||||
}
|
||||
|
||||
func toGlobalCurrency(currency string) string {
|
||||
return strings.ToUpper(currency)
|
||||
}
|
||||
|
||||
func toLocalCurrency(currency string) string {
|
||||
return strings.ToLower(currency)
|
||||
}
|
||||
|
||||
func toLocalSymbol(symbol string) string {
|
||||
return strings.ToLower(symbol)
|
||||
}
|
||||
|
||||
func toGlobalSymbol(symbol string) string {
|
||||
return strings.ToUpper(symbol)
|
||||
}
|
||||
|
||||
func toLocalSideType(side types.SideType) string {
|
||||
return strings.ToLower(string(side))
|
||||
}
|
||||
|
||||
func toGlobalSideType(v string) string {
|
||||
switch strings.ToLower(v) {
|
||||
case "bid":
|
||||
return "BUY"
|
||||
|
||||
case "ask":
|
||||
return "SELL"
|
||||
|
||||
case "self-trade":
|
||||
return "SELF"
|
||||
|
||||
}
|
||||
|
||||
return strings.ToUpper(v)
|
||||
}
|
||||
|
||||
func toLocalOrderType(orderType types.OrderType) (maxapi.OrderType, error) {
|
||||
switch orderType {
|
||||
case types.OrderTypeLimit:
|
||||
return maxapi.OrderTypeLimit, nil
|
||||
|
||||
case types.OrderTypeMarket:
|
||||
return maxapi.OrderTypeMarket, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("order type %s not supported", orderType)
|
||||
}
|
||||
|
||||
func convertRemoteTrade(t maxapi.Trade) (*types.Trade, error) {
|
||||
// skip trade ID that is the same. however this should not happen
|
||||
var side = toGlobalSideType(t.Side)
|
||||
|
||||
// trade time
|
||||
mts := time.Unix(0, t.CreatedAtMilliSeconds*int64(time.Millisecond))
|
||||
|
||||
price, err := strconv.ParseFloat(t.Price, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quantity, err := strconv.ParseFloat(t.Volume, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
quoteQuantity, err := strconv.ParseFloat(t.Funds, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fee, err := strconv.ParseFloat(t.Fee, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.Trade{
|
||||
ID: int64(t.ID),
|
||||
Price: price,
|
||||
Symbol: toGlobalSymbol(t.Market),
|
||||
Exchange: "max",
|
||||
Quantity: quantity,
|
||||
Side: side,
|
||||
IsBuyer: t.IsBuyer(),
|
||||
IsMaker: t.IsMaker(),
|
||||
Fee: fee,
|
||||
FeeCurrency: toGlobalCurrency(t.FeeCurrency),
|
||||
QuoteQuantity: quoteQuantity,
|
||||
Time: mts,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -19,20 +19,28 @@ const (
|
|||
type OrderState string
|
||||
|
||||
const (
|
||||
OrderStateDone = OrderState("done")
|
||||
OrderStateCancel = OrderState("cancel")
|
||||
OrderStateWait = OrderState("wait")
|
||||
OrderStateConvert = OrderState("convert")
|
||||
OrderStateDone = OrderState("done")
|
||||
OrderStateCancel = OrderState("cancel")
|
||||
OrderStateWait = OrderState("wait")
|
||||
OrderStateConvert = OrderState("convert")
|
||||
OrderStateFinalizing = OrderState("finalizing")
|
||||
OrderStateFailed = OrderState("failed")
|
||||
)
|
||||
|
||||
type OrderType string
|
||||
|
||||
// Order types that the API can return.
|
||||
const (
|
||||
OrderTypeMarket = OrderType("market")
|
||||
OrderTypeLimit = OrderType("limit")
|
||||
OrderTypeMarket = OrderType("market")
|
||||
OrderTypeLimit = OrderType("limit")
|
||||
OrderTypeStopLimit = OrderType("stop_limit")
|
||||
OrderTypeStopMarket = OrderType("stop_market")
|
||||
)
|
||||
|
||||
type QueryOrderOptions struct {
|
||||
GroupID int
|
||||
}
|
||||
|
||||
// OrderService manages the Order endpoint.
|
||||
type OrderService struct {
|
||||
client *RestClient
|
||||
|
@ -40,22 +48,52 @@ type OrderService struct {
|
|||
|
||||
// Order represents one returned order (POST order/GET order/GET orders) on the max platform.
|
||||
type Order struct {
|
||||
ID uint64 `json:"id,omitempty" db:"exchange_id"`
|
||||
Side string `json:"side" db:"side"`
|
||||
OrderType string `json:"ord_type,omitempty" db:"order_type"`
|
||||
Price string `json:"price" db:"price"`
|
||||
AveragePrice string `json:"avg_price,omitempty" db:"average_price"`
|
||||
State string `json:"state,omitempty" db:"state"`
|
||||
Market string `json:"market,omitempty" db:"market"`
|
||||
Volume string `json:"volume" db:"volume"`
|
||||
RemainingVolume string `json:"remaining_volume,omitempty" db:"remaining_volume"`
|
||||
ExecutedVolume string `json:"executed_volume,omitempty" db:"executed_volume"`
|
||||
TradesCount int64 `json:"trades_count,omitempty" db:"trades_count"`
|
||||
GroupID int64 `json:"group_id,omitempty" db:"group_id"`
|
||||
ClientOID string `json:"client_oid,omitempty" db:"client_oid"`
|
||||
CreatedAt time.Time `json:"-" db:"created_at"`
|
||||
CreatedAtMs int64 `json:"created_at_in_ms,omitempty"`
|
||||
InsertedAt time.Time `json:"-" db:"inserted_at"`
|
||||
ID uint64 `json:"id,omitempty" db:"exchange_id"`
|
||||
Side string `json:"side" db:"side"`
|
||||
OrderType OrderType `json:"ord_type,omitempty" db:"order_type"`
|
||||
Price string `json:"price" db:"price"`
|
||||
AveragePrice string `json:"avg_price,omitempty" db:"average_price"`
|
||||
State OrderState `json:"state,omitempty" db:"state"`
|
||||
Market string `json:"market,omitempty" db:"market"`
|
||||
Volume string `json:"volume" db:"volume"`
|
||||
RemainingVolume string `json:"remaining_volume,omitempty" db:"remaining_volume"`
|
||||
ExecutedVolume string `json:"executed_volume,omitempty" db:"executed_volume"`
|
||||
TradesCount int64 `json:"trades_count,omitempty" db:"trades_count"`
|
||||
GroupID int64 `json:"group_id,omitempty" db:"group_id"`
|
||||
ClientOID string `json:"client_oid,omitempty" db:"client_oid"`
|
||||
CreatedAt time.Time `json:"-" db:"created_at"`
|
||||
CreatedAtMs int64 `json:"created_at_in_ms,omitempty"`
|
||||
InsertedAt time.Time `json:"-" db:"inserted_at"`
|
||||
}
|
||||
|
||||
// Open returns open orders
|
||||
func (s *OrderService) Open(market string, options QueryOrderOptions) ([]Order, error) {
|
||||
payload := map[string]interface{}{
|
||||
"market": market,
|
||||
// "state": []OrderState{OrderStateWait, OrderStateConvert},
|
||||
"order_by": "desc",
|
||||
}
|
||||
|
||||
if options.GroupID > 0 {
|
||||
payload["group_id"] = options.GroupID
|
||||
}
|
||||
|
||||
req, err := s.client.newAuthenticatedRequest("GET", "v2/orders", payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := s.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var orders []Order
|
||||
if err := response.DecodeJSON(&orders); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orders, nil
|
||||
}
|
||||
|
||||
// All returns all orders for the authenticated account.
|
||||
|
@ -154,22 +192,10 @@ func (s *OrderService) Cancel(orderID uint64, clientOrderID string) error {
|
|||
}
|
||||
|
||||
type OrderCancelRequestParams struct {
|
||||
ID uint64 `json:"id"`
|
||||
ClientOrderID string `json:"client_oid"`
|
||||
}
|
||||
*PrivateRequestParams
|
||||
|
||||
func (p OrderCancelRequestParams) Map() map[string]interface{} {
|
||||
payload := make(map[string]interface{})
|
||||
|
||||
if p.ID > 0 {
|
||||
payload["id"] = p.ID
|
||||
}
|
||||
|
||||
if len(p.ClientOrderID) > 0 {
|
||||
payload["client_oid"] = p.ClientOrderID
|
||||
}
|
||||
|
||||
return payload
|
||||
ID uint64 `json:"id,omitempty"`
|
||||
ClientOrderID string `json:"client_oid,omitempty"`
|
||||
}
|
||||
|
||||
type OrderCancelRequest struct {
|
||||
|
@ -189,13 +215,21 @@ func (r *OrderCancelRequest) ClientOrderID(id string) *OrderCancelRequest {
|
|||
}
|
||||
|
||||
func (r *OrderCancelRequest) Do(ctx context.Context) error {
|
||||
payload := r.params.Map()
|
||||
req, err := r.client.newAuthenticatedRequest("POST", "v2/order/delete", payload)
|
||||
req, err := r.client.newAuthenticatedRequest("POST", "v2/order/delete", &r.params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = r.client.sendRequest(req)
|
||||
response, err := r.client.sendRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var order = Order{}
|
||||
if err := response.DecodeJSON(&order); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -230,7 +264,7 @@ func (s *OrderService) Get(orderID uint64) (*Order, error) {
|
|||
}
|
||||
|
||||
type MultiOrderRequestParams struct {
|
||||
PrivateRequestParams
|
||||
*PrivateRequestParams
|
||||
|
||||
Market string `json:"market"`
|
||||
Orders []Order `json:"orders"`
|
||||
|
@ -281,12 +315,12 @@ func (s *OrderService) NewCreateMultiOrderRequest() *CreateMultiOrderRequest {
|
|||
}
|
||||
|
||||
type CreateOrderRequestParams struct {
|
||||
PrivateRequestParams
|
||||
*PrivateRequestParams
|
||||
|
||||
Market string `json:"market"`
|
||||
Volume string `json:"volume"`
|
||||
Price string `json:"price"`
|
||||
StopPrice string `json:"stop_price"`
|
||||
Price string `json:"price,omitempty"`
|
||||
StopPrice string `json:"stop_price,omitempty"`
|
||||
Side string `json:"side"`
|
||||
OrderType string `json:"ord_type"`
|
||||
ClientOrderID string `json:"client_oid,omitempty"`
|
||||
|
@ -335,7 +369,7 @@ func (r *CreateOrderRequest) ClientOrderID(clientOrderID string) *CreateOrderReq
|
|||
}
|
||||
|
||||
func (r *CreateOrderRequest) Do(ctx context.Context) (order *Order, err error) {
|
||||
req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", r.params)
|
||||
req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", &r.params)
|
||||
if err != nil {
|
||||
return order, errors.Wrapf(err, "order create error")
|
||||
}
|
||||
|
@ -346,8 +380,8 @@ func (r *CreateOrderRequest) Do(ctx context.Context) (order *Order, err error) {
|
|||
}
|
||||
|
||||
order = &Order{}
|
||||
if errJson := response.DecodeJSON(order); errJson != nil {
|
||||
return order, errJson
|
||||
if err := response.DecodeJSON(order); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return order, err
|
||||
|
|
|
@ -174,6 +174,8 @@ func (e *BookEvent) Time() time.Time {
|
|||
}
|
||||
|
||||
func (e *BookEvent) OrderBook() (snapshot types.OrderBook, err error) {
|
||||
snapshot.Symbol = strings.ToUpper(e.Market)
|
||||
|
||||
for _, bid := range e.Bids {
|
||||
pv, err := bid.PriceVolumePair()
|
||||
if err != nil {
|
||||
|
|
|
@ -250,7 +250,6 @@ func getPrivateRequestParamsObject(v interface{}) (*PrivateRequestParams, error)
|
|||
vt = vt.Elem()
|
||||
}
|
||||
|
||||
|
||||
if vt.Kind() != reflect.Struct {
|
||||
return nil, errors.New("reflect error: given object is not a struct" + vt.Kind().String())
|
||||
}
|
||||
|
|
|
@ -49,6 +49,8 @@ func NewStream(key, secret string) *Stream {
|
|||
return
|
||||
}
|
||||
|
||||
newbook.Symbol = toGlobalSymbol(e.Market)
|
||||
|
||||
switch e.Event {
|
||||
case "snapshot":
|
||||
stream.EmitBookSnapshot(newbook)
|
||||
|
@ -89,7 +91,7 @@ func NewStream(key, secret string) *Stream {
|
|||
}
|
||||
|
||||
func (s *Stream) Subscribe(channel types.Channel, symbol string, options types.SubscribeOptions) {
|
||||
s.websocketService.Subscribe(string(channel), symbol)
|
||||
s.websocketService.Subscribe(string(channel), toLocalSymbol(symbol))
|
||||
}
|
||||
|
||||
func (s *Stream) Connect(ctx context.Context) error {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package fixedpoint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const DefaultPrecision = 8
|
||||
|
@ -39,6 +42,33 @@ func (v Value) Add(v2 Value) Value {
|
|||
return Value(int64(v) + int64(v2))
|
||||
}
|
||||
|
||||
func (v *Value) UnmarshalJSON(data []byte) error {
|
||||
var a interface{}
|
||||
var err = json.Unmarshal(data, &a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch d := a.(type) {
|
||||
case float64:
|
||||
*v = NewFromFloat(d)
|
||||
|
||||
case float32:
|
||||
*v = NewFromFloat32(d)
|
||||
|
||||
case int:
|
||||
*v = NewFromInt(d)
|
||||
case int64:
|
||||
*v = NewFromInt64(d)
|
||||
|
||||
default:
|
||||
return errors.Errorf("unsupported type: %T %v", d, d)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFromString(input string) (Value, error) {
|
||||
v, err := strconv.ParseFloat(input, 64)
|
||||
if err != nil {
|
||||
|
@ -52,6 +82,10 @@ func NewFromFloat(val float64) Value {
|
|||
return Value(int64(math.Round(val * DefaultPow)))
|
||||
}
|
||||
|
||||
func NewFromFloat32(val float32) Value {
|
||||
return Value(int64(math.Round(float64(val) * DefaultPow)))
|
||||
}
|
||||
|
||||
func NewFromInt(val int) Value {
|
||||
return Value(int64(val * DefaultPow))
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ func (s *Strategy) SetMaxAssetQuantity(q float64) *Strategy {
|
|||
return s
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||
|
||||
session.Stream.OnKLineClosed(func(kline types.KLine) {
|
||||
|
@ -63,7 +63,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s
|
|||
}
|
||||
}
|
||||
|
||||
err := orderExecutor.SubmitOrder(ctx, types.SubmitOrder{
|
||||
_, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
Symbol: kline.Symbol,
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeMarket,
|
||||
|
|
53
pkg/strategy/skeleton/strategy.go
Normal file
53
pkg/strategy/skeleton/strategy.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package skeleton
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterExchangeStrategy("skeleton", &Strategy{})
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
Symbol string `json:"symbol"`
|
||||
}
|
||||
|
||||
func New(symbol string) *Strategy {
|
||||
return &Strategy{
|
||||
Symbol: symbol,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
|
||||
session.Stream.OnKLineClosed(func(kline types.KLine) {
|
||||
market, ok := session.Market(s.Symbol)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
quoteBalance, ok := session.Account.Balance(market.QuoteCurrency)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
_ = quoteBalance
|
||||
|
||||
_, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
Symbol: kline.Symbol,
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeMarket,
|
||||
Quantity: 0.01,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Error("submit order error")
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
182
pkg/strategy/xpuremaker/strategy.go
Normal file
182
pkg/strategy/xpuremaker/strategy.go
Normal file
|
@ -0,0 +1,182 @@
|
|||
package xpuremaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterExchangeStrategy("xpuremaker", &Strategy{})
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Side string `json:"side"`
|
||||
NumOrders int `json:"numOrders"`
|
||||
BehindVolume fixedpoint.Value `json:"behindVolume"`
|
||||
PriceTick fixedpoint.Value `json:"priceTick"`
|
||||
BaseQuantity fixedpoint.Value `json:"baseQuantity"`
|
||||
BuySellRatio float64 `json:"buySellRatio"`
|
||||
|
||||
book *types.StreamOrderBook
|
||||
activeOrders map[string]types.Order
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
|
||||
|
||||
s.book = types.NewStreamBook(s.Symbol)
|
||||
s.book.BindStream(session.Stream)
|
||||
|
||||
s.activeOrders = make(map[string]types.Order)
|
||||
|
||||
// We can move the go routine to the parent level.
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
s.update(orderExecutor, session)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-s.book.C:
|
||||
s.update(orderExecutor, session)
|
||||
|
||||
case <-ticker.C:
|
||||
s.update(orderExecutor, session)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) cancelOrders(session *bbgo.ExchangeSession) {
|
||||
var deletedIDs []string
|
||||
for clientOrderID, o := range s.activeOrders {
|
||||
log.Infof("canceling order: %+v", o)
|
||||
|
||||
if err := session.Exchange.CancelOrders(context.Background(), o); err != nil {
|
||||
log.WithError(err).Error("cancel order error")
|
||||
continue
|
||||
}
|
||||
|
||||
deletedIDs = append(deletedIDs, clientOrderID)
|
||||
}
|
||||
s.book.C.Drain(1*time.Second, 3*time.Second)
|
||||
|
||||
for _, id := range deletedIDs {
|
||||
delete(s.activeOrders, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) update(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) {
|
||||
s.cancelOrders(session)
|
||||
|
||||
switch s.Side {
|
||||
case "buy":
|
||||
s.updateOrders(orderExecutor, session, types.SideTypeBuy)
|
||||
case "sell":
|
||||
s.updateOrders(orderExecutor, session, types.SideTypeSell)
|
||||
case "both":
|
||||
s.updateOrders(orderExecutor, session, types.SideTypeBuy)
|
||||
s.updateOrders(orderExecutor, session, types.SideTypeSell)
|
||||
|
||||
default:
|
||||
log.Panicf("undefined side: %s", s.Side)
|
||||
}
|
||||
|
||||
s.book.C.Drain(1*time.Second, 3*time.Second)
|
||||
}
|
||||
|
||||
func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession, side types.SideType) {
|
||||
var book = s.book.Copy()
|
||||
var pvs = book.PriceVolumesBySide(side)
|
||||
if pvs == nil || len(pvs) == 0 {
|
||||
log.Warnf("empty side: %s", side)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("placing order behind volume: %f", s.BehindVolume.Float64())
|
||||
|
||||
idx := pvs.IndexByVolumeDepth(s.BehindVolume)
|
||||
if idx == -1 || idx > len(pvs)-1 {
|
||||
// do not place orders
|
||||
log.Warn("depth is not enough")
|
||||
return
|
||||
}
|
||||
|
||||
var depthPrice = pvs[idx].Price
|
||||
var orders = s.generateOrders(s.Symbol, side, depthPrice, s.PriceTick, s.BaseQuantity, s.NumOrders)
|
||||
if len(orders) == 0 {
|
||||
log.Warn("empty orders")
|
||||
return
|
||||
}
|
||||
|
||||
createdOrders, err := orderExecutor.SubmitOrders(context.Background(), orders...)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("order submit error")
|
||||
return
|
||||
}
|
||||
|
||||
// add created orders to the list
|
||||
for i, o := range createdOrders {
|
||||
s.activeOrders[o.ClientOrderID] = createdOrders[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseQuantity fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) {
|
||||
var expBase = fixedpoint.NewFromFloat(0.0)
|
||||
|
||||
switch side {
|
||||
case types.SideTypeBuy:
|
||||
if priceTick > 0 {
|
||||
priceTick = -priceTick
|
||||
}
|
||||
|
||||
case types.SideTypeSell:
|
||||
if priceTick < 0 {
|
||||
priceTick = -priceTick
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < numOrders; i++ {
|
||||
volume := math.Exp(expBase.Float64()) * baseQuantity.Float64()
|
||||
|
||||
// skip order less than 10usd
|
||||
if volume*price.Float64() < 10.0 {
|
||||
log.Warnf("amount too small (< 10usd). price=%f volume=%f amount=%f", price.Float64(), volume, volume*price.Float64())
|
||||
continue
|
||||
}
|
||||
|
||||
orders = append(orders, types.SubmitOrder{
|
||||
Symbol: symbol,
|
||||
Side: side,
|
||||
Type: types.OrderTypeLimit,
|
||||
Price: price.Float64(),
|
||||
Quantity: volume,
|
||||
})
|
||||
|
||||
log.Infof("%s order: %.2f @ %f", side, volume, price.Float64())
|
||||
|
||||
if len(orders) >= numOrders {
|
||||
break
|
||||
}
|
||||
|
||||
price = price + priceTick
|
||||
declog := math.Log10(math.Abs(priceTick.Float64()))
|
||||
expBase += fixedpoint.NewFromFloat(math.Pow10(-int(declog)) * math.Abs(priceTick.Float64()))
|
||||
}
|
||||
|
||||
return orders
|
||||
}
|
|
@ -54,7 +54,11 @@ type Exchange interface {
|
|||
|
||||
QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []Withdraw, err error)
|
||||
|
||||
SubmitOrder(ctx context.Context, order SubmitOrder) error
|
||||
SubmitOrders(ctx context.Context, orders ...SubmitOrder) (createdOrders []Order, err error)
|
||||
|
||||
QueryOpenOrders(ctx context.Context, symbol string) (orders []Order, err error)
|
||||
|
||||
CancelOrders(ctx context.Context, orders ...Order) error
|
||||
}
|
||||
|
||||
type TradeQueryOptions struct {
|
||||
|
|
|
@ -26,8 +26,7 @@ type Market struct {
|
|||
TickSize float64
|
||||
}
|
||||
|
||||
func (m Market) FormatPrice(val float64) string {
|
||||
|
||||
func (m Market) FormatPriceCurrency(val float64) string {
|
||||
switch m.QuoteCurrency {
|
||||
|
||||
case "USD", "USDT":
|
||||
|
@ -41,10 +40,19 @@ func (m Market) FormatPrice(val float64) string {
|
|||
|
||||
}
|
||||
|
||||
return m.FormatPrice(val)
|
||||
}
|
||||
|
||||
func (m Market) FormatPrice(val float64) string {
|
||||
|
||||
p := math.Pow10(m.PricePrecision)
|
||||
val = math.Trunc(val*p) / p
|
||||
return strconv.FormatFloat(val, 'f', m.PricePrecision, 64)
|
||||
}
|
||||
|
||||
func (m Market) FormatVolume(val float64) string {
|
||||
p := math.Pow10(m.PricePrecision)
|
||||
val = math.Trunc(val*p) / p
|
||||
return strconv.FormatFloat(val, 'f', m.VolumePrecision, 64)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"github.com/adshao/go-binance"
|
||||
"time"
|
||||
|
||||
"github.com/slack-go/slack"
|
||||
)
|
||||
|
||||
|
@ -9,23 +10,50 @@ import (
|
|||
type OrderType string
|
||||
|
||||
const (
|
||||
OrderTypeLimit OrderType = "LIMIT"
|
||||
OrderTypeMarket OrderType = "MARKET"
|
||||
OrderTypeLimit OrderType = "LIMIT"
|
||||
OrderTypeMarket OrderType = "MARKET"
|
||||
OrderTypeStopLimit OrderType = "STOP_LIMIT"
|
||||
OrderTypeStopMarket OrderType = "STOP_MARKET"
|
||||
)
|
||||
|
||||
type OrderStatus string
|
||||
|
||||
const (
|
||||
OrderStatusNew OrderStatus = "NEW"
|
||||
OrderStatusFilled OrderStatus = "FILLED"
|
||||
OrderStatusPartiallyFilled OrderStatus = "PARTIALLY_FILLED"
|
||||
OrderStatusCanceled OrderStatus = "CANCELED"
|
||||
OrderStatusRejected OrderStatus = "REJECTED"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
SubmitOrder
|
||||
|
||||
OrderID uint64 `json:"orderID"` // order id
|
||||
Status OrderStatus `json:"status"`
|
||||
ExecutedQuantity float64 `json:"executedQuantity"`
|
||||
CreationTime time.Time
|
||||
}
|
||||
|
||||
type SubmitOrder struct {
|
||||
Symbol string
|
||||
Side SideType
|
||||
Type OrderType
|
||||
ClientOrderID string `json:"clientOrderID"`
|
||||
|
||||
Symbol string
|
||||
Side SideType
|
||||
Type OrderType
|
||||
|
||||
Quantity float64
|
||||
Price float64
|
||||
StopPrice float64
|
||||
|
||||
Market Market
|
||||
|
||||
// TODO: we can probably remove these field
|
||||
StopPriceString string
|
||||
PriceString string
|
||||
QuantityString string
|
||||
|
||||
TimeInForce binance.TimeInForceType
|
||||
TimeInForce string `json:"timeInForce"` // GTC, IOC, FOK
|
||||
}
|
||||
|
||||
func (o *SubmitOrder) SlackAttachment() slack.Attachment {
|
||||
|
|
|
@ -116,6 +116,19 @@ type OrderBook struct {
|
|||
asksChangeCallbacks []func(pvs PriceVolumeSlice)
|
||||
}
|
||||
|
||||
func (b *OrderBook) PriceVolumesBySide(side SideType) PriceVolumeSlice {
|
||||
switch side {
|
||||
|
||||
case SideTypeBuy:
|
||||
return b.Bids
|
||||
|
||||
case SideTypeSell:
|
||||
return b.Asks
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *OrderBook) Copy() (book OrderBook) {
|
||||
book = *b
|
||||
book.Bids = b.Bids.Copy()
|
||||
|
|
|
@ -6,12 +6,14 @@ type SideType string
|
|||
const (
|
||||
SideTypeBuy = SideType("BUY")
|
||||
SideTypeSell = SideType("SELL")
|
||||
SideTypeSelf = SideType("SELF")
|
||||
)
|
||||
|
||||
func (side SideType) Color() string {
|
||||
if side == SideTypeBuy {
|
||||
return Green
|
||||
}
|
||||
|
||||
if side == SideTypeSell {
|
||||
return Red
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ type Trade struct {
|
|||
QuoteQuantity float64 `json:"quoteQuantity" db:"quote_quantity"`
|
||||
Symbol string `json:"symbol" db:"symbol"`
|
||||
|
||||
Side string `json:"side" db:"side"`
|
||||
Side SideType `json:"side" db:"side"`
|
||||
IsBuyer bool `json:"isBuyer" db:"is_buyer"`
|
||||
IsMaker bool `json:"isMaker" db:"is_maker"`
|
||||
Time time.Time `json:"tradedAt" db:"traded_at"`
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package types
|
||||
|
||||
import "context"
|
||||
|
||||
type OrderExecutor interface {
|
||||
SubmitOrder(ctx context.Context, order SubmitOrder) error
|
||||
}
|
||||
|
||||
type OrderExecutionRouter interface {
|
||||
// SubmitOrderTo submit order to a specific exchange session
|
||||
SubmitOrderTo(ctx context.Context, session string, order SubmitOrder) error
|
||||
}
|
Loading…
Reference in New Issue
Block a user