all: resolve import cycle

This commit is contained in:
c9s 2022-08-23 02:12:26 +08:00
parent 0947c28294
commit c86b29e6dc
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
29 changed files with 267 additions and 245 deletions

View File

@ -30,6 +30,7 @@ import (
"github.com/c9s/bbgo/pkg/slack/slacklog" "github.com/c9s/bbgo/pkg/slack/slacklog"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/util"
"github.com/c9s/bbgo/pkg/util/templateutil"
) )
func init() { func init() {
@ -366,7 +367,7 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
case "$session": case "$session":
defaultOrderUpdateHandler := func(order types.Order) { defaultOrderUpdateHandler := func(order types.Order) {
text := util.Render(TemplateOrderReport, order) text := templateutil.Render(TemplateOrderReport, order)
Notify(text, &order) Notify(text, &order)
} }
for name := range environ.sessions { for name := range environ.sessions {
@ -376,7 +377,7 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
channel, ok := Notification.SessionChannelRouter.Route(name) channel, ok := Notification.SessionChannelRouter.Route(name)
if ok { if ok {
session.UserDataStream.OnOrderUpdate(func(order types.Order) { session.UserDataStream.OnOrderUpdate(func(order types.Order) {
text := util.Render(TemplateOrderReport, order) text := templateutil.Render(TemplateOrderReport, order)
NotifyTo(channel, text, &order) NotifyTo(channel, text, &order)
}) })
} else { } else {
@ -397,7 +398,7 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
// use same handler for each session // use same handler for each session
handler := func(order types.Order) { handler := func(order types.Order) {
text := util.Render(TemplateOrderReport, order) text := templateutil.Render(TemplateOrderReport, order)
channel, ok := Notification.RouteObject(&order) channel, ok := Notification.RouteObject(&order)
if ok { if ok {
NotifyTo(channel, text, &order) NotifyTo(channel, text, &order)

View File

@ -8,6 +8,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/interact" "github.com/c9s/bbgo/pkg/interact"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
@ -526,7 +527,7 @@ func (it *CoreInteraction) Initialize() error {
// getStrategySignature returns strategy instance unique signature // getStrategySignature returns strategy instance unique signature
func getStrategySignature(strategy SingleExchangeStrategy) (string, error) { func getStrategySignature(strategy SingleExchangeStrategy) (string, error) {
// Returns instance ID // Returns instance ID
var signature = CallID(strategy) var signature = dynamic.CallID(strategy)
if signature != "" { if signature != "" {
return signature, nil return signature, nil
} }

View File

@ -82,7 +82,7 @@ func (p *Persistence) Save(val interface{}, subIDs ...string) error {
} }
func (p *Persistence) Sync(obj interface{}) error { func (p *Persistence) Sync(obj interface{}) error {
id := CallID(obj) id := dynamic.CallID(obj)
if len(id) == 0 { if len(id) == 0 {
return nil return nil
} }
@ -93,7 +93,7 @@ func (p *Persistence) Sync(obj interface{}) error {
// Sync syncs the object properties into the persistence layer // Sync syncs the object properties into the persistence layer
func Sync(obj interface{}) { func Sync(obj interface{}) {
id := CallID(obj) id := dynamic.CallID(obj)
if len(id) == 0 { if len(id) == 0 {
log.Warnf("InstanceID() is not provided, can not sync persistence") log.Warnf("InstanceID() is not provided, can not sync persistence")
return return

View File

@ -57,13 +57,13 @@ func preparePersistentServices() []service.PersistenceService {
func Test_CallID(t *testing.T) { func Test_CallID(t *testing.T) {
t.Run("default", func(t *testing.T) { t.Run("default", func(t *testing.T) {
id := CallID(&TestStruct{}) id := dynamic.CallID(&TestStruct{})
assert.NotEmpty(t, id) assert.NotEmpty(t, id)
assert.Equal(t, "test-struct", id) assert.Equal(t, "test-struct", id)
}) })
t.Run("fallback", func(t *testing.T) { t.Run("fallback", func(t *testing.T) {
id := CallID(&TestStructWithoutInstanceID{Symbol: "BTCUSDT"}) id := dynamic.CallID(&TestStructWithoutInstanceID{Symbol: "BTCUSDT"})
assert.Equal(t, "test-struct-no-instance-id:BTCUSDT", id) assert.Equal(t, "test-struct-no-instance-id:BTCUSDT", id)
}) })
} }
@ -121,7 +121,7 @@ func Test_storePersistenceFields(t *testing.T) {
for _, ps := range pss { for _, ps := range pss {
psName := reflect.TypeOf(ps).Elem().String() psName := reflect.TypeOf(ps).Elem().String()
t.Run("all/"+psName, func(t *testing.T) { t.Run("all/"+psName, func(t *testing.T) {
id := CallID(a) id := dynamic.CallID(a)
err := storePersistenceFields(a, id, ps) err := storePersistenceFields(a, id, ps)
assert.NoError(t, err) assert.NoError(t, err)

View File

@ -1,32 +1,2 @@
package bbgo package bbgo
import (
"reflect"
"github.com/c9s/bbgo/pkg/dynamic"
)
type InstanceIDProvider interface {
InstanceID() string
}
func CallID(obj interface{}) string {
sv := reflect.ValueOf(obj)
st := reflect.TypeOf(obj)
if st.Implements(reflect.TypeOf((*InstanceIDProvider)(nil)).Elem()) {
m := sv.MethodByName("InstanceID")
ret := m.Call(nil)
return ret[0].String()
}
if symbol, ok := dynamic.LookupSymbolField(sv); ok {
m := sv.MethodByName("ID")
ret := m.Call(nil)
return ret[0].String() + ":" + symbol
}
// fallback to just ID
m := sv.MethodByName("ID")
ret := m.Call(nil)
return ret[0].String() + ":"
}

View File

@ -14,6 +14,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/c9s/bbgo/pkg/cache" "github.com/c9s/bbgo/pkg/cache"
"github.com/c9s/bbgo/pkg/util/templateutil"
exchange2 "github.com/c9s/bbgo/pkg/exchange" exchange2 "github.com/c9s/bbgo/pkg/exchange"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
@ -848,7 +849,7 @@ func (session *ExchangeSession) SlackAttachment() slack.Attachment {
Title: session.Name, Title: session.Name,
Fields: fields, Fields: fields,
FooterIcon: footerIcon, FooterIcon: footerIcon,
Footer: util.Render("update time {{ . }}", time.Now().Format(time.RFC822)), Footer: templateutil.Render("update time {{ . }}", time.Now().Format(time.RFC822)),
} }
} }

View File

@ -384,7 +384,7 @@ func (trader *Trader) LoadState() error {
log.Infof("loading strategies states...") log.Infof("loading strategies states...")
return trader.IterateStrategies(func(strategy StrategyID) error { return trader.IterateStrategies(func(strategy StrategyID) error {
id := CallID(strategy) id := dynamic.CallID(strategy)
return loadPersistenceFields(strategy, id, ps) return loadPersistenceFields(strategy, id, ps)
}) })
} }
@ -420,7 +420,7 @@ func (trader *Trader) SaveState() error {
log.Infof("saving strategies states...") log.Infof("saving strategies states...")
return trader.IterateStrategies(func(strategy StrategyID) error { return trader.IterateStrategies(func(strategy StrategyID) error {
id := CallID(strategy) id := dynamic.CallID(strategy)
if len(id) == 0 { if len(id) == 0 {
return nil return nil
} }

30
pkg/dynamic/id.go Normal file
View File

@ -0,0 +1,30 @@
package dynamic
import (
"reflect"
)
type InstanceIDProvider interface {
InstanceID() string
}
func CallID(obj interface{}) string {
sv := reflect.ValueOf(obj)
st := reflect.TypeOf(obj)
if st.Implements(reflect.TypeOf((*InstanceIDProvider)(nil)).Elem()) {
m := sv.MethodByName("InstanceID")
ret := m.Call(nil)
return ret[0].String()
}
if symbol, ok := LookupSymbolField(sv); ok {
m := sv.MethodByName("ID")
ret := m.Call(nil)
return ret[0].String() + ":" + symbol
}
// fallback to just ID
m := sv.MethodByName("ID")
ret := m.Call(nil)
return ret[0].String() + ":"
}

View File

@ -1,4 +1,4 @@
package util package dynamic
import ( import (
"fmt" "fmt"
@ -11,25 +11,10 @@ import (
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text" "github.com/jedib0t/go-pretty/v6/text"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
) )
func NewDefaultTableStyle() *table.Style {
style := table.Style{
Name: "StyleRounded",
Box: table.StyleBoxRounded,
Format: table.FormatOptionsDefault,
HTML: table.DefaultHTMLOptions,
Options: table.OptionsDefault,
Title: table.TitleOptionsDefault,
Color: table.ColorOptionsYellowWhiteOnBlack,
}
style.Color.Row = text.Colors{text.FgHiYellow, text.BgHiBlack}
style.Color.RowAlternate = text.Colors{text.FgYellow, text.BgBlack}
return &style
}
func DefaultWhiteList() []string { func DefaultWhiteList() []string {
return []string{"Window", "Interval", "Symbol"} return []string{"Window", "Interval", "Symbol"}
} }
@ -58,7 +43,7 @@ func PrintConfig(s interface{}, f io.Writer, style *table.Style, withColor bool,
}) })
t.AppendHeader(table.Row{"json", "struct field name", "type", "value"}) t.AppendHeader(table.Row{"json", "struct field name", "type", "value"})
} }
write(f, "---- %s Settings ---\n", bbgo.CallID(s)) write(f, "---- %s Settings ---\n", CallID(s))
embeddedWhiteSet := map[string]struct{}{} embeddedWhiteSet := map[string]struct{}{}
for _, whiteList := range whiteLists { for _, whiteList := range whiteLists {
@ -71,7 +56,7 @@ func PrintConfig(s interface{}, f io.Writer, style *table.Style, withColor bool,
val := reflect.ValueOf(s) val := reflect.ValueOf(s)
if val.Type().Kind() == Pointer { if val.Type().Kind() == util.Pointer {
val = val.Elem() val = val.Elem()
} }
var values types.JsonArr var values types.JsonArr
@ -88,7 +73,7 @@ func PrintConfig(s interface{}, f io.Writer, style *table.Style, withColor bool,
if t.Anonymous { if t.Anonymous {
var target reflect.Type var target reflect.Type
var field reflect.Value var field reflect.Value
if t.Type.Kind() == Pointer { if t.Type.Kind() == util.Pointer {
target = t.Type.Elem() target = t.Type.Elem()
field = val.Field(i).Elem() field = val.Field(i).Elem()
} else { } else {

View File

@ -6,10 +6,10 @@ import (
"reflect" "reflect"
"unsafe" "unsafe"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/util"
"github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/table"
"github.com/c9s/bbgo/pkg/dynamic"
style2 "github.com/c9s/bbgo/pkg/style"
) )
func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) { func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) {
@ -95,7 +95,7 @@ func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) {
func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) { func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) {
var style *table.Style var style *table.Style
if pretty { if pretty {
style = util.NewDefaultTableStyle() style = style2.NewDefaultTableStyle()
} }
util.PrintConfig(s, f, style, len(withColor) > 0 && withColor[0], util.DefaultWhiteList()...) dynamic.PrintConfig(s, f, style, len(withColor) > 0 && withColor[0], dynamic.DefaultWhiteList()...)
} }

View File

@ -15,6 +15,7 @@ import (
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/util"
"github.com/c9s/bbgo/pkg/util/templateutil"
) )
const ID = "xbalance" const ID = "xbalance"
@ -39,7 +40,7 @@ func (s *State) IsOver24Hours() bool {
} }
func (s *State) PlainText() string { func (s *State) PlainText() string {
return util.Render(`{{ .Asset }} transfer stats: return templateutil.Render(`{{ .Asset }} transfer stats:
daily number of transfers: {{ .DailyNumberOfTransfers }} daily number of transfers: {{ .DailyNumberOfTransfers }}
daily amount of transfers {{ .DailyAmountOfTransfers.Float64 }}`, s) daily amount of transfers {{ .DailyAmountOfTransfers.Float64 }}`, s)
} }
@ -53,12 +54,12 @@ func (s *State) SlackAttachment() slack.Attachment {
{Title: "Total Number of Transfers", Value: fmt.Sprintf("%d", s.DailyNumberOfTransfers), Short: true}, {Title: "Total Number of Transfers", Value: fmt.Sprintf("%d", s.DailyNumberOfTransfers), Short: true},
{Title: "Total Amount of Transfers", Value: util.FormatFloat(s.DailyAmountOfTransfers.Float64(), 4), Short: true}, {Title: "Total Amount of Transfers", Value: util.FormatFloat(s.DailyAmountOfTransfers.Float64(), 4), Short: true},
}, },
Footer: util.Render("Since {{ . }}", time.Unix(s.Since, 0).Format(time.RFC822)), Footer: templateutil.Render("Since {{ . }}", time.Unix(s.Since, 0).Format(time.RFC822)),
} }
} }
func (s *State) Reset() { func (s *State) Reset() {
var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local()) var beginningOfTheDay = types.BeginningOfTheDay(time.Now().Local())
*s = State{ *s = State{
DailyNumberOfTransfers: 0, DailyNumberOfTransfers: 0,
DailyAmountOfTransfers: fixedpoint.Zero, DailyAmountOfTransfers: fixedpoint.Zero,
@ -93,7 +94,7 @@ func (r *WithdrawalRequest) PlainText() string {
func (r *WithdrawalRequest) SlackAttachment() slack.Attachment { func (r *WithdrawalRequest) SlackAttachment() slack.Attachment {
var color = "#DC143C" var color = "#DC143C"
title := util.Render(`Withdraw Request {{ .Asset }}`, r) title := templateutil.Render(`Withdraw Request {{ .Asset }}`, r)
return slack.Attachment{ return slack.Attachment{
// Pretext: "", // Pretext: "",
// Text: text, // Text: text,
@ -105,7 +106,7 @@ func (r *WithdrawalRequest) SlackAttachment() slack.Attachment {
{Title: "From", Value: r.FromSession}, {Title: "From", Value: r.FromSession},
{Title: "To", Value: r.ToSession}, {Title: "To", Value: r.ToSession},
}, },
Footer: util.Render("Time {{ . }}", time.Now().Format(time.RFC822)), Footer: templateutil.Render("Time {{ . }}", time.Now().Format(time.RFC822)),
// FooterIcon: "", // FooterIcon: "",
} }
} }

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/util/templateutil"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -32,11 +33,11 @@ type State struct {
} }
func (s *State) IsOver24Hours() bool { func (s *State) IsOver24Hours() bool {
return util.Over24Hours(time.Unix(s.Since, 0)) return types.Over24Hours(time.Unix(s.Since, 0))
} }
func (s *State) PlainText() string { func (s *State) PlainText() string {
return util.Render(`{{ .Asset }} transfer stats: return templateutil.Render(`{{ .Asset }} transfer stats:
daily number of transfers: {{ .DailyNumberOfTransfers }} daily number of transfers: {{ .DailyNumberOfTransfers }}
daily amount of transfers {{ .DailyAmountOfTransfers.Float64 }}`, s) daily amount of transfers {{ .DailyAmountOfTransfers.Float64 }}`, s)
} }
@ -46,12 +47,12 @@ func (s *State) SlackAttachment() slack.Attachment {
// Pretext: "", // Pretext: "",
// Text: text, // Text: text,
Fields: []slack.AttachmentField{}, Fields: []slack.AttachmentField{},
Footer: util.Render("Since {{ . }}", time.Unix(s.Since, 0).Format(time.RFC822)), Footer: templateutil.Render("Since {{ . }}", time.Unix(s.Since, 0).Format(time.RFC822)),
} }
} }
func (s *State) Reset() { func (s *State) Reset() {
var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local()) var beginningOfTheDay = types.BeginningOfTheDay(time.Now().Local())
*s = State{ *s = State{
Since: beginningOfTheDay.Unix(), Since: beginningOfTheDay.Unix(),
} }

5
pkg/style/colors.go Normal file
View File

@ -0,0 +1,5 @@
package style
const GreenColor = "#228B22"
const RedColor = "#800000"
const GrayColor = "#f0f0f0"

61
pkg/style/pnl.go Normal file
View File

@ -0,0 +1,61 @@
package style
import (
"github.com/c9s/bbgo/pkg/fixedpoint"
)
var LossEmoji = "🔥"
var ProfitEmoji = "💰"
var DefaultPnLLevelResolution = fixedpoint.NewFromFloat(0.001)
func PnLColor(pnl fixedpoint.Value) string {
if pnl.Sign() > 0 {
return GreenColor
}
return RedColor
}
func PnLSignString(pnl fixedpoint.Value) string {
if pnl.Sign() > 0 {
return "+" + pnl.String()
}
return pnl.String()
}
func PnLEmojiSimple(pnl fixedpoint.Value) string {
if pnl.Sign() < 0 {
return LossEmoji
}
if pnl.IsZero() {
return ""
}
return ProfitEmoji
}
func PnLEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) {
if margin.IsZero() {
return PnLEmojiSimple(pnl)
}
if pnl.Sign() < 0 {
out = LossEmoji
level := (margin.Neg()).Div(resolution).Int()
for i := 1; i < level; i++ {
out += LossEmoji
}
return out
}
if pnl.IsZero() {
return out
}
out = ProfitEmoji
level := margin.Div(resolution).Int()
for i := 1; i < level; i++ {
out += ProfitEmoji
}
return out
}

21
pkg/style/table.go Normal file
View File

@ -0,0 +1,21 @@
package style
import (
"github.com/jedib0t/go-pretty/v6/table"
"github.com/jedib0t/go-pretty/v6/text"
)
func NewDefaultTableStyle() *table.Style {
style := table.Style{
Name: "StyleRounded",
Box: table.StyleBoxRounded,
Format: table.FormatOptionsDefault,
HTML: table.DefaultHTMLOptions,
Options: table.OptionsDefault,
Title: table.TitleOptionsDefault,
Color: table.ColorOptionsYellowWhiteOnBlack,
}
style.Color.Row = text.Colors{text.FgHiYellow, text.BgHiBlack}
style.Color.RowAlternate = text.Colors{text.FgYellow, text.BgBlack}
return &style
}

2
pkg/types/colors.go Normal file
View File

@ -0,0 +1,2 @@
package types

5
pkg/types/instance.go Normal file
View File

@ -0,0 +1,5 @@
package types
type InstanceIDProvider interface {
InstanceID() string
}

View File

@ -7,7 +7,7 @@ import (
"github.com/slack-go/slack" "github.com/slack-go/slack"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/style"
) )
type Direction int type Direction int
@ -214,11 +214,11 @@ func (k KLine) GetChange() fixedpoint.Value {
func (k KLine) Color() string { func (k KLine) Color() string {
if k.Direction() > 0 { if k.Direction() > 0 {
return util.GreenColor return style.GreenColor
} else if k.Direction() < 0 { } else if k.Direction() < 0 {
return util.RedColor return style.RedColor
} }
return util.GrayColor return style.GrayColor
} }
func (k KLine) String() string { func (k KLine) String() string {
@ -237,29 +237,29 @@ func (k KLine) SlackAttachment() slack.Attachment {
Text: fmt.Sprintf("*%s* KLine %s", k.Symbol, k.Interval), Text: fmt.Sprintf("*%s* KLine %s", k.Symbol, k.Interval),
Color: k.Color(), Color: k.Color(),
Fields: []slack.AttachmentField{ Fields: []slack.AttachmentField{
{Title: "Open", Value: util.FormatValue(k.Open, 2), Short: true}, {Title: "Open", Value: k.Open.FormatString(2), Short: true},
{Title: "High", Value: util.FormatValue(k.High, 2), Short: true}, {Title: "High", Value: k.High.FormatString(2), Short: true},
{Title: "Low", Value: util.FormatValue(k.Low, 2), Short: true}, {Title: "Low", Value: k.Low.FormatString(2), Short: true},
{Title: "Close", Value: util.FormatValue(k.Close, 2), Short: true}, {Title: "Close", Value: k.Close.FormatString(2), Short: true},
{Title: "Mid", Value: util.FormatValue(k.Mid(), 2), Short: true}, {Title: "Mid", Value: k.Mid().FormatString(2), Short: true},
{Title: "Change", Value: util.FormatValue(k.GetChange(), 2), Short: true}, {Title: "Change", Value: k.GetChange().FormatString(2), Short: true},
{Title: "Volume", Value: util.FormatValue(k.Volume, 2), Short: true}, {Title: "Volume", Value: k.Volume.FormatString(2), Short: true},
{Title: "Taker Buy Base Volume", Value: util.FormatValue(k.TakerBuyBaseAssetVolume, 2), Short: true}, {Title: "Taker Buy Base Volume", Value: k.TakerBuyBaseAssetVolume.FormatString(2), Short: true},
{Title: "Taker Buy Quote Volume", Value: util.FormatValue(k.TakerBuyQuoteAssetVolume, 2), Short: true}, {Title: "Taker Buy Quote Volume", Value: k.TakerBuyQuoteAssetVolume.FormatString(2), Short: true},
{Title: "Max Change", Value: util.FormatValue(k.GetMaxChange(), 2), Short: true}, {Title: "Max Change", Value: k.GetMaxChange().FormatString(2), Short: true},
{ {
Title: "Thickness", Title: "Thickness",
Value: util.FormatValue(k.GetThickness(), 4), Value: k.GetThickness().FormatString(4),
Short: true, Short: true,
}, },
{ {
Title: "UpperShadowRatio", Title: "UpperShadowRatio",
Value: util.FormatValue(k.GetUpperShadowRatio(), 4), Value: k.GetUpperShadowRatio().FormatString(4),
Short: true, Short: true,
}, },
{ {
Title: "LowerShadowRatio", Title: "LowerShadowRatio",
Value: util.FormatValue(k.GetLowerShadowRatio(), 4), Value: k.GetLowerShadowRatio().FormatString(4),
Short: true, Short: true,
}, },
}, },
@ -368,11 +368,11 @@ func (k KLineWindow) GetTrend() int {
func (k KLineWindow) Color() string { func (k KLineWindow) Color() string {
if k.GetTrend() > 0 { if k.GetTrend() > 0 {
return util.GreenColor return style.GreenColor
} else if k.GetTrend() < 0 { } else if k.GetTrend() < 0 {
return util.RedColor return style.RedColor
} }
return util.GrayColor return style.GrayColor
} }
// Mid price // Mid price
@ -491,34 +491,34 @@ func (k KLineWindow) SlackAttachment() slack.Attachment {
Text: fmt.Sprintf("*%s* KLineWindow %s x %d", first.Symbol, first.Interval, windowSize), Text: fmt.Sprintf("*%s* KLineWindow %s x %d", first.Symbol, first.Interval, windowSize),
Color: k.Color(), Color: k.Color(),
Fields: []slack.AttachmentField{ Fields: []slack.AttachmentField{
{Title: "Open", Value: util.FormatValue(k.GetOpen(), 2), Short: true}, {Title: "Open", Value: k.GetOpen().FormatString(2), Short: true},
{Title: "High", Value: util.FormatValue(k.GetHigh(), 2), Short: true}, {Title: "High", Value: k.GetHigh().FormatString(2), Short: true},
{Title: "Low", Value: util.FormatValue(k.GetLow(), 2), Short: true}, {Title: "Low", Value: k.GetLow().FormatString(2), Short: true},
{Title: "Close", Value: util.FormatValue(k.GetClose(), 2), Short: true}, {Title: "Close", Value: k.GetClose().FormatString(2), Short: true},
{Title: "Mid", Value: util.FormatValue(k.Mid(), 2), Short: true}, {Title: "Mid", Value: k.Mid().FormatPercentage(2), Short: true},
{ {
Title: "Change", Title: "Change",
Value: util.FormatValue(k.GetChange(), 2), Value: k.GetChange().FormatString(2),
Short: true, Short: true,
}, },
{ {
Title: "Max Change", Title: "Max Change",
Value: util.FormatValue(k.GetMaxChange(), 2), Value: k.GetMaxChange().FormatString(2),
Short: true, Short: true,
}, },
{ {
Title: "Thickness", Title: "Thickness",
Value: util.FormatValue(k.GetThickness(), 4), Value: k.GetThickness().FormatString(4),
Short: true, Short: true,
}, },
{ {
Title: "UpperShadowRatio", Title: "UpperShadowRatio",
Value: util.FormatValue(k.GetUpperShadowRatio(), 4), Value: k.GetUpperShadowRatio().FormatString(4),
Short: true, Short: true,
}, },
{ {
Title: "LowerShadowRatio", Title: "LowerShadowRatio",
Value: util.FormatValue(k.GetLowerShadowRatio(), 4), Value: k.GetLowerShadowRatio().FormatString(4),
Short: true, Short: true,
}, },
}, },

View File

@ -11,7 +11,7 @@ import (
"github.com/slack-go/slack" "github.com/slack-go/slack"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/util/templateutil"
) )
func init() { func init() {
@ -384,6 +384,6 @@ func (o Order) SlackAttachment() slack.Attachment {
// Text: "", // Text: "",
Fields: fields, Fields: fields,
FooterIcon: footerIcon, FooterIcon: footerIcon,
Footer: strings.ToLower(o.Exchange.String()) + util.Render(" creation time {{ . }}", o.CreationTime.Time().Format(time.StampMilli)), Footer: strings.ToLower(o.Exchange.String()) + templateutil.Render(" creation time {{ . }}", o.CreationTime.Time().Format(time.StampMilli)),
} }
} }

View File

@ -8,7 +8,7 @@ import (
"github.com/slack-go/slack" "github.com/slack-go/slack"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/util/templateutil"
) )
type PositionType string type PositionType string
@ -352,7 +352,7 @@ func (p *Position) SlackAttachment() slack.Attachment {
color = "#DC143C" color = "#DC143C"
} }
title := util.Render(string(posType)+` Position {{ .Symbol }} `, p) title := templateutil.Render(string(posType)+` Position {{ .Symbol }} `, p)
fields := []slack.AttachmentField{ fields := []slack.AttachmentField{
{Title: "Average Cost", Value: averageCost.String() + " " + p.QuoteCurrency, Short: true}, {Title: "Average Cost", Value: averageCost.String() + " " + p.QuoteCurrency, Short: true},
@ -378,7 +378,7 @@ func (p *Position) SlackAttachment() slack.Attachment {
Title: title, Title: title,
Color: color, Color: color,
Fields: fields, Fields: fields,
Footer: util.Render("update time {{ . }}", time.Now().Format(time.RFC822)), Footer: templateutil.Render("update time {{ . }}", time.Now().Format(time.RFC822)),
// FooterIcon: "", // FooterIcon: "",
} }
} }

View File

@ -7,7 +7,7 @@ import (
"github.com/slack-go/slack" "github.com/slack-go/slack"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/style"
) )
// Profit struct stores the PnL information // Profit struct stores the PnL information
@ -64,17 +64,17 @@ type Profit struct {
} }
func (p *Profit) SlackAttachment() slack.Attachment { func (p *Profit) SlackAttachment() slack.Attachment {
var color = util.PnLColor(p.Profit) var color = style.PnLColor(p.Profit)
var title = fmt.Sprintf("%s PnL ", p.Symbol) var title = fmt.Sprintf("%s PnL ", p.Symbol)
title += util.PnLEmojiMargin(p.Profit, p.ProfitMargin, util.DefaultPnLLevelResolution) + " " title += style.PnLEmojiMargin(p.Profit, p.ProfitMargin, style.DefaultPnLLevelResolution) + " "
title += util.PnLSignString(p.Profit) + " " + p.QuoteCurrency title += style.PnLSignString(p.Profit) + " " + p.QuoteCurrency
var fields []slack.AttachmentField var fields []slack.AttachmentField
if !p.NetProfit.IsZero() { if !p.NetProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Net Profit", Title: "Net Profit",
Value: util.PnLSignString(p.NetProfit) + " " + p.QuoteCurrency, Value: style.PnLSignString(p.NetProfit) + " " + p.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -130,9 +130,9 @@ func (p *Profit) SlackAttachment() slack.Attachment {
func (p *Profit) PlainText() string { func (p *Profit) PlainText() string {
var emoji string var emoji string
if !p.ProfitMargin.IsZero() { if !p.ProfitMargin.IsZero() {
emoji = util.PnLEmojiMargin(p.Profit, p.ProfitMargin, util.DefaultPnLLevelResolution) emoji = style.PnLEmojiMargin(p.Profit, p.ProfitMargin, style.DefaultPnLLevelResolution)
} else { } else {
emoji = util.PnLEmojiSimple(p.Profit) emoji = style.PnLEmojiSimple(p.Profit)
} }
return fmt.Sprintf("%s trade profit %s %s %s (%s), net profit =~ %s %s (%s)", return fmt.Sprintf("%s trade profit %s %s %s (%s), net profit =~ %s %s (%s)",
@ -220,7 +220,7 @@ func (s *ProfitStats) ResetToday() {
s.TodayGrossProfit = fixedpoint.Zero s.TodayGrossProfit = fixedpoint.Zero
s.TodayGrossLoss = fixedpoint.Zero s.TodayGrossLoss = fixedpoint.Zero
var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local()) var beginningOfTheDay = BeginningOfTheDay(time.Now().Local())
s.TodaySince = beginningOfTheDay.Unix() s.TodaySince = beginningOfTheDay.Unix()
} }
@ -247,8 +247,8 @@ func (s *ProfitStats) PlainText() string {
} }
func (s *ProfitStats) SlackAttachment() slack.Attachment { func (s *ProfitStats) SlackAttachment() slack.Attachment {
var color = util.PnLColor(s.AccumulatedPnL) var color = style.PnLColor(s.AccumulatedPnL)
var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, util.PnLSignString(s.AccumulatedPnL), s.QuoteCurrency) var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, style.PnLSignString(s.AccumulatedPnL), s.QuoteCurrency)
since := time.Unix(s.AccumulatedSince, 0).Local() since := time.Unix(s.AccumulatedSince, 0).Local()
title += " Since " + since.Format(time.RFC822) title += " Since " + since.Format(time.RFC822)
@ -258,7 +258,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.TodayPnL.IsZero() { if !s.TodayPnL.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "P&L Today", Title: "P&L Today",
Value: util.PnLSignString(s.TodayPnL) + " " + s.QuoteCurrency, Value: style.PnLSignString(s.TodayPnL) + " " + s.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -266,7 +266,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.TodayNetProfit.IsZero() { if !s.TodayNetProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Net Profit Today", Title: "Net Profit Today",
Value: util.PnLSignString(s.TodayNetProfit) + " " + s.QuoteCurrency, Value: style.PnLSignString(s.TodayNetProfit) + " " + s.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -274,7 +274,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.TodayGrossProfit.IsZero() { if !s.TodayGrossProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Gross Profit Today", Title: "Gross Profit Today",
Value: util.PnLSignString(s.TodayGrossProfit) + " " + s.QuoteCurrency, Value: style.PnLSignString(s.TodayGrossProfit) + " " + s.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -282,7 +282,7 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.TodayGrossLoss.IsZero() { if !s.TodayGrossLoss.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Gross Loss Today", Title: "Gross Loss Today",
Value: util.PnLSignString(s.TodayGrossLoss) + " " + s.QuoteCurrency, Value: style.PnLSignString(s.TodayGrossLoss) + " " + s.QuoteCurrency,
Short: true, Short: true,
}) })
} }
@ -290,28 +290,28 @@ func (s *ProfitStats) SlackAttachment() slack.Attachment {
if !s.AccumulatedPnL.IsZero() { if !s.AccumulatedPnL.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Accumulated P&L", Title: "Accumulated P&L",
Value: util.PnLSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency, Value: style.PnLSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency,
}) })
} }
if !s.AccumulatedGrossProfit.IsZero() { if !s.AccumulatedGrossProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Accumulated Gross Profit", Title: "Accumulated Gross Profit",
Value: util.PnLSignString(s.AccumulatedGrossProfit) + " " + s.QuoteCurrency, Value: style.PnLSignString(s.AccumulatedGrossProfit) + " " + s.QuoteCurrency,
}) })
} }
if !s.AccumulatedGrossLoss.IsZero() { if !s.AccumulatedGrossLoss.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Accumulated Gross Loss", Title: "Accumulated Gross Loss",
Value: util.PnLSignString(s.AccumulatedGrossLoss) + " " + s.QuoteCurrency, Value: style.PnLSignString(s.AccumulatedGrossLoss) + " " + s.QuoteCurrency,
}) })
} }
if !s.AccumulatedNetProfit.IsZero() { if !s.AccumulatedNetProfit.IsZero() {
fields = append(fields, slack.AttachmentField{ fields = append(fields, slack.AttachmentField{
Title: "Accumulated Net Profit", Title: "Accumulated Net Profit",
Value: util.PnLSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency, Value: style.PnLSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency,
}) })
} }

View File

@ -6,7 +6,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/style"
) )
// SideType define side type of order // SideType define side type of order
@ -76,14 +76,14 @@ func (side SideType) String() string {
func (side SideType) Color() string { func (side SideType) Color() string {
if side == SideTypeBuy { if side == SideTypeBuy {
return util.GreenColor return style.GreenColor
} }
if side == SideTypeSell { if side == SideTypeSell {
return util.RedColor return style.RedColor
} }
return util.GrayColor return style.GrayColor
} }
func SideToColorName(side SideType) string { func SideToColorName(side SideType) string {

View File

@ -7,8 +7,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/c9s/bbgo/pkg/util"
) )
var numOfDigitsOfUnixTimestamp = len(strconv.FormatInt(time.Now().Unix(), 10)) var numOfDigitsOfUnixTimestamp = len(strconv.FormatInt(time.Now().Unix(), 10))
@ -264,7 +262,7 @@ func ParseLooseFormatTime(s string) (LooseFormatTime, error) {
} }
tv, err := util.ParseTimeWithFormats(s, looseTimeFormats) tv, err := ParseTimeWithFormats(s, looseTimeFormats)
if err != nil { if err != nil {
return LooseFormatTime{}, err return LooseFormatTime{}, err
} }
@ -294,7 +292,7 @@ func (t *LooseFormatTime) UnmarshalJSON(data []byte) error {
return err return err
} }
tv, err := util.ParseTimeWithFormats(v, looseTimeFormats) tv, err := ParseTimeWithFormats(v, looseTimeFormats)
if err != nil { if err != nil {
return err return err
} }
@ -340,3 +338,23 @@ func (t *Timestamp) UnmarshalJSON(o []byte) error {
*t = Timestamp(time.Unix(timestamp, 0)) *t = Timestamp(time.Unix(timestamp, 0))
return nil return nil
} }
func ParseTimeWithFormats(strTime string, formats []string) (time.Time, error) {
for _, format := range formats {
tt, err := time.Parse(format, strTime)
if err == nil {
return tt, nil
}
}
return time.Time{}, fmt.Errorf("failed to parse time %s, valid formats are %+v", strTime, formats)
}
func BeginningOfTheDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func Over24Hours(since time.Time) bool {
return time.Since(since) >= 24*time.Hour
}

View File

@ -11,7 +11,7 @@ import (
"github.com/slack-go/slack" "github.com/slack-go/slack"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/util" "github.com/c9s/bbgo/pkg/util/templateutil"
) )
func init() { func init() {
@ -184,7 +184,7 @@ func (trade Trade) SlackAttachment() slack.Attachment {
} }
liquidity := trade.Liquidity() liquidity := trade.Liquidity()
text := util.Render(slackTradeTextTemplate, trade) text := templateutil.Render(slackTradeTextTemplate, trade)
footerIcon := ExchangeFooterIcon(trade.Exchange) footerIcon := ExchangeFooterIcon(trade.Exchange)
return slack.Attachment{ return slack.Attachment{
@ -203,7 +203,7 @@ func (trade Trade) SlackAttachment() slack.Attachment {
{Title: "Order ID", Value: strconv.FormatUint(trade.OrderID, 10), Short: true}, {Title: "Order ID", Value: strconv.FormatUint(trade.OrderID, 10), Short: true},
}, },
FooterIcon: footerIcon, FooterIcon: footerIcon,
Footer: strings.ToLower(trade.Exchange.String()) + util.Render(" creation time {{ . }}", trade.Time.Time().Format(time.StampMilli)), Footer: strings.ToLower(trade.Exchange.String()) + templateutil.Render(" creation time {{ . }}", trade.Time.Time().Format(time.StampMilli)),
} }
} }

