|
| 1 | +# `optimalportfolios.examples` |
| 2 | + |
| 3 | +Runnable scripts illustrating every solver, covariance estimator, and end-to-end |
| 4 | +workflow in the package. Each file runs as `python -m optimalportfolios.examples.<path>` |
| 5 | +or by executing the script directly; outputs go to figures and/or local PDFs. |
| 6 | + |
| 7 | +## Layout |
| 8 | + |
| 9 | +``` |
| 10 | +examples/ |
| 11 | +├── data/ fixtures (asset universes loaded once, reused everywhere) |
| 12 | +├── solvers/ one demo per single-objective solver |
| 13 | +├── backtests/ end-to-end rolling backtest workflows |
| 14 | +├── comparisons/ A-vs-B examples (covar / optimiser / parameter / config) |
| 15 | +├── covar_estimation/ covariance estimator demos (EWMA, LASSO, GLASSO, factor model) |
| 16 | +└── sp500_universe.py S&P 500 universe loader (kept at top level for assignment refs) |
| 17 | +``` |
| 18 | + |
| 19 | +The folder split follows three orthogonal axes: |
| 20 | + |
| 21 | +- **What** the script demonstrates — a *solver*, an *estimator*, or a *full workflow*. |
| 22 | +- **How many configurations** it runs — a single one (`solvers/`, `backtests/`) or a sweep over several (`comparisons/`). |
| 23 | +- **Where the data comes from** — a shared fixture in `data/`, or a script-local download. |
| 24 | + |
| 25 | +Most files expose a `LocalTests` enum and a `run_local_test()` function so individual |
| 26 | +demos can be selected without editing code. |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## `data/` — fixtures |
| 31 | + |
| 32 | +Shared universe-loading helpers imported by demos elsewhere in the tree. Place new |
| 33 | +fixture-style helpers here so that any example referencing them can use one stable |
| 34 | +import path. |
| 35 | + |
| 36 | +| File | Purpose | |
| 37 | +|---|---| |
| 38 | +| `universe.py` | Two helpers for example demos. `fetch_benchmark_universe_data()` returns a 15-ETF universe across 5 asset classes (Equities, Bonds, IG, HY, Commodities) with asset-class loadings and benchmark weights — used by most `solvers/`, `backtests/`, and `comparisons/` files. `fetch_minimal_universe_data()` returns a compact 8-ETF universe with a 3-tuple `(prices, benchmark_prices, group_data)` — used by `backtests/minimal_backtest` and `solvers/long_short`. Both helpers fetch via `yfinance`. | |
| 39 | + |
| 40 | +**Companion fixture at top level (intentionally not moved):** |
| 41 | + |
| 42 | +| File | Purpose | |
| 43 | +|---|---| |
| 44 | +| `sp500_universe.py` | S&P 500 historical constituents with point-in-time inclusion indicators from [fja05680/sp500](https://github.com/fja05680/sp500). Kept at the top level because external assignment material references its path. | |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## `solvers/` — one demo per single-objective solver |
| 49 | + |
| 50 | +Each file shows both a single-date solve via `wrapper_*` and a rolling backtest via |
| 51 | +`rolling_*` for the same objective. Useful as a Rosetta stone between objectives. |
| 52 | + |
| 53 | +| File | Solver | Method | |
| 54 | +|---|---|---| |
| 55 | +| `min_variance.py` | `rolling_quadratic_optimisation` (MIN_VARIANCE) | CVXPY QP. Minimise `w′Σw`. | |
| 56 | +| `max_sharpe.py` | `rolling_maximize_portfolio_sharpe` | CVXPY SOCP via Charnes–Cooper transformation. Rolling EWMA mean + covar. | |
| 57 | +| `max_diversification.py` | `rolling_maximise_diversification` | SciPy SLSQP. Maximise `DR(w) = w′σ / sqrt(w′Σw)`. | |
| 58 | +| `risk_budgeting.py` | `rolling_risk_budgeting` | pyrb ADMM. Equal or specified risk contributions. | |
| 59 | +| `carra_mixture.py` | `rolling_maximize_cara_mixture` | SciPy SLSQP. Expected CARA utility under K-component Gaussian mixture. | |
| 60 | +| `tracking_error.py` | `rolling_maximise_alpha_over_tre` | CVXPY QCQP. Maximise α′(w − w_b) subject to TE budget. EWMA momentum signal vs ETF benchmark. | |
| 61 | +| `target_return.py` | `rolling_maximise_alpha_with_target_return` | CVXPY. Maximise alpha subject to a target portfolio return (yield + price-return). | |
| 62 | +| `long_short.py` | dispatcher `compute_rolling_optimal_weights` | Demonstrates the long-short constraint flow (`is_long_only=False`, `min_exposure`/`max_exposure` set explicitly). | |
| 63 | + |
| 64 | +All these use the `data/universe.py` fixture except `target_return.py`, which |
| 65 | +builds its own universe inline (it needs extra columns: yields, dividends, and |
| 66 | +target returns not provided by the shared fixture). |
| 67 | + |
| 68 | +--- |
| 69 | + |
| 70 | +## `backtests/` — end-to-end rolling workflows |
| 71 | + |
| 72 | +Full SAA-style workflows: load universe → estimate covariance → run rolling solver → generate factsheet PDF. Pick one as a starting template. |
| 73 | + |
| 74 | +| File | What it shows | |
| 75 | +|---|---| |
| 76 | +| `minimal_backtest.py` | Smallest end-to-end example: defines a universe, runs `compute_rolling_optimal_weights` for one objective, prints/plots NAVs. Best starting point for new users. | |
| 77 | +| `balanced_risk_budgets.py` | Illustrates `solve_for_risk_budgets_from_given_weights`: given a static 60/40 weight, back out the equivalent risk-budget portfolio and compare weights vs risk contributions. Useful for translating between mandate languages (weight-based ↔ risk-based). | |
| 78 | +| `tracking_error_decomposition.py` | Computes per-asset *tracking-error contributions* of a portfolio vs benchmark. Two modes: marginal TE contributions (sum to total TE) and independent (diagonal) TE contributions. Decomposition tool, not a solver demo. | |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## `comparisons/` — A-vs-B sweeps |
| 83 | + |
| 84 | +Each file runs the same underlying workflow under several configurations and |
| 85 | +compares the results in a single factsheet or table. Use these as templates when |
| 86 | +calibrating production parameters. |
| 87 | + |
| 88 | +| File | Axis being swept | |
| 89 | +|---|---| |
| 90 | +| `optimisers.py` | Several `PortfolioObjective` values on the same universe and covariance estimator. Compares NAV, turnover, group exposure across objectives. | |
| 91 | +| `covar_estimators.py` | EWMA vs LASSO vs Group LASSO factor covariance, with and without vol-normalisation. Same optimiser throughout. | |
| 92 | +| `parameter_sensitivity.py` | One-method, multiple parameter values (e.g. carra grid, span grid). Backtester sensitivity panel. | |
| 93 | +| `pyrb_vs_scipy.py` | Two implementations of constrained risk budgeting: the ADMM (pyrb) and a naive SciPy SLSQP. Demonstrates why pyrb is the production backend. | |
| 94 | +| `sp500_minvar_spans.py` | Min-variance on S&P 500 across EWMA spans of 26 / 52 / 104 / 208 weeks (half-lives 6m / 1y / 2y / 4y). Imports `load_sp500_universe_yahoo` from the top-level `sp500_universe.py`. | |
| 95 | +| `drift_policy.py` | Compares `OptimiserConfig.use_drifted_weights_0 = True` (production default, B) vs `False` (legacy, A) using `rolling_quadratic_optimisation` with a binding L1 turnover budget. Shows that under (A) the realised turnover exceeds the optimiser's apparent turnover by ~20%; under (B) the two agree. | |
| 96 | + |
| 97 | +--- |
| 98 | + |
| 99 | +## `covar_estimation/` — estimator demos |
| 100 | + |
| 101 | +Focused on the covariance side of the workflow; no portfolio optimisation involved. |
| 102 | +Useful as inputs / diagnostics for the backtest examples above. |
| 103 | + |
| 104 | +| File | What it shows | |
| 105 | +|---|---| |
| 106 | +| `simulate_factor_returns.py` | Simulates a factor model (`Y = X β + ε`) with controllable correlation and noise structure. Used as ground truth for the LASSO estimator below. | |
| 107 | +| `lasso_covar_estimation.py` | Fits the LASSO / Group LASSO factor model on simulated and real data; compares to a vanilla EWMA covariance. Demonstrates `FactorCovarEstimator`. | |
| 108 | +| `demo_covar_different_estimation_freqs.py` | Same estimator, different return frequencies (D / W-WED / ME). Shows annualisation factor and sample-size trade-off. | |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## Recommended reading order for newcomers |
| 113 | + |
| 114 | +1. `data/universe.py` — understand the test fixture everything builds on. |
| 115 | +2. `backtests/minimal_backtest.py` — see one full workflow end-to-end. |
| 116 | +3. `solvers/min_variance.py` — minimal solver demo with both single-date and rolling forms. |
| 117 | +4. `solvers/tracking_error.py` — the production TAA pattern (alpha + benchmark + TE constraint). |
| 118 | +5. `comparisons/optimisers.py` — see how objectives differ on the same universe. |
| 119 | +6. `covar_estimation/lasso_covar_estimation.py` — when EWMA isn't enough, this is the next step. |
| 120 | + |
| 121 | +--- |
| 122 | + |
| 123 | +## Conventions used across the demos |
| 124 | + |
| 125 | +- **Universe loading.** Demos share two helpers in `data/universe.py`: `fetch_benchmark_universe_data` (15-ETF universe with asset-class loadings and benchmark weights, used by `solvers/`, `backtests/balanced_risk_budgets`, and most of `comparisons/`) and `fetch_minimal_universe_data` (compact 8-ETF universe with a 3-tuple return, used by `backtests/minimal_backtest` and `solvers/long_short`). `solvers/target_return.py` defines its own loader inline because it needs extra columns (dividends, yields, target returns) the shared fixtures don't provide. |
| 126 | +- **Time period.** Most demos use `qis.TimePeriod('31Jan2007', '17Apr2025')`. Adjust as needed; covariance estimators warm up over the early part of this window. |
| 127 | +- **Rebalancing.** Default is `'QE'` for SAA-style examples; faster cadences are tuned per file when relevant. |
| 128 | +- **Transaction costs.** Where simulated, `rebalancing_costs=0.0003` (3 bp). Adjust to match the realism of your asset class. |
| 129 | +- **Output.** Factsheet PDFs go to `optimalportfolios.local_path.get_output_path()`. Console diagnostics print summary statistics directly. |
| 130 | +- **Solver config.** Most demos rely on `OptimiserConfig()` defaults. Override `solver`, `verbose`, or `use_drifted_weights_0` (production default `True`) as needed. |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## Migration note |
| 135 | + |
| 136 | +This layout reorganises the previous flat structure. If you have notebooks or |
| 137 | +scripts referencing the old paths, update as follows: |
| 138 | + |
| 139 | +| Old path | New path | |
| 140 | +|---|---| |
| 141 | +| `examples.universe` | `examples.data.universe` | |
| 142 | +| `examples.optimal_portfolio_backtest` | `examples.backtests.minimal_backtest` | |
| 143 | +| `examples.solve_risk_budgets_balanced_portfolio` | `examples.backtests.balanced_risk_budgets` | |
| 144 | +| `examples.computation_of_tracking_error` | `examples.backtests.tracking_error_decomposition` | |
| 145 | +| `examples.multi_optimisers_backtest` | `examples.comparisons.optimisers` | |
| 146 | +| `examples.multi_covar_estimation_backtest` | `examples.comparisons.covar_estimators` | |
| 147 | +| `examples.parameter_sensitivity_backtest` | `examples.comparisons.parameter_sensitivity` | |
| 148 | +| `examples.risk_budgeting_pyrb_vs_scipy` | `examples.comparisons.pyrb_vs_scipy` | |
| 149 | +| `examples.sp500_minvar` | `examples.comparisons.sp500_minvar_spans` | |
| 150 | +| `examples.long_short_optimisation` | `examples.solvers.long_short` | |
| 151 | +| `examples.sp500_universe` | unchanged (deliberately kept at top level) | |
0 commit comments