diff --git a/.gitignore b/.gitignore index 009fede..ed268ef 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ go.work # Other .DS_Store +*.html diff --git a/backtesting.go b/backtesting.go index 8dd70fb..c2774d6 100644 --- a/backtesting.go +++ b/backtesting.go @@ -2,10 +2,14 @@ package autotrader import ( "errors" + "fmt" "log" + "os" "strconv" "time" + "github.com/go-echarts/go-echarts/v2/charts" + "github.com/go-echarts/go-echarts/v2/opts" "golang.org/x/exp/rand" ) @@ -26,11 +30,55 @@ func Backtest(trader *Trader) { log.Println("Backtest complete.") log.Println("Stats:") log.Println(trader.Stats().String()) + + chart := charts.NewLine() + chart.SetGlobalOptions(charts.WithTitleOpts(opts.Title{ + Title: "Backtest", + Subtitle: fmt.Sprintf("%s %s %T", trader.Symbol, trader.Frequency, trader.Strategy), + })) + chart.SetXAxis(seriesStringArray(trader.Stats().Dates())). + AddSeries("Equity", lineDataFromSeries(trader.Stats().Series("Equity"))) + + // Draw the chart to a file. + f, err := os.Create("backtest.html") + if err != nil { + panic(err) + } + chart.Render(f) + f.Close() + + // Open the chart in the default browser. + if err := Open("backtest.html"); err != nil { + panic(err) + } default: log.Fatalf("Backtesting is only supported with a TestBroker. Got %T", broker) } } +func lineDataFromSeries(s Series) []opts.LineData { + data := make([]opts.LineData, s.Len()) + for i := 0; i < s.Len(); i++ { + data[i] = opts.LineData{Value: s.Value(i)} + } + return data +} + +func seriesStringArray(s Series) []string { + data := make([]string, s.Len()) + for i := 0; i < s.Len(); i++ { + switch val := s.Value(i).(type) { + case time.Time: + data[i] = val.Format(time.DateTime) + case string: + data[i] = fmt.Sprintf("%q", val) + default: + data[i] = fmt.Sprintf("%v", val) + } + } + return data +} + // TestBroker is a broker that can be used for testing. It implements the Broker interface and fulfills orders // // Signals: diff --git a/go.mod b/go.mod index 0f93cc8..09b0f0f 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( ) require ( + github.com/go-echarts/go-echarts/v2 v2.2.6 // indirect github.com/google/go-cmp v0.5.8 // indirect github.com/guptarohit/asciigraph v0.5.1 // indirect github.com/mattn/go-runewidth v0.0.7 // indirect diff --git a/go.sum b/go.sum index 509b366..4e008bf 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,7 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cinar/indicator v1.2.24/go.mod h1:5eX8f1PG9g3RKSoHsoQxKd8bIN97Cf/gbgxXjihROpI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cnkei/gospline v0.0.0-20191204072713-842a72f86331/go.mod h1:DXXGDL64/wxXgBSgmGMEL0vYC0tdvpgNhkJrvavhqDM= @@ -72,6 +73,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-co-op/gocron v1.26.0 h1:dbX2xdy8tRE2o02PYhtYmK8WCBL8j7tVn/qgETBLL1g= github.com/go-co-op/gocron v1.26.0/go.mod h1:JHrQDY4iE1HZPkgTyoccY4xtDgLbrUwL+xODIbEQdnc= +github.com/go-echarts/go-echarts/v2 v2.2.6 h1:Gg4SXDxFwi/KzRvBuH6ed89b6bqP4F7ysANDdWiziBY= +github.com/go-echarts/go-echarts/v2 v2.2.6/go.mod h1:IN5P8jIRZKENmAJf2lHXBzv8U9YwdVnY9urdzGkEDA0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -224,6 +227,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tealeg/xlsx/v3 v3.0.0/go.mod h1:fSua0Owrk9yAMAFGZI7piq5UL2BcubuQuLNOEhr3X80= @@ -470,6 +474,7 @@ gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= diff --git a/utils.go b/utils.go index 6330f83..591de1c 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,9 @@ package autotrader import ( + "os/exec" + "runtime" + "golang.org/x/exp/constraints" ) @@ -46,3 +49,21 @@ func LeverageToMargin(leverage float64) float64 { func MarginToLeverage(margin float64) float64 { return 1 / margin } + +// Open opens the specified URL in the default browser of the user. +func Open(url string) error { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "cmd" + args = []string{"/c", "start"} + case "darwin": + cmd = "open" + default: // "linux", "freebsd", "openbsd", "netbsd" + cmd = "xdg-open" + } + args = append(args, url) + return exec.Command(cmd, args...).Start() +}