Skip to content

Commit

Permalink
fix data structures
Browse files Browse the repository at this point in the history
  • Loading branch information
crhntr committed Dec 27, 2023
1 parent 10b4f63 commit 556007e
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 92 deletions.
3 changes: 2 additions & 1 deletion examples/60-40_portfolio.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
type: Portfolio
spec:
metadata:
name: 60/40
benchmark: BIGPX
spec:
assets: [ACWI, AGG]
policy:
weights: [60, 40]
Expand Down
7 changes: 4 additions & 3 deletions examples/maang_portfolio.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
---
type: Portfolio
spec:
metadata:
name: MAANG
benchmark: SPY
spec:
assets:
- {id: META}
- AMZN
- AAPL
- NFLX
- GOOG
policy:
rebalancing_interval: Quarterly
benchmark: SPY
rebalancing_interval: Quarterly
10 changes: 5 additions & 5 deletions fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

// ParseSpecificationFile opens a file and parses the contents into a Specification
// It supports YAML files at the moment but may support other encodings in the future.
func ParseSpecificationFile(specificationFilePath string) ([]Specification, error) {
func ParseSpecificationFile(specificationFilePath string) ([]Document, error) {
if err := checkPortfolioFileName(specificationFilePath); err != nil {
return nil, err
}
Expand All @@ -33,8 +33,8 @@ func checkPortfolioFileName(fileName string) error {
}
}

func portfoliosFromFile(fileName string, file fs.File) ([]Specification, error) {
result, err := ParseSpecifications(file)
func portfoliosFromFile(fileName string, file fs.File) ([]Document, error) {
result, err := ParseDocuments(file)
if err != nil {
return result, err
}
Expand All @@ -45,8 +45,8 @@ func portfoliosFromFile(fileName string, file fs.File) ([]Specification, error)
return result, nil
}

func WalkDirectoryAndParseSpecificationFiles(dir fs.FS) ([]Specification, error) {
var result []Specification
func WalkDirectoryAndParseSpecificationFiles(dir fs.FS) ([]Document, error) {
var result []Document
return result, fs.WalkDir(dir, ".", func(filePath string, d fs.DirEntry, err error) error {
if err != nil {
return err
Expand Down
56 changes: 33 additions & 23 deletions fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,28 @@ func TestParseSpecificationFile(t *testing.T) {
Name string
FilePath string
ErrorStringContains string
Portfolios []portfolio.Specification
Portfolios []portfolio.Document
}{
{
Name: "asset ids with policy weights",
FilePath: filepath.Join("examples", "60-40_portfolio.yml"),
Portfolios: []portfolio.Specification{
Portfolios: []portfolio.Document{
{
Name: "60/40",
Benchmark: portfolio.Component{ID: "BIGPX"},
Assets: []portfolio.Component{
{ID: "ACWI"},
{ID: "AGG"},
Type: "Portfolio",
Metadata: portfolio.Metadata{
Name: "60/40",
Benchmark: portfolio.Component{ID: "BIGPX"},
},
Policy: portfolio.Policy{
Weights: []float64{60, 40},
WeightsAlgorithm: allocation.ConstantWeightsAlgorithmName,
RebalancingInterval: "Quarterly",
Spec: portfolio.Specification{
Assets: []portfolio.Component{
{ID: "ACWI"},
{ID: "AGG"},
},
Policy: portfolio.Policy{
Weights: []float64{60, 40},
WeightsAlgorithm: allocation.ConstantWeightsAlgorithmName,
RebalancingInterval: "Quarterly",
},
},
Filepath: "examples/60-40_portfolio.yml",
},
Expand All @@ -47,20 +52,25 @@ func TestParseSpecificationFile(t *testing.T) {
{
Name: "mixed asset spec node type and weight algorithm",
FilePath: filepath.Join("examples", "maang_portfolio.yml"),
Portfolios: []portfolio.Specification{
Portfolios: []portfolio.Document{
{
Name: "MAANG",
Benchmark: portfolio.Component{ID: "SPY"},
Assets: []portfolio.Component{
{ID: "META"},
{ID: "AMZN"},
{ID: "AAPL"},
{ID: "NFLX"},
{ID: "GOOG"},
Type: "Portfolio",
Metadata: portfolio.Metadata{
Name: "MAANG",
Benchmark: portfolio.Component{ID: "SPY"},
},
Policy: portfolio.Policy{
RebalancingInterval: "Quarterly",
WeightsAlgorithm: allocation.EqualWeightsAlgorithmName,
Spec: portfolio.Specification{
Assets: []portfolio.Component{
{ID: "META"},
{ID: "AMZN"},
{ID: "AAPL"},
{ID: "NFLX"},
{ID: "GOOG"},
},
Policy: portfolio.Policy{
RebalancingInterval: "Quarterly",
WeightsAlgorithm: allocation.EqualWeightsAlgorithmName,
},
},
Filepath: "examples/maang_portfolio.yml",
},
Expand Down
69 changes: 29 additions & 40 deletions portfolio.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ type Document struct {
Type string `json:"type" yaml:"type" bson:"type"`
Metadata Metadata `json:"metadata" yaml:"metadata" bson:"metadata"`
Spec Specification `json:"spec" yaml:"spec" bson:"spec"`

Filepath string `json:"filepath,omitempty" yaml:"-" bson:"filepath,omitempty"`
FileIndex int `json:"index,omitempty" yaml:"-" bson:"index,omitempty"`
}

func (d Document) Validate() error {
if d.Type != portfolioTypeName {
return fmt.Errorf("incorrect specification type got %q but expected %q", d.Type, portfolioTypeName)
}
return d.Spec.Validate()
}

type Metadata struct {
Expand All @@ -37,69 +47,53 @@ type Metadata struct {

// Specification models a portfolio.
type Specification struct {
Name string `yaml:"name"`
Benchmark Component `yaml:"benchmark"`
Assets []Component `yaml:"assets"`
Policy Policy `yaml:"policy"`

Filepath string `yaml:"-"`
FileIndex int `yaml:"-"`
Assets []Component `yaml:"assets"`
Policy Policy `yaml:"policy"`
}

// typedSpecificationFile may be exported some day.
// For now, it provides a bit of indirection for specs and files.
type typedSpecificationFile[S interface {
Specification
}] struct {
ID string `yaml:"id"`
Type string `yaml:"type"`
Spec S `yaml:"spec"`
}

// ParseOneSpecification decodes the contents of in to a Specification
// ParseOneDocument decodes the contents of in to a Specification
// It supports a string containing YAML.
// The resulting Specification may have default values for unset fields.
func ParseOneSpecification(in string) (Specification, error) {
result, err := ParseSpecifications(strings.NewReader(in))
func ParseOneDocument(in string) (Document, error) {
result, err := ParseDocuments(strings.NewReader(in))
if err != nil {
return Specification{}, err
return Document{}, err
}
if len(result) != 1 {
return Specification{}, fmt.Errorf("expected input to have exactly one portfolio especified")
return Document{}, fmt.Errorf("expected input to have exactly one portfolio especified")
}
return result[0], nil
}

const portfolioTypeName = "Portfolio"

// ParseSpecifications decodes the contents of in to a list of Specifications
// ParseDocuments decodes the contents of in to a list of Specifications
// The resulting Specification may have default values for unset fields.
func ParseSpecifications(r io.Reader) ([]Specification, error) {
func ParseDocuments(r io.Reader) ([]Document, error) {
dec := yaml.NewDecoder(r)
dec.KnownFields(true)
var result []Specification
var result []Document
for {
var spec typedSpecificationFile[Specification]
if err := dec.Decode(&spec); err != nil {
var document Document
if err := dec.Decode(&document); err != nil {
if err == io.EOF {
return result, nil
}
return result, err
}
switch spec.Type {
switch document.Type {
case portfolioTypeName:
default:
return result, fmt.Errorf("incorrect specification type got %q but expected %q", spec.Type, portfolioTypeName)
return result, fmt.Errorf("incorrect specification type got %q but expected %q", document.Type, portfolioTypeName)
}

pf := spec.Spec
pf.setDefaultPolicyWeightAlgorithm()
if pf.Policy.WeightsAlgorithm == allocation.ConstantWeightsAlgorithmName {
if len(pf.Policy.Weights) != len(pf.Assets) {
return result, errAssetAndWeightsLenMismatch(&spec.Spec)
document.Spec.setDefaultPolicyWeightAlgorithm()
if document.Spec.Policy.WeightsAlgorithm == allocation.ConstantWeightsAlgorithmName {
if len(document.Spec.Policy.Weights) != len(document.Spec.Assets) {
return result, errAssetAndWeightsLenMismatch(&document.Spec)
}
}
result = append(result, pf)
result = append(result, document)
}
}

Expand Down Expand Up @@ -154,11 +148,6 @@ func (pf *Specification) Validate() error {
for _, asset := range pf.Assets {
list = append(list, asset.Validate())
}
if pf.Benchmark.ID != "" {
if err := pf.Benchmark.Validate(); err != nil {
list = append(list, err)
}
}
return errors.Join(list...)
}

Expand Down
48 changes: 28 additions & 20 deletions portfolio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,26 @@ func writeJSONResponse(res http.ResponseWriter, data any) {

func ExampleParse() {
// language=yaml
const specYAML = `
specYAML := `
---
type: Portfolio
spec:
metadata:
name: 60/40
benchmark: BIGPX
spec:
assets: [ACWI, AGG]
policy:
weights: [60, 40]
weights_algorithm: Constant Weights
rebalancing_interval: Quarterly
`

pf, err := portfolio.ParseOneSpecification(specYAML)
pf, err := portfolio.ParseOneDocument(specYAML)
if err != nil {
panic(err)
}
fmt.Println("Name:", pf.Name)
fmt.Println("Alg:", pf.Policy.WeightsAlgorithm)
fmt.Println("Name:", pf.Metadata.Name)
fmt.Println("Alg:", pf.Spec.Policy.WeightsAlgorithm)

// Output:
// Name: 60/40
Expand All @@ -88,8 +89,8 @@ func ExampleOpen() {
panic(err)
}
pf := portfolios[0]
fmt.Println("Name:", pf.Name)
fmt.Println("Alg:", pf.Policy.WeightsAlgorithm)
fmt.Println("Name:", pf.Metadata.Name)
fmt.Println("Alg:", pf.Spec.Policy.WeightsAlgorithm)

// Output:
// Name: 60/40
Expand Down Expand Up @@ -144,12 +145,12 @@ func TestParse(t *testing.T) {
{
Name: "component kind is not correct",
// language=yaml
SpecYAML: `{type: Portfolio, spec: {benchmark: []}}`,
SpecYAML: `{type: Portfolio, metadata: {benchmark: []}}`,
ErrorStringContains: "wrong YAML type:",
},
} {
t.Run(tt.Name, func(t *testing.T) {
p, err := portfolio.ParseOneSpecification(tt.SpecYAML)
p, err := portfolio.ParseOneDocument(tt.SpecYAML)
if tt.ErrorStringContains == "" {
assert.NoError(t, err)
assert.Equal(t, tt.Portfolio, p)
Expand All @@ -162,29 +163,30 @@ func TestParse(t *testing.T) {

func ExampleSpecification_Backtest() {
// language=yaml
const portfolioSpecYAML = `---
portfolioSpecYAML := `---
type: Portfolio
spec:
metadata:
name: 60/40
benchmark: BIGPX
spec:
assets: [ACWI, AGG]
policy:
weights: [60, 40]
weights_algorithm: ConstantWeights
rebalancing_interval: Quarterly
`

pf, err := portfolio.ParseOneSpecification(portfolioSpecYAML)
pf, err := portfolio.ParseOneDocument(portfolioSpecYAML)
if err != nil {
panic(err)
}

ctx := context.Background()
assets, err := pf.AssetReturns(ctx)
assets, err := pf.Spec.AssetReturns(ctx)
if err != nil {
panic(err)
}
result, err := pf.Backtest(ctx, assets, nil)
result, err := pf.Spec.Backtest(ctx, assets, nil)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -261,23 +263,29 @@ func TestPortfolio_Backtest_custom_function(t *testing.T) {
func Test_Portfolio_Validate(t *testing.T) {
for _, tt := range []struct {
Name string
Portfolio portfolio.Specification
Portfolio portfolio.Document
ExpectErr bool
}{
{
Name: "okay", Portfolio: portfolio.Specification{}, ExpectErr: false,
Name: "okay", Portfolio: portfolio.Document{
Type: "Portfolio",
}, ExpectErr: false,
},
{
Name: "bad asset",
Portfolio: portfolio.Specification{
Assets: []portfolio.Component{{ID: "_"}},
Portfolio: portfolio.Document{
Spec: portfolio.Specification{
Assets: []portfolio.Component{{ID: "_"}},
},
},
ExpectErr: true,
},
{
Name: "benchmark",
Portfolio: portfolio.Specification{
Benchmark: portfolio.Component{ID: "()"},
Portfolio: portfolio.Document{
Metadata: portfolio.Metadata{
Benchmark: portfolio.Component{ID: "()"},
},
},
ExpectErr: true,
},
Expand Down

0 comments on commit 556007e

Please sign in to comment.