From 944b673626b1e97e5f19069044def0d4fd3337c0 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Oct 2020 17:58:45 +0800 Subject: [PATCH 01/12] Add skeleton strategy --- pkg/strategy/skeleton/strategy.go | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 pkg/strategy/skeleton/strategy.go diff --git a/pkg/strategy/skeleton/strategy.go b/pkg/strategy/skeleton/strategy.go new file mode 100644 index 000000000..5e7893fde --- /dev/null +++ b/pkg/strategy/skeleton/strategy.go @@ -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 types.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.SubmitOrder(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 +} From 3721714f009bafe0576c2066e6fea1ee42b57b74 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Oct 2020 18:21:43 +0800 Subject: [PATCH 02/12] Support json unmarshaller for fixedpoint --- pkg/fixedpoint/convert.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/pkg/fixedpoint/convert.go b/pkg/fixedpoint/convert.go index b7fd3e9e7..8d065288c 100644 --- a/pkg/fixedpoint/convert.go +++ b/pkg/fixedpoint/convert.go @@ -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)) } From 1e12de28dac89401a1346882aefb5b360e8777cd Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Oct 2020 18:22:23 +0800 Subject: [PATCH 03/12] Add xpuremaker skeleton --- config/bbgo.yaml | 1 + pkg/cmd/run.go | 1 + pkg/strategy/xpuremaker/strategy.go | 182 ++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 pkg/strategy/xpuremaker/strategy.go diff --git a/config/bbgo.yaml b/config/bbgo.yaml index 7763bcb78..8d48fe93a 100644 --- a/config/bbgo.yaml +++ b/config/bbgo.yaml @@ -1,6 +1,7 @@ --- imports: - github.com/c9s/bbgo/pkg/strategy/buyandhold +- github.com/c9s/bbgo/pkg/strategy/xpuremaker notifications: slack: defaultChannel: "bbgo" diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 461d91dfb..0fade8b27 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -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.") diff --git a/pkg/strategy/xpuremaker/strategy.go b/pkg/strategy/xpuremaker/strategy.go new file mode 100644 index 000000000..a928cbd99 --- /dev/null +++ b/pkg/strategy/xpuremaker/strategy.go @@ -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 +} + +func New(symbol string) *Strategy { + return &Strategy{ + Symbol: symbol, + } +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, session *bbgo.ExchangeSession) error { + session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{}) + + s.book = types.NewStreamBook(s.Symbol) + s.book.BindStream(session.Stream) + + // We can move the go routine to the parent level. + go func() { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-s.book.C: + s.book.C.Drain(2*time.Second, 5*time.Second) + s.update() + + case <-ticker.C: + s.update() + } + } + }() + + /* + 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.SubmitOrder(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 +} + +func (s *Strategy) update() { + switch s.Side { + case "buy": + s.updateOrders(types.SideTypeBuy) + case "sell": + s.updateOrders(types.SideTypeSell) + case "both": + s.updateOrders(types.SideTypeBuy) + s.updateOrders(types.SideTypeSell) + } +} + +func (s *Strategy) updateOrders(side types.SideType) { + book := s.book.Copy() + + var pvs types.PriceVolumeSlice + + switch side { + case types.SideTypeBuy: + pvs = book.Bids + case types.SideTypeSell: + pvs = book.Asks + } + + if pvs == nil || len(pvs) == 0 { + log.Warn("empty bids or asks") + return + } + + index := pvs.IndexByVolumeDepth(s.BehindVolume) + if index == -1 { + // do not place orders + log.Warn("depth is not enough") + return + } + + var price = pvs[index].Price + var orders = s.generateOrders(s.Symbol, side, price, s.PriceTick, s.BaseQuantity, s.NumOrders) + if len(orders) == 0 { + log.Warn("empty orders") + return + } + log.Infof("submitting %d orders", len(orders)) +} + +func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseVolume 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()) * baseVolume.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 @ %.3f", 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())) + log.Infof("expBase: %f", expBase.Float64()) + } + + return orders +} From 0d570dd4c829d272cf6fca52221827fdedd15a6b Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 24 Oct 2020 20:08:08 +0800 Subject: [PATCH 04/12] Add sample config for xpuremaker --- config/xpuremaker.yaml | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 config/xpuremaker.yaml diff --git a/config/xpuremaker.yaml b/config/xpuremaker.yaml new file mode 100644 index 000000000..cd8072f63 --- /dev/null +++ b/config/xpuremaker.yaml @@ -0,0 +1,41 @@ +--- +notifications: + slack: + defaultChannel: "bbgo" + errorChannel: "bbgo-error" + +reportTrades: + channelBySymbol: + "btcusdt": "bbgo-btcusdt" + "ethusdt": "bbgo-ethusdt" + "bnbusdt": "bbgo-bnbusdt" + "sxpusdt": "bbgo-sxpusdt" + +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.01 + baseQuantity: 100.0 From 308427416a98a31d754b985fe00edb3201e91067 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 25 Oct 2020 18:26:10 +0800 Subject: [PATCH 05/12] Add more exchange order features - use uuid for client order id - add stop limit and stop market order types - add order convert functions - improve submit orders --- config/bbgo.yaml | 9 + config/xpuremaker.yaml | 1 + examples/max-eqmaker/main.go | 2 +- pkg/bbgo/environment.go | 2 +- pkg/bbgo/loader.go | 2 - pkg/bbgo/loader_test.go | 2 - pkg/bbgo/order_processor.go | 2 +- pkg/bbgo/trader.go | 51 ++++-- pkg/exchange/binance/convert.go | 150 ++++++++++++++++ pkg/exchange/binance/exchange.go | 140 ++++++--------- pkg/exchange/binance/parse.go | 3 +- pkg/exchange/max/convert.go | 217 +++++++++++++++++++++++ pkg/exchange/max/exchange.go | 180 +++++-------------- pkg/exchange/max/maxapi/order.go | 90 +++++++--- pkg/exchange/max/maxapi/public_parser.go | 2 + pkg/exchange/max/maxapi/restapi.go | 1 - pkg/exchange/max/stream.go | 4 +- pkg/strategy/buyandhold/strategy.go | 2 +- pkg/strategy/skeleton/strategy.go | 2 +- pkg/strategy/xpuremaker/strategy.go | 31 ++-- pkg/types/exchange.go | 4 +- pkg/types/market.go | 12 +- pkg/types/order.go | 34 +++- pkg/types/side.go | 2 + pkg/types/trade.go | 2 +- pkg/types/trader.go | 4 +- 26 files changed, 658 insertions(+), 293 deletions(-) delete mode 100644 pkg/bbgo/loader.go delete mode 100644 pkg/bbgo/loader_test.go create mode 100644 pkg/exchange/binance/convert.go create mode 100644 pkg/exchange/max/convert.go diff --git a/config/bbgo.yaml b/config/bbgo.yaml index 8d48fe93a..400df0f73 100644 --- a/config/bbgo.yaml +++ b/config/bbgo.yaml @@ -2,6 +2,7 @@ imports: - github.com/c9s/bbgo/pkg/strategy/buyandhold - github.com/c9s/bbgo/pkg/strategy/xpuremaker + notifications: slack: defaultChannel: "bbgo" @@ -40,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 diff --git a/config/xpuremaker.yaml b/config/xpuremaker.yaml index cd8072f63..c75bc4873 100644 --- a/config/xpuremaker.yaml +++ b/config/xpuremaker.yaml @@ -10,6 +10,7 @@ reportTrades: "ethusdt": "bbgo-ethusdt" "bnbusdt": "bbgo-bnbusdt" "sxpusdt": "bbgo-sxpusdt" + "maxusdt": "max-maxusdt" reportPnL: - averageCostBySymbols: diff --git a/examples/max-eqmaker/main.go b/examples/max-eqmaker/main.go index b393b9693..c343839dc 100644 --- a/examples/max-eqmaker/main.go +++ b/examples/max-eqmaker/main.go @@ -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), diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index cb661165c..d61105a41 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -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) } } diff --git a/pkg/bbgo/loader.go b/pkg/bbgo/loader.go deleted file mode 100644 index 920078f66..000000000 --- a/pkg/bbgo/loader.go +++ /dev/null @@ -1,2 +0,0 @@ -package bbgo - diff --git a/pkg/bbgo/loader_test.go b/pkg/bbgo/loader_test.go deleted file mode 100644 index 920078f66..000000000 --- a/pkg/bbgo/loader_test.go +++ /dev/null @@ -1,2 +0,0 @@ -package bbgo - diff --git a/pkg/bbgo/order_processor.go b/pkg/bbgo/order_processor.go index 753f2821a..8ae5d888a 100644 --- a/pkg/bbgo/order_processor.go +++ b/pkg/bbgo/order_processor.go @@ -126,7 +126,7 @@ func (p *OrderProcessor) Submit(ctx context.Context, order types.SubmitOrder) er order.QuantityString = market.FormatVolume(quantity) */ - return p.Exchange.SubmitOrder(ctx, order) + return p.Exchange.SubmitOrders(ctx, order) } func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount float64) float64 { diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 5a1e8ac21..2ebff9431 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -81,7 +81,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 +152,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 } @@ -325,30 +326,52 @@ type ExchangeOrderExecutionRouter struct { sessions map[string]*ExchangeSession } -func (e *ExchangeOrderExecutionRouter) SubmitOrderTo(ctx context.Context, session string, order types.SubmitOrder) error { +func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, session string, orders ...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) + for _, order := range orders { + market, ok := es.Market(order.Symbol) + if !ok { + return errors.Errorf("market is not defined: %s", order.Symbol) + } - order.PriceString = order.Market.FormatVolume(order.Price) - order.QuantityString = order.Market.FormatVolume(order.Quantity) - return es.Exchange.SubmitOrder(ctx, order) + order.PriceString = market.FormatPrice(order.Price) + order.QuantityString = market.FormatVolume(order.Quantity) + e.Notify(":memo: Submitting order to %s %s %s %s with quantity: %s", session, order.Symbol, order.Type, order.Side, order.QuantityString, order) + + if err := es.Exchange.SubmitOrders(ctx, order); err != nil { + return err + } + } + + return nil } // ExchangeOrderExecutor is an order executor wrapper for single exchange instance. type ExchangeOrderExecutor struct { Notifiability - Exchange types.Exchange + Session *ExchangeSession } -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) +func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) error { + for _, order := range orders { + market, ok := e.Session.Market(order.Symbol) + if !ok { + return errors.Errorf("market is not defined: %s", order.Symbol) + } - order.PriceString = order.Market.FormatVolume(order.Price) - order.QuantityString = order.Market.FormatVolume(order.Quantity) - return e.Exchange.SubmitOrder(ctx, order) + order.Market = market + order.PriceString = market.FormatPrice(order.Price) + order.QuantityString = market.FormatVolume(order.Quantity) + + 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, order) + } + + return nil } diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go new file mode 100644 index 000000000..afc4d7b4f --- /dev/null +++ b/pkg/exchange/binance/convert.go @@ -0,0 +1,150 @@ +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{ + 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) +} diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 3d47b3d57..a1ca5589f 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -3,10 +3,10 @@ package binance import ( "context" "fmt" - "strconv" "time" "github.com/adshao/go-binance" + "github.com/google/uuid" "github.com/sirupsen/logrus" @@ -55,7 +55,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 +65,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,7 +268,25 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { return a, nil } -func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) error { +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 orders, err + } + + for _, binanceOrder := range remoteOrders { + order , err := toGlobalOrder(binanceOrder) + if err != nil { + return orders, err + } + + orders = append(orders, *order) + } + + return orders, err +} + +func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) error { /* limit order example @@ -281,40 +299,39 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) err Price(priceString). Do(ctx) */ + for _, order := range orders { + orderType, err := toLocalOrderType(order.Type) + if err != nil { + return err + } - orderType, err := toLocalOrderType(order.Type) - if err != nil { - return err + clientOrderID := uuid.New().String() + 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)) + } + + retOrder, err := req.Do(ctx) + if err != nil { + return err + } + + log.Infof("order created: %+v", retOrder) } - req := e.Client.NewCreateOrderService(). - Symbol(order.Symbol). - Side(binance.SideType(order.Side)). - Type(orderType). - Quantity(order.QuantityString) - - if len(order.PriceString) > 0 { - req.Price(order.PriceString) - } - if len(order.TimeInForce) > 0 { - req.TimeInForce(order.TimeInForce) - } - - retOrder, err := req.Do(ctx) - log.Infof("order created: %+v", retOrder) - return err -} - -func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) { - switch orderType { - case types.OrderTypeLimit: - return binance.OrderTypeLimit, nil - - case types.OrderTypeMarket: - return binance.OrderTypeMarket, nil - } - - return "", fmt.Errorf("order type %s not supported", orderType) + return nil } func (e *Exchange) QueryKLines(ctx context.Context, symbol, interval string, options types.KLineQueryOptions) ([]types.KLine, error) { @@ -393,7 +410,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 +423,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 @@ -496,3 +465,4 @@ func (e *Exchange) BatchQueryKLineWindows(ctx context.Context, symbol string, in return klineWindows, nil } + diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 9f4de6207..ed5e682a7 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/adshao/go-binance" "github.com/valyala/fastjson" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -99,7 +100,7 @@ func (e *ExecutionReportEvent) Trade() (*types.Trade, error) { Price: util.MustParseFloat(e.LastExecutedPrice), Quantity: util.MustParseFloat(e.LastExecutedQuantity), QuoteQuantity: util.MustParseFloat(e.LastQuoteAssetTransactedQuantity), - Side: e.Side, + Side: toGlobalSideType(binance.SideType(e.Side)), IsBuyer: e.Side == "BUY", IsMaker: e.IsMaker, Time: tt, diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go new file mode 100644 index 000000000..f2da72479 --- /dev/null +++ b/pkg/exchange/max/convert.go @@ -0,0 +1,217 @@ +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{ + 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) +} diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index ba30f9ac1..9a3a061f4 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -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,52 @@ 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) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) error { + for _, order := range orders { + orderType, err := toLocalOrderType(order.Type) + if err != nil { + return err + } + + clientOrderID := uuid.New().String() + req := e.client.OrderService.NewCreateOrderRequest(). + Market(toLocalSymbol(order.Symbol)). + OrderType(string(orderType)). + Side(toLocalSideType(order.Side)). + ClientOrderID(clientOrderID). + Volume(order.QuantityString) + + if len(order.PriceString) > 0 { + req.Price(order.PriceString) + } + + retOrder, err := req.Do(ctx) + if err != nil { + return err + } + + logger.Infof("order created: %+v", retOrder) + } + + return nil } // PlatformFeeCurrency @@ -230,7 +256,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 +266,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 +306,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 +361,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 -} diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index d672c126a..bf8666af6 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -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. @@ -281,12 +319,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 +373,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") } diff --git a/pkg/exchange/max/maxapi/public_parser.go b/pkg/exchange/max/maxapi/public_parser.go index 1a487654a..b78d6ad2d 100644 --- a/pkg/exchange/max/maxapi/public_parser.go +++ b/pkg/exchange/max/maxapi/public_parser.go @@ -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 { diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index e275d5ac5..27d14e3f8 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -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()) } diff --git a/pkg/exchange/max/stream.go b/pkg/exchange/max/stream.go index 1f02067a1..5dbfcc07a 100644 --- a/pkg/exchange/max/stream.go +++ b/pkg/exchange/max/stream.go @@ -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 { diff --git a/pkg/strategy/buyandhold/strategy.go b/pkg/strategy/buyandhold/strategy.go index a26d35783..396f034e5 100644 --- a/pkg/strategy/buyandhold/strategy.go +++ b/pkg/strategy/buyandhold/strategy.go @@ -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, diff --git a/pkg/strategy/skeleton/strategy.go b/pkg/strategy/skeleton/strategy.go index 5e7893fde..cd50038b7 100644 --- a/pkg/strategy/skeleton/strategy.go +++ b/pkg/strategy/skeleton/strategy.go @@ -37,7 +37,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s } _ = quoteBalance - err := orderExecutor.SubmitOrder(ctx, types.SubmitOrder{ + err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: kline.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeMarket, diff --git a/pkg/strategy/xpuremaker/strategy.go b/pkg/strategy/xpuremaker/strategy.go index a928cbd99..3fa0a0a57 100644 --- a/pkg/strategy/xpuremaker/strategy.go +++ b/pkg/strategy/xpuremaker/strategy.go @@ -45,6 +45,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() + s.update(orderExecutor) + for { select { case <-ctx.Done(): @@ -52,10 +54,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s case <-s.book.C: s.book.C.Drain(2*time.Second, 5*time.Second) - s.update() + s.update(orderExecutor) case <-ticker.C: - s.update() + s.update(orderExecutor) } } }() @@ -89,19 +91,22 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s return nil } -func (s *Strategy) update() { +func (s *Strategy) update(orderExecutor types.OrderExecutor) { switch s.Side { case "buy": - s.updateOrders(types.SideTypeBuy) + s.updateOrders(orderExecutor, types.SideTypeBuy) case "sell": - s.updateOrders(types.SideTypeSell) + s.updateOrders(orderExecutor, types.SideTypeSell) case "both": - s.updateOrders(types.SideTypeBuy) - s.updateOrders(types.SideTypeSell) + s.updateOrders(orderExecutor, types.SideTypeBuy) + s.updateOrders(orderExecutor, types.SideTypeSell) + + default: + log.Panicf("undefined side: %s", s.Side) } } -func (s *Strategy) updateOrders(side types.SideType) { +func (s *Strategy) updateOrders(orderExecutor types.OrderExecutor, side types.SideType) { book := s.book.Copy() var pvs types.PriceVolumeSlice @@ -118,6 +123,8 @@ func (s *Strategy) updateOrders(side types.SideType) { return } + log.Infof("placing order behind volume: %f", s.BehindVolume.Float64()) + index := pvs.IndexByVolumeDepth(s.BehindVolume) if index == -1 { // do not place orders @@ -132,6 +139,10 @@ func (s *Strategy) updateOrders(side types.SideType) { return } log.Infof("submitting %d orders", len(orders)) + if err := orderExecutor.SubmitOrders(context.Background(), orders...); err != nil { + log.WithError(err).Errorf("order submit error") + return + } } func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseVolume fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) { @@ -166,7 +177,7 @@ func (s *Strategy) generateOrders(symbol string, side types.SideType, price, pri Quantity: volume, }) - log.Infof("%s order: %.2f @ %.3f", side, volume, price.Float64()) + log.Infof("%s order: %.2f @ %f", side, volume, price.Float64()) if len(orders) >= numOrders { break @@ -175,7 +186,7 @@ func (s *Strategy) generateOrders(symbol string, side types.SideType, price, pri price = price + priceTick declog := math.Log10(math.Abs(priceTick.Float64())) expBase += fixedpoint.NewFromFloat(math.Pow10(-int(declog)) * math.Abs(priceTick.Float64())) - log.Infof("expBase: %f", expBase.Float64()) + // log.Infof("expBase: %f", expBase.Float64()) } return orders diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index b85e409c6..3acb030dc 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -54,7 +54,9 @@ 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) error + + QueryOpenOrders(ctx context.Context, symbol string) (orders []Order, err error) } type TradeQueryOptions struct { diff --git a/pkg/types/market.go b/pkg/types/market.go index 4af98d6ec..437a07235 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -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) } diff --git a/pkg/types/order.go b/pkg/types/order.go index 4b6033a7e..996d353f1 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -1,7 +1,6 @@ package types import ( - "github.com/adshao/go-binance" "github.com/slack-go/slack" ) @@ -9,14 +8,35 @@ 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"` +} + type SubmitOrder struct { - Symbol string - Side SideType - Type OrderType + Symbol string + Side SideType + Type OrderType + Quantity float64 Price float64 @@ -25,7 +45,7 @@ type SubmitOrder struct { PriceString string QuantityString string - TimeInForce binance.TimeInForceType + TimeInForce string `json:"timeInForce"` // GTC, IOC, FOK } func (o *SubmitOrder) SlackAttachment() slack.Attachment { diff --git a/pkg/types/side.go b/pkg/types/side.go index 7fd24785b..8d75f4893 100644 --- a/pkg/types/side.go +++ b/pkg/types/side.go @@ -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 } diff --git a/pkg/types/trade.go b/pkg/types/trade.go index 7637dbccf..f49a216d6 100644 --- a/pkg/types/trade.go +++ b/pkg/types/trade.go @@ -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"` diff --git a/pkg/types/trader.go b/pkg/types/trader.go index 933941db1..d89199bc3 100644 --- a/pkg/types/trader.go +++ b/pkg/types/trader.go @@ -3,10 +3,10 @@ package types import "context" type OrderExecutor interface { - SubmitOrder(ctx context.Context, order SubmitOrder) error + SubmitOrders(ctx context.Context, orders ...SubmitOrder) error } type OrderExecutionRouter interface { // SubmitOrderTo submit order to a specific exchange session - SubmitOrderTo(ctx context.Context, session string, order SubmitOrder) error + SubmitOrdersTo(ctx context.Context, session string, orders ...SubmitOrder) error } From 391767953abd42e25984a295b71459fdfa6fdedc Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 25 Oct 2020 18:30:06 +0800 Subject: [PATCH 06/12] Fix binance trade transaction time convertion --- pkg/exchange/binance/parse.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index ed5e682a7..b62816361 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -93,7 +93,7 @@ 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, From fa30f6b52a50265abc3f37439dd7c445b2f174f1 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 25 Oct 2020 18:56:07 +0800 Subject: [PATCH 07/12] Support binance order update execution type convertion --- pkg/exchange/binance/exchange.go | 4 ++ pkg/exchange/binance/parse.go | 35 +++++++++++++++-- pkg/exchange/binance/parse_test.go | 61 ++++++++++++++++++++++++++++++ pkg/exchange/max/exchange.go | 4 ++ pkg/types/order.go | 8 ++++ 5 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 pkg/exchange/binance/parse_test.go diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index a1ca5589f..64235c515 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -306,6 +306,10 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder } clientOrderID := uuid.New().String() + if len(order.ClientOrderID) > 0 { + clientOrderID = order.ClientOrderID + } + req := e.Client.NewCreateOrderService(). Symbol(order.Symbol). Side(binance.SideType(order.Side)). diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index b62816361..45d3ff299 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -75,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"` @@ -85,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) { @@ -93,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 * int64(time.Millisecond)) + 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: toGlobalSideType(binance.SideType(e.Side)), IsBuyer: e.Side == "BUY", IsMaker: e.IsMaker, Time: tt, diff --git a/pkg/exchange/binance/parse_test.go b/pkg/exchange/binance/parse_test.go new file mode 100644 index 000000000..298834a16 --- /dev/null +++ b/pkg/exchange/binance/parse_test.go @@ -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) +} diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 9a3a061f4..638415f24 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -98,6 +98,10 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder } clientOrderID := uuid.New().String() + if len(order.ClientOrderID) > 0 { + clientOrderID = order.ClientOrderID + } + req := e.client.OrderService.NewCreateOrderRequest(). Market(toLocalSymbol(order.Symbol)). OrderType(string(orderType)). diff --git a/pkg/types/order.go b/pkg/types/order.go index 996d353f1..859a21ac8 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -1,6 +1,8 @@ package types import ( + "time" + "github.com/slack-go/slack" ) @@ -30,18 +32,24 @@ type Order struct { OrderID uint64 `json:"orderID"` // order id Status OrderStatus `json:"status"` ExecutedQuantity float64 `json:"executedQuantity"` + CreationTime time.Time } type SubmitOrder struct { + 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 From de11ef10f51a42d9b85c0a649a2c5321d2905ff3 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 25 Oct 2020 19:18:03 +0800 Subject: [PATCH 08/12] return created order objects from SubmitOrders method --- go.mod | 2 +- go.sum | 2 ++ pkg/bbgo/order_processor.go | 4 ++- pkg/bbgo/trader.go | 10 ++++-- pkg/exchange/binance/exchange.go | 58 +++++++++++++++++++++----------- pkg/exchange/max/exchange.go | 17 +++++++--- pkg/exchange/max/maxapi/order.go | 4 +-- pkg/types/exchange.go | 2 +- 8 files changed, 68 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index f9ab3dd48..b82c1c8e7 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index e6a306b4c..6335a56dd 100644 --- a/go.sum +++ b/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= diff --git a/pkg/bbgo/order_processor.go b/pkg/bbgo/order_processor.go index 8ae5d888a..871879020 100644 --- a/pkg/bbgo/order_processor.go +++ b/pkg/bbgo/order_processor.go @@ -126,7 +126,9 @@ func (p *OrderProcessor) Submit(ctx context.Context, order types.SubmitOrder) er order.QuantityString = market.FormatVolume(quantity) */ - return p.Exchange.SubmitOrders(ctx, order) + createdOrders, err := p.Exchange.SubmitOrders(ctx, order) + _ = createdOrders + return err } func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount float64) float64 { diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 2ebff9431..bec928104 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -342,7 +342,8 @@ func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, sessi order.QuantityString = market.FormatVolume(order.Quantity) e.Notify(":memo: Submitting order to %s %s %s %s with quantity: %s", session, order.Symbol, order.Type, order.Side, order.QuantityString, order) - if err := es.Exchange.SubmitOrders(ctx, order); err != nil { + if createdOrders, err := es.Exchange.SubmitOrders(ctx, order); err != nil { + _ = createdOrders return err } } @@ -370,7 +371,12 @@ func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...type 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, order) + createdOrders, err := e.Session.Exchange.SubmitOrders(ctx, order) + if err != nil { + return err + } + + _ = createdOrders } return nil diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 64235c515..3a8467cba 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -7,6 +7,7 @@ import ( "github.com/adshao/go-binance" "github.com/google/uuid" + "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -275,7 +276,7 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ } for _, binanceOrder := range remoteOrders { - order , err := toGlobalOrder(binanceOrder) + order, err := toGlobalOrder(binanceOrder) if err != nil { return orders, err } @@ -286,23 +287,11 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ return orders, err } -func (e *Exchange) SubmitOrders(ctx context.Context, orders ...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) - */ +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 err + return createdOrders, err } clientOrderID := uuid.New().String() @@ -327,15 +316,45 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder req.TimeInForce(binance.TimeInForceType(order.TimeInForce)) } - retOrder, err := req.Do(ctx) + response, err := req.Do(ctx) if err != nil { - return err + return createdOrders, err } - log.Infof("order created: %+v", retOrder) + 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 nil + return createdOrders, err } func (e *Exchange) QueryKLines(ctx context.Context, symbol, interval string, options types.KLineQueryOptions) ([]types.KLine, error) { @@ -469,4 +488,3 @@ func (e *Exchange) BatchQueryKLineWindows(ctx context.Context, symbol string, in return klineWindows, nil } - diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index 638415f24..dbe97a5bc 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -90,11 +90,11 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ return orders, err } -func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) error { +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 err + return nil, err } clientOrderID := uuid.New().String() @@ -115,13 +115,22 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder retOrder, err := req.Do(ctx) if err != nil { - return err + 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 nil + return createdOrders, err } // PlatformFeeCurrency diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index bf8666af6..48cc1a8c2 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -384,8 +384,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 diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 3acb030dc..4ac299682 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -54,7 +54,7 @@ type Exchange interface { QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []Withdraw, err error) - SubmitOrders(ctx context.Context, orders ...SubmitOrder) error + SubmitOrders(ctx context.Context, orders ...SubmitOrder) (createdOrders []Order, err error) QueryOpenOrders(ctx context.Context, symbol string) (orders []Order, err error) } From 336fb4d25b082b5cc4cbf9e2dada21e3b6454b20 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 25 Oct 2020 22:41:54 +0800 Subject: [PATCH 09/12] max: fix order cancel request payload --- pkg/exchange/max/maxapi/order.go | 35 +++++++++++--------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index 48cc1a8c2..d88332aab 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -6,6 +6,7 @@ import ( "time" "github.com/pkg/errors" + log "github.com/sirupsen/logrus" ) type OrderStateToQuery int @@ -24,16 +25,16 @@ const ( OrderStateWait = OrderState("wait") OrderStateConvert = OrderState("convert") OrderStateFinalizing = OrderState("finalizing") - OrderStateFailed = OrderState("failed") + OrderStateFailed = OrderState("failed") ) type OrderType string // Order types that the API can return. const ( - OrderTypeMarket = OrderType("market") - OrderTypeLimit = OrderType("limit") - OrderTypeStopLimit = OrderType("stop_limit") + OrderTypeMarket = OrderType("market") + OrderTypeLimit = OrderType("limit") + OrderTypeStopLimit = OrderType("stop_limit") OrderTypeStopMarket = OrderType("stop_market") ) @@ -192,22 +193,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 { @@ -227,13 +216,13 @@ 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) + log.Infof("response: %+v", response) return err } @@ -268,7 +257,7 @@ func (s *OrderService) Get(orderID uint64) (*Order, error) { } type MultiOrderRequestParams struct { - PrivateRequestParams + *PrivateRequestParams Market string `json:"market"` Orders []Order `json:"orders"` From 145264aae45503c18f3afe330b3ab9c15f32e80c Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Oct 2020 00:26:17 +0800 Subject: [PATCH 10/12] cancel orders and re-submit maker orders --- pkg/bbgo/order_execution.go | 69 ++++++++++++++++++++++++++++ pkg/bbgo/trader.go | 71 ++++------------------------- pkg/exchange/binance/convert.go | 13 +++--- pkg/exchange/binance/exchange.go | 23 ++++++++++ pkg/exchange/max/convert.go | 1 + pkg/exchange/max/exchange.go | 35 +++++++++++--- pkg/exchange/max/maxapi/order.go | 11 ++++- pkg/strategy/buyandhold/strategy.go | 4 +- pkg/strategy/skeleton/strategy.go | 4 +- pkg/strategy/xpuremaker/strategy.go | 47 ++++++++++++++++--- pkg/types/exchange.go | 2 + pkg/types/trader.go | 12 ----- 12 files changed, 193 insertions(+), 99 deletions(-) create mode 100644 pkg/bbgo/order_execution.go delete mode 100644 pkg/types/trader.go diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go new file mode 100644 index 000000000..44a206cd8 --- /dev/null +++ b/pkg/bbgo/order_execution.go @@ -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...) +} diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index bec928104..0b09fc04e 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -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 { @@ -157,7 +156,7 @@ func (trader *Trader) Run(ctx context.Context) error { // that way we can mount the notification on the exchange with DSL orderExecutor := &ExchangeOrderExecutor{ Notifiability: trader.Notifiability, - Session: session, + session: session, } for _, strategy := range strategies { @@ -320,64 +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) SubmitOrdersTo(ctx context.Context, session string, orders ...types.SubmitOrder) error { - es, ok := e.sessions[session] - if !ok { - return errors.Errorf("exchange session %s not found", session) - } - - for _, order := range orders { - market, ok := es.Market(order.Symbol) - if !ok { - return errors.Errorf("market is not defined: %s", order.Symbol) - } - - order.PriceString = market.FormatPrice(order.Price) - order.QuantityString = market.FormatVolume(order.Quantity) - e.Notify(":memo: Submitting order to %s %s %s %s with quantity: %s", session, order.Symbol, order.Type, order.Side, order.QuantityString, order) - - if createdOrders, err := es.Exchange.SubmitOrders(ctx, order); err != nil { - _ = createdOrders - return err - } - } - - return nil -} - -// ExchangeOrderExecutor is an order executor wrapper for single exchange instance. -type ExchangeOrderExecutor struct { - Notifiability - - Session *ExchangeSession -} - -func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) error { - for _, order := range orders { - market, ok := e.Session.Market(order.Symbol) - if !ok { - return errors.Errorf("market is not defined: %s", order.Symbol) - } - - order.Market = market - order.PriceString = market.FormatPrice(order.Price) - order.QuantityString = market.FormatVolume(order.Quantity) - - e.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order) - - createdOrders, err := e.Session.Exchange.SubmitOrders(ctx, order) - if err != nil { - return err - } - - _ = createdOrders - } - - return nil +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) } diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go index afc4d7b4f..9ae077790 100644 --- a/pkg/exchange/binance/convert.go +++ b/pkg/exchange/binance/convert.go @@ -32,12 +32,13 @@ func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) { func toGlobalOrder(binanceOrder *binance.Order) (*types.Order, error) { return &types.Order{ SubmitOrder: types.SubmitOrder{ - 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), + 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), diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 3a8467cba..c51533797 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -287,6 +287,29 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ return orders, err } +func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err2 error) { + for _, o := range orders { + var req = e.Client.NewCancelOrderService() + + // 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 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) diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go index f2da72479..92130b5a6 100644 --- a/pkg/exchange/max/convert.go +++ b/pkg/exchange/max/convert.go @@ -139,6 +139,7 @@ func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { return &types.Order{ SubmitOrder: types.SubmitOrder{ + ClientOrderID: maxOrder.ClientOID, Symbol: toGlobalSymbol(maxOrder.Market), Side: toGlobalSideType(maxOrder.Side), Type: toGlobalOrderType(maxOrder.OrderType), diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index dbe97a5bc..747bcc874 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -90,25 +90,46 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ 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 nil, err - } - - clientOrderID := uuid.New().String() - if len(order.ClientOrderID) > 0 { - clientOrderID = order.ClientOrderID + return createdOrders, err } req := e.client.OrderService.NewCreateOrderRequest(). Market(toLocalSymbol(order.Symbol)). OrderType(string(orderType)). Side(toLocalSideType(order.Side)). - ClientOrderID(clientOrderID). 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) } diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index d88332aab..0b292c9a1 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -6,7 +6,6 @@ import ( "time" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" ) type OrderStateToQuery int @@ -222,7 +221,15 @@ func (r *OrderCancelRequest) Do(ctx context.Context) error { } response, err := r.client.sendRequest(req) - log.Infof("response: %+v", response) + if err != nil { + return err + } + + var order = Order{} + if err := response.DecodeJSON(&order); err != nil { + return err + } + return err } diff --git a/pkg/strategy/buyandhold/strategy.go b/pkg/strategy/buyandhold/strategy.go index 396f034e5..70ed576a4 100644 --- a/pkg/strategy/buyandhold/strategy.go +++ b/pkg/strategy/buyandhold/strategy.go @@ -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.SubmitOrders(ctx, types.SubmitOrder{ + _, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: kline.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeMarket, diff --git a/pkg/strategy/skeleton/strategy.go b/pkg/strategy/skeleton/strategy.go index cd50038b7..ac647a8f0 100644 --- a/pkg/strategy/skeleton/strategy.go +++ b/pkg/strategy/skeleton/strategy.go @@ -23,7 +23,7 @@ func New(symbol string) *Strategy { } } -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: "1m"}) session.Stream.OnKLineClosed(func(kline types.KLine) { market, ok := session.Market(s.Symbol) @@ -37,7 +37,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s } _ = quoteBalance - err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ + _, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: kline.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeMarket, diff --git a/pkg/strategy/xpuremaker/strategy.go b/pkg/strategy/xpuremaker/strategy.go index 3fa0a0a57..827527531 100644 --- a/pkg/strategy/xpuremaker/strategy.go +++ b/pkg/strategy/xpuremaker/strategy.go @@ -25,7 +25,8 @@ type Strategy struct { BaseQuantity fixedpoint.Value `json:"baseQuantity"` BuySellRatio float64 `json:"buySellRatio"` - book *types.StreamOrderBook + book *types.StreamOrderBook + activeOrders map[string]types.Order } func New(symbol string) *Strategy { @@ -34,12 +35,14 @@ func New(symbol string) *Strategy { } } -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.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) @@ -53,7 +56,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s return case <-s.book.C: - s.book.C.Drain(2*time.Second, 5*time.Second) s.update(orderExecutor) case <-ticker.C: @@ -91,7 +93,28 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s return nil } -func (s *Strategy) update(orderExecutor types.OrderExecutor) { +func (s *Strategy) cancelOrders(orderExecutor bbgo.OrderExecutor) { + var deletedIDs []string + for clientOrderID, o := range s.activeOrders { + log.Infof("canceling order: %+v", o) + + if err := orderExecutor.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) { + s.cancelOrders(orderExecutor) + switch s.Side { case "buy": s.updateOrders(orderExecutor, types.SideTypeBuy) @@ -104,9 +127,11 @@ func (s *Strategy) update(orderExecutor types.OrderExecutor) { default: log.Panicf("undefined side: %s", s.Side) } + + s.book.C.Drain(1*time.Second, 3*time.Second) } -func (s *Strategy) updateOrders(orderExecutor types.OrderExecutor, side types.SideType) { +func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, side types.SideType) { book := s.book.Copy() var pvs types.PriceVolumeSlice @@ -138,11 +163,21 @@ func (s *Strategy) updateOrders(orderExecutor types.OrderExecutor, side types.Si log.Warn("empty orders") return } + log.Infof("submitting %d orders", len(orders)) - if err := orderExecutor.SubmitOrders(context.Background(), orders...); err != nil { + 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 { + log.Infof("adding order: %s => %+v", o.ClientOrderID, o) + s.activeOrders[o.ClientOrderID] = createdOrders[i] + } + + log.Infof("active orders: %+v", s.activeOrders) } func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseVolume fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) { diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 4ac299682..dc9cb7761 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -57,6 +57,8 @@ type Exchange interface { 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 { diff --git a/pkg/types/trader.go b/pkg/types/trader.go deleted file mode 100644 index d89199bc3..000000000 --- a/pkg/types/trader.go +++ /dev/null @@ -1,12 +0,0 @@ -package types - -import "context" - -type OrderExecutor interface { - SubmitOrders(ctx context.Context, orders ...SubmitOrder) error -} - -type OrderExecutionRouter interface { - // SubmitOrderTo submit order to a specific exchange session - SubmitOrdersTo(ctx context.Context, session string, orders ...SubmitOrder) error -} From fbba9b12ce0a78fff5e972ce69d81e8ee03a7503 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Oct 2020 10:01:18 +0800 Subject: [PATCH 11/12] xpuremaker: final clean up --- pkg/strategy/xpuremaker/strategy.go | 58 ++++++----------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/pkg/strategy/xpuremaker/strategy.go b/pkg/strategy/xpuremaker/strategy.go index 827527531..e983277ed 100644 --- a/pkg/strategy/xpuremaker/strategy.go +++ b/pkg/strategy/xpuremaker/strategy.go @@ -29,12 +29,6 @@ type Strategy struct { activeOrders map[string]types.Order } -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.BookChannel, s.Symbol, types.SubscribeOptions{}) @@ -48,7 +42,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() - s.update(orderExecutor) + s.update(orderExecutor, session) for { select { @@ -56,49 +50,23 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return case <-s.book.C: - s.update(orderExecutor) + s.update(orderExecutor, session) case <-ticker.C: - s.update(orderExecutor) + s.update(orderExecutor, session) } } }() - /* - 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.SubmitOrder(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 } -func (s *Strategy) cancelOrders(orderExecutor bbgo.OrderExecutor) { +func (s *Strategy) cancelOrders(session *bbgo.ExchangeSession) { var deletedIDs []string for clientOrderID, o := range s.activeOrders { log.Infof("canceling order: %+v", o) - if err := orderExecutor.Session().Exchange.CancelOrders(context.Background(), o); err != nil { + if err := session.Exchange.CancelOrders(context.Background(), o); err != nil { log.WithError(err).Error("cancel order error") continue } @@ -112,8 +80,8 @@ func (s *Strategy) cancelOrders(orderExecutor bbgo.OrderExecutor) { } } -func (s *Strategy) update(orderExecutor bbgo.OrderExecutor) { - s.cancelOrders(orderExecutor) +func (s *Strategy) update(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) { + s.cancelOrders(session) switch s.Side { case "buy": @@ -150,21 +118,20 @@ func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, side types.Sid log.Infof("placing order behind volume: %f", s.BehindVolume.Float64()) - index := pvs.IndexByVolumeDepth(s.BehindVolume) - if index == -1 { + idx := pvs.IndexByVolumeDepth(s.BehindVolume) + if idx == -1 { // do not place orders log.Warn("depth is not enough") return } - var price = pvs[index].Price - var orders = s.generateOrders(s.Symbol, side, price, s.PriceTick, s.BaseQuantity, s.NumOrders) + 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 } - log.Infof("submitting %d orders", len(orders)) createdOrders, err := orderExecutor.SubmitOrders(context.Background(), orders...) if err != nil { log.WithError(err).Errorf("order submit error") @@ -173,11 +140,8 @@ func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, side types.Sid // add created orders to the list for i, o := range createdOrders { - log.Infof("adding order: %s => %+v", o.ClientOrderID, o) s.activeOrders[o.ClientOrderID] = createdOrders[i] } - - log.Infof("active orders: %+v", s.activeOrders) } func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseVolume fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) { From aa6ccbf905413b724b885deb7808dbfd2b68b69f Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 26 Oct 2020 10:08:58 +0800 Subject: [PATCH 12/12] refactor xpuremaker strategy --- config/xpuremaker.yaml | 2 +- pkg/strategy/xpuremaker/strategy.go | 32 ++++++++++------------------- pkg/types/orderbook.go | 13 ++++++++++++ 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/config/xpuremaker.yaml b/config/xpuremaker.yaml index c75bc4873..c145959ae 100644 --- a/config/xpuremaker.yaml +++ b/config/xpuremaker.yaml @@ -38,5 +38,5 @@ exchangeStrategies: numOrders: 2 side: both behindVolume: 1000.0 - priceTick: 0.01 + priceTick: 0.001 baseQuantity: 100.0 diff --git a/pkg/strategy/xpuremaker/strategy.go b/pkg/strategy/xpuremaker/strategy.go index e983277ed..f9506f4a5 100644 --- a/pkg/strategy/xpuremaker/strategy.go +++ b/pkg/strategy/xpuremaker/strategy.go @@ -85,12 +85,12 @@ func (s *Strategy) update(orderExecutor bbgo.OrderExecutor, session *bbgo.Exchan switch s.Side { case "buy": - s.updateOrders(orderExecutor, types.SideTypeBuy) + s.updateOrders(orderExecutor, session, types.SideTypeBuy) case "sell": - s.updateOrders(orderExecutor, types.SideTypeSell) + s.updateOrders(orderExecutor, session, types.SideTypeSell) case "both": - s.updateOrders(orderExecutor, types.SideTypeBuy) - s.updateOrders(orderExecutor, types.SideTypeSell) + s.updateOrders(orderExecutor, session, types.SideTypeBuy) + s.updateOrders(orderExecutor, session, types.SideTypeSell) default: log.Panicf("undefined side: %s", s.Side) @@ -99,27 +99,18 @@ func (s *Strategy) update(orderExecutor bbgo.OrderExecutor, session *bbgo.Exchan s.book.C.Drain(1*time.Second, 3*time.Second) } -func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, side types.SideType) { - book := s.book.Copy() - - var pvs types.PriceVolumeSlice - - switch side { - case types.SideTypeBuy: - pvs = book.Bids - case types.SideTypeSell: - pvs = book.Asks - } - +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.Warn("empty bids or asks") + 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 { + if idx == -1 || idx > len(pvs)-1 { // do not place orders log.Warn("depth is not enough") return @@ -144,7 +135,7 @@ func (s *Strategy) updateOrders(orderExecutor bbgo.OrderExecutor, side types.Sid } } -func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseVolume fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) { +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 { @@ -160,7 +151,7 @@ func (s *Strategy) generateOrders(symbol string, side types.SideType, price, pri } for i := 0; i < numOrders; i++ { - volume := math.Exp(expBase.Float64()) * baseVolume.Float64() + volume := math.Exp(expBase.Float64()) * baseQuantity.Float64() // skip order less than 10usd if volume*price.Float64() < 10.0 { @@ -185,7 +176,6 @@ func (s *Strategy) generateOrders(symbol string, side types.SideType, price, pri price = price + priceTick declog := math.Log10(math.Abs(priceTick.Float64())) expBase += fixedpoint.NewFromFloat(math.Pow10(-int(declog)) * math.Abs(priceTick.Float64())) - // log.Infof("expBase: %f", expBase.Float64()) } return orders diff --git a/pkg/types/orderbook.go b/pkg/types/orderbook.go index 51cd1e3ff..6f47bf19e 100644 --- a/pkg/types/orderbook.go +++ b/pkg/types/orderbook.go @@ -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()