Nathan Gomes

Project 02  /  Portfolio construction & risk

Strata

A portfolio construction and risk system: build an allocation from any set of tickers, or let an optimizer solve for one, then see what the rule earned out of sample, what it cost to run, and the range of outcomes that could follow.

The verdict: of four allocation policies on eight TSX stocks, Balanced earned the best risk-adjusted return (Sharpe 0.97 against 0.82 for XIC) with a shallower worst loss than the index. Growth's extra return came with a −48.7% drawdown. A minimum-variance optimizer delivered the lower volatility it promised but lost to plain equal weighting on Sharpe (0.79 against 1.00) while trading 2.7× as much.
Test window
Jan 2019 – Aug 2026
1,923 daily sessions, CAD
Universe
8 TSX stocks · 5 sectors
XIC.TO benchmark
Rules
Growth · Income · Balanced
Low volatility · 12 optimizers
Stack
Python · pandas · NumPy
SQLite · cvxpy · FastAPI
Balanced · Sharpe 0.97
XIC 0.82 · drawdown −28.7%
Best of the four policies
Growth · annual return 27.8%
XIC 16.2% · volatility 28.6%
11.6 pts ahead
Growth · worst drawdown −48.7%
XIC −37.2% · Balanced −28.7%
11.5 pts deeper than XIC
Min variance · Sharpe 0.79
Equal weight 1.00 · risk parity 0.98
Optimizer lost out of sample

CAD 100,000 per portfolio from Jan 2019 to Aug 2026, rebalanced monthly to target, 10 bps on traded notional. Optimizers re-solve at each rebalance from the previous 252 sessions and are judged only on the returns that followed. Benchmark: XIC.TO, held without rebalancing.

The thirty-second version

1

Balance beat concentration on risk-adjusted terms

Balanced returned 19.1% a year against XIC's 16.2% at almost the same volatility, and its worst loss was −28.7% against the index's −37.2%. One sample and one universe, so this is an observed result, not evidence of persistent skill.

2

Growth's return came with half its value at risk

A 65% technology target compounded at 27.8% a year but fell −48.7% from peak. In five-year scenarios it drops below 80% of the starting balance in 24.2% of paths, against 8.8% for Balanced, and finishes behind Balanced in 19.3% of shared market paths.

3

Optimizing did not beat equal weight

Minimum variance achieved lower volatility (15.2% against 16.9%) but a Sharpe of 0.79 against 1.00 for equal weighting, with 2.7× the turnover. An optimal answer to estimated inputs is not the best realized portfolio; the system reports that instead of assuming the sophisticated method wins.

02

The application

Build a portfolio, test it, see where it could land

Strata is the deployed version of the research: enter Yahoo Finance tickers and weights, or pick one of twelve registered construction methods, and a single run returns the realized history, the costs, a drawdown profile and 5,000 simulated five-year paths. Up to four portfolios run side by side in the same simulated markets, so differences come from construction rather than a luckier draw.

Strata app: portfolio builder on the left; realized value since 2016 continuing into a five-year scenario fan, and a results table for Growth, Balanced, Income and XIC
One continuous picture. Realized value through today, then the five-year scenario fan, with costs, Sharpe, Sortino and drawdown for every portfolio.
Strata scenario panel: median, weak and strong five-year outcomes, loss frequency and drawdown, with a range of final values per portfolio
Where five years could land. Median, weak and strong outcomes with their sampling intervals, loss frequency and typical worst drop.
Strata optimizer panel: shrinkage, effective bets, diversification ratio, estimated against realized volatility and turnover for a walk-forward minimum-variance portfolio
Optimization, out of sample. What the risk model expected against what the portfolio realized, and how the answer was reached.
BUILD

Portfolios & strategy

Tickers and weights, or a registered method: minimum variance, risk parity, maximum diversification, maximum Sharpe and their convex variants.

MANDATE

Constraints

Position caps, sector and country limits, turnover budgets, tracking-error ceilings, and optional spread and impact costs.

RISK

Risk models

Sample covariance, Ledoit-Wolf shrinkage, or a statistical factor model whose factor count follows the Marchenko-Pastur noise edge.

REPLAY

History & drawdowns

Monthly rebalancing with daily drift, costs on traded notional, rolling one- and three-year return ranges, depth of every drawdown.

SIMULATE

Five-year scenarios

5,000 block-bootstrap paths shared across portfolios, head-to-head comparisons in the same markets, and sampling intervals on each percentile.

RECORD

Run records

Every result carries an identifier built from the request, the price data and the source code, with library versions, and exports in full.

03

Historical results

What CAD 100,000 in each policy became

Growth, Income and Balanced hold fixed target weights; Low volatility weights by inverse volatility estimated from the first 252 sessions and then frozen. Weights drift daily and reset at the first close of each month, and costs come out of the portfolio before the next day's return. Log scale, so equal slopes mean equal percentage growth.

$100k $150k $200k $300k $400k $600k 2019 2020 2021 2022 2023 2024 2025 2026 $649k $380k $313k $344k $315k
GrowthBalancedIncomeLow volatilityXIC benchmark

