Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 59 additions & 26 deletions .github/workflows/reusable-drift-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,18 @@ on:
required: false
type: string
default: ""
caller_event_name:
required: true
type: string
caller_pr_head_repository:
required: false
type: string
default: ""
secrets:
codex_audit_service_url:
required: false
snapshot_repository_token:
required: false

permissions:
contents: read
Expand All @@ -41,42 +50,65 @@ jobs:
LIFECYCLE_LOCAL_ROOT: ${{ github.workspace }}/data/lifecycle_store

steps:
- name: Resolve QuantPlatformKit ref
id: quant-platform-kit-ref
shell: bash
run: |
set -euo pipefail
workflow_ref="${{ github.workflow_ref }}"
ref="${workflow_ref##*@}"
case "$ref" in
refs/heads/*)
ref="${ref#refs/heads/}"
;;
refs/tags/*)
ref="${ref#refs/tags/}"
;;
esac
if [ -z "$ref" ]; then
ref="main"
fi
echo "ref=$ref" >> "$GITHUB_OUTPUT"

- name: Validate trusted caller
env:
CALLER_REPOSITORY: ${{ github.repository }}
CALLER_EVENT_NAME: ${{ inputs.caller_event_name }}
CALLER_PR_HEAD_REPOSITORY: ${{ inputs.caller_pr_head_repository }}
SNAPSHOT_REPOSITORY: ${{ inputs.snapshot_repository }}
SNAPSHOT_REPOSITORY_TOKEN: ${{ secrets.snapshot_repository_token }}
shell: bash
run: |
set -euo pipefail
caller_repo="${{ github.repository }}"
snapshot_repo="${{ inputs.snapshot_repository }}"
caller_repo="$CALLER_REPOSITORY"
caller_event="$CALLER_EVENT_NAME"
caller_pr_head_repo="$CALLER_PR_HEAD_REPOSITORY"
snapshot_repo="$SNAPSHOT_REPOSITORY"
expected_snapshot_repo=""
case "$caller_repo" in
QuantStrategyLab/CnEquityStrategies|QuantStrategyLab/HkEquityStrategies|QuantStrategyLab/UsEquityStrategies|QuantStrategyLab/CryptoStrategies)
QuantStrategyLab/CnEquityStrategies)
expected_snapshot_repo="QuantStrategyLab/CnEquitySnapshotPipelines"
;;
QuantStrategyLab/HkEquityStrategies)
expected_snapshot_repo="QuantStrategyLab/HkEquitySnapshotPipelines"
;;
QuantStrategyLab/UsEquityStrategies)
expected_snapshot_repo="QuantStrategyLab/UsEquitySnapshotPipelines"
;;
QuantStrategyLab/CryptoStrategies)
expected_snapshot_repo="QuantStrategyLab/CryptoLivePoolPipelines"
;;
*)
echo "::error::Untrusted reusable workflow caller: $caller_repo"
exit 1
;;
esac
if [ "$snapshot_repo" != "$caller_repo" ]; then
echo "::error::snapshot_repository must match caller repository: $snapshot_repo"
case "$caller_event" in
schedule|workflow_dispatch)
;;
pull_request|pull_request_target)
if [ -n "$caller_pr_head_repo" ] && [ "$caller_pr_head_repo" != "$caller_repo" ]; then
echo "::error::Fork pull_request callers are not trusted: $caller_pr_head_repo"
else
echo "::error::pull_request callers are not trusted: $caller_repo"
fi
exit 1
;;
"")
echo "::error::caller_event_name must be provided by the caller workflow"
exit 1
;;
*)
echo "::error::Unsupported caller_event_name: $caller_event"
exit 1
;;
esac
if [ "$snapshot_repo" != "$expected_snapshot_repo" ]; then
echo "::error::snapshot_repository mismatch for caller $caller_repo: $snapshot_repo"
exit 1
fi
if [ "$snapshot_repo" != "$caller_repo" ] && [ -z "$SNAPSHOT_REPOSITORY_TOKEN" ]; then
echo "::error::snapshot_repository_token is required for private cross-repo snapshot checkout"
exit 1
fi

Expand All @@ -88,13 +120,14 @@ jobs:
with:
repository: ${{ inputs.snapshot_repository }}
ref: main
token: ${{ secrets.snapshot_repository_token || github.token }}
path: ${{ inputs.snapshot_checkout_path }}

- name: Checkout QuantPlatformKit
uses: actions/checkout@v6
with:
repository: QuantStrategyLab/QuantPlatformKit
ref: ${{ steps.quant-platform-kit-ref.outputs.ref }}
ref: ${{ github.workflow_sha }}
path: external/QuantPlatformKit

- name: Set up Python
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ def persist_result(
domain: str,
params: Mapping[str, Any],
param_set_id: str = "",
param_version: int = 1,
param_version: int | None = None,
) -> BacktestResult:
enriched = BacktestResult(
strategy_profile=strategy_profile,
domain=domain,
param_set_id=param_set_id or result.param_set_id or _run_id(),
params=dict(params),
param_version=max(param_version, 1),
param_version=max(int((result.param_version if param_version is None else param_version) or 1), 1),
sharpe_ratio=result.sharpe_ratio,
calmar_ratio=result.calmar_ratio,
sortino_ratio=result.sortino_ratio,
Expand Down
47 changes: 47 additions & 0 deletions tests/test_backtest_orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,53 @@ def test_run_enriches_and_persists(self) -> None:
self.assertTrue(result.computed_at)
self.assertEqual(result.source_script, "backtest_orchestrator")

def test_persist_result_preserves_existing_param_version_by_default(self) -> None:
result = BacktestResult(
strategy_profile="test_strat",
domain="us_equity",
param_set_id="candidate",
params={"lookback": 30},
param_version=7,
sharpe_ratio=1.3,
cagr=0.12,
max_drawdown=-0.08,
observation_count=252,
)

persisted = self.orchestrator.persist_result(
result,
strategy_profile="test_strat",
domain="us_equity",
params={"lookback": 30},
param_set_id="persisted",
)

self.assertEqual(persisted.param_version, 7)

def test_persist_result_clamps_explicit_zero_param_version(self) -> None:
result = BacktestResult(
strategy_profile="test_strat",
domain="us_equity",
param_set_id="candidate",
params={"lookback": 30},
param_version=7,
sharpe_ratio=1.3,
cagr=0.12,
max_drawdown=-0.08,
observation_count=252,
)

persisted = self.orchestrator.persist_result(
result,
strategy_profile="test_strat",
domain="us_equity",
params={"lookback": 30},
param_set_id="persisted",
param_version=0,
)

self.assertEqual(persisted.param_version, 1)

def test_run_raises_without_runner(self) -> None:
with self.assertRaises(ValueError):
self.orchestrator.run("test_strat", domain="cn_equity", params={})
Expand Down
13 changes: 11 additions & 2 deletions tests/test_reusable_drift_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@ def test_reusable_drift_workflow_enforces_lifecycle_preflight() -> None:
assert "workflow_call:" in workflow
assert "strategy_domain:" in workflow
assert "snapshot_repository:" in workflow
assert "snapshot_repository_token:" in workflow
assert "snapshot_checkout_path:" in workflow
assert "ai_gateway_service_url:" in workflow
assert "caller_event_name:" in workflow
assert "caller_pr_head_repository:" in workflow
assert "codex_audit_service_url:" in workflow
assert 'python-version: ${{ inputs.python_version }}' in workflow
assert 'LIFECYCLE_PERFORMANCE_BUCKET: ${{ vars.LIFECYCLE_PERFORMANCE_BUCKET || \'\' }}' in workflow
assert "Validate trusted caller" in workflow
assert "Untrusted reusable workflow caller" in workflow
assert "snapshot_repository must match caller repository" in workflow
assert "caller_event_name must be provided by the caller workflow" in workflow
assert "Unsupported caller_event_name" in workflow
assert "pull_request callers are not trusted" in workflow
assert "snapshot_repository mismatch for caller" in workflow
assert "Fork pull_request callers are not trusted" in workflow
assert "snapshot_repository_token is required for private cross-repo snapshot checkout" in workflow
assert "quant-lifecycle monitor --domain ${{ inputs.strategy_domain }}" in workflow
assert (
"quant-lifecycle doctor --domain ${{ inputs.strategy_domain }} --require-snapshot "
Expand All @@ -28,7 +36,8 @@ def test_reusable_drift_workflow_enforces_lifecycle_preflight() -> None:
assert "quant-lifecycle drift --domain ${{ inputs.strategy_domain }} --no-alerts" in workflow
assert 'repository: ${{ inputs.snapshot_repository }}' in workflow
assert 'path: ${{ inputs.snapshot_checkout_path }}' in workflow
assert 'ref: ${{ steps.quant-platform-kit-ref.outputs.ref }}' in workflow
assert 'token: ${{ secrets.snapshot_repository_token || github.token }}' in workflow
assert 'ref: ${{ github.workflow_sha }}' in workflow
assert 'GH_TOKEN: ${{ github.token }}' in workflow
assert "create_issues_for_domain" in workflow
assert 'CODEX_AUDIT_SERVICE_URL: ${{ secrets.codex_audit_service_url }}' in workflow
Expand Down
Loading