Skip to content

Commit 4044ec7

Browse files
Pigbibicodex
andcommitted
fix(ci): align drift baseline store and workflow pin
Co-Authored-By: Codex <noreply@openai.com>
1 parent ed2ef3f commit 4044ec7

4 files changed

Lines changed: 127 additions & 69 deletions

File tree

.github/workflows/drift-check.yml

Lines changed: 11 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,65 +14,14 @@ permissions:
1414

1515
jobs:
1616
drift:
17-
runs-on: ubuntu-latest
18-
timeout-minutes: 15
19-
env:
20-
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
21-
STRATEGY_DOMAIN: crypto
22-
23-
steps:
24-
- name: Checkout
25-
uses: actions/checkout@v6
26-
27-
- name: Checkout QuantPlatformKit
28-
uses: actions/checkout@v6
29-
with:
30-
repository: QuantStrategyLab/QuantPlatformKit
31-
ref: main
32-
path: external/QuantPlatformKit
33-
34-
- name: Set up Python
35-
uses: actions/setup-python@v6
36-
with:
37-
python-version: "3.11"
38-
39-
- name: Install dependencies
40-
run: |
41-
set -euo pipefail
42-
python -m pip install --upgrade pip
43-
python -m pip install -e . pandas
44-
python -m pip install --no-deps -e external/QuantPlatformKit
45-
46-
- name: Run drift detection
47-
run: quant-lifecycle drift --domain crypto --no-alerts
48-
49-
- name: Sync drift alerts to GitHub Issues
50-
env:
51-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52-
GITHUB_REPOSITORY: ${{ github.repository }}
53-
run: python scripts/run_drift_github_issues.py
54-
55-
- name: Checkout AIAuditBridge
56-
uses: actions/checkout@v6
57-
with:
58-
repository: QuantStrategyLab/AIAuditBridge
59-
ref: main
60-
path: external/AIAuditBridge
61-
62-
- name: Dual-review critical drift
63-
env:
64-
AIAUDIT_BRIDGE_ROOT: external/AIAuditBridge
65-
CODEX_AUDIT_SERVICE_URL: ${{ secrets.CODEX_AUDIT_SERVICE_URL }}
66-
AI_GATEWAY_SERVICE_URL: ${{ vars.AI_GATEWAY_SERVICE_URL }}
67-
run: |
68-
script="external/AIAuditBridge/scripts/run_drift_dual_review.py"
69-
if [ ! -f "$script" ]; then
70-
echo "::notice::dual-review scripts unavailable; skipping until AIAuditBridge is merged"
71-
exit 0
72-
fi
73-
if [ -z "${CODEX_AUDIT_SERVICE_URL:-}" ]; then
74-
echo "::notice::CODEX_AUDIT_SERVICE_URL not configured; skipping dual-review dispatch"
75-
exit 0
76-
fi
77-
PYTHONPATH=external/AIAuditBridge python "$script" \
78-
--domain "${STRATEGY_DOMAIN}" --dispatch
17+
uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@335c7a22bc3f570bd5705427ccc40172eda6b289
18+
with:
19+
strategy_domain: crypto
20+
caller_event_name: ${{ github.event_name }}
21+
caller_pr_head_repository: ${{ github.event.pull_request.head.repo.full_name || '' }}
22+
snapshot_repository: QuantStrategyLab/CryptoLivePoolPipelines
23+
snapshot_checkout_path: external/CryptoLivePoolPipelines
24+
ai_gateway_service_url: ${{ vars.AI_GATEWAY_SERVICE_URL }}
25+
secrets:
26+
codex_audit_service_url: ${{ secrets.CODEX_AUDIT_SERVICE_URL }}
27+
snapshot_repository_token: ${{ secrets.SNAPSHOT_REPOSITORY_TOKEN }}

scripts/run_walk_forward_backtest.py

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
from __future__ import annotations
55

66
import argparse
7+
import copy
8+
import hashlib
79
import json
10+
import re
811
from datetime import date
912
from pathlib import Path
1013
from typing import Any
@@ -45,6 +48,40 @@ def _result_payload(item: Any) -> dict[str, Any]:
4548
}
4649

4750

