Skip to content

Commit 6887174

Browse files
Pigbibicodex
andcommitted
fix: derive crypto drift baseline from walk-forward returns
Co-Authored-By: Codex <noreply@openai.com>
1 parent b3e81a7 commit 6887174

6 files changed

Lines changed: 69 additions & 39 deletions

File tree

.github/workflows/drift-check.yml

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ jobs:
1616
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
1717
runs-on: ubuntu-latest
1818
timeout-minutes: 30
19+
outputs:
20+
snapshot_repository_ref: ${{ steps.snapshot-input.outputs.snapshot_repository_ref }}
1921
steps:
2022
- name: Checkout
2123
uses: actions/checkout@v6
@@ -24,7 +26,7 @@ jobs:
2426
uses: actions/checkout@v6
2527
with:
2628
repository: QuantStrategyLab/QuantPlatformKit
27-
ref: 9bb8f31e898ea238a6446472f9f5e58133128d0c
29+
ref: bda6afdab0a2dd693c35d14493176829f4da1231
2830
path: external/QuantPlatformKit
2931

3032
- name: Set up Python
@@ -40,6 +42,7 @@ jobs:
4042
python -m pip install --no-deps -e external/QuantPlatformKit
4143
4244
- name: Download latest trusted lifecycle inputs
45+
id: snapshot-input
4346
env:
4447
GH_TOKEN: ${{ secrets.SNAPSHOT_REPOSITORY_TOKEN }}
4548
INPUT_ROOT: ${{ runner.temp }}/crypto-lifecycle-inputs
@@ -62,17 +65,24 @@ jobs:
6265
pages = json.loads((Path(os.environ["RUNNER_TEMP"]) / "snapshot-artifacts.json").read_text())
6366
run_pages = json.loads((Path(os.environ["RUNNER_TEMP"]) / "trusted-snapshot-runs.json").read_text())
6467
artifacts = [item for page in pages for item in page.get("artifacts", [])]
65-
trusted_run_ids = {run["id"] for page in run_pages for run in page.get("workflow_runs", [])}
66-
candidates = [
67-
item for item in artifacts
68-
if not item.get("expired")
69-
and str(item.get("name", "")).startswith("crypto-lifecycle-inputs-")
70-
and item.get("workflow_run", {}).get("head_branch") == "main"
71-
and item.get("workflow_run", {}).get("id") in trusted_run_ids
72-
]
73-
if not candidates:
68+
trusted_runs = [run for page in run_pages for run in page.get("workflow_runs", [])]
69+
selected = None
70+
for run in sorted(
71+
trusted_runs,
72+
key=lambda item: (item.get("run_number", 0), item.get("run_attempt", 0)),
73+
reverse=True,
74+
):
75+
matches = [
76+
item for item in artifacts
77+
if not item.get("expired")
78+
and str(item.get("name", "")).startswith("crypto-lifecycle-inputs-")
79+
and item.get("workflow_run", {}).get("id") == run["id"]
80+
]
81+
if matches:
82+
selected = max(matches, key=lambda item: item["created_at"])
83+
break
84+
if selected is None:
7485
raise SystemExit("no trusted crypto lifecycle input artifact is available")
75-
selected = max(candidates, key=lambda item: item["created_at"])
7686
print(selected["id"], selected["workflow_run"]["id"])
7787
PY
7888
read -r artifact_id workflow_run_id < "${RUNNER_TEMP}/snapshot-artifact-selection.txt"
@@ -94,6 +104,19 @@ jobs:
94104
if mismatches:
95105
raise SystemExit(f"lifecycle input provenance check failed: {mismatches}")
96106
PY
107+
snapshot_repository_ref="$(python - <<'PY'
108+
import json
109+
import os
110+
from pathlib import Path
111+
run = json.loads((Path(os.environ["RUNNER_TEMP"]) / "snapshot-workflow-run.json").read_text())
112+
print(run["head_sha"])
113+
PY
114+
)"
115+
if [[ ! "${snapshot_repository_ref}" =~ ^[0-9a-f]{40}$ ]]; then
116+
echo "::error::Invalid snapshot producer head SHA"
117+
exit 1
118+
fi
119+
echo "snapshot_repository_ref=${snapshot_repository_ref}" >> "${GITHUB_OUTPUT}"
97120
gh api "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/artifacts/${artifact_id}/zip" \
98121
> "${RUNNER_TEMP}/snapshot-artifact.zip"
99122
python - <<'PY'
@@ -176,15 +199,16 @@ jobs:
176199
contents: read
177200
issues: write
178201
id-token: write
179-
uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@9bb8f31e898ea238a6446472f9f5e58133128d0c
202+
uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@bda6afdab0a2dd693c35d14493176829f4da1231
180203
with:
181204
strategy_domain: crypto
182205
caller_event_name: ${{ github.event_name }}
183206
caller_pr_head_repository: ${{ github.event.pull_request.head.repo.full_name || '' }}
184207
snapshot_repository: QuantStrategyLab/CryptoLivePoolPipelines
185208
snapshot_checkout_path: external/CryptoLivePoolPipelines
209+
snapshot_repository_ref: ${{ needs.preflight_backtests.outputs.snapshot_repository_ref }}
186210
ai_gateway_service_url: ${{ vars.AI_GATEWAY_SERVICE_URL }}
187-
quant_platform_kit_ref: 9bb8f31e898ea238a6446472f9f5e58133128d0c
211+
quant_platform_kit_ref: bda6afdab0a2dd693c35d14493176829f4da1231
188212
lifecycle_preflight_artifact: lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }}
189213
secrets:
190214
codex_audit_service_url: ${{ secrets.CODEX_AUDIT_SERVICE_URL }}

