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
40 changes: 40 additions & 0 deletions .github/workflows/reusable-drift-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ on:
required: false
type: string
default: ""
gcp_workload_identity_provider:
description: "Optional WIF provider for writing LIFECYCLE_PERFORMANCE_BUCKET."
required: false
type: string
default: ""
gcp_service_account:
description: "Optional GCP service account email for lifecycle bucket writes."
required: false
type: string
default: ""
gcp_project_id:
description: "Optional GCP project id for gcloud setup."
required: false
type: string
default: ""
secrets:
codex_audit_service_url:
required: false
Expand Down Expand Up @@ -160,6 +175,31 @@ jobs:
python -m pip install -e . pandas
python -m pip install --no-deps -e external/QuantPlatformKit

- name: Gate cloud lifecycle writes without GCP auth
env:
WIF_PROVIDER: ${{ inputs.gcp_workload_identity_provider }}
WIF_SA: ${{ inputs.gcp_service_account }}
shell: bash
run: |
set -euo pipefail
if [ -n "${LIFECYCLE_PERFORMANCE_BUCKET:-}" ] && { [ -z "${WIF_PROVIDER}" ] || [ -z "${WIF_SA}" ]; }; then
echo "::warning::LIFECYCLE_PERFORMANCE_BUCKET is set without GCP WIF inputs; keeping local-only lifecycle store for this run"
echo "LIFECYCLE_PERFORMANCE_BUCKET=" >> "$GITHUB_ENV"
fi

- name: Authenticate to Google Cloud
if: ${{ inputs.gcp_workload_identity_provider != '' && inputs.gcp_service_account != '' }}
uses: google-github-actions/auth@v3
with:
workload_identity_provider: ${{ inputs.gcp_workload_identity_provider }}
service_account: ${{ inputs.gcp_service_account }}

- name: Set up gcloud
if: ${{ inputs.gcp_workload_identity_provider != '' && inputs.gcp_service_account != '' && inputs.gcp_project_id != '' }}
uses: google-github-actions/setup-gcloud@v3
with:
project_id: ${{ inputs.gcp_project_id }}

- name: Download lifecycle preflight artifact
if: inputs.lifecycle_preflight_artifact != ''
uses: actions/download-artifact@v7
Expand Down
8 changes: 7 additions & 1 deletion src/quant_platform_kit/risk/production_drift_new_risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,15 @@ def production_drift_new_risk_reasons(status: object) -> tuple[str, ...]:


def production_drift_status_from_result(drift: Any) -> str | None:
"""Extract status string from a ``DriftResult``-like object (inject helper)."""
"""Extract status string from a ``DriftResult``-like object (inject helper).

Missing baseline (``baseline_available=False``) omits status so NEW_RISK
does not treat no-baseline 0.0 scores as healthy.
"""
if drift is None:
return None
if getattr(drift, "baseline_available", True) is False:
return None
status = getattr(drift, "status", drift)
return normalize_production_drift_status(status)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,15 @@ def resolve_injected_drift_score(
drift: DriftResult | None = None,
snapshot: StrategyPerformanceSnapshot | None = None,
) -> float | None:
"""Prefer latest DriftResult score, else snapshot.drift_score; never invent 0.0."""
"""Prefer latest DriftResult score, else snapshot.drift_score; never invent 0.0.

DriftResult with ``baseline_available=False`` is treated as unavailable even
when ``drift_score`` is 0.0 (no-baseline detect_drift must not look healthy).
"""

if drift is not None:
if getattr(drift, "baseline_available", True) is False:
return None
try:
return sanitize_unit_drift_score(drift.drift_score)
except ValueError:
Expand Down
12 changes: 12 additions & 0 deletions tests/test_production_drift_new_risk.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ def test_status_from_drift_result() -> None:
assert production_drift_status_from_result(None) is None


def test_status_from_result_omits_when_baseline_unavailable() -> None:
drift = DriftResult(
strategy_profile="demo",
domain="us_equity",
as_of=date(2026, 9, 7),
drift_score=0.0,
status=DriftStatus.HEALTHY,
baseline_available=False,
)
assert production_drift_status_from_result(drift) is None



def test_probe_summary_parked_omits_status() -> None:
assert production_drift_status_from_probe_summary(None) is None
Expand Down
20 changes: 20 additions & 0 deletions tests/test_production_drift_score_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,23 @@ def test_resolve_returns_none_when_missing_or_invalid() -> None:
drift_score=1.5,
)
assert resolve_injected_drift_score(snapshot=bad) is None


def test_resolve_omits_score_when_baseline_unavailable() -> None:
drift = DriftResult(
strategy_profile="demo",
domain="us_equity",
as_of=date(2026, 9, 7),
drift_score=0.0,
status=DriftStatus.HEALTHY,
baseline_available=False,
)
snapshot = StrategyPerformanceSnapshot(
strategy_profile="demo",
domain="us_equity",
platform="test",
as_of=date(2026, 9, 6),
drift_score=0.2,
)
# Must not fall through to snapshot when drift exists but baseline is missing.
assert resolve_injected_drift_score(drift=drift, snapshot=snapshot) is None