2022-06-18 08:31:53 +00:00
package bbgo
import (
"context"
2022-07-04 12:13:54 +00:00
"fmt"
2023-06-16 06:56:22 +00:00
"github.com/c9s/bbgo/pkg/report"
2022-06-27 10:17:57 +00:00
"strings"
2022-09-16 04:19:30 +00:00
"time"
2022-06-18 08:31:53 +00:00
2022-11-02 04:31:35 +00:00
"github.com/pkg/errors"
2022-06-18 08:31:53 +00:00
log "github.com/sirupsen/logrus"
2022-09-09 10:41:06 +00:00
"go.uber.org/multierr"
2022-06-18 08:31:53 +00:00
2023-07-04 13:42:24 +00:00
"github.com/c9s/bbgo/pkg/core"
2023-06-29 02:56:07 +00:00
"github.com/c9s/bbgo/pkg/exchange/retry"
2022-06-18 08:31:53 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
2022-09-16 04:19:30 +00:00
"github.com/c9s/bbgo/pkg/util"
2022-06-18 08:31:53 +00:00
)
2022-09-23 17:25:28 +00:00
var ErrExceededSubmitOrderRetryLimit = errors . New ( "exceeded submit order retry limit" )
// quantityReduceDelta is used to modify the order to submit, especially for the market order
var quantityReduceDelta = fixedpoint . NewFromFloat ( 0.005 )
// submitOrderRetryLimit is used when SubmitOrder failed, we will re-submit the order.
// This is for the maximum retries
const submitOrderRetryLimit = 5
2022-06-18 08:31:53 +00:00
// GeneralOrderExecutor implements the general order executor for strategy
type GeneralOrderExecutor struct {
session * ExchangeSession
symbol string
strategy string
strategyInstanceID string
position * types . Position
activeMakerOrders * ActiveOrderBook
2023-07-04 13:42:24 +00:00
orderStore * core . OrderStore
2023-07-05 07:26:36 +00:00
tradeCollector * core . TradeCollector
2022-09-16 04:19:30 +00:00
2023-03-02 08:16:14 +00:00
logger log . FieldLogger
2022-09-16 04:19:30 +00:00
marginBaseMaxBorrowable , marginQuoteMaxBorrowable fixedpoint . Value
2022-09-19 05:12:49 +00:00
2023-03-23 04:51:52 +00:00
maxRetries uint
2022-11-16 09:10:58 +00:00
disableNotify bool
2022-06-18 08:31:53 +00:00
}
func NewGeneralOrderExecutor ( session * ExchangeSession , symbol , strategy , strategyInstanceID string , position * types . Position ) * GeneralOrderExecutor {
// Always update the position fields
position . Strategy = strategy
position . StrategyInstanceID = strategyInstanceID
2023-07-04 13:42:24 +00:00
orderStore := core . NewOrderStore ( symbol )
2022-09-16 04:19:30 +00:00
executor := & GeneralOrderExecutor {
2022-06-18 08:31:53 +00:00
session : session ,
symbol : symbol ,
strategy : strategy ,
strategyInstanceID : strategyInstanceID ,
position : position ,
activeMakerOrders : NewActiveOrderBook ( symbol ) ,
orderStore : orderStore ,
2023-07-05 07:26:36 +00:00
tradeCollector : core . NewTradeCollector ( symbol , position , orderStore ) ,
2022-06-18 08:31:53 +00:00
}
2022-09-16 04:19:30 +00:00
2023-07-04 13:31:47 +00:00
if session != nil && session . Margin {
2022-09-16 04:19:30 +00:00
executor . startMarginAssetUpdater ( context . Background ( ) )
}
return executor
}
2022-11-16 09:10:58 +00:00
func ( e * GeneralOrderExecutor ) DisableNotify ( ) {
e . disableNotify = true
}
2023-03-23 04:51:52 +00:00
func ( e * GeneralOrderExecutor ) SetMaxRetries ( maxRetries uint ) {
e . maxRetries = maxRetries
}
2022-09-16 04:19:30 +00:00
func ( e * GeneralOrderExecutor ) startMarginAssetUpdater ( ctx context . Context ) {
marginService , ok := e . session . Exchange . ( types . MarginBorrowRepayService )
if ! ok {
log . Warnf ( "session %s (%T) exchange does not support MarginBorrowRepayService" , e . session . Name , e . session . Exchange )
return
}
go e . marginAssetMaxBorrowableUpdater ( ctx , 30 * time . Minute , marginService , e . position . Market )
}
2022-09-19 01:10:59 +00:00
func ( e * GeneralOrderExecutor ) updateMarginAssetMaxBorrowable ( ctx context . Context , marginService types . MarginBorrowRepayService , market types . Market ) {
maxBorrowable , err := marginService . QueryMarginAssetMaxBorrowable ( ctx , market . BaseCurrency )
if err != nil {
2022-09-19 06:56:13 +00:00
log . WithError ( err ) . Errorf ( "can not query margin base asset %s max borrowable" , market . BaseCurrency )
2022-09-19 01:10:59 +00:00
} else {
log . Infof ( "updating margin base asset %s max borrowable amount: %f" , market . BaseCurrency , maxBorrowable . Float64 ( ) )
e . marginBaseMaxBorrowable = maxBorrowable
}
maxBorrowable , err = marginService . QueryMarginAssetMaxBorrowable ( ctx , market . QuoteCurrency )
if err != nil {
2022-09-19 06:56:13 +00:00
log . WithError ( err ) . Errorf ( "can not query margin quote asset %s max borrowable" , market . QuoteCurrency )
2022-09-19 01:10:59 +00:00
} else {
log . Infof ( "updating margin quote asset %s max borrowable amount: %f" , market . QuoteCurrency , maxBorrowable . Float64 ( ) )
e . marginQuoteMaxBorrowable = maxBorrowable
}
}
2022-09-16 04:19:30 +00:00
func ( e * GeneralOrderExecutor ) marginAssetMaxBorrowableUpdater ( ctx context . Context , interval time . Duration , marginService types . MarginBorrowRepayService , market types . Market ) {
2022-09-19 01:10:59 +00:00
t := time . NewTicker ( util . MillisecondsJitter ( interval , 500 ) )
defer t . Stop ( )
2022-09-16 04:19:30 +00:00
2022-09-19 01:25:54 +00:00
e . updateMarginAssetMaxBorrowable ( ctx , marginService , market )
2022-09-16 04:19:30 +00:00
for {
select {
case <- ctx . Done ( ) :
return
2022-09-19 01:10:59 +00:00
case <- t . C :
e . updateMarginAssetMaxBorrowable ( ctx , marginService , market )
2022-09-16 04:19:30 +00:00
}
}
2022-06-18 08:31:53 +00:00
}
2023-07-04 13:42:24 +00:00
func ( e * GeneralOrderExecutor ) OrderStore ( ) * core . OrderStore {
2022-11-16 09:10:58 +00:00
return e . orderStore
}
2022-07-28 10:34:12 +00:00
func ( e * GeneralOrderExecutor ) ActiveMakerOrders ( ) * ActiveOrderBook {
return e . activeMakerOrders
}
2022-06-18 08:31:53 +00:00
func ( e * GeneralOrderExecutor ) BindEnvironment ( environ * Environment ) {
e . tradeCollector . OnProfit ( func ( trade types . Trade , profit * types . Profit ) {
environ . RecordPosition ( e . position , trade , profit )
} )
}
func ( e * GeneralOrderExecutor ) BindTradeStats ( tradeStats * types . TradeStats ) {
e . tradeCollector . OnProfit ( func ( trade types . Trade , profit * types . Profit ) {
if profit == nil {
return
}
2022-07-05 03:14:50 +00:00
tradeStats . Add ( profit )
2022-06-18 08:31:53 +00:00
} )
}
2022-06-19 04:29:36 +00:00
func ( e * GeneralOrderExecutor ) BindProfitStats ( profitStats * types . ProfitStats ) {
2022-06-18 08:31:53 +00:00
e . tradeCollector . OnProfit ( func ( trade types . Trade , profit * types . Profit ) {
profitStats . AddTrade ( trade )
if profit == nil {
return
}
profitStats . AddProfit ( * profit )
2022-07-08 08:43:32 +00:00
2023-03-25 17:32:47 +00:00
if ! e . disableNotify {
Notify ( profit )
Notify ( profitStats )
}
2022-06-18 08:31:53 +00:00
} )
}
2023-06-16 06:56:22 +00:00
func ( e * GeneralOrderExecutor ) BindProfitTracker ( profitTracker * report . ProfitTracker ) {
profitTracker . Bind ( e . tradeCollector , e . session )
}
2022-11-16 09:10:58 +00:00
func ( e * GeneralOrderExecutor ) Bind ( ) {
2022-06-18 08:31:53 +00:00
e . activeMakerOrders . BindStream ( e . session . UserDataStream )
e . orderStore . BindStream ( e . session . UserDataStream )
2022-11-16 09:10:58 +00:00
if ! e . disableNotify {
2022-11-09 08:43:51 +00:00
// trade notify
e . tradeCollector . OnTrade ( func ( trade types . Trade , profit , netProfit fixedpoint . Value ) {
Notify ( trade )
} )
2022-06-18 08:31:53 +00:00
2022-11-09 08:43:51 +00:00
e . tradeCollector . OnPositionUpdate ( func ( position * types . Position ) {
log . Infof ( "position changed: %s" , position )
Notify ( position )
} )
}
2022-06-18 08:31:53 +00:00
e . tradeCollector . BindStream ( e . session . UserDataStream )
}
2022-06-30 16:57:19 +00:00
// CancelOrders cancels the given order objects directly
2022-06-26 08:13:58 +00:00
func ( e * GeneralOrderExecutor ) CancelOrders ( ctx context . Context , orders ... types . Order ) error {
2022-08-11 05:49:16 +00:00
err := e . session . Exchange . CancelOrders ( ctx , orders ... )
if err != nil { // Retry once
err = e . session . Exchange . CancelOrders ( ctx , orders ... )
}
return err
2022-06-26 08:13:58 +00:00
}
2023-03-02 08:16:14 +00:00
func ( e * GeneralOrderExecutor ) SetLogger ( logger log . FieldLogger ) {
e . logger = logger
}
2022-06-19 07:57:59 +00:00
func ( e * GeneralOrderExecutor ) SubmitOrders ( ctx context . Context , submitOrders ... types . SubmitOrder ) ( types . OrderSlice , error ) {
2022-06-18 08:31:53 +00:00
formattedOrders , err := e . session . FormatOrders ( submitOrders )
if err != nil {
2022-06-19 07:57:59 +00:00
return nil , err
2022-06-18 08:31:53 +00:00
}
2023-02-23 15:34:26 +00:00
orderCreateCallback := func ( createdOrder types . Order ) {
e . orderStore . Add ( createdOrder )
e . activeMakerOrders . Add ( createdOrder )
2023-02-24 04:50:43 +00:00
e . tradeCollector . Process ( )
2022-06-18 08:31:53 +00:00
}
2022-09-09 09:55:51 +00:00
2023-03-23 04:51:52 +00:00
if e . maxRetries == 0 {
createdOrders , _ , err := BatchPlaceOrder ( ctx , e . session . Exchange , orderCreateCallback , formattedOrders ... )
return createdOrders , err
}
2023-03-02 08:57:29 +00:00
createdOrders , _ , err := BatchRetryPlaceOrder ( ctx , e . session . Exchange , nil , orderCreateCallback , e . logger , formattedOrders ... )
2023-03-01 07:29:26 +00:00
return createdOrders , err
2022-06-18 08:31:53 +00:00
}
2022-09-09 05:57:39 +00:00
type OpenPositionOptions struct {
// Long is for open a long position
2022-09-12 15:24:37 +00:00
// Long or Short must be set, avoid loading it from the config file
// it should be set from the strategy code
Long bool ` json:"-" yaml:"-" `
2022-09-09 05:57:39 +00:00
// Short is for open a short position
// Long or Short must be set
2022-09-12 15:24:37 +00:00
Short bool ` json:"-" yaml:"-" `
2022-09-09 05:57:39 +00:00
// Leverage is used for leveraged position and account
2022-09-12 15:48:40 +00:00
// Leverage is not effected when using non-leverage spot account
2022-09-22 04:01:26 +00:00
Leverage fixedpoint . Value ` json:"leverage,omitempty" modifiable:"true" `
2022-09-09 05:57:39 +00:00
2022-09-12 15:48:40 +00:00
// Quantity will be used first, it will override the leverage if it's given
2022-09-29 11:15:10 +00:00
Quantity fixedpoint . Value ` json:"quantity,omitempty" modifiable:"true" `
2022-09-09 09:50:21 +00:00
// LimitOrder set to true to open a position with a limit order
2022-09-22 04:01:26 +00:00
// default is false, and will send MarketOrder
LimitOrder bool ` json:"limitOrder,omitempty" modifiable:"true" `
2022-09-09 09:50:21 +00:00
2022-09-12 15:24:37 +00:00
// LimitOrderTakerRatio is used when LimitOrder = true, it adjusts the price of the limit order with a ratio.
2022-09-09 09:50:21 +00:00
// 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.
2022-09-12 15:48:40 +00:00
//
// limitOrderTakerRatio is the price ratio to adjust your limit order as a taker order. e.g., 0.1%
// for sell order, 0.1% ratio means your final price = price * (1 - 0.1%)
// for buy order, 0.1% ratio means your final price = price * (1 + 0.1%)
// this is only enabled when the limitOrder option set to true
2022-09-12 15:24:37 +00:00
LimitOrderTakerRatio fixedpoint . Value ` json:"limitOrderTakerRatio,omitempty" `
Price fixedpoint . Value ` json:"-" yaml:"-" `
Tags [ ] string ` json:"-" yaml:"-" `
2022-09-09 05:57:39 +00:00
}
2022-09-22 11:26:18 +00:00
func ( e * GeneralOrderExecutor ) reduceQuantityAndSubmitOrder ( ctx context . Context , price fixedpoint . Value , submitOrder types . SubmitOrder ) ( types . OrderSlice , error ) {
2022-09-23 17:25:28 +00:00
var err error
for i := 0 ; i < submitOrderRetryLimit ; i ++ {
q := submitOrder . Quantity . Mul ( fixedpoint . One . Sub ( quantityReduceDelta ) )
2022-11-26 16:25:29 +00:00
if ! e . session . Futures && ! e . session . Margin {
2022-10-18 10:59:04 +00:00
if submitOrder . Side == types . SideTypeSell {
if baseBalance , ok := e . session . GetAccount ( ) . Balance ( e . position . Market . BaseCurrency ) ; ok {
q = fixedpoint . Min ( q , baseBalance . Available )
}
} else {
if quoteBalance , ok := e . session . GetAccount ( ) . Balance ( e . position . Market . QuoteCurrency ) ; ok {
q = fixedpoint . Min ( q , quoteBalance . Available . Div ( price ) )
}
2022-09-28 11:06:37 +00:00
}
}
log . Warnf ( "retrying order, adjusting order quantity: %v -> %v" , submitOrder . Quantity , q )
2022-09-23 17:25:28 +00:00
submitOrder . Quantity = q
if e . position . Market . IsDustQuantity ( submitOrder . Quantity , price ) {
2023-06-06 07:50:07 +00:00
return nil , types . NewZeroAssetError ( fmt . Errorf ( "dust quantity, quantity = %f, price = %f" , submitOrder . Quantity . Float64 ( ) , price . Float64 ( ) ) )
2022-09-23 17:25:28 +00:00
}
2022-09-22 11:26:18 +00:00
createdOrder , err2 := e . SubmitOrders ( ctx , submitOrder )
if err2 != nil {
2022-09-23 17:25:28 +00:00
// collect the error object
err = multierr . Append ( err , err2 )
2022-09-22 11:26:18 +00:00
continue
}
2022-09-23 17:25:28 +00:00
2022-09-22 11:26:18 +00:00
log . Infof ( "created order: %+v" , createdOrder )
return createdOrder , nil
}
2022-09-23 17:25:28 +00:00
return nil , multierr . Append ( ErrExceededSubmitOrderRetryLimit , err )
2022-09-22 11:26:18 +00:00
}
2022-09-22 04:01:26 +00:00
2022-11-21 03:13:19 +00:00
// Create new submitOrder from OpenPositionOptions.
// @param ctx: golang context type.
// @param options: OpenPositionOptions to control the generated SubmitOrder in a higher level way. Notice that the Price in options will be updated as the submitOrder price.
// @return *types.SubmitOrder: SubmitOrder with calculated quantity and price.
// @return error: Error message.
func ( e * GeneralOrderExecutor ) NewOrderFromOpenPosition ( ctx context . Context , options * OpenPositionOptions ) ( * types . SubmitOrder , error ) {
2022-09-12 15:24:37 +00:00
price := options . Price
2022-09-09 05:57:39 +00:00
submitOrder := types . SubmitOrder {
Symbol : e . position . Symbol ,
Type : types . OrderTypeMarket ,
MarginSideEffect : types . SideEffectTypeMarginBuy ,
2022-09-10 18:36:45 +00:00
Tag : strings . Join ( options . Tags , "," ) ,
2022-09-09 05:57:39 +00:00
}
2022-09-28 11:06:37 +00:00
baseBalance , _ := e . session . GetAccount ( ) . Balance ( e . position . Market . BaseCurrency )
2022-09-19 05:12:49 +00:00
// FIXME: fix the max quote borrowing checking
// quoteBalance, _ := e.session.Account.Balance(e.position.Market.QuoteCurrency)
2022-09-12 15:24:37 +00:00
if ! options . LimitOrderTakerRatio . IsZero ( ) {
2022-09-23 17:27:28 +00:00
if options . Price . IsZero ( ) {
return nil , fmt . Errorf ( "OpenPositionOptions.Price is zero, can not adjust limit taker order price, options given: %+v" , options )
}
2022-09-09 05:57:39 +00:00
if options . Long {
// use higher price to buy (this ensures that our order will be filled)
2022-09-12 15:24:37 +00:00
price = price . Mul ( one . Add ( options . LimitOrderTakerRatio ) )
2022-11-09 08:43:51 +00:00
options . Price = price
2022-09-09 05:57:39 +00:00
} else if options . Short {
// use lower price to sell (this ensures that our order will be filled)
2022-09-12 15:24:37 +00:00
price = price . Mul ( one . Sub ( options . LimitOrderTakerRatio ) )
2022-11-09 08:43:51 +00:00
options . Price = price
2022-09-09 05:57:39 +00:00
}
}
2022-09-22 04:01:26 +00:00
if options . LimitOrder {
2022-09-09 05:57:39 +00:00
submitOrder . Type = types . OrderTypeLimit
submitOrder . Price = price
}
quantity := options . Quantity
if options . Long {
if quantity . IsZero ( ) {
2022-09-09 09:40:17 +00:00
quoteQuantity , err := CalculateQuoteQuantity ( ctx , e . session , e . position . QuoteCurrency , options . Leverage )
2022-09-09 05:57:39 +00:00
if err != nil {
2022-09-22 04:01:26 +00:00
return nil , err
2022-09-09 05:57:39 +00:00
}
2022-09-22 11:26:18 +00:00
2023-06-06 07:50:07 +00:00
if price . IsZero ( ) {
return nil , errors . New ( "unable to calculate quantity: zero price given" )
}
2022-09-09 05:57:39 +00:00
quantity = quoteQuantity . Div ( price )
}
2023-06-06 07:50:07 +00:00
2022-09-22 11:26:18 +00:00
if e . position . Market . IsDustQuantity ( quantity , price ) {
2023-06-06 07:50:07 +00:00
log . Errorf ( "can not submit order: dust quantity, quantity = %f, price = %f" , quantity . Float64 ( ) , price . Float64 ( ) )
2022-09-22 04:01:26 +00:00
return nil , nil
}
2022-09-09 05:57:39 +00:00
2022-09-19 01:10:59 +00:00
quoteQuantity := quantity . Mul ( price )
2022-09-19 08:00:12 +00:00
if e . session . Margin && ! e . marginQuoteMaxBorrowable . IsZero ( ) && quoteQuantity . Compare ( e . marginQuoteMaxBorrowable ) > 0 {
2022-09-19 01:10:59 +00:00
log . Warnf ( "adjusting quantity %f according to the max margin quote borrowable amount: %f" , quantity . Float64 ( ) , e . marginQuoteMaxBorrowable . Float64 ( ) )
quantity = AdjustQuantityByMaxAmount ( quantity , price , e . marginQuoteMaxBorrowable )
}
2022-09-09 05:57:39 +00:00
submitOrder . Side = types . SideTypeBuy
submitOrder . Quantity = quantity
2022-11-09 08:43:51 +00:00
return & submitOrder , nil
2022-09-09 05:57:39 +00:00
} else if options . Short {
if quantity . IsZero ( ) {
var err error
2022-09-09 09:40:17 +00:00
quantity , err = CalculateBaseQuantity ( e . session , e . position . Market , price , quantity , options . Leverage )
2022-09-09 05:57:39 +00:00
if err != nil {
2022-09-22 04:01:26 +00:00
return nil , err
2022-09-09 05:57:39 +00:00
}
}
2022-09-22 11:26:18 +00:00
if e . position . Market . IsDustQuantity ( quantity , price ) {
2022-09-22 04:01:26 +00:00
log . Warnf ( "dust quantity: %v" , quantity )
return nil , nil
}
2022-09-09 05:57:39 +00:00
2022-09-19 08:00:12 +00:00
if e . session . Margin && ! e . marginBaseMaxBorrowable . IsZero ( ) && quantity . Sub ( baseBalance . Available ) . Compare ( e . marginBaseMaxBorrowable ) > 0 {
2022-09-19 01:10:59 +00:00
log . Warnf ( "adjusting %f quantity according to the max margin base borrowable amount: %f" , quantity . Float64 ( ) , e . marginBaseMaxBorrowable . Float64 ( ) )
2022-09-19 05:12:49 +00:00
// quantity = fixedpoint.Min(quantity, e.marginBaseMaxBorrowable)
quantity = baseBalance . Available . Add ( e . marginBaseMaxBorrowable )
2022-09-19 01:10:59 +00:00
}
2022-09-09 05:57:39 +00:00
submitOrder . Side = types . SideTypeSell
submitOrder . Quantity = quantity
2022-11-09 08:43:51 +00:00
return & submitOrder , nil
2022-09-09 05:57:39 +00:00
}
2022-09-22 04:01:26 +00:00
return nil , errors . New ( "options Long or Short must be set" )
2022-09-09 05:57:39 +00:00
}
2022-11-21 03:13:19 +00:00
// OpenPosition sends the orders generated from OpenPositionOptions to the exchange by calling SubmitOrders or reduceQuantityAndSubmitOrder.
// @param ctx: golang context type.
// @param options: OpenPositionOptions to control the generated SubmitOrder in a higher level way. Notice that the Price in options will be updated as the submitOrder price.
// @return types.OrderSlice: Created orders with information from exchange.
// @return error: Error message.
2022-11-09 08:43:51 +00:00
func ( e * GeneralOrderExecutor ) OpenPosition ( ctx context . Context , options OpenPositionOptions ) ( types . OrderSlice , error ) {
2023-06-29 05:29:31 +00:00
if e . position . IsClosing ( ) {
return nil , errors . Wrap ( ErrPositionAlreadyClosing , "unable to open position" )
}
2022-11-21 03:13:19 +00:00
submitOrder , err := e . NewOrderFromOpenPosition ( ctx , & options )
2022-11-09 08:43:51 +00:00
if err != nil {
return nil , err
}
2023-06-06 07:50:07 +00:00
2022-11-09 08:43:51 +00:00
if submitOrder == nil {
return nil , nil
}
2023-06-06 07:50:07 +00:00
2022-11-09 08:43:51 +00:00
price := options . Price
side := "long"
if submitOrder . Side == types . SideTypeSell {
side = "short"
}
2023-06-06 07:50:07 +00:00
Notify ( "Opening %s %s position with quantity %f at price %f" , e . position . Symbol , side , submitOrder . Quantity . Float64 ( ) , price . Float64 ( ) )
2022-11-09 08:43:51 +00:00
createdOrder , err := e . SubmitOrders ( ctx , * submitOrder )
if err == nil {
return createdOrder , nil
}
return e . reduceQuantityAndSubmitOrder ( ctx , price , * submitOrder )
}
2022-06-30 16:57:19 +00:00
// GracefulCancelActiveOrderBook cancels the orders from the active orderbook.
2022-11-02 04:34:04 +00:00
func ( e * GeneralOrderExecutor ) GracefulCancelActiveOrderBook ( ctx context . Context , activeOrders * ActiveOrderBook ) error {
2022-07-14 03:46:19 +00:00
if activeOrders . NumOfOrders ( ) == 0 {
return nil
}
2022-11-02 04:25:34 +00:00
2022-11-02 04:34:04 +00:00
if err := activeOrders . GracefulCancel ( ctx , e . session . Exchange ) ; err != nil {
2022-08-11 05:49:16 +00:00
// Retry once
if err = activeOrders . GracefulCancel ( ctx , e . session . Exchange ) ; err != nil {
2022-11-02 04:34:04 +00:00
return errors . Wrap ( err , "graceful cancel error" )
2022-08-11 05:49:16 +00:00
}
2022-06-18 08:31:53 +00:00
}
2022-06-19 05:40:10 +00:00
e . tradeCollector . Process ( )
2022-06-18 08:31:53 +00:00
return nil
}
2022-10-17 04:38:58 +00:00
// GracefulCancel cancels all active maker orders if orders are not given, otherwise cancel all the given orders
func ( e * GeneralOrderExecutor ) GracefulCancel ( ctx context . Context , orders ... types . Order ) error {
2022-11-02 04:34:04 +00:00
if err := e . activeMakerOrders . GracefulCancel ( ctx , e . session . Exchange , orders ... ) ; err != nil {
return errors . Wrap ( err , "graceful cancel error" )
}
return nil
2022-10-17 04:38:58 +00:00
}
2023-06-29 05:29:31 +00:00
var ErrPositionAlreadyClosing = errors . New ( "position is already in closing process" )
2023-06-28 10:09:10 +00:00
2022-09-10 18:01:48 +00:00
// 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.
2022-06-27 10:17:57 +00:00
func ( e * GeneralOrderExecutor ) ClosePosition ( ctx context . Context , percentage fixedpoint . Value , tags ... string ) error {
2023-06-28 10:09:10 +00:00
if ! e . position . SetClosing ( true ) {
return ErrPositionAlreadyClosing
2022-06-18 08:31:53 +00:00
}
2023-06-28 10:09:10 +00:00
defer e . position . SetClosing ( false )
2022-06-18 08:31:53 +00:00
2023-06-28 10:09:10 +00:00
submitOrder := e . position . NewMarketCloseOrder ( percentage )
if submitOrder == nil {
2022-09-19 05:23:23 +00:00
return nil
}
2022-10-07 05:06:32 +00:00
if e . session . Futures { // Futures: Use base qty in e.position
2023-03-29 10:28:25 +00:00
submitOrder . Quantity = e . position . GetBase ( ) . Abs ( )
submitOrder . ReduceOnly = true
2023-03-29 09:46:54 +00:00
2022-10-07 05:06:32 +00:00
if e . position . IsLong ( ) {
submitOrder . Side = types . SideTypeSell
2022-10-07 05:28:24 +00:00
} else if e . position . IsShort ( ) {
2022-10-07 05:06:32 +00:00
submitOrder . Side = types . SideTypeBuy
2022-10-07 05:28:24 +00:00
} else {
2023-03-29 09:46:54 +00:00
return fmt . Errorf ( "unexpected position side: %+v" , e . position )
2022-10-07 05:28:24 +00:00
}
2022-10-07 05:06:32 +00:00
} else { // Spot and spot margin
// check base balance and adjust the close position order
if e . position . IsLong ( ) {
if baseBalance , ok := e . session . Account . Balance ( e . position . Market . BaseCurrency ) ; ok {
submitOrder . Quantity = fixedpoint . Min ( submitOrder . Quantity , baseBalance . Available )
2022-09-19 05:23:23 +00:00
}
2022-10-07 05:06:32 +00:00
if submitOrder . Quantity . IsZero ( ) {
return fmt . Errorf ( "insufficient base balance, can not sell: %+v" , submitOrder )
}
} else if e . position . IsShort ( ) {
// TODO: check quote balance here, we also need the current price to validate, need to design.
/ *
if quoteBalance , ok := e . session . Account . Balance ( e . position . Market . QuoteCurrency ) ; ok {
// AdjustQuantityByMaxAmount(submitOrder.Quantity, quoteBalance.Available)
// submitOrder.Quantity = fixedpoint.Min(submitOrder.Quantity,)
}
* /
}
2022-09-11 09:46:23 +00:00
}
2022-09-10 18:33:32 +00:00
tagStr := strings . Join ( tags , "," )
submitOrder . Tag = tagStr
2023-03-29 09:46:54 +00:00
Notify ( "Closing %s position %s with tags: %s" , e . symbol , percentage . Percentage ( ) , tagStr )
2022-09-10 18:36:45 +00:00
2023-06-28 10:09:10 +00:00
createdOrders , err := e . SubmitOrders ( ctx , * submitOrder )
if err != nil {
return err
}
2023-06-28 10:11:00 +00:00
if queryOrderService , ok := e . session . Exchange . ( types . ExchangeOrderQueryService ) ; ok && ! IsBackTesting {
2023-06-28 10:09:10 +00:00
switch submitOrder . Type {
case types . OrderTypeMarket :
2023-06-29 09:17:32 +00:00
_ , err2 := retry . QueryOrderUntilFilled ( ctx , queryOrderService , createdOrders [ 0 ] . Symbol , createdOrders [ 0 ] . OrderID )
2023-06-29 02:56:07 +00:00
if err2 != nil {
log . WithError ( err2 ) . Errorf ( "unable to query order" )
}
2023-06-28 10:09:10 +00:00
}
}
return nil
2022-06-18 08:31:53 +00:00
}
2022-06-19 05:40:10 +00:00
2023-07-05 07:26:36 +00:00
func ( e * GeneralOrderExecutor ) TradeCollector ( ) * core . TradeCollector {
2022-06-19 05:40:10 +00:00
return e . tradeCollector
}
2022-06-26 08:13:58 +00:00
func ( e * GeneralOrderExecutor ) Session ( ) * ExchangeSession {
return e . session
}
func ( e * GeneralOrderExecutor ) Position ( ) * types . Position {
return e . position
}
2022-09-14 07:54:43 +00:00
// This implements PositionReader interface
func ( e * GeneralOrderExecutor ) CurrentPosition ( ) * types . Position {
return e . position
}
// This implements PositionResetter interface
func ( e * GeneralOrderExecutor ) ResetPosition ( ) error {
e . position . Reset ( )
return nil
}