|
6 | 6 | import importlib |
7 | 7 | import sys |
8 | 8 | from collections.abc import Callable, Sequence |
9 | | -from dataclasses import replace |
| 9 | +from pathlib import Path |
10 | 10 | from typing import Any |
11 | 11 |
|
12 | 12 |
|
@@ -34,24 +34,71 @@ def _run_monitor(args: argparse.Namespace) -> int: |
34 | 34 | return 0 |
35 | 35 |
|
36 | 36 |
|
| 37 | +def _parse_baseline_bucket(value: str) -> tuple[str, str]: |
| 38 | + location = value.strip() |
| 39 | + if location.startswith("gs://"): |
| 40 | + location = location[5:] |
| 41 | + bucket, _, prefix = location.partition("/") |
| 42 | + if not bucket: |
| 43 | + raise ValueError("baseline bucket must include a bucket name") |
| 44 | + return bucket, prefix.strip("/") |
| 45 | + |
| 46 | + |
| 47 | +def _baseline_store_from_args(args: argparse.Namespace): |
| 48 | + from quant_platform_kit.strategy_lifecycle.performance_store import PerformanceStore |
| 49 | + |
| 50 | + local_root = getattr(args, "baseline_local_root", None) |
| 51 | + bucket_value = getattr(args, "baseline_bucket", None) |
| 52 | + if not local_root and not bucket_value: |
| 53 | + return None |
| 54 | + if not bucket_value: |
| 55 | + return PerformanceStore(local_root=Path(local_root)) |
| 56 | + |
| 57 | + bucket, prefix = _parse_baseline_bucket(bucket_value) |
| 58 | + environment_store = PerformanceStore.from_env() |
| 59 | + return PerformanceStore( |
| 60 | + cloud_bucket=bucket, |
| 61 | + cloud_prefix=prefix, |
| 62 | + local_root=Path(local_root) if local_root else None, |
| 63 | + project_id=environment_store.project_id, |
| 64 | + client_factory=environment_store.client_factory, |
| 65 | + ) |
| 66 | + |
| 67 | + |
| 68 | +def _baseline_lineage_policy_from_args(args: argparse.Namespace) -> str: |
| 69 | + allow_legacy = getattr(args, "allow_legacy_baseline_history", False) |
| 70 | + strict = getattr(args, "strict_baseline_lineage", False) |
| 71 | + if allow_legacy and strict: |
| 72 | + raise ValueError("baseline lineage flags are mutually exclusive") |
| 73 | + if allow_legacy: |
| 74 | + return "migration" |
| 75 | + return "strict" if strict else "auto" |
| 76 | + |
| 77 | + |
| 78 | +def _add_baseline_options(parser: argparse.ArgumentParser) -> None: |
| 79 | + parser.add_argument("--baseline-local-root", default=None) |
| 80 | + parser.add_argument( |
| 81 | + "--baseline-bucket", |
| 82 | + default=None, |
| 83 | + help="Accepted-baseline bucket or gs://bucket/prefix URI.", |
| 84 | + ) |
| 85 | + lineage = parser.add_mutually_exclusive_group() |
| 86 | + lineage.add_argument("--strict-baseline-lineage", action="store_true") |
| 87 | + lineage.add_argument( |
| 88 | + "--allow-legacy-baseline-history", |
| 89 | + action="store_true", |
| 90 | + help="One-time migration: reuse untagged prior drift status with an external accepted baseline.", |
| 91 | + ) |
| 92 | + |
| 93 | + |
37 | 94 | def _run_drift(args: argparse.Namespace) -> int: |
38 | 95 | _print(f"[drift] Running drift detection for domain={args.domain}") |
39 | 96 | run_drift_detection = _load_callable( |
40 | 97 | "quant_platform_kit.strategy_lifecycle.drift_detector", |
41 | 98 | "run_drift_detection", |
42 | 99 | ) |
43 | | - baseline_store = None |
44 | | - if getattr(args, "baseline_local_root", None): |
45 | | - from pathlib import Path |
46 | | - from quant_platform_kit.strategy_lifecycle.performance_store import PerformanceStore |
47 | | - |
48 | | - baseline_store = replace( |
49 | | - PerformanceStore.from_env(), |
50 | | - local_root=Path(args.baseline_local_root), |
51 | | - ) |
52 | | - baseline_lineage_policy = "migration" if getattr(args, "allow_legacy_baseline_history", False) else ( |
53 | | - "strict" if getattr(args, "strict_baseline_lineage", False) else "auto" |
54 | | - ) |
| 100 | + baseline_store = _baseline_store_from_args(args) |
| 101 | + baseline_lineage_policy = _baseline_lineage_policy_from_args(args) |
55 | 102 | results = run_drift_detection( |
56 | 103 | domain=args.domain, |
57 | 104 | strategy_profile=args.strategy, |
@@ -153,6 +200,10 @@ def _run_lifecycle(args: argparse.Namespace) -> int: |
153 | 200 | strategy=None, |
154 | 201 | no_alerts=args.no_alerts, |
155 | 202 | dry_run_alerts=args.dry_run_alerts, |
| 203 | + baseline_local_root=getattr(args, "baseline_local_root", None), |
| 204 | + baseline_bucket=getattr(args, "baseline_bucket", None), |
| 205 | + strict_baseline_lineage=getattr(args, "strict_baseline_lineage", False), |
| 206 | + allow_legacy_baseline_history=getattr(args, "allow_legacy_baseline_history", False), |
156 | 207 | ) |
157 | 208 | ) |
158 | 209 | if drift_status != 0: |
@@ -251,13 +302,7 @@ def build_parser() -> argparse.ArgumentParser: |
251 | 302 | drift.add_argument("--strategy", default=None) |
252 | 303 | drift.add_argument("--no-alerts", action="store_true") |
253 | 304 | drift.add_argument("--dry-run-alerts", action="store_true") |
254 | | - drift.add_argument("--baseline-local-root", default=None) |
255 | | - drift.add_argument("--strict-baseline-lineage", action="store_true") |
256 | | - drift.add_argument( |
257 | | - "--allow-legacy-baseline-history", |
258 | | - action="store_true", |
259 | | - help="One-time migration: reuse untagged prior drift status with an external accepted baseline.", |
260 | | - ) |
| 305 | + _add_baseline_options(drift) |
261 | 306 | drift.set_defaults(func=_run_drift) |
262 | 307 |
|
263 | 308 | optimize = subparsers.add_parser("optimize", help="Run parameter optimization for one strategy.") |
@@ -309,6 +354,7 @@ def build_parser() -> argparse.ArgumentParser: |
309 | 354 | lifecycle.add_argument("--skip-optimization", action="store_true") |
310 | 355 | lifecycle.add_argument("--no-alerts", action="store_true") |
311 | 356 | lifecycle.add_argument("--dry-run-alerts", action="store_true") |
| 357 | + _add_baseline_options(lifecycle) |
312 | 358 | lifecycle.set_defaults(func=_run_lifecycle) |
313 | 359 |
|
314 | 360 | return parser |
|
0 commit comments