mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-14 11:03:53 +00:00
Merge pull request #233 from c9s/refactor/notifier
This commit is contained in:
commit
ab51007b62
|
@ -359,8 +359,7 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
|
||||||
|
|
||||||
case "$session":
|
case "$session":
|
||||||
defaultTradeUpdateHandler := func(trade types.Trade) {
|
defaultTradeUpdateHandler := func(trade types.Trade) {
|
||||||
text := util.Render(TemplateTradeReport, trade)
|
environ.Notify(&trade)
|
||||||
environ.Notify(text, &trade)
|
|
||||||
}
|
}
|
||||||
for name := range environ.sessions {
|
for name := range environ.sessions {
|
||||||
session := environ.sessions[name]
|
session := environ.sessions[name]
|
||||||
|
@ -369,8 +368,7 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
|
||||||
channel, ok := environ.SessionChannelRouter.Route(name)
|
channel, ok := environ.SessionChannelRouter.Route(name)
|
||||||
if ok {
|
if ok {
|
||||||
session.Stream.OnTradeUpdate(func(trade types.Trade) {
|
session.Stream.OnTradeUpdate(func(trade types.Trade) {
|
||||||
text := util.Render(TemplateTradeReport, trade)
|
environ.NotifyTo(channel, &trade)
|
||||||
environ.NotifyTo(channel, text, &trade)
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
session.Stream.OnTradeUpdate(defaultTradeUpdateHandler)
|
session.Stream.OnTradeUpdate(defaultTradeUpdateHandler)
|
||||||
|
@ -390,12 +388,11 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
|
||||||
|
|
||||||
// use same handler for each session
|
// use same handler for each session
|
||||||
handler := func(trade types.Trade) {
|
handler := func(trade types.Trade) {
|
||||||
text := util.Render(TemplateTradeReport, trade)
|
|
||||||
channel, ok := environ.RouteObject(&trade)
|
channel, ok := environ.RouteObject(&trade)
|
||||||
if ok {
|
if ok {
|
||||||
environ.NotifyTo(channel, text, &trade)
|
environ.NotifyTo(channel, &trade)
|
||||||
} else {
|
} else {
|
||||||
environ.Notify(text, &trade)
|
environ.Notify(&trade)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, session := range environ.sessions {
|
for _, session := range environ.sessions {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
package bbgo
|
package bbgo
|
||||||
|
|
||||||
type Notifier interface {
|
type Notifier interface {
|
||||||
NotifyTo(channel, format string, args ...interface{})
|
NotifyTo(channel string, obj interface{}, args ...interface{})
|
||||||
Notify(format string, args ...interface{})
|
Notify(obj interface{}, args ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type NullNotifier struct{}
|
type NullNotifier struct{}
|
||||||
|
|
||||||
func (n *NullNotifier) NotifyTo(channel, format string, args ...interface{}) {}
|
func (n *NullNotifier) NotifyTo(channel string, obj interface{}, args ...interface{}) {}
|
||||||
|
|
||||||
func (n *NullNotifier) Notify(format string, args ...interface{}) {}
|
func (n *NullNotifier) Notify(obj interface{}, args ...interface{}) {}
|
||||||
|
|
||||||
type Notifiability struct {
|
type Notifiability struct {
|
||||||
notifiers []Notifier
|
notifiers []Notifier
|
||||||
|
@ -18,7 +18,7 @@ type Notifiability struct {
|
||||||
ObjectChannelRouter *ObjectChannelRouter `json:"-"`
|
ObjectChannelRouter *ObjectChannelRouter `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteSession routes symbol name to channel
|
// RouteSymbol routes symbol name to channel
|
||||||
func (m *Notifiability) RouteSymbol(symbol string) (channel string, ok bool) {
|
func (m *Notifiability) RouteSymbol(symbol string) (channel string, ok bool) {
|
||||||
if m.SymbolChannelRouter != nil {
|
if m.SymbolChannelRouter != nil {
|
||||||
return m.SymbolChannelRouter.Route(symbol)
|
return m.SymbolChannelRouter.Route(symbol)
|
||||||
|
@ -47,14 +47,14 @@ func (m *Notifiability) AddNotifier(notifier Notifier) {
|
||||||
m.notifiers = append(m.notifiers, notifier)
|
m.notifiers = append(m.notifiers, notifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Notifiability) Notify(format string, args ...interface{}) {
|
func (m *Notifiability) Notify(obj interface{}, args ...interface{}) {
|
||||||
for _, n := range m.notifiers {
|
for _, n := range m.notifiers {
|
||||||
n.Notify(format, args...)
|
n.Notify(obj, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Notifiability) NotifyTo(channel, format string, args ...interface{}) {
|
func (m *Notifiability) NotifyTo(channel string, obj interface{}, args ...interface{}) {
|
||||||
for _, n := range m.notifiers {
|
for _, n := range m.notifiers {
|
||||||
n.NotifyTo(channel, format, args...)
|
n.NotifyTo(channel, obj, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,4 @@ type TradeReporter struct {
|
||||||
*Notifiability
|
*Notifiability
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateTradeReport = `:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`
|
|
||||||
|
|
||||||
const TemplateOrderReport = `:handshake: {{ .Symbol }} {{ .Side }} Order Update @ {{ .Price }}`
|
const TemplateOrderReport = `:handshake: {{ .Symbol }} {{ .Side }} Order Update @ {{ .Price }}`
|
||||||
|
|
|
@ -35,32 +35,26 @@ func New(token, channel string, options ...NotifyOption) *Notifier {
|
||||||
return notifier
|
return notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) Notify(format string, args ...interface{}) {
|
func (n *Notifier) Notify(obj interface{}, args ...interface{}) {
|
||||||
n.NotifyTo(n.channel, format, args...)
|
n.NotifyTo(n.channel, obj, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) NotifyTo(channel, format string, args ...interface{}) {
|
func filterSlackAttachments(args []interface{}) (slackAttachments []slack.Attachment, pureArgs []interface{}) {
|
||||||
if len(channel) == 0 {
|
var firstAttachmentOffset = -1
|
||||||
channel = n.channel
|
|
||||||
}
|
|
||||||
|
|
||||||
var slackAttachments []slack.Attachment
|
|
||||||
var slackArgsOffset = -1
|
|
||||||
|
|
||||||
for idx, arg := range args {
|
for idx, arg := range args {
|
||||||
switch a := arg.(type) {
|
switch a := arg.(type) {
|
||||||
|
|
||||||
// concrete type assert first
|
// concrete type assert first
|
||||||
case slack.Attachment:
|
case slack.Attachment:
|
||||||
if slackArgsOffset == -1 {
|
if firstAttachmentOffset == -1 {
|
||||||
slackArgsOffset = idx
|
firstAttachmentOffset = idx
|
||||||
}
|
}
|
||||||
|
|
||||||
slackAttachments = append(slackAttachments, a)
|
slackAttachments = append(slackAttachments, a)
|
||||||
|
|
||||||
case SlackAttachmentCreator:
|
case SlackAttachmentCreator:
|
||||||
if slackArgsOffset == -1 {
|
if firstAttachmentOffset == -1 {
|
||||||
slackArgsOffset = idx
|
firstAttachmentOffset = idx
|
||||||
}
|
}
|
||||||
|
|
||||||
slackAttachments = append(slackAttachments, a.SlackAttachment())
|
slackAttachments = append(slackAttachments, a.SlackAttachment())
|
||||||
|
@ -68,19 +62,45 @@ func (n *Notifier) NotifyTo(channel, format string, args ...interface{}) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var nonSlackArgs = args
|
pureArgs = args
|
||||||
if slackArgsOffset > -1 {
|
if firstAttachmentOffset > -1 {
|
||||||
nonSlackArgs = args[:slackArgsOffset]
|
pureArgs = args[:firstAttachmentOffset]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{}) {
|
||||||
|
if len(channel) == 0 {
|
||||||
|
channel = n.channel
|
||||||
|
}
|
||||||
|
|
||||||
|
slackAttachments, pureArgs := filterSlackAttachments(args)
|
||||||
|
|
||||||
|
var opts []slack.MsgOption
|
||||||
|
|
||||||
|
switch a := obj.(type) {
|
||||||
|
case string:
|
||||||
|
opts = append(opts, slack.MsgOptionText(fmt.Sprintf(a, pureArgs...), true),
|
||||||
|
slack.MsgOptionAttachments(slackAttachments...))
|
||||||
|
|
||||||
|
case slack.Attachment:
|
||||||
|
opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a}, slackAttachments...)...))
|
||||||
|
|
||||||
|
case SlackAttachmentCreator:
|
||||||
|
// convert object to slack attachment (if supported)
|
||||||
|
opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a.SlackAttachment()}, slackAttachments...)...))
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Errorf("slack notifier error, unsupported object: %T %+v", a, a)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_, _, err := n.client.PostMessageContext(context.Background(), channel,
|
_, _, err := n.client.PostMessageContext(context.Background(), channel, opts...)
|
||||||
slack.MsgOptionText(fmt.Sprintf(format, nonSlackArgs...), true),
|
|
||||||
slack.MsgOptionAttachments(slackAttachments...))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).
|
log.WithError(err).
|
||||||
WithField("channel", channel).
|
WithField("channel", channel).
|
||||||
Errorf("slack error: %s", err.Error())
|
Errorf("slack api error: %s", err.Error())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,9 @@ type Notifier struct {
|
||||||
|
|
||||||
type NotifyOption func(notifier *Notifier)
|
type NotifyOption func(notifier *Notifier)
|
||||||
|
|
||||||
// start bot daemon
|
|
||||||
|
// New
|
||||||
|
// TODO: register interaction with channel, so that we can route message to the specific telegram bot
|
||||||
func New(interaction *Interaction, options ...NotifyOption) *Notifier {
|
func New(interaction *Interaction, options ...NotifyOption) *Notifier {
|
||||||
notifier := &Notifier{
|
notifier := &Notifier{
|
||||||
interaction: interaction,
|
interaction: interaction,
|
||||||
|
@ -25,14 +27,12 @@ func New(interaction *Interaction, options ...NotifyOption) *Notifier {
|
||||||
return notifier
|
return notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) Notify(format string, args ...interface{}) {
|
func (n *Notifier) Notify(obj interface{}, args ...interface{}) {
|
||||||
n.NotifyTo("", format, args...)
|
n.NotifyTo("", obj, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) NotifyTo(_, format string, args ...interface{}) {
|
func filterPlaintextMessages(args []interface{}) (texts []string, pureArgs []interface{}) {
|
||||||
var textArgsOffset = -1
|
var textArgsOffset = -1
|
||||||
var texts []string
|
|
||||||
|
|
||||||
for idx, arg := range args {
|
for idx, arg := range args {
|
||||||
switch a := arg.(type) {
|
switch a := arg.(type) {
|
||||||
|
|
||||||
|
@ -40,21 +40,44 @@ func (n *Notifier) NotifyTo(_, format string, args ...interface{}) {
|
||||||
texts = append(texts, a.PlainText())
|
texts = append(texts, a.PlainText())
|
||||||
textArgsOffset = idx
|
textArgsOffset = idx
|
||||||
|
|
||||||
|
case types.Stringer:
|
||||||
|
texts = append(texts, a.String())
|
||||||
|
textArgsOffset = idx
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var simpleArgs = args
|
pureArgs = args
|
||||||
if textArgsOffset > -1 {
|
if textArgsOffset > -1 {
|
||||||
simpleArgs = args[:textArgsOffset]
|
pureArgs = args[:textArgsOffset]
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof(format, simpleArgs...)
|
return texts, pureArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{}) {
|
||||||
|
var texts, pureArgs = filterPlaintextMessages(args)
|
||||||
|
var message string
|
||||||
|
|
||||||
|
switch a := obj.(type) {
|
||||||
|
|
||||||
|
case string:
|
||||||
|
log.Infof(a, pureArgs...)
|
||||||
|
message = fmt.Sprintf(a, pureArgs...)
|
||||||
|
|
||||||
|
case types.Stringer:
|
||||||
|
message = a.String()
|
||||||
|
|
||||||
|
case types.PlainText:
|
||||||
|
message = a.PlainText()
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Errorf("unsupported notification format: %T %+v", a, a)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
message := fmt.Sprintf(format, simpleArgs...)
|
|
||||||
n.interaction.SendToOwner(message)
|
n.interaction.SendToOwner(message)
|
||||||
|
|
||||||
for _, text := range texts {
|
for _, text := range texts {
|
||||||
n.interaction.SendToOwner(text)
|
n.interaction.SendToOwner(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,7 +110,7 @@ func (s *TradeService) Sync(ctx context.Context, exchange types.Exchange, symbol
|
||||||
trade.Side,
|
trade.Side,
|
||||||
trade.Price,
|
trade.Price,
|
||||||
trade.Quantity,
|
trade.Quantity,
|
||||||
trade.MakerOrTakerLabel(),
|
trade.Liquidity(),
|
||||||
trade.Time.String())
|
trade.Time.String())
|
||||||
|
|
||||||
if err := s.Insert(trade); err != nil {
|
if err := s.Insert(trade); err != nil {
|
||||||
|
|
|
@ -3,3 +3,7 @@ package types
|
||||||
type PlainText interface {
|
type PlainText interface {
|
||||||
PlainText() string
|
PlainText() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Stringer interface {
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
|
@ -81,6 +81,8 @@ func (trade Trade) PlainText() string {
|
||||||
util.FormatFloat(trade.QuoteQuantity, 2))
|
util.FormatFloat(trade.QuoteQuantity, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var slackTradeTextTemplate = ":handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}"
|
||||||
|
|
||||||
func (trade Trade) SlackAttachment() slack.Attachment {
|
func (trade Trade) SlackAttachment() slack.Attachment {
|
||||||
var color = "#DC143C"
|
var color = "#DC143C"
|
||||||
|
|
||||||
|
@ -88,25 +90,27 @@ func (trade Trade) SlackAttachment() slack.Attachment {
|
||||||
color = "#228B22"
|
color = "#228B22"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
liquidity := trade.Liquidity()
|
||||||
|
text := util.Render(slackTradeTextTemplate, trade)
|
||||||
return slack.Attachment{
|
return slack.Attachment{
|
||||||
Text: fmt.Sprintf("*%s* Trade %s", trade.Symbol, trade.Side),
|
Text: text,
|
||||||
Color: color,
|
|
||||||
// Pretext: "",
|
// Pretext: "",
|
||||||
// Text: "",
|
Color: color,
|
||||||
Fields: []slack.AttachmentField{
|
Fields: []slack.AttachmentField{
|
||||||
{Title: "Exchange", Value: trade.Exchange, Short: true},
|
{Title: "Exchange", Value: trade.Exchange, Short: true},
|
||||||
{Title: "Price", Value: util.FormatFloat(trade.Price, 2), Short: true},
|
{Title: "Price", Value: util.FormatFloat(trade.Price, 2), Short: true},
|
||||||
{Title: "Quote", Value: util.FormatFloat(trade.Quantity, 4), Short: true},
|
{Title: "Quantity", Value: util.FormatFloat(trade.Quantity, 4), Short: true},
|
||||||
{Title: "QuoteQuantity", Value: util.FormatFloat(trade.QuoteQuantity, 2)},
|
{Title: "QuoteQuantity", Value: util.FormatFloat(trade.QuoteQuantity, 2)},
|
||||||
{Title: "Fee", Value: util.FormatFloat(trade.Fee, 4), Short: true},
|
{Title: "Fee", Value: util.FormatFloat(trade.Fee, 4), Short: true},
|
||||||
{Title: "FeeCurrency", Value: trade.FeeCurrency, Short: true},
|
{Title: "FeeCurrency", Value: trade.FeeCurrency, Short: true},
|
||||||
|
{Title: "Liquidity", Value: liquidity, Short: true},
|
||||||
},
|
},
|
||||||
// Footer: tradingCtx.TradeStartTime.Format(time.RFC822),
|
// Footer: tradingCtx.TradeStartTime.Format(time.RFC822),
|
||||||
// FooterIcon: "",
|
// FooterIcon: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (trade Trade) MakerOrTakerLabel() (o string) {
|
func (trade Trade) Liquidity() (o string) {
|
||||||
if trade.IsMaker {
|
if trade.IsMaker {
|
||||||
o += "MAKER"
|
o += "MAKER"
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,21 +2,24 @@ package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Render(tpl string, args interface{}) string {
|
func Render(tpl string, args interface{}) string {
|
||||||
var buf = bytes.NewBuffer(nil)
|
var buf = bytes.NewBuffer(nil)
|
||||||
tmpl, err := template.New("tmp").Parse(tpl)
|
tmpl, err := template.New("tmp").Parse(tpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("template error")
|
logrus.WithError(err).Error("template parse error")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tmpl.Execute(buf, args)
|
err = tmpl.Execute(buf, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("template error")
|
logrus.WithError(err).Error("template execute error")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user