@@ -53,7 +53,7 @@ def main(log_level: str, log_file: str | None) -> None:
5353@click .option ("--seed" , default = 42 , show_default = True , type = int )
5454def generate_demo (output : str , start : str , end : str , seed : int ) -> None :
5555 """Generate synthetic demo prices and NLP data (no API keys needed)."""
56- from .utils .demo_data import save_demo_prices , save_demo_nlp
56+ from .utils .demo_data import save_demo_nlp , save_demo_prices
5757 save_demo_prices (output , start = start , end = end , seed = seed )
5858 save_demo_nlp (output , start = start , end = end , seed = seed )
5959 click .echo (f"Demo data written to { output } " )
@@ -79,9 +79,9 @@ def build_dataset(
7979 validate_experiment_config (cfg )
8080 logger .info ("Building dataset (demo=%s)" , use_demo )
8181
82- from .data .market_data import MarketDataLoader
83- from .data .macro_data import MacroDataLoader
8482 from .data .feature_store import FeatureStore
83+ from .data .macro_data import MacroDataLoader
84+ from .data .market_data import MarketDataLoader
8585
8686 mdl = MarketDataLoader (cfg , use_demo = use_demo )
8787 prices = mdl .load ()
@@ -121,13 +121,11 @@ def build_nlp_features(
121121 if experiment_config :
122122 paths .append (experiment_config )
123123 cfg = load_config (* paths )
124- nlp_cfg = cfg .extra .get ("sources" , {})
125-
126- from .nlp .source_connectors import ConnectorRegistry
124+ from .features .nlp_features import NLPFeatureBuilder
127125 from .nlp .entity_linking import EntityLinker
128- from .nlp .sentiment_model import SentimentModel
129126 from .nlp .event_detector import EventDetector
130- from .features .nlp_features import NLPFeatureBuilder
127+ from .nlp .sentiment_model import SentimentModel
128+ from .nlp .source_connectors import ConnectorRegistry
131129
132130 registry = ConnectorRegistry (cfg , use_demo = use_demo )
133131 posts = registry .fetch_all ()
@@ -169,6 +167,7 @@ def build_nlp_features(
169167 feature_store_path = Path (cfg .paths .data_processed ) / "features.parquet"
170168 if feature_store_path .exists () and not features .empty :
171169 import pandas as pd
170+
172171 from .data .feature_store import FeatureStore
173172
174173 feature_store = pd .read_parquet (feature_store_path )
@@ -265,7 +264,11 @@ def collect_news(config: str, base_config: str) -> None:
265264@main .command ("import-fnspid" )
266265@click .option ("--config" , default = "configs/base.yaml" , show_default = True )
267266@click .option ("--nlp-config" , default = "configs/nlp.yaml" , show_default = True )
268- @click .option ("--tickers" , default = None , help = "Comma-separated symbols; defaults to configured universe" )
267+ @click .option (
268+ "--tickers" ,
269+ default = None ,
270+ help = "Comma-separated symbols; defaults to configured universe" ,
271+ )
269272@click .option ("--start" , default = None , help = "Inclusive date; defaults to configured start" )
270273@click .option ("--end" , default = None , help = "Exclusive date; defaults to configured end" )
271274@click .option (
@@ -426,8 +429,8 @@ def train_model(
426429 from .experiments .forecast_evaluation import (
427430 development_slice ,
428431 persist_forecast_evaluation ,
429- validate_feature_store_metadata ,
430432 validate_experiment_config ,
433+ validate_feature_store_metadata ,
431434 )
432435 validate_experiment_config (cfg )
433436 model_name = model or cfg .extra .get ("active_model" , "xgboost" )
@@ -440,18 +443,19 @@ def train_model(
440443 validate_feature_store_metadata (features_path , cfg )
441444
442445 import pandas as pd
443- from . models . ensemble import build_model
446+
444447 from .backtest .walk_forward import WalkForwardSplitter
448+ from .models .ensemble import build_model
445449
446- X = development_slice (pd .read_parquet (features_path ), cfg , unlock_lockbox )
450+ features = development_slice (pd .read_parquet (features_path ), cfg , unlock_lockbox )
447451 splitter = WalkForwardSplitter (cfg )
448452
449453 results = []
450- for fold , (train_idx , val_idx , test_idx ) in enumerate (splitter .split (X )):
454+ for fold , (train_idx , val_idx , test_idx ) in enumerate (splitter .split (features )):
451455 mdl = build_model (model_name , cfg )
452- train = X .iloc [train_idx ]
453- val = X .iloc [val_idx ]
454- test = X .iloc [test_idx ]
456+ train = features .iloc [train_idx ]
457+ val = features .iloc [val_idx ]
458+ test = features .iloc [test_idx ]
455459 mdl .fit (train , train ["target" ], val , val ["target" ])
456460 preds = mdl .predict (test )
457461 fold_result = test [["target" ]].copy ()
@@ -617,7 +621,9 @@ def prepare_fnspid_v2_inputs(
617621 coverage = len (covered_stocks ) / len (stock_tickers )
618622 covered_membership = relevant .loc [relevant ["ticker" ].isin (covered_stocks )].copy ()
619623 covered_membership .to_csv (out / "membership_intervals_covered.csv" , index = False )
620- pd .DataFrame ({"ticker" : missing_stocks }).to_csv (out / "missing_membership_prices.csv" , index = False )
624+ pd .DataFrame ({"ticker" : missing_stocks }).to_csv (
625+ out / "missing_membership_prices.csv" , index = False
626+ )
621627 prices .to_parquet (out / "prices_with_sector_etfs.parquet" )
622628 repair_audit .to_csv (out / "price_adjustment_repairs.csv" , index = False )
623629 sectors = infer_sector_etf_history (prices , covered_stocks )
@@ -879,6 +885,10 @@ def run_exit_prediction_v8_command(
879885 help = "Train fixed config parameters only; intended for debugging, not evidence runs." ,
880886)
881887@click .option ("--validation-days" , default = 42 , show_default = True , type = int )
888+ @click .option ("--tuning-folds" , default = 3 , show_default = True , type = int )
889+ @click .option ("--purge-days" , default = None , type = int )
890+ @click .option ("--optuna-trials" , default = 50 , show_default = True , type = int )
891+ @click .option ("--optuna-timeout-seconds" , default = None , type = int )
882892@click .option ("--selection-metric" , default = "top40_raw_excess" , show_default = True )
883893def run_live_forward_snapshot_command (
884894 base_config : str ,
@@ -895,6 +905,10 @@ def run_live_forward_snapshot_command(
895905 use_static_training_dataset : bool ,
896906 skip_hyperparameter_tuning : bool ,
897907 validation_days : int ,
908+ tuning_folds : int ,
909+ purge_days : int | None ,
910+ optuna_trials : int ,
911+ optuna_timeout_seconds : int | None ,
898912 selection_metric : str ,
899913) -> None :
900914 """Create a frozen paper-trading snapshot from latest yfinance data."""
@@ -917,6 +931,10 @@ def run_live_forward_snapshot_command(
917931 use_static_training_dataset = use_static_training_dataset ,
918932 tune_hyperparameters = not skip_hyperparameter_tuning ,
919933 validation_days = validation_days ,
934+ tuning_folds = tuning_folds ,
935+ purge_days = purge_days ,
936+ optuna_trials = optuna_trials ,
937+ optuna_timeout_seconds = optuna_timeout_seconds ,
920938 selection_metric = selection_metric ,
921939 )
922940 click .echo (json .dumps ({
@@ -934,6 +952,110 @@ def run_live_forward_snapshot_command(
934952 }, indent = 2 ))
935953
936954
955+ @main .command ("report-live-intraday" )
956+ @click .option (
957+ "--snapshot-dir" ,
958+ default = None ,
959+ help = "Live snapshot directory; defaults to reports/live_forward/<as-of>." ,
960+ )
961+ @click .option ("--as-of" , default = None , help = "Snapshot date used when snapshot-dir is omitted." )
962+ @click .option ("--benchmark" , default = "SPY" , show_default = True )
963+ @click .option ("--interval" , default = "60m" , show_default = True )
964+ @click .option ("--monitor-date" , default = None , help = "Intraday date to plot; defaults to today." )
965+ @click .option (
966+ "--baseline" ,
967+ default = "previous-close" ,
968+ show_default = True ,
969+ type = click .Choice (["previous-close" , "first-bar" ]),
970+ )
971+ def report_live_intraday_command (
972+ snapshot_dir : str | None ,
973+ as_of : str | None ,
974+ benchmark : str ,
975+ interval : str ,
976+ monitor_date : str | None ,
977+ baseline : str ,
978+ ) -> None :
979+ """Generate intraday portfolio-vs-benchmark CSV and SVG for a live snapshot."""
980+ import json
981+
982+ import pandas as pd
983+
984+ from .experiments .live_forward_test import generate_intraday_benchmark_report
985+
986+ if snapshot_dir is None :
987+ date = as_of or pd .Timestamp .today ().strftime ("%Y-%m-%d" )
988+ snapshot_dir = str (Path ("reports/live_forward" ) / date )
989+ result = generate_intraday_benchmark_report (
990+ snapshot_dir ,
991+ benchmark = benchmark ,
992+ interval = interval ,
993+ monitor_date = monitor_date ,
994+ baseline = baseline ,
995+ )
996+ click .echo (json .dumps (result , indent = 2 ))
997+
998+
999+ @main .command ("evaluate-live-forward" )
1000+ @click .option (
1001+ "--snapshot-dir" ,
1002+ default = None ,
1003+ help = "Live snapshot directory; defaults to reports/live_forward/<as-of>." ,
1004+ )
1005+ @click .option ("--as-of" , default = None , help = "Snapshot date used when snapshot-dir is omitted." )
1006+ @click .option ("--eval-date" , required = True , help = "Evaluation date." )
1007+ @click .option ("--benchmark" , default = "SPY" , show_default = True )
1008+ @click .option ("--interval" , default = "60m" , show_default = True )
1009+ @click .option (
1010+ "--baseline" ,
1011+ default = "previous-close" ,
1012+ show_default = True ,
1013+ type = click .Choice (["previous-close" , "first-bar" ]),
1014+ )
1015+ @click .option ("--horizon-label" , default = None , help = "Display/storage label for this horizon." )
1016+ def evaluate_live_forward_command (
1017+ snapshot_dir : str | None ,
1018+ as_of : str | None ,
1019+ eval_date : str ,
1020+ benchmark : str ,
1021+ interval : str ,
1022+ baseline : str ,
1023+ horizon_label : str | None ,
1024+ ) -> None :
1025+ """Evaluate a frozen live-forward snapshot versus a benchmark."""
1026+ import json
1027+
1028+ import pandas as pd
1029+
1030+ from .experiments .live_forward_test import evaluate_live_forward_snapshot
1031+
1032+ if snapshot_dir is None :
1033+ date = as_of or pd .Timestamp .today ().strftime ("%Y-%m-%d" )
1034+ snapshot_dir = str (Path ("reports/live_forward" ) / date )
1035+ result = evaluate_live_forward_snapshot (
1036+ snapshot_dir ,
1037+ eval_date = eval_date ,
1038+ benchmark = benchmark ,
1039+ interval = interval ,
1040+ baseline = baseline ,
1041+ horizon_label = horizon_label ,
1042+ )
1043+ click .echo (json .dumps (result , indent = 2 ))
1044+
1045+
1046+ @main .command ("build-live-dashboard" )
1047+ @click .option ("--live-root" , default = "reports/live_forward" , show_default = True )
1048+ @click .option ("--output-file" , default = "dashboard.html" , show_default = True )
1049+ def build_live_dashboard_command (live_root : str , output_file : str ) -> None :
1050+ """Build a static dashboard for live-forward snapshots and evaluations."""
1051+ import json
1052+
1053+ from .experiments .live_forward_test import build_live_dashboard
1054+
1055+ result = build_live_dashboard (live_root , output_file = output_file )
1056+ click .echo (json .dumps (result , indent = 2 ))
1057+
1058+
9371059@main .command ("run-sparse-sentiment-ablation" )
9381060@click .option (
9391061 "--v2-base-config" , default = "configs/experiments/residual_rank_v2.yaml" ,
@@ -1193,6 +1315,7 @@ def select_pairs(
11931315 cfg = load_config (* paths )
11941316
11951317 import pandas as pd
1318+
11961319 from .data .market_data import MarketDataLoader
11971320 from .statarb .pair_selection import PairSelector
11981321
@@ -1274,7 +1397,6 @@ def run_ablation(config: str, base_config: str, output: str) -> None:
12741397 return
12751398
12761399 from .backtest .engine import BacktestEngine
1277- from .utils .config import _deep_merge
12781400
12791401 out = Path (output )
12801402 out .mkdir (parents = True , exist_ok = True )
0 commit comments