Merge pull request #39 from c9s/feature/notification-router

feature: strategy injection
This commit is contained in:
Yo-An Lin 2020-10-27 21:46:59 +08:00 committed by GitHub
commit 501edb1bca
8 changed files with 142 additions and 108 deletions

140
README.md
View File

@ -22,6 +22,13 @@ Aim to release v1.0 before 11/14
- MAX Exchange (located in Taiwan)
- Binance Exchange
## Requirements
Get your exchange API key and secret after you register the accounts:
- For MAX: <https://max.maicoin.com/signup?r=c7982718>
- For Binance: <https://www.binancezh.com/en/register?ref=VGDGLT80>
## Installation
Install the builtin commands:
@ -49,11 +56,6 @@ MYSQL_DATABASE=bbgo
MYSQL_URL=root@tcp(127.0.0.1:3306)/bbgo
```
You can get your API key and secret after you register the accounts:
- For MAX: <https://max.maicoin.com/signup?r=c7982718>
- For Binance: <https://www.binancezh.com/en/register?ref=VGDGLT80>
Then run the `migrate` command to initialize your database:
```sh
@ -82,6 +84,77 @@ To calculate pnl:
dotenv -f .env.local -- bbgo pnl --exchange binance --asset BTC --since "2019-01-01"
```
To run strategy:
```sh
dotenv -f .env.local -- bbgo run --config config/buyandhold.yaml
```
## Built-in Strategies
Check out the strategy directory [strategy](pkg/strategy) for all built-in strategies:
- pricealert strategy demonstrates how to use the notification system [pricealert](pkg/strategy/pricealert)
- xpuremaker strategy demonstrates how to maintain the orderbook and submit maker orders [xpuremaker](pkg/strategy/xpuremaker)
- buyandhold strategy demonstrates how to subscribe kline events and submit market order [buyandhold](pkg/strategy/buyandhold)
## Write your own strategy
Create your go package, and initialize the repository with `go mod` and add bbgo as a dependency:
```
go mod init
go get github.com/c9s/bbgo
```
Write your own strategy in the strategy directory like `pkg/strategy/mystrategy`:
```
mkdir pkg/strategy/mystrategy
vim pkg/strategy/mystrategy/strategy.go
```
You can grab the skeleton strategy from <https://github.com/c9s/bbgo/blob/main/pkg/strategy/skeleton/strategy.go>
Now add your config:
```
mkdir config
(cd config && curl -o bbgo.yaml https://raw.githubusercontent.com/c9s/bbgo/main/config/minimal.yaml)
```
Add your strategy package path to the config file `config/bbgo.yaml`
```yaml
imports:
- github.com/xxx/yyy/pkg/strategy/mystrategy
```
Run `bbgo run` command, bbgo will compile a wrapper binary that imports your strategy:
```sh
dotenv -f .env.local -- bbgo run --config config/bbgo.yaml
```
## Dynamic Injection
In order to minimize the strategy code, bbgo supports dynamic dependency injection.
Before executing your strategy, bbgo injects the components into your strategy object if
it found the embedded field that is using bbgo component. for example:
```go
type Strategy struct {
*bbgo.Notifiability
}
```
And then, in your code, you can call the methods of Notifiability.
Supported components (single exchange strategy only for now):
- `*bbgo.Notifiability`
- `bbgo.OrderExecutor`
## Exchange API Examples
@ -107,63 +180,6 @@ streambook := types.NewStreamBook(symbol)
streambook.BindStream(stream)
```
## Built-in Strategies
Check out the strategy directory [strategy](pkg/strategy) for all built-in strategies:
- pricealert strategy demonstrates how to use the notification system [pricealert](pkg/strategy/pricealert)
- xpuremaker strategy demonstrates how to maintain the orderbook and submit maker orders [xpuremaker](pkg/strategy/xpuremaker)
- buyandhold strategy demonstrates how to subscribe kline events and submit market order [buyandhold](pkg/strategy/buyandhold)
## New API Design
_**still under construction**_
```go
package main
import (
"github.com/c9s/bbgo"
)
func main() {
mysqlURL := viper.GetString("mysql-url")
mysqlURL = fmt.Sprintf("%s?parseTime=true", mysqlURL)
db, err := sqlx.Connect("mysql", mysqlURL)
if err != nil {
return err
}
environment := bbgo.NewEnvironment(db)
environment.AddExchange("binance", binance.New(viper.Getenv("binance-api-key"), viper.Getenv("binance-api-secret"))))
environment.AddExchange("max", max.New(viper.Getenv("max-key"), viper.Getenv("max-secret"))))
trader := bbgo.NewTrader(bbgo.Config{
Environment: environment,
DB: db,
})
trader.AddNotifier(slacknotifier.New(slackToken))
trader.AddLogHook(slacklog.NewLogHook(slackToken))
// when any trade execution happened
trader.OnTrade(func(session string, exchange types.Exchange, trade types.Trade) {
notify(trade)
notifyPnL()
})
// mount strategy on an exchange
trader.AddExchangeStrategy("binance",
bondtrade.New("btcusdt", "5m"),
bondtrade.New("ethusdt", "5m"))
// mount cross exchange strategy
trader.AddCrossExchangeStrategy(hedgemaker.New("max", "binance"))
t.Run(ctx)
}
```
## Support
### By contributing pull requests

10
config/minimal.yaml Normal file
View File

@ -0,0 +1,10 @@
---
exchangeStrategies:
- on: max
xpuremaker:
symbol: MAXUSDT
numOrders: 2
side: both
behindVolume: 1000.0
priceTick: 0.01
baseQuantity: 100.0

View File

@ -1,19 +1,15 @@
package bbgo
type Notifier interface {
NotifyTo(channel, format string, args ...interface{}) error
Notify(format string, args ...interface{}) error
NotifyTo(channel, format string, args ...interface{})
Notify(format string, args ...interface{})
}
type NullNotifier struct{}
func (n *NullNotifier) NotifyTo(channel, format string, args ...interface{}) error {
return nil
}
func (n *NullNotifier) NotifyTo(channel, format string, args ...interface{}) {}
func (n *NullNotifier) Notify(format string, args ...interface{}) error {
return nil
}
func (n *NullNotifier) Notify(format string, args ...interface{}) {}
type Notifiability struct {
notifiers []Notifier
@ -38,22 +34,14 @@ func (m *Notifiability) AddNotifier(notifier Notifier) {
m.notifiers = append(m.notifiers, notifier)
}
func (m *Notifiability) Notify(msg string, args ...interface{}) (err error) {
func (m *Notifiability) Notify(format string, args ...interface{}) {
for _, n := range m.notifiers {
if err2 := n.Notify(msg, args...); err2 != nil {
err = err2
}
n.Notify(format, args...)
}
return err
}
func (m *Notifiability) NotifyTo(channel, msg string, args ...interface{}) (err error) {
func (m *Notifiability) NotifyTo(channel, format string, args ...interface{}) {
for _, n := range m.notifiers {
if err2 := n.NotifyTo(channel, msg, args...); err2 != nil {
err = err2
}
n.NotifyTo(channel, format, args...)
}
return err
}

View File

@ -4,7 +4,6 @@ import (
"regexp"
"github.com/robfig/cron/v3"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/accounting/pnl"
"github.com/c9s/bbgo/pkg/types"
@ -183,7 +182,5 @@ func (reporter *TradeReporter) Report(trade types.Trade) {
var channel = reporter.getChannel(trade.Symbol)
var text = util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade)
if err := reporter.notifier.NotifyTo(channel, text, trade); err != nil {
logrus.WithError(err).Errorf("notifier error, channel=%s", channel)
}
reporter.notifier.NotifyTo(channel, text, trade)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"reflect"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/types"
@ -130,14 +131,15 @@ func (trader *Trader) Run(ctx context.Context) error {
for _, strategy := range strategies {
rs := reflect.ValueOf(strategy)
if rs.Elem().Kind() == reflect.Struct {
// get the struct element
rs = rs.Elem()
field := rs.FieldByName("Notifiability")
if field.IsValid() {
log.Infof("found Notifiability in strategy %T, configuring...", strategy)
if !field.CanSet() {
log.Panicf("strategy %T field Notifiability can not be set", strategy)
}
field.Set(reflect.ValueOf(trader.Notifiability))
if err := injectStrategyField(strategy, rs, "Notifiability", &trader.Notifiability); err != nil {
log.WithError(err).Errorf("strategy notifiability injection failed")
}
if err := injectStrategyField(strategy, rs, "OrderExecutor", orderExecutor); err != nil {
log.WithError(err).Errorf("strategy orderExecutor injection failed")
}
}
@ -163,6 +165,35 @@ func (trader *Trader) Run(ctx context.Context) error {
return trader.environment.Connect(ctx)
}
func injectStrategyField(strategy SingleExchangeStrategy, rs reflect.Value, fieldName string, obj interface{}) error {
field := rs.FieldByName(fieldName)
if !field.IsValid() {
return nil
}
log.Infof("found %s in strategy %T, injecting %T...", fieldName, strategy, obj)
if !field.CanSet() {
return errors.Errorf("field %s of strategy %T can not be set", fieldName, strategy)
}
rv := reflect.ValueOf(obj)
if field.Kind() == reflect.Ptr {
if field.Type() != rv.Type() {
return errors.Errorf("field type mismatches: %s != %s", field.Type(), rv.Type())
}
field.Set(rv)
} else if field.Kind() == reflect.Interface {
field.Set(rv)
} else {
// set as value
field.Set(rv.Elem())
}
return nil
}
/*
func (trader *OrderExecutor) RunStrategyWithHotReload(ctx context.Context, strategy SingleExchangeStrategy, configFile string) (chan struct{}, error) {
var done = make(chan struct{})
@ -266,14 +297,6 @@ func (trader *OrderExecutor) RunStrategy(ctx context.Context, strategy SingleExc
}
*/
/*
func (trader *Trader) reportPnL() {
report := trader.ProfitAndLossCalculator.Calculate()
report.Print()
trader.NotifyPnL(report)
}
*/
// ReportPnL configure and set the PnLReporter with the given notifier
func (trader *Trader) ReportPnL() *PnLReporterManager {
return NewPnLReporter(&trader.Notifiability)

View File

@ -114,7 +114,7 @@ func runConfig(ctx context.Context, userConfig *bbgo.Config) error {
// for slack
slackToken := viper.GetString("slack-token")
if len(slackToken) > 0 {
if len(slackToken) > 0 && userConfig.Notifications != nil {
if conf := userConfig.Notifications.Slack; conf != nil {
if conf.ErrorChannel != "" {
log.Infof("found slack configured, setting up log hook...")

View File

@ -34,11 +34,11 @@ func New(token, channel string, options ...NotifyOption) *Notifier {
return notifier
}
func (n *Notifier) Notify(format string, args ...interface{}) error {
return n.NotifyTo(n.channel, format, args...)
func (n *Notifier) Notify(format string, args ...interface{}) {
n.NotifyTo(n.channel, format, args...)
}
func (n *Notifier) NotifyTo(channel, format string, args ...interface{}) error {
func (n *Notifier) NotifyTo(channel, format string, args ...interface{}) {
var slackAttachments []slack.Attachment
var slackArgsOffset = -1
@ -77,7 +77,7 @@ func (n *Notifier) NotifyTo(channel, format string, args ...interface{}) error {
logrus.WithError(err).Errorf("slack error: %s", err.Error())
}
return err
return
}
/*

View File

@ -32,9 +32,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
if math.Abs(kline.GetChange()) > s.MinChange {
if channel, ok := s.RouteSymbol(s.Symbol); ok {
_ = s.NotifyTo(channel, "%s hit price %s, change %f", s.Symbol, market.FormatPrice(kline.Close), kline.GetChange())
s.NotifyTo(channel, "%s hit price %s, change %f", s.Symbol, market.FormatPrice(kline.Close), kline.GetChange())
} else {
_ = s.Notify("%s hit price %s, change %f", s.Symbol, market.FormatPrice(kline.Close), kline.GetChange())
s.Notify("%s hit price %s, change %f", s.Symbol, market.FormatPrice(kline.Close), kline.GetChange())
}
}
})