Merge pull request #927 from c9s/refactor/submit-order

refactor: simplify submit order
This commit is contained in:
Yo-An Lin 2022-09-11 02:58:47 +08:00 committed by GitHub
commit d45bc9e509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 431 additions and 370 deletions

View File

@ -94,7 +94,7 @@ var rootCmd = &cobra.Command{
time.Sleep(time.Second) time.Sleep(time.Second)
createdOrders, err := exchange.SubmitOrders(ctx, types.SubmitOrder{ createdOrder, err := exchange.SubmitOrder(ctx, types.SubmitOrder{
Symbol: symbol, Symbol: symbol,
Market: market, Market: market,
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
@ -108,7 +108,7 @@ var rootCmd = &cobra.Command{
return err return err
} }
log.Info(createdOrders) log.Info(createdOrder)
cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM) cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
return nil return nil

View File

@ -169,31 +169,23 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O
return nil, nil return nil, nil
} }
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) {
for _, order := range orders { symbol := order.Symbol
symbol := order.Symbol matching, ok := e.matchingBook(symbol)
matching, ok := e.matchingBook(symbol) if !ok {
if !ok { return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol)
return nil, fmt.Errorf("matching engine is not initialized for symbol %s", symbol) }
}
createdOrder, _, err := matching.PlaceOrder(order) createdOrder, _, err = matching.PlaceOrder(order)
if err != nil { if createdOrder != nil {
return nil, err // market order can be closed immediately.
} switch createdOrder.Status {
case types.OrderStatusFilled, types.OrderStatusCanceled, types.OrderStatusRejected:
if createdOrder != nil { e.addClosedOrder(*createdOrder)
createdOrders = append(createdOrders, *createdOrder)
// market order can be closed immediately.
switch createdOrder.Status {
case types.OrderStatusFilled, types.OrderStatusCanceled, types.OrderStatusRejected:
e.addClosedOrder(*createdOrder)
}
} }
} }
return createdOrders, nil return createdOrder, err
} }
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {

View File

@ -35,7 +35,7 @@ func TestTrailingStop_ShortPosition(t *testing.T) {
mockEx := mocks.NewMockExchange(mockCtrl) mockEx := mocks.NewMockExchange(mockCtrl)
mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2)
mockEx.EXPECT().SubmitOrders(gomock.Any(), types.SubmitOrder{ mockEx.EXPECT().SubmitOrder(gomock.Any(), types.SubmitOrder{
Symbol: "BTCUSDT", Symbol: "BTCUSDT",
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
Type: types.OrderTypeMarket, Type: types.OrderTypeMarket,
@ -113,7 +113,7 @@ func TestTrailingStop_LongPosition(t *testing.T) {
mockEx := mocks.NewMockExchange(mockCtrl) mockEx := mocks.NewMockExchange(mockCtrl)
mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2)
mockEx.EXPECT().SubmitOrders(gomock.Any(), types.SubmitOrder{ mockEx.EXPECT().SubmitOrder(gomock.Any(), types.SubmitOrder{
Symbol: "BTCUSDT", Symbol: "BTCUSDT",
Side: types.SideTypeSell, Side: types.SideTypeSell,
Type: types.OrderTypeMarket, Type: types.OrderTypeMarket,

View File

@ -18,6 +18,10 @@ type PositionCloser interface {
ClosePosition(ctx context.Context, percentage fixedpoint.Value) error ClosePosition(ctx context.Context, percentage fixedpoint.Value) error
} }
type PositionResetter interface {
ResetPosition() error
}
type PositionReader interface { type PositionReader interface {
CurrentPosition() *types.Position CurrentPosition() *types.Position
} }
@ -172,7 +176,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) {
reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) reply.AddMultipleButtons(generateStrategyButtonsForm(strategies))
reply.Message("Please choose one strategy") reply.Message("Please choose one strategy")
} else { } else {
reply.Message("No strategy supports PositionReader") reply.Message("No any strategy supports PositionReader")
} }
return nil return nil
}).Cycle(func(signature string, reply interact.Reply) error { }).Cycle(func(signature string, reply interact.Reply) error {
@ -206,6 +210,47 @@ func (it *CoreInteraction) Commands(i *interact.Interact) {
return nil return nil
}) })
i.PrivateCommand("/resetposition", "Reset position", func(reply interact.Reply) error {
// it.trader.exchangeStrategies
// send symbol options
if strategies, found := filterStrategyByInterface((*PositionResetter)(nil), it.exchangeStrategies); found {
reply.AddMultipleButtons(generateStrategyButtonsForm(strategies))
reply.Message("Please choose one strategy")
} else {
reply.Message("No strategy supports PositionResetter interface")
}
return nil
}).Next(func(signature string, reply interact.Reply) error {
strategy, ok := it.exchangeStrategies[signature]
if !ok {
reply.Message("Strategy not found")
return fmt.Errorf("strategy %s not found", signature)
}
resetter, implemented := strategy.(PositionResetter)
if implemented {
return resetter.ResetPosition()
}
reset := false
err := dynamic.IterateFields(strategy, func(ft reflect.StructField, fv reflect.Value) error {
posType := reflect.TypeOf(&types.Position{})
if ft.Type == posType {
if pos, typeOk := fv.Interface().(*types.Position); typeOk {
pos.Reset()
reset = true
}
}
return nil
})
if reset {
reply.Message("Position is reset")
}
return err
})
i.PrivateCommand("/closeposition", "Close position", func(reply interact.Reply) error { i.PrivateCommand("/closeposition", "Close position", func(reply interact.Reply) error {
// it.trader.exchangeStrategies // it.trader.exchangeStrategies
// send symbol options // send symbol options
@ -213,7 +258,7 @@ func (it *CoreInteraction) Commands(i *interact.Interact) {
reply.AddMultipleButtons(generateStrategyButtonsForm(strategies)) reply.AddMultipleButtons(generateStrategyButtonsForm(strategies))
reply.Message("Please choose one strategy") reply.Message("Please choose one strategy")
} else { } else {
reply.Message("No strategy supports PositionCloser") reply.Message("No strategy supports PositionCloser interface")
} }
return nil return nil
}).Next(func(signature string, reply interact.Reply) error { }).Next(func(signature string, reply interact.Reply) error {

View File

@ -6,6 +6,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"go.uber.org/multierr"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
@ -42,7 +43,42 @@ func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, sessi
return nil, err return nil, err
} }
return es.Exchange.SubmitOrders(ctx, formattedOrders...) createdOrders, _, err := BatchPlaceOrder(ctx, es.Exchange, formattedOrders...)
return createdOrders, err
}
func BatchRetryPlaceOrder(ctx context.Context, exchange types.Exchange, errIdx []int, submitOrders ...types.SubmitOrder) (types.OrderSlice, error) {
var createdOrders types.OrderSlice
var err error
for _, idx := range errIdx {
createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrders[idx])
if err2 != nil {
err = multierr.Append(err, err2)
} else if createdOrder != nil {
createdOrders = append(createdOrders, *createdOrder)
}
}
return createdOrders, err
}
// BatchPlaceOrder
func BatchPlaceOrder(ctx context.Context, exchange types.Exchange, submitOrders ...types.SubmitOrder) (types.OrderSlice, []int, error) {
var createdOrders types.OrderSlice
var err error
var errIndexes []int
for i, submitOrder := range submitOrders {
createdOrder, err2 := exchange.SubmitOrder(ctx, submitOrder)
if err2 != nil {
err = multierr.Append(err, err2)
errIndexes = append(errIndexes, i)
} else if createdOrder != nil {
createdOrder.Tag = submitOrder.Tag
createdOrders = append(createdOrders, *createdOrder)
}
}
return createdOrders, errIndexes, err
} }
func (e *ExchangeOrderExecutionRouter) CancelOrdersTo(ctx context.Context, session string, orders ...types.Order) error { func (e *ExchangeOrderExecutionRouter) CancelOrdersTo(ctx context.Context, session string, orders ...types.Order) error {
@ -105,7 +141,8 @@ func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...type
e.notifySubmitOrders(formattedOrders...) e.notifySubmitOrders(formattedOrders...)
return e.Session.Exchange.SubmitOrders(ctx, formattedOrders...) createdOrders, _, err := BatchPlaceOrder(ctx, e.Session.Exchange, formattedOrders...)
return createdOrders, err
} }
func (e *ExchangeOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error { func (e *ExchangeOrderExecutor) CancelOrders(ctx context.Context, orders ...types.Order) error {

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"go.uber.org/multierr"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
@ -110,28 +111,14 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ..
return nil, err return nil, err
} }
var createdOrders types.OrderSlice createdOrders, errIdx, err := BatchPlaceOrder(ctx, e.session.Exchange, formattedOrders...)
if len(errIdx) > 0 {
retOrders, err := e.session.Exchange.SubmitOrders(ctx, formattedOrders...) createdOrders2, err2 := BatchRetryPlaceOrder(ctx, e.session.Exchange, errIdx, formattedOrders...)
if len(retOrders) > 0 { if err2 != nil {
createdOrders = append(createdOrders, retOrders...) err = multierr.Append(err, err2)
} } else {
createdOrders = append(createdOrders, createdOrders2...)
if err != nil {
// retry once
retOrders, err = e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
if len(retOrders) > 0 {
createdOrders = append(createdOrders, retOrders...)
} }
if err != nil {
err = fmt.Errorf("can not place orders: %w", err)
}
}
// FIXME: map by price and volume
for i := 0; i < len(createdOrders); i++ {
createdOrders[i].Tag = formattedOrders[i].Tag
} }
e.orderStore.Add(createdOrders...) e.orderStore.Add(createdOrders...)
@ -156,27 +143,25 @@ type OpenPositionOptions struct {
Quantity fixedpoint.Value `json:"quantity,omitempty"` Quantity fixedpoint.Value `json:"quantity,omitempty"`
// MarketOrder set to true to open a position with a market order // MarketOrder set to true to open a position with a market order
MarketOrder bool MarketOrder bool `json:"marketOrder,omitempty"`
// LimitOrder set to true to open a position with a limit order // LimitOrder set to true to open a position with a limit order
LimitOrder bool LimitOrder bool `json:"limitOrder,omitempty"`
// LimitTakerRatio is used when LimitOrder = true, it adjusts the price of the limit order with a ratio. // LimitTakerRatio is used when LimitOrder = true, it adjusts the price of the limit order with a ratio.
// So you can ensure that the limit order can be a taker order. Higher the ratio, higher the chance it could be a taker order. // So you can ensure that the limit order can be a taker order. Higher the ratio, higher the chance it could be a taker order.
LimitTakerRatio fixedpoint.Value LimitTakerRatio fixedpoint.Value `json:"limitTakerRatio,omitempty"`
CurrentPrice fixedpoint.Value CurrentPrice fixedpoint.Value `json:"currentPrice,omitempty"`
Tag string Tags []string `json:"tags"`
} }
func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) error { func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) error {
log.Infof("opening %s position: %+v", e.position.Symbol, options)
price := options.CurrentPrice price := options.CurrentPrice
submitOrder := types.SubmitOrder{ submitOrder := types.SubmitOrder{
Symbol: e.position.Symbol, Symbol: e.position.Symbol,
Type: types.OrderTypeMarket, Type: types.OrderTypeMarket,
MarginSideEffect: types.SideEffectTypeMarginBuy, MarginSideEffect: types.SideEffectTypeMarginBuy,
Tag: options.Tag, Tag: strings.Join(options.Tags, ","),
} }
if !options.LimitTakerRatio.IsZero() { if !options.LimitTakerRatio.IsZero() {
@ -211,6 +196,7 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos
submitOrder.Side = types.SideTypeBuy submitOrder.Side = types.SideTypeBuy
submitOrder.Quantity = quantity submitOrder.Quantity = quantity
Notify("Opening %s long position with quantity %f at price %f", e.position.Symbol, quantity.Float64(), price.Float64())
createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) createdOrder, err2 := e.SubmitOrders(ctx, submitOrder)
if err2 != nil { if err2 != nil {
return err2 return err2
@ -229,6 +215,7 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos
submitOrder.Side = types.SideTypeSell submitOrder.Side = types.SideTypeSell
submitOrder.Quantity = quantity submitOrder.Quantity = quantity
Notify("Opening %s short position with quantity %f at price %f", e.position.Symbol, quantity.Float64(), price.Float64())
createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) createdOrder, err2 := e.SubmitOrders(ctx, submitOrder)
if err2 != nil { if err2 != nil {
return err2 return err2
@ -275,14 +262,20 @@ func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders) return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders)
} }
// ClosePosition closes the current position by a percentage.
// percentage 0.1 means close 10% position
// tag is the order tag you want to attach, you may pass multiple tags, the tags will be combined into one tag string by commas.
func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error { func (e *GeneralOrderExecutor) ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error {
submitOrder := e.position.NewMarketCloseOrder(percentage) submitOrder := e.position.NewMarketCloseOrder(percentage)
if submitOrder == nil { if submitOrder == nil {
return nil return nil
} }
log.Infof("closing %s position with tags: %v", e.symbol, tags) tagStr := strings.Join(tags, ",")
submitOrder.Tag = strings.Join(tags, ",") submitOrder.Tag = tagStr
Notify("closing %s position %s with tags: %v", e.symbol, percentage.Percentage(), tagStr)
_, err := e.SubmitOrders(ctx, *submitOrder) _, err := e.SubmitOrders(ctx, *submitOrder)
return err return err
} }

View File

@ -384,12 +384,12 @@ var submitOrderCmd = &cobra.Command{
so.TimeInForce = types.TimeInForceGTC so.TimeInForce = types.TimeInForceGTC
} }
co, err := session.Exchange.SubmitOrders(ctx, so) co, err := session.Exchange.SubmitOrder(ctx, so)
if err != nil { if err != nil {
return err return err
} }
log.Infof("submitted order: %+v\ncreated order: %+v", so, co[0]) log.Infof("submitted order: %+v\ncreated order: %+v", so, co)
return nil return nil
}, },
} }

