mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 00:35:15 +00:00
Merge pull request #39 from c9s/feature/notification-router
feature: strategy injection
This commit is contained in:
commit
501edb1bca
140
README.md
140
README.md
|
@ -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
10
config/minimal.yaml
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
exchangeStrategies:
|
||||
- on: max
|
||||
xpuremaker:
|
||||
symbol: MAXUSDT
|
||||
numOrders: 2
|
||||
side: both
|
||||
behindVolume: 1000.0
|
||||
priceTick: 0.01
|
||||
baseQuantity: 100.0
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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...")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue
Block a user