This repository is a leakage-aware research platform for equity selection, portfolio construction, financial NLP, pair-trading research, and realistic backtesting. The project is not presented as a production alpha product. Its strongest current value is an auditable ML stock-selection pipeline with explicit controls, failed-gate reporting, and documented limits.
Status date: 2026-07-17
The current best development result is the repaired weekly ML winner-selection experiment:
| Portfolio, 2020-04-24 to 2023-06-30 | Net CAGR | Sharpe | Max drawdown |
|---|---|---|---|
| Full weekly rank-40 ML portfolio | 37.22% | 0.99 | -22.77% |
| Smart-universe plus weekly rank-40 | 27.15% | 0.87 | -27.31% |
| SPY | 13.84% | 0.83 | -29.35% |
The full weekly rank-40 result supports the project direction: use historical price and feature data to train an ML ranking model that predicts future weekly relative winners, then hold the top-ranked names. However, this is still development evidence. The model edge is positive but thin at the raw prediction layer and must be confirmed on a frozen future period before any market-beating claim.
Current ML rank diagnostics from the repaired training run:
| Diagnostic | Value |
|---|---|
| Overall rank IC | 0.0059 |
| Positive IC rate | 51.10% |
| Top-40 weekly forward return | 0.2716% |
| Universe weekly forward return | 0.1030% |
| Top-40 excess weekly return | 0.1686% |
| Top-minus-bottom weekly return | 0.1915% |
| Net information ratio vs equal weight | 0.7266 |
| Shuffled-score permutation p-value | 0.0099 |
The right claim is therefore:
The ML stock-selection idea shows promising development evidence, especially in the repaired full weekly rank-40 portfolio, but the result is not yet a confirmed production alpha because the evidence is uneven by year and still needs a frozen confirmation period.
flowchart LR
SPY["SPY<br/>13.84% CAGR"]
SMART["Smart + Rank40<br/>27.15% CAGR"]
FULL["Full Rank40<br/>37.22% CAGR"]
SPY --> SMART --> FULL
flowchart LR
Y2020["2020<br/>+0.5215% weekly excess"]
Y2021["2021<br/>-0.0375%"]
Y2022["2022<br/>-0.0411%"]
Y2023["2023 H1<br/>+0.2941%"]
Y2020 --> Y2021 --> Y2022 --> Y2023
The portfolio result is strong, but the signal chart explains why the documentation stays careful: the model edge is not equally stable across years.
flowchart LR
A["Repaired price panel<br/>coverage gate passed"] --> B["Weekly ML winner selection<br/>positive development result"]
B --> C["Rank diagnostics<br/>IC 0.0059, p=0.0099"]
C --> D{"Ready to claim alpha?"}
D -->|"No"| E["Needs frozen confirmation<br/>and stronger stability evidence"]
B --> F["Smart-universe filter"]
F -->|"underperformed full rank-40"| G["Keep archived, not active"]
B --> H["Survival exit prediction"]
H -->|"in-sample works, holdout weak"| I["Research-only for now"]
flowchart TD
A["Market, membership, macro, factor, and text data"] --> B["Incremental audited caches"]
B --> C["Point-in-time eligibility and lagged liquidity"]
C --> D["Feature store"]
D --> E["Future residual-return rank target"]
E --> F["Nested purged ML ranking"]
F --> G["Weekly top-40 stock selection"]
G --> H["Portfolio construction and costs"]
H --> I["SPY, equal-weight, zero-score, shuffled-score controls"]
I --> J["Experiment logs, metrics, and gate decisions"]
D --> K["Slow sentiment and topic features"]
K -. "research adapter; not final authority" .-> F
B --> L["Pair-trading research"]
L -. "disabled until gates pass" .-> H
The main branch should keep the finalized, reusable research system:
- incremental price and text caching;
- point-in-time membership and lagged liquidity filters;
- repaired historical price-panel handling;
- residual-return rank target construction;
- nested purged Ridge/XGBoost rank modeling;
- weekly top-k stock selection and cost-aware backtesting;
- rank-model evaluation matrix;
- benchmark, placebo, bootstrap, and permutation diagnostics;
- final documentation and reproducibility contracts.
Detailed experiment history should be pushed to the archive-experiments branch:
- failed or superseded experiment manifests;
- smart-universe v7/v8 development runs;
- Fama-French target replacement study;
- topic modeling and sentiment ablations;
- timestamped-news event study;
- pair-trading activation experiments;
- public confirmation and lockbox experiments;
- generated report summaries and dated experiment logs.
Those experiments are valuable evidence, but they should not make the main branch look like a
collection of active production strategies.
External research on Microsoft Qlib supports the same broad winner-selection pattern:
model score -> cross-sectional rank -> top-K portfolio -> signal and portfolio reports
The most useful improvement to borrow is TopK-Drop replacement. Instead of fully refreshing the
top-40 book each week, the next design can hold K=40 and replace only Drop=3-5 names per
rebalance.
flowchart LR
A["Current v8<br/>rank all names weekly"] --> B["Select top 40"]
B --> C["High performance<br/>but stability still unproven"]
C --> D["v9 proposal<br/>TopK=40, Drop=3-5"]
D --> E["Lower turnover<br/>cleaner confirmation test"]
D --> F["Qlib-style reports<br/>IC, ICIR, quantile returns, costs"]
See Qlib and ML winner-selection research notes.
The controlled U.S. replication is now implemented with the exact 158-feature contract, a Qlib-style LightGBM regressor, raw and sector-neutral residual targets, identical temporal folds, and a fixed 40-stock portfolio. Build the cached feature panel and run all four arms with:
pip install -e ".[tuning]"
python -m src.cli build-qlib-alpha158-dataset
python -m src.cli run-qlib-alpha158-comparison
python -m src.cli run-alpha158-feature-ablation
python -m src.cli run-double-ensemble-v10 \
--output reports/experiments/double_ensemble_v10_consensusThe Ridge-controlled feature ablation found no verified incremental Alpha158 benefit: adding Alpha158 raised mean Rank IC from 0.0102 to 0.0114, but the paired confidence interval included zero and top-40 excess return fell from 0.1006% to 0.0646% per week. See the Alpha158 U.S. experiment record.
The model-specific v10 study gave Ridge, LightGBM, and XGBoost independent Optuna searches and fold-safe shuffle-ablation selection. XGBoost achieved the strongest top-40 point estimate (0.2121% weekly excess) but its paired confidence interval included zero. LightGBM and the validation-weighted ensemble failed out of sample, so the ensemble remains disabled. See the v10 experiment record.
| Module | Role |
|---|---|
src/data |
Market data, macro data, point-in-time universe, price repair, and caching |
src/features |
Technical, Alpha158, cross-sectional, macro, and NLP feature generation |
src/targets |
Weekly residual-return ranking targets |
src/models |
Ridge, XGBoost ranking, and LightGBM cross-sectional models |
src/portfolio |
Sparse ranking, active tilts, risk models, and costs |
src/backtest |
Walk-forward execution, accounting, and metrics |
src/nlp |
FinBERT, topic, event, and text-source pipelines |
src/statarb |
Pair-trading research infrastructure |
src/experiments |
Experiment runners, controls, and evidence gates |
Train the repaired weekly rank model and emit the rank evaluation matrix:
python -m src.cli train-residual-rank \
--v2-base-config configs/experiments/residual_rank_v2.yaml \
--experiment-config configs/experiments/sparse_rank_v3_fnspid_2018_2023.yaml \
--dataset data/processed/residual_rank_v2_repaired/features.parquet \
--output reports/experiments/sparse_rank_v3_repaired_2018_2023Run the current smart-universe comparison experiment:
python -m src.cli run-slow-smart-universe-v8Evaluate an existing out-of-sample rank-score file:
python -m src.cli evaluate-rank-model \
--oos-file reports/experiments/sparse_rank_v3_repaired_2018_2023/oos_rank_scores.parquet \
--output reports/experiments/sparse_rank_v3_repaired_2018_2023/rank_model_diagnostics \
--top-k 40Install the optional tuning dependency, then create a frozen live-forward paper snapshot from the latest yfinance close:
pip install -e ".[tuning]"
python -m src.cli run-live-forward-snapshot \
--as-of 2026-07-20 \
--optuna-trials 25 \
--tuning-folds 3 \
--validation-days 42 \
--purge-days 1 \
--optuna-timeout-seconds 600This rebuilds a rolling daily-proxy training panel from recent yfinance prices, builds only labels
whose future one-session returns are already known, and uses Optuna to choose between Ridge and
XGBoost rankers on purged rolling temporal validation folds. After selection, the winning model is
refit on all data available before the decision date, the active membership list is scored using
the latest close, and a frozen paper portfolio plus model_selection.csv are saved under
reports/live_forward/YYYY-MM-DD/. Do not edit a saved snapshot before measuring realized
next-session returns; otherwise the forward test stops being clean. This daily proxy is designed to
collect evidence faster than waiting ten calendar weeks for ten weekly rebalance observations.
For the recurrent after-close experiment, use the one-call daily pipeline instead of running the monitor, two model families, and dashboard separately:
python -m src.cli run-live-forward-daily --as-of 2026-07-22The live-forward operating guide documents the close-data guard, frozen-snapshot contract, dashboard timestamps, model-overlap metrics, and generated artifacts. The GitHub Pages deployment guide documents the public-data boundary, static-site publication, validation gate, and one-time repository configuration.
The daily command first refreshes the latest prior Ridge and XGBoost portfolios against the
completed session. Yahoo's final hourly bar begins at 15:30, so the command requires that bar and
retries five times at 60-second intervals if it is not available yet. It then requires an official
daily price row for the decision date, independently tunes and refits Ridge and XGBoost, saves both
new top-40 portfolios, writes selection_stability_YYYY-MM-DD.json, and rebuilds the shared
dashboard. The stability report tracks same-day model overlap, day-over-day retention, top-10
persistence, rank correlation, entrants/exits, implied replacement, and multi-day consensus names.
This ordering prevents a successful model retrain from leaving the performance chart stuck at an
earlier intraday bar.
The July 20, 2026 production-style run requested 25 Optuna trials with a 600-second timeout. Ten
trials completed, using three 42-session validation folds separated from training by a one-session
purge. The selected model was Ridge with alpha=6156.9973, aggregate validation Rank IC of
approximately 0.0121, and average top-40 raw excess return of approximately 0.067% per
validation session. Treat that as model-selection evidence only; the true forward result comes
from evaluating the frozen snapshot after the next session is realized.
Generate an hourly live-monitoring chart versus SPY for a saved snapshot:
python -m src.cli report-live-intraday \
--as-of 2026-07-17 \
--monitor-date 2026-07-20 \
--benchmark SPY \
--interval 60m \
--baseline previous-closeThis writes intraday_vs_benchmark.csv, intraday_vs_benchmark_summary.json, and a lightweight
SVG chart under the snapshot folder. The default baseline is the monitored day's previous close,
matching common one-day market charts. It is a monitoring view, not a replacement for the frozen
next-session evaluation.
Evaluate a snapshot at stock and portfolio level for a named horizon:
python -m src.cli evaluate-live-forward \
--as-of 2026-07-17 \
--eval-date 2026-07-20 \
--benchmark SPY \
--interval 60m \
--baseline previous-close \
--horizon-label monday_intradayBuild the static monitoring dashboard:
python -m src.cli build-live-dashboardThe dashboard reads every saved snapshot under reports/live_forward/, overlays Ridge, XGBoost,
and SPY on one multi-session chart, provides date-range controls, identifies the next frozen
portfolios whose returns are pending, and shows human-readable model and stock-level evidence. It
is refreshed by the daily pipeline, snapshot, intraday-report, and evaluation commands.
Publish and validate the sanitized GitHub Pages bundle without retraining:
python -m src.cli publish-live-dashboard
python -m src.cli validate-live-dashboard-siteThe daily pipeline runs the publication step automatically. Only site/ is deployed; private
research evidence under reports/live_forward/, cached inputs, trained artifacts, and local paths
remain outside the public bundle.
Build the earlier conservative final-reference package:
python -m src.cli build-final-versionGenerated datasets, cached downloads, trained artifacts, and reports remain outside Git unless explicitly documented as small reproducibility records.
Use two branches for the current cleanup:
| Branch | Purpose |
|---|---|
archive-experiments |
Preserve the full experimental trail and dated evidence logs |
main |
Keep the finalized reusable pipeline, concise docs, and stable project narrative |
Recommended push order:
Follow docs/GITHUB_PUSH_PLAN.md. It separates the archive branch
from the final-facing main commit and includes the newer docs/research and equations files.
Before committing on main, inspect git status --short and avoid adding generated data under
data/ or reports/.
- The strongest rank-40 result is development evidence, not a frozen confirmation result.
- Public data does not fully solve survivorship bias; CRSP/WRDS or equivalent licensed data is still a future data-quality upgrade.
- Sector labels are proxies where official historical GICS is unavailable.
- The prediction edge is modest and uneven by year.
- 2021 and 2022 rank diagnostics are weak compared with 2020 and 2023 H1.
- Smart-universe filtering has not yet improved the full weekly rank-40 portfolio.
- Slow sentiment is promising but coverage and timestamp quality are not strong enough for a tactical claim.
- Pair trading remains research-only until confidence, random-pair, and execution gates pass.
- Static transaction-cost assumptions do not model capacity, dynamic spreads, taxes, borrow, or partial fills.
- No production deployment should happen before a frozen confirmation period passes its gates.