View File

@ -11,7 +11,6 @@ func TestLower(t *testing.T) {
assert.Equal(t, []float64{10.0, 11.0}, out) assert.Equal(t, []float64{10.0, 11.0}, out)
} }
func TestHigher(t *testing.T) { func TestHigher(t *testing.T) {
out := Higher([]float64{10.0, 11.0, 12.0, 13.0, 15.0}, 12.0) out := Higher([]float64{10.0, 11.0, 12.0, 13.0, 15.0}, 12.0)
assert.Equal(t, []float64{13.0, 15.0}, out) assert.Equal(t, []float64{13.0, 15.0}, out)

View File

@ -1251,33 +1251,20 @@ func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder)
return createdOrder, err return createdOrder, err
} }
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) {
for _, order := range orders { if err := orderLimiter.Wait(ctx); err != nil {
if err := orderLimiter.Wait(ctx); err != nil { log.WithError(err).Errorf("order rate limiter wait error")
log.WithError(err).Errorf("order rate limiter wait error")
}
var createdOrder *types.Order
if e.IsMargin {
createdOrder, err = e.submitMarginOrder(ctx, order)
} else if e.IsFutures {
createdOrder, err = e.submitFuturesOrder(ctx, order)
} else {
createdOrder, err = e.submitSpotOrder(ctx, order)
}
if err != nil {
return createdOrders, err
}
if createdOrder == nil {
return createdOrders, errors.New("nil converted order")
}
createdOrders = append(createdOrders, *createdOrder)
} }
return createdOrders, err if e.IsMargin {
createdOrder, err = e.submitMarginOrder(ctx, order)
} else if e.IsFutures {
createdOrder, err = e.submitFuturesOrder(ctx, order)
} else {
createdOrder, err = e.submitSpotOrder(ctx, order)
}
return createdOrder, err
} }
// QueryKLines queries the Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time. // QueryKLines queries the Kline/candlestick bars for a symbol. Klines are uniquely identified by their open time.

