2022-11-02 10:26:30 +00:00
package grid2
import (
"context"
"fmt"
2022-12-25 17:35:37 +00:00
"sort"
2022-12-05 11:23:39 +00:00
"strconv"
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
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo"
"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 { } )
}
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
}
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
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" `
2022-12-05 11:37:42 +00:00
ResetPositionWhenStart bool ` json:"resetPositionWhenStart" `
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" `
2022-12-15 10:09:43 +00:00
SkipSpreadCheck bool ` json:"skipSpreadCheck" `
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
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
2022-12-05 11:23:39 +00:00
historicalTrades * bbgo . TradeStore
2022-11-10 12:58:46 +00:00
2022-11-02 10:26:30 +00:00
// groupID is the group ID used for the strategy instance for canceling orders
groupID uint32
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 ( )
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
}
2022-12-06 07:55:52 +00:00
if s . GridNum == 0 {
return fmt . Errorf ( "gridNum can not be zero" )
}
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
}
2022-12-07 04:29:14 +00:00
if ! s . QuantityOrAmount . IsSet ( ) && s . QuoteInvestment . IsZero ( ) {
return fmt . Errorf ( "either quantity, amount or quoteInvestment must be set" )
2022-11-02 10:26:30 +00:00
}
return nil
}
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..." )
2022-12-04 05:04:14 +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
if tq . Compare ( o . Quantity ) != 0 {
s . logger . Warnf ( "order trades missing. expected: %f actual: %f" ,
o . Quantity . Float64 ( ) ,
tq . Float64 ( ) )
return false
}
return true
}
2022-12-06 03:48:32 +00:00
// aggregateOrderBaseFee collects the base fee quantity from the given order
// it falls back to query the trades via the RESTful API when the websocket trades are not all received.
func ( s * Strategy ) aggregateOrderBaseFee ( o types . Order ) fixedpoint . Value {
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 {
s . logger . Infof ( "found filled order trades: %+v" , orderTrades )
}
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
fees := collectTradeFee ( orderTrades )
if fee , ok := fees [ s . Market . BaseCurrency ] ; ok {
return fee
}
return fixedpoint . Zero
}
// if we don't support orderQueryService, then we should just skip
2022-12-06 03:48:32 +00:00
if s . orderQueryService == nil {
2022-12-06 03:56:30 +00:00
return fixedpoint . Zero
2022-12-06 03:48:32 +00:00
}
s . logger . Warnf ( "missing order trades or missing trade fee, pulling order trades from API" )
// 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 {
s . logger . WithError ( err ) . Errorf ( "query order trades error" )
} else {
2022-12-06 03:56:30 +00:00
s . logger . Infof ( "fetched api trades: %+v" , apiOrderTrades )
2022-12-06 03:48:32 +00:00
orderTrades = apiOrderTrades
}
}
2022-12-06 03:56:30 +00:00
return fixedpoint . Zero
2022-12-06 03:48:32 +00:00
}
2022-12-15 06:57:25 +00:00
func ( s * Strategy ) processFilledOrder ( o types . Order ) {
2022-12-05 11:37:42 +00:00
// check order fee
newSide := types . SideTypeSell
newPrice := o . Price
newQuantity := o . Quantity
orderQuoteQuantity := o . Quantity . Mul ( o . Price )
2022-12-05 11:23:39 +00:00
// collect trades
2022-12-05 11:30:06 +00:00
baseSellQuantityReduction := fixedpoint . Zero
2022-12-05 11:23:39 +00:00
2022-12-05 11:37:42 +00:00
// baseSellQuantityReduction calculation should be only for BUY order
// 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
if o . Side == types . SideTypeBuy {
2022-12-06 03:48:32 +00:00
baseSellQuantityReduction = s . aggregateOrderBaseFee ( o )
2022-12-06 03:56:30 +00:00
2022-12-15 10:57:21 +00:00
s . logger . Infof ( "GRID BUY ORDER BASE FEE: %s %s" , baseSellQuantityReduction . String ( ) , s . Market . BaseCurrency )
2022-12-06 03:56:30 +00:00
2022-12-06 03:48:32 +00:00
newQuantity = newQuantity . Sub ( baseSellQuantityReduction )
2022-12-05 11:23:39 +00:00
}
2022-12-04 06:45:04 +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 {
2022-12-05 18:07:05 +00:00
newQuantity = fixedpoint . Max ( orderQuoteQuantity . Div ( newPrice ) , s . Market . MinQuantity )
2022-12-03 07:21:03 +00:00
}
2022-12-03 07:18:47 +00:00
2022-12-04 07:01:52 +00:00
profit := s . calculateProfit ( o , newPrice , newQuantity )
s . logger . Infof ( "GENERATED GRID PROFIT: %+v" , profit )
2022-12-04 07:24:13 +00:00
s . GridProfitStats . AddProfit ( profit )
2022-12-04 07:01:52 +00:00
2022-12-26 10:15:39 +00:00
s . EmitGridProfit ( s . GridProfitStats , profit )
2022-12-15 10:47:45 +00:00
bbgo . Notify ( profit )
bbgo . Notify ( s . GridProfitStats )
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
if s . EarnBase {
2022-12-05 18:07:05 +00:00
newQuantity = fixedpoint . Max ( orderQuoteQuantity . Div ( newPrice ) . Sub ( baseSellQuantityReduction ) , s . Market . MinQuantity )
2022-12-03 08:40:40 +00:00
}
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 {
Symbol : s . Symbol ,
Market : s . Market ,
Type : types . OrderTypeLimit ,
Price : newPrice ,
Side : newSide ,
TimeInForce : types . TimeInForceGTC ,
Quantity : newQuantity ,
2022-12-07 08:30:51 +00:00
Tag : orderTag ,
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
2022-12-03 06:46:05 +00:00
if createdOrders , err := s . orderExecutor . SubmitOrders ( context . Background ( ) , orderForm ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "can not submit arbitrage order" )
} else {
2022-12-17 03:57:32 +00:00
s . logger . Infof ( "GRID REVERSE ORDER IS CREATED: %+v" , createdOrders )
2022-12-03 06:46:05 +00:00
}
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
}
s . logger . Infof ( "GRID ORDER FILLED: %s" , o . String ( ) )
s . processFilledOrder ( o )
}
2022-11-17 09:40:59 +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
}
2022-11-17 09:40:59 +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
}
2022-11-27 11:11:45 +00:00
func ( s * Strategy ) calculateQuoteInvestmentQuantity ( quoteInvestment , lastPrice fixedpoint . Value , pins [ ] Pin ) ( fixedpoint . Value , error ) {
// quoteInvestment = (p1 * q) + (p2 * q) + (p3 * q) + ....
// =>
// quoteInvestment = (p1 + p2 + p3) * q
// q = quoteInvestment / (p1 + p2 + p3)
totalQuotePrice := fixedpoint . Zero
2022-12-05 17:51:50 +00:00
si := - 1
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
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 )
} else if i > 0 { // 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 )
}
} 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
}
totalQuotePrice = totalQuotePrice . Add ( price )
}
}
return quoteInvestment . Div ( totalQuotePrice ) , nil
}
2022-11-30 04:46:39 +00:00
func ( s * Strategy ) calculateQuoteBaseInvestmentQuantity ( quoteInvestment , baseInvestment , lastPrice fixedpoint . Value , pins [ ] Pin ) ( fixedpoint . Value , error ) {
2022-12-03 03:31:44 +00:00
s . logger . Infof ( "calculating quantity by quote/base 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
numberOfSellOrders := 0
for i := len ( pins ) - 1 ; i >= 0 ; i -- {
pin := pins [ i ]
price := fixedpoint . Value ( pin )
2022-12-05 17:19:24 +00:00
sellPrice := price
if s . ProfitSpread . Sign ( ) > 0 {
sellPrice = sellPrice . Add ( s . ProfitSpread )
}
if sellPrice . Compare ( lastPrice ) < 0 {
2022-11-30 04:46:39 +00:00
break
}
2022-12-05 17:21:41 +00:00
2022-11-30 04:46:39 +00:00
numberOfSellOrders ++
}
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.
2022-11-30 04:57:53 +00:00
maxNumberOfSellOrders := numberOfSellOrders + 1
2022-11-30 04:55:23 +00:00
minBaseQuantity := fixedpoint . Max ( s . Market . MinNotional . Div ( lastPrice ) , s . Market . MinQuantity )
2022-11-30 04:46:39 +00:00
maxBaseQuantity := fixedpoint . Zero
2022-12-01 06:51:28 +00:00
for maxBaseQuantity . Compare ( s . Market . MinQuantity ) <= 0 || maxBaseQuantity . Compare ( minBaseQuantity ) <= 0 {
2022-11-30 04:57:53 +00:00
maxNumberOfSellOrders --
maxBaseQuantity = baseInvestment . Div ( fixedpoint . NewFromInt ( int64 ( maxNumberOfSellOrders ) ) )
2022-11-30 04:46:39 +00:00
}
2022-12-03 06:58:53 +00:00
s . logger . Infof ( "grid base investment sell orders: %d" , maxNumberOfSellOrders )
2022-12-03 03:25:18 +00:00
if maxNumberOfSellOrders > 0 {
2022-12-03 06:58:53 +00:00
s . logger . Infof ( "grid base investment quantity range: %f <=> %f" , minBaseQuantity . Float64 ( ) , maxBaseQuantity . Float64 ( ) )
2022-12-03 03:25:18 +00:00
}
2022-11-30 04:46:39 +00:00
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
2022-12-05 17:57:33 +00:00
for i := len ( pins ) - 1 - maxNumberOfSellOrders ; 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
}
totalQuotePrice = totalQuotePrice . Add ( price )
}
}
2022-12-01 06:51:28 +00:00
quoteSideQuantity := quoteInvestment . Div ( totalQuotePrice )
2022-12-03 03:25:18 +00:00
if maxNumberOfSellOrders > 0 {
2022-12-07 03:44:22 +00:00
return fixedpoint . Min ( quoteSideQuantity , maxBaseQuantity ) , nil
2022-12-03 03:25:18 +00:00
}
return quoteSideQuantity , 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 ) {
s . handleOrderFilled ( o )
bbgo . Sync ( ctx , s )
}
}
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 ( ) )
2022-12-04 10:01:58 +00:00
if err := s . closeGrid ( ctx ) ; err != nil {
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 ( ) )
if err := s . closeGrid ( ctx ) ; err != nil {
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
}
} )
}
2022-12-04 05:04:14 +00:00
// closeGrid closes the grid orders
func ( s * Strategy ) closeGrid ( ctx context . Context ) error {
2022-12-04 04:58:01 +00:00
bbgo . Sync ( ctx , s )
// now we can cancel the open orders
s . logger . Infof ( "canceling grid orders..." )
if err := s . orderExecutor . GracefulCancel ( ctx ) ; err != nil {
return err
}
2022-12-04 06:23:00 +00:00
// free the grid object
s . grid = nil
2022-12-26 10:15:39 +00:00
s . EmitGridClosed ( )
2022-12-04 04:58:01 +00:00
return nil
}
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
2022-12-04 06:22:11 +00:00
if s . grid != nil {
return nil
}
2022-12-07 06:19:49 +00:00
s . grid = s . newGrid ( )
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 {
return errors . Wrap ( err , "failed to get the last trade price" )
}
2022-11-10 15:34:55 +00:00
// check if base and quote are enough
baseBalance , ok := session . Account . Balance ( s . Market . BaseCurrency )
if ! ok {
return fmt . Errorf ( "base %s balance not found" , s . Market . BaseCurrency )
}
quoteBalance , ok := session . Account . Balance ( s . Market . QuoteCurrency )
if ! ok {
return fmt . Errorf ( "quote %s balance not found" , s . Market . QuoteCurrency )
}
totalBase := baseBalance . Available
totalQuote := quoteBalance . Available
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 {
return err2
}
}
if amount := s . QuantityOrAmount . Amount ; ! amount . IsZero ( ) {
if _ , _ , err2 := s . checkRequiredInvestmentByAmount ( totalBase , totalQuote , lastPrice , amount , s . grid . Pins ) ; err != nil {
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
2022-11-30 04:46:39 +00:00
if ! s . QuoteInvestment . IsZero ( ) && ! s . BaseInvestment . IsZero ( ) {
quantity , err2 := s . calculateQuoteBaseInvestmentQuantity ( s . QuoteInvestment , s . BaseInvestment , lastPrice , s . grid . Pins )
if err2 != nil {
return err2
}
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 {
return err2
}
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 {
return fmt . Errorf ( "baseInvestment setup %f is greater than the total base balance %f" , s . BaseInvestment . Float64 ( ) , totalBase . Float64 ( ) )
}
if s . QuoteInvestment . Compare ( totalQuote ) > 0 {
return fmt . Errorf ( "quoteInvestment setup %f is greater than the total quote balance %f" , s . QuoteInvestment . Float64 ( ) , totalQuote . Float64 ( ) )
}
if ! s . QuantityOrAmount . IsSet ( ) {
// TODO: calculate and override the quantity here
}
}
2022-12-04 16:20:18 +00:00
submitOrders , err := s . generateGridOrders ( totalQuote , totalBase , lastPrice )
if err != nil {
return err
}
2022-12-15 10:30:28 +00:00
s . debugGridOrders ( submitOrders , lastPrice )
2022-12-04 16:20:18 +00:00
createdOrders , err2 := s . orderExecutor . SubmitOrders ( ctx , submitOrders ... )
if err2 != nil {
return err
}
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" )
2022-12-26 10:15:39 +00:00
s . EmitGridReady ( )
2022-12-06 08:35:52 +00:00
return nil
}
func ( s * Strategy ) debugGridOrders ( submitOrders [ ] types . SubmitOrder , lastPrice fixedpoint . Value ) {
2022-12-04 16:20:18 +00:00
s . logger . Infof ( "GRID ORDERS: [" )
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 {
2022-12-05 11:43:58 +00:00
s . logger . Infof ( " - LAST PRICE: %f" , lastPrice . Float64 ( ) )
2022-12-05 11:42:36 +00:00
}
2022-12-04 16:20:18 +00:00
s . logger . Info ( " - " , order . String ( ) )
}
s . logger . Infof ( "] END OF GRID ORDERS" )
}
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
var si = len ( pins ) - 1
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
2022-11-24 07:55:02 +00:00
// TODO: add fee if we don't have the platform token. BNB, OKB or MAX...
2022-11-10 12:58:46 +00:00
if price . Compare ( lastPrice ) >= 0 {
2022-12-04 16:20:18 +00:00
si = i
2022-11-24 07:55:02 +00:00
if usedBase . Add ( quantity ) . Compare ( totalBase ) < 0 {
submitOrders = append ( submitOrders , types . SubmitOrder {
2022-11-24 08:35:31 +00:00
Symbol : s . Symbol ,
2022-12-03 06:46:05 +00:00
Type : types . OrderTypeLimit ,
2022-11-24 08:35:31 +00:00
Side : types . SideTypeSell ,
2022-12-05 17:17:29 +00:00
Price : sellPrice ,
2022-11-24 08:35:31 +00:00
Quantity : quantity ,
Market : s . Market ,
TimeInForce : types . TimeInForceGTC ,
2022-12-07 08:32:28 +00:00
Tag : orderTag ,
2022-11-24 07:55:02 +00:00
} )
usedBase = usedBase . Add ( quantity )
} else if i > 0 {
// 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 {
2022-11-24 08:35:31 +00:00
Symbol : s . Symbol ,
2022-12-03 06:46:05 +00:00
Type : types . OrderTypeLimit ,
2022-11-24 08:35:31 +00:00
Side : types . SideTypeBuy ,
Price : nextPrice ,
Quantity : quantity ,
Market : s . Market ,
TimeInForce : types . TimeInForceGTC ,
2022-12-07 08:32:28 +00:00
Tag : orderTag ,
2022-11-24 07:55:02 +00:00
} )
quoteQuantity := quantity . Mul ( price )
usedQuote = usedQuote . Add ( quoteQuantity )
2022-12-04 16:20:18 +00:00
} else if i == 0 {
// skip i == 0
2022-11-10 12:58:46 +00:00
}
2022-11-24 07:55:02 +00:00
} else {
2022-12-05 17:17:29 +00:00
if s . ProfitSpread . IsZero ( ) && i + 1 == si {
2022-12-04 16:20:18 +00:00
continue
}
2022-11-24 08:35:31 +00:00
submitOrders = append ( submitOrders , types . SubmitOrder {
2022-11-24 07:55:02 +00:00
Symbol : s . Symbol ,
2022-12-03 06:46:05 +00:00
Type : types . OrderTypeLimit ,
2022-11-24 07:55:02 +00:00
Side : types . SideTypeBuy ,
Price : price ,
2022-11-24 08:35:31 +00:00
Quantity : quantity ,
2022-11-24 07:55:02 +00:00
Market : s . Market ,
TimeInForce : types . TimeInForceGTC ,
2022-12-07 08:30:51 +00:00
Tag : orderTag ,
2022-11-24 07:55:02 +00:00
} )
2022-11-24 08:35:31 +00:00
quoteQuantity := quantity . Mul ( price )
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
openOrders , err := session . Exchange . QueryOpenOrders ( ctx , s . Symbol )
if err != nil {
return err
}
err = session . Exchange . CancelOrders ( ctx , openOrders ... )
if err != nil {
return err
}
return nil
}
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
2022-12-06 08:09:46 +00:00
func calculateMinimalQuoteInvestment ( market types . Market , lowerPrice , upperPrice fixedpoint . Value , gridNum int64 ) fixedpoint . Value {
num := fixedpoint . NewFromInt ( gridNum )
minimalAmountLowerPrice := fixedpoint . Max ( lowerPrice . Mul ( market . MinQuantity ) , market . MinNotional )
minimalAmountUpperPrice := fixedpoint . Max ( upperPrice . Mul ( market . MinQuantity ) , market . MinNotional )
return fixedpoint . Max ( minimalAmountLowerPrice , minimalAmountUpperPrice ) . Mul ( num )
}
2022-12-06 07:55:52 +00:00
func ( s * Strategy ) checkMinimalQuoteInvestment ( ) error {
2022-12-06 08:09:46 +00:00
minimalQuoteInvestment := calculateMinimalQuoteInvestment ( s . Market , s . LowerPrice , s . UpperPrice , s . GridNum )
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
}
2022-12-24 06:48:30 +00:00
func ( s * Strategy ) recoverGrid ( ctx context . Context , historyService types . ExchangeTradeHistoryService , openOrders [ ] types . Order ) error {
grid := s . newGrid ( )
// Add all open orders to the local order book
gridPriceMap := buildGridPriceMap ( grid )
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
} else {
if oid , ok := findEarliestOrderID ( openOrders ) ; ok {
lastOrderID = oid
}
2022-12-23 16:54:40 +00:00
}
activeOrderBook := s . orderExecutor . ActiveMakerOrders ( )
2022-12-20 09:33:53 +00:00
// Allocate a local order book
orderBook := bbgo . NewActiveOrderBook ( s . Symbol )
// Ensure that orders are grid orders
// The price must be at the grid pin
for _ , openOrder := range openOrders {
if _ , exists := gridPriceMap [ openOrder . Price . String ( ) ] ; exists {
orderBook . Add ( openOrder )
2022-12-23 16:54:40 +00:00
// put the order back to the active order book so that we can receive order update
activeOrderBook . Add ( openOrder )
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
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" )
return nil
2022-12-24 09:08:50 +00:00
} else {
// 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
}
2022-12-24 06:52:01 +00:00
debugGrid ( grid , orderBook )
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" )
return nil
}
// for reverse order recovering, we need the orders to be sort by update time ascending-ly
types . SortOrdersUpdateTimeAscending ( tmpOrders )
if len ( tmpOrders ) > 1 && len ( tmpOrders ) == int ( s . GridNum ) + 1 {
// remove the latest updated order because it's near the empty slot
tmpOrders = tmpOrders [ : len ( tmpOrders ) - 1 ]
}
// 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
s . logger . Infof ( "GRID RECOVER: found %d filled grid orders" , len ( filledOrders ) )
s . grid = grid
for _ , o := range filledOrders {
s . processFilledOrder ( o )
}
s . logger . Infof ( "GRID RECOVER COMPLETE" )
2022-12-24 06:52:01 +00:00
debugGrid ( grid , s . orderExecutor . ActiveMakerOrders ( ) )
2022-12-24 06:48:30 +00:00
return nil
}
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.
2022-12-24 06:48:30 +00:00
func ( s * Strategy ) replayOrderHistory ( ctx context . Context , grid * Grid , orderBook * bbgo . ActiveOrderBook , historyService types . ExchangeTradeHistoryService , startTime , endTime time . Time , lastOrderID uint64 ) error {
gridPriceMap := buildGridPriceMap ( grid )
2022-12-20 07:56:38 +00:00
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
if _ , ok := gridPriceMap [ closedOrder . Price . String ( ) ] ; ! ok {
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
}
2022-12-23 16:54:40 +00:00
func isCompleteGridOrderBook ( orderBook * bbgo . ActiveOrderBook , gridNum int64 ) bool {
tmpOrders := orderBook . Orders ( )
2022-12-24 07:58:02 +00:00
if len ( tmpOrders ) == int ( gridNum ) && types . OrdersAll ( tmpOrders , types . IsActiveOrder ) {
2022-12-23 16:54:40 +00:00
return true
}
return false
}
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
}
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 ( )
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
}
2022-12-04 04:58:01 +00:00
s . logger = log . WithFields ( logrus . Fields {
"symbol" : s . Symbol ,
} )
s . groupID = util . FNV32 ( instanceID )
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 } )
s . UpperPrice = fixedpoint . NewFromFloat ( pivotHigh . Last ( ) )
s . LowerPrice = fixedpoint . NewFromFloat ( pivotLow . Last ( ) )
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 )
}
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 {
if err := s . checkMinimalQuoteInvestment ( ) ; err != nil {
return err
}
}
2022-12-05 11:23:39 +00:00
s . historicalTrades = bbgo . 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 ) )
2022-12-15 10:42:25 +00:00
2022-12-07 06:19:49 +00:00
s . orderExecutor = orderExecutor
2022-12-04 04:58:01 +00:00
2022-12-05 10:11:44 +00:00
// TODO: detect if there are previous grid orders on the order book
2022-12-04 05:04:14 +00:00
if s . ClearOpenOrdersWhenStart {
if err := s . clearOpenOrders ( ctx , session ) ; err != nil {
return err
}
}
2022-12-23 08:48:40 +00:00
openOrders , err := session . Exchange . QueryOpenOrders ( ctx , s . Symbol )
if err != nil {
return err
}
2022-12-23 11:15:46 +00:00
if s . RecoverOrdersWhenStart && len ( openOrders ) > 0 {
2022-12-23 09:54:30 +00:00
s . logger . Infof ( "recoverWhenStart is set, found %d open orders, trying to recover grid orders..." , len ( openOrders ) )
2022-12-23 08:48:40 +00:00
historyService , implemented := session . Exchange . ( types . ExchangeTradeHistoryService )
2022-12-23 09:54:30 +00:00
if ! implemented {
s . logger . Warn ( "ExchangeTradeHistoryService is not implemented, can not recover grid" )
} else {
2022-12-23 08:48:40 +00:00
if err := s . recoverGrid ( ctx , historyService , openOrders ) ; err != nil {
2022-12-23 09:54:30 +00:00
return errors . Wrap ( err , "recover grid error" )
2022-12-23 08:48:40 +00:00
}
}
}
2022-12-04 04:58:01 +00:00
bbgo . OnShutdown ( ctx , func ( ctx context . Context , wg * sync . WaitGroup ) {
defer wg . Done ( )
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
}
2022-12-04 05:04:14 +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 ) )
}
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 ( ) {
session . UserDataStream . OnStart ( func ( ) {
2022-12-04 10:27:21 +00:00
if err := s . openGrid ( ctx , session ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "failed to setup grid orders" )
}
2022-12-25 17:35:37 +00:00
} )
}
2022-12-04 04:58:01 +00:00
return nil
}