View File

@ -1,5 +1,2 @@
package util package util
const GreenColor = "#228B22"
const RedColor = "#800000"
const GrayColor = "#f0f0f0"

View File

@ -1,63 +1,3 @@
package util package util
import (
"github.com/c9s/bbgo/pkg/fixedpoint"
)
var LossEmoji = "🔥"
var ProfitEmoji = "💰"
var DefaultPnLLevelResolution = fixedpoint.NewFromFloat(0.001)
func PnLColor(pnl fixedpoint.Value) string {
if pnl.Sign() > 0 {
return GreenColor
}
return RedColor
}
func PnLSignString(pnl fixedpoint.Value) string {
if pnl.Sign() > 0 {
return "+" + pnl.String()
}
return pnl.String()
}
func PnLEmojiSimple(pnl fixedpoint.Value) string {
if pnl.Sign() < 0 {
return LossEmoji
}
if pnl.IsZero() {
return ""
}
return ProfitEmoji
}
func PnLEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) {
if margin.IsZero() {
return PnLEmojiSimple(pnl)
}
if pnl.Sign() < 0 {
out = LossEmoji
level := (margin.Neg()).Div(resolution).Int()
for i := 1; i < level; i++ {
out += LossEmoji
}
return out
}
if pnl.IsZero() {
return out
}
out = ProfitEmoji
level := margin.Div(resolution).Int()
for i := 1; i < level; i++ {
out += ProfitEmoji
}
return out
}