View File

@ -290,7 +290,7 @@ func (e *Exchange) _queryKLines(ctx context.Context, symbol string, interval typ
} }
} }
resp, err := e.newRest().HistoricalPrices(ctx, toLocalSymbol(symbol), interval, int64(options.Limit), options.StartTime, options.EndTime) resp, err := e.newRest().marketRequest.HistoricalPrices(ctx, toLocalSymbol(symbol), interval, int64(options.Limit), options.StartTime, options.EndTime)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -401,61 +401,54 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since,
return return
} }
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) { func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) {
var createdOrders types.OrderSlice
// TODO: currently only support limit and market order // TODO: currently only support limit and market order
// TODO: support time in force // TODO: support time in force
for _, so := range orders { so := order
if err := requestLimit.Wait(ctx); err != nil { if err := requestLimit.Wait(ctx); err != nil {
logrus.WithError(err).Error("rate limit error") logrus.WithError(err).Error("rate limit error")
}
orderType, err := toLocalOrderType(so.Type)
if err != nil {
logrus.WithError(err).Error("type error")
}
submitQuantity := so.Quantity
switch orderType {
case ftxapi.OrderTypeLimit, ftxapi.OrderTypeStopLimit:
submitQuantity = so.Quantity.Div(e.orderAmountReduceFactor)
}
req := e.client.NewPlaceOrderRequest()
req.Market(toLocalSymbol(TrimUpperString(so.Symbol)))
req.OrderType(orderType)
req.Side(ftxapi.Side(TrimLowerString(string(so.Side))))
req.Size(submitQuantity)
switch so.Type {
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
req.Price(so.Price)
}
if so.Type == types.OrderTypeLimitMaker {
req.PostOnly(true)
}
if so.TimeInForce == types.TimeInForceIOC {
req.Ioc(true)
}
req.ClientID(newSpotClientOrderID(so.ClientOrderID))
or, err := req.Do(ctx)
if err != nil {
return createdOrders, fmt.Errorf("failed to place order %+v: %w", so, err)
}
globalOrder, err := toGlobalOrderNew(*or)
if err != nil {
return createdOrders, fmt.Errorf("failed to convert response to global order")
}
createdOrders = append(createdOrders, globalOrder)
} }
return createdOrders, nil
orderType, err := toLocalOrderType(so.Type)
if err != nil {
logrus.WithError(err).Error("type error")
}
submitQuantity := so.Quantity
switch orderType {
case ftxapi.OrderTypeLimit, ftxapi.OrderTypeStopLimit:
submitQuantity = so.Quantity.Div(e.orderAmountReduceFactor)
}
req := e.client.NewPlaceOrderRequest()
req.Market(toLocalSymbol(TrimUpperString(so.Symbol)))
req.OrderType(orderType)
req.Side(ftxapi.Side(TrimLowerString(string(so.Side))))
req.Size(submitQuantity)
switch so.Type {
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
req.Price(so.Price)
}
if so.Type == types.OrderTypeLimitMaker {
req.PostOnly(true)
}
if so.TimeInForce == types.TimeInForceIOC {
req.Ioc(true)
}
req.ClientID(newSpotClientOrderID(so.ClientOrderID))
or, err := req.Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to place order %+v: %w", so, err)
}
globalOrder, err := toGlobalOrderNew(*or)
return &globalOrder, err
} }
func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) { func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.Order, error) {
@ -470,8 +463,12 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O
return nil, err return nil, err
} }
order, err := toGlobalOrderNew(*ftxOrder) o, err := toGlobalOrderNew(*ftxOrder)
return &order, err if err != nil {
return nil, err
}
return &o, err
} }
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {
@ -572,7 +569,6 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke
} }
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
var tickers = make(map[string]types.Ticker) var tickers = make(map[string]types.Ticker)
markets, err := e._queryMarkets(ctx) markets, err := e._queryMarkets(ctx)
@ -586,7 +582,6 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri
} }
rest := e.newRest() rest := e.newRest()
for k, v := range markets { for k, v := range markets {
// if we provide symbol as condition then we only query the gieven symbol , // if we provide symbol as condition then we only query the gieven symbol ,
@ -603,7 +598,7 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri
now := time.Now() now := time.Now()
since := now.Add(time.Duration(-1) * time.Hour) since := now.Add(time.Duration(-1) * time.Hour)
until := now until := now
prices, err := rest.HistoricalPrices(ctx, v.Market.LocalSymbol, types.Interval1h, 1, &since, &until) prices, err := rest.marketRequest.HistoricalPrices(ctx, v.Market.LocalSymbol, types.Interval1h, 1, &since, &until)
if err != nil || !prices.Success || len(prices.Result) == 0 { if err != nil || !prices.Success || len(prices.Result) == 0 {
continue continue
} }

View File

@ -34,7 +34,7 @@ func TestExchange_IOCOrder(t *testing.T) {
} }
ex := NewExchange(key, secret, "") ex := NewExchange(key, secret, "")
createdOrder, err := ex.SubmitOrders(context.Background(), types.SubmitOrder{ createdOrder, err := ex.SubmitOrder(context.Background(), types.SubmitOrder{
Symbol: "LTCUSDT", Symbol: "LTCUSDT",
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
Type: types.OrderTypeLimitMaker, Type: types.OrderTypeLimitMaker,

View File

@ -207,78 +207,74 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type
return klines, nil return klines, nil
} }
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) {
for _, order := range orders { req := e.client.TradeService.NewPlaceOrderRequest()
req := e.client.TradeService.NewPlaceOrderRequest() req.Symbol(toLocalSymbol(order.Symbol))
req.Symbol(toLocalSymbol(order.Symbol)) req.Side(toLocalSide(order.Side))
req.Side(toLocalSide(order.Side))
if order.ClientOrderID != "" { if order.ClientOrderID != "" {
req.ClientOrderID(order.ClientOrderID) req.ClientOrderID(order.ClientOrderID)
}
if order.Market.Symbol != "" {
req.Size(order.Market.FormatQuantity(order.Quantity))
} else {
// TODO: report error?
req.Size(order.Quantity.FormatString(8))
}
// set price field for limit orders
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker:
if order.Market.Symbol != "" {
req.Price(order.Market.FormatPrice(order.Price))
} else {
// TODO: report error?
req.Price(order.Price.FormatString(8))
}
}
if order.Type == types.OrderTypeLimitMaker {
req.PostOnly(true)
}
switch order.TimeInForce {
case "FOK":
req.TimeInForce(kucoinapi.TimeInForceFOK)
case "IOC":
req.TimeInForce(kucoinapi.TimeInForceIOC)
default:
// default to GTC
req.TimeInForce(kucoinapi.TimeInForceGTC)
}
switch order.Type {
case types.OrderTypeStopLimit:
req.OrderType(kucoinapi.OrderTypeStopLimit)
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
req.OrderType(kucoinapi.OrderTypeLimit)
case types.OrderTypeMarket:
req.OrderType(kucoinapi.OrderTypeMarket)
}
orderResponse, err := req.Do(ctx)
if err != nil {
return createdOrders, err
}
createdOrders = append(createdOrders, types.Order{
SubmitOrder: order,
Exchange: types.ExchangeKucoin,
OrderID: hashStringID(orderResponse.OrderID),
UUID: orderResponse.OrderID,
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(time.Now()),
UpdateTime: types.Time(time.Now()),
})
} }
return createdOrders, err if order.Market.Symbol != "" {
req.Size(order.Market.FormatQuantity(order.Quantity))
} else {
// TODO: report error?
req.Size(order.Quantity.FormatString(8))
}
// set price field for limit orders
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker:
if order.Market.Symbol != "" {
req.Price(order.Market.FormatPrice(order.Price))
} else {
// TODO: report error?
req.Price(order.Price.FormatString(8))
}
}
if order.Type == types.OrderTypeLimitMaker {
req.PostOnly(true)
}
switch order.TimeInForce {
case "FOK":
req.TimeInForce(kucoinapi.TimeInForceFOK)
case "IOC":
req.TimeInForce(kucoinapi.TimeInForceIOC)
default:
// default to GTC
req.TimeInForce(kucoinapi.TimeInForceGTC)
}
switch order.Type {
case types.OrderTypeStopLimit:
req.OrderType(kucoinapi.OrderTypeStopLimit)
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
req.OrderType(kucoinapi.OrderTypeLimit)
case types.OrderTypeMarket:
req.OrderType(kucoinapi.OrderTypeMarket)
}
orderResponse, err := req.Do(ctx)
if err != nil {
return createdOrder, err
}
return &types.Order{
SubmitOrder: order,
Exchange: types.ExchangeKucoin,
OrderID: hashStringID(orderResponse.OrderID),
UUID: orderResponse.OrderID,
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(time.Now()),
UpdateTime: types.Time(time.Now()),
}, nil
} }
// QueryOpenOrders // QueryOpenOrders

