diff --git a/pkg/bbgo/interact.go b/pkg/bbgo/interact.go index cf52a9110..1648e7df6 100644 --- a/pkg/bbgo/interact.go +++ b/pkg/bbgo/interact.go @@ -21,24 +21,6 @@ type PositionReader interface { CurrentPosition() *types.Position } -type StrategyStatusProvider interface { - GetStatus() types.StrategyStatus -} - -type Suspender interface { - Suspend(ctx context.Context) error - Resume(ctx context.Context) error -} - -type StrategyController interface { - StrategyStatusProvider - Suspender -} - -type EmergencyStopper interface { - EmergencyStop(ctx context.Context) error -} - type closePositionContext struct { signature string closer PositionCloser @@ -61,12 +43,21 @@ func NewCoreInteraction(environment *Environment, trader *Trader) *CoreInteracti } } -func (it *CoreInteraction) FilterStrategyByInterface(checkInterface interface{}) (strategies []string, found bool) { +func getStrategySignatures(exchangeStrategies map[string]SingleExchangeStrategy) []string { + var strategies []string + for signature := range exchangeStrategies { + strategies = append(strategies, signature) + } + + return strategies +} + +func filterStrategyByInterface(checkInterface interface{}, exchangeStrategies map[string]SingleExchangeStrategy) (strategies map[string]SingleExchangeStrategy, found bool) { found = false rt := reflect.TypeOf(checkInterface).Elem() - for signature, strategy := range it.exchangeStrategies { + for signature, strategy := range exchangeStrategies { if ok := reflect.TypeOf(strategy).Implements(rt); ok { - strategies = append(strategies, signature) + strategies[signature] = strategy found = true } } @@ -74,7 +65,7 @@ func (it *CoreInteraction) FilterStrategyByInterface(checkInterface interface{}) return strategies, found } -func GenerateStrategyButtonsForm(strategies []string) [][3]string { +func generateStrategyButtonsForm(strategies []string) [][3]string { var buttonsForm [][3]string for _, strategy := range strategies { buttonsForm = append(buttonsForm, [3]string{strategy, "strategy", strategy}) @@ -130,11 +121,11 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { i.PrivateCommand("/position", "Show Position", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := it.FilterStrategyByInterface((*PositionReader)(nil)); found { - reply.AddMultipleButtons(GenerateStrategyButtonsForm(strategies)) + if strategies, found := filterStrategyByInterface((*PositionReader)(nil), it.exchangeStrategies); found { + reply.AddMultipleButtons(generateStrategyButtonsForm(getStrategySignatures(strategies))) reply.Message("Please choose one strategy") } else { - reply.Message("No any strategy supports PositionReader") + reply.Message("No strategy supports PositionReader") } return nil }).Cycle(func(signature string, reply interact.Reply) error { @@ -171,11 +162,11 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { i.PrivateCommand("/closeposition", "Close position", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := it.FilterStrategyByInterface((*PositionCloser)(nil)); found { - reply.AddMultipleButtons(GenerateStrategyButtonsForm(strategies)) + if strategies, found := filterStrategyByInterface((*PositionCloser)(nil), it.exchangeStrategies); found { + reply.AddMultipleButtons(generateStrategyButtonsForm(getStrategySignatures(strategies))) reply.Message("Please choose one strategy") } else { - reply.Message("No any strategy supports PositionCloser") + reply.Message("No strategy supports PositionCloser") } return nil }).Next(func(signature string, reply interact.Reply) error { @@ -240,11 +231,11 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { i.PrivateCommand("/status", "Strategy Status", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := it.FilterStrategyByInterface((*StrategyStatusProvider)(nil)); found { - reply.AddMultipleButtons(GenerateStrategyButtonsForm(strategies)) - reply.Message("Please choose one strategy") + if strategies, found := filterStrategyByInterface((*StrategyStatusReader)(nil), it.exchangeStrategies); found { + reply.AddMultipleButtons(generateStrategyButtonsForm(getStrategySignatures(strategies))) + reply.Message("Please choose a strategy") } else { - reply.Message("No any strategy supports StrategyStatusProvider") + reply.Message("No strategy supports StrategyStatusReader") } return nil }).Next(func(signature string, reply interact.Reply) error { @@ -254,10 +245,10 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { return fmt.Errorf("strategy %s not found", signature) } - controller, implemented := strategy.(StrategyStatusProvider) + controller, implemented := strategy.(StrategyStatusReader) if !implemented { - reply.Message(fmt.Sprintf("Strategy %s does not support strategy status provider", signature)) - return fmt.Errorf("strategy %s does not implement StrategyStatusProvider interface", signature) + reply.Message(fmt.Sprintf("Strategy %s does not support StrategyStatusReader", signature)) + return fmt.Errorf("strategy %s does not implement StrategyStatusReader", signature) } status := controller.GetStatus() @@ -278,11 +269,11 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { i.PrivateCommand("/suspend", "Suspend Strategy", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := it.FilterStrategyByInterface((*StrategyController)(nil)); found { - reply.AddMultipleButtons(GenerateStrategyButtonsForm(strategies)) + if strategies, found := filterStrategyByInterface((*StrategyToggler)(nil), it.exchangeStrategies); found { + reply.AddMultipleButtons(generateStrategyButtonsForm(getStrategySignatures(strategies))) reply.Message("Please choose one strategy") } else { - reply.Message("No any strategy supports StrategyController") + reply.Message("No strategy supports StrategyToggler") } return nil }).Next(func(signature string, reply interact.Reply) error { @@ -292,26 +283,23 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { return fmt.Errorf("strategy %s not found", signature) } - controller, implemented := strategy.(StrategyController) + controller, implemented := strategy.(StrategyToggler) if !implemented { - reply.Message(fmt.Sprintf("Strategy %s does not support strategy suspend", signature)) - return fmt.Errorf("strategy %s does not implement StrategyController interface", signature) + reply.Message(fmt.Sprintf("Strategy %s does not support StrategyToggler", signature)) + return fmt.Errorf("strategy %s does not implement StrategyToggler", signature) } // Check strategy status before suspend - status := controller.GetStatus() - if status != types.StrategyStatusRunning { + if controller.GetStatus() != types.StrategyStatusRunning { reply.Message(fmt.Sprintf("Strategy %s is not running.", signature)) return nil } - err := controller.Suspend(context.Background()) - if kc, ok := reply.(interact.KeyboardController); ok { kc.RemoveKeyboard() } - if err != nil { + if err := controller.Suspend(); err != nil { reply.Message(fmt.Sprintf("Failed to suspend the strategy, %s", err.Error())) return err } @@ -323,11 +311,11 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { i.PrivateCommand("/resume", "Resume Strategy", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := it.FilterStrategyByInterface((*StrategyController)(nil)); found { - reply.AddMultipleButtons(GenerateStrategyButtonsForm(strategies)) + if strategies, found := filterStrategyByInterface((*StrategyToggler)(nil), it.exchangeStrategies); found { + reply.AddMultipleButtons(generateStrategyButtonsForm(getStrategySignatures(strategies))) reply.Message("Please choose one strategy") } else { - reply.Message("No any strategy supports StrategyController") + reply.Message("No strategy supports StrategyToggler") } return nil }).Next(func(signature string, reply interact.Reply) error { @@ -337,26 +325,23 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { return fmt.Errorf("strategy %s not found", signature) } - controller, implemented := strategy.(StrategyController) + controller, implemented := strategy.(StrategyToggler) if !implemented { - reply.Message(fmt.Sprintf("Strategy %s does not support strategy resume", signature)) - return fmt.Errorf("strategy %s does not implement StrategyController interface", signature) + reply.Message(fmt.Sprintf("Strategy %s does not support StrategyToggler", signature)) + return fmt.Errorf("strategy %s does not implement StrategyToggler", signature) } - // Check strategy status before resume - status := controller.GetStatus() - if status != types.StrategyStatusStopped { + // Check strategy status before suspend + if controller.GetStatus() != types.StrategyStatusStopped { reply.Message(fmt.Sprintf("Strategy %s is running.", signature)) return nil } - err := controller.Resume(context.Background()) - if kc, ok := reply.(interact.KeyboardController); ok { kc.RemoveKeyboard() } - if err != nil { + if err := controller.Resume(); err != nil { reply.Message(fmt.Sprintf("Failed to resume the strategy, %s", err.Error())) return err } @@ -368,11 +353,11 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { i.PrivateCommand("/emergencystop", "Emergency Stop", func(reply interact.Reply) error { // it.trader.exchangeStrategies // send symbol options - if strategies, found := it.FilterStrategyByInterface((*EmergencyStopper)(nil)); found { - reply.AddMultipleButtons(GenerateStrategyButtonsForm(strategies)) + if strategies, found := filterStrategyByInterface((*EmergencyStopper)(nil), it.exchangeStrategies); found { + reply.AddMultipleButtons(generateStrategyButtonsForm(getStrategySignatures(strategies))) reply.Message("Please choose one strategy") } else { - reply.Message("No any strategy supports EmergencyStopper") + reply.Message("No strategy supports EmergencyStopper") } return nil }).Next(func(signature string, reply interact.Reply) error { @@ -384,18 +369,16 @@ func (it *CoreInteraction) Commands(i *interact.Interact) { controller, implemented := strategy.(EmergencyStopper) if !implemented { - reply.Message(fmt.Sprintf("Strategy %s does not support emergency stop", signature)) - return fmt.Errorf("strategy %s does not implement EmergencyStopper interface", signature) + reply.Message(fmt.Sprintf("Strategy %s does not support EmergencyStopper", signature)) + return fmt.Errorf("strategy %s does not implement EmergencyStopper", signature) } - err := controller.EmergencyStop(context.Background()) - if kc, ok := reply.(interact.KeyboardController); ok { kc.RemoveKeyboard() } - if err != nil { - reply.Message(fmt.Sprintf("Failed to stop the strategy, %s", err.Error())) + if err := controller.EmergencyStop(); err != nil { + reply.Message(fmt.Sprintf("Failed to emergency stop the strategy, %s", err.Error())) return err } diff --git a/pkg/bbgo/strategy_controller.go b/pkg/bbgo/strategy_controller.go new file mode 100644 index 000000000..e9e866258 --- /dev/null +++ b/pkg/bbgo/strategy_controller.go @@ -0,0 +1,57 @@ +package bbgo + +import ( + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate callbackgen -type StrategyController -interface +type StrategyController struct { + Status types.StrategyStatus + + // Callbacks + suspendCallbacks []func() + resumeCallbacks []func() + emergencyStopCallbacks []func() +} + +func (s *StrategyController) GetStatus() types.StrategyStatus { + return s.Status +} + +func (s *StrategyController) Suspend() error { + s.Status = types.StrategyStatusStopped + + s.EmitSuspend() + + return nil +} + +func (s *StrategyController) Resume() error { + s.Status = types.StrategyStatusRunning + + s.EmitResume() + + return nil +} + +func (s *StrategyController) EmergencyStop() error { + s.Status = types.StrategyStatusStopped + + s.EmitEmergencyStop() + + return nil +} + +type StrategyStatusReader interface { + GetStatus() types.StrategyStatus +} + +type StrategyToggler interface { + StrategyStatusReader + Suspend() error + Resume() error +} + +type EmergencyStopper interface { + EmergencyStop() error +} diff --git a/pkg/bbgo/strategycontroller_callbacks.go b/pkg/bbgo/strategycontroller_callbacks.go new file mode 100644 index 000000000..caa3e68fb --- /dev/null +++ b/pkg/bbgo/strategycontroller_callbacks.go @@ -0,0 +1,35 @@ +// Code generated by "callbackgen -type StrategyController strategy_controller.go"; DO NOT EDIT. + +package bbgo + +import () + +func (s *StrategyController) OnSuspend(cb func()) { + s.suspendCallbacks = append(s.suspendCallbacks, cb) +} + +func (s *StrategyController) EmitSuspend() { + for _, cb := range s.suspendCallbacks { + cb() + } +} + +func (s *StrategyController) OnResume(cb func()) { + s.resumeCallbacks = append(s.resumeCallbacks, cb) +} + +func (s *StrategyController) EmitResume() { + for _, cb := range s.resumeCallbacks { + cb() + } +} + +func (s *StrategyController) OnEmergencyStop(cb func()) { + s.emergencyStopCallbacks = append(s.emergencyStopCallbacks, cb) +} + +func (s *StrategyController) EmitEmergencyStop() { + for _, cb := range s.emergencyStopCallbacks { + cb() + } +} diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index 8ad5171fe..91aaeabdd 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -170,8 +170,9 @@ type Strategy struct { tradeCollector *bbgo.TradeCollector - orderStore *bbgo.OrderStore - state *State + orderStore *bbgo.OrderStore + activeOrders *bbgo.LocalActiveOrderBook + state *State triggerEMA *indicator.EWMA longTermEMA *indicator.EWMA @@ -181,7 +182,7 @@ type Strategy struct { trailingStopControl *TrailingStopControl // StrategyController - status types.StrategyStatus + bbgo.StrategyController } func (s *Strategy) ID() string { @@ -249,55 +250,7 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu } s.orderStore.Add(createdOrders...) - return err -} - -// StrategyController - -func (s *Strategy) GetStatus() types.StrategyStatus { - return s.status -} - -func (s *Strategy) Suspend(ctx context.Context) error { - s.status = types.StrategyStatusStopped - - var err error - // Cancel all order - for _, order := range s.orderStore.Orders() { - err = s.cancelOrder(order.OrderID, ctx, s.orderExecutor) - } - if err != nil { - errMsg := "Not all orders are cancelled! Please check again." - log.WithError(err).Errorf(errMsg) - s.Notify(errMsg) - } else { - s.Notify("All orders cancelled.") - } - - // Save state - if err2 := s.SaveState(); err2 != nil { - log.WithError(err2).Errorf("can not save state: %+v", s.state) - } else { - log.Infof("%s position is saved.", s.Symbol) - } - - return nil -} - -func (s *Strategy) Resume(ctx context.Context) error { - s.status = types.StrategyStatusRunning - - return nil -} - -func (s *Strategy) EmergencyStop(ctx context.Context) error { - // Close 100% position - percentage, _ := fixedpoint.NewFromString("100%") - err := s.ClosePosition(ctx, percentage) - - // Suspend strategy - _ = s.Suspend(ctx) - + s.activeOrders.Add(createdOrders...) return err } @@ -352,6 +305,7 @@ func (s *Strategy) submitOrders(ctx context.Context, orderExecutor bbgo.OrderExe } s.orderStore.Add(createdOrders...) + s.activeOrders.Add(createdOrders...) s.tradeCollector.Emit() return createdOrders, nil } @@ -439,7 +393,41 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor = orderExecutor // StrategyController - s.status = types.StrategyStatusRunning + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + // Cancel all order + if err := s.activeOrders.GracefulCancel(ctx, session.Exchange); err != nil { + errMsg := fmt.Sprintf("Not all %s orders are cancelled! Please check again.", s.Symbol) + log.WithError(err).Errorf(errMsg) + s.Notify(errMsg) + } else { + s.Notify("All %s orders are cancelled.", s.Symbol) + } + + // Save state + if err := s.SaveState(); err != nil { + log.WithError(err).Errorf("can not save state: %+v", s.state) + } else { + log.Infof("%s state is saved.", s.Symbol) + } + }) + + s.OnEmergencyStop(func() { + // Close 100% position + percentage := fixedpoint.NewFromFloat(1.0) + if err := s.ClosePosition(context.Background(), percentage); err != nil { + errMsg := "failed to close position" + log.WithError(err).Errorf(errMsg) + s.Notify(errMsg) + } + + if err := s.Suspend(); err != nil { + errMsg := "failed to suspend strategy" + log.WithError(err).Errorf(errMsg) + s.Notify(errMsg) + } + }) // set default values if s.Interval == "" { @@ -487,6 +475,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderStore = bbgo.NewOrderStore(s.Symbol) s.orderStore.BindStream(session.UserDataStream) + s.activeOrders = bbgo.NewLocalActiveOrderBook(s.Symbol) + s.activeOrders.BindStream(session.UserDataStream) + if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { s.trailingStopControl = &TrailingStopControl{ symbol: s.Symbol, @@ -510,7 +501,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Update trailing stop when the position changes s.tradeCollector.OnPositionUpdate(func(position *types.Position) { // StrategyController - if s.status != types.StrategyStatusRunning { + if s.Status != types.StrategyStatusRunning { return } @@ -562,7 +553,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { // StrategyController - if s.status != types.StrategyStatusRunning { + if s.Status != types.StrategyStatusRunning { return } @@ -757,9 +748,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Cancel trailing stop order if s.TrailingStopTarget.TrailingStopCallbackRatio.Sign() > 0 { - if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil { - log.WithError(err).Errorf("Can not cancel the trailing stop order!") + // Cancel all orders + if err := s.activeOrders.GracefulCancel(ctx, session.Exchange); err != nil { + errMsg := "Not all {s.Symbol} orders are cancelled! Please check again." + log.WithError(err).Errorf(errMsg) + s.Notify(errMsg) + } else { + s.Notify("All {s.Symbol} orders are cancelled.") } + s.trailingStopControl.OrderID = 0 } diff --git a/pkg/types/strategy_status.go b/pkg/types/strategy_status.go index b98b1bbb0..00efde484 100644 --- a/pkg/types/strategy_status.go +++ b/pkg/types/strategy_status.go @@ -6,4 +6,5 @@ type StrategyStatus string const ( StrategyStatusRunning StrategyStatus = "RUNNING" StrategyStatusStopped StrategyStatus = "STOPPED" + StrategyStatusUnknown StrategyStatus = "UNKNOWN" )