mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
implement position closer interaction
This commit is contained in:
parent
77c2a6e10b
commit
93722e6db3
1
go.mod
1
go.mod
|
@ -30,6 +30,7 @@ require (
|
|||
github.com/lestrrat-go/strftime v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.10 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
|
|
2
go.sum
2
go.sum
|
@ -313,6 +313,8 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
|
|||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
|
|
|
@ -716,6 +716,10 @@ func (environ *Environment) setupTelegram(userConfig *Config, telegramBotToken s
|
|||
|
||||
// you must restore the session after the notifier updates
|
||||
messenger.RestoreSession(session)
|
||||
|
||||
// right now it's only for telegram, should we share the session (?)
|
||||
interact.Default().SetOriginState(interact.StateAuthenticated)
|
||||
interact.Default().SetState(interact.StateAuthenticated)
|
||||
}
|
||||
|
||||
messenger.OnAuthorized(func(a *interact.TelegramAuthorizer) {
|
||||
|
|
172
pkg/bbgo/interact.go
Normal file
172
pkg/bbgo/interact.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/interact"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type PositionCloser interface {
|
||||
ClosePosition(ctx context.Context, percentage float64) error
|
||||
}
|
||||
|
||||
type PositionReader interface {
|
||||
CurrentPosition() *types.Position
|
||||
}
|
||||
|
||||
type closePositionContext struct {
|
||||
signature string
|
||||
closer PositionCloser
|
||||
percentage float64
|
||||
}
|
||||
|
||||
type CoreInteraction struct {
|
||||
environment *Environment
|
||||
trader *Trader
|
||||
|
||||
exchangeStrategies map[string]SingleExchangeStrategy
|
||||
closePositionContext closePositionContext
|
||||
}
|
||||
|
||||
func NewCoreInteraction(environment *Environment, trader *Trader) *CoreInteraction {
|
||||
return &CoreInteraction{
|
||||
environment: environment,
|
||||
trader: trader,
|
||||
exchangeStrategies: make(map[string]SingleExchangeStrategy),
|
||||
}
|
||||
}
|
||||
|
||||
func (it *CoreInteraction) Commands(i *interact.Interact) {
|
||||
i.PrivateCommand("/closeposition", "close the position of a strategy", func(reply interact.Reply) error {
|
||||
// it.trader.exchangeStrategies
|
||||
// send symbol options
|
||||
found := false
|
||||
for signature, strategy := range it.exchangeStrategies {
|
||||
if _, ok := strategy.(PositionCloser); ok {
|
||||
reply.AddButton(signature)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
reply.Message("Please choose your position from the current running strategies")
|
||||
} else {
|
||||
reply.Message("No any strategy supports PositionCloser")
|
||||
}
|
||||
return nil
|
||||
}).Next(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)
|
||||
}
|
||||
|
||||
closer, implemented := strategy.(PositionCloser)
|
||||
if !implemented {
|
||||
reply.Message(fmt.Sprintf("Strategy %s does not support position close", signature))
|
||||
return fmt.Errorf("strategy %s does not implement PositionCloser interface", signature)
|
||||
}
|
||||
|
||||
it.closePositionContext.closer = closer
|
||||
it.closePositionContext.signature = signature
|
||||
|
||||
if reader, implemented := strategy.(PositionReader); implemented {
|
||||
position := reader.CurrentPosition()
|
||||
if position != nil {
|
||||
reply.Send("Your current position:")
|
||||
reply.Send(position.String())
|
||||
|
||||
if position.Base == 0 {
|
||||
reply.Message("No opened position")
|
||||
reply.RemoveKeyboard()
|
||||
return fmt.Errorf("no opened position")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reply.Message("Choose or enter the percentage to close")
|
||||
for _, symbol := range []string{"5%", "25%", "50%", "80%", "100%"} {
|
||||
reply.AddButton(symbol)
|
||||
}
|
||||
|
||||
return nil
|
||||
}).Next(func(percentageStr string, reply interact.Reply) error {
|
||||
percentage, err := parseFloatPercent(percentageStr, 64)
|
||||
if err != nil {
|
||||
reply.Message(fmt.Sprintf("%q is not a valid percentage string", percentageStr))
|
||||
return err
|
||||
}
|
||||
|
||||
reply.RemoveKeyboard()
|
||||
|
||||
err = it.closePositionContext.closer.ClosePosition(context.Background(), percentage)
|
||||
if err != nil {
|
||||
reply.Message(fmt.Sprintf("Failed to close the position, %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (it *CoreInteraction) Initialize() error {
|
||||
// re-map exchange strategies into the signature-object map
|
||||
for sessionID, strategies := range it.trader.exchangeStrategies {
|
||||
for _, strategy := range strategies {
|
||||
signature, err := getStrategySignature(strategy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := sessionID + "." + signature
|
||||
it.exchangeStrategies[key] = strategy
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStrategySignature(strategy SingleExchangeStrategy) (string, error) {
|
||||
rv := reflect.ValueOf(strategy).Elem()
|
||||
if rv.Kind() != reflect.Struct {
|
||||
return "", fmt.Errorf("strategy %T instance is not a struct", strategy)
|
||||
}
|
||||
|
||||
var signature = path.Base(rv.Type().PkgPath())
|
||||
|
||||
var id = strategy.ID()
|
||||
|
||||
if !strings.EqualFold(id, signature) {
|
||||
signature += "." + strings.ToLower(id)
|
||||
}
|
||||
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
field := rv.Field(i)
|
||||
if field.Kind() == reflect.String {
|
||||
str := field.String()
|
||||
if len(str) > 0 {
|
||||
signature += "." + field.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
func parseFloatPercent(s string, bitSize int) (f float64, err error) {
|
||||
i := strings.Index(s, "%")
|
||||
if i < 0 {
|
||||
return strconv.ParseFloat(s, bitSize)
|
||||
}
|
||||
|
||||
f, err = strconv.ParseFloat(s[:i], bitSize)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return f / 100.0, nil
|
||||
}
|
28
pkg/bbgo/interact_test.go
Normal file
28
pkg/bbgo/interact_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type myStrategy struct {
|
||||
Symbol string `json:"symbol"`
|
||||
}
|
||||
|
||||
func (m myStrategy) ID() string {
|
||||
return "mystrategy"
|
||||
}
|
||||
|
||||
func (m *myStrategy) Run(ctx context.Context, orderExecutor OrderExecutor, session *ExchangeSession) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_getStrategySignature(t *testing.T) {
|
||||
signature, err := getStrategySignature(&myStrategy{
|
||||
Symbol: "BTCUSDT",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "bbgo.mystrategy.BTCUSDT", signature)
|
||||
}
|
|
@ -10,6 +10,8 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/interact"
|
||||
)
|
||||
|
||||
// SingleExchangeStrategy represents the single Exchange strategy
|
||||
|
@ -291,6 +293,11 @@ func (trader *Trader) RunAllSingleExchangeStrategy(ctx context.Context) error {
|
|||
}
|
||||
|
||||
func (trader *Trader) Run(ctx context.Context) error {
|
||||
// before we start the interaction,
|
||||
// register the core interaction, because we can only get the strategies in this scope
|
||||
// trader.environment.Connect will call interact.Start
|
||||
interact.AddCustomInteraction(NewCoreInteraction(trader.environment, trader))
|
||||
|
||||
trader.Subscribe()
|
||||
|
||||
if err := trader.environment.Start(ctx); err != nil {
|
||||
|
|
|
@ -13,7 +13,7 @@ func SetMessenger(messenger Messenger) {
|
|||
}
|
||||
|
||||
func AddCustomInteraction(custom CustomInteraction) {
|
||||
custom.Commands(defaultInteraction)
|
||||
defaultInteraction.AddCustomInteraction(custom)
|
||||
}
|
||||
|
||||
func Start(ctx context.Context) error {
|
||||
|
|
|
@ -12,6 +12,10 @@ type CustomInteraction interface {
|
|||
Commands(interact *Interact)
|
||||
}
|
||||
|
||||
type Initializer interface {
|
||||
Initialize() error
|
||||
}
|
||||
|
||||
type Messenger interface {
|
||||
TextMessageResponder
|
||||
CommandResponder
|
||||
|
@ -33,6 +37,8 @@ type Interact struct {
|
|||
|
||||
originState, currentState State
|
||||
|
||||
customInteractions []CustomInteraction
|
||||
|
||||
messenger Messenger
|
||||
}
|
||||
|
||||
|
@ -40,6 +46,7 @@ func New() *Interact {
|
|||
return &Interact{
|
||||
startTime: time.Now(),
|
||||
commands: make(map[string]*Command),
|
||||
privateCommands: make(map[string]*Command),
|
||||
originState: StatePublic,
|
||||
currentState: StatePublic,
|
||||
states: make(map[State]State),
|
||||
|
@ -53,10 +60,11 @@ func (it *Interact) SetOriginState(s State) {
|
|||
|
||||
func (it *Interact) AddCustomInteraction(custom CustomInteraction) {
|
||||
custom.Commands(it)
|
||||
it.customInteractions = append(it.customInteractions, custom)
|
||||
}
|
||||
|
||||
func (it *Interact) PrivateCommand(command string, f interface{}) *Command {
|
||||
cmd := NewCommand(command, "", f)
|
||||
func (it *Interact) PrivateCommand(command, desc string, f interface{}) *Command {
|
||||
cmd := NewCommand(command, desc, f)
|
||||
it.privateCommands[command] = cmd
|
||||
return cmd
|
||||
}
|
||||
|
@ -84,7 +92,7 @@ func (it *Interact) getNextState(currentState State) (nextState State, final boo
|
|||
return it.originState, final
|
||||
}
|
||||
|
||||
func (it *Interact) setState(s State) {
|
||||
func (it *Interact) SetState(s State) {
|
||||
log.Infof("[interact] transiting state from %s -> %s", it.currentState, s)
|
||||
it.currentState = s
|
||||
}
|
||||
|
@ -111,11 +119,11 @@ func (it *Interact) handleResponse(text string, ctxObjects ...interface{}) error
|
|||
|
||||
nextState, end := it.getNextState(it.currentState)
|
||||
if end {
|
||||
it.setState(it.originState)
|
||||
it.SetState(it.originState)
|
||||
return nil
|
||||
}
|
||||
|
||||
it.setState(nextState)
|
||||
it.SetState(nextState)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -146,7 +154,7 @@ func (it *Interact) runCommand(command string, args []string, ctxObjects ...inte
|
|||
return err
|
||||
}
|
||||
|
||||
it.setState(cmd.initState)
|
||||
it.SetState(cmd.initState)
|
||||
if _, err := parseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -154,11 +162,11 @@ func (it *Interact) runCommand(command string, args []string, ctxObjects ...inte
|
|||
// if we can successfully execute the command, then we can go to the next state.
|
||||
nextState, end := it.getNextState(it.currentState)
|
||||
if end {
|
||||
it.setState(it.originState)
|
||||
it.SetState(it.originState)
|
||||
return nil
|
||||
}
|
||||
|
||||
it.setState(nextState)
|
||||
it.SetState(nextState)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -186,7 +194,19 @@ func (it *Interact) init() error {
|
|||
return err
|
||||
}
|
||||
|
||||
for n, cmd := range it.commands {
|
||||
if err := it.registerCommands(it.commands); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := it.registerCommands(it.privateCommands); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (it *Interact) registerCommands(commands map[string]*Command) error {
|
||||
for n, cmd := range commands {
|
||||
for s1, s2 := range cmd.states {
|
||||
if _, exist := it.states[s1]; exist {
|
||||
return fmt.Errorf("state %s already exists", s1)
|
||||
|
@ -209,7 +229,6 @@ func (it *Interact) init() error {
|
|||
return it.runCommand(commandName, args, append(ctxObjects, reply)...)
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -218,6 +237,16 @@ func (it *Interact) Start(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
for _, custom := range it.customInteractions {
|
||||
log.Infof("checking %T custom interaction...", custom)
|
||||
if initializer, ok := custom.(Initializer); ok {
|
||||
log.Infof("initializing %T custom interaction...", custom)
|
||||
if err := initializer.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: use go routine and context
|
||||
it.messenger.Start(ctx)
|
||||
return nil
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"text/scanner"
|
||||
|
||||
"github.com/mattn/go-shellwords"
|
||||
)
|
||||
|
||||
func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) {
|
||||
|
@ -112,6 +114,13 @@ func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{})
|
|||
}
|
||||
|
||||
func parseCommand(src string) (args []string) {
|
||||
var err error
|
||||
args, err = shellwords.Parse(src)
|
||||
if err == nil {
|
||||
return args
|
||||
}
|
||||
|
||||
// fallback to go text/scanner
|
||||
var s scanner.Scanner
|
||||
s.Init(strings.NewReader(src))
|
||||
s.Filename = "command"
|
||||
|
@ -125,4 +134,3 @@ func parseCommand(src string) (args []string) {
|
|||
|
||||
return args
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package interact
|
||||
|
||||
type Reply interface {
|
||||
Send(message string)
|
||||
Message(message string)
|
||||
AddButton(text string)
|
||||
RemoveKeyboard()
|
||||
|
|
|
@ -12,12 +12,18 @@ import (
|
|||
|
||||
type TelegramReply struct {
|
||||
bot *telebot.Bot
|
||||
chat *telebot.Chat
|
||||
|
||||
message string
|
||||
menu *telebot.ReplyMarkup
|
||||
buttons [][]telebot.Btn
|
||||
set bool
|
||||
}
|
||||
|
||||
func (r *TelegramReply) Send(message string) {
|
||||
checkSendErr(r.bot.Send(r.chat, message))
|
||||
}
|
||||
|
||||
func (r *TelegramReply) Message(message string) {
|
||||
r.message = message
|
||||
r.set = true
|
||||
|
@ -115,7 +121,7 @@ func (tm *Telegram) Start(context.Context) {
|
|||
}
|
||||
|
||||
authorizer := tm.newAuthorizer(m)
|
||||
reply := tm.newReply()
|
||||
reply := tm.newReply(m)
|
||||
if tm.textMessageResponder != nil {
|
||||
if err := tm.textMessageResponder(m.Text, reply, authorizer); err != nil {
|
||||
log.WithError(err).Errorf("[telegram] response handling error")
|
||||
|
@ -148,30 +154,35 @@ func (tm *Telegram) Start(context.Context) {
|
|||
go tm.Bot.Start()
|
||||
}
|
||||
|
||||
func checkSendErr(m *telebot.Message, err error) {
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("[telegram] message send error")
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *Telegram) AddCommand(cmd *Command, responder Responder) {
|
||||
tm.commands = append(tm.commands, cmd)
|
||||
tm.Bot.Handle(cmd.Name, func(m *telebot.Message) {
|
||||
authorizer := tm.newAuthorizer(m)
|
||||
reply := tm.newReply()
|
||||
reply := tm.newReply(m)
|
||||
if err := responder(m.Payload, reply, authorizer); err != nil {
|
||||
log.WithError(err).Errorf("[telegram] responder error")
|
||||
tm.Bot.Send(m.Sender, fmt.Sprintf("error: %v", err))
|
||||
checkSendErr(tm.Bot.Send(m.Sender, fmt.Sprintf("error: %v", err)))
|
||||
return
|
||||
}
|
||||
|
||||
// build up the response objects
|
||||
if reply.set {
|
||||
reply.build()
|
||||
if _, err := tm.Bot.Send(m.Sender, reply.message, reply.menu); err != nil {
|
||||
log.WithError(err).Errorf("[telegram] message send error")
|
||||
}
|
||||
checkSendErr(tm.Bot.Send(m.Sender, reply.message, reply.menu))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (tm *Telegram) newReply() *TelegramReply {
|
||||
func (tm *Telegram) newReply(m *telebot.Message) *TelegramReply {
|
||||
return &TelegramReply{
|
||||
bot: tm.Bot,
|
||||
chat: m.Chat,
|
||||
menu: &telebot.ReplyMarkup{ResizeReplyKeyboard: true},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -166,6 +166,47 @@ func (s *Strategy) Validate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) CurrentPosition() *types.Position {
|
||||
return s.state.Position
|
||||
}
|
||||
|
||||
func (s *Strategy) ClosePosition(ctx context.Context, percentage float64) error {
|
||||
base := s.state.Position.GetBase()
|
||||
if base == 0 {
|
||||
return fmt.Errorf("no opened %s position", s.state.Position.Symbol)
|
||||
}
|
||||
|
||||
// make it negative
|
||||
quantity := base.MulFloat64(percentage).Abs()
|
||||
side := types.SideTypeBuy
|
||||
if base > 0 {
|
||||
side = types.SideTypeSell
|
||||
}
|
||||
|
||||
if quantity.Float64() < s.market.MinQuantity {
|
||||
return fmt.Errorf("order quantity %f is too small, less than %f", quantity.Float64(), s.market.MinQuantity)
|
||||
}
|
||||
|
||||
submitOrder := types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Side: side,
|
||||
Type: types.OrderTypeMarket,
|
||||
Quantity: quantity.Float64(),
|
||||
Market: s.market,
|
||||
}
|
||||
|
||||
s.Notify("Submitting %s %s order to close position by %f", s.Symbol, side.String(), percentage, submitOrder)
|
||||
|
||||
createdOrders, err := s.session.Exchange.SubmitOrders(ctx, submitOrder)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("can not place position close order")
|
||||
}
|
||||
|
||||
s.orderStore.Add(createdOrders...)
|
||||
s.activeMakerOrders.Add(createdOrders...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Strategy) SaveState() error {
|
||||
if err := s.Persistence.Save(s.state, ID, s.Symbol, stateKey); err != nil {
|
||||
return err
|
||||
|
|
Loading…
Reference in New Issue
Block a user