Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3fa6471
feat: support accepted baseline drift stores
Pigbibi Jul 10, 2026
93d0516
fix: keep drift status with accepted baseline
Pigbibi Jul 10, 2026
ccb0c43
fix: separate drift transition store from baseline store
Pigbibi Jul 10, 2026
47f7fa9
fix: require explicit cross-lineage drift state
Pigbibi Jul 10, 2026
2d3c3cc
fix: track drift baseline lineage
Pigbibi Jul 10, 2026
a32919c
fix: default drift transitions to active store
Pigbibi Jul 10, 2026
b54d4f0
fix: enforce drift baseline lineage
Pigbibi Jul 10, 2026
4db0efd
fix: preserve legacy drift transition state
Pigbibi Jul 10, 2026
68473c9
fix: isolate explicit baseline transitions
Pigbibi Jul 10, 2026
6269a8f
fix: require explicit baseline lineage match
Pigbibi Jul 10, 2026
cb3ce59
fix: keep lifecycle drift invocation compatible
Pigbibi Jul 10, 2026
a8a8897
fix: make drift lineage policy explicit
Pigbibi Jul 10, 2026
6064d70
fix: clear missing drift baseline lineage
Pigbibi Jul 10, 2026
360f9e8
fix: default external drift baselines to strict lineage
Pigbibi Jul 10, 2026
b854781
test: preserve drift result positional compatibility
Pigbibi Jul 10, 2026
1bcf86b
fix: preserve default drift transition history
Pigbibi Jul 10, 2026
cd3bf62
fix: preserve drift lineage during baseline migration
Pigbibi Jul 11, 2026
6a1e27f
fix: retain drift continuity across baseline gaps
Pigbibi Jul 11, 2026
215308f
fix: mark baseline gaps without stale drift metrics
Pigbibi Jul 11, 2026
c5556bf
fix: retain remote baseline and canonical daily drift
Pigbibi Jul 11, 2026
a306ab9
fix: harden accepted baseline drift handling
Pigbibi Jul 11, 2026
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
73 changes: 72 additions & 1 deletion src/quant_platform_kit/strategy_lifecycle/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import importlib
import sys
from collections.abc import Callable, Sequence
from pathlib import Path
from typing import Any


Expand Down Expand Up @@ -33,13 +34,77 @@ def _run_monitor(args: argparse.Namespace) -> int:
return 0


def _parse_baseline_bucket(value: str) -> tuple[str, str]:
location = value.strip()
if location.startswith("gs://"):
location = location[5:]
bucket, _, prefix = location.partition("/")
if not bucket:
raise ValueError("baseline bucket must include a bucket name")
return bucket, prefix.strip("/")


def _baseline_store_from_args(args: argparse.Namespace):
from quant_platform_kit.strategy_lifecycle.performance_store import PerformanceStore

local_root = getattr(args, "baseline_local_root", None)
bucket_value = getattr(args, "baseline_bucket", None)
if not local_root and not bucket_value:
return None
if not bucket_value:
return PerformanceStore(local_root=Path(local_root))

bucket, prefix = _parse_baseline_bucket(bucket_value)
environment_store = PerformanceStore.from_env()
return PerformanceStore(
cloud_bucket=bucket,
cloud_prefix=prefix,
local_root=Path(local_root) if local_root else None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent baseline buckets from reading candidate local data

When --baseline-bucket is used without --baseline-local-root, this passes local_root=None, but PerformanceStore interprets None as DEFAULT_LOCAL_ROOT and load_latest_backtest() merges cloud keys with local keys. In cloud candidate runs the active store also writes candidate backtests to that default local root, so the accepted-baseline store can select a local candidate backtest instead of the accepted bucket baseline, defeating the candidate-vs-accepted comparison. Use an isolated or disabled local root for bucket-only baselines.

Useful? React with 👍 / 👎.

project_id=environment_store.project_id,
client_factory=environment_store.client_factory,
)


def _baseline_lineage_policy_from_args(args: argparse.Namespace) -> str:
allow_legacy = getattr(args, "allow_legacy_baseline_history", False)
strict = getattr(args, "strict_baseline_lineage", False)
if allow_legacy and strict:
raise ValueError("baseline lineage flags are mutually exclusive")
if allow_legacy:
return "migration"
return "strict" if strict else "auto"


