Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions scripts/benchmark_buy_and_hold_benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env python3
"""Manual complexity check for the Full-A buy-and-hold benchmark.

Run with ``PYTHONPATH=src python scripts/benchmark_buy_and_hold_benchmark.py``.
It prints timings for the retired repeated-mask reference and the grouped
implementation; it deliberately makes no timing assertion.
"""
from __future__ import annotations

import time
from types import SimpleNamespace

import pandas as pd

from quantpilot_core.evaluation.real_data_walk_forward_smoke import _buy_and_hold_benchmark


def reference_benchmark(frame: pd.DataFrame, windows: tuple[SimpleNamespace, ...], initial_cash: float) -> float:
ordered = frame.sort_values(["date", "symbol"], kind="stable")
symbols = tuple(sorted(str(symbol) for symbol in ordered["symbol"].dropna().unique()))
valid = []
for symbol in symbols:
group = ordered.loc[ordered["symbol"] == symbol]
start_rows = group.loc[group["date"] >= windows[0].test_start]
end_rows = group.loc[group["date"] <= windows[-1].test_end]
if not start_rows.empty and not end_rows.empty:
start_price, end_price = float(start_rows.iloc[0]["close"]), float(end_rows.iloc[-1]["close"])
if start_price > 0 and end_price > 0:
valid.append((start_price, end_price))
return round(sum((initial_cash / len(valid) / start) * end for start, end in valid), 6)


def main() -> None:
symbol_count, date_count = 500, 80
dates = pd.bdate_range("2024-01-02", periods=date_count).date.astype(str)
frame = pd.DataFrame([
{"date": date, "symbol": f"{index:06d}.SZ", "close": 10.0 + index / 1000.0 + day / 100.0}
for day, date in enumerate(dates)
for index in range(symbol_count)
]).sample(frac=1.0, random_state=7).reset_index(drop=True)
windows = (SimpleNamespace(test_start=dates[20], test_end=dates[-1]),)

started = time.perf_counter(); reference = reference_benchmark(frame, windows, 1_000_000.0); reference_seconds = time.perf_counter() - started
started = time.perf_counter(); optimized = _buy_and_hold_benchmark(frame, windows, 1_000_000.0); optimized_seconds = time.perf_counter() - started

assert reference == optimized["benchmark_final_equity"]
print(f"symbols={symbol_count} rows={len(frame)}")
print(f"reference repeated full-column comparisons: {symbol_count}; seconds={reference_seconds:.6f}")
print(f"grouped benchmark full-column comparisons: 0; seconds={optimized_seconds:.6f}")


if __name__ == "__main__":
main()
72 changes: 72 additions & 0 deletions scripts/run_full_a_strategy_ml_oos_ablation_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python
"""Manual-only, offline-after-snapshot Full-A strategy/ML OOS ablation."""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

from quantpilot_core.evaluation import FullAStrategyMLOOSAblationConfig, run_full_a_strategy_ml_oos_ablation_v1
from quantpilot_core.real_data_provider import ProviderError, SnapshotDailyBarProvider


def _build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Manual-only Full-A rule/ML OOS ablation (no broker, no DeepSeek).")
parser.add_argument("--snapshot-root", type=Path, required=True)
parser.add_argument("--start-date", default="2023-01-01"); parser.add_argument("--end-date", default="2024-12-31")
parser.add_argument("--artifact-path", default="artifacts/full_a_strategy_ml_oos_ablation/latest_report.json")
parser.add_argument("--symbols", default=None); parser.add_argument("--initial-cash", type=float, default=1_000_000.0)
parser.add_argument("--fold-count", type=int, default=5); parser.add_argument("--min-fold-count", type=int, default=3)
parser.add_argument("--train-window-days", type=int, default=60)
parser.add_argument("--validation-window-days", type=int, default=20); parser.add_argument("--test-window-days", type=int, default=20)
parser.add_argument("--max-windows-per-fold", type=int, default=1); parser.add_argument("--min-symbols-required", type=int, default=20)
parser.add_argument("--target-position-count", type=int, default=10); parser.add_argument("--fixed-rule-modes", default=None)
parser.add_argument("--no-ml", action="store_true"); parser.add_argument("--no-dynamic-selector", action="store_true")
parser.add_argument("--no-progress", action="store_true", help="Suppress flushed JSON progress events on stderr.")
return parser


def _config_from_args(args: argparse.Namespace, *, provider: SnapshotDailyBarProvider, symbols: tuple[str, ...], modes: tuple[str, ...]) -> FullAStrategyMLOOSAblationConfig:
return FullAStrategyMLOOSAblationConfig(
symbols=symbols, start_date=args.start_date, end_date=args.end_date, provider=provider,
initial_cash=args.initial_cash, fold_count=args.fold_count, min_fold_count=args.min_fold_count,
train_window_days=args.train_window_days, validation_window_days=args.validation_window_days,
test_window_days=args.test_window_days, max_windows_per_fold=args.max_windows_per_fold,
min_symbols_required=args.min_symbols_required, target_position_count=args.target_position_count,
fixed_rule_modes=modes, include_ml_candidate=not args.no_ml,
include_dynamic_selector=not args.no_dynamic_selector, artifact_path=args.artifact_path,
)