View File

@ -1,25 +1,2 @@
package util package util
import (
"bytes"
"text/template"
"github.com/sirupsen/logrus"
)
func Render(tpl string, args interface{}) string {
var buf = bytes.NewBuffer(nil)
tmpl, err := template.New("tmp").Parse(tpl)
if err != nil {
logrus.WithError(err).Error("template parse error")
return ""
}
err = tmpl.Execute(buf, args)
if err != nil {
logrus.WithError(err).Error("template execute error")
return ""
}
return buf.String()
}

View File

@ -0,0 +1,25 @@
package templateutil
import (
"bytes"
"text/template"
"github.com/sirupsen/logrus"
)
func Render(tpl string, args interface{}) string {
var buf = bytes.NewBuffer(nil)
tmpl, err := template.New("tmp").Parse(tpl)
if err != nil {
logrus.WithError(err).Error("template parse error")
return ""
}
err = tmpl.Execute(buf, args)
if err != nil {
logrus.WithError(err).Error("template execute error")
return ""
}
return buf.String()
}

View File

@ -1,7 +1,6 @@
package util package util
import ( import (
"fmt"
"math/rand" "math/rand"
"time" "time"
) )
@ -11,25 +10,7 @@ func MillisecondsJitter(d time.Duration, jitterInMilliseconds int) time.Duration
return d + time.Duration(n)*time.Millisecond return d + time.Duration(n)*time.Millisecond
} }
func BeginningOfTheDay(t time.Time) time.Time {
year, month, day := t.Date()
return time.Date(year, month, day, 0, 0, 0, 0, t.Location())
}
func Over24Hours(since time.Time) bool {
return time.Since(since) >= 24*time.Hour
}
func UnixMilli() int64 { func UnixMilli() int64 {
return time.Now().UnixNano() / int64(time.Millisecond) return time.Now().UnixNano() / int64(time.Millisecond)
} }
func ParseTimeWithFormats(strTime string, formats []string) (time.Time, error) {
for _, format := range formats {
tt, err := time.Parse(format, strTime)
if err == nil {
return tt, nil
}
}
return time.Time{}, fmt.Errorf("failed to parse time %s, valid formats are %+v", strTime, formats)
}