Merge pull request #801 from c9s/feature/optimizer-metrics-tsv-format

feature: optimizer: support --tsv option and render tsv output
This commit is contained in:
Yo-An Lin 2022-07-07 06:23:49 +08:00 committed by GitHub
commit e778db1f24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 138 additions and 12 deletions

View File

@ -4,12 +4,16 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"strconv"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/optimizer" "github.com/c9s/bbgo/pkg/optimizer"
) )
@ -17,6 +21,7 @@ func init() {
optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file") optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file")
optimizeCmd.Flags().String("output", "output", "backtest report output directory") optimizeCmd.Flags().String("output", "output", "backtest report output directory")
optimizeCmd.Flags().Bool("json", false, "print optimizer metrics in json format") optimizeCmd.Flags().Bool("json", false, "print optimizer metrics in json format")
optimizeCmd.Flags().Bool("tsv", false, "print optimizer metrics in csv format")
RootCmd.AddCommand(optimizeCmd) RootCmd.AddCommand(optimizeCmd)
} }
@ -43,6 +48,11 @@ var optimizeCmd = &cobra.Command{
return err return err
} }
printTsvFormat, err := cmd.Flags().GetBool("tsv")
if err != nil {
return err
}
outputDirectory, err := cmd.Flags().GetString("output") outputDirectory, err := cmd.Flags().GetString("output")
if err != nil { if err != nil {
return err return err
@ -104,6 +114,10 @@ var optimizeCmd = &cobra.Command{
// print metrics JSON to stdout // print metrics JSON to stdout
fmt.Println(string(out)) fmt.Println(string(out))
} else if printTsvFormat {
if err := formatMetricsTsv(metrics, os.Stdout); err != nil {
return err
}
} else { } else {
for n, values := range metrics { for n, values := range metrics {
if len(values) == 0 { if len(values) == 0 {
@ -120,3 +134,95 @@ var optimizeCmd = &cobra.Command{
return nil return nil
}, },
} }
func transformMetricsToRows(metrics map[string][]optimizer.Metric) (headers []string, rows [][]interface{}) {
var metricsKeys []string
for k := range metrics {
metricsKeys = append(metricsKeys, k)
}
var numEntries int
var paramLabels []string
for _, ms := range metrics {
for _, m := range ms {
paramLabels = m.Labels
break
}
numEntries = len(ms)
break
}
headers = append(paramLabels, metricsKeys...)
rows = make([][]interface{}, numEntries)
var metricsRows = make([][]interface{}, numEntries)
// build params into the rows
for i, m := range metrics[metricsKeys[0]] {
rows[i] = m.Params
}
for _, metricKey := range metricsKeys {
for i, ms := range metrics[metricKey] {
if len(metricsRows[i]) == 0 {
metricsRows[i] = make([]interface{}, 0, len(metricsKeys))
}
metricsRows[i] = append(metricsRows[i], ms.Value)
}
}
// merge rows
for i := range rows {
rows[i] = append(rows[i], metricsRows[i]...)
}
return headers, rows
}
func formatMetricsTsv(metrics map[string][]optimizer.Metric, writer io.WriteCloser) error {
headers, rows := transformMetricsToRows(metrics)
w := tsv.NewWriter(writer)
if err := w.Write(headers); err != nil {
return err
}
for _, row := range rows {
var cells []string
for _, o := range row {
cell, err := castCellValue(o)
if err != nil {
return err
}
cells = append(cells, cell)
}
if err := w.Write(cells); err != nil {
return err
}
}
return w.Close()
}
func castCellValue(a interface{}) (string, error) {
switch tv := a.(type) {
case fixedpoint.Value:
return tv.String(), nil
case float64:
return strconv.FormatFloat(tv, 'f', -1, 64), nil
case int64:
return strconv.FormatInt(tv, 10), nil
case int32:
return strconv.FormatInt(int64(tv), 10), nil
case int:
return strconv.Itoa(tv), nil
case bool:
return strconv.FormatBool(tv), nil
case string:
return tv, nil
case []byte:
return string(tv), nil
default:
return "", fmt.Errorf("unsupported object type: %T value: %v", tv, tv)
}
}

View File

@ -17,16 +17,31 @@ import (
type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value type MetricValueFunc func(summaryReport *backtest.SummaryReport) fixedpoint.Value
var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value { var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value {
if summaryReport == nil {
return fixedpoint.Zero
}
return summaryReport.TotalProfit return summaryReport.TotalProfit
} }
var TotalVolume = func(summaryReport *backtest.SummaryReport) fixedpoint.Value {
if len(summaryReport.SymbolReports) == 0 {
return fixedpoint.Zero
}
buyVolume := summaryReport.SymbolReports[0].PnL.BuyVolume
sellVolume := summaryReport.SymbolReports[0].PnL.SellVolume
return buyVolume.Add(sellVolume)
}
type Metric struct { type Metric struct {
Labels []string `json:"labels,omitempty"` // Labels is the labels of the given parameters
Params []interface{} `json:"params,omitempty"` Labels []string `json:"labels,omitempty"`
Value fixedpoint.Value `json:"value,omitempty"`
// Params is the parameters used to output the metrics result
Params []interface{} `json:"params,omitempty"`
// Key is the metric name
Key string `json:"key"`
// Value is the metric value of the metric
Value fixedpoint.Value `json:"value,omitempty"`
} }
func copyParams(params []interface{}) []interface{} { func copyParams(params []interface{}) []interface{} {
@ -172,6 +187,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][]
var valueFunctions = map[string]MetricValueFunc{ var valueFunctions = map[string]MetricValueFunc{
"totalProfit": TotalProfitMetricValueFunc, "totalProfit": TotalProfitMetricValueFunc,
"totalVolume": TotalVolume,
} }
var metrics = map[string][]Metric{} var metrics = map[string][]Metric{}
@ -220,16 +236,20 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][]
close(taskC) // this will shut down the executor close(taskC) // this will shut down the executor
for result := range resultsC { for result := range resultsC {
for metricName, metricFunc := range valueFunctions { if result.Report == nil {
if result.Report == nil { log.Errorf("no summaryReport found for params: %+v", result.Params)
log.Errorf("no summaryReport found for params: %+v", result.Params) continue
} }
for metricKey, metricFunc := range valueFunctions {
var metricValue = metricFunc(result.Report) var metricValue = metricFunc(result.Report)
bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricName, metricValue)) bar.Set("log", fmt.Sprintf("params: %+v => %s %+v", result.Params, metricKey, metricValue))
bar.Increment() bar.Increment()
metrics[metricName] = append(metrics[metricName], Metric{
metrics[metricKey] = append(metrics[metricKey], Metric{
Params: result.Params, Params: result.Params,
Labels: result.Labels, Labels: result.Labels,
Key: metricKey,
Value: metricValue, Value: metricValue,
}) })
} }