Skip to content

HanSun103/AI-Driven-Portfolio-Optimization-Backtesting-System

Repository files navigation

AI-Driven Portfolio Research & Backtesting System

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.

Current Status

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.

Visual Summary

Development Performance

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
Loading

Model Signal Quality

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
Loading

The portfolio result is strong, but the signal chart explains why the documentation stays careful: the model edge is not equally stable across years.

Evidence Decision Map

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"]
Loading

Final-Facing Architecture

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
Loading

What Is Active

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.

What Is Archived

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.

Qlib-Inspired Next Improvement

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"]
Loading

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_consensus

The 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.

Key Modules

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

Main Commands

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_2023

Run the current smart-universe comparison experiment:

python -m src.cli run-slow-smart-universe-v8

Evaluate 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 40

Install 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 600

This 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-22

The 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-close

This 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_intraday

Build the static monitoring dashboard:

python -m src.cli build-live-dashboard

The 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-site

The 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-version

Generated datasets, cached downloads, trained artifacts, and reports remain outside Git unless explicitly documented as small reproducibility records.

Branch Policy

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/.

Known Limitations

  1. The strongest rank-40 result is development evidence, not a frozen confirmation result.
  2. Public data does not fully solve survivorship bias; CRSP/WRDS or equivalent licensed data is still a future data-quality upgrade.
  3. Sector labels are proxies where official historical GICS is unavailable.
  4. The prediction edge is modest and uneven by year.
  5. 2021 and 2022 rank diagnostics are weak compared with 2020 and 2023 H1.
  6. Smart-universe filtering has not yet improved the full weekly rank-40 portfolio.
  7. Slow sentiment is promising but coverage and timestamp quality are not strong enough for a tactical claim.
  8. Pair trading remains research-only until confidence, random-pair, and execution gates pass.
  9. Static transaction-cost assumptions do not model capacity, dynamic spreads, taxes, borrow, or partial fills.
  10. No production deployment should happen before a frozen confirmation period passes its gates.

Documentation

About

A production-style research project combining ML-based return forecasting, CVaR-aware portfolio optimization, pair-trading, realistic transaction costs, and a semantic NLP event pipeline for S&P 500 equities.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages