A from-scratch portfolio construction and risk toolkit, written in pure
standard-library Python — no numpy, no scipy, no pandas. The only third-party
dependency is rich for the terminal
tables. Everything else — matrix inversion, the normal distribution, Monte
Carlo correlation, even the SVG chart — is hand-built.
$ portopt
╭──────────────────────────────────────────────────────────────────────────────╮
│ portopt 0.1.0 — mean-variance optimizer + risk lab │
│ synthetic: 4 assets x 1,260 daily obs (seed 42) │
│ annual risk-free rate 3.00%, VaR tail 5% │
╰──────────────────────────────────────────────────────────────────────────────╯
| Module | Problem it solves | Math behind it |
|---|---|---|
linalg |
Matrix math without numpy | Gauss–Jordan with partial pivoting, Cholesky |
data |
Reproducible market data | Seeded correlated normals + CSV loading |
stats |
Return moments & distributions | Sample moments, erf-based normal CDF, Acklam's inverse |
optimizer |
Find the "best" portfolio | Closed-form Markowitz (Lagrange) |
frontier |
Map return vs. risk trade-off | Two-fund theorem sweep |
risk |
How bad can it get? | Historical / parametric / Monte Carlo VaR & CVaR |
capm |
What return should an asset give? | OLS beta, Jensen's alpha, R² |
sim |
Distribution of outcomes over time | Compounded correlated wealth paths |
chart |
Visualize the frontier | Hand-written SVG renderer |
Each maps one-to-one onto a CLI subcommand.
Requires Python 3.9+ and a current pip (≥21.3, for PEP 660 editable installs). The macOS system Python ships an old pip, so upgrade it first.
python3 -m venv .venv && source .venv/bin/activate
pip install --upgrade pip
pip install -e .
portopt # full report: stats, optimize, frontier, risk, capm, sim
portopt chart # render the efficient frontier to frontier.svg
portopt --csv my_returns.csv optimize # run on real dataRun the test suite (94 tests):
python -m unittest discover testsGiven expected returns μ, covariance Σ, and weights w summing to one,
portfolio return is μᵀw and portfolio variance is wᵀΣw. Every portfolio
here is an analytic solution to "minimize variance for a target return"
— no quadratic-programming solver needed:
min-variance : w* = Σ⁻¹1 / (1ᵀ Σ⁻¹ 1)
max-Sharpe : w* = Σ⁻¹(μ − rf) / (1ᵀ Σ⁻¹ (μ − rf))
target-return: convex combination of two frontier portfolios
The max-Sharpe formula is only valid while rf sits below the
min-variance return — at or above it the fully-invested Sharpe is genuinely
unbounded and the closed form silently flips to the worst (short) side. The
optimizer detects this and falls back to a bounded-leverage scan of the
frontier (default 2× leverage).
The frontier is the set of return-maximizing portfolios for every level of
risk. It is independent of the risk-free rate, so the CLI samples it by
sweeping target returns from the global minimum-variance point to the best
single asset and re-solving the closed-form optimizer at each step. The
capital market line then sits on top — the line from (0, rf) through the
tangency (max-Sharpe) portfolio.
Value at Risk is reported as a loss (positive number): "the 1-day 95% VaR is 2.1%" means a 2.1% loss is the worst 1-in-20 outcome. Three independent engines are offered, and it's meaningful when they agree:
- Historical — quantile of the actual observed return series
- Parametric — assumes normality:
VaR = −(μ + z_α σ), wherez_αcomes from the hand-implemented inverse-normal CDF - Monte Carlo — quantile of Cholesky-correlated simulated returns
CVaR (expected shortfall) is the mean loss beyond the VaR cut — it captures how bad the tail actually is.
r_i − rf = α_i + β_i (r_m − rf) + ε_i
Beta is the OLS slope cov(r_i, r_m)/var(r_m); Jensen's alpha is the
intercept — the return delivered beyond what market exposure warrants. The
natural self-check is a regression of an asset against itself: beta exactly 1,
alpha exactly 0.
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Portfolio ┃ Return ┃ Vol ┃ Sharpe ┃ Weights ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Min variance │ 3.5% │ 5.3% │ 0.10 │ 10% / -3% / 78% / 16% │
│ Max Sharpe │ 17.5% │ 27.9% │ 0.52 │ -56% / 29% / -47% / 175% │
│ Target return 7% │ 7.0% │ 8.7% │ 0.47 │ -7% / 5% / 46% / 56% │
└──────────────────┴────────┴───────┴────────┴──────────────────────────┘
Negative weights are shorts — the optimizer is unconstrained. The frontier samples 40 points between the min-variance portfolio and the highest-return asset, then the capital market line lands the max-Sharpe tangency:
Tangency (max-Sharpe): return 17.5%, vol 27.9%, Sharpe 0.52 slope of the capital market line
Tail risk on the max-Sharpe portfolio — all three engines agree on daily 5% VaR:
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┓
┃ Engine ┃ VaR ┃ CVaR ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━┩
│ Historical │ 2.88% │ 3.57% │
│ Parametric (normal) │ 2.82% │ — │
│ Monte Carlo │ 2.83% │ — │
└─────────────────────┴───────┴───────┘
portopt chart writes a self-contained SVG (no plotting dependency, dark
terminal styling) — open it in any browser or embed it straight into docs:
Point --csv at your own returns (or prices with --prices); the market
proxy for the CAPM regression defaults to the first column (--market N).
portopt --csv data/returns.csv --prices allportopt/
├── __init__.py # version
├── linalg.py # inverse, solve, Cholesky, dot/matmul
├── data.py # seeded synthetic market data + CSV loading
├── stats.py # moments, annualization, normal CDF/inverse
├── optimizer.py # min-variance, tangency, target-return portfolios
├── frontier.py # efficient-frontier sweep + capital market line
├── risk.py # historical/parametric/Monte Carlo VaR & CVaR, drawdown
├── capm.py # beta, Jensen's alpha, R², Treynor, info ratio
├── sim.py # Monte Carlo wealth-path simulation
├── chart.py # hand-written SVG efficient-frontier renderer
└── cli.py # rich terminal UI
tests/ # 94 unittest assertions across every module
- Long/short box constraints (
0 ≤ w ≤ 1) via the same closed-form core - Rolling-window backtest comparing candidate portfolios
- Factor-model attribution beyond single-factor CAPM (Fama–French)
-
.stl— export the frontier path for 3D plotting? (why not) - Optional numpy backend for larger universes (drop-in, same API)
MIT. Educational project — not investment advice.