diff --git a/pkg/bbgo/livenote.go b/pkg/bbgo/livenote.go index 935ecc9ef..743c27098 100644 --- a/pkg/bbgo/livenote.go +++ b/pkg/bbgo/livenote.go @@ -3,19 +3,19 @@ package bbgo import ( "github.com/sirupsen/logrus" - "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/livenote" ) // PostLiveNote posts a live note to slack or other services // The MessageID will be set after the message is posted if it's not set. -func PostLiveNote(note *types.LiveNote) { +func PostLiveNote(obj livenote.Object) { for _, poster := range Notification.liveNotePosters { - if err := poster.PostLiveNote(note); err != nil { - logrus.WithError(err).Errorf("unable to post live note: %+v", note) + if err := poster.PostLiveNote(obj); err != nil { + logrus.WithError(err).Errorf("unable to post live note: %+v", obj) } } } type LiveNotePoster interface { - PostLiveNote(note *types.LiveNote) error + PostLiveNote(note livenote.Object) error } diff --git a/pkg/livenote/livenote.go b/pkg/livenote/livenote.go new file mode 100644 index 000000000..1a5fdfb20 --- /dev/null +++ b/pkg/livenote/livenote.go @@ -0,0 +1,73 @@ +package livenote + +import "sync" + +type Object interface { + ObjectID() string +} + +type LiveNote struct { + // MessageID is the unique identifier of the message + // for slack, it's the timestamp of the message + MessageID string `json:"messageId"` + + ChannelID string `json:"channelId"` + + Object Object +} + +func NewLiveNote(object Object) *LiveNote { + return &LiveNote{ + Object: object, + } +} + +func (n *LiveNote) ObjectID() string { + return n.Object.ObjectID() +} + +func (n *LiveNote) SetMessageID(messageID string) { + n.MessageID = messageID +} + +func (n *LiveNote) SetChannelID(channelID string) { + n.ChannelID = channelID +} + +type Pool struct { + notes map[string]*LiveNote + mu sync.Mutex +} + +func NewPool() *Pool { + return &Pool{ + notes: make(map[string]*LiveNote, 100), + } +} + +func (p *Pool) Update(obj Object) *LiveNote { + objID := obj.ObjectID() + + p.mu.Lock() + defer p.mu.Unlock() + + for _, note := range p.notes { + if note.ObjectID() == objID { + return note + } + } + + note := NewLiveNote(obj) + p.add(objID, note) + return note +} + +func (p *Pool) add(id string, note *LiveNote) { + p.notes[id] = note +} + +func (p *Pool) Add(note *LiveNote) { + p.mu.Lock() + p.add(note.ObjectID(), note) + p.mu.Unlock() +} diff --git a/pkg/livenote/livenote_test.go b/pkg/livenote/livenote_test.go new file mode 100644 index 000000000..22c4247fc --- /dev/null +++ b/pkg/livenote/livenote_test.go @@ -0,0 +1,44 @@ +package livenote + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/types" +) + +func TestLiveNotePool(t *testing.T) { + t.Run("same-kline", func(t *testing.T) { + pool := NewPool() + k := &types.KLine{ + Symbol: "BTCUSDT", + Interval: types.Interval1m, + StartTime: types.Time(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), + } + + note := pool.Update(k) + note2 := pool.Update(k) + assert.Equal(t, note, note2, "the returned note object should be the same") + }) + + t.Run("different-kline", func(t *testing.T) { + pool := NewPool() + k := &types.KLine{ + Symbol: "BTCUSDT", + Interval: types.Interval1m, + StartTime: types.Time(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)), + } + + k2 := &types.KLine{ + Symbol: "BTCUSDT", + Interval: types.Interval1m, + StartTime: types.Time(time.Date(2021, 1, 1, 0, 1, 0, 0, time.UTC)), + } + + note := pool.Update(k) + note2 := pool.Update(k2) + assert.NotEqual(t, note, note2, "the returned note object should be different") + }) +} diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index 4b1f6851a..c17216276 100644 --- a/pkg/notifier/slacknotifier/slack.go +++ b/pkg/notifier/slacknotifier/slack.go @@ -8,6 +8,7 @@ import ( "golang.org/x/time/rate" + "github.com/c9s/bbgo/pkg/livenote" "github.com/c9s/bbgo/pkg/types" log "github.com/sirupsen/logrus" @@ -67,7 +68,9 @@ func (n *Notifier) worker() { } } -func (n *Notifier) PostLiveNote(note *types.LiveNote) error { +func (n *Notifier) PostLiveNote(obj livenote.Object) error { + // TODO: maintain the object pool for live notes + var note = livenote.NewLiveNote(obj) ctx := context.Background() channel := note.ChannelID diff --git a/pkg/strategy/example/livenote/strategy.go b/pkg/strategy/example/livenote/strategy.go new file mode 100644 index 000000000..7e859586c --- /dev/null +++ b/pkg/strategy/example/livenote/strategy.go @@ -0,0 +1,67 @@ +package livenote + +import ( + "context" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// ID is the unique strategy ID, it needs to be in all lower case +// For example, grid strategy uses "grid" +const ID = "livenote" + +// log is a logrus.Entry that will be reused. +// This line attaches the strategy field to the logger with our ID, so that the logs from this strategy will be tagged with our ID +var log = logrus.WithField("strategy", ID) + +var ten = fixedpoint.NewFromInt(10) + +// init is a special function of golang, it will be called when the program is started +// importing this package will trigger the init function call. +func init() { + // Register our struct type to BBGO + // Note that you don't need to field the fields. + // BBGO uses reflect to parse your type information. + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +// Strategy is a struct that contains the settings of your strategy. +// These settings will be loaded from the BBGO YAML config file "bbgo.yaml" automatically. +type Strategy struct { + Symbol string `json:"symbol"` +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return ID + "-" + s.Symbol +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) +} + +// This strategy simply spent all available quote currency to buy the symbol whenever kline gets closed +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + callback := func(k types.KLine) { + // bbgo.PostLiveNote(k) + } + + // register our kline event handler + session.MarketDataStream.OnKLine(callback) + session.MarketDataStream.OnKLineClosed(callback) + + // if you need to do something when the user data stream is ready + // note that you only receive order update, trade update, balance update when the user data stream is connect. + session.UserDataStream.OnStart(func() { + log.Infof("connected") + }) + + return nil +} diff --git a/pkg/types/kline.go b/pkg/types/kline.go index bd31ed9a2..187646ccc 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -74,6 +74,10 @@ type KLine struct { Closed bool `json:"closed" db:"closed"` } +func (k *KLine) ObjectID() string { + return "kline-" + k.Symbol + k.Interval.String() + k.StartTime.Time().Format(time.RFC3339) +} + func (k *KLine) Set(o *KLine) { k.GID = o.GID k.Exchange = o.Exchange diff --git a/pkg/types/livenote.go b/pkg/types/livenote.go deleted file mode 100644 index b5d328a7d..000000000 --- a/pkg/types/livenote.go +++ /dev/null @@ -1,33 +0,0 @@ -package types - -type LiveNoteObject interface { - ObjectID() string -} - -type LiveNote struct { - // MessageID is the unique identifier of the message - // for slack, it's the timestamp of the message - MessageID string `json:"messageId"` - - ChannelID string `json:"channelId"` - - Object LiveNoteObject -} - -func NewLiveNote(object LiveNoteObject) *LiveNote { - return &LiveNote{ - Object: object, - } -} - -func (n *LiveNote) ObjectID() string { - return n.Object.ObjectID() -} - -func (n *LiveNote) SetMessageID(messageID string) { - n.MessageID = messageID -} - -func (n *LiveNote) SetChannelID(channelID string) { - n.ChannelID = channelID -}