bbgo_origin/pkg/bbgo/order_execution.go

309 lines
9.7 KiB
Go
Raw Normal View History

package bbgo
import (
"context"
2020-11-09 08:34:35 +00:00
"fmt"
2020-10-26 10:28:34 +00:00
"math"
"github.com/pkg/errors"
2021-02-10 16:21:56 +00:00
log "github.com/sirupsen/logrus"
2020-10-26 10:17:18 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type OrderExecutor interface {
SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error)
OnTradeUpdate(cb func(trade types.Trade))
OnOrderUpdate(cb func(order types.Order))
EmitTradeUpdate(trade types.Trade)
EmitOrderUpdate(order types.Order)
}
type OrderExecutionRouter interface {
// SubmitOrdersTo submit order to a specific exchange Session
SubmitOrdersTo(ctx context.Context, session string, orders ...types.SubmitOrder) (createdOrders types.OrderSlice, err error)
}
type ExchangeOrderExecutionRouter struct {
Notifiability
sessions map[string]*ExchangeSession
executors map[string]OrderExecutor
}
func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, session string, orders ...types.SubmitOrder) (types.OrderSlice, error) {
if executor, ok := e.executors[session] ; ok {
return executor.SubmitOrders(ctx, orders...)
}
es, ok := e.sessions[session]
if !ok {
2021-02-10 16:21:56 +00:00
return nil, fmt.Errorf("exchange session %s not found", session)
}
2020-11-09 07:02:12 +00:00
formattedOrders, err := formatOrders(es, orders)
2020-10-26 10:17:18 +00:00
if err != nil {
return nil, err
}
return es.Exchange.SubmitOrders(ctx, formattedOrders...)
}
// ExchangeOrderExecutor is an order executor wrapper for single exchange instance.
//go:generate callbackgen -type ExchangeOrderExecutor
type ExchangeOrderExecutor struct {
// MinQuoteBalance fixedpoint.Value `json:"minQuoteBalance,omitempty" yaml:"minQuoteBalance,omitempty"`
Notifiability `json:"-" yaml:"-"`
Session *ExchangeSession `json:"-" yaml:"-"`
// private trade update callbacks
tradeUpdateCallbacks []func(trade types.Trade)
// private order update callbacks
orderUpdateCallbacks []func(order types.Order)
}
2020-10-30 21:21:17 +00:00
func (e *ExchangeOrderExecutor) notifySubmitOrders(orders ...types.SubmitOrder) {
for _, order := range orders {
// pass submit order as an interface object.
channel, ok := e.RouteObject(&order)
if ok {
e.NotifyTo(channel, ":memo: Submitting %s %s %s order with quantity: %s at price: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order.PriceString, &order)
2020-10-30 21:21:17 +00:00
} else {
e.Notify(":memo: Submitting %s %s %s order with quantity: %s at price: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order.PriceString, &order)
2020-10-30 21:21:17 +00:00
}
}
}
func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) (types.OrderSlice, error) {
formattedOrders, err := formatOrders(e.Session, orders)
2020-10-26 10:17:18 +00:00
if err != nil {
return nil, err
}
2020-10-30 21:21:17 +00:00
for _, order := range formattedOrders {
// pass submit order as an interface object.
channel, ok := e.RouteObject(&order)
if ok {
e.NotifyTo(channel, ":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order)
} else {
e.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order)
}
2020-12-29 10:32:51 +00:00
2021-02-10 16:21:56 +00:00
log.Infof("submitting order: %s", order.String())
2020-10-30 21:21:17 +00:00
}
e.notifySubmitOrders(formattedOrders...)
2020-10-26 10:17:18 +00:00
return e.Session.Exchange.SubmitOrders(ctx, formattedOrders...)
}
2020-11-09 06:56:54 +00:00
type BasicRiskController struct {
2021-02-10 16:21:56 +00:00
Logger *log.Logger
2020-10-26 10:17:18 +00:00
MaxOrderAmount fixedpoint.Value `json:"maxOrderAmount,omitempty" yaml:"maxOrderAmount,omitempty"`
MinQuoteBalance fixedpoint.Value `json:"minQuoteBalance,omitempty" yaml:"minQuoteBalance,omitempty"`
MaxBaseAssetBalance fixedpoint.Value `json:"maxBaseAssetBalance,omitempty" yaml:"maxBaseAssetBalance,omitempty"`
MinBaseAssetBalance fixedpoint.Value `json:"minBaseAssetBalance,omitempty" yaml:"minBaseAssetBalance,omitempty"`
2020-10-26 10:17:18 +00:00
}
2020-11-09 06:56:54 +00:00
// ProcessOrders filters and modifies the submit order objects by:
// 1. Increase the quantity by the minimal requirement
// 2. Decrease the quantity by risk controls
// 3. If the quantity does not meet minimal requirement, we should ignore the submit order.
2020-11-09 07:02:12 +00:00
func (c *BasicRiskController) ProcessOrders(session *ExchangeSession, orders ...types.SubmitOrder) (outOrders []types.SubmitOrder, errs []error) {
2020-11-09 06:56:54 +00:00
balances := session.Account.Balances()
addError := func(err error) {
errs = append(errs, err)
}
2020-11-09 06:56:54 +00:00
accumulativeQuoteAmount := 0.0
accumulativeBaseSellQuantity := 0.0
2020-10-26 10:28:34 +00:00
for _, order := range orders {
2020-11-09 06:56:54 +00:00
lastPrice, ok := session.LastPrice(order.Symbol)
if !ok {
2020-11-09 08:34:35 +00:00
addError(fmt.Errorf("the last price of symbol %q is not found, order: %s", order.Symbol, order.String()))
2020-11-09 06:56:54 +00:00
continue
2020-10-26 10:28:34 +00:00
}
2020-11-09 06:56:54 +00:00
market, ok := session.Market(order.Symbol)
if !ok {
2020-11-09 08:34:35 +00:00
addError(fmt.Errorf("the market config of symbol %q is not found, order: %s", order.Symbol, order.String()))
2020-11-09 06:56:54 +00:00
continue
}
price := order.Price
2020-10-26 10:28:34 +00:00
quantity := order.Quantity
2020-11-09 06:56:54 +00:00
switch order.Type {
case types.OrderTypeMarket:
price = lastPrice
}
2020-10-26 10:28:34 +00:00
switch order.Side {
case types.SideTypeBuy:
2020-11-09 06:56:54 +00:00
// Critical conditions for placing buy orders
quoteBalance, ok := balances[market.QuoteCurrency]
if !ok {
2020-11-09 08:34:35 +00:00
addError(fmt.Errorf("can not place buy order, quote balance %s not found", market.QuoteCurrency))
2020-11-09 06:56:54 +00:00
continue
}
2020-10-26 10:28:34 +00:00
2020-11-10 06:19:33 +00:00
if quoteBalance.Available < c.MinQuoteBalance {
addError(errors.Wrapf(ErrQuoteBalanceLevelTooLow, "can not place buy order, quote balance level is too low: %s < %s, order: %s",
2020-11-10 06:19:33 +00:00
types.USD.FormatMoneyFloat64(quoteBalance.Available.Float64()),
types.USD.FormatMoneyFloat64(c.MinQuoteBalance.Float64()), order.String()))
2020-11-09 06:56:54 +00:00
continue
}
2020-10-26 10:28:34 +00:00
2020-11-09 06:56:54 +00:00
// Increase the quantity if the amount is not enough,
// this is the only increase op, later we will decrease the quantity if it meets the criteria
quantity = AdjustFloatQuantityByMinAmount(quantity, price, market.MinAmount*1.01)
2020-11-09 06:56:54 +00:00
if c.MaxOrderAmount > 0 {
quantity = AdjustFloatQuantityByMaxAmount(quantity, price, c.MaxOrderAmount.Float64())
2020-11-09 06:56:54 +00:00
}
2020-11-10 06:19:33 +00:00
quoteAssetQuota := math.Max(0.0, quoteBalance.Available.Float64()-c.MinQuoteBalance.Float64())
2020-11-09 06:56:54 +00:00
if quoteAssetQuota < market.MinAmount {
addError(
errors.Wrapf(
ErrInsufficientQuoteBalance,
"can not place buy order, insufficient quote balance: quota %f < min amount %f, order: %s",
quoteAssetQuota, market.MinAmount, order.String()))
2020-11-09 06:56:54 +00:00
continue
}
quantity = AdjustFloatQuantityByMaxAmount(quantity, price, quoteAssetQuota)
2020-10-26 10:28:34 +00:00
2020-11-09 06:56:54 +00:00
// if MaxBaseAssetBalance is enabled, we should check the current base asset balance
if baseBalance, hasBaseAsset := balances[market.BaseCurrency]; hasBaseAsset && c.MaxBaseAssetBalance > 0 {
2020-11-10 06:19:33 +00:00
if baseBalance.Available > c.MaxBaseAssetBalance {
addError(
errors.Wrapf(
ErrAssetBalanceLevelTooHigh,
"should not place buy order, asset balance level is too high: %f > %f, order: %s",
2020-11-10 06:19:33 +00:00
baseBalance.Available.Float64(),
c.MaxBaseAssetBalance.Float64(),
order.String()))
2020-11-09 06:56:54 +00:00
continue
2020-10-26 10:28:34 +00:00
}
2020-11-10 06:19:33 +00:00
baseAssetQuota := math.Max(0.0, c.MaxBaseAssetBalance.Float64()-baseBalance.Available.Float64())
2020-11-09 06:56:54 +00:00
if quantity > baseAssetQuota {
quantity = baseAssetQuota
2020-10-26 10:28:34 +00:00
}
}
2020-11-09 06:56:54 +00:00
// if the amount is still too small, we should skip it.
notional := quantity * lastPrice
if notional < market.MinAmount {
addError(
2020-11-09 08:34:35 +00:00
fmt.Errorf(
"can not place buy order, quote amount too small: notional %f < min amount %f, order: %s",
notional,
market.MinAmount,
order.String()))
2020-11-09 06:56:54 +00:00
continue
}
accumulativeQuoteAmount += notional
2020-10-26 10:28:34 +00:00
case types.SideTypeSell:
2020-11-09 06:56:54 +00:00
// Critical conditions for placing SELL orders
baseAssetBalance, ok := balances[market.BaseCurrency]
if !ok {
addError(
2020-11-09 08:34:35 +00:00
fmt.Errorf(
"can not place sell order, no base asset balance %s, order: %s",
market.BaseCurrency,
order.String()))
2020-11-09 06:56:54 +00:00
continue
}
2020-10-26 10:28:34 +00:00
2020-11-09 06:56:54 +00:00
// if the amount is too small, we should increase it.
quantity = AdjustFloatQuantityByMinAmount(quantity, price, market.MinNotional*1.01)
2020-10-26 10:28:34 +00:00
2020-11-09 06:56:54 +00:00
// we should not SELL too much
2020-11-10 06:19:33 +00:00
quantity = math.Min(quantity, baseAssetBalance.Available.Float64())
2020-10-26 10:28:34 +00:00
2020-11-09 06:56:54 +00:00
if c.MinBaseAssetBalance > 0 {
2020-11-10 06:19:33 +00:00
if baseAssetBalance.Available < c.MinBaseAssetBalance {
addError(
errors.Wrapf(
ErrAssetBalanceLevelTooLow,
2020-11-10 06:19:33 +00:00
"asset balance level is too low: %f > %f", baseAssetBalance.Available.Float64(), c.MinBaseAssetBalance.Float64()))
2020-11-09 06:56:54 +00:00
continue
2020-10-26 10:28:34 +00:00
}
2020-11-10 06:19:33 +00:00
quantity = math.Min(quantity, baseAssetBalance.Available.Float64()-c.MinBaseAssetBalance.Float64())
2020-11-09 06:56:54 +00:00
if quantity < market.MinQuantity {
addError(
errors.Wrapf(
ErrInsufficientAssetBalance,
"insufficient asset balance: %f > minimal quantity %f",
2020-11-10 06:19:33 +00:00
baseAssetBalance.Available.Float64(),
market.MinQuantity))
2020-11-09 06:56:54 +00:00
continue
2020-10-26 10:28:34 +00:00
}
2020-11-09 06:56:54 +00:00
}
2020-10-26 10:28:34 +00:00
2020-11-09 06:56:54 +00:00
if c.MaxOrderAmount > 0 {
quantity = AdjustFloatQuantityByMaxAmount(quantity, price, c.MaxOrderAmount.Float64())
2020-11-09 06:56:54 +00:00
}
2020-10-26 10:28:34 +00:00
2020-11-09 06:56:54 +00:00
notional := quantity * lastPrice
if notional < market.MinNotional {
addError(
2020-11-09 08:34:35 +00:00
fmt.Errorf(
"can not place sell order, notional %f < min notional: %f, order: %s",
notional,
market.MinNotional,
order.String()))
2020-11-09 06:56:54 +00:00
continue
2020-10-26 10:28:34 +00:00
}
2020-11-09 06:56:54 +00:00
2021-02-10 16:21:56 +00:00
if quantity < market.MinQuantity {
addError(
2020-11-09 08:34:35 +00:00
fmt.Errorf(
"can not place sell order, quantity %f is less than the minimal lot %f, order: %s",
quantity,
2021-02-10 16:21:56 +00:00
market.MinQuantity,
order.String()))
2020-11-09 06:56:54 +00:00
continue
}
accumulativeBaseSellQuantity += quantity
2020-10-26 10:28:34 +00:00
}
// update quantity and format the order
2020-10-26 10:28:34 +00:00
order.Quantity = quantity
2020-11-09 06:56:54 +00:00
outOrders = append(outOrders, order)
2020-10-26 10:17:18 +00:00
}
2020-11-09 06:56:54 +00:00
return outOrders, nil
}
2020-11-09 07:02:12 +00:00
func formatOrders(session *ExchangeSession, orders []types.SubmitOrder) (formattedOrders []types.SubmitOrder, err error) {
for _, order := range orders {
o, err := session.FormatOrder(order)
2020-10-26 10:28:34 +00:00
if err != nil {
return formattedOrders, err
}
2020-10-26 10:28:34 +00:00
formattedOrders = append(formattedOrders, o)
}
2020-10-26 10:17:18 +00:00
return formattedOrders, err
}
2020-11-10 06:19:33 +00:00
func max(a, b int64) int64 {
if a > b {
return a
}
return b
}