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) var notionalModifier = fixedpoint.NewFromFloat(1.0001) 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{}) } type Strategy struct { // 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. types.Market `json:"-" yaml:"-"` // These fields will be filled from the config file (it translates YAML to JSON) Symbol string `json:"symbol" yaml:"symbol"` // ProfitSpread is the fixed profit spread you want to submit the sell order ProfitSpread fixedpoint.Value `json:"profitSpread" yaml:"profitSpread"` // GridNum is the grid number, how many orders you want to post on the orderbook. GridNum int64 `json:"gridNumber" yaml:"gridNumber"` UpperPrice fixedpoint.Value `json:"upperPrice" yaml:"upperPrice"` LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"` bbgo.QuantityOrAmount 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 // 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) } s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) // we don't persist orders so that we can not clear the previous orders for now. just need time to support this. s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) s.activeOrders.OnFilled(s.handleOrderFilled) s.activeOrders.BindStream(session.UserDataStream) s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.Position, s.orderStore) s.tradeCollector.OnTrade(func(trade types.Trade, profit, netProfit fixedpoint.Value) { bbgo.Notify(trade) s.ProfitStats.AddTrade(trade) }) s.tradeCollector.OnPositionUpdate(func(position *types.Position) { bbgo.Notify(position) }) s.tradeCollector.BindStream(session.UserDataStream) 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() { }) return nil }