From 5e7ea716138d5b16e128e60c08f6561c65a36330 Mon Sep 17 00:00:00 2001 From: zenix Date: Wed, 17 Aug 2022 13:42:01 +0900 Subject: [PATCH] feature: withdraw print config functionality from drift to be a general function --- pkg/bbgo/exit.go | 43 +++++++++++ pkg/bbgo/interact.go | 2 +- pkg/bbgo/persistence.go | 4 +- pkg/bbgo/persistence_test.go | 9 +-- pkg/bbgo/reflect.go | 3 +- pkg/bbgo/trader.go | 4 +- pkg/strategy/drift/output.go | 119 ++++------------------------ pkg/strategy/output.go | 146 +++++++++++++++++++++++++++++++++++ 8 files changed, 214 insertions(+), 116 deletions(-) create mode 100644 pkg/strategy/output.go diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index 5352e5582..9084547c7 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -1,6 +1,8 @@ package bbgo import ( + "bytes" + "encoding/json" "reflect" "github.com/pkg/errors" @@ -35,6 +37,47 @@ type ExitMethod struct { TrailingStop *TrailingStop2 `json:"trailingStop"` } +func (e ExitMethod) String() string { + var buf bytes.Buffer + if e.RoiStopLoss != nil { + b, _ := json.Marshal(e.RoiStopLoss) + if string(b) != "null" { + buf.WriteString("roiStopLoss: " + string(b) + ", ") + } + } + if e.ProtectiveStopLoss != nil { + b, _ := json.Marshal(e.ProtectiveStopLoss) + if string(b) != "null" { + buf.WriteString("protectiveStopLoss: " + string(b) + ", ") + } + } + if e.RoiTakeProfit != nil { + b, _ := json.Marshal(e.RoiTakeProfit) + if string(b) != "null" { + buf.WriteString("rioTakeProft: " + string(b) + ", ") + } + } + if e.LowerShadowTakeProfit == nil { + b, _ := json.Marshal(e.LowerShadowTakeProfit) + if string(b) != "null" { + buf.WriteString("lowerShadowTakeProft: " + string(b) + ", ") + } + } + if e.CumulatedVolumeTakeProfit == nil { + b, _ := json.Marshal(e.CumulatedVolumeTakeProfit) + if string(b) != "null" { + buf.WriteString("cumulatedVolumeTakeProfit: " + string(b) + ", ") + } + } + if e.TrailingStop == nil { + b, _ := json.Marshal(e.TrailingStop) + if string(b) != "null" { + buf.WriteString("trailingStop: " + string(b) + ", ") + } + } + return buf.String() +} + // Inherit is used for inheriting properties from the given strategy struct // for example, some exit method requires the default interval and symbol name from the strategy param object func (m *ExitMethod) Inherit(parent interface{}) { diff --git a/pkg/bbgo/interact.go b/pkg/bbgo/interact.go index 820097219..389402d9a 100644 --- a/pkg/bbgo/interact.go +++ b/pkg/bbgo/interact.go @@ -526,7 +526,7 @@ func (it *CoreInteraction) Initialize() error { // getStrategySignature returns strategy instance unique signature func getStrategySignature(strategy SingleExchangeStrategy) (string, error) { // Returns instance ID - var signature = callID(strategy) + var signature = CallID(strategy) if signature != "" { return signature, nil } diff --git a/pkg/bbgo/persistence.go b/pkg/bbgo/persistence.go index 89b4179df..e4232035e 100644 --- a/pkg/bbgo/persistence.go +++ b/pkg/bbgo/persistence.go @@ -82,7 +82,7 @@ func (p *Persistence) Save(val interface{}, subIDs ...string) error { } func (p *Persistence) Sync(obj interface{}) error { - id := callID(obj) + id := CallID(obj) if len(id) == 0 { return nil } @@ -93,7 +93,7 @@ func (p *Persistence) Sync(obj interface{}) error { // Sync syncs the object properties into the persistence layer func Sync(obj interface{}) { - id := callID(obj) + id := CallID(obj) if len(id) == 0 { log.Warnf("InstanceID() is not provided, can not sync persistence") return diff --git a/pkg/bbgo/persistence_test.go b/pkg/bbgo/persistence_test.go index 0eea57ed5..843ce6777 100644 --- a/pkg/bbgo/persistence_test.go +++ b/pkg/bbgo/persistence_test.go @@ -21,7 +21,6 @@ func (s *TestStructWithoutInstanceID) ID() string { return "test-struct-no-instance-id" } - type TestStruct struct { *Environment @@ -56,15 +55,15 @@ func preparePersistentServices() []service.PersistenceService { return pss } -func Test_callID(t *testing.T) { +func Test_CallID(t *testing.T) { t.Run("default", func(t *testing.T) { - id := callID(&TestStruct{}) + id := CallID(&TestStruct{}) assert.NotEmpty(t, id) assert.Equal(t, "test-struct", id) }) t.Run("fallback", func(t *testing.T) { - id := callID(&TestStructWithoutInstanceID{Symbol: "BTCUSDT"}) + id := CallID(&TestStructWithoutInstanceID{Symbol: "BTCUSDT"}) assert.Equal(t, "test-struct-no-instance-id:BTCUSDT", id) }) } @@ -122,7 +121,7 @@ func Test_storePersistenceFields(t *testing.T) { for _, ps := range pss { psName := reflect.TypeOf(ps).Elem().String() t.Run("all/"+psName, func(t *testing.T) { - id := callID(a) + id := CallID(a) err := storePersistenceFields(a, id, ps) assert.NoError(t, err) diff --git a/pkg/bbgo/reflect.go b/pkg/bbgo/reflect.go index 263c2cc87..f4662b44f 100644 --- a/pkg/bbgo/reflect.go +++ b/pkg/bbgo/reflect.go @@ -10,7 +10,7 @@ type InstanceIDProvider interface { InstanceID() string } -func callID(obj interface{}) string { +func CallID(obj interface{}) string { sv := reflect.ValueOf(obj) st := reflect.TypeOf(obj) if st.Implements(reflect.TypeOf((*InstanceIDProvider)(nil)).Elem()) { @@ -30,4 +30,3 @@ func callID(obj interface{}) string { ret := m.Call(nil) return ret[0].String() + ":" } - diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index a1224fe2b..ad9a4ec07 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -384,7 +384,7 @@ func (trader *Trader) LoadState() error { log.Infof("loading strategies states...") return trader.IterateStrategies(func(strategy StrategyID) error { - id := callID(strategy) + id := CallID(strategy) return loadPersistenceFields(strategy, id, ps) }) } @@ -420,7 +420,7 @@ func (trader *Trader) SaveState() error { log.Infof("saving strategies states...") return trader.IterateStrategies(func(strategy StrategyID) error { - id := callID(strategy) + id := CallID(strategy) if len(id) == 0 { return nil } diff --git a/pkg/strategy/drift/output.go b/pkg/strategy/drift/output.go index 1043c3e1e..9cccdc1ed 100644 --- a/pkg/strategy/drift/output.go +++ b/pkg/strategy/drift/output.go @@ -4,14 +4,10 @@ import ( "fmt" "io" "reflect" - "sort" - "strings" "unsafe" - "github.com/c9s/bbgo/pkg/util" - "github.com/fatih/color" + "github.com/c9s/bbgo/pkg/strategy" "github.com/jedib0t/go-pretty/v6/table" - "github.com/jedib0t/go-pretty/v6/text" ) type jsonStruct struct { @@ -26,6 +22,16 @@ func (a jsonArr) Len() int { return len(a) } func (a jsonArr) Less(i, j int) bool { return a[i].key < a[j].key } func (a jsonArr) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func canInt(v reflect.Value) bool { + k := v.Type().Kind() + switch k { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + default: + return false + } +} + func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) { length := 1 if len(seriesLength) > 0 && seriesLength[0] > 0 { @@ -75,7 +81,7 @@ func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) { } } else if canString { fmt.Fprintf(f, "%s: %s\n", fieldName, stringFunc.Call(nil)[0].String()) - } else if field.CanConvert(reflect.TypeOf(int(0))) { + } else if canInt(field) { fmt.Fprintf(f, "%s: %d\n", fieldName, field.Int()) } else if field.CanConvert(reflect.TypeOf(float64(0))) { fmt.Fprintf(f, "%s: %.4f\n", fieldName, field.Float()) @@ -107,104 +113,9 @@ func (s *Strategy) ParamDump(f io.Writer, seriesLength ...int) { } func (s *Strategy) Print(f io.Writer, pretty bool, withColor ...bool) { - //b, _ := json.MarshalIndent(s.ExitMethods, " ", " ") - - t := table.NewWriter() - style := table.Style{ - Name: "StyleRounded", - Box: table.StyleBoxRounded, - Color: table.ColorOptionsDefault, - Format: table.FormatOptionsDefault, - HTML: table.DefaultHTMLOptions, - Options: table.OptionsDefault, - Title: table.TitleOptionsDefault, - } - var hiyellow func(io.Writer, string, ...interface{}) - if len(withColor) > 0 && withColor[0] { - if pretty { - style.Color = table.ColorOptionsYellowWhiteOnBlack - style.Color.Row = text.Colors{text.FgHiYellow, text.BgHiBlack} - style.Color.RowAlternate = text.Colors{text.FgYellow, text.BgBlack} - } - hiyellow = color.New(color.FgHiYellow).FprintfFunc() - } else { - hiyellow = func(a io.Writer, format string, args ...interface{}) { - fmt.Fprintf(a, format, args...) - } - } + var style *table.Style if pretty { - t.SetOutputMirror(f) - t.SetStyle(style) - t.AppendHeader(table.Row{"json", "struct field name", "type", "value"}) - } - hiyellow(f, "------ %s Settings ------\n", s.InstanceID()) - - embeddedWhiteSet := map[string]struct{}{"Window": {}, "Interval": {}, "Symbol": {}} - redundantSet := map[string]struct{}{} - var rows []table.Row - val := reflect.ValueOf(*s) - var values jsonArr - for i := 0; i < val.Type().NumField(); i++ { - t := val.Type().Field(i) - if !t.IsExported() { - continue - } - fieldName := t.Name - switch jsonTag := t.Tag.Get("json"); jsonTag { - case "-": - case "": - if t.Anonymous { - var target reflect.Type - if t.Type.Kind() == util.Pointer { - target = t.Type.Elem() - } else { - target = t.Type - } - for j := 0; j < target.NumField(); j++ { - tt := target.Field(j) - if !tt.IsExported() { - continue - } - fieldName := tt.Name - if _, ok := embeddedWhiteSet[fieldName]; !ok { - continue - } - - if jtag := tt.Tag.Get("json"); jtag != "" && jtag != "-" { - name := strings.Split(jtag, ",")[0] - if _, ok := redundantSet[name]; ok { - continue - } - redundantSet[name] = struct{}{} - var value interface{} - if t.Type.Kind() == util.Pointer { - value = val.Field(i).Elem().Field(j).Interface() - } else { - value = val.Field(i).Field(j).Interface() - } - values = append(values, jsonStruct{key: fieldName, json: name, tp: tt.Type.String(), value: value}) - } - } - } - default: - name := strings.Split(jsonTag, ",")[0] - if _, ok := redundantSet[name]; ok { - continue - } - redundantSet[name] = struct{}{} - values = append(values, jsonStruct{key: fieldName, json: name, tp: t.Type.String(), value: val.Field(i).Interface()}) - } - } - sort.Sort(values) - for _, value := range values { - if pretty { - rows = append(rows, table.Row{value.json, value.key, value.tp, value.value}) - } else { - hiyellow(f, "%s: %v\n", value.json, value.value) - } - } - if pretty { - t.AppendRows(rows) - t.Render() + style = strategy.DefaultStyle() } + strategy.PrintConfig(s, f, style, len(withColor) > 0 && withColor[0], strategy.DefaultWhiteList()...) } diff --git a/pkg/strategy/output.go b/pkg/strategy/output.go new file mode 100644 index 000000000..b50045a12 --- /dev/null +++ b/pkg/strategy/output.go @@ -0,0 +1,146 @@ +package strategy + +import ( + "fmt" + "io" + "reflect" + "sort" + "strings" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/util" + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" +) + +type JsonStruct struct { + Key string + Json string + Type string + Value interface{} +} + +type JsonArr []JsonStruct + +func (a JsonArr) Len() int { return len(a) } +func (a JsonArr) Less(i, j int) bool { return a[i].Key < a[j].Key } +func (a JsonArr) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +func DefaultStyle() *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 { + return []string{"Window", "Interval", "Symbol"} +} + +func PrintConfig(s interface{}, f io.Writer, style *table.Style, withColor bool, whiteLists ...string) { + t := table.NewWriter() + var write func(io.Writer, string, ...interface{}) + + if withColor { + write = color.New(color.FgHiYellow).FprintfFunc() + } else { + write = func(a io.Writer, format string, args ...interface{}) { + fmt.Fprintf(a, format, args...) + } + } + if style != nil { + t.SetOutputMirror(f) + t.SetStyle(*style) + t.SetColumnConfigs([]table.ColumnConfig{ + {Number: 4, WidthMax: 50, WidthMaxEnforcer: text.WrapText}, + }) + t.AppendHeader(table.Row{"json", "struct field name", "type", "value"}) + } + write(f, "---- %s Settings ---\n", bbgo.CallID(s)) + + embeddedWhiteSet := map[string]struct{}{} + for _, whiteList := range whiteLists { + embeddedWhiteSet[whiteList] = struct{}{} + } + + redundantSet := map[string]struct{}{} + + var rows []table.Row + + val := reflect.ValueOf(s) + + if val.Type().Kind() == util.Pointer { + val = val.Elem() + } + var values JsonArr + for i := 0; i < val.Type().NumField(); i++ { + t := val.Type().Field(i) + if !t.IsExported() { + continue + } + fieldName := t.Name + switch jsonTag := t.Tag.Get("json"); jsonTag { + case "-": + case "": + // we only fetch fields from the first layer of the embedded struct + if t.Anonymous { + var target reflect.Type + var field reflect.Value + if t.Type.Kind() == util.Pointer { + target = t.Type.Elem() + field = val.Field(i).Elem() + } else { + target = t.Type + field = val.Field(i) + } + for j := 0; j < target.NumField(); j++ { + tt := target.Field(j) + if !tt.IsExported() { + continue + } + fieldName := tt.Name + if _, ok := embeddedWhiteSet[fieldName]; !ok { + continue + } + if jtag := tt.Tag.Get("json"); jtag != "" && jtag != "-" { + name := strings.Split(jtag, ",")[0] + if _, ok := redundantSet[name]; ok { + continue + } + redundantSet[name] = struct{}{} + value := field.Field(j).Interface() + values = append(values, JsonStruct{Key: fieldName, Json: name, Type: tt.Type.String(), Value: value}) + } + } + } + default: + name := strings.Split(jsonTag, ",")[0] + if _, ok := redundantSet[name]; ok { + continue + } + redundantSet[name] = struct{}{} + values = append(values, JsonStruct{Key: fieldName, Json: name, Type: t.Type.String(), Value: val.Field(i).Interface()}) + } + } + sort.Sort(values) + for _, value := range values { + if style != nil { + rows = append(rows, table.Row{value.Json, value.Key, value.Type, value.Value}) + } else { + write(f, "%s: %v\n", value.Json, value.Value) + } + } + if style != nil { + t.AppendRows(rows) + t.Render() + } +}