Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

re-model portfolio data structure #5

Merged
merged 14 commits into from
Dec 18, 2023
29 changes: 29 additions & 0 deletions allocation/constant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package allocation

import (
"context"
"errors"
"time"

"github.com/portfoliotree/portfolio/returns"
)

const ConstantWeightsAlgorithmName = "Constant Weights"

type ConstantWeights struct {
weights []float64
}

func (cw *ConstantWeights) Name() string { return ConstantWeightsAlgorithmName }

func (cw *ConstantWeights) PolicyWeights(_ context.Context, _ time.Time, _ returns.Table, ws []float64) ([]float64, error) {
if len(cw.weights) != len(ws) {
return nil, errors.New("expected the number of policy weights to be the same as the number of assets")
}
copy(ws, cw.weights)
return ws, nil
}

func (cw *ConstantWeights) SetWeights(in []float64) {
cw.weights = in
}
21 changes: 21 additions & 0 deletions allocation/equal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package allocation

import (
"context"
"time"

"github.com/portfoliotree/portfolio/returns"
)

const EqualWeightsAlgorithmName = "Equal Weights"

type EqualWeights struct{}

func (*EqualWeights) Name() string { return EqualWeightsAlgorithmName }

func (*EqualWeights) PolicyWeights(_ context.Context, _ time.Time, _ returns.Table, ws []float64) ([]float64, error) {
for i := range ws {
ws[i] = 1.0 / float64(len(ws))
}
return ws, nil
}
42 changes: 42 additions & 0 deletions allocation/functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package allocation

import (
"golang.org/x/exp/slices"

"github.com/portfoliotree/portfolio/backtest"
)

type Algorithm interface {
backtest.PolicyWeightCalculator
Name() string
}

func NewDefaultAlgorithmsList() []Algorithm {
return []Algorithm{
new(ConstantWeights),
new(EqualWeights),
new(EqualInverseVariance),
new(EqualRiskContribution),
new(EqualVolatility),
new(EqualInverseVolatility),
}
}

func AlgorithmNames(algorithmOptions []Algorithm) []string {
names := make([]string, 0, len(algorithmOptions))
for _, alg := range algorithmOptions {
names = append(names, alg.Name())
}
slices.Sort(names)
names = slices.Compact(names)
return names
}

type WeightSetter interface {
SetWeights([]float64)
}

func AlgorithmRequiresWeights(alg Algorithm) bool {
_, ok := alg.(WeightSetter)
return ok
}
44 changes: 44 additions & 0 deletions allocation/inverse_variance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package allocation

import (
"context"
"math"
"time"

"github.com/portfoliotree/portfolio/returns"
)

type EqualInverseVariance struct{}

func (cw *EqualInverseVariance) Name() string { return "Equal Inverse Variance" }

func (*EqualInverseVariance) PolicyWeights(_ context.Context, _ time.Time, assetReturns returns.Table, ws []float64) ([]float64, error) {
if isOnlyZeros(ws) {
for i := range ws {
ws[i] = 1.0
}
scaleToUnitRange(ws)
}

err := ensureEnoughReturns(assetReturns)
if err != nil {
return ws, err
}

assetRisks := assetReturns.RisksFromStdDev()
for i := range assetRisks {
assetRisks[i] = 1.0 / math.Pow(assetRisks[i], 2)
}

sumOfAssetRisks := 0.0
for i := range assetRisks {
sumOfAssetRisks += assetRisks[i]
}

newWeights := make([]float64, len(assetRisks))
for i := range assetRisks {
newWeights[i] = assetRisks[i] / sumOfAssetRisks
}

return newWeights, nil
}
95 changes: 95 additions & 0 deletions allocation/optimizer_internal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package allocation

import (
"context"
"errors"

"gonum.org/v1/gonum/optimize"

"github.com/portfoliotree/portfolio/returns"
)

const (
maxTries = 50_000
skipContextCheckCount = 500
preCancelCheckTries = 10_000
)

func checkTries(ctx context.Context, try int) error {
switch {
case try > preCancelCheckTries && try%skipContextCheckCount == 0:
return ctx.Err()
case try > maxTries:
return errors.New("reached max tries to calculate policy")
default:
return nil
}
}

func optWeights(ctx context.Context, weights []float64, fn func(ws []float64) float64) error {
var (
try = 0
m = &optimize.NelderMead{}
s = &optimize.Settings{
Converger: &optimize.FunctionConverge{
Absolute: 1e-10,
Relative: 1,
Iterations: 1000,
},
}
ws = make([]float64, len(weights))
p = optimize.Problem{
Func: func(x []float64) float64 {
copy(ws, x)
scaleToUnitRange(ws)
return fn(ws)
},
Status: func() (optimize.Status, error) {
err := checkTries(ctx, try)
if err != nil {
return optimize.RuntimeLimit, err
}
try++
return optimize.NotTerminated, nil
},
}
)
optResult, err := optimize.Minimize(p, weights, s, m)
if err != nil {
return err
}

copy(weights, optResult.X)
scaleToUnitRange(weights)

return nil
}

