A compact, reproducible research pipeline that converts dated return forecasts into long-only allocations and backtests them against daily prices. The project is designed for method review: inputs are explicit, execution timing is conservative by default, and synthetic data can exercise the entire workflow without proprietary datasets.
- Aligns forecast dates to the next available trading day.
- Estimates covariance from prices strictly before each rebalance.
- Solves a fully invested, long-only maximum-Sharpe allocation, with a minimum-variance fallback.
- Expands rebalance weights over the trading calendar.
- Runs a T+1 backtest with optional turnover-based transaction costs.
- Exports daily P&L, summary metrics, and an equity chart.
Requires Python 3.10 or newer.
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python examples/generate_synthetic_data.py
python portfolio_optimizer.py \
--selections examples/synthetic_data/selections.csv \
--prices examples/synthetic_data/prices.csv \
--output results/portfolio_weights.csv \
--lookback 252
python backtest.py \
--prices examples/synthetic_data/prices.csv \
--weights results/portfolio_weights.csv \
--transaction-cost-bps 5 \
--output-dir resultsThe final command writes:
backtest_pnl.csv: gross return, turnover, cost, net return, and NAV by daybacktest_metrics.csv: CAGR, annualized volatility, Sharpe ratio, cumulative return, and maximum drawdownbacktest_equity.png: growth of one dollar, with optional SPX/QQQ overlays
Forecast selections:
| Column | Meaning |
|---|---|
trade_date |
Forecast/rebalance date |
tic |
Security identifier |
predicted_return |
Expected return used by the optimizer |
Daily prices use date (or datadate), tic, and one of adj_close, adjcp, close, prccd, or price. If both prccd and ajexdi are present, the backtester uses prccd / ajexdi.
Weights use trade_date, tic, and weights. Each date is normalized to one, and negative or zero-total portfolios are rejected.
- Covariance estimation excludes the rebalance day's close.
- T+1 execution is the default, reducing same-day look-ahead risk.
--same-dayis available only for an intentional comparison. - Missing held-security prices and missing selected tickers raise errors instead of being converted into zero returns.
- Duplicate date/ticker rows follow a documented last-observation-wins rule.
--transaction-cost-bpscharges half-turnover at a linear per-side rate; initial deployment is not charged.- Annual return is a geometric CAGR rather than arithmetic daily mean multiplied by 252.
This is an offline research framework, not a trading system or investment recommendation. Results remain sensitive to forecast construction, survivorship bias, delistings, corporate-action quality, stale prices, liquidity, market impact, taxes, benchmark definitions, and the simplified transaction-cost model. The optimizer can also concentrate heavily when expected returns or covariance estimates are unstable. Validate all source data and assumptions before interpreting results.
No market data or trained model artifacts are included. The example generator creates fictional tickers and deterministic synthetic prices.
python -m unittest discover -s tests -vThe suite covers duplicate observations, covariance dimensions, long-only constraints, date alignment, weight normalization, T+1 execution, missing-price failures, transaction costs, and geometric performance metrics. GitHub Actions runs it on Python 3.10 and 3.13.
This repository is a cleaned, standalone reconstruction of the author's portfolio-optimization and backtesting work from a graduate financial data science project. See NOTICE for the publication boundary.
Code is released under the MIT License. Data providers may impose separate terms on any datasets you supply.