def _add_baseline_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--baseline-local-root", default=None)
parser.add_argument(
"--baseline-bucket",
default=None,
help="Accepted-baseline bucket or gs://bucket/prefix URI.",
)
lineage = parser.add_mutually_exclusive_group()
lineage.add_argument("--strict-baseline-lineage", action="store_true")
lineage.add_argument(
"--allow-legacy-baseline-history",
action="store_true",
help="One-time migration: reuse untagged prior drift status with an external accepted baseline.",
)


def _run_drift(args: argparse.Namespace) -> int:
_print(f"[drift] Running drift detection for domain={args.domain}")
run_drift_detection = _load_callable(
"quant_platform_kit.strategy_lifecycle.drift_detector",
"run_drift_detection",
)
results = run_drift_detection(domain=args.domain, strategy_profile=args.strategy)
baseline_store = _baseline_store_from_args(args)
baseline_lineage_policy = _baseline_lineage_policy_from_args(args)
results = run_drift_detection(
domain=args.domain,
strategy_profile=args.strategy,
baseline_store=baseline_store,
baseline_lineage_policy=baseline_lineage_policy,
)
critical_count = sum(1 for item in results if getattr(getattr(item, "status", None), "value", None) == "critical")
review_count = sum(1 for item in results if getattr(getattr(item, "status", None), "value", None) == "review")
_print(f"[drift] {len(results)} strategies checked, {critical_count} critical, {review_count} review")
Expand Down Expand Up @@ -135,6 +200,10 @@ def _run_lifecycle(args: argparse.Namespace) -> int:
strategy=None,
no_alerts=args.no_alerts,
dry_run_alerts=args.dry_run_alerts,
baseline_local_root=getattr(args, "baseline_local_root", None),
baseline_bucket=getattr(args, "baseline_bucket", None),
strict_baseline_lineage=getattr(args, "strict_baseline_lineage", False),
allow_legacy_baseline_history=getattr(args, "allow_legacy_baseline_history", False),
)
)
if drift_status != 0:
Expand Down Expand Up @@ -233,6 +302,7 @@ def build_parser() -> argparse.ArgumentParser:
drift.add_argument("--strategy", default=None)
drift.add_argument("--no-alerts", action="store_true")
drift.add_argument("--dry-run-alerts", action="store_true")
_add_baseline_options(drift)
drift.set_defaults(func=_run_drift)

optimize = subparsers.add_parser("optimize", help="Run parameter optimization for one strategy.")
Expand Down Expand Up @@ -284,6 +354,7 @@ def build_parser() -> argparse.ArgumentParser:
lifecycle.add_argument("--skip-optimization", action="store_true")
lifecycle.add_argument("--no-alerts", action="store_true")
lifecycle.add_argument("--dry-run-alerts", action="store_true")
_add_baseline_options(lifecycle)
lifecycle.set_defaults(func=_run_lifecycle)

return parser
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ def _run_drift_phase(domain: str, store: PerformanceStore) -> tuple[list, list]:
"""Phase 2: run drift detection, return (all_drifts, alerting_drifts)."""
from quant_platform_kit.strategy_lifecycle.drift_detector import run_drift_detection
drifts = run_drift_detection(domain, store=store)
alerts = [d for d in drifts if d.status != DriftStatus.HEALTHY]
alerts = [d for d in drifts if d.status != DriftStatus.HEALTHY and not d.alert_suppressed]
Comment on lines 405 to +406

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Thread accepted baselines through auto-pilot

When operators run quant-lifecycle autopilot or run_auto_pilot_cycle in a candidate/accepted-store rollout, this phase still invokes drift detection with only the candidate store, and I checked that neither the auto-pilot CLI nor run_auto_pilot_cycle accepts a baseline store or lineage policy. That means auto-pilot can optimize and create actions against candidate backtests while standalone drift/lifecycle compare to accepted baselines; thread the accepted baseline options through the auto-pilot drift and issue phases as well.

Useful? React with 👍 / 👎.

return drifts, alerts