Drawdown from the running peak

-40% -20% 0% 2019 2020 2021 2022 2023 2024 2025 2026
GrowthBalancedXIC benchmark
PortfolioTotal returnAnnual returnVolatilitySharpeMax drawdownCosts paid
Growth549%27.8%28.6%0.90−48.7%$1,857
Balanced280%19.1%16.3%0.97−28.7%$777
Income213%16.1%16.1%0.83−33.1%$396
Low volatility244%17.6%15.3%0.94−29.7%$574
XIC benchmark215%16.2%16.4%0.82−37.2%$0
One comparison, several objectives. Growth leads on return, Balanced on Sharpe, and Low volatility on realized volatility (15.3% against XIC's 16.4%). Each answers a different investment question, so the useful output is the trade-off, not a winner. Income shows why a label needs testing against holdings: banks, utilities and energy gave it XIC's return and a smaller drawdown with no meaningful volatility advantage. Adjusted prices approximate reinvested dividends; judging it as an income strategy would need a cash-distribution ledger.
How much is the benchmark choice doing? XIC is the natural same-currency reference, but these portfolios hold eight names in five sectors against an index of more than 200, so every gap mixes sector exposure with security selection. They are observed differences for these chosen names, not isolated manager skill.
04

Did optimizing help?

Optimized weights, judged only on what came next

Beyond fixed targets, Strata re-solves the weights at every monthly rebalance from the previous 252 sessions, using Ledoit-Wolf covariance and a 35% cap per holding, and holds them over returns the optimizer never saw. Equal weighting on the same dates is the honest control: it needs no estimates at all.

Construction ruleAnnual returnVolatilitySharpeTurnover a yearCosts paid
Minimum variance14.9%15.2%0.791.40$1,698
Risk parity18.1%15.2%0.980.55$826
Equal weight20.2%16.9%1.000.51$886
XIC benchmark16.2%16.4%0.820.00$0
Minimum variance did its job and still lost. It ran at 15.2% volatility against equal weighting's 16.9%, but its Sharpe of 0.79 trailed equal weighting's 1.00, and re-solving every month cost $1,698 against $886. The app shows why on a longer 2017–2026 run of the same rule: the risk model expected 8.7% volatility and the portfolio realized 14.5%. Estimated covariance understates risk exactly when correlations rise. This is consistent with the estimation-error problem studied by DeMiguel, Garlappi and Uppal (2009), not a replication of it.
Risk parity was the middle road. It matched minimum variance's volatility (15.2%) with a Sharpe of 0.98, close to equal weighting, at the lowest turnover of the optimized rules. My ETF research pipeline runs the same class of optimizer on a less correlated six-ETF universe and reaches the opposite conclusion about whether optimizing helps, which is the point: the answer depends on the universe, not on the method's sophistication.
05

Five-year scenarios

The same CAD 100,000, five more years

Every portfolio starts a fresh CAD 100,000 on Aug 2026 and is compounded through the same 5,000 sequences of 20-session blocks drawn from its completed history. Shared blocks mean Growth and Balanced face identical markets in each path, so they can be compared path by path. Shaded: 5th–95th and 25th–75th percentiles; line: median.

Growth

$0k $200k $400k $600k $800k $1,000k Start Yr 1 Yr 2 Yr 3 Yr 4 Yr 5 $872k $329k $115k

Balanced

$0k $200k $400k $600k $800k $1,000k Start Yr 1 Yr 2 Yr 3 Yr 4 Yr 5 $395k $236k $132k
Portfolio5th percentileMedian95th percentileEnds below startBelow 80% at any pointMedian worst drop
Growth$114,708$329,002$872,3623.1%24.2%−33.5%
Balanced$131,665$235,938$394,9530.7%8.8%−20.3%
Income$119,698$211,349$341,7581.6%12.2%−22.1%
Low volatility$129,046$222,660$355,2500.8%9.2%−19.4%
XIC benchmark$106,182$213,678$360,3503.9%16.1%−24.2%
Compare in the same markets, not side by side. Growth has the higher median, but it finishes behind Balanced in 19.3% of shared paths, and at the 5th percentile of the path-by-path difference it trails by $50,893. Subtracting the two portfolios' separate 5th percentiles would not give that number; pairing the paths does.
How the history is sampled moves the downside. With 126-session blocks instead of 20, which keep longer runs of history together, Growth's 5th-percentile ending value falls to $90,882, below the starting CAD 100,000, and its chance of ending at a loss roughly doubles (3.1% to 6.2%). It still keeps the higher median. The sample contains an unusually strong decade for Canadian equities, so these are ranges under a historical-repeat assumption, not forecasts, and they have not been validated against unseen five-year outcomes.
06

How it works

A traceable path from prices to a published data mart

01  EXTRACT

Checksummed inputs

Adjusted prices from yfinance or a frozen CSV verified by SHA-256, target holdings and a security master.

yfinancepandas
02  VALIDATE

Reject bad data

Conflicting duplicates, missing or non-finite prices, unknown holdings and incomplete ticker-date coverage all stop the run.

pandaspytest
03  CALCULATE

Portfolio accounting

Previous-close weights, daily drift, monthly resets, costs on traded notional, then returns, risk and drawdowns.

NumPycvxpy
04  PUBLISH

SQL data mart

Five tables replace the SQLite snapshot atomically after positions reconcile to NAV; failed runs keep the previous database.

SQLiteSQL
Accounting conventions

Portfolio return

rp,t = ∑ wi,t−1 ri,t

Weights from the previous close. End-of-day weights would let today's move choose the allocation that earned it.

Trading cost

cost = V × ∑ |wtarget − wdrifted| × 0.001

10 bps on total traded notional, purchases and sales, charged before the next return.

Annualized return

CAGR = (Vend / Vstart)252/N − 1

On net portfolio value over N observed daily returns.

Sharpe ratio

mean(r − rf) / s(r) × √252

A fixed 3% annual risk-free rate, converted to a daily compound rate.

Drawdown

Dt = Vt / max(V0…Vt) − 1

Starting capital counts as the first peak, so an immediate loss registers.

Scenarios

5,000 paths × 1,260 sessions

Blocks of 20 completed net returns, the same block draws for every portfolio; costs are embedded in the history sampled.

The data mart holds securities, security_daily_analytics, portfolio_positions, portfolio_daily_summary and pipeline_runs. The analysis queries use CTEs, ROW_NUMBER for month-end NAV, LAG for compounded returns and DENSE_RANK for volatility ranking.

07

Research to production

A contract between a researcher's rule and running software

Each construction method is a function from a context to a weight vector, registered with metadata. The API advertises the registry, the interface builds its menu from it, and the walk-forward loop calls the same contract, so a new method inherits every mandate limit and check without knowing they exist.

Conformance gate

Fifteen checks on every method

Valid weights, respected constraints, repeatable results, asset-order invariance, degenerate inputs and a solve-time budget. It runs in CI and fails the build; tests feed it deliberately broken methods to prove it rejects them.

12 of 12 registered methods pass

What the gate caught

Defects with investment consequences

A maximum-Sharpe solver that blew its 750 ms budget with redundant restarts, mandate parameters leaking between objectives, and a ratio transform whose answer changed with the units of the returns.

Each now covered by a regression test

Provenance

Every result has a run record

An identifier built from the request, the price data and the source code, with library versions. Two results are the same only if their records match, not because their links do.

Checksummed inputs · run manifest

Operations

Honest failure modes

Runs report progress, reuse cached results that match exactly, and surface data-source failures rather than silently substituting frozen prices. Solver status and constraint duals are reported, so infeasible is never shown as optimal.

Deployed on Render · offline single-file demo
08

Validation

Tests aimed at the errors that would change the conclusion

  • Each day's return is earned by the weights held at the previous close
  • Low-volatility weights come from the first 252 sessions only; rewriting later prices leaves them unchanged
  • Rewriting the final month of prices leaves every earlier optimized weight unchanged
  • Scenarios start after the last completed backtest date and sample only completed returns
  • Positions reconcile to NAV before the data mart is published
  • Paired scenario differences are tested against subtracting separate percentiles
  • Three independent implementations of two optimizers agree, including a MATLAB cross-check under Octave
  • A 10% loss then a 10% gain compounds to a 1% loss; costs reduce final value
What the tests cannot remove. The eight stocks and the four policies were chosen with hindsight, so results carry selection and survivorship bias even with correctly timed weights. Adjusted prices are today's snapshot, not a point-in-time archive, and the constant risk-free rate and flat costs are simplifications. 178 automated tests across the research pipeline and the application run in GitHub Actions with the conformance gate, the MATLAB cross-check, a synthetic pipeline run and the demo build.
09

Next research iteration

Test the downside harder before trusting the upside

Rerun the scenarios under explicit adverse regimes and other historical windows, add a trading calendar and coverage audit, scale the market-impact model by average daily volume, replace the cardinality heuristic with a mixed-integer program, and use point-in-time universes once membership and delisting data are available. If an allocation only looks attractive under one set of assumptions, that dependence belongs in the decision.

Portfolio construction

Fixed-target and inverse-volatility policies, walk-forward convex optimization with mandates, shrinkage and factor risk models, cost-aware rebalancing.

cvxpyLedoit-WolfRisk parity

Risk & simulation

Drawdown analysis, block-bootstrap scenarios shared across portfolios, paired comparisons, sampling sensitivity and percentile intervals.

NumPyBootstrapDrawdowns

Data engineering

Validated ETL with checksums, an atomically published SQLite mart, reconciliation, run provenance, a strategy contract with a CI conformance gate.

SQLpytestGitHub Actions

Try it

Build a portfolio and see where five years could land

The free instance may take a minute to wake up; the browser demo runs instantly on nine frozen securities. Scenarios are simulations, not predictions, and nothing here is investment advice.