This project reproduces the topological portfolio model of Goel, Sharma and
Kanniainen (2026, arXiv:2601.03974). I implemented the Takens embedding, the persistence landscape and its
Using topology of data we can avoid model-based estimation errors linked to distributional assumptions or statistical inputs like mean and covariance. A 2026 January paper by Goel, Sharma, Kanniainen (arXiv: https://doi.org/10.48550/arXiv.2601.03974) reports that portfolios constructed using Topological Data Analysis methods outperformed seven popular portfolio optimization models and two benchmark portfolio strategies, the naive 1/N portfolio and the S&P 500 market index, in terms of excess mean return and several financial ratios.
The practical argument is dimensional. Authors used
The intuitive idea of the method is as follows. We map a one-dimensional time-series of returns into
It may happen that at some radius
From a topological point of view, more scattering of the point cloud means less stable returns; a more concentrated cloud, more stable. The amount of scatteredness among return observations over time can be quantified using the
[1] Bubenik, P., et al. Statistical topological data analysis using persistence landscapes. J. Mach. Learn. Res. 16(1), 77–102 (2015).
Implementation and verification of the method, plus characterisation of the risk measure. Portfolio performance is not evaluated here; testing the paper's performance claim requires their 462-constituent universe; a 19-name basket cannot settle it.
How Λ relates to volatility. Across 19 US large-cap tickers (18 stocks plus the SPY ETF, 2018–2023), Λ ranked assets similarly but not identically to annualised volatility (Spearman ρ ≈ 0.73). On a wider 20-instrument cross-section spanning FX, bonds, commodities, indices and crypto (2020–2026), the agreement is much stronger (ρ ≈ 0.95), suggesting the lower correlation within equities reflects the narrow volatility range of that sample rather than genuinely independent information.
git clone https://github.com/gitiann/topological-portfolio-analysis
cd topological-portfolio-analysis
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,qp]"
python scripts/make_sample_data.py
pytest
toporisk build --prices data/sample_prices.csv --out figures/weights.png
Fetch a cross-asset basket (FX, bonds, commodities, indices, crypto) and compare
topological risk against annualised volatility. Requires yfinance:
pip install yfinance
bash scripts/example_cross_asset.sh 2020-01-01 2026-08-07Downloads into data/cross_asset/ and prints Λ and annualised volatility per
instrument with their ranks, plus the Spearman rank correlation between the two.
Edit the ticker list in the script to use your own basket.
Quote tickers containing = or ^ so the shell doesn't expand them. Do not
glob data/*.csv — that would include sample_prices.csv, which is a wide
multi-asset panel rather than a single series, and it will be misread as one
asset.
Λ is dimensionless, since simple returns carry no units and so neither do distances in the embedding, which is what makes it comparable across instruments with entirely different price scales.
For each asset independently: take its simple-return series, cut it into overlapping sub-windows, Takens-embed each sub-window into a point cloud, compute the H0 (0th homology group, connected components) persistence landscape of each cloud, and measure how much the landscape's L¹ norm varies across sub-windows. That variability is the asset's topological risk Λᵢ. Assets are then coupled only through a diagonal risk matrix Q = diag(Λᵢ) and a long-only quadratic program that minimises wᵀQw. There is no cross-asset topology in this model, by design (the paper flags cross-asset coupling as future work).
Fixed parameters, per the paper (verify against your copy): simple returns; d = 3, τ = 1; sub-window length T̃ = 126, shift h = 21; k = 1; norm order p = 1.
src/toporisk/
data.py load prices, simple returns
embedding.py sub_windows, takens_embedding
topology.py Gudhi H0 diagram, finite_pairs
landscape.py mean_landscape, persistence_landscape, lp_norm
risk.py risk_matrix, asset_topological_risk
portfolio.py min_topological_risk_portfolio
cli.py toporisk build ...
viz.py weight + landscape plots
tests/ pytest (34 passing)
scripts/ make_sample_data.py, fetch_prices.py, compare_risk_vs_vol.py, example_cross_asset.sh
The pipeline is built and tested end to end. Key functions:
-
embedding.takens_embedding— length-T̃series,d=3, τ=1→T̃-2points in$\mathbb{R}^3$ . -
landscape.persistence_landscape— tent functions sampled on a fixed shared grid, so landscapes from different sub-windows can be averaged pointwise. -
landscape.lp_norm— discrete Lᵖ norm of a sampled landscape (p=1), grid-spacing aware. -
risk.asset_topological_risk— composes 1–3 over the sub-windows. The reference term is‖mean landscape‖. Atp=1this coincides withmean(‖landscape‖)since the L¹ norm is linear on non-negative functions, but the distinction matters ifpis changed. -
portfolio.min_topological_risk_portfolio— the convex QP; for diagonalQit reduces to inverse-risk weighting, with zero-risk assets excluded. - Landscape convention: this implementation uses Bubenik's tent, peak
(d−b)/2. Gudhi'srepresentations.Landscapemeasures perpendicular distance to the diagonal, peak(d−b)/√2. The two agree up to that factor (verified intest_landscape_matches_gudhi), which scales Λ by 2 and leaves portfolio weights unchanged, since the QP's argmin is scale-invariant.