51+
def _baseline_param_set_id(profile: str, params: dict[str, Any]) -> str:
52+
fingerprint = hashlib.sha256(json.dumps(params, sort_keys=True, default=str).encode("utf-8")).hexdigest()[:12]
53+
return f"{profile}_baseline_{fingerprint}"
54+
55+
56+
def _current_qpk_pin() -> str:
57+
text = (Path(__file__).resolve().parents[1] / "qsl.toml").read_text(encoding="utf-8")
58+
match = re.search(r"QuantPlatformKit\.git@([0-9a-f]{40})", text)
59+
return match.group(1) if match else "unknown"
60+
61+
62+
def _baseline_identity_params(
63+
params: dict[str, Any],
64+
*,
65+
synthetic_days: int,
66+
baseline_result: Any,
67+
) -> dict[str, Any]:
68+
identity = copy.deepcopy(params)
69+
identity["_baseline_start_date"] = baseline_result.start_date.isoformat() if baseline_result.start_date else None
70+
identity["_baseline_end_date"] = baseline_result.end_date.isoformat() if baseline_result.end_date else None
71+
identity["_qpk_pin"] = _current_qpk_pin()
72+
identity["_synthetic_days"] = synthetic_days
73+
return identity
74+
75+
76+
def _build_runner(*, profile: str, synthetic_days: int, panel: Any = None, market_history: Any = None):
77+
return build_backtest_runner(
78+
profile,
79+
panel=panel,
80+
market_history=market_history,
81+
synthetic_days=synthetic_days,
82+
)
83+
84+
4885
def run_walk_forward(
4986
*,
5087
profile: str,
@@ -61,21 +98,43 @@ def run_walk_forward(
6198
raise ValueError(f"unsupported profile={profile!r}; supported={sorted(SUPPORTED_PROFILES)}")
6299

63100
params = dict(PROFILE_DEFAULTS.get(profile, {"min_history_days": DEFAULT_MIN_HISTORY_DAYS}))
64-
runner = build_backtest_runner(
65-
profile,
101+
store = PerformanceStore(local_root=store_root) if store_root is not None else PerformanceStore.from_env()
102+
orchestrator = BacktestOrchestrator(store=store)
103+
104+
baseline_params = copy.deepcopy(params)
105+
runner = _build_runner(
106+
profile=profile,
66107
panel=panel,
67108
market_history=market_history,
68109
synthetic_days=synthetic_days,
69110
)
70-
store = PerformanceStore(local_root=store_root or Path("/tmp/crypto_wf_store"))
71-
orchestrator = BacktestOrchestrator(store=store)
72111
orchestrator.register_runner("crypto", runner)
73-
74-
baseline = runner.run(profile, params)
112+
baseline_probe = orchestrator.run(
113+
profile,
114+
domain="crypto",
115+
params=copy.deepcopy(baseline_params),
116+
param_set_id="__discarded__",
117+
start_date=None,
118+
end_date=None,
119+
)
120+
baseline_store_params = _baseline_identity_params(
121+
baseline_params,
122+
synthetic_days=synthetic_days,
123+
baseline_result=baseline_probe,
124+
)
125+
baseline = orchestrator.run(
126+
profile,
127+
domain="crypto",
128+
params=baseline_store_params,
129+
param_set_id=_baseline_param_set_id(profile, baseline_store_params),
130+
start_date=None,
131+
end_date=None,
132+
)
133+
wf_params = copy.deepcopy(params)
75134
wf_results = orchestrator.walk_forward(
76135
profile,
77136
domain="crypto",
78-
params=params,
137+
params=wf_params,
79138
windows=windows,
80139
param_set_id=f"{profile}_wf",
81140
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from pathlib import Path
2+
3+
4+
def test_drift_workflow_wires_pipeline_repo_and_lifecycle_env() -> None:
5+
workflow = (Path(__file__).resolve().parents[1] / ".github" / "workflows" / "drift-check.yml").read_text(encoding="utf-8")
6+
7+
assert "uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@335c7a22bc3f570bd5705427ccc40172eda6b289" in workflow
8+
assert "strategy_domain: crypto" in workflow
9+
assert "caller_event_name: ${{ github.event_name }}" in workflow
10+
assert "caller_pr_head_repository: ${{ github.event.pull_request.head.repo.full_name || '' }}" in workflow
11+
assert "snapshot_repository: QuantStrategyLab/CryptoLivePoolPipelines" in workflow
12+
assert "snapshot_checkout_path: external/CryptoLivePoolPipelines" in workflow
13+
assert "ai_gateway_service_url: ${{ vars.AI_GATEWAY_SERVICE_URL }}" in workflow
14+
assert "codex_audit_service_url: ${{ secrets.CODEX_AUDIT_SERVICE_URL }}" in workflow
15+
assert "snapshot_repository_token: ${{ secrets.SNAPSHOT_REPOSITORY_TOKEN }}" in workflow
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import sys
5+
from pathlib import Path
6+
7+
ROOT = Path(__file__).resolve().parents[1]
8+
SRC = ROOT / "src"
9+
QPK_SRC = ROOT.parent / "QuantPlatformKit" / "src"
10+
if str(QPK_SRC) not in sys.path:
11+
sys.path.insert(0, str(QPK_SRC))
12+
if str(SRC) not in sys.path:
13+
sys.path.insert(0, str(SRC))
14+
15+
from scripts.run_walk_forward_backtest import run_walk_forward
16+
17+
18+
def test_run_walk_forward_persists_lifecycle_baseline(tmp_path: Path) -> None:
19+
payload = run_walk_forward(
20+
profile="crypto_live_pool_rotation",
21+
synthetic_days=2200,
22+
store_root=tmp_path,
23+
)
24+
25+
records = [
26+
json.loads(path.read_text(encoding="utf-8"))
27+
for path in (tmp_path / "backtest" / "crypto" / "crypto_live_pool_rotation").glob("*.json")
28+
]
29+
30+
assert payload["baseline"]["sharpe_ratio"] is not None
31+
baseline_records = [record for record in records if "_baseline_" in record["param_set_id"]]
32+
assert baseline_records
33+
assert baseline_records[-1]["params"]["_qpk_pin"]
34+
assert baseline_records[-1]["params"]["_baseline_end_date"]
35+
assert any("_wf" in record["param_set_id"] for record in records)

0 commit comments

Comments
 (0)