mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 00:35:15 +00:00
Merge pull request #492 from andycheng123/tg-control
feature: strategy controller
This commit is contained in:
commit
1a29bc7362
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
9
pkg/types/strategy_status.go
Normal file
9
pkg/types/strategy_status.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
// StrategyStatus define strategy status
|
||||||
|
type StrategyStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StrategyStatusRunning StrategyStatus = "RUNNING"
|
||||||
|
StrategyStatusStopped StrategyStatus = "STOPPED"
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user