2020-10-25 16:26:17 +00:00
package bbgo
import (
"context"
2020-10-26 10:28:34 +00:00
"fmt"
"math"
2020-10-25 16:26:17 +00:00
"github.com/pkg/errors"
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"
)
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 {
return nil , errors . Errorf ( "exchange session %s not found" , session )
}
2020-10-26 10:17:18 +00:00
formattedOrders , err := formatOrders ( orders , es )
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.
type ExchangeOrderExecutor struct {
2020-10-26 10:17:18 +00:00
Notifiability ` json:"-" `
2020-10-25 16:26:17 +00:00
2020-10-26 10:17:18 +00:00
session * ExchangeSession ` json:"-" `
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-10-26 10:17:18 +00:00
formattedOrders , err := formatOrders ( orders , e . session )
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 )
}
}
e . notifySubmitOrders ( formattedOrders ... )
2020-10-26 10:17:18 +00:00
return e . session . Exchange . SubmitOrders ( ctx , formattedOrders ... )
2020-10-25 16:26:17 +00:00
}
2020-10-26 13:36:47 +00:00
type BasicRiskControlOrderExecutor struct {
2020-10-27 01:58:21 +00:00
* ExchangeOrderExecutor
2020-10-26 10:17:18 +00:00
MinQuoteBalance fixedpoint . Value ` json:"minQuoteBalance,omitempty" `
MaxAssetBalance fixedpoint . Value ` json:"maxBaseAssetBalance,omitempty" `
MinAssetBalance fixedpoint . Value ` json:"minBaseAssetBalance,omitempty" `
MaxOrderAmount fixedpoint . Value ` json:"maxOrderAmount,omitempty" `
}
2020-10-31 11:54:05 +00:00
func ( e * BasicRiskControlOrderExecutor ) SubmitOrders ( ctx context . Context , orders ... types . SubmitOrder ) ( types . OrderSlice , error ) {
2020-10-26 10:28:34 +00:00
var formattedOrders [ ] types . SubmitOrder
for _ , order := range orders {
2020-10-28 08:27:25 +00:00
currentPrice , ok := e . session . LastPrice ( order . Symbol )
if ! ok {
2020-10-26 10:28:34 +00:00
return nil , errors . Errorf ( "the last price of symbol %q is not found" , order . Symbol )
}
market := order . Market
quantity := order . Quantity
balances := e . session . Account . Balances ( )
switch order . Side {
case types . SideTypeBuy :
if balance , ok := balances [ market . QuoteCurrency ] ; ok {
if balance . Available < e . MinQuoteBalance . Float64 ( ) {
return nil , errors . Wrapf ( ErrQuoteBalanceLevelTooLow , "quote balance level is too low: %s < %s" ,
types . USD . FormatMoneyFloat64 ( balance . Available ) ,
types . USD . FormatMoneyFloat64 ( e . MinQuoteBalance . Float64 ( ) ) )
}
if baseBalance , ok := balances [ market . BaseCurrency ] ; ok {
if e . MaxAssetBalance > 0 && baseBalance . Available > e . MaxAssetBalance . Float64 ( ) {
2020-10-26 13:36:47 +00:00
return nil , errors . Wrapf ( ErrAssetBalanceLevelTooHigh , "asset balance level is too high: %f > %f" , baseBalance . Available , e . MaxAssetBalance . Float64 ( ) )
2020-10-26 10:28:34 +00:00
}
}
available := math . Max ( 0.0 , balance . Available - e . MinQuoteBalance . Float64 ( ) )
if available < market . MinAmount {
return nil , errors . Wrapf ( ErrInsufficientQuoteBalance , "insufficient quote balance: %f < min amount %f" , available , market . MinAmount )
}
quantity = adjustQuantityByMinAmount ( quantity , currentPrice , market . MinAmount * 1.01 )
quantity = adjustQuantityByMaxAmount ( quantity , currentPrice , available )
amount := quantity * currentPrice
if amount < market . MinAmount {
return nil , fmt . Errorf ( "amount too small: %f < min amount %f" , amount , market . MinAmount )
}
}
case types . SideTypeSell :
if balance , ok := balances [ market . BaseCurrency ] ; ok {
if e . MinAssetBalance > 0 && balance . Available < e . MinAssetBalance . Float64 ( ) {
2020-10-26 13:36:47 +00:00
return nil , errors . Wrapf ( ErrAssetBalanceLevelTooLow , "asset balance level is too low: %f > %f" , balance . Available , e . MinAssetBalance . Float64 ( ) )
2020-10-26 10:28:34 +00:00
}
quantity = adjustQuantityByMinAmount ( quantity , currentPrice , market . MinNotional * 1.01 )
available := balance . Available
quantity = math . Min ( quantity , available )
if quantity < market . MinQuantity {
return nil , errors . Wrapf ( ErrInsufficientAssetBalance , "insufficient asset balance: %f > minimal quantity %f" , available , market . MinQuantity )
}
notional := quantity * currentPrice
if notional < market . MinNotional {
return nil , fmt . Errorf ( "notional %f < min notional: %f" , notional , market . MinNotional )
}
if quantity < market . MinLot {
return nil , fmt . Errorf ( "quantity %f less than min lot %f" , quantity , market . MinLot )
}
notional = quantity * currentPrice
if notional < market . MinNotional {
return nil , fmt . Errorf ( "notional %f < min notional: %f" , notional , market . MinNotional )
}
}
}
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
o , err := formatOrder ( order , e . session )
if err != nil {
return nil , err
}
formattedOrders = append ( formattedOrders , o )
2020-10-26 10:17:18 +00:00
}
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-10-26 10:28:34 +00:00
func formatOrder ( order types . SubmitOrder , session * ExchangeSession ) ( types . SubmitOrder , error ) {
market , ok := session . Market ( order . Symbol )
if ! ok {
return order , errors . Errorf ( "market is not defined: %s" , order . Symbol )
}
order . Market = market
2020-10-26 14:08:16 +00:00
switch order . Type {
case types . OrderTypeMarket , types . OrderTypeStopMarket :
order . Price = 0.0
order . PriceString = ""
default :
order . PriceString = market . FormatPrice ( order . Price )
}
2020-10-28 09:17:57 +00:00
order . QuantityString = market . FormatQuantity ( order . Quantity )
2020-10-26 10:28:34 +00:00
return order , nil
}
2020-10-26 10:17:18 +00:00
func formatOrders ( orders [ ] types . SubmitOrder , session * ExchangeSession ) ( formattedOrders [ ] types . SubmitOrder , err error ) {
2020-10-25 16:26:17 +00:00
for _ , order := range orders {
2020-10-26 10:28:34 +00:00
o , err := formatOrder ( order , session )
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
}