View File

@ -485,80 +485,73 @@ func (e *Exchange) Withdraw(ctx context.Context, asset string, amount fixedpoint
return nil return nil
} }
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) {
walletType := maxapi.WalletTypeSpot walletType := maxapi.WalletTypeSpot
if e.MarginSettings.IsMargin { if e.MarginSettings.IsMargin {
walletType = maxapi.WalletTypeMargin walletType = maxapi.WalletTypeMargin
} }
for _, o := range orders { o := order
orderType, err := toLocalOrderType(o.Type) orderType, err := toLocalOrderType(o.Type)
if err != nil { if err != nil {
return createdOrders, err return createdOrder, err
}
// case IOC type
if orderType == maxapi.OrderTypeLimit && o.TimeInForce == types.TimeInForceIOC {
orderType = maxapi.OrderTypeIOCLimit
}
var quantityString string
if o.Market.Symbol != "" {
quantityString = o.Market.FormatQuantity(o.Quantity)
} else {
quantityString = o.Quantity.String()
}
clientOrderID := NewClientOrderID(o.ClientOrderID)
req := e.v3order.NewCreateWalletOrderRequest(walletType)
req.Market(toLocalSymbol(o.Symbol)).
Side(toLocalSideType(o.Side)).
Volume(quantityString).
OrderType(orderType).
ClientOrderID(clientOrderID)
switch o.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker:
var priceInString string
if o.Market.Symbol != "" {
priceInString = o.Market.FormatPrice(o.Price)
} else {
priceInString = o.Price.String()
}
req.Price(priceInString)
}
// set stop price field for limit orders
switch o.Type {
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
var priceInString string
if o.Market.Symbol != "" {
priceInString = o.Market.FormatPrice(o.StopPrice)
} else {
priceInString = o.StopPrice.String()
}
req.StopPrice(priceInString)
}
retOrder, err := req.Do(ctx)
if err != nil {
return createdOrders, err
}
if retOrder == nil {
return createdOrders, errors.New("returned nil order")
}
createdOrder, err := toGlobalOrder(*retOrder)
if err != nil {
return createdOrders, err
}
createdOrders = append(createdOrders, *createdOrder)
} }
return createdOrders, err // case IOC type
if orderType == maxapi.OrderTypeLimit && o.TimeInForce == types.TimeInForceIOC {
orderType = maxapi.OrderTypeIOCLimit
}
var quantityString string
if o.Market.Symbol != "" {
quantityString = o.Market.FormatQuantity(o.Quantity)
} else {
quantityString = o.Quantity.String()
}
clientOrderID := NewClientOrderID(o.ClientOrderID)
req := e.v3order.NewCreateWalletOrderRequest(walletType)
req.Market(toLocalSymbol(o.Symbol)).
Side(toLocalSideType(o.Side)).
Volume(quantityString).
OrderType(orderType).
ClientOrderID(clientOrderID)
switch o.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker:
var priceInString string
if o.Market.Symbol != "" {
priceInString = o.Market.FormatPrice(o.Price)
} else {
priceInString = o.Price.String()
}
req.Price(priceInString)
}
// set stop price field for limit orders
switch o.Type {
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
var priceInString string
if o.Market.Symbol != "" {
priceInString = o.Market.FormatPrice(o.StopPrice)
} else {
priceInString = o.StopPrice.String()
}
req.StopPrice(priceInString)
}
retOrder, err := req.Do(ctx)
if err != nil {
return createdOrder, err
}
if retOrder == nil {
return createdOrder, errors.New("returned nil order")
}
createdOrder, err = toGlobalOrder(*retOrder)
return createdOrder, err
} }
// PlatformFeeCurrency // PlatformFeeCurrency