Expand Down
8 changes: 8 additions & 0 deletions src/quant_platform_kit/strategy_lifecycle/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ class DriftResult:
escalated: bool = False
cooldown_active: bool = False
alert_suppressed: bool = False
baseline_param_set_id: str | None = None
baseline_available: bool = True
baseline_param_version: int | None = None
baseline_artifact_id: str | None = None

def to_dict(self) -> dict[str, object]:
return {
Expand All @@ -193,6 +197,10 @@ def to_dict(self) -> dict[str, object]:
"status": self.status.value,
"dimensions": {k: v.to_dict() for k, v in self.dimensions.items()},
"previous_status": self.previous_status.value if self.previous_status else None,
"baseline_param_set_id": self.baseline_param_set_id,
"baseline_available": self.baseline_available,
"baseline_param_version": self.baseline_param_version,
"baseline_artifact_id": self.baseline_artifact_id,
"escalated": self.escalated,
"cooldown_active": self.cooldown_active,
"alert_suppressed": self.alert_suppressed,
Expand Down
120 changes: 113 additions & 7 deletions src/quant_platform_kit/strategy_lifecycle/drift_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from __future__ import annotations


from dataclasses import replace

import numpy as np

from quant_platform_kit.strategy_lifecycle.contracts import (
Expand Down Expand Up @@ -32,6 +34,12 @@
]


def _baseline_artifact_id(backtest: BacktestResult | None) -> str | None:
if backtest is None:
return None
return backtest.run_id or backtest.computed_at or None


def _compute_dimension(
key: str, metric: str,
actual_val: float, expected_val: float, threshold: float,
Expand Down Expand Up @@ -123,7 +131,11 @@ def detect_drift(
strategy_profile=snapshot.strategy_profile, domain=snapshot.domain,
as_of=snapshot.as_of, drift_score=round(drift_score, 4),
status=status, dimensions=dimensions,
previous_status=previous_status, escalated=escalated,
previous_status=previous_status,
baseline_param_set_id=backtest.param_set_id if backtest else None,
baseline_param_version=backtest.param_version if backtest else None,
baseline_artifact_id=_baseline_artifact_id(backtest),
escalated=escalated,
)


Expand All @@ -134,9 +146,28 @@ def run_drift_detection(
policy: DriftPolicy | None = None,
fail_on_empty: bool = True,
store: PerformanceStore | None = None,
baseline_store: PerformanceStore | None = None,
previous_drift_store: PerformanceStore | None = None,
baseline_lineage_policy: str = "auto",
) -> list[DriftResult]:
"""Run drift detection for all strategies in a domain."""
"""Run drift detection with explicit baseline and transition-state stores.

External baselines are strict by default. ``migration`` is an explicit,
one-run compatibility mode for legacy drift history without a lineage ID;
it writes the accepted baseline ID into the next result.
"""
store = store or PerformanceStore.from_env()
explicit_baseline_store = baseline_store is not None and baseline_store is not store
if baseline_lineage_policy not in {"auto", "compatible", "migration", "strict"}:
raise ValueError("baseline_lineage_policy must be auto, compatible, migration, or strict")
if explicit_baseline_store and baseline_lineage_policy == "compatible":
raise ValueError("compatible baseline lineage is not allowed with an external baseline store")
if baseline_lineage_policy == "migration" and not explicit_baseline_store:
raise ValueError("migration baseline lineage requires an external baseline store")
if baseline_lineage_policy == "auto":
baseline_lineage_policy = "strict" if explicit_baseline_store else "compatible"
baseline_store = baseline_store or store
read_previous = previous_drift_store or store
policy = policy or DriftPolicy.load_default()

from quant_platform_kit.strategy_lifecycle.return_collector import ReturnCollector
Expand All @@ -158,11 +189,86 @@ def run_drift_detection(
if snapshot is None:
missing_snapshots += 1
continue
backtest = store.load_latest_backtest(domain, profile)
previous = store.load_latest_drift(domain, profile)
result = detect_drift(snapshot, backtest=backtest, policy=policy,
previous_status=previous.status if previous else None)
store.save_drift_result(result)
backtest = baseline_store.load_latest_backtest(domain, profile)
Comment thread
Pigbibi marked this conversation as resolved.
previous = read_previous.load_latest_drift(domain, profile)
previous_before_lineage_check = previous
current_baseline_id = backtest.param_set_id if backtest else None
current_baseline_version = backtest.param_version if backtest else None
current_baseline_artifact_id = _baseline_artifact_id(backtest)
if previous:
previous_baseline_id = previous.baseline_param_set_id
previous_baseline_version = previous.baseline_param_version
previous_baseline_artifact_id = previous.baseline_artifact_id
if baseline_lineage_policy == "strict":
if not (
previous_baseline_id
and current_baseline_id
and previous_baseline_id == current_baseline_id
and previous_baseline_version is not None
and previous_baseline_version == current_baseline_version
and (
not current_baseline_artifact_id
or previous_baseline_artifact_id == current_baseline_artifact_id
)
):
previous = None
elif baseline_lineage_policy == "migration":
if current_baseline_id is None or (
previous_baseline_id is not None and previous_baseline_id != current_baseline_id
) or (
previous_baseline_version is not None
and previous_baseline_version != current_baseline_version
) or (
previous_baseline_artifact_id is not None
and previous_baseline_artifact_id != current_baseline_artifact_id
):
previous = None
if backtest is None:
continuity_result = (
previous_before_lineage_check
if baseline_lineage_policy != "migration"
and previous_before_lineage_check is not None
and previous_before_lineage_check.baseline_param_set_id
else None
)
if continuity_result is not None:
result = DriftResult(
strategy_profile=snapshot.strategy_profile,
domain=snapshot.domain,
as_of=snapshot.as_of,
drift_score=0.0,
status=continuity_result.status,
previous_status=continuity_result.status,
alert_suppressed=True,
Comment thread
Pigbibi marked this conversation as resolved.
baseline_param_set_id=continuity_result.baseline_param_set_id,
baseline_param_version=continuity_result.baseline_param_version,
baseline_artifact_id=continuity_result.baseline_artifact_id,
baseline_available=False,
)
elif explicit_baseline_store:
result = DriftResult(
strategy_profile=snapshot.strategy_profile,
domain=snapshot.domain,
as_of=snapshot.as_of,
drift_score=0.0,
status=DriftStatus.REVIEW,
alert_suppressed=True,
baseline_available=False,
)
else:
result = replace(
detect_drift(snapshot, backtest=None, policy=policy),
alert_suppressed=True,
baseline_available=False,
)
else:
result = detect_drift(snapshot, backtest=backtest, policy=policy,
previous_status=previous.status if previous else None)
if backtest is not None or not (
previous_before_lineage_check is not None
and previous_before_lineage_check.as_of == snapshot.as_of
):
store.save_drift_result(result)
results.append(result)
if not results and fail_on_empty:
raise RuntimeError(
Expand Down
13 changes: 13 additions & 0 deletions src/quant_platform_kit/strategy_lifecycle/performance_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,19 @@ def _drift_from_dict(data: Mapping[str, Any]) -> DriftResult | None:
drift_score=float(data.get("drift_score", 0)),
status=DriftStatus(str(data.get("status", "healthy"))),
dimensions=dimensions,
previous_status=DriftStatus(str(data["previous_status"])) if data.get("previous_status") else None,
baseline_param_set_id=str(data["baseline_param_set_id"]) if data.get("baseline_param_set_id") else None,
baseline_available=bool(data.get("baseline_available", True)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve suppression when reloading drift results

When the new missing-baseline paths persist a placeholder result, DriftResult.to_dict() writes alert_suppressed=True, but this deserializer only restores the new baseline fields and leaves alert_suppressed at its default False. After PerformanceStore.load_latest_drift() round-trips one of these REVIEW placeholders it looks alertable again to any caller that consumes persisted drift results rather than the immediate run_drift_detection() return value, so baseline outages can leak back into alert/issue flows. Restore the persisted suppression flag when reconstructing the result.

Useful? React with 👍 / 👎.

baseline_param_version=(
int(data["baseline_param_version"])
if data.get("baseline_param_version") is not None
else None
),
baseline_artifact_id=(
str(data["baseline_artifact_id"])
if data.get("baseline_artifact_id")
else None
),
)
except Exception:
return None
Expand Down
Loading
Loading