2022-11-02 10:26:30 +00:00
package grid2
import (
"context"
"fmt"
2023-02-16 13:55:53 +00:00
"math"
2022-12-25 17:35:37 +00:00
"sort"
2022-12-05 11:23:39 +00:00
"strconv"
2023-03-09 16:46:12 +00:00
"strings"
2022-11-02 10:26:30 +00:00
"sync"
2022-12-20 07:56:38 +00:00
"time"
2022-11-02 10:26:30 +00:00
2023-03-10 07:47:42 +00:00
"github.com/google/uuid"
2022-11-02 10:26:30 +00:00
"github.com/pkg/errors"
2023-01-10 12:15:51 +00:00
"github.com/prometheus/client_golang/prometheus"
2022-11-02 10:26:30 +00:00
"github.com/sirupsen/logrus"
2023-03-01 08:16:26 +00:00
"go.uber.org/multierr"
2022-11-02 10:26:30 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
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-11-02 10:26:30 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)
const ID = "grid2"
2022-12-07 08:30:51 +00:00
const orderTag = "grid2"
2022-11-02 10:26:30 +00:00
var log = logrus . WithField ( "strategy" , ID )
2022-12-06 03:56:30 +00:00
var maxNumberOfOrderTradesQueryTries = 10
2022-12-24 09:08:50 +00:00
const historyRollbackDuration = 3 * 24 * time . Hour
const historyRollbackOrderIdRange = 1000
2022-11-02 10:26:30 +00:00
func init ( ) {
// Register the pointer of the strategy struct,
// so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON)
// Note: built-in strategies need to imported manually in the bbgo cmd package.
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
2023-02-15 14:17:36 +00:00
type PrettyPins [ ] Pin
func ( pp PrettyPins ) String ( ) string {
var ss [ ] string
for _ , p := range pp {
price := fixedpoint . Value ( p )
ss = append ( ss , price . String ( ) )
}
return fmt . Sprintf ( "%v" , ss )
}
2022-12-07 06:19:49 +00:00
//go:generate mockgen -destination=mocks/order_executor.go -package=mocks . OrderExecutor
type OrderExecutor interface {
SubmitOrders ( ctx context . Context , submitOrders ... types . SubmitOrder ) ( types . OrderSlice , error )
ClosePosition ( ctx context . Context , percentage fixedpoint . Value , tags ... string ) error
GracefulCancel ( ctx context . Context , orders ... types . Order ) error
2022-12-23 16:54:40 +00:00
ActiveMakerOrders ( ) * bbgo . ActiveOrderBook
2022-12-07 06:19:49 +00:00
}
2023-03-01 08:16:26 +00:00
type advancedOrderCancelApi interface {
2023-03-01 08:35:09 +00:00
CancelAllOrders ( ctx context . Context ) ( [ ] types . Order , error )
2023-03-01 08:16:26 +00:00
CancelOrdersBySymbol ( ctx context . Context , symbol string ) ( [ ] types . Order , error )
2023-03-01 08:35:09 +00:00
CancelOrdersByGroupID ( ctx context . Context , groupID uint32 ) ( [ ] types . Order , error )
2023-03-01 08:16:26 +00:00
}
2022-12-26 10:15:39 +00:00
//go:generate callbackgen -type Strategy
2022-11-02 10:26:30 +00:00
type Strategy struct {
2022-11-10 12:58:46 +00:00
Environment * bbgo . Environment
2022-11-02 10:26:30 +00:00
// Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc
// This field will be injected automatically since we defined the Symbol field.
2022-11-10 15:34:55 +00:00
types . Market ` json:"-" `
2022-11-02 10:26:30 +00:00
// These fields will be filled from the config file (it translates YAML to JSON)
2022-11-10 15:34:55 +00:00
Symbol string ` json:"symbol" `
2022-11-02 10:26:30 +00:00
// ProfitSpread is the fixed profit spread you want to submit the sell order
2022-12-05 17:17:29 +00:00
// When ProfitSpread is enabled, the grid will shift up, e.g.,
// If you opened a grid with the price range 10_000 to 20_000
// With profit spread set to 3_000
// The sell orders will be placed in the range 13_000 to 23_000
// And the buy orders will be placed in the original price range 10_000 to 20_000
2022-11-10 15:34:55 +00:00
ProfitSpread fixedpoint . Value ` json:"profitSpread" `
2022-11-02 10:26:30 +00:00
// GridNum is the grid number, how many orders you want to post on the orderbook.
2022-11-10 15:34:55 +00:00
GridNum int64 ` json:"gridNumber" `
2022-11-02 10:26:30 +00:00
2023-05-22 10:07:40 +00:00
// BaseGridNum is an optional field used for base investment sell orders
BaseGridNum int ` json:"baseGridNumber,omitempty" `
2022-12-25 16:29:31 +00:00
AutoRange * types . SimpleDuration ` json:"autoRange" `
2022-12-24 12:39:11 +00:00
2022-11-10 15:34:55 +00:00
UpperPrice fixedpoint . Value ` json:"upperPrice" `
2022-11-02 10:26:30 +00:00
2022-11-10 15:34:55 +00:00
LowerPrice fixedpoint . Value ` json:"lowerPrice" `
2022-11-09 08:25:00 +00:00
2022-12-03 07:21:03 +00:00
// Compound option is used for buying more inventory when
// the profit is made by the filled sell order.
Compound bool ` json:"compound" `
2022-12-03 08:03:01 +00:00
// EarnBase option is used for earning profit in base currency.
// e.g. earn BTC in BTCUSDT and earn ETH in ETHUSDT
// instead of earn USDT in BTCUSD
EarnBase bool ` json:"earnBase" `
2022-11-10 15:34:55 +00:00
// QuantityOrAmount embeds the Quantity field and the Amount field
// If you set up the Quantity field or the Amount field, you don't need to set the QuoteInvestment and BaseInvestment
2022-11-02 10:26:30 +00:00
bbgo . QuantityOrAmount
2022-11-10 15:34:55 +00:00
// If Quantity and Amount is not set, we can use the quote investment to calculate our quantity.
QuoteInvestment fixedpoint . Value ` json:"quoteInvestment" `
// BaseInvestment is the total base quantity you want to place as the sell order.
BaseInvestment fixedpoint . Value ` json:"baseInvestment" `
2022-12-04 10:01:58 +00:00
TriggerPrice fixedpoint . Value ` json:"triggerPrice" `
2022-12-04 06:22:11 +00:00
StopLossPrice fixedpoint . Value ` json:"stopLossPrice" `
TakeProfitPrice fixedpoint . Value ` json:"takeProfitPrice" `
2022-12-04 03:47:01 +00:00
// CloseWhenCancelOrder option is used to close the grid if any of the order is canceled.
// This option let you simply remote control the grid from the crypto exchange mobile app.
CloseWhenCancelOrder bool ` json:"closeWhenCancelOrder" `
2022-12-04 04:58:01 +00:00
// KeepOrdersWhenShutdown option is used for keeping the grid orders when shutting down bbgo
KeepOrdersWhenShutdown bool ` json:"keepOrdersWhenShutdown" `
2022-12-23 11:15:46 +00:00
// RecoverOrdersWhenStart option is used for recovering grid orders
RecoverOrdersWhenStart bool ` json:"recoverOrdersWhenStart" `
2022-12-23 09:54:30 +00:00
2022-12-04 05:04:14 +00:00
// ClearOpenOrdersWhenStart
// If this is set, when bbgo started, it will clear the open orders in the same market (by symbol)
ClearOpenOrdersWhenStart bool ` json:"clearOpenOrdersWhenStart" `
2023-02-08 08:43:25 +00:00
ClearOpenOrdersIfMismatch bool ` json:"clearOpenOrdersIfMismatch" `
2023-03-09 16:46:12 +00:00
ClearDuplicatedPriceOpenOrders bool ` json:"clearDuplicatedPriceOpenOrders" `
2023-03-01 08:16:26 +00:00
// UseCancelAllOrdersApiWhenClose uses a different API to cancel all the orders on the market when closing a grid
UseCancelAllOrdersApiWhenClose bool ` json:"useCancelAllOrdersApiWhenClose" `
2023-03-02 07:50:10 +00:00
// ResetPositionWhenStart resets the position when the strategy is started
2022-12-05 11:37:42 +00:00
ResetPositionWhenStart bool ` json:"resetPositionWhenStart" `
2023-03-02 07:53:42 +00:00
// StopIfLessThanMinimalQuoteInvestment stops the strategy if the quote investment does not match
2023-03-02 07:50:10 +00:00
StopIfLessThanMinimalQuoteInvestment bool ` json:"stopIfLessThanMinimalQuoteInvestment" `
2023-03-03 06:10:34 +00:00
OrderFillDelay types . Duration ` json:"orderFillDelay" `
2023-01-10 12:15:51 +00:00
// PrometheusLabels will be used as the base prometheus labels
PrometheusLabels prometheus . Labels ` json:"prometheusLabels" `
2023-02-08 08:26:37 +00:00
// OrderGroupID is the group ID used for the strategy instance for canceling orders
OrderGroupID uint32 ` json:"orderGroupID" `
2023-02-15 09:33:07 +00:00
LogFields logrus . Fields ` json:"logFields" `
2022-12-05 10:15:30 +00:00
// FeeRate is used for calculating the minimal profit spread.
// it makes sure that your grid configuration is profitable.
FeeRate fixedpoint . Value ` json:"feeRate" `
2023-05-19 10:41:07 +00:00
SkipSpreadCheck bool ` json:"skipSpreadCheck" `
RecoverGridByScanningTrades bool ` json:"recoverGridByScanningTrades" `
2023-05-24 06:58:19 +00:00
RecoverGridWithin time . Duration ` json:"recoverGridWithin" `
2022-12-15 10:09:43 +00:00
2023-04-26 08:14:53 +00:00
EnableProfitFixer bool ` json:"enableProfitFixer" `
FixProfitSince * types . Time ` json:"fixProfitSince" `
2023-03-09 16:50:25 +00:00
// Debug enables the debug mode
Debug bool ` json:"debug" `
2022-12-25 17:24:56 +00:00
GridProfitStats * GridProfitStats ` persistence:"grid_profit_stats" `
Position * types . Position ` persistence:"position" `
2022-11-02 10:26:30 +00:00
2023-02-20 08:52:39 +00:00
// ExchangeSession is an injection field
ExchangeSession * bbgo . ExchangeSession
2022-12-04 07:43:27 +00:00
grid * Grid
session * bbgo . ExchangeSession
orderQueryService types . ExchangeOrderQueryService
2022-12-07 06:19:49 +00:00
orderExecutor OrderExecutor
2023-07-04 13:42:24 +00:00
historicalTrades * core . TradeStore
2022-11-10 12:58:46 +00:00
2022-12-03 03:31:44 +00:00
logger * logrus . Entry
2022-12-26 10:15:39 +00:00
gridReadyCallbacks [ ] func ( )
gridProfitCallbacks [ ] func ( stats * GridProfitStats , profit * GridProfit )
gridClosedCallbacks [ ] func ( )
2023-01-10 12:15:51 +00:00
gridErrorCallbacks [ ] func ( err error )
2023-02-06 17:38:25 +00:00
2023-03-10 07:33:19 +00:00
// filledOrderIDMap is used to prevent processing the same order ID twice.
filledOrderIDMap * types . SyncOrderMap
2023-02-06 17:38:25 +00:00
// mu is used for locking the grid object field, avoid double grid opening
mu sync . Mutex
2023-03-03 10:42:08 +00:00
tradingCtx , writeCtx context . Context
cancelWrite context . CancelFunc
2023-04-26 09:40:01 +00:00
2023-10-17 08:13:05 +00:00
activeOrdersRecoverC chan struct { }
2023-09-18 09:57:12 +00:00
2023-04-26 09:40:01 +00:00
// this ensures that bbgo.Sync to lock the object
sync . Mutex
2022-11-02 10:26:30 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) Validate ( ) error {
2022-12-25 16:29:31 +00:00
if s . AutoRange == nil {
if s . UpperPrice . IsZero ( ) {
return errors . New ( "upperPrice can not be zero, you forgot to set?" )
}
2022-11-02 10:26:30 +00:00
2022-12-25 16:29:31 +00:00
if s . LowerPrice . IsZero ( ) {
return errors . New ( "lowerPrice can not be zero, you forgot to set?" )
}
2022-11-02 10:26:30 +00:00
2022-12-25 16:29:31 +00:00
if s . UpperPrice . Compare ( s . LowerPrice ) <= 0 {
return fmt . Errorf ( "upperPrice (%s) should not be less than or equal to lowerPrice (%s)" , s . UpperPrice . String ( ) , s . LowerPrice . String ( ) )
}
2022-11-02 10:26:30 +00:00
}
2023-02-13 08:11:42 +00:00
if s . GridNum == 0 || s . GridNum == 1 {
return fmt . Errorf ( "gridNum can not be zero or one" )
2022-12-06 07:55:52 +00:00
}
2022-12-15 10:09:43 +00:00
if ! s . SkipSpreadCheck {
if err := s . checkSpread ( ) ; err != nil {
return errors . Wrapf ( err , "spread is too small, please try to reduce your gridNum or increase the price range (upperPrice and lowerPrice)" )
}
2022-12-03 08:59:47 +00:00
}
2023-05-26 08:09:07 +00:00
if ! s . QuantityOrAmount . IsSet ( ) && s . QuoteInvestment . IsZero ( ) && s . BaseInvestment . IsZero ( ) {
2022-12-07 04:29:14 +00:00
return fmt . Errorf ( "either quantity, amount or quoteInvestment must be set" )
2022-11-02 10:26:30 +00:00
}
return nil
}
2023-01-10 12:15:51 +00:00
func ( s * Strategy ) Defaults ( ) error {
2023-02-20 08:52:39 +00:00
if s . LogFields == nil {
s . LogFields = logrus . Fields { }
}
s . LogFields [ "symbol" ] = s . Symbol
s . LogFields [ "strategy" ] = ID
return nil
}
func ( s * Strategy ) Initialize ( ) error {
2023-03-10 07:33:19 +00:00
s . filledOrderIDMap = types . NewSyncOrderMap ( )
2023-02-20 08:52:39 +00:00
s . logger = log . WithFields ( s . LogFields )
2023-01-10 12:15:51 +00:00
return nil
}
2022-11-02 10:26:30 +00:00
func ( s * Strategy ) Subscribe ( session * bbgo . ExchangeSession ) {
2022-12-04 06:22:11 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : types . Interval1m } )
2022-12-25 16:56:03 +00:00
if s . AutoRange != nil {
interval := s . AutoRange . Interval ( )
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : interval } )
}
2022-11-02 10:26:30 +00:00
}
// InstanceID returns the instance identifier from the current grid configuration parameters
func ( s * Strategy ) InstanceID ( ) string {
2022-12-25 17:40:59 +00:00
id := fmt . Sprintf ( "%s-%s-size-%d" , ID , s . Symbol , s . GridNum )
if s . AutoRange != nil {
id += "-autoRange-" + s . AutoRange . String ( )
} else {
id += "-" + s . UpperPrice . String ( ) + "-" + s . LowerPrice . String ( )
}
return id
2022-11-02 10:26:30 +00:00
}
2022-12-07 04:24:52 +00:00
func ( s * Strategy ) checkSpread ( ) error {
gridNum := fixedpoint . NewFromInt ( s . GridNum )
spread := s . ProfitSpread
if spread . IsZero ( ) {
spread = s . UpperPrice . Sub ( s . LowerPrice ) . Div ( gridNum )
}
feeRate := s . FeeRate
if feeRate . IsZero ( ) {
feeRate = fixedpoint . NewFromFloat ( 0.075 * 0.01 )
}
// the min fee rate from 2 maker/taker orders (with 0.1 rate for profit)
gridFeeRate := feeRate . Mul ( fixedpoint . NewFromFloat ( 2.01 ) )
if spread . Div ( s . LowerPrice ) . Compare ( gridFeeRate ) < 0 {
return fmt . Errorf ( "profitSpread %f %s is too small for lower price, less than the grid fee rate: %s" , spread . Float64 ( ) , spread . Div ( s . LowerPrice ) . Percentage ( ) , gridFeeRate . Percentage ( ) )
}
if spread . Div ( s . UpperPrice ) . Compare ( gridFeeRate ) < 0 {
return fmt . Errorf ( "profitSpread %f %s is too small for upper price, less than the grid fee rate: %s" , spread . Float64 ( ) , spread . Div ( s . UpperPrice ) . Percentage ( ) , gridFeeRate . Percentage ( ) )
}
return nil
}
2022-12-04 03:47:01 +00:00
func ( s * Strategy ) handleOrderCanceled ( o types . Order ) {
s . logger . Infof ( "GRID ORDER CANCELED: %s" , o . String ( ) )
ctx := context . Background ( )
if s . CloseWhenCancelOrder {
s . logger . Infof ( "one of the grid orders is canceled, now closing grid..." )
2023-01-12 06:33:09 +00:00
if err := s . CloseGrid ( ctx ) ; err != nil {
2022-12-04 03:47:01 +00:00
s . logger . WithError ( err ) . Errorf ( "graceful order cancel error" )
}
}
}
2022-12-04 07:01:52 +00:00
func ( s * Strategy ) calculateProfit ( o types . Order , buyPrice , buyQuantity fixedpoint . Value ) * GridProfit {
if s . EarnBase {
// sell quantity - buy quantity
profitQuantity := o . Quantity . Sub ( buyQuantity )
profit := & GridProfit {
Currency : s . Market . BaseCurrency ,
Profit : profitQuantity ,
Time : o . UpdateTime . Time ( ) ,
2022-12-04 07:24:59 +00:00
Order : o ,
2022-12-04 07:01:52 +00:00
}
return profit
}
// earn quote
// (sell_price - buy_price) * quantity
profitQuantity := o . Price . Sub ( buyPrice ) . Mul ( o . Quantity )
profit := & GridProfit {
Currency : s . Market . QuoteCurrency ,
Profit : profitQuantity ,
Time : o . UpdateTime . Time ( ) ,
2022-12-04 07:24:59 +00:00
Order : o ,
2022-12-04 07:01:52 +00:00
}
return profit
}
2022-12-06 07:40:57 +00:00
func ( s * Strategy ) verifyOrderTrades ( o types . Order , trades [ ] types . Trade ) bool {
tq := aggregateTradesQuantity ( trades )
2022-12-05 11:23:39 +00:00
2023-04-28 07:58:12 +00:00
// on MAX: if order.status == filled, it does not mean order.executedQuantity == order.quantity
// order.executedQuantity can be less than order.quantity
// so here we use executed quantity to check if the total trade quantity matches to order.executedQuantity
executedQuantity := o . ExecutedQuantity
if executedQuantity . IsZero ( ) {
// fall back to the original quantity if the executed quantity is zero
executedQuantity = o . Quantity
}
// early return here if it matches
c := tq . Compare ( executedQuantity )
if c == 0 {
return true
}
if c < 0 {
2023-03-03 05:51:50 +00:00
s . logger . Warnf ( "order trades missing. expected: %s got: %s" ,
2023-04-28 08:07:03 +00:00
executedQuantity . String ( ) ,
2023-03-03 05:51:50 +00:00
tq . String ( ) )
2022-12-05 11:23:39 +00:00
return false
2023-04-28 07:58:12 +00:00
} else if c > 0 {
2023-04-28 08:07:03 +00:00
s . logger . Errorf ( "aggregated trade quantity %s > order executed quantity %s, something is wrong, please check" , tq . String ( ) , executedQuantity . String ( ) )
2023-04-28 07:58:12 +00:00
return true
2022-12-05 11:23:39 +00:00
}
2023-04-28 07:58:12 +00:00
// shouldn't reach here
2022-12-05 11:23:39 +00:00
return true
}
2023-09-05 10:28:10 +00:00
// aggregateOrderQuoteAmountAndBaseFee collects the base fee quantity from the given order
2022-12-06 03:48:32 +00:00
// it falls back to query the trades via the RESTful API when the websocket trades are not all received.
2023-09-05 10:28:10 +00:00
func ( s * Strategy ) aggregateOrderQuoteAmountAndFee ( o types . Order ) ( fixedpoint . Value , fixedpoint . Value , string ) {
2022-12-06 03:56:30 +00:00
// try to get the received trades (websocket trades)
2022-12-06 03:48:32 +00:00
orderTrades := s . historicalTrades . GetOrderTrades ( o )
if len ( orderTrades ) > 0 {
2023-04-28 08:07:03 +00:00
s . logger . Infof ( "GRID: found filled order trades: %+v" , orderTrades )
2022-12-06 03:48:32 +00:00
}
2023-03-03 05:11:56 +00:00
feeCurrency := s . Market . BaseCurrency
if o . Side == types . SideTypeSell {
feeCurrency = s . Market . QuoteCurrency
}
2022-12-06 03:56:30 +00:00
for maxTries := maxNumberOfOrderTradesQueryTries ; maxTries > 0 ; maxTries -- {
// if one of the trades is missing, we need to query the trades from the RESTful API
if s . verifyOrderTrades ( o , orderTrades ) {
// if trades are verified
2023-09-05 10:28:10 +00:00
quoteAmount := aggregateTradesQuoteQuantity ( orderTrades )
2022-12-06 03:56:30 +00:00
fees := collectTradeFee ( orderTrades )
2023-03-03 05:11:56 +00:00
if fee , ok := fees [ feeCurrency ] ; ok {
2023-09-05 10:28:10 +00:00
return quoteAmount , fee , feeCurrency
2022-12-06 03:56:30 +00:00
}
2023-09-05 10:28:10 +00:00
return quoteAmount , fixedpoint . Zero , feeCurrency
2022-12-06 03:56:30 +00:00
}
// if we don't support orderQueryService, then we should just skip
2022-12-06 03:48:32 +00:00
if s . orderQueryService == nil {
2023-09-05 10:28:10 +00:00
return fixedpoint . Zero , fixedpoint . Zero , feeCurrency
2022-12-06 03:48:32 +00:00
}
2023-04-28 08:07:03 +00:00
s . logger . Warnf ( "GRID: missing #%d order trades or missing trade fee, pulling order trades from API" , o . OrderID )
2022-12-06 03:48:32 +00:00
// if orderQueryService is supported, use it to query the trades of the filled order
apiOrderTrades , err := s . orderQueryService . QueryOrderTrades ( context . Background ( ) , types . OrderQuery {
Symbol : o . Symbol ,
OrderID : strconv . FormatUint ( o . OrderID , 10 ) ,
} )
if err != nil {
2023-04-28 08:07:03 +00:00
s . logger . WithError ( err ) . Errorf ( "query #%d order trades error" , o . OrderID )
2022-12-06 03:48:32 +00:00
} else {
2023-04-28 08:07:03 +00:00
s . logger . Infof ( "GRID: fetched api #%d order trades: %+v" , o . OrderID , apiOrderTrades )
2022-12-06 03:48:32 +00:00
orderTrades = apiOrderTrades
}
}
2023-09-05 10:28:10 +00:00
quoteAmount := aggregateTradesQuoteQuantity ( orderTrades )
2023-04-28 08:07:03 +00:00
// still try to aggregate the trades quantity if we can:
2023-04-28 08:12:57 +00:00
fees := collectTradeFee ( orderTrades )
if fee , ok := fees [ feeCurrency ] ; ok {
2023-09-05 10:28:10 +00:00
return quoteAmount , fee , feeCurrency
2023-04-28 08:07:03 +00:00
}
2023-09-05 10:28:10 +00:00
return quoteAmount , fixedpoint . Zero , feeCurrency
2022-12-06 03:48:32 +00:00
}
2022-12-15 06:57:25 +00:00
func ( s * Strategy ) processFilledOrder ( o types . Order ) {
2023-03-01 12:05:35 +00:00
var profit * GridProfit = nil
2022-12-05 11:37:42 +00:00
// check order fee
newSide := types . SideTypeSell
newPrice := o . Price
2023-04-28 07:58:12 +00:00
executedQuantity := o . ExecutedQuantity
// A safeguard check, fallback to the original quantity
if executedQuantity . IsZero ( ) {
executedQuantity = o . Quantity
}
newQuantity := executedQuantity
2023-03-03 06:18:37 +00:00
2023-04-28 07:58:12 +00:00
if o . ExecutedQuantity . Compare ( o . Quantity ) != 0 {
2023-04-28 08:16:23 +00:00
s . logger . Warnf ( "order #%d is filled, but order executed quantity %s != order quantity %s, something is wrong" , o . OrderID , o . ExecutedQuantity , o . Quantity )
2023-04-28 07:58:12 +00:00
}
2023-03-03 06:18:37 +00:00
/ *
if o . AveragePrice . Sign ( ) > 0 {
executedPrice = o . AveragePrice
}
* /
2023-02-24 10:45:18 +00:00
2023-03-07 05:36:33 +00:00
// collect trades for fee
// fee calculation is used to reduce the order quantity
2023-03-03 05:51:50 +00:00
// because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC
// if we don't reduce the sell quantity, than we might fail to place the sell order
2023-09-05 10:28:10 +00:00
orderExecutedQuoteAmount , fee , feeCurrency := s . aggregateOrderQuoteAmountAndFee ( o )
2023-03-03 05:51:50 +00:00
s . logger . Infof ( "GRID ORDER #%d %s FEE: %s %s" ,
o . OrderID , o . Side ,
2023-03-07 05:36:33 +00:00
fee . String ( ) , feeCurrency )
2022-12-05 11:23:39 +00:00
2022-12-03 06:46:05 +00:00
switch o . Side {
case types . SideTypeSell :
newSide = types . SideTypeBuy
2022-12-03 08:59:47 +00:00
if ! s . ProfitSpread . IsZero ( ) {
newPrice = newPrice . Sub ( s . ProfitSpread )
} else {
if pin , ok := s . grid . NextLowerPin ( newPrice ) ; ok {
newPrice = fixedpoint . Value ( pin )
}
2022-12-03 06:46:05 +00:00
}
2022-12-03 07:18:47 +00:00
// use the profit to buy more inventory in the grid
2022-12-03 08:40:40 +00:00
if s . Compound || s . EarnBase {
2023-03-03 05:51:50 +00:00
// if it's not using the platform fee currency, reduce the quote quantity for the buy order
2023-07-31 10:12:28 +00:00
if feeCurrency == s . Market . QuoteCurrency && fee . Sign ( ) > 0 {
2023-03-07 05:36:33 +00:00
orderExecutedQuoteAmount = orderExecutedQuoteAmount . Sub ( fee )
2023-03-03 05:51:50 +00:00
}
2023-03-07 05:36:33 +00:00
// for quote amount, always round down with price precision to prevent the quote currency fund locking rounding issue
origQuoteAmount := orderExecutedQuoteAmount
orderExecutedQuoteAmount = orderExecutedQuoteAmount . Round ( s . Market . PricePrecision , fixedpoint . Down )
2023-03-07 10:37:45 +00:00
s . logger . Infof ( "round down %s %s order quote quantity %s to %s by quote precision %d" , s . Symbol , newSide , origQuoteAmount . String ( ) , orderExecutedQuoteAmount . String ( ) , s . Market . PricePrecision )
2023-03-07 05:36:33 +00:00
2023-03-07 10:37:45 +00:00
newQuantity = orderExecutedQuoteAmount . Div ( newPrice )
2023-03-07 05:36:33 +00:00
2023-03-07 10:37:45 +00:00
origQuantity := newQuantity
newQuantity = newQuantity . Round ( s . Market . VolumePrecision , fixedpoint . Down )
s . logger . Infof ( "round down %s %s order base quantity %s to %s by base precision %d" , s . Symbol , newSide , origQuantity . String ( ) , newQuantity . String ( ) , s . Market . VolumePrecision )
newQuantity = fixedpoint . Max ( newQuantity , s . Market . MinQuantity )
2023-02-24 09:08:38 +00:00
} else if s . QuantityOrAmount . Quantity . Sign ( ) > 0 {
newQuantity = s . QuantityOrAmount . Quantity
2022-12-03 07:21:03 +00:00
}
2022-12-03 07:18:47 +00:00
2023-02-24 09:08:38 +00:00
// TODO: need to consider sell order fee for the profit calculation
2023-03-01 12:05:35 +00:00
profit = s . calculateProfit ( o , newPrice , newQuantity )
2022-12-15 10:47:45 +00:00
2022-12-03 06:46:05 +00:00
case types . SideTypeBuy :
newSide = types . SideTypeSell
2022-12-03 08:59:47 +00:00
if ! s . ProfitSpread . IsZero ( ) {
newPrice = newPrice . Add ( s . ProfitSpread )
} else {
if pin , ok := s . grid . NextHigherPin ( newPrice ) ; ok {
newPrice = fixedpoint . Value ( pin )
}
2022-12-03 06:46:05 +00:00
}
2022-12-03 08:40:40 +00:00
2023-07-31 10:12:28 +00:00
if feeCurrency == s . Market . BaseCurrency && fee . Sign ( ) > 0 {
2023-03-07 05:36:33 +00:00
newQuantity = newQuantity . Sub ( fee )
}
// if EarnBase is enabled, we should sell less to get the same quote amount back
2022-12-03 08:40:40 +00:00
if s . EarnBase {
2023-03-07 05:36:33 +00:00
newQuantity = fixedpoint . Max ( orderExecutedQuoteAmount . Div ( newPrice ) . Sub ( fee ) , s . Market . MinQuantity )
2022-12-03 08:40:40 +00:00
}
2023-03-07 05:36:33 +00:00
// always round down the base quantity for placing sell order to avoid the base currency fund locking issue
origQuantity := newQuantity
newQuantity = newQuantity . Round ( s . Market . VolumePrecision , fixedpoint . Down )
s . logger . Infof ( "round down sell order quantity %s to %s by base quantity precision %d" , origQuantity . String ( ) , newQuantity . String ( ) , s . Market . VolumePrecision )
2022-12-03 06:46:05 +00:00
}
2022-11-02 10:26:30 +00:00
2022-12-03 06:46:05 +00:00
orderForm := types . SubmitOrder {
2023-03-10 07:47:42 +00:00
Symbol : s . Symbol ,
Market : s . Market ,
Type : types . OrderTypeLimit ,
Price : newPrice ,
Side : newSide ,
TimeInForce : types . TimeInForceGTC ,
Quantity : newQuantity ,
Tag : orderTag ,
GroupID : s . OrderGroupID ,
2023-03-13 13:27:13 +00:00
ClientOrderID : s . newClientOrderID ( ) ,
2022-12-03 06:46:05 +00:00
}
2022-12-06 07:45:28 +00:00
s . logger . Infof ( "SUBMIT GRID REVERSE ORDER: %s" , orderForm . String ( ) )
2022-12-03 07:17:31 +00:00
2023-03-03 10:42:08 +00:00
writeCtx := s . getWriteContext ( )
createdOrders , err := s . orderExecutor . SubmitOrders ( writeCtx , orderForm )
2023-03-01 12:05:35 +00:00
if err != nil {
s . logger . WithError ( err ) . Errorf ( "GRID REVERSE ORDER SUBMISSION ERROR: order: %s" , orderForm . String ( ) )
return
2022-12-03 06:46:05 +00:00
}
2023-02-23 14:39:47 +00:00
2023-03-01 12:05:35 +00:00
s . logger . Infof ( "GRID REVERSE ORDER IS CREATED: %+v" , createdOrders )
// we calculate profit only when the order is placed successfully
if profit != nil {
s . GridProfitStats . AddProfit ( profit )
2023-03-10 07:33:19 +00:00
s . logger . Infof ( "GENERATED GRID PROFIT: %+v; TOTAL GRID PROFIT BECOMES: %f" , profit , s . GridProfitStats . TotalQuoteProfit . Float64 ( ) )
2023-03-01 12:05:35 +00:00
s . EmitGridProfit ( s . GridProfitStats , profit )
}
2022-11-02 10:26:30 +00:00
}
2022-12-15 06:57:25 +00:00
// handleOrderFilled is called when an order status is FILLED
func ( s * Strategy ) handleOrderFilled ( o types . Order ) {
if s . grid == nil {
2022-12-15 11:20:15 +00:00
s . logger . Warn ( "grid is not opened yet, skip order update event" )
2022-12-15 06:57:25 +00:00
return
}
2023-03-10 07:33:19 +00:00
if s . filledOrderIDMap . Exists ( o . OrderID ) {
2023-03-10 09:00:09 +00:00
s . logger . Warnf ( "duplicated id (%d) of filled order detected" , o . OrderID )
2023-03-10 07:33:19 +00:00
return
}
s . filledOrderIDMap . Add ( o )
2022-12-15 06:57:25 +00:00
s . logger . Infof ( "GRID ORDER FILLED: %s" , o . String ( ) )
2023-03-08 13:36:19 +00:00
s . updateFilledOrderMetrics ( o )
2022-12-15 06:57:25 +00:00
s . processFilledOrder ( o )
}
2023-08-31 05:48:56 +00:00
func ( s * Strategy ) checkRequiredInvestmentByQuantity (
baseBalance , quoteBalance , quantity , lastPrice fixedpoint . Value , pins [ ] Pin ,
) ( requiredBase , requiredQuote fixedpoint . Value , err error ) {
2022-11-15 07:29:30 +00:00
// check more investment budget details
2022-11-16 04:09:55 +00:00
requiredBase = fixedpoint . Zero
requiredQuote = fixedpoint . Zero
// when we need to place a buy-to-sell conversion order, we need to mark the price
2022-12-05 17:51:50 +00:00
si := - 1
2022-11-16 04:09:55 +00:00
for i := len ( pins ) - 1 ; i >= 0 ; i -- {
2022-11-15 07:29:30 +00:00
pin := pins [ i ]
2022-11-14 09:37:32 +00:00
price := fixedpoint . Value ( pin )
2022-11-15 07:29:30 +00:00
// TODO: add fee if we don't have the platform token. BNB, OKB or MAX...
2022-11-14 09:37:32 +00:00
if price . Compare ( lastPrice ) >= 0 {
2022-12-05 07:19:24 +00:00
si = i
2022-11-14 09:37:32 +00:00
// for orders that sell
// if we still have the base balance
2022-11-16 04:09:55 +00:00
if requiredBase . Add ( quantity ) . Compare ( baseBalance ) <= 0 {
2022-11-14 09:37:32 +00:00
requiredBase = requiredBase . Add ( quantity )
2022-11-15 07:29:30 +00:00
} else if i > 0 { // we do not want to sell at i == 0
// convert sell to buy quote and add to requiredQuote
2022-12-05 03:21:07 +00:00
nextLowerPin := pins [ i - 1 ]
2022-11-14 09:37:32 +00:00
nextLowerPrice := fixedpoint . Value ( nextLowerPin )
requiredQuote = requiredQuote . Add ( quantity . Mul ( nextLowerPrice ) )
}
} else {
2022-11-15 07:29:30 +00:00
// for orders that buy
2022-12-05 07:19:24 +00:00
if i + 1 == si {
2022-11-16 04:09:55 +00:00
continue
}
2022-11-14 09:37:32 +00:00
requiredQuote = requiredQuote . Add ( quantity . Mul ( price ) )
}
}
2022-11-15 07:29:30 +00:00
if requiredBase . Compare ( baseBalance ) > 0 && requiredQuote . Compare ( quoteBalance ) > 0 {
2022-11-16 05:29:55 +00:00
return requiredBase , requiredQuote , fmt . Errorf ( "both base balance (%f %s) or quote balance (%f %s) is not enough, required = base %f + quote %f" ,
2022-11-15 07:29:30 +00:00
baseBalance . Float64 ( ) , s . Market . BaseCurrency ,
2022-11-16 05:29:55 +00:00
quoteBalance . Float64 ( ) , s . Market . QuoteCurrency ,
requiredBase . Float64 ( ) ,
requiredQuote . Float64 ( ) )
}
2022-11-16 07:11:42 +00:00
if requiredBase . Compare ( baseBalance ) > 0 {
return requiredBase , requiredQuote , fmt . Errorf ( "base balance (%f %s), required = base %f" ,
baseBalance . Float64 ( ) , s . Market . BaseCurrency ,
requiredBase . Float64 ( ) ,
)
}
if requiredQuote . Compare ( quoteBalance ) > 0 {
return requiredBase , requiredQuote , fmt . Errorf ( "quote balance (%f %s) is not enough, required = quote %f" ,
quoteBalance . Float64 ( ) , s . Market . QuoteCurrency ,
requiredQuote . Float64 ( ) ,
)
}
return requiredBase , requiredQuote , nil
}
2023-08-31 05:48:56 +00:00
func ( s * Strategy ) checkRequiredInvestmentByAmount (
baseBalance , quoteBalance , amount , lastPrice fixedpoint . Value , pins [ ] Pin ,
) ( requiredBase , requiredQuote fixedpoint . Value , err error ) {
2022-11-16 07:11:42 +00:00
// check more investment budget details
requiredBase = fixedpoint . Zero
requiredQuote = fixedpoint . Zero
// when we need to place a buy-to-sell conversion order, we need to mark the price
2022-12-05 17:51:50 +00:00
si := - 1
2022-11-16 07:11:42 +00:00
for i := len ( pins ) - 1 ; i >= 0 ; i -- {
pin := pins [ i ]
price := fixedpoint . Value ( pin )
// TODO: add fee if we don't have the platform token. BNB, OKB or MAX...
if price . Compare ( lastPrice ) >= 0 {
2022-12-05 07:19:24 +00:00
si = i
2022-11-16 07:11:42 +00:00
// for orders that sell
// if we still have the base balance
quantity := amount . Div ( lastPrice )
if requiredBase . Add ( quantity ) . Compare ( baseBalance ) <= 0 {
requiredBase = requiredBase . Add ( quantity )
} else if i > 0 { // we do not want to sell at i == 0
// convert sell to buy quote and add to requiredQuote
2022-12-05 03:21:07 +00:00
nextLowerPin := pins [ i - 1 ]
2022-11-16 07:11:42 +00:00
nextLowerPrice := fixedpoint . Value ( nextLowerPin )
requiredQuote = requiredQuote . Add ( quantity . Mul ( nextLowerPrice ) )
}
} else {
// for orders that buy
2022-12-05 17:19:24 +00:00
if s . ProfitSpread . IsZero ( ) && i + 1 == si {
2022-11-16 07:11:42 +00:00
continue
}
2022-12-05 17:19:24 +00:00
2022-11-16 07:11:42 +00:00
requiredQuote = requiredQuote . Add ( amount )
}
}
if requiredBase . Compare ( baseBalance ) > 0 && requiredQuote . Compare ( quoteBalance ) > 0 {
return requiredBase , requiredQuote , fmt . Errorf ( "both base balance (%f %s) or quote balance (%f %s) is not enough, required = base %f + quote %f" ,
baseBalance . Float64 ( ) , s . Market . BaseCurrency ,
quoteBalance . Float64 ( ) , s . Market . QuoteCurrency ,
requiredBase . Float64 ( ) ,
requiredQuote . Float64 ( ) )
}
2022-11-16 05:29:55 +00:00
if requiredBase . Compare ( baseBalance ) > 0 {
return requiredBase , requiredQuote , fmt . Errorf ( "base balance (%f %s), required = base %f" ,
baseBalance . Float64 ( ) , s . Market . BaseCurrency ,
requiredBase . Float64 ( ) ,
)
}
if requiredQuote . Compare ( quoteBalance ) > 0 {
return requiredBase , requiredQuote , fmt . Errorf ( "quote balance (%f %s) is not enough, required = quote %f" ,
quoteBalance . Float64 ( ) , s . Market . QuoteCurrency ,
requiredQuote . Float64 ( ) ,
)
2022-11-14 09:37:32 +00:00
}
2022-11-16 04:09:55 +00:00
return requiredBase , requiredQuote , nil
2022-11-14 08:28:07 +00:00
}
2023-08-31 05:48:56 +00:00
func ( s * Strategy ) calculateQuoteInvestmentQuantity (
quoteInvestment , lastPrice fixedpoint . Value , pins [ ] Pin ,
) ( fixedpoint . Value , error ) {
2022-11-27 11:11:45 +00:00
// quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + ....
// =>
// quoteInvestment = (p1 + p2 + p3) * q
// q = quoteInvestment / (p1 + p2 + p3)
totalQuotePrice := fixedpoint . Zero
2023-02-16 06:56:28 +00:00
si := len ( pins )
2023-02-16 13:55:53 +00:00
cntOrder := 0
2022-11-27 11:11:45 +00:00
for i := len ( pins ) - 1 ; i >= 0 ; i -- {
pin := pins [ i ]
price := fixedpoint . Value ( pin )
if price . Compare ( lastPrice ) >= 0 {
2022-12-05 07:19:24 +00:00
si = i
2023-02-16 06:56:28 +00:00
// do not place sell order on the bottom price
if i == 0 {
continue
}
2022-11-27 11:11:45 +00:00
// for orders that sell
// if we still have the base balance
// quantity := amount.Div(lastPrice)
2022-12-05 17:51:50 +00:00
if s . ProfitSpread . Sign ( ) > 0 {
totalQuotePrice = totalQuotePrice . Add ( price )
2023-02-16 06:56:28 +00:00
} else { // we do not want to sell at i == 0
2022-11-27 11:11:45 +00:00
// convert sell to buy quote and add to requiredQuote
2022-12-05 03:21:07 +00:00
nextLowerPin := pins [ i - 1 ]
2022-11-27 11:11:45 +00:00
nextLowerPrice := fixedpoint . Value ( nextLowerPin )
totalQuotePrice = totalQuotePrice . Add ( nextLowerPrice )
}
2023-02-16 13:55:53 +00:00
cntOrder ++
2022-11-27 11:11:45 +00:00
} else {
// for orders that buy
2022-12-05 17:19:24 +00:00
if s . ProfitSpread . IsZero ( ) && i + 1 == si {
2022-11-27 11:11:45 +00:00
continue
}
2023-02-13 06:05:52 +00:00
// should never place a buy order at the upper price
if i == len ( pins ) - 1 {
continue
}
2022-11-27 11:11:45 +00:00
totalQuotePrice = totalQuotePrice . Add ( price )
2023-02-16 13:55:53 +00:00
cntOrder ++
2022-11-27 11:11:45 +00:00
}
}
2023-02-16 13:55:53 +00:00
orderDusts := fixedpoint . NewFromFloat ( math . Pow10 ( - s . Market . PricePrecision ) * float64 ( cntOrder ) )
adjustedQuoteInvestment := quoteInvestment . Sub ( orderDusts )
q := adjustedQuoteInvestment . Div ( totalQuotePrice )
s . logger . Infof ( "calculateQuoteInvestmentQuantity: adjustedQuoteInvestment=%f sumOfPrice=%f quantity=%f" , adjustedQuoteInvestment . Float64 ( ) , totalQuotePrice . Float64 ( ) , q . Float64 ( ) )
2023-02-16 06:56:28 +00:00
return q , nil
2022-11-27 11:11:45 +00:00
}
2023-08-31 05:48:56 +00:00
func ( s * Strategy ) calculateBaseQuoteInvestmentQuantity (
quoteInvestment , baseInvestment , lastPrice fixedpoint . Value , pins [ ] Pin ,
) ( fixedpoint . Value , error ) {
2023-02-15 07:54:49 +00:00
s . logger . Infof ( "calculating quantity by base/quote investment: %f / %f" , baseInvestment . Float64 ( ) , quoteInvestment . Float64 ( ) )
2022-11-30 04:46:39 +00:00
// q_p1 = q_p2 = q_p3 = q_p4
// baseInvestment = q_p1 + q_p2 + q_p3 + q_p4 + ....
// baseInvestment = numberOfSellOrders * q
// maxBaseQuantity = baseInvestment / numberOfSellOrders
// if maxBaseQuantity < minQuantity or maxBaseQuantity * priceLowest < minNotional
// then reduce the numberOfSellOrders
2023-05-22 10:07:40 +00:00
numberOfSellOrders := s . BaseGridNum
2023-05-26 06:51:06 +00:00
// if it's not configured, calculate the number of sell orders
2023-05-22 10:07:40 +00:00
if numberOfSellOrders == 0 {
for i := len ( pins ) - 1 ; i >= 0 ; i -- {
pin := pins [ i ]
price := fixedpoint . Value ( pin )
sellPrice := price
if s . ProfitSpread . Sign ( ) > 0 {
sellPrice = sellPrice . Add ( s . ProfitSpread )
}
2022-12-05 17:19:24 +00:00
2023-05-22 10:07:40 +00:00
if sellPrice . Compare ( lastPrice ) < 0 {
break
}
2022-12-05 17:21:41 +00:00
2023-05-22 10:07:40 +00:00
numberOfSellOrders ++
}
2022-11-30 04:46:39 +00:00
2023-05-22 10:07:40 +00:00
// avoid placing a sell order above the last price
if numberOfSellOrders > 0 {
numberOfSellOrders --
}
2023-05-22 09:20:16 +00:00
}
2023-05-19 08:37:44 +00:00
2022-11-30 04:52:04 +00:00
// if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders
// so that the quantity can be increased.
2023-05-19 07:04:17 +00:00
baseQuantity := s . Market . TruncateQuantity (
baseInvestment . Div (
fixedpoint . NewFromInt (
int64 ( numberOfSellOrders ) ) ) )
2023-05-18 10:23:58 +00:00
2023-05-19 07:04:17 +00:00
minBaseQuantity := fixedpoint . Max (
2023-05-19 08:37:44 +00:00
s . Market . MinNotional . Div ( s . UpperPrice ) ,
2023-05-19 07:04:17 +00:00
s . Market . MinQuantity )
2023-05-18 10:23:58 +00:00
2023-05-19 07:04:17 +00:00
if baseQuantity . Compare ( minBaseQuantity ) <= 0 {
2023-05-19 08:46:17 +00:00
baseQuantity = s . Market . RoundUpQuantityByPrecision ( minBaseQuantity )
2023-05-22 09:25:00 +00:00
numberOfSellOrders = int ( math . Floor ( baseInvestment . Div ( baseQuantity ) . Float64 ( ) ) )
2022-12-03 03:25:18 +00:00
}
2022-11-30 04:46:39 +00:00
2023-05-19 07:04:17 +00:00
s . logger . Infof ( "grid base investment sell orders: %d" , numberOfSellOrders )
s . logger . Infof ( "grid base investment quantity: %f (base investment) / %d (number of sell orders) = %f (base quantity per order)" , baseInvestment . Float64 ( ) , numberOfSellOrders , baseQuantity . Float64 ( ) )
2022-12-05 17:57:33 +00:00
// calculate quantity with quote investment
2022-11-30 04:46:39 +00:00
totalQuotePrice := fixedpoint . Zero
// quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + ....
// =>
// quoteInvestment = (p1 + p2 + p3) * q
// maxBuyQuantity = quoteInvestment / (p1 + p2 + p3)
2022-12-05 17:51:50 +00:00
si := - 1
2023-05-19 07:04:17 +00:00
for i := len ( pins ) - 1 - numberOfSellOrders ; i >= 0 ; i -- {
2022-11-30 04:46:39 +00:00
pin := pins [ i ]
price := fixedpoint . Value ( pin )
2022-12-05 17:51:50 +00:00
// buy price greater than the last price will trigger taker order.
2022-11-30 04:46:39 +00:00
if price . Compare ( lastPrice ) >= 0 {
2022-12-05 17:21:41 +00:00
si = i
2022-12-05 17:57:33 +00:00
// when profit spread is set, we count all the grid prices as buy prices
if s . ProfitSpread . Sign ( ) > 0 {
totalQuotePrice = totalQuotePrice . Add ( price )
} else if i > 0 {
// when profit spread is not set
// we do not want to place sell order at i == 0
// here we submit an order to convert a buy order into a sell order
2022-12-05 03:21:07 +00:00
nextLowerPin := pins [ i - 1 ]
2022-11-30 04:46:39 +00:00
nextLowerPrice := fixedpoint . Value ( nextLowerPin )
// requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice))
totalQuotePrice = totalQuotePrice . Add ( nextLowerPrice )
}
} else {
// for orders that buy
2022-12-05 17:21:41 +00:00
if s . ProfitSpread . IsZero ( ) && i + 1 == si {
2022-11-30 04:46:39 +00:00
continue
}
2023-02-13 06:05:52 +00:00
// should never place a buy order at the upper price
if i == len ( pins ) - 1 {
continue
}
2022-11-30 04:46:39 +00:00
totalQuotePrice = totalQuotePrice . Add ( price )
}
}
2023-05-26 06:49:56 +00:00
if totalQuotePrice . Sign ( ) > 0 && quoteInvestment . Sign ( ) > 0 {
quoteSideQuantity := quoteInvestment . Div ( totalQuotePrice )
if numberOfSellOrders > 0 {
return fixedpoint . Min ( quoteSideQuantity , baseQuantity ) , nil
}
return quoteSideQuantity , nil
2022-12-03 03:25:18 +00:00
}
2023-05-26 06:49:56 +00:00
return baseQuantity , nil
2022-11-30 04:46:39 +00:00
}
2022-12-04 06:22:11 +00:00
func ( s * Strategy ) newTriggerPriceHandler ( ctx context . Context , session * bbgo . ExchangeSession ) types . KLineCallback {
return types . KLineWith ( s . Symbol , types . Interval1m , func ( k types . KLine ) {
if s . TriggerPrice . Compare ( k . High ) > 0 || s . TriggerPrice . Compare ( k . Low ) < 0 {
return
}
2022-12-04 10:21:43 +00:00
if s . grid != nil {
return
}
2022-12-04 10:17:05 +00:00
s . logger . Infof ( "the last price %f hits triggerPrice %f, opening grid" , k . Close . Float64 ( ) , s . TriggerPrice . Float64 ( ) )
2022-12-04 06:22:11 +00:00
if err := s . openGrid ( ctx , session ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "failed to setup grid orders" )
2022-12-04 10:01:58 +00:00
return
}
} )
}
2022-12-15 10:47:45 +00:00
func ( s * Strategy ) newOrderUpdateHandler ( ctx context . Context , session * bbgo . ExchangeSession ) func ( o types . Order ) {
return func ( o types . Order ) {
2023-03-03 06:10:34 +00:00
if s . OrderFillDelay > 0 {
time . Sleep ( s . OrderFillDelay . Duration ( ) )
}
2022-12-15 10:47:45 +00:00
s . handleOrderFilled ( o )
2023-03-01 13:05:28 +00:00
// sync the profits to redis
2022-12-15 10:47:45 +00:00
bbgo . Sync ( ctx , s )
2023-02-23 16:44:50 +00:00
2023-03-08 13:36:19 +00:00
s . updateGridNumOfOrdersMetricsWithLock ( )
2022-12-15 10:47:45 +00:00
}
}
2022-12-04 10:01:58 +00:00
func ( s * Strategy ) newStopLossPriceHandler ( ctx context . Context , session * bbgo . ExchangeSession ) types . KLineCallback {
return types . KLineWith ( s . Symbol , types . Interval1m , func ( k types . KLine ) {
if s . StopLossPrice . Compare ( k . Low ) < 0 {
return
}
2022-12-04 13:09:39 +00:00
s . logger . Infof ( "last low price %f hits stopLossPrice %f, closing grid" , k . Low . Float64 ( ) , s . StopLossPrice . Float64 ( ) )
2023-01-12 06:33:09 +00:00
if err := s . CloseGrid ( ctx ) ; err != nil {
2022-12-04 10:01:58 +00:00
s . logger . WithError ( err ) . Errorf ( "can not close grid" )
return
}
2022-12-04 13:09:39 +00:00
base := s . Position . GetBase ( )
if base . Sign ( ) < 0 {
2022-12-04 13:06:52 +00:00
return
}
2022-12-04 13:09:39 +00:00
s . logger . Infof ( "position base %f > 0, closing position..." , base . Float64 ( ) )
2022-12-04 10:01:58 +00:00
if err := s . orderExecutor . ClosePosition ( ctx , fixedpoint . One , "grid2:stopLoss" ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "can not close position" )
return
2022-12-04 06:22:11 +00:00
}
} )
}
2022-12-05 11:46:08 +00:00
func ( s * Strategy ) newTakeProfitHandler ( ctx context . Context , session * bbgo . ExchangeSession ) types . KLineCallback {
return types . KLineWith ( s . Symbol , types . Interval1m , func ( k types . KLine ) {
2022-12-05 15:42:03 +00:00
if s . TakeProfitPrice . Compare ( k . High ) > 0 {
2022-12-05 11:46:08 +00:00
return
}
s . logger . Infof ( "last high price %f hits takeProfitPrice %f, closing grid" , k . High . Float64 ( ) , s . TakeProfitPrice . Float64 ( ) )
2023-01-12 06:33:09 +00:00
if err := s . CloseGrid ( ctx ) ; err != nil {
2022-12-05 11:46:08 +00:00
s . logger . WithError ( err ) . Errorf ( "can not close grid" )
return
}
base := s . Position . GetBase ( )
if base . Sign ( ) < 0 {
return
}
s . logger . Infof ( "position base %f > 0, closing position..." , base . Float64 ( ) )
if err := s . orderExecutor . ClosePosition ( ctx , fixedpoint . One , "grid2:takeProfit" ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "can not close position" )
return
}
} )
}
2023-01-12 06:33:09 +00:00
func ( s * Strategy ) OpenGrid ( ctx context . Context ) error {
return s . openGrid ( ctx , s . session )
}
2023-03-03 10:18:39 +00:00
// TODO: make sure the context here is the trading context or the shutdown context?
2023-03-02 09:33:58 +00:00
func ( s * Strategy ) cancelAll ( ctx context . Context ) error {
2023-03-01 08:16:26 +00:00
var werr error
2023-03-02 09:33:58 +00:00
session := s . session
if session == nil {
session = s . ExchangeSession
}
2023-03-01 08:16:26 +00:00
2023-03-02 09:33:58 +00:00
service , support := session . Exchange . ( advancedOrderCancelApi )
2023-03-01 08:35:09 +00:00
if s . UseCancelAllOrdersApiWhenClose && ! support {
2023-03-02 10:05:48 +00:00
s . logger . Warnf ( "advancedOrderCancelApi interface is not implemented, fallback to default graceful cancel, exchange %T" , session )
2023-03-01 08:35:09 +00:00
s . UseCancelAllOrdersApiWhenClose = false
}
2023-03-01 08:16:26 +00:00
if s . UseCancelAllOrdersApiWhenClose {
s . logger . Infof ( "useCancelAllOrdersApiWhenClose is set, using advanced order cancel api for canceling..." )
2023-03-03 10:18:39 +00:00
for {
s . logger . Infof ( "checking %s open orders..." , s . Symbol )
2023-03-01 08:16:26 +00:00
2023-06-29 02:56:07 +00:00
openOrders , err := retry . QueryOpenOrdersUntilSuccessful ( ctx , session . Exchange , s . Symbol )
2023-03-10 05:08:29 +00:00
if err != nil {
2023-03-03 10:55:15 +00:00
s . logger . WithError ( err ) . Errorf ( "CancelOrdersByGroupID api call error" )
werr = multierr . Append ( werr , err )
2023-03-01 08:16:26 +00:00
}
2023-03-03 10:18:39 +00:00
if len ( openOrders ) == 0 {
break
2023-03-01 08:35:09 +00:00
}
2023-03-03 10:18:39 +00:00
s . logger . Infof ( "found %d open orders left, using cancel all orders api" , len ( openOrders ) )
2023-03-01 08:35:09 +00:00
2023-03-06 02:37:34 +00:00
s . logger . Infof ( "using cancal all orders api for canceling grid orders..." )
2023-06-29 02:56:07 +00:00
if err := retry . CancelAllOrdersUntilSuccessful ( ctx , service ) ; err != nil {
2023-03-06 02:37:34 +00:00
s . logger . WithError ( err ) . Errorf ( "CancelAllOrders api call error" )
werr = multierr . Append ( werr , err )
2023-03-01 08:16:26 +00:00
}
2023-03-03 10:18:39 +00:00
time . Sleep ( 1 * time . Second )
2023-03-01 08:16:26 +00:00
}
} else {
if err := s . orderExecutor . GracefulCancel ( ctx ) ; err != nil {
werr = multierr . Append ( werr , err )
}
2022-12-04 04:58:01 +00:00
}
2023-03-02 09:33:58 +00:00
return werr
}
// CloseGrid closes the grid orders
func ( s * Strategy ) CloseGrid ( ctx context . Context ) error {
s . logger . Infof ( "closing %s grid" , s . Symbol )
defer s . EmitGridClosed ( )
bbgo . Sync ( ctx , s )
// now we can cancel the open orders
s . logger . Infof ( "canceling grid orders..." )
err := s . cancelAll ( ctx )
2022-12-04 06:23:00 +00:00
// free the grid object
2023-02-16 10:11:38 +00:00
s . setGrid ( nil )
2023-03-08 13:36:19 +00:00
s . updateGridNumOfOrdersMetricsWithLock ( )
2023-03-02 09:33:58 +00:00
return err
2022-12-04 04:58:01 +00:00
}
2022-12-07 06:19:49 +00:00
func ( s * Strategy ) newGrid ( ) * Grid {
grid := NewGrid ( s . LowerPrice , s . UpperPrice , fixedpoint . NewFromInt ( s . GridNum ) , s . Market . TickSize )
grid . CalculateArithmeticPins ( )
return grid
}
2022-12-04 05:04:14 +00:00
// openGrid
2022-11-24 07:55:02 +00:00
// 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate.
// 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment.
2022-12-04 05:04:14 +00:00
func ( s * Strategy ) openGrid ( ctx context . Context , session * bbgo . ExchangeSession ) error {
2022-12-04 10:27:21 +00:00
// grid object guard
2023-02-06 17:38:25 +00:00
s . mu . Lock ( )
2023-03-09 03:26:02 +00:00
defer s . mu . Unlock ( )
2023-02-06 17:38:25 +00:00
2022-12-04 06:22:11 +00:00
if s . grid != nil {
return nil
}
2023-03-09 03:26:02 +00:00
grid := s . newGrid ( )
s . grid = grid
2022-12-05 11:42:36 +00:00
s . logger . Info ( "OPENING GRID: " , s . grid . String ( ) )
2022-12-04 06:22:11 +00:00
2022-11-10 12:58:46 +00:00
lastPrice , err := s . getLastTradePrice ( ctx , session )
if err != nil {
2023-04-14 07:15:28 +00:00
err2 := errors . Wrap ( err , "unable to get the last trade price" )
s . EmitGridError ( err2 )
return err2
2022-11-10 12:58:46 +00:00
}
2022-11-10 15:34:55 +00:00
// check if base and quote are enough
2023-02-14 08:44:59 +00:00
var totalBase = fixedpoint . Zero
var totalQuote = fixedpoint . Zero
2022-11-10 15:34:55 +00:00
baseBalance , ok := session . Account . Balance ( s . Market . BaseCurrency )
2023-02-14 08:44:59 +00:00
if ok {
totalBase = baseBalance . Available
2022-11-10 15:34:55 +00:00
}
quoteBalance , ok := session . Account . Balance ( s . Market . QuoteCurrency )
2023-02-14 08:44:59 +00:00
if ok {
totalQuote = quoteBalance . Available
2022-11-10 15:34:55 +00:00
}
2022-11-14 08:28:07 +00:00
// shift 1 grid because we will start from the buy order
// if the buy order is filled, then we will submit another sell order at the higher grid.
2022-11-17 09:40:59 +00:00
if s . QuantityOrAmount . IsSet ( ) {
if quantity := s . QuantityOrAmount . Quantity ; ! quantity . IsZero ( ) {
if _ , _ , err2 := s . checkRequiredInvestmentByQuantity ( totalBase , totalQuote , lastPrice , s . QuantityOrAmount . Quantity , s . grid . Pins ) ; err != nil {
2023-04-14 07:15:28 +00:00
s . EmitGridError ( err2 )
2022-11-17 09:40:59 +00:00
return err2
}
}
if amount := s . QuantityOrAmount . Amount ; ! amount . IsZero ( ) {
if _ , _ , err2 := s . checkRequiredInvestmentByAmount ( totalBase , totalQuote , lastPrice , amount , s . grid . Pins ) ; err != nil {
2023-04-14 07:15:28 +00:00
s . EmitGridError ( err2 )
2022-11-17 09:40:59 +00:00
return err2
}
2022-11-10 15:34:55 +00:00
}
2022-11-26 16:19:59 +00:00
} else {
2022-12-01 08:30:44 +00:00
// calculate the quantity from the investment configuration
2023-05-26 06:49:56 +00:00
if ! s . BaseInvestment . IsZero ( ) {
2023-02-15 07:54:49 +00:00
quantity , err2 := s . calculateBaseQuoteInvestmentQuantity ( s . QuoteInvestment , s . BaseInvestment , lastPrice , s . grid . Pins )
2022-11-30 04:46:39 +00:00
if err2 != nil {
2023-04-14 07:15:28 +00:00
s . EmitGridError ( err2 )
2022-11-30 04:46:39 +00:00
return err2
}
2023-04-14 07:15:28 +00:00
2022-11-30 04:46:39 +00:00
s . QuantityOrAmount . Quantity = quantity
} else if ! s . QuoteInvestment . IsZero ( ) {
2022-11-27 11:11:45 +00:00
quantity , err2 := s . calculateQuoteInvestmentQuantity ( s . QuoteInvestment , lastPrice , s . grid . Pins )
if err2 != nil {
2023-04-14 07:15:28 +00:00
s . EmitGridError ( err2 )
2022-11-27 11:11:45 +00:00
return err2
}
2023-04-14 07:15:28 +00:00
2022-11-30 04:46:39 +00:00
s . QuantityOrAmount . Quantity = quantity
2022-11-27 11:11:45 +00:00
}
2022-11-10 15:34:55 +00:00
}
2022-12-01 16:09:47 +00:00
// if base investment and quote investment is set, when we should check if the
// investment configuration is valid with the current balances
2022-11-24 07:55:02 +00:00
if ! s . BaseInvestment . IsZero ( ) && ! s . QuoteInvestment . IsZero ( ) {
if s . BaseInvestment . Compare ( totalBase ) > 0 {
2023-04-14 07:15:28 +00:00
err2 := fmt . Errorf ( "baseInvestment setup %f is greater than the total base balance %f" , s . BaseInvestment . Float64 ( ) , totalBase . Float64 ( ) )
s . EmitGridError ( err2 )
return err2
2022-11-24 07:55:02 +00:00
}
if s . QuoteInvestment . Compare ( totalQuote ) > 0 {
2023-04-14 07:15:28 +00:00
err2 := fmt . Errorf ( "quoteInvestment setup %f is greater than the total quote balance %f" , s . QuoteInvestment . Float64 ( ) , totalQuote . Float64 ( ) )
s . EmitGridError ( err2 )
return err2
2022-11-24 07:55:02 +00:00
}
2023-02-17 10:35:42 +00:00
}
2022-11-24 07:55:02 +00:00
2023-02-17 10:35:42 +00:00
var submitOrders [ ] types . SubmitOrder
if ! s . BaseInvestment . IsZero ( ) || ! s . QuoteInvestment . IsZero ( ) {
submitOrders , err = s . generateGridOrders ( s . QuoteInvestment , s . BaseInvestment , lastPrice )
} else {
submitOrders , err = s . generateGridOrders ( totalQuote , totalBase , lastPrice )
2022-11-24 07:55:02 +00:00
}
2022-12-04 16:20:18 +00:00
if err != nil {
2023-04-14 07:15:28 +00:00
s . EmitGridError ( err )
2022-12-04 16:20:18 +00:00
return err
}
2022-12-15 10:30:28 +00:00
s . debugGridOrders ( submitOrders , lastPrice )
2023-03-03 10:42:08 +00:00
writeCtx := s . getWriteContext ( ctx )
2023-03-03 10:43:47 +00:00
createdOrders , err2 := s . orderExecutor . SubmitOrders ( writeCtx , submitOrders ... )
if err2 != nil {
2023-04-14 07:15:28 +00:00
s . EmitGridError ( err2 )
2023-03-03 10:43:47 +00:00
return err2
2022-12-04 16:20:18 +00:00
}
2023-02-16 14:49:22 +00:00
// try to always emit grid ready
defer s . EmitGridReady ( )
2023-01-10 12:15:51 +00:00
// update the number of orders to metrics
baseLabels := s . newPrometheusLabels ( )
metricsGridNumOfOrders . With ( baseLabels ) . Set ( float64 ( len ( createdOrders ) ) )
2022-12-25 17:35:37 +00:00
var orderIds [ ] uint64
2022-12-06 08:35:52 +00:00
for _ , order := range createdOrders {
2022-12-25 17:35:37 +00:00
orderIds = append ( orderIds , order . OrderID )
2022-12-06 08:35:52 +00:00
s . logger . Info ( order . String ( ) )
}
2022-12-25 17:35:37 +00:00
sort . Slice ( orderIds , func ( i , j int ) bool {
return orderIds [ i ] < orderIds [ j ]
} )
if len ( orderIds ) > 0 {
s . GridProfitStats . InitialOrderID = orderIds [ 0 ]
bbgo . Sync ( ctx , s )
}
2022-12-15 10:54:02 +00:00
s . logger . Infof ( "ALL GRID ORDERS SUBMITTED" )
2023-01-10 12:15:51 +00:00
2023-03-09 03:26:02 +00:00
s . updateGridNumOfOrdersMetrics ( grid )
2022-12-06 08:35:52 +00:00
return nil
}
2023-03-08 13:36:19 +00:00
func ( s * Strategy ) updateFilledOrderMetrics ( order types . Order ) {
labels := s . newPrometheusLabels ( )
labels [ "side" ] = order . Side . String ( )
metricsGridFilledOrderPrice . With ( labels ) . Set ( order . Price . Float64 ( ) )
}
func ( s * Strategy ) updateGridNumOfOrdersMetricsWithLock ( ) {
2023-06-29 17:05:18 +00:00
if s . mu . TryLock ( ) {
grid := s . grid
s . mu . Unlock ( )
s . updateGridNumOfOrdersMetrics ( grid )
2023-06-30 03:07:02 +00:00
} else {
s . logger . Warnf ( "updateGridNumOfOrdersMetricsWithLock: failed to acquire the lock to update metrics" )
2023-06-29 17:05:18 +00:00
}
2023-03-09 03:26:02 +00:00
}
func ( s * Strategy ) updateGridNumOfOrdersMetrics ( grid * Grid ) {
2023-02-23 14:39:47 +00:00
baseLabels := s . newPrometheusLabels ( )
2023-03-08 13:36:19 +00:00
makerOrders := s . orderExecutor . ActiveMakerOrders ( )
numOfOrders := makerOrders . NumOfOrders ( )
2023-02-23 14:49:03 +00:00
metricsGridNumOfOrders . With ( baseLabels ) . Set ( float64 ( numOfOrders ) )
2023-03-08 13:36:19 +00:00
metricsGridLowerPrice . With ( baseLabels ) . Set ( s . LowerPrice . Float64 ( ) )
metricsGridUpperPrice . With ( baseLabels ) . Set ( s . UpperPrice . Float64 ( ) )
metricsGridQuoteInvestment . With ( baseLabels ) . Set ( s . QuoteInvestment . Float64 ( ) )
metricsGridBaseInvestment . With ( baseLabels ) . Set ( s . BaseInvestment . Float64 ( ) )
2023-02-23 14:49:03 +00:00
2023-03-09 03:26:02 +00:00
if grid != nil {
2023-02-23 14:49:03 +00:00
gridNum := grid . Size . Int ( )
metricsGridNum . With ( baseLabels ) . Set ( float64 ( gridNum ) )
numOfMissingOrders := gridNum - 1 - numOfOrders
metricsGridNumOfMissingOrders . With ( baseLabels ) . Set ( float64 ( numOfMissingOrders ) )
2023-03-08 13:36:19 +00:00
var numOfOrdersWithCorrectPrice int
2023-03-15 13:40:44 +00:00
priceSet := make ( map [ fixedpoint . Value ] struct { } )
2023-03-08 13:36:19 +00:00
for _ , order := range makerOrders . Orders ( ) {
2023-03-15 13:40:44 +00:00
// filter out duplicated prices
if _ , ok := priceSet [ order . Price ] ; ok {
continue
}
priceSet [ order . Price ] = struct { } { }
2023-03-08 13:36:19 +00:00
if grid . HasPin ( Pin ( order . Price ) ) {
numOfOrdersWithCorrectPrice ++
}
}
numOfMissingOrdersWithCorrectPrice := gridNum - 1 - numOfOrdersWithCorrectPrice
metricsGridNumOfOrdersWithCorrectPrice . With ( baseLabels ) . Set ( float64 ( numOfOrdersWithCorrectPrice ) )
metricsGridNumOfMissingOrdersWithCorrectPrice . With ( baseLabels ) . Set ( float64 ( numOfMissingOrdersWithCorrectPrice ) )
2023-02-23 14:49:03 +00:00
}
2023-02-23 14:39:47 +00:00
}
2022-12-06 08:35:52 +00:00
func ( s * Strategy ) debugGridOrders ( submitOrders [ ] types . SubmitOrder , lastPrice fixedpoint . Value ) {
2023-03-09 16:50:25 +00:00
if ! s . Debug {
return
}
2023-03-09 16:46:12 +00:00
var sb strings . Builder
2023-03-13 16:29:13 +00:00
sb . WriteString ( "GRID ORDERS [\n" )
2022-12-05 11:42:36 +00:00
for i , order := range submitOrders {
if i > 0 && lastPrice . Compare ( order . Price ) >= 0 && lastPrice . Compare ( submitOrders [ i - 1 ] . Price ) <= 0 {
2023-03-11 07:31:09 +00:00
sb . WriteString ( fmt . Sprintf ( " - LAST PRICE: %f\n" , lastPrice . Float64 ( ) ) )
2022-12-05 11:42:36 +00:00
}
2023-03-11 07:31:09 +00:00
sb . WriteString ( " - " + order . String ( ) + "\n" )
2023-03-09 16:46:12 +00:00
}
sb . WriteString ( "] END OF GRID ORDERS" )
s . logger . Infof ( sb . String ( ) )
}
func ( s * Strategy ) debugOrders ( desc string , orders [ ] types . Order ) {
2023-03-09 16:50:25 +00:00
if ! s . Debug {
return
}
2023-03-09 16:46:12 +00:00
var sb strings . Builder
if desc == "" {
desc = "ORDERS"
2022-12-04 16:20:18 +00:00
}
2023-03-09 16:46:12 +00:00
2023-03-13 16:29:13 +00:00
sb . WriteString ( desc + " [\n" )
2023-03-09 16:46:12 +00:00
for i , order := range orders {
2023-03-11 07:31:09 +00:00
sb . WriteString ( fmt . Sprintf ( " - %d) %s\n" , i , order . String ( ) ) )
2023-03-09 16:46:12 +00:00
}
sb . WriteString ( "]" )
s . logger . Infof ( sb . String ( ) )
2022-12-04 16:20:18 +00:00
}
2023-03-16 08:43:56 +00:00
func ( s * Strategy ) debugLog ( format string , args ... interface { } ) {
if ! s . Debug {
return
}
s . logger . Infof ( format , args ... )
}
2022-12-04 16:20:18 +00:00
func ( s * Strategy ) generateGridOrders ( totalQuote , totalBase , lastPrice fixedpoint . Value ) ( [ ] types . SubmitOrder , error ) {
2022-11-24 07:55:02 +00:00
var pins = s . grid . Pins
var usedBase = fixedpoint . Zero
var usedQuote = fixedpoint . Zero
var submitOrders [ ] types . SubmitOrder
2022-12-04 16:20:18 +00:00
// si is for sell order price index
2023-02-15 08:51:12 +00:00
var si = len ( pins )
2022-11-24 07:55:02 +00:00
for i := len ( pins ) - 1 ; i >= 0 ; i -- {
pin := pins [ i ]
2022-11-10 12:58:46 +00:00
price := fixedpoint . Value ( pin )
2022-12-05 17:17:29 +00:00
sellPrice := price
// when profitSpread is set, the sell price is shift upper with the given spread
if s . ProfitSpread . Sign ( ) > 0 {
sellPrice = sellPrice . Add ( s . ProfitSpread )
}
2022-11-24 07:55:02 +00:00
quantity := s . QuantityOrAmount . Quantity
if quantity . IsZero ( ) {
quantity = s . QuantityOrAmount . Amount . Div ( price )
}
2022-11-10 12:58:46 +00:00
2023-05-22 10:10:51 +00:00
placeSell := price . Compare ( lastPrice ) >= 0
2023-05-22 10:13:51 +00:00
// override the relative price position for sell order if BaseGridNum is defined
2023-05-24 09:52:14 +00:00
if s . BaseGridNum > 0 {
placeSell = i >= len ( pins ) - 1 - s . BaseGridNum
2023-05-22 10:13:51 +00:00
}
2023-05-22 10:10:51 +00:00
if placeSell {
2022-12-04 16:20:18 +00:00
si = i
2023-02-15 13:51:22 +00:00
2023-05-22 09:20:16 +00:00
// do not place sell order when i == 0 (the bottom of grid)
2023-02-15 13:51:22 +00:00
if i == 0 {
continue
}
2023-05-23 09:34:03 +00:00
if usedBase . Add ( quantity ) . Compare ( totalBase ) <= 0 {
2022-11-24 07:55:02 +00:00
submitOrders = append ( submitOrders , types . SubmitOrder {
2023-03-10 07:47:42 +00:00
Symbol : s . Symbol ,
Type : types . OrderTypeLimit ,
Side : types . SideTypeSell ,
Price : sellPrice ,
Quantity : quantity ,
Market : s . Market ,
TimeInForce : types . TimeInForceGTC ,
Tag : orderTag ,
GroupID : s . OrderGroupID ,
2023-03-13 13:27:13 +00:00
ClientOrderID : s . newClientOrderID ( ) ,
2022-11-24 07:55:02 +00:00
} )
usedBase = usedBase . Add ( quantity )
2023-02-15 13:51:22 +00:00
} else {
2023-02-10 09:51:50 +00:00
// if we don't have enough base asset
// then we need to place a buy order at the next price.
2022-12-05 03:21:07 +00:00
nextPin := pins [ i - 1 ]
2022-11-24 07:55:02 +00:00
nextPrice := fixedpoint . Value ( nextPin )
submitOrders = append ( submitOrders , types . SubmitOrder {
2023-03-10 07:47:42 +00:00
Symbol : s . Symbol ,
Type : types . OrderTypeLimit ,
Side : types . SideTypeBuy ,
Price : nextPrice ,
Quantity : quantity ,
Market : s . Market ,
TimeInForce : types . TimeInForceGTC ,
Tag : orderTag ,
GroupID : s . OrderGroupID ,
2023-03-13 13:27:13 +00:00
ClientOrderID : s . newClientOrderID ( ) ,
2022-11-24 07:55:02 +00:00
} )
2023-02-17 14:52:58 +00:00
quoteQuantity := quantity . Mul ( nextPrice )
2022-11-24 07:55:02 +00:00
usedQuote = usedQuote . Add ( quoteQuantity )
2022-11-10 12:58:46 +00:00
}
2022-11-24 07:55:02 +00:00
} else {
2023-02-10 09:51:50 +00:00
// if price spread is not enabled, and we have already placed a sell order index on the top of this price,
// then we should skip
2022-12-05 17:17:29 +00:00
if s . ProfitSpread . IsZero ( ) && i + 1 == si {
2022-12-04 16:20:18 +00:00
continue
}
2023-02-10 09:51:50 +00:00
// should never place a buy order at the upper price
if i == len ( pins ) - 1 {
continue
}
2023-02-16 13:38:48 +00:00
quoteQuantity := quantity . Mul ( price )
if usedQuote . Add ( quoteQuantity ) . Compare ( totalQuote ) > 0 {
2023-02-17 14:52:58 +00:00
s . logger . Warnf ( "used quote %f > total quote %f, this should not happen" , usedQuote . Add ( quoteQuantity ) . Float64 ( ) , totalQuote . Float64 ( ) )
2023-02-16 13:38:48 +00:00
continue
}
2022-11-24 08:35:31 +00:00
submitOrders = append ( submitOrders , types . SubmitOrder {
2023-03-10 07:47:42 +00:00
Symbol : s . Symbol ,
Type : types . OrderTypeLimit ,
Side : types . SideTypeBuy ,
Price : price ,
Quantity : quantity ,
Market : s . Market ,
TimeInForce : types . TimeInForceGTC ,
Tag : orderTag ,
GroupID : s . OrderGroupID ,
2023-03-13 13:27:13 +00:00
ClientOrderID : s . newClientOrderID ( ) ,
2022-11-24 07:55:02 +00:00
} )
2022-11-24 08:35:31 +00:00
usedQuote = usedQuote . Add ( quoteQuantity )
}
2022-12-04 10:33:28 +00:00
}
2022-11-24 07:55:02 +00:00
2022-12-04 16:20:18 +00:00
return submitOrders , nil
2022-11-10 12:58:46 +00:00
}
2022-12-04 05:04:14 +00:00
func ( s * Strategy ) clearOpenOrders ( ctx context . Context , session * bbgo . ExchangeSession ) error {
// clear open orders when start
2023-06-29 02:56:07 +00:00
openOrders , err := retry . QueryOpenOrdersUntilSuccessful ( ctx , session . Exchange , s . Symbol )
2022-12-04 05:04:14 +00:00
if err != nil {
return err
}
2023-06-29 02:56:07 +00:00
return retry . CancelOrdersUntilSuccessful ( ctx , session . Exchange , openOrders ... )
2022-12-04 05:04:14 +00:00
}
2022-11-10 12:58:46 +00:00
func ( s * Strategy ) getLastTradePrice ( ctx context . Context , session * bbgo . ExchangeSession ) ( fixedpoint . Value , error ) {
if bbgo . IsBackTesting {
price , ok := session . LastPrice ( s . Symbol )
if ! ok {
return fixedpoint . Zero , fmt . Errorf ( "last price of %s not found" , s . Symbol )
}
return price , nil
}
tickers , err := session . Exchange . QueryTickers ( ctx , s . Symbol )
if err != nil {
return fixedpoint . Zero , err
}
if ticker , ok := tickers [ s . Symbol ] ; ok {
if ! ticker . Last . IsZero ( ) {
return ticker . Last , nil
}
// fallback to buy price
return ticker . Buy , nil
}
return fixedpoint . Zero , fmt . Errorf ( "%s ticker price not found" , s . Symbol )
}
2022-12-04 04:58:01 +00:00
2023-02-21 09:48:40 +00:00
func calculateMinimalQuoteInvestment ( market types . Market , grid * Grid ) fixedpoint . Value {
// upperPrice for buy order
2023-02-21 09:58:11 +00:00
lowerPrice := grid . LowerPrice
minQuantity := fixedpoint . Max ( market . MinNotional . Div ( lowerPrice ) , market . MinQuantity )
2023-02-21 09:48:40 +00:00
var pins = grid . Pins
var totalQuote = fixedpoint . Zero
for i := len ( pins ) - 2 ; i >= 0 ; i -- {
pin := pins [ i ]
price := fixedpoint . Value ( pin )
totalQuote = totalQuote . Add ( price . Mul ( minQuantity ) )
}
return totalQuote
2022-12-06 08:09:46 +00:00
}
2023-02-21 09:48:40 +00:00
func ( s * Strategy ) checkMinimalQuoteInvestment ( grid * Grid ) error {
minimalQuoteInvestment := calculateMinimalQuoteInvestment ( s . Market , grid )
2022-12-06 07:55:52 +00:00
if s . QuoteInvestment . Compare ( minimalQuoteInvestment ) <= 0 {
return fmt . Errorf ( "need at least %f %s for quote investment, %f %s given" ,
minimalQuoteInvestment . Float64 ( ) ,
s . Market . QuoteCurrency ,
s . QuoteInvestment . Float64 ( ) ,
s . Market . QuoteCurrency )
}
return nil
}
2023-08-31 05:48:56 +00:00
func ( s * Strategy ) recoverGridWithOpenOrders (
ctx context . Context , historyService types . ExchangeTradeHistoryService , openOrders [ ] types . Order ,
) error {
2022-12-24 06:48:30 +00:00
grid := s . newGrid ( )
2023-02-15 14:32:55 +00:00
s . logger . Infof ( "GRID RECOVER: %s" , grid . String ( ) )
2022-12-23 11:15:46 +00:00
lastOrderID := uint64 ( 1 )
2022-12-23 09:54:30 +00:00
now := time . Now ( )
2022-12-23 16:54:40 +00:00
firstOrderTime := now . AddDate ( 0 , 0 , - 7 )
2022-12-23 09:54:30 +00:00
lastOrderTime := firstOrderTime
2022-12-23 15:41:36 +00:00
if since , until , ok := scanOrderCreationTimeRange ( openOrders ) ; ok {
firstOrderTime = since
lastOrderTime = until
2022-12-20 07:56:38 +00:00
}
2022-12-24 09:08:50 +00:00
_ = lastOrderTime
2022-12-23 16:54:40 +00:00
// for MAX exchange we need the order ID to query the closed order history
2022-12-25 17:35:37 +00:00
if s . GridProfitStats != nil && s . GridProfitStats . InitialOrderID > 0 {
lastOrderID = s . GridProfitStats . InitialOrderID
2023-03-05 09:34:50 +00:00
s . logger . Infof ( "found initial order id #%d from grid stats" , lastOrderID )
2022-12-25 17:35:37 +00:00
} else {
if oid , ok := findEarliestOrderID ( openOrders ) ; ok {
lastOrderID = oid
2023-03-05 09:34:50 +00:00
s . logger . Infof ( "found earliest order id #%d from open orders" , lastOrderID )
2022-12-25 17:35:37 +00:00
}
2022-12-23 16:54:40 +00:00
}
2023-02-21 07:50:25 +00:00
// Allocate a local order book for querying the history orders
2022-12-20 09:33:53 +00:00
orderBook := bbgo . NewActiveOrderBook ( s . Symbol )
// Ensure that orders are grid orders
// The price must be at the grid pin
2023-02-21 07:50:25 +00:00
gridOrders := grid . FilterOrders ( openOrders )
for _ , gridOrder := range gridOrders {
orderBook . Add ( gridOrder )
2022-12-20 09:33:53 +00:00
}
2022-12-24 09:08:50 +00:00
// if all open orders are the grid orders, then we don't have to recover
2023-02-15 14:17:36 +00:00
s . logger . Infof ( "GRID RECOVER: verifying pins %v" , PrettyPins ( grid . Pins ) )
2022-12-24 08:14:39 +00:00
missingPrices := scanMissingPinPrices ( orderBook , grid . Pins )
2022-12-24 09:08:50 +00:00
if numMissing := len ( missingPrices ) ; numMissing <= 1 {
2022-12-24 08:14:39 +00:00
s . logger . Infof ( "GRID RECOVER: no missing grid prices, stop re-playing order history" )
2023-03-05 06:29:31 +00:00
s . addOrdersToActiveOrderBook ( gridOrders )
2023-02-15 14:44:07 +00:00
s . setGrid ( grid )
2023-02-16 13:33:42 +00:00
s . EmitGridReady ( )
2023-03-08 13:36:19 +00:00
s . updateGridNumOfOrdersMetricsWithLock ( )
2022-12-24 08:14:39 +00:00
return nil
2022-12-24 09:08:50 +00:00
} else {
2023-02-15 14:17:36 +00:00
s . logger . Infof ( "GRID RECOVER: found missing prices: %v" , missingPrices )
2022-12-24 09:08:50 +00:00
// Note that for MAX Exchange, the order history API only uses fromID parameter to query history order.
// The time range does not matter.
// TODO: handle context correctly
startTime := firstOrderTime
endTime := now
2022-12-26 10:08:36 +00:00
maxTries := 5
2022-12-26 10:05:35 +00:00
localHistoryRollbackDuration := historyRollbackDuration
2022-12-24 09:08:50 +00:00
for maxTries > 0 {
maxTries --
if err := s . replayOrderHistory ( ctx , grid , orderBook , historyService , startTime , endTime , lastOrderID ) ; err != nil {
return err
}
2022-12-24 08:14:39 +00:00
2022-12-24 09:08:50 +00:00
// Verify if there are still missing prices
missingPrices = scanMissingPinPrices ( orderBook , grid . Pins )
if len ( missingPrices ) <= 1 {
// skip this order history loop and start recovering
break
}
// history rollback range
2022-12-26 10:05:35 +00:00
startTime = startTime . Add ( - localHistoryRollbackDuration )
2022-12-24 09:08:50 +00:00
if newFromOrderID := lastOrderID - historyRollbackOrderIdRange ; newFromOrderID > 1 {
lastOrderID = newFromOrderID
}
s . logger . Infof ( "GRID RECOVER: there are still more than two missing orders, rolling back query start time to earlier time point %s, fromID %d" , startTime . String ( ) , lastOrderID )
2022-12-26 10:05:35 +00:00
localHistoryRollbackDuration = localHistoryRollbackDuration * 2
2022-12-24 09:08:50 +00:00
}
2022-12-24 06:48:30 +00:00
}
2023-02-22 07:16:47 +00:00
debugGrid ( s . logger , grid , orderBook )
2022-12-24 06:48:30 +00:00
2023-03-05 09:41:05 +00:00
// note that the tmpOrders contains FILLED and NEW orders
2022-12-24 06:48:30 +00:00
tmpOrders := orderBook . Orders ( )
// if all orders on the order book are active orders, we don't need to recover.
if isCompleteGridOrderBook ( orderBook , s . GridNum ) {
s . logger . Infof ( "GRID RECOVER: all orders are active orders, do not need recover" )
2023-03-05 06:29:31 +00:00
s . addOrdersToActiveOrderBook ( gridOrders )
2023-02-15 14:44:07 +00:00
s . setGrid ( grid )
2023-02-16 13:33:42 +00:00
s . EmitGridReady ( )
2023-03-08 13:36:19 +00:00
s . updateGridNumOfOrdersMetricsWithLock ( )
2022-12-24 06:48:30 +00:00
return nil
}
// for reverse order recovering, we need the orders to be sort by update time ascending-ly
types . SortOrdersUpdateTimeAscending ( tmpOrders )
// we will only submit reverse orders for filled orders
2022-12-24 07:58:02 +00:00
filledOrders := types . OrdersFilled ( tmpOrders )
2022-12-24 06:48:30 +00:00
2023-03-05 09:41:05 +00:00
// if the number of FILLED orders and NEW orders equals to GridNum, then we need to remove an extra filled order for the replay events
2023-03-05 15:20:17 +00:00
if len ( tmpOrders ) == int ( s . GridNum ) && len ( filledOrders ) > 0 {
2023-03-05 09:41:05 +00:00
// remove the latest updated order because it's near the empty slot
filledOrders = filledOrders [ 1 : ]
}
2023-03-05 09:55:04 +00:00
s . logger . Infof ( "GRID RECOVER: found %d/%d filled grid orders, gridNumber=%d, will re-replay the order event in the following order:" , len ( filledOrders ) , len ( tmpOrders ) , int ( s . GridNum ) )
2023-02-20 14:25:00 +00:00
for i , o := range filledOrders {
2023-02-21 17:08:19 +00:00
s . logger . Infof ( "%d) %s" , i + 1 , o . String ( ) )
2023-02-20 14:25:00 +00:00
}
2023-02-21 07:50:25 +00:00
// before we re-play the orders,
// we need to add these open orders to the active order book
2023-03-05 06:29:31 +00:00
s . addOrdersToActiveOrderBook ( gridOrders )
2023-02-15 14:44:07 +00:00
s . setGrid ( grid )
2023-02-16 13:33:42 +00:00
s . EmitGridReady ( )
2023-03-08 13:36:19 +00:00
s . updateGridNumOfOrdersMetricsWithLock ( )
2023-02-06 17:38:25 +00:00
2023-02-24 18:24:39 +00:00
for i := range filledOrders {
// avoid using the iterator
o := filledOrders [ i ]
2022-12-24 06:48:30 +00:00
s . processFilledOrder ( o )
2023-02-21 17:11:34 +00:00
time . Sleep ( 100 * time . Millisecond )
2022-12-24 06:48:30 +00:00
}
2023-02-21 17:10:49 +00:00
// wait for the reverse order to be placed
time . Sleep ( 2 * time . Second )
2022-12-24 06:48:30 +00:00
s . logger . Infof ( "GRID RECOVER COMPLETE" )
2023-02-22 07:16:47 +00:00
debugGrid ( s . logger , grid , s . orderExecutor . ActiveMakerOrders ( ) )
2023-02-23 14:39:47 +00:00
2023-03-08 13:36:19 +00:00
s . updateGridNumOfOrdersMetricsWithLock ( )
2022-12-24 06:48:30 +00:00
return nil
}
2023-03-05 06:29:31 +00:00
func ( s * Strategy ) addOrdersToActiveOrderBook ( gridOrders [ ] types . Order ) {
activeOrderBook := s . orderExecutor . ActiveMakerOrders ( )
for _ , gridOrder := range gridOrders {
// put the order back to the active order book so that we can receive order update
activeOrderBook . Add ( gridOrder )
}
}
2023-02-15 14:44:07 +00:00
func ( s * Strategy ) setGrid ( grid * Grid ) {
s . mu . Lock ( )
s . grid = grid
s . mu . Unlock ( )
}
2023-02-23 14:49:03 +00:00
func ( s * Strategy ) getGrid ( ) * Grid {
s . mu . Lock ( )
grid := s . grid
s . mu . Unlock ( )
return grid
}
2022-12-24 08:14:39 +00:00
// replayOrderHistory queries the closed order history from the API and rebuild the orderbook from the order history.
// startTime, endTime is the time range of the order history.
2023-08-31 05:48:56 +00:00
func ( s * Strategy ) replayOrderHistory (
ctx context . Context , grid * Grid , orderBook * bbgo . ActiveOrderBook , historyService types . ExchangeTradeHistoryService ,
startTime , endTime time . Time , lastOrderID uint64 ,
) error {
2022-12-23 04:56:19 +00:00
// a simple guard, in reality, this startTime is not possible to exceed the endTime
// because the queries closed orders might still in the range.
2022-12-24 08:14:39 +00:00
orderIdChanged := true
for startTime . Before ( endTime ) && orderIdChanged {
2022-12-23 04:56:19 +00:00
closedOrders , err := historyService . QueryClosedOrders ( ctx , s . Symbol , startTime , endTime , lastOrderID )
if err != nil {
return err
}
2022-12-24 08:14:39 +00:00
// need to prevent infinite loop for:
// if there is only one order and the order creation time matches our startTime
if len ( closedOrders ) == 0 || len ( closedOrders ) == 1 && closedOrders [ 0 ] . OrderID == lastOrderID {
2022-12-23 04:56:19 +00:00
break
2022-12-20 09:33:53 +00:00
}
2022-12-23 04:56:19 +00:00
// for each closed order, if it's newer than the open order's update time, we will update it.
2022-12-24 08:14:39 +00:00
orderIdChanged = false
2022-12-23 04:56:19 +00:00
for _ , closedOrder := range closedOrders {
2022-12-24 08:14:39 +00:00
if closedOrder . OrderID > lastOrderID {
lastOrderID = closedOrder . OrderID
orderIdChanged = true
}
2022-12-23 11:15:46 +00:00
// skip orders that are not limit order
if closedOrder . Type != types . OrderTypeLimit {
continue
}
2022-12-23 17:08:28 +00:00
// skip canceled orders (?)
if closedOrder . Status == types . OrderStatusCanceled {
continue
}
2022-12-23 04:56:19 +00:00
creationTime := closedOrder . CreationTime . Time ( )
if creationTime . After ( startTime ) {
startTime = creationTime
}
// skip non-grid order prices
2023-02-15 14:17:36 +00:00
if ! grid . HasPrice ( closedOrder . Price ) {
2022-12-23 04:56:19 +00:00
continue
}
existingOrder := orderBook . Lookup ( func ( o types . Order ) bool {
2022-12-23 11:15:46 +00:00
return o . Price . Eq ( closedOrder . Price )
2022-12-23 04:56:19 +00:00
} )
2022-12-23 11:15:46 +00:00
if existingOrder == nil {
orderBook . Add ( closedOrder )
} else {
2022-12-23 09:54:30 +00:00
// To update order, we need to remove the old order, because it's using order ID as the key of the map.
2022-12-23 11:15:46 +00:00
if creationTime . After ( existingOrder . CreationTime . Time ( ) ) {
2022-12-23 04:56:19 +00:00
orderBook . Remove ( * existingOrder )
orderBook . Add ( closedOrder )
}
}
}
}
return nil
}
2023-03-07 13:42:53 +00:00
// isCompleteGridOrderBook checks if the number of open orders == gridNum - 1 and all orders are active order
2022-12-23 16:54:40 +00:00
func isCompleteGridOrderBook ( orderBook * bbgo . ActiveOrderBook , gridNum int64 ) bool {
tmpOrders := orderBook . Orders ( )
2023-03-08 08:02:31 +00:00
activeOrders := types . OrdersActive ( tmpOrders )
return len ( activeOrders ) == int ( gridNum ) - 1
2022-12-23 16:54:40 +00:00
}
func findEarliestOrderID ( orders [ ] types . Order ) ( uint64 , bool ) {
if len ( orders ) == 0 {
return 0 , false
}
earliestOrderID := orders [ 0 ] . OrderID
for _ , o := range orders {
if o . OrderID < earliestOrderID {
earliestOrderID = o . OrderID
}
}
return earliestOrderID , true
}
2022-12-23 15:41:36 +00:00
// scanOrderCreationTimeRange finds the earliest creation time and the latest creation time from the given orders
func scanOrderCreationTimeRange ( orders [ ] types . Order ) ( time . Time , time . Time , bool ) {
if len ( orders ) == 0 {
return time . Time { } , time . Time { } , false
}
firstOrderTime := orders [ 0 ] . CreationTime . Time ( )
lastOrderTime := firstOrderTime
for _ , o := range orders {
createTime := o . CreationTime . Time ( )
if createTime . Before ( firstOrderTime ) {
firstOrderTime = createTime
} else if createTime . After ( lastOrderTime ) {
lastOrderTime = createTime
}
}
return firstOrderTime , lastOrderTime , true
}
2022-12-24 06:48:30 +00:00
// scanMissingPinPrices finds the missing grid order prices
func scanMissingPinPrices ( orderBook * bbgo . ActiveOrderBook , pins [ ] Pin ) PriceMap {
2022-12-23 04:56:19 +00:00
// Add all open orders to the local order book
gridPrices := make ( PriceMap )
missingPrices := make ( PriceMap )
2022-12-24 06:48:30 +00:00
for _ , pin := range pins {
2022-12-23 04:56:19 +00:00
price := fixedpoint . Value ( pin )
gridPrices [ price . String ( ) ] = price
2022-12-20 09:33:53 +00:00
existingOrder := orderBook . Lookup ( func ( o types . Order ) bool {
2022-12-23 04:56:19 +00:00
return o . Price . Compare ( price ) == 0
2022-12-20 09:33:53 +00:00
} )
if existingOrder == nil {
2022-12-23 04:56:19 +00:00
missingPrices [ price . String ( ) ] = price
2022-12-20 09:33:53 +00:00
}
}
2022-12-23 04:56:19 +00:00
return missingPrices
2022-12-20 07:56:38 +00:00
}
2023-01-10 12:15:51 +00:00
func ( s * Strategy ) newPrometheusLabels ( ) prometheus . Labels {
labels := prometheus . Labels {
2023-02-23 16:44:50 +00:00
"exchange" : "default" ,
2023-01-10 16:47:36 +00:00
"symbol" : s . Symbol ,
2023-01-10 12:15:51 +00:00
}
2023-02-23 16:44:50 +00:00
if s . session != nil {
labels [ "exchange" ] = s . session . Name
}
2023-01-10 12:15:51 +00:00
if s . PrometheusLabels == nil {
return labels
}
return mergeLabels ( s . PrometheusLabels , labels )
}
2023-02-20 08:52:39 +00:00
func ( s * Strategy ) CleanUp ( ctx context . Context ) error {
2023-03-02 10:05:48 +00:00
if s . ExchangeSession != nil {
s . session = s . ExchangeSession
}
2023-03-02 10:08:26 +00:00
_ = s . Initialize ( )
2023-03-02 10:16:09 +00:00
defer s . EmitGridClosed ( )
2023-03-02 09:33:58 +00:00
return s . cancelAll ( ctx )
2023-02-20 08:52:39 +00:00
}
2023-03-03 10:42:08 +00:00
func ( s * Strategy ) getWriteContext ( fallbackCtxList ... context . Context ) context . Context {
if s . writeCtx != nil {
return s . writeCtx
}
for _ , c := range fallbackCtxList {
if c != nil {
return c
}
}
if s . tradingCtx != nil {
return s . tradingCtx
}
// final fallback to context background
return context . Background ( )
}
2022-12-07 06:19:49 +00:00
func ( s * Strategy ) Run ( ctx context . Context , _ bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
2022-12-04 04:58:01 +00:00
instanceID := s . InstanceID ( )
2023-03-03 10:42:08 +00:00
// allocate a context for write operation (submitting orders)
s . tradingCtx = ctx
s . writeCtx , s . cancelWrite = context . WithCancel ( ctx )
2022-12-04 06:24:04 +00:00
s . session = session
2022-12-04 07:43:27 +00:00
if service , ok := session . Exchange . ( types . ExchangeOrderQueryService ) ; ok {
s . orderQueryService = service
}
2023-02-08 08:26:37 +00:00
if s . OrderGroupID == 0 {
2023-03-07 03:54:45 +00:00
s . OrderGroupID = util . FNV32 ( instanceID ) % math . MaxInt32
2023-02-08 08:26:37 +00:00
}
2022-12-25 16:56:03 +00:00
if s . AutoRange != nil {
indicatorSet := session . StandardIndicatorSet ( s . Symbol )
interval := s . AutoRange . Interval ( )
pivotLow := indicatorSet . PivotLow ( types . IntervalWindow { Interval : interval , Window : s . AutoRange . Num } )
pivotHigh := indicatorSet . PivotHigh ( types . IntervalWindow { Interval : interval , Window : s . AutoRange . Num } )
2023-05-31 11:35:44 +00:00
s . UpperPrice = fixedpoint . NewFromFloat ( pivotHigh . Last ( 0 ) )
s . LowerPrice = fixedpoint . NewFromFloat ( pivotLow . Last ( 0 ) )
2022-12-25 16:56:03 +00:00
s . logger . Infof ( "autoRange is enabled, using pivot high %f and pivot low %f" , s . UpperPrice . Float64 ( ) , s . LowerPrice . Float64 ( ) )
}
2022-12-04 04:58:01 +00:00
2022-12-05 18:13:32 +00:00
if s . ProfitSpread . Sign ( ) > 0 {
s . ProfitSpread = s . Market . TruncatePrice ( s . ProfitSpread )
}
2022-12-04 06:45:04 +00:00
if s . GridProfitStats == nil {
2022-12-04 07:24:13 +00:00
s . GridProfitStats = newGridProfitStats ( s . Market )
2022-12-04 06:45:04 +00:00
}
2022-12-04 04:58:01 +00:00
if s . Position == nil {
s . Position = types . NewPositionFromMarket ( s . Market )
}
2023-01-10 12:15:51 +00:00
// initialize and register prometheus metrics
if s . PrometheusLabels != nil {
initMetrics ( labelKeys ( s . PrometheusLabels ) )
} else {
initMetrics ( nil )
}
registerMetrics ( )
2022-12-05 11:37:42 +00:00
if s . ResetPositionWhenStart {
s . Position . Reset ( )
}
2022-12-06 07:55:52 +00:00
// we need to check the minimal quote investment here, because we need the market info
if s . QuoteInvestment . Sign ( ) > 0 {
2023-02-21 09:48:40 +00:00
grid := s . newGrid ( )
if err := s . checkMinimalQuoteInvestment ( grid ) ; err != nil {
2023-03-02 07:50:10 +00:00
if s . StopIfLessThanMinimalQuoteInvestment {
s . logger . WithError ( err ) . Errorf ( "check minimal quote investment failed, market info: %+v" , s . Market )
return err
} else {
// if no, just warning
s . logger . WithError ( err ) . Warnf ( "minimal quote investment may be not enough, market info: %+v" , s . Market )
}
2022-12-06 07:55:52 +00:00
}
}
2023-07-04 13:42:24 +00:00
s . historicalTrades = core . NewTradeStore ( )
2022-12-05 16:55:08 +00:00
s . historicalTrades . EnablePrune = true
2022-12-05 11:23:39 +00:00
s . historicalTrades . BindStream ( session . UserDataStream )
2022-12-04 10:42:03 +00:00
2022-12-07 06:19:49 +00:00
orderExecutor := bbgo . NewGeneralOrderExecutor ( session , s . Symbol , ID , instanceID , s . Position )
orderExecutor . BindEnvironment ( s . Environment )
orderExecutor . Bind ( )
orderExecutor . TradeCollector ( ) . OnTrade ( func ( trade types . Trade , _ , _ fixedpoint . Value ) {
2022-12-06 02:05:43 +00:00
s . GridProfitStats . AddTrade ( trade )
} )
2022-12-07 06:19:49 +00:00
orderExecutor . TradeCollector ( ) . OnPositionUpdate ( func ( position * types . Position ) {
2022-12-04 04:58:01 +00:00
bbgo . Sync ( ctx , s )
} )
2022-12-15 10:47:45 +00:00
orderExecutor . ActiveMakerOrders ( ) . OnFilled ( s . newOrderUpdateHandler ( ctx , session ) )
2023-10-11 09:33:07 +00:00
orderExecutor . SetMaxRetries ( 5 )
2022-12-15 10:42:25 +00:00
2023-03-02 08:16:14 +00:00
if s . logger != nil {
orderExecutor . SetLogger ( s . logger )
}
2022-12-07 06:19:49 +00:00
s . orderExecutor = orderExecutor
2022-12-04 04:58:01 +00:00
2023-01-10 12:15:51 +00:00
s . OnGridProfit ( func ( stats * GridProfitStats , profit * GridProfit ) {
2023-04-26 16:33:42 +00:00
if profit != nil {
bbgo . Notify ( profit )
}
2023-01-10 12:15:51 +00:00
bbgo . Notify ( stats )
} )
s . OnGridProfit ( func ( stats * GridProfitStats , profit * GridProfit ) {
labels := s . newPrometheusLabels ( )
metricsGridProfit . With ( labels ) . Set ( stats . TotalQuoteProfit . Float64 ( ) )
} )
2022-12-04 04:58:01 +00:00
bbgo . OnShutdown ( ctx , func ( ctx context . Context , wg * sync . WaitGroup ) {
defer wg . Done ( )
2023-03-05 09:10:11 +00:00
if s . cancelWrite != nil {
s . cancelWrite ( )
}
2022-12-04 04:58:01 +00:00
if s . KeepOrdersWhenShutdown {
2022-12-23 09:54:30 +00:00
s . logger . Infof ( "keepOrdersWhenShutdown is set, will keep the orders on the exchange" )
2022-12-04 04:58:01 +00:00
return
}
2023-01-12 06:33:09 +00:00
if err := s . CloseGrid ( ctx ) ; err != nil {
2022-12-04 04:58:01 +00:00
s . logger . WithError ( err ) . Errorf ( "grid graceful order cancel error" )
}
} )
2022-12-04 06:22:11 +00:00
if ! s . TriggerPrice . IsZero ( ) {
session . MarketDataStream . OnKLineClosed ( s . newTriggerPriceHandler ( ctx , session ) )
}
2022-12-04 10:01:58 +00:00
if ! s . StopLossPrice . IsZero ( ) {
session . MarketDataStream . OnKLineClosed ( s . newStopLossPriceHandler ( ctx , session ) )
}
2022-12-05 11:46:08 +00:00
if ! s . TakeProfitPrice . IsZero ( ) {
session . MarketDataStream . OnKLineClosed ( s . newTakeProfitHandler ( ctx , session ) )
}
2023-02-23 15:34:26 +00:00
// detect if there are previous grid orders on the order book
session . UserDataStream . OnStart ( func ( ) {
if s . ClearOpenOrdersWhenStart {
s . logger . Infof ( "clearOpenOrdersWhenStart is set, clearing open orders..." )
if err := s . clearOpenOrders ( ctx , session ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "clearOpenOrdersWhenStart error" )
}
}
if s . ClearOpenOrdersIfMismatch {
s . logger . Infof ( "clearOpenOrdersIfMismatch is set, checking mismatched orders..." )
mismatch , err := s . openOrdersMismatches ( ctx , session )
if err != nil {
s . logger . WithError ( err ) . Errorf ( "clearOpenOrdersIfMismatch error" )
} else if mismatch {
if err2 := s . clearOpenOrders ( ctx , session ) ; err2 != nil {
s . logger . WithError ( err2 ) . Errorf ( "clearOpenOrders error" )
}
}
}
2023-03-09 16:46:12 +00:00
if s . ClearDuplicatedPriceOpenOrders {
s . logger . Infof ( "clearDuplicatedPriceOpenOrders is set, finding duplicated open orders..." )
if err := s . cancelDuplicatedPriceOpenOrders ( ctx , session ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "cancelDuplicatedPriceOpenOrders error" )
}
}
2023-02-23 15:34:26 +00:00
} )
2022-12-25 17:35:37 +00:00
// if TriggerPrice is zero, that means we need to open the grid when start up
if s . TriggerPrice . IsZero ( ) {
2023-10-16 08:02:43 +00:00
session . UserDataStream . OnAuth ( func ( ) {
s . logger . Infof ( "user data stream authenticated, start the process" )
2023-03-05 09:07:01 +00:00
if ! bbgo . IsBackTesting {
2023-10-06 10:04:57 +00:00
time . AfterFunc ( 3 * time . Second , func ( ) {
if err := s . startProcess ( ctx , session ) ; err != nil {
return
}
2023-10-09 09:06:22 +00:00
s . recoverActiveOrdersPeriodically ( ctx )
2023-08-31 09:08:00 +00:00
} )
2023-03-05 09:07:01 +00:00
} else {
s . startProcess ( ctx , session )
2023-03-03 10:22:31 +00:00
}
2022-12-25 17:35:37 +00:00
} )
}
2022-12-04 04:58:01 +00:00
return nil
}
2023-02-08 08:43:25 +00:00
2023-10-06 10:04:57 +00:00
func ( s * Strategy ) startProcess ( ctx context . Context , session * bbgo . ExchangeSession ) error {
2023-03-05 09:07:01 +00:00
if s . RecoverOrdersWhenStart {
// do recover only when triggerPrice is not set and not in the back-test mode
s . logger . Infof ( "recoverWhenStart is set, trying to recover grid orders..." )
2023-03-09 09:42:37 +00:00
if err := s . recoverGrid ( ctx , session ) ; err != nil {
2023-03-20 08:27:08 +00:00
// if recover fail, return and do not open grid
2023-04-14 10:30:38 +00:00
s . logger . WithError ( err ) . Error ( "failed to start process, recover error" )
2023-03-20 08:27:08 +00:00
s . EmitGridError ( errors . Wrapf ( err , "failed to start process, recover error" ) )
2023-10-06 10:04:57 +00:00
return err
2023-03-05 09:07:01 +00:00
}
}
// avoid using goroutine here for back-test
if err := s . openGrid ( ctx , session ) ; err != nil {
2023-03-20 08:27:08 +00:00
s . EmitGridError ( errors . Wrapf ( err , "failed to start process, setup grid orders error" ) )
2023-10-06 10:04:57 +00:00
return err
2023-03-05 09:07:01 +00:00
}
2023-10-06 10:04:57 +00:00
return nil
2023-03-05 09:07:01 +00:00
}
2023-03-09 09:42:37 +00:00
func ( s * Strategy ) recoverGrid ( ctx context . Context , session * bbgo . ExchangeSession ) error {
if s . RecoverGridByScanningTrades {
2023-04-06 16:40:32 +00:00
s . debugLog ( "recovering grid by scanning trades" )
2023-04-06 08:12:19 +00:00
return s . recoverByScanningTrades ( ctx , session )
2023-03-09 09:42:37 +00:00
}
2023-04-06 16:40:32 +00:00
s . debugLog ( "recovering grid by scanning orders" )
2023-04-06 08:12:19 +00:00
return s . recoverByScanningOrders ( ctx , session )
2023-03-09 09:42:37 +00:00
}
2023-04-06 08:12:19 +00:00
func ( s * Strategy ) recoverByScanningOrders ( ctx context . Context , session * bbgo . ExchangeSession ) error {
2023-06-29 02:56:07 +00:00
openOrders , err := retry . QueryOpenOrdersUntilSuccessful ( ctx , session . Exchange , s . Symbol )
2023-02-15 13:49:25 +00:00
if err != nil {
return err
}
2023-02-23 03:19:10 +00:00
// do recover only when openOrders > 0
if len ( openOrders ) == 0 {
2023-03-05 09:34:50 +00:00
s . logger . Warn ( "0 open orders, skip recovery process" )
2023-02-23 03:19:10 +00:00
return nil
}
2023-02-15 13:49:25 +00:00
s . logger . Infof ( "found %d open orders left on the %s order book" , len ( openOrders ) , s . Symbol )
2023-02-23 03:19:10 +00:00
historyService , implemented := session . Exchange . ( types . ExchangeTradeHistoryService )
if ! implemented {
s . logger . Warn ( "ExchangeTradeHistoryService is not implemented, can not recover grid" )
return nil
}
2023-02-15 13:49:25 +00:00
2023-02-23 03:19:10 +00:00
if err := s . recoverGridWithOpenOrders ( ctx , historyService , openOrders ) ; err != nil {
2023-03-05 09:34:50 +00:00
return errors . Wrap ( err , "grid recover error" )
2023-02-15 13:49:25 +00:00
}
2023-02-23 10:08:21 +00:00
return nil
2023-02-15 13:49:25 +00:00
}
2023-02-08 08:43:25 +00:00
// openOrdersMismatches verifies if the open orders are on the grid pins
// return true if mismatches
func ( s * Strategy ) openOrdersMismatches ( ctx context . Context , session * bbgo . ExchangeSession ) ( bool , error ) {
openOrders , err := session . Exchange . QueryOpenOrders ( ctx , s . Symbol )
if err != nil {
return false , err
}
if len ( openOrders ) == 0 {
return false , nil
}
grid := s . newGrid ( )
for _ , o := range openOrders {
// if any of the open order is not on the grid, or out of the range
// we should cancel all of them
if ! grid . HasPrice ( o . Price ) || grid . OutOfRange ( o . Price ) {
return true , nil
}
}
return false , nil
2023-02-20 08:52:39 +00:00
}
2023-03-01 14:21:24 +00:00
2023-03-09 16:46:12 +00:00
func ( s * Strategy ) cancelDuplicatedPriceOpenOrders ( ctx context . Context , session * bbgo . ExchangeSession ) error {
2023-06-29 02:56:07 +00:00
openOrders , err := retry . QueryOpenOrdersUntilSuccessful ( ctx , session . Exchange , s . Symbol )
2023-03-09 16:46:12 +00:00
if err != nil {
return err
}
if len ( openOrders ) == 0 {
return nil
}
dupOrders := s . findDuplicatedPriceOpenOrders ( openOrders )
if len ( dupOrders ) > 0 {
s . debugOrders ( "DUPLICATED ORDERS" , dupOrders )
return session . Exchange . CancelOrders ( ctx , dupOrders ... )
}
s . logger . Infof ( "no duplicated order found" )
return nil
}
func ( s * Strategy ) findDuplicatedPriceOpenOrders ( openOrders [ ] types . Order ) ( dupOrders [ ] types . Order ) {
orderBook := bbgo . NewActiveOrderBook ( s . Symbol )
for _ , openOrder := range openOrders {
existingOrder := orderBook . Lookup ( func ( o types . Order ) bool {
return o . Price . Compare ( openOrder . Price ) == 0
} )
if existingOrder != nil {
// found duplicated order
// compare creation time and remove the latest created order
// if the creation time equals, then we can just cancel one of them
s . debugOrders (
fmt . Sprintf ( "found duplicated order at price %s, comparing orders" , openOrder . Price . String ( ) ) ,
[ ] types . Order { * existingOrder , openOrder } )
dupOrder := * existingOrder
if openOrder . CreationTime . After ( existingOrder . CreationTime . Time ( ) ) {
dupOrder = openOrder
} else if openOrder . CreationTime . Before ( existingOrder . CreationTime . Time ( ) ) {
// override the existing order and take the existing order as a duplicated one
orderBook . Add ( openOrder )
}
dupOrders = append ( dupOrders , dupOrder )
} else {
orderBook . Add ( openOrder )
}
2023-03-03 05:51:50 +00:00
}
2023-03-09 16:46:12 +00:00
return dupOrders
2023-03-09 09:42:37 +00:00
}
2023-03-10 05:00:13 +00:00
2023-03-13 13:27:13 +00:00
func ( s * Strategy ) newClientOrderID ( ) string {
if s . session != nil && s . session . ExchangeName == types . ExchangeMax {
return uuid . New ( ) . String ( )
}
return ""
}
2023-08-17 08:26:06 +00:00
2023-08-31 05:48:56 +00:00
func ( s * Strategy ) recoverActiveOrders ( ctx context . Context , session * bbgo . ExchangeSession ) {
2023-08-31 05:59:44 +00:00
s . logger . Infof ( "recovering active orders after websocket connect" )
2023-08-17 08:26:06 +00:00
grid := s . getGrid ( )
if grid == nil {
return
}
2023-08-31 05:48:56 +00:00
// this lock avoids recovering the active orders while the openGrid is executing
s . mu . Lock ( )
defer s . mu . Unlock ( )
2023-08-17 08:26:06 +00:00
// TODO: move this logics into the active maker orders component, like activeOrders.Sync(ctx)
activeOrderBook := s . orderExecutor . ActiveMakerOrders ( )
activeOrders := activeOrderBook . Orders ( )
2023-08-31 05:59:44 +00:00
if len ( activeOrders ) == 0 {
return
}
s . logger . Infof ( "found %d active orders to update..." , len ( activeOrders ) )
2023-09-25 09:43:00 +00:00
for i , o := range activeOrders {
s . logger . Infof ( "updating %d/%d order #%d..." , i + 1 , len ( activeOrders ) , o . OrderID )
2023-08-31 05:59:44 +00:00
2023-09-19 03:12:14 +00:00
updatedOrder , err := retry . QueryOrderUntilSuccessful ( ctx , s . orderQueryService , types . OrderQuery {
Symbol : o . Symbol ,
OrderID : strconv . FormatUint ( o . OrderID , 10 ) ,
2023-08-17 08:26:06 +00:00
} )
if err != nil {
s . logger . WithError ( err ) . Errorf ( "unable to query order" )
return
}
2023-09-25 09:43:00 +00:00
s . logger . Infof ( "triggering updated order #%d: %s" , o . OrderID , o . String ( ) )
2023-08-17 08:26:06 +00:00
activeOrderBook . Update ( * updatedOrder )
}
}