Skip to content

Repository files navigation

portopt — mean-variance optimizer + risk lab

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%                                     │
╰──────────────────────────────────────────────────────────────────────────────╯

What it does

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.

Quickstart

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 data

Run the test suite (94 tests):

python -m unittest discover tests

The math

1. Mean-variance optimization (Markowitz, 1952)

Given 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).

2. The efficient frontier

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.

3. VaR and CVaR

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_α σ), where z_α 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.

4. CAPM

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.

Example: default report (seed 42)

┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ 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% │ —     │
└─────────────────────┴───────┴───────┘

The chart

portopt chart writes a self-contained SVG (no plotting dependency, dark terminal styling) — open it in any browser or embed it straight into docs:

Efficient frontier

Real data

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 all

Project layout

portopt/
├── __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

Roadmap

  • 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)

License

MIT. Educational project — not investment advice.

About

Markowitz mean-variance portfolio optimizer + risk analytics toolkit built from scratch in pure Python — no numpy, no scipy, no pandas. Gauss-Jordan matrix inversion, closed-form efficient frontier, VaR/CVaR, CAPM beta & alpha, Monte Carlo simulation, and an SVG chart renderer.94 tests, CLI with rich terminal tables.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages