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/lestrrat-go/strftime v1.0.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.4 // indirect
|
github.com/magiconair/properties v1.8.4 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.7 // 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/mattn/go-sqlite3 v1.14.10 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||||
github.com/pelletier/go-toml v1.8.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.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
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-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.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 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
|
||||||
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
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
|
// you must restore the session after the notifier updates
|
||||||
messenger.RestoreSession(session)
|
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) {
|
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"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/interact"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SingleExchangeStrategy represents the single Exchange strategy
|
// 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 {
|
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()
|
trader.Subscribe()
|
||||||
|
|
||||||
if err := trader.environment.Start(ctx); err != nil {
|
if err := trader.environment.Start(ctx); err != nil {
|
||||||
|
|
|
@ -13,7 +13,7 @@ func SetMessenger(messenger Messenger) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddCustomInteraction(custom CustomInteraction) {
|
func AddCustomInteraction(custom CustomInteraction) {
|
||||||
custom.Commands(defaultInteraction)
|
defaultInteraction.AddCustomInteraction(custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(ctx context.Context) error {
|
func Start(ctx context.Context) error {
|
||||||
|
|
|
@ -12,6 +12,10 @@ type CustomInteraction interface {
|
||||||
Commands(interact *Interact)
|
Commands(interact *Interact)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Initializer interface {
|
||||||
|
Initialize() error
|
||||||
|
}
|
||||||
|
|
||||||
type Messenger interface {
|
type Messenger interface {
|
||||||
TextMessageResponder
|
TextMessageResponder
|
||||||
CommandResponder
|
CommandResponder
|
||||||
|
@ -33,6 +37,8 @@ type Interact struct {
|
||||||
|
|
||||||
originState, currentState State
|
originState, currentState State
|
||||||
|
|
||||||
|
customInteractions []CustomInteraction
|
||||||
|
|
||||||
messenger Messenger
|
messenger Messenger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +46,7 @@ func New() *Interact {
|
||||||
return &Interact{
|
return &Interact{
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
commands: make(map[string]*Command),
|
commands: make(map[string]*Command),
|
||||||
|
privateCommands: make(map[string]*Command),
|
||||||
originState: StatePublic,
|
originState: StatePublic,
|
||||||
currentState: StatePublic,
|
currentState: StatePublic,
|
||||||
states: make(map[State]State),
|
states: make(map[State]State),
|
||||||
|
@ -53,10 +60,11 @@ func (it *Interact) SetOriginState(s State) {
|
||||||
|
|
||||||
func (it *Interact) AddCustomInteraction(custom CustomInteraction) {
|
func (it *Interact) AddCustomInteraction(custom CustomInteraction) {
|
||||||
custom.Commands(it)
|
custom.Commands(it)
|
||||||
|
it.customInteractions = append(it.customInteractions, custom)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (it *Interact) PrivateCommand(command string, f interface{}) *Command {
|
func (it *Interact) PrivateCommand(command, desc string, f interface{}) *Command {
|
||||||
cmd := NewCommand(command, "", f)
|
cmd := NewCommand(command, desc, f)
|
||||||
it.privateCommands[command] = cmd
|
it.privateCommands[command] = cmd
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
@ -84,7 +92,7 @@ func (it *Interact) getNextState(currentState State) (nextState State, final boo
|
||||||
return it.originState, final
|
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)
|
log.Infof("[interact] transiting state from %s -> %s", it.currentState, 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)
|
nextState, end := it.getNextState(it.currentState)
|
||||||
if end {
|
if end {
|
||||||
it.setState(it.originState)
|
it.SetState(it.originState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
it.setState(nextState)
|
it.SetState(nextState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +154,7 @@ func (it *Interact) runCommand(command string, args []string, ctxObjects ...inte
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
it.setState(cmd.initState)
|
it.SetState(cmd.initState)
|
||||||
if _, err := parseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil {
|
if _, err := parseFuncArgsAndCall(cmd.F, args, ctxObjects...); err != nil {
|
||||||
return err
|
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.
|
// if we can successfully execute the command, then we can go to the next state.
|
||||||
nextState, end := it.getNextState(it.currentState)
|
nextState, end := it.getNextState(it.currentState)
|
||||||
if end {
|
if end {
|
||||||
it.setState(it.originState)
|
it.SetState(it.originState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
it.setState(nextState)
|
it.SetState(nextState)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +194,19 @@ func (it *Interact) init() error {
|
||||||
return err
|
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 {
|
for s1, s2 := range cmd.states {
|
||||||
if _, exist := it.states[s1]; exist {
|
if _, exist := it.states[s1]; exist {
|
||||||
return fmt.Errorf("state %s already exists", s1)
|
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 it.runCommand(commandName, args, append(ctxObjects, reply)...)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,6 +237,16 @@ func (it *Interact) Start(ctx context.Context) error {
|
||||||
return err
|
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
|
// TODO: use go routine and context
|
||||||
it.messenger.Start(ctx)
|
it.messenger.Start(ctx)
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"text/scanner"
|
"text/scanner"
|
||||||
|
|
||||||
|
"github.com/mattn/go-shellwords"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseFuncArgsAndCall(f interface{}, args []string, objects ...interface{}) (State, error) {
|
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) {
|
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
|
var s scanner.Scanner
|
||||||
s.Init(strings.NewReader(src))
|
s.Init(strings.NewReader(src))
|
||||||
s.Filename = "command"
|
s.Filename = "command"
|
||||||
|
@ -125,4 +134,3 @@ func parseCommand(src string) (args []string) {
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package interact
|
package interact
|
||||||
|
|
||||||
type Reply interface {
|
type Reply interface {
|
||||||
|
Send(message string)
|
||||||
Message(message string)
|
Message(message string)
|
||||||
AddButton(text string)
|
AddButton(text string)
|
||||||
RemoveKeyboard()
|
RemoveKeyboard()
|
||||||
|
|
|
@ -12,12 +12,18 @@ import (
|
||||||
|
|
||||||
type TelegramReply struct {
|
type TelegramReply struct {
|
||||||
bot *telebot.Bot
|
bot *telebot.Bot
|
||||||
|
chat *telebot.Chat
|
||||||
|
|
||||||
message string
|
message string
|
||||||
menu *telebot.ReplyMarkup
|
menu *telebot.ReplyMarkup
|
||||||
buttons [][]telebot.Btn
|
buttons [][]telebot.Btn
|
||||||
set bool
|
set bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *TelegramReply) Send(message string) {
|
||||||
|
checkSendErr(r.bot.Send(r.chat, message))
|
||||||
|
}
|
||||||
|
|
||||||
func (r *TelegramReply) Message(message string) {
|
func (r *TelegramReply) Message(message string) {
|
||||||
r.message = message
|
r.message = message
|
||||||
r.set = true
|
r.set = true
|
||||||
|
@ -115,7 +121,7 @@ func (tm *Telegram) Start(context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
authorizer := tm.newAuthorizer(m)
|
authorizer := tm.newAuthorizer(m)
|
||||||
reply := tm.newReply()
|
reply := tm.newReply(m)
|
||||||
if tm.textMessageResponder != nil {
|
if tm.textMessageResponder != nil {
|
||||||
if err := tm.textMessageResponder(m.Text, reply, authorizer); err != nil {
|
if err := tm.textMessageResponder(m.Text, reply, authorizer); err != nil {
|
||||||
log.WithError(err).Errorf("[telegram] response handling error")
|
log.WithError(err).Errorf("[telegram] response handling error")
|
||||||
|
@ -148,30 +154,35 @@ func (tm *Telegram) Start(context.Context) {
|
||||||
go tm.Bot.Start()
|
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) {
|
func (tm *Telegram) AddCommand(cmd *Command, responder Responder) {
|
||||||
tm.commands = append(tm.commands, cmd)
|
tm.commands = append(tm.commands, cmd)
|
||||||
tm.Bot.Handle(cmd.Name, func(m *telebot.Message) {
|
tm.Bot.Handle(cmd.Name, func(m *telebot.Message) {
|
||||||
authorizer := tm.newAuthorizer(m)
|
authorizer := tm.newAuthorizer(m)
|
||||||
reply := tm.newReply()
|
reply := tm.newReply(m)
|
||||||
if err := responder(m.Payload, reply, authorizer); err != nil {
|
if err := responder(m.Payload, reply, authorizer); err != nil {
|
||||||
log.WithError(err).Errorf("[telegram] responder error")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// build up the response objects
|
// build up the response objects
|
||||||
if reply.set {
|
if reply.set {
|
||||||
reply.build()
|
reply.build()
|
||||||
if _, err := tm.Bot.Send(m.Sender, reply.message, reply.menu); err != nil {
|
checkSendErr(tm.Bot.Send(m.Sender, reply.message, reply.menu))
|
||||||
log.WithError(err).Errorf("[telegram] message send error")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *Telegram) newReply() *TelegramReply {
|
func (tm *Telegram) newReply(m *telebot.Message) *TelegramReply {
|
||||||
return &TelegramReply{
|
return &TelegramReply{
|
||||||
bot: tm.Bot,
|
bot: tm.Bot,
|
||||||
|
chat: m.Chat,
|
||||||
menu: &telebot.ReplyMarkup{ResizeReplyKeyboard: true},
|
menu: &telebot.ReplyMarkup{ResizeReplyKeyboard: true},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,6 +166,47 @@ func (s *Strategy) Validate() error {
|
||||||
return nil
|
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 {
|
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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user