View File

@ -159,78 +159,97 @@ func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap,
return balanceMap, nil return balanceMap, nil
} }
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error) { func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) {
var reqs []*okexapi.PlaceOrderRequest orderReq := e.client.TradeService.NewPlaceOrderRequest()
for _, order := range orders {
orderReq := e.client.TradeService.NewPlaceOrderRequest()
orderType, err := toLocalOrderType(order.Type) orderType, err := toLocalOrderType(order.Type)
if err != nil {
return nil, err
}
orderReq.InstrumentID(toLocalSymbol(order.Symbol))
orderReq.Side(toLocalSideType(order.Side))
if order.Market.Symbol != "" {
orderReq.Quantity(order.Market.FormatQuantity(order.Quantity))
} else {
// TODO report error
orderReq.Quantity(order.Quantity.FormatString(8))
}
// set price field for limit orders
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit:
if order.Market.Symbol != "" {
orderReq.Price(order.Market.FormatPrice(order.Price))
} else {
// TODO report error
orderReq.Price(order.Price.FormatString(8))
}
}
switch order.TimeInForce {
case "FOK":
orderReq.OrderType(okexapi.OrderTypeFOK)
case "IOC":
orderReq.OrderType(okexapi.OrderTypeIOC)
default:
orderReq.OrderType(orderType)
}
reqs = append(reqs, orderReq)
}
batchReq := e.client.TradeService.NewBatchPlaceOrderRequest()
batchReq.Add(reqs...)
orderHeads, err := batchReq.Do(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for idx, orderHead := range orderHeads { orderReq.InstrumentID(toLocalSymbol(order.Symbol))
orderID, err := strconv.ParseInt(orderHead.OrderID, 10, 64) orderReq.Side(toLocalSideType(order.Side))
if err != nil {
return createdOrders, err
}
submitOrder := orders[idx] if order.Market.Symbol != "" {
createdOrders = append(createdOrders, types.Order{ orderReq.Quantity(order.Market.FormatQuantity(order.Quantity))
SubmitOrder: submitOrder, } else {
Exchange: types.ExchangeOKEx, // TODO report error
OrderID: uint64(orderID), orderReq.Quantity(order.Quantity.FormatString(8))
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(time.Now()),
UpdateTime: types.Time(time.Now()),
IsMargin: false,
IsIsolated: false,
})
} }
return createdOrders, nil // set price field for limit orders
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeLimit:
if order.Market.Symbol != "" {
orderReq.Price(order.Market.FormatPrice(order.Price))
} else {
// TODO report error
orderReq.Price(order.Price.FormatString(8))
}
}
switch order.TimeInForce {
case "FOK":
orderReq.OrderType(okexapi.OrderTypeFOK)
case "IOC":
orderReq.OrderType(okexapi.OrderTypeIOC)
default:
orderReq.OrderType(orderType)
}
orderHead, err := orderReq.Do(ctx)
if err != nil {
return nil, err
}
orderID, err := strconv.ParseInt(orderHead.OrderID, 10, 64)
if err != nil {
return nil, err
}
return &types.Order{
SubmitOrder: order,
Exchange: types.ExchangeOKEx,
OrderID: uint64(orderID),
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(time.Now()),
UpdateTime: types.Time(time.Now()),
IsMargin: false,
IsIsolated: false,
}, nil
// TODO: move this to batch place orders interface
/*
batchReq := e.client.TradeService.NewBatchPlaceOrderRequest()
batchReq.Add(reqs...)
orderHeads, err := batchReq.Do(ctx)
if err != nil {
return nil, err
}
for idx, orderHead := range orderHeads {
orderID, err := strconv.ParseInt(orderHead.OrderID, 10, 64)
if err != nil {
return createdOrder, err
}
submitOrder := order[idx]
createdOrder = append(createdOrder, types.Order{
SubmitOrder: submitOrder,
Exchange: types.ExchangeOKEx,
OrderID: uint64(orderID),
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(time.Now()),
UpdateTime: types.Time(time.Now()),
IsMargin: false,
IsIsolated: false,
})
}
*/
} }
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {

View File

@ -9,6 +9,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"go.uber.org/multierr"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/reflection" "google.golang.org/grpc/reflection"
@ -46,20 +47,27 @@ func (s *TradingService) SubmitOrder(ctx context.Context, request *pb.SubmitOrde
} }
} }
createdOrders, err := session.Exchange.SubmitOrders(ctx, submitOrders...) createdOrders, errIdx, err := bbgo.BatchPlaceOrder(ctx, session.Exchange, submitOrders...)
if err != nil { if len(errIdx) > 0 {
return nil, err createdOrders2, err2 := bbgo.BatchRetryPlaceOrder(ctx, session.Exchange, errIdx, submitOrders...)
if err2 != nil {
err = multierr.Append(err, err2)
} else {
createdOrders = append(createdOrders, createdOrders2...)
}
} }
// convert response
resp := &pb.SubmitOrderResponse{ resp := &pb.SubmitOrderResponse{
Session: sessionName, Session: sessionName,
Orders: nil, Orders: nil,
} }
for _, createdOrder := range createdOrders { for _, createdOrder := range createdOrders {
resp.Orders = append(resp.Orders, transOrder(session, createdOrder)) resp.Orders = append(resp.Orders, transOrder(session, createdOrder))
} }
return resp, nil return resp, err
} }
func (s *TradingService) CancelOrder(ctx context.Context, request *pb.CancelOrderRequest) (*pb.CancelOrderResponse, error) { func (s *TradingService) CancelOrder(ctx context.Context, request *pb.CancelOrderRequest) (*pb.CancelOrderResponse, error) {

View File

@ -132,13 +132,14 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
// s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder) // s.Notify("Submitting %s %s order to close position by %v", s.Symbol, side.String(), percentage, submitOrder)
createdOrders, err := s.session.Exchange.SubmitOrders(ctx, submitOrder) createdOrder, err := s.session.Exchange.SubmitOrder(ctx, submitOrder)
if err != nil { if err != nil {
log.WithError(err).Errorf("can not place position close order") log.WithError(err).Errorf("can not place position close order")
} else if createdOrder != nil {
s.orderStore.Add(*createdOrder)
s.activeMakerOrders.Add(*createdOrder)
} }
s.orderStore.Add(createdOrders...)
s.activeMakerOrders.Add(createdOrders...)
return err return err
} }
func (s *Strategy) InstanceID() string { func (s *Strategy) InstanceID() string {
@ -464,7 +465,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// Price: kline.Close.Mul(fixedpoint.One.Add(s.Spread)), // Price: kline.Close.Mul(fixedpoint.One.Add(s.Spread)),
// Quantity: fixedpoint.NewFromFloat(math.Max(math.Min(eq, 0.003), 0.0005)), //0.0005 // Quantity: fixedpoint.NewFromFloat(math.Max(math.Min(eq, 0.003), 0.0005)), //0.0005
// } // }
// createdOrders, err = orderExecutor.SubmitOrders(ctx, submitOrder) // createdOrders, err = orderExecutor.SubmitOrder(ctx, submitOrder)
// if err != nil { // if err != nil {
// log.WithError(err).Errorf("can not place orders") // log.WithError(err).Errorf("can not place orders")
// } // }
@ -495,7 +496,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// Price: kline.Close.Mul(fixedpoint.One.Sub(s.Spread)), // Price: kline.Close.Mul(fixedpoint.One.Sub(s.Spread)),
// Quantity: fixedpoint.NewFromFloat(math.Max(math.Min(eq, 0.003), 0.0005)), //0.0005 // Quantity: fixedpoint.NewFromFloat(math.Max(math.Min(eq, 0.003), 0.0005)), //0.0005
// } // }
// createdOrders, err = orderExecutor.SubmitOrders(ctx, submitOrder) // createdOrders, err = orderExecutor.SubmitOrder(ctx, submitOrder)
// if err != nil { // if err != nil {
// log.WithError(err).Errorf("can not place orders") // log.WithError(err).Errorf("can not place orders")
// } // }

View File

@ -350,7 +350,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
s.tradingMarket.MinNotional.Mul(NotionModifier).Div(price)) s.tradingMarket.MinNotional.Mul(NotionModifier).Div(price))
} }
createdOrders, err := tradingSession.Exchange.SubmitOrders(ctx, types.SubmitOrder{ createdOrders, _, err := bbgo.BatchPlaceOrder(ctx, tradingSession.Exchange, types.SubmitOrder{
Symbol: s.Symbol, Symbol: s.Symbol,
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
Type: types.OrderTypeLimit, Type: types.OrderTypeLimit,
@ -369,6 +369,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se
// TimeInForce: types.TimeInForceGTC, // TimeInForce: types.TimeInForceGTC,
GroupID: s.groupID, GroupID: s.groupID,
}) })
if err != nil { if err != nil {
log.WithError(err).Error("order submit error") log.WithError(err).Error("order submit error")
} }

