An empirical study of whether Gaussian- and t-Copula scenario generation adds a useful tail-risk perspective to long-only CVaR portfolio optimization.
This project does not claim that t-Copula CVaR is universally superior. In the current walk-forward backtest, it improves substantially on naive equal weighting, but it does not produce lower realized CVaR than the strong Minimum Variance benchmark. The contribution is an explicit joint-tail scenario framework, together with a reproducible comparison that shows both its value and its limitations.
Project deck: GitHub edition
Traditional covariance models describe average co-movement. This project asks a narrower question:
Can a portfolio optimized on simulated joint left-tail scenarios improve realized downside risk when several asset classes sell off together?
Eight liquid ETFs are used as broad asset-class proxies: SPY, EFA, EEM, TLT, LQD, GLD, VNQ, and DBC.
The canonical code_current experiment uses 3,000 monthly scenarios, t-Copula degrees of freedom nu = 4, a 35% single-asset cap, and a 30% one-way turnover cap. The out-of-sample period is 2008-03-03 to 2025-12-31 after a 504-trading-day warm-up.
| Strategy | Ann. return | Ann. vol. | Sharpe¹ | Max drawdown | Daily CVaR 95%² | Avg. monthly turnover |
|---|---|---|---|---|---|---|
| Equal Weight | 5.77% | 12.88% | 0.45 | -39.52% | 1.96% | 1.36% |
| Minimum Variance | 6.43% | 8.13% | 0.79 | -20.31% | 1.18% | 2.50% |
| Gaussian-Copula CVaR | 7.23% | 8.26% | 0.88 | -19.53% | 1.21% | 5.66% |
| t-Copula CVaR | 7.27% | 8.33% | 0.87 | -19.81% | 1.22% | 5.32% |
¹ Sharpe ratios use a zero risk-free rate.
² CVaR is reported as a positive daily loss. Lower is better.
The t-Copula portfolio has the highest annualized return by a small margin, but Gaussian-Copula has the highest Sharpe and best maximum drawdown among the four main strategies. Minimum Variance has the lowest realized CVaR. These differences are descriptive backtest results, not evidence of statistical superiority.
The original presentation and source code described different parameters. Both historical specifications were rerun with the corrected monthly buy-and-hold mechanics:
| Configuration | Scenarios | t-Copula nu |
Single-asset cap | TLT + LQD cap | t-Copula ann. return | Max drawdown | CVaR 95% |
|---|---|---|---|---|---|---|---|
code_current |
3,000 | 4 | 35% | None | 7.27% | -19.81% | 1.22% |
ppt_legacy |
2,000 | 6 | 25% | 45% | 6.87% | -24.33% | 1.30% |
This is a multi-parameter configuration comparison, not a one-factor sensitivity test. The result cannot identify whether scenario count, nu, or allocation constraints caused the difference.
The comparison files are saved under outputs/comparison/. The original course implementation is preserved in the Git tag original-course-project.
- 504 trading days of trailing returns at each month-end.
- Adjusted prices include distributions and split adjustments.
- Daily simple returns drive the backtest; log returns are used only for diagnostics.
- Crisis observations are intentionally retained.
For each rolling window:
- Convert asset returns into rank-based pseudo-observations.
- Estimate Kendall's tau and map it to an elliptical-copula correlation matrix.
- Draw Gaussian- or t-Copula joint percentiles.
- Map percentiles through each asset's empirical marginal distribution.
- Compound 21 simulated daily returns into one monthly asset-return scenario.
The optimizer minimizes 95% CVaR of monthly portfolio loss using the Rockafellar-Uryasev formulation, with long-only weights, an L2 stabilization penalty, allocation limits, and a turnover constraint.
Turnover uses the standard one-way definition:
turnover = 0.5 × sum(abs(target weight - pre-trade weight))
The 30% limit therefore means at most 30% of portfolio value is traded at a core-strategy rebalance.
Weights are set once at month-end and then drift naturally with asset performance until the next rebalance. The next turnover calculation compares the new target against these drifted pre-trade weights. The original course implementation applied the same target weights to every daily return, which implicitly assumed free daily rebalancing.
An appendix strategy uses trailing VIX, SPY/TLT trends, and stock-bond stress signals to set Normal, Caution, or Stress regimes. It varies t-Copula tail thickness, correlation stress, and a 0%/10%/30% cash target.
This overlay is deliberately outside the main claim. Its cash rule is exogenous and therefore is not subject to the core 30% turnover cap; realized turnover is still measured and reported. Cash earns 0% in this academic implementation.
Python 3.12 is the tested environment.
pip install -r requirements.txt
# Canonical experiment: writes to outputs/
python src/main.py --experiment code_current
# Parameters recovered from the original presentation
python src/main.py --experiment ppt_legacy
# Rebuild the comparison tables
python src/compare_experiments.py
# Fast mechanics and constraint tests
pip install -r requirements-dev.txt
python -m pytest -qOn the development machine, a full experiment takes roughly 5–8 minutes. Exact runtime depends on the CVXPY solver and CPU.
Raw inputs were obtained from Yahoo Finance using yfinance, with a documented Yahoo chart-API fallback. Metadata records the requested period, actual method, row count, missing values, and package version.
python download_etf_prices.py
python download_vix.pyThe included data are for educational reproducibility. Users should review the upstream provider's terms before redistributing or using them commercially. No proprietary terminal data or API credentials are included.
Every experiment writes its exact parameters to experiment_config.json, along with solver status, target weights, pre-trade weights, realized turnover, returns, metrics, and charts.
.
├── data/ # Local ETF/VIX inputs and metadata
├── src/
│ ├── main.py # Experiment definitions and entry point
│ ├── backtest.py # Walk-forward timing and natural drift
│ ├── copula_scenarios.py # Gaussian/t-Copula scenario engine
│ ├── cvar_optimizer.py # CVaR optimization and constraints
│ ├── baselines.py # Equal-weight and MinVar benchmarks
│ ├── metrics.py # Performance and tail-risk metrics
│ └── compare_experiments.py # Original/code/PPT comparison
├── tests/ # Fast mechanics and constraint tests
├── docs/ # Interview-ready project presentation
├── outputs/ # Canonical results and selected comparisons
├── download_etf_prices.py
├── download_vix.py
├── requirements.txt
└── README.md
The following are intentionally documented rather than optimized away:
- Fixed t-Copula degrees of freedom.
nuis specified, not statistically estimated. - Finite empirical marginals. Simulated marginal losses cannot extend beyond the rolling sample's observed support; the t-Copula mainly changes cross-sectional joint-tail dependence.
- IID daily scenario draws. Volatility clustering and serial dependence inside the 21-day horizon are not modeled.
- Finite Monte Carlo sample. The canonical result uses one seed and 3,000 scenarios; no seed or scenario-count stability study is claimed.
- No transaction costs or taxes. Turnover is reported, but performance is gross of trading costs.
- Zero risk-free rate and zero cash return. Sharpe and the regime cash sleeve use simplified academic assumptions.
- No formal inference. Differences in Sharpe, drawdown, and CVaR are not accompanied by confidence intervals or multiple-testing corrections.
- Hand-designed regime overlay. Signal thresholds and cash levels may be sample-specific and are treated only as exploratory.
Natural extensions include copula calibration tests, block/bootstrap scenarios, volatility-scaled marginals, transaction-cost-aware optimization, estimated nu, and confidence intervals for performance differences.
This repository is an educational research project, not investment advice. Backtested results are hypothetical and do not represent live trading performance.
MIT License. See LICENSE.
