Merge pull request #492 from andycheng123/tg-control

feature: strategy controller
This commit is contained in:
Yo-An Lin 2022-03-26 15:41:59 +08:00 committed by GitHub
commit 1a29bc7362
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 292 additions and 19 deletions

View File

@ -21,6 +21,24 @@ type PositionReader interface {
CurrentPosition() *types.Position 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 { type closePositionContext struct {
signature string signature string
closer PositionCloser closer PositionCloser
@ -43,6 +61,20 @@ func NewCoreInteraction(environment *Environment, trader *Trader) *CoreInteracti
} }
} }
func (it *CoreInteraction) AddSupportedStrategyButtons(checkInterface interface{}) ([][3]string, bool) {
found := false
var buttonsForm [][3]string
rt := reflect.TypeOf(checkInterface).Elem()
for signature, strategy := range it.exchangeStrategies {
if ok := reflect.TypeOf(strategy).Implements(rt); ok {
buttonsForm = append(buttonsForm, [3]string{signature, "strategy", signature})
found = true
}
}
return buttonsForm, found
}
func (it *CoreInteraction) Commands(i *interact.Interact) { func (it *CoreInteraction) Commands(i *interact.Interact) {
i.PrivateCommand("/sessions", "List Exchange Sessions", func(reply interact.Reply) error { i.PrivateCommand("/sessions", "List Exchange Sessions", func(reply interact.Reply) error {
switch r := reply.(type) { switch r := reply.(type) {
@ -90,15 +122,8 @@ func (it *CoreInteraction) Commands(i *interact.Interact) {
i.PrivateCommand("/position", "Show Position", func(reply interact.Reply) error { i.PrivateCommand("/position", "Show Position", func(reply interact.Reply) error {
// it.trader.exchangeStrategies // it.trader.exchangeStrategies
// send symbol options // send symbol options
found := false if buttonsForm, found := it.AddSupportedStrategyButtons((*PositionReader)(nil)); found {
for signature, strategy := range it.exchangeStrategies { reply.AddMultipleButtons(buttonsForm)
if _, ok := strategy.(PositionReader); ok {
reply.AddButton(signature, "strategy", signature)
found = true
}
}
if found {
reply.Message("Please choose one strategy") reply.Message("Please choose one strategy")
} else { } else {
reply.Message("No any strategy supports PositionReader") reply.Message("No any strategy supports PositionReader")
@ -138,16 +163,9 @@ func (it *CoreInteraction) Commands(i *interact.Interact) {
i.PrivateCommand("/closeposition", "Close position", func(reply interact.Reply) error { i.PrivateCommand("/closeposition", "Close position", func(reply interact.Reply) error {
// it.trader.exchangeStrategies // it.trader.exchangeStrategies
// send symbol options // send symbol options
found := false if buttonsForm, found := it.AddSupportedStrategyButtons((*PositionCloser)(nil)); found {
for signature, strategy := range it.exchangeStrategies { reply.AddMultipleButtons(buttonsForm)
if _, ok := strategy.(PositionCloser); ok { reply.Message("Please choose one strategy")
reply.AddButton(signature, signature, signature)
found = true
}
}
if found {
reply.Message("Please choose your position from the current running strategies")
} else { } else {
reply.Message("No any strategy supports PositionCloser") reply.Message("No any strategy supports PositionCloser")
} }
@ -210,6 +228,172 @@ func (it *CoreInteraction) Commands(i *interact.Interact) {
reply.Message("Done") reply.Message("Done")
return nil return nil
}) })
i.PrivateCommand("/status", "Strategy Status", func(reply interact.Reply) error {
// it.trader.exchangeStrategies
// send symbol options
if buttonsForm, found := it.AddSupportedStrategyButtons((*StrategyStatusProvider)(nil)); found {
reply.AddMultipleButtons(buttonsForm)
reply.Message("Please choose one strategy")
} else {
reply.Message("No any strategy supports StrategyStatusProvider")
}
return nil
}).Cycle(func(signature string, reply interact.Reply) error {
strategy, ok := it.exchangeStrategies[signature]
if !ok {
reply.Message("Strategy not found")
return fmt.Errorf("strategy %s not found", signature)
}
controller, implemented := strategy.(StrategyStatusProvider)
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)
}
status := controller.GetStatus()
if kc, ok := reply.(interact.KeyboardController); ok {
kc.RemoveKeyboard()
}
if status == types.StrategyStatusRunning {
reply.Message(fmt.Sprintf("Strategy %s is running.", signature))
} else if status == types.StrategyStatusStopped {
reply.Message(fmt.Sprintf("Strategy %s is not running.", signature))
}
return nil
})
i.PrivateCommand("/suspend", "Suspend Strategy", func(reply interact.Reply) error {
// it.trader.exchangeStrategies
// send symbol options
if buttonsForm, found := it.AddSupportedStrategyButtons((*StrategyController)(nil)); found {
reply.AddMultipleButtons(buttonsForm)
reply.Message("Please choose one strategy")
} else {
reply.Message("No any strategy supports StrategyController")
}
return nil
}).Cycle(func(signature string, reply interact.Reply) error {
strategy, ok := it.exchangeStrategies[signature]
if !ok {
reply.Message("Strategy not found")
return fmt.Errorf("strategy %s not found", signature)
}
controller, implemented := strategy.(StrategyController)
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)
}
// Check strategy status before suspend
status := controller.GetStatus()
if status != 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 {
reply.Message(fmt.Sprintf("Failed to suspend the strategy, %s", err.Error()))
return err
}
reply.Message(fmt.Sprintf("Strategy %s suspended.", signature))
return nil
})
i.PrivateCommand("/resume", "Resume Strategy", func(reply interact.Reply) error {
// it.trader.exchangeStrategies
// send symbol options
if buttonsForm, found := it.AddSupportedStrategyButtons((*StrategyController)(nil)); found {
reply.AddMultipleButtons(buttonsForm)
reply.Message("Please choose one strategy")
} else {
reply.Message("No any strategy supports StrategyController")
}
return nil
}).Cycle(func(signature string, reply interact.Reply) error {
strategy, ok := it.exchangeStrategies[signature]
if !ok {
reply.Message("Strategy not found")
return fmt.Errorf("strategy %s not found", signature)
}
controller, implemented := strategy.(StrategyController)
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)
}
// Check strategy status before resume
status := controller.GetStatus()
if status != 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 {
reply.Message(fmt.Sprintf("Failed to resume the strategy, %s", err.Error()))
return err
}
reply.Message(fmt.Sprintf("Strategy %s resumed.", signature))
return nil
})
i.PrivateCommand("/emergencystop", "Emergency Stop", func(reply interact.Reply) error {
// it.trader.exchangeStrategies
// send symbol options
if buttonsForm, found := it.AddSupportedStrategyButtons((*EmergencyStopper)(nil)); found {
reply.AddMultipleButtons(buttonsForm)
reply.Message("Please choose one strategy")
} else {
reply.Message("No any strategy supports EmergencyStopper")
}
return nil
}).Cycle(func(signature string, reply interact.Reply) error {
strategy, ok := it.exchangeStrategies[signature]
if !ok {
reply.Message("Strategy not found")
return fmt.Errorf("strategy %s not found", signature)
}
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)
}
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()))
return err
}
reply.Message(fmt.Sprintf("Strategy %s stopped and the position closed.", signature))
return nil
})
} }
func (it *CoreInteraction) Initialize() error { func (it *CoreInteraction) Initialize() error {

View File

@ -38,6 +38,9 @@ type Reply interface {
// AddButton adds the button to the reply // AddButton adds the button to the reply
AddButton(text string, name, value string) AddButton(text string, name, value string)
// AddMultipleButtons adds multiple buttons to the reply
AddMultipleButtons(buttonsForm [][3]string)
// Choose(prompt string, options ...Option) // Choose(prompt string, options ...Option)
// Confirm shows the confirm dialog or confirm button in the user interface // Confirm shows the confirm dialog or confirm button in the user interface
// Confirm(prompt string) // Confirm(prompt string)

View File

@ -68,6 +68,12 @@ func (reply *SlackReply) AddButton(text string, name string, value string) {
}) })
} }
func (reply *SlackReply) AddMultipleButtons(buttonsForm [][3]string) {
for _, buttonForm := range buttonsForm {
reply.AddButton(buttonForm[0], buttonForm[1], buttonForm[2])
}
}
func (reply *SlackReply) build() interface{} { func (reply *SlackReply) build() interface{} {
// you should avoid using this modal view request, because it interrupts the interaction flow // you should avoid using this modal view request, because it interrupts the interaction flow
// once we send the modal view request, we can't go back to the channel. // once we send the modal view request, we can't go back to the channel.

View File

@ -81,6 +81,12 @@ func (r *TelegramReply) AddButton(text string, name string, value string) {
r.set = true r.set = true
} }
func (r *TelegramReply) AddMultipleButtons(buttonsForm [][3]string) {
for _, buttonForm := range buttonsForm {
r.AddButton(buttonForm[0], buttonForm[1], buttonForm[2])
}
}
func (r *TelegramReply) build() { func (r *TelegramReply) build() {
var rows []telebot.Row var rows []telebot.Row
for _, button := range r.buttons { for _, button := range r.buttons {

View File

@ -179,6 +179,9 @@ type Strategy struct {
// Trailing stop // Trailing stop
TrailingStopTarget TrailingStopTarget `json:"trailingStopTarget"` TrailingStopTarget TrailingStopTarget `json:"trailingStopTarget"`
trailingStopControl *TrailingStopControl trailingStopControl *TrailingStopControl
// StrategyController
status types.StrategyStatus
} }
func (s *Strategy) ID() string { func (s *Strategy) ID() string {
@ -249,6 +252,55 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
return err 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)
return err
}
func (s *Strategy) SaveState() error { func (s *Strategy) SaveState() error {
if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil { if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil {
return err return err
@ -384,6 +436,9 @@ func (s *Strategy) calculateQuantity(session *bbgo.ExchangeSession, side types.S
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
s.orderExecutor = orderExecutor s.orderExecutor = orderExecutor
// StrategyController
s.status = types.StrategyStatusRunning
// set default values // set default values
if s.Interval == "" { if s.Interval == "" {
s.Interval = types.Interval5m s.Interval = types.Interval5m
@ -452,6 +507,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() {
// Update trailing stop when the position changes // Update trailing stop when the position changes
s.tradeCollector.OnPositionUpdate(func(position *types.Position) { s.tradeCollector.OnPositionUpdate(func(position *types.Position) {
// StrategyController
if s.status != types.StrategyStatusRunning {
return
}
if position.Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position if position.Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position
// Cancel the original order // Cancel the original order
if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil { if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil {
@ -497,6 +557,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// go s.tradeCollector.Run(ctx) // go s.tradeCollector.Run(ctx)
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
// StrategyController
if s.status != types.StrategyStatusRunning {
return
}
// skip k-lines from other symbols // skip k-lines from other symbols
if kline.Symbol != s.Symbol { if kline.Symbol != s.Symbol {
return return

View File

@ -0,0 +1,9 @@
package types
// StrategyStatus define strategy status
type StrategyStatus string
const (
StrategyStatusRunning StrategyStatus = "RUNNING"
StrategyStatusStopped StrategyStatus = "STOPPED"
)