2020-10-25 16:26:17 +00:00
package bbgo
import (
"context"
2020-11-09 08:34:35 +00:00
"fmt"
2020-10-26 10:28:34 +00:00
"math"
2020-10-25 16:26:17 +00:00
"github.com/pkg/errors"
2021-02-10 16:21:56 +00:00
log "github.com/sirupsen/logrus"
2020-10-25 16:26:17 +00:00
2020-10-26 10:17:18 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
2020-10-25 16:26:17 +00:00
"github.com/c9s/bbgo/pkg/types"
)
2020-12-21 05:47:40 +00:00
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 ) )
}
type OrderExecutionRouter interface {
// SubmitOrderTo submit order to a specific exchange Session
SubmitOrdersTo ( ctx context . Context , session string , orders ... types . SubmitOrder ) ( createdOrders types . OrderSlice , err error )
}
2020-10-25 16:26:17 +00:00
type ExchangeOrderExecutionRouter struct {
Notifiability
sessions map [ string ] * ExchangeSession
}
2020-10-31 11:54:05 +00:00
func ( e * ExchangeOrderExecutionRouter ) SubmitOrdersTo ( ctx context . Context , session string , orders ... types . SubmitOrder ) ( types . OrderSlice , error ) {
2020-10-25 16:26:17 +00:00
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-10-25 16:26:17 +00:00
}
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
2020-10-25 16:26:17 +00:00
}
return es . Exchange . SubmitOrders ( ctx , formattedOrders ... )
}
// ExchangeOrderExecutor is an order executor wrapper for single exchange instance.
2020-12-21 05:40:23 +00:00
//go:generate callbackgen -type ExchangeOrderExecutor
2020-10-25 16:26:17 +00:00
type ExchangeOrderExecutor struct {
2020-10-26 10:17:18 +00:00
Notifiability ` json:"-" `
2020-10-25 16:26:17 +00:00
2020-11-17 00:19:22 +00:00
Session * ExchangeSession
2020-12-21 05:40:23 +00:00
// private trade update callbacks
tradeUpdateCallbacks [ ] func ( trade types . Trade )
// private order update callbacks
orderUpdateCallbacks [ ] func ( order types . Order )
2020-10-25 16:26:17 +00:00
}
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 {
2020-10-31 12:36:58 +00:00
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 {
2020-10-31 12:36:58 +00:00
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
}
}
}
2020-10-31 11:54:05 +00:00
func ( e * ExchangeOrderExecutor ) SubmitOrders ( ctx context . Context , orders ... types . SubmitOrder ) ( types . OrderSlice , error ) {
2020-11-17 00:19:22 +00:00
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
2020-11-17 00:19:22 +00:00
return e . Session . Exchange . SubmitOrders ( ctx , formattedOrders ... )
2020-10-25 16:26:17 +00:00
}
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
2021-03-01 05:44:58 +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 ( )
2020-11-09 07:29:40 +00:00
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 )
2020-10-28 08:27:25 +00:00
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 {
2020-11-09 07:29:40 +00:00
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 ( ) ) ,
2020-11-09 07:29:40 +00:00
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
2021-05-10 12:13:23 +00:00
quantity = AdjustQuantityByMinAmount ( quantity , price , market . MinAmount * 1.01 )
2020-11-09 06:56:54 +00:00
if c . MaxOrderAmount > 0 {
2021-05-10 12:13:23 +00:00
quantity = AdjustQuantityByMaxAmount ( 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 {
2020-11-09 07:29:40 +00:00
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
}
2021-05-10 12:13:23 +00:00
quantity = AdjustQuantityByMaxAmount ( 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 {
2020-11-09 07:29:40 +00:00
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 ( ) ,
2020-11-09 07:29:40 +00:00
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 {
2020-11-09 07:29:40 +00:00
addError (
2020-11-09 08:34:35 +00:00
fmt . Errorf (
2020-11-09 07:29:40 +00:00
"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 {
2020-11-09 07:29:40 +00:00
addError (
2020-11-09 08:34:35 +00:00
fmt . Errorf (
2020-11-09 07:29:40 +00:00
"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.
2021-05-10 12:13:23 +00:00
quantity = AdjustQuantityByMinAmount ( 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 {
2020-11-09 07:29:40 +00:00
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 {
2020-11-09 07:29:40 +00:00
addError (
errors . Wrapf (
ErrInsufficientAssetBalance ,
"insufficient asset balance: %f > minimal quantity %f" ,
2020-11-10 06:19:33 +00:00
baseAssetBalance . Available . Float64 ( ) ,
2020-11-09 07:29:40 +00:00
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 {
2021-05-10 12:13:23 +00:00
quantity = AdjustQuantityByMaxAmount ( 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 {
2020-11-09 07:29:40 +00:00
addError (
2020-11-09 08:34:35 +00:00
fmt . Errorf (
2020-11-09 07:29:40 +00:00
"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 {
2020-11-09 07:29:40 +00:00
addError (
2020-11-09 08:34:35 +00:00
fmt . Errorf (
2020-11-09 07:29:40 +00:00
"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 ,
2020-11-09 07:29:40 +00:00
order . String ( ) ) )
2020-11-09 06:56:54 +00:00
continue
}
accumulativeBaseSellQuantity += quantity
2020-10-26 10:28:34 +00:00
}
2020-10-26 13:36:47 +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 ) {
2020-10-25 16:26:17 +00:00
for _ , order := range orders {
2020-12-21 05:47:40 +00:00
o , err := session . FormatOrder ( order )
2020-10-26 10:28:34 +00:00
if err != nil {
return formattedOrders , err
2020-10-25 16:26:17 +00:00
}
2020-10-26 10:28:34 +00:00
formattedOrders = append ( formattedOrders , o )
2020-10-25 16:26:17 +00:00
}
2020-10-26 10:17:18 +00:00
return formattedOrders , err
2020-10-25 16:26:17 +00:00
}
2020-11-10 06:19:33 +00:00
func max ( a , b int64 ) int64 {
if a > b {
return a
}
return b
}