View File

@ -96,7 +96,7 @@ type ExchangeTradeService interface {
QueryAccountBalances(ctx context.Context) (BalanceMap, error) QueryAccountBalances(ctx context.Context) (BalanceMap, error)
SubmitOrders(ctx context.Context, orders ...SubmitOrder) (createdOrders OrderSlice, err error) SubmitOrder(ctx context.Context, order SubmitOrder) (createdOrder *Order, err error)
QueryOpenOrders(ctx context.Context, symbol string) (orders []Order, err error) QueryOpenOrders(ctx context.Context, symbol string) (orders []Order, err error)

View File

@ -206,22 +206,17 @@ func (mr *MockExchangeMockRecorder) QueryTickers(arg0 interface{}, arg1 ...inter
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTickers", reflect.TypeOf((*MockExchange)(nil).QueryTickers), varargs...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryTickers", reflect.TypeOf((*MockExchange)(nil).QueryTickers), varargs...)
} }
// SubmitOrders mocks base method. // SubmitOrder mocks base method.
func (m *MockExchange) SubmitOrders(arg0 context.Context, arg1 ...types.SubmitOrder) (types.OrderSlice, error) { func (m *MockExchange) SubmitOrder(arg0 context.Context, arg1 types.SubmitOrder) (*types.Order, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{arg0} ret := m.ctrl.Call(m, "SubmitOrder", arg0, arg1)
for _, a := range arg1 { ret0, _ := ret[0].(*types.Order)
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SubmitOrders", varargs...)
ret0, _ := ret[0].(types.OrderSlice)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// SubmitOrders indicates an expected call of SubmitOrders. // SubmitOrder indicates an expected call of SubmitOrder.
func (mr *MockExchangeMockRecorder) SubmitOrders(arg0 interface{}, arg1 ...interface{}) *gomock.Call { func (mr *MockExchangeMockRecorder) SubmitOrder(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrder", reflect.TypeOf((*MockExchange)(nil).SubmitOrder), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrders", reflect.TypeOf((*MockExchange)(nil).SubmitOrders), varargs...)
} }