Skip to content

Commit 584e72b

Browse files
committed
Add live-forward paper trading workflow
1 parent 248d16f commit 584e72b

3 files changed

Lines changed: 618 additions & 10 deletions

File tree

README.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,21 +47,22 @@ The right claim is therefore:
4747
### Development Performance
4848

4949
```mermaid
50-
xychart-beta
51-
title "Net CAGR, repaired development window"
52-
x-axis ["SPY", "Smart + Rank40", "Full Rank40"]
53-
y-axis "Net CAGR (%)" 0 --> 40
54-
bar [13.84, 27.15, 37.22]
50+
flowchart LR
51+
SPY["SPY<br/>13.84% CAGR"]
52+
SMART["Smart + Rank40<br/>27.15% CAGR"]
53+
FULL["Full Rank40<br/>37.22% CAGR"]
54+
SPY --> SMART --> FULL
5555
```
5656

5757
### Model Signal Quality
5858

5959
```mermaid
60-
xychart-beta
61-
title "Rank signal by year: top-40 excess weekly return"
62-
x-axis ["2020", "2021", "2022", "2023 H1"]
63-
y-axis "Excess weekly return (%)" -0.10 --> 0.60
64-
bar [0.5215, -0.0375, -0.0411, 0.2941]
60+
flowchart LR
61+
Y2020["2020<br/>+0.5215% weekly excess"]
62+
Y2021["2021<br/>-0.0375%"]
63+
Y2022["2022<br/>-0.0411%"]
64+
Y2023["2023 H1<br/>+0.2941%"]
65+
Y2020 --> Y2021 --> Y2022 --> Y2023
6566
```
6667

6768
The portfolio result is strong, but the signal chart explains why the documentation stays
@@ -195,6 +196,21 @@ python -m src.cli evaluate-rank-model \
195196
--top-k 40
196197
```
197198

199+
Create a frozen live-forward paper snapshot from the latest yfinance close:
200+
201+
```bash
202+
python -m src.cli run-live-forward-snapshot --as-of 2026-07-17
203+
```
204+
205+
By default this rebuilds a rolling daily-proxy training panel from recent yfinance prices, trains
206+
candidate Ridge and XGBoost rankers on all known one-session forward residual-rank labels, selects
207+
the winner on a trailing temporal validation window, refits the selected model, scores the active
208+
membership list using the latest close, and saves a local paper portfolio plus
209+
`model_selection.csv` under `reports/live_forward/YYYY-MM-DD/`. Do not edit a saved snapshot
210+
before measuring realized next-session returns; otherwise the forward test stops being clean. This
211+
daily proxy is designed to collect evidence faster than waiting ten calendar weeks for ten weekly
212+
rebalance observations.
213+
198214
Build the earlier conservative final-reference package:
199215

200216
```bash

src/cli.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,120 @@ def run_exit_prediction_v8_command(
820820
click.echo(json.dumps(summary, indent=2))
821821

822822

823+
@main.command("run-live-forward-snapshot")
824+
@click.option(
825+
"--base-config",
826+
default="configs/base.yaml",
827+
show_default=True,
828+
type=click.Path(exists=True),
829+
)
830+
@click.option(
831+
"--data-config",
832+
default="configs/data.yaml",
833+
show_default=True,
834+
type=click.Path(exists=True),
835+
)
836+
@click.option(
837+
"--experiment-config",
838+
default="configs/experiments/sparse_rank_v3_fnspid_2018_2023.yaml",
839+
show_default=True,
840+
type=click.Path(exists=True),
841+
)
842+
@click.option(
843+
"--training-dataset",
844+
default="data/processed/residual_rank_v2_repaired/features.parquet",
845+
show_default=True,
846+
type=click.Path(exists=True),
847+
)
848+
@click.option(
849+
"--membership-file",
850+
default="data/raw/sp500_ticker_start_end_source.csv",
851+
show_default=True,
852+
type=click.Path(exists=True),
853+
)
854+
@click.option(
855+
"--sector-file",
856+
default="data/processed/residual_rank_v2_repaired/inferred_sector_history.csv",
857+
show_default=True,
858+
type=click.Path(exists=True),
859+
)
860+
@click.option("--output-root", default="reports/live_forward", show_default=True)
861+
@click.option("--as-of", default=None, help="Decision date, defaults to today.")
862+
@click.option("--holdings", default=40, show_default=True, type=int)
863+
@click.option(
864+
"--label-horizon-sessions",
865+
default=1,
866+
show_default=True,
867+
type=int,
868+
help="Forward sessions for the live proxy label; 1 collects evidence fastest.",
869+
)
870+
@click.option("--recent-training-start", default="2024-01-01", show_default=True)
871+
@click.option(
872+
"--use-static-training-dataset",
873+
is_flag=True,
874+
help="Use the old repaired weekly parquet instead of rebuilding recent daily proxy labels.",
875+
)
876+
@click.option(
877+
"--skip-hyperparameter-tuning",
878+
is_flag=True,
879+
help="Train fixed config parameters only; intended for debugging, not evidence runs.",
880+
)
881+
@click.option("--validation-days", default=42, show_default=True, type=int)
882+
@click.option("--selection-metric", default="top40_raw_excess", show_default=True)
883+
def run_live_forward_snapshot_command(
884+
base_config: str,
885+
data_config: str,
886+
experiment_config: str,
887+
training_dataset: str,
888+
membership_file: str,
889+
sector_file: str,
890+
output_root: str,
891+
as_of: str | None,
892+
holdings: int,
893+
label_horizon_sessions: int,
894+
recent_training_start: str,
895+
use_static_training_dataset: bool,
896+
skip_hyperparameter_tuning: bool,
897+
validation_days: int,
898+
selection_metric: str,
899+
) -> None:
900+
"""Create a frozen paper-trading snapshot from latest yfinance data."""
901+
import json
902+
903+
from .experiments.live_forward_test import run_live_forward_snapshot
904+
905+
cfg = load_config(base_config, data_config)
906+
result = run_live_forward_snapshot(
907+
cfg,
908+
experiment_config,
909+
training_dataset=training_dataset,
910+
membership_file=membership_file,
911+
sector_file=sector_file,
912+
output_root=output_root,
913+
as_of=as_of,
914+
holdings=holdings,
915+
label_horizon_sessions=label_horizon_sessions,
916+
recent_training_start=recent_training_start,
917+
use_static_training_dataset=use_static_training_dataset,
918+
tune_hyperparameters=not skip_hyperparameter_tuning,
919+
validation_days=validation_days,
920+
selection_metric=selection_metric,
921+
)
922+
click.echo(json.dumps({
923+
"decision_date": result["decision_date"],
924+
"latest_price_date": result["latest_price_date"],
925+
"training_mode": result["training_mode"],
926+
"training_end": result["training_end"],
927+
"label_horizon_sessions": result["label_horizon_sessions"],
928+
"model": result["model"],
929+
"model_params": result["model_params"],
930+
"model_selection": result["model_selection"],
931+
"live_scored_names": result["live_scored_names"],
932+
"holdings": result["holdings"],
933+
"output": result["output"],
934+
}, indent=2))
935+
936+
823937
@main.command("run-sparse-sentiment-ablation")
824938
@click.option(
825939
"--v2-base-config", default="configs/experiments/residual_rank_v2.yaml",

0 commit comments

Comments
 (0)