func ensureEnoughReturns(assetReturns returns.Table) error {
if assetReturns.NumberOfColumns() == 0 || assetReturns.NumberOfRows() < 2 {
return errors.New("not enough data")
}
return nil
}

func isOnlyZeros(a []float64) bool {
for _, v := range a {
if v != 0 {
return false
}
}
return true
}

func scaleToUnitRange(list []float64) {
sum := 0.0
for _, v := range list {
sum += v
}
if sum == 0 {
return
}
for i := range list {
list[i] /= sum
}
}
46 changes: 46 additions & 0 deletions allocation/risk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package allocation

import (
"context"
"math"
"time"

"github.com/portfoliotree/portfolio/calculations"
"github.com/portfoliotree/portfolio/returns"
)

type EqualRiskContribution struct{}

func (*EqualRiskContribution) Name() string { return "Equal Risk Contribution" }

func (*EqualRiskContribution) PolicyWeights(ctx context.Context, _ time.Time, assetReturns returns.Table, ws []float64) ([]float64, error) {
if isOnlyZeros(ws) {
for i := range ws {
ws[i] = 1.0
}
scaleToUnitRange(ws)
}

err := ensureEnoughReturns(assetReturns)
if err != nil {
return ws, err
}

assetRisks := assetReturns.RisksFromStdDev()

target := 1.0 / float64(len(assetRisks))

cm := assetReturns.CorrelationMatrix()

weights := make([]float64, len(ws))
copy(weights, ws)

return weights, optWeights(ctx, weights, func(ws []float64) float64 {
_, _, riskWeights := calculations.RiskFromRiskContribution(assetRisks, ws, cm)
var diff float64
for i := range riskWeights {
diff += math.Abs(target - riskWeights[i])
}
return diff
})
}
75 changes: 75 additions & 0 deletions allocation/volatility.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package allocation

import (
"context"
"time"

"github.com/portfoliotree/portfolio/returns"
)

type EqualVolatility struct{}

func (*EqualVolatility) Name() string { return "Equal Volatility" }

func (*EqualVolatility) PolicyWeights(_ context.Context, _ time.Time, assetReturns returns.Table, ws []float64) ([]float64, error) {
if isOnlyZeros(ws) {
for i := range ws {
ws[i] = 1.0
}
scaleToUnitRange(ws)
}

err := ensureEnoughReturns(assetReturns)
if err != nil {
return ws, err
}

assetRisks := assetReturns.RisksFromStdDev()

sumOfAssetRisks := 0.0
for i := range assetRisks {
sumOfAssetRisks += assetRisks[i]
}

newWeights := make([]float64, len(assetRisks))
for i := range assetRisks {
newWeights[i] = assetRisks[i] / sumOfAssetRisks
}

return newWeights, nil
}

type EqualInverseVolatility struct{}

func (*EqualInverseVolatility) Name() string { return "Equal Inverse Volatility" }

func (*EqualInverseVolatility) PolicyWeights(_ context.Context, _ time.Time, assetReturns returns.Table, ws []float64) ([]float64, error) {
if isOnlyZeros(ws) {
for i := range ws {
ws[i] = 1.0
}
scaleToUnitRange(ws)
}

err := ensureEnoughReturns(assetReturns)
if err != nil {
return ws, err
}

assetRisks := assetReturns.RisksFromStdDev()
for i := range assetRisks {
assetRisks[i] = 1.0 / assetRisks[i]
}

sumOfAssetRisks := 0.0
for i := range assetRisks {
sumOfAssetRisks += assetRisks[i]
}

newWeights := make([]float64, len(assetRisks))
for i := range assetRisks {
newWeights[i] = assetRisks[i] / sumOfAssetRisks
}

return newWeights, nil
}
18 changes: 18 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"os"
"strings"

"go.mongodb.org/mongo-driver/bson/primitive"

"github.com/portfoliotree/portfolio/returns"
)

Expand Down Expand Up @@ -57,6 +59,22 @@ func (pf *Specification) AssetReturns(ctx context.Context) (returns.Table, error
return doJSONRequest[returns.Table](http.DefaultClient.Do, req)
}

func ParseComponentsFromURL(values url.Values, prefix string) ([]Component, error) {
assetValues, ok := values[prefix+"-id"]
if !ok {
return nil, errors.New("use asset-id parameters to specify asset returns")
}
components := make([]Component, 0, len(assetValues))
for _, v := range assetValues {
if _, err := primitive.ObjectIDFromHex(v); err == nil {
components = append(components, Component{Type: "Portfolio", ID: v})
continue
}
components = append(components, Component{Type: "Security", ID: v})
}
return components, nil
}

func doJSONRequest[T any](do func(r *http.Request) (*http.Response, error), req *http.Request) (T, error) {
var result T
req.Header.Set("accept", "application/json")
Expand Down
Loading