Skip to content

Drift Check

Drift Check #48

Workflow file for this run

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: 3acab1923a97b805b077c85c6c19657be0143bac
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@3acab1923a97b805b077c85c6c19657be0143bac
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: 3acab1923a97b805b077c85c6c19657be0143bac
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 }}