Skip to content

Commit ec91e85

Browse files
Pigbibicodex
andcommitted
fix: derive crypto baseline from exact return tail
Co-Authored-By: Codex <noreply@openai.com>
1 parent 7135cf5 commit ec91e85

4 files changed

Lines changed: 56 additions & 18 deletions

File tree

.github/workflows/drift-check.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,26 +49,35 @@ jobs:
4949
echo "::error::SNAPSHOT_REPOSITORY_TOKEN is required for lifecycle input artifact access"
5050
exit 1
5151
fi
52-
gh api --paginate --slurp "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/artifacts?per_page=100" > "${RUNNER_TEMP}/snapshot-artifacts.json"
52+
gh api --paginate --slurp \
53+
"/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/artifacts?per_page=100" \
54+
> "${RUNNER_TEMP}/snapshot-artifacts.json"
55+
gh api --paginate --slurp \
56+
"/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/workflows/publish-lifecycle-inputs.yml/runs?branch=main&status=success&per_page=100" \
57+
> "${RUNNER_TEMP}/trusted-snapshot-runs.json"
5358
python - <<'PY' > "${RUNNER_TEMP}/snapshot-artifact-selection.txt"
5459
import json
5560
import os
5661
from pathlib import Path
5762
pages = json.loads((Path(os.environ["RUNNER_TEMP"]) / "snapshot-artifacts.json").read_text())
63+
run_pages = json.loads((Path(os.environ["RUNNER_TEMP"]) / "trusted-snapshot-runs.json").read_text())
5864
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", [])}
5966
candidates = [
6067
item for item in artifacts
6168
if not item.get("expired")
6269
and str(item.get("name", "")).startswith("crypto-lifecycle-inputs-")
6370
and item.get("workflow_run", {}).get("head_branch") == "main"
71+
and item.get("workflow_run", {}).get("id") in trusted_run_ids
6472
]
6573
if not candidates:
6674
raise SystemExit("no trusted crypto lifecycle input artifact is available")
6775
selected = max(candidates, key=lambda item: item["created_at"])
6876
print(selected["id"], selected["workflow_run"]["id"])
6977
PY
7078
read -r artifact_id workflow_run_id < "${RUNNER_TEMP}/snapshot-artifact-selection.txt"
71-
gh api "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/runs/${workflow_run_id}" > "${RUNNER_TEMP}/snapshot-workflow-run.json"
79+
gh api "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/runs/${workflow_run_id}" \
80+
> "${RUNNER_TEMP}/snapshot-workflow-run.json"
7281
python - <<'PY'
7382
import json
7483
import os
@@ -85,7 +94,8 @@ jobs:
8594
if mismatches:
8695
raise SystemExit(f"lifecycle input provenance check failed: {mismatches}")
8796
PY
88-
gh api "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/artifacts/${artifact_id}/zip" > "${RUNNER_TEMP}/snapshot-artifact.zip"
97+
gh api "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/artifacts/${artifact_id}/zip" \
98+
> "${RUNNER_TEMP}/snapshot-artifact.zip"
8999
python - <<'PY'
90100
import os
91101
import zipfile

scripts/run_walk_forward_backtest.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import hashlib
99
import json
1010
import tempfile
11+
from dataclasses import replace
1112
from datetime import date
1213
from pathlib import Path
1314
from typing import Any
@@ -21,6 +22,7 @@
2122
SUPPORTED_PROFILES,
2223
build_backtest_runner,
2324
)
25+
from crypto_strategies.backtest.live_pool_simulator import _performance_metrics
2426
from crypto_strategies.strategies.crypto_equity_combo import PROFILE_NAME as CRYPTO_EQUITY_COMBO_PROFILE
2527

