Skip to content
This repository was archived by the owner on Apr 24, 2025. It is now read-only.

Commit 1ef8134

Browse files
committed
initial commit
0 parents  commit 1ef8134

6 files changed

Lines changed: 191 additions & 0 deletions

File tree

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FROM golang:1.20-alpine
2+
COPY . /code/backtest-action
3+
RUN cd /code/backtest-action && go install -v .
4+
ENTRYPOINT ["backtest-action"]

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Portfolio Backtest Action
2+
3+
## Example usage
4+
5+
```yaml
6+
name: Back Test
7+
8+
on:
9+
push:
10+
branches: [ "main" ]
11+
paths-ignore:
12+
- 'README.md'
13+
14+
jobs:
15+
back_test:
16+
name: Run BackTest
17+
steps:
18+
- uses: portfoliotree/backtest-action@v0
19+
```

action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# action.yml
2+
name: 'Portfolio Backtest Action'
3+
description: 'Backtest simple portfolio specifications.'
4+
runs:
5+
using: 'docker'
6+
image: 'Dockerfile'

go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module github.com/portfoliotree/backtest-action
2+
3+
go 1.20
4+
5+
require (
6+
github.com/portfoliotree/portfolio v0.1.1-0.20230629074126-e6d8a7b39ff4
7+
github.com/portfoliotree/round v0.0.0-20221115082401-a830c3a8a159
8+
)
9+
10+
require (
11+
github.com/kr/pretty v0.3.1 // indirect
12+
go.mongodb.org/mongo-driver v1.12.0 // indirect
13+
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect
14+
golang.org/x/text v0.9.0 // indirect
15+
gonum.org/v1/gonum v0.12.0 // indirect
16+
gopkg.in/yaml.v3 v3.0.1 // indirect
17+
)

go.sum

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
5+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
6+
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
7+
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
8+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
9+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
10+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
11+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
12+
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
13+
github.com/onsi/gomega v1.27.1 h1:rfztXRbg6nv/5f+Raen9RcGoSecHIFgBBLQK3Wdj754=
14+
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
15+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
16+
github.com/portfoliotree/portfolio v0.1.1-0.20230629074126-e6d8a7b39ff4 h1:7zIt8zKP+gzRpV8i49hOw+iNxkoW0HzTHdmf7P4wnxA=
17+
github.com/portfoliotree/portfolio v0.1.1-0.20230629074126-e6d8a7b39ff4/go.mod h1:FQolsEObY22Kaa9YoEcqZAc7jIWBX0iDRtHzJpfZ1Tg=
18+
github.com/portfoliotree/round v0.0.0-20221115082401-a830c3a8a159 h1:tyuE5WS9to6hUgqrq1ZzSrpxxbydo0M1/N7sRjvFbLM=
19+
github.com/portfoliotree/round v0.0.0-20221115082401-a830c3a8a159/go.mod h1:x31vr2Ldsfel4fHca08ISxi8KeVq7BMS3StIrXytBio=
20+
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
21+
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
22+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
23+
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
24+
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
25+
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
26+
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
27+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
28+
go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE=
29+
go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0=
30+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
31+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
32+
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
33+
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
34+
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
35+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
36+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
37+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
38+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
39+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
40+
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
41+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
42+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
43+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
44+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
46+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
47+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
48+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
49+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
50+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
51+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
52+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
53+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
54+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
55+
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
56+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
57+
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
58+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
59+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
60+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
61+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
62+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
63+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
64+
gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o=
65+
gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY=
66+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
67+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
68+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
69+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

main.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"os"
9+
"strings"
10+
"time"
11+
12+
"github.com/portfoliotree/round"
13+
14+
"github.com/portfoliotree/portfolio"
15+
"github.com/portfoliotree/portfolio/backtest"
16+
"github.com/portfoliotree/portfolio/returns"
17+
)
18+
19+
func main() {
20+
specs, err := portfolio.WalkDirectoryAndParseSpecificationFiles(os.DirFS("."))
21+
if err != nil {
22+
log.Fatal(err)
23+
}
24+
25+
backtestResultsJSON, err := os.Create("backtest_results.json")
26+
if err != nil {
27+
log.Fatal(err)
28+
}
29+
defer func() {
30+
_ = backtestResultsJSON.Close()
31+
}()
32+
backtestResultsEncoder := json.NewEncoder(backtestResultsJSON)
33+
34+
var table returns.Table
35+
ctx := context.Background()
36+
var names []string
37+
for _, spec := range specs {
38+
log.Printf("running backtest with %d assets for %s: %q\n", len(spec.Assets), spec.Filepath, spec.Name)
39+
result, err := spec.Backtest(ctx, nil)
40+
if err != nil {
41+
log.Fatal(err)
42+
}
43+
if err := backtestResultsEncoder.Encode(result); err != nil {
44+
log.Fatal(err)
45+
}
46+
rs := result.Returns()
47+
log.Println(report(spec, result))
48+
table = table.AddColumn(rs)
49+
names = append(names, spec.Filepath)
50+
}
51+
returnsCSV, err := os.Create("returns.csv")
52+
if err != nil {
53+
log.Fatal(err)
54+
}
55+
defer func() {
56+
_ = returnsCSV.Close()
57+
}()
58+
if err := table.WriteCSV(returnsCSV, names); err != nil {
59+
log.Fatal(err)
60+
}
61+
}
62+
63+
func report(spec portfolio.Specification, result backtest.Result) string {
64+
rs := result.Returns()
65+
var s strings.Builder
66+
s.WriteString(fmt.Sprintf("finished backtest for %s: %q\n", spec.Filepath, spec.Name))
67+
s.WriteString(fmt.Sprintf("\tlast_time: %s\n", rs.LastTime().Format(time.DateOnly)))
68+
s.WriteString(fmt.Sprintf("\tfirst_time: %s\n", rs.FirstTime().Format(time.DateOnly)))
69+
s.WriteString(fmt.Sprintf("\tnumber_of_returns: %d\n", rs.Len()))
70+
s.WriteString(fmt.Sprintf("\tannualized_arithmetic_return: %f\n", round.Decimal(rs.AnnualizedArithmeticReturn()*100, 6)))
71+
s.WriteString(fmt.Sprintf("\tannualized_risk: %f\n", round.Decimal(rs.AnnualizedRisk()*100, 6)))
72+
s.WriteString(fmt.Sprintf("\tnumber_of_assets: %d\n", len(spec.Assets)))
73+
s.WriteString(fmt.Sprintf("\trebalance_count: %d\n", len(result.RebalanceTimes)))
74+
s.WriteString(fmt.Sprintf("\tpolicy_weight_update_count: %d\n", len(result.PolicyUpdateTimes)))
75+
return s.String()
76+
}

0 commit comments

Comments
 (0)