def main() -> int:
parser = _build_parser()
args = parser.parse_args()
try: provider = SnapshotDailyBarProvider(args.snapshot_root)
except ProviderError as exc: parser.error(str(exc))
symbols = tuple(item.strip() for item in args.symbols.split(",") if item.strip()) if args.symbols else provider.daily_symbol_union(args.start_date, args.end_date)
modes = tuple(item.strip() for item in args.fixed_rule_modes.split(",") if item.strip()) if args.fixed_rule_modes else FullAStrategyMLOOSAblationConfig().fixed_rule_modes
def progress(event: dict[str, object]) -> None:
print(json.dumps(event, sort_keys=True), file=sys.stderr, flush=True)

try:
report = run_full_a_strategy_ml_oos_ablation_v1(
_config_from_args(args, provider=provider, symbols=symbols, modes=modes),
progress_callback=None if args.no_progress else progress,
)
except KeyboardInterrupt:
print("Full-A ablation interrupted; no completed artifact was written.", file=sys.stderr, flush=True)
return 130
print("QuantPilot Full-A strategy/ML OOS ablation v1 (manual-only; offline; no broker; no DeepSeek)")
print(f"provider: {report.provider}; snapshot digest: {report.snapshot_provenance.get('snapshot_digest')}")
print(f"folds: {len(report.common_folds)}; candidates: {len(report.per_candidate_metrics)}")
for name, row in report.per_candidate_metrics.items(): print(f"{name}: excess={row.get('strategy_excess_return')} return={row.get('total_return')} drawdown={row.get('max_drawdown')}")
if args.no_dynamic_selector:
print(f"dynamic selector: disabled; recommendation: {report.recommendation}")
else:
print(f"dynamic selections: {len(report.selection_history)}; recommendation: {report.recommendation}")
print(f"artifact: {report.artifact_path}")
return 0 if report.run_status == "completed" else 2


if __name__ == "__main__": raise SystemExit(main())
18 changes: 18 additions & 0 deletions src/quantpilot_core/evaluation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@
TurnoverAwareRebalanceOptimizationReport,
run_turnover_aware_rebalance_optimization_v1,
)
from quantpilot_core.evaluation.full_a_strategy_ml_oos_ablation import (
DEFAULT_FIXED_RULE_MODES,
DEFAULT_FULL_A_STRATEGY_ML_OOS_ABLATION_ARTIFACT_PATH,
DynamicSelectorPolicy,
FullAStrategyMLOOSAblationConfig,
FullAStrategyMLOOSAblationReport,
ProvisionalCandidateThresholds,
run_full_a_strategy_ml_oos_ablation_v1,
score_dynamic_candidates,
)
from quantpilot_core.evaluation.a_share_market_reality_execution import (
DEFAULT_A_SHARE_MARKET_REALITY_EXECUTION_REPORT_ARTIFACT_PATH,
AShareMarketRealityExecutionConfig,
Expand Down Expand Up @@ -82,6 +92,8 @@

__all__ = [
"DEFAULT_FACTOR_RANKING_BASELINE_ARTIFACT_PATH",
"DEFAULT_FIXED_RULE_MODES",
"DEFAULT_FULL_A_STRATEGY_ML_OOS_ABLATION_ARTIFACT_PATH",
"DEFAULT_A_SHARE_MARKET_REALITY_EXECUTION_REPORT_ARTIFACT_PATH",
"DEFAULT_A_SHARE_TRADABILITY_METADATA_ENRICHMENT_REPORT_ARTIFACT_PATH",
"DEFAULT_FACTOR_RANKING_SWEEP_INTEGRATION_ARTIFACT_PATH",
Expand All @@ -99,6 +111,10 @@
"FactorRankingSweepIntegrationConfig",
"FactorRankingSweepIntegrationReport",
"FactorScore",
"DynamicSelectorPolicy",
"FullAStrategyMLOOSAblationConfig",
"FullAStrategyMLOOSAblationReport",
"ProvisionalCandidateThresholds",
"AShareMarketRealityExecutionConfig",
"AShareMarketRealityExecutionReport",
"AShareTradabilityMetadataEnrichmentConfig",
Expand Down Expand Up @@ -142,6 +158,8 @@
"run_real_data_walk_forward_scaleup_v1",
"run_real_data_walk_forward_smoke",
"run_factor_ranking_baseline_v1",
"run_full_a_strategy_ml_oos_ablation_v1",
"run_ml_factor_training_v1",
"run_profitability_smoke_test",
"score_dynamic_candidates",
]
Loading
Loading