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"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"strconv"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/optimizer"
)
@ -17,6 +21,7 @@ func init() {
optimizeCmd.Flags().String("optimizer-config", "optimizer.yaml", "config file")
optimizeCmd.Flags().String("output", "output", "backtest report output directory")
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)
}
@ -43,6 +48,11 @@ var optimizeCmd = &cobra.Command{
return err
}
printTsvFormat, err := cmd.Flags().GetBool("tsv")
if err != nil {
return err
}
outputDirectory, err := cmd.Flags().GetString("output")
if err != nil {
return err
@ -104,6 +114,10 @@ var optimizeCmd = &cobra.Command{
// print metrics JSON to stdout
fmt.Println(string(out))
} else if printTsvFormat {
if err := formatMetricsTsv(metrics, os.Stdout); err != nil {
return err
}
} else {
for n, values := range metrics {
if len(values) == 0 {
@ -120,3 +134,95 @@ var optimizeCmd = &cobra.Command{
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
var TotalProfitMetricValueFunc = func(summaryReport *backtest.SummaryReport) fixedpoint.Value {
if summaryReport == nil {
return fixedpoint.Zero
}
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 {
Labels []string `json:"labels,omitempty"`
Params []interface{} `json:"params,omitempty"`
Value fixedpoint.Value `json:"value,omitempty"`
// Labels is the labels of the given parameters
Labels []string `json:"labels,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{} {
@ -172,6 +187,7 @@ func (o *GridOptimizer) Run(executor Executor, configJson []byte) (map[string][]
var valueFunctions = map[string]MetricValueFunc{
"totalProfit": TotalProfitMetricValueFunc,
"totalVolume": TotalVolume,
}
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
for result := range resultsC {
for metricName, metricFunc := range valueFunctions {
if result.Report == nil {
log.Errorf("no summaryReport found for params: %+v", result.Params)
}
if result.Report == nil {
log.Errorf("no summaryReport found for params: %+v", result.Params)
continue
}
for metricKey, metricFunc := range valueFunctions {
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()
metrics[metricName] = append(metrics[metricName], Metric{
metrics[metricKey] = append(metrics[metricKey], Metric{
Params: result.Params,
Labels: result.Labels,
Key: metricKey,
Value: metricValue,
})
}