Skip to content

Commit 5d44b59

Browse files
Pigbibicodex
andcommitted
feat(strategy): load validated preflight panel
Co-Authored-By: Codex <noreply@openai.com>
1 parent e9c9020 commit 5d44b59

4 files changed

Lines changed: 74 additions & 7 deletions

File tree

scripts/export_lifecycle_preflight_inputs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def export_lifecycle_inputs(panel: pd.DataFrame, output_dir: Path) -> dict[str,
7474
market_history.to_csv(market_path, index=False, compression="gzip")
7575
manifest = {
7676
"contract_version": "crypto.lifecycle_preflight.v1",
77+
"strategy_profile": "crypto_live_pool_rotation",
7778
"panel_rows": int(len(lifecycle_panel)),
7879
"panel_symbols": sorted(lifecycle_panel["symbol"].unique().tolist()),
7980
"market_rows": int(len(market_history)),

src/strategy_lifecycle/backtest_wrapper.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,53 @@
33
from __future__ import annotations
44

55
from datetime import date
6+
import json
7+
import os
8+
from pathlib import Path
69
from typing import Any, Mapping
710

811
import pandas as pd
912

1013
from quant_platform_kit.strategy_lifecycle.contracts import BacktestResult
1114

12-
from .orchestrator_runner import CryptoLivePoolBacktestRunner
15+
from .orchestrator_runner import CryptoLivePoolBacktestRunner, PROFILE_NAME
1316

1417

1518
class InsufficientEvidenceError(RuntimeError):
1619
"""Raised when lifecycle wiring does not provide a real market panel."""
1720

1821

22+
PREFLIGHT_CONTRACT_VERSION = "crypto.lifecycle_preflight.v1"
23+
PREFLIGHT_ENV = "CRYPTO_LIFECYCLE_PREFLIGHT_ROOT"
24+
25+
26+
def load_preflight_panel() -> pd.DataFrame:
27+
configured = os.environ.get(PREFLIGHT_ENV) or os.environ.get("PREFLIGHT_BUNDLE_ROOT")
28+
if not configured:
29+
raise InsufficientEvidenceError(f"{PREFLIGHT_ENV} is required for no-arg lifecycle registration")
30+
root = Path(configured).expanduser().resolve()
31+
if not (root / "research_panel.csv.gz").exists():
32+
raise InsufficientEvidenceError(f"preflight bundle missing research_panel.csv.gz: {root}")
33+
try:
34+
manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
35+
panel = pd.read_csv(root / "research_panel.csv.gz", compression="gzip")
36+
except (OSError, UnicodeError, json.JSONDecodeError, ValueError) as exc:
37+
raise InsufficientEvidenceError(f"invalid lifecycle preflight bundle: {root}") from exc
38+
if manifest.get("contract_version") != PREFLIGHT_CONTRACT_VERSION or manifest.get("strategy_profile") != PROFILE_NAME:
39+
raise InsufficientEvidenceError("lifecycle preflight manifest mismatch")
40+
required = {"date", "symbol", "in_universe", "open", "final_score"}
41+
if not required.issubset(panel.columns):
42+
raise InsufficientEvidenceError("research_panel.csv.gz missing required columns")
43+
panel["date"] = pd.to_datetime(panel["date"], errors="coerce")
44+
panel["open"] = pd.to_numeric(panel["open"], errors="coerce")
45+
panel["final_score"] = pd.to_numeric(panel["final_score"], errors="coerce")
46+
if panel.empty or panel["date"].isna().any() or panel["open"].isna().any() or panel["final_score"].isna().any():
47+
raise InsufficientEvidenceError("research_panel.csv.gz contains invalid numeric/date content")
48+
if (date.today() - panel["date"].dt.normalize().max().date()).days > 3:
49+
raise InsufficientEvidenceError("research panel preflight artifact is stale")
50+
return panel.set_index(["date", "symbol"]).sort_index()
51+
52+
1953
class CryptoBacktestRunner:
2054
"""Expose the real crypto backtest engine through the lifecycle contract.
2155
@@ -25,8 +59,8 @@ class CryptoBacktestRunner:
2559
runner used by tests/research fixtures.
2660
"""
2761

28-
def __init__(self, *, panel: pd.DataFrame) -> None:
29-
self._runner = CryptoLivePoolBacktestRunner(panel=panel)
62+
def __init__(self, *, panel: pd.DataFrame | None = None) -> None:
63+
self._runner = CryptoLivePoolBacktestRunner(panel=panel if panel is not None else load_preflight_panel())
3064

3165
def run(
3266
self,
@@ -40,6 +74,4 @@ def run(
4074

4175
def build_backtest_runner(*, panel: pd.DataFrame | None = None) -> CryptoBacktestRunner:
4276
"""Build the real adapter; lifecycle wiring must inject a prepared panel."""
43-
if panel is None:
44-
raise InsufficientEvidenceError("build_backtest_runner requires a real prepared market panel")
4577
return CryptoBacktestRunner(panel=panel)

tests/test_export_lifecycle_preflight_inputs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def test_export_lifecycle_inputs_writes_real_panel_contract(self) -> None:
4040
self.assertEqual(set(market_history["symbol"]), {"BTCUSDT", "ETHUSDT"})
4141
self.assertEqual(manifest, persisted_manifest)
4242
self.assertEqual(manifest["contract_version"], "crypto.lifecycle_preflight.v1")
43+
self.assertEqual(manifest["strategy_profile"], "crypto_live_pool_rotation")
4344

4445
def test_export_rejects_scored_date_without_universe(self) -> None:
4546
panel = self._valid_panel()

tests/test_orchestrator_runner.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
import sys
44
import tempfile
55
import unittest
6+
import json
7+
import os
68
from datetime import date
79
from pathlib import Path
810

11+
import pandas as pd
12+
913
PROJECT_ROOT = Path(__file__).resolve().parents[1]
1014
if str(PROJECT_ROOT) not in sys.path:
1115
sys.path.insert(0, str(PROJECT_ROOT))
@@ -24,8 +28,37 @@ def test_production_wrapper_requires_real_panel(self) -> None:
2428
self.assertIsNotNone(build_backtest_runner)
2529
from src.strategy_lifecycle.backtest_wrapper import InsufficientEvidenceError
2630

27-
with self.assertRaises(InsufficientEvidenceError):
28-
build_backtest_runner()
31+
old = os.environ.pop("CRYPTO_LIFECYCLE_PREFLIGHT_ROOT", None)
32+
try:
33+
with self.assertRaises(InsufficientEvidenceError):
34+
build_backtest_runner()
35+
finally:
36+
if old is not None:
37+
os.environ["CRYPTO_LIFECYCLE_PREFLIGHT_ROOT"] = old
38+
39+
def test_no_arg_factory_loads_valid_preflight_bundle(self) -> None:
40+
from src.strategy_lifecycle.backtest_wrapper import build_backtest_runner
41+
42+
with tempfile.TemporaryDirectory() as tmp:
43+
root = Path(tmp)
44+
dates = pd.date_range(end=pd.Timestamp.today().normalize(), periods=3, freq="D")
45+
pd.DataFrame({
46+
"date": dates,
47+
"symbol": ["BTCUSDT"] * 3,
48+
"in_universe": [True] * 3,
49+
"open": [100.0, 101.0, 102.0],
50+
"final_score": [0.1, 0.2, 0.3],
51+
}).to_csv(root / "research_panel.csv.gz", index=False, compression="gzip")
52+
(root / "manifest.json").write_text(json.dumps({"contract_version": "crypto.lifecycle_preflight.v1", "strategy_profile": "crypto_live_pool_rotation"}), encoding="utf-8")
53+
old = os.environ.get("CRYPTO_LIFECYCLE_PREFLIGHT_ROOT")
54+
os.environ["CRYPTO_LIFECYCLE_PREFLIGHT_ROOT"] = str(root)
55+
try:
56+
self.assertIsNotNone(build_backtest_runner())
57+
finally:
58+
if old is None:
59+
os.environ.pop("CRYPTO_LIFECYCLE_PREFLIGHT_ROOT", None)
60+
else:
61+
os.environ["CRYPTO_LIFECYCLE_PREFLIGHT_ROOT"] = old
2962

3063
def test_supported_profile(self) -> None:
3164
self.assertIn(PROFILE_NAME, SUPPORTED_PROFILES)

0 commit comments

Comments
 (0)