|
26 | 26 | STRATEGY_PROFILES_ASSET = ROOT / "web" / "strategy-switch-console" / "strategy_profiles_asset.js" |
27 | 27 | INDEX_HTML = ROOT / "web" / "strategy-switch-console" / "index.html" |
28 | 28 | LIVE_CANDIDATE_QUEUE_STAGES = {"ai_monitored_candidate", "shadow_candidate", "live_candidate"} |
| 29 | +AUTOMATION_REGISTRY_SCHEMA_VERSION = "strategy_automation_registry.v1" |
29 | 30 | CRITICAL_STRATEGY_PROFILE_FIELDS = { |
30 | 31 | "profile", |
31 | 32 | "domain", |
@@ -168,6 +169,81 @@ def build_live_candidate_queue(strategy_catalog: object | None = None) -> list[d |
168 | 169 | return sorted(queue, key=lambda item: (stage_rank.get(str(item["lifecycle_stage"]), 99), str(item["domain"]), str(item["profile"]))) |
169 | 170 |
|
170 | 171 |
|
| 172 | +def _automation_policy_for_strategy(profile: str, strategy: dict) -> dict[str, object]: |
| 173 | + lifecycle_stage = str(strategy.get("lifecycle_stage") or "").strip() |
| 174 | + can_switch_live = strategy.get("can_switch_live") is True |
| 175 | + runtime_enabled = strategy.get("runtime_enabled") is True |
| 176 | + blocked_reason = str(strategy.get("blocked_live_reason") or "").strip() |
| 177 | + features = strategy.get("features") if isinstance(strategy.get("features"), dict) else {} |
| 178 | + if runtime_enabled and can_switch_live and lifecycle_stage == "runtime_enabled": |
| 179 | + lane = "live_equivalent_optimization" |
| 180 | + triggers = ["health_degradation", "parameter_drift", "scheduled_retest", "market_regime_shift"] |
| 181 | + max_autonomy = "auto_pr_or_trusted_live_equivalent" |
| 182 | + approval_required = False |
| 183 | + evidence_required = ["backtest", "shadow_or_regression", "rollback_plan"] |
| 184 | + elif lifecycle_stage == "live_candidate": |
| 185 | + lane = "promotion_review" |
| 186 | + triggers = ["evidence_package_ready", "shadow_outperformance"] |
| 187 | + max_autonomy = "human_review_required" |
| 188 | + approval_required = True |
| 189 | + evidence_required = ["live_candidate_evidence", "operator_approval"] |
| 190 | + elif lifecycle_stage in {"shadow_candidate", "ai_monitored_candidate"}: |
| 191 | + lane = "shadow_research" |
| 192 | + triggers = ["shadow_disagreement", "web_research_signal", "scheduled_retest"] |
| 193 | + max_autonomy = "auto_pr_research_only" |
| 194 | + approval_required = True |
| 195 | + evidence_required = ["shadow_metrics", "risk_review"] |
| 196 | + else: |
| 197 | + lane = "research_backlog" |
| 198 | + triggers = ["web_research_signal", "manual_request", "scheduled_research"] |
| 199 | + max_autonomy = "auto_pr_research_only" |
| 200 | + approval_required = True |
| 201 | + evidence_required = ["backtest", "design_review"] |
| 202 | + return { |
| 203 | + "profile": profile, |
| 204 | + "label": strategy.get("label_zh") or strategy.get("label") or profile, |
| 205 | + "domain": strategy.get("domain", ""), |
| 206 | + "lifecycle_stage": lifecycle_stage, |
| 207 | + "automation_lane": lane, |
| 208 | + "max_autonomy": max_autonomy, |
| 209 | + "approval_required": approval_required, |
| 210 | + "can_switch_live": can_switch_live, |
| 211 | + "blocked_live_reason": blocked_reason, |
| 212 | + "triggers": triggers, |
| 213 | + "evidence_required": evidence_required, |
| 214 | + "position_control_sensitive": bool(features.get("combo") or features.get("option_overlay")), |
| 215 | + } |
| 216 | + |
| 217 | + |
| 218 | +def build_strategy_automation_registry(config: dict | None = None) -> dict[str, object]: |
| 219 | + """Build strategy-level automation policy for AIAuditBridge and management UIs.""" |
| 220 | + config = config if config is not None else load_config() |
| 221 | + profiles = [ |
| 222 | + _automation_policy_for_strategy(profile, strategy) |
| 223 | + for profile, strategy in sorted(config.get("strategies", {}).items()) |
| 224 | + if isinstance(strategy, dict) |
| 225 | + ] |
| 226 | + lane_counts: dict[str, int] = {} |
| 227 | + for item in profiles: |
| 228 | + lane = str(item["automation_lane"]) |
| 229 | + lane_counts[lane] = lane_counts.get(lane, 0) + 1 |
| 230 | + return { |
| 231 | + "schema_version": AUTOMATION_REGISTRY_SCHEMA_VERSION, |
| 232 | + "summary": { |
| 233 | + "strategy_profile_count": len(profiles), |
| 234 | + "lane_counts": lane_counts, |
| 235 | + "live_switchable_count": sum(1 for item in profiles if item["can_switch_live"]), |
| 236 | + "approval_required_count": sum(1 for item in profiles if item["approval_required"]), |
| 237 | + }, |
| 238 | + "profiles": profiles, |
| 239 | + "guardrails": [ |
| 240 | + "Do not auto-promote new or reconstructed strategies to live.", |
| 241 | + "Live-equivalent optimization still requires trusted proof before auto-merge.", |
| 242 | + "Position-control-sensitive changes require human review unless service proof explicitly narrows the change.", |
| 243 | + ], |
| 244 | + } |
| 245 | + |
| 246 | + |
171 | 247 | def report_strategy_profile_derivation_drift(config: dict, strategy_catalog: object | None = None) -> list[str]: |
172 | 248 | """Report whether the generated strategy profile catalog drifts from platform-config.json.""" |
173 | 249 | expected = strategy_to_json_compat(config.get("strategies", {})) |
@@ -209,6 +285,7 @@ def build_platform_health_report( |
209 | 285 | default_profile_errors = report_default_strategy_profile_drift(config, catalog) |
210 | 286 | derivation_errors = report_strategy_profile_derivation_drift(config, catalog) |
211 | 287 | live_candidate_queue = build_live_candidate_queue(catalog) |
| 288 | + automation_registry = build_strategy_automation_registry(config) |
212 | 289 | runtime_enabled_profiles = [ |
213 | 290 | profile |
214 | 291 | for profile, strategy in catalog.items() |
@@ -264,8 +341,10 @@ def build_platform_health_report( |
264 | 341 | "strategy_profile_count": len(catalog), |
265 | 342 | "runtime_enabled_switchable_count": len(runtime_enabled_profiles), |
266 | 343 | "live_candidate_queue_count": len(live_candidate_queue), |
| 344 | + "automation_lane_counts": automation_registry["summary"]["lane_counts"], |
267 | 345 | }, |
268 | 346 | "live_candidate_queue": live_candidate_queue, |
| 347 | + "automation_registry": automation_registry, |
269 | 348 | "codex_repair_context": { |
270 | 349 | "safe_to_attempt": bool(failed_checks), |
271 | 350 | "scope": "QuantRuntimeSettings platform-config and generated strategy switch assets", |
@@ -503,13 +582,17 @@ def main() -> int: |
503 | 582 | parser.add_argument("--check", action="store_true", help="Only validate config, don't write files") |
504 | 583 | parser.add_argument("--live-candidate-queue", action="store_true", help="Print live-candidate queue JSON and exit") |
505 | 584 | parser.add_argument("--platform-health-report", action="store_true", help="Print platform health report JSON and exit") |
| 585 | + parser.add_argument("--automation-registry", action="store_true", help="Print strategy automation registry JSON and exit") |
506 | 586 | args = parser.parse_args() |
507 | 587 |
|
508 | 588 | config = load_config() |
509 | 589 | if args.platform_health_report: |
510 | 590 | report = build_platform_health_report(config) |
511 | 591 | print(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)) |
512 | 592 | return 0 if report["status"] != "unhealthy" else 1 |
| 593 | + if args.automation_registry: |
| 594 | + print(json.dumps(build_strategy_automation_registry(config), ensure_ascii=False, indent=2, sort_keys=True)) |
| 595 | + return 0 |
513 | 596 |
|
514 | 597 | errors = validate(config) |
515 | 598 | if args.check: |
|
0 commit comments