scripts/run_walk_forward_backtest.py

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -233,34 +233,17 @@ def run_walk_forward(
233233
panel=panel,
234234
market_history=market_history,
235235
)
236-
return_matrix_runner = _build_runner(
237-
profile=profile,
238-
panel=shared_panel,
239-
market_history=shared_market_history,
240-
synthetic_days=synthetic_days,
241-
)
242-
full_start = min(start for start, _ in windows)
243-
baseline_end = max(end for _, end in windows)
244-
full_window_raw = return_matrix_runner.run(
245-
profile,
246-
copy.deepcopy(baseline_params),
247-
start_date=full_start,
248-
end_date=baseline_end,
249-
)
250-
full_window_returns = return_matrix_runner.last_daily_returns
251-
if len(full_window_returns) < DRIFT_BASELINE_HORIZON_DAYS:
252-
raise ValueError("full-window returns do not cover the 126-day drift baseline")
253-
baseline_raw = _baseline_from_return_tail(full_window_raw, full_window_returns)
254236
with tempfile.TemporaryDirectory(prefix=f"{profile}_wf_", dir=target_root) as scratch_dir:
255237
scratch_orchestrator = BacktestOrchestrator(store=PerformanceStore(local_root=Path(scratch_dir)))
238+
walk_forward_runner = _build_runner(
239+
profile=profile,
240+
panel=shared_panel,
241+
market_history=shared_market_history,
242+
synthetic_days=synthetic_days,
243+
)
256244
scratch_orchestrator.register_runner(
257245
"crypto",
258-
_build_runner(
259-
profile=profile,
260-
panel=shared_panel,
261-
market_history=shared_market_history,
262-
synthetic_days=synthetic_days,
263-
),
246+
walk_forward_runner,
264247
)
265248
wf_results = scratch_orchestrator.walk_forward(
266249
profile,
@@ -269,6 +252,10 @@ def run_walk_forward(
269252
windows=windows,
270253
param_set_id=f"{profile}_wf",
271254
)
255+
full_window_returns = pd.concat(walk_forward_runner.run_return_history).sort_index()
256+
if len(full_window_returns) < DRIFT_BASELINE_HORIZON_DAYS:
257+
raise ValueError("walk-forward returns do not cover the 126-day drift baseline")
258+
baseline_raw = _baseline_from_return_tail(wf_results[-1], full_window_returns)
272259
orchestrator = BacktestOrchestrator(store=PerformanceStore(local_root=target_root))
273260
baseline = orchestrator.persist_result(
274261
baseline_raw,

src/crypto_strategies/backtest/orchestrator_runner.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,16 @@ def __init__(self, *, panel: pd.DataFrame | None = None, synthetic_days: int = 1
145145
self._panel = panel
146146
self._synthetic_days = int(synthetic_days)
147147
self._last_daily_returns = pd.Series(dtype=float)
148+
self._run_return_history: list[pd.Series] = []
148149

149150
@property
150151
def last_daily_returns(self) -> pd.Series:
151152
return self._last_daily_returns.copy()
152153

154+
@property
155+
def run_return_history(self) -> tuple[pd.Series, ...]:
156+
return tuple(item.copy() for item in self._run_return_history)
157+
153158
def run(
154159
self,
155160
strategy_profile: str,
@@ -186,6 +191,7 @@ def run(
186191
start_date=start_date,
187192
end_date=end_date,
188193
)
194+
self._run_return_history.append(self._last_daily_returns.copy())
189195
elapsed = (datetime.now(timezone.utc) - started).total_seconds()
190196
eval_dates = sliced.index.get_level_values("date")
191197
return _metrics_to_result(
@@ -210,11 +216,16 @@ def __init__(
210216
self._market_history = market_history
211217
self._synthetic_days = int(synthetic_days)
212218
self._last_daily_returns = pd.Series(dtype=float)
219+
self._run_return_history: list[pd.Series] = []
213220

214221
@property
215222
def last_daily_returns(self) -> pd.Series:
216223
return self._last_daily_returns.copy()
217224

225+
@property
226+
def run_return_history(self) -> tuple[pd.Series, ...]:
227+
return tuple(item.copy() for item in self._run_return_history)
228+
218229
def run(
219230
self,
220231
strategy_profile: str,
@@ -260,6 +271,7 @@ def run(
260271
start_date=start_date,
261272
end_date=end_date,
262273
)
274+
self._run_return_history.append(self._last_daily_returns.copy())
263275
elapsed = (datetime.now(timezone.utc) - started).total_seconds()
264276
eval_frame = sliced
265277
if start_date is not None:

tests/test_drift_workflow_config.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ def test_drift_workflow_wires_real_pipeline_inputs_and_preflight_bundle() -> Non
66

77
assert "preflight_backtests:" in workflow
88
assert "needs: preflight_backtests" in workflow
9+
assert "snapshot_repository_ref: ${{ steps.snapshot-input.outputs.snapshot_repository_ref }}" in workflow
10+
assert "id: snapshot-input" in workflow
11+
assert 'print(run["head_sha"])' in workflow
12+
assert "snapshot_repository_ref: ${{ needs.preflight_backtests.outputs.snapshot_repository_ref }}" in workflow
913
assert "Download latest trusted lifecycle inputs" in workflow
1014
assert "gh api --paginate --slurp" in workflow
1115
assert "trusted-snapshot-runs.json" in workflow
@@ -15,7 +19,7 @@ def test_drift_workflow_wires_real_pipeline_inputs_and_preflight_bundle() -> Non
1519
assert "research_panel.csv.gz" in workflow
1620
assert "market_history.csv.gz" in workflow
1721
assert "repository: QuantStrategyLab/QuantPlatformKit" in workflow
18-
assert "ref: 9bb8f31e898ea238a6446472f9f5e58133128d0c" in workflow
22+
assert "ref: bda6afdab0a2dd693c35d14493176829f4da1231" in workflow
1923
assert "python -m pip install --no-deps -e external/QuantPlatformKit" in workflow
2024
assert "scripts/run_walk_forward_backtest.py" in workflow
2125
assert '"--list-profiles"' in workflow
@@ -25,7 +29,7 @@ def test_drift_workflow_wires_real_pipeline_inputs_and_preflight_bundle() -> Non
2529
assert "Upload lifecycle preflight artifact" in workflow
2630
assert "lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }}" in workflow
2731
assert workflow.count("github.ref == format('refs/heads/{0}', github.event.repository.default_branch)") == 2
28-
assert "uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@9bb8f31e898ea238a6446472f9f5e58133128d0c" in workflow
32+
assert "uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@bda6afdab0a2dd693c35d14493176829f4da1231" in workflow
2933
assert "strategy_domain: crypto" in workflow
3034
assert "snapshot_repository: QuantStrategyLab/CryptoLivePoolPipelines" in workflow
3135
assert "snapshot_checkout_path: external/CryptoLivePoolPipelines" in workflow

tests/test_orchestrator_runner.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def test_run_returns_backtest_result(self) -> None:
4343
self.assertGreaterEqual(runner.last_daily_returns.index.min().date(), date(2023, 6, 1))
4444
self.assertLessEqual(runner.last_daily_returns.index.max().date(), date(2024, 6, 1))
4545
self.assertEqual(result.observation_count, len(runner.last_daily_returns))
46+
self.assertEqual(len(runner.run_return_history), 1)
4647

4748
def test_walk_forward_produces_one_result_per_window(self) -> None:
4849
from pathlib import Path
@@ -82,6 +83,7 @@ def test_run_returns_backtest_result(self) -> None:
8283
self.assertGreaterEqual(runner.last_daily_returns.index.min().date(), date(2023, 6, 1))
8384
self.assertLessEqual(runner.last_daily_returns.index.max().date(), date(2024, 6, 1))
8485
self.assertEqual(result.observation_count, len(runner.last_daily_returns))
86+
self.assertEqual(len(runner.run_return_history), 1)
8587

8688
def test_invalid_combo_mode_raises(self) -> None:
8789
runner = CryptoEquityComboBacktestRunner(synthetic_days=1600)

tests/test_run_walk_forward_backtest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def test_run_walk_forward_uses_real_panel_and_writes_return_matrix(
131131
assert payload["baseline"]["observation_count"] == 126
132132
assert {"as_of", "crypto_live_pool_rotation", "buy_hold_BTC"} <= set(return_matrix.columns)
133133
assert len(return_matrix) > payload["baseline"]["observation_count"]
134+
assert len(return_matrix) == sum(item["observation_count"] for item in payload["walk_forward_folds"])
134135

135136

136137
def test_baseline_uses_exact_tail_of_full_return_stream() -> None:

0 commit comments

Comments
 (0)