2022-11-02 10:26:30 +00:00
package grid2
import (
"context"
"fmt"
"sync"
"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"
var log = logrus . WithField ( "strategy" , ID )
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-11-14 08:28:07 +00:00
type GridProfitStats struct {
TotalProfit fixedpoint . Value ` json:"totalProfit" `
FloatProfit fixedpoint . Value ` json:"floatProfit" `
GridProfit fixedpoint . Value ` json:"gridProfit" `
ArbitrageCount int ` json:"arbitrageCount" `
2022-11-14 08:28:42 +00:00
TotalFee fixedpoint . Value ` json:"totalFee" `
Volume fixedpoint . Value ` json:"volume" `
2022-11-14 08:28:07 +00:00
}
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-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-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-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" `
grid * Grid
2022-11-02 10:26:30 +00:00
ProfitStats * types . ProfitStats ` persistence:"profit_stats" `
Position * types . Position ` persistence:"position" `
// orderStore is used to store all the created orders, so that we can filter the trades.
orderStore * bbgo . OrderStore
// activeOrders is the locally maintained active order book of the maker orders.
activeOrders * bbgo . ActiveOrderBook
tradeCollector * bbgo . TradeCollector
2022-11-10 12:58:46 +00:00
orderExecutor * bbgo . GeneralOrderExecutor
2022-11-02 10:26:30 +00:00
// groupID is the group ID used for the strategy instance for canceling orders
groupID uint32
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) Validate ( ) error {
if s . UpperPrice . IsZero ( ) {
return errors . New ( "upperPrice can not be zero, you forgot to set?" )
}
if s . LowerPrice . IsZero ( ) {
return errors . New ( "lowerPrice can not be zero, you forgot to set?" )
}
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 ( ) )
}
if s . ProfitSpread . Sign ( ) <= 0 {
// If profitSpread is empty or its value is negative
return fmt . Errorf ( "profit spread should bigger than 0" )
}
if s . GridNum == 0 {
return fmt . Errorf ( "gridNum can not be zero" )
}
if err := s . QuantityOrAmount . Validate ( ) ; err != nil {
return err
}
return nil
}
func ( s * Strategy ) Subscribe ( session * bbgo . ExchangeSession ) {
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : "1m" } )
}
// InstanceID returns the instance identifier from the current grid configuration parameters
func ( s * Strategy ) InstanceID ( ) string {
return fmt . Sprintf ( "%s-%s-%d-%d-%d" , ID , s . Symbol , s . GridNum , s . UpperPrice . Int ( ) , s . LowerPrice . Int ( ) )
}
func ( s * Strategy ) handleOrderFilled ( o types . Order ) {
}
func ( s * Strategy ) Run ( ctx context . Context , orderExecutor bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
instanceID := s . InstanceID ( )
s . groupID = util . FNV32 ( instanceID )
log . Infof ( "using group id %d from fnv(%s)" , s . groupID , instanceID )
if s . ProfitStats == nil {
s . ProfitStats = types . NewProfitStats ( s . Market )
}
if s . Position == nil {
s . Position = types . NewPositionFromMarket ( s . Market )
}
2022-11-10 12:58:46 +00:00
s . orderExecutor = bbgo . NewGeneralOrderExecutor ( session , s . Symbol , ID , instanceID , s . Position )
s . orderExecutor . BindEnvironment ( s . Environment )
s . orderExecutor . BindProfitStats ( s . ProfitStats )
s . orderExecutor . Bind ( )
s . orderExecutor . TradeCollector ( ) . OnPositionUpdate ( func ( position * types . Position ) {
bbgo . Sync ( ctx , s )
2022-11-02 10:26:30 +00:00
} )
2022-11-10 12:58:46 +00:00
s . grid = NewGrid ( s . LowerPrice , s . UpperPrice , fixedpoint . NewFromInt ( s . GridNum ) , s . Market . TickSize )
s . grid . CalculateArithmeticPins ( )
2022-11-02 10:26:30 +00:00
bbgo . OnShutdown ( ctx , func ( ctx context . Context , wg * sync . WaitGroup ) {
defer wg . Done ( )
bbgo . Sync ( ctx , s )
// now we can cancel the open orders
log . Infof ( "canceling active orders..." )
if err := session . Exchange . CancelOrders ( context . Background ( ) , s . activeOrders . Orders ( ) ... ) ; err != nil {
log . WithError ( err ) . Errorf ( "cancel order error" )
}
} )
session . UserDataStream . OnStart ( func ( ) {
2022-11-10 12:58:46 +00:00
if err := s . setupGridOrders ( ctx , session ) ; err != nil {
log . WithError ( err ) . Errorf ( "failed to setup grid orders" )
}
2022-11-02 10:26:30 +00:00
} )
return nil
}
2022-11-10 12:58:46 +00:00
2022-11-15 07:29:30 +00:00
type InvestmentBudget struct {
baseInvestment fixedpoint . Value
quoteInvestment fixedpoint . Value
baseBalance fixedpoint . Value
quoteBalance fixedpoint . Value
}
2022-11-16 04:09:55 +00:00
func ( s * Strategy ) checkRequiredInvestmentByQuantity ( baseInvestment , quoteInvestment , baseBalance , quoteBalance , quantity , lastPrice fixedpoint . Value , pins [ ] Pin ) ( requiredBase , requiredQuote fixedpoint . Value , err error ) {
2022-11-15 07:29:30 +00:00
if baseInvestment . Compare ( baseBalance ) > 0 {
2022-11-16 04:09:55 +00:00
return fixedpoint . Zero , fixedpoint . Zero , fmt . Errorf ( "baseInvestment setup %f is greater than the total base balance %f" , baseInvestment . Float64 ( ) , baseBalance . Float64 ( ) )
2022-11-15 07:29:30 +00:00
}
if quoteInvestment . Compare ( quoteBalance ) > 0 {
2022-11-16 04:09:55 +00:00
return fixedpoint . Zero , fixedpoint . Zero , fmt . Errorf ( "quoteInvestment setup %f is greater than the total quote balance %f" , quoteInvestment . Float64 ( ) , quoteBalance . Float64 ( ) )
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
buyPlacedPrice := fixedpoint . Zero
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 {
// 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-11-16 04:09:55 +00:00
nextLowerPin := pins [ i - 1 ]
2022-11-14 09:37:32 +00:00
nextLowerPrice := fixedpoint . Value ( nextLowerPin )
requiredQuote = requiredQuote . Add ( quantity . Mul ( nextLowerPrice ) )
2022-11-16 04:09:55 +00:00
buyPlacedPrice = nextLowerPrice
2022-11-14 09:37:32 +00:00
}
} else {
2022-11-15 07:29:30 +00:00
// for orders that buy
2022-11-16 04:09:55 +00:00
if price . Compare ( buyPlacedPrice ) == 0 {
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
}
func ( s * Strategy ) checkRequiredInvestmentByAmount ( baseInvestment , quoteInvestment , baseBalance , quoteBalance , amount , lastPrice fixedpoint . Value , pins [ ] Pin ) ( requiredBase , requiredQuote fixedpoint . Value , err error ) {
if baseInvestment . Compare ( baseBalance ) > 0 {
return fixedpoint . Zero , fixedpoint . Zero , fmt . Errorf ( "baseInvestment setup %f is greater than the total base balance %f" , baseInvestment . Float64 ( ) , baseBalance . Float64 ( ) )
}
if quoteInvestment . Compare ( quoteBalance ) > 0 {
return fixedpoint . Zero , fixedpoint . Zero , fmt . Errorf ( "quoteInvestment setup %f is greater than the total quote balance %f" , quoteInvestment . Float64 ( ) , quoteBalance . Float64 ( ) )
}
// 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
buyPlacedPrice := fixedpoint . Zero
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 {
// 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
nextLowerPin := pins [ i - 1 ]
nextLowerPrice := fixedpoint . Value ( nextLowerPin )
requiredQuote = requiredQuote . Add ( quantity . Mul ( nextLowerPrice ) )
buyPlacedPrice = nextLowerPrice
}
} else {
// for orders that buy
if price . Compare ( buyPlacedPrice ) == 0 {
continue
}
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-10 12:58:46 +00:00
func ( s * Strategy ) setupGridOrders ( ctx context . Context , session * bbgo . ExchangeSession ) error {
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.
quantityOrAmountIsSet := s . QuantityOrAmount . IsSet ( )
2022-11-10 15:34:55 +00:00
if quantityOrAmountIsSet {
2022-11-16 04:09:55 +00:00
if _ , _ , err2 := s . checkRequiredInvestmentByQuantity (
2022-11-14 09:37:32 +00:00
s . BaseInvestment , s . QuoteInvestment ,
totalBase , totalQuote ,
lastPrice , s . QuantityOrAmount . Quantity , s . grid . Pins ) ; err != nil {
return err2
2022-11-10 15:34:55 +00:00
}
}
2022-11-10 12:58:46 +00:00
for i := len ( s . grid . Pins ) - 2 ; i >= 0 ; i ++ {
pin := s . grid . Pins [ i ]
price := fixedpoint . Value ( pin )
if price . Compare ( lastPrice ) >= 0 {
2022-11-10 15:34:55 +00:00
// check sell order
if quantityOrAmountIsSet {
if s . QuantityOrAmount . Quantity . Sign ( ) > 0 {
quantity := s . QuantityOrAmount . Quantity
createdOrders , err2 := s . orderExecutor . SubmitOrders ( ctx , types . SubmitOrder {
Symbol : s . Symbol ,
Side : types . SideTypeBuy ,
Type : types . OrderTypeLimit ,
Quantity : quantity ,
Price : price ,
Market : s . Market ,
TimeInForce : types . TimeInForceGTC ,
Tag : "grid" ,
} )
if err2 != nil {
return err2
}
_ = createdOrders
} else if s . QuantityOrAmount . Amount . Sign ( ) > 0 {
2022-11-10 12:58:46 +00:00
2022-11-10 15:34:55 +00:00
}
} else if s . BaseInvestment . Sign ( ) > 0 {
2022-11-10 12:58:46 +00:00
2022-11-10 15:34:55 +00:00
} else {
// error: either quantity, amount, baseInvestment is not set.
2022-11-10 12:58:46 +00:00
}
}
}
return nil
}
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 )
}