Drift Check #44
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Drift Check | |
| # Daily drift detection for crypto strategies. | |
| on: | |
| schedule: | |
| - cron: "0 6 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| issues: write | |
| jobs: | |
| preflight_backtests: | |
| if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| snapshot_repository_ref: ${{ steps.snapshot-input.outputs.snapshot_repository_ref }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Checkout QuantPlatformKit | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: QuantStrategyLab/QuantPlatformKit | |
| ref: 4f8465b28a6787d39d21e50f9d95a77841d6ad56 | |
| path: external/QuantPlatformKit | |
| - name: Set up Python | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: "3.11" | |
| - name: Install dependencies | |
| run: | | |
| set -euo pipefail | |
| python -m pip install --upgrade pip | |
| python -m pip install -e . pandas | |
| python -m pip install --no-deps -e external/QuantPlatformKit | |
| - name: Download latest trusted lifecycle inputs | |
| id: snapshot-input | |
| env: | |
| GH_TOKEN: ${{ secrets.SNAPSHOT_REPOSITORY_TOKEN || secrets.QSL_REPO_SYNC_TOKEN || github.token }} | |
| INPUT_ROOT: ${{ runner.temp }}/crypto-lifecycle-inputs | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${GH_TOKEN:-}" ]; then | |
| echo "::error::SNAPSHOT_REPOSITORY_TOKEN is required for lifecycle input artifact access" | |
| exit 1 | |
| fi | |
| gh api --paginate --slurp \ | |
| "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/artifacts?per_page=100" \ | |
| > "${RUNNER_TEMP}/snapshot-artifacts.json" | |
| gh api --paginate --slurp \ | |
| "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/workflows/publish-lifecycle-inputs.yml/runs?branch=main&status=success&per_page=100" \ | |
| > "${RUNNER_TEMP}/trusted-snapshot-runs.json" | |
| python - <<'PY' > "${RUNNER_TEMP}/snapshot-artifact-selection.txt" | |
| import json | |
| import os | |
| from pathlib import Path | |
| pages = json.loads((Path(os.environ["RUNNER_TEMP"]) / "snapshot-artifacts.json").read_text()) | |
| run_pages = json.loads((Path(os.environ["RUNNER_TEMP"]) / "trusted-snapshot-runs.json").read_text()) | |
| artifacts = [item for page in pages for item in page.get("artifacts", [])] | |
| trusted_runs = [run for page in run_pages for run in page.get("workflow_runs", [])] | |
| selected = None | |
| for run in sorted( | |
| trusted_runs, | |
| key=lambda item: (item.get("run_number", 0), item.get("run_attempt", 0)), | |
| reverse=True, | |
| ): | |
| matches = [ | |
| item for item in artifacts | |
| if not item.get("expired") | |
| and str(item.get("name", "")).startswith("crypto-lifecycle-inputs-") | |
| and item.get("workflow_run", {}).get("id") == run["id"] | |
| ] | |
| if matches: | |
| selected = max(matches, key=lambda item: item["created_at"]) | |
| break | |
| if selected is None: | |
| raise SystemExit("no trusted crypto lifecycle input artifact is available") | |
| print(selected["id"], selected["workflow_run"]["id"]) | |
| PY | |
| read -r artifact_id workflow_run_id < "${RUNNER_TEMP}/snapshot-artifact-selection.txt" | |
| gh api "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/runs/${workflow_run_id}" \ | |
| > "${RUNNER_TEMP}/snapshot-workflow-run.json" | |
| python - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| run = json.loads((Path(os.environ["RUNNER_TEMP"]) / "snapshot-workflow-run.json").read_text()) | |
| expected = { | |
| "conclusion": "success", | |
| "head_branch": "main", | |
| "path": ".github/workflows/publish-lifecycle-inputs.yml", | |
| } | |
| mismatches = {key: run.get(key) for key, value in expected.items() if run.get(key) != value} | |
| if run.get("head_repository", {}).get("full_name") != "QuantStrategyLab/CryptoLivePoolPipelines": | |
| mismatches["head_repository"] = run.get("head_repository", {}).get("full_name") | |
| if mismatches: | |
| raise SystemExit(f"lifecycle input provenance check failed: {mismatches}") | |
| PY | |
| snapshot_repository_ref="$(python - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| run = json.loads((Path(os.environ["RUNNER_TEMP"]) / "snapshot-workflow-run.json").read_text()) | |
| print(run["head_sha"]) | |
| PY | |
| )" | |
| if [[ ! "${snapshot_repository_ref}" =~ ^[0-9a-f]{40}$ ]]; then | |
| echo "::error::Invalid snapshot producer head SHA" | |
| exit 1 | |
| fi | |
| echo "snapshot_repository_ref=${snapshot_repository_ref}" >> "${GITHUB_OUTPUT}" | |
| gh api "/repos/QuantStrategyLab/CryptoLivePoolPipelines/actions/artifacts/${artifact_id}/zip" \ | |
| > "${RUNNER_TEMP}/snapshot-artifact.zip" | |
| python - <<'PY' | |
| import os | |
| import zipfile | |
| from pathlib import Path | |
| archive = Path(os.environ["RUNNER_TEMP"]) / "snapshot-artifact.zip" | |
| target_root = Path(os.environ["INPUT_ROOT"]) | |
| required = {"research_panel.csv.gz", "market_history.csv.gz", "manifest.json"} | |
| with zipfile.ZipFile(archive) as bundle: | |
| by_name = {Path(name).name: name for name in bundle.namelist() if Path(name).name in required} | |
| missing = sorted(required - set(by_name)) | |
| if missing: | |
| raise SystemExit(f"crypto lifecycle artifact is missing: {', '.join(missing)}") | |
| target_root.mkdir(parents=True, exist_ok=True) | |
| for name, member in by_name.items(): | |
| (target_root / name).write_bytes(bundle.read(member)) | |
| PY | |
| - name: Build lifecycle preflight bundle | |
| env: | |
| INPUT_ROOT: ${{ runner.temp }}/crypto-lifecycle-inputs | |
| LIFECYCLE_PREFLIGHT_BUNDLE_ROOT: ${{ runner.temp }}/lifecycle-preflight-bundle | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| from pathlib import Path | |
| profiles = json.loads( | |
| subprocess.check_output( | |
| ["python", "scripts/run_walk_forward_backtest.py", "--list-profiles"], | |
| text=True, | |
| ) | |
| )["profiles"] | |
| input_root = Path(os.environ["INPUT_ROOT"]) | |
| bundle_root = Path(os.environ["LIFECYCLE_PREFLIGHT_BUNDLE_ROOT"]) | |
| store_root = bundle_root / "data" / "lifecycle_store" | |
| for profile in profiles: | |
| returns_output = ( | |
| bundle_root | |
| / "external" | |
| / "CryptoLivePoolPipelines" | |
| / "data" | |
| / "output" | |
| / profile | |
| / "portfolio_and_tracker_returns.csv" | |
| ) | |
| subprocess.check_call( | |
| [ | |
| "python", | |
| "scripts/run_walk_forward_backtest.py", | |
| "--profile", | |
| profile, | |
| "--panel", | |
| str(input_root / "research_panel.csv.gz"), | |
| "--market-history", | |
| str(input_root / "market_history.csv.gz"), | |
| "--store-root", | |
| str(store_root), | |
| "--returns-output", | |
| str(returns_output), | |
| ] | |
| ) | |
| PY | |
| - name: Upload lifecycle preflight artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ runner.temp }}/lifecycle-preflight-bundle | |
| if-no-files-found: error | |
| drift: | |
| if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) | |
| needs: preflight_backtests | |
| permissions: | |
| contents: read | |
| issues: write | |
| id-token: write | |
| uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@4f8465b28a6787d39d21e50f9d95a77841d6ad56 | |
| with: | |
| strategy_domain: crypto | |
| caller_event_name: ${{ github.event_name }} | |
| caller_pr_head_repository: ${{ github.event.pull_request.head.repo.full_name || '' }} | |
| snapshot_repository: QuantStrategyLab/CryptoLivePoolPipelines | |
| snapshot_checkout_path: external/CryptoLivePoolPipelines | |
| snapshot_repository_ref: ${{ needs.preflight_backtests.outputs.snapshot_repository_ref }} | |
| ai_gateway_service_url: ${{ vars.AI_GATEWAY_SERVICE_URL }} | |
| quant_platform_kit_ref: 4f8465b28a6787d39d21e50f9d95a77841d6ad56 | |
| lifecycle_preflight_artifact: lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }} | |
| secrets: | |
| codex_audit_service_url: ${{ secrets.CODEX_AUDIT_SERVICE_URL }} | |
| snapshot_repository_token: ${{ secrets.SNAPSHOT_REPOSITORY_TOKEN || secrets.QSL_REPO_SYNC_TOKEN || github.token }} |