2628
DEFAULT_WINDOWS: tuple[tuple[date, date], ...] = (
@@ -177,6 +179,26 @@ def _write_return_matrix(
177179
frame.reset_index().to_csv(output_path, index=False)
178180

179181

182+
def _baseline_from_return_tail(full_result: Any, returns: pd.Series) -> Any:
183+
tail = returns.tail(DRIFT_BASELINE_HORIZON_DAYS)
184+
metrics = _performance_metrics(tail)
185+
max_drawdown = float(metrics["Max Drawdown"])
186+
cagr = float(metrics["CAGR"])
187+
return replace(
188+
full_result,
189+
sharpe_ratio=float(metrics["Sharpe"]),
190+
calmar_ratio=abs(cagr / max_drawdown) if max_drawdown else None,
191+
max_drawdown=max_drawdown,
192+
cagr=cagr,
193+
volatility=float(metrics["Annualized Volatility"]),
194+
win_rate=float(metrics["Win Rate"]),
195+
total_return=float(metrics["total_return"]),
196+
start_date=tail.index.min().date(),
197+
end_date=tail.index.max().date(),
198+
observation_count=int(metrics["Trading Days"]),
199+
)
200+
201+
180202
def run_walk_forward(
181203
*,
182204
profile: str,
@@ -214,7 +236,7 @@ def run_walk_forward(
214236
)
215237
full_start = min(start for start, _ in windows)
216238
baseline_end = max(end for _, end in windows)
217-
return_matrix_runner.run(
239+
full_window_raw = return_matrix_runner.run(
218240
profile,
219241
copy.deepcopy(baseline_params),
220242
start_date=full_start,
@@ -223,19 +245,7 @@ def run_walk_forward(
223245
full_window_returns = return_matrix_runner.last_daily_returns
224246
if len(full_window_returns) < DRIFT_BASELINE_HORIZON_DAYS:
225247
raise ValueError("full-window returns do not cover the 126-day drift baseline")
226-
baseline_start = full_window_returns.index[-DRIFT_BASELINE_HORIZON_DAYS].date()
227-
baseline_runner = _build_runner(
228-
profile=profile,
229-
panel=shared_panel,
230-
market_history=shared_market_history,
231-
synthetic_days=synthetic_days,
232-
)
233-
baseline_raw = baseline_runner.run(
234-
profile,
235-
copy.deepcopy(baseline_params),
236-
start_date=baseline_start,
237-
end_date=baseline_end,
238-
)
248+
baseline_raw = _baseline_from_return_tail(full_window_raw, full_window_returns)
239249
with tempfile.TemporaryDirectory(prefix=f"{profile}_wf_", dir=target_root) as scratch_dir:
240250
scratch_orchestrator = BacktestOrchestrator(store=PerformanceStore(local_root=Path(scratch_dir)))
241251
scratch_orchestrator.register_runner(

tests/test_drift_workflow_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ def test_drift_workflow_wires_real_pipeline_inputs_and_preflight_bundle() -> Non
88
assert "needs: preflight_backtests" in workflow
99
assert "Download latest trusted lifecycle inputs" in workflow
1010
assert "gh api --paginate --slurp" in workflow
11+
assert "trusted-snapshot-runs.json" in workflow
1112
assert "crypto-lifecycle-inputs-" in workflow
1213
assert '"path": ".github/workflows/publish-lifecycle-inputs.yml"' in workflow
1314
assert '"conclusion": "success"' in workflow

tests/test_run_walk_forward_backtest.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import scripts.run_walk_forward_backtest as walk_forward
1919
import crypto_strategies.backtest.orchestrator_runner as orchestrator_runner
20-
from scripts.run_walk_forward_backtest import _baseline_param_set_id, run_walk_forward
20+
from scripts.run_walk_forward_backtest import _baseline_from_return_tail, _baseline_param_set_id, run_walk_forward
2121

2222

2323
def test_run_walk_forward_persists_lifecycle_baseline(tmp_path: Path) -> None:
@@ -125,3 +125,20 @@ def test_run_walk_forward_uses_real_panel_and_writes_return_matrix(
125125
assert payload["baseline"]["observation_count"] == 126
126126
assert {"as_of", "crypto_live_pool_rotation", "buy_hold_BTC"} <= set(return_matrix.columns)
127127
assert len(return_matrix) > payload["baseline"]["observation_count"]
128+
129+
130+
def test_baseline_uses_exact_tail_of_full_return_stream() -> None:
131+
from quant_platform_kit.strategy_lifecycle.contracts import BacktestResult
132+
133+
index = pd.date_range("2024-01-01", periods=200, freq="D")
134+
returns = pd.Series(range(200), index=index, dtype=float) / 100000
135+
full_result = BacktestResult(
136+
strategy_profile="crypto_live_pool_rotation", domain="crypto", param_set_id="", params={}
137+
)
138+
139+
baseline = _baseline_from_return_tail(full_result, returns)
140+
141+
expected = returns.tail(126)
142+
assert baseline.start_date == expected.index.min().date()
143+
assert baseline.end_date == expected.index.max().date()
144+
assert baseline.observation_count == len(expected)

0 commit comments

Comments
 (0)