A Markowitz mean-variance optimizer paired with a three-method VaR engine, applied to five CAC 40 stocks (Airbus, BNP Paribas, L'Oréal, Sanofi, TotalEnergies). I kept the optimizer and the risk engine as two separate pieces on purpose, so I could test and reason about each one on its own instead of debugging them tangled together.
Portfolio optimization
Markowitz mean-variance optimizer for the max Sharpe and min volatility portfolios, with the efficient frontier built by solving a constrained quadratic program at each target return. Long only, fully invested, no short selling.
Risk engine
Three ways of estimating VaR on the same portfolio: historical simulation (empirical, no distributional assumption), parametric (variance-covariance, closed form under normality), and Monte Carlo (10,000 correlated simulations via Cholesky decomposition). Expected Shortfall (CVaR) comes alongside every VaR estimate, since VaR alone doesn't say how bad the tail actually is.
Data pipeline
Price fetching via yfinance, daily returns, annualization.
Visualizations
Efficient frontier with the tangency and minimum-variance portfolios marked, the portfolio return distribution with VaR thresholds overlaid, a comparison of the three VaR methods across confidence levels, and how portfolio volatility responds to shocking individual weights.
pip install -r requirements.txt
python main.pyfrom portfolio_optimization.data.market_data import load_portfolio_data
from portfolio_optimization.optimization.markowitz import MarkowitzOptimizer
tickers = ["AIR.PA", "BNP.PA", "OR.PA", "SAN.PA", "TTE.PA"]
data = load_portfolio_data(tickers, start="2021-01-01", end="2026-01-01")
optimizer = MarkowitzOptimizer(data, risk_free_rate=0.02)
max_sharpe = optimizer.max_sharpe_portfolio()
print(max_sharpe.expected_return, max_sharpe.volatility, max_sharpe.sharpe_ratio)
print(max_sharpe.as_dict(data.tickers))from portfolio_optimization.risk.engine import RiskEngine
engine = RiskEngine(data, max_sharpe.weights, n_simulations=10_000)
summary = engine.summary(confidence_levels=(0.95, 0.99))
print(summary)Each method also exposes .expected_shortfall(confidence) for the average loss beyond the VaR threshold.
pytest tests/ -vTests run on synthetic correlated returns rather than live data, so they stay fast and deterministic. They check that optimized weights sum to one, that the minimum-variance portfolio actually has lower volatility than an equal-weight benchmark, that the efficient frontier is monotonic in return, and that the three VaR methods stay within a reasonable range of each other.
portfolio_optimization/
├── base.py PortfolioData and Portfolio dataclasses
├── data/
│ └── market_data.py Price fetching and returns computation
├── optimization/
│ └── markowitz.py Mean-variance optimizer, efficient frontier
├── risk/
│ ├── historical.py Empirical VaR
│ ├── parametric.py Variance-covariance VaR
│ ├── monte_carlo.py Simulated VaR (Cholesky decomposition)
│ └── engine.py Combines all three into one summary
└── utils/
└── visualization.py Frontier, VaR distribution, sensitivity plots
Portfolio return and volatility, given weights
Max Sharpe optimization solves, subject to
Parametric VaR assumes portfolio returns are normal, so the loss at confidence level
Monte Carlo VaR draws correlated asset returns using the Cholesky factor
and then takes the empirical percentile of the resulting simulated portfolio returns, the same way the historical method does on real data.
MIT