Skip to content

Commit b0eacd2

Browse files
Pigbibicodex
andauthored
feat: add strategy lifecycle CLI (#160)
- Add quant-lifecycle console script backed by QuantPlatformKit lifecycle modules - Keep the CLI dependency-free by using argparse - Publish drift alerts from the drift command and run real lifecycle steps - Cover command delegation, lifecycle execution, and error exit behavior Co-Authored-By: Codex <noreply@openai.com>
1 parent 6d36759 commit b0eacd2

5 files changed

Lines changed: 413 additions & 0 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ python -m pip install -e .
3131
python -m pytest -q
3232
```
3333

34+
## Strategy lifecycle CLI
35+
36+
`quant-lifecycle` provides the shared lifecycle entrypoint formerly wrapped by `QuantStrategyLifecycle`.
37+
Production schedules should live in domain repositories and call this CLI or the underlying
38+
`quant_platform_kit.strategy_lifecycle` modules directly.
39+
40+
```bash
41+
quant-lifecycle monitor --domain us_equity
42+
quant-lifecycle drift --domain us_equity
43+
quant-lifecycle autopilot --domain us_equity --dry-run
44+
quant-lifecycle dashboard --format all
45+
```
46+
3447
## Cloud provider abstraction
3548

3649
QuantPlatformKit includes a cloud provider abstraction layer at `quant_platform_kit.cloud`. It defines protocol interfaces for common cloud services — secret management, object storage, document databases, compute discovery, and deployment context — so that platform code can be written without hard-wiring to a specific cloud provider.

README.zh-CN.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ python -m pip install -e .
3131
python -m pytest -q
3232
```
3333

34+
## 策略生命周期 CLI
35+
36+
`quant-lifecycle` 是原 `QuantStrategyLifecycle` wrapper 职责迁入后的共享入口。
37+
生产定时任务应放在各 domain 仓库,并调用这个 CLI 或底层
38+
`quant_platform_kit.strategy_lifecycle` 模块。
39+
40+
```bash
41+
quant-lifecycle monitor --domain us_equity
42+
quant-lifecycle drift --domain us_equity
43+
quant-lifecycle autopilot --domain us_equity --dry-run
44+
quant-lifecycle dashboard --format all
45+
```
46+
3447
## 云服务抽象层
3548

3649
`quant_platform_kit.cloud` 包为常用云服务定义了协议接口——密钥管理、对象存储、文档数据库、计算发现和部署上下文。平台代码可以通过这些接口编写,无需硬编码到特定云厂商。

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ authors = [
1414
]
1515
dependencies = []
1616

17+
[project.scripts]
18+
quant-lifecycle = "quant_platform_kit.strategy_lifecycle.cli:main"
19+
1720
[tool.setuptools]
1821
package-dir = { "" = "src" }
1922

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"""Command-line entrypoint for strategy lifecycle operations."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import importlib
7+
import sys
8+
from collections.abc import Callable, Sequence
9+
from typing import Any
10+
11+
12+
def _load_callable(module_name: str, function_name: str) -> Callable[..., Any]:
13+
module = importlib.import_module(module_name)
14+
return getattr(module, function_name)
15+
16+
17+
def _print(message: str) -> None:
18+
print(message)
19+
20+
21+
def _run_monitor(args: argparse.Namespace) -> int:
22+
_print(f"[monitor] Running performance monitor for domain={args.domain}")
23+
run_monitor = _load_callable(
24+
"quant_platform_kit.strategy_lifecycle.performance_monitor",
25+
"run_monitor",
26+
)
27+
snapshots = run_monitor(
28+
domain=args.domain,
29+
strategy_profile=args.strategy,
30+
output_dir=args.output_dir,
31+
)
32+
_print(f"[monitor] Generated {len(snapshots)} performance snapshots")
33+
return 0
34+
35+
36+
def _run_drift(args: argparse.Namespace) -> int:
37+
_print(f"[drift] Running drift detection for domain={args.domain}")
38+
run_drift_detection = _load_callable(
39+
"quant_platform_kit.strategy_lifecycle.drift_detector",
40+
"run_drift_detection",
41+
)
42+
results = run_drift_detection(domain=args.domain, strategy_profile=args.strategy)
43+
critical_count = sum(1 for item in results if getattr(getattr(item, "status", None), "value", None) == "critical")
44+
review_count = sum(1 for item in results if getattr(getattr(item, "status", None), "value", None) == "review")
45+
_print(f"[drift] {len(results)} strategies checked, {critical_count} critical, {review_count} review")
46+
if not getattr(args, "no_alerts", False):
47+
build_drift_alert = _load_callable(
48+
"quant_platform_kit.strategy_lifecycle.drift_alerts",
49+
"build_drift_alert",
50+
)
51+
publish_drift_alerts = _load_callable(
52+
"quant_platform_kit.strategy_lifecycle.drift_alerts",
53+
"publish_drift_alerts",
54+
)
55+
events = [event for event in (build_drift_alert(result) for result in results) if event is not None]
56+
counts = publish_drift_alerts(events, dry_run=getattr(args, "dry_run_alerts", False))
57+
_print(f"[drift] Alerts published: {sum(counts.values())}")
58+
return 0
59+
60+
61+
def _run_optimize(args: argparse.Namespace) -> int:
62+
_print(f"[optimize] Optimizing {args.strategy} with method={args.method}")
63+
run_optimization = _load_callable(
64+
"quant_platform_kit.strategy_lifecycle.param_optimizer",
65+
"run_optimization",
66+
)
67+
proposal = run_optimization(strategy_profile=args.strategy, method=args.method)
68+
_print(f"[optimize] Recommendation: {getattr(proposal, 'recommendation', '')}")
69+
improvement_score = getattr(proposal, "improvement_score", None)
70+
if improvement_score is not None:
71+
_print(f"[optimize] Improvement score: {improvement_score:.3f}")
72+
return 0
73+
74+
75+
def _run_update(args: argparse.Namespace) -> int:
76+
_print(f"[update] Processing proposal: {args.proposal}")
77+
process_update = _load_callable(
78+
"quant_platform_kit.strategy_lifecycle.update_orchestrator",
79+
"process_update",
80+
)
81+
result = process_update(proposal_path=args.proposal, auto_approve=args.auto_approve)
82+
_print(f"[update] Result: stage={result.get('stage')}, reason={result.get('reason', '')}")
83+
return 1 if result.get("stage") == "error" else 0
84+
85+
86+
def _run_dashboard(args: argparse.Namespace) -> int:
87+
_print(f"[dashboard] Building health dashboard (format={args.output_format})")
88+
build_dashboard = _load_callable(
89+
"quant_platform_kit.strategy_lifecycle.health_dashboard",
90+
"build_dashboard",
91+
)
92+
result = build_dashboard(output_dir=args.output_dir, output_format=args.output_format)
93+
_print(f"[dashboard] Dashboard built with {result.get('strategy_count', 0)} strategies")
94+
return 0
95+
96+
97+
def _run_autopilot(args: argparse.Namespace) -> int:
98+
_print(f"[autopilot] Running auto-pilot cycle for domain={args.domain} (dry_run={args.dry_run})")
99+
run_auto_pilot_cycle = _load_callable(
100+
"quant_platform_kit.strategy_lifecycle.codex_integration",
101+
"run_auto_pilot_cycle",
102+
)
103+
summary = run_auto_pilot_cycle(
104+
args.domain,
105+
dry_run=args.dry_run,
106+
create_issues=not args.no_issues,
107+
trigger_optimization=True,
108+
)
109+
_print(f"[autopilot] Snapshots: {summary.get('snapshots_count', 0)}")
110+
_print(f"[autopilot] Drifts checked: {summary.get('drifts_checked', 0)}")
111+
_print(f"[autopilot] Drifts alerting: {summary.get('drifts_alerting', 0)}")
112+
_print(f"[autopilot] Issues created: {summary.get('issues_created', 0)}")
113+
_print(f"[autopilot] Actions: {len(summary.get('actions', []))}")
114+
for action in summary.get("actions", []):
115+
decision = action.get("ai_decision", {})
116+
_print(
117+
f" - {action['strategy']}: drift={action['drift_status']}, "
118+
f"optimize={decision.get('optimization_needed')}, "
119+
f"method={decision.get('recommended_method', 'none')}"
120+
)
121+
return 0
122+
123+
124+
def _run_lifecycle(args: argparse.Namespace) -> int:
125+
_print(f"[lifecycle] Running full lifecycle for domain={args.domain}")
126+
_print("[lifecycle] Step: monitor")
127+
monitor_status = _run_monitor(argparse.Namespace(domain=args.domain, strategy=None, output_dir=None))
128+
if monitor_status != 0:
129+
return monitor_status
130+
131+
_print("[lifecycle] Step: drift")
132+
drift_status = _run_drift(
133+
argparse.Namespace(
134+
domain=args.domain,
135+
strategy=None,
136+
no_alerts=args.no_alerts,
137+
dry_run_alerts=args.dry_run_alerts,
138+
)
139+
)
140+
if drift_status != 0:
141+
return drift_status
142+
143+
if not args.skip_optimization:
144+
if args.strategy:
145+
_print("[lifecycle] Step: optimize")
146+
optimize_status = _run_optimize(argparse.Namespace(strategy=args.strategy, method=args.method))
147+
if optimize_status != 0:
148+
return optimize_status
149+
else:
150+
_print("[lifecycle] Step: optimize skipped (no --strategy provided)")
151+
152+
_print("[lifecycle] Step: dashboard")
153+
dashboard_status = _run_dashboard(argparse.Namespace(output_dir=None, output_format=args.output_format))
154+
if dashboard_status != 0:
155+
return dashboard_status
156+
157+
_print("[lifecycle] Full lifecycle complete")
158+
return 0
159+
160+
161+
def build_parser() -> argparse.ArgumentParser:
162+
parser = argparse.ArgumentParser(prog="quant-lifecycle", description="Quant strategy lifecycle CLI.")
163+
parser.add_argument("--version", action="version", version="quant-lifecycle 0.10.0")
164+
subparsers = parser.add_subparsers(dest="command", required=True)
165+
166+
monitor = subparsers.add_parser("monitor", help="Run the continuous performance monitor for one domain.")
167+
monitor.add_argument("--domain", default="us_equity")
168+
monitor.add_argument("--strategy", default=None)
169+
monitor.add_argument("--output-dir", default=None)
170+
monitor.set_defaults(func=_run_monitor)
171+
172+
drift = subparsers.add_parser("drift", help="Run drift detection and publish drift alerts.")
173+
drift.add_argument("--domain", default="us_equity")
174+
drift.add_argument("--strategy", default=None)
175+
drift.add_argument("--no-alerts", action="store_true")
176+
drift.add_argument("--dry-run-alerts", action="store_true")
177+
drift.set_defaults(func=_run_drift)
178+
179+
optimize = subparsers.add_parser("optimize", help="Run parameter optimization for one strategy.")
180+
optimize.add_argument("--strategy", required=True)
181+
optimize.add_argument("--method", default="grid_search")
182+
optimize.set_defaults(func=_run_optimize)
183+
184+
update = subparsers.add_parser("update", help="Process a parameter update proposal.")
185+
update.add_argument("--proposal", required=True)
186+
update.add_argument("--auto-approve", action="store_true")
187+
update.set_defaults(func=_run_update)
188+
189+
dashboard = subparsers.add_parser("dashboard", help="Build the unified strategy health dashboard.")
190+
dashboard.add_argument("--output-dir", default=None)
191+
dashboard.add_argument("--format", dest="output_format", default="all")
192+
dashboard.set_defaults(func=_run_dashboard)
193+
194+
autopilot = subparsers.add_parser("autopilot", help="Run a full auto-pilot cycle.")
195+
autopilot.add_argument("--domain", default="us_equity")
196+
autopilot.add_argument("--dry-run", action="store_true")
197+
autopilot.add_argument("--no-issues", action="store_true")
198+
autopilot.set_defaults(func=_run_autopilot)
199+
200+
lifecycle = subparsers.add_parser("lifecycle", help="Run the full lifecycle pipeline.")
201+
lifecycle.add_argument("--domain", default="us_equity")
202+
lifecycle.add_argument("--strategy", default=None)
203+
lifecycle.add_argument("--method", default="grid_search")
204+
lifecycle.add_argument("--format", dest="output_format", default="all")
205+
lifecycle.add_argument("--skip-optimization", action="store_true")
206+
lifecycle.add_argument("--no-alerts", action="store_true")
207+
lifecycle.add_argument("--dry-run-alerts", action="store_true")
208+
lifecycle.set_defaults(func=_run_lifecycle)
209+
210+
return parser
211+
212+
213+
def main(argv: Sequence[str] | None = None) -> int:
214+
parser = build_parser()
215+
args = parser.parse_args(argv)
216+
try:
217+
return int(args.func(args))
218+
except Exception as exc: # noqa: BLE001
219+
print(f"[{args.command}] Error: {exc}", file=sys.stderr)
220+
return 1
221+
222+
223+
if __name__ == "__main__":
224+
raise SystemExit(main())

0 commit comments

Comments
 (0)