import os
import sys
import json
import math
import copy
import re
import signal
import pathlib
import subprocess
import hashlib
import atexit
from src.flock_compat import flock, LOCK_EX, LOCK_SH, LOCK_UN, LOCK_NB
import psutil
import threading
from contextlib import contextmanager
def is_process_running(pid: int) -> bool:
    import psutil
    try:
        p = psutil.Process(pid)
        if not p.is_running() or p.status() == psutil.STATUS_ZOMBIE:
            return False
        cmdline = " ".join(p.cmdline()).lower()
        try:
            exe = str(p.exe()).lower()
        except Exception:
            exe = ""
        combined = f"{exe} {cmdline}"
        if (
            "--daemon" in combined
            or "gold_trader.py" in combined
            or "src/pipeline.py" in combined
            or "pipeline.py" in combined
            or "goldautotradeengine" in combined
            or "lobsterquant" in combined
        ):
            return True
        return False
    except psutil.NoSuchProcess:
        return False
    except Exception:
        # Fallback to os.kill if psutil fails for some reason
        try:
            import os
            os.kill(pid, 0)
            return True
        except OSError:
            return False

import asyncio
import datetime
import time
import logging
import psutil
import uuid
import urllib.request
from contextlib import asynccontextmanager

from fastapi import FastAPI, BackgroundTasks, HTTPException, Request, Body, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Set, Optional, Union

# 禁用 Uvicorn 默认的访问日志输出,防止刷屏
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)


# 添加当前目录到 sys.path,以便能引用 bridge 和 config_manager
sys.path.insert(0, str(pathlib.Path(__file__).parent))

import config_manager
import updater as app_updater
from src.subprocess_util import DAEMON_SUBPROCESS_FLAGS, popen as _popen_safe, run as _run_safe
from src.market_context import is_defensive_regime, is_range_regime, normalize_regime_label
from src.agent_decision_contract import (
    load_agent_decision_contract,
    update_decision_policy_contract,
)
from src.diagnostics_bundle import create_diagnostic_bundle
from src.mt5_execution import export_history_via_ea
from src.position_lifecycle import load_position_lifecycle
from src.runtime_state.decision_snapshot import load_decision_snapshot
from src.runtime_state.training_review_state import (
    build_training_review_state,
    public_training_review_state,
)
from bridge.trader_api import (
    load_state,
    get_price,
    get_price_truth,
    get_agent_direction,
    get_technical_trend_bias,
    is_mt5_running,
    DAEMON_LOG,
)


def _run_mt5_bridge_autodeploy(*, include_deep_scan: bool = False) -> None:
    try:
        from src.startup_gate import ensure_mt5_bridge_ea_ready

        for check in ensure_mt5_bridge_ea_ready(include_deep_scan=include_deep_scan):
            if str(check.get("check", "")) != "mt5_bridge/ea_autodeploy":
                continue
            status = str(check.get("status", "")).upper()
            detail = str(check.get("detail", "") or check.get("reason", "")).strip()
            metadata = check.get("metadata", {}) if isinstance(check.get("metadata"), dict) else {}
            copied = metadata.get("copied", []) if isinstance(metadata.get("copied"), list) else []

            if status == "PASS" and copied:
                _record_system_message(
                    kind="mt5_bridge",
                    title="MT5桥接EA已自动同步",
                    desc=detail or "已自动复制桥接EA到 MT5 Experts 目录。",
                    color="success",
                    source="mt5_bridge_autodeploy",
                    metadata={"check": check},
                )
            elif status == "FAIL":
                _record_system_message(
                    kind="mt5_bridge",
                    title="MT5桥接EA自动同步失败",
                    desc=detail or "自动复制桥接EA失败，请检查 MT5 数据目录权限。",
                    color="danger",
                    source="mt5_bridge_autodeploy",
                    metadata={"check": check},
                )
    except Exception as exc:
        _record_system_message(
            kind="mt5_bridge",
            title="MT5桥接EA自动同步异常",
            desc=str(exc),
            color="warning",
            source="mt5_bridge_autodeploy",
        )


_EVOLUTION_INTERVAL_SEC: float = 604800.0  # 7 days
_LAST_EVOLUTION_TS: float = 0.0

def _evolution_watch_worker() -> None:
    global _LAST_EVOLUTION_TS
    while not _TRADING_SAFETY_PATROL_STOP_EVENT.is_set():
        if _LAST_EVOLUTION_TS > 0 and time.time() - _LAST_EVOLUTION_TS < _EVOLUTION_INTERVAL_SEC:
            time.sleep(3600)
            continue
        try:
            from src.evolution_pipeline import run_evolution_cycle as _run_ev_cycle
            _result = _run_ev_cycle()
            _LAST_EVOLUTION_TS = time.time()
            import logging as _log
            _log.getLogger("gui_backend").info(f"[evolution] cycle complete: {_result.get('summary', 'ok')}")
        except Exception:
            pass
        time.sleep(3600)

def _start_evolution_watcher() -> None:
    _t = threading.Thread(target=_evolution_watch_worker, daemon=True, name="gui-evolution-watcher")
    _t.start()


@asynccontextmanager
async def _app_lifespan(_: FastAPI):
    _run_mt5_bridge_autodeploy()
    _startup_supervisor()
    _ensure_trading_safety_patrol_thread()
    # Auto-migrate to unified strategy state on startup
    try:
        from src.unified_state_migration import ensure_unified_state
        ensure_unified_state()
        import logging as _log
        _log.getLogger("gui_backend").info("[lifespan] Unified strategy state ready")
    except Exception:
        import logging as _log2
        _log2.getLogger("gui_backend").warning("[lifespan] Unified state migration skipped — will fall back to legacy")
    # Ensure LLM provider routes are mounted (retry if src.llm import happened before app creation)
    try:
        from src.llm import mount_provider_routes
        mounted = mount_provider_routes()
        if not mounted:
            logger.warning("[lifespan] Provider routes NOT mounted — app may not have been available at import time")
        else:
            logger.info("[lifespan] Provider routes mounted successfully")
    except Exception:
        pass
    yield
    _TRADING_SAFETY_PATROL_STOP_EVENT.set()


app = FastAPI(lifespan=_app_lifespan)

# 允许跨域(本地开发和混合部署时都需要)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# WebSocket real-time push (替代前端轮询)
try:
    from src.ws_push import ws_router, ws_manager as _ws_manager
    app.include_router(ws_router)
    _WS_AVAILABLE = True
except Exception:
    _WS_AVAILABLE = False
    _ws_manager = None

    _start_evolution_watcher()
# i18n language support — serve locale files + translate API
_SUP_LANGS = {"zh-CN": {"name": "简体中文", "name_en": "Simplified Chinese", "dir": "ltr"}}
_I18N_READY = False
_LM = None
try:
    from src.i18n.manager import get_lang_manager as _get_lm, SUPPORTED_LANGUAGES as _SL
    _SUP_LANGS = _SL
    _LM = _get_lm()
    _I18N_READY = True
except Exception:
    pass
    _SUP_LANGS = {"zh-CN": {"name": "简体中文", "dir": "ltr"}}

# 获取动态路径
APP_DIR = config_manager.get_app_dir()
DATA_DIR = config_manager.get_data_dir()
LOGS_DIR = config_manager.get_logs_dir()
CONFIG_DIR = config_manager.get_config_dir()
_BACKEND_START_TS = time.time()

# PyInstaller 打包后 __file__ 指向只读的临时目录，需要使用可写目录
if getattr(sys, 'frozen', False):
    REPO_ROOT = pathlib.Path(sys.executable).parent
else:
    REPO_ROOT = pathlib.Path(__file__).resolve().parent
RELEASE_ROOT = REPO_ROOT
_SRC_DIR = RELEASE_ROOT / "src" if (RELEASE_ROOT / "src").is_dir() else APP_DIR / "src"
_GUI_LOCK_HANDLE = None
_STRATEGY_STATE_LOCK = threading.RLock()
_POSITION_MONITOR_HISTORY_LOCK = threading.Lock()
_POSITION_MONITOR_HISTORY: list[str] = []


def _isoformat_mtime(path: pathlib.Path | None) -> str:
    if path is None or not path.exists():
        return ""
    try:
        return datetime.datetime.fromtimestamp(
            path.stat().st_mtime,
            tz=datetime.timezone.utc,
        ).astimezone().isoformat(timespec="seconds")
    except Exception:
        return ""


def _resolve_runtime_app_version() -> tuple[str, pathlib.Path | None]:
    candidates = [
        APP_DIR / "VERSION",
        RELEASE_ROOT / "VERSION",
        pathlib.Path(__file__).resolve().parent / "VERSION",
    ]
    for candidate in candidates:
        try:
            if candidate.exists():
                version = candidate.read_text(encoding="utf-8").strip()
                if version:
                    return version, candidate
        except Exception:
            continue
    return "1.0.0", None


def _resolve_runtime_frontend_dist() -> pathlib.Path:
    runtime_frontend_dist = globals().get("FRONTEND_DIST")
    if isinstance(runtime_frontend_dist, pathlib.Path):
        return runtime_frontend_dist
    candidates = [
        RELEASE_ROOT / "gui_react" / "dist",
        APP_DIR / "gui_react" / "dist",
        pathlib.Path(__file__).resolve().parent / "gui_react" / "dist",
    ]
    return next((path for path in candidates if path.is_dir()), candidates[0])


def _parse_frontend_entry_assets(frontend_dist: pathlib.Path) -> dict[str, Any]:
    index_file = frontend_dist / "index.html"
    info: dict[str, Any] = {
        "distPath": str(frontend_dist),
        "indexPath": str(index_file),
        "indexExists": bool(index_file.exists()),
        "indexMtime": _isoformat_mtime(index_file),
        "entryAsset": "",
        "cssAsset": "",
        "entryAssetPath": "",
        "entryAssetMtime": "",
        "cssAssetPath": "",
        "cssAssetMtime": "",
    }
    if not index_file.exists():
        return info
    try:
        shell = index_file.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return info
    script_match = re.search(r'src="/assets/([^"]+)"', shell)
    css_match = re.search(r'href="/assets/([^"]+\.css)"', shell)
    entry_asset = script_match.group(1).strip() if script_match else ""
    css_asset = css_match.group(1).strip() if css_match else ""
    info["entryAsset"] = entry_asset
    info["cssAsset"] = css_asset
    if entry_asset:
        entry_path = frontend_dist / "assets" / entry_asset
        info["entryAssetPath"] = str(entry_path)
        info["entryAssetMtime"] = _isoformat_mtime(entry_path)
    if css_asset:
        css_path = frontend_dist / "assets" / css_asset
        info["cssAssetPath"] = str(css_path)
        info["cssAssetMtime"] = _isoformat_mtime(css_path)
    return info


def _build_runtime_build_info_payload() -> dict[str, Any]:
    app_version, version_path = _resolve_runtime_app_version()
    frontend_dist = _resolve_runtime_frontend_dist()
    frontend_info = _parse_frontend_entry_assets(frontend_dist)
    runtime_family = ("", "")
    try:
        runtime_family = _current_runtime_family_token()
    except Exception:
        runtime_family = ("", "")
    fingerprint_seed = "|".join(
        [
            app_version,
            str(frontend_info.get("entryAsset", "") or ""),
            str(frontend_info.get("cssAsset", "") or ""),
            str(APP_DIR),
        ]
    )
    return {
        "ok": True,
        "backend": {
            "appVersion": app_version,
            "versionFile": str(version_path) if version_path else "",
            "versionFileMtime": _isoformat_mtime(version_path),
            "runtimeMode": "frozen" if getattr(sys, "frozen", False) else "source",
            "pid": os.getpid(),
            "startedAt": datetime.datetime.fromtimestamp(
                _BACKEND_START_TS,
                tz=datetime.timezone.utc,
            ).astimezone().isoformat(timespec="seconds"),
            "uptimeSec": max(0, int(time.time() - _BACKEND_START_TS)),
            "appDir": str(APP_DIR),
            "releaseRoot": str(RELEASE_ROOT),
        },
        "frontend": frontend_info,
        "runtime": {
            "guiPort": int(os.environ.get("GUI_PORT", "18790") or 18790),
            "runtimeFamily": str(runtime_family[0] or ""),
            "runtimeRevision": str(runtime_family[1] or ""),
            "fingerprint": hashlib.sha256(fingerprint_seed.encode("utf-8")).hexdigest()[:12],
        },
    }


def _acquire_gui_singleton_lock(lock_dir: Optional[pathlib.Path] = None):
    global _GUI_LOCK_HANDLE
    """Use a Windows kernel mutex for reliable single-instance detection.

    A kernel mutex is auto-released by the OS when the process exits
    (even on crash), so there is NEVER a stale lock. No popup, no file
    cleanup, no false positives — just silent exit if already running.
    """
    global _GUI_LOCK_HANDLE
    import ctypes as _ctypes
    from ctypes import wintypes as _wintypes

    _MUTEX_NAME = "Global\\GoldAutoTrade_Singleton_v1.0.15"
    _ERROR_ALREADY_EXISTS = 183

    _k32 = _ctypes.windll.kernel32
    _k32.CreateMutexW.argtypes = [_wintypes.LPVOID, _wintypes.BOOL, _wintypes.LPCWSTR]
    _k32.CreateMutexW.restype = _wintypes.HANDLE
    _k32.GetLastError.restype = _wintypes.DWORD

    _h = _k32.CreateMutexW(None, False, _MUTEX_NAME)
    if _k32.GetLastError() == _ERROR_ALREADY_EXISTS:
        if _h:
            _k32.CloseHandle(_h)
        raise SystemExit(0)
    _GUI_LOCK_HANDLE = _h


def _release_gui_singleton_lock() -> None:
    global _GUI_LOCK_HANDLE
    import ctypes as _ctypes
    _h = _GUI_LOCK_HANDLE
    _GUI_LOCK_HANDLE = None
    if _h is not None:
        _ctypes.windll.kernel32.CloseHandle(_h)


def _write_json_atomic(path: pathlib.Path, payload: dict[str, Any]) -> pathlib.Path:
    from src.atomic_write import atomic_write_json
    atomic_write_json(path, payload)
    # Periodic cleanup of stale tmp files older than 2 minutes
    try:
        import time as _t
        _now = _t.time()
        for f in path.parent.glob(f".{path.name}.*.tmp"):
            if _now - f.stat().st_mtime > 120:
                f.unlink(missing_ok=True)
    except Exception:
        pass
    return path


def _append_ndjson_line(path: pathlib.Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as handle:
        handle.write(json.dumps(payload, ensure_ascii=False) + "\n")


@contextmanager
def _strategy_market_state_transaction(shared: bool = False):
    lock_path = CONFIG_DIR / "strategy_market_state.lock"
    lock_path.parent.mkdir(parents=True, exist_ok=True)
    with _STRATEGY_STATE_LOCK:
        handle = lock_path.open("a+", encoding="utf-8")
        lock_type = LOCK_SH if shared else LOCK_EX
        try:
            handle.seek(0)
            flock(handle.fileno(), lock_type)
            yield
        finally:
            try:
                handle.seek(0)
                flock(handle.fileno(), LOCK_UN)
            except Exception:
                pass
            handle.close()


class UpdateRequest(BaseModel):
    manifest: Optional[str] = None
    force: bool = False


class UpdateConfigRequest(BaseModel):
    update_manifest_url: str = ""


class DiagnosticBundleRequest(BaseModel):
    note: str = ""
    label: str = "manual"


# ── 策略定制模块 Models ────────────────────────────────────────────────

class StrategyParamUpdate(BaseModel):
    key: str
    value: Any


class StrategyParamsBatchUpdate(BaseModel):
    strategy_id: str
    params: List[StrategyParamUpdate]
    change_note: str = ""
    bypass_paper_mode: bool = False
    bypass_runtime_safety: bool = False


class StrategyCloneRequest(BaseModel):
    strategy_id_from: str
    new_name: str
    change_note: str = ""


class AgentModelUpdateRequest(BaseModel):
    role: str = ""
    model: str = ""


class StrategyToggleRequest(BaseModel):
    strategy_id: str
    enabled: bool


class StrategyDeleteRequest(BaseModel):
    strategy_id: str


class StrategyPresetSave(BaseModel):
    name: str
    description: str = ""
    strategy_id: str
    params_snapshot: Optional[Dict[str, Any]] = None


# ── 策略定制缓存 ────────────────────────────────────────────────────

_STRATEGY_CUSTOM_STATE_CACHE: Dict[str, Any] = {"ts": 0.0, "payload": None}
_STRATEGY_CUSTOM_STATE_CACHE_TTL_SEC = 2.0

_PARAMS_SCHEMA_CACHE: Optional[List[Dict[str, Any]]] = None
_PARAMS_SCHEMA_CACHE_FAILED = False
_STRATEGY_CUSTOM_CALL_LOG_LIMIT = 12


def _load_params_schema() -> list:
    global _PARAMS_SCHEMA_CACHE, _PARAMS_SCHEMA_CACHE_FAILED
    if _PARAMS_SCHEMA_CACHE is None and not _PARAMS_SCHEMA_CACHE_FAILED:
        try:
            from src.strategy_params_schema import (
                export_schema_for_frontend,
                get_params_by_group,
                get_all_groups_sorted,
                validate_params_batch,
                compute_params_summary,
                get_param_def,
            )
            _PARAMS_SCHEMA_CACHE = export_schema_for_frontend()
        except Exception:
            _PARAMS_SCHEMA_CACHE_FAILED = True
    return _PARAMS_SCHEMA_CACHE or []


def _load_strategy_market_state() -> dict:
    """Delegates to unified_strategy_state module — single source of truth."""
    try:
        from src.unified_strategy_state import load_unified_state, instances_as_list
        unified = load_unified_state()
        return {
            "strategies": unified.get("strategies", {}),
            "runtime_instances": instances_as_list(unified),
            "decision_policy": unified.get("decision_policy", {}),
            "scheduler": unified.get("scheduler", {}),
            "updated_at": unified.get("updated_at", ""),
        }
    except Exception:
        # Fallback for boot before migration
        path = _resolve_runtime_artifact("config", "strategy_market_state.json") or (CONFIG_DIR / "strategy_market_state.json")
        try:
            return json.loads(path.read_text(encoding="utf-8"))
        except Exception:
            return {"strategies": {}, "runtime_instances": [], "decision_policy": {}}


def _save_strategy_market_state(state: dict) -> None:
    """Delegates to unified_strategy_state module — single source of truth."""
    try:
        from src.unified_strategy_state import save_unified_state, load_unified_state, instances_as_list
        # Preserve existing unified data, apply changes from state dict
        try:
            unified = load_unified_state(use_cache=False)
        except Exception:
            unified = {}
        if "strategies" in state:
            unified["strategies"] = state["strategies"]
        if "runtime_instances" in state:
            # Convert list back to dict, preserving instance_id keys
            instances = {}
            for inst in (state.get("runtime_instances") or []):
                if isinstance(inst, dict):
                    iid = str(inst.get("instance_id", "")).strip()
                    if iid:
                        instances[iid] = inst
            unified["instances"] = instances
        if "decision_policy" in state:
            unified["decision_policy"] = state["decision_policy"]
        if "scheduler" in state:
            unified["scheduler"] = state["scheduler"]
        unified["updated_at"] = state.get("updated_at", "")
        save_unified_state(unified)
    except Exception:
        # Fallback: direct write to legacy file
        path = CONFIG_DIR / "strategy_market_state.json"
        state["updated_at"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
        _write_json_atomic(path, state)


def _load_strategy_custom_state() -> dict:
    """Redirect to v2 loader — deduplicated to single definition."""
    return _load_strategy_custom_state_v2__impl()


__load_strategy_custom_state_placeholder = None  # prevent runtime NameError from removed duplicate


def _save_strategy_custom_state(state: dict) -> None:
    """Redirect to v2 saver — deduplicated to single definition."""
    _save_strategy_custom_state_v2__impl(state)


def _sync_generated_history_entry(
    strategy_id: str,
    *,
    row: Optional[dict[str, Any]] = None,
    lifecycle_stage: str = "",
) -> None:
    strategy_key = str(strategy_id or "").strip()
    if not strategy_key:
        return
    payload = _load_strategy_custom_state()
    history = payload.get("generatedHistory", [])
    if not isinstance(history, list) or not history:
        return

    market_row = row if isinstance(row, dict) else {}
    next_stage = str(
        lifecycle_stage
        or market_row.get("lifecycle_stage", "")
        or "marketplace"
    ).strip().lower() or "marketplace"
    review_status = str(
        market_row.get("review_status", "")
        or "pending"
    ).strip().lower() or "pending"
    promotion_state = (
        copy.deepcopy(market_row.get("promotion_state", {}))
        if isinstance(market_row.get("promotion_state", {}), dict)
        else {}
    )
    if not isinstance(promotion_state, dict):
        promotion_state = {}
    promotion_state["stage"] = next_stage
    promotion_state["review_status"] = review_status

    changed = False
    for item in history:
        if not isinstance(item, dict):
            continue
        if str(item.get("strategy_id", "") or "").strip() != strategy_key:
            continue
        if str(item.get("lifecycle_stage", "") or "") != next_stage:
            item["lifecycle_stage"] = next_stage
            changed = True
        if str(item.get("review_status", "") or "") != review_status:
            item["review_status"] = review_status
            changed = True
        if item.get("promotion_state") != promotion_state:
            item["promotion_state"] = promotion_state
            changed = True
        break

    if changed:
        _save_strategy_custom_state(payload)


def _remove_generated_history_entry(strategy_id: str) -> None:
    strategy_key = str(strategy_id or "").strip()
    if not strategy_key:
        return
    payload = _load_strategy_custom_state()
    history = payload.get("generatedHistory", [])
    if not isinstance(history, list) or not history:
        return
    next_history = [
        item for item in history
        if str(item.get("strategy_id", "") or "").strip() != strategy_key
    ]
    if len(next_history) == len(history):
        return
    payload["generatedHistory"] = next_history
    _save_strategy_custom_state(payload)


def _load_strategy_rules_v3() -> dict:
    path = CONFIG_DIR / "strategy_rules_v3.json"
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def _save_strategy_rules_v3(rules: dict) -> None:
    path = CONFIG_DIR / "strategy_rules_v3.json"
    rules["last_updated"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    _write_json_atomic(path, rules)


def _default_strategy_template(strategy_id: str) -> dict:
    return {
        "timeframe": "M5",
        "risk_multiplier": 1.0,
        "sl_multiplier": 1.0,
        "tp_multiplier": 1.0,
        "strategy_id": strategy_id,
    }


def _resolve_strategy_params(strategy_id: str) -> dict:
    """将当前规则文件展平为 key->value 字典，供前端展示编辑。"""
    rules = _load_strategy_rules_v3()
    state = _load_strategy_market_state()
    strat = state.get("strategies", {}).get(strategy_id, {})
    template = strat.get("template", {}) if isinstance(strat, dict) else {}

    flat: Dict[str, Any] = {}

    def _flatten(prefix: str, obj: Any) -> None:
        if isinstance(obj, dict) and not any(
            isinstance(v, (dict, list)) for v in obj.values()
        ):
            for k, v in obj.items():
                flat[f"{prefix}.{k}"] = v
        elif isinstance(obj, dict):
            for k, v in obj.items():
                _flatten(f"{prefix}.{k}", v)
        else:
            flat[prefix] = obj

    _flatten("trend_vote_model", rules.get("trend_vote_model", {}))
    _flatten("entry_rules", rules.get("entry_rules", {}))
    _flatten("entry_direction_filter", rules.get("entry_direction_filter", {}))
    _flatten("exit_strategy", rules.get("exit_strategy", {}))
    _flatten("risk_governor", rules.get("risk_governor", {}))
    _flatten("position_sizing", rules.get("position_sizing", {}))

    # Always expose the normalized decision policy so newly added contract
    # fields show up in the parameter panel even before the first manual save.
    dp = _normalize_decision_policy(state.get("decision_policy", {}) if isinstance(state, dict) else {})
    _flatten("decision_policy", dp)

    # Strategy template params
    for k, v in template.items():
        flat[f"strategy.{k}"] = v

    return flat


def _apply_param_update(base: dict, dotted_key: str, value: Any) -> dict:
    """将 dotted key 写回到嵌套 dict 中。返回修改后的 dict（原地修改）。"""
    parts = dotted_key.split(".")
    current = base
    for i, part in enumerate(parts[:-1]):
        if part.isdigit():
            part_idx = int(part)
            if not isinstance(current, list):
                current = []
            while len(current) <= part_idx:
                current.append({})
            current = current[part_idx]
        else:
            if part not in current or not isinstance(current[part], (dict, list)):
                current[part] = {}
            current = current[part]
    last = parts[-1]
    if last.isdigit():
        last_idx = int(last)
        if not isinstance(current, list):
            current = []
        while len(current) <= last_idx:
            current.append(None)
        current[last_idx] = value
    else:
        current[last] = value
    return base


def _apply_params_to_rules(rules: dict, state: dict, params: Dict[str, Any], strategy_id: str = "") -> tuple[dict, dict]:
    """将展平参数写回到 rules v3 和 state 的正确位置。"""
    for key, value in params.items():
        if key.startswith("decision_policy."):
            dp_key = key[len("decision_policy."):]
            dp = state.setdefault("decision_policy", {})
            _apply_param_update(dp, dp_key, value)
        elif key.startswith("strategy."):
            sk = key[len("strategy."):]
            if strategy_id:
                template = state.setdefault("strategies", {}).setdefault(strategy_id, {}).setdefault("template", {})
                template[sk] = value
        else:
            _apply_param_update(rules, key, value)
    if isinstance(state.get("decision_policy", {}), dict):
        state["decision_policy"] = _normalize_decision_policy(state.get("decision_policy", {}))
    return rules, state


# ── 策略定制 API ────────────────────────────────────────────────────

@app.get("/api/strategy/params-schema")
def get_params_schema():
    """返回所有可调参数的 schema 元数据 + 分组信息。"""
    try:
        from src.strategy_params_schema import (
            export_schema_for_frontend,
            get_params_by_group,
            get_all_groups_sorted,
        )
        schema = export_schema_for_frontend()
        grouped = {}
        for group_meta in get_all_groups_sorted():
            gk = group_meta["key"]
            grouped[gk] = {
                **group_meta,
                "params": [p for p in schema if p["group"] == gk],
            }
        return {
            "success": True,
            "schema": schema,
            "groups": grouped,
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.get("/api/strategy/params/{strategy_id}")
def get_strategy_params(strategy_id: str):
    """获取指定策略的所有当前参数值（已展平）。"""
    try:
        flat = _resolve_strategy_params(strategy_id)
        if not flat:
            return {"success": False, "error": f"未知策略: {strategy_id}"}
        return {"success": True, "strategy_id": strategy_id, "params": flat}
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.post("/api/strategy/params/validate")
def validate_strategy_params(req: StrategyParamsBatchUpdate):
    """校验参数值是否合法。不保存，只返回校验结果。"""
    try:
        from src.strategy_params_schema import validate_params_batch, compute_params_summary

        params_dict = {p.key: p.value for p in req.params}
        errors = validate_params_batch(params_dict)
        summary = compute_params_summary(params_dict)
        return {
            "success": len(errors) == 0,
            "errors": errors,
            "summary": summary,
            "param_count": len(params_dict),
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.post("/api/strategy/params/apply")
def apply_strategy_params(req: StrategyParamsBatchUpdate):
    """应用参数更新。自动通过 validation 和 paper mode 闸门。"""
    try:
        from src.strategy_params_schema import validate_params_batch, compute_params_summary, get_param_def as _get_param_def

        params_dict = {p.key: p.value for p in req.params}
        errors = validate_params_batch(params_dict)
        if errors:
            return {
                "success": False,
                "error": "参数校验未通过",
                "validation_errors": errors,
            }

        summary = compute_params_summary(params_dict)

        # Paper mode gate: critical/high-risk params require paper mode first
        if summary["requires_paper_mode"] and not req.bypass_paper_mode:
            return {
                "success": False,
                "error": "修改包含关键风控参数，必须先进入 Paper Mode 运行 3 天验证",
                "requires_paper_mode": True,
                "paper_mode_required_for": [
                    p.key for p in req.params
                    if _get_param_def(p.key) and _get_param_def(p.key).requires_paper_mode
                ],
                "summary": summary,
            }

        # Runtime safety gate (P1): block cold params when engine is busy / positions open
        if not req.bypass_runtime_safety:
            from src.strategy_params_schema import runtime_safety_check as _safety_check
            es = _runtime_execution_state_payload()
            engine_busy = bool(es.get("isTrading", False) or es.get("bridgeActive", False))
            has_positions = bool(es.get("positionOpen", False) or
                                 (isinstance(es.get("positions"), list) and len(es.get("positions", [])) > 0))
            safety = _safety_check(params_dict, engine_busy=engine_busy, has_positions=has_positions)
            if safety.get("block"):
                return {
                    "success": False,
                    "error": "参数运行时安全锁已激活",
                    "runtime_safety": {
                        "blocked": safety["block"],
                        "warnings": safety.get("warn", []),
                        "engine_busy": engine_busy,
                        "has_positions": has_positions,
                    },
                }

        rules = _load_strategy_rules_v3()
        state = _load_strategy_market_state()
        rules, state = _apply_params_to_rules(rules, state, params_dict, strategy_id=req.strategy_id)
        decision_policy_touched = any(key.startswith("decision_policy.") for key in params_dict)

        # Version the strategy
        strat = state.get("strategies", {}).get(req.strategy_id)
        if isinstance(strat, dict):
            versions = strat.get("versions", [])
            if not isinstance(versions, list):
                versions = []
            cur_ver = str(strat.get("current_version", "1.0.0"))
            try:
                parts = cur_ver.split(".")
                new_ver = f"{parts[0]}.{parts[1]}.{int(parts[2]) + 1}"
            except Exception:
                new_ver = "1.0.1"
            now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
            versions.append({
                "version": new_ver,
                "note": req.change_note or "manual_params_update",
                "published_at": now_iso,
                "params_changed": list(params_dict.keys()),
                "summary": summary,
            })
            strat["current_version"] = new_ver
            strat["versions"] = versions

        _save_strategy_rules_v3(rules)
        if decision_policy_touched:
            update_decision_policy_contract(state.get("decision_policy", {}), source="strategy_params")
        _sync_runtime_scheduler_to_effective(state)
        _save_strategy_market_state(state)

        return {
            "success": True,
            "message": f"策略参数已更新 ({len(params_dict)} 项)",
            "strategy_id": req.strategy_id,
            "new_version": (
                state.get("strategies", {}).get(req.strategy_id, {}).get("current_version", "")
            ),
            "summary": summary,
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.post("/api/strategy/clone")
def clone_strategy(req: StrategyCloneRequest):
    """克隆已有策略为新策略，保留所有参数配置。"""
    try:
        state = _load_strategy_market_state()
        src = state.get("strategies", {}).get(req.strategy_id_from)
        if not isinstance(src, dict):
            return {"success": False, "error": f"源策略不存在: {req.strategy_id_from}"}

        slug = "".join(ch.lower() if ch.isalnum() else "_" for ch in req.new_name).strip("_")[:28]
        new_id = f"clone_{slug}_{int(time.time())}"
        now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")

        import copy
        new_strat = copy.deepcopy(src)
        new_strat["display_name"] = req.new_name
        new_strat["is_custom"] = True
        new_strat["cloned_from"] = req.strategy_id_from
        new_strat["versions"] = [{
            "version": "1.0.0",
            "note": f"cloned from {req.strategy_id_from}",
            "published_at": now_iso,
        }]
        new_strat["current_version"] = "1.0.0"
        new_strat["created_at"] = now_iso
        # 更新 template 中的 strategy_id
        if isinstance(new_strat.get("template"), dict):
            new_strat["template"]["strategy_id"] = new_id

        state["strategies"][new_id] = new_strat
        _save_strategy_market_state(state)
        return {
            "success": True,
            "message": f"策略已克隆: {req.new_name}",
            "strategy_id": new_id,
            "version": "1.0.0",
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.post("/api/strategy/toggle")
def toggle_strategy(req: StrategyToggleRequest):
    """启用/禁用策略。"""
    state = _load_strategy_market_state()
    strat = state.get("strategies", {}).get(req.strategy_id)
    if not isinstance(strat, dict):
        return {"success": False, "error": f"策略不存在: {req.strategy_id}"}
    strat["enabled"] = bool(req.enabled)
    _save_strategy_market_state(state)
    return {
        "success": True,
        "strategy_id": req.strategy_id,
        "enabled": req.enabled,
    }


@app.post("/api/strategy/delete")
def delete_strategy(req: StrategyDeleteRequest):
    """删除策略草稿/未进入正式流转的自定义策略。"""
    state = _load_strategy_market_state()
    strat = state.get("strategies", {}).get(req.strategy_id)
    if not isinstance(strat, dict):
        return {"success": False, "error": f"策略不存在: {req.strategy_id}"}
    if strat.get("default_mode") == "builtin" and not strat.get("is_custom"):
        return {"success": False, "error": "内置策略不可删除"}
    lifecycle_stage = str(strat.get("lifecycle_stage", "") or "marketplace").strip().lower()
    review_status = str(strat.get("review_status", "") or "pending").strip().lower()
    if lifecycle_stage == "marketplace" and review_status in {"pending", "needs_revision", "rejected"}:
        return {
            "success": False,
            "error": "待审核策略请使用待审核删除入口，避免绕过审核治理规则",
        }
    if lifecycle_stage in {"paper", "canary", "full"}:
        return {"success": False, "error": "已进入运行链路的策略不可直接删除，请先撤出部署或回退"}
    del state["strategies"][req.strategy_id]
    # 清理关联的 runtime instances
    state["runtime_instances"] = [
        inst for inst in state.get("runtime_instances", [])
        if str(inst.get("strategy_id", "")) != req.strategy_id
    ]
    _save_strategy_market_state(state)
    _remove_generated_history_entry(req.strategy_id)
    return {
        "success": True,
        "message": f"策略已删除: {req.strategy_id}",
    }


@app.post("/api/strategy/delete-pending-review")
def delete_pending_review_strategy(req: StrategyDeleteRequest):
    """仅允许删除集市里仍处于待审核状态的自定义策略。"""
    strategy_id = str(req.strategy_id or "").strip()
    if not strategy_id:
        return {"success": False, "error": "缺少策略 ID"}
    state = _load_strategy_market_state()
    strategies = state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}
    row = strategies.get(strategy_id)
    if not isinstance(row, dict):
        return {"success": False, "error": f"策略不存在: {strategy_id}"}
    if row.get("default_mode") == "builtin" and not row.get("is_custom"):
        return {"success": False, "error": "内置策略不可删除"}
    lifecycle_stage = str(row.get("lifecycle_stage", "") or "marketplace").strip().lower()
    review_status = str(row.get("review_status", "") or "pending").strip().lower()
    if lifecycle_stage != "marketplace":
        return {"success": False, "error": "仅允许删除集市待审核中的策略"}
    if review_status == "approved":
        return {"success": False, "error": "审核已通过的策略不可删除"}
    runtime_instances = state.get("runtime_instances", [])
    if not isinstance(runtime_instances, list):
        runtime_instances = []
    if any(
        isinstance(inst, dict) and str(inst.get("strategy_id", "") or "").strip() == strategy_id
        for inst in runtime_instances
    ):
        return {"success": False, "error": "已有运行实例的策略不可删除"}
    del strategies[strategy_id]
    state["strategies"] = strategies
    state["runtime_instances"] = [
        inst for inst in runtime_instances
        if not (isinstance(inst, dict) and str(inst.get("strategy_id", "") or "").strip() == strategy_id)
    ]
    state["updated_at"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    _save_strategy_market_state(state)
    _remove_generated_history_entry(strategy_id)
    return {
        "success": True,
        "strategy_id": strategy_id,
        "message": f"已删除待审核策略: {strategy_id}",
    }


@app.get("/api/strategy/history/{strategy_id}")
def get_strategy_version_history(strategy_id: str):
    """获取策略的版本历史与变更记录。"""
    state = _load_strategy_market_state()
    strat = state.get("strategies", {}).get(strategy_id)
    if not isinstance(strat, dict):
        return {"success": False, "error": f"未知策略: {strategy_id}"}
    return {
        "success": True,
        "strategy_id": strategy_id,
        "current_version": strat.get("current_version", ""),
        "versions": strat.get("versions", []),
    }


@app.get("/api/strategy/compare")
def compare_strategy_params(strategy_id_a: str = "", strategy_id_b: str = ""):
    """比较两个策略的参数差异。"""
    if not strategy_id_a or not strategy_id_b:
        return {"success": False, "error": "需要提供两个策略 ID"}
    params_a = _resolve_strategy_params(strategy_id_a)
    params_b = _resolve_strategy_params(strategy_id_b)
    all_keys = set(params_a.keys()) | set(params_b.keys())
    diff = {}
    for key in sorted(all_keys):
        va = params_a.get(key)
        vb = params_b.get(key)
        if va != vb:
            diff[key] = {"a": va, "b": vb}
    return {
        "success": True,
        "strategy_a": strategy_id_a,
        "strategy_b": strategy_id_b,
        "diff_count": len(diff),
        "diff": diff,
    }


@app.get("/api/strategy/risk-budget")
def get_risk_budget():
    """获取当前策略风险预算分配与投票权重。"""
    try:
        from src.strategy_intelligence import get_intelligence_hub
        from pathlib import Path

        hub = get_intelligence_hub()
        state = _load_strategy_market_state()
        instances = state.get("runtime_instances", [])
        if not isinstance(instances, list):
            instances = []

        project_root = Path(APP_DIR)
        health_scores = hub.refresh_all(project_root, instances)
        voting_weights = hub.compute_voting_weights(health_scores)
        budget = hub.allocate_risk_budget(health_scores, total_budget_lots=0.20)

        return {
            "success": True,
            "health_scores": {k: v.to_dict() for k, v in health_scores.items()},
            "voting_weights": voting_weights,
            "risk_budget": budget,
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.get("/api/strategy/presets")
def list_strategy_presets():
    """列出所有已保存的策略预设模板。"""
    presets_dir = APP_DIR / "presets"
    if not presets_dir.exists():
        return {"success": True, "presets": []}
    presets = []
    for f in sorted(presets_dir.glob("*.json")):
        try:
            data = json.loads(f.read_text(encoding="utf-8"))
            presets.append({
                "id": f.stem,
                "name": data.get("name", f.stem),
                "description": data.get("description", ""),
                "strategy_id": data.get("strategy_id", ""),
                "saved_at": data.get("saved_at", ""),
            })
        except Exception:
            pass
    return {"success": True, "presets": presets}


@app.post("/api/strategy/presets/save")
def save_strategy_preset(req: StrategyPresetSave):
    """保存当前参数配置为预设模板。"""
    presets_dir = APP_DIR / "presets"
    presets_dir.mkdir(parents=True, exist_ok=True)
    params = req.params_snapshot or _resolve_strategy_params(req.strategy_id)
    payload = {
        "name": req.name,
        "description": req.description,
        "strategy_id": req.strategy_id,
        "params": params,
        "saved_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
    }
    slug = "".join(ch.lower() if ch.isalnum() else "_" for ch in req.name)[:40]
    path = presets_dir / f"{slug}.json"
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    return {"success": True, "message": f"预设已保存: {req.name}", "preset_id": slug}


@app.post("/api/strategy/presets/apply")
def apply_strategy_preset(preset_id: str = "", target_strategy_id: str = ""):
    """将预设模板应用到指定策略。"""
    if not preset_id or not target_strategy_id:
        return {"success": False, "error": "需要 preset_id 和 target_strategy_id"}
    path = APP_DIR / "presets" / f"{preset_id}.json"
    if not path.exists():
        return {"success": False, "error": f"预设不存在: {preset_id}"}
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
        params = data.get("params", {})
        if not isinstance(params, dict) or not params:
            return {"success": False, "error": "预设中无有效参数"}
    except Exception as e:
        return {"success": False, "error": f"读取预设失败: {e}"}

    rules = _load_strategy_rules_v3()
    state = _load_strategy_market_state()
    rules, state = _apply_params_to_rules(rules, state, params, strategy_id=target_strategy_id)
    _save_strategy_rules_v3(rules)
    _save_strategy_market_state(state)

    return {
        "success": True,
        "message": f"预设 {preset_id} 已应用到 {target_strategy_id}",
        "params_applied": len(params),
    }


_UPDATE_STATUS_CACHE: Dict[str, Any] = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
}
_TIMELINE_CACHE: Dict[str, Any] = {"ts": 0.0, "payload": None}
_TIMELINE_CACHE_TTL_SECONDS = 2.0
_TRAINING_AUTOPILOT_THREAD: Optional[threading.Thread] = None
_TRAINING_AUTOPILOT_CONTEXT = threading.local()


def _thread_is_alive(thread: Optional[threading.Thread]) -> bool:
    return bool(thread is not None and getattr(thread, "is_alive", lambda: False)())


def _find_alive_thread_by_name(thread_name: str) -> Optional[threading.Thread]:
    for thread in threading.enumerate():
        try:
            if thread.name == thread_name and thread.is_alive():
                return thread
        except Exception:
            continue
    return None


def _ensure_singleton_daemon_thread(
    *,
    holder_name: str,
    thread_name: str,
    target,
    args: tuple[Any, ...] = (),
) -> bool:
    current = globals().get(holder_name)
    if _thread_is_alive(current):
        return False
    existing = _find_alive_thread_by_name(thread_name)
    if existing is not None:
        globals()[holder_name] = existing
        return False
    thread = threading.Thread(
        target=target,
        args=args,
        daemon=True,
        name=thread_name,
    )
    globals()[holder_name] = thread
    thread.start()
    return True

# --- MT5 每日统计缓存 ---
# 为了避免每 5 秒前端轮询时都对 MT5 进行高强度的磁盘 IO 和历史导出，
# 我们将 MT5 的历史查询结果缓存在内存中。
# 更新机制：每 60 秒自动强制刷新一次；或者当检测到持仓数量发生变化（有单子平仓）时立即刷新。
_MT5_DAILY_CACHE = {
    "data": {
        "trades": 0, "pnl": 0.0, "wins": 0, "losses": 0, 
        "win_rate": 0.0, "avg_win": 0.0, "avg_loss": 0.0,
        "_ok": False, "_source": "init",
    },
    "updated_at": 0.0,
    "last_positions_count": -1  # 记录上一次查询时的持仓数量
}


def _system_messages_file() -> pathlib.Path:
    return LOGS_DIR / "system_messages.ndjson"


def _file_repair_events_file() -> pathlib.Path:
    return LOGS_DIR / "file_repair_events.ndjson"


def _system_message_candidates() -> list[pathlib.Path]:
    return _runtime_scope_candidates("logs", "system_messages.ndjson")


def _file_repair_event_candidates() -> list[pathlib.Path]:
    return _runtime_scope_candidates("logs", "file_repair_events.ndjson")


def _empty_trade_stats(*, ok: bool = False, source: str = "unknown") -> dict:
    return {
        "trades": 0,
        "pnl": 0.0,
        "wins": 0,
        "losses": 0,
        "win_rate": 0.0,
        "avg_win": 0.0,
        "avg_loss": 0.0,
        "_ok": ok,
        "_source": source,
    }


def _normalize_trade_stats(stats: Optional[dict], *, ok: bool = False, source: str = "unknown") -> dict:
    base = _empty_trade_stats(ok=ok, source=source)
    if not isinstance(stats, dict):
        return base
    normalized = dict(base)
    for key in ("trades", "wins", "losses"):
        try:
            normalized[key] = max(0, int(stats.get(key, normalized[key]) or 0))
        except Exception:
            normalized[key] = base[key]
    for key in ("pnl", "win_rate", "avg_win", "avg_loss"):
        try:
            normalized[key] = float(stats.get(key, normalized[key]) or 0.0)
        except Exception:
            normalized[key] = base[key]
    normalized["_ok"] = bool(stats.get("_ok", ok))
    normalized["_source"] = str(stats.get("_source", source) or source)
    return normalized


def _coerce_float(value: Any, default: float = 0.0) -> float:
    try:
        return float(value or 0.0)
    except Exception:
        return default


def _today_local_midnight_epoch() -> float:
    """返回本地时区今日 00:00:00 的 epoch 秒数（所有统计统一用此边界）"""
    return datetime.datetime.combine(
        datetime.datetime.now(datetime.timezone.utc).astimezone().date(),
        datetime.time.min,
    ).timestamp()


def _parse_trade_ledger_timestamp(value: Any) -> Optional[datetime.datetime]:
    if value is None or value == "":
        return None
    # Epoch number (int/float) — the format gold_trader.py writes
    if isinstance(value, (int, float)) and value > 1000000000:
        try:
            return datetime.datetime.fromtimestamp(float(value), tz=datetime.timezone.utc)
        except Exception:
            pass
    # String: try epoch first, then ISO
    text = str(value).strip()
    if not text:
        return None
    try:
        num = float(text)
        if num > 1000000000:
            return datetime.datetime.fromtimestamp(num, tz=datetime.timezone.utc)
    except ValueError:
        pass
    try:
        if text.endswith("Z"):
            text = text[:-1] + "+00:00"
        return datetime.datetime.fromisoformat(text)
    except Exception:
        return None


def _read_today_stats_from_trading_ledger(ledger_file: Optional[pathlib.Path]) -> dict:
    if ledger_file is None or not ledger_file.exists():
        return _empty_trade_stats(ok=False, source="trading_ledger_missing")

    stats = _empty_trade_stats(ok=True, source="trading_ledger")
    today_boundary = _today_local_midnight_epoch()
    pnl_sum = 0.0
    win_pnl = 0.0
    loss_pnl = 0.0
    by_strategy: dict[str, dict] = {}

    try:
        with ledger_file.open("r", encoding="utf-8") as handle:
            for line in handle:
                if not line.strip():
                    continue
                try:
                    row = json.loads(line)
                except Exception:
                    continue
                action = str(row.get("action", "CLOSE") or "CLOSE").upper()
                if action != "CLOSE":
                    continue
                status = str(row.get("status", "FILLED") or "FILLED").upper()
                if status not in {"", "FILLED", "RECONCILED"}:
                    continue
                ts = _parse_trade_ledger_timestamp(row.get("ts"))
                if ts is not None and ts.timestamp() < today_boundary:
                    continue
                try:
                    pnl = float(row.get("pnl", 0.0) or 0.0)
                except Exception:
                    continue
                stats["trades"] += 1
                pnl_sum += pnl
                if pnl > 0:
                    stats["wins"] += 1
                    win_pnl += pnl
                elif pnl < 0:
                    stats["losses"] += 1
                    loss_pnl += pnl
                strategy_id = str(row.get("strategy_id", row.get("strategy", "")) or "")
                if strategy_id:
                    s = by_strategy.setdefault(strategy_id, {"trades": 0, "pnl": 0.0, "wins": 0, "losses": 0})
                    s["trades"] += 1
                    s["pnl"] += pnl
                    if pnl > 0:
                        s["wins"] += 1
                    elif pnl < 0:
                        s["losses"] += 1
    except Exception:
        return _empty_trade_stats(ok=False, source="trading_ledger_error")

    stats["pnl"] = round(pnl_sum, 2)
    if stats["trades"] > 0:
        stats["win_rate"] = round((stats["wins"] / stats["trades"]) * 100, 1)
    if stats["wins"] > 0:
        stats["avg_win"] = round(win_pnl / stats["wins"], 2)
    if stats["losses"] > 0:
        stats["avg_loss"] = round(loss_pnl / stats["losses"], 2)
    if by_strategy:
        stats["_by_strategy"] = by_strategy
    return stats


_TODAY_OPEN_FILL_CACHE = {
    "ts": 0.0,
    "mtime": 0.0,
    "value": 0,
}


def _read_today_open_fills_from_trading_ledger(ledger_file: Optional[pathlib.Path]) -> int:
    if ledger_file is None or not ledger_file.exists():
        return 0
    now = time.time()
    mtime = 0.0
    try:
        mtime = float(ledger_file.stat().st_mtime)
    except Exception:
        mtime = 0.0
    if (
        _TODAY_OPEN_FILL_CACHE.get("value") is not None
        and _TODAY_OPEN_FILL_CACHE.get("mtime") == mtime
        and (now - float(_TODAY_OPEN_FILL_CACHE.get("ts", 0.0))) < 15.0
    ):
        return int(_TODAY_OPEN_FILL_CACHE.get("value", 0) or 0)

    today_date = datetime.datetime.now(datetime.timezone.utc).astimezone().date()
    count = 0
    try:
        with ledger_file.open("r", encoding="utf-8") as handle:
            for line in handle:
                if not line.strip():
                    continue
                try:
                    row = json.loads(line)
                except Exception:
                    continue
                if str(row.get("status", "") or "").upper() != "FILLED":
                    continue
                if str(row.get("action", "") or "").upper() != "OPEN":
                    continue
                ts = _parse_trade_ledger_timestamp(row.get("ts"))
                if ts is not None and ts.astimezone().date() != today_date:
                    continue
                count += 1
    except Exception:
        count = 0

    _TODAY_OPEN_FILL_CACHE.update({"ts": now, "mtime": mtime, "value": count})
    return int(count)

def _safe_load_state_worker() -> None:
    data: Dict[str, Any] = {}
    try:
        loaded = load_state()
        if isinstance(loaded, dict):
            data = loaded
    except Exception:
        data = {}
    with _STATE_LOAD_LOCK:
        _STATE_LOAD_CACHE["value"] = data
        _STATE_LOAD_CACHE["ts"] = time.time()
        _STATE_LOAD_CACHE["inflight"] = False
        _STATE_LOAD_CACHE["thread"] = None


def _safe_load_state(timeout_sec: float = 0.25, max_stale_sec: float = 3.0) -> Dict[str, Any]:
    """
    Avoid /api/status spawning unbounded worker threads when SQLite is contended.
    Keep one in-flight loader thread and return recent cached state immediately.
    """
    now = time.time()
    inflight_thread = None
    with _STATE_LOAD_LOCK:
        cached = _STATE_LOAD_CACHE.get("value", {})
        cached_ts = float(_STATE_LOAD_CACHE.get("ts", 0.0))
        if isinstance(cached, dict) and cached and (now - cached_ts) <= max_stale_sec:
            return dict(cached)
        inflight = bool(_STATE_LOAD_CACHE.get("inflight", False))
        current_thread = _STATE_LOAD_CACHE.get("thread")
        if isinstance(current_thread, threading.Thread):
            inflight_thread = current_thread
        if not inflight:
            t = threading.Thread(
                target=_safe_load_state_worker,
                daemon=True,
                name="gui-state-loader",
            )
            _STATE_LOAD_CACHE["inflight"] = True
            _STATE_LOAD_CACHE["thread"] = t
            inflight_thread = t
            t.start()
    if isinstance(inflight_thread, threading.Thread):
        inflight_thread.join(timeout=max(0.0, float(timeout_sec)))
    with _STATE_LOAD_LOCK:
        latest = _STATE_LOAD_CACHE.get("value", {})
        return dict(latest) if isinstance(latest, dict) else {}

def _resolve_terminal_status_file(mt5_dir: pathlib.Path) -> Optional[pathlib.Path]:
    """Resolve freshest terminal_status.json across EA legacy/new paths.

    Also attempts a broad scan if the primary paths don't exist, so that
    brand-name MT5 installations (WeTrade, FTMO, etc.) with non-standard
    layouts can still be discovered at runtime.
    """
    candidates = [
        mt5_dir / "gold_autotrade/bridge/from_ea/terminal_status.json",
        mt5_dir / "gold_autotrade/bridge/terminal_status.json",
    ]
    existing = [p for p in candidates if p.exists()]
    if existing:
        return max(existing, key=lambda p: p.stat().st_mtime)

    # Fallback: scan for terminal_status.json under potential MT5 data dirs
    try:
        from config_manager import _scan_windows_mt5_dirs
        scanned = _scan_windows_mt5_dirs()
        for inst_path_str in scanned.get("instances", []):
            p = pathlib.Path(inst_path_str) / "gold_autotrade/bridge/terminal_status.json"
            if p.exists():
                return p
            p2 = pathlib.Path(inst_path_str) / "gold_autotrade/bridge/from_ea/terminal_status.json"
            if p2.exists():
                return p2
    except Exception:
        pass
    return None


_POSITION_STATUS_ALERT_STATE: dict[str, Any] = {
    "fingerprint": "",
    "ts": 0.0,
}


def _position_status_alert_file() -> pathlib.Path:
    return LOGS_DIR / "position_status_alerts.ndjson"


def _normalize_terminal_status_position(raw: Any) -> Optional[dict[str, Any]]:
    if not isinstance(raw, dict):
        return None
    try:
        ticket = int(raw.get("ticket", 0) or 0)
    except Exception:
        ticket = 0
    try:
        volume = float(raw.get("volume", raw.get("vol", 0.0)) or 0.0)
    except Exception:
        volume = 0.0
    try:
        entry = float(raw.get("entry", raw.get("entry_price", 0.0)) or 0.0)
    except Exception:
        entry = 0.0
    try:
        pnl = float(raw.get("pnl", raw.get("profit", 0.0)) or 0.0)
    except Exception:
        pnl = 0.0
    side = str(raw.get("side", raw.get("direction", "")) or "").upper()
    if side not in {"BUY", "SELL"}:
        return None
    return {
        **raw,
        "ticket": ticket,
        "side": side,
        "volume": volume,
        "entry": entry,
        "sl": float(raw.get("sl", 0.0) or 0.0),
        "tp": float(raw.get("tp", 0.0) or 0.0),
        "pnl": pnl,
    }


def _extract_terminal_status_positions(payload: Any) -> list[dict[str, Any]]:
    if not isinstance(payload, dict):
        return []
    for key in ("positions_all", "positions"):
        rows = payload.get(key, [])
        if not isinstance(rows, list):
            continue
        normalized = [
            item
            for item in (_normalize_terminal_status_position(row) for row in rows)
            if item is not None
        ]
        if normalized or key in payload:
            return normalized
    return []


def _terminal_status_positions_count(payload: Any) -> Optional[int]:
    if not isinstance(payload, dict):
        return None
    positions = _extract_terminal_status_positions(payload)
    if positions:
        return len(positions)
    for key in ("positions_count", "count"):
        raw_value = payload.get(key)
        if raw_value is None:
            continue
        try:
            value = int(raw_value)
        except Exception:
            continue
        if value >= 0:
            return value
    if "positions_all" in payload or "positions" in payload:
        return 0
    return None


def _read_terminal_status_snapshot(
    mt5_dir: Optional[pathlib.Path] = None,
) -> tuple[Optional[pathlib.Path], Optional[dict[str, Any]]]:
    try:
        resolved_mt5_dir = mt5_dir or config_manager.get_mt5_dir()
        if not resolved_mt5_dir:
            return None, None
        sf = _resolve_terminal_status_file(resolved_mt5_dir)
        if not sf or not sf.exists():
            return sf, None
        if int(sf.stat().st_size) > 512 * 1024:
            return sf, None
        payload = json.loads(sf.read_text(encoding="utf-8"))
        if isinstance(payload, dict):
            return sf, payload
        return sf, None
    except Exception:
        return None, None


def _emit_position_status_alert(reason: str, **details: Any) -> None:
    fingerprint = json.dumps(
        {"reason": str(reason or ""), "details": details},
        ensure_ascii=False,
        sort_keys=True,
    )
    now = time.time()
    last_fingerprint = str(_POSITION_STATUS_ALERT_STATE.get("fingerprint", "") or "")
    last_ts = float(_POSITION_STATUS_ALERT_STATE.get("ts", 0.0) or 0.0)
    if fingerprint == last_fingerprint and (now - last_ts) < 30.0:
        return
    payload = {
        "ts": now,
        "reason": str(reason or "unknown_position_status_anomaly"),
        "details": details,
    }
    try:
        _append_ndjson_line(_position_status_alert_file(), payload)
    except Exception:
        pass
    logging.warning("position status anomaly: %s %s", payload["reason"], details)
    _POSITION_STATUS_ALERT_STATE["fingerprint"] = fingerprint
    _POSITION_STATUS_ALERT_STATE["ts"] = now


def _read_terminal_status_positions_count() -> Optional[int]:
    _, st_data = _read_terminal_status_snapshot()
    return _terminal_status_positions_count(st_data)


def _map_risk_block_reason(raw_reason: str) -> dict:
    reason = str(raw_reason or "").strip()
    if reason.startswith("CONTEXT_ARB_hermes_session_block"):
        return {
            "categoryCode": "SESSION_WINDOW",
            "categoryLabel": "会话时段约束",
            "summary": "欧美重叠时段被会话规则拦截",
        }
    if reason.startswith("CONTEXT_ARB_hermes_session_matrix_block"):
        return {
            "categoryCode": "SESSION_MATRIX",
            "categoryLabel": "时段矩阵风控",
            "summary": "命中时段分层矩阵的阻断条件",
        }
    if reason.startswith("CONTEXT_ARB_hermes_regime_block"):
        return {
            "categoryCode": "REGIME_GUARD",
            "categoryLabel": "行情结构约束",
            "summary": "当前结构与策略偏好不匹配",
        }
    if reason.startswith("PERF_GATE_session_rule_blocked"):
        return {
            "categoryCode": "PERFORMANCE_GATE",
            "categoryLabel": "性能门控",
            "summary": "命中性能门控规则，策略暂缓执行",
        }
    if reason.startswith("LOW_CONFIDENCE"):
        return {
            "categoryCode": "CONFIDENCE_GATE",
            "categoryLabel": "置信门槛",
            "summary": "置信度低于当前执行门槛",
        }
    if reason.startswith(("KILL_SWITCH", "LOSS_LIMIT", "DAILY_LOSS_LIMIT")):
        return {
            "categoryCode": "TERMINAL_RISK",
            "categoryLabel": "终局风控",
            "summary": "触发账户级终局风险保护",
        }
    if reason.startswith(("WIDE_SPREAD", "MAX_POSITIONS")):
        return {
            "categoryCode": "MARKET_GUARD",
            "categoryLabel": "市场保护",
            "summary": "点差或仓位限制触发保护",
        }
    if reason.startswith("DYNAMIC_COOLDOWN"):
        return {
            "categoryCode": "COOLDOWN",
            "categoryLabel": "动态冷却",
            "summary": "距离上次平仓过近，处于冷却期",
        }
    if reason.startswith(("BOLLINGER_", "RSI_")):
        return {
            "categoryCode": "TECH_FILTER",
            "categoryLabel": "技术过滤",
            "summary": "命中技术过滤条件",
        }
    return {
        "categoryCode": "UNKNOWN",
        "categoryLabel": "未分类",
        "summary": "未匹配到已知阻断类型",
    }


_NON_RISK_HOLD_MARKERS = (
    "AGGREGATED_HOLD_LOW_NET_SCORE",
    "AGGREGATED_HOLD_NO_ACTIONABLE_SIGNAL",
)


def _normalize_blocker_reasons(raw_items: Any) -> list[str]:
    if not isinstance(raw_items, list):
        return []
    normalized: list[str] = []
    seen: set[str] = set()
    for item in raw_items:
        reason = str(item or "").strip()
        if not reason:
            continue
        if any(marker in reason for marker in _NON_RISK_HOLD_MARKERS):
            continue
        if reason in seen:
            continue
        seen.add(reason)
        normalized.append(reason)
    return normalized


def _default_risk_block_payload() -> dict:
    return {
        "exists": False,
        "rawReason": "",
        "categoryCode": "NONE",
        "categoryLabel": "无阻断",
        "summary": "",
        "time": "",
        "line": "",
        "eventTs": 0,
        "ageSec": None,
        "fresh": False,
        "maxAgeSec": 1800,
    }


def _read_tail_lines(path: pathlib.Path, max_bytes: int = 128 * 1024, max_lines: int = 3000) -> list[str]:
    """
    Read only the tail of a log file.
    This avoids loading large log files into memory on every /api/status poll.
    """
    try:
        with path.open("rb") as f:
            f.seek(0, os.SEEK_END)
            size = f.tell()
            read_size = min(max_bytes, size)
            f.seek(max(0, size - read_size), os.SEEK_SET)
            chunk = f.read(read_size)
    except Exception:
        return []
    text = chunk.decode("utf-8", errors="ignore")
    lines = text.splitlines()
    if len(lines) > max_lines:
        lines = lines[-max_lines:]
    return lines


_DECISION_HALL_EVENT_MAX_AGE_SEC = 30 * 60


def _decorate_decision_hall_event(payload: dict, ts_value: Any) -> dict:
    data = dict(payload) if isinstance(payload, dict) else {}
    event_ts = _parse_timestamp_value(ts_value)
    age_sec: Optional[float] = None
    is_fresh = False
    if event_ts is not None and event_ts > 0:
        age_sec = max(0.0, time.time() - event_ts)
        is_fresh = age_sec <= _DECISION_HALL_EVENT_MAX_AGE_SEC
    data["eventTs"] = int(event_ts) if event_ts is not None and event_ts > 0 else 0
    data["ageSec"] = round(age_sec, 3) if age_sec is not None else None
    data["fresh"] = is_fresh
    data["maxAgeSec"] = _DECISION_HALL_EVENT_MAX_AGE_SEC
    return data


def _get_fresh_final_reject_event(state: dict) -> dict:
    payload = state.get("last_final_reject", {}) if isinstance(state.get("last_final_reject", {}), dict) else {}
    if not payload:
        return {}
    decorated = _decorate_decision_hall_event(payload, payload.get("ts"))
    return decorated if decorated.get("fresh") else {}


def _is_readable_file(path: pathlib.Path) -> bool:
    try:
        return path.exists() and path.is_file() and os.access(path, os.R_OK)
    except Exception:
        return False


def _is_readable_dir(path: pathlib.Path) -> bool:
    try:
        return path.exists() and path.is_dir() and os.access(path, os.R_OK | os.X_OK)
    except Exception:
        return False


def _latest_mtime_in_dir(path: pathlib.Path) -> float:
    latest = 0.0
    try:
        for candidate in path.glob("*.jsonl"):
            latest = max(latest, candidate.stat().st_mtime)
    except Exception:
        return 0.0
    return latest


def _parse_hms_seconds(value: str) -> Optional[int]:
    try:
        hh, mm, ss = str(value).split(":")
        return int(hh) * 3600 + int(mm) * 60 + int(ss)
    except Exception:
        return None


def _entry_sort_key(item: dict) -> tuple[float, str]:
    ts = item.get("ts")
    if isinstance(ts, (int, float)):
        return (float(ts), str(item.get("event_id", "")))
    raw_timestamp = str(item.get("timestamp", "")).strip()
    if raw_timestamp:
        try:
            return (
                datetime.datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00")).timestamp(),
                str(item.get("event_id", "")),
            )
        except Exception:
            pass
    inferred = item.get("_sort_ts")
    if isinstance(inferred, datetime.datetime):
        return (inferred.timestamp(), str(item.get("event_id", "")))
    return (float("-inf"), str(item.get("time", "")))


def _load_swarm_entries_with_inferred_ts(log_file: pathlib.Path, max_lines: int) -> list[dict]:
    entries: list[dict] = []
    try:
        raw_lines = _read_tail_lines(log_file, max_lines=max_lines, max_bytes=128 * 1024)
    except Exception:
        return entries

    for line in raw_lines:
        if not line.strip():
            continue
        try:
            entries.append(json.loads(line))
        except Exception:
            continue

    if not entries:
        return entries

    base_date = datetime.datetime.fromtimestamp(log_file.stat().st_mtime).date()
    current_date = base_date
    last_seconds: Optional[int] = None

    # 反向扫描，遇到 `23:xx -> 00:xx` 这类回绕时把日期减一天，补上跨天顺序。
    for entry in reversed(entries):
        seconds = _parse_hms_seconds(str(entry.get("time", "")))
        if seconds is not None and last_seconds is not None and seconds > last_seconds:
            current_date -= datetime.timedelta(days=1)
        raw_timestamp = str(entry.get("timestamp", "")).strip()
        if raw_timestamp:
            try:
                parsed = datetime.datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00"))
                entry["_sort_ts"] = parsed
                continue
            except Exception:
                pass
        ts = entry.get("ts")
        if isinstance(ts, (int, float)):
            entry["_sort_ts"] = datetime.datetime.fromtimestamp(float(ts), datetime.timezone.utc)
            continue
        if seconds is not None:
            entry["_sort_ts"] = datetime.datetime.combine(
                current_date,
                datetime.time(second=0),
            ) + datetime.timedelta(seconds=seconds)
            last_seconds = seconds
        else:
            entry["_sort_ts"] = datetime.datetime.combine(current_date, datetime.time.min)

    return entries


def _resolve_active_swarm_dir() -> Optional[pathlib.Path]:
    candidates = _runtime_root_candidates("logs", "swarm")
    readable = [path for path in candidates if _is_readable_dir(path)]
    if not readable:
        return None
    readable.sort(key=lambda path: (_latest_mtime_in_dir(path), str(path)))
    return readable[-1]


def _resolve_existing_file(paths: list[pathlib.Path]) -> Optional[pathlib.Path]:
    for path in paths:
        if _is_readable_file(path):
            return path
    return None


def _dedupe_paths(paths: list[pathlib.Path]) -> list[pathlib.Path]:
    deduped: list[pathlib.Path] = []
    seen: set[str] = set()
    for path in paths:
        try:
            key = str(path.resolve())
        except Exception:
            key = str(path)
        if key in seen:
            continue
        seen.add(key)
        deduped.append(path)
    return deduped


def _resolve_freshest_existing_file(paths: list[pathlib.Path]) -> Optional[pathlib.Path]:
    freshest_path: Optional[pathlib.Path] = None
    freshest_key: tuple[float, int, str] = (0.0, -1, "")
    for path in _dedupe_paths(paths):
        if not _is_readable_file(path):
            continue
        try:
            stat = path.stat()
            candidate_key = (float(stat.st_mtime), int(stat.st_size), str(path))
        except Exception:
            candidate_key = (0.0, 0, str(path))
        if freshest_path is None or candidate_key > freshest_key:
            freshest_path = path
            freshest_key = candidate_key
    return freshest_path


def _managed_release_log_candidates(name: str) -> list[pathlib.Path]:
    candidates: list[pathlib.Path] = []
    releases_dirs = [
        APP_DIR / "managed_app" / "releases",
    ]
    # 仅在 macOS 上追加 LobsterQuant 旧版安装路径（跨平台兼容）
    if sys.platform == "darwin":
        releases_dirs.append(
            pathlib.Path.home() / "Library" / "Application Support" / "LobsterQuant" / "managed_app" / "releases"
        )
    seen_roots: set[str] = set()
    for releases_dir in releases_dirs:
        if not _is_readable_dir(releases_dir):
            continue
        try:
            release_roots = sorted(
                [path for path in releases_dir.iterdir() if path.is_dir()],
                key=lambda path: path.stat().st_mtime,
                reverse=True,
            )
        except Exception:
            continue
        for release_root in release_roots:
            root_key = str(release_root.resolve())
            if root_key in seen_roots:
                continue
            seen_roots.add(root_key)
            candidates.append(release_root / "logs" / name)
    return candidates


_ACTIVE_TRADER_LOG_CACHE: dict[str, Any] = {
    "ts": 0.0,
    "paths": {},
}
_ACTIVE_TRADER_LOG_CACHE_MAX_KEYS = 128
_ACTIVE_RUNTIME_ROOT_CACHE: dict[str, Any] = {
    "ts": 0.0,
    "roots": [],
}


def _normalize_runtime_log_root(root: pathlib.Path) -> pathlib.Path:
    try:
        candidate = root.resolve()
    except Exception:
        candidate = root
    if candidate.name in {"bridge", "src"}:
        parent = candidate.parent
        if (parent / "logs").exists() or (parent / "config").exists() or (parent / "gui_backend.py").exists():
            return parent
    # When a pipeline process runs from a monorepo root (e.g. openclaw/),
    # the actual gold_autotrade project lives one level deeper.
    for sub in ["projects/gold_autotrade"]:
        sub_path = candidate / sub
        if (sub_path / "logs").exists() or (sub_path / "gui_backend.py").exists():
            return sub_path
    return candidate


def _managed_storage_root_for_runtime_root(root: pathlib.Path) -> Optional[pathlib.Path]:
    normalized_root = _normalize_runtime_log_root(root)
    windows_appdata = os.environ.get("APPDATA", "").strip()
    if windows_appdata:
        try:
            exe_path = normalized_root / "GoldAutoTradeEngine.exe"
            appdata_root = pathlib.Path(windows_appdata) / "GoldAutoTrade"
            if exe_path.exists() and (appdata_root / "logs").exists():
                return appdata_root
        except Exception:
            pass
    for parent in normalized_root.parents:
        releases_dir = parent / "managed_app" / "releases"
        try:
            normalized_root.relative_to(releases_dir)
            return parent
        except ValueError:
            continue
    return None


def _path_identity(path: pathlib.Path) -> str:
    try:
        return str(path.resolve())
    except Exception:
        return str(path)


def _runtime_family_token(root: pathlib.Path) -> tuple[str, str]:
    normalized_root = _normalize_runtime_log_root(root)
    managed_storage_root = _managed_storage_root_for_runtime_root(normalized_root)
    if managed_storage_root is not None:
        return ("managed_storage", _path_identity(managed_storage_root))
    return ("runtime_root", _path_identity(normalized_root))


def _current_runtime_family_token() -> tuple[str, str]:
    return _runtime_family_token(APP_DIR)


def _active_trader_runtime_roots_for_gui() -> list[pathlib.Path]:
    current_family = _current_runtime_family_token()
    same_family_roots = [
        root for root in _active_trader_runtime_roots() if _runtime_family_token(root) == current_family
    ]
    if same_family_roots:
        return same_family_roots
    if APP_DIR.resolve() == REPO_ROOT.resolve():
        return _active_trader_runtime_roots()
    return []


def _active_trader_runtime_roots() -> list[pathlib.Path]:
    now = time.time()
    cached_roots = _ACTIVE_RUNTIME_ROOT_CACHE.get("roots", [])
    if (
        isinstance(cached_roots, list)
        and (now - float(_ACTIVE_RUNTIME_ROOT_CACHE.get("ts", 0.0) or 0.0)) < 3.0
    ):
        return [path for path in cached_roots if isinstance(path, pathlib.Path)]

    roots: list[pathlib.Path] = []
    seen: set[str] = set()
    try:
        for proc in psutil.process_iter(["pid", "cmdline", "name", "exe"]):
            try:
                cmdline = [str(part) for part in (proc.info.get("cmdline") or []) if str(part).strip()]
            except Exception:
                cmdline = []
            proc_name = str(proc.info.get("name") or "").strip().lower()
            exe_path_raw = str(proc.info.get("exe") or "").strip()
            exe_name = pathlib.Path(exe_path_raw).name.lower() if exe_path_raw else ""
            combined = " ".join(cmdline)
            is_python_runtime = bool(cmdline) and (
                "gold_trader.py" in combined or "src/pipeline.py" in combined
            )
            is_windows_engine = proc_name == "goldautotradeengine.exe" or exe_name == "goldautotradeengine.exe"
            if not is_python_runtime and not is_windows_engine:
                continue

            proc_roots: list[pathlib.Path] = []
            try:
                cwd = proc.cwd()
                if cwd:
                    proc_roots.append(pathlib.Path(cwd))
            except Exception:
                pass

            if is_windows_engine and exe_path_raw:
                try:
                    proc_roots.append(pathlib.Path(exe_path_raw).parent)
                except Exception:
                    pass

            for part in cmdline:
                try:
                    script_path = pathlib.Path(part)
                except Exception:
                    continue
                if script_path.name == "gold_trader.py" and script_path.parent.name == "bridge":
                    proc_roots.append(script_path.parent.parent)
                elif script_path.name == "pipeline.py" and script_path.parent.name == "src":
                    proc_roots.append(script_path.parent.parent)

            for root in proc_roots:
                normalized_root = _normalize_runtime_log_root(root)
                try:
                    root_key = str(normalized_root.resolve())
                except Exception:
                    root_key = str(normalized_root)
                if not root_key or root_key in seen:
                    continue
                seen.add(root_key)
                roots.append(normalized_root)
    except Exception:
        pass

    _ACTIVE_RUNTIME_ROOT_CACHE["ts"] = now
    _ACTIVE_RUNTIME_ROOT_CACHE["roots"] = list(roots)
    return roots


def _runtime_scope_candidates(scope: str, *relative_parts: str) -> list[pathlib.Path]:
    rel_path = pathlib.Path(*relative_parts)
    candidates: list[pathlib.Path] = []
    scope_dir = {
        "logs": LOGS_DIR,
        "data": DATA_DIR,
        "config": CONFIG_DIR,
    }.get(scope)
    if scope_dir is not None:
        candidates.append(scope_dir / rel_path)

    # Bind GUI reads to the same runtime family as the current GUI process.
    # This avoids mixing repo and managed-release artifacts after reboot.
    for root in _active_trader_runtime_roots_for_gui():
        normalized = _normalize_runtime_log_root(root)
        candidates.append(normalized / scope / rel_path)
        candidates.append(root / scope / rel_path)
        managed_storage_root = _managed_storage_root_for_runtime_root(root)
        if managed_storage_root is not None:
            candidates.append(managed_storage_root / scope / rel_path)

    if scope == "logs":
        candidates.append(RELEASE_ROOT / "logs" / rel_path)
    elif APP_DIR.resolve() == REPO_ROOT.resolve():
        candidates.append(REPO_ROOT / scope / rel_path)

    return _dedupe_paths(candidates)


def _resolve_runtime_artifact(scope: str, *relative_parts: str) -> Optional[pathlib.Path]:
    return _resolve_freshest_existing_file(_runtime_scope_candidates(scope, *relative_parts))


def _runtime_root_candidates(*relative_parts: str) -> list[pathlib.Path]:
    rel_path = pathlib.Path(*relative_parts)
    candidates: list[pathlib.Path] = [APP_DIR / rel_path]
    current_managed_storage_root = _managed_storage_root_for_runtime_root(APP_DIR)
    if current_managed_storage_root is not None:
        candidates.append(current_managed_storage_root / rel_path)

    for root in _active_trader_runtime_roots_for_gui():
        candidates.append(root / rel_path)
        managed_storage_root = _managed_storage_root_for_runtime_root(root)
        if managed_storage_root is not None:
            candidates.append(managed_storage_root / rel_path)

    candidates.append(RELEASE_ROOT / rel_path)
    if APP_DIR.resolve() == REPO_ROOT.resolve():
        candidates.append(REPO_ROOT / rel_path)
    else:
        candidates.append(REPO_ROOT / rel_path)
    return _dedupe_paths(candidates)


def _resolve_runtime_root_artifact(*relative_parts: str) -> Optional[pathlib.Path]:
    return _resolve_freshest_existing_file(_runtime_root_candidates(*relative_parts))


def _runtime_execution_state_file() -> pathlib.Path:
    return _resolve_runtime_artifact("data", "execution_state.json") or (DATA_DIR / "execution_state.json")


def _runtime_execution_state_payload() -> dict[str, Any]:
    return _read_json_file(_runtime_execution_state_file())


def _runtime_open_guard_file() -> pathlib.Path:
    return _resolve_runtime_root_artifact("data", "runtime_open_guard.json") or (DATA_DIR / "runtime_open_guard.json")


def _runtime_open_guard_payload() -> dict[str, Any]:
    return _read_json_file(_runtime_open_guard_file())


def _runtime_derived_state(timeout_sec: float = 0.8) -> tuple[dict[str, Any], dict[str, Any], bool]:
    runtime_open_guard = _runtime_open_guard_payload()
    runtime_guard_present = isinstance(runtime_open_guard, dict) and bool(runtime_open_guard)
    aux_state = _safe_load_state(timeout_sec=timeout_sec)
    aux_state = aux_state if isinstance(aux_state, dict) else {}
    state = dict(aux_state)
    if runtime_guard_present:
        state.update(runtime_open_guard)
    return state, runtime_open_guard, runtime_guard_present


def _position_tier_label(tier: str) -> str:
    mapping = {
        "NORMAL": "正常仓位",
        "DEFENSIVE": "防守仓位",
        "SURVIVAL": "生存仓位",
        "BLOCKED": "阻断开仓",
    }
    return mapping.get(str(tier or "").upper(), "未知仓位")


def _position_tier_tone(tier: str) -> str:
    mapping = {
        "NORMAL": "success",
        "DEFENSIVE": "warning",
        "SURVIVAL": "warning",
        "BLOCKED": "danger",
    }
    return mapping.get(str(tier or "").upper(), "neutral")


def _build_position_tier_progress_items(recovery_progress: Dict[str, Any]) -> tuple[list[dict[str, Any]], float]:
    progress = recovery_progress if isinstance(recovery_progress, dict) else {}
    items: list[dict[str, Any]] = []

    def _append(
        key: str,
        label: str,
        current: float,
        target: float,
        *,
        unit: str = "",
        boolean_gate: bool = False,
    ) -> None:
        safe_target = max(1.0, float(target or 0.0)) if not boolean_gate else 1.0
        safe_current = float(current or 0.0)
        ratio = max(0.0, min(1.0, safe_current / safe_target))
        done = ratio >= 0.999
        if boolean_gate:
            display = "已满足" if done else "未满足"
        elif unit:
            display = f"{safe_current:.0f}/{safe_target:.0f}{unit}"
        else:
            display = f"{safe_current:.0f}/{safe_target:.0f}"
        items.append(
            {
                "key": key,
                "label": label,
                "current": safe_current,
                "target": safe_target,
                "ratio": ratio,
                "percent": round(ratio * 100.0, 1),
                "done": done,
                "display": display,
            }
        )

    if "non_loss_streak" in progress:
        _append(
            "non_loss_streak",
            "非亏损连续笔数",
            float(progress.get("non_loss_streak", 0.0) or 0.0),
            float(progress.get("target_non_loss_streak", 1.0) or 1.0),
            unit="笔",
        )
    if "strategy_samples" in progress:
        _append(
            "strategy_samples",
            "策略真实样本",
            float(progress.get("strategy_samples", 0.0) or 0.0),
            float(progress.get("target_strategy_samples", 1.0) or 1.0),
            unit="笔",
        )
    if "market_stable" in progress:
        _append(
            "market_stable",
            "市场稳定条件",
            float(progress.get("market_stable", 0.0) or 0.0),
            1.0,
            boolean_gate=True,
        )

    if not items:
        return [], 100.0
    avg_ratio = sum(float(item.get("ratio", 0.0) or 0.0) for item in items) / max(1, len(items))
    return items, round(avg_ratio * 100.0, 1)


def _build_position_tier_snapshot(runtime_state: Dict[str, Any]) -> dict[str, Any]:
    runtime_state = runtime_state if isinstance(runtime_state, dict) else {}
    tier = str(runtime_state.get("position_tier", "NORMAL") or "NORMAL").upper()
    target_lots = float(runtime_state.get("tier_target_lots", 0.0) or 0.0)
    recovery_needed = (
        runtime_state.get("tier_recovery_needed", [])
        if isinstance(runtime_state.get("tier_recovery_needed", []), list)
        else []
    )
    recovery_progress = (
        runtime_state.get("tier_recovery_progress", {})
        if isinstance(runtime_state.get("tier_recovery_progress", {}), dict)
        else {}
    )
    active_rules = (
        runtime_state.get("tier_active_rules", [])
        if isinstance(runtime_state.get("tier_active_rules", []), list)
        else []
    )
    since_ts = float(runtime_state.get("tier_since_ts", 0.0) or 0.0)
    updated_at = str(runtime_state.get("updated_at", "") or "")
    progress_items, progress_percent = _build_position_tier_progress_items(recovery_progress)
    return {
        "tier": tier,
        "tierLabel": _position_tier_label(tier),
        "tone": _position_tier_tone(tier),
        "targetLots": round(target_lots, 4),
        "reasonCode": str(runtime_state.get("tier_reason_code", "") or ""),
        "reasonText": str(runtime_state.get("tier_reason_text", "") or ""),
        "activeRules": active_rules,
        "recoveryNeeded": recovery_needed,
        "recoveryProgress": recovery_progress,
        "recoveryProgressItems": progress_items,
        "recoveryProgressPercent": progress_percent,
        "updatedAt": updated_at,
        "sinceTs": since_ts,
        "sinceAt": datetime.datetime.fromtimestamp(since_ts, datetime.timezone.utc).isoformat() if since_ts > 0 else "",
        "lastTransition": str(runtime_state.get("tier_last_transition", "") or ""),
        "consecutiveLosses": int(runtime_state.get("consecutive_losses", 0) or 0),
        "regime": str(runtime_state.get("regime", "UNKNOWN") or "UNKNOWN"),
        "positionOpen": bool(runtime_state.get("position_open", False)),
    }


def _build_position_tier_review(limit: int = 8) -> dict[str, Any]:
    events = _load_instance_trade_events()
    if not isinstance(events, list) or not events:
        return {"rows": [], "count": 0}
    rows: list[dict[str, Any]] = []
    for record in reversed(events):
        if not isinstance(record, dict):
            continue
        action = str(record.get("action", "") or "").upper()
        status = str(record.get("status", "") or "").upper()
        if action != "OPEN" or status not in {"FILLED", "ACKED"}:
            continue
        tier = str(record.get("position_tier", "") or "").upper()
        tier_reason = str(record.get("tier_reason_text", "") or "")
        if not tier and not tier_reason:
            continue
        rows.append(
            {
                "ts": str(record.get("ts", record.get("timestamp", "")) or ""),
                "strategyId": str(
                    record.get("strategy_id")
                    or record.get("selected_strategy_id")
                    or record.get("attributed_strategy_id")
                    or ""
                ),
                "instanceId": str(
                    record.get("instance_id")
                    or record.get("selected_instance_id")
                    or record.get("attributed_instance_id")
                    or ""
                ),
                "side": str(record.get("side", "NONE") or "NONE").upper(),
                "status": status,
                "requestedLots": float(record.get("requested_volume_lots", 0.0) or 0.0),
                "baseLots": float(record.get("configured_base_lots", 0.0) or 0.0),
                "ruleCapLots": float(record.get("rule_cap_lots", 0.0) or 0.0),
                "tier": tier or "NORMAL",
                "tierLabel": _position_tier_label(tier or "NORMAL"),
                "tone": _position_tier_tone(tier or "NORMAL"),
                "tierTargetLots": float(record.get("tier_target_lots", 0.0) or 0.0),
                "finalLots": float(record.get("volume_lots", record.get("volume", 0.0)) or 0.0),
                "reasonCode": str(record.get("tier_reason_code", "") or ""),
                "reasonText": tier_reason,
                "recoveryNeeded": (
                    record.get("tier_recovery_needed", [])
                    if isinstance(record.get("tier_recovery_needed", []), list)
                    else []
                ),
                "decisionId": str(record.get("decision_id", record.get("signal_id", "")) or ""),
            }
        )
        if len(rows) >= limit:
            break
    return {"rows": rows, "count": len(rows)}


def _runtime_strategy_status_file() -> pathlib.Path:
    return _resolve_runtime_artifact("logs", "strategy_runtime_status.json") or (LOGS_DIR / "strategy_runtime_status.json")


def _synthesize_runtime_strategy_status_payload() -> dict[str, Any]:
    runtime_scheduler = _load_runtime_scheduler_snapshot()
    if not isinstance(runtime_scheduler, dict) or not runtime_scheduler:
        return {}
    route_payload = (
        runtime_scheduler.get("route", {})
        if isinstance(runtime_scheduler.get("route", {}), dict)
        else {}
    )
    market_state = _load_strategy_market_state()
    runtime_instances = (
        market_state.get("runtime_instances", [])
        if isinstance(market_state.get("runtime_instances", []), list)
        else []
    )
    active_strategy_ids = _clean_str_list(runtime_scheduler.get("active_strategy_ids"))
    if not active_strategy_ids:
        active_strategy_ids = _clean_str_list(
            route_payload.get("compatible_strategy_ids")
            or route_payload.get("candidate_strategy_ids")
            or route_payload.get("routed_strategy_ids")
        )
    selected_strategy_id = _first_non_empty_str(
        runtime_scheduler.get("selected_strategy_id"),
        runtime_scheduler.get("entry_owner_strategy_id"),
    )
    if not active_strategy_ids and selected_strategy_id:
        active_strategy_ids = [selected_strategy_id]
    active_instance_ids = _clean_str_list(runtime_scheduler.get("active_instance_ids"))
    if not active_instance_ids:
        active_instance_ids = [
            str(item.get("instance_id", "") or "")
            for item in runtime_instances
            if isinstance(item, dict)
            and str(item.get("status", "running") or "running").lower() == "running"
            and (
                not active_strategy_ids
                or str(item.get("strategy_id", "") or "") in active_strategy_ids
            )
        ]
        active_instance_ids = [item for item in active_instance_ids if item]
    selected_instance_id = _first_non_empty_str(
        runtime_scheduler.get("selected_instance_id"),
        runtime_scheduler.get("entry_owner_instance_id"),
    )
    if selected_instance_id and selected_instance_id not in active_instance_ids:
        active_instance_ids.append(selected_instance_id)
    source_versions = (
        runtime_scheduler.get("source_versions", {})
        if isinstance(runtime_scheduler.get("source_versions", {}), dict)
        else {}
    )
    config_revision = str(source_versions.get("effective_params", "manual") or "manual")
    registered = list(_strategy_catalog_from_state(market_state).keys())
    health = {
        strategy_id: {
            "strategy_id": strategy_id,
            "loaded": True,
            "healthy": True,
            "last_error": None,
            "last_run_at_ms": None,
            "last_latency_ms": None,
            "consecutive_failures": 0,
            "config_revision": config_revision,
        }
        for strategy_id in active_strategy_ids
    }
    route_family = str(route_payload.get("route_family", "") or "")
    market_context = (
        route_payload.get("market_context", {})
        if isinstance(route_payload.get("market_context", {}), dict)
        else {}
    )
    candidate_fit_map = (
        route_payload.get("candidate_fit_map", {})
        if isinstance(route_payload.get("candidate_fit_map", {}), dict)
        else {}
    )
    selection = (
        route_payload.get("selection", {})
        if isinstance(route_payload.get("selection", {}), dict)
        else {}
    )
    session_scheduler = (
        runtime_scheduler.get("session_scheduler", {})
        if isinstance(runtime_scheduler.get("session_scheduler", {}), dict)
        else {}
    )
    selection_summary = _summarize_selection_diagnostics(
        {
            "selected_strategy_id": selected_strategy_id,
            "selected_instance_id": selected_instance_id,
            "route_family": route_family,
            "market_context": market_context,
            "candidate_strategy_ids": _clean_str_list(
                route_payload.get("candidate_strategy_ids")
                or route_payload.get("compatible_strategy_ids")
                or route_payload.get("routed_strategy_ids")
            ),
            "selection": selection,
            "candidate_fit_map": candidate_fit_map,
            "session_scheduler": session_scheduler,
        },
        runtime_scheduler=runtime_scheduler,
    )
    return {
        "ts_ms": int(time.time() * 1000),
        "active": {
            "strategy_id": selected_strategy_id,
            "strategy_ids": active_strategy_ids,
            "instance_ids": active_instance_ids,
            "mode": str(runtime_scheduler.get("mode", "multi_active") or "multi_active"),
            "arbiter": str(runtime_scheduler.get("arbiter", "allocation-weighted") or "allocation-weighted"),
            "reason": str(runtime_scheduler.get("reason", "runtime_scheduler_snapshot") or "runtime_scheduler_snapshot"),
            "selected_strategy_id": selected_strategy_id,
            "selected_instance_id": selected_instance_id,
            "route_family": route_family,
            "market_context": market_context,
            "config_revision": config_revision,
            "effective_params_path": str(_resolve_runtime_artifact("logs", "effective_params.json") or (LOGS_DIR / "effective_params.json")),
        },
        "runtime_scheduler": runtime_scheduler,
        "registered": registered,
        "manifests": [],
        "loaded": active_strategy_ids,
        "health": health,
        "selection_diagnostics": {
            "selected_strategy_id": selected_strategy_id,
            "selected_instance_id": selected_instance_id,
            "route_family": route_family,
            "market_context": market_context,
            "candidate_strategy_ids": _clean_str_list(
                route_payload.get("candidate_strategy_ids")
                or route_payload.get("compatible_strategy_ids")
                or route_payload.get("routed_strategy_ids")
            ),
            "selection": selection,
            "candidate_fit_map": candidate_fit_map,
            "session_scheduler": session_scheduler,
            "summary": selection_summary,
        },
    }


def _summarize_selection_diagnostics(
    selection_diagnostics: dict[str, Any],
    *,
    runtime_scheduler: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    if not isinstance(selection_diagnostics, dict):
        selection_diagnostics = {}
    runtime_scheduler = runtime_scheduler if isinstance(runtime_scheduler, dict) else {}
    selection = (
        selection_diagnostics.get("selection", {})
        if isinstance(selection_diagnostics.get("selection", {}), dict)
        else {}
    )
    candidate_fit_map = (
        selection_diagnostics.get("candidate_fit_map", {})
        if isinstance(selection_diagnostics.get("candidate_fit_map", {}), dict)
        else {}
    )
    selected_strategy_id = str(selection_diagnostics.get("selected_strategy_id", "") or "")
    selected_instance_id = str(selection_diagnostics.get("selected_instance_id", "") or "")
    route_family = str(selection_diagnostics.get("route_family", "") or "")
    market_context = (
        selection_diagnostics.get("market_context", {})
        if isinstance(selection_diagnostics.get("market_context", {}), dict)
        else {}
    )
    selection_passed = bool(selection.get("passed", False))
    selection_reason = str(selection.get("reason", "") or "")
    risk_decision = (
        runtime_scheduler.get("risk_decision", {})
        if isinstance(runtime_scheduler.get("risk_decision", {}), dict)
        else {}
    )
    execution_decision = (
        runtime_scheduler.get("execution_decision", {})
        if isinstance(runtime_scheduler.get("execution_decision", {}), dict)
        else {}
    )
    risk_code = str(
        risk_decision.get("risk_code")
        or execution_decision.get("risk_code")
        or execution_decision.get("riskCode")
        or ""
    )
    eligible_candidate_ids: list[str] = []
    blocked_candidate_ids: list[str] = []
    blocker_counts: dict[str, int] = {}
    blocker_priority = {
        "entry_family_mismatch": 0,
        "market_family_mismatch": 1,
        "market_phase_soft_mismatch": 2,
        "confidence_low": 3,
        "session_match": 4,
        "volatility_match": 5,
    }

    def _reason_rank(reason: str) -> tuple[int, str]:
        prefix = str(reason or "").split(":", 1)[0]
        return blocker_priority.get(prefix, 9), str(reason or "")

    for strategy_id, fit_result in candidate_fit_map.items():
        if not isinstance(fit_result, dict):
            continue
        normalized_strategy_id = str(strategy_id or "").strip()
        if not normalized_strategy_id:
            continue
        if bool(fit_result.get("eligible", False)):
            eligible_candidate_ids.append(normalized_strategy_id)
            continue
        blocked_candidate_ids.append(normalized_strategy_id)
        reasons = fit_result.get("reasons", [])
        if not isinstance(reasons, list):
            continue
        for reason in reasons:
            normalized_reason = str(reason or "").strip()
            if not normalized_reason:
                continue
            blocker_counts[normalized_reason] = blocker_counts.get(normalized_reason, 0) + 1

    top_blockers = [
        {"reason": reason, "count": count}
        for reason, count in sorted(
            blocker_counts.items(),
            key=lambda item: (-item[1], _reason_rank(item[0])[0], item[0]),
        )[:3]
    ]
    selected_owner_only = bool(selected_strategy_id and not selection_passed)
    if risk_code:
        status = "risk_blocked"
    elif selection_passed and selected_strategy_id:
        status = "selected"
    elif blocked_candidate_ids and not eligible_candidate_ids:
        status = "no_eligible_candidate"
    elif selected_owner_only:
        status = "owner_only"
    else:
        status = "pending"
    headline_parts = []
    if selection_reason:
        headline_parts.append(selection_reason)
    if top_blockers:
        headline_parts.append(top_blockers[0]["reason"])
    if risk_code:
        headline_parts.append(risk_code)
    headline = " | ".join(part for part in headline_parts if part)
    return {
        "status": status,
        "route_family": route_family,
        "selection_passed": selection_passed,
        "selection_reason": selection_reason,
        "risk_code": risk_code,
        "selected_strategy_id": selected_strategy_id,
        "selected_instance_id": selected_instance_id,
        "selected_owner_only": selected_owner_only,
        "eligible_candidate_ids": eligible_candidate_ids,
        "blocked_candidate_ids": blocked_candidate_ids,
        "top_blockers": top_blockers,
        "market_regime": str(market_context.get("regime", "") or ""),
        "selection_regime": str(market_context.get("selection_regime", "") or ""),
        "session_bucket": str(market_context.get("session_bucket", "") or ""),
        "headline": headline,
    }


def _merge_runtime_strategy_status_payload(
    raw_payload: dict[str, Any],
    synthesized_payload: dict[str, Any],
) -> dict[str, Any]:
    if not isinstance(raw_payload, dict) or not raw_payload:
        return synthesized_payload if isinstance(synthesized_payload, dict) else {}
    if not isinstance(synthesized_payload, dict) or not synthesized_payload:
        return raw_payload
    merged = dict(raw_payload)
    for key in ("active", "runtime_scheduler", "selection_diagnostics"):
        if isinstance(synthesized_payload.get(key), dict) and synthesized_payload.get(key):
            merged[key] = synthesized_payload.get(key)
    synthesized_loaded = synthesized_payload.get("loaded", [])
    if isinstance(synthesized_loaded, list) and synthesized_loaded:
        merged["loaded"] = synthesized_loaded
    synthesized_health = synthesized_payload.get("health", {})
    if isinstance(synthesized_health, dict) and synthesized_health:
        merged["health"] = synthesized_health
    if not isinstance(merged.get("registered", []), list) or not merged.get("registered", []):
        merged["registered"] = synthesized_payload.get("registered", [])
    if not isinstance(merged.get("manifests", []), list) or not merged.get("manifests", []):
        merged["manifests"] = synthesized_payload.get("manifests", [])
    merged["ts_ms"] = max(
        int(raw_payload.get("ts_ms", 0) or 0),
        int(synthesized_payload.get("ts_ms", 0) or 0),
    )
    return merged


def _runtime_strategy_status_payload() -> dict[str, Any]:
    payload = _read_json_file(_runtime_strategy_status_file())
    synthesized = _synthesize_runtime_strategy_status_payload()
    if isinstance(payload, dict) and payload:
        return _merge_runtime_strategy_status_payload(payload, synthesized)
    return synthesized


def _read_json_from_candidates(paths: list[pathlib.Path]) -> tuple[Optional[pathlib.Path], Dict[str, Any]]:
    path = _resolve_freshest_existing_file(paths)
    if path is None:
        return None, {}
    return path, _read_json_file(path)


def _file_mtime_ts(path: Optional[pathlib.Path]) -> float:
    if path is None:
        return 0.0
    try:
        return float(path.stat().st_mtime)
    except Exception:
        return 0.0


def _runtime_activity_anchor_ts() -> float:
    anchor_paths = [
        _resolve_runtime_root_artifact("blackboard_state.json"),
        _resolve_runtime_artifact("logs", "strategy_runtime_status.json"),
        _resolve_runtime_artifact("logs", "runtime_scheduler_snapshot.json"),
        _resolve_runtime_artifact("logs", "effective_params.json"),
        _resolve_runtime_artifact("data", "execution_state.json"),
        _resolve_runtime_artifact("data", "position_lifecycle.json"),
        _resolve_runtime_artifact("config", "strategy_market_state.json"),
    ]
    return max((_file_mtime_ts(path) for path in anchor_paths), default=0.0)


def _entry_event_ts(entry: dict) -> Optional[float]:
    ts = entry.get("ts")
    if isinstance(ts, (int, float)):
        return float(ts)
    raw_timestamp = str(entry.get("timestamp", "")).strip()
    if raw_timestamp:
        try:
            return datetime.datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00")).timestamp()
        except Exception:
            pass
    inferred = entry.get("_sort_ts")
    if isinstance(inferred, datetime.datetime):
        return inferred.timestamp()
    return None


def _is_event_fresh(entry_ts: Optional[float], *, anchor_ts: float, max_age_sec: float) -> bool:
    if anchor_ts <= 0:
        return True
    if entry_ts is None or entry_ts <= 0:
        return False
    return (anchor_ts - entry_ts) <= max_age_sec


def _active_trader_workspace_log_candidates(name: str) -> list[pathlib.Path]:
    now = time.time()
    cached_paths = _ACTIVE_TRADER_LOG_CACHE.get("paths", {})
    if (
        isinstance(cached_paths, dict)
        and (now - float(_ACTIVE_TRADER_LOG_CACHE.get("ts", 0.0))) < 3.0
        and name in cached_paths
    ):
        cached = cached_paths.get(name)
        return list(cached) if isinstance(cached, list) else []

    candidates = _runtime_scope_candidates("logs", name)

    if not isinstance(cached_paths, dict):
        cached_paths = {}
    cached_paths[name] = list(candidates)
    if len(cached_paths) > _ACTIVE_TRADER_LOG_CACHE_MAX_KEYS:
        cached_paths = dict(list(cached_paths.items())[-_ACTIVE_TRADER_LOG_CACHE_MAX_KEYS:])
    _ACTIVE_TRADER_LOG_CACHE["ts"] = now
    _ACTIVE_TRADER_LOG_CACHE["paths"] = cached_paths
    return candidates


def _runtime_log_candidates(name: str) -> list[pathlib.Path]:
    candidates = [
        *_active_trader_workspace_log_candidates(name),
    ]
    # Always check the runtime data directory first (where pipeline writes logs).
    candidates.append(APP_DIR / "logs" / name)
    # Only consult the repo workspace when the GUI is actually running from the repo.
    if APP_DIR.resolve() == REPO_ROOT.resolve():
        candidates.append(REPO_ROOT / "logs" / name)
    else:
        candidates.extend(_managed_release_log_candidates(name))
    return _dedupe_paths(candidates)


def _read_text_from_candidates(paths: list[pathlib.Path], encoding: str = "utf-8") -> tuple[Optional[pathlib.Path], str]:
    path = _resolve_existing_file(paths)
    if path is None:
        return None, ""
    try:
        return path, path.read_text(encoding=encoding)
    except PermissionError:
        return None, ""
    except Exception:
        return None, ""


def _ensure_runtime_import_paths() -> None:
    candidates = [
        RELEASE_ROOT,
        RELEASE_ROOT / "src",
        REPO_ROOT,
        REPO_ROOT / "src",
    ]
    for candidate in candidates:
        try:
            candidate_str = str(candidate)
            if candidate.exists() and candidate_str not in sys.path:
                sys.path.insert(0, candidate_str)
        except Exception:
            continue


def _load_global_blackboard():
    _ensure_runtime_import_paths()
    try:
        from src.swarm.blackboard import global_blackboard

        return global_blackboard
    except Exception:
        return None


def _existing_log_candidates(
    paths: list[pathlib.Path],
    *,
    preserve_priority: bool = False,
) -> list[pathlib.Path]:
    existing: list[pathlib.Path] = []
    for path in paths:
        try:
            if path.exists() and path.is_file():
                existing.append(path)
        except Exception:
            continue
    if not preserve_priority:
        existing.sort(key=lambda p: (p.stat().st_size > 0, p.stat().st_mtime), reverse=True)
    return existing


def _resolve_log_targets(log_type: str) -> list[pathlib.Path]:
    today_log = f"gold_trader.{time.strftime('%Y%m%d')}.log"
    if log_type == "daemon":
        candidates = [
            *_runtime_log_candidates("gold_trader.daemon.log"),
            RELEASE_ROOT / "lobsterquant_trader.log",
            RELEASE_ROOT / "bridge" / "logs" / "gold_trader.daemon.log",
            REPO_ROOT / "bridge" / "logs" / "gold_trader.daemon.log",
            pathlib.Path(DAEMON_LOG),
        ]
    elif log_type == "system":
        candidates = [
            *_runtime_log_candidates("runtime.log"),
            *_runtime_log_candidates(today_log),
            RELEASE_ROOT / "lobsterquant_trader.log",
            APP_DIR / "logs" / "gui_backend.manual.log",
            APP_DIR / "logs" / "supervisor.launchd.log",
            *_runtime_log_candidates("gold_trader.daemon.log"),
            pathlib.Path(DAEMON_LOG),
        ]
    else:
        candidates = []
    return _existing_log_candidates(candidates, preserve_priority=True)


def _resolve_signal_pipeline_log_file() -> Optional[pathlib.Path]:
    direct_candidates = _runtime_log_candidates("runtime.log")
    direct_match = _resolve_freshest_existing_file(direct_candidates)
    if direct_match is not None:
        return direct_match

    rollover_candidates: list[pathlib.Path] = []
    seen_dirs: set[str] = set()
    for candidate in direct_candidates:
        parent = candidate.parent
        try:
            parent_key = str(parent.resolve())
        except Exception:
            parent_key = str(parent)
        if parent_key in seen_dirs or not _is_readable_dir(parent):
            continue
        seen_dirs.add(parent_key)
        try:
            for log_path in parent.glob("gold_trader.*.log"):
                if ".daemon." in log_path.name:
                    continue
                rollover_candidates.append(log_path)
        except Exception:
            continue
    return _resolve_freshest_existing_file(rollover_candidates)


def _resolve_gold_trader_daily_log_file() -> Optional[pathlib.Path]:
    today_log = f"gold_trader.{time.strftime('%Y%m%d')}.log"
    direct_candidates = _runtime_log_candidates(today_log)
    direct_match = _resolve_freshest_existing_file(direct_candidates)
    if direct_match is not None:
        return direct_match

    rollover_candidates: list[pathlib.Path] = []
    seen_dirs: set[str] = set()
    for candidate in direct_candidates:
        parent = candidate.parent
        try:
            parent_key = str(parent.resolve())
        except Exception:
            parent_key = str(parent)
        if parent_key in seen_dirs or not _is_readable_dir(parent):
            continue
        seen_dirs.add(parent_key)
        try:
            for log_path in parent.glob("gold_trader.*.log"):
                if ".daemon." in log_path.name:
                    continue
                rollover_candidates.append(log_path)
        except Exception:
            continue
    best = _resolve_freshest_existing_file(rollover_candidates)
    # Fallback: pipeline writes runtime.log, not gold_trader.*.log
    if best is None:
        rt_candidates = _runtime_log_candidates("runtime.log")
        best = _resolve_freshest_existing_file(rt_candidates)
    return best


def _resolve_gold_trader_bridge_log_files() -> list[pathlib.Path]:
    candidates: list[pathlib.Path] = []
    daily_log = _resolve_gold_trader_daily_log_file()
    if daily_log is not None:
        candidates.append(daily_log)
    daemon_candidates = _resolve_log_targets("daemon")
    if daemon_candidates:
        candidates.append(daemon_candidates[0])
    return _dedupe_paths(candidates)


def _format_log_time_label(value: Any, *, fallback_ts: float = 0.0) -> str:
    ts = _parse_timestamp_value(value)
    if ts is None and fallback_ts > 0:
        ts = fallback_ts
    if ts is not None and ts > 0:
        try:
            return datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S")
        except Exception:
            pass
    raw = str(value or "").strip()
    if raw.startswith("[") and "]" in raw:
        return raw[1:].split("]", 1)[0]
    return raw or "--:--:--"


def _pipeline_log_level(entry: dict[str, Any]) -> str:
    execution_report = entry.get("execution_report", {}) if isinstance(entry.get("execution_report", {}), dict) else {}
    status = str(execution_report.get("status", "") or "").upper()
    incidents = entry.get("incidents", []) if isinstance(entry.get("incidents", []), list) else []
    if status in {"REJECTED", "TIMEOUT", "VETOED", "FAILED"}:
        return "WARN"
    if incidents:
        return "WARN"
    return "INFO"


def _pipeline_side_label(side: str) -> str:
    normalized = str(side or "").upper()
    return {
        "BUY": "做多",
        "SELL": "做空",
        "LONG": "做多",
        "SHORT": "做空",
        "NONE": "无方向",
        "UNKNOWN": "未知方向",
    }.get(normalized, normalized or "未知方向")


def _pipeline_action_label(action: str, side: str) -> str:
    normalized_action = str(action or "").upper()
    open_side_label = {
        "BUY": "多单",
        "LONG": "多单",
        "SELL": "空单",
        "SHORT": "空单",
    }.get(str(side or "").upper(), f"{_pipeline_side_label(side)}持仓")
    if normalized_action == "OPEN":
        return f"策略尝试开{open_side_label}"
    if normalized_action == "CLOSE":
        return f"策略尝试平掉{open_side_label}"
    if normalized_action == "MODIFY":
        return f"策略尝试调整{open_side_label}"
    if normalized_action == "REDUCE":
        return f"策略尝试减仓{open_side_label}"
    if normalized_action == "HOLD":
        return "本轮未触发开仓动作"
    return f"{normalized_action} {open_side_label}".strip()


def _pipeline_entry_style_label(entry_style: str, materialized_action: str = "") -> str:
    normalized_style = str(entry_style or "").upper()
    normalized_action = str(materialized_action or "").upper()
    if normalized_style == "PULLBACK_LIMIT":
        return "回调限价挂单"
    if normalized_action == "OPEN_LIMIT" or normalized_style == "LIMIT":
        return "限价挂单"
    if normalized_action == "ARM_WAIT_CONFIRMATION" or normalized_style == "CONFIRMATION_ONLY":
        return "等待确认"
    if normalized_action == "OPEN_MARKET" or normalized_style == "MARKET":
        return "市价直入"
    return normalized_style or normalized_action or "未标注"


def _pipeline_pending_entry_summary(
    latest_signal: dict[str, Any],
    status_payload: dict[str, Any],
) -> str:
    active_soft_limits = (
        status_payload.get("soft_limits", [])
        if isinstance(status_payload.get("soft_limits", []), list)
        else []
    )
    entry_style = str(latest_signal.get("entryStyle", "") or "")
    materialized_action = str(latest_signal.get("materializedAction", "") or "")
    label = _pipeline_entry_style_label(entry_style, materialized_action)
    if active_soft_limits:
        return "回调限价挂单" if label == "未标注" else label
    return "" if label == "未标注" else label


def _format_confidence_percent_label(value: Any) -> str:
    try:
        confidence = float(value or 0.0)
    except Exception:
        confidence = 0.0
    if confidence <= 1.0:
        confidence *= 100.0
    confidence = max(0.0, min(100.0, confidence))
    return f"{confidence:.1f}%"


def _pipeline_status_label(status: str) -> str:
    normalized = str(status or "").upper()
    return {
        "ACKED": "EA已确认",
        "REJECTED": "已拒绝",
        "BLOCKED": "已阻断",
        "CREATED": "已生成",
        "VETOED": "已否决",
        "FAILED": "执行失败",
        "ERROR": "执行异常",
        "FILLED": "已成交",
        "SENT": "已发送",
        "PENDING": "待确认",
        "RECONCILED": "已归档",
        "SKIPPED": "已跳过",
        "TIMEOUT": "等待超时",
        "VALIDATED": "已校验",
    }.get(normalized, normalized or "未知状态")


def _pipeline_reason_label(reason: str) -> str:
    normalized = str(reason or "").upper()
    if normalized.startswith("ROUTE_ALLOWED_SIDE_BLOCK"):
        return "当前方向路由不允许执行"
    return {
        "ACTIVE_POSITION_EXISTS": "当前已有活动持仓",
        "AGGREGATED_HOLD_NO_ELIGIBLE_OPPORTUNITY": "当前无可执行机会",
        "DIRECTION_BLOCKED": "宏观方向风控阻断",
        "MACRO_DIRECTION_CONFLICT": "宏观方向与信号冲突",
        "LOW_CONFIDENCE": "信号置信度不足",
        "LIMIT_ONLY": "当前环境只允许保守执行",
        "MARKET_DATA_DELAY_GATE": "行情数据延迟，已暂停执行",
        "NO_SIGNAL": "当前无有效信号",
        "NO_ACTIONABLE_SETUP": "当前没有合格入场形态",
        "POST_CLOSE_COOLDOWN": "平仓冷却中",
        "RISK_BLOCKED": "风险闸门阻断",
        "WAIT_CONFIRMATION": "等待确认后再执行",
    }.get(normalized, str(reason or "-"))


def _pipeline_reason_code(reason: str) -> str:
    raw = str(reason or "").strip()
    if not raw:
        return ""
    upper_raw = raw.upper()
    known_codes = [
        "AGGREGATED_HOLD_NO_ELIGIBLE_OPPORTUNITY",
        "ROUTE_ALLOWED_SIDE_BLOCK",
        "NO_ACTIONABLE_SETUP",
        "ACTIVE_POSITION_EXISTS",
        "DIRECTION_BLOCKED",
        "MACRO_DIRECTION_CONFLICT",
        "LOW_CONFIDENCE",
        "LIMIT_ONLY",
        "MARKET_DATA_DELAY_GATE",
        "NO_SIGNAL",
        "POST_CLOSE_COOLDOWN",
        "RISK_BLOCKED",
        "WAIT_CONFIRMATION",
    ]
    for code in known_codes:
        if code in upper_raw:
            return code
    return upper_raw


def _pipeline_display_reason(raw_reason: str, blocked_reasons: Optional[List[str]] = None) -> tuple[str, str]:
    candidates: list[str] = []
    if isinstance(blocked_reasons, list):
        candidates.extend(str(item or "").strip() for item in blocked_reasons if str(item or "").strip())
    raw_text = str(raw_reason or "").strip()
    if raw_text:
        candidates.append(raw_text)
    for candidate in candidates:
        code = _pipeline_reason_code(candidate)
        if code:
            return _pipeline_reason_label(code), code
    return _pipeline_reason_label(raw_text or "-"), raw_text or "-"


def _pipeline_strategy_summary(arbitration: dict[str, Any]) -> tuple[str, str]:
    summary = arbitration.get("summary", {}) if isinstance(arbitration.get("summary", {}), dict) else {}
    contributors = arbitration.get("contributors", []) if isinstance(arbitration.get("contributors", []), list) else []
    active_strategy_ids: list[str] = []
    for item in contributors:
        if not isinstance(item, dict):
            continue
        strategy_id = str(item.get("strategyId", "") or "").strip()
        if strategy_id and strategy_id not in active_strategy_ids:
            active_strategy_ids.append(strategy_id)
    selected_strategy_id = str(summary.get("selectedStrategyId", "") or "").strip()
    selection = arbitration.get("selection", {}) if isinstance(arbitration.get("selection", {}), dict) else {}
    if not selected_strategy_id:
        selected_strategy_id = str(selection.get("winnerStrategyId", "") or "").strip()
    if not selected_strategy_id and contributors:
        first = contributors[0] if isinstance(contributors[0], dict) else {}
        selected_strategy_id = str(first.get("strategyId", "") or "").strip()
    return ", ".join(active_strategy_ids[:4]), selected_strategy_id


def _pipeline_policy_sentence(macro: str, advice: str, structure: str, strategy_id: str) -> str:
    macro_label = _pipeline_side_label(macro)
    structure_label = _pipeline_side_label(structure)
    advice_label = {
        "LIMIT_ONLY": "仅允许保守执行",
        "BLOCK_ALL": "当前禁止自动执行",
        "NORMAL": "允许正常执行",
        "UNKNOWN": "执行建议未明确",
    }.get(str(advice or "").upper(), str(advice or "UNKNOWN"))
    return (
        f"策略 {strategy_id} 当前看到的环境是：宏观偏{macro_label}，"
        f"局部结构偏{structure_label}，执行建议为“{advice_label}”。"
    )


def _pipeline_guard_sentence(open_guard: dict[str, Any], reason: str) -> str:
    allow_open = bool(open_guard.get("allow_open", False))
    guard_reason = str(open_guard.get("reason", reason) or reason)
    active_ticket = str(open_guard.get("active_ticket", "") or "").strip()
    active_side = str(open_guard.get("active_side", "") or "").strip()
    if allow_open:
        return f"执行闸门放行，本轮信号允许继续进入下单阶段。（{guard_reason or 'ALLOW_OPEN'}）"
    reason_label = _pipeline_reason_label(guard_reason)
    if active_side and active_ticket:
        return (
            f"执行闸门拦截：检测到已有{_pipeline_side_label(active_side)}持仓 {active_ticket}，"
            f"因此禁止继续反向开仓。（{guard_reason}）"
        )
    if active_side:
        return (
            f"执行闸门拦截：当前已有{_pipeline_side_label(active_side)}持仓在场，"
            f"本轮信号不允许继续开仓。（{guard_reason}）"
        )
    return f"执行闸门拦截：{reason_label}。（{guard_reason}）"


def _format_pipeline_runtime_lines(log_path: pathlib.Path, lines: int) -> list[str]:
    entry_limit = max(12, min(160, max(lines, 1)))
    entries = _read_ndjson_tail(log_path, limit=entry_limit)
    rendered: list[str] = []
    for entry in entries:
        if not isinstance(entry, dict):
            continue
        signal = entry.get("signal", {}) if isinstance(entry.get("signal", {}), dict) else {}
        execution_report = entry.get("execution_report", {}) if isinstance(entry.get("execution_report", {}), dict) else {}
        trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
        policy = trace.get("policy", {}) if isinstance(trace.get("policy", {}), dict) else {}
        open_guard = execution_report.get("open_guard", {}) if isinstance(execution_report.get("open_guard", {}), dict) else {}
        fallback_ts = 0.0
        try:
            fallback_ts = float(log_path.stat().st_mtime)
        except Exception:
            fallback_ts = 0.0
        time_label = _format_log_time_label(entry.get("timestamp"), fallback_ts=fallback_ts)
        action = str(signal.get("action", "HOLD") or "HOLD").upper()
        side = str(signal.get("side", "NONE") or "NONE").upper()
        confidence = float(trace.get("confidence", 0.0) or 0.0)
        status = str(execution_report.get("status", "PENDING") or "PENDING").upper()
        reason = str(execution_report.get("reason", "") or signal.get("reject_reason", "") or "-")
        macro = str(
            policy.get("binding_macro_direction")
            or policy.get("effective_macro_direction")
            or policy.get("macro_direction")
            or "UNKNOWN"
        ).upper()
        advice = str(policy.get("execution_advice", "UNKNOWN") or "UNKNOWN").upper()
        structure = str(policy.get("structural_side", "UNKNOWN") or "UNKNOWN").upper()
        strategy_id = str(signal.get("strategy_id", signal.get("selected_strategy_id", "-")) or "-")
        effects = policy.get("effects", []) if isinstance(policy.get("effects", []), list) else []
        incidents = entry.get("incidents", []) if isinstance(entry.get("incidents", []), list) else []
        auto_actions = entry.get("auto_actions", []) if isinstance(entry.get("auto_actions", []), list) else []
        rendered.append(
            f"[{time_label}] [{_pipeline_log_level(entry)}] [PipelineDecision] "
            f"{_pipeline_action_label(action, side)}，信号置信度 {confidence:.2f}；"
            f"结果：{_pipeline_status_label(status)}（{_pipeline_reason_label(reason)} / {status} / {reason}）"
        )
        rendered.append(
            f"[{time_label}] [INFO] [PipelinePolicy] "
            f"{_pipeline_policy_sentence(macro, advice, structure, strategy_id)} "
            f"(macro={macro} | advice={advice} | structure={structure} | strategy={strategy_id})"
        )
        if effects:
            rendered.append(
                f"[{time_label}] [INFO] [PipelineEffects] {', '.join(str(item) for item in effects[:4])}"
            )
        if open_guard:
            active_ticket = str(open_guard.get("active_ticket", "") or "").strip()
            active_side = str(open_guard.get("active_side", "") or "").strip()
            guard_bits = [
                f"allow_open={str(bool(open_guard.get('allow_open', False))).lower()}",
                f"reason={str(open_guard.get('reason', reason) or reason)}",
            ]
            if active_side:
                guard_bits.append(f"active_side={active_side}")
            if active_ticket:
                guard_bits.append(f"ticket={active_ticket}")
            rendered.append(
                f"[{time_label}] [WARN] [PipelineGuard] "
                f"{_pipeline_guard_sentence(open_guard, reason)} "
                f"({' | '.join(guard_bits)})"
            )
        if incidents:
            rendered.append(
                f"[{time_label}] [WARN] [PipelineIncident] "
                + " | ".join(str(item) for item in incidents[:3])
            )
        if auto_actions:
            rendered.append(
                f"[{time_label}] [INFO] [PipelineAction] "
                + " | ".join(str(item) for item in auto_actions[:3])
            )
    return rendered[-max(lines, 1) :]


def _filter_gold_trader_execution_lines(lines: list[str]) -> list[str]:
    markers = (
        "EA report:",
        "[OK] POSITION OPENED",
        "[OK] PARTIAL CLOSED:",
        "[OK] CLOSED:",
        "EA REJECTED:",
        "[WARN]️ PARTIAL_CLOSE 被拒",
        "[GREEN] 持仓监控启动",
        "[RED] 持仓退出",
        "出场完成，已平仓",
        "EA 响应延迟",
        "to_ea积压",
        "MODIFY_SL",
        "pending confirm",
        "confirmed by MT5",
        "Signal poll deferred:",
        "OPEN timeout pending confirm",
        "Skip duplicate OPEN:",
        "无法读取 MT5 持仓状态",
        "MT5持仓存在但entry=0",
        "监控退出：已平仓",
        "监控循环异常",
        "Review request:",
        "Reconciliation:",
        "Potential ghost ticket=",
        "Waiting for MT5 sync",
        "ticket=",
        "position",
        "positions_count",
        "terminal_status",
        "agent_state",
        "持仓",
        "仓位",
    )
    filtered: list[str] = []
    last_normalized = ""
    for line in lines:
        normalized = str(line).strip()
        if not normalized:
            continue
        if "[Pipeline]" in normalized or "[MarketInterpreter]" in normalized or "[DirectionGate]" in normalized:
            continue
        normalized_lower = normalized.lower()
        if not any(
            marker in normalized if marker == marker.lower() else marker in normalized
            for marker in markers
        ):
            # Reconciliation / position-state lines often use mixed free text rather
            # than the legacy execution markers above. Keep them visible in the
            # MT5 execution panel so live position sync changes are not hidden.
            if not (
                "reconciliation" in normalized_lower
                or "ghost ticket=" in normalized_lower
                or "mt5 sync" in normalized_lower
                or "position" in normalized_lower
                or "positions_count" in normalized_lower
                or "terminal_status" in normalized_lower
                or "agent_state" in normalized_lower
                or "持仓" in normalized
                or "仓位" in normalized
            ):
                continue
        if (
            "failed to resolve bridge layout" in normalized_lower
            and "src.ea_bridge_paths" in normalized_lower
        ):
            continue
        if normalized == last_normalized:
            continue
        last_normalized = normalized
        filtered.append(line)
    return filtered


def _extract_log_line_sort_key(line: str) -> tuple[int, int, str]:
    normalized = str(line).strip()
    if normalized.startswith("[") and "]" in normalized:
        token = normalized[1:].split("]", 1)[0]
        seconds = _parse_hms_seconds(token)
        if seconds is not None:
            return (0, seconds, normalized)
    try:
        head = normalized.split(" ", 2)
        if len(head) >= 2:
            dt = datetime.datetime.fromisoformat(f"{head[0]}T{head[1].split(',')[0]}")
            return (1, dt.hour * 3600 + dt.minute * 60 + dt.second, normalized)
    except Exception:
        pass
    return (2, 0, normalized)


def _system_log_status_payload() -> dict[str, Any]:
    with _STATUS_CACHE_LOCK:
        cached = _STATUS_RESPONSE_CACHE.get("value")
    if isinstance(cached, dict) and cached:
        return dict(cached)
    try:
        return _build_status_payload()
    except Exception:
        return _build_status_fallback_payload()


def _build_pipeline_position_snapshot_line() -> Optional[str]:
    status_payload = _system_log_status_payload()
    execution_state = _runtime_execution_state_payload()
    positions = status_payload.get("positions", []) if isinstance(status_payload.get("positions", []), list) else []
    ts = time.time()
    price_source_path = str(status_payload.get("priceSourcePath", "") or "")
    if price_source_path:
        try:
            p = pathlib.Path(price_source_path)
            if p.exists():
                ts = float(p.stat().st_mtime)
        except Exception:
            pass
    time_label = datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S")
    active_signal_count = 0
    try:
        signals = execution_state.get("signals", {}) if isinstance(execution_state.get("signals", {}), dict) else {}
        terminal_states = {"FILLED", "REJECTED", "VETOED", "TIMEOUT", "RECONCILED"}
        active_signal_count = sum(
            1
            for item in signals.values()
            if isinstance(item, dict) and str(item.get("state", "CREATED") or "CREATED").upper() not in terminal_states
        )
    except Exception:
        active_signal_count = 0

    if not bool(status_payload.get("positionOpen", False)):
        signal_rows = _collect_execution_signal_rows(limit=6)
        latest_signal = None
        arbitration = None
        signal_candidates = [
            candidate
            for candidate in signal_rows.get("active", []) + signal_rows.get("recent", [])
            if isinstance(candidate, dict)
        ]
        signal_candidates.sort(key=lambda item: float(item.get("latestTs", 0.0) or 0.0), reverse=True)
        if signal_candidates:
            latest_signal = signal_candidates[0]
        latest_ts = float(latest_signal.get("latestTs", 0.0) or 0.0) if isinstance(latest_signal, dict) else 0.0
        age_sec = max(0.0, time.time() - latest_ts) if latest_ts > 0 else 10**9
        if latest_signal is None or age_sec > 1800:
            runtime_log_path = _resolve_existing_file(_runtime_log_candidates("runtime.log"))
            latest_envelope = (
                _read_latest_jsonl_dict_from_path(runtime_log_path)
                if runtime_log_path is not None and runtime_log_path.exists()
                else {}
            )
            signal = latest_envelope.get("signal", {}) if isinstance(latest_envelope.get("signal", {}), dict) else {}
            execution_report = (
                latest_envelope.get("execution_report", {})
                if isinstance(latest_envelope.get("execution_report", {}), dict)
                else {}
            )
            if signal or execution_report:
                latest_time_label = _format_log_time_label(
                    latest_envelope.get("timestamp"),
                    fallback_ts=ts,
                )
                action = str(signal.get("action", "HOLD") or "HOLD").upper()
                side = str(signal.get("side", "NONE") or "NONE").upper()
                state = str(
                    execution_report.get("status")
                    or signal.get("governor_status")
                    or "UNKNOWN"
                ).upper()
                strategy_id = str(
                    signal.get("strategy_id")
                    or signal.get("selected_strategy_id")
                    or ""
                ).strip()
                raw_reason = str(
                    execution_report.get("reason")
                    or signal.get("materialized_reason")
                    or signal.get("reject_reason")
                    or "-"
                ).strip() or "-"
                blocked_reasons = (
                    execution_report.get("hold_diagnostics", {}).get("blocked_reasons", [])
                    if isinstance(execution_report.get("hold_diagnostics", {}), dict)
                    and isinstance(execution_report.get("hold_diagnostics", {}).get("blocked_reasons", []), list)
                    else []
                )
                reason_label, reason_code = _pipeline_display_reason(raw_reason, blocked_reasons)
                arbitration = _build_runtime_arbitration_payload()
                active_strategy_ids, selected_strategy_id = _pipeline_strategy_summary(arbitration)
                if not strategy_id:
                    strategy_id = selected_strategy_id
                entry_summary = _pipeline_entry_style_label(
                    str(signal.get("entry_style", "") or ""),
                    str(signal.get("materialized_action", "") or ""),
                )
                return (
                    f"[{latest_time_label}] [INFO] 当前信号: {_pipeline_action_label(action, side)}"
                    f"{f' | 策略={strategy_id}' if strategy_id else ''}"
                    f"{f' | 活跃策略={active_strategy_ids}' if active_strategy_ids else ''}"
                    f"{f' | 选中策略={selected_strategy_id}' if selected_strategy_id and selected_strategy_id != strategy_id else ''}"
                    f"{f' | 入场={entry_summary}' if entry_summary and entry_summary != '未标注' else ''}"
                    f" | 状态={_pipeline_status_label(state)}"
                    f" | 原因={reason_label} / {reason_code}"
                    f" | active_signals={active_signal_count}"
                    f" | source=pipeline_signal_state"
                )
            return None
        latest_time_label = str(latest_signal.get("latestTime", "") or "").strip() or time_label
        action = str(latest_signal.get("action", "HOLD") or "HOLD").upper()
        side = str(latest_signal.get("side", "NONE") or "NONE").upper()
        state = str(latest_signal.get("state", "UNKNOWN") or "UNKNOWN").upper()
        strategy_id = str(latest_signal.get("strategyId", "") or "").strip()
        raw_reason = str(latest_signal.get("reason", "") or "").strip()
        blocked_reasons: list[str] = []
        active_strategy_ids = ""
        selected_strategy_id = ""
        if not raw_reason:
            arbitration = _build_runtime_arbitration_payload()
            summary = arbitration.get("summary", {}) if isinstance(arbitration.get("summary", {}), dict) else {}
            blocked_reasons = (
                summary.get("blockedReasons", [])
                if isinstance(summary.get("blockedReasons", []), list)
                else []
            )
            if not strategy_id:
                strategy_id = str(summary.get("selectedStrategyId", "") or "").strip()
            raw_reason = str(
                (blocked_reasons[0] if blocked_reasons else "")
                or summary.get("reason", "")
                or ""
            ).strip()
        if not strategy_id and arbitration is None:
            arbitration = _build_runtime_arbitration_payload()
        if arbitration is None:
            arbitration = _build_runtime_arbitration_payload()
        if isinstance(arbitration, dict):
            summary = arbitration.get("summary", {}) if isinstance(arbitration.get("summary", {}), dict) else {}
            if not blocked_reasons:
                blocked_reasons = (
                    summary.get("blockedReasons", [])
                    if isinstance(summary.get("blockedReasons", []), list)
                    else []
                )
            active_strategy_ids, selected_strategy_id = _pipeline_strategy_summary(arbitration)
            if not strategy_id:
                strategy_id = str(summary.get("selectedStrategyId", "") or "").strip() or selected_strategy_id
        reason_label, reason_code = _pipeline_display_reason(raw_reason or "-", blocked_reasons)
        entry_summary = _pipeline_pending_entry_summary(latest_signal, status_payload)
        return (
            f"[{latest_time_label}] [INFO] 当前信号: {_pipeline_action_label(action, side)}"
            f"{f' | 策略={strategy_id}' if strategy_id else ''}"
            f"{f' | 活跃策略={active_strategy_ids}' if active_strategy_ids else ''}"
            f"{f' | 选中策略={selected_strategy_id}' if selected_strategy_id and selected_strategy_id != strategy_id else ''}"
            f"{f' | 入场={entry_summary}' if entry_summary else ''}"
            f" | 状态={_pipeline_status_label(state)}"
            f" | 原因={reason_label} / {reason_code}"
            f" | active_signals={active_signal_count}"
            f" | source=pipeline_signal_state"
        )

    first = positions[0] if positions and isinstance(positions[0], dict) else {}
    side = str(
        status_payload.get("positionSide")
        or first.get("side")
        or first.get("type")
        or "UNKNOWN"
    ).upper()
    ticket = str(first.get("ticket", "") or "-")
    volume = float(first.get("volume", first.get("lots", 0.0)) or 0.0)
    entry = float(
        status_payload.get("positionEntry")
        or first.get("entry")
        or first.get("entry_price")
        or 0.0
    )
    pnl = float(first.get("pnl", 0.0) or 0.0)
    bid = float(status_payload.get("bid", 0.0) or 0.0)
    ask = float(status_payload.get("ask", 0.0) or 0.0)
    quote_origin = str(status_payload.get("quoteOrigin", "unknown") or "unknown")
    price_source = str(status_payload.get("priceSource", "unknown") or "unknown")
    strategy_id = str(
        first.get("entry_owner_strategy_id")
        or first.get("strategy_id")
        or first.get("current_manager_strategy_id")
        or first.get("position_strategy_id")
        or ""
    ).strip()
    return (
        f"[{time_label}] [INFO] 监控 #{ticket}: {side}"
        f" bid={bid:.2f} ask={ask:.2f} | 主仓浮盈~${pnl:.2f}"
        f" | open_positions={max(1, len(positions))} | pending_signals={active_signal_count}"
        f" | entry={entry:.2f} lots={volume:.2f}"
        f"{f' | 开仓策略={strategy_id}' if strategy_id else ''}"
        f" | source=pipeline_status/{price_source}/{quote_origin}"
    )


def _build_pipeline_open_context_line() -> Optional[str]:
    status_payload = _system_log_status_payload()
    if not bool(status_payload.get("positionOpen", False)):
        return None
    execution_state = _runtime_execution_state_payload()
    positions = status_payload.get("positions", []) if isinstance(status_payload.get("positions", []), list) else []
    first = positions[0] if positions and isinstance(positions[0], dict) else {}
    target_strategy_id = str(
        first.get("entry_owner_strategy_id")
        or first.get("strategy_id")
        or first.get("current_manager_strategy_id")
        or first.get("position_strategy_id")
        or ""
    ).strip()
    target_side = str(
        status_payload.get("positionSide")
        or first.get("side")
        or first.get("type")
        or ""
    ).upper()
    signals = execution_state.get("signals", {}) if isinstance(execution_state.get("signals", {}), dict) else {}
    best_match: Optional[dict[str, Any]] = None
    best_ts = 0.0
    for signal_id, entry in signals.items():
        if not isinstance(entry, dict):
            continue
        if str(entry.get("state", "") or "").upper() != "FILLED":
            continue
        history = entry.get("history", []) if isinstance(entry.get("history", []), list) else []
        latest = history[-1] if history else {}
        latest_metadata = (
            latest.get("metadata", {})
            if isinstance(latest, dict) and isinstance(latest.get("metadata", {}), dict)
            else {}
        )
        metadata = entry.get("metadata", {}) if isinstance(entry.get("metadata", {}), dict) else {}
        action = str(
            latest_metadata.get("action")
            or metadata.get("action")
            or entry.get("action")
            or ""
        ).upper()
        if action != "OPEN":
            continue
        strategy_id = str(
            latest_metadata.get("entry_owner_strategy_id")
            or latest_metadata.get("attributed_strategy_id")
            or latest_metadata.get("strategy_id")
            or latest_metadata.get("selected_strategy_id")
            or metadata.get("entry_owner_strategy_id")
            or metadata.get("attributed_strategy_id")
            or metadata.get("strategy_id")
            or metadata.get("selected_strategy_id")
            or entry.get("strategy_id")
            or ""
        ).strip()
        side = str(
            latest_metadata.get("side")
            or metadata.get("side")
            or entry.get("side")
            or ""
        ).upper()
        if target_strategy_id and strategy_id and strategy_id != target_strategy_id:
            continue
        if target_side and side and side != target_side:
            continue
        contributors = (
            latest_metadata.get("contributors", [])
            if isinstance(latest_metadata.get("contributors", []), list)
            else metadata.get("contributors", [])
            if isinstance(metadata.get("contributors", []), list)
            else []
        )
        first_contributor = contributors[0] if contributors and isinstance(contributors[0], dict) else {}
        confidence = (
            first_contributor.get("confidence")
            or latest_metadata.get("requested_confidence")
            or latest_metadata.get("confidence")
            or metadata.get("requested_confidence")
            or metadata.get("confidence")
            or 0.0
        )
        latest_ts = _parse_timestamp_value(
            latest.get("ts") if isinstance(latest, dict) else None
        ) or _parse_timestamp_value(entry.get("updated_at")) or _parse_timestamp_value(entry.get("created_at")) or 0.0
        if latest_ts < best_ts:
            continue
        best_ts = latest_ts
        best_match = {
            "signalId": str(entry.get("signal_id") or signal_id),
            "timeLabel": _format_log_time_label(
                latest.get("ts") if isinstance(latest, dict) else "",
                fallback_ts=latest_ts,
            ),
            "strategyId": strategy_id or target_strategy_id,
            "side": side or target_side,
            "entryStyle": str(
                latest_metadata.get("entry_style")
                or metadata.get("entry_style")
                or entry.get("entry_style")
                or ""
            ),
            "materializedAction": str(
                latest_metadata.get("materialized_action")
                or metadata.get("materialized_action")
                or entry.get("materialized_action")
                or ""
            ),
            "confidence": confidence,
        }
    if not isinstance(best_match, dict):
        return None
    strategy_id = str(best_match.get("strategyId", "") or "").strip()
    entry_style = str(best_match.get("entryStyle", "") or "")
    materialized_action = str(best_match.get("materializedAction", "") or "")
    return (
        f"[{best_match['timeLabel']}] [INFO] 开仓信息: "
        f"{_pipeline_action_label('OPEN', str(best_match.get('side', '') or ''))}"
        f"{f' | 策略={strategy_id}' if strategy_id else ''}"
        f" | 开仓置信度={_format_confidence_percent_label(best_match.get('confidence', 0.0))}"
        f"{f' | 入场={_pipeline_entry_style_label(entry_style, materialized_action)}' if entry_style or materialized_action else ''}"
        f" | signal={best_match['signalId']}"
        f" | source=pipeline_open_context"
    )


def _remember_position_monitor_line(line: Optional[str]) -> None:
    normalized = str(line or "").strip()
    if not normalized:
        return
    with _POSITION_MONITOR_HISTORY_LOCK:
        if _POSITION_MONITOR_HISTORY and _POSITION_MONITOR_HISTORY[-1] == normalized:
            return
        _POSITION_MONITOR_HISTORY.append(normalized)
        if len(_POSITION_MONITOR_HISTORY) > 120:
            del _POSITION_MONITOR_HISTORY[:-120]


def _recent_position_monitor_lines(limit: int) -> list[str]:
    safe_limit = max(1, min(120, int(limit or 20)))
    with _POSITION_MONITOR_HISTORY_LOCK:
        return list(_POSITION_MONITOR_HISTORY[-safe_limit:])


def _read_gold_trader_bridge_progress_lines(lines: int) -> tuple[str, list[str]]:
    rendered: list[str] = []
    source_paths: list[str] = []
    for path in _resolve_gold_trader_bridge_log_files():
        source_paths.append(str(path))
        if path.name == "gold_trader.daemon.log":
            raw_lines = _read_tail_lines(path, max_lines=max(lines * 10, 500), max_bytes=300000)
        else:
            raw_lines = _read_tail_lines(path, max_lines=max(lines * 8, 400), max_bytes=300000)
        rendered.extend(_filter_gold_trader_execution_lines(raw_lines))
    position_snapshot_line = _build_pipeline_position_snapshot_line()
    if position_snapshot_line:
        _remember_position_monitor_line(position_snapshot_line)
        rendered.append(position_snapshot_line)
    open_context_line = _build_pipeline_open_context_line()
    if open_context_line:
        _remember_position_monitor_line(open_context_line)
        rendered.append(open_context_line)
    rendered.extend(_recent_position_monitor_lines(max(lines, 12)))
    deduped: list[str] = []
    seen: set[str] = set()
    for line in sorted(rendered, key=_extract_log_line_sort_key):
        normalized = str(line).strip()
        if normalized in seen:
            continue
        seen.add(normalized)
        deduped.append(line)
    source = " | ".join(source_paths)
    return source, deduped[-max(lines, 1) :]


def _filter_log_lines(log_type: str, lines: list[str]) -> list[str]:
    if log_type != "system":
        return lines
    filtered: list[str] = []
    startup_lines: list[str] = []
    last_normalized = ""
    for line in lines:
        normalized = str(line).strip()
        if not normalized:
            continue
        if "MallocStackLogging: can't turn off malloc stack logging because it was not enabled." in normalized:
            continue
        if normalized.startswith("Traceback (most recent call last):"):
            continue
        if normalized.startswith("File \"") and ", line " in normalized:
            continue
        if normalized.startswith("INFO:     ") and " - \"" in normalized:
            continue
        if normalized.startswith("await ") or normalized.startswith("raise "):
            continue
        # Keep at most one startup line as fallback; don't let it dominate.
        if "[GREEN] 系统启动 | PID=" in normalized:
            startup_lines.append(line)
            continue
        if normalized.startswith("[daemon] started PID "):
            startup_lines.append(line)
            continue
        # Collapse contiguous duplicates to keep realtime panel readable.
        if normalized == last_normalized:
            continue
        last_normalized = normalized
        filtered.append(line)
    if filtered:
        return filtered
    if startup_lines:
        return [startup_lines[-1]]
    return filtered


def _filter_system_event_lines(lines: list[str]) -> list[str]:
    markers = (
        "监控 #",
        "高频监控:",
        "[OK] POSITION OPENED",
        "自动执行:",
        "Signal 仓位调整",
        "[MarketInterpreter]",
        "[Gate]",
        "[DynamicAdj]",
        "EA report:",
        "FILLED OPEN",
        "OPEN timeout pending confirm",
        "Fallback",
        "Signal:",
        "BREAKOUT BUY",
        "BREAKOUT SELL",
        "持仓",
        "仓位",
    )
    filtered: list[str] = []
    last_normalized = ""
    for line in lines:
        normalized = str(line or "").strip()
        if not normalized:
            continue
        normalized_lower = normalized.lower()
        if "mallocstacklogging" in normalized_lower:
            continue
        if normalized == last_normalized:
            continue
        if not any(marker in normalized for marker in markers):
            continue
        last_normalized = normalized
        filtered.append(line)
    return filtered


def _read_system_event_lines(lines: int) -> tuple[str, list[str]]:
    candidates = _resolve_log_targets("system")
    pipeline_path = _resolve_signal_pipeline_log_file()
    merged_lines: list[str] = []
    supplemental_lines: list[str] = []
    used_sources: list[str] = []
    line_limit = max(lines * 12, 800)

    if pipeline_path is not None and pipeline_path.exists():
        pipeline_raw = _format_pipeline_runtime_lines(pipeline_path, max(lines * 6, 180))
        pipeline_filtered = _filter_log_lines("system", _filter_system_event_lines(pipeline_raw))
        if pipeline_filtered:
            merged_lines.extend(pipeline_filtered)
            used_sources.append(str(pipeline_path))

    for path in candidates:
        if pipeline_path is not None and str(path) == str(pipeline_path):
            continue
        path_name = path.name
        raw_lines = _read_tail_lines(path, max_lines=line_limit, max_bytes=500000)
        filtered = _filter_system_event_lines(raw_lines)
        filtered = _filter_log_lines("system", filtered)
        if not filtered:
            continue
        supplemental_lines.extend(filtered)
        used_sources.append(str(path))

    merged_lines.extend(supplemental_lines)
    deduped: list[str] = []
    seen: set[str] = set()
    for line in sorted(merged_lines, key=_extract_log_line_sort_key):
        normalized = str(line).strip()
        if not normalized or normalized in seen:
            continue
        seen.add(normalized)
        deduped.append(line)
    merged_lines = deduped
    position_snapshot_line = _build_pipeline_position_snapshot_line()
    if position_snapshot_line:
        _remember_position_monitor_line(position_snapshot_line)
        merged = list(merged_lines)
        merged.append(position_snapshot_line)
        open_context_line = _build_pipeline_open_context_line()
        if open_context_line:
            _remember_position_monitor_line(open_context_line)
            merged.append(open_context_line)
        merged.extend(_recent_position_monitor_lines(max(lines, 12)))
        merged_lines = []
        seen = set()
        for line in sorted(merged, key=_extract_log_line_sort_key):
            normalized = str(line).strip()
            if not normalized or normalized in seen:
                continue
            seen.add(normalized)
            merged_lines.append(line)
    source_parts: list[str] = []
    if pipeline_path is not None and pipeline_path.exists():
        source_parts.append(f"pipeline:{pipeline_path}")
    supplemental_paths = [src for src in dict.fromkeys(used_sources) if pipeline_path is None or src != str(pipeline_path)]
    if supplemental_paths:
        source_parts.append("supplement:" + " | ".join(supplemental_paths))
    source_parts.append("snapshot:pipeline_status")
    source = " || ".join(source_parts)
    return source, merged_lines[-max(lines, 1) :]


def _score_log_candidate(log_type: str, path: pathlib.Path, lines: list[str]) -> tuple[int, float, int]:
    meaningful_lines = [
        line for line in lines if str(line).strip() and not str(line).strip().startswith(f"[{log_type}] source:")
    ]
    startup_only = all(
        str(line).strip().startswith("[daemon] started PID ")
        or "[GREEN] 系统启动 | PID=" in str(line).strip()
        for line in meaningful_lines
    )
    try:
        stat = path.stat()
        mtime = float(stat.st_mtime)
        size = int(stat.st_size)
    except Exception:
        mtime = 0.0
        size = 0
    score = len(meaningful_lines) * 3
    if meaningful_lines and not startup_only:
        score += 1000
    if log_type == "system":
        path_name = path.name
        age_sec = max(0.0, time.time() - mtime) if mtime > 0 else 0.0
        # Signal Pipeline tiered scoring — system.log shows the trading
        # system's signal evaluation and trade execution pipeline.
        # Tier 1: gold_trader daily log — the canonical signal pipeline
        #          (DynamicAdj / MarketInterpreter / DirectionGate).
        # Tier 2: runtime.log — pipeline JSONL output (realtime fallback).
        # Tier 3: lobsterquant_trader.log — pipeline stdout (buffered).
        # Tier 4: daemon log (>=10KB) — watchdog/worker heartbeat fallback.
        if path_name.startswith("gold_trader.") and path_name.endswith(".log") and ".daemon." not in path_name:
            score += 2000
        elif path_name == "runtime.log":
            score += 1500
        elif path_name == "lobsterquant_trader.log":
            score += 1200
        elif path_name == "gold_trader.daemon.log":
            if size >= 10000:
                score += 800
            else:
                score -= 1200
        trade_markers = (
            "EA report: FILLED OPEN",
            "[OK] POSITION OPENED",
            "[GREEN] 持仓监控启动",
            "[RED] 持仓退出",
            "[OK] CLOSED:",
            "[OK] PARTIAL CLOSED:",
            "出场完成，已平仓",
        )
        trade_line_hits = sum(1 for line in meaningful_lines if any(marker in str(line) for marker in trade_markers))
        if trade_line_hits > 0:
            score += 800 + min(300, trade_line_hits * 80)
        # Time decay scales inversely with content volume — a rich file stays
        # relevant longer, a nearly-empty file decays fast.
        line_count = max(1, len(meaningful_lines))
        # Content-aware hard-decay threshold: richer files stay relevant longer.
        hard_decay_sec = max(1800, 300 + line_count * 60)
        if age_sec > hard_decay_sec:
            score -= 4000
        elif age_sec > 300:
            decay_per_tick = max(5, 90 // line_count)
            score -= min(3000, int((age_sec - 300) // 30) * decay_per_tick)
    return score, mtime, size


def _read_swarm_jsonl_logs(limit: int = 200) -> list[dict]:
    logs: list[dict] = []
    swarm_dir = _resolve_active_swarm_dir()
    if not swarm_dir:
        return logs
    for log_file in sorted(
        swarm_dir.glob("*.jsonl"),
        key=lambda path: (path.stat().st_mtime, path.name),
    ):
        logs.extend(_load_swarm_entries_with_inferred_ts(log_file, max_lines=limit))

    logs.sort(key=_entry_sort_key)
    return logs[-limit:]


def _has_trustworthy_mailbox_timestamp(entry: dict) -> bool:
    if isinstance(entry.get("ts"), (int, float)):
        return True
    raw_timestamp = str(entry.get("timestamp", "")).strip()
    if raw_timestamp:
        try:
            datetime.datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00"))
            return True
        except Exception:
            return False
    inferred = entry.get("_sort_ts")
    return isinstance(inferred, datetime.datetime)


def _read_latest_risk_block_event() -> dict:
    log_paths = [
        LOGS_DIR / f"gold_trader.{time.strftime('%Y%m%d')}.log",
        pathlib.Path("/tmp/gold_trader.log"),
    ]
    now = time.time()
    latest_line = ""
    latest_file = ""
    latest_mtime = 0.0
    latest_size = 0
    for p in log_paths:
        if not p.exists():
            continue
        try:
            stat = p.stat()
            mtime = float(stat.st_mtime)
            size = int(stat.st_size)
        except Exception:
            mtime = 0.0
            size = 0
        if (
            _RISK_BLOCK_CACHE.get("value") is not None
            and str(_RISK_BLOCK_CACHE.get("file", "")) == str(p)
            and float(_RISK_BLOCK_CACHE.get("mtime", 0.0)) == mtime
            and int(_RISK_BLOCK_CACHE.get("size", 0)) == size
            and (now - float(_RISK_BLOCK_CACHE.get("ts", 0.0))) < 3.0
        ):
            cached_value = _RISK_BLOCK_CACHE.get("value")
            return dict(cached_value) if isinstance(cached_value, dict) else _default_risk_block_payload()
        try:
            lines = _read_tail_lines(p)
        except Exception:
            continue
        for line in reversed(lines[-3000:]):
            if "RiskManager BLOCKED:" in line:
                latest_line = line
                latest_file = str(p)
                latest_mtime = mtime
                latest_size = size
                break

    if not latest_line:
        payload = _default_risk_block_payload()
        _RISK_BLOCK_CACHE.update({
            "ts": now,
            "file": latest_file,
            "mtime": latest_mtime,
            "size": latest_size,
            "value": payload,
        })
        return payload

    reason = latest_line.split("RiskManager BLOCKED:", 1)[1].strip()
    if " [" in reason:
        reason = reason.split(" [", 1)[0].strip()

    mapped = _map_risk_block_reason(reason)
    block_time = ""
    if latest_line.startswith("[") and "]" in latest_line:
        block_time = latest_line[1:].split("]", 1)[0]
    payload = {
        "exists": True,
        "rawReason": reason,
        "categoryCode": mapped["categoryCode"],
        "categoryLabel": mapped["categoryLabel"],
        "summary": mapped["summary"],
        "time": block_time,
        "line": latest_line,
    }
    payload = _decorate_decision_hall_event(payload, block_time)
    if not payload.get("fresh"):
        payload = _default_risk_block_payload()
    _RISK_BLOCK_CACHE.update({
        "ts": now,
        "file": latest_file,
        "mtime": latest_mtime,
        "size": latest_size,
        "value": payload,
    })
    return payload

def _query_mt5_daily() -> dict:
    """
    直接向 MT5 EA 发 EXPORT_HISTORY 请求，等待最多 5 秒，
    解析当天已平仓成交的 real profit，返回每日统计。
    加入智能缓存机制：如果持仓数量没变，且未到 60 秒，直接返回缓存；
    一旦检测到持仓数减少（发生平仓），立即穿透缓存重新查询。
    """
    import threading, queue, json  # datetime/time already global

    global _MT5_DAILY_CACHE
    now = time.time()
    
    # 获取当前的持仓数量
    current_positions_count = _read_terminal_status_positions_count() or 0

    # 缓存失效条件：超过 60 秒，或者持仓数量变少了（说明有单子平掉了，必须刷新历史）
    # 或者持仓数量变多了（说明新开仓了，虽然不影响历史，但我们顺便刷新）
    positions_changed = current_positions_count != _MT5_DAILY_CACHE.get("last_positions_count", -1)
    if not positions_changed and (now - _MT5_DAILY_CACHE["updated_at"] < 60):
        return dict(_MT5_DAILY_CACHE["data"])

    _MT5_DAILY_CACHE["last_positions_count"] = current_positions_count

    now_ts = int(time.time())

    result_queue = queue.Queue()

    def _do_query():
        try:
            sig_id = f"daily_{int(now_ts * 1000)}"
            data = export_history_via_ea(APP_DIR, days=2, timeout_s=5.0, signal_id=sig_id)
            if data is None:
                result_queue.put(None)
                return

            # 解析 deals，计算今日统计
            entries = data.get("deals", [])
            trades = 0; pnl = 0.0
            wins = 0; losses = 0
            win_pnl = 0.0; loss_pnl = 0.0

            # 核心修正：寻找这批订单里最新的时间，以此作为 MT5 服务器当前时间的锚点
            # 然后计算出这个锚点对应服务器当天的 00:00:00 的时间戳。
            max_ts = 0
            for entry in entries:
                t = int(entry.get("time", 0))
                if t > max_ts:
                    max_ts = t
            
            if max_ts == 0:
                # 兜底：如果完全没有历史，就取当前系统时间加 3 小时当做服务器时间
                max_ts = int(time.time()) + 3 * 3600
                
            # =========================================================================
            # 彻底解决跨时区与夏令时(DST)导致“今日”数据截断的持久性方案：
            # 无论 MT5 处于东欧时间、UTC 还是经历了夏令时切换，
            # 只要以过去 24 小时（86400秒）内的数据，或者当前本地时间的自然日为准，
            # 这里统一采用：以当前最新的订单时间往前推算“今天”。
            # 由于时区偏移问题可能导致本地 00:00 时 MT5 还在昨天，
            # 最稳妥的方法是利用 MT5 传来的最新时间戳(max_ts)所处的“自然日”来卡点。
            # =========================================================================
            import datetime
            # 将 MT5 最新的时间戳转换为 datetime 对象（这里先不考虑时区，只当成一个绝对流逝秒数）
            # 或者更符合交易员直觉的：直接获取距离当前物理时间 24 小时内的订单。
            # 但为了严格对应“今日面板”，我们计算：获取本地电脑自然日的 00:00。
            # 因为不论 MT5 是什么时间，用户在电脑前看到的“今天”就是电脑的今天。
            # 考虑到 MT5 传回来的 `time` 其实是 EA 里通过 `TimeCurrent()` 生成并直接当整数发过来的
            # 它是“经纪商服务器的本地时间（例如 2026-03-27 10:00:00 被硬当做 Unix 时间戳）”
            # 这会导致它和真实的 UTC 时间戳存在固定偏差（夏令时会变）。
            
            # MT5 的 time 字段是服务器本地时间直接当成 Unix 时间戳存进来的（”伪 UTC”）。
            # 对这种值，取 86400 的余数切除，就得到服务器当天的 00:00:00，
            # 无需任何 datetime/时区转换。
            server_today_start_ts = max_ts - (max_ts % 86400)

            for entry in entries:
                # 过滤条件1：只统计我们 EA 的订单 (magic=1024)
                magic = int(entry.get("magic", 0))
                if magic != 1024:
                    continue

                # 过滤条件2：只统计出场交易 (entry_type: 1 = OUT, 2 = INOUT)
                entry_type = int(entry.get("entry_type", 0))
                if entry_type not in (1, 2):
                    continue
                    
                entry_time_ts = int(entry.get("time", 0))
                if not entry_time_ts:
                    continue
                    
                # 过滤条件3：必须大于等于服务器的当日 00:00:00
                if entry_time_ts < server_today_start_ts:
                    continue

                profit = float(entry.get("profit", 0) or 0)
                commission = float(entry.get("commission", 0) or 0)
                swap = float(entry.get("swap", 0) or 0)
                fee = float(entry.get("fee", 0) or 0)
                net_profit = profit + commission + swap + fee

                trades += 1
                pnl += net_profit
                if net_profit > 0:
                    wins += 1
                    win_pnl += net_profit
                elif net_profit < 0:
                    losses += 1
                    loss_pnl += net_profit

            wr = round(wins / trades * 100, 1) if trades > 0 else 0.0
            aw = round(win_pnl / wins, 2) if wins > 0 else 0.0
            al = round(loss_pnl / losses, 2) if losses > 0 else 0.0

            result_queue.put(_normalize_trade_stats({
                "trades": trades, "pnl": round(pnl, 2),
                "wins": wins, "losses": losses,
                "win_rate": wr, "avg_win": aw, "avg_loss": al,
            }, ok=True, source="mt5_export_history"))
        except Exception:
            result_queue.put(None)

    t = threading.Thread(target=_do_query, daemon=True)
    t.start()
    t.join(timeout=6)

    try:
        result = result_queue.get_nowait()
    except queue.Empty:
        result = None

    if result is None:
        result = _empty_trade_stats(ok=False, source="mt5_export_history_unavailable")

    _MT5_DAILY_CACHE["data"] = dict(result)
    _MT5_DAILY_CACHE["updated_at"] = now
    return dict(result)

PID_FILE = LOGS_DIR / "gold_trader.pid"
MANUAL_STOP_FILE = LOGS_DIR / ".daemon_manual_stop.json"
ENGINE_LIFECYCLE_LOG = LOGS_DIR / "engine_lifecycle.ndjson"
TRAINING_PID_FILE = DATA_DIR / "offline" / "training_pid.txt"
MACRO_REFRESH_PID_FILE = LOGS_DIR / "macro_refresh.pid"
MACRO_REFRESH_STATUS_FILE = LOGS_DIR / "macro_refresh_status.json"
_MACRO_REFRESH_TIMEOUT = 150  # 秒：超过此时间认为线程已卡死，允许重新触发

# Watchdog 进程的锁文件和日志（用于自动重启崩溃的 GoldAutoTradeEngine.exe）
WATCHDOG_LOCK_FILE = LOGS_DIR / "watchdog.lock"
WATCHDOG_LOG = LOGS_DIR / "watchdog.log"
_TODAY_STATS_CACHE: dict = {
    "ts": 0.0,
    "db_mtime": 0.0,
    "data_source": "",  # 数据源指纹：mt5/ledger/sqlite/none，源变即穿透缓存
    "value": {
        "trades": 0,
        "pnl": 0.0,
        "wins": 0,
        "losses": 0,
        "win_rate": 0.0,
        "avg_win": 0.0,
        "avg_loss": 0.0,
    },
}
_STATUS_RESPONSE_CACHE = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
    "thread": None,
    "refresh_started_ts": 0.0,
    "terminal_status_mtime": 0.0,
    "terminal_status_size": 0,
}
_STATUS_CACHE_TTL_SEC = 20.0
_STATUS_CACHE_LOCK = threading.Lock()
_LIGHTWEIGHT_STATUS_CACHE = {
    "ts": 0.0,
    "value": None,
}
_LIGHTWEIGHT_STATUS_CACHE_TTL_SEC = 2.0
_LICENSE_OK = False
_LICENSE_ERROR = ""
_LICENSE_STARTUP_THREAD: Optional[threading.Thread] = None
_STARTUP_TRACE_LOG = LOGS_DIR / "startup_trace.log"
_STATE_LOAD_CACHE = {
    "ts": 0.0,
    "value": {},
    "inflight": False,
    "thread": None,
}
_STATE_LOAD_LOCK = threading.Lock()
_RISK_BLOCK_CACHE = {
    "ts": 0.0,
    "file": "",
    "mtime": 0.0,
    "size": 0,
    "value": None,
}
_BLACKBOARD_CACHE = {
    "ts": 0.0,
    "file": "",
    "mtime": 0.0,
    "size": 0,
    "value": None,
}
_TRADE_EVENTS_CACHE = {
    "ts": 0.0,
    "mtime": 0.0,
    "size": 0,
    "path": "",
    "value": [],
}
_RUNTIME_OBSERVABILITY_CACHE = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
    "thread": None,
    "refresh_started_ts": 0.0,
}
_RUNTIME_OBSERVABILITY_CACHE_TTL_SEC = 8.0  # 蜂群大厅需近实时数据，60s→8s
_RUNTIME_OBSERVABILITY_REFRESH_STUCK_SEC = 60.0
_RUNTIME_OBSERVABILITY_CACHE_LOCK = threading.Lock()
_TRADING_SAFETY_PATROL_THREAD: Optional[threading.Thread] = None
_TRADING_SAFETY_PATROL_STOP_EVENT = threading.Event()
_TRADING_SAFETY_PATROL_INTERVAL_SEC = 6.0
_TRADING_SAFETY_LAST_SIGNATURE = {"value": ""}
_TRAINING_STATUS_RESPONSE_CACHE = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
    "thread": None,
}
_TRAINING_STATUS_CACHE_TTL_SEC = 30.0
_TRAINING_GOVERNANCE_CACHE = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
    "thread": None,
}
_TRAINING_GOVERNANCE_CACHE_TTL_SEC = 30.0
_TRAINING_APPROVAL_CHAIN_CACHE = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
    "thread": None,
}
_TRAINING_APPROVAL_CHAIN_CACHE_TTL_SEC = 30.0
_TRAINING_APPROVAL_PENDING_MAX_AGE_SEC = 24 * 3600.0
_GOVERNANCE_LIFECYCLE_CACHE = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
    "thread": None,
}
_GOVERNANCE_LIFECYCLE_CACHE_TTL_SEC = 30.0
_STRATEGY_WORKBENCH_CACHE = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
    "thread": None,
}
_STRATEGY_WORKBENCH_CACHE_TTL_SEC = 60.0
_STRATEGY_MARKETPLACE_CACHE = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
    "thread": None,
}
_STRATEGY_MARKETPLACE_CACHE_TTL_SEC = 60.0
_RUNTIME_INSTANCES_CACHE = {
    "ts": 0.0,
    "value": None,
    "refreshing": False,
    "thread": None,
}
_RUNTIME_INSTANCES_CACHE_TTL_SEC = 45.0
_RUNTIME_ARBITRATION_CACHE = {
    "path": "",
    "mtime": 0.0,
    "size": 0,
    "value": None,
}
_AUX_RESPONSE_CACHE_LOCK = threading.Lock()
_TIMELINE_DIRECTION_MAX_AGE_SEC = 2 * 60 * 60
_TECHNICAL_TREND_CACHE = {
    "ts": 0.0,
    "direction": "NEUTRAL",
    "data": {},
    "refreshing": False,
}
_GOAL_TARGETS_CACHE = {
    "ts": 0.0,
    "value": {},
}
_TRAINING_PNL_HISTORY_CACHE = {
    "mtime": 0.0,
    "value": [],
}
_TRAINING_PNL_HISTORY_MAX_POINTS = 360
_MACRO_REFRESH_THREAD = None
_MACRO_WATCHDOG_THREAD: Optional[threading.Thread] = None
_MACRO_WATCHDOG_STATE = {
    "last_trigger_ts": 0.0,
}
_MACRO_WATCHDOG_MIN_REFRESH_SEC = 300.0
_GUI_BACKGROUND_AUTOPILOT_ENABLED = str(os.getenv("GUI_BACKGROUND_AUTOPILOT", "0") or "0").strip().lower() in {
    "1",
    "true",
    "yes",
    "on",
}
_ENGINE_INSTANCE_MODE = "--engine-instance" in sys.argv
_PANIC_ACTION_THREAD = None
_PANIC_ACTION_STATE = {
    "status": "idle",
    "message": "",
    "updated_at": "",
    "result": {},
}

# 允许跨域(方便前端开发阶段)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

_pipeline_daemon_thread: Optional[threading.Thread] = None  # unused; kept for compat


def _is_daemon_running():
    if PID_FILE.exists():
        try:
            pid = int(PID_FILE.read_text(encoding="utf-8").strip())
            return is_process_running(pid)
        except (ValueError, ProcessLookupError, OSError):
            pass
    return _find_runtime_daemon_pid() is not None


def _find_runtime_daemon_pid() -> Optional[int]:
    """Fallback runtime detector when pidfile is missing or stale."""
    patterns = (
        "gold_trader.py --daemon",
        "gold_trader.py --fg",
        "src/pipeline.py --source ea_feed --runtime-intent live_trade",
        "pipeline.py --source ea_feed --runtime-intent live_trade",
        "--daemon-pipeline",
    )
    try:
        for proc in psutil.process_iter(["pid", "cmdline"]):
            cmdline = proc.info.get("cmdline") or []
            combined = " ".join(str(x) for x in cmdline).lower()
            if any(p in combined for p in patterns):
                return int(proc.info["pid"])
    except Exception:
        return None
    return None


def _read_today_stats_cached(db_path: pathlib.Path) -> dict:
    """Cache today's PnL aggregation to avoid high CPU on frequent UI polling."""
    now = time.time()
    ttl_sec = 15.0
    db_mtime = 0.0
    try:
        if db_path.exists():
            db_mtime = float(db_path.stat().st_mtime)
    except Exception:
        db_mtime = 0.0

    # 优先检查 mtime：DB 有写入时立即穿透缓存，不等待 TTL
    db_mtime_changed = db_mtime != _TODAY_STATS_CACHE.get("db_mtime")
    if not db_mtime_changed and (now - float(_TODAY_STATS_CACHE.get("ts", 0.0))) < ttl_sec:
        return dict(_TODAY_STATS_CACHE.get("value", {}))

    stats = _empty_trade_stats()
    used_source = "none"

    mt5_stats = _normalize_trade_stats(_query_mt5_daily(), source="mt5_export_history")
    if mt5_stats.get("_ok"):
        stats = mt5_stats
        used_source = "mt5"
    else:
        ledger_file = _resolve_existing_file(_runtime_log_candidates("trading_ledger.ndjson"))
        ledger_stats = _read_today_stats_from_trading_ledger(ledger_file)
        if ledger_stats.get("_ok"):
            stats = ledger_stats
            used_source = "ledger"
        elif db_path.exists():
            try:
                import sqlite3

                today_start_ts = int(_today_local_midnight_epoch())
                # 状态接口不能被 DB 锁阻塞数秒；锁冲突时快速失败并使用缓存。
                conn = sqlite3.connect(str(db_path), timeout=0.2)
                conn.execute("PRAGMA journal_mode=WAL")
                c = conn.cursor()
                table = c.execute(
                    "SELECT name FROM sqlite_master WHERE type='table' AND name='ledger_kv'"
                ).fetchone()
                rows = []
                if table:
                    c.execute("SELECT value FROM ledger_kv WHERE updated_at >= ?", (today_start_ts,))
                    rows = c.fetchall()
                conn.close()

                pnl_sum = 0.0
                wins = 0
                losses = 0
                win_pnl = 0.0
                loss_pnl = 0.0
                trades = 0
                for row in rows:
                    try:
                        data = json.loads(row[0])
                        if "close_pnl" in data and "ticket" in data:
                            pnl = float(data.get("close_pnl", 0.0))
                            if abs(pnl) < 0.01:
                                continue
                            trades += 1
                            pnl_sum += pnl
                            if pnl > 0:
                                wins += 1
                                win_pnl += pnl
                            else:
                                losses += 1
                                loss_pnl += pnl
                    except Exception:
                        pass

                stats = _normalize_trade_stats(
                    {
                        "trades": trades,
                        "pnl": round(pnl_sum, 2),
                        "wins": wins,
                        "losses": losses,
                        "win_rate": round((wins / trades) * 100, 1) if trades > 0 else 0.0,
                        "avg_win": round(win_pnl / wins, 2) if wins > 0 else 0.0,
                        "avg_loss": round(loss_pnl / losses, 2) if losses > 0 else 0.0,
                    },
                    ok=bool(table),
                    source="ledger_kv",
                )
                used_source = "sqlite"
            except Exception:
                stats = _empty_trade_stats(ok=False, source="today_stats_fallback_error")

    # 数据源变更时强制刷新（不返回旧源数据）
    prev_source = _TODAY_STATS_CACHE.get("data_source", "")
    source_changed = prev_source and prev_source != used_source
    if source_changed and not stats.get("_ok"):
        # 新源无数据时仍然尝试返回旧源缓存
        cached_value = _TODAY_STATS_CACHE.get("value", {})
        if cached_value.get("_ok"):
            return dict(cached_value)

    _TODAY_STATS_CACHE.update({
        "ts": now, "db_mtime": db_mtime, "value": stats, "data_source": used_source,
    })
    return dict(stats)


def _load_goal_targets_cached() -> dict:
    now = time.time()
    ttl_sec = 30.0
    if (now - float(_GOAL_TARGETS_CACHE.get("ts", 0.0))) < ttl_sec:
        cached = _GOAL_TARGETS_CACHE.get("value", {})
        return dict(cached) if isinstance(cached, dict) else {}
    try:
        from src.metrics_agg import build_tenant_metrics

        tenant_metrics = build_tenant_metrics(APP_DIR, "default")
        goal_targets = tenant_metrics.get("goal_targets", {}) if isinstance(tenant_metrics, dict) else {}
        if not isinstance(goal_targets, dict):
            goal_targets = {}
    except Exception:
        goal_targets = {}
    _GOAL_TARGETS_CACHE.update({"ts": now, "value": goal_targets})
    return dict(goal_targets)

def _set_manual_stop(enabled: bool):
    if enabled:
        MANUAL_STOP_FILE.write_text(json.dumps({"manual_stop": True, "ts": time.time()}, ensure_ascii=False), encoding="utf-8")
    else:
        MANUAL_STOP_FILE.unlink(missing_ok=True)

def _is_manual_stop_requested() -> bool:
    return MANUAL_STOP_FILE.exists()


def _record_engine_lifecycle(event: str, *, detail: str = "") -> None:
    """Record engine start/stop events with timestamps for troubleshooting."""
    try:
        LOGS_DIR.mkdir(parents=True, exist_ok=True)
        entry = {
            "ts": time.time(),
            "iso": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
            "event": event,
            "detail": detail,
        }
        with ENGINE_LIFECYCLE_LOG.open("a", encoding="utf-8") as fh:
            fh.write(json.dumps(entry, ensure_ascii=False) + "\n")
    except Exception:
        pass


def _watchdog_main():
    """
    Watchdog 主循环：以子进程方式启动 GoldAutoTradeEngine.exe --engine-instance，
    检测到崩溃后自动重启，退避防抖动。用户手动停止（MANUAL_STOP_FILE）则正常退出。
    """
    LOGS_DIR.mkdir(parents=True, exist_ok=True)
    lock_handle = WATCHDOG_LOCK_FILE.open("a+", encoding="utf-8")
    try:
        lock_handle.seek(0)
        flock(lock_handle.fileno(), LOCK_EX | LOCK_NB)
    except BlockingIOError:
        lock_handle.close()
        print("Watchdog lock already held — another watchdog is running")
        sys.exit(0)
    WATCHDOG_LOCK_FILE.with_name("watchdog.lock.owner").write_text(
        f"pid={os.getpid()}\n", encoding="utf-8"
    )

    exe_path = sys.executable
    if not exe_path or not pathlib.Path(exe_path).exists():
        print("Cannot determine executable path for watchdog")
        sys.exit(1)

    backoff = 1
    max_backoff = 30
    stable_window = 300  # 稳定运行 5 分钟后重置退避
    stable_ts = 0.0

    while True:
        if _is_manual_stop_requested():
            _set_manual_stop(False)
            break

        proc = _popen_safe(
            [exe_path, "--engine-instance"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
        )
        with WATCHDOG_LOG.open("a", encoding="utf-8") as log_fh:
            log_fh.write(f"[{datetime.datetime.now()}] Engine started PID={proc.pid}\n")

        exit_code = proc.wait()

        if _is_manual_stop_requested():
            _set_manual_stop(False)
            with WATCHDOG_LOG.open("a", encoding="utf-8") as log_fh:
                log_fh.write(f"[{datetime.datetime.now()}] Clean exit (manual stop)\n")
            break

        now = time.time()
        crash_duration = now - stable_ts if stable_ts else 0
        if crash_duration >= stable_window:
            backoff = 1

        # exit_code 0 = clean shutdown (singleton conflict, graceful exit, etc.)
        # Don't treat as crash — log and retry gently without ramping backoff.
        if exit_code == 0:
            with WATCHDOG_LOG.open("a", encoding="utf-8") as log_fh:
                log_fh.write(
                    f"[{datetime.datetime.now()}] Engine exited cleanly exit_code=0 "
                    f"uptime={crash_duration:.0f}s — restarting in {backoff}s\n"
                )
            time.sleep(backoff)
            backoff = min(backoff, 5)  # cap clean-exit backoff at 5s
            continue

        with WATCHDOG_LOG.open("a", encoding="utf-8") as log_fh:
            log_fh.write(
                f"[{datetime.datetime.now()}] Engine crashed exit_code={exit_code} "
                f"uptime={crash_duration:.0f}s restarting in {backoff}s\n"
            )

        time.sleep(backoff)
        backoff = min(backoff * 2, max_backoff)
        if stable_ts == 0.0:
            stable_ts = now


def _build_live_pipeline_cmd(app_dir: pathlib.Path) -> list[str]:
    # Auto-detect symbol from EA chart via config_manager (frozen-safe MT5 path)
    _symbol = "XAUUSD"
    try:
        from config_manager import get_mt5_dir as _gmd
        _mt5_dir = pathlib.Path(_gmd())
        _ts = _mt5_dir / "gold_autotrade" / "bridge" / "terminal_status.json"
        if not _ts.exists():
            _ts = _mt5_dir / "gold_autotrade" / "bridge" / "market_snapshot.json"
        if _ts.exists():
            _snap = json.loads(_ts.read_text(encoding="utf-8"))
            _detected = str(_snap.get("symbol", "") or "").strip().upper()
            if _detected and _detected != "XAUUSD":
                _symbol = _detected
                # Log the symbol switch for observability
                try:
                    _lpath = (_mt5_dir / "gold_autotrade" / "bridge").parent.parent / "logs"
                except Exception:
                    _lpath = None
    except Exception:
        pass
    if _symbol != "XAUUSD":
        print(f"[PIPELINE_CMD] detected active symbol: {_symbol}")
    return [
        sys.executable,
        str(_SRC_DIR / "pipeline.py"),
        "--source", "ea_feed",
        "--runtime-intent", "live_trade",
        "--tenant-id", "default",
        "--execution-mode", "ea_bridge",
        "--symbol", _symbol,
        "--timeframe", "M5",
        "--input-path", str(app_dir / "data" / "inbox" / "request.json"),
        "--output-path", str(app_dir / "data" / "outbox" / "signal.json"),
        "--execution-report-path", str(app_dir / "data" / "inbox" / "execution_report.json"),
        "--poll-seconds", "2",
        "--enable-incident-autofix", "true",
    ]


def _record_runtime_guard_issues(source: str, issues: list[dict[str, Any]]) -> None:
    for issue in issues:
        level = str(issue.get("level", "WARNING") or "WARNING").upper()
        code = str(issue.get("code", "runtime_guard") or "runtime_guard")
        message = str(issue.get("message", "") or "").strip()
        if not message:
            continue
        _record_system_message(
            kind="runtime_guard",
            title="交易运行时告警",
            desc=message,
            color="danger" if level == "ERROR" else "warning",
            source=source,
            metadata={"level": level, "code": code},
        )


def _emit_runtime_health_warnings() -> None:
    try:
        from src.runtime_topology import inspect_runtime_config, inspect_runtime_processes

        topology = inspect_runtime_processes(APP_DIR, current_pid=os.getpid())
        runtime_config = inspect_runtime_config(APP_DIR)
        _record_runtime_guard_issues("runtime_topology", topology.get("issues", []))
        _record_runtime_guard_issues("runtime_config", runtime_config.get("issues", []))
    except Exception:
        pass


def _spawn_daemon() -> tuple[bool, str]:
    # Diagnostic trace: log every spawn attempt
    import traceback as _tb
    try:
        _t = LOGS_DIR / "spawn_trace.log"
        _t.parent.mkdir(parents=True, exist_ok=True)
        with open(_t, "a", encoding="utf-8") as _f:
            _f.write(f"\n[{datetime.datetime.now()}] _spawn_daemon() called\n")
            _f.write(f"  argv: {sys.argv[:6]}\n")
            _f.write(f"  GAT_SUBPROCESS: {os.environ.get('GAT_SUBPROCESS', 'NOT SET')}\n")
            _f.write(f"  daemon_running: {_is_daemon_running()}\n")
            for _l in _tb.format_stack()[-5:-1]:
                _f.write(f"  {_l.strip()}\n")
    except Exception:
        pass

    # Ultimate safety: if THIS process is already a daemon subprocess,
    # NEVER spawn another one.  This breaks the infinite window loop
    # even if launcher._is_daemon_subprocess() somehow fails.
    # Uses the canonical list from subprocess_util (single source of truth).
    if _ENGINE_INSTANCE_MODE or any(f in sys.argv for f in DAEMON_SUBPROCESS_FLAGS):
        return False, "Refusing to spawn daemon from daemon subprocess"
    if os.environ.get("GAT_SUBPROCESS", "").strip() == "1":
        return False, "Refusing to spawn daemon from subprocess (env guard)"

    # Hard limit: max 3 daemon spawn attempts per foreground GUI process.
    # Count only real spawn attempts; startup-gate rejections should surface
    # their own reason instead of consuming the retry budget.
    _spawn_tracker = LOGS_DIR / "daemon_spawn_count.txt"
    current_pid = os.getpid()
    count = 0
    def _reset_spawn_tracker() -> None:
        try:
            _spawn_tracker.write_text(
                json.dumps({"pid": current_pid, "count": 0}, ensure_ascii=False),
                encoding="utf-8",
            )
        except Exception:
            pass

    try:
        raw_tracker = _spawn_tracker.read_text(encoding="utf-8").strip()
        if raw_tracker:
            tracker_payload = json.loads(raw_tracker)
            if isinstance(tracker_payload, dict):
                tracker_pid = int(tracker_payload.get("pid", 0) or 0)
                tracker_count = int(tracker_payload.get("count", 0) or 0)
                if tracker_pid == current_pid:
                    count = max(0, tracker_count)
            else:
                count = int(tracker_payload or 0)
    except Exception:
        count = 0
    if count >= 3:
        return False, f"Daemon spawn limit reached ({count} attempts) — refusing to spawn"
    if _is_daemon_running():
        return False, "System already running"

    try:
        from src.startup_gate import run_runtime_startup_gate

        gate = run_runtime_startup_gate(project_root=APP_DIR, app_dir=APP_DIR, current_pid=os.getpid())
        if not gate.get("ok", False):
            runtime_mode = str(gate.get("runtime_mode", "BOOTSTRAP_REPAIR") or "BOOTSTRAP_REPAIR")
            _record_system_message(
                kind="runtime_guard",
                title="交易运行时启动被拦截",
                desc=f"{runtime_mode}: " + ("; ".join(gate.get("failures", [])[:3]) or "startup gate blocked live runtime"),
                color="danger",
                source="startup_gate",
                metadata={
                    "gate": gate,
                    "runtime_mode": runtime_mode,
                    "permissions": gate.get("permissions", {}),
                },
            )
            return False, f"Live runtime blocked by startup gate ({runtime_mode})"
    except Exception as exc:
        _record_system_message(
            kind="runtime_guard",
            title="交易运行时启动门禁异常",
            desc=str(exc),
            color="danger",
            source="startup_gate",
        )
        return False, "Startup gate failed"

    # 单入口：实盘由 src/pipeline.py 统一编排，不再直接启动 bridge/gold_trader.py --daemon
    # Frozen mode: launch via subprocess (same as source mode). The pipeline runs
    # in a separate GoldAutoTradeEngine.exe process to avoid GIL contention, sys.argv
    # races, and crash isolation issues that plague the daemon-thread approach.
    if getattr(sys, 'frozen', False):
        cmd = [sys.executable, "--auto", "--daemon-pipeline"]
    else:
        cmd = _build_live_pipeline_cmd(APP_DIR)

    count += 1
    _spawn_tracker.write_text(
        json.dumps({"pid": current_pid, "count": count}, ensure_ascii=False),
        encoding="utf-8",
    )

    out_fd = None
    err_fd = None
    try:
        env = os.environ.copy()
        env["GAT_SUBPROCESS"] = "1"  # prevents recursive daemon spawning
        existing_pythonpath = str(env.get("PYTHONPATH", "") or "").strip()
        app_dir_str = str(APP_DIR)
        if existing_pythonpath:
            env["PYTHONPATH"] = f"{app_dir_str}{os.pathsep}{existing_pythonpath}"
        else:
            env["PYTHONPATH"] = app_dir_str
        env["LOBSTERQUANT_APP_DIR"] = app_dir_str
        env["GAT_RUNTIME_GATE_PASSED"] = "1"

        pipeline_out = LOGS_DIR / "pipeline_stdout.log"
        pipeline_err = LOGS_DIR / "pipeline_stderr.log"
        pipeline_out.parent.mkdir(parents=True, exist_ok=True)
        out_fd = open(pipeline_out, "a", encoding="utf-8")
        err_fd = open(pipeline_err, "a", encoding="utf-8")

        try:
            popen_kwargs = {
                "stdout": out_fd,
                "stderr": err_fd,
                "env": env,
            }
            if sys.platform != "win32":
                popen_kwargs["start_new_session"] = True
            proc = _popen_safe(cmd, **popen_kwargs)
        except Exception as exc:
            _reset_spawn_tracker()
            _record_system_message(
                kind="pipeline_spawn_error",
                title="交易引擎启动失败",
                desc=f"后台进程未能拉起：{exc}",
                color="error",
                source="pipeline_spawn",
            )
            return False, f"pipeline_spawn_failed:{exc}"

        try:
            PID_FILE.write_text(str(proc.pid), encoding="utf-8")
        except Exception:
            pass

        # Verify the pipeline process stays alive for at least 3 seconds
        # (immediate exit indicates config/license/connect failure)
        import time as _time
        _time.sleep(3.0)
        poll_code = proc.poll()
        if poll_code is not None:
            # Pipeline exited immediately — read stderr for diagnostics
            err_text = ""
            try:
                err_fd.flush()
                with open(pipeline_err, "r", encoding="utf-8", errors="ignore") as _ef:
                    _ef.seek(0, os.SEEK_END)
                    _pos = max(0, _ef.tell() - 2000)
                    _ef.seek(_pos)
                    err_text = _ef.read()[-500:]
            except Exception:
                pass
            _record_system_message(
                kind="pipeline_crash",
                title="交易引擎启动失败",
                desc=f"pipeline 进程在启动 {3.0:.0f} 秒后退出 (exitcode={poll_code})。"
                     f"请检查 MT5 是否运行、EA 是否已附加到图表。\n\n"
                     f"stderr 尾部:\n{err_text[:300] if err_text else '(无输出)'}",
                color="error",
                source="pipeline_health_check",
            )
            _record_engine_lifecycle("start_failed", detail=f"exitcode={poll_code}")
            _reset_spawn_tracker()
            return False, f"pipeline_crashed_exitcode={poll_code}"

        _record_engine_lifecycle("start", detail=f"pid={proc.pid}")
        _reset_spawn_tracker()
    except Exception as exc:
        _reset_spawn_tracker()
        _record_system_message(
            kind="pipeline_spawn_error",
            title="交易引擎启动失败",
            desc=f"后台启动链异常中断：{exc}",
            color="error",
            source="pipeline_spawn",
        )
        return False, f"pipeline_spawn_failed:{exc}"
    finally:
        for _fd in (out_fd, err_fd):
            try:
                if _fd is not None:
                    _fd.close()
            except Exception:
                pass

    return True, "Starting gateway..."

def _ensure_daemon_running(force: bool = False):
    if _is_daemon_running():
        return
    if not force and _is_manual_stop_requested():
        return
    _spawn_daemon()


def _is_pid_alive(pid: int) -> bool:
    try:
        p = psutil.Process(pid)
        return p.is_running() and p.status() != psutil.STATUS_ZOMBIE
    except Exception:
        return False


def _read_macro_refresh_status() -> dict:
    if not MACRO_REFRESH_STATUS_FILE.exists():
        return {}
    try:
        data = json.loads(MACRO_REFRESH_STATUS_FILE.read_text(encoding="utf-8"))
        if not isinstance(data, dict):
            return {}
        pid = int(data.get("pid", 0) or 0)
        status = str(data.get("status", "") or "").lower()
        global _MACRO_REFRESH_THREAD
        named_thread = _find_alive_thread_by_name("macro-refresh-worker")
        if status == "running" and named_thread is not None:
            _MACRO_REFRESH_THREAD = named_thread
        elif status == "running" and _MACRO_REFRESH_THREAD is not None and not _MACRO_REFRESH_THREAD.is_alive():
            data["status"] = "failed"
            data["finished_at"] = datetime.datetime.now().isoformat()
            data["message"] = str(data.get("message", "") or "宏观刷新线程已退出")
            _MACRO_REFRESH_THREAD = None
            _write_macro_refresh_status(data)
        elif status == "running" and pid > 0 and not _is_pid_alive(pid):
            data["status"] = "failed"
            data["finished_at"] = datetime.datetime.now().isoformat()
            data["message"] = str(data.get("message", "") or "宏观刷新任务已退出")
            data["stale_pid"] = pid
            _MACRO_REFRESH_THREAD = None
            try:
                MACRO_REFRESH_PID_FILE.unlink(missing_ok=True)
            except Exception:
                pass
            _write_macro_refresh_status(data)
        return data
    except Exception:
        return {}


def _write_macro_refresh_status(payload: dict):
    try:
        MACRO_REFRESH_STATUS_FILE.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
    except Exception:
        pass


def _finish_macro_refresh_status(status: str, message: str, pid: int = 0, **extra):
    payload = _read_macro_refresh_status()
    payload.update(
        {
            "status": status,
            "message": message,
            "pid": int(pid or payload.get("pid", 0) or 0),
            "finished_at": datetime.datetime.now().isoformat(),
        }
    )
    payload.update(extra)
    _write_macro_refresh_status(payload)
    try:
        current_pid = int(payload.get("pid", 0) or 0)
        if current_pid > 0:
            MACRO_REFRESH_PID_FILE.unlink(missing_ok=True)
    except Exception:
        pass


def _macro_refresh_worker(force: bool = True):
    global _MACRO_REFRESH_THREAD
    pid = os.getpid()
    try:
        from src.deepseek_agent import run_macro_analysis

        run_macro_analysis(force_refresh=force, trigger_reason="hourly")
        _finish_macro_refresh_status(
            "completed",
            "宏观方向刷新完成",
            pid=pid,
            force=force,
        )
    except Exception as exc:
        _finish_macro_refresh_status(
            "failed",
            f"宏观方向刷新失败: {exc}",
            pid=pid,
            force=force,
        )
    finally:
        _MACRO_REFRESH_THREAD = None


def _spawn_macro_refresh(force: bool = True) -> tuple[bool, str, int]:
    global _MACRO_REFRESH_THREAD
    alive_thread = _find_alive_thread_by_name("macro-refresh-worker")
    if alive_thread is not None:
        _MACRO_REFRESH_THREAD = alive_thread
        # 检查是否超时：超过 _MACRO_REFRESH_TIMEOUT 秒则认为卡死，允许重新触发
        import datetime as _dt
        try:
            status = _read_macro_refresh_status()
            started_str = str(status.get("started_at", "") or "")
            if started_str:
                started = _dt.datetime.fromisoformat(started_str)
                elapsed = (_dt.datetime.now() - started).total_seconds()
                if elapsed >= _MACRO_REFRESH_TIMEOUT:
                    return False, f"宏观刷新线程已超时 {elapsed:.0f}s，已阻止重复拉起", os.getpid()
                return False, f"宏观刷新任务正在运行（已运行 {elapsed:.0f}s）", os.getpid()
        except Exception:
            pass
        return False, "宏观刷新任务正在运行", os.getpid()
    if _thread_is_alive(_MACRO_REFRESH_THREAD):
        return False, "宏观刷新任务正在运行", os.getpid()

    pid = os.getpid()
    MACRO_REFRESH_PID_FILE.write_text(str(pid), encoding="utf-8")
    _write_macro_refresh_status(
        {
            "status": "running",
            "force": bool(force),
            "pid": pid,
            "started_at": datetime.datetime.now().isoformat(),
            "message": "宏观方向强制刷新已触发",
        }
    )
    _ensure_singleton_daemon_thread(
        holder_name="_MACRO_REFRESH_THREAD",
        thread_name="macro-refresh-worker",
        target=_macro_refresh_worker,
        args=(bool(force),),
    )
    return True, "宏观方向强制刷新已触发", pid


def _set_panic_action_state(status: str, message: str, result: Optional[dict] = None):
    _PANIC_ACTION_STATE.update(
        {
            "status": str(status or "idle"),
            "message": str(message or ""),
            "updated_at": datetime.datetime.now().isoformat(),
            "result": dict(result or {}),
        }
    )


def _panic_action_worker():
    try:
        from bridge.trader_api import trigger_panic_close_and_block

        result = trigger_panic_close_and_block(reason="MANUAL_PANIC_GUI", source="gui")
        _set_panic_action_state(
            "completed" if bool(result.get("success", False)) else "failed",
            str(result.get("message", "阻断指令已下发")),
            result=result,
        )
    except Exception as exc:
        _set_panic_action_state("failed", f"阻断执行失败: {exc}", result={})


def _spawn_panic_action() -> tuple[bool, str, dict]:
    global _PANIC_ACTION_THREAD
    if _PANIC_ACTION_THREAD is not None and _PANIC_ACTION_THREAD.is_alive():
        return False, "阻断任务正在执行", dict(_PANIC_ACTION_STATE)

    _set_panic_action_state("running", "紧急阻断已触发，正在后台执行", result={})
    _PANIC_ACTION_THREAD = threading.Thread(
        target=_panic_action_worker,
        daemon=True,
        name="panic-action-worker",
    )
    _PANIC_ACTION_THREAD.start()
    return True, "紧急阻断已触发，正在后台执行", dict(_PANIC_ACTION_STATE)

def _daemon_supervisor_loop():
    crash_count = 0
    base_sleep = 15
    while True:
        try:
            was_running = _is_daemon_running()
            _ensure_daemon_running(force=False)
            still_running = _is_daemon_running()
            if was_running and not still_running and not _is_manual_stop_requested():
                _record_engine_lifecycle("stop", detail="unexpected")
            if not was_running and not still_running:
                crash_count += 1
            else:
                crash_count = 0
        except Exception:
            crash_count += 1
        backoff = min(crash_count * base_sleep, 300)
        if crash_count >= 5:
            _record_system_message(
                kind="daemon_crash_loop",
                title="交易守护进程持续崩溃",
                desc=f"daemon 已连续崩溃 {crash_count} 次，暂停 {backoff}s 后重试",
                color="warning",
                source="daemon_supervisor",
            )
        time.sleep(backoff)


def _training_autopilot_loop():
    while True:
        try:
            _run_training_autopilot_step()
        except Exception:
            pass
        time.sleep(8)


def _training_autopilot_worker() -> None:
    global _TRAINING_AUTOPILOT_THREAD
    try:
        _training_autopilot_loop()
    finally:
        _TRAINING_AUTOPILOT_THREAD = None


def _run_macro_watchdog_step() -> None:
    try:
        _status = get_status()
    except Exception:
        return
    direction_data = _status.get("agent_direction_data", {})
    if not isinstance(direction_data, dict):
        return
    macro_age_sec = float(direction_data.get("macro_age_sec", 999999.0) or 999999.0)
    degraded = bool(direction_data.get("degraded", False) or direction_data.get("macro_failure", False))
    running = bool(_read_macro_refresh_status().get("status") == "running")
    if running:
        return
    last_trigger_ts = float(_MACRO_WATCHDOG_STATE.get("last_trigger_ts", 0.0) or 0.0)
    if (time.time() - last_trigger_ts) < _MACRO_WATCHDOG_MIN_REFRESH_SEC:
        return
    if degraded or macro_age_sec >= 900.0:
        ok, _msg, _pid = _spawn_macro_refresh(force=True)
        if ok:
            _MACRO_WATCHDOG_STATE["last_trigger_ts"] = time.time()


def _macro_watchdog_loop():
    while True:
        try:
            _run_macro_watchdog_step()
        except Exception:
            pass
        time.sleep(60)


def _macro_watchdog_worker() -> None:
    global _MACRO_WATCHDOG_THREAD
    try:
        _macro_watchdog_loop()
    finally:
        _MACRO_WATCHDOG_THREAD = None


def _should_enable_training_autopilot(
    status: Dict[str, Any],
    approval: Optional[Dict[str, Any]] = None,
) -> bool:
    if bool(status.get("isTraining", False)):
        return True

    approval = approval if isinstance(approval, dict) else get_training_approval_chain()
    decision = approval.get("decision", {}) if isinstance(approval.get("decision", {}), dict) else {}
    recommended_actions = decision.get("recommendedActions", []) if isinstance(decision.get("recommendedActions", []), list) else []
    dynamic_trigger = decision.get("dynamicTrigger", {}) if isinstance(decision.get("dynamicTrigger", {}), dict) else {}
    dynamic_action = str(dynamic_trigger.get("recommendedNextAction", "") or "")
    candidate_value = decision.get("candidateValue", {}) if isinstance(decision.get("candidateValue", {}), dict) else {}
    suggestion_stage = str(approval.get("stage", "NO_SUGGESTION") or "NO_SUGGESTION")
    has_training_output = bool(status.get("currentEpoch", 0)) or (DATA_DIR / "offline" / "best_params.json").exists()
    waiting_same_invalid_error = False

    if waiting_same_invalid_error:
        return False

    if dynamic_action in {"run_offline_training", "generate_suggestion", "repair_blockers", "approve_to_shadow"}:
        return True
    if suggestion_stage == "NO_SUGGESTION" and has_training_output:
        return True
    if suggestion_stage == "PENDING_APPROVAL" and "approve_to_shadow" in recommended_actions:
        return True
    if any(
        isinstance(action, str)
        and (action.startswith("repair:") or action == "rebuild_shadow_params")
        for action in recommended_actions
    ):
        return True
    return False


def _kick_training_autopilot() -> None:
    if not _GUI_BACKGROUND_AUTOPILOT_ENABLED:
        return
    # Startup hooks can be skipped by some launch paths; make the worker self-healing.
    if bool(getattr(_TRAINING_AUTOPILOT_CONTEXT, "active", False)):
        return
    try:
        _ensure_singleton_daemon_thread(
            holder_name="_TRAINING_AUTOPILOT_THREAD",
            thread_name="training-autopilot",
            target=_training_autopilot_worker,
        )
    except Exception:
        pass

_LICENSE_ONLINE_CHECK_THREAD: Optional[threading.Thread] = None


def _append_startup_trace(stage: str, **metadata: Any) -> None:
    try:
        _STARTUP_TRACE_LOG.parent.mkdir(parents=True, exist_ok=True)
        payload = {
            "ts": _dt.datetime.now(_dt.timezone.utc).isoformat(),
            "pid": os.getpid(),
            "stage": str(stage or "").strip(),
        }
        if metadata:
            payload["meta"] = metadata
        with _STARTUP_TRACE_LOG.open("a", encoding="utf-8") as handle:
            handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
    except Exception:
        pass


def _startup_license_check_worker() -> None:
    global _LICENSE_OK, _LICENSE_ERROR, _LICENSE_STARTUP_THREAD
    _append_startup_trace("license_check_begin")
    try:
        from src.license.guard import enforce_license

        enforce_license()
        _LICENSE_OK = True
        _LICENSE_ERROR = ""
        _append_startup_trace("license_check_ok")
    except Exception as exc:
        _LICENSE_OK = False
        _LICENSE_ERROR = str(exc)
        _append_startup_trace("license_check_warn", error=_LICENSE_ERROR[:500])
        print(f"[LICENSE] {_LICENSE_ERROR}")
    finally:
        _LICENSE_STARTUP_THREAD = None
        try:
            _invalidate_runtime_status_caches(schedule_refresh=True)
        except Exception:
            pass


def _start_startup_license_check() -> None:
    _ensure_singleton_daemon_thread(
        holder_name="_LICENSE_STARTUP_THREAD",
        thread_name="license-startup-check",
        target=_startup_license_check_worker,
    )


def _license_online_check_loop() -> None:
    """Periodic (hourly) online license revocation check. Silent on failure."""
    CHECK_INTERVAL = 3600  # 1 hour
    while True:
        try:
            from src.license.validator import check_online
            from src.license.activate import DEFAULT_SERVER
            result = check_online(DEFAULT_SERVER)
            if result.get("error"):
                _record_system_message(
                    kind="license",
                    title="授权证吊销检查",
                    desc=f"License issue: {result['error']}",
                    color="warning",
                    source="license_guard",
                )
        except Exception:
            pass  # offline — silent
        time.sleep(CHECK_INTERVAL)


def _license_online_check_worker() -> None:
    global _LICENSE_ONLINE_CHECK_THREAD
    try:
        _license_online_check_loop()
    finally:
        _LICENSE_ONLINE_CHECK_THREAD = None


def _start_license_online_check() -> None:
    _ensure_singleton_daemon_thread(
        holder_name="_LICENSE_ONLINE_CHECK_THREAD",
        thread_name="license-online-check",
        target=_license_online_check_worker,
    )


def _startup_supervisor():
    _cleanup_corrupt_backups()
    # Start background license online revocation check (hourly)
    _start_license_online_check()
    # GUI 默认不再自带后台认知/训练循环，避免控制面把整机资源顶满。
    if _GUI_BACKGROUND_AUTOPILOT_ENABLED:
        _kick_training_autopilot()
        _ensure_singleton_daemon_thread(
            holder_name="_MACRO_WATCHDOG_THREAD",
            thread_name="macro-watchdog",
            target=_macro_watchdog_worker,
        )
    try:
        from bridge.trader_api import preload_runtime_bindings
        preload_runtime_bindings()
    except Exception:
        pass
    _emit_runtime_health_warnings()
    _ensure_daemon_running(force=False)
    try:
        _schedule_status_cache_refresh()
    except Exception:
        pass
    for cache, builder, thread_name in (
        (_STRATEGY_MARKETPLACE_CACHE, _build_strategy_workbench_marketplace_response, "gui-strategy-marketplace-prewarm"),
        (_RUNTIME_INSTANCES_CACHE, _build_runtime_instances_response, "gui-runtime-instances-prewarm"),
        (_STRATEGY_WORKBENCH_CACHE, _build_strategy_workbench_response, "gui-strategy-workbench-prewarm"),
    ):
        try:
            _schedule_aux_payload_refresh(cache, builder, thread_name)
        except Exception:
            pass

@app.get("/api/price_probe")
def price_probe():
    """
    本地模型价格探活:对比 MT5 实时 vs yfinance 参考
    返回: mt5_price, ref_price, divergence, status
    """
    from bridge.health_monitor import probe_price_freshness
    ok, detail = probe_price_freshness()
    return {
        "ok": ok,
        "detail": detail,
        "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ")
    }

@app.get("/api/ping")
async def ping():
    """Lightweight health endpoint for launcher/keepalive checks."""
    app_version, _ = _resolve_runtime_app_version()
    return {"ok": True, "ts": time.time(), "appVersion": app_version}


@app.post("/api/system/restart")
async def restart_engine():
    """Restart the trading engine gracefully.

    Spawns a PowerShell script that waits for the current process to exit,
    then relaunches GoldAutoTradeEngine.exe with --engine-instance to ensure
    silent restart without browser auto-open.
    """
    import subprocess, sys, os, time as _time
    exe_dir = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.getcwd()
    exe_path = os.path.join(exe_dir, "GoldAutoTradeEngine.exe")
    bat_path = os.path.join(exe_dir, "启动.bat")

    # Write restart script — always restart EXE directly with --engine-instance
    # to prevent browser auto-open. Never go through 启动.bat for automated restart.
    restart_script = os.path.join(exe_dir, "_restart.ps1")
    with open(restart_script, "w", encoding="utf-8") as f:
        f.write(f'''# Gold AutoTrade — Auto-restart script (silent)
$pid = {os.getpid()}
Write-Host "Waiting for process $pid to exit..."
while (Get-Process -Id $pid -ErrorAction SilentlyContinue) {{ Start-Sleep -Seconds 2 }}
Write-Host "Restarting Gold AutoTrade (silent)..."
Start-Process -FilePath "{exe_path}" -ArgumentList "--engine-instance" -WorkingDirectory "{exe_dir}"
''')
    _popen_safe(
        ["powershell", "-ExecutionPolicy", "Bypass", "-File", restart_script],
    )
    return {"ok": True, "message": "引擎将在3-5秒后重启，请刷新页面"}


@app.get("/api/runtime/build-info")
def get_runtime_build_info():
    """Expose runtime build metadata so the GUI can detect stale pages and mixed releases."""
    return _build_runtime_build_info_payload()

    # Start MT5 trade history sync thread (syncs every 120s)
    try:
        _sync_mt5_history_thread = threading.Thread(
            target=_mt5_history_sync_worker,
            daemon=True,
            name='mt5-history-sync'
        )
        _sync_mt5_history_thread.start()
    except Exception:
        pass



def _build_lightweight_payload_from_status(
    status_payload: Optional[dict[str, Any]],
    *,
    cache_age_sec: Optional[float] = None,
    refreshing: bool = False,
    now: Optional[float] = None,
) -> dict[str, Any]:
    payload = dict(status_payload) if isinstance(status_payload, dict) else {}
    now_ts = float(now if now is not None else time.time())
    mt5_online = bool(payload.get("mt5Online", False))
    bridge_ok = bool(payload.get("bridgeOk", False))
    is_running = bool(payload.get("isRunning", True))
    light = str(payload.get("light") or ("GREEN" if (is_running and mt5_online and bridge_ok) else ("YELLOW" if mt5_online else "RED")))
    mt5_latency = -1
    system_stats = payload.get("system_stats")
    if isinstance(system_stats, dict):
        try:
            mt5_latency = int(system_stats.get("mt5_latency", -1) or -1)
        except Exception:
            mt5_latency = -1
    # Detect MT5 connection state for first-run guidance
    mt5_dir_exists = False
    try:
        mt5_dir = config_manager.get_mt5_dir()
        mt5_dir_exists = bool(mt5_dir and mt5_dir.exists())
    except Exception:
        pass
    mt5_process_running = bool(payload.get("mt5Online", False))
    bridge_file_fresh = bool(payload.get("bridgeOk", False))

    # Build actionable startup guidance
    startup_actions: list[str] = []
    if not mt5_process_running:
        startup_actions.append("launch_mt5")
    if mt5_process_running and not bridge_file_fresh:
        startup_actions.append("attach_ea")
    if not mt5_dir_exists:
        startup_actions.append("configure_mt5_path")

    cache_age = None if cache_age_sec is None else round(max(0.0, float(cache_age_sec)), 3)
    return {
        "ok": True,
        "backendStarted": True,
        "isRunning": is_running,
        "mt5Online": mt5_online,
        "bridgeOk": bridge_ok,
        "light": light,
        "mt5Latency": mt5_latency,
        "priceSource": str(payload.get("priceSource", "unknown") or "unknown"),
        "priceAge": float(payload.get("priceAge", 0.0) or 0.0),
        "statusRefreshing": bool(refreshing),
        "statusCacheAgeSec": cache_age,
        "warmingUp": not bool(payload),
        "startupGuidance": {
            "mt5Required": True,
            "mt5Running": mt5_process_running,
            "bridgeReady": bridge_file_fresh,
            "mt5PathConfigured": mt5_dir_exists,
            "actions": startup_actions,
            "steps": {
                "launch_mt5": {"title": "启动 MT5 终端", "desc": "请打开 MetaTrader 5 (WeTrade) 并登录交易账户"},
                "attach_ea": {"title": "附加 EA 到图表", "desc": "在 MT5 中打开 XAUUSD M5 图表，将 GoldAutoTradeBridgeEA 拖入图表，启用 DLL 导入"},
                "configure_mt5_path": {"title": "配置 MT5 路径", "desc": "在 设置→MT5路径 中指定 MT5 数据目录"},
            },
        },
        "ts": now_ts,
    }


def _store_lightweight_status_payload(payload: dict[str, Any], now: Optional[float] = None) -> dict[str, Any]:
    cache_ts = float(now if now is not None else time.time())
    _LIGHTWEIGHT_STATUS_CACHE["ts"] = cache_ts
    _LIGHTWEIGHT_STATUS_CACHE["value"] = payload
    return payload


def _refresh_aux_payload_cache_worker(cache: dict[str, Any], builder) -> None:
    try:
        payload = builder()
        if isinstance(payload, dict):
            with _AUX_RESPONSE_CACHE_LOCK:
                cache["ts"] = time.time()
                cache["value"] = payload
    finally:
        with _AUX_RESPONSE_CACHE_LOCK:
            cache["refreshing"] = False
            cache["thread"] = None


def _schedule_aux_payload_refresh(cache: dict[str, Any], builder, thread_name: str) -> bool:
    with _AUX_RESPONSE_CACHE_LOCK:
        if bool(cache.get("refreshing", False)):
            return False
        existing = _find_alive_thread_by_name(thread_name)
        if existing is not None:
            cache["refreshing"] = True
            cache["thread"] = existing
            return False
        t = threading.Thread(
            target=_refresh_aux_payload_cache_worker,
            args=(cache, builder),
            daemon=True,
            name=thread_name,
        )
        cache["refreshing"] = True
        cache["thread"] = t
        t.start()
        return True


def _get_cached_aux_payload(
    cache: dict[str, Any],
    ttl_sec: float,
    builder,
    thread_name: str,
    warming_payload: dict[str, Any],
    *,
    force_refresh: bool = False,
) -> dict[str, Any]:
    if force_refresh:
        payload = builder()
        if isinstance(payload, dict):
            with _AUX_RESPONSE_CACHE_LOCK:
                cache["ts"] = time.time()
                cache["value"] = payload
        return payload
    cache_now = time.time()
    with _AUX_RESPONSE_CACHE_LOCK:
        cached_payload = cache.get("value")
        cache_age = cache_now - float(cache.get("ts", 0.0) or 0.0)
        refreshing = bool(cache.get("refreshing", False))
    if isinstance(cached_payload, dict) and cache_age < ttl_sec:
        return cached_payload
    if isinstance(cached_payload, dict):
        if not refreshing:
            _schedule_aux_payload_refresh(cache, builder, thread_name)
        stale_payload = dict(cached_payload)
        stale_payload["stale"] = True
        stale_payload["cacheAgeSec"] = round(cache_age, 1)
        return stale_payload
    if not refreshing:
        _schedule_aux_payload_refresh(cache, builder, thread_name)
    fallback_payload = dict(warming_payload)
    fallback_payload["warming"] = True
    fallback_payload["stale"] = True
    return fallback_payload


def _resolve_daemon_running() -> bool:
    daemon_running = False
    fallback_pid = None
    if PID_FILE.exists():
        try:
            pid = int(PID_FILE.read_text(encoding="utf-8").strip())
            if is_process_running(pid):
                daemon_running = True
            else:
                PID_FILE.unlink(missing_ok=True)
        except Exception:
            pass
    if not daemon_running:
        fallback_pid = _find_runtime_daemon_pid()
        if fallback_pid:
            daemon_running = True
            try:
                PID_FILE.write_text(str(fallback_pid), encoding="utf-8")
            except Exception:
                pass
    return daemon_running


def _probe_bridge_runtime(
    *,
    daemon_running: bool,
    mt5_online: bool,
    price_source: str = "",
) -> tuple[bool, Optional[pathlib.Path], Optional[pathlib.Path], Optional[float]]:
    bridge_ok = False
    mt5_dir = None
    sf = None
    diff = None
    try:
        mt5_dir = config_manager.get_mt5_dir()
        if mt5_dir:
            sf = _resolve_terminal_status_file(mt5_dir)
            if sf and sf.exists():
                mtime = sf.stat().st_mtime
                diff = time.time() - mtime
                bridge_ok = diff < 180  # 3分钟内有更新都视为桥接在线，减少瞬时抖动误报
    except Exception:
        pass
    normalized_price_source = str(price_source or "").lower()
    if (
        not bridge_ok
        and daemon_running
        and mt5_online
        and (
            normalized_price_source in {"mt5", "stale"}
            or normalized_price_source.startswith("mt5:")
            or normalized_price_source.startswith("stale:")
        )
    ):
        # terminal_status 读取偶发抖动时，用价格源兜底，避免 UI 频繁误报“离线”。
        bridge_ok = True
    return bridge_ok, mt5_dir, sf, diff


def _build_lightweight_status_payload() -> dict[str, Any]:
    now = time.time()
    with _STATUS_CACHE_LOCK:
        maybe_cached = _STATUS_RESPONSE_CACHE.get("value")
        cached_status = dict(maybe_cached) if isinstance(maybe_cached, dict) else None
        cached_status_ts = float(_STATUS_RESPONSE_CACHE.get("ts", 0.0) or 0.0)
        refreshing = bool(_STATUS_RESPONSE_CACHE.get("refreshing", False))
    cache_age = (now - cached_status_ts) if cached_status_ts > 0 else None
    if cached_status is None and not refreshing:
        _schedule_status_cache_refresh()
    return _build_lightweight_payload_from_status(
        cached_status,
        cache_age_sec=cache_age,
        refreshing=refreshing,
        now=now,
    )


@app.get("/api/lightweight-status")
def get_lightweight_status():
    now = time.time()
    cached = _LIGHTWEIGHT_STATUS_CACHE.get("value")
    cached_ts = float(_LIGHTWEIGHT_STATUS_CACHE.get("ts", 0.0) or 0.0)
    if isinstance(cached, dict) and (now - cached_ts) < _LIGHTWEIGHT_STATUS_CACHE_TTL_SEC:
        cached["license_ok"] = _LICENSE_OK
        cached["license_error"] = _LICENSE_ERROR
        return cached
    payload = _build_lightweight_status_payload()
    payload["license_ok"] = _LICENSE_OK
    payload["license_error"] = _LICENSE_ERROR
    return _store_lightweight_status_payload(payload, now=now)


# ── i18n API ──────────────────────────────────────────────────────────

@app.get("/api/i18n/languages")
def api_i18n_languages():
    """List supported languages."""
    return {
        "languages": _SUP_LANGS if _I18N_READY else {"zh-CN": {"name": "简体中文", "dir": "ltr"}},
        "current": _LM.current_language if _LM else "zh-CN",
    }


@app.post("/api/i18n/switch")
def api_i18n_switch(lang: str = "zh-CN"):
    """Switch current language. Accepts form field 'lang'."""
    if _I18N_READY and _LM:
        ok = _LM.set_language(lang)
        return {"ok": ok, "language": _LM.current_language}
    return {"ok": False, "language": "zh-CN", "error": "i18n not available"}


@app.get("/api/i18n/translate")
def api_i18n_translate(key: str = "", lang: str = ""):
    """Translate a dot-notation key. Query: ?key=dashboard.title&lang=en"""
    if _I18N_READY and _LM:
        l = lang or _LM.current_language
        return {"key": key, "lang": l, "text": _LM.translate(key, lang=l)}
    return {"key": key, "lang": "zh-CN", "text": key}


# ── Internal helpers ──────────────────────────────────────────────────

def _resolve_active_symbol_for_gui() -> str:
    """EA auto-follow: 优先读 EA 图表品种，其次用户配置，兜底 XAUUSD"""
    try:
        mt5_dir = config_manager.get_mt5_dir()
        for fname in ("terminal_status.json", "market_snapshot.json"):
            sp = mt5_dir / "gold_autotrade" / "bridge" / fname
            if sp.exists():
                snap = json.loads(sp.read_text(encoding="utf-8"))
                sym = snap.get("symbol", "") or snap.get("marketSymbol", "") or ""
                if sym: return str(sym).strip().upper()
                instruments = snap.get("instruments", {})
                if isinstance(instruments, dict) and instruments:
                    return str(next(iter(instruments.keys()))).strip().upper()
    except Exception: pass
    try:
        inst_path = config_manager.get_config_dir() / "active_instances.json"
        if inst_path.exists():
            inst = json.loads(inst_path.read_text(encoding="utf-8"))
            for _k, _v in inst.items():
                if isinstance(_v, dict) and _v.get("symbol"):
                    return str(_v["symbol"]).strip().upper()
    except Exception: pass
    return "XAUUSD"


@app.get("/api/settings/active_symbol")
def get_active_symbol():
    """Return EA auto-follow symbol with user override fallback."""
    try:
        sym = _resolve_active_symbol_for_gui()
        source = "ea_auto_follow" if sym != "XAUUSD" else "fallback"
        return {"success": True, "active_symbol": sym, "source": source}
    except Exception as e:
        return {"success": False, "active_symbol": None, "error": str(e)}


def _trading_permissions_for_mode(runtime_mode: str) -> dict[str, bool]:
    if runtime_mode == "FULL_TRADING":
        return {
            "can_open_new_position": True,
            "can_close_position": True,
            "can_modify_sl_tp": True,
            "can_sync_positions": True,
            "can_emit_alerts": True,
        }
    if runtime_mode == "MANAGE_ONLY":
        return {
            "can_open_new_position": False,
            "can_close_position": True,
            "can_modify_sl_tp": True,
            "can_sync_positions": True,
            "can_emit_alerts": True,
        }
    return {
        "can_open_new_position": False,
        "can_close_position": False,
        "can_modify_sl_tp": False,
        "can_sync_positions": False,
        "can_emit_alerts": True,
    }


def _build_trading_safety_payload(
    *,
    daemon_running: bool,
    mt5_online: bool,
    bridge_ok: bool,
    position_open: bool,
    terminal_status_file: Optional[pathlib.Path],
) -> dict[str, Any]:
    heartbeat_age_sec = None
    execution_report_age_sec = None
    if terminal_status_file and terminal_status_file.exists():
        try:
            heartbeat_age_sec = round(time.time() - terminal_status_file.stat().st_mtime, 1)
        except Exception:
            heartbeat_age_sec = None
    execution_report_file = DATA_DIR / "inbox" / "execution_report.json"
    if execution_report_file.exists():
        try:
            execution_report_age_sec = round(time.time() - execution_report_file.stat().st_mtime, 1)
        except Exception:
            execution_report_age_sec = None

    heartbeat_ok = heartbeat_age_sec is not None and heartbeat_age_sec <= 120.0
    execution_report_ok = execution_report_age_sec is not None and execution_report_age_sec <= 300.0
    management_signal_ok = heartbeat_ok or execution_report_ok or bridge_ok or mt5_online

    issues: list[dict[str, str]] = []
    if not daemon_running:
        issues.append({"code": "DAEMON_OFFLINE", "message": "交易运行时未启动，先进入修复态。"})
    if not mt5_online:
        issues.append({"code": "MT5_OFFLINE", "message": "MT5 未在线，新开仓能力保持关闭。"})
    if not bridge_ok:
        issues.append({"code": "BRIDGE_NOT_READY", "message": "桥接链路未完全就绪，只保留安全能力。"})
    if position_open and not management_signal_ok:
        issues.append({"code": "POSITION_PROTECTION_UNCERTAIN", "message": "已有持仓保护信号不可信，只保留只读观察。"})

    if not daemon_running:
        runtime_mode = "BOOTSTRAP_REPAIR"
    elif daemon_running and mt5_online and bridge_ok:
        runtime_mode = "FULL_TRADING"
    elif position_open and management_signal_ok:
        runtime_mode = "MANAGE_ONLY"
    else:
        runtime_mode = "SAFE_OBSERVE"

    mode_label_map = {
        "FULL_TRADING": "全功能交易",
        "MANAGE_ONLY": "仅管理已有持仓",
        "SAFE_OBSERVE": "只读观察",
        "BOOTSTRAP_REPAIR": "修复态",
    }
    mode_hint_map = {
        "FULL_TRADING": "桥接链路可信，允许新开仓、平仓、止损止盈维护与状态同步。",
        "MANAGE_ONLY": "禁止新开仓，但保留已有持仓的平仓、止损止盈维护与状态同步。",
        "SAFE_OBSERVE": "只保留日志、监控、告警与配置查看，不下发交易指令。",
        "BOOTSTRAP_REPAIR": "交易运行时未完全就绪，先修复基础链路后再恢复交易能力。",
    }
    permissions = _trading_permissions_for_mode(runtime_mode)

    return {
        "runtimeMode": runtime_mode,
        "modeLabel": mode_label_map.get(runtime_mode, runtime_mode),
        "summary": mode_hint_map.get(runtime_mode, ""),
        "autoRestoreEligible": bool(daemon_running and mt5_online and bridge_ok),
        "permissions": permissions,
        "issues": issues,
        "bridgeHealth": {
            "heartbeatOk": heartbeat_ok,
            "heartbeatAgeSec": heartbeat_age_sec,
            "executionReportOk": execution_report_ok,
            "executionReportAgeSec": execution_report_age_sec,
            "terminalStatusFile": str(terminal_status_file) if terminal_status_file else "",
            "executionReportFile": str(execution_report_file),
            "positionOpen": position_open,
        },
    }


def _record_trading_safety_transition(trading_safety: dict[str, Any]) -> None:
    if not isinstance(trading_safety, dict) or not trading_safety:
        return
    runtime_mode = str(trading_safety.get("runtimeMode", "") or "")
    if not runtime_mode:
        return
    issues = trading_safety.get("issues", []) if isinstance(trading_safety.get("issues", []), list) else []
    issue_codes = [
        str(item.get("code", "") or "").strip()
        for item in issues
        if isinstance(item, dict) and str(item.get("code", "") or "").strip()
    ]
    signature = "|".join(
        [
            runtime_mode,
            ",".join(issue_codes[:6]),
            "open" if bool(trading_safety.get("bridgeHealth", {}).get("positionOpen", False)) else "flat",
            "restore" if bool(trading_safety.get("autoRestoreEligible", False)) else "hold",
        ]
    )
    if _TRADING_SAFETY_LAST_SIGNATURE.get("value") == signature:
        return
    _TRADING_SAFETY_LAST_SIGNATURE["value"] = signature

    tone_map = {
        "FULL_TRADING": "success",
        "MANAGE_ONLY": "warning",
        "SAFE_OBSERVE": "warning",
        "BOOTSTRAP_REPAIR": "danger",
    }
    title_map = {
        "FULL_TRADING": "交易能力已恢复为全功能",
        "MANAGE_ONLY": "交易能力降级为仅管理已有持仓",
        "SAFE_OBSERVE": "交易能力降级为只读观察",
        "BOOTSTRAP_REPAIR": "交易运行进入修复态",
    }
    summary = str(trading_safety.get("summary", "") or "").strip()
    issue_text = "；".join(
        [
            str(item.get("message", "") or "").strip()
            for item in issues
            if isinstance(item, dict) and str(item.get("message", "") or "").strip()
        ][:3]
    )
    desc = issue_text or summary or runtime_mode
    _record_system_message(
        kind="runtime_guard",
        title=title_map.get(runtime_mode, "交易能力状态变化"),
        desc=desc,
        color=tone_map.get(runtime_mode, "warning"),
        source="trading_safety",
        metadata={"tradingSafety": trading_safety},
    )


def _build_status_payload():
    # EA auto-follow: 使用 EA 当前图表品种，不再硬编码 XAUUSD
    active_sym = _resolve_active_symbol_for_gui()
    price_truth = get_price_truth(active_sym)
    if bool(price_truth.get("ok", False)):
        bid = float(price_truth.get("bid", 0.0) or 0.0)
        ask = float(price_truth.get("ask", 0.0) or 0.0)
        spr = float(price_truth.get("spread", 0.0) or 0.0)
        price_source = str(price_truth.get("source", "mt5") or "mt5")
        price_age = price_truth.get("ageSeconds") or 0
    else:
        bid, ask, spr, price_source, price_age = None, None, None, "error", None

    state, runtime_open_guard, runtime_guard_present = _runtime_derived_state(timeout_sec=0.8)
    authoritative_state = _safe_load_state(timeout_sec=0.8)
    authoritative_state = authoritative_state if isinstance(authoritative_state, dict) else {}
    runtime_guard_position_open = bool(runtime_open_guard.get("position_open", False)) if runtime_guard_present else False
    runtime_guard_ticket = int(runtime_open_guard.get("position_ticket", 0) or 0) if runtime_guard_present else 0
    runtime_guard_side = (
        str(runtime_open_guard.get("position_side", "") or "").upper() if runtime_guard_present else ""
    )

    # 检测守护进程
    daemon_running = _resolve_daemon_running()

    mt5_online = is_mt5_running()

    # 检查 EA bridge 状态
    bridge_ok, mt5_dir, sf, diff = _probe_bridge_runtime(
        daemon_running=daemon_running,
        mt5_online=mt5_online,
        price_source=str(price_source or ""),
    )

    # 指示灯逻辑:运行且 MT5 在线为绿,否则黄/红
    light = "GREEN" if (daemon_running and mt5_online and bridge_ok) else ("YELLOW" if mt5_online else "RED")

    # 获取当前 Agent 的宏观方向定调
    try:
        agent_dir_current, agent_dir_data = get_agent_direction()
    except Exception:
        agent_dir_current = "NEUTRAL"
        agent_dir_data = {}
    try:
        direction_snapshot = load_decision_snapshot()
    except Exception:
        direction_snapshot = {}
    snapshot_macro = (
        direction_snapshot.get("macro", {})
        if isinstance(direction_snapshot.get("macro", {}), dict)
        and direction_snapshot.get("macro", {})
        else direction_snapshot
        if isinstance(direction_snapshot, dict)
        else {}
    )
    snapshot_contract = (
        direction_snapshot.get("contract", {})
        if isinstance(direction_snapshot.get("contract", {}), dict)
        else {}
    )
    if snapshot_macro:
        # Only let snapshot override if agent_dir_data is empty or stale;
        # a fresh macro_commander blackboard read takes priority.
        agent_source = str(agent_dir_data.get("engine", "") or "")
        if agent_source == "macro_commander" and agent_dir_data.get("direction") in {"LONG", "SHORT"}:
            base_direction_data = dict(agent_dir_data) if isinstance(agent_dir_data, dict) else {}
            base_direction_data.setdefault("binding_direction", agent_dir_current)
            base_direction_data.setdefault("effective_direction", agent_dir_current)
            base_direction_data.setdefault("allowed_side", "BOTH")
            base_direction_data.setdefault("state", "FRESH")
        else:
            agent_dir_current = str(
                snapshot_macro.get("binding_direction", agent_dir_current) or agent_dir_current
            ).upper()
            base_direction_data = dict(agent_dir_data) if isinstance(agent_dir_data, dict) else {}
            base_direction_data.update(
                {
                    "direction": str(snapshot_macro.get("direction", agent_dir_current) or agent_dir_current).upper(),
                    "binding_direction": str(snapshot_macro.get("binding_direction", agent_dir_current) or agent_dir_current).upper(),
                    "effective_direction": str(snapshot_macro.get("effective_direction", "NEUTRAL") or "NEUTRAL").upper(),
                    "allowed_side": str(snapshot_macro.get("allowed_side", "BOTH") or "BOTH").upper(),
                    "state": str(snapshot_macro.get("state", "UNKNOWN") or "UNKNOWN").upper(),
                    "lineage": (
                        snapshot_contract.get("lineage", {})
                        if isinstance(snapshot_contract.get("lineage", {}), dict)
                        else {}
                    ),
                }
        )
        agent_dir_data = base_direction_data
    technical_trend_bias, technical_trend_data = _get_technical_trend_snapshot()

    # 获取 CPU / RAM
    try:
        cpu_percent = psutil.cpu_percent(interval=0.0)
        ram = psutil.virtual_memory()
        ram_used_mb = int(ram.used / (1024 * 1024))
        ram_percent = ram.percent
        process_rss_mb = round(psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024, 1)
    except:
        cpu_percent = 0.0
        ram_used_mb = 0
        ram_percent = 0.0
        process_rss_mb = 0.0

    # 计算 MT5 延迟
    mt5_latency = -1
    try:
        if mt5_dir and sf and sf.exists() and diff is not None:
            mt5_latency = int(diff * 1000) if diff > 0 else 0
    except:
        pass

    # ── 从本地数据库(sqlite)聚合今日盈亏（加缓存，防止高频轮询拉高 CPU） ──
    today_stats = {"trades": 0, "pnl": 0.0, "wins": 0, "losses": 0, "win_rate": 0.0, "avg_win": 0.0, "avg_loss": 0.0}
    today_open_trades = 0
    try:
        db_path = config_manager.get_data_dir() / "bridge" / "state" / "agent_state.db"
        today_stats = _read_today_stats_cached(db_path)
    except Exception:
        pass
    try:
        ledger_file = _resolve_existing_file(_runtime_log_candidates("trading_ledger.ndjson"))
        today_open_trades = _read_today_open_fills_from_trading_ledger(ledger_file)
    except Exception:
        today_open_trades = 0

    today_closed_trades = max(0, int(today_stats.get("trades", 0) or 0))
    today_trades = max(0, int(today_stats.get("trades", 0) or 0))  # 主计数使用已平仓成交数（有确定盈亏）
    today_pnl = _coerce_float(today_stats.get("pnl", 0.0), 0.0)
    today_wins = max(0, int(today_stats.get("wins", 0) or 0))
    today_losses = max(0, int(today_stats.get("losses", 0) or 0))
    today_win_rate = _coerce_float(today_stats.get("win_rate", 0.0), 0.0)
    today_avg_win = _coerce_float(today_stats.get("avg_win", 0.0), 0.0)
    today_avg_loss = _coerce_float(today_stats.get("avg_loss", 0.0), 0.0)
    today_stats_by_strategy = today_stats.get("_by_strategy", {}) if isinstance(today_stats.get("_by_strategy", {}), dict) else {}

    blackboard = _get_blackboard_data()
    blackboard_preview = _compact_blackboard_for_status(blackboard)
    micro_sniper = blackboard.get("Micro_Sniper", {}) if isinstance(blackboard, dict) else {}

    # ── 只展示 pipeline pending entries；legacy soft_limits 不再作为状态真源 ──
    active_soft_limits = []
    now_ts = time.time()
    # 读取 Pipeline 的 pending_entry_intents.json，映射为挂单显示
    try:
        pending_path = project_root / "data" / "pending_entry_intents.json"
        if pending_path.exists():
            pending_payload = json.loads(pending_path.read_text(encoding="utf-8"))
            pending_entries = pending_payload.get("entries", [])
            if isinstance(pending_entries, list):
                for entry in pending_entries:
                    if not isinstance(entry, dict):
                        continue
                    expire_ts = float(entry.get("expire_ts", 0) or 0)
                    if expire_ts <= now_ts:
                        continue
                    sig = entry.get("signal", {}) if isinstance(entry.get("signal", {}), dict) else {}
                    materialized = str(entry.get("materialized_action", "") or "").upper()
                    if materialized not in ("OPEN_LIMIT", "ARM_WAIT_CONFIRMATION"):
                        continue
                    remain_mins = int((expire_ts - now_ts) / 60)
                    active_soft_limits.append({
                        "id": entry.get("pending_entry_id", ""),
                        "side": entry.get("side", "NONE"),
                        "limit_price": entry.get("entry_price", 0),
                        "vol": float(sig.get("volume_lots", sig.get("vol", 0)) or 0),
                        "remain_mins": max(1, remain_mins),
                        "reason": materialized,
                        "atr_offset_usd": None,
                        "spread_floor_usd": None,
                        "min_offset_usd": None,
                        "max_offset_usd": None,
                        "final_offset_usd": None,
                    })
    except Exception:
        pass

    regime_current = micro_sniper.get("current_regime") or state.get("regime", "UNKNOWN")
    if "is_defensive_env" in micro_sniper:
        is_defensive_env = bool(micro_sniper.get("is_defensive_env"))
    else:
        normalized_regime = normalize_regime_label(regime_current)
        is_defensive_env = is_range_regime(normalized_regime) or is_defensive_regime(normalized_regime)

    latest_risk_block = _read_latest_risk_block_event()
    final_reject = _get_fresh_final_reject_event(state)
    budget = state.get("opportunity_budget", {}) if isinstance(state.get("opportunity_budget", {}), dict) else {}
    hour_key = datetime.datetime.now().strftime("%Y%m%d%H")
    overlap_budget = budget.get(hour_key, {}) if isinstance(budget.get(hour_key, {}), dict) else {}
    overlap_budget_target = int(config_manager.get_effective_param("overlap_min_attempts_per_hour", 2) or 2)
    runtime_positions = (
        runtime_open_guard.get("positions_all", [])
        if runtime_guard_present and isinstance(runtime_open_guard.get("positions_all", []), list)
        else []
    )
    state_positions = (
        authoritative_state.get("positions_all", [])
        if isinstance(authoritative_state.get("positions_all", []), list)
        else []
    )
    state_position_open = bool(authoritative_state.get("position_open", False))
    raw_positions = (
        runtime_positions
        if runtime_guard_present and (state_position_open or state_positions)
        else state_positions
    )
    enriched_positions = _enrich_positions_with_attribution(raw_positions)
    _, terminal_status_payload = _read_terminal_status_snapshot(mt5_dir)
    terminal_positions = _extract_terminal_status_positions(terminal_status_payload)
    terminal_positions_count = _terminal_status_positions_count(terminal_status_payload)
    if terminal_positions_count == 0:
        enriched_positions = []
        runtime_guard_position_open = False
        runtime_guard_ticket = 0
        runtime_guard_side = ""
    elif (
        terminal_positions_count is not None
        and terminal_positions_count > 0
        and len(enriched_positions) != terminal_positions_count
    ):
        _emit_position_status_alert(
            "position_snapshot_count_mismatch",
            terminal_positions_count=terminal_positions_count,
            state_positions_count=len(enriched_positions),
            state_position_open=state_position_open,
        )
        if terminal_positions:
            enriched_positions = _enrich_positions_with_attribution(terminal_positions)
            state_position_open = True
    if runtime_guard_present and runtime_guard_position_open and runtime_guard_ticket > 0 and enriched_positions:
        matched_positions = [
            row
            for row in enriched_positions
            if int(row.get("ticket", 0) or 0) == runtime_guard_ticket
        ]
        if matched_positions:
            enriched_positions = matched_positions

    position_side = runtime_guard_side or authoritative_state.get("position_side")
    position_entry = (
        runtime_open_guard.get("position_entry", authoritative_state.get("position_entry", 0))
        if runtime_guard_present and (state_position_open or state_positions)
        else authoritative_state.get("position_entry", 0)
    )
    if enriched_positions:
        first_position = enriched_positions[0]
        if not position_side:
            position_side = first_position.get("side")
        if not position_entry:
            position_entry = first_position.get("entry", 0)
    elif terminal_positions_count == 0 or (runtime_guard_present and not runtime_guard_position_open and not enriched_positions):
        position_side = None
        position_entry = 0

    position_open = (
        bool(enriched_positions) or runtime_guard_position_open
        if runtime_guard_present and (state_position_open or state_positions)
        else bool(enriched_positions) or state_position_open or bool((terminal_positions_count or 0) > 0)
    )

    payload = {
        "isRunning": daemon_running,
        "mt5Online": mt5_online,
        "bridgeOk": bridge_ok,
        "bid": bid or 0,
        "ask": ask or 0,
        "spread": spr or 0,
        "priceSource": price_source or "unknown",
        "priceAge": price_age or 0,
        "marketSymbol": str(price_truth.get("marketSymbol", "XAUUSD") or "XAUUSD"),
        "quoteOrigin": str(price_truth.get("quoteOrigin", "invalid") or "invalid"),
        "quoteSymbol": str(price_truth.get("quoteSymbol", "") or ""),
        "topLevelSymbol": str(price_truth.get("topLevelSymbol", "") or ""),
        "priceTruthValid": bool(price_truth.get("ok", False)),
        "priceTruthReason": str(price_truth.get("reason", "") or ""),
        "priceSourcePath": str(price_truth.get("sourcePath", "") or ""),
        "priceSchemaState": str(price_truth.get("schemaState", "unknown") or "unknown"),
        "riskLevel": state.get("risk_level", "平衡"),
        "light": light,
        "agent_dir": agent_dir_current,
        "agent_direction_data": agent_dir_data,
        "position_binding_direction": str(state.get("position_binding_direction", "") or "").upper(),
        "position_macro_authority": str(state.get("position_macro_authority", "") or "").upper(),
        "position_allowed_side": str(state.get("position_allowed_side", "") or "").upper(),
        "technical_trend_bias": technical_trend_bias,
        "technical_trend_data": technical_trend_data,
        "is_defensive": is_defensive_env,
        "regime": regime_current,
        "risk_block": latest_risk_block,
        "final_reject": final_reject,
        "overlap_budget": overlap_budget,
        "overlap_budget_target": overlap_budget_target,
        "tradesCount": state.get("total_trades", 0),
        "todayTrades": int(today_trades or 0),
        "todayOpenTrades": int(today_open_trades or 0),
        "todayClosedTrades": int(today_closed_trades or 0),
        "todayPnl": round(today_pnl, 2),
        "todayWinRate": today_win_rate,
        "todayAvgWin": today_avg_win,
        "todayAvgLoss": today_avg_loss,
        "todayWins": today_wins,
        "todayLosses": today_losses,
        "todayStatsByStrategy": today_stats_by_strategy,
        "positionOpen": position_open,
        "positionSide": position_side,
        "positionEntry": position_entry,
        "positions": enriched_positions,
        "positionTier": _build_position_tier_snapshot(runtime_open_guard if runtime_guard_present else state),
        "account_total_loss_stop_pct": float(state.get("account_total_loss_stop_threshold_pct", 30.0) or 30.0),
        "account_total_loss_current_pct": float(state.get("account_total_loss_current_pct", 0.0) or 0.0),
        "account_total_loss_stop_active": bool(state.get("account_total_loss_stop_active", False)),
        "account_total_loss_stop_source": str(state.get("account_total_loss_stop_source", "NONE") or "NONE"),
        "soft_limits": active_soft_limits,  # 暴露出软挂单给前端雷达
        "system_stats": {
            "cpu_percent": cpu_percent,
            "ram_used_mb": ram_used_mb,
            "ram_percent": ram_percent,
            "process_rss_mb": process_rss_mb,
            "memory_warning": bool(ram_percent >= 85.0 or process_rss_mb >= 1024.0),
            "mt5_latency": mt5_latency
        },
        "blackboard": blackboard_preview,
        "tradingSafety": _build_trading_safety_payload(
            daemon_running=daemon_running,
            mt5_online=mt5_online,
            bridge_ok=bridge_ok,
            position_open=position_open,
            terminal_status_file=sf if sf and sf.exists() else None,
        ),
    }
    return payload


def _trading_safety_patrol_loop() -> None:
    stop_event = _TRADING_SAFETY_PATROL_STOP_EVENT
    while not stop_event.wait(_TRADING_SAFETY_PATROL_INTERVAL_SEC):
        try:
            _schedule_status_cache_refresh()
        except Exception:
            logger.debug("Trading safety patrol failed to schedule status refresh", exc_info=True)
        try:
            _schedule_runtime_observability_refresh()
        except Exception:
            logger.debug("Trading safety patrol failed to schedule runtime observability refresh", exc_info=True)


def _ensure_trading_safety_patrol_thread() -> None:
    global _TRADING_SAFETY_PATROL_THREAD
    if _thread_is_alive(_TRADING_SAFETY_PATROL_THREAD):
        return
    _TRADING_SAFETY_PATROL_STOP_EVENT.clear()
    _TRADING_SAFETY_PATROL_THREAD = threading.Thread(
        target=_trading_safety_patrol_loop,
        daemon=True,
        name="gui-trading-safety-patrol",
    )
    _TRADING_SAFETY_PATROL_THREAD.start()


def _refresh_status_cache_worker() -> None:
    refresh_ts = time.time()
    try:
        payload = _build_status_payload()
        terminal_status_mtime = 0.0
        terminal_status_size = 0
        try:
            mt5_dir = config_manager.get_mt5_dir()
            if mt5_dir:
                sf = _resolve_terminal_status_file(mt5_dir)
                if sf and sf.exists():
                    st = sf.stat()
                    terminal_status_mtime = float(st.st_mtime)
                    terminal_status_size = int(st.st_size)
        except Exception:
            terminal_status_mtime = 0.0
            terminal_status_size = 0
        with _STATUS_CACHE_LOCK:
            _STATUS_RESPONSE_CACHE["ts"] = refresh_ts
            _STATUS_RESPONSE_CACHE["value"] = payload
            _STATUS_RESPONSE_CACHE["terminal_status_mtime"] = terminal_status_mtime
            _STATUS_RESPONSE_CACHE["terminal_status_size"] = terminal_status_size
        _record_trading_safety_transition(
            payload.get("tradingSafety", {})
            if isinstance(payload.get("tradingSafety", {}), dict)
            else {}
        )
        _store_lightweight_status_payload(
            _build_lightweight_payload_from_status(payload, cache_age_sec=0.0, refreshing=False, now=refresh_ts),
            now=refresh_ts,
        )
    finally:
        with _STATUS_CACHE_LOCK:
            _STATUS_RESPONSE_CACHE["refreshing"] = False
            _STATUS_RESPONSE_CACHE["thread"] = None
            _STATUS_RESPONSE_CACHE["refresh_started_ts"] = 0.0

def _mt5_history_sync_worker() -> None:
    import time as _time
    while True:
        try:
            from src.sync_mt5_history import sync_to_brain
            sync_to_brain(days_back=30)
        except Exception:
            pass
        _time.sleep(120)


def _schedule_status_cache_refresh() -> bool:
    with _STATUS_CACHE_LOCK:
        if bool(_STATUS_RESPONSE_CACHE.get("refreshing", False)):
            return False
        existing = _find_alive_thread_by_name("gui-status-refresh")
        if existing is not None:
            _STATUS_RESPONSE_CACHE["refreshing"] = True
            _STATUS_RESPONSE_CACHE["thread"] = existing
            _STATUS_RESPONSE_CACHE["refresh_started_ts"] = float(
                _STATUS_RESPONSE_CACHE.get("refresh_started_ts", 0.0) or time.time()
            )
            return False
        t = threading.Thread(
            target=_refresh_status_cache_worker,
            daemon=True,
            name="gui-status-refresh",
        )
        _STATUS_RESPONSE_CACHE["refreshing"] = True
        _STATUS_RESPONSE_CACHE["thread"] = t
        _STATUS_RESPONSE_CACHE["refresh_started_ts"] = time.time()
        t.start()
        return True


def _invalidate_runtime_status_caches(*, schedule_refresh: bool = False) -> None:
    with _STATUS_CACHE_LOCK:
        _STATUS_RESPONSE_CACHE.update(
            {
                "ts": 0.0,
                "value": None,
                "refreshing": False,
                "thread": None,
                "refresh_started_ts": 0.0,
                "terminal_status_mtime": 0.0,
                "terminal_status_size": 0,
            }
        )
    _LIGHTWEIGHT_STATUS_CACHE.update({"ts": 0.0, "value": None})
    with _RUNTIME_OBSERVABILITY_CACHE_LOCK:
        _RUNTIME_OBSERVABILITY_CACHE.update(
            {
                "ts": 0.0,
                "value": None,
                "refreshing": False,
                "thread": None,
                "refresh_started_ts": 0.0,
            }
        )
    if schedule_refresh:
        try:
            _schedule_status_cache_refresh()
        except Exception:
            pass
        try:
            _schedule_runtime_observability_refresh()
        except Exception:
            pass


def _build_status_fallback_payload() -> dict[str, Any]:
    return {
        "isRunning": True,
        "mt5Online": False,
        "bridgeOk": False,
        "bid": 0,
        "ask": 0,
        "spread": 0,
        "priceSource": "warming_up",
        "priceAge": 0,
        "riskLevel": "平衡",
        "light": "YELLOW",
        "agent_dir": "NEUTRAL",
        "agent_direction_data": {},
        "technical_trend_bias": "UNKNOWN",
        "technical_trend_data": {},
        "is_defensive": True,
        "regime": "UNKNOWN",
        "risk_block": {},
        "final_reject": {},
        "overlap_budget": {},
        "overlap_budget_target": 0,
        "tradesCount": 0,
        "todayTrades": 0,
        "todayOpenTrades": 0,
        "todayClosedTrades": 0,
        "todayPnl": 0.0,
        "todayWinRate": 0.0,
        "todayAvgWin": 0.0,
        "todayAvgLoss": 0.0,
        "todayWins": 0,
        "todayLosses": 0,
        "positionOpen": False,
        "positionSide": None,
        "positionEntry": 0,
        "positions": [],
        "positionTier": {
            "tier": "NORMAL",
            "tierLabel": "正常仓位",
            "tone": "success",
            "targetLots": 0.0,
            "reasonCode": "",
            "reasonText": "",
            "activeRules": [],
            "recoveryNeeded": [],
            "recoveryProgress": {},
            "recoveryProgressItems": [],
            "recoveryProgressPercent": 100.0,
            "updatedAt": "",
            "sinceTs": 0.0,
            "sinceAt": "",
            "lastTransition": "",
            "consecutiveLosses": 0,
            "regime": "UNKNOWN",
            "positionOpen": False,
        },
        "account_total_loss_stop_pct": 30.0,
        "account_total_loss_current_pct": 0.0,
        "account_total_loss_stop_active": False,
        "account_total_loss_stop_source": "NONE",
        "soft_limits": [],
        "system_stats": {
            "cpu_percent": 0.0,
            "ram_used_mb": 0,
            "ram_percent": 0.0,
            "process_rss_mb": 0.0,
            "memory_warning": False,
            "mt5_latency": -1,
        },
        "blackboard": {},
        "tradingSafety": {
            "runtimeMode": "BOOTSTRAP_REPAIR",
            "permissions": _trading_permissions_for_mode("BOOTSTRAP_REPAIR"),
            "issues": [{"code": "STATUS_WARMING_UP", "message": "状态正在预热，暂未放开交易能力。"}],
            "bridgeHealth": {
                "heartbeatOk": False,
                "heartbeatAgeSec": None,
                "executionReportOk": False,
                "executionReportAgeSec": None,
                "terminalStatusFile": "",
                "executionReportFile": str(DATA_DIR / "inbox" / "execution_report.json"),
                "positionOpen": False,
            },
        },
        "warmingUp": True,
    }


@app.get("/api/status")
def get_status():
    """获取整体运行状态与行情"""
    cache_now = time.time()
    cached_payload = None
    cache_age = 0.0
    refreshing = False
    cached_terminal_status_mtime = 0.0
    cached_terminal_status_size = 0
    schedule_refresh_after_unlock = False
    with _STATUS_CACHE_LOCK:
        cached_payload = _STATUS_RESPONSE_CACHE.get("value")
        cache_age = cache_now - float(_STATUS_RESPONSE_CACHE.get("ts", 0.0) or 0.0)
        refreshing = bool(_STATUS_RESPONSE_CACHE.get("refreshing", False))
        cached_terminal_status_mtime = float(_STATUS_RESPONSE_CACHE.get("terminal_status_mtime", 0.0) or 0.0)
        cached_terminal_status_size = int(_STATUS_RESPONSE_CACHE.get("terminal_status_size", 0) or 0)
        if refreshing:
            thread = _STATUS_RESPONSE_CACHE.get("thread")
            started_ts = float(_STATUS_RESPONSE_CACHE.get("refresh_started_ts", 0.0) or 0.0)
            thread_alive = bool(getattr(thread, "is_alive", lambda: False)()) if thread is not None else False
            stuck = started_ts > 0 and (cache_now - started_ts) >= 60.0
            if (thread is not None and not thread_alive) or stuck:
                _STATUS_RESPONSE_CACHE["refreshing"] = False
                _STATUS_RESPONSE_CACHE["thread"] = None
                _STATUS_RESPONSE_CACHE["refresh_started_ts"] = 0.0
                refreshing = False
                schedule_refresh_after_unlock = True
    if schedule_refresh_after_unlock:
        _schedule_status_cache_refresh()

    if cached_payload is not None and cache_age < _STATUS_CACHE_TTL_SEC:
        daemon_running_now = _resolve_daemon_running()
        if bool(cached_payload.get("isRunning", False)) != daemon_running_now:
            _schedule_status_cache_refresh()
            patched = dict(cached_payload) if isinstance(cached_payload, dict) else {}
            patched["isRunning"] = daemon_running_now
            mt5_online_cached = bool(patched.get("mt5Online", False))
            bridge_ok_cached = bool(patched.get("bridgeOk", False))
            patched["light"] = "GREEN" if (daemon_running_now and mt5_online_cached and bridge_ok_cached) else ("YELLOW" if mt5_online_cached else "RED")
            return patched
        terminal_status_changed = False
        sf = None
        try:
            mt5_dir = config_manager.get_mt5_dir()
            if mt5_dir:
                sf = _resolve_terminal_status_file(mt5_dir)
            if sf and sf.exists():
                st = sf.stat()
                terminal_status_changed = (
                    float(st.st_mtime) != cached_terminal_status_mtime
                    or int(st.st_size) != cached_terminal_status_size
                )
        except Exception:
            terminal_status_changed = False
            sf = None

        if terminal_status_changed:
            _schedule_status_cache_refresh()
            try:
                if sf and sf.exists():
                    _, st_data = _read_terminal_status_snapshot(mt5_dir)
                    terminal_positions = _extract_terminal_status_positions(st_data)
                    terminal_positions_count = _terminal_status_positions_count(st_data)
                    if terminal_positions_count == 0:
                        patched = dict(cached_payload) if isinstance(cached_payload, dict) else {}
                        patched["positionOpen"] = False
                        patched["positionSide"] = None
                        patched["positionEntry"] = 0
                        patched["positions"] = []
                        return patched
                    if terminal_positions:
                        patched = dict(cached_payload) if isinstance(cached_payload, dict) else {}
                        patched_positions = _enrich_positions_with_attribution(terminal_positions)
                        patched["positionOpen"] = True
                        patched["positionSide"] = patched_positions[0].get("side")
                        patched["positionEntry"] = patched_positions[0].get("entry", 0)
                        patched["positions"] = patched_positions
                        return patched
            except Exception:
                pass

        cached_positions = (
            cached_payload.get("positions", [])
            if isinstance(cached_payload, dict) and isinstance(cached_payload.get("positions", []), list)
            else []
        )
        cached_position_open = bool(cached_payload.get("positionOpen", False)) if isinstance(cached_payload, dict) else False
        if sf and sf.exists() and not cached_position_open and not cached_positions:
            try:
                _, st_data = _read_terminal_status_snapshot(mt5_dir)
                terminal_positions = _extract_terminal_status_positions(st_data)
                terminal_positions_count = _terminal_status_positions_count(st_data)
                if (terminal_positions_count or 0) > 0 and terminal_positions:
                    _schedule_status_cache_refresh()
                    patched = dict(cached_payload) if isinstance(cached_payload, dict) else {}
                    patched_positions = _enrich_positions_with_attribution(terminal_positions)
                    patched["positionOpen"] = True
                    patched["positionSide"] = patched_positions[0].get("side")
                    patched["positionEntry"] = patched_positions[0].get("entry", 0)
                    patched["positions"] = patched_positions
                    return patched
            except Exception:
                pass

        return cached_payload

    with _STATUS_CACHE_LOCK:
        cached_payload = _STATUS_RESPONSE_CACHE.get("value")
        refreshing = bool(_STATUS_RESPONSE_CACHE.get("refreshing", False))
        if not refreshing:
            t = threading.Thread(
                target=_refresh_status_cache_worker,
                daemon=True,
                name="gui-status-refresh",
            )
            _STATUS_RESPONSE_CACHE["refreshing"] = True
            _STATUS_RESPONSE_CACHE["thread"] = t
            _STATUS_RESPONSE_CACHE["refresh_started_ts"] = cache_now
            t.start()
        if cached_payload is not None:
            return cached_payload
    return _build_status_fallback_payload()


_HEAP_SNAPSHOT_START = None  # (timestamp, tracemalloc_snapshot)


def _deep_getsizeof(obj, seen=None):
    """递归计算对象及其引用子树的实际字节数（避免重复计算循环引用）"""
    if seen is None:
        seen = set()
    obj_id = id(obj)
    if obj_id in seen:
        return 0
    seen.add(obj_id)
    size = sys.getsizeof(obj)
    if isinstance(obj, dict):
        size += sum(_deep_getsizeof(k, seen) + _deep_getsizeof(v, seen) for k, v in obj.items())
    elif isinstance(obj, (list, tuple, set, frozenset)):
        size += sum(_deep_getsizeof(item, seen) for item in obj)
    elif isinstance(obj, (str, bytes, bytearray)):
        pass  # getsizeof already captured string/bytes overhead
    elif hasattr(obj, '__dict__'):
        size += _deep_getsizeof(obj.__dict__, seen)
    return size


def get_memory_debug(deep: int = 0):
    """内存诊断（默认轻量）。deep=1 时才执行深度扫描，避免拖垮 18790。"""
    import gc

    result = {
        "process_rss_mb": 0,
        "python_heap_mb": None,
        "caches": {},
        "blackboard_mb": None,
        "file_sizes": {},
        "gc_stats": None,
        "mode": "deep" if deep else "light",
    }
    try:
        import psutil
        p = psutil.Process(os.getpid())
        result["process_rss_mb"] = round(p.memory_info().rss / 1024 / 1024, 1)
    except Exception:
        pass
    result["gc_stats"] = gc.get_stats()
    try:
        cache_keys = [
            "_MT5_DAILY_CACHE", "_STATUS_RESPONSE_CACHE", "_TODAY_STATS_CACHE",
            "_RUNTIME_OBSERVABILITY_CACHE", "_GOAL_TARGETS_CACHE",
            "_PANIC_ACTION_STATE", "_DIRECTION_V2_CACHE",
            "_MACRO_REFRESH_THREAD",
        ]
        for k in cache_keys:
            if k in globals():
                try:
                    if deep:
                        sz = _deep_getsizeof(globals()[k])
                        result["caches"][k] = f"{sz / 1024 / 1024:.2f} MB"
                    else:
                        sz = sys.getsizeof(globals()[k])
                        result["caches"][k] = f"{sz / 1024:.1f} KB"
                except Exception as ex:
                    result["caches"][k] = f"error: {ex}"
        # 关键文件尺寸
        for fname, rel in [
            ("blackboard_state.json", "blackboard_state.json"),
            ("agent_state.json", "bridge/state/agent_state.json"),
            ("execution_state.json", "execution_state.json"),
            ("position_lifecycle.json", "position_lifecycle.json"),
            ("agent_decision_contract.json", "agent_decision_contract.json"),
        ]:
            fpath = DATA_DIR / rel
            if fpath.exists():
                result["file_sizes"][fname] = f"{fpath.stat().st_size / 1024:.1f} KB"
        if deep:
            try:
                gc.collect()
                result["python_heap_mb"] = round(sum(sys.getsizeof(o) for o in gc.get_objects()) / 1024 / 1024, 1)
            except Exception as ex:
                result["python_heap_mb"] = f"error: {ex}"
            try:
                global_blackboard = _load_global_blackboard()
                bb = global_blackboard._ensure() if global_blackboard and hasattr(global_blackboard, "_ensure") else None
                if bb is None and global_blackboard is not None:
                    bb = getattr(global_blackboard, "_instance", None)
                if bb is not None:
                    result["blackboard_mb"] = round(_deep_getsizeof(vars(bb)) / 1024 / 1024, 3)
            except Exception as ex:
                result["blackboard_mb"] = f"error: {ex}"
    except Exception as e:
        result["error"] = str(e)
    return result

def _get_blackboard_data():
    try:
        bb_file = _resolve_runtime_root_artifact("blackboard_state.json")
        now = time.time()
        if bb_file is None or not bb_file.exists():
            return None
        try:
            stat = bb_file.stat()
            mtime = float(stat.st_mtime)
            size = int(stat.st_size)
        except Exception:
            mtime = 0.0
            size = 0
        cached_value = _BLACKBOARD_CACHE.get("value")
        if (
            cached_value is not None
            and (now - float(_BLACKBOARD_CACHE.get("ts", 0.0))) < 2.0
            and str(_BLACKBOARD_CACHE.get("file", "")) == str(bb_file)
            and float(_BLACKBOARD_CACHE.get("mtime", 0.0)) == mtime
            and int(_BLACKBOARD_CACHE.get("size", 0)) == size
        ):
            return cached_value
        # A very large blackboard file can stall the status route; keep serving cached value.
        if size > 2 * 1024 * 1024 and cached_value is not None:
            return cached_value
        import json
        data = json.loads(bb_file.read_text(encoding="utf-8"))
        _BLACKBOARD_CACHE.update({
            "ts": now,
            "file": str(bb_file),
            "mtime": mtime,
            "size": size,
            "value": data,
        })
        return data
    except Exception:
        pass
    return None


def _clean_str_list(values: Any) -> list[str]:
    if not isinstance(values, list):
        return []
    return [str(item).strip() for item in values if str(item).strip()]


def _first_non_empty_str(*values: Any) -> str:
    for value in values:
        text = str(value or "").strip()
        if text:
            return text
    return ""


def _normalize_route_selection(
    raw_selection: Any,
    *,
    selected_strategy_id: str = "",
    selected_instance_id: str = "",
) -> dict[str, Any]:
    selection = raw_selection if isinstance(raw_selection, dict) else {}
    selection_passed = bool(selection.get("passed", False))
    winner_strategy_id = _first_non_empty_str(
        selection.get("winner_strategy_id"),
        selected_strategy_id if selection_passed else "",
    )
    winner_instance_id = _first_non_empty_str(
        selection.get("winner_instance_id"),
        selected_instance_id if selection_passed else "",
    )
    ranked_candidates = (
        copy.deepcopy(selection.get("ranked_candidates", []))
        if isinstance(selection.get("ranked_candidates", []), list)
        else []
    )
    return {
        "passed": selection_passed,
        "reason": _first_non_empty_str(selection.get("reason"), "NO_SELECTION"),
        "winnerStrategyId": winner_strategy_id,
        "winnerInstanceId": winner_instance_id,
        "winnerScore": round(float(selection.get("winner_score", 0.0) or 0.0), 4),
        "runnerUpScore": round(float(selection.get("runner_up_score", 0.0) or 0.0), 4),
        "selectionMargin": round(float(selection.get("selection_margin", 0.0) or 0.0), 4),
        "rankedCandidates": ranked_candidates,
    }


def _resolve_scheduler_route_winner_fields(raw_scheduler: Any) -> tuple[str, str]:
    scheduler = raw_scheduler if isinstance(raw_scheduler, dict) else {}
    route = scheduler.get("route", {}) if isinstance(scheduler.get("route", {}), dict) else {}
    selection = (
        scheduler.get("selection", route.get("selection", {}))
        if isinstance(scheduler.get("selection", route.get("selection", {})), dict)
        else {}
    )
    if not bool(selection.get("passed", False)):
        return "", ""
    return (
        _first_non_empty_str(selection.get("winner_strategy_id")),
        _first_non_empty_str(selection.get("winner_instance_id")),
    )


def _resolve_scheduler_selected_fields(raw_scheduler: Any) -> tuple[str, str]:
    scheduler = raw_scheduler if isinstance(raw_scheduler, dict) else {}
    route = scheduler.get("route", {}) if isinstance(scheduler.get("route", {}), dict) else {}
    selection = (
        scheduler.get("selection", route.get("selection", {}))
        if isinstance(scheduler.get("selection", route.get("selection", {})), dict)
        else {}
    )
    execution_decision = (
        scheduler.get("execution_decision", {})
        if isinstance(scheduler.get("execution_decision", {}), dict)
        else {}
    )
    selected_strategy_id = _first_non_empty_str(
        execution_decision.get("executed_strategy_id"),
        execution_decision.get("selected_strategy_id"),
        selection.get("winner_strategy_id"),
        scheduler.get("selected_strategy_id"),
    )
    selected_instance_id = _first_non_empty_str(
        execution_decision.get("executed_instance_id"),
        execution_decision.get("selected_instance_id"),
        selection.get("winner_instance_id"),
        scheduler.get("selected_instance_id"),
    )
    return selected_strategy_id, selected_instance_id


def _normalize_canonical_market_snapshot(raw_snapshot: Any) -> dict[str, Any]:
    snapshot = raw_snapshot if isinstance(raw_snapshot, dict) else {}
    normalized = (
        snapshot.get("normalized", {})
        if isinstance(snapshot.get("normalized", {}), dict)
        else {}
    )
    return {
        "snapshotId": _first_non_empty_str(snapshot.get("snapshot_id")),
        "generatedAt": _first_non_empty_str(snapshot.get("generated_at")),
        "symbol": _first_non_empty_str(snapshot.get("symbol"), "XAUUSD"),
        "timeframe": _first_non_empty_str(snapshot.get("timeframe"), "M5"),
        "marketFamily": _first_non_empty_str(normalized.get("market_family")),
        "marketPhase": _first_non_empty_str(normalized.get("market_phase")),
        "entryFamily": _first_non_empty_str(normalized.get("entry_family")),
        "sessionBucket": _first_non_empty_str(normalized.get("session_bucket")),
        "directionBias": _first_non_empty_str(normalized.get("direction_bias")),
        "volatilityState": _first_non_empty_str(normalized.get("volatility_state")),
        "structureState": _first_non_empty_str(normalized.get("structure_state")),
        "confidence": round(float(normalized.get("confidence", 0.0) or 0.0), 4),
        "normalizationReason": _first_non_empty_str(normalized.get("normalization_reason")),
        "normalized": copy.deepcopy(normalized),
        "raw": (
            copy.deepcopy(snapshot.get("raw", {}))
            if isinstance(snapshot.get("raw", {}), dict)
            else {}
        ),
        "sourceContext": (
            copy.deepcopy(snapshot.get("source_context", {}))
            if isinstance(snapshot.get("source_context", {}), dict)
            else {}
        ),
    }


def _normalize_execution_decision(
    raw_decision: Any,
    *,
    route_payload: Optional[dict[str, Any]] = None,
    selected_strategy_id: str = "",
    selected_instance_id: str = "",
) -> dict[str, Any]:
    decision = raw_decision if isinstance(raw_decision, dict) else {}
    route = route_payload if isinstance(route_payload, dict) else {}
    canonical_market = _normalize_canonical_market_snapshot(
        route.get("canonical_market_snapshot", {})
        if isinstance(route.get("canonical_market_snapshot", {}), dict)
        else {}
    )
    normalized_action = _first_non_empty_str(decision.get("action"), "HOLD").upper()
    normalized_side = _first_non_empty_str(decision.get("side"), "NONE").upper()
    normalized_decision = _first_non_empty_str(
        decision.get("decision"),
        "EXECUTE" if normalized_action == "OPEN" and normalized_side in {"BUY", "SELL"} else "HOLD",
    ).upper()
    return {
        "decision": normalized_decision,
        "action": normalized_action,
        "side": normalized_side,
        "selectedStrategyId": _first_non_empty_str(
            decision.get("selected_strategy_id"),
            selected_strategy_id,
        ),
        "selectedInstanceId": _first_non_empty_str(
            decision.get("selected_instance_id"),
            selected_instance_id,
        ),
        "entryOwnerStrategyId": _first_non_empty_str(
            decision.get("entry_owner_strategy_id"),
            route.get("entry_owner_strategy_id"),
        ),
        "entryOwnerInstanceId": _first_non_empty_str(
            decision.get("entry_owner_instance_id"),
            route.get("entry_owner_instance_id"),
        ),
        "executedStrategyId": _first_non_empty_str(
            decision.get("executed_strategy_id"),
            decision.get("entry_owner_strategy_id"),
            route.get("entry_owner_strategy_id"),
        ),
        "executedInstanceId": _first_non_empty_str(
            decision.get("executed_instance_id"),
            decision.get("entry_owner_instance_id"),
            route.get("entry_owner_instance_id"),
        ),
        "entryOwnerAttributionStatus": (
            "resolved"
            if _first_non_empty_str(
                decision.get("entry_owner_strategy_id"),
                route.get("entry_owner_strategy_id"),
            )
            else "missing"
        ),
        "executionAttributionStatus": (
            "resolved"
            if _first_non_empty_str(
                decision.get("executed_strategy_id"),
                decision.get("entry_owner_strategy_id"),
                route.get("entry_owner_strategy_id"),
            )
            else "missing"
        ),
        "reason": _first_non_empty_str(decision.get("reason")),
        "confidence": round(float(decision.get("confidence", 0.0) or 0.0), 4),
        "routeFamily": _first_non_empty_str(decision.get("route_family"), route.get("route_family")),
        "marketFamily": _first_non_empty_str(
            decision.get("market_family"),
            canonical_market.get("marketFamily"),
        ),
        "entryFamily": _first_non_empty_str(
            decision.get("entry_family"),
            canonical_market.get("entryFamily"),
        ),
        "fitReason": _first_non_empty_str(decision.get("fit_reason")),
        "decisionSource": _first_non_empty_str(decision.get("decision_source")),
    }


def _load_decision_snapshot_macro_payload() -> dict[str, Any]:
    try:
        snapshot = load_decision_snapshot()
    except Exception:
        return {}
    if not isinstance(snapshot, dict):
        return {}
    macro_payload = snapshot.get("macro", {})
    if isinstance(macro_payload, dict) and macro_payload:
        return macro_payload
    return snapshot


def _normalize_role_card_bias(value: Any) -> str:
    normalized = str(value or "").strip().upper()
    if normalized in {"BUY", "BULL"}:
        normalized = "LONG"
    elif normalized in {"SELL", "BEAR"}:
        normalized = "SHORT"
    if normalized in {"LONG", "SHORT", "NEUTRAL"}:
        return normalized
    return "UNKNOWN"


def _normalize_role_card_confidence(*values: Any) -> Optional[float]:
    for value in values:
        if value is None or value == "":
            continue
        try:
            return max(0.0, min(1.0, float(value)))
        except (TypeError, ValueError):
            continue
    return None


def _build_macro_commander_preview(
    macro: dict[str, Any],
    route_payload: dict[str, Any],
) -> dict[str, Any]:
    decision_macro = _load_decision_snapshot_macro_payload()
    macro_bias = _normalize_role_card_bias(macro.get("bias"))
    decision_direction = _normalize_role_card_bias(decision_macro.get("direction"))
    decision_effective_direction = _normalize_role_card_bias(
        decision_macro.get("effective_direction")
    )
    route_direction = _normalize_role_card_bias(route_payload.get("macro_direction"))
    route_effective_direction = _normalize_role_card_bias(
        route_payload.get("effective_macro_direction")
    )
    route_binding_direction = _normalize_role_card_bias(
        route_payload.get("binding_macro_direction")
    )
    decision_state = str(decision_macro.get("state", "") or "").strip().upper()
    if macro_bias != "UNKNOWN":
        bias = macro_bias
    elif decision_direction in {"LONG", "SHORT"}:
        bias = decision_direction
    elif decision_effective_direction in {"LONG", "SHORT"}:
        bias = decision_effective_direction
    elif route_direction != "UNKNOWN":
        bias = route_direction
    elif route_effective_direction != "UNKNOWN":
        bias = route_effective_direction
    elif route_binding_direction != "UNKNOWN":
        bias = route_binding_direction
    elif decision_state and decision_state != "UNKNOWN" and decision_direction != "UNKNOWN":
        bias = decision_direction
    else:
        bias = decision_direction
    return {
        "bias": bias,
        "confidence": _normalize_role_card_confidence(
            macro.get("confidence"),
            decision_macro.get("bias_weight"),
        ),
        "daily_calls": macro.get("daily_calls", 0),
        "trigger_reason": macro.get("trigger_reason"),
    }


def _build_micro_sniper_preview(
    micro: dict[str, Any],
    route_payload: dict[str, Any],
) -> dict[str, Any]:
    micro_regime = _first_non_empty_str(micro.get("current_regime"))
    route_regime = _first_non_empty_str(
        route_payload.get("regime"),
        route_payload.get("regime_subtype"),
    )
    current_regime = micro_regime or route_regime
    normalized_regime = normalize_regime_label(current_regime)
    display_regime = (
        str(route_regime).strip().upper()
        if not micro_regime and route_regime
        else normalized_regime or None
    )
    if "is_defensive_env" in micro:
        is_defensive_env = bool(micro.get("is_defensive_env"))
    else:
        is_defensive_env = is_range_regime(normalized_regime) or is_defensive_regime(normalized_regime)
    return {
        "is_defensive_env": is_defensive_env,
        "knife_catching_risk": bool(micro.get("knife_catching_risk", False)),
        "ny_session_active": micro.get("ny_session_active"),
        "current_regime": display_regime,
        "override_counter": micro.get("override_counter", 0),
        "override_threshold": micro.get("override_threshold", 2),
    }


def _build_risk_commissar_preview(risk: dict[str, Any]) -> dict[str, Any]:
    try:
        position_multiplier = float(risk.get("position_multiplier", 1.0) or 1.0)
    except (TypeError, ValueError):
        position_multiplier = 1.0
    return {
        "position_multiplier": max(0.0, position_multiplier),
    }


def _strategy_name_from_catalog(catalog: dict[str, str], strategy_id: str, default: str = "未知策略") -> str:
    normalized = str(strategy_id or "").strip()
    if not normalized:
        return default
    return str(catalog.get(normalized) or normalized)


def _normalize_session_scheduler_state(
    raw_scheduler: Any,
    *,
    configured_scheduler: dict[str, Any],
    source: str,
) -> dict[str, Any]:
    scheduler = raw_scheduler if isinstance(raw_scheduler, dict) else {}
    decision = scheduler.get("decision", {}) if isinstance(scheduler.get("decision", {}), dict) else {}
    current_policy = (
        scheduler.get("current_policy", {})
        if isinstance(scheduler.get("current_policy", {}), dict)
        else scheduler.get("currentPolicy", {})
        if isinstance(scheduler.get("currentPolicy", {}), dict)
        else {}
    )
    configured_default = (
        configured_scheduler.get("default_when_disabled", {})
        if isinstance(configured_scheduler.get("default_when_disabled", {}), dict)
        else {}
    )
    configured_policies = (
        configured_scheduler.get("policies", {})
        if isinstance(configured_scheduler.get("policies", {}), dict)
        else {}
    )
    return {
        "enabled": bool(scheduler.get("enabled", configured_scheduler.get("enabled", False))),
        "mode": _first_non_empty_str(scheduler.get("mode"), configured_scheduler.get("mode"), "soft_weight"),
        "currentSession": _first_non_empty_str(
            scheduler.get("current_session"),
            scheduler.get("currentSession"),
        ),
        "currentSessionProfile": _first_non_empty_str(
            scheduler.get("current_session_profile"),
            scheduler.get("currentSessionProfile"),
        ),
        "currentPolicy": copy.deepcopy(current_policy),
        "qualifiedStrategyIds": _clean_str_list(
            scheduler.get("qualified_strategy_ids") or scheduler.get("qualifiedStrategyIds")
        ),
        "blockedStrategyIds": _clean_str_list(
            scheduler.get("blocked_strategy_ids") or scheduler.get("blockedStrategyIds")
        ),
        "candidateStrategyIds": _clean_str_list(
            scheduler.get("candidate_strategy_ids") or scheduler.get("candidateStrategyIds")
        ),
        "allowedSessions": _clean_str_list(
            scheduler.get("user_allowed_sessions")
            or scheduler.get("userAllowedSessions")
            or configured_scheduler.get("allowed_sessions")
        ),
        "blockedSessions": _clean_str_list(
            scheduler.get("user_blocked_sessions")
            or scheduler.get("userBlockedSessions")
            or configured_scheduler.get("blocked_sessions")
        ),
        "defaultWhenDisabled": copy.deepcopy(configured_default),
        "policies": copy.deepcopy(configured_policies),
        "decision": {
            "allowEntry": bool(decision.get("allow_entry")) if "allow_entry" in decision else None,
            "allowExit": bool(decision.get("allow_exit")) if "allow_exit" in decision else None,
            "allowAddPosition": (
                bool(decision.get("allow_add_position"))
                if "allow_add_position" in decision
                else None
            ),
            "reduceOnly": bool(decision.get("reduce_only")) if "reduce_only" in decision else None,
            "reasonCode": str(decision.get("reason_code", "") or ""),
            "reasonText": str(decision.get("reason_text", "") or ""),
        },
        "source": source,
        "hasRuntimeState": bool(
            scheduler
            and (
                _first_non_empty_str(
                    scheduler.get("current_session"),
                    scheduler.get("currentSession"),
                )
                or decision
                or current_policy
                or scheduler.get("qualified_strategy_ids")
                or scheduler.get("qualifiedStrategyIds")
                or scheduler.get("blocked_strategy_ids")
                or scheduler.get("blockedStrategyIds")
                or scheduler.get("candidate_strategy_ids")
                or scheduler.get("candidateStrategyIds")
            )
        ),
    }


def _build_session_scheduler_projection(
    *,
    decision_policy: dict[str, Any],
    runtime_scheduler: dict[str, Any],
    scheduler_fallback: dict[str, Any],
) -> dict[str, Any]:
    configured_scheduler = (
        decision_policy.get("session_scheduler", {})
        if isinstance(decision_policy.get("session_scheduler", {}), dict)
        else {}
    )
    runtime_session = (
        runtime_scheduler.get("session_scheduler", {})
        if isinstance(runtime_scheduler.get("session_scheduler", {}), dict)
        else {}
    )
    fallback_session = (
        scheduler_fallback.get("session_scheduler", {})
        if isinstance(scheduler_fallback.get("session_scheduler", {}), dict)
        else {}
    )
    configured_projection = {
        "enabled": bool(configured_scheduler.get("enabled", False)),
        "mode": _first_non_empty_str(configured_scheduler.get("mode"), "soft_weight"),
        "allowedSessions": _clean_str_list(configured_scheduler.get("allowed_sessions")),
        "blockedSessions": _clean_str_list(configured_scheduler.get("blocked_sessions")),
        "defaultWhenDisabled": copy.deepcopy(
            configured_scheduler.get("default_when_disabled", {})
            if isinstance(configured_scheduler.get("default_when_disabled", {}), dict)
            else {}
        ),
        "policies": copy.deepcopy(
            configured_scheduler.get("policies", {})
            if isinstance(configured_scheduler.get("policies", {}), dict)
            else {}
        ),
    }
    runtime_projection = _normalize_session_scheduler_state(
        runtime_session,
        configured_scheduler=configured_scheduler,
        source="runtime_scheduler_snapshot" if runtime_session else "none",
    )
    fallback_projection = _normalize_session_scheduler_state(
        fallback_session,
        configured_scheduler=configured_scheduler,
        source="effective_runtime_scheduler" if fallback_session else "none",
    )
    effective_source = (
        "runtime_scheduler_snapshot"
        if runtime_session
        else "effective_runtime_scheduler"
        if fallback_session
        else "configured"
    )
    effective_projection = _normalize_session_scheduler_state(
        runtime_session if runtime_session else fallback_session,
        configured_scheduler=configured_scheduler,
        source=effective_source,
    )
    return {
        "configured": configured_projection,
        "runtime": runtime_projection,
        "fallback": fallback_projection,
        "effective": effective_projection,
        "summary": {
            "enabled": bool(effective_projection.get("enabled", False)),
            "mode": str(effective_projection.get("mode", "soft_weight") or "soft_weight"),
            "currentSession": str(effective_projection.get("currentSession", "") or ""),
            "currentSessionProfile": str(effective_projection.get("currentSessionProfile", "") or ""),
            "allowEntry": (
                effective_projection.get("decision", {})
                if isinstance(effective_projection.get("decision", {}), dict)
                else {}
            ).get("allowEntry"),
            "reduceOnly": (
                effective_projection.get("decision", {})
                if isinstance(effective_projection.get("decision", {}), dict)
                else {}
            ).get("reduceOnly"),
            "reasonCode": str(
                (
                    effective_projection.get("decision", {})
                    if isinstance(effective_projection.get("decision", {}), dict)
                    else {}
                ).get("reasonCode", "")
                or ""
            ),
            "reasonText": str(
                (
                    effective_projection.get("decision", {})
                    if isinstance(effective_projection.get("decision", {}), dict)
                    else {}
                ).get("reasonText", "")
                or ""
            ),
            "qualifiedStrategyCount": len(
                effective_projection.get("qualifiedStrategyIds", [])
                if isinstance(effective_projection.get("qualifiedStrategyIds", []), list)
                else []
            ),
            "blockedStrategyCount": len(
                effective_projection.get("blockedStrategyIds", [])
                if isinstance(effective_projection.get("blockedStrategyIds", []), list)
                else []
            ),
            "candidateStrategyCount": len(
                effective_projection.get("candidateStrategyIds", [])
                if isinstance(effective_projection.get("candidateStrategyIds", []), list)
                else []
            ),
            "sourceLayer": effective_source,
        },
    }


def _normalize_strategy_projection(
    *,
    catalog: dict[str, str],
    effective_payload: dict[str, Any],
    runtime_active: dict[str, Any],
    runtime_scheduler: dict[str, Any],
    scheduler_fallback: dict[str, Any],
    config_revision: str = "",
    effective_updated_at: Optional[float] = None,
    loaded_set: Optional[set[str]] = None,
) -> dict[str, Any]:
    runtime_snapshot_source = bool(
        runtime_scheduler
        and (
            _first_non_empty_str(
                runtime_scheduler.get("snapshot_id"),
                runtime_scheduler.get("generated_at"),
            )
            or bool(
                runtime_scheduler.get("source_versions", {})
                if isinstance(runtime_scheduler.get("source_versions", {}), dict)
                else {}
            )
        )
    )
    configured_strategy_id = _first_non_empty_str(
        effective_payload.get("strategy_type"),
        runtime_active.get("strategy_id"),
        runtime_scheduler.get("selected_strategy_id"),
        scheduler_fallback.get("selected_strategy_id"),
    )
    configured_source = (
        "effective_params"
        if _first_non_empty_str(effective_payload.get("strategy_type"))
        else "strategy_runtime_status"
        if _first_non_empty_str(runtime_active.get("strategy_id"))
        else "blackboard_scheduler"
        if _first_non_empty_str(runtime_scheduler.get("selected_strategy_id"))
        else "effective_runtime_scheduler"
        if _first_non_empty_str(scheduler_fallback.get("selected_strategy_id"))
        else "default"
    )
    configured_name = _strategy_name_from_catalog(catalog, configured_strategy_id)

    runtime_active_strategy_ids = _clean_str_list(
        runtime_scheduler.get("active_strategy_ids")
        or runtime_active.get("strategy_ids")
    )
    runtime_active_instance_ids = _clean_str_list(
        runtime_scheduler.get("active_instance_ids")
        or runtime_active.get("instance_ids")
    )
    runtime_selected_strategy_id, runtime_selected_instance_id = _resolve_scheduler_selected_fields(
        runtime_scheduler
    )
    runtime_route_winner_strategy_id, runtime_route_winner_instance_id = _resolve_scheduler_route_winner_fields(
        runtime_scheduler
    )
    runtime_selected_strategy_id = _first_non_empty_str(
        runtime_selected_strategy_id,
        runtime_active.get("selected_strategy_id"),
    )
    runtime_selected_instance_id = _first_non_empty_str(
        runtime_selected_instance_id,
        runtime_active.get("selected_instance_id"),
    )
    runtime_mode_raw = _first_non_empty_str(
        runtime_scheduler.get("mode"),
        runtime_active.get("mode"),
    )
    runtime_arbiter_raw = _first_non_empty_str(
        runtime_scheduler.get("arbiter"),
        runtime_active.get("arbiter"),
    )
    runtime_reason_raw = _first_non_empty_str(
        runtime_scheduler.get("reason"),
        runtime_active.get("reason"),
    )
    runtime_route_payload = (
        runtime_scheduler.get("route", {})
        if isinstance(runtime_scheduler.get("route", {}), dict)
        else {}
    )
    runtime_direction_guard = (
        runtime_scheduler.get("direction_guard", {})
        if isinstance(runtime_scheduler.get("direction_guard", {}), dict)
        else runtime_route_payload.get("direction_guard", {})
        if isinstance(runtime_route_payload.get("direction_guard", {}), dict)
        else {}
    )
    runtime_candidate_strategy_ids = _clean_str_list(
        runtime_scheduler.get("candidate_strategy_ids")
    )
    runtime_router_weights = (
        runtime_scheduler.get("router_weights")
        if isinstance(runtime_scheduler.get("router_weights", {}), dict)
        else runtime_route_payload.get("weights", {})
        if isinstance(runtime_route_payload.get("weights", {}), dict)
        else {}
    )
    if not runtime_router_weights and isinstance(runtime_route_payload.get("router_weights", {}), dict):
        runtime_router_weights = runtime_route_payload.get("router_weights", {})
    runtime_strategy_weights = (
        runtime_scheduler.get("strategy_weights")
        if isinstance(runtime_scheduler.get("strategy_weights", {}), dict)
        else {}
    )
    runtime_instance_weights = (
        runtime_scheduler.get("instance_weights")
        if isinstance(runtime_scheduler.get("instance_weights", {}), dict)
        else {}
    )
    runtime_route_family = _first_non_empty_str(
        runtime_scheduler.get("route_family"),
        runtime_route_payload.get("route_family"),
    )
    runtime_allowed_side = _first_non_empty_str(
        runtime_scheduler.get("allowed_side"),
        runtime_route_payload.get("allowed_side"),
    )
    runtime_source = (
        "runtime_scheduler_snapshot"
        if runtime_snapshot_source
        else "blackboard_scheduler"
        if (
            runtime_active_strategy_ids
            or runtime_active_instance_ids
            or runtime_selected_strategy_id
            or runtime_selected_instance_id
            or runtime_mode_raw
            or runtime_arbiter_raw
            or runtime_reason_raw
            or runtime_route_payload
            or runtime_direction_guard
            or runtime_candidate_strategy_ids
            or runtime_router_weights
            or runtime_strategy_weights
            or runtime_instance_weights
        )
        and bool(runtime_scheduler)
        else "strategy_runtime_status"
        if bool(runtime_active)
        else "none"
    )

    fallback_active_strategy_ids = _clean_str_list(scheduler_fallback.get("active_strategy_ids"))
    fallback_active_instance_ids = _clean_str_list(
        scheduler_fallback.get("active_instance_ids")
        or scheduler_fallback.get("active_instances")
    )
    fallback_selected_strategy_id, fallback_selected_instance_id = _resolve_scheduler_selected_fields(
        scheduler_fallback
    )
    fallback_route_winner_strategy_id, fallback_route_winner_instance_id = _resolve_scheduler_route_winner_fields(
        scheduler_fallback
    )
    fallback_mode_raw = _first_non_empty_str(scheduler_fallback.get("mode"))
    fallback_arbiter_raw = _first_non_empty_str(scheduler_fallback.get("arbiter"))
    fallback_reason_raw = _first_non_empty_str(scheduler_fallback.get("reason"))
    fallback_route_payload = (
        scheduler_fallback.get("route", {})
        if isinstance(scheduler_fallback.get("route", {}), dict)
        else {}
    )
    fallback_direction_guard = (
        scheduler_fallback.get("direction_guard", {})
        if isinstance(scheduler_fallback.get("direction_guard", {}), dict)
        else fallback_route_payload.get("direction_guard", {})
        if isinstance(fallback_route_payload.get("direction_guard", {}), dict)
        else {}
    )
    fallback_candidate_strategy_ids = _clean_str_list(
        scheduler_fallback.get("candidate_strategy_ids")
        or fallback_route_payload.get("candidate_strategy_ids")
    )
    fallback_router_weights = (
        scheduler_fallback.get("router_weights")
        if isinstance(scheduler_fallback.get("router_weights", {}), dict)
        else fallback_route_payload.get("weights", {})
        if isinstance(fallback_route_payload.get("weights", {}), dict)
        else {}
    )
    if not fallback_router_weights and isinstance(fallback_route_payload.get("router_weights", {}), dict):
        fallback_router_weights = fallback_route_payload.get("router_weights", {})
    fallback_strategy_weights = (
        scheduler_fallback.get("strategy_weights")
        if isinstance(scheduler_fallback.get("strategy_weights", {}), dict)
        else {}
    )
    fallback_instance_weights = (
        scheduler_fallback.get("instance_weights")
        if isinstance(scheduler_fallback.get("instance_weights", {}), dict)
        else {}
    )
    fallback_route_family = _first_non_empty_str(
        scheduler_fallback.get("route_family"),
        fallback_route_payload.get("route_family"),
    )
    fallback_allowed_side = _first_non_empty_str(
        scheduler_fallback.get("allowed_side"),
        fallback_route_payload.get("allowed_side"),
    )
    fallback_source = "effective_runtime_scheduler" if bool(scheduler_fallback) else "none"

    runtime_selection = _normalize_route_selection(
        runtime_scheduler.get("selection", runtime_route_payload.get("selection", {})),
        selected_strategy_id=runtime_selected_strategy_id,
        selected_instance_id=runtime_selected_instance_id,
    )
    runtime_selection_raw = (
        runtime_scheduler.get("selection", runtime_route_payload.get("selection", {}))
        if isinstance(runtime_scheduler.get("selection", runtime_route_payload.get("selection", {})), dict)
        else {}
    )
    fallback_selection = _normalize_route_selection(
        scheduler_fallback.get("selection", fallback_route_payload.get("selection", {})),
        selected_strategy_id=fallback_selected_strategy_id,
        selected_instance_id=fallback_selected_instance_id,
    )
    fallback_selection_raw = (
        scheduler_fallback.get("selection", fallback_route_payload.get("selection", {}))
        if isinstance(scheduler_fallback.get("selection", fallback_route_payload.get("selection", {})), dict)
        else {}
    )
    effective_selection = (
        runtime_selection
        if runtime_selection.get("winnerStrategyId") or runtime_selection.get("winnerInstanceId")
        else fallback_selection
    )
    runtime_canonical_market = _normalize_canonical_market_snapshot(
        runtime_scheduler.get(
            "canonical_market_snapshot",
            runtime_route_payload.get("canonical_market_snapshot", {}),
        )
    )
    fallback_canonical_market = _normalize_canonical_market_snapshot(
        scheduler_fallback.get(
            "canonical_market_snapshot",
            fallback_route_payload.get("canonical_market_snapshot", {}),
        )
    )
    effective_canonical_market = (
        runtime_canonical_market
        if runtime_canonical_market.get("snapshotId")
        else fallback_canonical_market
    )
    runtime_execution_decision = _normalize_execution_decision(
        runtime_scheduler.get("execution_decision", {}),
        route_payload=runtime_route_payload,
        selected_strategy_id=runtime_selected_strategy_id,
        selected_instance_id=runtime_selected_instance_id,
    )
    runtime_execution_raw = (
        runtime_scheduler.get("execution_decision", {})
        if isinstance(runtime_scheduler.get("execution_decision", {}), dict)
        else {}
    )
    runtime_policy_sources = (
        runtime_scheduler.get("policy_sources", {})
        if isinstance(runtime_scheduler.get("policy_sources", {}), dict)
        else {}
    )
    fallback_execution_decision = _normalize_execution_decision(
        scheduler_fallback.get("execution_decision", {}),
        route_payload=fallback_route_payload,
        selected_strategy_id=fallback_selected_strategy_id,
        selected_instance_id=fallback_selected_instance_id,
    )
    fallback_execution_raw = (
        scheduler_fallback.get("execution_decision", {})
        if isinstance(scheduler_fallback.get("execution_decision", {}), dict)
        else {}
    )
    fallback_policy_sources = (
        scheduler_fallback.get("policy_sources", {})
        if isinstance(scheduler_fallback.get("policy_sources", {}), dict)
        else {}
    )
    effective_execution_decision = (
        runtime_execution_decision
        if runtime_execution_decision.get("selectedStrategyId")
        or runtime_execution_decision.get("selectedInstanceId")
        or runtime_execution_decision.get("reason")
        else fallback_execution_decision
    )

    runtime_bundle_ready = bool(
        runtime_route_payload
        or runtime_selection_raw
        or runtime_canonical_market.get("snapshotId")
        or runtime_execution_raw
        or (
            runtime_snapshot_source
            and (
                runtime_active_strategy_ids
                or runtime_active_instance_ids
                or runtime_selected_strategy_id
                or runtime_selected_instance_id
            )
        )
    )
    fallback_bundle_ready = bool(
        fallback_active_strategy_ids
        or fallback_active_instance_ids
        or fallback_selected_strategy_id
        or fallback_selected_instance_id
        or fallback_route_payload
        or fallback_selection_raw
        or fallback_canonical_market.get("snapshotId")
        or fallback_execution_raw
    )
    effective_source_bundle = (
        "runtime" if runtime_bundle_ready else "fallback" if fallback_bundle_ready else "configured"
    )
    if effective_source_bundle == "runtime":
        effective_active_strategy_ids = runtime_active_strategy_ids or ([configured_strategy_id] if configured_strategy_id else [])
        effective_active_instance_ids = runtime_active_instance_ids
        effective_selected_strategy_id = runtime_selected_strategy_id
        effective_selected_instance_id = runtime_selected_instance_id
        effective_mode = _first_non_empty_str(runtime_mode_raw, "multi_active")
        effective_arbiter = _first_non_empty_str(runtime_arbiter_raw, "allocation-weighted")
        effective_reason = _first_non_empty_str(runtime_reason_raw, effective_arbiter)
        effective_route_payload = runtime_route_payload
        effective_direction_guard = runtime_direction_guard
        effective_candidate_strategy_ids = runtime_candidate_strategy_ids
        effective_router_weights = runtime_router_weights
        effective_strategy_weights = runtime_strategy_weights
        effective_instance_weights = runtime_instance_weights
        effective_route_family = runtime_route_family
        effective_allowed_side = runtime_allowed_side
        effective_selection = runtime_selection
        effective_canonical_market = runtime_canonical_market
        effective_execution_decision = runtime_execution_decision
    elif effective_source_bundle == "fallback":
        effective_active_strategy_ids = fallback_active_strategy_ids or ([configured_strategy_id] if configured_strategy_id else [])
        effective_active_instance_ids = fallback_active_instance_ids
        effective_selected_strategy_id = fallback_selected_strategy_id
        effective_selected_instance_id = fallback_selected_instance_id
        effective_mode = _first_non_empty_str(fallback_mode_raw, "multi_active")
        effective_arbiter = _first_non_empty_str(fallback_arbiter_raw, "allocation-weighted")
        effective_reason = _first_non_empty_str(fallback_reason_raw, effective_arbiter)
        effective_route_payload = fallback_route_payload
        effective_direction_guard = fallback_direction_guard
        effective_candidate_strategy_ids = fallback_candidate_strategy_ids
        effective_router_weights = fallback_router_weights
        effective_strategy_weights = fallback_strategy_weights
        effective_instance_weights = fallback_instance_weights
        effective_route_family = fallback_route_family
        effective_allowed_side = fallback_allowed_side
        effective_selection = fallback_selection
        effective_canonical_market = fallback_canonical_market
        effective_execution_decision = fallback_execution_decision
    else:
        effective_active_strategy_ids = [configured_strategy_id] if configured_strategy_id else []
        effective_active_instance_ids = []
        effective_selected_strategy_id = ""
        effective_selected_instance_id = ""
        effective_mode = "multi_active"
        effective_arbiter = "allocation-weighted"
        effective_reason = "allocation-weighted"
        effective_route_payload = {}
        effective_direction_guard = {}
        effective_candidate_strategy_ids = []
        effective_router_weights = {}
        effective_strategy_weights = {}
        effective_instance_weights = {}
        effective_route_family = ""
        effective_allowed_side = ""
        effective_selection = _normalize_route_selection({}, selected_strategy_id="", selected_instance_id="")
        effective_canonical_market = _normalize_canonical_market_snapshot({})
        effective_execution_decision = _normalize_execution_decision({})

    field_sources = {
        "activeStrategyIds": effective_source_bundle if effective_source_bundle != "configured" else "configured",
        "activeInstanceIds": effective_source_bundle if effective_source_bundle != "configured" else "none",
        "selectedStrategyId": effective_source_bundle if effective_source_bundle != "configured" else "configured",
        "selectedInstanceId": effective_source_bundle if effective_source_bundle != "configured" else "none",
        "mode": effective_source_bundle if effective_source_bundle != "configured" else "default",
        "arbiter": effective_source_bundle if effective_source_bundle != "configured" else "default",
        "reason": effective_source_bundle if effective_source_bundle != "configured" else "default",
        "route": effective_source_bundle if effective_source_bundle != "configured" else "none",
        "selection": effective_source_bundle if effective_source_bundle != "configured" else "none",
        "canonicalMarket": effective_source_bundle if effective_source_bundle != "configured" else "none",
        "executionDecision": effective_source_bundle if effective_source_bundle != "configured" else "none",
        "policySources": effective_source_bundle if effective_source_bundle != "configured" else "none",
    }
    source_layer = (
        effective_source_bundle
        if effective_source_bundle != "configured"
        else field_sources["activeStrategyIds"]
    )
    runtime_loaded = bool(
        loaded_set
        and any(strategy_id in loaded_set for strategy_id in effective_active_strategy_ids)
    )

    configured_projection = {
        "strategyId": configured_strategy_id,
        "strategyName": configured_name,
        "configRevision": str(config_revision or ""),
        "effectiveUpdatedAt": _utc_iso_from_ts(effective_updated_at),
        "source": configured_source,
        "runtimeLoaded": runtime_loaded,
    }
    runtime_projection = {
        "mode": runtime_mode_raw,
        "arbiter": runtime_arbiter_raw,
        "reason": runtime_reason_raw,
        "activeStrategyIds": runtime_active_strategy_ids,
        "activeInstanceIds": runtime_active_instance_ids,
        "selectedStrategyId": runtime_selected_strategy_id,
        "selectedStrategyName": _strategy_name_from_catalog(catalog, runtime_selected_strategy_id, ""),
        "selectedInstanceId": runtime_selected_instance_id,
        "actualCalledStrategyId": runtime_selected_strategy_id,
        "actualCalledStrategyName": _strategy_name_from_catalog(catalog, runtime_selected_strategy_id, ""),
        "actualCalledInstanceId": runtime_selected_instance_id,
        "routeWinnerStrategyId": runtime_route_winner_strategy_id,
        "routeWinnerStrategyName": _strategy_name_from_catalog(catalog, runtime_route_winner_strategy_id, ""),
        "routeWinnerInstanceId": runtime_route_winner_instance_id,
        "route": runtime_route_payload,
        "routeFamily": runtime_route_family,
        "allowedSide": runtime_allowed_side,
        "candidateStrategyIds": runtime_candidate_strategy_ids,
        "routerWeights": runtime_router_weights,
        "directionGuard": runtime_direction_guard,
        "strategyWeights": runtime_strategy_weights,
        "instanceWeights": runtime_instance_weights,
        "selection": runtime_selection,
        "canonicalMarket": runtime_canonical_market,
        "executionDecision": runtime_execution_decision,
        "policySources": runtime_policy_sources,
        "source": runtime_source,
        "snapshotId": str(runtime_scheduler.get("snapshot_id", "") or ""),
        "generatedAt": str(runtime_scheduler.get("generated_at", "") or ""),
        "sourceVersions": (
            runtime_scheduler.get("source_versions", {})
            if isinstance(runtime_scheduler.get("source_versions", {}), dict)
            else {}
        ),
    }
    fallback_projection = {
        "mode": fallback_mode_raw,
        "arbiter": fallback_arbiter_raw,
        "reason": fallback_reason_raw,
        "activeStrategyIds": fallback_active_strategy_ids,
        "activeInstanceIds": fallback_active_instance_ids,
        "selectedStrategyId": fallback_selected_strategy_id,
        "selectedStrategyName": _strategy_name_from_catalog(catalog, fallback_selected_strategy_id, ""),
        "selectedInstanceId": fallback_selected_instance_id,
        "actualCalledStrategyId": fallback_selected_strategy_id,
        "actualCalledStrategyName": _strategy_name_from_catalog(catalog, fallback_selected_strategy_id, ""),
        "actualCalledInstanceId": fallback_selected_instance_id,
        "routeWinnerStrategyId": fallback_route_winner_strategy_id,
        "routeWinnerStrategyName": _strategy_name_from_catalog(catalog, fallback_route_winner_strategy_id, ""),
        "routeWinnerInstanceId": fallback_route_winner_instance_id,
        "route": fallback_route_payload,
        "routeFamily": fallback_route_family,
        "allowedSide": fallback_allowed_side,
        "candidateStrategyIds": fallback_candidate_strategy_ids,
        "routerWeights": fallback_router_weights,
        "directionGuard": fallback_direction_guard,
        "strategyWeights": fallback_strategy_weights,
        "instanceWeights": fallback_instance_weights,
        "selection": fallback_selection,
        "canonicalMarket": fallback_canonical_market,
        "executionDecision": fallback_execution_decision,
        "policySources": fallback_policy_sources,
        "updatedAt": str(scheduler_fallback.get("updated_at", "") or ""),
        "source": fallback_source,
    }
    effective_projection = {
        "mode": effective_mode,
        "arbiter": effective_arbiter,
        "reason": effective_reason,
        "activeStrategyIds": effective_active_strategy_ids,
        "activeStrategyNames": [
            _strategy_name_from_catalog(catalog, strategy_id, strategy_id)
            for strategy_id in effective_active_strategy_ids
        ],
        "activeInstanceIds": effective_active_instance_ids,
        "selectedStrategyId": effective_selected_strategy_id,
        "selectedStrategyName": _strategy_name_from_catalog(catalog, effective_selected_strategy_id),
        "selectedInstanceId": effective_selected_instance_id,
        "actualCalledStrategyId": effective_selected_strategy_id,
        "actualCalledStrategyName": _strategy_name_from_catalog(catalog, effective_selected_strategy_id),
        "actualCalledInstanceId": effective_selected_instance_id,
        "routeWinnerStrategyId": (
            runtime_route_winner_strategy_id
            if effective_source_bundle == "runtime"
            else fallback_route_winner_strategy_id
            if effective_source_bundle == "fallback"
            else ""
        ),
        "routeWinnerStrategyName": _strategy_name_from_catalog(
            catalog,
            runtime_route_winner_strategy_id
            if effective_source_bundle == "runtime"
            else fallback_route_winner_strategy_id
            if effective_source_bundle == "fallback"
            else "",
        ),
        "routeWinnerInstanceId": (
            runtime_route_winner_instance_id
            if effective_source_bundle == "runtime"
            else fallback_route_winner_instance_id
            if effective_source_bundle == "fallback"
            else ""
        ),
        "route": effective_route_payload,
        "routeFamily": effective_route_family,
        "allowedSide": effective_allowed_side,
        "candidateStrategyIds": effective_candidate_strategy_ids,
        "routerWeights": effective_router_weights,
        "directionGuard": effective_direction_guard,
        "strategyWeights": effective_strategy_weights,
        "instanceWeights": effective_instance_weights,
        "selection": effective_selection,
        "canonicalMarket": effective_canonical_market,
        "executionDecision": effective_execution_decision,
        "policySources": (
            runtime_policy_sources
            if effective_source_bundle == "runtime"
            else fallback_policy_sources
            if effective_source_bundle == "fallback"
            else {}
        ),
        "runtimeLoaded": runtime_loaded,
        "sourceLayer": source_layer,
        "fieldSources": field_sources,
        "snapshotId": str(runtime_scheduler.get("snapshot_id", "") or ""),
        "generatedAt": str(runtime_scheduler.get("generated_at", "") or ""),
        "sourceVersions": (
            runtime_scheduler.get("source_versions", {})
            if isinstance(runtime_scheduler.get("source_versions", {}), dict)
            else {}
        ),
    }
    current_projection = {
        "strategyId": configured_strategy_id,
        "strategyName": configured_name,
        "activeStrategyIds": effective_active_strategy_ids,
        "activeInstanceIds": effective_active_instance_ids,
        "mode": effective_mode,
        "arbiter": effective_arbiter,
        "selectedStrategyId": effective_selected_strategy_id,
        "selectedInstanceId": effective_selected_instance_id,
        "actualCalledStrategyId": effective_selected_strategy_id,
        "actualCalledStrategyName": effective_projection["selectedStrategyName"],
        "actualCalledInstanceId": effective_selected_instance_id,
        "routeWinnerStrategyId": effective_projection.get("routeWinnerStrategyId", ""),
        "routeWinnerStrategyName": effective_projection.get("routeWinnerStrategyName", ""),
        "routeWinnerInstanceId": effective_projection.get("routeWinnerInstanceId", ""),
        "effectiveStrategyId": effective_selected_strategy_id,
        "effectiveStrategyName": effective_projection["selectedStrategyName"],
        "configRevision": str(config_revision or ""),
        "runtimeLoaded": runtime_loaded,
        "effectiveUpdatedAt": _utc_iso_from_ts(effective_updated_at),
        "sourceLayer": source_layer,
        "configuredSource": configured_source,
        "runtimeSource": runtime_source,
        "fallbackSource": fallback_source,
        "routeFamily": effective_route_family,
        "allowedSide": effective_allowed_side,
        "reason": effective_reason,
        "selection": (
            effective_projection.get("selection", {})
            if isinstance(effective_projection.get("selection", {}), dict)
            else {}
        ),
        "marketContext": (
            effective_route_payload.get("market_context", {})
            if isinstance(effective_route_payload.get("market_context", {}), dict)
            else {}
        ),
        "regimeAudit": (
            effective_projection.get("regimeAudit", {})
            if isinstance(effective_projection.get("regimeAudit", {}), dict)
            else {}
        ),
        "executionDecision": (
            effective_projection.get("executionDecision", {})
            if isinstance(effective_projection.get("executionDecision", {}), dict)
            else {}
        ),
    }
    return {
        "configured": configured_projection,
        "runtime": runtime_projection,
        "fallback": fallback_projection,
        "effective": effective_projection,
        "current": current_projection,
    }


def _compact_blackboard_for_status(blackboard: Any) -> dict:
    if not isinstance(blackboard, dict):
        blackboard = {}
    macro = blackboard.get("Macro_Commander", {}) if isinstance(blackboard.get("Macro_Commander", {}), dict) else {}
    micro = blackboard.get("Micro_Sniper", {}) if isinstance(blackboard.get("Micro_Sniper", {}), dict) else {}
    risk = blackboard.get("Risk_Commissar", {}) if isinstance(blackboard.get("Risk_Commissar", {}), dict) else {}
    scheduler = blackboard.get("Scheduler_Runtime", {}) if isinstance(blackboard.get("Scheduler_Runtime", {}), dict) else {}
    arbitration = blackboard.get("Execution_Arbitration", {}) if isinstance(blackboard.get("Execution_Arbitration", {}), dict) else {}
    strategy_instances = blackboard.get("Strategy_Instances", {}) if isinstance(blackboard.get("Strategy_Instances", {}), dict) else {}
    risk_protocol = blackboard.get("Risk_Protocol", {}) if isinstance(blackboard.get("Risk_Protocol", {}), dict) else {}
    runtime_scheduler_snapshot = _load_runtime_scheduler_snapshot()
    runtime_scheduler_runtime = runtime_scheduler_snapshot if runtime_scheduler_snapshot else scheduler
    runtime_status = _runtime_strategy_status_payload()
    runtime_active = runtime_status.get("active", {}) if isinstance(runtime_status.get("active", {}), dict) else {}
    effective_payload = _load_effective_params_payload()
    scheduler_fallback = (
        effective_payload.get("runtime_scheduler", {})
        if isinstance(effective_payload.get("runtime_scheduler", {}), dict)
        else {}
    )
    market_state = _load_strategy_market_state()
    market_instances = (
        market_state.get("runtime_instances", [])
        if isinstance(market_state.get("runtime_instances", []), list)
        else []
    )

    catalog = _strategy_catalog_from_state(market_state)
    strategy_projection = _normalize_strategy_projection(
        catalog=catalog,
        effective_payload=effective_payload,
        runtime_active=runtime_active,
        runtime_scheduler=runtime_scheduler_runtime,
        scheduler_fallback=scheduler_fallback,
    )
    session_scheduler_projection = _build_session_scheduler_projection(
        decision_policy=_normalize_decision_policy(
            market_state.get("decision_policy", {})
            if isinstance(market_state.get("decision_policy", {}), dict)
            else {}
        ),
        runtime_scheduler=runtime_scheduler_runtime,
        scheduler_fallback=scheduler_fallback,
    )
    effective_projection = (
        strategy_projection.get("effective", {})
        if isinstance(strategy_projection.get("effective", {}), dict)
        else {}
    )
    active_strategy_ids = _clean_str_list(effective_projection.get("activeStrategyIds"))
    active_instance_ids = _clean_str_list(effective_projection.get("activeInstanceIds"))
    selected_strategy_id = _first_non_empty_str(effective_projection.get("selectedStrategyId"))
    selected_instance_id = _first_non_empty_str(effective_projection.get("selectedInstanceId"))
    route_payload = (
        effective_projection.get("route", {})
        if isinstance(effective_projection.get("route", {}), dict)
        else {}
    )
    direction_guard = (
        effective_projection.get("directionGuard", {})
        if isinstance(effective_projection.get("directionGuard", {}), dict)
        else {}
    )
    snapshot_route = (
        runtime_scheduler_snapshot.get("route", {})
        if isinstance(runtime_scheduler_snapshot.get("route", {}), dict)
        else {}
    )
    snapshot_risk_decision = (
        runtime_scheduler_snapshot.get("risk_decision", {})
        if isinstance(runtime_scheduler_snapshot.get("risk_decision", {}), dict)
        else {}
    )
    snapshot_execution_owner = (
        runtime_scheduler_snapshot.get("execution_owner", {})
        if isinstance(runtime_scheduler_snapshot.get("execution_owner", {}), dict)
        and runtime_scheduler_snapshot.get("execution_owner", {})
        else snapshot_risk_decision.get("execution_owner", {})
        if isinstance(snapshot_risk_decision.get("execution_owner", {}), dict)
        else {}
    )

    strategy_instance_rows = (
        strategy_instances.get("instances", {})
        if isinstance(strategy_instances.get("instances", {}), dict)
        else {}
    )
    if not strategy_instance_rows and active_instance_ids:
        built_rows: dict[str, Any] = {}
        market_instance_map = {
            str(item.get("instance_id", "")).strip(): item
            for item in market_instances
            if isinstance(item, dict) and str(item.get("instance_id", "")).strip()
        }
        instance_weights = (
            effective_projection.get("instanceWeights", {})
            if isinstance(effective_projection.get("instanceWeights", {}), dict)
            else {}
        )
        for instance_id in active_instance_ids:
            row = market_instance_map.get(instance_id, {})
            built_rows[instance_id] = {
                "strategy_id": str(row.get("strategy_id", "") or ""),
                "role": str(row.get("role", "") or ""),
                "allocation": float(row.get("allocation", 0.0) or 0.0),
                "weight": float(instance_weights.get(instance_id, 0.0) or 0.0),
                "status": str(row.get("status", "") or ""),
                "selected": instance_id == selected_instance_id,
            }
        strategy_instance_rows = built_rows

    governor_blockers = _normalize_blocker_reasons(risk_protocol.get("governor_blockers", []))
    if not governor_blockers:
        governor_blockers = _normalize_blocker_reasons(snapshot_risk_decision.get("governor_blockers", []))
    if not governor_blockers:
        governor_blockers = _normalize_blocker_reasons(snapshot_risk_decision.get("blocked_reasons", []))
    if not governor_blockers:
        governor_blockers = _normalize_blocker_reasons(arbitration.get("blocked_reasons", []))
    risk_protocol_global_state = str(
        risk_protocol.get("global_state")
        or snapshot_risk_decision.get("global_state")
        or ""
    ).lower()
    if risk_protocol_global_state not in {"blocked", "normal"}:
        risk_protocol_global_state = "normal"
    if risk_protocol_global_state == "blocked" and not governor_blockers:
        risk_protocol_global_state = "normal"

    return {
        "Macro_Commander": _build_macro_commander_preview(macro, route_payload),
        "Micro_Sniper": _build_micro_sniper_preview(micro, route_payload),
        "Risk_Commissar": _build_risk_commissar_preview(risk),
        "Scheduler_Runtime": {
            "mode": effective_projection.get("mode"),
            "arbiter": effective_projection.get("arbiter"),
            "reason": effective_projection.get("reason"),
            "active_strategy_ids": active_strategy_ids,
            "active_instance_ids": active_instance_ids,
            "selected_strategy_id": selected_strategy_id,
            "selected_instance_id": selected_instance_id,
            "loaded_strategy_ids": (
                scheduler.get("loaded_strategy_ids", [])
                if isinstance(scheduler.get("loaded_strategy_ids", []), list)
                and scheduler.get("loaded_strategy_ids", [])
                else active_strategy_ids
            ),
            "strategy_health": (
                scheduler.get("strategy_health", {})
                if isinstance(scheduler.get("strategy_health", {}), dict)
                else {}
            ),
            "route": route_payload,
            "session_scheduler": (
                session_scheduler_projection.get("effective", {})
                if isinstance(session_scheduler_projection.get("effective", {}), dict)
                else {}
            ),
            "route_family": effective_projection.get("routeFamily"),
            "allowed_side": effective_projection.get("allowedSide"),
            "selection": (
                effective_projection.get("selection", {})
                if isinstance(effective_projection.get("selection", {}), dict)
                else {}
            ),
            "canonical_market": (
                effective_projection.get("canonicalMarket", {})
                if isinstance(effective_projection.get("canonicalMarket", {}), dict)
                else {}
            ),
            "execution_decision": (
                effective_projection.get("executionDecision", {})
                if isinstance(effective_projection.get("executionDecision", {}), dict)
                else {}
            ),
            "candidate_strategy_ids": _clean_str_list(
                effective_projection.get("candidateStrategyIds")
                or route_payload.get("candidate_strategy_ids")
            ),
            "router_weights": (
                effective_projection.get("routerWeights", {})
                if isinstance(effective_projection.get("routerWeights", {}), dict)
                and effective_projection.get("routerWeights", {})
                else route_payload.get("weights", {})
                if isinstance(route_payload.get("weights", {}), dict)
                and route_payload.get("weights", {})
                else route_payload.get("router_weights", {})
                if isinstance(route_payload.get("router_weights", {}), dict)
                else {}
            ),
            "direction_guard": direction_guard,
            "strategy_weights": (
                effective_projection.get("strategyWeights", {})
                if isinstance(effective_projection.get("strategyWeights", {}), dict)
                else {}
            ),
            "instance_weights": (
                effective_projection.get("instanceWeights", {})
                if isinstance(effective_projection.get("instanceWeights", {}), dict)
                else {}
            ),
            "configured_strategy_id": strategy_projection.get("configured", {}).get("strategyId"),
            "configured_strategy_name": strategy_projection.get("configured", {}).get("strategyName"),
            "selected_strategy_name": effective_projection.get("selectedStrategyName"),
            "source_layer": effective_projection.get("sourceLayer"),
            "field_sources": (
                effective_projection.get("fieldSources", {})
                if isinstance(effective_projection.get("fieldSources", {}), dict)
                else {}
            ),
            "snapshot_id": str(runtime_scheduler_snapshot.get("snapshot_id", "") or ""),
            "generated_at": str(runtime_scheduler_snapshot.get("generated_at", "") or ""),
            "source_versions": (
                runtime_scheduler_snapshot.get("source_versions", {})
                if isinstance(runtime_scheduler_snapshot.get("source_versions", {}), dict)
                else {}
            ),
        },
        "Execution_Arbitration": {
            "final_action": arbitration.get("final_action"),
            "final_side": arbitration.get("final_side"),
            "net_score": arbitration.get("net_score"),
            "confidence": arbitration.get("confidence"),
            "hold_mode": arbitration.get("hold_mode"),
            "blocked_reasons": (
                arbitration.get("blocked_reasons", [])
                if isinstance(arbitration.get("blocked_reasons", []), list) and arbitration.get("blocked_reasons", [])
                else runtime_scheduler_snapshot.get("blocked_reasons", [])
                if isinstance(runtime_scheduler_snapshot.get("blocked_reasons", []), list)
                else []
            ),
            "last_execution_status": (
                arbitration.get("last_execution_status")
                or runtime_scheduler_snapshot.get("last_execution_status")
            ),
            "selected_instance_id": str(runtime_scheduler_snapshot.get("selected_instance_id", "") or ""),
            "selected_strategy_id": str(runtime_scheduler_snapshot.get("selected_strategy_id", "") or ""),
            "entry_owner_instance_id": str(
                snapshot_execution_owner.get("entry_owner_instance_id")
                or runtime_scheduler_snapshot.get("entry_owner_instance_id", "")
                or snapshot_route.get("entry_owner_instance_id", "")
                or ""
            ),
            "entry_owner_strategy_id": str(
                snapshot_execution_owner.get("entry_owner_strategy_id")
                or runtime_scheduler_snapshot.get("entry_owner_strategy_id", "")
                or snapshot_route.get("entry_owner_strategy_id", "")
                or ""
            ),
            "selection": (
                effective_projection.get("selection", {})
                if isinstance(effective_projection.get("selection", {}), dict)
                else {}
            ),
            "canonical_market": (
                effective_projection.get("canonicalMarket", {})
                if isinstance(effective_projection.get("canonicalMarket", {}), dict)
                else {}
            ),
            "execution_decision": (
                effective_projection.get("executionDecision", {})
                if isinstance(effective_projection.get("executionDecision", {}), dict)
                else {}
            ),
            "source_layer": "runtime_snapshot" if runtime_scheduler_snapshot else "blackboard",
        },
        "Strategy_Instances": {
            "selected_strategy_id": strategy_instances.get("selected_strategy_id") or selected_strategy_id,
            "selected_instance_id": strategy_instances.get("selected_instance_id") or selected_instance_id,
            "contributors_count": strategy_instances.get("contributors_count") or len(strategy_instance_rows),
            "instances": strategy_instance_rows,
        },
        "Risk_Protocol": {
            "global_state": risk_protocol_global_state,
            "governor_blockers": governor_blockers,
            "decision": str(risk_protocol.get("decision") or snapshot_risk_decision.get("decision") or ""),
            "risk_code": str(risk_protocol.get("risk_code") or snapshot_risk_decision.get("risk_code") or ""),
            "risk_layer": str(risk_protocol.get("risk_layer") or snapshot_risk_decision.get("risk_layer") or ""),
            "risk_source": str(risk_protocol.get("risk_source") or snapshot_risk_decision.get("risk_source") or ""),
            "blocking_rule_id": str(
                risk_protocol.get("blocking_rule_id") or snapshot_risk_decision.get("blocking_rule_id") or ""
            ),
            "active_strategy_ids": (
                risk_protocol.get("active_strategy_ids", [])
                if isinstance(risk_protocol.get("active_strategy_ids", []), list) and risk_protocol.get("active_strategy_ids", [])
                else active_strategy_ids
            ),
            "active_instance_ids": (
                risk_protocol.get("active_instance_ids", [])
                if isinstance(risk_protocol.get("active_instance_ids", []), list) and risk_protocol.get("active_instance_ids", [])
                else active_instance_ids
            ),
            "instance_risk_state": risk_protocol.get("instance_risk_state", {}),
            "instance_health_score": risk_protocol.get("instance_health_score", {}),
            "execution_owner": snapshot_execution_owner,
            "source_layer": "runtime_snapshot" if runtime_scheduler_snapshot else "blackboard",
        },
        "Strategy_Projection": {
            "configured": (
                strategy_projection.get("configured", {})
                if isinstance(strategy_projection.get("configured", {}), dict)
                else {}
            ),
            "runtime": (
                strategy_projection.get("runtime", {})
                if isinstance(strategy_projection.get("runtime", {}), dict)
                else {}
            ),
            "fallback": (
                strategy_projection.get("fallback", {})
                if isinstance(strategy_projection.get("fallback", {}), dict)
                else {}
            ),
            "effective": effective_projection,
        },
    }


def _refresh_technical_trend_cache(force_refresh: bool = False) -> None:
    try:
        direction, data = get_technical_trend_bias(force_refresh=force_refresh)
        _TECHNICAL_TREND_CACHE["ts"] = time.time()
        _TECHNICAL_TREND_CACHE["direction"] = direction or "NEUTRAL"
        _TECHNICAL_TREND_CACHE["data"] = data if isinstance(data, dict) else {}
    finally:
        _TECHNICAL_TREND_CACHE["refreshing"] = False


def _get_technical_trend_snapshot() -> tuple[str, dict]:
    now = time.time()
    cache_age = now - float(_TECHNICAL_TREND_CACHE.get("ts", 0.0) or 0.0)
    if cache_age < 30.0:
        return (
            str(_TECHNICAL_TREND_CACHE.get("direction", "NEUTRAL") or "NEUTRAL"),
            dict(_TECHNICAL_TREND_CACHE.get("data", {}) or {}),
        )
    if not bool(_TECHNICAL_TREND_CACHE.get("refreshing", False)):
        _TECHNICAL_TREND_CACHE["refreshing"] = True
        threading.Thread(
            target=_refresh_technical_trend_cache,
            kwargs={"force_refresh": False},
            daemon=True,
            name="gui-technical-trend-refresh",
        ).start()
    return (
        str(_TECHNICAL_TREND_CACHE.get("direction", "NEUTRAL") or "NEUTRAL"),
        dict(_TECHNICAL_TREND_CACHE.get("data", {}) or {}),
    )

@app.post("/api/action/panic")
def panic_system():
    """紧急阻断: 立即阻断开仓并请求当前持仓平仓，但不主动杀 daemon。"""
    return execute_action("panic")

@app.post("/api/start")
def start_system():
    if PID_FILE.exists():
        try:
            pid = int(PID_FILE.read_text(encoding="utf-8").strip())
            if is_process_running(pid):
                _set_manual_stop(False)
                _invalidate_runtime_status_caches(schedule_refresh=True)
                return {"success": False, "message": "System already running"}
            PID_FILE.unlink(missing_ok=True)
        except Exception:
            pass

    _set_manual_stop(False)
    ok, message = _spawn_daemon()
    _invalidate_runtime_status_caches(schedule_refresh=True)
    return {"success": ok, "message": message}

@app.post("/api/stop")
def stop_gateway():
    """停止交易网关"""
    _set_manual_stop(True)
    _record_engine_lifecycle("stop", detail="manual")
    if PID_FILE.exists():
        try:
            pid = int(PID_FILE.read_text(encoding="utf-8").strip())
            os.kill(pid, signal.SIGTERM)
            _invalidate_runtime_status_caches(schedule_refresh=True)
            return {"success": True, "message": f"Killed PID {pid}"}
        except Exception as e:
            _invalidate_runtime_status_caches(schedule_refresh=True)
            return {"success": False, "error": str(e)}
    _invalidate_runtime_status_caches(schedule_refresh=True)
    return {"success": True, "message": "Not running"}


@app.post("/api/macro/refresh")
def force_refresh_macro_direction():
    """
    强制刷新宏观方向：立即唤醒 M2 决策主脑输出最新宏观方向。
    """
    try:
        ok, msg, pid = _spawn_macro_refresh(force=True)
        status = _read_macro_refresh_status()
        return {
            "success": ok,
            "message": msg,
            "pid": pid,
            "refresh_status": status,
        }
    except Exception as e:
        return {"success": False, "message": f"宏观方向刷新失败: {e}"}


@app.post("/api/diagnostics/export")
def export_diagnostic_bundle(payload: Optional[DiagnosticBundleRequest] = None):
    note = payload.note if payload else ""
    label = payload.label if payload else "manual"
    try:
        bundle = create_diagnostic_bundle(
            app_dir=APP_DIR,
            data_dir=DATA_DIR,
            logs_dir=LOGS_DIR,
            config_dir=CONFIG_DIR,
            note=note,
            label=label,
        )
        return {
            "success": True,
            "message": "诊断包已生成",
            "bundle": bundle,
            "downloadUrl": f"/api/diagnostics/download/{bundle['archive_name']}",
        }
    except Exception as exc:
        return {
            "success": False,
            "message": f"诊断包生成失败: {exc}",
        }





@app.get("/api/diagnostics/download/{archive_name}")
def download_diagnostic_bundle(archive_name: str):
    diagnostics_dir = (DATA_DIR / "diagnostics").resolve()
    target = (diagnostics_dir / archive_name).resolve()
    if diagnostics_dir not in target.parents or target.suffix.lower() != ".zip":
        raise HTTPException(status_code=400, detail="Invalid archive path")
    if not target.exists() or not target.is_file():
        raise HTTPException(status_code=404, detail="Diagnostic archive not found")
    return FileResponse(
        str(target),
        media_type="application/zip",
        filename=target.name,
    )





@app.post("/api/action/{action_type}")
def execute_action(action_type: str):
    """处理底部的一键操作按钮逻辑"""
    import time
    if action_type == "install":
        # 模拟安装过程
        time.sleep(2)
        return {"success": True, "message": "依赖安装完成,环境已就绪"}
    elif action_type == "repair":
        # 模拟修复连接过程,比如重启 MT5
        time.sleep(1)
        from bridge.trader_api import restart_mt5
        # restart_mt5() # 实际环境中可取消注释
        return {"success": True, "message": "MT5 及网络连接已尝试修复"}
    elif action_type == "export":
        bundle = create_diagnostic_bundle(
            app_dir=APP_DIR,
            data_dir=DATA_DIR,
            logs_dir=LOGS_DIR,
            config_dir=CONFIG_DIR,
            label="action_export",
        )
        return {
            "success": True,
            "message": f"诊断包已导出: {bundle['archive_path']}",
            "bundle": bundle,
            "downloadUrl": f"/api/diagnostics/download/{bundle['archive_name']}",
        }
    elif action_type == "import":
        # 模拟导入配置
        return {"success": True, "message": "客户配置已成功导入并覆盖当前设置"}
    elif action_type == "approve_evolution":
        return approve_evolution_action()
    elif action_type == "reject_evolution":
        return reject_evolution_action()
    elif action_type == "refresh_macro":
        return force_refresh_macro_direction()
    elif action_type == "panic":
        try:
            ok, msg, state = _spawn_panic_action()
            return {
                "success": bool(ok),
                "message": msg,
                "result": state.get("result", {}),
                "panic_status": state,
            }
        except Exception as e:
            return {"success": False, "message": f"紧急平仓失败: {e}"}
    else:
        return {"success": False, "message": "未知的操作类型"}

@app.get("/api/setup_status")
def get_setup_status():
    """获取开箱即用各环节的状态"""
    # 0. 检查 AI Provider API Key 是否已配置
    api_key_ok = False
    try:
        llm_config_dir = config_manager.get_llm_config_dir()
        openclaw = llm_config_dir / "openclaw.json"
        if openclaw.exists():
            cfg = json.loads(openclaw.read_text(encoding="utf-8"))
            for info in cfg.get("models", {}).get("providers", {}).values():
                if info.get("apiKey", ""):
                    api_key_ok = True
                    break
    except Exception:
        pass
    if not api_key_ok:
        config = config_manager.load_app_config()
        api_key_ok = bool(config.get("DEEPSEEK_API_KEY", ""))

    # 1. 检查 Python / OpenClaw 环境
    env_ok = True

    # 2. 检查 MT5 是否已配置
    try:
        mt5_files_path = config_manager.get_mt5_dir()
    except Exception:
        mt5_files_path = None
    mt5_ok = bool(mt5_files_path and mt5_files_path.exists())

    # 3. 检查依赖是否已安装
    deps_ok = True

    # 4. 检查是否有历史运行记录或状态文件
    agent_ok = (DATA_DIR / "bridge" / "state" / "agent_state.json").exists()

    # Detect app version for upgrade-aware wizard reset
    app_version = "1.0.0"
    try:
        version_path = config_manager.get_app_dir() / "VERSION"
        if version_path.exists():
            app_version = version_path.read_text(encoding="utf-8").strip()
    except Exception:
        pass

    return {
        "appVersion": app_version,
        "steps": [
            { "id": "apikey", "title": "AI 模型 API Key", "desc": "配置大模型供应商 API Key 以启用 AI 决策", "status": "success" if api_key_ok else "warning" },
            { "id": "env", "title": "环境与依赖检测", "desc": "检查 Python 及基础运行库是否就绪", "status": "success" if env_ok else "pending" },
            { "id": "mt5", "title": "MT5 终端连接", "desc": "检测是否已找到 MT5 数据目录并允许 DLL 桥接", "status": "success" if mt5_ok else "warning" },
            { "id": "deps", "title": "量化组件加载", "desc": "安装 yfinance, pandas 等离线数据计算组件", "status": "success" if deps_ok else "pending" },
            { "id": "agent", "title": "策略引擎初始化", "desc": "启动后台守护进程与风控大脑", "status": "success" if agent_ok else "pending" }
        ]
    }

@app.get("/api/alerts")
def get_alerts():
    """获取关键风控告警与风险事件"""
    risk_file = LOGS_DIR / "risk_events.ndjson"
    alerts = []

    if risk_file.exists():
        try:
            lines = _read_tail_lines(risk_file, max_lines=50, max_bytes=50000)
            for line in reversed(lines[-20:]):
                if not line.strip(): continue
                try:
                    event = json.loads(line)
                    # 转换时间格式
                    ts = event.get("timestamp", 0)
                    dt = datetime.datetime.fromtimestamp(ts) if ts else datetime.datetime.now()
                    time_str = dt.strftime("%H:%M:%S")

                    event_type = event.get("event_type", "UNKNOWN")
                    if event_type == "RISK_INTERCEPT":
                        type_label = "风控拦截"
                        color_class = "warning"
                        desc = event.get("reason", "阻止了高风险操作")
                    elif event_type == "FORCE_CLOSE":
                        type_label = "强制平仓"
                        color_class = "danger"
                        desc = f"触发强平,票号: {event.get('ticket', 'N/A')}"
                    elif event_type == "M2_REVIEW":
                        type_label = "人工复核"
                        color_class = "primary"
                        desc = f"挂起信号,原因: {event.get('reason', '需要人工确认')}"
                    else:
                        type_label = "系统告警"
                        color_class = "warning"
                        desc = event.get("reason", str(event))

                    alerts.append({
                        "id": f"alert_{ts}_{len(alerts)}",
                        "type": type_label,
                        "time": time_str,
                        "desc": desc,
                        "color": color_class,
                        "ts": float(ts or time.time()),
                        "kind": "risk_event",
                    })
                except Exception:
                    pass
        except Exception as e:
            print(f"Error reading alerts: {e}")

    repair_events_file = _resolve_freshest_existing_file(_file_repair_event_candidates()) or _file_repair_events_file()
    if repair_events_file.exists():
        try:
            lines = _read_tail_lines(repair_events_file, max_lines=30, max_bytes=30000)
            for line in reversed(lines[-10:]):
                if not line.strip():
                    continue
                try:
                    event = json.loads(line)
                except Exception:
                    continue
                ts = float(event.get("timestamp", time.time()) or time.time())
                dt = datetime.datetime.fromtimestamp(ts)
                filename = str(event.get("filename", "") or pathlib.Path(str(event.get("file", "") or "")).name or "unknown.json")
                backup_path = str(event.get("backup_path", "") or "")
                backup_suffix = "，已备份损坏原文件" if backup_path else ""
                desc = (
                    f"检测到配置/状态文件损坏，系统已自动恢复 `{filename}`{backup_suffix}。"
                    f" 原因: {str(event.get('reason', 'invalid_json') or 'invalid_json')}"
                )
                alerts.append({
                    "id": f"repair_{int(ts)}_{filename}",
                    "type": "自动修复",
                    "time": dt.strftime("%H:%M:%S"),
                    "desc": desc,
                    "color": "warning",
                    "ts": ts,
                    "kind": "file_repair",
                })
        except Exception as e:
            print(f"Error reading file repair alerts: {e}")

    # 兜底:如果完全没有数据,还是显示空状态,而不是写死假数据
    alerts.sort(key=lambda item: float(item.get("ts", 0.0) or 0.0), reverse=True)
    return {"success": True, "alerts": alerts}


@app.get("/api/system_messages")
def get_system_messages(limit: int = 80):
    rows: list[dict[str, Any]] = []
    safe_limit = max(1, min(200, int(limit or 80)))

    system_messages_file = _resolve_freshest_existing_file(_system_message_candidates()) or _system_messages_file()
    if system_messages_file.exists():
        try:
            lines = _read_tail_lines(system_messages_file, max_lines=max(80, safe_limit * 2), max_bytes=120000)
            for line in reversed(lines[-safe_limit:]):
                if not line.strip():
                    continue
                try:
                    event = json.loads(line)
                except Exception:
                    continue
                ts = float(event.get("timestamp", time.time()) or time.time())
                rows.append(
                    {
                        "id": f"sys_{int(ts * 1000)}_{len(rows)}",
                        "kind": str(event.get("kind", "system") or "system"),
                        "title": str(event.get("title", "系统消息") or "系统消息"),
                        "desc": str(event.get("desc", "") or ""),
                        "color": str(event.get("color", "primary") or "primary"),
                        "source": str(event.get("source", "system") or "system"),
                        "metadata": event.get("metadata", {}) if isinstance(event.get("metadata", {}), dict) else {},
                        "ts": ts,
                        "time": datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S"),
                    }
                )
        except Exception:
            pass

    rows.sort(key=lambda item: float(item.get("ts", 0.0) or 0.0), reverse=True)
    return {
        "success": True,
        "messages": rows[:safe_limit],
        "source": str(system_messages_file) if system_messages_file.exists() else "",
        "summary": {
            "total": len(rows[:safe_limit]),
            "publishCount": sum(1 for item in rows[:safe_limit] if str(item.get("kind", "")) == "publish"),
            "rollbackCount": sum(1 for item in rows[:safe_limit] if str(item.get("kind", "")) == "rollback"),
            "templateCount": sum(1 for item in rows[:safe_limit] if str(item.get("kind", "")) in {"template", "template_hot_update"}),
            "repairCount": sum(1 for item in rows[:safe_limit] if str(item.get("kind", "")) == "file_repair"),
        },
    }

@app.get("/api/logs/{log_type}")
def get_logs(log_type: str, lines: int = 50):
    """获取指定类型的实时日志"""
    if log_type not in {"daemon", "system"}:
        return {"success": False, "error": "Unknown log type"}

    try:
        source = ""
        lines_data: list[str] = []
        if log_type == "system":
            source, lines_data = _read_system_event_lines(lines)
        else:
            source, lines_data = _read_gold_trader_bridge_progress_lines(lines)
        if not lines_data:
            if log_type == "daemon" and source:
                lines_data = ["[gold_trader] 当前暂无新的 MT5 执行/回报事件。"]
            else:
                lines_data = [f"[{log_type}] Log file not found or not created yet."]
        return {"success": True, "source": source, "lines": lines_data}
    except Exception as e:
        return {"success": False, "error": str(e)}

_SYNTHETIC_LOG_RING: list[dict] = []
_SYNTHETIC_LOG_RING_MAX = 120


@app.get("/api/mailbox")
def get_mailbox():
    """获取多Agent通信的决策大厅历史 (Blackboard)"""
    global _SYNTHETIC_LOG_RING
    logs = []
    try:
        # 只读取跨进程稳定可见的持久化来源，避免把当前 API 进程内的旧黑板内存
        # 混进来，导致蜂群大厅显示陈旧消息或接口被导入/锁竞争拖慢。
        source_groups: list[list[dict]] = []

        # 来源 A：所有可达的 blackboard_state.json（合并所有候选路径的日志）
        bb_candidates = _runtime_root_candidates("blackboard_state.json")
        for bb_file in _dedupe_paths(bb_candidates):
            if not (bb_file is not None and bb_file.exists()):
                continue
            try:
                data = json.loads(bb_file.read_text(encoding="utf-8"))
                # A1: explicit Swarm_Logs key (if any process writes it)
                bb_logs = data.get("Swarm_Logs", [])
                if bb_logs:
                    source_groups.append(bb_logs)
                # A2: per-section _logs (pipeline writes via _runtime_blackboard_log)
                for section_name, section in data.items():
                    if not isinstance(section, dict):
                        continue
                    section_logs = section.get("_logs", [])
                    if not isinstance(section_logs, list):
                        continue
                    for entry in section_logs:
                        if not isinstance(entry, dict):
                            continue
                        ts_val = entry.get("ts")
                        if not isinstance(ts_val, (int, float)):
                            continue
                        ts_str = str(ts_val)
                        source_groups.append([{
                            "role": section_name,
                            "ts": ts_val,
                            "timestamp": ts_str,
                            "time": ts_str,
                            "text": str(entry.get("text", "")),
                            "event_id": f"{section_name}|{ts_str}|{str(entry.get('text', ''))[:80]}",
                        }])
            except Exception:
                pass

        # 来源 B：logs/swarm/*.jsonl（异步刷盘，批量完整性好）
        disk_logs = _read_swarm_jsonl_logs(limit=200)
        if disk_logs:
            source_groups.append(disk_logs)

        # 来源 C：从 signal.json + blackboard 生成合成日志（pipeline 主输出，每2秒刷新）
        now_ts = time.time()
        signal_file = _resolve_runtime_artifact("data", "outbox", "signal.json")
        if signal_file is not None and signal_file.exists():
            try:
                sig = json.loads(signal_file.read_text(encoding="utf-8"))
                sig_payload = sig.get("signal", {}) if isinstance(sig, dict) else {}
                sig_ts = sig.get("timestamp", "")
                sig_ts_val = now_ts
                if sig_ts:
                    try:
                        sig_ts_val = datetime.datetime.fromisoformat(sig_ts.replace("Z", "+00:00")).timestamp()
                    except Exception:
                        pass

                action = str(sig_payload.get("action", "HOLD") or "HOLD")
                side = str(sig_payload.get("side", "NONE") or "NONE")
                confidence = float(sig_payload.get("confidence", 0) or 0)
                regime = str((sig.get("regime_audit", {}) or {}).get("regime", "UNKNOWN"))
                route = str((sig.get("route_audit", {}) or {}).get("route_family", "unknown"))
                macro = str((sig_payload.get("trace", {}) or {}).get("policy", {}).get("effective_macro_direction", "NEUTRAL"))
                reject = str(sig_payload.get("reject_reason", "") or "")
                selected_strategy = str(sig_payload.get("selected_strategy_id", "") or "")

                # Build a live status line
                status_parts = [f"行情={regime}", f"路由={route}", f"宏观={macro}"]
                if action == "HOLD" and reject:
                    status_parts.append(f"拦截={reject[:60]}")
                elif action != "HOLD":
                    status_parts.append(f"信号={action} {side} 置信度={confidence:.0%}")

                source_groups.append([{
                    "role": "Risk_Commissar",
                    "ts": sig_ts_val,
                    "timestamp": sig_ts if sig_ts else str(sig_ts_val),
                    "time": sig_ts if sig_ts else str(sig_ts_val),
                    "text": "蜂群状态 | " + " | ".join(status_parts),
                    "event_id": f"live_signal|{sig_ts}|{action}|{side}",
                }])

                # Generate quote line from terminal status
                try:
                    mt5_status_path = pathlib.Path(os.environ.get("APPDATA", ".")) / "MetaQuotes" / "Terminal" / "Common" / "Files" / "gold_autotrade" / "bridge" / "terminal_status.json"
                    if mt5_status_path.exists():
                        ts_data = json.loads(mt5_status_path.read_text(encoding="utf-8"))
                        bid = float(ts_data.get("bid", 0) or 0)
                        ask = float(ts_data.get("ask", 0) or 0)
                        if bid > 0 and ask > 0:
                            source_groups.append([{
                                "role": "Micro_Sniper",
                                "ts": now_ts,
                                "timestamp": str(now_ts),
                                "time": str(now_ts),
                                "text": f"探明最新报价 {bid:.2f}/{ask:.2f}，当前环境: {regime}",
                                "event_id": f"live_quote|{round(bid,1)}|{round(ask,1)}",
                            }])
                except Exception:
                    pass
            except Exception:
                pass

        # Accumulate synthetic logs in a ring buffer so the decision hall
        # shows a scrolling history instead of just the latest snapshot.
        for group in source_groups:
            for log in group:
                if not isinstance(log, dict):
                    continue
                eid = str(log.get("event_id", ""))
                if eid and not any(
                    eid == str(e.get("event_id", "")) for e in _SYNTHETIC_LOG_RING[-10:]
                ):
                    _SYNTHETIC_LOG_RING.append(dict(log))
        if len(_SYNTHETIC_LOG_RING) > _SYNTHETIC_LOG_RING_MAX:
            _SYNTHETIC_LOG_RING = _SYNTHETIC_LOG_RING[-_SYNTHETIC_LOG_RING_MAX:]
        # Merge ring buffer into sources
        if _SYNTHETIC_LOG_RING:
            source_groups.append(list(_SYNTHETIC_LOG_RING))

        combined_by_key: dict[str, dict] = {}
        for group in source_groups:
            for log in group:
                if not isinstance(log, dict):
                    continue
                key = str(log.get("event_id") or (
                    f"{str(log.get('role', ''))}|{str(log.get('time', ''))}|{str(log.get('text', ''))}"
                ))
                if key in combined_by_key:
                    combined_by_key.pop(key)
                combined_by_key[key] = log
        combined_logs = list(combined_by_key.values())
        combined_logs.sort(key=_entry_sort_key)
        freshness_anchor_ts = _runtime_activity_anchor_ts()

        # Cascading freshness windows: show entries from progressively wider time
        # ranges when recent data is sparse, so the decision hall never goes blank
        # during quiet periods (e.g. monitor-only mode).
        windows = [30 * 60, 2 * 3600, 6 * 3600, None]  # 30min, 2h, 6h, unlimited
        min_entries_per_window = [10, 8, 5, 1]
        selected_window_sec: Optional[int] = windows[0]

        def _collect_logs(window_sec: Optional[int]) -> list[dict]:
            collected: list[dict] = []
            for log in combined_logs:
                text = log.get("text", "")
                role = log.get("role", "System")
                if not _has_trustworthy_mailbox_timestamp(log):
                    continue
                if window_sec is not None and not _is_event_fresh(
                    _entry_event_ts(log),
                    anchor_ts=freshness_anchor_ts,
                    max_age_sec=window_sec,
                ):
                    continue
                event_timestamp = str(log.get("timestamp", "")).strip()
                if not event_timestamp:
                    inferred_ts = log.get("_sort_ts")
                    if isinstance(inferred_ts, datetime.datetime):
                        event_timestamp = (
                            inferred_ts.astimezone(datetime.timezone.utc)
                            .isoformat()
                            .replace("+00:00", "Z")
                        )
                event_id = str(log.get("event_id", "") or "")
                if not event_id:
                    event_id = f"{role}|{event_timestamp or str(log.get('time', ''))}|{text}"

                if any(kw in text for kw in [
                    "Signal: HOLD", "Signal: OPEN", "Signal: CLOSE",
                    "MT5 terminal_status.json not found", "MT5异常",
                    "信号状态 |"
                ]):
                    continue
                if role == "Risk_Commissar" and (
                    ("本轮未受理" in text and "未进入审批链" in text)
                    or text.startswith("本轮继续观望：")
                    or text.startswith("shadow 当前仅作离线观察与训练证据，")
                ):
                    continue

                collected.append({
                    "sender": role,
                    "timestamp": log.get("time", ""),
                    "eventTimestamp": event_timestamp,
                    "eventId": event_id,
                    "content": text
                })
            return collected

        for window_sec, min_entries in zip(windows, min_entries_per_window):
            logs = _collect_logs(window_sec)
            if len(logs) >= min_entries:
                selected_window_sec = window_sec
                break

        # 降序返回（最新在最前），前端直接按接口顺序渲染。
        logs = list(reversed(logs[-50:]))
    except Exception:
        pass
    return JSONResponse(
        content={
            "success": True,
            "logs": logs,
            "source": {
                "blackboard": str(bb_file) if bb_file is not None else "",
                "swarmDir": str(_resolve_active_swarm_dir() or ""),
                "freshnessAnchorTs": freshness_anchor_ts,
                "freshnessWindowSec": selected_window_sec,
            },
            "generatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z"),
        },
        headers={
            "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
            "Pragma": "no-cache",
            "Expires": "0",
        },
    )


def _run_update_apply_job(manifest: Optional[str], force: bool):
    try:
        app_updater.run_auto(manifest, force=force)
    except Exception as e:
        app_updater.write_state({"status": "apply_failed", "error": repr(e)})


@app.get("/api/update/status")
def get_update_status():
    now = time.time()
    cached_value = _UPDATE_STATUS_CACHE.get("value")
    cache_age = now - float(_UPDATE_STATUS_CACHE.get("ts", 0.0) or 0.0)

    version = app_updater.load_display_version()
    current_release = app_updater.get_current_release()
    candidate_release = app_updater.get_candidate_release()
    update_state = app_updater.load_update_state()

    if isinstance(cached_value, dict) and cache_age < 60.0:
        return {
            "success": True,
            "version": version,
            "currentRelease": current_release,
            "candidateRelease": candidate_release,
            "updateState": update_state,
            "quality": cached_value,
            "qualityStale": False,
        }

    fallback_quality = cached_value if isinstance(cached_value, dict) else {
        "runtime": {},
        "learning": {},
        "blockers": [],
        "safe_to_switch": True,
    }
    return {
        "success": True,
        "version": version,
        "currentRelease": current_release,
        "candidateRelease": candidate_release,
        "updateState": update_state,
        "quality": fallback_quality,
        "qualityStale": True,
    }


@app.get("/api/settings/apikey")
def get_api_key_status():
    """Return API key status for all configured providers."""
    providers_status = {}
    try:
        llm_config_dir = config_manager.get_llm_config_dir()
        openclaw = llm_config_dir / "openclaw.json"
        if openclaw.exists():
            cfg = json.loads(openclaw.read_text(encoding="utf-8"))
            for name, info in cfg.get("models", {}).get("providers", {}).items():
                if name == "omlx":
                    continue
                has_key = bool(info.get("apiKey", ""))
                providers_status[name] = {
                    "configured": has_key,
                    "masked_key": _mask_api_key(info.get("apiKey", "")),
                    "has_key": has_key,
                }
    except Exception:
        pass

    any_configured = any(v["has_key"] for v in providers_status.values()) if providers_status else False
    return {
        "success": True,
        "configured": any_configured,
        "providers": providers_status,
    }


@app.post("/api/settings/apikey")
def save_api_key(payload: dict):
    """Save API key for a specific provider to openclaw.json."""
    provider = (payload.get("provider") or "").strip().lower()
    api_key = (payload.get("api_key") or payload.get("apiKey") or "").strip()

    if not provider:
        # Backward compat: treat as DeepSeek
        provider = "deepseek"
    if not api_key:
        return {"success": False, "error": "api_key is required"}

    saved_to_openclaw = config_manager.save_provider_api_key(provider, api_key)

    # Mark onboarding step
    try:
        from src.onboarding.wizard import save_api_key_config
        save_api_key_config(provider)
    except Exception:
        pass

    return {"success": True, "configured": True, "masked_key": _mask_api_key(api_key),
            "provider": provider, "saved_to": "llm_providers" if saved_to_openclaw else "app_config"}


def _mask_api_key(key: str) -> str:
    """Mask API key for safe display. Shows first 4 + **** + last 4 chars.
    Aligned with src.llm.provider_admin._mask_api_key() for consistency.
    """
    if not key:
        return ""
    if len(key) <= 8:
        return key[:4] + "****"
    return key[:4] + "****" + key[-4:]


@app.post("/api/settings/mt5path")
def save_mt5_path(payload: dict):
    mt5_path = (payload.get("mt5_data_dir") or "").strip()
    if not mt5_path:
        return {"success": False, "error": "mt5_data_dir is required"}
    config_manager.save_app_config({"mt5_data_dir": mt5_path})
    return {"success": True}


@app.get("/api/settings/mt5path")
def get_mt5_path():
    config = config_manager.load_app_config()
    return {"success": True, "mt5_data_dir": config.get("mt5_data_dir", "")}


@app.get("/api/update/config")
def get_update_config():
    config = config_manager.load_app_config()
    return {
        "success": True,
        "update_manifest_url": config.get("update_manifest_url", ""),
    }


@app.post("/api/update/config")
def save_update_config(payload: UpdateConfigRequest):
    config_manager.save_app_config({"update_manifest_url": payload.update_manifest_url.strip()})
    return {"success": True, "update_manifest_url": payload.update_manifest_url.strip()}


@app.post("/api/update/check")
def check_update(payload: Optional[UpdateRequest] = None):
    manifest = payload.manifest if payload else None
    result = app_updater.run_check(app_updater.resolve_manifest_source(manifest))
    return {"success": True, **result}


@app.post("/api/update/apply")
def apply_update(payload: Optional[UpdateRequest] = None):
    manifest = payload.manifest if payload else None
    force = bool(payload.force) if payload else False
    result = app_updater.run_auto(manifest, force=force)
    success = result.get("status") in {"candidate_ready", "installed", "up_to_date", "no_manifest"}
    app_updater.write_state({"status": result.get("status", "apply_failed")})
    return {"success": success, **result}


@app.post("/api/update/promote")
def promote_update(payload: Optional[UpdateRequest] = None):
    force = bool(payload.force) if payload else False
    result = app_updater.promote_candidate(force=force)
    return {"success": result.get("status") in {"promoted", "installer_started"}, **result}


@app.post("/api/update/restart")
def restart_app_after_update():
    result = app_updater.schedule_restart()
    return {"success": result.get("status") == "restarting", **result}


@app.post("/api/update/rollback")
def rollback_update():
    result = app_updater.rollback_to_previous()
    return {"success": result.get("status") == "rolled_back", **result}


@app.get("/api/config/check_sync")
def check_config_sync():
    result = app_updater.check_config_sync()
    return {"success": result.get("status") != "error", **result}


@app.post("/api/config/sync")
def sync_config(background_tasks: BackgroundTasks = None):
    if background_tasks is None:
        result = app_updater.sync_config()
        return {"success": result.get("status") in ("applied", "partial"), **result}
    background_tasks.add_task(app_updater.sync_config)
    return {"success": True, "status": "sync_started"}


@app.post("/api/license/activate")
def activate_license(payload: dict):
    """GUI license activation — wraps the CLI activate flow."""
    license_key = str(payload.get("license_key", "")).strip()
    if not license_key:
        return {"ok": False, "error": "MISSING_FIELDS", "message": "请输入许可证密钥"}
    try:
        from src.license.activate import activate as do_activate, DEFAULT_SERVER
        import io
        old_stdout = sys.stdout
        capture = io.StringIO()
        sys.stdout = capture
        try:
            rc = do_activate(license_key, DEFAULT_SERVER)
        finally:
            sys.stdout = old_stdout
        output = capture.getvalue()
        if rc == 0:
            from src.license.validator import get_license_status
            status = get_license_status()
            return {"ok": True, "status": status}
        else:
            return {"ok": False, "error": "ACTIVATION_FAILED",
                    "message": f"激活失败: {output.strip()[-200:]}"}
    except Exception as e:
        return {"ok": False, "error": "ACTIVATION_ERROR", "message": str(e)}


@app.get("/api/license/status")
def api_get_license_status():
    """Get current license status for the GUI."""
    from src.license.validator import get_license_status
    return get_license_status()


def _strategy_generation_capabilities() -> dict[str, Any]:
    from src.license.validator import get_license_status

    status = get_license_status()
    features = status.get("features", []) if isinstance(status.get("features", []), list) else []
    feature_set = {str(item or "").strip() for item in features if str(item or "").strip()}
    tier = str(status.get("tier", "trial") or "trial").strip().lower()
    if not feature_set:
        if tier == "enterprise":
            feature_set = {
                "basic",
                "strategy_interactive",
                "strategy_template_generate",
                "strategy_plugin_generate",
                "advanced_params",
                "review_explain",
                "health_center",
                "paper_feedback",
            }
        elif tier == "pro":
            feature_set = {
                "basic",
                "strategy_interactive",
                "strategy_template_generate",
                "strategy_plugin_generate",
                "advanced_params",
                "review_explain",
                "health_center",
            }
        else:
            feature_set = {
                "basic",
                "strategy_interactive",
                "strategy_template_generate",
            }
    allow_plugin = "strategy_plugin_generate" in feature_set
    return {
        "status": str(status.get("status", "UNLICENSED") or "UNLICENSED"),
        "tier": tier,
        "features": sorted(feature_set),
        "allowInteractive": "strategy_interactive" in feature_set or "basic" in feature_set,
        "allowTemplateGenerate": "strategy_template_generate" in feature_set or "basic" in feature_set,
        "allowPluginGenerate": allow_plugin,
        "allowAdvancedParams": "advanced_params" in feature_set or allow_plugin,
        "allowReviewExplain": "review_explain" in feature_set or allow_plugin,
        "allowPaperFeedback": "paper_feedback" in feature_set,
        "recommendedAudience": "pro" if allow_plugin else "standard",
    }


def _enforce_strategy_compile_target_access(compile_target: str) -> str:
    normalized = str(compile_target or "template").strip().lower() or "template"
    caps = _strategy_generation_capabilities()
    if normalized == "plugin" and not bool(caps.get("allowPluginGenerate", False)):
        raise PermissionError("当前授权仅支持模板策略，插件策略仅对专业版开放")
    if normalized == "template" and not bool(caps.get("allowTemplateGenerate", False)):
        raise PermissionError("当前授权暂不允许生成策略资产")
    return normalized


@app.get("/api/strategy/custom/capabilities")
def get_strategy_custom_capabilities():
    return {"success": True, "capabilities": _strategy_generation_capabilities()}


@app.get("/api/timeline")
def get_timeline():
    """获取大模型交易复盘时间轴数据 (从真实状态解析)"""
    return _get_timeline_payload_cached(limit=40)

@app.get("/api/pending_signals")
def get_pending_signals():
    """获取等待人工复核的信号列表"""
    REVIEW_DIR = LOGS_DIR / "review_pending"
    if not REVIEW_DIR.exists():
        return {"success": True, "signalReviewPendingCount": 0, "signals": []}

    signals = []
    for f in REVIEW_DIR.glob("*.review.json"):
        try:
            d = json.loads(f.read_text(encoding="utf-8"))
            stable_id = str(d.get("stable_id") or f.stem)
            signal_id = str(d.get("signal_id") or "")
            signals.append({
                # action_id 统一使用 stable_id（与 review 文件名一致）
                "id": stable_id,
                "stable_id": stable_id,
                "signal_id": signal_id,
                "action": d.get("action"),
                "side": d.get("side"),
                "entry": d.get("entry"),
                "confidence": d.get("requested_confidence", d.get("confidence", 0)),
                "confidenceRaw": d.get("confidence", 0),
                "reason": d.get("reason", "低置信度需复核")
            })
        except:
            pass
    return {
        "success": True,
        "signalReviewPendingCount": len(signals),
        "signals": signals,
    }

@app.post("/api/signals/{signal_id}/{action}")
def process_signal(signal_id: str, action: str):
    """处理待复核信号 (approve/reject)"""
    import shutil
    REVIEW_DIR = LOGS_DIR / "review_pending"
    src = REVIEW_DIR / f"{signal_id}.review.json"
    resolved_id = signal_id

    # 兼容旧前端传 signal_id（带时间戳）而非 stable_id 的情况。
    if not src.exists():
        for f in REVIEW_DIR.glob("*.review.json"):
            try:
                d = json.loads(f.read_text(encoding="utf-8"))
            except Exception:
                continue
            if str(d.get("signal_id") or "") == signal_id:
                src = f
                resolved_id = f.stem
                break

    if not src.exists():
        return {"success": False, "message": "信号已过期或不存在"}

    dest_dir = REVIEW_DIR / action
    dest_dir.mkdir(exist_ok=True)
    shutil.move(str(src), str(dest_dir / f"{resolved_id}.json"))

    return {"success": True, "message": f"信号已 {action}", "id": resolved_id}

from pydantic import BaseModel

class RiskParams(BaseModel):
    risk_level: str
    max_position_size: float
    max_open_positions: int
    sl_pips: int
    tp_pips: int
    human_in_the_loop: bool
    min_confidence: float
    account_total_loss_stop_pct: float


_DEFAULT_BASE_LOTS = 0.05
_RISK_LEVEL_PROFILES = {
    "conservative": {
        "max_position_size": 0.05,
        "max_open_positions": 1,
        "sl_pips": 20,
        "tp_pips": 40,
        "min_confidence": 80.0,
    },
    "balanced": {
        "max_position_size": 0.10,
        "max_open_positions": 2,
        "sl_pips": 40,
        "tp_pips": 80,
        "min_confidence": 70.0,
    },
    "aggressive": {
        "max_position_size": 0.20,
        "max_open_positions": 3,
        "sl_pips": 60,
        "tp_pips": 120,
        "min_confidence": 60.0,
    },
}


def _normalize_risk_level(level: Any) -> str:
    value = str(level or "").strip().lower()
    if value in _RISK_LEVEL_PROFILES:
        return value
    return "balanced"


def _risk_profile_for_level(level: Any) -> Dict[str, Any]:
    return dict(_RISK_LEVEL_PROFILES[_normalize_risk_level(level)])


def _risk_level_from_max_position_size(max_position_size: float) -> str:
    size = float(max_position_size or 0.0)
    if size <= 0.051:
        return "conservative"
    if size >= 0.195:
        return "aggressive"
    return "balanced"


def _strategy_rules_file() -> pathlib.Path:
    runtime_file = CONFIG_DIR / "strategy_rules_v3.json"
    if runtime_file.exists():
        return runtime_file
    repo_file = pathlib.Path(__file__).resolve().parent / "config" / "strategy_rules_v3.json"
    if repo_file.exists():
        return repo_file
    return runtime_file


def _load_strategy_rules_for_update() -> tuple[pathlib.Path, dict]:
    strategy_file = _strategy_rules_file()
    data: dict = {}
    if strategy_file.exists():
        try:
            loaded = json.loads(strategy_file.read_text(encoding="utf-8"))
            if isinstance(loaded, dict):
                data = loaded
        except Exception:
            data = {}
    data.setdefault("position_sizing", {})
    data.setdefault("risk_governor", {})
    return strategy_file, data


class TrainingStartRequest(BaseModel):
    model: Optional[str] = None
    dataset: Optional[str] = None


class TrainingAnalyzeRequest(BaseModel):
    model: Optional[str] = None


class TrainingEvolveRequest(BaseModel):
    model: Optional[str] = None


DEFAULT_ANALYSIS_MODEL = "deepseek/deepseek-v4-pro"
TRAINING_RUNTIME_CONFIG = {
    "engine": "Grid Search",
    "source": "Yahoo Finance",
    "symbol": "GC=F",
    "lookbackDays": 10,
    "interval": "5m",
    "mode": "train",
}
TRAINING_DATASET_PRESETS = [
    {
        "id": "gc_f_5m_10d",
        "label": "GC=F · 10d · 5m · Yahoo Finance",
        "symbol": "GC=F",
        "lookbackDays": 10,
        "interval": "5m",
        "source": "Yahoo Finance",
    },
    {
        "id": "gc_f_5m_30d",
        "label": "GC=F · 30d · 5m · Yahoo Finance",
        "symbol": "GC=F",
        "lookbackDays": 30,
        "interval": "5m",
        "source": "Yahoo Finance",
    },
    {
        "id": "gc_f_15m_30d",
        "label": "GC=F · 30d · 15m · Yahoo Finance",
        "symbol": "GC=F",
        "lookbackDays": 30,
        "interval": "15m",
        "source": "Yahoo Finance",
    },
]


def _default_training_dataset_label() -> str:
    return (
        f"{TRAINING_RUNTIME_CONFIG['symbol']} · "
        f"{TRAINING_RUNTIME_CONFIG['lookbackDays']}d · "
        f"{TRAINING_RUNTIME_CONFIG['interval']} · "
        f"{TRAINING_RUNTIME_CONFIG['source']}"
    )


def _normalize_training_dataset(dataset: Optional[str]) -> str:
    value = (dataset or "").strip()
    if not value or value in {"-", "loading", "TRAINING_BLOCKED"}:
        return _default_training_dataset_label()
    return value


def _get_training_dataset_presets() -> List[Dict[str, Any]]:
    return [
        {
            **preset,
            "label": preset.get("label") or (
                f"{preset.get('symbol')} · {preset.get('lookbackDays')}d · "
                f"{preset.get('interval')} · {preset.get('source')}"
            ),
        }
        for preset in TRAINING_DATASET_PRESETS
    ]


def _resolve_training_dataset(dataset: Optional[str]) -> Dict[str, Any]:
    value = _normalize_training_dataset(dataset)
    for preset in _get_training_dataset_presets():
        if value in {str(preset.get("id", "")), str(preset.get("label", ""))}:
            return preset
    return {
        "id": "custom",
        "label": value,
        "symbol": TRAINING_RUNTIME_CONFIG["symbol"],
        "lookbackDays": TRAINING_RUNTIME_CONFIG["lookbackDays"],
        "interval": TRAINING_RUNTIME_CONFIG["interval"],
        "source": TRAINING_RUNTIME_CONFIG["source"],
    }


def _normalize_analysis_model(model: Optional[str]) -> str:
    value = (model or "").strip()
    if not value or value in {"-", "loading", "hybrid-ensemble"}:
        return DEFAULT_ANALYSIS_MODEL
    return value


def _read_json_file(path: pathlib.Path) -> Dict[str, Any]:
    if not path.exists():
        return {}
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return payload if isinstance(payload, dict) else {}


_RUNTIME_SCHEDULER_SNAPSHOT_MAX_AGE_SEC = 5 * 60


def _load_runtime_scheduler_snapshot() -> dict[str, Any]:
    path, payload = _read_json_from_candidates(_runtime_scope_candidates("logs", "runtime_scheduler_snapshot.json"))
    if not isinstance(payload, dict) or not payload:
        return {}
    snapshot_ts = _parse_timestamp_value(payload.get("generated_at"))
    if snapshot_ts is None and path is not None:
        snapshot_ts = _file_mtime_ts(path)
    if snapshot_ts is None or snapshot_ts <= 0:
        return {}
    if (time.time() - snapshot_ts) > _RUNTIME_SCHEDULER_SNAPSHOT_MAX_AGE_SEC:
        return {}
    return payload


def _record_file_repair_event(
    path: pathlib.Path,
    *,
    reason: str,
    backup_path: str = "",
) -> None:
    event = {
        "timestamp": time.time(),
        "event_type": "FILE_AUTO_REPAIRED",
        "file": str(path),
        "filename": path.name,
        "backup_path": backup_path,
        "reason": str(reason or "invalid_json"),
    }
    try:
        repair_events_file = _file_repair_events_file()
        repair_events_file.parent.mkdir(parents=True, exist_ok=True)
        with repair_events_file.open("a", encoding="utf-8") as handle:
            handle.write(json.dumps(event, ensure_ascii=False) + "\n")
    except Exception:
        pass


def _record_system_message(
    *,
    kind: str,
    title: str,
    desc: str,
    color: str = "primary",
    source: str = "system",
    metadata: Optional[Dict[str, Any]] = None,
) -> None:
    payload = {
        "timestamp": time.time(),
        "kind": str(kind or "system"),
        "title": str(title or "系统消息"),
        "desc": str(desc or ""),
        "color": str(color or "primary"),
        "source": str(source or "system"),
        "metadata": metadata if isinstance(metadata, dict) else {},
    }
    try:
        system_messages_file = _system_messages_file()
        system_messages_file.parent.mkdir(parents=True, exist_ok=True)
        with system_messages_file.open("a", encoding="utf-8") as handle:
            handle.write(json.dumps(payload, ensure_ascii=False) + "\n")
    except Exception:
        pass


def _recover_corrupt_json_file(
    path: pathlib.Path,
    default_payload: Dict[str, Any],
    *,
    reason: str,
) -> None:
    backup_path = ""
    if path.exists():
        try:
            ts_suffix = int(time.time())
            backup_target = path.with_name(f"{path.stem}.corrupt.{ts_suffix}{path.suffix}")
            path.replace(backup_target)
            backup_path = str(backup_target)
        except Exception:
            backup_path = ""
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        tmp_path = path.with_name(f".{path.name}.recover.{os.getpid()}.tmp")
        tmp_path.write_text(
            json.dumps(default_payload, ensure_ascii=False, indent=2) + "\n",
            encoding="utf-8",
        )
        os.replace(tmp_path, path)
    except Exception as exc:
        _record_file_repair_event(
            path,
            reason=f"{reason}; rewrite_failed={exc}",
            backup_path=backup_path,
        )
        return
    _record_file_repair_event(path, reason=reason, backup_path=backup_path)
    _record_system_message(
        kind="file_repair",
        title="自动修复已执行",
        desc=(
            f"检测到 `{path.name}` 损坏，系统已自动恢复默认结构"
            f"{'，并备份损坏原文件' if backup_path else ''}。"
        ),
        color="warning",
        source="auto_repair",
        metadata={
            "file": str(path),
            "filename": path.name,
            "backup_path": backup_path,
            "reason": reason,
        },
    )


def _cleanup_corrupt_backups(max_age_days: int = 7) -> int:
    """Remove .corrupt.*.json backup files older than max_age_days."""
    import time as _time, glob as _glob
    removed = 0
    config_dir = CONFIG_DIR
    if not config_dir.exists():
        return 0
    now = _time.time()
    cutoff = now - max_age_days * 86400
    for p in sorted(config_dir.glob("*.corrupt.*.json")):
        try:
            if p.stat().st_mtime < cutoff:
                p.unlink()
                removed += 1
        except Exception:
            pass
    if removed:
        _record_system_message(
            kind="cleanup",
            title="清理损坏备份",
            desc=f"已清理 {removed} 个过期损坏备份文件（超过 {max_age_days} 天）",
            color="info",
            source="auto_cleanup",
        )
    return removed


def _parse_timestamp_value(value: Any) -> Optional[float]:
    if value is None:
        return None
    if isinstance(value, (int, float)):
        ts = float(value)
        return ts if ts > 0 else None
    if isinstance(value, str):
        text = value.strip()
        if not text:
            return None
        try:
            return float(text)
        except Exception:
            pass
        try:
            normalized = text.replace("Z", "+00:00")
            return datetime.datetime.fromisoformat(normalized).timestamp()
        except Exception:
            return None
    return None


def _utc_iso_from_ts(ts: Optional[float]) -> str:
    if not ts:
        return ""
    return datetime.datetime.fromtimestamp(ts, datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _collect_projection_source_versions(
    source_paths: Dict[str, Optional[pathlib.Path]],
) -> Dict[str, str]:
    source_versions: Dict[str, str] = {}
    for source_name, path in source_paths.items():
        if not source_name or path is None:
            continue
        try:
            if path.exists():
                source_versions[source_name] = _utc_iso_from_ts(path.stat().st_mtime)
        except Exception:
            continue
    return source_versions


def _attach_projection_metadata(
    payload: Dict[str, Any],
    *,
    snapshot_name: str,
    source_paths: Dict[str, Optional[pathlib.Path]],
) -> Dict[str, Any]:
    projection = dict(payload)
    snapshot_ts = time.time()
    projection["snapshot_id"] = f"{snapshot_name}_{int(snapshot_ts * 1000)}_{uuid.uuid4().hex[:8]}"
    projection["generated_at"] = _utc_iso_from_ts(snapshot_ts)
    projection["source_versions"] = _collect_projection_source_versions(source_paths)
    return projection


def _runtime_projection_source_paths() -> Dict[str, Optional[pathlib.Path]]:
    strategy_rules_file, _strategy_rules = _load_strategy_rules_for_update()
    return {
        "strategy_market_state": _resolve_runtime_artifact("config", "strategy_market_state.json"),
        "strategy_runtime_status": _resolve_runtime_artifact("logs", "strategy_runtime_status.json"),
        "runtime_scheduler_snapshot": _resolve_runtime_artifact("logs", "runtime_scheduler_snapshot.json"),
        "effective_params": _resolve_runtime_artifact("logs", "effective_params.json"),
        "strategy_rules": strategy_rules_file,
        "execution_state": _resolve_runtime_artifact("data", "execution_state.json"),
        "position_lifecycle": _resolve_runtime_artifact("data", "position_lifecycle.json"),
        "blackboard_state": _resolve_runtime_root_artifact("blackboard_state.json"),
    }


def _age_seconds_from_ts(ts: Optional[float]) -> Optional[float]:
    if not ts:
        return None
    return round(max(0.0, time.time() - float(ts)), 1)


def _timeline_event(
    *,
    source: str,
    ts: Optional[float],
    role: str,
    event_type: str,
    title: str,
    content: str,
    data: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    event_ts = float(ts or time.time())
    return {
        "source": source,
        "time": datetime.datetime.fromtimestamp(event_ts).strftime("%H:%M:%S"),
        "timestamp": event_ts,
        "role": role,
        "type": event_type,
        "title": title,
        "content": content,
        "data": data or {},
    }


def _read_ndjson_tail(path: pathlib.Path, limit: int = 20) -> list[dict[str, Any]]:
    if not path.exists():
        return []
    try:
        lines = path.read_text(encoding="utf-8").splitlines()
    except Exception:
        return []
    rows: list[dict[str, Any]] = []
    for raw in lines[-max(limit * 3, limit) :]:
        if not raw.strip():
            continue
        try:
            payload = json.loads(raw)
        except Exception:
            continue
        if isinstance(payload, dict):
            rows.append(payload)
    return rows[-limit:]


def _collect_execution_signal_rows(limit: int = 12) -> dict[str, list[dict[str, Any]]]:
    execution_state = _runtime_execution_state_payload()
    signals = execution_state.get("signals", {}) if isinstance(execution_state.get("signals", {}), dict) else {}
    rows: list[dict[str, Any]] = []
    terminal_states = {"FILLED", "REJECTED", "VETOED", "TIMEOUT", "RECONCILED"}
    for signal_id, entry in signals.items():
        if not isinstance(entry, dict):
            continue
        history = entry.get("history", []) if isinstance(entry.get("history", []), list) else []
        latest = history[-1] if history else {}
        metadata = entry.get("metadata", {}) if isinstance(entry.get("metadata", {}), dict) else {}
        latest_metadata = (
            latest.get("metadata", {})
            if isinstance(latest, dict) and isinstance(latest.get("metadata", {}), dict)
            else {}
        )
        latest_ts = _parse_timestamp_value(
            latest.get("ts")
            if isinstance(latest, dict)
            else None
        ) or _parse_timestamp_value(entry.get("updated_at")) or _parse_timestamp_value(entry.get("created_at"))
        state = str(entry.get("state", "CREATED") or "CREATED").upper()
        rows.append(
            {
                "signalId": str(entry.get("signal_id") or signal_id),
                "traceId": str(entry.get("trace_id") or entry.get("signal_id") or signal_id),
                "state": state,
                "latestTs": latest_ts or 0.0,
                "latestTime": datetime.datetime.fromtimestamp(latest_ts).strftime("%H:%M:%S") if latest_ts else "",
                "reason": str(
                    latest.get("reason", "") if isinstance(latest, dict) else ""
                ) or str(latest_metadata.get("reject_reason", "") or metadata.get("reject_reason", "") or ""),
                "side": str(
                    latest_metadata.get("side")
                    or latest_metadata.get("final_side")
                    or metadata.get("side")
                    or entry.get("side")
                    or (latest.get("side") if isinstance(latest, dict) else "")
                    or ""
                ),
                "strategyId": str(
                    latest_metadata.get("entry_owner_strategy_id")
                    or latest_metadata.get("attributed_strategy_id")
                    or latest_metadata.get("strategy_id")
                    or latest_metadata.get("selected_strategy_id")
                    or metadata.get("entry_owner_strategy_id")
                    or metadata.get("attributed_strategy_id")
                    or metadata.get("strategy_id")
                    or metadata.get("strategy")
                    or entry.get("entry_owner_strategy_id")
                    or entry.get("strategy_id")
                    or ""
                ),
                "action": str(
                    latest_metadata.get("action")
                    or latest_metadata.get("final_action")
                    or metadata.get("action")
                    or entry.get("action")
                    or (latest.get("action") if isinstance(latest, dict) else "")
                    or ""
                ),
                "entry": latest_metadata.get("entry") or metadata.get("entry") or entry.get("entry"),
                "entryStyle": str(
                    latest_metadata.get("entry_style")
                    or metadata.get("entry_style")
                    or entry.get("entry_style")
                    or ""
                ),
                "materializedAction": str(
                    latest_metadata.get("materialized_action")
                    or metadata.get("materialized_action")
                    or entry.get("materialized_action")
                    or ""
                ),
                "history": history[-4:],
            }
        )
    rows.sort(key=lambda item: float(item.get("latestTs", 0.0) or 0.0), reverse=True)
    active = [row for row in rows if str(row.get("state", "")).upper() not in terminal_states][:limit]
    recent = rows[:limit]
    return {"active": active, "recent": recent}


def _read_latest_jsonl_dict_from_path(
    path: pathlib.Path,
    *,
    max_bytes: int = 256 * 1024,
    max_lines: int = 4000,
) -> dict[str, Any]:
    try:
        lines = _read_tail_lines(path, max_bytes=max_bytes, max_lines=max_lines)
    except Exception:
        return {}
    for raw in reversed(lines):
        raw = str(raw or "").strip()
        if not raw:
            continue
        try:
            payload = json.loads(raw)
        except Exception:
            continue
        if isinstance(payload, dict):
            return payload
    return {}


def _read_latest_jsonl_dict(paths: list[pathlib.Path]) -> dict[str, Any]:
    path = _resolve_existing_file(paths)
    if path is None:
        return {}
    return _read_latest_jsonl_dict_from_path(path)


def _signal_reason_summary(signal: dict[str, Any]) -> str:
    reject_reason = str(
        signal.get("materialized_reason")
        or signal.get("reject_reason")
        or ""
    ).strip()
    if reject_reason:
        return reject_reason
    trace = signal.get("trace", {}) if isinstance(signal.get("trace", {}), dict) else {}
    evidence = trace.get("evidence", []) if isinstance(trace.get("evidence", []), list) else []
    if evidence:
        return " | ".join(str(item) for item in evidence[:4])
    return str(signal.get("mode", signal.get("action", "HOLD")) or "HOLD")


def _hold_mode_label(
    aggregated_action: str,
    contributors: list[dict[str, Any]],
    blocked_reasons: list[str],
) -> str:
    if aggregated_action != "HOLD":
        return "executed"
    has_directional_contributor = any(
        str(item.get("action", "HOLD") or "HOLD").upper() != "HOLD"
        and str(item.get("side", "NONE") or "NONE").upper() in {"BUY", "SELL"}
        for item in contributors
        if isinstance(item, dict)
    )
    if has_directional_contributor:
        return "conflict_suppressed_hold"
    if blocked_reasons:
        return "blocked_hold"
    return "proactive_hold"


def _build_runtime_arbitration_payload() -> dict[str, Any]:
    log_path = _resolve_existing_file(_runtime_log_candidates("runtime.log"))
    if log_path is None:
        return {
            "generatedAt": "",
            "summary": {
                "finalAction": "UNKNOWN",
                "finalSide": "NONE",
                "holdMode": "unknown",
                "reason": "暂无运行解释数据",
            },
            "contributors": [],
        }
    try:
        stat = log_path.stat()
    except Exception:
        stat = None
    if stat is not None:
        cache_key = (str(log_path), float(stat.st_mtime), int(stat.st_size))
        cached_path = str(_RUNTIME_ARBITRATION_CACHE.get("path", "") or "")
        cached_mtime = float(_RUNTIME_ARBITRATION_CACHE.get("mtime", 0.0) or 0.0)
        cached_size = int(_RUNTIME_ARBITRATION_CACHE.get("size", 0) or 0)
        cached_value = _RUNTIME_ARBITRATION_CACHE.get("value")
        if (
            isinstance(cached_value, dict)
            and cached_path == cache_key[0]
            and cached_mtime == cache_key[1]
            and cached_size == cache_key[2]
        ):
            return dict(cached_value)
    latest_envelope = _read_latest_jsonl_dict_from_path(log_path)
    if not latest_envelope:
        return {
            "generatedAt": "",
            "summary": {
                "finalAction": "UNKNOWN",
                "finalSide": "NONE",
                "holdMode": "unknown",
                "reason": "暂无运行解释数据",
            },
            "contributors": [],
        }

    signal = latest_envelope.get("signal", {}) if isinstance(latest_envelope.get("signal", {}), dict) else {}
    execution_report = (
        latest_envelope.get("execution_report", {})
        if isinstance(latest_envelope.get("execution_report", {}), dict)
        else {}
    )
    aggregated = (
        latest_envelope.get("_aggregated_intent", {})
        if isinstance(latest_envelope.get("_aggregated_intent", {}), dict)
        else {}
    )
    strategy_intents = (
        latest_envelope.get("_strategy_intents", [])
        if isinstance(latest_envelope.get("_strategy_intents", []), list)
        else []
    )
    scheduler = (
        latest_envelope.get("_scheduler", {})
        if isinstance(latest_envelope.get("_scheduler", {}), dict)
        else {}
    )
    contributor_rows = (
        aggregated.get("contributors", [])
        if isinstance(aggregated.get("contributors", []), list)
        else []
    )
    signal_by_instance: dict[str, dict[str, Any]] = {}
    for item in strategy_intents:
        if not isinstance(item, dict):
            continue
        instance_id = str(item.get("_instance_id", "") or "").strip()
        if instance_id:
            signal_by_instance[instance_id] = item
    hold_diagnostics = (
        execution_report.get("hold_diagnostics", {})
        if isinstance(execution_report.get("hold_diagnostics", {}), dict)
        else {}
    )
    blocked_reasons = (
        [str(item) for item in aggregated.get("blocked_reasons", []) if str(item).strip()]
        if isinstance(aggregated.get("blocked_reasons", []), list)
        else []
    )
    if not blocked_reasons and isinstance(hold_diagnostics.get("blocked_reasons", []), list):
        blocked_reasons = [
            str(item) for item in hold_diagnostics.get("blocked_reasons", []) if str(item).strip()
        ]
    if not blocked_reasons:
        fallback_reason = str(
            execution_report.get("reason")
            or signal.get("materialized_reason")
            or signal.get("reject_reason")
            or ""
        ).strip()
        if fallback_reason:
            blocked_reasons = [fallback_reason]
    normalized: list[dict[str, Any]] = []
    for row in contributor_rows:
        if not isinstance(row, dict):
            continue
        instance_id = str(row.get("instance_id", "") or "").strip()
        raw_signal = signal_by_instance.get(instance_id, {})
        signed_score = float(row.get("signed_score", 0.0) or 0.0)
        action = str(row.get("action", "HOLD") or "HOLD").upper()
        side = str(row.get("side", "NONE") or "NONE").upper()
        classification = "neutral"
        if action == "HOLD":
            classification = "hold"
        elif signed_score > 0:
            classification = "support_buy"
        elif signed_score < 0:
            classification = "support_sell"
        suppression_reason = ""
        if str(aggregated.get("action", "HOLD") or "HOLD").upper() == "HOLD" and action != "HOLD":
            suppression_reason = "conflict_or_low_net_score"
        elif action != "HOLD" and side != str(aggregated.get("side", "NONE") or "NONE").upper():
            suppression_reason = "opposite_side_offset"
        normalized.append(
            {
                "instanceId": instance_id,
                "strategyId": str(row.get("strategy_id", "") or ""),
                "action": action,
                "side": side,
                "weight": round(float(row.get("weight", 0.0) or 0.0), 4),
                "confidence": round(float(row.get("confidence", 0.0) or 0.0), 4),
                "signedScore": round(signed_score, 6),
                "absoluteScore": round(abs(signed_score), 6),
                "classification": classification,
                "wasSelected": instance_id == str(aggregated.get("selected_instance_id", "") or ""),
                "wasSuppressed": bool(suppression_reason),
                "suppressionReason": suppression_reason,
                "rawReason": _signal_reason_summary(raw_signal) if raw_signal else str(row.get("reason", "") or ""),
                "rawRejectReason": str(raw_signal.get("reject_reason", "") or "") if raw_signal else "",
                "paramOverrides": raw_signal.get("param_overrides", {}) if isinstance(raw_signal.get("param_overrides", {}), dict) else {},
            }
        )
    normalized.sort(
        key=lambda item: (
            -float(item.get("absoluteScore", 0.0) or 0.0),
            not bool(item.get("wasSelected", False)),
            str(item.get("instanceId", "")),
        )
    )
    final_action = str(aggregated.get("action", signal.get("action", "UNKNOWN")) or "UNKNOWN").upper()
    final_side = str(aggregated.get("side", signal.get("side", "NONE")) or "NONE").upper()
    hold_mode = _hold_mode_label(final_action, contributor_rows, blocked_reasons)
    selected_strategy_id, selected_instance_id = _resolve_scheduler_selected_fields(
        {
            "selected_strategy_id": aggregated.get("selected_strategy_id") or signal.get("selected_strategy_id") or signal.get("strategy_id") or "",
            "selected_instance_id": aggregated.get("selected_instance_id") or "",
            "selection": aggregated.get("selection", {}),
            "execution_decision": aggregated.get("execution_decision", {}),
            "route": scheduler.get("route", {}) if isinstance(scheduler.get("route", {}), dict) else {},
        }
    )
    payload = {
        "generatedAt": str(
            latest_envelope.get("timestamp")
            or latest_envelope.get("generated_at")
            or ""
        ),
        "scheduler": {
            "mode": str(scheduler.get("mode", "") or ""),
            "arbiter": str(scheduler.get("arbiter", "") or ""),
            "reason": str(scheduler.get("reason", "") or ""),
        },
        "summary": {
            "finalAction": final_action,
            "finalSide": final_side,
            "netScore": round(float(aggregated.get("net_score", 0.0) or 0.0), 6),
            "confidence": round(float(aggregated.get("confidence", 0.0) or 0.0), 4),
            "selectedInstanceId": str(selected_instance_id or ""),
            "selectedStrategyId": str(selected_strategy_id or ""),
            "holdMode": hold_mode,
            "reason": str(blocked_reasons[0] if blocked_reasons else _signal_reason_summary(signal)),
            "blockedReasons": blocked_reasons,
        },
        "contributors": normalized,
        "selection": _normalize_route_selection(
            aggregated.get("selection", {}),
            selected_strategy_id=str(selected_strategy_id or ""),
            selected_instance_id=str(selected_instance_id or ""),
        ),
        "canonicalMarket": _normalize_canonical_market_snapshot(
            aggregated.get("canonical_market_snapshot", {})
        ),
        "executionDecision": _normalize_execution_decision(
            aggregated.get("execution_decision", {}),
            route_payload=(
                scheduler.get("route", {})
                if isinstance(scheduler.get("route", {}), dict)
                else {}
            ),
            selected_strategy_id=str(selected_strategy_id or ""),
            selected_instance_id=str(selected_instance_id or ""),
        ),
    }
    if stat is not None:
        _RUNTIME_ARBITRATION_CACHE.update(
            {
                "path": str(log_path),
                "mtime": float(stat.st_mtime),
                "size": int(stat.st_size),
                "value": dict(payload),
            }
        )
    return payload


def _build_instance_execution_stats() -> dict[str, dict[str, Any]]:
    ledger_path = _resolve_existing_file(_runtime_log_candidates("trading_ledger.ndjson"))
    if ledger_path is None or not ledger_path.exists():
        return {}
    today = datetime.datetime.now(datetime.timezone.utc).date()
    success_statuses = {"ACKED", "FILLED"}
    reject_statuses = {"REJECTED", "TIMEOUT", "RETRY_EXHAUSTED", "INIT_FAILED"}
    stats: dict[str, dict[str, Any]] = {}
    latest_events: dict[tuple[str, str], dict[str, Any]] = {}
    try:
        lines = ledger_path.read_text(encoding="utf-8").splitlines()
    except Exception:
        return {}
    for raw in lines:
        raw = str(raw or "").strip()
        if not raw:
            continue
        try:
            record = json.loads(raw)
        except Exception:
            continue
        if not isinstance(record, dict):
            continue
        action = str(record.get("action", "") or "").upper()
        if action not in {"OPEN", "CLOSE"}:
            continue
        instance_id = str(
            record.get("instance_id")
            or record.get("selected_instance_id")
            or record.get("attributed_instance_id")
            or ""
        ).strip()
        if not instance_id:
            continue
        ts_raw = str(record.get("ts", record.get("timestamp", "")) or "").strip()
        ts_value: Optional[datetime.datetime] = None
        if ts_raw:
            try:
                ts_value = datetime.datetime.fromisoformat(ts_raw.replace("Z", "+00:00")).astimezone(datetime.timezone.utc)
            except Exception:
                ts_value = None
        status = str(record.get("status", "UNKNOWN") or "UNKNOWN").upper()
        event_id = str(
            record.get("intent_id")
            or record.get("decision_id")
            or record.get("signal_id")
            or record.get("ticket")
            or record.get("order_id")
            or record.get("deal_id")
            or ""
        ).strip()
        if not event_id:
            event_id = f"{instance_id}:{action}:{ts_raw}:{len(latest_events)}"
        event_key = (instance_id, f"{action}:{event_id}")
        existing = latest_events.get(event_key)
        if existing is None:
            latest_events[event_key] = {
                "status": status,
                "ts_raw": ts_raw,
                "ts_value": ts_value,
                "action": action,
            }
            continue
        existing_ts_raw = str(existing.get("ts_raw", "") or "")
        if ts_raw and (not existing_ts_raw or ts_raw >= existing_ts_raw):
            latest_events[event_key] = {
                "status": status,
                "ts_raw": ts_raw,
                "ts_value": ts_value,
                "action": action,
            }

    for (instance_id, _), event in latest_events.items():
        bucket = stats.setdefault(
            instance_id,
            {
                "today": 0,
                "reject": 0,
                "latestFilledAt": "",
                "latestAction": "",
                "latestStatus": "",
            },
        )
        status = str(event.get("status", "UNKNOWN") or "UNKNOWN").upper()
        action = str(event.get("action", "") or "").upper()
        ts_raw = str(event.get("ts_raw", "") or "")
        ts_value = event.get("ts_value")
        if ts_value is not None and ts_value.date() == today:
            if status in success_statuses:
                bucket["today"] = int(bucket.get("today", 0) or 0) + 1
            elif status in reject_statuses:
                bucket["reject"] = int(bucket.get("reject", 0) or 0) + 1
        if status in success_statuses and ts_raw:
            latest_filled_at = str(bucket.get("latestFilledAt", "") or "")
            if not latest_filled_at or ts_raw >= latest_filled_at:
                bucket["latestFilledAt"] = ts_raw
                bucket["latestAction"] = action
                bucket["latestStatus"] = status
    return stats


def _trade_event_timestamp(record: dict[str, Any]) -> float:
    for key in ("ts", "timestamp"):
        parsed = _parse_timestamp_value(record.get(key))
        if parsed:
            return float(parsed)
    return 0.0


def _event_identifier_candidates(record: dict[str, Any]) -> list[str]:
    keys: list[str] = []
    for field in ("ticket", "order_id", "deal_id", "signal_id", "decision_id", "intent_id"):
        value = record.get(field)
        if value is None:
            continue
        text = str(value).strip()
        if text and text not in keys and text != "0":
            keys.append(text)
    return keys


def _position_identifier_candidates(position: dict[str, Any]) -> list[str]:
    keys: list[str] = []
    for field in ("ticket", "order_id", "deal_id", "signal_id", "decision_id", "intent_id"):
        value = position.get(field)
        if value is None:
            continue
        text = str(value).strip()
        if text and text not in keys and text != "0":
            keys.append(text)
    return keys


def _normalize_position_side(position: dict[str, Any]) -> str:
    side = str(position.get("side", position.get("type", "")) or "").upper().strip()
    if side in {"BUY", "SELL"}:
        return side
    if side in {"LONG"}:
        return "BUY"
    if side in {"SHORT"}:
        return "SELL"
    return side or "UNKNOWN"


def _resolve_trade_event_owner(
    record: Optional[dict[str, Any]],
    *,
    preferred_side: str = "",
) -> tuple[str, str]:
    payload = record if isinstance(record, dict) else {}
    owner_instance_id = str(
        payload.get("entry_owner_instance_id")
        or payload.get("attributed_instance_id")
        or payload.get("instance_id")
        or payload.get("selected_instance_id")
        or ""
    ).strip()
    owner_strategy_id = str(
        payload.get("entry_owner_strategy_id")
        or payload.get("attributed_strategy_id")
        or payload.get("strategy_id")
        or payload.get("selected_strategy_id")
        or ""
    ).strip()
    side_upper = str(preferred_side or payload.get("side", "") or "").upper().strip()
    contributors = payload.get("contributors", []) if isinstance(payload.get("contributors", []), list) else []
    if side_upper not in {"BUY", "SELL"} or not contributors:
        return owner_instance_id, owner_strategy_id

    direct_side = ""
    for contributor in contributors:
        if not isinstance(contributor, dict):
            continue
        contributor_strategy_id = str(
            contributor.get("strategy_id")
            or contributor.get("strategyId")
            or ""
        ).strip()
        contributor_instance_id = str(
            contributor.get("instance_id")
            or contributor.get("instanceId")
            or ""
        ).strip()
        if (
            owner_strategy_id
            and contributor_strategy_id == owner_strategy_id
        ) or (
            owner_instance_id
            and contributor_instance_id == owner_instance_id
        ):
            direct_side = str(contributor.get("side", "") or "").upper().strip()
            break

    if owner_strategy_id and direct_side and direct_side == side_upper:
        return owner_instance_id, owner_strategy_id

    same_side_candidates = [
        contributor
        for contributor in contributors
        if isinstance(contributor, dict)
        and str(contributor.get("side", "") or "").upper().strip() == side_upper
        and str(contributor.get("action", "OPEN") or "OPEN").upper() != "HOLD"
    ]
    if not same_side_candidates:
        return owner_instance_id, owner_strategy_id

    best_owner = max(
        same_side_candidates,
        key=lambda contributor: (
            abs(float(contributor.get("signed_score", contributor.get("signedScore", 0.0)) or 0.0)),
            float(contributor.get("confidence", 0.0) or 0.0),
            float(contributor.get("weight", 0.0) or 0.0),
        ),
    )
    return (
        str(best_owner.get("instance_id") or best_owner.get("instanceId") or owner_instance_id or "").strip(),
        str(best_owner.get("strategy_id") or best_owner.get("strategyId") or owner_strategy_id or "").strip(),
    )


def _load_instance_trade_events() -> list[dict[str, Any]]:
    ledger_path = _resolve_existing_file(_runtime_log_candidates("trading_ledger.ndjson"))
    if ledger_path is None or not ledger_path.exists():
        return []
    now = time.time()
    try:
        stat = ledger_path.stat()
        mtime = float(stat.st_mtime)
        size = int(stat.st_size)
    except Exception:
        mtime = 0.0
        size = 0
    cached_value = _TRADE_EVENTS_CACHE.get("value")
    if (
        isinstance(cached_value, list)
        and (now - float(_TRADE_EVENTS_CACHE.get("ts", 0.0) or 0.0)) < 2.0
        and float(_TRADE_EVENTS_CACHE.get("mtime", 0.0) or 0.0) == mtime
        and int(_TRADE_EVENTS_CACHE.get("size", 0) or 0) == size
        and str(_TRADE_EVENTS_CACHE.get("path", "") or "") == str(ledger_path)
    ):
        return cached_value

    records: list[dict[str, Any]] = []
    try:
        lines: list[str]
        if size > 1024 * 1024:
            chunk_size = 1024 * 1024
            start = max(0, size - chunk_size)
            raw_bytes = b""
            try:
                with ledger_path.open("rb") as f:
                    f.seek(start)
                    raw_bytes = f.read()
            except Exception:
                raw_bytes = b""
            text = raw_bytes.decode("utf-8", errors="ignore")
            lines = text.splitlines()
            if len(lines) > 6000:
                lines = lines[-6000:]
        else:
            lines = ledger_path.read_text(encoding="utf-8", errors="ignore").splitlines()

        for raw in lines:
            raw = str(raw or "").strip()
            if not raw:
                continue
            try:
                payload = json.loads(raw)
            except Exception:
                continue
            if isinstance(payload, dict):
                records.append(payload)
    except Exception:
        return cached_value if isinstance(cached_value, list) else []

    records.sort(key=_trade_event_timestamp)
    _TRADE_EVENTS_CACHE.update(
        {
            "ts": now,
            "mtime": mtime,
            "size": size,
            "path": str(ledger_path),
            "value": records,
        }
    )
    return records


def _enrich_positions_with_attribution(positions: list[dict[str, Any]]) -> list[dict[str, Any]]:
    if not isinstance(positions, list) or not positions:
        return []
    normalized_positions = [item for item in positions if isinstance(item, dict)]
    if not normalized_positions:
        return []
    events = _load_instance_trade_events()
    lifecycle_payload = load_position_lifecycle(DATA_DIR)
    lifecycle_current = (
        lifecycle_payload.get("current", {})
        if isinstance(lifecycle_payload.get("current", {}), dict)
        else {}
    )

    success_statuses = {"ACKED", "FILLED"}
    open_events_by_key: dict[str, dict[str, Any]] = {}
    latest_close_events_by_key: dict[str, dict[str, Any]] = {}
    unmatched_open_events: list[dict[str, Any]] = []
    for record in events:
        if not isinstance(record, dict):
            continue
        action = str(record.get("action", "") or "").upper()
        status = str(record.get("status", "UNKNOWN") or "UNKNOWN").upper()
        keys = _event_identifier_candidates(record)
        if action == "OPEN" and status in success_statuses:
            if keys:
                for key in keys:
                    open_events_by_key[key] = record
            else:
                unmatched_open_events.append(record)
        elif action == "CLOSE":
            if keys:
                for key in keys:
                    latest_close_events_by_key[key] = record

    fallback_events_by_side: dict[str, list[dict[str, Any]]] = {}
    for record in unmatched_open_events:
        side = str(record.get("side", "UNKNOWN") or "UNKNOWN").upper()
        fallback_events_by_side.setdefault(side, []).append(record)
    for rows in fallback_events_by_side.values():
        rows.sort(key=_trade_event_timestamp, reverse=True)

    enriched: list[dict[str, Any]] = []
    used_fallback_signal_ids: set[str] = set()
    lifecycle_symbol = str(lifecycle_current.get("symbol", "") or "").strip().upper()
    lifecycle_ticket = str(lifecycle_current.get("ticket", "") or "").strip()
    lifecycle_has_owner = bool(
        str(lifecycle_current.get("entry_owner_instance_id", "") or "").strip()
        or str(lifecycle_current.get("current_manager_instance_id", "") or "").strip()
    )
    for position in normalized_positions:
        keys = _position_identifier_candidates(position)
        matched_open: Optional[dict[str, Any]] = None
        matched_close: Optional[dict[str, Any]] = None
        for key in keys:
            candidate = open_events_by_key.get(key)
            if candidate is not None and (
                matched_open is None or _trade_event_timestamp(candidate) >= _trade_event_timestamp(matched_open)
            ):
                matched_open = candidate
            close_candidate = latest_close_events_by_key.get(key)
            if close_candidate is not None and (
                matched_close is None or _trade_event_timestamp(close_candidate) >= _trade_event_timestamp(matched_close)
            ):
                matched_close = close_candidate
        if matched_open is None:
            side = _normalize_position_side(position)
            for candidate in fallback_events_by_side.get(side, []):
                signal_id = str(candidate.get("signal_id", "") or "")
                if signal_id and signal_id in used_fallback_signal_ids:
                    continue
                matched_open = candidate
                if signal_id:
                    used_fallback_signal_ids.add(signal_id)
                break

        position_side = _normalize_position_side(position)
        entry_instance_id, entry_strategy_id = _resolve_trade_event_owner(
            matched_open,
            preferred_side=position_side,
        )
        entry_selected_instance_id = str(
            (matched_open or {}).get("selected_instance_id")
            or (matched_open or {}).get("instance_id")
            or (matched_open or {}).get("attributed_instance_id")
            or ""
        ).strip()
        entry_selected_strategy_id = str(
            (matched_open or {}).get("selected_strategy_id")
            or (matched_open or {}).get("strategy_id")
            or (matched_open or {}).get("attributed_strategy_id")
            or ""
        ).strip()
        exit_instance_id, exit_strategy_id = _resolve_trade_event_owner(
            matched_close,
            preferred_side=position_side,
        )
        contributors = (
            (matched_open or {}).get("contributors", [])
            if isinstance((matched_open or {}).get("contributors", []), list)
            else []
        )
        matched_open_report = (
            (matched_open or {}).get("execution_report", {})
            if isinstance((matched_open or {}).get("execution_report", {}), dict)
            else {}
        )
        matched_close_report = (
            (matched_close or {}).get("execution_report", {})
            if isinstance((matched_close or {}).get("execution_report", {}), dict)
            else {}
        )
        position_ticket = str(position.get("ticket", "") or "").strip()
        lifecycle_match = bool(
            lifecycle_current
            and lifecycle_ticket
            and position_ticket
            and lifecycle_ticket == position_ticket
        )
        position_symbol = str(position.get("symbol", "") or "").strip().upper()
        lifecycle_symbol_match = not lifecycle_symbol or not position_symbol or lifecycle_symbol == position_symbol
        lifecycle_fallback = bool(
            not lifecycle_match
            and lifecycle_has_owner
            and lifecycle_symbol_match
            and len(normalized_positions) == 1
        )
        lifecycle_entry_strategy_id = str(
            lifecycle_current.get("entry_owner_strategy_id", "") if (lifecycle_match or lifecycle_fallback) else ""
        ).strip()
        if (
            lifecycle_entry_strategy_id
            and entry_strategy_id
            and lifecycle_entry_strategy_id != entry_strategy_id
        ):
            pass
        enriched.append(
            {
                **position,
                "entry_owner_instance_id": str(
                    lifecycle_current.get("entry_owner_instance_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip() or entry_instance_id,
                "entry_owner_strategy_id": str(
                    lifecycle_current.get("entry_owner_strategy_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip() or entry_strategy_id,
                "entry_selected_instance_id": str(
                    lifecycle_current.get("selected_instance_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip() or entry_selected_instance_id,
                "entry_selected_strategy_id": str(
                    lifecycle_current.get("selected_strategy_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip() or entry_selected_strategy_id,
                "current_manager_instance_id": str(
                    lifecycle_current.get("current_manager_instance_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str(matched_open_report.get("current_manager_instance_id", "") or "").strip()
                or entry_instance_id,
                "current_manager_strategy_id": str(
                    lifecycle_current.get("current_manager_strategy_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str(matched_open_report.get("current_manager_strategy_id", "") or "").strip()
                or entry_strategy_id,
                "expected_exit_owner_instance_id": str(
                    lifecycle_current.get("expected_exit_owner_instance_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str(matched_open_report.get("expected_exit_owner_instance_id", "") or "").strip()
                or str(matched_open_report.get("current_manager_instance_id", "") or "").strip()
                or entry_instance_id,
                "expected_exit_owner_strategy_id": str(
                    lifecycle_current.get("expected_exit_owner_strategy_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str(matched_open_report.get("expected_exit_owner_strategy_id", "") or "").strip()
                or str(matched_open_report.get("current_manager_strategy_id", "") or "").strip()
                or entry_strategy_id,
                "exit_owner_instance_id": str(
                    lifecycle_current.get("exit_owner_instance_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str(matched_close_report.get("exit_owner_instance_id", "") or "").strip()
                or exit_instance_id,
                "exit_owner_strategy_id": str(
                    lifecycle_current.get("exit_owner_strategy_id", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str(matched_close_report.get("exit_owner_strategy_id", "") or "").strip()
                or exit_strategy_id,
                "contributors": (
                    lifecycle_current.get("contributors", [])
                    if (lifecycle_match or lifecycle_fallback)
                    and isinstance(lifecycle_current.get("contributors", []), list)
                    else contributors
                ),
                "agent_direction": str(
                    lifecycle_current.get("agent_direction", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str((matched_open or {}).get("agent_direction", "") or "").strip(),
                "binding_direction": str(
                    lifecycle_current.get("binding_direction", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str((matched_open or {}).get("binding_direction", "") or "").strip(),
                "macro_authority": str(
                    lifecycle_current.get("macro_authority", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str((matched_open or {}).get("macro_authority", "") or "").strip(),
                "allowed_side": str(
                    lifecycle_current.get("allowed_side", "") if (lifecycle_match or lifecycle_fallback) else ""
                ).strip()
                or str((matched_open or {}).get("allowed_side", "") or "").strip(),
                "entry_signal_id": str((matched_open or {}).get("signal_id", "") or ""),
                "entry_decision_id": str((matched_open or {}).get("decision_id", "") or ""),
                "exit_signal_id": str((matched_close or {}).get("signal_id", "") or ""),
                "attribution_status": (
                    "matched" if matched_open else "lifecycle_fallback" if lifecycle_fallback else "unattributed"
                ),
            }
        )
    return enriched


def _build_instance_position_runtime(
    runtime_instances: list[dict[str, Any]],
    positions: list[dict[str, Any]],
) -> dict[str, Any]:
    execution_state = _runtime_execution_state_payload()
    lifecycle_payload = load_position_lifecycle(DATA_DIR)
    lifecycle_current = (
        lifecycle_payload.get("current", {})
        if isinstance(lifecycle_payload.get("current", {}), dict)
        else {}
    )
    signals = execution_state.get("signals", {}) if isinstance(execution_state.get("signals", {}), dict) else {}
    terminal_states = {"FILLED", "REJECTED", "VETOED", "TIMEOUT", "RECONCILED"}
    strategy_instance_rows: dict[str, list[dict[str, Any]]] = {}
    for instance in runtime_instances:
        if not isinstance(instance, dict):
            continue
        strategy_id = str(instance.get("strategy_id", "") or "").strip()
        instance_id = str(instance.get("instance_id", "") or "").strip()
        if not strategy_id or not instance_id:
            continue
        strategy_instance_rows.setdefault(strategy_id, []).append(instance)

    def _resolve_runtime_instance_id(raw_instance_id: str, raw_strategy_id: str) -> str:
        instance_id = str(raw_instance_id or "").strip()
        if instance_id:
            return instance_id
        strategy_id = str(raw_strategy_id or "").strip()
        if not strategy_id:
            return ""
        candidate_rows = strategy_instance_rows.get(strategy_id, [])
        if len(candidate_rows) == 1:
            return str(candidate_rows[0].get("instance_id", "") or "").strip()
        running_rows = [
            row
            for row in candidate_rows
            if str(row.get("status", "running") or "running").strip().lower() == "running"
        ]
        if len(running_rows) == 1:
            return str(running_rows[0].get("instance_id", "") or "").strip()
        return ""

    current_positions_by_instance: dict[str, list[dict[str, Any]]] = {}
    for position in positions:
        if not isinstance(position, dict):
            continue
        instance_id = _resolve_runtime_instance_id(
            str(
                position.get("entry_owner_instance_id")
                or position.get("current_manager_instance_id")
                or position.get("entry_selected_instance_id")
                or ""
            ).strip(),
            str(
                position.get("entry_owner_strategy_id")
                or position.get("current_manager_strategy_id")
                or position.get("entry_selected_strategy_id")
                or position.get("strategy_id")
                or ""
            ).strip(),
        )
        if not instance_id:
            continue
        current_positions_by_instance.setdefault(instance_id, []).append(position)

    active_signals_by_instance: dict[str, list[dict[str, Any]]] = {}
    for signal_id, entry in signals.items():
        if not isinstance(entry, dict):
            continue
        state = str(entry.get("state", "CREATED") or "CREATED").upper()
        if state in terminal_states:
            continue
        metadata = entry.get("metadata", {}) if isinstance(entry.get("metadata", {}), dict) else {}
        instance_id = str(
            metadata.get("attributed_instance_id")
            or metadata.get("selected_instance_id")
            or metadata.get("instance_id")
            or ""
        ).strip()
        if not instance_id:
            continue
        active_signals_by_instance.setdefault(instance_id, []).append(
            {
                "signalId": str(entry.get("signal_id", signal_id) or signal_id),
                "state": state,
                "action": str(metadata.get("action", "") or "").upper(),
                "side": str(metadata.get("side", "NONE") or "NONE").upper(),
                "volumeLots": float(metadata.get("volume_lots", 0.0) or 0.0),
            }
        )

    execution_stats = _build_instance_execution_stats()
    runtime_map: dict[str, dict[str, Any]] = {}
    current_holders: list[dict[str, Any]] = []
    lifecycle_status = str(lifecycle_current.get("status", "") or "").strip().lower()
    lifecycle_entry_owner = _resolve_runtime_instance_id(
        str(lifecycle_current.get("entry_owner_instance_id", "") or "").strip(),
        str(
            lifecycle_current.get("entry_owner_strategy_id")
            or lifecycle_current.get("selected_strategy_id")
            or ""
        ).strip(),
    )
    lifecycle_manager = _resolve_runtime_instance_id(
        str(lifecycle_current.get("current_manager_instance_id", "") or "").strip(),
        str(
            lifecycle_current.get("current_manager_strategy_id")
            or lifecycle_current.get("entry_owner_strategy_id")
            or ""
        ).strip(),
    )
    lifecycle_side = str(lifecycle_current.get("side", "NONE") or "NONE").upper()
    lifecycle_volume = float(lifecycle_current.get("volume", 0.0) or 0.0)
    lifecycle_ticket = str(lifecycle_current.get("ticket", "") or "").strip()
    terminal_positions_count = _read_terminal_status_positions_count()
    lifecycle_can_represent_live_position = terminal_positions_count != 0
    for instance in runtime_instances:
        if not isinstance(instance, dict):
            continue
        instance_id = str(instance.get("instance_id", "") or "").strip()
        if not instance_id:
            continue
        positions_for_instance = current_positions_by_instance.get(instance_id, [])
        active_signals = active_signals_by_instance.get(instance_id, [])
        opening_signals = [item for item in active_signals if str(item.get("action", "")).upper() == "OPEN"]
        closing_signals = [item for item in active_signals if str(item.get("action", "")).upper() in {"CLOSE", "REDUCE"}]
        position_sides = {
            _normalize_position_side(item)
            for item in positions_for_instance
            if _normalize_position_side(item) not in {"", "UNKNOWN"}
        }
        if len(position_sides) == 1:
            current_side = next(iter(position_sides))
        elif len(position_sides) > 1:
            current_side = "MIXED"
        elif opening_signals:
            current_side = str(opening_signals[0].get("side", "NONE") or "NONE").upper()
        else:
            current_side = "NONE"
        current_lots = round(
            sum(float(item.get("volume", item.get("lots", 0.0)) or 0.0) for item in positions_for_instance),
            4,
        )
        pending_open_lots = round(
            sum(float(item.get("volumeLots", 0.0) or 0.0) for item in opening_signals),
            4,
        )
        last_execution = execution_stats.get(instance_id, {})
        latest_action = str(last_execution.get("latestAction", "") or "")
        latest_filled_at = str(last_execution.get("latestFilledAt", "") or "")
        latest_status = str(last_execution.get("latestStatus", "") or "")
        if positions_for_instance:
            state = "holding"
            state_label = "持仓中"
        elif (
            lifecycle_status == "open"
            and lifecycle_entry_owner == instance_id
            and lifecycle_ticket
            and lifecycle_volume > 0
            and lifecycle_can_represent_live_position
        ):
            state = "holding"
            state_label = "持仓中"
            if current_side == "NONE":
                current_side = lifecycle_side
            if current_lots <= 0:
                current_lots = round(lifecycle_volume, 4)
        elif lifecycle_status == "closing" and lifecycle_manager == instance_id:
            state = "closing"
            state_label = "平仓中"
            if current_side == "NONE":
                current_side = lifecycle_side
            if current_lots <= 0:
                current_lots = round(lifecycle_volume, 4)
        elif opening_signals:
            state = "opening"
            state_label = "开仓中"
        elif closing_signals:
            state = "closing"
            state_label = "平仓中"
        elif latest_action == "CLOSE":
            state = "just_closed"
            state_label = "刚平仓"
        else:
            state = "flat"
            state_label = "空闲"
        runtime_entry = {
            "state": state,
            "stateLabel": state_label,
            "currentSide": current_side,
            "currentLots": current_lots,
            "pendingOpenLots": pending_open_lots,
            "positionsCount": len(positions_for_instance),
            "tickets": [
                str(item.get("ticket", "") or "")
                for item in positions_for_instance
                if str(item.get("ticket", "") or "").strip()
            ] or ([lifecycle_ticket] if lifecycle_ticket and state in {"holding", "closing"} else []),
            "activeSignals": active_signals,
            "latestFilledAt": latest_filled_at,
            "latestAction": latest_action,
            "latestStatus": latest_status,
            "currentManagerInstanceId": lifecycle_manager if lifecycle_manager == instance_id else "",
            "currentManagerStrategyId": (
                str(lifecycle_current.get("current_manager_strategy_id", "") or "").strip()
                if lifecycle_manager == instance_id
                else ""
            ),
        }
        runtime_map[instance_id] = runtime_entry
        if state == "holding":
            current_holders.append(
                {
                    "instanceId": instance_id,
                    "strategyId": str(instance.get("strategy_id", "") or ""),
                    "side": current_side,
                    "lots": current_lots,
                    "positionsCount": len(positions_for_instance),
                }
            )
    current_holders.sort(
        key=lambda item: (-float(item.get("lots", 0.0) or 0.0), str(item.get("instanceId", "")))
    )
    return {
        "byInstance": runtime_map,
        "currentHolders": current_holders,
        "holdingInstanceIds": [str(item.get("instanceId", "")) for item in current_holders],
    }


def _build_strategy_marketplace_payload(
    status_payload: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    runtime_payload_path, _runtime_payload_file = _read_json_from_candidates(
        _runtime_scope_candidates("logs", "strategy_runtime_status.json")
    )
    effective_params_path, effective_payload = _read_json_from_candidates(
        _runtime_scope_candidates("logs", "effective_params.json")
    )
    runtime_scheduler_path, runtime_scheduler_snapshot = _read_json_from_candidates(
        _runtime_scope_candidates("logs", "runtime_scheduler_snapshot.json")
    )
    strategy_file, strategy_rules = _load_strategy_rules_for_update()
    market_state_path = _resolve_runtime_artifact("config", "strategy_market_state.json")
    market_state = _load_strategy_market_state()
    market_strategies = (
        market_state.get("strategies", {})
        if isinstance(market_state.get("strategies", {}), dict)
        else {}
    )
    runtime_instances = (
        market_state.get("runtime_instances", [])
        if isinstance(market_state.get("runtime_instances", []), list)
        else []
    )
    status_payload = status_payload or get_status()
    current_positions = (
        status_payload.get("positions", [])
        if isinstance(status_payload.get("positions", []), list)
        else []
    )
    position_runtime = _build_instance_position_runtime(runtime_instances, current_positions)
    runtime_payload = _runtime_strategy_status_payload()
    active = runtime_payload.get("active", {}) if isinstance(runtime_payload.get("active", {}), dict) else {}
    manifests = runtime_payload.get("manifests", []) if isinstance(runtime_payload.get("manifests", []), list) else []
    manifest_map = {
        str(item.get("strategy_id", "") or ""): item
        for item in manifests
        if isinstance(item, dict)
    }
    health_map = runtime_payload.get("health", {}) if isinstance(runtime_payload.get("health", {}), dict) else {}
    loaded_set = {
        str(item)
        for item in runtime_payload.get("loaded", [])
        if isinstance(item, (str, int))
    }
    scheduler_payload = (
        effective_payload.get("runtime_scheduler", {})
        if isinstance(effective_payload.get("runtime_scheduler", {}), dict)
        else {}
    )
    decision_policy = _normalize_decision_policy(
        scheduler_payload.get("decision_policy", market_state.get("decision_policy"))
    )
    catalog = _strategy_catalog_from_state(market_state)
    config_revision = str(
        active.get("config_revision")
        or effective_payload.get("config_revision")
        or effective_payload.get("revision")
        or ""
    )
    effective_params_file = effective_params_path or (LOGS_DIR / "effective_params.json")
    effective_updated_at = _parse_timestamp_value(effective_payload.get("strategy_switched_at"))
    if effective_updated_at is None and effective_params_file.exists():
        effective_updated_at = effective_params_file.stat().st_mtime
    runtime_payload_updated_at = _utc_iso_from_ts(_file_mtime_ts(runtime_payload_path))
    runtime_scheduler_updated_at = _utc_iso_from_ts(_file_mtime_ts(runtime_scheduler_path))
    market_state_updated_at = _utc_iso_from_ts(_file_mtime_ts(market_state_path))
    strategy_projection = _normalize_strategy_projection(
        catalog=catalog,
        effective_payload=effective_payload,
        runtime_active=active,
        runtime_scheduler=runtime_scheduler_snapshot,
        scheduler_fallback=scheduler_payload,
        config_revision=config_revision,
        effective_updated_at=effective_updated_at,
        loaded_set=loaded_set,
    )
    session_scheduler_projection = _build_session_scheduler_projection(
        decision_policy=decision_policy,
        runtime_scheduler=runtime_scheduler_snapshot,
        scheduler_fallback=scheduler_payload,
    )
    configured_projection = (
        strategy_projection.get("configured", {})
        if isinstance(strategy_projection.get("configured", {}), dict)
        else {}
    )
    effective_projection = (
        strategy_projection.get("effective", {})
        if isinstance(strategy_projection.get("effective", {}), dict)
        else {}
    )
    fallback_projection = (
        strategy_projection.get("fallback", {})
        if isinstance(strategy_projection.get("fallback", {}), dict)
        else {}
    )
    active_strategy_ids_runtime = {
        strategy_id
        for strategy_id in _clean_str_list(effective_projection.get("activeStrategyIds"))
        if strategy_id
    }
    survival_snapshot_map = _build_strategy_survival_snapshot_map(market_state)
    strategies: list[dict[str, Any]] = []
    for strategy_id, fallback_name in catalog.items():
        manifest = manifest_map.get(strategy_id, {})
        health = health_map.get(strategy_id, {}) if isinstance(health_map.get(strategy_id, {}), dict) else {}
        market_row = market_strategies.get(strategy_id, {}) if isinstance(market_strategies.get(strategy_id, {}), dict) else {}
        versions = market_row.get("versions", []) if isinstance(market_row.get("versions", []), list) else []
        template = market_row.get("template", {}) if isinstance(market_row.get("template", {}), dict) else {}
        strategy_instances = [
            item for item in runtime_instances
            if isinstance(item, dict) and str(item.get("strategy_id", "")) == strategy_id
        ]
        name = str(
            market_row.get("display_name")
            or manifest.get("name")
            or fallback_name
        )
        version = str(
            market_row.get("current_version")
            or manifest.get("version")
            or health.get("strategy_version")
            or ("legacy" if strategy_id == "ema_macd" else "builtin")
        )
        manifest_description = str(manifest.get("description") or "")
        if manifest_description.startswith("Workspace strategy asset backed by "):
            manifest_description = ""
        healthy = bool(health.get("healthy", False)) if health else False
        is_runtime_active = strategy_id in active_strategy_ids_runtime
        has_runtime_instances = any(isinstance(item, dict) for item in strategy_instances)
        loaded = (
            strategy_id in loaded_set
            or bool(health.get("loaded", False))
            or is_runtime_active
            or has_runtime_instances
        )
        display_stage = _resolve_marketplace_display_stage(
            strategy_id,
            market_row,
            strategy_instances,
            is_runtime_active=is_runtime_active,
        )
        display_enabled = _resolve_marketplace_enabled(
            market_row,
            strategy_instances,
            is_runtime_active=is_runtime_active,
        )
        strategies.append(
            {
                "id": strategy_id,
                "name": name,
                "version": version,
                "_lifecycle_stage": display_stage,
                "kind": str(manifest.get("kind") or "signal"),
                "description": str(
                    market_row.get("source_document")
                    or market_row.get("description")
                    or manifest_description
                    or fallback_name
                ),
                "components": (
                    market_row.get("components", [])
                    if isinstance(market_row.get("components", []), list)
                    else []
                ),
                "isComposite": bool(market_row.get("is_composite", False)),
                "is_custom": bool(market_row.get("is_custom", False)),
                "default_mode": str(market_row.get("default_mode", "") or ""),
                "enabled": display_enabled,
                "active": is_runtime_active,
                "loaded": loaded,
                "healthy": healthy,
                "isDeployed": has_runtime_instances or is_runtime_active,
                "configRevision": str(health.get("config_revision") or config_revision or ""),
                "review_status": str(market_row.get("review_status", "") or ""),
                "promotion_state": (
                    market_row.get("promotion_state", {})
                    if isinstance(market_row.get("promotion_state", {}), dict)
                    else {}
                ),
                "survival_state": (
                    survival_snapshot_map.get(strategy_id, {})
                    if isinstance(survival_snapshot_map.get(strategy_id, {}), dict)
                    else {}
                ),
                "lastLatencyMs": health.get("last_latency_ms"),
                "lastRunAtMs": health.get("last_run_at_ms"),
                "lastError": str(health.get("last_error") or ""),
                "consecutiveFailures": int(health.get("consecutive_failures", 0) or 0),
                "versionsCount": len(versions),
                "template": template,
                "componentsCount": len(template.keys()),
                "instances": strategy_instances,
            }
        )
    strategies.sort(
        key=lambda item: (
            not bool(item.get("active", False)),
            not bool(item.get("loaded", False)),
            str(item.get("name", "")),
        )
    )
    by_stage = _group_marketplace_strategies_by_stage(strategies)
    risk_governor = strategy_rules.get("risk_governor", {}) if isinstance(strategy_rules.get("risk_governor", {}), dict) else {}
    position_sizing = strategy_rules.get("position_sizing", {}) if isinstance(strategy_rules.get("position_sizing", {}), dict) else {}
    compact_blackboard = _compact_blackboard_for_status(_get_blackboard_data())
    compact_risk_protocol = (
        compact_blackboard.get("Risk_Protocol", {})
        if isinstance(compact_blackboard.get("Risk_Protocol", {}), dict)
        else {}
    )
    compact_arbitration = (
        compact_blackboard.get("Execution_Arbitration", {})
        if isinstance(compact_blackboard.get("Execution_Arbitration", {}), dict)
        else {}
    )
    today_orders = int(status_payload.get("todayTrades", 0) or 0)
    risk_state = "blocked" if bool(status_payload.get("account_total_loss_stop_active", False)) else "normal"
    instance_execution_stats = _build_instance_execution_stats()
    runtime_instances_enriched: list[dict[str, Any]] = []
    for item in runtime_instances:
        if not isinstance(item, dict):
            continue
        sid = str(item.get("strategy_id", "")).strip()
        instance_id = str(item.get("instance_id", "")).strip()
        execution_stats = (
            instance_execution_stats.get(instance_id, {})
            if instance_id
            else {}
        )
        position_state = (
            position_runtime.get("byInstance", {}).get(instance_id, {})
            if isinstance(position_runtime.get("byInstance", {}), dict)
            else {}
        )
        health = health_map.get(sid, {}) if isinstance(health_map.get(sid, {}), dict) else {}
        healthy = bool(health.get("healthy", False))
        consecutive_failures = int(health.get("consecutive_failures", 0) or 0)
        health_score = max(45, 95 - min(40, consecutive_failures * 8))
        if healthy and consecutive_failures == 0:
            health_score = 92
        runtime_instances_enriched.append(
            {
                **item,
                "schedulerState": str(item.get("status", "running") or "running"),
                "instanceWeight": float(
                    effective_projection.get("instanceWeights", {}).get(str(item.get("instance_id", "")).strip(), 0.0)
                    if isinstance(effective_projection.get("instanceWeights", {}), dict)
                    else 0.0
                ),
                "health": {
                    "status": (
                        str(
                            compact_risk_protocol.get("instance_risk_state", {}).get(instance_id, "")
                            if isinstance(compact_risk_protocol.get("instance_risk_state", {}), dict)
                            else ""
                        )
                        or ("ok" if healthy or consecutive_failures == 0 else "warn")
                    ),
                    "score": float(
                        compact_risk_protocol.get("instance_health_score", {}).get(instance_id, health_score)
                        if isinstance(compact_risk_protocol.get("instance_health_score", {}), dict)
                        else health_score
                    ),
                },
                "risk": {
                    "status": str(
                        compact_risk_protocol.get("instance_risk_state", {}).get(instance_id, risk_state)
                        if isinstance(compact_risk_protocol.get("instance_risk_state", {}), dict)
                        else risk_state
                    ),
                    "decision": str(compact_risk_protocol.get("decision", "") or ""),
                    "riskCode": str(compact_risk_protocol.get("risk_code", "") or ""),
                },
                "positionState": position_state,
                "orders": {
                    "today": int(execution_stats.get("today", 0) or 0),
                    "reject": int(execution_stats.get("reject", 0) or 0),
                    "latestFilledAt": str(execution_stats.get("latestFilledAt", "") or ""),
                    "latestAction": str(execution_stats.get("latestAction", "") or ""),
                    "latestStatus": str(execution_stats.get("latestStatus", "") or ""),
                    "fallbackTodayTrades": today_orders,
                },
            }
        )
    runtime_instances_enriched.sort(
        key=lambda row: (str(row.get("schedulerState", "")) != "running", str(row.get("strategy_name", "")))
    )
    final_reject = status_payload.get("final_reject", {}) if isinstance(status_payload.get("final_reject", {}), dict) else {}
    governor_blockers: list[str] = _normalize_blocker_reasons(
        compact_risk_protocol.get("governor_blockers", [])
    )
    if bool(status_payload.get("account_total_loss_stop_active", False)):
        governor_blockers.append("account_total_loss_stop_active")
    final_reject_reason = str(final_reject.get("reason", "")).strip()
    if final_reject_reason:
        governor_blockers.append(final_reject_reason)
    governor_blockers = _normalize_blocker_reasons(governor_blockers)
    strategy_weights = (
        effective_projection.get("strategyWeights", {})
        if isinstance(effective_projection.get("strategyWeights", {}), dict)
        else {}
    )
    active_strategy_ids = _clean_str_list(effective_projection.get("activeStrategyIds"))
    active_instance_ids = _clean_str_list(effective_projection.get("activeInstanceIds"))
    instance_weights = (
        effective_projection.get("instanceWeights", {})
        if isinstance(effective_projection.get("instanceWeights", {}), dict)
        else {}
    )
    instance_health_score: dict[str, float] = {}
    instance_risk_state: dict[str, str] = {}
    strategy_budget_usage_rows: dict[str, dict[str, Any]] = {}
    for row in runtime_instances_enriched:
        if not isinstance(row, dict):
            continue
        instance_id = str(row.get("instance_id", "")).strip()
        strategy_id = str(row.get("strategy_id", "")).strip()
        allocation_pct = round(max(0.0, float(row.get("allocation", 0.0) or 0.0)) * 100.0, 2)
        if instance_id:
            instance_health_score[instance_id] = round(
                float(row.get("health", {}).get("score", 0.0) or 0.0)
                if isinstance(row.get("health", {}), dict)
                else 0.0,
                2,
            )
            instance_risk_state[instance_id] = str(
                row.get("risk", {}).get("status", risk_state)
                if isinstance(row.get("risk", {}), dict)
                else risk_state
            )
        if not strategy_id:
            continue
        bucket = strategy_budget_usage_rows.setdefault(
            strategy_id,
            {
                "strategy_id": strategy_id,
                "instance_count": 0,
                "allocation_pct": 0.0,
            },
        )
        bucket["instance_count"] = int(bucket.get("instance_count", 0) or 0) + 1
        bucket["allocation_pct"] = round(
            float(bucket.get("allocation_pct", 0.0) or 0.0) + allocation_pct,
            2,
        )
    strategy_ids_for_budget = sorted(
        {
            str(item)
            for item in list(strategy_budget_usage_rows.keys()) + list(strategy_weights.keys()) + active_strategy_ids
            if str(item).strip()
        }
    )
    strategy_budget_usage: list[dict[str, Any]] = []
    for strategy_id in strategy_ids_for_budget:
        bucket = strategy_budget_usage_rows.get(strategy_id, {})
        weight_pct = round(max(0.0, float(strategy_weights.get(strategy_id, 0.0) or 0.0)) * 100.0, 2)
        allocation_pct = round(float(bucket.get("allocation_pct", 0.0) or 0.0), 2)
        strategy_budget_usage.append(
            {
                "strategy_id": strategy_id,
                "instance_count": int(bucket.get("instance_count", 0) or 0),
                "allocation_pct": allocation_pct,
                "weight_pct": weight_pct,
                "risk_budget_pct": max(weight_pct, allocation_pct),
                "state": "blocked" if risk_state == "blocked" else "normal",
            }
        )
    global_loss_limit_pct = float(
        status_payload.get(
            "account_total_loss_stop_pct",
            strategy_rules.get("risk_governor", {}).get("account_total_loss_stop_pct", 0.0),
        )
        or 0.0
    )
    global_loss_current_pct = float(status_payload.get("account_total_loss_current_pct", 0.0) or 0.0)
    risk_budget_usage = {
        "global": {
            "limit_pct": round(global_loss_limit_pct, 2),
            "current_pct": round(global_loss_current_pct, 2),
            "remaining_pct": round(max(0.0, global_loss_limit_pct - global_loss_current_pct), 2),
            "state": risk_state,
        },
        "strategy": strategy_budget_usage,
        "instance": [
            {
                "instance_id": str(item.get("instance_id", "")).strip(),
                "strategy_id": str(item.get("strategy_id", "")).strip(),
                "allocation_pct": round(max(0.0, float(item.get("allocation", 0.0) or 0.0)) * 100.0, 2),
                "health_score": instance_health_score.get(str(item.get("instance_id", "")).strip(), 0.0),
                "risk_state": instance_risk_state.get(str(item.get("instance_id", "")).strip(), risk_state),
            }
            for item in runtime_instances_enriched
            if isinstance(item, dict)
        ],
    }
    risk_protocol = {
        "version": "v1",
        "generated_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
        "active_strategy_ids": active_strategy_ids,
        "active_instance_ids": active_instance_ids,
        "instance_weights": instance_weights,
        "strategy_weights": strategy_weights,
        "risk_budget_usage": risk_budget_usage,
        "governor_blockers": governor_blockers,
        "instance_health_score": instance_health_score,
        "instance_risk_state": instance_risk_state,
        "per_instance_health_details": (
            compact_risk_protocol.get("per_instance_health_details", {})
            if isinstance(compact_risk_protocol.get("per_instance_health_details", {}), dict)
            else {}
        ),
        "cross_strategy_correlation": (
            compact_risk_protocol.get("cross_strategy_correlation", {})
            if isinstance(compact_risk_protocol.get("cross_strategy_correlation", {}), dict)
            else {}
        ),
        "voting_weights": (
            compact_risk_protocol.get("voting_weights", {})
            if isinstance(compact_risk_protocol.get("voting_weights", {}), dict)
            else {}
        ),
        "decision": str(compact_risk_protocol.get("decision", "") or ""),
        "risk_code": str(compact_risk_protocol.get("risk_code", "") or ""),
        "risk_layer": str(compact_risk_protocol.get("risk_layer", "") or ""),
        "risk_source": str(compact_risk_protocol.get("risk_source", "") or ""),
        "blocking_rule_id": str(compact_risk_protocol.get("blocking_rule_id", "") or ""),
        "execution_owner": (
            compact_risk_protocol.get("execution_owner", {})
            if isinstance(compact_risk_protocol.get("execution_owner", {}), dict)
            else {}
        ),
        "source_layer": str(compact_risk_protocol.get("source_layer", "") or "derived"),
    }
    arbitration = _build_runtime_arbitration_payload()
    arbitration["entryOwnerInstanceId"] = str(compact_arbitration.get("entry_owner_instance_id", "") or "")
    arbitration["entryOwnerStrategyId"] = str(compact_arbitration.get("entry_owner_strategy_id", "") or "")
    arbitration["selectedInstanceId"] = str(compact_arbitration.get("selected_instance_id", "") or "")
    arbitration["selectedStrategyId"] = str(compact_arbitration.get("selected_strategy_id", "") or "")
    arbitration["sourceLayer"] = str(compact_arbitration.get("source_layer", "") or "derived")
    arbitration["selection"] = (
        effective_projection.get("selection", {})
        if isinstance(effective_projection.get("selection", {}), dict)
        else {}
    )
    arbitration["canonicalMarket"] = (
        effective_projection.get("canonicalMarket", {})
        if isinstance(effective_projection.get("canonicalMarket", {}), dict)
        else {}
    )
    arbitration["executionDecision"] = (
        effective_projection.get("executionDecision", {})
        if isinstance(effective_projection.get("executionDecision", {}), dict)
        else {}
    )

    payload = {
        "current": {
            **(
                strategy_projection.get("current", {})
                if isinstance(strategy_projection.get("current", {}), dict)
                else {}
            ),
            "registeredCount": len(strategies),
        },
        "survivalSummary": {
            "pauseCount": sum(
                1
                for item in strategies
                if str(item.get("survival_state", {}).get("candidatePoolAction", "") or "") == "pause"
            ),
            "downweightCount": sum(
                1
                for item in strategies
                if str(item.get("survival_state", {}).get("candidatePoolAction", "") or "") == "downweight"
            ),
            "promoteCandidateCount": sum(
                1
                for item in strategies
                if bool(item.get("survival_state", {}).get("worthPromoting", False))
            ),
        },
        "manifestSource": {
            "runtimeStatusFile": str(LOGS_DIR / "strategy_runtime_status.json"),
            "effectiveParamsFile": str(LOGS_DIR / "effective_params.json"),
            "strategyRulesFile": str(strategy_file),
        },
        "switching": {
            "canSwitch": True,
            "persistByDefault": True,
            "registered": runtime_payload.get("registered", []),
        },
        "strategyRules": {
            "riskGovernor": risk_governor,
            "positionSizing": position_sizing,
        },
        "strategyProjection": {
            "configured": configured_projection,
            "runtime": (
                strategy_projection.get("runtime", {})
                if isinstance(strategy_projection.get("runtime", {}), dict)
                else {}
            ),
            "fallback": fallback_projection,
            "effective": effective_projection,
            "sessionScheduler": session_scheduler_projection,
        },
        "selectionDiagnostics": (
            runtime_payload.get("selection_diagnostics", {})
            if isinstance(runtime_payload.get("selection_diagnostics", {}), dict)
            else {}
        ),
        "runtimeScheduler": {
            "mode": str(effective_projection.get("mode", "multi_active") or "multi_active"),
            "arbiter": str(effective_projection.get("arbiter", "allocation-weighted") or "allocation-weighted"),
            "activeInstances": (
                scheduler_payload.get("active_instances", [])
                if isinstance(scheduler_payload.get("active_instances", []), list)
                else []
            ),
            "activeInstanceIds": active_instance_ids,
            "activeStrategyIds": active_strategy_ids,
            "instanceWeights": instance_weights,
            "weights": strategy_weights,
            "strategyInstanceMap": (
                scheduler_payload.get("strategy_instance_map", {})
                if isinstance(scheduler_payload.get("strategy_instance_map", {}), dict)
                else {}
            ),
            "selectedStrategyId": str(effective_projection.get("selectedStrategyId", "") or ""),
            "selectedStrategyName": str(effective_projection.get("selectedStrategyName", "") or ""),
            "selectedInstanceId": str(effective_projection.get("selectedInstanceId", "") or ""),
            "selection": (
                effective_projection.get("selection", {})
                if isinstance(effective_projection.get("selection", {}), dict)
                else {}
            ),
            "canonicalMarket": (
                effective_projection.get("canonicalMarket", {})
                if isinstance(effective_projection.get("canonicalMarket", {}), dict)
                else {}
            ),
            "executionDecision": (
                effective_projection.get("executionDecision", {})
                if isinstance(effective_projection.get("executionDecision", {}), dict)
                else {}
            ),
            "updatedAt": str(
                effective_projection.get("generatedAt")
                or runtime_scheduler_snapshot.get("generated_at", "")
                or fallback_projection.get("updatedAt", "")
                or runtime_payload_updated_at
                or runtime_scheduler_updated_at
                or market_state_updated_at
                or ""
            ),
            "reason": str(effective_projection.get("reason", "allocation-weighted") or "allocation-weighted"),
            "sourceLayer": str(effective_projection.get("sourceLayer", "configured") or "configured"),
            "source": str(
                (
                    strategy_projection.get("current", {}).get("runtimeSource", "")
                    if isinstance(strategy_projection.get("current", {}), dict)
                    else ""
                )
                or "none"
            ),
            "fieldSources": (
                effective_projection.get("fieldSources", {})
                if isinstance(effective_projection.get("fieldSources", {}), dict)
                else {}
            ),
            "snapshotId": str(effective_projection.get("snapshotId", "") or ""),
            "generatedAt": str(effective_projection.get("generatedAt", "") or ""),
            "sourceVersions": (
                effective_projection.get("sourceVersions", {})
                if isinstance(effective_projection.get("sourceVersions", {}), dict)
                else {}
            ),
            "configuredStrategyId": str(configured_projection.get("strategyId", "") or ""),
            "configuredStrategyName": str(configured_projection.get("strategyName", "") or ""),
            "entryOwnerInstanceId": str(compact_arbitration.get("entry_owner_instance_id", "") or ""),
            "entryOwnerStrategyId": str(compact_arbitration.get("entry_owner_strategy_id", "") or ""),
            "executionOwner": (
                compact_risk_protocol.get("execution_owner", {})
                if isinstance(compact_risk_protocol.get("execution_owner", {}), dict)
                else {}
            ),
            "sessionScheduler": session_scheduler_projection,
            "runtime": (
                strategy_projection.get("runtime", {})
                if isinstance(strategy_projection.get("runtime", {}), dict)
                else {}
            ),
            "fallback": fallback_projection,
        },
        "riskProtocol": risk_protocol,
        "positionRuntime": position_runtime,
        "arbitration": arbitration,
        "decisionPolicy": decision_policy,
        "runtimeInstances": runtime_instances_enriched,
        "byStage": by_stage,
        "by_stage": by_stage,
        "strategies": strategies,
    }
    return payload


def _build_template_diff(
    market_template: dict[str, Any],
    instance_row: dict[str, Any],
) -> dict[str, Any]:
    market_default = market_template if isinstance(market_template, dict) else {}
    template_snapshot = (
        instance_row.get("template_snapshot", {})
        if isinstance(instance_row.get("template_snapshot", {}), dict)
        else {}
    )
    param_overrides = (
        instance_row.get("param_overrides", {})
        if isinstance(instance_row.get("param_overrides", {}), dict)
        else {}
    )
    keys = sorted(
        {
            str(key)
            for key in list(market_default.keys())
            + list(template_snapshot.keys())
            + list(param_overrides.keys())
            if str(key).strip()
        }
    )
    entries: list[dict[str, Any]] = []
    changed_from_market = 0
    override_count = 0
    snapshot_drift_count = 0
    for key in keys:
        market_value = market_default.get(key)
        snapshot_value = template_snapshot.get(key, market_value)
        effective_value = param_overrides.get(key, snapshot_value)
        has_override = key in param_overrides
        drift_from_market = effective_value != market_value
        snapshot_drift = snapshot_value != market_value
        if has_override:
            override_count += 1
        if snapshot_drift:
            snapshot_drift_count += 1
        if drift_from_market:
            changed_from_market += 1
        if not (has_override or drift_from_market or snapshot_drift):
            continue
        entries.append(
            {
                "key": key,
                "marketDefault": market_value,
                "instanceSnapshot": snapshot_value,
                "effectiveValue": effective_value,
                "overrideValue": param_overrides.get(key),
                "changedFromMarket": drift_from_market,
                "changedFromSnapshot": effective_value != snapshot_value,
                "hasOverride": has_override,
                "snapshotDrift": snapshot_drift,
            }
        )
    return {
        "summary": {
            "overrideCount": override_count,
            "changedKeys": changed_from_market,
            "snapshotDriftCount": snapshot_drift_count,
            "isDirty": bool(entries),
        },
        "entries": entries,
        "marketDefault": market_default,
        "instanceOverrides": param_overrides,
        "templateSnapshot": template_snapshot,
    }


def _build_bridge_runtime_snapshot(
    status_payload: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    status_payload = status_payload or get_status()
    execution_rows = _collect_execution_signal_rows(limit=24)
    active_rows = execution_rows.get("active", []) if isinstance(execution_rows.get("active", []), list) else []
    recent_rows = execution_rows.get("recent", []) if isinstance(execution_rows.get("recent", []), list) else []
    blocked_rows = [
        row for row in recent_rows if str(row.get("state", "")).upper() in {"REJECTED", "VETOED"}
    ]
    timed_out_rows = [
        row for row in recent_rows if str(row.get("state", "")).upper() == "TIMEOUT"
    ]
    retrying_rows = [
        row for row in active_rows if "retry" in str(row.get("reason", "") or "").lower()
    ]
    delayed_rows = [
        row
        for row in active_rows
        if float(time.time() - float(row.get("latestTs", 0.0) or 0.0)) >= 45.0
    ]
    pending_exit_rows = [
        row
        for row in active_rows
        if str(row.get("action", "")).upper() in {"CLOSE", "REDUCE"}
    ]

    terminal_status_file = None
    heartbeat_age_sec = None
    to_ea_queue_depth = 0
    from_ea_queue_depth = 0
    try:
        mt5_dir = config_manager.get_mt5_dir()
        if mt5_dir:
            terminal_status_file = _resolve_terminal_status_file(mt5_dir)
            if terminal_status_file and terminal_status_file.exists():
                heartbeat_age_sec = round(time.time() - terminal_status_file.stat().st_mtime, 1)
    except Exception:
        terminal_status_file = None

    request_file = DATA_DIR / "inbox" / "request.json"
    report_file = DATA_DIR / "inbox" / "execution_report.json"
    if request_file.exists():
        to_ea_queue_depth += 1
    if report_file.exists():
        from_ea_queue_depth += 1

    ledger_path = _resolve_existing_file(_runtime_log_candidates("trading_ledger.ndjson"))
    request_summary = {
        "acked": 0,
        "filled": 0,
        "rejected": 0,
        "timeout": 0,
        "pending": len(active_rows),
    }
    recent_requests: list[dict[str, Any]] = []
    if ledger_path is not None:
        for payload in _read_ndjson_tail(ledger_path, limit=10):
            status_text = str(payload.get("status", "") or "").upper()
            if status_text == "ACKED":
                request_summary["acked"] += 1
            elif status_text == "FILLED":
                request_summary["filled"] += 1
            elif status_text in {"REJECTED", "RETRY_EXHAUSTED", "INIT_FAILED"}:
                request_summary["rejected"] += 1
            elif status_text == "TIMEOUT":
                request_summary["timeout"] += 1
            recent_requests.append(
                {
                    "ts": str(payload.get("ts", "") or ""),
                    "instanceId": str(payload.get("instance_id", "") or ""),
                    "strategyId": str(payload.get("strategy", payload.get("strategy_id", "")) or ""),
                    "action": str(payload.get("action", "") or "").upper(),
                    "side": str(payload.get("side", "") or "").upper(),
                    "status": status_text,
                    "ticket": str(payload.get("ticket", "") or ""),
                    "volume": float(payload.get("volume", 0.0) or 0.0),
                    "price": payload.get("price"),
                    "reason": str(payload.get("reason", payload.get("message", "")) or ""),
                }
            )

    focus_rows = (
        [{"kind": "blocked", "count": len(blocked_rows), "items": blocked_rows[:3]}]
        + [{"kind": "delayed", "count": len(delayed_rows), "items": delayed_rows[:3]}]
        + [{"kind": "retrying", "count": len(retrying_rows), "items": retrying_rows[:3]}]
        + [{"kind": "timed_out", "count": len(timed_out_rows), "items": timed_out_rows[:3]}]
        + [{"kind": "pending_exit", "count": len(pending_exit_rows), "items": pending_exit_rows[:3]}]
    )
    focus_rows = [item for item in focus_rows if int(item.get("count", 0) or 0) > 0]

    return {
        "focus": focus_rows,
        "queue": {
            "toEaDepth": to_ea_queue_depth,
            "fromEaDepth": from_ea_queue_depth,
            "activeSignals": len(active_rows),
            "pendingExit": len(pending_exit_rows),
        },
        "health": {
            "bridgeOk": bool(status_payload.get("bridgeOk", False)),
            "mt5Online": bool(status_payload.get("mt5Online", False)),
            "heartbeatAgeSec": heartbeat_age_sec,
            "terminalStatusFile": str(terminal_status_file) if terminal_status_file else "",
            "requestSummary": request_summary,
        },
        "requests": recent_requests,
    }


def _health_status_rank(status: str) -> int:
    normalized = str(status or "ok").strip().lower()
    if normalized == "danger":
        return 3
    if normalized == "warn":
        return 2
    return 1


def _build_runtime_health_center_payload(
    *,
    status_payload: dict[str, Any],
    runtime_observability: dict[str, Any],
    marketplace_payload: dict[str, Any],
    bridge_runtime: dict[str, Any],
    runtime_instances: list[dict[str, Any]],
) -> dict[str, Any]:
    build_info = _build_runtime_build_info_payload()
    api_key_state = get_api_key_status()
    gateway = (
        runtime_observability.get("gateway", {})
        if isinstance(runtime_observability.get("gateway", {}), dict)
        else {}
    )
    strategies = (
        marketplace_payload.get("strategies", [])
        if isinstance(marketplace_payload.get("strategies", []), list)
        else []
    )
    pending_review_count = sum(
        1
        for item in strategies
        if isinstance(item, dict)
        and str(item.get("_lifecycle_stage", "") or "") == "marketplace"
        and str(item.get("review_status", "pending") or "pending").lower() != "approved"
    )
    daemon_running = bool(status_payload.get("isRunning", False))
    mt5_online = bool(status_payload.get("mt5Online", False))
    bridge_ok = bool(status_payload.get("bridgeOk", False))
    heartbeat_age = bridge_runtime.get("health", {}).get("heartbeatAgeSec") if isinstance(bridge_runtime.get("health", {}), dict) else None
    deploy_status = "ok"
    deploy_detail = "后台、MT5 和执行桥都在线，可继续推进运行链路。"
    if not daemon_running:
        deploy_status = "danger"
        deploy_detail = "后台主链路未运行，当前无法完成部署或执行。"
    elif not mt5_online or not bridge_ok:
        deploy_status = "warn"
        deploy_detail = "后台已运行，但 MT5 或执行桥未完全就绪，部署链路需要先恢复。"

    ws_status = "ok" if _WS_AVAILABLE and daemon_running else ("warn" if daemon_running else "danger")
    ws_detail = "实时双向沟通已就绪。" if ws_status == "ok" else (
        "后台在线，但实时通信能力未完全就绪，可先使用 HTTP 回退。" if ws_status == "warn" else "后台未运行，实时沟通不可用。"
    )
    ai_status = "ok" if bool(api_key_state.get("configured", False)) else "warn"
    ai_detail = "已检测到可用 AI Provider Key。" if ai_status == "ok" else "尚未配置 AI Provider Key，AI 策略开发师会降级。"
    marketplace_status = "ok" if strategies else "warn"
    marketplace_detail = (
        f"策略集市已加载，共 {len(strategies)} 个策略，待处理审核 {pending_review_count} 个。"
        if strategies
        else "当前未读取到策略集市资产，请检查策略状态文件。"
    )
    unhealthy_instance_count = 0
    for item in runtime_instances:
        if not isinstance(item, dict):
            continue
        health = item.get("health", {}) if isinstance(item.get("health", {}), dict) else {}
        risk = item.get("risk", {}) if isinstance(item.get("risk", {}), dict) else {}
        score = float(health.get("runtimeScore", health.get("score", 0.0)) or 0.0)
        if score < 60 or str(risk.get("status", "") or "").lower() in {"blocked", "danger"}:
            unhealthy_instance_count += 1
    runtime_status = "ok"
    runtime_detail = f"当前运行实例 {len(runtime_instances)} 个，异常实例 {unhealthy_instance_count} 个。"
    if not runtime_instances:
        runtime_status = "warn"
        runtime_detail = "当前没有运行实例，可先从策略集市部署后再观察。"
    elif unhealthy_instance_count > 0:
        runtime_status = "warn"
    version_info = build_info.get("frontend", {}) if isinstance(build_info.get("frontend", {}), dict) else {}
    entry_asset = str(version_info.get("entryAsset", "") or "")
    backend_info = build_info.get("backend", {}) if isinstance(build_info.get("backend", {}), dict) else {}
    version_status = "ok" if entry_asset and str(backend_info.get("appVersion", "") or "").strip() else "warn"
    version_detail = (
        f"前端包 {entry_asset}，后端版本 {backend_info.get('appVersion', '')}。"
        if version_status == "ok"
        else "构建信息不完整，可能存在运行态与页面不一致风险。"
    )
    checks = [
        {
            "code": "realtime_comm",
            "title": "实时沟通",
            "status": ws_status,
            "detail": ws_detail,
            "action": "若异常，先检查后台是否在线，再看 WebSocket 是否可用。",
        },
        {
            "code": "ai_service",
            "title": "AI 服务",
            "status": ai_status,
            "detail": ai_detail,
            "action": "未配置时先补 API Key，再重试互动开发。",
        },
        {
            "code": "marketplace",
            "title": "策略集市",
            "status": marketplace_status,
            "detail": marketplace_detail,
            "action": "如果资产为空，先检查策略状态文件和集市接口。",
        },
        {
            "code": "deploy_chain",
            "title": "部署链路",
            "status": deploy_status,
            "detail": deploy_detail,
            "action": "先恢复后台、MT5 和执行桥，再继续晋级或部署。",
        },
        {
            "code": "runtime_instances",
            "title": "运行实例",
            "status": runtime_status,
            "detail": runtime_detail,
            "action": "异常实例较多时，优先处理风险阻断和实例健康分。",
        },
        {
            "code": "version_consistency",
            "title": "版本一致性",
            "status": version_status,
            "detail": version_detail,
            "action": "如果构建信息缺失，先重建前端并重新打包。",
        },
    ]
    overall_status = "ok"
    for item in checks:
        if _health_status_rank(str(item.get("status", "ok") or "ok")) > _health_status_rank(overall_status):
            overall_status = str(item.get("status", "ok") or "ok")
    return {
        "overallStatus": overall_status,
        "headline": (
            "系统健康正常"
            if overall_status == "ok"
            else ("系统存在待处理风险" if overall_status == "warn" else "系统当前存在阻断")
        ),
        "checkedAt": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
        "bridgeHeartbeatAgeSec": heartbeat_age,
        "checks": checks,
        "summary": {
            "strategyCount": len(strategies),
            "pendingReviewCount": pending_review_count,
            "runningInstanceCount": len(runtime_instances),
            "unhealthyInstanceCount": unhealthy_instance_count,
            "daemonRunning": daemon_running,
            "mt5Online": mt5_online,
            "bridgeOk": bridge_ok,
            "decisionContractHealth": str(gateway.get("decisionContract", {}).get("healthStatus", "") or "")
            if isinstance(gateway.get("decisionContract", {}), dict)
            else "",
        },
    }


def _build_agent_diagnostics(
    runtime_instances: list[dict[str, Any]],
    marketplace_payload: dict[str, Any],
    arbitration_payload: dict[str, Any],
    runtime_observability: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    decision_policy = (
        marketplace_payload.get("decisionPolicy", {})
        if isinstance(marketplace_payload.get("decisionPolicy", {}), dict)
        else {}
    )
    current = marketplace_payload.get("current", {}) if isinstance(marketplace_payload.get("current", {}), dict) else {}
    runtime_gateway = (
        runtime_observability.get("gateway", {})
        if isinstance(runtime_observability, dict) and isinstance(runtime_observability.get("gateway", {}), dict)
        else {}
    )
    decision_contract = (
        runtime_gateway.get("decisionContract", {})
        if isinstance(runtime_gateway.get("decisionContract", {}), dict)
        else {}
    )
    lineage = (
        decision_contract.get("lineage", {})
        if isinstance(decision_contract.get("lineage", {}), dict)
        else {}
    )
    contributors = (
        arbitration_payload.get("contributors", [])
        if isinstance(arbitration_payload.get("contributors", []), list)
        else []
    )
    suppressions = [
        {
            "instanceId": str(item.get("instanceId", "") or ""),
            "strategyId": str(item.get("strategyId", "") or ""),
            "reason": str(item.get("suppressionReason", "") or "suppressed"),
            "rawReason": str(item.get("rawReason", "") or ""),
        }
        for item in contributors
        if isinstance(item, dict) and bool(item.get("wasSuppressed", False))
    ]

    profile_warnings: list[dict[str, Any]] = []
    if not current.get("activeInstanceIds"):
        profile_warnings.append(
            {
                "kind": "scheduler",
                "message": "当前没有运行实例，调度层无法产出实盘候选",
            }
        )
    if str(decision_policy.get("profile", "") or "").strip() != "agent_guided_multi_strategy":
        profile_warnings.append(
            {
                "kind": "policy_profile",
                "message": f"当前 profile 为 `{decision_policy.get('profile', 'unknown')}`，未使用多 agent 多策略统一协议",
            }
        )
    for key in ("macro", "interpreter", "decision_policy"):
        if not str(lineage.get(key, "") or "").strip():
            profile_warnings.append(
                {
                    "kind": "contract_lineage",
                    "message": f"统一合同缺少 `{key}` 来源，可能仍有旧链路回退",
                }
            )

    override_warnings: list[dict[str, Any]] = []
    for item in runtime_instances:
        if not isinstance(item, dict):
            continue
        diff = item.get("templateDiff", {}) if isinstance(item.get("templateDiff", {}), dict) else {}
        summary = diff.get("summary", {}) if isinstance(diff.get("summary", {}), dict) else {}
        if int(summary.get("overrideCount", 0) or 0) <= 0 and int(summary.get("changedKeys", 0) or 0) <= 0:
            continue
        override_warnings.append(
            {
                "instanceId": str(item.get("instanceId", "") or ""),
                "strategyId": str(item.get("strategyId", "") or ""),
                "overrideCount": int(summary.get("overrideCount", 0) or 0),
                "changedKeys": int(summary.get("changedKeys", 0) or 0),
                "message": "实例已偏离市场默认模板，需确认是否保留覆写",
            }
        )

    return {
        "summary": {
            "suppressionCount": len(suppressions),
            "profileWarningCount": len(profile_warnings),
            "overrideWarningCount": len(override_warnings),
        },
        "suppressions": suppressions,
        "profileWarnings": profile_warnings,
        "overrideWarnings": override_warnings,
    }


def _load_param_suggestion_payload() -> dict[str, Any]:
    candidates = [
        CONFIG_DIR / "params_suggestion.json",
        LOGS_DIR / "params_suggestion.json",
    ]
    for path in candidates:
        try:
            if path.exists():
                payload = json.loads(path.read_text(encoding="utf-8"))
                if isinstance(payload, dict):
                    return payload
        except Exception:
            continue
    return {}


def _read_all_paper_trades_by_strategy() -> dict[str, list[dict[str, Any]]]:
    candidates = [
        LOGS_DIR / "paper_trading_ledger.ndjson",
        DATA_DIR / "bridge" / "paper_trading_ledger.ndjson",
    ]
    ledger_path = next((path for path in candidates if path.exists()), candidates[0])
    grouped: dict[str, list[dict[str, Any]]] = {}
    if not ledger_path.exists():
        return grouped
    try:
        with ledger_path.open("r", encoding="utf-8") as handle:
            for raw in handle:
                line = raw.strip()
                if not line:
                    continue
                try:
                    payload = json.loads(line)
                except Exception:
                    continue
                if not isinstance(payload, dict):
                    continue
                strategy_id = str(payload.get("strategy_id", "") or "").strip()
                if not strategy_id:
                    continue
                grouped.setdefault(strategy_id, []).append(payload)
    except Exception:
        return {}
    return grouped


def _read_paper_trades_for_strategy(strategy_id: str) -> list[dict[str, Any]]:
    return list(_read_all_paper_trades_by_strategy().get(strategy_id, []))


def _calc_profit_factor_from_trades(trades: list[dict[str, Any]]) -> float:
    wins = [
        float(item.get("pnl", 0.0) or 0.0)
        for item in trades
        if float(item.get("pnl", 0.0) or 0.0) > 0
    ]
    losses = [
        float(item.get("pnl", 0.0) or 0.0)
        for item in trades
        if float(item.get("pnl", 0.0) or 0.0) < 0
    ]
    gross_profit = sum(wins)
    gross_loss = abs(sum(losses))
    if gross_loss > 0:
        return round(gross_profit / gross_loss, 2)
    if gross_profit > 0:
        return round(gross_profit, 2)
    return 0.0


def _build_regime_stats_for_trades(trades: list[dict[str, Any]]) -> list[dict[str, Any]]:
    grouped: dict[str, list[dict[str, Any]]] = {}
    for item in trades:
        if not isinstance(item, dict):
            continue
        regime = normalize_regime_label(
            item.get("regime")
            or item.get("selection_regime")
            or item.get("market_structure")
            or item.get("regime_label")
            or "UNKNOWN"
        )
        grouped.setdefault(regime, []).append(item)
    rows: list[dict[str, Any]] = []
    for regime, regime_trades in grouped.items():
        total = len(regime_trades)
        wins = sum(
            1
            for item in regime_trades
            if float(item.get("pnl", 0.0) or 0.0) > 0
        )
        total_pnl = round(
            sum(float(item.get("pnl", 0.0) or 0.0) for item in regime_trades),
            2,
        )
        profit_factor = _calc_profit_factor_from_trades(regime_trades)
        fitness = "neutral"
        if total >= 4 and total_pnl > 0 and profit_factor >= 1.2:
            fitness = "strong"
        elif total >= 4 and total_pnl < 0 and profit_factor < 0.9:
            fitness = "weak"
        rows.append(
            {
                "regime": regime,
                "trades": total,
                "winRate": round(wins / max(total, 1) * 100.0, 1),
                "profitFactor": profit_factor,
                "totalPnl": total_pnl,
                "fitness": fitness,
            }
        )
    rows.sort(
        key=lambda item: (
            -int(item.get("trades", 0) or 0),
            -float(item.get("totalPnl", 0.0) or 0.0),
            str(item.get("regime", "")),
        )
    )
    return rows


def _build_strategy_survival_state(
    strategy_id: str,
    row: dict[str, Any],
    trades: list[dict[str, Any]],
    performance_artifact: dict[str, Any] | None = None,
) -> dict[str, Any]:
    performance_payload = performance_artifact if isinstance(performance_artifact, dict) else {}
    strategy_bucket = (
        performance_payload.get("strategy", {}).get(str(strategy_id or "").strip().lower(), {})
        if isinstance(performance_payload.get("strategy", {}), dict)
        else {}
    )
    total_trades = len(trades)
    wins = [
        item for item in trades if float(item.get("pnl", 0.0) or 0.0) > 0
    ]
    gross_profit = sum(float(item.get("pnl", 0.0) or 0.0) for item in wins)
    gross_loss = abs(
        sum(
            float(item.get("pnl", 0.0) or 0.0)
            for item in trades
            if float(item.get("pnl", 0.0) or 0.0) < 0
        )
    )
    total_pnl = round(sum(float(item.get("pnl", 0.0) or 0.0) for item in trades), 2)
    win_rate = round((len(wins) / total_trades) * 100.0, 1) if total_trades else 0.0
    profit_factor = round(gross_profit / gross_loss, 2) if gross_loss > 0 else (round(gross_profit, 2) if gross_profit > 0 else 0.0)
    equity = 0.0
    peak_equity = 0.0
    max_drawdown_pct = 0.0
    max_loss_streak = 0
    current_loss_streak = 0
    for item in trades:
        pnl = float(item.get("pnl", 0.0) or 0.0)
        equity += pnl
        peak_equity = max(peak_equity, equity)
        if peak_equity > 0:
            max_drawdown_pct = max(
                max_drawdown_pct,
                max(0.0, (peak_equity - equity) / peak_equity * 100.0),
            )
        if pnl < 0:
            current_loss_streak += 1
            max_loss_streak = max(max_loss_streak, current_loss_streak)
        else:
            current_loss_streak = 0
    score = 55.0
    reasons: list[str] = []
    status = "keep"
    candidate_pool_action = "keep"
    lifecycle_action = "keep_in_paper"
    recommended_action = "继续保留在候选池内，滚动观察后再决定是否晋级。"
    lifecycle_stage = str(row.get("lifecycle_stage", "") or "").strip().lower()
    if lifecycle_stage != "paper" and total_trades == 0:
        score = 35.0
        status = "not_in_paper"
        lifecycle_action = "enter_paper_first"
        recommended_action = "先进入 Paper 并收集真实样本，再谈优胜劣汰。"
        reasons.append("尚未进入 Paper 或没有真实运行样本")
    elif total_trades < 5:
        score = 42.0 + min(10.0, total_trades * 2.0)
        status = "warmup"
        candidate_pool_action = "observe"
        lifecycle_action = "collect_samples"
        recommended_action = "样本还少，先继续收集样本，不急着淘汰或晋级。"
        reasons.append(f"Paper 样本仍少于 5 笔，当前仅 {total_trades} 笔")
    else:
        if total_trades >= 15:
            score += 8.0
        elif total_trades >= 8:
            score += 4.0
        if win_rate >= 60.0:
            score += 10.0
        elif win_rate >= 50.0:
            score += 4.0
        elif win_rate < 45.0:
            score -= 8.0
            reasons.append(f"胜率偏低:{win_rate:.1f}%")
        if profit_factor >= 1.35:
            score += 12.0
        elif profit_factor >= 1.15:
            score += 6.0
        elif profit_factor < 1.0:
            score -= 12.0
            reasons.append(f"盈亏比偏弱:{profit_factor:.2f}")
        if total_pnl > 0:
            score += 6.0
        elif total_pnl < 0:
            score -= 10.0
            reasons.append(f"累计收益为负:{total_pnl:.2f}")
        if max_drawdown_pct > 30.0:
            score -= 18.0
            reasons.append(f"最大回撤过大:{max_drawdown_pct:.1f}%")
        elif max_drawdown_pct > 20.0:
            score -= 8.0
        elif 0 < max_drawdown_pct <= 12.0:
            score += 4.0
        if current_loss_streak >= 5:
            score -= 18.0
            reasons.append(f"近期连续亏损达到 {current_loss_streak} 笔")
        elif current_loss_streak >= 3:
            score -= 10.0
            reasons.append(f"近期连续亏损达到 {current_loss_streak} 笔")
    bucket_decision = str(strategy_bucket.get("decision", "neutral") or "neutral").strip().lower()
    bucket_multiplier = round(float(strategy_bucket.get("live_multiplier", 1.0) or 1.0), 4)
    bucket_bias = round(float(strategy_bucket.get("confidence_bias", 0.0) or 0.0), 4)
    if bucket_decision == "block":
        score -= 22.0
        reasons.append(f"真实成交表现已触发阻断:{strategy_bucket.get('reason', '')}")
    elif bucket_decision == "downweight":
        score -= 10.0
        reasons.append(f"真实成交表现建议降权:{strategy_bucket.get('reason', '')}")
    elif bucket_decision == "favor":
        score += 8.0
        reasons.append("真实成交表现稳定，可适度保留优先级")
    score = round(max(0.0, min(100.0, score)), 1)
    worth_promoting = False
    if status not in {"not_in_paper", "warmup"}:
        if bucket_decision == "block" or score < 28.0:
            status = "pause"
            candidate_pool_action = "pause"
            lifecycle_action = "pause_and_optimize"
            recommended_action = "先暂停进入候选池，修正参数和风控后再恢复 Paper。"
        elif bucket_decision == "downweight" or score < 55.0:
            status = "downweight"
            candidate_pool_action = "downweight"
            lifecycle_action = "keep_in_paper"
            recommended_action = "继续留在 Paper，但先降权和缩量，再观察是否恢复。"
        elif (
            total_trades >= 15
            and score >= 78.0
            and win_rate >= 55.0
            and profit_factor >= 1.2
            and max_drawdown_pct <= 20.0
        ):
            status = "promote_candidate"
            candidate_pool_action = "keep"
            lifecycle_action = "prepare_review"
            recommended_action = "样本和稳定性达标，可准备复审或推进更高等级验证。"
            worth_promoting = True
        else:
            status = "keep"
            candidate_pool_action = "keep"
            lifecycle_action = "keep_in_paper"
            recommended_action = "继续留在候选池滚动比较，观察是否进一步拉开优势。"
    regime_stats = _build_regime_stats_for_trades(trades)
    best_regime = ""
    for item in regime_stats:
        if str(item.get("fitness", "neutral") or "neutral") == "strong":
            best_regime = str(item.get("regime", "") or "")
            break
    if not best_regime and regime_stats:
        best_regime = str(regime_stats[0].get("regime", "") or "")
    return {
        "status": status,
        "score": score,
        "sampleSize": total_trades,
        "candidatePoolAction": candidate_pool_action,
        "lifecycleAction": lifecycle_action,
        "recommendedAction": recommended_action,
        "reason": "；".join(item for item in reasons if str(item).strip()) or "样本表现暂时稳定",
        "liveMultiplier": bucket_multiplier,
        "confidenceBias": bucket_bias,
        "worthPromoting": worth_promoting,
        "shouldPause": candidate_pool_action == "pause",
        "shouldDownweight": candidate_pool_action == "downweight",
        "bestRegime": best_regime,
        "regimeStats": regime_stats[:4],
        "profitFactor": profit_factor,
        "winRate": win_rate,
        "totalPnl": total_pnl,
        "maxDrawdownPct": round(max_drawdown_pct, 2),
        "currentLossStreak": current_loss_streak,
        "maxLossStreak": max_loss_streak,
        "updatedAt": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
    }


def _build_strategy_survival_snapshot_map(
    state: dict[str, Any],
    *,
    trades_by_strategy: dict[str, list[dict[str, Any]]] | None = None,
    performance_artifact: dict[str, Any] | None = None,
) -> dict[str, dict[str, Any]]:
    payload = state if isinstance(state, dict) else {}
    strategies = (
        payload.get("strategies", {})
        if isinstance(payload.get("strategies", {}), dict)
        else {}
    )
    trade_map = (
        trades_by_strategy
        if isinstance(trades_by_strategy, dict)
        else _read_all_paper_trades_by_strategy()
    )
    artifact = performance_artifact if isinstance(performance_artifact, dict) else {}
    if not artifact:
        try:
            from src.live_performance_factors import load_live_performance_factors

            artifact = load_live_performance_factors(force_refresh=False)
        except Exception:
            artifact = {}
    snapshots: dict[str, dict[str, Any]] = {}
    for item_strategy_id, row in strategies.items():
        if not isinstance(row, dict):
            continue
        snapshots[str(item_strategy_id)] = _build_strategy_survival_state(
            str(item_strategy_id),
            row,
            trade_map.get(str(item_strategy_id), []),
            artifact,
        )
    return snapshots


def _build_strategy_paper_feedback(strategy_id: str) -> dict[str, Any]:
    state = _load_strategy_market_state()
    strategies = (
        state.get("strategies", {})
        if isinstance(state.get("strategies", {}), dict)
        else {}
    )
    row = strategies.get(strategy_id, {})
    if not isinstance(row, dict) or not row:
        raise KeyError(f"未找到策略: {strategy_id}")
    trades = _read_paper_trades_for_strategy(strategy_id)
    total_trades = len(trades)
    wins = [item for item in trades if float(item.get("pnl", 0.0) or 0.0) > 0]
    losses = [item for item in trades if float(item.get("pnl", 0.0) or 0.0) < 0]
    gross_profit = sum(float(item.get("pnl", 0.0) or 0.0) for item in wins)
    gross_loss = abs(sum(float(item.get("pnl", 0.0) or 0.0) for item in losses))
    total_pnl = round(sum(float(item.get("pnl", 0.0) or 0.0) for item in trades), 2)
    win_rate = round((len(wins) / total_trades * 100.0), 1) if total_trades else 0.0
    profit_factor = round(gross_profit / gross_loss, 2) if gross_loss > 0 else (round(gross_profit, 2) if gross_profit > 0 else 0.0)
    equity = 0.0
    peak_equity = 0.0
    max_drawdown_pct = 0.0
    avg_confidence = 0.0
    if trades:
        avg_confidence = round(
            sum(float(item.get("confidence", 0.0) or 0.0) for item in trades) / total_trades,
            4,
        )
    for item in trades:
        equity += float(item.get("pnl", 0.0) or 0.0)
        peak_equity = max(peak_equity, equity)
        if peak_equity > 0:
            max_drawdown_pct = max(
                max_drawdown_pct,
                max(0.0, (peak_equity - equity) / peak_equity * 100.0),
            )
    survival = _build_strategy_survival_snapshot_map(
        state,
        trades_by_strategy={strategy_id: trades},
    ).get(strategy_id, {})
    suggestion_payload = _load_param_suggestion_payload()
    suggestion_params = {}
    if isinstance(suggestion_payload.get("params", {}), dict):
        suggestion_params = suggestion_payload.get("params", {})
    elif isinstance(suggestion_payload.get("suggestion", {}), dict):
        nested = suggestion_payload.get("suggestion", {})
        suggestion_params = nested.get("params", {}) if isinstance(nested.get("params", {}), dict) else {}
    blockers: list[str] = []
    recommendations: list[str] = []
    if str(row.get("lifecycle_stage", "") or "").strip().lower() != "paper":
        blockers.append("该策略当前不在 Paper 阶段，暂时没有可回流的运行样本。")
    if total_trades < 15:
        blockers.append(f"Paper 样本不足，当前仅 {total_trades} 笔，建议继续积累到 15 笔以上。")
    if total_trades > 0 and win_rate < 40.0:
        blockers.append(f"胜率仅 {win_rate:.1f}%，暂不建议直接复审。")
    if total_trades > 0 and gross_loss > 0 and profit_factor < 1.2:
        blockers.append(f"盈亏比仅 {profit_factor:.2f}，需要先优化出场或过滤条件。")
    if max_drawdown_pct > 30.0:
        blockers.append(f"最大回撤达到 {max_drawdown_pct:.1f}%，建议先收紧风险。")
    if total_trades == 0:
        recommendations.append("先让策略继续跑 Paper，收集至少一轮真实样本后再优化。")
    elif win_rate < 45.0:
        recommendations.append("优先收紧入场过滤条件，减少弱信号和假突破。")
    if profit_factor and profit_factor < 1.2:
        recommendations.append("优先优化出场规则，避免止盈过短或止损过宽。")
    if max_drawdown_pct > 15.0:
        recommendations.append("优先下调单笔风险和最大持仓数量，把回撤先压住。")
    if suggestion_params:
        params_preview = "，".join(
            f"{key}={value}"
            for key, value in list(suggestion_params.items())[:4]
        )
        recommendations.append(f"已识别参数建议，可重点复核：{params_preview}")
    if str(survival.get("candidatePoolAction", "") or "") == "downweight":
        recommendations.append("该策略最近处于降权观察期，建议继续缩量并对比同组策略。")
    elif str(survival.get("candidatePoolAction", "") or "") == "pause":
        recommendations.append("该策略已达到暂停候选池的条件，建议先回到参数优化或风控修正。")
    if not recommendations:
        recommendations.append("当前表现基本稳定，可保留参数并继续观察，再决定是否提交复审。")
    next_action = "继续跑 Paper 收集样本"
    if bool(survival.get("worthPromoting", False)) and not blockers:
        next_action = str(
            survival.get("recommendedAction", "")
            or "可带着当前结果重新提交审核或继续准备部署"
        )
    elif total_trades >= 15 and not blockers:
        next_action = "可带着当前结果重新提交审核或继续准备部署"
    elif suggestion_params:
        next_action = "先按运行反馈优化参数，再继续 Paper 验证"
    elif str(survival.get("recommendedAction", "") or "").strip():
        next_action = str(survival.get("recommendedAction", "") or "")
    return {
        "strategy_id": strategy_id,
        "name": str(row.get("display_name", strategy_id) or strategy_id),
        "lifecycle_stage": str(row.get("lifecycle_stage", "") or ""),
        "stats": {
            "totalTrades": total_trades,
            "winRate": win_rate,
            "profitFactor": profit_factor,
            "totalPnl": total_pnl,
            "maxDrawdownPct": round(max_drawdown_pct, 2),
            "avgConfidence": avg_confidence,
        },
        "blockers": blockers,
        "recommendations": recommendations,
        "paramsSuggestion": suggestion_params,
        "survival": survival,
        "next_action": next_action,
        "worth_resubmitting": bool(survival.get("worthPromoting", False)) and not blockers,
    }


def _build_instance_runtime_view(
    instance_row: dict[str, Any],
    market_template: dict[str, Any],
    risk_protocol: dict[str, Any],
    arbitration_payload: dict[str, Any],
    decision_policy: dict[str, Any],
    bridge_requests: list[dict[str, Any]],
) -> dict[str, Any]:
    instance_id = str(instance_row.get("instance_id", "") or "")
    strategy_id = str(instance_row.get("strategy_id", "") or "")
    contributor_map = {
        str(item.get("instanceId", "") or ""): item
        for item in (
            arbitration_payload.get("contributors", [])
            if isinstance(arbitration_payload.get("contributors", []), list)
            else []
        )
        if isinstance(item, dict)
    }
    contributor = contributor_map.get(instance_id, {})
    position_state = (
        instance_row.get("positionState", {})
        if isinstance(instance_row.get("positionState", {}), dict)
        else {}
    )
    orders = instance_row.get("orders", {}) if isinstance(instance_row.get("orders", {}), dict) else {}
    health = instance_row.get("health", {}) if isinstance(instance_row.get("health", {}), dict) else {}
    template_diff = _build_template_diff(market_template, instance_row)
    instance_risk_state = (
        risk_protocol.get("instance_risk_state", {})
        if isinstance(risk_protocol.get("instance_risk_state", {}), dict)
        else {}
    )
    instance_health_score = (
        risk_protocol.get("instance_health_score", {})
        if isinstance(risk_protocol.get("instance_health_score", {}), dict)
        else {}
    )
    per_instance_health_details = (
        risk_protocol.get("per_instance_health_details", {})
        if isinstance(risk_protocol.get("per_instance_health_details", {}), dict)
        else {}
    )

    def _get_health_detail(key: str, default: Any = None) -> Any:
        inst_detail = per_instance_health_details.get(instance_id, {}) if isinstance(per_instance_health_details.get(instance_id, {}), dict) else {}
        return inst_detail.get(key, default)
    selected_instance_id = str(
        arbitration_payload.get("summary", {}).get("selectedInstanceId", "")
        if isinstance(arbitration_payload.get("summary", {}), dict)
        else ""
    )
    blocked_reason = (
        str(contributor.get("rawRejectReason", "") or "")
        or str(contributor.get("suppressionReason", "") or "")
    )
    timeline = [
        {"label": "deployed", "value": str(instance_row.get("deployed_at", "") or "")},
        {"label": "updated", "value": str(instance_row.get("updated_at", "") or "")},
        {"label": "latest_fill", "value": str(orders.get("latestFilledAt", "") or "")},
        {"label": "latest_status", "value": str(orders.get("latestStatus", "") or "")},
    ]
    timeline = [item for item in timeline if str(item.get("value", "") or "").strip()]
    instance_requests = [
        item
        for item in bridge_requests
        if isinstance(item, dict)
        and (
            str(item.get("instanceId", "") or "") == instance_id
            or str(item.get("strategyId", "") or "") == strategy_id
        )
    ][:4]
    return {
        "instanceId": instance_id,
        "strategyId": strategy_id,
        "strategyName": str(instance_row.get("strategy_name", strategy_id) or strategy_id),
        "strategyVersion": str(instance_row.get("strategy_version", "legacy") or "legacy"),
        "role": str(instance_row.get("role", "live_support") or "live_support"),
        "allocationPct": round(max(0.0, float(instance_row.get("allocation", 0.0) or 0.0)) * 100.0, 2),
        "active": str(instance_row.get("schedulerState", "running") or "running") == "running",
        "selected": instance_id == selected_instance_id,
        "scheduler": {
            "state": str(instance_row.get("schedulerState", "running") or "running"),
            "weight": round(float(instance_row.get("instanceWeight", 0.0) or 0.0), 4),
        },
        "health": {
            "status": str(health.get("status", "warn") or "warn"),
            "score": round(float(health.get("score", 0.0) or 0.0), 2),
            "runtimeScore": round(float(instance_health_score.get(instance_id, 0.0) or 0.0), 2),
            "grade": str(_get_health_detail("grade", "") or ""),
            "factorScores": {
                "winRate": round(float(_get_health_detail("win_rate_score", 0.0) or 0.0), 1),
                "profitFactor": round(float(_get_health_detail("profit_factor_score", 0.0) or 0.0), 1),
                "consistency": round(float(_get_health_detail("consistency_score", 0.0) or 0.0), 1),
                "regimeFit": round(float(_get_health_detail("regime_fit_score", 0.0) or 0.0), 1),
                "recency": round(float(_get_health_detail("recency_score", 0.0) or 0.0), 1),
            },
            "primaryLossPattern": str(_get_health_detail("primary_loss_pattern", "") or ""),
            "bestRegime": str(_get_health_detail("best_regime", "") or ""),
            "bestRegimeWinRate": round(float(_get_health_detail("best_regime_win_rate", 0.0) or 0.0), 4),
            "currentRegimeFitness": round(float(_get_health_detail("current_regime_fitness", 0.0) or 0.0), 4),
            "rehabilitationProgress": round(float(_get_health_detail("rehabilitation_progress", 0.0) or 0.0), 4),
            "suggestedAction": str(_get_health_detail("suggested_action", "normal") or "normal"),
            "suggestedScale": round(float(_get_health_detail("suggested_scale", 1.0) or 1.0), 4),
            "lossStreak": int(_get_health_detail("loss_streak", 0) or 0),
        },
        "risk": {
            "status": str(instance_risk_state.get(instance_id, instance_row.get("risk", {}).get("status", "normal")) or "normal"),
            "globalBlocked": bool(risk_protocol.get("risk_budget_usage", {}).get("global", {}).get("state") == "blocked")
            if isinstance(risk_protocol.get("risk_budget_usage", {}), dict)
            else False,
            "blockedReason": blocked_reason,
            "governorBlockers": risk_protocol.get("governor_blockers", [])
            if isinstance(risk_protocol.get("governor_blockers", []), list)
            else [],
        },
        "orders": orders,
        "positionState": position_state,
        "signal": {
            "action": str(contributor.get("action", "HOLD") or "HOLD"),
            "side": str(contributor.get("side", "NONE") or "NONE"),
            "confidence": float(contributor.get("confidence", 0.0) or 0.0),
            "contributionScore": float(contributor.get("signedScore", 0.0) or 0.0),
            "reason": str(contributor.get("rawReason", "") or ""),
            "wasSelected": bool(contributor.get("wasSelected", False)),
            "wasSuppressed": bool(contributor.get("wasSuppressed", False)),
            "suppressionReason": str(contributor.get("suppressionReason", "") or ""),
        },
        "agentProfile": {
            "decisionProfile": str(decision_policy.get("profile", "unknown") or "unknown"),
            "macroRole": str(
                decision_policy.get("macro", {}).get("role", "unknown")
                if isinstance(decision_policy.get("macro", {}), dict)
                else "unknown"
            ),
            "interpreterRole": str(
                decision_policy.get("interpreter", {}).get("role", "unknown")
                if isinstance(decision_policy.get("interpreter", {}), dict)
                else "unknown"
            ),
            "riskRole": str(
                decision_policy.get("risk", {}).get("role", "unknown")
                if isinstance(decision_policy.get("risk", {}), dict)
                else "unknown"
            ),
        },
        "templateDiff": template_diff,
        "bridge": {
            "activeSignals": position_state.get("activeSignals", [])
            if isinstance(position_state.get("activeSignals", []), list)
            else [],
            "recentRequests": instance_requests,
        },
        "timeline": timeline,
    }


def _build_runtime_operations_payload(
    status_payload: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    status_payload = status_payload or get_status()
    runtime_observability = get_runtime_observability()
    marketplace = _build_strategy_marketplace_payload(status_payload=status_payload)
    arbitration = _build_runtime_arbitration_payload()
    bridge_runtime = _build_bridge_runtime_snapshot(status_payload=status_payload)
    decision_policy = (
        marketplace.get("decisionPolicy", {})
        if isinstance(marketplace.get("decisionPolicy", {}), dict)
        else {}
    )
    selection_diagnostics = (
        marketplace.get("selectionDiagnostics", {})
        if isinstance(marketplace.get("selectionDiagnostics", {}), dict)
        else {}
    )
    selection_summary = (
        selection_diagnostics.get("summary", {})
        if isinstance(selection_diagnostics.get("summary", {}), dict)
        else {}
    )
    risk_protocol = (
        marketplace.get("riskProtocol", {})
        if isinstance(marketplace.get("riskProtocol", {}), dict)
        else {}
    )
    market_state = _load_strategy_market_state()
    market_templates = {
        str(strategy_id): (
            row.get("template", {})
            if isinstance(row, dict) and isinstance(row.get("template", {}), dict)
            else {}
        )
        for strategy_id, row in (
            market_state.get("strategies", {})
            if isinstance(market_state.get("strategies", {}), dict)
            else {}
        ).items()
    }
    runtime_instances = [
        _build_instance_runtime_view(
            item,
            market_templates.get(str(item.get("strategy_id", "") or ""), {}),
            risk_protocol,
            arbitration,
            decision_policy,
            bridge_runtime.get("requests", []) if isinstance(bridge_runtime.get("requests", []), list) else [],
        )
        for item in (
            marketplace.get("runtimeInstances", [])
            if isinstance(marketplace.get("runtimeInstances", []), list)
            else []
        )
        if isinstance(item, dict)
    ]
    runtime_agent_diagnostics = _build_agent_diagnostics(
        runtime_instances,
        marketplace,
        arbitration,
        runtime_observability if isinstance(runtime_observability, dict) else {},
    )
    risk_governor = _build_risk_governor_snapshot(status_payload=status_payload)
    runtime_scheduler = marketplace.get("runtimeScheduler", {}) if isinstance(marketplace.get("runtimeScheduler", {}), dict) else {}
    current = marketplace.get("current", {}) if isinstance(marketplace.get("current", {}), dict) else {}
    runtime_status = {
        "gateway": {
            "daemonRunning": bool(status_payload.get("isRunning", False)),
            "mt5Online": bool(status_payload.get("mt5Online", False)),
            "bridgeOk": bool(status_payload.get("bridgeOk", False)),
            "light": str(status_payload.get("light", "RED") or "RED"),
            "tradingSafety": (
                status_payload.get("tradingSafety", {})
                if isinstance(status_payload.get("tradingSafety", {}), dict)
                else {}
            ),
        },
        "market": {
            "bid": float(status_payload.get("bid", 0.0) or 0.0),
            "ask": float(status_payload.get("ask", 0.0) or 0.0),
            "spread": float(status_payload.get("spread", 0.0) or 0.0),
            "symbol": str(status_payload.get("marketSymbol", "XAUUSD") or "XAUUSD"),
            "quoteOrigin": str(status_payload.get("quoteOrigin", "invalid") or "invalid"),
            "truthValid": bool(status_payload.get("priceTruthValid", False)),
            "truthReason": str(status_payload.get("priceTruthReason", "") or ""),
            "regime": str(status_payload.get("regime", "UNKNOWN") or "UNKNOWN"),
        },
        "performance": {
            "todayTrades": int(status_payload.get("todayTrades", 0) or 0),
            "todayPnl": float(status_payload.get("todayPnl", 0.0) or 0.0),
            "todayWinRate": float(status_payload.get("todayWinRate", 0.0) or 0.0),
        },
        "riskGovernor": {
            "summary": risk_governor.get("summary", {}),
            "governor": risk_governor.get("governor", {}),
        },
        "strategy": {
            "selectedStrategyId": str(current.get("selectedStrategyId", "") or ""),
            "selectedInstanceId": str(current.get("selectedInstanceId", "") or ""),
            "routeFamily": str(current.get("routeFamily", "") or ""),
            "activeStrategyIds": (
                current.get("activeStrategyIds", [])
                if isinstance(current.get("activeStrategyIds", []), list)
                else []
            ),
            "sourceLayer": str(current.get("sourceLayer", "") or ""),
            "runtimeSource": str(current.get("runtimeSource", "") or ""),
            "fallbackSource": str(current.get("fallbackSource", "") or ""),
            "reason": str(current.get("reason", "") or ""),
            "selection": (
                current.get("selection", {})
                if isinstance(current.get("selection", {}), dict)
                else {}
            ),
            "marketContext": (
                current.get("marketContext", {})
                if isinstance(current.get("marketContext", {}), dict)
                else {}
            ),
            "regimeAudit": (
                current.get("regimeAudit", {})
                if isinstance(current.get("regimeAudit", {}), dict)
                else {}
            ),
            "executionDecision": (
                current.get("executionDecision", {})
                if isinstance(current.get("executionDecision", {}), dict)
                else {}
            ),
            "selectionSummary": selection_summary,
        },
    }
    active_instance_ids = (
        runtime_scheduler.get("activeInstanceIds", [])
        if isinstance(runtime_scheduler.get("activeInstanceIds", []), list)
        else []
    )
    active_strategy_ids = (
        runtime_scheduler.get("activeStrategyIds", [])
        if isinstance(runtime_scheduler.get("activeStrategyIds", []), list)
        else []
    )
    session_scheduler = (
        runtime_scheduler.get("sessionScheduler", {})
        if isinstance(runtime_scheduler.get("sessionScheduler", {}), dict)
        else {}
    )
    session_scheduler_summary = (
        session_scheduler.get("summary", {})
        if isinstance(session_scheduler.get("summary", {}), dict)
        else {}
    )
    runtime_scheduler["summary"] = {
        "activeInstanceCount": len(active_instance_ids),
        "activeStrategyCount": len(active_strategy_ids),
        "candidateStrategyCount": len(marketplace.get("strategies", []))
        if isinstance(marketplace.get("strategies", []), list)
        else 0,
        "sessionQualifiedStrategyCount": int(session_scheduler_summary.get("qualifiedStrategyCount", 0) or 0),
        "sessionBlockedStrategyCount": int(session_scheduler_summary.get("blockedStrategyCount", 0) or 0),
        "sessionCandidateStrategyCount": int(session_scheduler_summary.get("candidateStrategyCount", 0) or 0),
        "suppressedCount": int(runtime_agent_diagnostics.get("summary", {}).get("suppressionCount", 0) or 0),
        "maxConcurrency": int(
            risk_governor.get("governor", {}).get("maxOpenPositions", 0)
            if isinstance(risk_governor.get("governor", {}), dict)
            else 0
        ),
    }
    runtime_explain = {
        "arbitration": arbitration,
        "current": current,
        "decisionPolicy": decision_policy,
        "riskProtocol": risk_protocol,
        "selectionDiagnostics": (
            selection_diagnostics
        ),
    }
    market_state_updated_ts = _parse_timestamp_value(market_state.get("updated_at")) or _file_mtime_ts(
        _resolve_runtime_artifact("config", "strategy_market_state.json")
    )
    runtime_status_updated_ts = _file_mtime_ts(_resolve_runtime_artifact("logs", "strategy_runtime_status.json"))
    orchestration_snapshot = {
        "updatedAt": _utc_iso_from_ts(max(market_state_updated_ts, runtime_status_updated_ts)),
        "configRevision": str(current.get("configRevision", "") or ""),
        "strategyVersions": {
            str(item.get("id", "") or ""): str(item.get("version", "") or "")
            for item in (
                marketplace.get("strategies", [])
                if isinstance(marketplace.get("strategies", []), list)
                else []
            )
            if isinstance(item, dict)
        },
        "supportsRollback": True,
        "runtimeInstanceCount": len(runtime_instances),
    }
    health_center = _build_runtime_health_center_payload(
        status_payload=status_payload,
        runtime_observability=runtime_observability if isinstance(runtime_observability, dict) else {},
        marketplace_payload=marketplace,
        bridge_runtime=bridge_runtime,
        runtime_instances=runtime_instances,
    )
    return {
        "success": True,
        "runtimeInstances": runtime_instances,
        "runtimeScheduler": runtime_scheduler,
        "runtimeExplain": runtime_explain,
        "runtimeAgentDiagnostics": runtime_agent_diagnostics,
        "runtimeBridgeQueue": bridge_runtime.get("queue", {}),
        "runtimeBridgeRequests": bridge_runtime.get("requests", []),
        "runtimeExecutionFocus": bridge_runtime.get("focus", []),
        "runtimeBridgeHealth": bridge_runtime.get("health", {}),
        "runtimeTradingSafety": (
            status_payload.get("tradingSafety", {})
            if isinstance(status_payload.get("tradingSafety", {}), dict)
            else {}
        ),
        "runtimeStatus": runtime_status,
        "orchestrationSnapshot": orchestration_snapshot,
        "healthCenter": health_center,
    }


def _build_runtime_ai_evaluation(
    goal: str,
    *,
    status_payload: Optional[dict[str, Any]] = None,
    runtime_payload: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    status_payload = status_payload or get_status()
    runtime_payload = runtime_payload or _build_runtime_operations_payload(status_payload=status_payload)
    runtime_status = (
        runtime_payload.get("runtimeStatus", {})
        if isinstance(runtime_payload.get("runtimeStatus", {}), dict)
        else {}
    )
    gateway = runtime_status.get("gateway", {}) if isinstance(runtime_status.get("gateway", {}), dict) else {}
    market = runtime_status.get("market", {}) if isinstance(runtime_status.get("market", {}), dict) else {}
    performance = (
        runtime_status.get("performance", {})
        if isinstance(runtime_status.get("performance", {}), dict)
        else {}
    )
    risk_governor = (
        runtime_status.get("riskGovernor", {})
        if isinstance(runtime_status.get("riskGovernor", {}), dict)
        else {}
    )
    risk_summary = (
        risk_governor.get("summary", {})
        if isinstance(risk_governor.get("summary", {}), dict)
        else {}
    )
    risk_state = (
        risk_governor.get("governor", {})
        if isinstance(risk_governor.get("governor", {}), dict)
        else {}
    )
    runtime_scheduler = (
        runtime_payload.get("runtimeScheduler", {})
        if isinstance(runtime_payload.get("runtimeScheduler", {}), dict)
        else {}
    )
    scheduler_summary = (
        runtime_scheduler.get("summary", {})
        if isinstance(runtime_scheduler.get("summary", {}), dict)
        else {}
    )
    runtime_explain = (
        runtime_payload.get("runtimeExplain", {})
        if isinstance(runtime_payload.get("runtimeExplain", {}), dict)
        else {}
    )
    arbitration = (
        runtime_explain.get("arbitration", {})
        if isinstance(runtime_explain.get("arbitration", {}), dict)
        else {}
    )
    arbitration_summary = (
        arbitration.get("summary", {})
        if isinstance(arbitration.get("summary", {}), dict)
        else {}
    )
    runtime_instances = (
        runtime_payload.get("runtimeInstances", [])
        if isinstance(runtime_payload.get("runtimeInstances", []), list)
        else []
    )
    bridge_health = (
        runtime_payload.get("runtimeBridgeHealth", {})
        if isinstance(runtime_payload.get("runtimeBridgeHealth", {}), dict)
        else {}
    )
    execution_focus = (
        runtime_payload.get("runtimeExecutionFocus", [])
        if isinstance(runtime_payload.get("runtimeExecutionFocus", []), list)
        else []
    )
    diagnostics = (
        runtime_payload.get("runtimeAgentDiagnostics", {})
        if isinstance(runtime_payload.get("runtimeAgentDiagnostics", {}), dict)
        else {}
    )
    diagnostics_summary = (
        diagnostics.get("summary", {})
        if isinstance(diagnostics.get("summary", {}), dict)
        else {}
    )

    candidates: list[dict[str, Any]] = []
    for item in runtime_instances:
        if not isinstance(item, dict):
            continue
        signal = item.get("signal", {}) if isinstance(item.get("signal", {}), dict) else {}
        risk = item.get("risk", {}) if isinstance(item.get("risk", {}), dict) else {}
        health = item.get("health", {}) if isinstance(item.get("health", {}), dict) else {}
        if str(risk.get("status", "normal") or "normal") == "blocked":
            continue
        candidates.append(
            {
                "strategyName": str(item.get("strategyName", item.get("strategyId", "")) or ""),
                "instanceId": str(item.get("instanceId", "") or ""),
                "score": float(signal.get("contributionScore", 0.0) or 0.0),
                "confidence": float(signal.get("confidence", 0.0) or 0.0),
                "healthScore": float(health.get("runtimeScore", health.get("score", 0.0)) or 0.0),
                "selected": bool(item.get("selected", False)),
            }
        )
    candidates.sort(
        key=lambda row: (
            row.get("score", 0.0),
            row.get("confidence", 0.0),
            row.get("healthScore", 0.0),
        ),
        reverse=True,
    )
    recommended_candidates = [row for row in candidates if float(row.get("score", 0.0)) > 0][:2]
    if not recommended_candidates:
        recommended_candidates = [row for row in candidates if bool(row.get("selected", False))][:2]
    recommended_combo_label = (
        " + ".join(
            [
                str(row.get("strategyName", "") or row.get("instanceId", "") or "unknown")
                for row in recommended_candidates
            ]
        )
        if recommended_candidates
        else "待评估"
    )

    final_action = str(arbitration_summary.get("finalAction", "UNKNOWN") or "UNKNOWN").upper()
    final_side = str(arbitration_summary.get("finalSide", "NONE") or "NONE").upper()
    confidence = float(arbitration_summary.get("confidence", 0.0) or 0.0)
    net_score = float(arbitration_summary.get("netScore", 0.0) or 0.0)
    spread = float(market.get("spread", 0.0) or 0.0)
    blocker_count = int(risk_summary.get("blockerCount", 0) or 0)
    suppressed_count = int(scheduler_summary.get("suppressedCount", 0) or 0)

    verdict = "pass"
    verdict_label = "可继续评估推进"
    if not bool(gateway.get("daemonRunning", False)) or not bool(gateway.get("mt5Online", False)):
        verdict = "blocked"
        verdict_label = "运行链未就绪"
    elif not bool(bridge_health.get("bridgeOk", False)):
        verdict = "blocked"
        verdict_label = "执行桥异常"
    elif bool(risk_summary.get("blocked", False)):
        verdict = "blocked"
        verdict_label = "全局风控阻断"
    elif final_action == "HOLD" or confidence < 0.2:
        verdict = "watch"
        verdict_label = "当前组合仍需观察"
    elif suppressed_count > 0 or len(execution_focus) > 0:
        verdict = "caution"
        verdict_label = "组合存在互搏或阻塞"

    goal_text = str(goal or "").strip() or "提高胜率并减少互搏"
    conclusion_parts: list[str] = []
    if verdict == "blocked":
        conclusion_parts.append("当前不建议直接调整活跃组合，先处理运行或风控阻断。")
    elif verdict == "watch":
        conclusion_parts.append("当前组合没有形成稳定可执行优势，暂不建议贸然扩大组合。")
    elif verdict == "caution":
        conclusion_parts.append("当前组合可继续运行，但要先压缩互搏和执行阻塞。")
    else:
        conclusion_parts.append("当前组合具备继续运行基础，可以围绕高贡献实例做微调。")
    conclusion_parts.append(f"本轮评估目标：{goal_text}。")

    recommendations: list[str] = []
    if recommended_combo_label != "待评估":
        recommendations.append(f"建议活跃组合优先保持 `{recommended_combo_label}`。")
    if suppressed_count > 0:
        recommendations.append(f"当前存在 {suppressed_count} 条压制记录，建议减少互相对冲的实例同时在线。")
    if final_action == "HOLD":
        recommendations.append("当前聚合结果仍是 HOLD，建议先提升高贡献实例的一致性，而不是继续叠加组合。")
    if len(execution_focus) > 0:
        recommendations.append("执行层已有异常聚焦，建议先清理执行堵点，再决定是否重组组合。")
    if not recommendations:
        recommendations.append("当前没有必须立刻调整的组合动作，可继续观察真实运行反馈。")

    risk_warnings: list[str] = []
    if blocker_count > 0:
        risk_warnings.append(f"全局风险总闸当前已有 {blocker_count} 条阻断信号。")
    if spread > 80:
        risk_warnings.append(f"当前点差 {spread:.2f} 偏高，重组组合也未必能立刻改善执行质量。")
    isolated_ids = (
        risk_state.get("isolatedStrategyIds", [])
        if isinstance(risk_state.get("isolatedStrategyIds", []), list)
        else []
    )
    if isolated_ids:
        risk_warnings.append(f"存在被隔离策略: {', '.join(str(item) for item in isolated_ids[:4])}。")
    if not bool(gateway.get("daemonRunning", False)) or not bool(gateway.get("mt5Online", False)):
        risk_warnings.append("运行链路未完全在线，评估结论仅供恢复链路前参考。")
    if not risk_warnings:
        risk_warnings.append("当前没有额外风控风险提示。")

    rationale_parts = [
        f"调度模式为 {str(runtime_scheduler.get('mode', 'idle') or 'idle')}，活跃实例 {int(scheduler_summary.get('activeInstanceCount', 0) or 0)} 个。",
        f"当前最终动作为 {final_action}/{final_side}，聚合置信度 {confidence * 100:.1f}%，加权净分 {net_score:.4f}。",
        f"今日成交 {int(performance.get('todayTrades', 0) or 0)} 笔，今日盈亏 {float(performance.get('todayPnl', 0.0) or 0.0):+.2f}。",
    ]
    if int(diagnostics_summary.get("suppressionCount", 0) or 0) > 0:
        rationale_parts.append(
            f"配置诊断里还有 {int(diagnostics_summary.get('suppressionCount', 0) or 0)} 条 suppression，需要关注是否仍在左右互博。"
        )
    now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    history_entry = {
        "ts": now_iso,
        "goal": goal_text,
        "verdict": verdict,
        "verdictLabel": verdict_label,
        "recommendedComboLabel": recommended_combo_label,
        "conclusion": " ".join(conclusion_parts),
    }
    return {
        "target": goal_text,
        "verdict": verdict,
        "verdictLabel": verdict_label,
        "currentConclusion": " ".join(conclusion_parts),
        "recommendedComboLabel": recommended_combo_label,
        "finalAction": final_action,
        "finalSide": final_side,
        "confidence": round(confidence, 4),
        "netScore": round(net_score, 4),
        "recommendations": recommendations[:4],
        "riskWarnings": risk_warnings[:4],
        "rationale": " ".join(rationale_parts),
        "applySuggestionAvailable": False,
        "applySuggestionReason": "运行中自动应用组合建议暂未开放，先由运营确认评估结果。",
        "updatedAt": now_iso,
        "historyEntry": history_entry,
    }


def _build_risk_governor_snapshot(status_payload: Optional[dict[str, Any]] = None) -> dict[str, Any]:
    status_payload = status_payload or get_status()
    effective_payload = _build_effective_runtime_params_payload()
    effective_governor = (
        effective_payload.get("governor", {})
        if isinstance(effective_payload.get("governor", {}), dict)
        else {}
    )
    effective_status = (
        effective_payload.get("status", {})
        if isinstance(effective_payload.get("status", {}), dict)
        else {}
    )
    effective_identity = (
        effective_payload.get("runtimeIdentity", {})
        if isinstance(effective_payload.get("runtimeIdentity", {}), dict)
        else {}
    )
    effective_sources = (
        effective_payload.get("fieldSources", {})
        if isinstance(effective_payload.get("fieldSources", {}), dict)
        else {}
    )
    params_payload = get_params()
    params = params_payload.get("params", {}) if isinstance(params_payload.get("params", {}), dict) else {}
    lifecycle = get_governance_lifecycle()
    runtime_observability = get_runtime_observability()
    blackboard = _compact_blackboard_for_status(_get_blackboard_data())
    risk_block = status_payload.get("risk_block", {}) if isinstance(status_payload.get("risk_block", {}), dict) else {}
    final_reject = status_payload.get("final_reject", {}) if isinstance(status_payload.get("final_reject", {}), dict) else {}
    risk_protocol = (
        blackboard.get("Risk_Protocol", {})
        if isinstance(blackboard.get("Risk_Protocol", {}), dict)
        else {}
    )
    blockers: list[str] = _normalize_blocker_reasons(risk_protocol.get("governor_blockers", []))
    if bool(status_payload.get("account_total_loss_stop_active", False)):
        blockers.append("account_total_loss_stop_active")
    final_reject_reason = str(final_reject.get("reason", "")).strip()
    if final_reject_reason:
        blockers.append(final_reject_reason)
    blockers = _normalize_blocker_reasons(blockers)
    return {
        "summary": {
            "riskLevel": str(params.get("risk_level", "balanced")),
            "positionMultiplier": float(
                blackboard.get("Risk_Commissar", {}).get("position_multiplier", 1.0)
                if isinstance(blackboard.get("Risk_Commissar", {}), dict)
                else 1.0
            ),
            "blocked": len(blockers) > 0,
            "blockerCount": len(blockers),
            "accountLossCurrentPct": _normalize_pct_value(
                status_payload.get("account_total_loss_current_pct", 0.0)
            ),
            "accountLossStopPct": float(status_payload.get("account_total_loss_stop_pct", params.get("account_total_loss_stop_pct", 0.0)) or 0.0),
            "truthReady": bool(effective_status.get("ready", False)),
            "truthSeverity": str(effective_status.get("severity", "unknown") or "unknown"),
            "truthReason": str(effective_status.get("reason", "") or ""),
        },
        "governor": {
            "riskLevel": str(
                effective_governor.get("riskLevel", params.get("risk_level", "balanced"))
                or "balanced"
            ),
            "maxPositionSize": float(
                effective_governor.get("maxPositionSize", params.get("max_position_size", 0.1))
                or 0.1
            ),
            "maxOpenPositions": int(
                effective_governor.get("maxOpenPositions", params.get("max_open_positions", 1))
                or 1
            ),
            "minConfidencePct": float(
                effective_governor.get("minConfidencePct", params.get("min_confidence", 70.0))
                or 70.0
            ),
            "minConfidenceThreshold": float(
                effective_governor.get("minConfidenceThreshold", 0.7) or 0.7
            ),
            "slPips": int(effective_governor.get("slPips", params.get("sl_pips", 0)) or 0),
            "tpPips": int(effective_governor.get("tpPips", params.get("tp_pips", 0)) or 0),
            "accountTotalLossStopPct": float(
                effective_governor.get(
                    "accountTotalLossStopPct",
                    params.get("account_total_loss_stop_pct", 0.0),
                )
                or 0.0
            ),
            "humanInTheLoop": bool(
                effective_governor.get("humanInTheLoop", params.get("human_in_the_loop", False))
            ),
            "accountTotalLossStopActive": bool(status_payload.get("account_total_loss_stop_active", False)),
            "accountTotalLossStopSource": str(status_payload.get("account_total_loss_stop_source", "NONE") or "NONE"),
            "riskBlock": risk_block,
            "finalReject": final_reject,
            "softLimits": status_payload.get("soft_limits", []),
            "blockers": blockers,
            "truthStatus": effective_status,
            "truthSources": effective_sources,
            "runtimeIdentity": effective_identity,
        },
        "blackboard": blackboard,
        "trainingLifecycle": lifecycle.get("trainingLifecycle", {}),
        "rollbackEvidence": lifecycle.get("rollbackEvidence", {}),
        "autoGovernance": lifecycle.get("autoGovernance", {}),
        "phaseReadiness": lifecycle.get("phaseReadiness", {}),
        "sessionRiskLens": runtime_observability.get("sessionRiskLens", {}),
        "macroPolling": runtime_observability.get("macroPolling", {}),
        "confidenceCalibration": runtime_observability.get("confidenceCalibration", {}),
    }


def _build_lifecycle_open_position_fallback() -> list[dict[str, Any]]:
    lifecycle_payload = load_position_lifecycle(DATA_DIR)
    current = (
        lifecycle_payload.get("current", {})
        if isinstance(lifecycle_payload.get("current", {}), dict)
        else {}
    )
    lifecycle_status = str(current.get("status", "") or "").strip().lower()
    side = str(current.get("side", "NONE") or "NONE").upper()
    volume = float(current.get("volume", current.get("volume_lots", 0.0)) or 0.0)
    entry_price = float(current.get("entry_price", 0.0) or 0.0)
    ticket = int(current.get("ticket", 0) or 0)
    terminal_positions_count = _read_terminal_status_positions_count()
    if lifecycle_status not in {"open", "opening", "closing"}:
        return []
    if side not in {"BUY", "SELL"} or volume <= 0 or entry_price <= 0 or ticket <= 0:
        return []
    if terminal_positions_count == 0:
        return []
    return [
        {
            "ticket": ticket,
            "side": side,
            "volume": volume,
            "entry": entry_price,
            "sl": float(current.get("sl", 0.0) or 0.0),
            "tp": float(current.get("tp", 0.0) or 0.0),
            "pnl": float(current.get("realized_pnl", 0.0) or 0.0),
            "symbol": str(current.get("symbol", "XAUUSD") or "XAUUSD"),
            "entry_owner_instance_id": str(current.get("entry_owner_instance_id", "") or ""),
            "entry_owner_strategy_id": str(current.get("entry_owner_strategy_id", "") or ""),
            "current_manager_instance_id": str(current.get("current_manager_instance_id", "") or ""),
            "current_manager_strategy_id": str(current.get("current_manager_strategy_id", "") or ""),
            "expected_exit_owner_instance_id": str(current.get("expected_exit_owner_instance_id", "") or ""),
            "expected_exit_owner_strategy_id": str(current.get("expected_exit_owner_strategy_id", "") or ""),
            "exit_owner_instance_id": str(current.get("exit_owner_instance_id", "") or ""),
            "exit_owner_strategy_id": str(current.get("exit_owner_strategy_id", "") or ""),
            "contributors": (
                current.get("contributors", [])
                if isinstance(current.get("contributors", []), list)
                else []
            ),
            "entry_signal_id": str(current.get("signal_id", "") or ""),
            "entry_decision_id": str(current.get("decision_id", "") or ""),
            "exit_signal_id": "",
            "attribution_status": "lifecycle_fallback",
            "lifecycle_status": lifecycle_status,
            "source": "position_lifecycle",
        }
    ]


def _backfill_open_positions_runtime_strategy(
    positions: list[dict[str, Any]],
    *,
    runtime_scheduler: dict[str, Any],
    effective_projection: dict[str, Any],
    current: dict[str, Any],
) -> list[dict[str, Any]]:
    if not isinstance(positions, list) or not positions:
        return []
    normalized_positions = [dict(item) for item in positions if isinstance(item, dict)]
    if not normalized_positions:
        return []

    selected_strategy_id = _first_non_empty_str(
        runtime_scheduler.get("selectedStrategyId"),
        effective_projection.get("selectedStrategyId"),
        current.get("selectedStrategyId"),
    )
    entry_owner_strategy_id = _first_non_empty_str(
        runtime_scheduler.get("entryOwnerStrategyId"),
        (
            runtime_scheduler.get("executionDecision", {})
            if isinstance(runtime_scheduler.get("executionDecision", {}), dict)
            else {}
        ).get("entryOwnerStrategyId"),
        effective_projection.get("entryOwnerStrategyId"),
        (
            effective_projection.get("executionDecision", {})
            if isinstance(effective_projection.get("executionDecision", {}), dict)
            else {}
        ).get("entryOwnerStrategyId"),
    )
    selected_instance_id = _first_non_empty_str(
        runtime_scheduler.get("selectedInstanceId"),
        (
            runtime_scheduler.get("executionDecision", {})
            if isinstance(runtime_scheduler.get("executionDecision", {}), dict)
            else {}
        ).get("selectedInstanceId"),
        effective_projection.get("selectedInstanceId"),
    )
    entry_owner_instance_id = _first_non_empty_str(
        runtime_scheduler.get("entryOwnerInstanceId"),
        (
            runtime_scheduler.get("executionDecision", {})
            if isinstance(runtime_scheduler.get("executionDecision", {}), dict)
            else {}
        ).get("entryOwnerInstanceId"),
        effective_projection.get("entryOwnerInstanceId"),
    )
    if not (
        selected_strategy_id
        or entry_owner_strategy_id
        or selected_instance_id
        or entry_owner_instance_id
    ):
        return normalized_positions

    for position in normalized_positions:
        inferred_position_strategy_id = _first_non_empty_str(
            position.get("strategy_id"),
            position.get("current_manager_strategy_id"),
            position.get("entry_selected_strategy_id"),
        )
        inferred_position_instance_id = _first_non_empty_str(
            position.get("instance_id"),
            position.get("current_manager_instance_id"),
            position.get("entry_selected_instance_id"),
        )
        if not str(position.get("entry_selected_strategy_id", "") or "").strip():
            position["entry_selected_strategy_id"] = inferred_position_strategy_id or selected_strategy_id
        if not str(position.get("entry_selected_instance_id", "") or "").strip():
            position["entry_selected_instance_id"] = inferred_position_instance_id or selected_instance_id
        existing_entry_owner_strategy_id = str(position.get("entry_owner_strategy_id", "") or "").strip()
        if (
            existing_entry_owner_strategy_id
            and entry_owner_strategy_id
            and existing_entry_owner_strategy_id != entry_owner_strategy_id
        ):
            pass
        if not str(position.get("entry_owner_strategy_id", "") or "").strip():
            position["entry_owner_strategy_id"] = inferred_position_strategy_id or entry_owner_strategy_id
        if not str(position.get("entry_owner_instance_id", "") or "").strip():
            position["entry_owner_instance_id"] = inferred_position_instance_id or entry_owner_instance_id
        position["_entry_owner_attribution_source"] = (
            "runtime"
            if str(position.get("entry_owner_strategy_id", "") or "").strip()
            else "missing"
        )
    return normalized_positions


def _build_execution_chain_snapshot(
    status_payload: Optional[dict[str, Any]] = None,
    marketplace_payload: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
    status_payload = status_payload or get_status()
    terminal_positions_count = _read_terminal_status_positions_count()
    open_positions = (
        status_payload.get("positions", [])
        if isinstance(status_payload.get("positions", []), list)
        else []
    )
    if terminal_positions_count == 0:
        open_positions = []
    if (
        terminal_positions_count is not None
        and terminal_positions_count > 0
        and len(open_positions) != terminal_positions_count
    ):
        _, st_data = _read_terminal_status_snapshot()
        if st_data:
            raw = _extract_terminal_status_positions(st_data)
            if raw:
                open_positions = _enrich_positions_with_attribution(raw)
    if not open_positions and terminal_positions_count != 0:
        open_positions = _build_lifecycle_open_position_fallback()
    pending_payload = get_pending_signals()
    pending_signals = pending_payload.get("signals", []) if isinstance(pending_payload.get("signals", []), list) else []
    signal_rows = _collect_execution_signal_rows(limit=10)
    marketplace = (
        marketplace_payload
        if isinstance(marketplace_payload, dict)
        else _build_strategy_marketplace_payload(status_payload=status_payload)
    )
    current = marketplace.get("current", {}) if isinstance(marketplace.get("current", {}), dict) else {}
    strategy_projection = (
        marketplace.get("strategyProjection", {})
        if isinstance(marketplace.get("strategyProjection", {}), dict)
        else {}
    )
    configured_projection = (
        strategy_projection.get("configured", {})
        if isinstance(strategy_projection.get("configured", {}), dict)
        else {}
    )
    effective_projection = (
        strategy_projection.get("effective", {})
        if isinstance(strategy_projection.get("effective", {}), dict)
        else {}
    )
    runtime_scheduler = (
        marketplace.get("runtimeScheduler", {})
        if isinstance(marketplace.get("runtimeScheduler", {}), dict)
        else {}
    )
    strategy_rows = (
        marketplace.get("strategies", [])
        if isinstance(marketplace.get("strategies", []), list)
        else []
    )
    strategy_name_map = {
        str(item.get("id", "") or "").strip(): str(item.get("name", "") or str(item.get("id", "") or "")).strip()
        for item in strategy_rows
        if isinstance(item, dict) and str(item.get("id", "") or "").strip()
    }
    runtime_selected_strategy_id = _first_non_empty_str(
        runtime_scheduler.get("actualCalledStrategyId"),
        runtime_scheduler.get("selectedStrategyId"),
        effective_projection.get("actualCalledStrategyId"),
        effective_projection.get("selectedStrategyId"),
        current.get("actualCalledStrategyId"),
        current.get("selectedStrategyId"),
        current.get("strategyId"),
    )
    runtime_route_winner_strategy_id = _first_non_empty_str(
        runtime_scheduler.get("routeWinnerStrategyId"),
        effective_projection.get("routeWinnerStrategyId"),
        current.get("routeWinnerStrategyId"),
    )
    runtime_active_strategy_ids = _clean_str_list(
        runtime_scheduler.get("activeStrategyIds")
        or effective_projection.get("activeStrategyIds")
        or current.get("activeStrategyIds")
    )
    runtime_selected_strategy_name = (
        str(runtime_scheduler.get("actualCalledStrategyName", "") or "").strip()
        or str(runtime_scheduler.get("selectedStrategyName", "") or "").strip()
        or str(effective_projection.get("actualCalledStrategyName", "") or "").strip()
        or str(effective_projection.get("selectedStrategyName", "") or "").strip()
        or strategy_name_map.get(runtime_selected_strategy_id)
        or str(current.get("strategyName", "") or "").strip()
        or str(configured_projection.get("strategyName", "") or "").strip()
        or "未知策略"
    )
    open_positions = _backfill_open_positions_runtime_strategy(
        open_positions,
        runtime_scheduler=runtime_scheduler,
        effective_projection=effective_projection,
        current=current,
    )
    return {
        "summary": {
            "openPositions": len(open_positions),
            "pendingReviews": len(pending_signals),
            "signalReviewPendingCount": len(pending_signals),
            "activeExecutions": len(signal_rows["active"]),
            "configuredStrategy": str(
                current.get("strategyId")
                or configured_projection.get("strategyId")
                or "ema_macd"
            ),
            "configuredStrategyName": str(
                current.get("strategyName")
                or configured_projection.get("strategyName")
                or "未知策略"
            ),
            "runtimeSelectedStrategyId": runtime_selected_strategy_id,
            "runtimeSelectedStrategyName": runtime_selected_strategy_name,
            "runtimeActualCalledStrategyId": runtime_selected_strategy_id,
            "runtimeActualCalledStrategyName": runtime_selected_strategy_name,
            "runtimeRouteWinnerStrategyId": runtime_route_winner_strategy_id,
            "runtimeRouteWinnerStrategyName": (
                strategy_name_map.get(runtime_route_winner_strategy_id)
                or runtime_route_winner_strategy_id
            ),
            "runtimeActiveStrategyIds": runtime_active_strategy_ids,
            "runtimeMode": str(
                runtime_scheduler.get("mode")
                or effective_projection.get("mode")
                or current.get("mode", "multi_active")
                or "multi_active"
            ),
            "runtimeArbiter": str(
                runtime_scheduler.get("arbiter")
                or effective_projection.get("arbiter")
                or current.get("arbiter", "allocation-weighted")
                or "allocation-weighted"
            ),
            "runtimeLoaded": bool(
                effective_projection.get("runtimeLoaded", current.get("runtimeLoaded", False))
            ),
            "runtimeSourceLayer": str(
                effective_projection.get("sourceLayer")
                or runtime_scheduler.get("sourceLayer")
                or current.get("sourceLayer", "configured")
                or "configured"
            ),
            "runtimeFieldSources": (
                effective_projection.get("fieldSources", {})
                if isinstance(effective_projection.get("fieldSources", {}), dict)
                else runtime_scheduler.get("fieldSources", {})
                if isinstance(runtime_scheduler.get("fieldSources", {}), dict)
                else {}
            ),
        },
        "openPositions": open_positions,
        "pendingSignals": pending_signals[:8],
        "signalReviewSignals": pending_signals[:8],
        "signalReviewPendingCount": len(pending_signals),
        "activeSignals": signal_rows["active"],
        "recentSignals": signal_rows["recent"],
    }


def _build_timeline_payload(limit: int = 40) -> dict[str, Any]:
    events: list[dict[str, Any]] = []
    source_counts: dict[str, int] = {}

    def push(event: dict[str, Any]) -> None:
        events.append(event)
        source = str(event.get("source", "other") or "other")
        source_counts[source] = source_counts.get(source, 0) + 1

    try:
        agent_dir = LOGS_DIR / "agent_decisions"
        if agent_dir.exists():
            for file_path in sorted(agent_dir.glob("direction_*.json"), key=os.path.getmtime, reverse=True)[:8]:
                payload = _read_json_file(file_path)
                if not payload:
                    continue
                file_mtime = file_path.stat().st_mtime
                parsed_ts = _parse_timestamp_value(payload.get("timestamp")) or _parse_timestamp_value(payload.get("updated"))
                # Prefer filesystem mtime as stable truth; only trust payload ts when it is close.
                ts = file_mtime
                if isinstance(parsed_ts, (int, float)) and abs(float(parsed_ts) - float(file_mtime)) <= 3600.0:
                    ts = float(parsed_ts)
                if ts is not None and (time.time() - float(ts)) > _TIMELINE_DIRECTION_MAX_AGE_SEC:
                    continue
                confidence = payload.get("confidence")
                if isinstance(confidence, (int, float)) and float(confidence) <= 1.0:
                    confidence_text = f"{float(confidence) * 100:.1f}%"
                else:
                    confidence_text = f"{float(confidence or 0):.1f}%"
                push(
                    _timeline_event(
                        source="direction",
                        ts=ts,
                        role="Data_Analyst",
                        event_type="analyze",
                        title=f"生成趋势预测: {payload.get('direction', '未知')}",
                        content=str(payload.get("reason", "") or "无趋势解读"),
                        data={"Confidence": confidence_text},
                    )
                )
    except Exception:
        pass

    try:
        review_dir = LOGS_DIR / "review_pending"
        if review_dir.exists():
            for file_path in sorted(review_dir.glob("*.review.json"), key=os.path.getmtime, reverse=True)[:8]:
                payload = _read_json_file(file_path)
                if not payload:
                    continue
                final_conf = float(payload.get("requested_confidence", payload.get("confidence", 0.0)) or 0.0)
                raw_conf = float(payload.get("confidence", 0.0) or 0.0)
                ts = file_path.stat().st_mtime
                push(
                    _timeline_event(
                        source="review_pending",
                        ts=ts,
                        role="Risk_Manager",
                        event_type="warning",
                        title="触发风控拦截",
                        content=(
                            f"系统拦截了一笔待执行请求，最终置信度 {final_conf * 100:.1f}% / "
                            f"原始 {raw_conf * 100:.1f}%。原因: {payload.get('reason', '未知')}"
                        ),
                        data={
                            "Action": payload.get("action"),
                            "Side": payload.get("side"),
                            "Entry": payload.get("entry"),
                        },
                    )
                )
    except Exception:
        pass

    try:
        signal_rows = _collect_execution_signal_rows(limit=10)
        for row in signal_rows["recent"]:
            push(
                _timeline_event(
                    source="execution_state",
                    ts=float(row.get("latestTs", 0.0) or 0.0),
                    role="Execution_State",
                    event_type="trade" if str(row.get("state", "")).upper() == "FILLED" else "analyze",
                    title=f"执行链状态更新: {row.get('state', 'UNKNOWN')}",
                    content=(
                        f"Signal {row.get('signalId', '-')}"
                        f" · {row.get('side', 'UNKNOWN')}"
                        f" · strategy={row.get('strategyId', 'unknown') or 'unknown'}"
                    ),
                    data={
                        "TraceId": row.get("traceId"),
                        "Reason": row.get("reason"),
                        "Action": row.get("action"),
                        "Entry": row.get("entry"),
                    },
                )
            )
    except Exception:
        pass

    try:
        archive_dir = DATA_DIR / "bridge" / "archive"
        if archive_dir.exists():
            for file_path in sorted(archive_dir.glob("*.report.json"), key=os.path.getmtime, reverse=True)[:8]:
                payload = _read_json_file(file_path)
                if not payload:
                    continue
                ts = file_path.stat().st_mtime
                push(
                    _timeline_event(
                        source="bridge_archive",
                        ts=ts,
                        role="Trend_Strategy_v2",
                        event_type="trade",
                        title=f"执行交易动作: {payload.get('action', '未知')}",
                        content=f"执行状态: {payload.get('status', '未知')}。返回信息: {payload.get('message', '')}",
                        data={
                            "Action": payload.get("action"),
                            "Ticket": payload.get("ticket"),
                            "Volume": payload.get("volume"),
                        },
                    )
                )
        ledger_file = _resolve_existing_file(_runtime_log_candidates("trading_ledger.ndjson"))
        if ledger_file is not None:
            for payload in _read_ndjson_tail(ledger_file, limit=8):
                ts = _parse_timestamp_value(payload.get("ts")) or ledger_file.stat().st_mtime
                push(
                    _timeline_event(
                        source="trading_ledger",
                        ts=ts,
                        role="Trade_Ledger",
                        event_type="trade",
                        title=f"交易账本记录: {payload.get('side', 'UNKNOWN')} {payload.get('volume', '')}",
                        content=f"Ticket: {payload.get('ticket', '')}, 策略: {payload.get('strategy', 'unknown')}",
                        data={
                            "Price": payload.get("price"),
                            "SL": payload.get("sl"),
                            "TP": payload.get("tp"),
                        },
                    )
                )
    except Exception:
        pass

    try:
        risk_file = LOGS_DIR / "risk_events.ndjson"
        for payload in _read_ndjson_tail(risk_file, limit=8):
            ts = _parse_timestamp_value(payload.get("ts")) or _parse_timestamp_value(payload.get("timestamp")) or risk_file.stat().st_mtime
            event_type = "warning"
            event_name = str(payload.get("event_type", "risk_event") or "risk_event")
            if "pass" in event_name:
                event_type = "analyze"
            push(
                _timeline_event(
                    source="risk_events",
                    ts=ts,
                    role="Risk_Commissar",
                    event_type=event_type,
                    title=f"风险事件: {event_name}",
                    content=str(payload.get("reason") or payload.get("summary") or payload.get("message") or "risk event"),
                    data={
                        "Action": payload.get("action"),
                        "Side": payload.get("side"),
                        "TraceId": payload.get("trace_id"),
                    },
                )
            )
    except Exception:
        pass

    try:
        closures_file = APP_DIR / "learning" / "events" / "closures.jsonl"
        for payload in _read_ndjson_tail(closures_file, limit=8):
            body = payload.get("payload", {}) if isinstance(payload.get("payload", {}), dict) else {}
            ts = _parse_timestamp_value(payload.get("event_time")) or closures_file.stat().st_mtime
            push(
                _timeline_event(
                    source="closure_events",
                    ts=ts,
                    role="Closure_Audit",
                    event_type="trade",
                    title=f"平仓归档: {body.get('done_reason', 'closed')}",
                    content=(
                        f"ticket={body.get('ticket', 0)}"
                        f" · pnl={body.get('realized_pnl', 0.0)}"
                        f" · recovered={bool(body.get('recovered', False))}"
                    ),
                    data={
                        "DecisionId": payload.get("decision_id"),
                        "RecoverySource": body.get("recovery_source"),
                    },
                )
            )
    except Exception:
        pass

    try:
        macro_status = _read_macro_refresh_status()
        if macro_status:
            ts = _parse_timestamp_value(macro_status.get("updated_at")) or _parse_timestamp_value(macro_status.get("started_at")) or _parse_timestamp_value(macro_status.get("finished_at"))
            push(
                _timeline_event(
                    source="macro_refresh",
                    ts=ts,
                    role="Macro_Commander",
                    event_type="analyze",
                    title=f"宏观刷新状态: {macro_status.get('status', 'unknown')}",
                    content=str(macro_status.get("message", "") or "宏观方向刷新状态更新"),
                    data={
                        "Pid": macro_status.get("pid"),
                        "Status": macro_status.get("status"),
                    },
                )
            )
    except Exception:
        pass

    try:
        runtime_status = _runtime_strategy_status_payload()
        active = runtime_status.get("active", {}) if isinstance(runtime_status.get("active", {}), dict) else {}
        if active:
            ts_ms = runtime_status.get("ts_ms")
            ts = float(ts_ms) / 1000.0 if isinstance(ts_ms, (int, float)) else None
            push(
                _timeline_event(
                    source="strategy_runtime",
                    ts=ts,
                    role="Strategy_Runtime",
                    event_type="analyze",
                    title=f"策略运行时快照: {active.get('strategy_id', 'unknown')}",
                    content=(
                        f"loaded={len(runtime_status.get('loaded', []))}"
                        f" · registered={len(runtime_status.get('registered', []))}"
                        f" · revision={active.get('config_revision', '')}"
                    ),
                    data={
                        "Loaded": runtime_status.get("loaded", []),
                        "Registered": runtime_status.get("registered", []),
                    },
                )
            )
    except Exception:
        pass

    try:
        autonomy_loop = _read_json_file(LOGS_DIR / "autonomy_loop_latest.json")
        if autonomy_loop:
            ts = _parse_timestamp_value(autonomy_loop.get("ts")) or (LOGS_DIR / "autonomy_loop_latest.json").stat().st_mtime
            push(
                _timeline_event(
                    source="autonomy_loop",
                    ts=ts,
                    role="Autonomy_Loop",
                    event_type="analyze",
                    title=f"自治闭环动作: {autonomy_loop.get('next_action', 'UNKNOWN')}",
                    content=str(autonomy_loop.get("notice") or autonomy_loop.get("governance_action") or "自治闭环已刷新"),
                    data={
                        "GovernanceAction": autonomy_loop.get("governance_action"),
                        "TenantId": autonomy_loop.get("tenant_id"),
                    },
                )
            )
    except Exception:
        pass

    try:
        briefing_file, content = _read_text_from_candidates(_runtime_log_candidates("morning_briefing_latest.md"))
        if briefing_file is not None:
            ts = briefing_file.stat().st_mtime
            dt = datetime.datetime.fromtimestamp(ts)
            if dt.date() == datetime.datetime.now().date():
                push(
                    _timeline_event(
                        source="morning_briefing",
                        ts=ts,
                        role="Agent",
                        event_type="analyze",
                        title="早间简报 (Morning Briefing)",
                        content=content,
                        data={"Status": "已送达"},
                    )
                )
    except Exception:
        pass

    try:
        report_file, content = _read_text_from_candidates(_runtime_log_candidates("evolution_llm_report_latest.txt"))
        if report_file is not None:
            ts = report_file.stat().st_mtime
            summary = content[:300] + "..." if len(content) > 300 else content
            push(
                _timeline_event(
                    source="evolution_report",
                    ts=ts,
                    role="Evolution_Agent",
                    event_type="analyze",
                    title="系统进化与训练诊断报告",
                    content=summary,
                    data={"FullReport": "已生成"},
                )
            )
    except Exception:
        pass

    events.sort(key=lambda item: float(item.get("timestamp", 0.0) or 0.0), reverse=True)
    if not events:
        events = [
            _timeline_event(
                source="system",
                ts=time.time(),
                role="System",
                event_type="analyze",
                title="系统启动并等待数据",
                content="正在监控行情和 AI Agent 的分析报告，稍后将在这里展示复盘流...",
            )
        ]
    source_meta = [
        {"id": source, "count": count}
        for source, count in sorted(source_counts.items(), key=lambda item: (-item[1], item[0]))
    ]
    return {"success": True, "events": events[:limit], "sources": source_meta, "totalEvents": len(events)}


def _get_timeline_payload_cached(limit: int = 40) -> dict[str, Any]:
    now = time.time()
    cached = _TIMELINE_CACHE.get("payload")
    cached_ts = float(_TIMELINE_CACHE.get("ts", 0.0) or 0.0)
    if isinstance(cached, dict) and (now - cached_ts) <= _TIMELINE_CACHE_TTL_SECONDS:
        return cached
    payload = _build_timeline_payload(limit=limit)
    _TIMELINE_CACHE["payload"] = payload
    _TIMELINE_CACHE["ts"] = now
    return payload


def _top_bucket_preview(group: Any, limit: int = 3) -> list[dict[str, Any]]:
    if not isinstance(group, dict):
        return []
    rows: list[dict[str, Any]] = []
    for name, stats in group.items():
        if not isinstance(stats, dict):
            continue
        rows.append(
            {
                "name": str(name),
                "trades": int(stats.get("trades", 0) or 0),
                "winRatePct": float(stats.get("win_rate_pct", 0.0) or 0.0),
                "avgPnl": float(stats.get("avg_pnl", 0.0) or 0.0),
                "totalPnl": float(stats.get("total_pnl", 0.0) or 0.0),
                "block": bool(stats.get("block", False)),
                "usable": bool(stats.get("usable", False)),
                "liveMultiplier": float(stats.get("live_multiplier", 1.0) or 1.0),
            }
        )
    rows.sort(
        key=lambda item: (
            item["trades"],
            item["winRatePct"],
            item["totalPnl"],
        ),
        reverse=True,
    )
    return rows[:limit]


def _training_status_defaults() -> Dict[str, Any]:
    return {
        "isTraining": False,
        "progress": 0.0,
        "currentBar": 0,
        "totalBars": 0,
        "currentEpoch": 0,
        "totalEpochs": 0,
        "winRate": 0.0,
        "pnl": 0.0,
        "totalTrades": 0,
        "activeDataset": _default_training_dataset_label(),
        "activeModel": DEFAULT_ANALYSIS_MODEL,
        "analysisModel": DEFAULT_ANALYSIS_MODEL,
        "trainingEngine": dict(TRAINING_RUNTIME_CONFIG),
        "pnlHistory": [],
        "qualityGate": {},
        "readyForTraining": False,
        "blockedReasons": [],
        "closureCoveragePct": 0.0,
        "criticalAnomalies": 0,
        "recoveredClosedRatioPct": 0.0,
        "naturalClosedRatioPct": 0.0,
        "naturalClosedTransitions": 0,
        "recoveredClosedTransitions": 0,
        "usableTradeCount": 0,
        "excludedIncompleteCount": 0,
        "excludedRecoveredCount": 0,
        "samplePolicy": {},
        "qualitySnapshotId": "",
        "qualitySnapshotAt": "",
        "errorDispatch": {},
        "repairCycle": {},
        "autoPilotEnabled": False,
        "autoPilotStage": "idle",
        "autoPilotMessage": "自动推进未开启",
        "autoPilotLastActionAt": "",
        "autoPilotLastError": "",
    }


def _read_training_quality_snapshot(
    *,
    force_refresh: bool = False,
    repair_cycle: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    fallback_path = _resolve_runtime_artifact("data", "offline", "training_quality_snapshot.json")
    fallback_payload = _read_json_file(fallback_path) if fallback_path is not None else {}
    try:
        from src.training_quality_snapshot import (
            ensure_training_quality_snapshot,
            read_training_quality_snapshot,
        )

        if force_refresh:
            refreshed = ensure_training_quality_snapshot(
                force_refresh=True,
                repair_cycle=repair_cycle,
                source="gui_backend",
            )
            return refreshed if isinstance(refreshed, dict) and refreshed else fallback_payload
        snapshot = read_training_quality_snapshot()
        snapshot_ts = _parse_timestamp_value(snapshot.get("generated_at")) if isinstance(snapshot, dict) else None
        if snapshot and snapshot_ts and (time.time() - snapshot_ts) <= 900:
            return snapshot
        refreshed = ensure_training_quality_snapshot(
            force_refresh=True,
            repair_cycle=repair_cycle,
            source="gui_backend",
        )
        if refreshed:
            return refreshed
        if snapshot:
            return snapshot
    except Exception:
        pass
    return fallback_payload if isinstance(fallback_payload, dict) else {}


def _read_training_candidate_value() -> Dict[str, Any]:
    try:
        from src.training_candidate_value import evaluate_training_candidate_value

        return evaluate_training_candidate_value(
            quality_snapshot=_read_training_quality_snapshot(force_refresh=False)
        )
    except Exception:
        return {}




def _read_training_status_payload() -> Dict[str, Any]:
    status_file = _resolve_runtime_artifact("data", "offline", "training_status.json") or (
        DATA_DIR / "offline" / "training_status.json"
    )
    payload = _training_status_defaults()
    if status_file.exists():
        try:
            loaded = json.loads(status_file.read_text(encoding="utf-8"))
            if isinstance(loaded, dict):
                payload.update(loaded)
        except Exception as exc:
            _recover_corrupt_json_file(
                status_file,
                payload,
                reason=f"training_status_invalid_json:{type(exc).__name__}",
            )
    payload["activeDataset"] = _normalize_training_dataset(str(payload.get("activeDataset", "")))
    payload["analysisModel"] = _normalize_analysis_model(str(payload.get("analysisModel", payload.get("activeModel", ""))))
    payload["activeModel"] = _normalize_analysis_model(str(payload.get("activeModel", payload["analysisModel"])))
    payload["trainingEngine"] = {
        **TRAINING_RUNTIME_CONFIG,
        **(payload.get("trainingEngine", {}) if isinstance(payload.get("trainingEngine"), dict) else {}),
    }
    return payload


def _compact_training_pnl_history(rows: Any, limit: int = _TRAINING_PNL_HISTORY_MAX_POINTS) -> list[dict[str, Any]]:
    if not isinstance(rows, list):
        return []
    normalized: list[dict[str, Any]] = []
    for item in rows:
        if not isinstance(item, dict):
            continue
        try:
            epoch = int(item.get("epoch", 0) or 0)
            loss = round(float(item.get("loss", 0.0) or 0.0), 2)
        except Exception:
            continue
        normalized.append({"epoch": epoch, "loss": loss})
    if len(normalized) <= limit:
        return normalized
    if limit <= 2:
        return normalized[-limit:]
    keep_indexes = {0, len(normalized) - 1}
    step = (len(normalized) - 1) / float(limit - 1)
    for idx in range(1, limit - 1):
        keep_indexes.add(int(round(idx * step)))
    return [normalized[idx] for idx in sorted(keep_indexes)[:limit]]


def _load_best_params_pnl_history(best_file: pathlib.Path) -> list[dict[str, Any]]:
    try:
        mtime = float(best_file.stat().st_mtime)
    except Exception:
        return []
    cached_mtime = float(_TRAINING_PNL_HISTORY_CACHE.get("mtime", 0.0) or 0.0)
    cached_value = _TRAINING_PNL_HISTORY_CACHE.get("value", [])
    if mtime == cached_mtime and isinstance(cached_value, list):
        return list(cached_value)
    history: list[dict[str, Any]] = []
    try:
        best_data = json.loads(best_file.read_text(encoding="utf-8"))
        trades = best_data.get("stats", {}).get("trades_list", []) if isinstance(best_data, dict) else []
        cum_pnl = 0.0
        for i, trade in enumerate(trades):
            if not isinstance(trade, dict):
                continue
            cum_pnl += float(trade.get("pnl", 0.0) or 0.0)
            history.append({"epoch": i + 1, "loss": round(cum_pnl, 2)})
    except Exception:
        history = []
    compacted = _compact_training_pnl_history(history)
    _TRAINING_PNL_HISTORY_CACHE["mtime"] = mtime
    _TRAINING_PNL_HISTORY_CACHE["value"] = compacted
    return list(compacted)


def _read_live_training_metrics() -> Dict[str, Any]:
    auto_governance_file = _resolve_runtime_artifact("logs", "auto_governance_status_latest.json") or (
        LOGS_DIR / "auto_governance_status_latest.json"
    )
    shadow_summary_file = _resolve_runtime_artifact("logs", "shadow_learner_summary_latest.json") or (
        LOGS_DIR / "shadow_learner_summary_latest.json"
    )
    live_summary_file = _resolve_runtime_artifact("logs", "live_learner_summary_latest.json") or (
        LOGS_DIR / "live_learner_summary_latest.json"
    )

    metrics: Dict[str, Any] = {
        "source": "training_status",
        "updatedAt": "",
        "updatedTs": 0.0,
        "totalTrades": 0,
        "winRate": 0.0,
        "drawdown": 0.0,
        "shadowClosedTrades": 0,
        "shadowWinRatePct": 0.0,
        "shadowMaxDrawdownAbs": 0.0,
        "learnerLiveSelections": 0,
        "baselineSelections": 0,
        "lastReason": "",
    }

    def _apply_mtime(path: pathlib.Path) -> None:
        try:
            ts = float(path.stat().st_mtime)
            if ts > float(metrics["updatedTs"]):
                metrics["updatedTs"] = ts
                metrics["updatedAt"] = datetime.datetime.fromtimestamp(ts).isoformat()
        except Exception:
            pass

    if auto_governance_file.exists():
        try:
            payload = json.loads(auto_governance_file.read_text(encoding="utf-8"))
            aggregate = (
                ((payload.get("evidence") or {}).get("aggregateMetrics", {}))
                or (((payload.get("goal_targets") or {}).get("window_month", {})))
            )
            if isinstance(aggregate, dict):
                metrics["source"] = "auto_governance"
                metrics["totalTrades"] = int(aggregate.get("closed_trades", 0) or 0)
                metrics["winRate"] = float(aggregate.get("win_rate_pct", 0.0) or 0.0)
                metrics["drawdown"] = float(aggregate.get("max_drawdown_pct", 0.0) or 0.0)
            _apply_mtime(auto_governance_file)
        except Exception:
            pass

    if shadow_summary_file.exists():
        try:
            payload = json.loads(shadow_summary_file.read_text(encoding="utf-8"))
            metrics["shadowClosedTrades"] = int(payload.get("shadowClosedTrades", 0) or 0)
            metrics["shadowWinRatePct"] = float(payload.get("shadowWinRatePct", 0.0) or 0.0)
            metrics["shadowMaxDrawdownAbs"] = float(payload.get("shadowMaxDrawdownAbs", 0.0) or 0.0)
            _apply_mtime(shadow_summary_file)
        except Exception:
            pass

    if live_summary_file.exists():
        try:
            payload = json.loads(live_summary_file.read_text(encoding="utf-8"))
            metrics["learnerLiveSelections"] = int(payload.get("learnerLiveSelections", 0) or 0)
            metrics["baselineSelections"] = int(payload.get("baselineSelections", 0) or 0)
            metrics["lastReason"] = str(payload.get("lastReason", "") or "")
            _apply_mtime(live_summary_file)
        except Exception:
            pass

    return metrics


def _write_training_status_payload(payload: Dict[str, Any]) -> pathlib.Path:
    status_file = DATA_DIR / "offline" / "training_status.json"
    status_file.parent.mkdir(parents=True, exist_ok=True)
    merged = _training_status_defaults()
    merged.update(payload)
    merged["activeDataset"] = _normalize_training_dataset(str(merged.get("activeDataset", "")))
    merged["analysisModel"] = _normalize_analysis_model(str(merged.get("analysisModel", merged.get("activeModel", ""))))
    merged["activeModel"] = _normalize_analysis_model(str(merged.get("activeModel", merged["analysisModel"])))
    merged["trainingEngine"] = {
        **TRAINING_RUNTIME_CONFIG,
        **(merged.get("trainingEngine", {}) if isinstance(merged.get("trainingEngine"), dict) else {}),
    }
    quality_gate = merged.get("qualityGate", {}) if isinstance(merged.get("qualityGate", {}), dict) else {}
    if quality_gate:
        merged["usableTradeCount"] = int(quality_gate.get("usable_trade_count", merged.get("usableTradeCount", 0)) or 0)
        merged["excludedIncompleteCount"] = int(quality_gate.get("excluded_incomplete_count", merged.get("excludedIncompleteCount", 0)) or 0)
        merged["excludedRecoveredCount"] = int(quality_gate.get("excluded_recovered_count", merged.get("excludedRecoveredCount", 0)) or 0)
        merged["samplePolicy"] = quality_gate.get("sample_policy", merged.get("samplePolicy", {}))
        merged["qualitySnapshotId"] = str(quality_gate.get("snapshot_id", merged.get("qualitySnapshotId", "")) or "")
        merged["qualitySnapshotAt"] = str(quality_gate.get("generated_at", merged.get("qualitySnapshotAt", "")) or "")
    status_file.write_text(json.dumps(merged, indent=2, ensure_ascii=False))
    return status_file


def _mark_training_autopilot(
    *,
    stage: str,
    message: str,
    enabled: Optional[bool] = None,
    error: str = "",
) -> Dict[str, Any]:
    status = _read_training_status_payload()
    if enabled is not None:
        status["autoPilotEnabled"] = enabled
    status["autoPilotStage"] = stage
    status["autoPilotMessage"] = message
    status["autoPilotLastActionAt"] = datetime.datetime.now().isoformat()
    status["autoPilotLastError"] = error
    _write_training_status_payload(status)
    return status


def _training_autopilot_ready(status: Dict[str, Any], min_interval_sec: int = 8) -> bool:
    last_action_at = str(status.get("autoPilotLastActionAt", "")).strip()
    if not last_action_at:
        return True
    try:
        last_dt = datetime.datetime.fromisoformat(last_action_at)
    except Exception:
        return True
    return (datetime.datetime.now() - last_dt).total_seconds() >= min_interval_sec


def _run_training_autopilot_step():
    status = _read_training_status_payload()
    approval = None
    if not status.get("autoPilotEnabled", False):
        approval = get_training_approval_chain()
        if not _should_enable_training_autopilot(status, approval):
            decision = approval.get("decision", {}) if isinstance(approval.get("decision", {}), dict) else {}
            recommended_actions = decision.get("recommendedActions", []) if isinstance(decision.get("recommendedActions", []), list) else []
            review_pending = bool(decision.get("reviewPending", False))
            can_promote = bool(decision.get("canPromote", False))
            wait_only_actions = [
                action for action in recommended_actions
                if isinstance(action, str) and action.startswith("wait_or_improve:")
            ]
            if review_pending:
                _mark_training_autopilot(
                    stage="await_manual_review",
                    message="自动流程已推进到人工复核闸门，请人工确认后再决定是否正式推进",
                    enabled=False,
                )
            elif can_promote:
                _mark_training_autopilot(
                    stage="await_manual_promote",
                    message=f"自动流程已完成分析/修复/校验，可人工决定是否推进到 {decision.get('promotionTarget', '下一阶段')}",
                    enabled=False,
                )
            elif wait_only_actions:
                _mark_training_autopilot(
                    stage="waiting_gates",
                    message=str(decision.get("nextStep", "当前只剩门槛等待项，自动流程暂不执行动作")),
                    enabled=False,
                )
            return
        _mark_training_autopilot(
            stage="reactivated",
            message="检测到待处理训练闭环，已自动恢复后台推进",
            enabled=True,
        )
        status = _read_training_status_payload()
    if status.get("isTraining", False):
        if status.get("autoPilotStage") != "training_running":
            _mark_training_autopilot(
                stage="training_running",
                message="训练进行中，等待离线训练完成后自动进入分析/修复/校验链路",
                enabled=True,
            )
        return
    if not _training_autopilot_ready(status):
        return

    approval = approval if isinstance(approval, dict) else get_training_approval_chain()
    decision = approval.get("decision", {}) if isinstance(approval, dict) else {}
    recommended_actions = decision.get("recommendedActions", []) if isinstance(decision, dict) else []
    dynamic_trigger = decision.get("dynamicTrigger", {}) if isinstance(decision.get("dynamicTrigger", {}), dict) else {}
    dynamic_action = str(dynamic_trigger.get("recommendedNextAction", "") or "")
    can_promote = bool(decision.get("canPromote", False))
    review_pending = bool(decision.get("reviewPending", False))
    decision_status = str(decision.get("status", "") or "")
    candidate_value = decision.get("candidateValue", {}) if isinstance(decision.get("candidateValue", {}), dict) else {}
    candidate_invalid = not bool(candidate_value.get("ready_for_manual_review", True))
    suggestion_stage = str(approval.get("stage", "NO_SUGGESTION"))
    has_training_output = bool(status.get("currentEpoch", 0)) or (DATA_DIR / "offline" / "best_params.json").exists()


    if dynamic_action == "run_offline_training" and not has_training_output:
        _mark_training_autopilot(
            stage="starting_training",
            message="动态触发阈值已满足，正在自动启动离线训练",
            enabled=True,
        )
        result = start_training()
        if not result.get("success", False):
            _mark_training_autopilot(
                stage="waiting_dynamic_trigger",
                message=str(result.get("message", "动态阈值建议训练，但当前未能启动训练")),
                enabled=False,
                error=str(result.get("message", "")),
            )
        return

    if suggestion_stage == "NO_SUGGESTION" and has_training_output and dynamic_action in {"generate_suggestion", "approve_to_shadow"}:
        _mark_training_autopilot(
            stage="generating_suggestion",
            message="训练已完成且动态阈值通过，正在自动生成新的进化建议",
            enabled=True,
        )
        result = evolve_training_candidate(
            TrainingEvolveRequest(model=status.get("analysisModel", DEFAULT_ANALYSIS_MODEL))
        )
        if not result.get("success", False):
            _mark_training_autopilot(
                stage="error",
                message="自动生成进化建议失败，请查看错误信息",
                enabled=True,
                error=str(result.get("error", result.get("message", "未知错误"))),
            )
        return

    wait_only_actions = [
        action for action in recommended_actions
        if isinstance(action, str) and action.startswith("wait_or_improve:")
    ]
    has_repair_actions = any(
        isinstance(action, str)
        and (action.startswith("repair:") or action == "rebuild_shadow_params")
        for action in recommended_actions
    )
    needs_repair = dynamic_action == "repair_blockers" or any(
        isinstance(action, str)
        and (
            action.startswith("repair:")
            or action.startswith("wait_or_improve:")
            or action == "rebuild_shadow_params"
        )
        for action in recommended_actions
    )
    if needs_repair and has_repair_actions:
        _mark_training_autopilot(
            stage="repairing",
            message="检测到阻断项，正在自动执行修复与影子参数重建",
            enabled=True,
        )
        result = repair_training_blockers()
        if not result.get("success", False):
            _mark_training_autopilot(
                stage="error",
                message="自动修复阻断项失败",
                enabled=True,
                error=str(result.get("message", result.get("error", "未知错误"))),
            )
            return
        run_training_promotion_check()
        return

    # 仅剩等待型门槛时，不再反复触发 repair_blockers 死循环。
    if wait_only_actions and not has_repair_actions:
        _mark_training_autopilot(
            stage="waiting_gates",
            message=f"等待型门槛未达成（{len(wait_only_actions)}项），暂停自动修复，等待新交易样本/收益因子",
            enabled=True,
        )
        return

    if dynamic_action == "collect_more_evidence":
        _mark_training_autopilot(
            stage="waiting_dynamic_evidence",
            message="动态触发阈值判定当前证据仍不足，暂停自动动作并等待更多交易样本",
            enabled=False,
        )
        return

    if dynamic_action == "freeze_and_monitor":
        _mark_training_autopilot(
            stage="frozen_by_dynamic_trigger",
            message="动态触发阈值进入冻结态，暂停训练推进，仅保留监控与基线保护",
            enabled=False,
        )
        return

    if suggestion_stage == "PENDING_APPROVAL" and not review_pending and (
        "approve_to_shadow" in recommended_actions or dynamic_action == "approve_to_shadow"
    ):
        _mark_training_autopilot(
            stage="approving_observe",
            message="动态触发阈值通过，正在自动把训练建议推进到可回滚观察态",
            enabled=True,
        )
        result = approve_evolution_action()
        if not result.get("success", False):
            _mark_training_autopilot(
                stage="error",
                message="自动推进观察态失败",
                enabled=True,
                error=str(result.get("message", result.get("error", "未知错误"))),
            )
        return

    if review_pending:
        _mark_training_autopilot(
            stage="await_manual_review",
            message="自动流程已推进到人工复核闸门，请人工确认后再决定是否正式推进",
            enabled=False,
        )
        return

    if can_promote:
        _mark_training_autopilot(
            stage="await_manual_promote",
            message=f"自动流程已完成分析/修复/校验，可人工决定是否推进到 {decision.get('promotionTarget', '下一阶段')}",
            enabled=False,
        )
        return

    _mark_training_autopilot(
        stage="waiting_gates",
        message=str(decision.get("nextStep", "自动流程已完成当前可执行动作，等待新的门槛变化")),
        enabled=False,
    )


def _extract_param_map(payload: Dict[str, Any]) -> Dict[str, Any]:
    if not isinstance(payload, dict):
        return {}
    try:
        from src.param_flow import extract_param_map as _extract_runtime_param_map
        return _extract_runtime_param_map(payload)
    except Exception:
        return {}


def _effective_param_source_path(source: str, strategy_file: pathlib.Path) -> str:
    source_map: dict[str, pathlib.Path] = {
        "override_config": config_manager.override_config_path(),
        "effective_runtime": config_manager.effective_config_path(),
        "baseline_config": strategy_file,
        "schema_default": REPO_ROOT / "src" / "strategy_params_schema.py",
    }
    path = source_map.get(str(source or ""))
    return str(path) if path is not None else ""


def _normalize_pct_value(value: Any) -> float:
    try:
        raw = abs(float(value or 0.0))
    except Exception:
        return 0.0
    # Legacy/runtime branches may occasionally persist ratio-form decimals.
    if 0.0 < raw <= 1.0:
        raw *= 100.0
    return round(raw, 4)


def _build_effective_runtime_params_payload() -> dict[str, Any]:
    strategy_file, _strategy_data = _load_strategy_rules_for_update()
    tracked_defaults = {
        "risk_level": "balanced",
        "max_position_size": 0.10,
        "max_open_positions": 1,
        "max_positions": 1,
        "sl_pips": 40,
        "tp_pips": 80,
        "human_in_the_loop": False,
        "min_confidence": 70.0,
        "min_confidence_threshold": 0.70,
        "account_total_loss_stop_pct": 30.0,
    }
    raw_snapshot = config_manager.get_effective_params_snapshot(tracked_defaults)
    source_map = (
        raw_snapshot.get("sources", {})
        if isinstance(raw_snapshot.get("sources", {}), dict)
        else {}
    )

    def _coerce_pct(value: Any, fallback: float) -> float:
        raw = float(value or fallback)
        return round(raw if raw > 1.0 else raw * 100.0, 1)

    def _coerce_ratio(value: Any, fallback: float) -> float:
        raw = float(value or fallback)
        return round(raw / 100.0 if raw > 1.0 else raw, 4)

    min_confidence_pct = _coerce_pct(raw_snapshot.get("min_confidence"), 70.0)
    min_confidence_threshold = _coerce_ratio(
        raw_snapshot.get("min_confidence_threshold", raw_snapshot.get("min_confidence")),
        0.70,
    )
    runtime_roots = _active_trader_runtime_roots()
    runtime_storage_roots = []
    for root in runtime_roots:
        storage_root = _managed_storage_root_for_runtime_root(root) or _normalize_runtime_log_root(root)
        runtime_storage_roots.append(str(storage_root))

    field_sources = {
        "riskLevel": {
            "param": "risk_level",
            "source": str(source_map.get("risk_level", "") or "unknown"),
            "sourcePath": _effective_param_source_path(
                str(source_map.get("risk_level", "") or ""),
                strategy_file,
            ),
        },
        "maxPositionSize": {
            "param": "max_position_size",
            "source": str(source_map.get("max_position_size", "") or "unknown"),
            "sourcePath": _effective_param_source_path(
                str(source_map.get("max_position_size", "") or ""),
                strategy_file,
            ),
        },
        "maxOpenPositions": {
            "param": "max_open_positions",
            "source": str(
                source_map.get("max_open_positions")
                or source_map.get("max_positions")
                or ""
            )
            or "unknown",
            "sourcePath": _effective_param_source_path(
                str(
                    source_map.get("max_open_positions")
                    or source_map.get("max_positions")
                    or ""
                )
                or "",
                strategy_file,
            ),
        },
        "minConfidencePct": {
            "param": "min_confidence",
            "source": str(
                source_map.get("min_confidence")
                or source_map.get("min_confidence_threshold")
                or ""
            )
            or "unknown",
            "sourcePath": _effective_param_source_path(
                str(
                    source_map.get("min_confidence")
                    or source_map.get("min_confidence_threshold")
                    or ""
                )
                or "",
                strategy_file,
            ),
        },
        "slPips": {
            "param": "sl_pips",
            "source": str(source_map.get("sl_pips", "") or "unknown"),
            "sourcePath": _effective_param_source_path(
                str(source_map.get("sl_pips", "") or ""),
                strategy_file,
            ),
        },
        "tpPips": {
            "param": "tp_pips",
            "source": str(source_map.get("tp_pips", "") or "unknown"),
            "sourcePath": _effective_param_source_path(
                str(source_map.get("tp_pips", "") or ""),
                strategy_file,
            ),
        },
        "humanInTheLoop": {
            "param": "human_in_the_loop",
            "source": str(source_map.get("human_in_the_loop", "") or "unknown"),
            "sourcePath": _effective_param_source_path(
                str(source_map.get("human_in_the_loop", "") or ""),
                strategy_file,
            ),
        },
        "accountTotalLossStopPct": {
            "param": "account_total_loss_stop_pct",
            "source": str(
                source_map.get("account_total_loss_stop_pct", "") or "unknown"
            ),
            "sourcePath": _effective_param_source_path(
                str(source_map.get("account_total_loss_stop_pct", "") or ""),
                strategy_file,
            ),
        },
    }

    runtime_identity = {
        "backendPid": os.getpid(),
        "backendFile": str(pathlib.Path(__file__).resolve()),
        "appDir": str(APP_DIR),
        "storageRoot": str(config_manager._get_storage_root()),
        "configDir": str(CONFIG_DIR),
        "dataDir": str(DATA_DIR),
        "logsDir": str(LOGS_DIR),
        "activeTraderRuntimeRoots": [str(root) for root in runtime_roots],
        "activeTraderStorageRoots": runtime_storage_roots,
        "pythonFrozen": bool(getattr(sys, "frozen", False)),
    }
    status_ready = bool(runtime_identity["backendPid"]) and bool(
        runtime_identity["storageRoot"]
    )

    payload = {
        "success": True,
        "status": {
            "ready": status_ready,
            "severity": "ok" if status_ready else "error",
            "reason": "" if status_ready else "RUNTIME_IDENTITY_UNAVAILABLE",
        },
        "governor": {
            "riskLevel": str(raw_snapshot.get("risk_level", "balanced") or "balanced"),
            "maxPositionSize": round(
                float(raw_snapshot.get("max_position_size", 0.10) or 0.10),
                4,
            ),
            "maxOpenPositions": int(
                raw_snapshot.get("max_open_positions", raw_snapshot.get("max_positions", 1))
                or 1
            ),
            "minConfidencePct": min_confidence_pct,
            "minConfidenceThreshold": min_confidence_threshold,
            "slPips": int(raw_snapshot.get("sl_pips", 40) or 40),
            "tpPips": int(raw_snapshot.get("tp_pips", 80) or 80),
            "humanInTheLoop": bool(raw_snapshot.get("human_in_the_loop", False)),
            "accountTotalLossStopPct": round(
                float(raw_snapshot.get("account_total_loss_stop_pct", 30.0) or 30.0),
                2,
            ),
        },
        "fieldSources": field_sources,
        "runtimeIdentity": runtime_identity,
        "sourceFiles": {
            "overrideConfigFile": str(config_manager.override_config_path()),
            "baselineConfigFile": str(strategy_file),
            "effectiveRuntimeFile": str(config_manager.effective_config_path()),
            "schemaDefaultsFile": str(REPO_ROOT / "src" / "strategy_params_schema.py"),
        },
    }
    return _attach_projection_metadata(
        payload,
        snapshot_name="runtime_effective_params",
        source_paths={
            **_runtime_projection_source_paths(),
            "override_config": config_manager.override_config_path(),
        },
    )

@app.get("/api/params")
def get_params():
    """获取 override 配置视图，不混入 baseline/effective。"""
    raw_data: dict[str, Any] = config_manager.load_override_config()

    effective_max_position_size = float(raw_data.get("max_position_size", 0.10) or 0.10)
    effective_risk_level = _normalize_risk_level(raw_data.get("risk_level") or _risk_level_from_max_position_size(effective_max_position_size))
    profile = _risk_profile_for_level(effective_risk_level)
    effective_max_open_positions = int(raw_data.get("max_open_positions", raw_data.get("max_positions", profile["max_open_positions"])) or profile["max_open_positions"])
    effective_sl_pips = int(raw_data.get("sl_pips", profile["sl_pips"]) or profile["sl_pips"])
    effective_tp_pips = int(raw_data.get("tp_pips", profile["tp_pips"]) or profile["tp_pips"])
    effective_human_in_the_loop = bool(raw_data.get("human_in_the_loop", False))
    effective_min_confidence_raw = float(raw_data.get("min_confidence", raw_data.get("min_confidence_threshold", 0.70)) or 0.70)
    effective_min_confidence_pct = round(
        effective_min_confidence_raw if effective_min_confidence_raw > 1.0 else effective_min_confidence_raw * 100.0,
        1,
    )
    effective_account_loss_stop_pct = float(raw_data.get("account_total_loss_stop_pct", 30.0) or 30.0)
    params = {
        "risk_level": effective_risk_level,
        "max_position_size": effective_max_position_size,
        "max_open_positions": max(1, effective_max_open_positions),
        "sl_pips": max(1, effective_sl_pips),
        "tp_pips": max(1, effective_tp_pips),
        "human_in_the_loop": effective_human_in_the_loop,
        "min_confidence": effective_min_confidence_pct,
        "account_total_loss_stop_pct": effective_account_loss_stop_pct,
    }

    for key, value in raw_data.items():
        if key in {"max_position_size", "min_confidence", "min_confidence_threshold", "risk_level"}:
            continue
        params[key] = value

    params["risk_level"] = effective_risk_level

    return {"success": True, "params": params}


@app.get("/api/runtime/effective-params")
def get_runtime_effective_params():
    """Return the exact effective parameters the serving gui_backend process resolves."""
    return _build_effective_runtime_params_payload()

@app.post("/api/params")
def save_params(params: RiskParams):
    """保存风控与执行 override，不改 baseline。"""
    config_file = config_manager.override_config_path()

    try:
        data = {}
        if config_file.exists():
            try:
                data = json.loads(config_file.read_text(encoding="utf-8"))
            except:
                pass

        payload = params.model_dump()
        selected_risk_level = _normalize_risk_level(payload.get("risk_level"))
        payload["risk_level"] = selected_risk_level
        payload["max_position_size"] = round(max(0.01, float(payload.get("max_position_size", 0.10) or 0.10)), 2)
        payload["max_open_positions"] = max(1, int(payload.get("max_open_positions", 1) or 1))
        payload["sl_pips"] = max(1, int(payload.get("sl_pips", 1) or 1))
        payload["tp_pips"] = max(1, int(payload.get("tp_pips", 1) or 1))
        payload["min_confidence"] = round(min(100.0, max(0.0, float(payload.get("min_confidence", 70.0) or 70.0))), 1)
        payload["account_total_loss_stop_pct"] = round(
            min(100.0, max(0.0, float(payload.get("account_total_loss_stop_pct", 30.0) or 0.0))),
            2,
        )
        payload["min_confidence_threshold"] = round(payload["min_confidence"] / 100.0, 4)
        payload["max_positions"] = payload["max_open_positions"]

        data.update(payload)
        config_file.parent.mkdir(parents=True, exist_ok=True)
        config_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
        refreshed = get_params()
        return {
            "success": True,
            "message": "参数已成功保存",
            "params": refreshed.get("params", payload) if isinstance(refreshed, dict) else payload,
        }
    except Exception as e:
        return {"success": False, "error": str(e)}

@app.post("/api/params/reset")
def reset_params():
    """重置 override 配置，回归 baseline/effective。"""
    config_file = config_manager.override_config_path()
    try:
        config_file.parent.mkdir(parents=True, exist_ok=True)
        config_file.write_text("{}\n", encoding="utf-8")
        return {"success": True, "message": "已成功清除 override 配置,回归 baseline/effective 模式"}
    except Exception as e:
        return {"success": False, "error": str(e)}

@app.post("/api/params/apply_best")
def apply_best_params():
    """将离线训练最优参数(best_params.json)写入effective_params.json,立即生效"""
    import time
    best_file = DATA_DIR / "offline" / "best_params.json"
    eff_file = config_manager.effective_config_path()
    try:
        if not best_file.exists():
            return {"success": False, "error": "离线训练结果不存在,请先运行训练"}
        best = json.loads(best_file.read_text(encoding="utf-8"))
        bp   = best.get("best_params", {})
        stats = best.get("stats", {})
        existing_payload = _load_effective_params_payload()
        suggestions = []
        for param, val in bp.items():
            suggestions.append({
                "id": "TRAINING_BEST",
                "param": param,
                "applied_value": val,
                "suggested_value": val,
                "applied": True,
                "auto_apply": True,
                "note": f"来源:离线训练({stats.get('total_trades',0)}笔,胜率{stats.get('win_rate',0):.1f}%)"
            })
        eff_data = {
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S.000000+00:00"),
            "source": "training_best",
            "approved_count": len(suggestions),
            "params": dict(bp),
            "suggestions": suggestions,
            "offline_params": {
                **bp,
                "review_pending": False,
            },
            "quality_context": {
                "source": "training_best",
                "total_trades": stats.get("total_trades", 0),
                "win_rate": stats.get("win_rate", 0),
                "total_pnl": stats.get("total_pnl", 0),
                "avg_trade_pnl": stats.get("avg_trade_pnl", 0),
            },
            "current": {
                "params": dict(bp),
                "stats": dict(stats) if isinstance(stats, dict) else {},
                "method": "training_best",
                "applied_at": time.strftime("%Y-%m-%dT%H:%M:%S.000000+00:00"),
                "auto_apply": True,
                "applied": True,
                "source": "training_best",
            },
            "history": [],
        }
        if isinstance(existing_payload.get("runtime_scheduler", {}), dict):
            eff_data["runtime_scheduler"] = dict(existing_payload.get("runtime_scheduler", {}))
        if existing_payload.get("strategy_type"):
            eff_data["strategy_type"] = existing_payload.get("strategy_type")
        if existing_payload.get("strategy_switched_at"):
            eff_data["strategy_switched_at"] = existing_payload.get("strategy_switched_at")
        eff_file.write_text(json.dumps(eff_data, indent=2, ensure_ascii=False), encoding="utf-8")
        return {
            "success": True,
            "message": f"已应用训练最优参数({len(suggestions)}项)",
            "params": bp,
            "stats": {"total_trades": stats.get("total_trades"), "win_rate": stats.get("win_rate"), "total_pnl": stats.get("total_pnl")}
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


# ── Strategy Hot-Reload ────────────────────────────────────────────────────

VALID_STRATEGIES = {
    "ema_macd":      "EMA+MACD+RSI+BB 综合趋势策略",
    "wyckoff":       "Wyckoff 量价结构（Spring / SOS / SOW）",
    "composite":     "多策略加权聚合（多策略共振）",
    "rsi_extreme":   "RSI极端值均值回归",
    "boll_band":     "布林带均值回归",
    "adx_breakout":  "ADX量能突破",
    "pullback_fade": "趋势回调衰竭",
    "grid_osc":      "震荡网格挂单",
    "key_level":     "关键位反转",
}

_TEMPLATE_RESERVED_KEYS = {"strategy_id", "version", "runtime_scheduler"}
_TEMPLATE_TIMEFRAME_ALLOWED = {"M1", "M5", "M15", "H1"}


def _sanitize_strategy_template_payload(template: Any) -> dict[str, Any]:
    if not isinstance(template, dict):
        return {}
    sanitized: dict[str, Any] = {}
    for key, value in template.items():
        key_text = str(key)
        if key_text in _TEMPLATE_RESERVED_KEYS:
            continue
        sanitized[key_text] = value
    return sanitized


def _normalize_strategy_template_payload(template: Any) -> dict[str, Any]:
    payload = _sanitize_strategy_template_payload(template)
    normalized = dict(payload)

    def _coerce_float(value: Any) -> Optional[float]:
        try:
            number = float(value)
        except Exception:
            return None
        return number if math.isfinite(number) else None

    if "risk_per_trade" not in normalized:
        legacy_risk_multiplier = _coerce_float(payload.get("risk_multiplier"))
        if legacy_risk_multiplier is not None:
            normalized["risk_per_trade"] = round(0.01 * legacy_risk_multiplier, 4)

    if "atr_sl_mult" not in normalized:
        legacy_sl_multiplier = _coerce_float(payload.get("sl_multiplier"))
        if legacy_sl_multiplier is not None:
            normalized["atr_sl_mult"] = round(1.8 * legacy_sl_multiplier, 4)

    if "tp_rr" not in normalized:
        legacy_tp_multiplier = _coerce_float(payload.get("tp_multiplier"))
        if legacy_tp_multiplier is not None:
            normalized["tp_rr"] = round(2.0 * legacy_tp_multiplier, 4)

    return normalized


def _strategy_taxonomy_profile(strategy_id: str) -> dict[str, Any]:
    sid = str(strategy_id or "").strip().lower()
    base = {
        "strategy_style": "trend",
        "execution_role": "primary",
        "applicable_markets": ["TRENDING"],
        "applicable_sessions": ["LONDON", "NEW_YORK"],
        "conflict_groups": ["trend"],
        "coordination_tags": ["TREND"],
    }
    profiles: dict[str, dict[str, Any]] = {
        "ema_macd": {
            "strategy_style": "trend",
            "execution_role": "primary",
            "applicable_markets": ["TRENDING"],
            "applicable_sessions": ["LONDON", "NEW_YORK"],
            "conflict_groups": ["trend"],
            "coordination_tags": ["TREND", "EMA_MACD"],
        },
        "wyckoff": {
            "strategy_style": "breakout",
            "execution_role": "primary",
            "applicable_markets": ["TRENDING", "TRANSITION"],
            "applicable_sessions": ["LONDON", "NEW_YORK"],
            "conflict_groups": ["breakout", "trend"],
            "coordination_tags": ["BREAKOUT", "VOLUME_STRUCTURE"],
        },
        "composite": {
            "strategy_style": "momentum",
            "execution_role": "primary",
            "applicable_markets": ["TRENDING", "TRANSITION"],
            "applicable_sessions": ["LONDON", "NEW_YORK"],
            "conflict_groups": ["composite", "momentum"],
            "coordination_tags": ["COMPOSITE", "MULTI_SIGNAL"],
        },
        "rsi_extreme": {
            "strategy_style": "mean_reversion",
            "execution_role": "primary",
            "applicable_markets": ["RANGING"],
            "applicable_sessions": ["ASIA", "LONDON"],
            "conflict_groups": ["mean_reversion"],
            "coordination_tags": ["MEAN_REVERSION", "RSI"],
        },
        "boll_band": {
            "strategy_style": "mean_reversion",
            "execution_role": "primary",
            "applicable_markets": ["RANGING"],
            "applicable_sessions": ["ASIA", "LONDON"],
            "conflict_groups": ["mean_reversion"],
            "coordination_tags": ["MEAN_REVERSION", "BOLLINGER"],
        },
        "adx_breakout": {
            "strategy_style": "breakout",
            "execution_role": "primary",
            "applicable_markets": ["TRENDING"],
            "applicable_sessions": ["LONDON", "NEW_YORK"],
            "conflict_groups": ["breakout", "trend"],
            "coordination_tags": ["BREAKOUT", "ADX"],
        },
        "pullback_fade": {
            "strategy_style": "trend",
            "execution_role": "primary",
            "applicable_markets": ["TRENDING"],
            "applicable_sessions": ["LONDON", "NEW_YORK"],
            "conflict_groups": ["trend", "pullback"],
            "coordination_tags": ["TREND", "PULLBACK"],
        },
        "grid_osc": {
            "strategy_style": "mean_reversion",
            "execution_role": "primary",
            "applicable_markets": ["RANGING", "LOW_VOLATILITY"],
            "applicable_sessions": ["ASIA"],
            "conflict_groups": ["grid", "mean_reversion"],
            "coordination_tags": ["GRID", "OSCILLATION"],
        },
        "key_level": {
            "strategy_style": "mean_reversion",
            "execution_role": "primary",
            "applicable_markets": ["RANGING", "TRANSITION"],
            "applicable_sessions": ["LONDON", "NEW_YORK"],
            "conflict_groups": ["reversal", "mean_reversion"],
            "coordination_tags": ["LEVEL", "REVERSAL"],
        },
    }
    profile = profiles.get(sid, base)
    return {
        "strategy_style": str(profile.get("strategy_style", base["strategy_style"])),
        "execution_role": str(profile.get("execution_role", base["execution_role"])),
        "applicable_markets": [str(item) for item in profile.get("applicable_markets", base["applicable_markets"])],
        "applicable_sessions": [str(item) for item in profile.get("applicable_sessions", base["applicable_sessions"])],
        "conflict_groups": [str(item) for item in profile.get("conflict_groups", base["conflict_groups"])],
        "coordination_tags": [str(item) for item in profile.get("coordination_tags", base["coordination_tags"])],
    }


def _apply_strategy_taxonomy_to_template(strategy_id: str, template: dict[str, Any]) -> dict[str, Any]:
    normalized = dict(template) if isinstance(template, dict) else {}
    profile = _strategy_taxonomy_profile(strategy_id)
    normalized["strategy_style"] = profile["strategy_style"]
    normalized["execution_role"] = profile["execution_role"]
    normalized["applicable_markets"] = list(profile["applicable_markets"])
    normalized["applicable_sessions"] = list(profile["applicable_sessions"])
    normalized["conflict_groups"] = list(profile["conflict_groups"])
    normalized["coordination_tags"] = list(profile["coordination_tags"])

    runtime_context = (
        dict(normalized.get("runtime_context", {}))
        if isinstance(normalized.get("runtime_context", {}), dict)
        else {}
    )
    runtime_context["applicable_markets"] = list(profile["applicable_markets"])
    runtime_context["applicable_sessions"] = list(profile["applicable_sessions"])
    normalized["runtime_context"] = runtime_context

    coordination_contract = (
        dict(normalized.get("coordination_contract", {}))
        if isinstance(normalized.get("coordination_contract", {}), dict)
        else {}
    )
    coordination_contract["strategy_style"] = profile["strategy_style"]
    coordination_contract["execution_role"] = profile["execution_role"]
    coordination_contract["conflict_groups"] = list(profile["conflict_groups"])
    coordination_contract["coordination_tags"] = list(profile["coordination_tags"])
    normalized["coordination_contract"] = coordination_contract
    return normalized


def _strategy_market_state_file() -> pathlib.Path:
    return _resolve_runtime_artifact("config", "strategy_market_state.json") or (CONFIG_DIR / "strategy_market_state.json")


def _strategy_market_state_seed_candidates() -> list[pathlib.Path]:
    state_file = _strategy_market_state_file()
    candidates: list[pathlib.Path] = []
    seen: set[str] = set()
    # Check standard config paths AND PyInstaller _internal/ path
    for root in (RELEASE_ROOT, REPO_ROOT):
        for subdir in ("config", "_internal/config"):
            candidate = pathlib.Path(root) / subdir / state_file.name
            if not candidate.exists():
                continue
            key = str(candidate.resolve())
            if key in seen or candidate == state_file:
                continue
            seen.add(key)
            candidates.append(candidate)
    return candidates


def _bootstrap_missing_strategy_market_state(
    state_file: pathlib.Path,
) -> Optional[dict[str, Any]]:
    for candidate in _strategy_market_state_seed_candidates():
        if not candidate.exists():
            continue
        try:
            payload = json.loads(candidate.read_text(encoding="utf-8"))
        except Exception:
            continue
        if not isinstance(payload, dict):
            continue
        try:
            _write_json_atomic(state_file, payload)
        except Exception:
            pass
        _record_system_message(
            kind="file_repair",
            title="策略市场状态已回填",
            desc=f"检测到 `{state_file.name}` 缺失，系统已从当前运行版本回填策略市场状态。",
            color="warning",
            source="state_bootstrap",
            metadata={
                "file": str(state_file),
                "filename": state_file.name,
                "seed_file": str(candidate),
                "reason": "missing_strategy_market_state",
            },
        )
        return payload
    return None


def _default_strategy_template(strategy_id: str) -> dict[str, Any]:
    template = {
        "contract_version": "1",
        "template_kind": "strategy_runtime_contract",
        "timeframe": "M5",
        "risk_per_trade": 0.01,
        "risk_multiplier": 1.0,
        "atr_sl_mult": 1.8,
        "sl_multiplier": 1.0,
        "tp_rr": 2.0,
        "tp_multiplier": 1.0,
        "max_positions": 1,
        "reduce_only": False,
        "strategy_style": "trend",
        "execution_role": "primary",
        "applicable_markets": ["TRENDING"],
        "applicable_sessions": ["LONDON", "NEW_YORK"],
        "conflict_groups": ["trend"],
        "coordination_tags": ["TREND"],
        "max_active_strategies": 2,
        "conflict_hold_threshold": 0.35,
        "allow_same_group_stack": False,
        "allow_counter_style_mix": False,
        "execution_baseline": {
            "timeframe": "M5",
            "risk_per_trade": 0.01,
            "atr_sl_mult": 1.8,
            "tp_rr": 2.0,
            "max_positions": 1,
            "reduce_only": False,
        },
        "runtime_context": {
            "applicable_markets": ["TRENDING"],
            "applicable_sessions": ["LONDON", "NEW_YORK"],
        },
        "coordination_contract": {
            "strategy_style": "trend",
            "execution_role": "primary",
            "conflict_groups": ["trend"],
            "coordination_tags": ["TREND"],
            "max_active_strategies": 2,
            "conflict_hold_threshold": 0.35,
            "allow_same_group_stack": False,
            "allow_counter_style_mix": False,
        },
    }
    return _apply_strategy_taxonomy_to_template(strategy_id, template)


def _clamp_allocation(value: Any) -> float:
    try:
        number = float(value)
    except Exception:
        number = 0.0
    return round(min(1.0, max(0.0, number)), 4)


def _load_effective_params_payload() -> dict[str, Any]:
    eff_file = _resolve_runtime_artifact("logs", "effective_params.json") or (LOGS_DIR / "effective_params.json")
    if not eff_file.exists():
        return {}
    try:
        payload = json.loads(eff_file.read_text(encoding="utf-8"))
        return payload if isinstance(payload, dict) else {}
    except Exception:
        return {}


def _effective_config_revision(payload: Optional[dict[str, Any]] = None) -> str:
    effective_payload = payload if isinstance(payload, dict) else _load_effective_params_payload()
    for key in ("config_revision", "revision", "synced_at", "generated_at"):
        value = effective_payload.get(key)
        if value:
            return str(value)
    return "manual"


def _normalize_runtime_instance(
    item: dict[str, Any],
    strategy_templates: Optional[dict[str, dict[str, Any]]] = None,
    config_revision: str = "manual",
) -> dict[str, Any]:
    strategy_id = str(item.get("strategy_id", "")).strip()
    template_map = strategy_templates if isinstance(strategy_templates, dict) else {}
    template_snapshot = (
        template_map.get(strategy_id, {})
        if isinstance(template_map.get(strategy_id, {}), dict)
        else {}
    )
    instance_id = str(item.get("instance_id", "")).strip() or f"inst_{strategy_id}_{int(time.time())}"
    status = str(item.get("status", "running") or "running").lower()
    if status not in {"running", "paused"}:
        status = "running"
    allocation = _clamp_allocation(item.get("allocation", 0.0))
    param_overrides = item.get("param_overrides", {})
    metadata = item.get("metadata", {})
    normalized = {
        "instance_id": instance_id,
        "strategy_id": strategy_id,
        "strategy_name": str(item.get("strategy_name", strategy_id) or strategy_id),
        "strategy_version": str(item.get("strategy_version", item.get("version", "legacy")) or "legacy"),
        "allocation": allocation,
        "status": status,
        "role": str(item.get("role", "live_support") or "live_support"),
        "config_revision": str(item.get("config_revision", config_revision) or config_revision),
        "param_overrides": param_overrides if isinstance(param_overrides, dict) else {},
        "template_snapshot": (
            item.get("template_snapshot", template_snapshot)
            if isinstance(item.get("template_snapshot", template_snapshot), dict)
            else template_snapshot
        ),
        "deployed_at": str(item.get("deployed_at", "") or ""),
        "updated_at": str(item.get("updated_at", "") or ""),
        "metadata": metadata if isinstance(metadata, dict) else {},
    }
    return normalized


def _validate_strategy_template_payload(template: dict[str, Any]) -> Optional[str]:
    _style_allowed = {"trend", "breakout", "mean_reversion", "momentum"}
    _execution_role_allowed = {"primary", "support", "confirm", "auto_infer"}
    _section_keys = {
        "execution_baseline",
        "runtime_context",
        "coordination_contract",
    }

    def _normalized_list(value: Any) -> Optional[list[str]]:
        if value is None:
            return None
        if not isinstance(value, list):
            raise ValueError("must_be_list")
        normalized = [str(item).strip() for item in value if str(item).strip()]
        return normalized

    def _bounded_number(value: Any, *, field: str, low: float, high: float) -> float:
        try:
            number = float(value)
        except Exception:
            raise ValueError(f"{field} 必须是数值")
        if number < low or number > high:
            raise ValueError(f"{field} 超出允许范围 [{low}, {high}]")
        return number

    def _optional_bool(value: Any, *, field: str) -> None:
        if value is not None and not isinstance(value, bool):
            raise ValueError(f"{field} 必须是布尔值")

    def _check_section(section_name: str) -> dict[str, Any]:
        value = template.get(section_name)
        if value is None:
            return {}
        if not isinstance(value, dict):
            raise ValueError(f"{section_name} 必须是对象")
        return value

    def _ensure_consistent(top_key: str, nested_value: Any, *, label: str) -> None:
        if top_key in template and nested_value is not None and template.get(top_key) != nested_value:
            raise ValueError(f"{top_key} 与 {label} 不一致")

    def _ensure_list_consistent(top_key: str, nested_value: Optional[list[str]], *, label: str) -> None:
        if top_key not in template or nested_value is None:
            return
        top_value = _normalized_list(template.get(top_key))
        if top_value != nested_value:
            raise ValueError(f"{top_key} 与 {label} 不一致")

    for key in template.keys():
        if str(key) in _TEMPLATE_RESERVED_KEYS:
            return f"模板包含保留键: {key}"
    try:
        for section_key in _section_keys:
            _check_section(section_key)

        execution_baseline = _check_section("execution_baseline")
        runtime_context = _check_section("runtime_context")
        coordination_contract = _check_section("coordination_contract")

        timeframe = template.get("timeframe", execution_baseline.get("timeframe"))
        if timeframe is not None and str(timeframe) not in _TEMPLATE_TIMEFRAME_ALLOWED:
            return "timeframe 仅支持 M1/M5/M15/H1"
        _ensure_consistent("timeframe", execution_baseline.get("timeframe"), label="execution_baseline.timeframe")

        numeric_bounds: list[tuple[str, float, float]] = [
            ("risk_per_trade", 0.001, 0.05),
            ("atr_sl_mult", 0.8, 4.0),
            ("tp_rr", 0.5, 5.0),
            ("max_positions", 1.0, 10.0),
            ("max_active_strategies", 1.0, 10.0),
            ("conflict_hold_threshold", 0.0, 1.0),
        ]
        for key, low, high in numeric_bounds:
            nested_value = None
            if key in {"risk_per_trade", "atr_sl_mult", "tp_rr", "max_positions"}:
                nested_value = execution_baseline.get(key)
            elif key in {"max_active_strategies", "conflict_hold_threshold"}:
                nested_value = coordination_contract.get(key)
            if key in template:
                _bounded_number(template.get(key), field=key, low=low, high=high)
            if nested_value is not None:
                _bounded_number(nested_value, field=f"{key}", low=low, high=high)
            if nested_value is not None and key in template and float(template.get(key)) != float(nested_value):
                raise ValueError(f"{key} 与 {('execution_baseline' if key in {'risk_per_trade', 'atr_sl_mult', 'tp_rr', 'max_positions'} else 'coordination_contract')}.{key} 不一致")

        _optional_bool(template.get("reduce_only"), field="reduce_only")
        _optional_bool(execution_baseline.get("reduce_only"), field="execution_baseline.reduce_only")
        _ensure_consistent("reduce_only", execution_baseline.get("reduce_only"), label="execution_baseline.reduce_only")
        _optional_bool(template.get("allow_same_group_stack"), field="allow_same_group_stack")
        _optional_bool(coordination_contract.get("allow_same_group_stack"), field="coordination_contract.allow_same_group_stack")
        _ensure_consistent(
            "allow_same_group_stack",
            coordination_contract.get("allow_same_group_stack"),
            label="coordination_contract.allow_same_group_stack",
        )
        _optional_bool(template.get("allow_counter_style_mix"), field="allow_counter_style_mix")
        _optional_bool(coordination_contract.get("allow_counter_style_mix"), field="coordination_contract.allow_counter_style_mix")
        _ensure_consistent(
            "allow_counter_style_mix",
            coordination_contract.get("allow_counter_style_mix"),
            label="coordination_contract.allow_counter_style_mix",
        )

        style = str(template.get("strategy_style", coordination_contract.get("strategy_style", "")) or "").strip()
        if style:
            if style not in _style_allowed:
                return "strategy_style 仅支持 trend/breakout/mean_reversion/momentum"
            _ensure_consistent("strategy_style", coordination_contract.get("strategy_style"), label="coordination_contract.strategy_style")

        execution_role = str(template.get("execution_role", coordination_contract.get("execution_role", "")) or "").strip()
        if execution_role:
            if execution_role not in _execution_role_allowed:
                return "execution_role 仅支持 primary/support/confirm/auto_infer"
            _ensure_consistent("execution_role", coordination_contract.get("execution_role"), label="coordination_contract.execution_role")

        applicable_markets = _normalized_list(
            template.get("applicable_markets", runtime_context.get("applicable_markets"))
        )
        applicable_markets = [normalize_regime_label(item) for item in applicable_markets] if applicable_markets is not None else None
        if applicable_markets is not None and len(applicable_markets) == 0:
            return "applicable_markets 至少包含一个市场标签"
        runtime_markets = _normalized_list(runtime_context.get("applicable_markets"))
        runtime_markets = [normalize_regime_label(item) for item in runtime_markets] if runtime_markets is not None else None
        if "applicable_markets" in template and applicable_markets != runtime_markets:
            raise ValueError("applicable_markets 与 runtime_context.applicable_markets 不一致")

        applicable_sessions = _normalized_list(
            template.get("applicable_sessions", runtime_context.get("applicable_sessions"))
        )
        if applicable_sessions is not None and len(applicable_sessions) == 0:
            return "applicable_sessions 至少包含一个时段标签"
        runtime_sessions = _normalized_list(runtime_context.get("applicable_sessions"))
        _ensure_list_consistent("applicable_sessions", runtime_sessions, label="runtime_context.applicable_sessions")

        conflict_groups = _normalized_list(
            template.get("conflict_groups", coordination_contract.get("conflict_groups"))
        )
        if conflict_groups is not None and len(conflict_groups) == 0:
            return "conflict_groups 至少包含一个冲突组"
        contract_conflicts = _normalized_list(coordination_contract.get("conflict_groups"))
        _ensure_list_consistent("conflict_groups", contract_conflicts, label="coordination_contract.conflict_groups")

        coordination_tags = _normalized_list(
            template.get("coordination_tags", coordination_contract.get("coordination_tags"))
        )
        if coordination_tags is not None and len(coordination_tags) == 0:
            return "coordination_tags 至少包含一个协同标签"
        contract_tags = _normalized_list(coordination_contract.get("coordination_tags"))
        _ensure_list_consistent("coordination_tags", contract_tags, label="coordination_contract.coordination_tags")
    except ValueError as exc:
        return str(exc)
    return None


def _default_decision_policy() -> dict[str, Any]:
    panel_min_confidence = config_manager.get_effective_min_confidence_threshold()
    return {
        "policy_version": "1",
        "profile": "agent_guided_multi_strategy",
        "macro": {
            "role": "directional_gate",
            "degraded_policy": "soften_to_neutral",
            "aligned_scale": 1.1,
            "countertrend_scale": 0.35,
            "neutral_scale": 1.0,
            "direction_loss_guard": {
                "enabled": True,
                "lookback": 8,
                "min_samples": 3,
                "aligned_loss_streak_soft": 2,
                "aligned_loss_streak_hard": 3,
                "counter_win_bias_delta": 0.15,
                "soft_scale": 0.55,
                "hard_scale": 0.2,
                "neutralize_on_hard": True,
            },
        },
        "interpreter": {
            "role": "execution_bias_only",
            "buy_scale": 1.0,
            "sell_scale": 1.0,
            "limit_countertrend_scale": 0.5,
            "block_countertrend_scale": 0.0,
        },
        "risk": {
            "role": "active_governor",
            "per_instance": {
                "health_scoring": {
                    "enabled": True,
                    "min_trades_for_scoring": 3,
                    "score_ttl_sec": 30,
                },
                "loss_guard": {
                    "enabled": True,
                    "soft_loss_streak": 2,
                    "hard_loss_streak": 4,
                    "shadow_loss_streak": 5,
                    "soft_scale": 0.55,
                    "hard_scale": 0.25,
                    "rehab_min_trades": 5,
                    "rehab_min_win_rate": 0.50,
                },
                "regime_fitness": {
                    "enabled": True,
                    "min_samples_per_regime": 2,
                    "unfamiliar_regime_scale": 0.6,
                },
            },
            "cross_strategy": {
                "correlation_tracking": {
                    "enabled": True,
                    "divergence_confidence_penalty": 0.4,
                    "divergence_volume_penalty": 0.6,
                    "divergence_cooldown_sec": 120,
                    "alignment_volume_bonus": 0.05,
                },
            },
            "risk_budget": {
                "allocation_method": "health_squared",
                "min_lot_per_instance": 0.01,
                "max_lot_per_instance": 0.20,
            },
        },
        "execution": {
            "aggregation": "weighted_net_confidence",
            "min_confidence": panel_min_confidence,
            "entry_policy": "regime_directed",
        },
        "session_scheduler": _default_session_scheduler_policy(),
        "router": {
            "trend": {
                "primary_strategy_id": "ema_macd",
                "support_strategy_ids": ["pullback_fade", "adx_breakout", "wyckoff"],
                "rank_weights": [0.4, 0.25, 0.2, 0.15],
            },
            "range": {
                "primary_strategy_id": "rsi_extreme",
                "support_strategy_ids": ["boll_band", "key_level", "grid_osc"],
                "rank_weights": [0.35, 0.3, 0.2, 0.15],
            },
            "uncertain": {
                "primary_strategy_id": "composite",
                "support_strategy_ids": ["wyckoff", "key_level", "ema_macd"],
                "rank_weights": [0.4, 0.25, 0.2, 0.15],
            },
        },
    }


def _sync_decision_policy_min_confidence(policy: dict[str, Any]) -> dict[str, Any]:
    execution = policy.get("execution", {}) if isinstance(policy.get("execution", {}), dict) else {}
    execution["min_confidence"] = config_manager.get_effective_min_confidence_threshold()
    policy["execution"] = execution
    return policy


def _persist_panel_min_confidence_threshold(value: Any) -> float:
    threshold = config_manager.normalize_min_confidence_threshold(value)
    config_file = CONFIG_DIR / "risk_governor_rules.json"
    data: dict[str, Any] = {}
    if config_file.exists():
        try:
            loaded = json.loads(config_file.read_text(encoding="utf-8"))
            if isinstance(loaded, dict):
                data = loaded
        except Exception:
            data = {}
    data["min_confidence"] = round(threshold * 100.0, 1)
    data["min_confidence_threshold"] = threshold
    config_file.parent.mkdir(parents=True, exist_ok=True)
    config_file.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")

    strategy_file, strategy_data = _load_strategy_rules_for_update()
    risk_governor = (
        strategy_data.setdefault("risk_governor", {})
        if isinstance(strategy_data, dict)
        else {}
    )
    if not isinstance(risk_governor, dict):
        risk_governor = {}
        strategy_data["risk_governor"] = risk_governor
    risk_governor["min_confidence"] = threshold
    strategy_data["last_updated"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    strategy_file.parent.mkdir(parents=True, exist_ok=True)
    strategy_file.write_text(json.dumps(strategy_data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    return threshold


def _default_session_scheduler_policy() -> dict[str, Any]:
    """Return the default session-aware scheduling contract."""
    return {
        "enabled": False,
        "mode": "soft_weight",
        "allowed_sessions": ["LONDON", "NEW_YORK", "OVERLAP"],
        "blocked_sessions": [],
        "default_when_disabled": {
            "session_as_soft_factor": True,
            "allow_new_entry": True,
        },
        "policies": {
            "ASIA": {
                "enabled": True,
                "allow_new_entry": False,
                "allow_add_position": False,
                "allow_reduce_only": True,
                "risk_scale": 0.3,
                "entry_mode": "observe_only",
                "allowed_strategy_groups": ["mean_reversion"],
                "allowed_strategy_ids": ["rsi_extreme", "boll_band", "key_level"],
                "soft_score_bias": -0.2,
                "min_confidence_override": 0.78,
            },
            "LONDON": {
                "enabled": True,
                "allow_new_entry": True,
                "allow_add_position": True,
                "allow_reduce_only": True,
                "risk_scale": 1.0,
                "entry_mode": "normal",
                "allowed_strategy_groups": ["trend", "breakout"],
                "allowed_strategy_ids": ["ema_macd", "pullback_fade", "adx_breakout", "wyckoff"],
                "soft_score_bias": 0.12,
                "min_confidence_override": 0.62,
            },
            "NEW_YORK": {
                "enabled": True,
                "allow_new_entry": True,
                "allow_add_position": True,
                "allow_reduce_only": True,
                "risk_scale": 0.9,
                "entry_mode": "normal",
                "allowed_strategy_groups": ["trend", "momentum"],
                "allowed_strategy_ids": ["ema_macd", "wyckoff", "composite"],
                "soft_score_bias": 0.08,
                "min_confidence_override": 0.64,
            },
            "OVERLAP": {
                "enabled": True,
                "allow_new_entry": True,
                "allow_add_position": True,
                "allow_reduce_only": True,
                "risk_scale": 1.0,
                "entry_mode": "aggressive",
                "allowed_strategy_groups": ["trend", "breakout", "momentum"],
                "allowed_strategy_ids": ["ema_macd", "pullback_fade", "wyckoff", "composite"],
                "soft_score_bias": 0.15,
                "min_confidence_override": 0.60,
            },
        },
    }


def _normalize_decision_policy(raw: Any) -> dict[str, Any]:
    base = _default_decision_policy()
    payload = raw if isinstance(raw, dict) else {}
    macro = payload.get("macro", {}) if isinstance(payload.get("macro", {}), dict) else {}
    interpreter = payload.get("interpreter", {}) if isinstance(payload.get("interpreter", {}), dict) else {}
    risk = payload.get("risk", {}) if isinstance(payload.get("risk", {}), dict) else {}
    execution = payload.get("execution", {}) if isinstance(payload.get("execution", {}), dict) else {}
    session_scheduler = (
        payload.get("session_scheduler", {})
        if isinstance(payload.get("session_scheduler", {}), dict)
        else {}
    )
    router = payload.get("router", {}) if isinstance(payload.get("router", {}), dict) else {}
    macro_guard = (
        macro.get("direction_loss_guard", {})
        if isinstance(macro.get("direction_loss_guard", {}), dict)
        else {}
    )

    def _bounded_float(value: Any, default: float, *, low: float = 0.0, high: float = 2.0) -> float:
        try:
            return round(max(low, min(high, float(value))), 4)
        except Exception:
            return default

    def _coerce_bool(value: Any, default: bool) -> bool:
        if isinstance(value, bool):
            return value
        if isinstance(value, str):
            lowered = value.strip().lower()
            if lowered in {"1", "true", "yes", "on"}:
                return True
            if lowered in {"0", "false", "no", "off"}:
                return False
        return default

    def _normalize_string_list(value: Any, fallback: list[str]) -> list[str]:
        if not isinstance(value, list):
            return list(fallback)
        items = [str(item).strip().upper() for item in value if str(item).strip()]
        return list(dict.fromkeys(items)) or list(fallback)

    def _normalize_session_policy(raw_policy: Any, base_policy: dict[str, Any]) -> dict[str, Any]:
        policy = raw_policy if isinstance(raw_policy, dict) else {}
        return {
            "enabled": _coerce_bool(policy.get("enabled", base_policy["enabled"]), base_policy["enabled"]),
            "allow_new_entry": _coerce_bool(
                policy.get("allow_new_entry", base_policy["allow_new_entry"]),
                base_policy["allow_new_entry"],
            ),
            "allow_add_position": _coerce_bool(
                policy.get("allow_add_position", base_policy["allow_add_position"]),
                base_policy["allow_add_position"],
            ),
            "allow_reduce_only": _coerce_bool(
                policy.get("allow_reduce_only", base_policy["allow_reduce_only"]),
                base_policy["allow_reduce_only"],
            ),
            "risk_scale": _bounded_float(policy.get("risk_scale", base_policy["risk_scale"]), base_policy["risk_scale"]),
            "entry_mode": str(policy.get("entry_mode", base_policy["entry_mode"]) or base_policy["entry_mode"]),
            "allowed_strategy_groups": _normalize_string_list(
                policy.get("allowed_strategy_groups", base_policy["allowed_strategy_groups"]),
                list(base_policy["allowed_strategy_groups"]),
            ),
            "allowed_strategy_ids": _normalize_string_list(
                policy.get("allowed_strategy_ids", base_policy["allowed_strategy_ids"]),
                list(base_policy["allowed_strategy_ids"]),
            ),
            "soft_score_bias": _bounded_float(
                policy.get("soft_score_bias", base_policy["soft_score_bias"]),
                base_policy["soft_score_bias"],
                low=-1.0,
                high=1.0,
            ),
            "min_confidence_override": _bounded_float(
                policy.get("min_confidence_override", base_policy["min_confidence_override"]),
                base_policy["min_confidence_override"],
                low=0.0,
                high=1.0,
            ),
        }

    def _normalize_router_family(raw_family: Any, base_family: dict[str, Any]) -> dict[str, Any]:
        family = raw_family if isinstance(raw_family, dict) else {}
        primary = str(
            family.get("primary_strategy_id", base_family["primary_strategy_id"])
            or base_family["primary_strategy_id"]
        ).strip() or base_family["primary_strategy_id"]
        supports_raw = family.get("support_strategy_ids", base_family["support_strategy_ids"])
        supports = (
            [str(item).strip() for item in supports_raw if str(item).strip()]
            if isinstance(supports_raw, list)
            else list(base_family["support_strategy_ids"])
        )
        candidate_count = max(1, len([primary, *supports]))
        weights_raw = family.get("rank_weights", base_family["rank_weights"])
        if isinstance(weights_raw, list):
            weights = [
                _bounded_float(
                    weights_raw[idx] if idx < len(weights_raw) else (base_family["rank_weights"][idx] if idx < len(base_family["rank_weights"]) else 0.0),
                    float(base_family["rank_weights"][idx] if idx < len(base_family["rank_weights"]) else 0.0),
                    low=0.0,
                    high=1.0,
                )
                for idx in range(candidate_count)
            ]
        else:
            weights = list(base_family["rank_weights"])
        total = sum(weights)
        if total > 0:
            weights = [round(weight / total, 4) for weight in weights]
        return {
            "primary_strategy_id": primary,
            "support_strategy_ids": supports,
            "rank_weights": weights,
        }

    normalized = {
        "policy_version": str(payload.get("policy_version", base["policy_version"]) or base["policy_version"]),
        "profile": str(payload.get("profile", base["profile"]) or base["profile"]),
        "macro": {
            "role": str(macro.get("role", base["macro"]["role"]) or base["macro"]["role"]),
            "degraded_policy": str(
                macro.get("degraded_policy", base["macro"]["degraded_policy"])
                or base["macro"]["degraded_policy"]
            ),
            "aligned_scale": _bounded_float(
                macro.get("aligned_scale", base["macro"]["aligned_scale"]),
                base["macro"]["aligned_scale"],
            ),
            "countertrend_scale": _bounded_float(
                macro.get("countertrend_scale", base["macro"]["countertrend_scale"]),
                base["macro"]["countertrend_scale"],
            ),
            "neutral_scale": _bounded_float(
                macro.get("neutral_scale", base["macro"]["neutral_scale"]),
                base["macro"]["neutral_scale"],
            ),
            "direction_loss_guard": {
                "enabled": _coerce_bool(
                    macro_guard.get("enabled", base["macro"]["direction_loss_guard"]["enabled"]),
                    base["macro"]["direction_loss_guard"]["enabled"],
                ),
                "lookback": int(
                    max(
                        1,
                        min(
                            40,
                            float(
                                macro_guard.get(
                                    "lookback",
                                    base["macro"]["direction_loss_guard"]["lookback"],
                                )
                                or base["macro"]["direction_loss_guard"]["lookback"]
                            ),
                        ),
                    )
                ),
                "min_samples": int(
                    max(
                        1,
                        min(
                            20,
                            float(
                                macro_guard.get(
                                    "min_samples",
                                    base["macro"]["direction_loss_guard"]["min_samples"],
                                )
                                or base["macro"]["direction_loss_guard"]["min_samples"]
                            ),
                        ),
                    )
                ),
                "aligned_loss_streak_soft": int(
                    max(
                        1,
                        min(
                            10,
                            float(
                                macro_guard.get(
                                    "aligned_loss_streak_soft",
                                    base["macro"]["direction_loss_guard"]["aligned_loss_streak_soft"],
                                )
                                or base["macro"]["direction_loss_guard"]["aligned_loss_streak_soft"]
                            ),
                        ),
                    )
                ),
                "aligned_loss_streak_hard": int(
                    max(
                        1,
                        min(
                            10,
                            float(
                                macro_guard.get(
                                    "aligned_loss_streak_hard",
                                    base["macro"]["direction_loss_guard"]["aligned_loss_streak_hard"],
                                )
                                or base["macro"]["direction_loss_guard"]["aligned_loss_streak_hard"]
                            ),
                        ),
                    )
                ),
                "counter_win_bias_delta": _bounded_float(
                    macro_guard.get(
                        "counter_win_bias_delta",
                        base["macro"]["direction_loss_guard"]["counter_win_bias_delta"],
                    ),
                    base["macro"]["direction_loss_guard"]["counter_win_bias_delta"],
                    low=0.0,
                    high=1.0,
                ),
                "soft_scale": _bounded_float(
                    macro_guard.get(
                        "soft_scale",
                        base["macro"]["direction_loss_guard"]["soft_scale"],
                    ),
                    base["macro"]["direction_loss_guard"]["soft_scale"],
                    low=0.0,
                    high=1.0,
                ),
                "hard_scale": _bounded_float(
                    macro_guard.get(
                        "hard_scale",
                        base["macro"]["direction_loss_guard"]["hard_scale"],
                    ),
                    base["macro"]["direction_loss_guard"]["hard_scale"],
                    low=0.0,
                    high=1.0,
                ),
                "neutralize_on_hard": _coerce_bool(
                    macro_guard.get(
                        "neutralize_on_hard",
                        base["macro"]["direction_loss_guard"]["neutralize_on_hard"],
                    ),
                    base["macro"]["direction_loss_guard"]["neutralize_on_hard"],
                ),
            },
        },
        "interpreter": {
            "role": str(interpreter.get("role", base["interpreter"]["role"]) or base["interpreter"]["role"]),
            "buy_scale": _bounded_float(
                interpreter.get("buy_scale", base["interpreter"]["buy_scale"]),
                base["interpreter"]["buy_scale"],
            ),
            "sell_scale": _bounded_float(
                interpreter.get("sell_scale", base["interpreter"]["sell_scale"]),
                base["interpreter"]["sell_scale"],
            ),
            "limit_countertrend_scale": _bounded_float(
                interpreter.get(
                    "limit_countertrend_scale",
                    base["interpreter"]["limit_countertrend_scale"],
                ),
                base["interpreter"]["limit_countertrend_scale"],
            ),
            "block_countertrend_scale": _bounded_float(
                interpreter.get(
                    "block_countertrend_scale",
                    base["interpreter"]["block_countertrend_scale"],
                ),
                base["interpreter"]["block_countertrend_scale"],
            ),
        },
        "risk": {
            "role": str(risk.get("role", base["risk"]["role"]) or base["risk"]["role"]),
            "per_instance": {
                "health_scoring": {
                    "enabled": _coerce_bool(
                        risk.get("per_instance", {}).get("health_scoring", {}).get("enabled", True),
                        True,
                    ),
                    "min_trades_for_scoring": int(float(
                        risk.get("per_instance", {}).get("health_scoring", {}).get("min_trades_for_scoring", 3) or 3
                    )),
                    "score_ttl_sec": int(float(
                        risk.get("per_instance", {}).get("health_scoring", {}).get("score_ttl_sec", 30) or 30
                    )),
                },
                "loss_guard": {
                    "enabled": _coerce_bool(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("enabled", True),
                        True,
                    ),
                    "soft_loss_streak": int(float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("soft_loss_streak", 2) or 2
                    )),
                    "hard_loss_streak": int(float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("hard_loss_streak", 4) or 4
                    )),
                    "shadow_loss_streak": int(float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("shadow_loss_streak", 5) or 5
                    )),
                    "soft_scale": _bounded_float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("soft_scale", 0.55),
                        0.55, low=0.0, high=1.0,
                    ),
                    "hard_scale": _bounded_float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("hard_scale", 0.25),
                        0.25, low=0.0, high=1.0,
                    ),
                    "rehab_min_trades": int(float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("rehab_min_trades", 5) or 5
                    )),
                    "rehab_min_win_rate": _bounded_float(
                        risk.get("per_instance", {}).get("loss_guard", {}).get("rehab_min_win_rate", 0.50),
                        0.50, low=0.0, high=1.0,
                    ),
                },
                "regime_fitness": {
                    "enabled": _coerce_bool(
                        risk.get("per_instance", {}).get("regime_fitness", {}).get("enabled", True),
                        True,
                    ),
                    "min_samples_per_regime": int(float(
                        risk.get("per_instance", {}).get("regime_fitness", {}).get("min_samples_per_regime", 2) or 2
                    )),
                    "unfamiliar_regime_scale": _bounded_float(
                        risk.get("per_instance", {}).get("regime_fitness", {}).get("unfamiliar_regime_scale", 0.6),
                        0.6, low=0.0, high=1.0,
                    ),
                },
            },
            "cross_strategy": {
                "correlation_tracking": {
                    "enabled": _coerce_bool(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("enabled", True),
                        True,
                    ),
                    "divergence_confidence_penalty": _bounded_float(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("divergence_confidence_penalty", 0.4),
                        0.4, low=0.0, high=1.0,
                    ),
                    "divergence_volume_penalty": _bounded_float(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("divergence_volume_penalty", 0.6),
                        0.6, low=0.0, high=1.0,
                    ),
                    "divergence_cooldown_sec": int(float(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("divergence_cooldown_sec", 120) or 120
                    )),
                    "alignment_volume_bonus": _bounded_float(
                        risk.get("cross_strategy", {}).get("correlation_tracking", {}).get("alignment_volume_bonus", 0.05),
                        0.05, low=0.0, high=0.5,
                    ),
                },
            },
            "risk_budget": {
                "allocation_method": str(
                    risk.get("risk_budget", {}).get("allocation_method", "health_squared") or "health_squared"
                ),
                "min_lot_per_instance": _bounded_float(
                    risk.get("risk_budget", {}).get("min_lot_per_instance", 0.01),
                    0.01, low=0.0, high=0.10,
                ),
                "max_lot_per_instance": _bounded_float(
                    risk.get("risk_budget", {}).get("max_lot_per_instance", 0.20),
                    0.20, low=0.0, high=10.0,
                ),
            },
        },
        "execution": {
            "aggregation": str(
                execution.get("aggregation", base["execution"]["aggregation"])
                or base["execution"]["aggregation"]
            ),
            "min_confidence": _bounded_float(
                execution.get("min_confidence", base["execution"]["min_confidence"]),
                base["execution"]["min_confidence"],
                low=0.0,
                high=1.0,
            ),
            "entry_policy": str(
                execution.get("entry_policy", base["execution"]["entry_policy"])
                or base["execution"]["entry_policy"]
            ),
        },
        "session_scheduler": {
            "enabled": _coerce_bool(
                session_scheduler.get("enabled", base["session_scheduler"]["enabled"]),
                base["session_scheduler"]["enabled"],
            ),
            "mode": str(
                session_scheduler.get("mode", base["session_scheduler"]["mode"])
                or base["session_scheduler"]["mode"]
            ),
            "allowed_sessions": _normalize_string_list(
                session_scheduler.get("allowed_sessions", base["session_scheduler"]["allowed_sessions"]),
                list(base["session_scheduler"]["allowed_sessions"]),
            ),
            "blocked_sessions": _normalize_string_list(
                session_scheduler.get("blocked_sessions", base["session_scheduler"]["blocked_sessions"]),
                list(base["session_scheduler"]["blocked_sessions"]),
            ),
            "default_when_disabled": {
                "session_as_soft_factor": _coerce_bool(
                    session_scheduler.get("default_when_disabled", {}).get(
                        "session_as_soft_factor",
                        base["session_scheduler"]["default_when_disabled"]["session_as_soft_factor"],
                    ),
                    base["session_scheduler"]["default_when_disabled"]["session_as_soft_factor"],
                ),
                "allow_new_entry": _coerce_bool(
                    session_scheduler.get("default_when_disabled", {}).get(
                        "allow_new_entry",
                        base["session_scheduler"]["default_when_disabled"]["allow_new_entry"],
                    ),
                    base["session_scheduler"]["default_when_disabled"]["allow_new_entry"],
                ),
            },
            "policies": {
                session_name: _normalize_session_policy(
                    session_scheduler.get("policies", {}).get(session_name, {}),
                    base["session_scheduler"]["policies"][session_name],
                )
                for session_name in ("ASIA", "LONDON", "NEW_YORK", "OVERLAP")
            },
        },
        "router": {
            "trend": _normalize_router_family(router.get("trend", {}), base["router"]["trend"]),
            "range": _normalize_router_family(router.get("range", {}), base["router"]["range"]),
            "uncertain": _normalize_router_family(
                router.get("uncertain", {}),
                base["router"]["uncertain"],
            ),
        },
    }
    router_payload = normalized.get("router", {}) if isinstance(normalized.get("router", {}), dict) else {}
    base_router = base.get("router", {}) if isinstance(base.get("router", {}), dict) else {}
    legacy_router_signatures = {
        "trend": ("ema_macd", ("composite", "wyckoff")),
        "range": ("wyckoff", ("composite", "ema_macd")),
        "uncertain": ("composite", ("ema_macd", "wyckoff")),
    }
    for family_name, fallback_family in base_router.items():
        current_family = (
            router_payload.get(family_name, {})
            if isinstance(router_payload.get(family_name, {}), dict)
            else {}
        )
        primary = str(current_family.get("primary_strategy_id", "") or "").strip()
        supports = tuple(
            str(item).strip()
            for item in current_family.get("support_strategy_ids", [])
            if str(item).strip()
        ) if isinstance(current_family.get("support_strategy_ids", []), list) else ()
        legacy_signature = legacy_router_signatures.get(family_name)
        has_invalid_builtin = bool(
            primary
            and not primary.startswith("custom_")
            and primary not in VALID_STRATEGIES
        ) or any(
            strategy_id
            and not strategy_id.startswith("custom_")
            and strategy_id not in VALID_STRATEGIES
            for strategy_id in supports
        )
        if (legacy_signature and (primary, supports) == legacy_signature) or has_invalid_builtin:
            router_payload[family_name] = copy.deepcopy(fallback_family)
    normalized["router"] = router_payload
    return _sync_decision_policy_min_confidence(normalized)


def _default_strategy_market_state() -> dict[str, Any]:
    now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    strategies: dict[str, Any] = {}
    for strategy_id in VALID_STRATEGIES:
        strategies[strategy_id] = {
            "enabled": True,
            "lifecycle_stage": "paper" if strategy_id == "ema_macd" else "marketplace",
            "template": _default_strategy_template(strategy_id),
            "template_versions": [
                {
                    "version": "1.0.0",
                    "template": _default_strategy_template(strategy_id),
                    "captured_at": now_iso,
                    "note": "initial release",
                }
            ],
            "versions": [
                {
                    "version": "1.0.0",
                    "note": "initial release",
                    "published_at": now_iso,
                }
            ],
            "current_version": "1.0.0",
        }
    return {
        "strategies": strategies,
        "runtime_instances": [],
        "decision_policy": _default_decision_policy(),
        "updated_at": now_iso,
    }


def _strategy_catalog_from_state(state: Optional[dict[str, Any]] = None) -> dict[str, str]:
    payload = state if isinstance(state, dict) else _load_strategy_market_state()
    catalog: dict[str, str] = dict(VALID_STRATEGIES)
    strategies = payload.get("strategies", {}) if isinstance(payload.get("strategies", {}), dict) else {}
    for strategy_id, row in strategies.items():
        if not isinstance(row, dict):
            continue
        sid = str(strategy_id or "").strip()
        if not sid:
            continue
        display_name = str(row.get("display_name") or row.get("name") or sid)
        if sid not in catalog:
            catalog[sid] = display_name
    return catalog


def _load_strategy_market_state() -> dict[str, Any]:
    """Delegates to unified_strategy_state — single source of truth.
    Returns the same dict shape as the old strategy_market_state.json for
    backward compatibility with 60+ call sites.
    """
    try:
        from src.unified_strategy_state import load_unified_state, instances_as_list
        unified = load_unified_state(use_cache=False)
        data = {
            "strategies": unified.get("strategies", {}),
            "runtime_instances": instances_as_list(unified),
            "decision_policy": unified.get("decision_policy", {}),
            "scheduler": unified.get("scheduler", {}),
            "updated_at": unified.get("updated_at", ""),
        }
        # Normalize runtime_instances for backward compat
        template_map = {
            str(sid): (
                row.get("template", {})
                if isinstance(row.get("template", {}), dict)
                else {}
            )
            for sid, row in data.get("strategies", {}).items()
            if isinstance(row, dict)
        }
        effective_payload = _load_effective_params_payload()
        config_revision = _effective_config_revision(effective_payload)
        data["runtime_instances"] = [
            _normalize_runtime_instance(item, strategy_templates=template_map, config_revision=config_revision)
            for item in data.get("runtime_instances", [])
            if isinstance(item, dict)
        ]
        data["decision_policy"] = _normalize_decision_policy(data.get("decision_policy"))
        _repair_strategy_market_assets(data)
        _reconcile_strategy_market_runtime_state(data)
        return data
    except Exception:
        pass

    # Legacy fallback when unified module is unavailable
    return _load_strategy_market_state_legacy()


def _load_strategy_market_state_legacy() -> dict[str, Any]:
    """Original legacy loader — preserved as fallback."""
    state_file = _strategy_market_state_file()
    data = _default_strategy_market_state()
    if state_file.exists():
        try:
            loaded = json.loads(state_file.read_text(encoding="utf-8"))
            if isinstance(loaded, dict):
                data.update(loaded)
        except Exception as exc:
            _recover_corrupt_json_file(
                state_file,
                data,
                reason=f"strategy_market_state_invalid_json:{type(exc).__name__}",
            )
    else:
        bootstrapped = _bootstrap_missing_strategy_market_state(state_file)
        if isinstance(bootstrapped, dict):
            data.update(bootstrapped)
    strategies = data.get("strategies", {}) if isinstance(data.get("strategies", {}), dict) else {}
    for strategy_id in VALID_STRATEGIES:
        row = strategies.get(strategy_id, {}) if isinstance(strategies.get(strategy_id, {}), dict) else {}
        default_row = _default_strategy_market_state()["strategies"][strategy_id]
        merged = {
            "enabled": bool(row.get("enabled", default_row["enabled"])),
            "lifecycle_stage": str(row.get("lifecycle_stage", default_row["lifecycle_stage"]) or default_row["lifecycle_stage"]),
            "template": _normalize_strategy_template_payload(
                row.get("template", default_row["template"])
                if isinstance(row.get("template", default_row["template"]), dict)
                else default_row["template"]
            ),
            "template_versions": row.get("template_versions", default_row["template_versions"])
            if isinstance(row.get("template_versions", default_row["template_versions"]), list)
            else default_row["template_versions"],
            "versions": row.get("versions", default_row["versions"]) if isinstance(row.get("versions", default_row["versions"]), list) else default_row["versions"],
            "current_version": str(row.get("current_version", default_row["current_version"]) or default_row["current_version"]),
        }
        strategies[strategy_id] = merged
    data["strategies"] = strategies
    template_map = {
        str(strategy_id): (
            row.get("template", {})
            if isinstance(row.get("template", {}), dict)
            else {}
        )
        for strategy_id, row in strategies.items()
        if isinstance(row, dict)
    }
    effective_payload = _load_effective_params_payload()
    config_revision = _effective_config_revision(effective_payload)
    raw_instances = data.get("runtime_instances", []) if isinstance(data.get("runtime_instances", []), list) else []
    data["runtime_instances"] = [
        _normalize_runtime_instance(item, strategy_templates=template_map, config_revision=config_revision)
        for item in raw_instances
        if isinstance(item, dict)
    ]
    data["decision_policy"] = _normalize_decision_policy(data.get("decision_policy"))
    data["updated_at"] = str(data.get("updated_at", ""))
    return data


def _save_strategy_market_state(data: dict[str, Any]) -> pathlib.Path:
    """Delegates to unified_strategy_state — single source of truth.
    Returns the state file path for backward compatibility.
    """
    payload = copy.deepcopy(data) if isinstance(data, dict) else {}
    _repair_strategy_market_assets(payload)
    payload["decision_policy"] = _normalize_decision_policy(payload.get("decision_policy"))
    if isinstance(payload.get("runtime_instances", []), list):
        try:
            payload["scheduler"] = _sync_runtime_scheduler_to_effective(payload)
        except Exception:
            pass
    try:
        from src.unified_strategy_state import save_unified_state, load_unified_state, invalidate_cache
        invalidate_cache()
        try:
            unified = load_unified_state(use_cache=False)
        except Exception:
            unified = {}
        if payload.get("strategies"):
            unified["strategies"] = payload["strategies"]
        if "runtime_instances" in payload:
            instances = {}
            for inst in (payload.get("runtime_instances") or []):
                if isinstance(inst, dict):
                    iid = str(inst.get("instance_id", "")).strip()
                    if iid:
                        instances[iid] = inst
            unified["instances"] = instances
        if payload.get("decision_policy"):
            unified["decision_policy"] = payload["decision_policy"]
        if payload.get("scheduler"):
            unified["scheduler"] = payload["scheduler"]
        save_unified_state(unified)
    except Exception:
        pass
    # Also write legacy file for backward compat during transition
    state_file = _strategy_market_state_file()
    payload["updated_at"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    return _write_json_atomic(state_file, payload)


def _save_effective_params_payload(payload: dict[str, Any]) -> pathlib.Path:
    eff_file = _resolve_runtime_artifact("logs", "effective_params.json") or (LOGS_DIR / "effective_params.json")
    return _write_json_atomic(eff_file, payload)


def _infer_strategy_id_from_instance_id(
    instance_id: str,
    catalog: dict[str, str],
) -> str:
    text = str(instance_id or "").strip()
    if not text:
        return ""
    if text.startswith("legacy:"):
        return text.split(":", 1)[1].strip()
    for strategy_id in sorted(catalog.keys(), key=len, reverse=True):
        if text == f"inst_{strategy_id}" or text.startswith(f"inst_{strategy_id}_"):
            return strategy_id
    return ""


def _load_enabled_active_instance_overrides() -> list[dict[str, Any]]:
    path = CONFIG_DIR / "active_instances.json"
    if not path.exists():
        return []
    try:
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return []
    raw_instances = payload.get("instances", []) if isinstance(payload.get("instances", []), list) else []
    return [item for item in raw_instances if isinstance(item, dict) and bool(item.get("enabled", True))]


def _drop_legacy_runtime_duplicates(
    runtime_instances: list[dict[str, Any]],
    active_overrides: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], bool]:
    explicit_by_strategy: dict[str, set[str]] = {}
    for item in active_overrides:
        if not isinstance(item, dict):
            continue
        strategy_id = str(item.get("strategy_id", "") or "").strip()
        instance_id = str(item.get("id", "") or "").strip()
        if strategy_id and instance_id:
            explicit_by_strategy.setdefault(strategy_id, set()).add(instance_id)
    if not explicit_by_strategy:
        return runtime_instances, False
    dropped = False
    cleaned: list[dict[str, Any]] = []
    for item in runtime_instances:
        if not isinstance(item, dict):
            continue
        strategy_id = str(item.get("strategy_id", "") or "").strip()
        instance_id = str(item.get("instance_id", "") or "").strip()
        explicit_ids = explicit_by_strategy.get(strategy_id, set())
        if explicit_ids and instance_id not in explicit_ids and instance_id.startswith(f"inst_{strategy_id}"):
            dropped = True
            continue
        cleaned.append(item)
    return cleaned, dropped


def _reconcile_strategy_market_runtime_state(data: dict[str, Any]) -> bool:
    if not isinstance(data, dict):
        return False
    strategies = data.get("strategies", {}) if isinstance(data.get("strategies", {}), dict) else {}
    runtime_instances = (
        data.get("runtime_instances", [])
        if isinstance(data.get("runtime_instances", []), list)
        else []
    )
    if not strategies:
        return False
    catalog = _strategy_catalog_from_state(data)
    effective_payload = _load_effective_params_payload()
    runtime_scheduler_snapshot = _load_runtime_scheduler_snapshot()
    runtime_status = _runtime_strategy_status_payload()
    runtime_active = (
        runtime_status.get("active", {})
        if isinstance(runtime_status.get("active", {}), dict)
        else {}
    )
    scheduler_fallback = (
        effective_payload.get("runtime_scheduler", {})
        if isinstance(effective_payload.get("runtime_scheduler", {}), dict)
        else {}
    )
    strategy_projection = _normalize_strategy_projection(
        catalog=catalog,
        effective_payload=effective_payload,
        runtime_active=runtime_active,
        runtime_scheduler=runtime_scheduler_snapshot,
        scheduler_fallback=scheduler_fallback,
    )
    effective_projection = (
        strategy_projection.get("effective", {})
        if isinstance(strategy_projection.get("effective", {}), dict)
        else {}
    )
    active_strategy_ids = _clean_str_list(effective_projection.get("activeStrategyIds"))
    active_instance_ids = _clean_str_list(effective_projection.get("activeInstanceIds"))
    if not active_strategy_ids and not active_instance_ids and not runtime_instances:
        return False
    template_map = {
        str(strategy_id): (
            row.get("template", {})
            if isinstance(row.get("template", {}), dict)
            else {}
        )
        for strategy_id, row in strategies.items()
        if isinstance(row, dict)
    }
    config_revision = _effective_config_revision(effective_payload)
    strategy_weights = (
        effective_projection.get("strategyWeights", {})
        if isinstance(effective_projection.get("strategyWeights", {}), dict)
        else {}
    )
    instance_weights = (
        effective_projection.get("instanceWeights", {})
        if isinstance(effective_projection.get("instanceWeights", {}), dict)
        else {}
    )
    now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    changed = False
    existing_by_id: dict[str, dict[str, Any]] = {}
    existing_by_strategy: dict[str, list[dict[str, Any]]] = {}
    strategy_runtime_count: dict[str, int] = {}
    for item in runtime_instances:
        if not isinstance(item, dict):
            continue
        instance_id = str(item.get("instance_id", "") or "").strip()
        strategy_id = str(item.get("strategy_id", "") or "").strip()
        if not strategy_id and instance_id:
            strategy_id = _infer_strategy_id_from_instance_id(instance_id, catalog)
            if strategy_id:
                item["strategy_id"] = strategy_id
                changed = True
        if strategy_id:
            strategy_runtime_count[strategy_id] = strategy_runtime_count.get(strategy_id, 0) + 1
            existing_by_strategy.setdefault(strategy_id, []).append(item)
        if instance_id:
            existing_by_id[instance_id] = item
    active_overrides = _load_enabled_active_instance_overrides()
    for override in active_overrides:
        strategy_id = str(override.get("strategy_id", "") or "").strip()
        instance_id = str(override.get("id", "") or "").strip()
        if not strategy_id:
            continue
        row = strategies.get(strategy_id, {}) if isinstance(strategies.get(strategy_id, {}), dict) else {}
        allocation = _clamp_allocation(override.get("allocation", 1.0))
        existing = existing_by_id.get(instance_id) if instance_id else None
        if not isinstance(existing, dict):
            strategy_rows = existing_by_strategy.get(strategy_id, [])
            existing = next(
                (
                    item for item in strategy_rows
                    if isinstance(item, dict) and str(item.get("status", "paused") or "paused").lower() != "running"
                ),
                None,
            )
        if isinstance(existing, dict):
            if instance_id and str(existing.get("instance_id", "") or "").strip() != instance_id:
                old_instance_id = str(existing.get("instance_id", "") or "").strip()
                if old_instance_id:
                    existing_by_id.pop(old_instance_id, None)
                existing["instance_id"] = instance_id
                existing_by_id[instance_id] = existing
                changed = True
            if str(existing.get("strategy_id", "") or "").strip() != strategy_id:
                existing["strategy_id"] = strategy_id
                changed = True
            if str(existing.get("status", "running") or "running").strip().lower() != "running":
                existing["status"] = "running"
                changed = True
            if float(existing.get("allocation", 0.0) or 0.0) != allocation:
                existing["allocation"] = allocation
                changed = True
            if str(existing.get("strategy_name", "") or "").strip() != str(
                row.get("display_name") or row.get("name") or catalog.get(strategy_id, strategy_id)
            ):
                existing["strategy_name"] = str(
                    row.get("display_name") or row.get("name") or catalog.get(strategy_id, strategy_id)
                )
                changed = True
            existing["updated_at"] = now_iso
            strategy_runtime_count[strategy_id] = max(strategy_runtime_count.get(strategy_id, 0), 1)
            continue
        if not instance_id:
            instance_id = f"inst_{strategy_id}_{int(time.time() * 1000)}"
        repaired = _normalize_runtime_instance(
            {
                "instance_id": instance_id,
                "strategy_id": strategy_id,
                "strategy_name": str(
                    row.get("display_name") or row.get("name") or catalog.get(strategy_id, strategy_id)
                ),
                "strategy_version": str(row.get("current_version", "1.0.0") or "1.0.0"),
                "allocation": allocation,
                "status": "running",
                "role": "live_support",
                "config_revision": config_revision,
                "param_overrides": override.get("params", {}) if isinstance(override.get("params", {}), dict) else {},
                "template_snapshot": template_map.get(strategy_id, {}),
                "deployed_at": now_iso,
                "updated_at": now_iso,
                "metadata": {"reconciled_from": "active_instances"},
            },
            strategy_templates=template_map,
            config_revision=config_revision,
        )
        runtime_instances.append(repaired)
        existing_by_id[instance_id] = repaired
        existing_by_strategy.setdefault(strategy_id, []).append(repaired)
        strategy_runtime_count[strategy_id] = strategy_runtime_count.get(strategy_id, 0) + 1
        changed = True
    for instance_id in active_instance_ids:
        strategy_id = ""
        existing = existing_by_id.get(instance_id)
        if isinstance(existing, dict):
            strategy_id = str(existing.get("strategy_id", "") or "").strip()
        if not strategy_id:
            strategy_id = _infer_strategy_id_from_instance_id(instance_id, catalog)
        if not strategy_id:
            continue
        strategy_runtime_count[strategy_id] = strategy_runtime_count.get(strategy_id, 0) + (
            0 if isinstance(existing, dict) else 1
        )
        row = strategies.get(strategy_id, {}) if isinstance(strategies.get(strategy_id, {}), dict) else {}
        if isinstance(existing, dict):
            if str(existing.get("status", "running") or "running").strip().lower() != "running":
                existing["status"] = "running"
                changed = True
            if str(existing.get("strategy_name", "") or "").strip() != str(
                row.get("display_name") or row.get("name") or catalog.get(strategy_id, strategy_id)
            ):
                existing["strategy_name"] = str(
                    row.get("display_name") or row.get("name") or catalog.get(strategy_id, strategy_id)
                )
                changed = True
            if str(existing.get("strategy_version", "") or "").strip() != str(
                row.get("current_version", "1.0.0") or "1.0.0"
            ):
                existing["strategy_version"] = str(row.get("current_version", "1.0.0") or "1.0.0")
                changed = True
            if str(existing.get("config_revision", "") or "").strip() != config_revision:
                existing["config_revision"] = config_revision
                changed = True
            existing["updated_at"] = now_iso
            continue
        allocation = float(instance_weights.get(instance_id, 0.0) or 0.0)
        if allocation <= 0:
            allocation = float(strategy_weights.get(strategy_id, 0.0) or 0.0)
        runtime_instances.append(
            _normalize_runtime_instance(
                {
                    "instance_id": instance_id,
                    "strategy_id": strategy_id,
                    "strategy_name": str(
                        row.get("display_name") or row.get("name") or catalog.get(strategy_id, strategy_id)
                    ),
                    "strategy_version": str(row.get("current_version", "1.0.0") or "1.0.0"),
                    "allocation": allocation,
                    "status": "running",
                    "role": "live_support",
                    "config_revision": config_revision,
                    "template_snapshot": template_map.get(strategy_id, {}),
                    "deployed_at": now_iso,
                    "updated_at": now_iso,
                    "metadata": {
                        "reconciled_from": "runtime_projection",
                    },
                },
                strategy_templates=template_map,
                config_revision=config_revision,
            )
        )
        changed = True
    runtime_instances, legacy_dropped = _drop_legacy_runtime_duplicates(runtime_instances, active_overrides)
    if legacy_dropped:
        changed = True
        strategy_runtime_count = {}
        for item in runtime_instances:
            if not isinstance(item, dict):
                continue
            strategy_id = str(item.get("strategy_id", "") or "").strip()
            if strategy_id:
                strategy_runtime_count[strategy_id] = strategy_runtime_count.get(strategy_id, 0) + 1
    active_strategy_set = set(active_strategy_ids)
    for strategy_id, row in list(strategies.items()):
        if not isinstance(row, dict):
            continue
        current_stage = str(row.get("lifecycle_stage", "marketplace") or "marketplace").strip().lower()
        has_runtime_presence = bool(strategy_runtime_count.get(strategy_id, 0))
        target_stage = current_stage
        if current_stage in {"canary", "full"}:
            target_stage = current_stage
        elif strategy_id in active_strategy_set or has_runtime_presence:
            target_stage = "paper"
        if current_stage != target_stage:
            row["lifecycle_stage"] = target_stage
            changed = True
        if (strategy_id in active_strategy_set or has_runtime_presence) and not bool(row.get("enabled", True)):
            row["enabled"] = True
            changed = True
        strategies[strategy_id] = row
    if changed:
        data["strategies"] = strategies
        data["runtime_instances"] = runtime_instances
    return changed


def _sync_runtime_scheduler_to_effective(state: Optional[dict[str, Any]] = None) -> dict[str, Any]:
    payload = state if isinstance(state, dict) else _load_strategy_market_state()
    instances = payload.get("runtime_instances", []) if isinstance(payload.get("runtime_instances", []), list) else []
    running = [
        item for item in instances
        if isinstance(item, dict) and str(item.get("status", "running")).lower() == "running"
    ]
    strategy_weights: dict[str, float] = {}
    active_ids: list[str] = []
    active_instance_ids: list[str] = []
    strategy_instance_map: dict[str, list[str]] = {}
    instance_allocations: dict[str, float] = {}
    for item in running:
        sid = str(item.get("strategy_id", "")).strip()
        instance_id = str(item.get("instance_id", "")).strip()
        if not sid:
            continue
        alloc = float(item.get("allocation", 0.0) or 0.0)
        strategy_weights[sid] = round(float(strategy_weights.get(sid, 0.0)) + max(0.0, alloc), 4)
        if sid not in active_ids:
            active_ids.append(sid)
        if instance_id:
            active_instance_ids.append(instance_id)
            instance_allocations[instance_id] = max(0.0, alloc)
            strategy_instance_map.setdefault(sid, []).append(instance_id)
    total_alloc = sum(instance_allocations.values())
    instance_weights: dict[str, float] = {}
    if active_instance_ids:
        if total_alloc > 0:
            for instance_id in active_instance_ids:
                instance_weights[instance_id] = round(instance_allocations.get(instance_id, 0.0) / total_alloc, 4)
        else:
            equal_weight = round(1.0 / max(1, len(active_instance_ids)), 4)
            for instance_id in active_instance_ids:
                instance_weights[instance_id] = equal_weight
    strategy_weights = {
        sid: round(
            sum(instance_weights.get(instance_id, 0.0) for instance_id in strategy_instance_map.get(sid, [])),
            4,
        )
        for sid in active_ids
    }
    selected_instance_id = ""
    selected_strategy_id = ""
    if active_instance_ids:
        top_instances = sorted(
            instance_weights.items(),
            key=lambda item: (-float(item[1]), item[0]),
        )
        if top_instances:
            selected_instance_id = str(top_instances[0][0] or "")
            for sid, mapped_instance_ids in strategy_instance_map.items():
                if selected_instance_id in mapped_instance_ids:
                    selected_strategy_id = sid
                    break
    mode = "idle"
    if len(active_instance_ids) == 1:
        mode = "single_active"
    elif len(active_instance_ids) > 1:
        mode = "multi_active"
    if not active_instance_ids:
        reason = "no_running_instances"
    elif len(active_instance_ids) == 1:
        reason = f"single_running_instance:{active_instance_ids[0]}"
    else:
        top_instances = sorted(
            instance_weights.items(),
            key=lambda item: (-float(item[1]), item[0]),
        )
        top_text = ",".join(f"{instance_id}={weight:.2f}" for instance_id, weight in top_instances[:3])
        reason = f"allocation_weighted_multi_instance:{top_text}"
    config_revision = _effective_config_revision()
    decision_policy = _normalize_decision_policy(payload.get("decision_policy"))
    configured_session_scheduler = (
        decision_policy.get("session_scheduler", {})
        if isinstance(decision_policy.get("session_scheduler", {}), dict)
        else {}
    )
    scheduler = {
        "mode": mode,
        "arbiter": "allocation-weighted",
        "reason": reason,
        "active_instances": active_instance_ids,
        "active_instance_ids": active_instance_ids,
        "active_strategy_ids": active_ids,
        "selected_instance_id": selected_instance_id,
        "selected_strategy_id": selected_strategy_id,
        "instance_weights": instance_weights,
        "strategy_weights": strategy_weights,
        "strategy_instance_map": strategy_instance_map,
        "decision_policy": decision_policy,
        # Keep effective params self-contained so the UI has a stable fallback
        # before the next runtime snapshot refresh lands.
        "session_scheduler": {
            "enabled": bool(configured_session_scheduler.get("enabled", False)),
            "mode": _first_non_empty_str(configured_session_scheduler.get("mode"), "soft_weight"),
            "user_allowed_sessions": _clean_str_list(configured_session_scheduler.get("allowed_sessions")),
            "user_blocked_sessions": _clean_str_list(configured_session_scheduler.get("blocked_sessions")),
            "current_session": "",
            "current_session_profile": "",
            "current_policy": {},
            "qualified_strategy_ids": [],
            "blocked_strategy_ids": [],
            "candidate_strategy_ids": [],
            "decision": {},
            "source": "effective_decision_policy",
        },
        "config_revision": config_revision,
        "updated_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
    }
    eff_data = _load_effective_params_payload()
    eff_data["runtime_scheduler"] = scheduler
    if len(active_ids) == 1:
        eff_data["strategy_type"] = active_ids[0]
    elif "composite" in active_ids:
        eff_data["strategy_type"] = "composite"
    elif selected_strategy_id:
        eff_data["strategy_type"] = selected_strategy_id
    else:
        eff_data["strategy_type"] = ""
    eff_data["strategy_switched_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
    _save_effective_params_payload(eff_data)
    return scheduler


def _next_patch_version(version: str) -> str:
    text = str(version or "1.0.0").strip()
    parts = text.split(".")
    if len(parts) != 3:
        return "1.0.1"
    try:
        major = int(parts[0])
        minor = int(parts[1])
        patch = int(parts[2])
    except Exception:
        return "1.0.1"
    return f"{major}.{minor}.{patch + 1}"


class StrategyDeployRequest(BaseModel):
    strategy_id: str
    allocation: float = 1.0
    activate: bool = True
    role: str = "live_support"
    param_overrides: Dict[str, Any] = Field(default_factory=dict)


def _strategy_deploy_guard(row: dict[str, Any]) -> str:
    payload = row if isinstance(row, dict) else {}
    lifecycle_stage = str(payload.get("lifecycle_stage", "") or "marketplace")
    review_status = str(payload.get("review_status", "") or "pending")
    if review_status != "approved":
        return "该策略尚未通过审核，不能直接部署"
    if lifecycle_stage not in {"paper", "canary", "full"}:
        return "该策略尚未晋级到 paper，不能直接部署"
    return ""


class StrategyDisableRequest(BaseModel):
    strategy_id: str
    disabled: bool = True


class StrategyUndeployRequest(BaseModel):
    strategy_id: str


class StrategyPublishRequest(BaseModel):
    strategy_id: str
    note: str = ""


class StrategyRollbackRequest(BaseModel):
    strategy_id: str
    target_version: Optional[str] = None


class StrategyTemplateRequest(BaseModel):
    strategy_id: str
    template: Dict[str, Any]


class StrategyInstanceActionRequest(BaseModel):
    instance_id: str
    action: str
    allocation: Optional[float] = None
    role: Optional[str] = None
    param_overrides: Dict[str, Any] = Field(default_factory=dict)


class StrategyDecisionPolicyRequest(BaseModel):
    profile: Optional[str] = None
    macro: Dict[str, Any] = Field(default_factory=dict)
    interpreter: Dict[str, Any] = Field(default_factory=dict)
    risk: Dict[str, Any] = Field(default_factory=dict)
    execution: Dict[str, Any] = Field(default_factory=dict)
    router: Dict[str, Any] = Field(default_factory=dict)
    session_scheduler: Dict[str, Any] = Field(default_factory=dict)


class StrategyCompositeCreateRequest(BaseModel):
    name: str
    components: List[str]


class StrategyDraftSaveRequest(BaseModel):
    name: str
    document: str
    optimized: Dict[str, Any] = {}
    evaluation: Dict[str, Any] = {}
    workflow_meta: Dict[str, Any] = Field(default_factory=dict)
    compile_target: str = ""


class StrategyDraftOptimizeRequest(BaseModel):
    name: str
    document: str
    temp_model: str = ""
    compile_target: str = ""


class StrategyDraftEvaluateRequest(BaseModel):
    name: str
    document: str
    optimized: Dict[str, Any] = {}
    temp_model: str = ""
    compile_target: str = ""


class RuntimeStrategyEvaluationRequest(BaseModel):
    goal: str = ""


class StrategyDraftGenerateRequest(BaseModel):
    name: str
    document: str
    optimized: Dict[str, Any] = {}
    evaluation: Dict[str, Any] = {}
    temp_model: str = ""
    compile_target: str = ""


class InteractiveStrategyGenerateRequest(BaseModel):
    temp_model: str = ""
    compile_target: str = "template"


class StrategyDraftImportUrlRequest(BaseModel):
    url: str
    name: str = ""
    symbol: str = "XAUUSD"
    force_heuristic: bool = False
    temp_model: str = ""


class StrategyDraftImportUrlsRequest(BaseModel):
    urls: List[str]
    name: str = ""
    symbol: str = "XAUUSD"
    force_heuristic: bool = False
    temp_model: str = ""


def _strategy_custom_state_file() -> pathlib.Path:
    return _resolve_runtime_artifact("config", "strategy_custom_state.json") or (CONFIG_DIR / "strategy_custom_state.json")


def _runtime_ai_evaluation_state_file() -> pathlib.Path:
    return DATA_DIR / "runtime_ai_evaluation_history.json"


def _load_strategy_custom_state_v2__impl() -> dict[str, Any]:
    default_data: dict[str, Any] = {
        "draft": {
            "name": "",
            "document": "",
            "compileTarget": "template",
            "strategySpec": {},
            "optimized": {},
            "evaluation": {},
            "validationReport": {},
            "workflowMeta": {},
            "saveState": "saved",
            "lastSavedAt": "",
        },
        "generatedHistory": [],
    }
    try:
        from src.unified_strategy_state import get_custom_workspace, load_unified_state

        workspace = get_custom_workspace(load_unified_state(use_cache=False))
        if isinstance(workspace, dict) and workspace:
            return workspace
    except Exception:
        pass

    state_file = _strategy_custom_state_file()
    data = copy.deepcopy(default_data)
    if state_file.exists():
        try:
            loaded = json.loads(state_file.read_text(encoding="utf-8"))
            if isinstance(loaded, dict):
                data.update(loaded)
        except Exception as exc:
            _recover_corrupt_json_file(
                state_file,
                data,
                reason=f"strategy_custom_state_invalid_json:{type(exc).__name__}",
            )
    if not isinstance(data.get("draft", {}), dict):
        data["draft"] = copy.deepcopy(default_data["draft"])
    if not isinstance(data["draft"].get("workflowMeta", {}), dict):
        data["draft"]["workflowMeta"] = {}
    if not isinstance(data.get("generatedHistory", []), list):
        data["generatedHistory"] = []
    return data


def _save_strategy_custom_state_v2__impl(payload: dict[str, Any]) -> pathlib.Path:
    try:
        from src.unified_strategy_state import (
            get_custom_workspace,
            invalidate_cache,
            load_unified_state,
            save_unified_state,
        )

        invalidate_cache()
        unified = load_unified_state(use_cache=False)
        current_workspace = get_custom_workspace(unified)
        next_workspace = copy.deepcopy(current_workspace)
        if isinstance(payload, dict):
            next_workspace.update(payload)
        unified["custom_workspace"] = next_workspace
        save_unified_state(unified)
    except Exception:
        pass

    state_file = _strategy_custom_state_file()
    state_file.parent.mkdir(parents=True, exist_ok=True)
    tmp_path = state_file.with_name(f".{state_file.name}.{os.getpid()}.tmp")
    tmp_path.write_text(
        json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    os.replace(tmp_path, state_file)
    return state_file


def _load_runtime_ai_evaluation_state() -> dict[str, Any]:
    state_file = _runtime_ai_evaluation_state_file()
    data: dict[str, Any] = {
        "lastEvaluation": {},
        "history": [],
        "updatedAt": "",
    }
    if state_file.exists():
        try:
            loaded = json.loads(state_file.read_text(encoding="utf-8"))
            if isinstance(loaded, dict):
                data.update(loaded)
        except Exception:
            pass
    if not isinstance(data.get("lastEvaluation", {}), dict):
        data["lastEvaluation"] = {}
    if not isinstance(data.get("history", []), list):
        data["history"] = []
    if not isinstance(data.get("updatedAt", ""), str):
        data["updatedAt"] = ""
    return data


def _save_runtime_ai_evaluation_state(payload: dict[str, Any]) -> pathlib.Path:
    data = _load_runtime_ai_evaluation_state()
    if isinstance(payload, dict):
        data.update(payload)
    data["updatedAt"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    return _write_json_atomic(_runtime_ai_evaluation_state_file(), data)


def _evaluate_and_persist_strategy_custom_draft(
    *,
    name: str,
    document: str,
    optimized: dict[str, Any],
    workflow_meta: Optional[dict[str, Any]] = None,
    temp_model: str = "",
    compile_target: str = "",
) -> dict[str, Any]:
    from src.strategy_spec_service import build_strategy_spec, build_validation_report

    evaluation_result = evaluate_strategy_custom_draft(
        StrategyDraftEvaluateRequest(
            name=name,
            document=document,
            optimized=optimized if isinstance(optimized, dict) else {},
            temp_model=temp_model,
            compile_target=compile_target,
        )
    )
    evaluation = (
        evaluation_result.get("evaluation", {})
        if isinstance(evaluation_result, dict)
        else {}
    )
    merged_workflow_meta = _merge_strategy_workflow_meta(
        workflow_meta if isinstance(workflow_meta, dict) else {},
        developer_agent=(
            evaluation_result.get("developerAgent", {})
            if isinstance(evaluation_result, dict)
            and isinstance(evaluation_result.get("developerAgent", {}), dict)
            else {}
        ),
        temp_model=str(temp_model or ""),
        evaluate_llm=(
            evaluation_result.get("llm", {})
            if isinstance(evaluation_result, dict)
            and isinstance(evaluation_result.get("llm", {}), dict)
            else {}
        ),
        call_logs=[
            evaluation_result.get("callLog", {})
            if isinstance(evaluation_result, dict)
            and isinstance(evaluation_result.get("callLog", {}), dict)
            else {}
        ],
    )
    now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    payload = _load_strategy_custom_state()
    strategy_spec = build_strategy_spec(
        name=name,
        document=document,
        optimized=optimized if isinstance(optimized, dict) else {},
        compile_target=compile_target,
    )
    validation_report = build_validation_report(
        evaluation=evaluation if isinstance(evaluation, dict) else {},
        optimized=optimized if isinstance(optimized, dict) else {},
    )
    payload["draft"] = {
        "name": str(name or ""),
        "document": str(document or ""),
        "compileTarget": str(strategy_spec.get("compile_target", "template") or "template"),
        "strategySpec": strategy_spec,
        "optimized": optimized if isinstance(optimized, dict) else {},
        "evaluation": evaluation if isinstance(evaluation, dict) else {},
        "validationReport": validation_report,
        "workflowMeta": merged_workflow_meta,
        "saveState": "saved",
        "lastSavedAt": now_iso,
    }
    _save_strategy_custom_state(payload)
    return payload["draft"]


_STRATEGY_DEVELOPER_ROLE = "AI 策略开发师"


# ── Interactive Strategy Builder State ──────────────────────────

_INTERACTIVE_STATE_FILE = CONFIG_DIR / "strategy_interactive_state.json"


def _load_interactive_state() -> dict[str, Any]:
    try:
        if _INTERACTIVE_STATE_FILE.exists():
            return json.loads(_INTERACTIVE_STATE_FILE.read_text(encoding="utf-8"))
    except Exception:
        pass
    return {}


def _save_interactive_state(state: dict[str, Any]) -> None:
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    sanitized_state, _ = _sanitize_interactive_state_storage(state)
    try:
        tmp_fd, tmp_path = __import__('tempfile').mkstemp(
            suffix=".json", prefix="interactive_state_", dir=str(CONFIG_DIR)
        )
        with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
            json.dump(sanitized_state, f, ensure_ascii=False, indent=2)
        os.replace(tmp_path, str(_INTERACTIVE_STATE_FILE))
    except Exception:
        _INTERACTIVE_STATE_FILE.write_text(
            json.dumps(sanitized_state, ensure_ascii=False, indent=2), encoding="utf-8"
        )


def _normalize_interactive_message(message: Any) -> dict[str, Any]:
    if not isinstance(message, dict):
        return {}
    role = str(message.get("role", "") or "").strip().lower()
    if role == "assistant":
        role = "ai"
    if role not in {"user", "ai"}:
        role = "ai"
    kind = str(message.get("kind", "text") or "text").strip().lower()
    if kind not in {"text", "code", "command"}:
        kind = "text"
    content = str(message.get("content", "") or "")
    if role == "ai":
        content = _sanitize_interactive_text(content)
    normalized = {
        "role": role,
        "content": content,
        "ts": str(
            message.get("ts", "")
            or datetime.datetime.now().astimezone().isoformat(timespec="seconds")
        ),
        "kind": kind,
    }
    meta = message.get("meta", {})
    if isinstance(meta, dict) and meta:
        normalized["meta"] = copy.deepcopy(meta)
    return normalized


def _interactive_message_kind(text: str, fallback: str = "text") -> str:
    compact = str(text or "").strip()
    if not compact:
        return fallback
    if compact.startswith("/"):
        return "command"
    if "```" in compact:
        return "code"
    return fallback


def _sanitize_interactive_text(content: str) -> str:
    sanitized = re.sub(
        r"<think>[\s\S]*?</think>",
        "",
        str(content or ""),
        flags=re.IGNORECASE,
    ).strip()
    return sanitized


def _extract_strategy_builder_risk_metrics(knowledge: dict[str, Any]) -> dict[str, Any]:
    risk_text = str(knowledge.get("risk_constraints", "") or "")
    normalized = risk_text.replace("％", "%")
    metrics: dict[str, Any] = {
        "risk_per_trade_pct": None,
        "max_positions": None,
        "has_session_filter": any(
            token in normalized.lower()
            for token in ("london", "new york", "asia", "session", "时段", "欧美盘", "亚盘")
        ),
    }
    percent_match = re.search(r"(\d+(?:\.\d+)?)\s*%", normalized)
    if percent_match:
        try:
            metrics["risk_per_trade_pct"] = float(percent_match.group(1))
        except Exception:
            metrics["risk_per_trade_pct"] = None
    max_pos_match = re.search(
        r"(?:最多|至多|最大|不超过|max(?:imum)?)[^\d]{0,8}(\d+)\s*(?:单|个|笔|仓|position)?",
        normalized,
        flags=re.IGNORECASE,
    )
    if max_pos_match:
        try:
            metrics["max_positions"] = int(max_pos_match.group(1))
        except Exception:
            metrics["max_positions"] = None
    return metrics


def _build_strategy_builder_candidate_plans(knowledge: dict[str, Any]) -> list[dict[str, Any]]:
    combined = " ".join(
        str(knowledge.get(key, "") or "")
        for key in ("market_hypothesis", "entry_rule", "exit_rule", "risk_constraints")
    ).lower()
    trend_score = sum(
        combined.count(token)
        for token in ("trend", "follow", "breakout", "突破", "顺势", "趋势", "追涨", "动量")
    )
    mean_reversion_score = sum(
        combined.count(token)
        for token in ("mean reversion", "reversion", "range", "回归", "震荡", "反转", "回撤反弹")
    )
    candidates: list[dict[str, Any]] = []
    if trend_score >= mean_reversion_score:
        candidates.append(
            {
                "id": "trend_breakout",
                "title": "方案A：顺势突破",
                "fit": "更适合趋势明确、伦敦或美盘波动放大的行情",
                "summary": "等待关键位突破后入场，用固定止损和分批止盈控制回撤。",
                "when_to_use": "你更看重抓主波段，接受低频但赔率更高的机会。",
            }
        )
    if mean_reversion_score > 0 or trend_score == 0:
        candidates.append(
            {
                "id": "range_reversion",
                "title": "方案B：区间回归",
                "fit": "更适合震荡整理、假突破较多的行情",
                "summary": "在极值或关键支撑阻力附近反向入场，用更紧的止损换更高胜率。",
                "when_to_use": "你更看重稳定频率，希望先把回撤压住。",
            }
        )
    if not candidates:
        candidates = [
            {
                "id": "trend_breakout",
                "title": "方案A：顺势突破",
                "fit": "适合方向明确时段",
                "summary": "优先等待趋势确认和突破后跟进。",
                "when_to_use": "想做更清晰的方向单。",
            },
            {
                "id": "range_reversion",
                "title": "方案B：区间回归",
                "fit": "适合震荡整理时段",
                "summary": "优先等待偏离极值后回归。",
                "when_to_use": "想把风险压得更保守。",
            },
        ]
    return candidates[:2]


def _build_strategy_builder_insights(knowledge: dict[str, Any]) -> dict[str, Any]:
    completeness, filled_dims, current_focus = _compute_knowledge_completeness(knowledge)
    missing_dims = [dim for dim in _DIMENSION_ORDER if dim not in filled_dims]
    market_text = str(knowledge.get("market_hypothesis", "") or "")
    entry_text = str(knowledge.get("entry_rule", "") or "")
    exit_text = str(knowledge.get("exit_rule", "") or "")
    risk_text = str(knowledge.get("risk_constraints", "") or "")
    combined = f"{market_text}\n{entry_text}\n{exit_text}\n{risk_text}".lower()
    risk_metrics = _extract_strategy_builder_risk_metrics(knowledge)
    conflict_warnings: list[str] = []
    risk_alerts: list[str] = []
    blockers: list[str] = []

    if any(token in combined for token in ("趋势", "trend", "突破", "breakout")) and any(
        token in combined for token in ("震荡", "回归", "reversion", "range")
    ):
        conflict_warnings.append("当前同时包含顺势突破和震荡回归两种逻辑，建议先明确主策略方向，避免信号互相打架。")
    if market_text and not entry_text:
        blockers.append("已有市场假设，但还没有把入场条件写成可执行规则。")
    if entry_text and not exit_text:
        blockers.append("已有入场规则，但缺少明确出场规则，暂时无法进入审核链路。")
    if not risk_text:
        blockers.append("缺少风控约束，暂时不能进入策略集市审核。")
    if exit_text and not any(token in exit_text.lower() for token in ("stop", "loss", "止损", "止盈", "tp", "sl")):
        risk_alerts.append("出场规则里还没有看到明确止损或止盈，实盘风险过高。")
    risk_per_trade_pct = risk_metrics.get("risk_per_trade_pct")
    if risk_per_trade_pct is None:
        risk_alerts.append("建议补充单笔风险百分比，例如每单风险不超过 0.5% - 1%。")
    elif float(risk_per_trade_pct) > 2.0:
        risk_alerts.append("当前单笔风险上限偏高，建议先降到 1% 附近，再进入 Paper 验证。")
    max_positions = risk_metrics.get("max_positions")
    if max_positions is None:
        risk_alerts.append("建议明确最大同时持仓数量，避免策略在波动时连续叠仓。")
    elif int(max_positions) > 3:
        risk_alerts.append("最大持仓数量偏高，容易把回撤放大，建议先压到 1-3 单。")
    if risk_text and not bool(risk_metrics.get("has_session_filter")):
        risk_alerts.append("建议增加交易时段过滤，先限定伦敦盘、美盘或重叠时段，减少噪音信号。")

    try:
        caps = _strategy_generation_capabilities()
    except Exception:
        caps = {
            "tier": "trial",
            "allowPluginGenerate": False,
            "allowAdvancedParams": False,
            "recommendedAudience": "standard",
        }
    allow_pro = bool(caps.get("allowPluginGenerate", False) or caps.get("allowAdvancedParams", False))
    audience_mode = "pro" if allow_pro else "standard"
    next_questions_map = {
        "market_hypothesis": [
            "你想抓的是趋势延续、突破，还是震荡回归？",
            "这个策略更适合什么行情和什么时段？",
        ],
        "entry_rule": [
            "请把入场条件说成能执行的规则，比如指标、阈值、确认条件。",
            "什么情况下信号成立，什么情况下直接放弃？",
        ],
        "exit_rule": [
            "止损、止盈、移动止损分别怎么定？",
            "如果行情没有按预期走，最晚什么时候退出？",
        ],
        "risk_constraints": [
            "单笔最多亏多少、最多同时开几单、哪些时段禁止交易？",
            "连续亏损几次后暂停，或者账户回撤到多少要收缩仓位？",
        ],
    }
    candidate_plans = _build_strategy_builder_candidate_plans(knowledge)
    recommended_plan_id = candidate_plans[0]["id"] if candidate_plans else ""
    return {
        "audience_mode": audience_mode,
        "license_tier": str(caps.get("tier", "trial") or "trial"),
        "recommended_audience": str(caps.get("recommendedAudience", "standard") or "standard"),
        "completion": {
            "ratio": completeness,
            "filled_dimensions": filled_dims,
            "missing_dimensions": missing_dims,
            "current_focus": current_focus,
        },
        "blockers": blockers,
        "conflict_warnings": conflict_warnings,
        "risk_alerts": risk_alerts,
        "candidate_plans": candidate_plans,
        "recommended_plan_id": recommended_plan_id,
        "next_questions": next_questions_map.get(current_focus, []),
    }


def _interactive_delivery_status(
    completeness: float,
    builder_insights: dict[str, Any] | None = None,
) -> dict[str, Any]:
    ready_to_generate = completeness >= 1.0
    insights = builder_insights if isinstance(builder_insights, dict) else {}
    blockers = insights.get("blockers", []) if isinstance(insights.get("blockers", []), list) else []
    risk_alerts = (
        insights.get("risk_alerts", [])
        if isinstance(insights.get("risk_alerts", []), list)
        else []
    )
    if ready_to_generate:
        next_action = "生成策略并保存到策略集市"
        if risk_alerts:
            next_action += "，但建议先确认风控提醒"
        return {
            "stage": "ready_to_generate",
            "can_generate": True,
            "can_publish_to_marketplace": True,
            "can_deploy": False,
            "next_action": next_action,
            "deploy_gate": "进入策略集市后需先审核通过并晋级到 paper，才能部署运行",
        }
    first_blocker = str(blockers[0] or "").strip() if blockers else ""
    return {
        "stage": "drafting",
        "can_generate": False,
        "can_publish_to_marketplace": False,
        "can_deploy": False,
        "next_action": first_blocker or "继续补全市场假设、入场、出场和风控四个维度",
        "deploy_gate": "当前仍是草稿，暂不能进入策略集市或部署运行",
    }


def _compose_interactive_strategy_document(knowledge: dict[str, Any]) -> str:
    dims = [
        ("market_hypothesis", "Market Hypothesis"),
        ("entry_rule", "Entry Rule"),
        ("exit_rule", "Exit Rule"),
        ("risk_constraints", "Risk Constraints"),
    ]
    parts: list[str] = []
    for key, label in dims:
        value = str(knowledge.get(key, "") or "").strip()
        if value:
            parts.append(f"## {label}\n{value}")
    return "\n\n".join(parts) or "Interactive strategy draft"


def _derive_interactive_strategy_name(knowledge: dict[str, Any]) -> str:
    seed = " ".join(str(knowledge.get("market_hypothesis", "") or "").split())
    if not seed:
        return "Interactive Strategy"
    return seed[:40]


def _interactive_state_payload(state: dict[str, Any] | None = None) -> dict[str, Any]:
    raw_state = state if isinstance(state, dict) else {}
    messages = raw_state.get("messages", [])
    if not isinstance(messages, list):
        messages = []
    knowledge = raw_state.get("knowledge_state", {})
    if not isinstance(knowledge, dict):
        knowledge = {}
    completeness = raw_state.get("completeness", 0.0)
    try:
        completeness = float(completeness or 0.0)
    except Exception:
        completeness = 0.0
    filled_dimensions = raw_state.get("filled_dimensions", [])
    if not isinstance(filled_dimensions, list):
        filled_dimensions = []
    draft_name = _derive_interactive_strategy_name(knowledge)
    draft_document = _compose_interactive_strategy_document(knowledge)
    builder_insights = _build_strategy_builder_insights(knowledge)
    payload = {
        "success": True,
        "conversation_id": str(raw_state.get("conversation_id", "") or ""),
        "messages": [
            normalized
            for normalized in (_normalize_interactive_message(item) for item in messages)
            if normalized
        ],
        "knowledge_state": {
            **knowledge,
            "completeness": completeness,
            "current_focus": str(
                knowledge.get("current_focus", "market_hypothesis")
                or "market_hypothesis"
            ),
        },
        "filled_dimensions": [str(item) for item in filled_dimensions],
        "completeness": completeness,
        "draft_name": draft_name,
        "draft_document": draft_document,
        "delivery_status": _interactive_delivery_status(completeness, builder_insights),
        "builder_insights": builder_insights,
    }
    return payload


def _sanitize_interactive_state_storage(state: dict[str, Any]) -> tuple[dict[str, Any], bool]:
    raw_state = copy.deepcopy(state) if isinstance(state, dict) else {}
    messages = raw_state.get("messages", [])
    if not isinstance(messages, list):
        messages = []
    sanitized_messages = [
        normalized
        for normalized in (_normalize_interactive_message(item) for item in messages)
        if normalized
    ]
    changed = sanitized_messages != messages
    raw_state["messages"] = sanitized_messages
    return raw_state, changed


def _load_interactive_state_with_fallback() -> dict[str, Any]:
    state = _load_interactive_state()
    if isinstance(state, dict) and state:
        sanitized_state, changed = _sanitize_interactive_state_storage(state)
        if changed:
            _save_interactive_state(sanitized_state)
        return sanitized_state
    try:
        from src.unified_strategy_state import get_custom_workspace, load_unified_state

        workspace = get_custom_workspace(load_unified_state(use_cache=False))
        if not isinstance(workspace, dict) or not workspace:
            return {}
        conversation = (
            workspace.get("conversation", {})
            if isinstance(workspace.get("conversation", {}), dict)
            else {}
        )
        knowledge = (
            workspace.get("knowledge_state", {})
            if isinstance(workspace.get("knowledge_state", {}), dict)
            else {}
        )
        if not conversation and not knowledge:
            return {}
        state_from_workspace = {
            "conversation_id": str(
                conversation.get("conversation_id", workspace.get("conversation_id", ""))
                or ""
            ),
            "messages": conversation.get("messages", []),
            "knowledge_state": knowledge,
            "filled_dimensions": conversation.get("filled_dimensions", []),
            "completeness": conversation.get(
                "completeness", knowledge.get("completeness", 0.0)
            ),
            "updated_at": str(
                conversation.get("updated_at", workspace.get("updated_at", "")) or ""
            ),
        }
        sanitized_state, changed = _sanitize_interactive_state_storage(state_from_workspace)
        if changed:
            _save_interactive_state(sanitized_state)
        return sanitized_state
    except Exception:
        return {}


def _sync_interactive_workspace(state: dict[str, Any]) -> None:
    try:
        from src.unified_strategy_state import (
            get_custom_workspace,
            invalidate_cache,
            load_unified_state,
            save_unified_state,
        )

        invalidate_cache()
        unified = load_unified_state(use_cache=False)
        current_workspace = get_custom_workspace(unified)
        next_workspace = copy.deepcopy(current_workspace)
        state_payload = _interactive_state_payload(state)
        knowledge = state_payload.get("knowledge_state", {})
        conversation = {
            "conversation_id": state_payload.get("conversation_id", ""),
            "messages": state_payload.get("messages", []),
            "filled_dimensions": state_payload.get("filled_dimensions", []),
            "completeness": state_payload.get("completeness", 0.0),
            "updated_at": str(state.get("updated_at", "") or ""),
        }
        draft = (
            next_workspace.get("draft", {})
            if isinstance(next_workspace.get("draft", {}), dict)
            else {}
        )
        workflow_meta = (
            draft.get("workflowMeta", {})
            if isinstance(draft.get("workflowMeta", {}), dict)
            else {}
        )
        developer_agent = (
            workflow_meta.get("developerAgent", {})
            if isinstance(workflow_meta.get("developerAgent", {}), dict)
            else {}
        )
        developer_agent.update(
            {
                "role": _STRATEGY_DEVELOPER_ROLE,
                "mode": "interactive",
                "transport": "websocket",
            }
        )
        workflow_meta["developerAgent"] = developer_agent
        draft.update(
            {
                "name": str(state_payload.get("draft_name", "") or ""),
                "document": str(state_payload.get("draft_document", "") or ""),
                "compileTarget": "template",
                "strategySpec": {},
                "optimized": {},
                "evaluation": {},
                "validationReport": {},
                "workflowMeta": workflow_meta,
                "saveState": "saved",
                "lastSavedAt": str(state.get("updated_at", "") or ""),
            }
        )
        next_workspace.update(
            {
                "mode": "interactive",
                "conversation": conversation,
                "knowledge_state": knowledge,
                "name": str(state_payload.get("draft_name", "") or ""),
                "document": str(state_payload.get("draft_document", "") or ""),
                "draft": draft,
            }
        )
        unified["custom_workspace"] = next_workspace
        save_unified_state(unified)
    except Exception:
        pass


def _clear_interactive_workspace_state() -> None:
    try:
        from src.unified_strategy_state import (
            get_custom_workspace,
            invalidate_cache,
            load_unified_state,
            save_unified_state,
        )

        invalidate_cache()
        unified = load_unified_state(use_cache=False)
        current_workspace = get_custom_workspace(unified)
        next_workspace = copy.deepcopy(current_workspace)
        for key in ("conversation", "knowledge_state", "mode", "name", "document"):
            next_workspace.pop(key, None)
        draft = (
            next_workspace.get("draft", {})
            if isinstance(next_workspace.get("draft", {}), dict)
            else {}
        )
        workflow_meta = (
            draft.get("workflowMeta", {})
            if isinstance(draft.get("workflowMeta", {}), dict)
            else {}
        )
        developer_agent = workflow_meta.get("developerAgent", {})
        if isinstance(developer_agent, dict) and str(developer_agent.get("mode", "")) == "interactive":
            draft.update(
                {
                    "name": "",
                    "document": "",
                    "compileTarget": "template",
                    "strategySpec": {},
                    "optimized": {},
                    "evaluation": {},
                    "validationReport": {},
                    "saveState": "saved",
                    "lastSavedAt": "",
                }
            )
            next_workspace["draft"] = draft
        unified["custom_workspace"] = next_workspace
        save_unified_state(unified)
    except Exception:
        pass


def _reset_interactive_state() -> None:
    try:
        if _INTERACTIVE_STATE_FILE.exists():
            _INTERACTIVE_STATE_FILE.unlink()
    except Exception:
        pass
    _clear_interactive_workspace_state()


_DIMENSION_ORDER = ["market_hypothesis", "entry_rule", "exit_rule", "risk_constraints"]


def _compute_knowledge_completeness(knowledge: dict[str, Any]) -> tuple[float, list[str], str]:
    """Return (completeness 0.0-1.0, filled_dimensions, current_focus)."""
    filled = []
    for dim in _DIMENSION_ORDER:
        val = knowledge.get(dim)
        if val and isinstance(val, str) and len(val.strip()) >= 6:
            filled.append(dim)
    completeness = len(filled) / max(len(_DIMENSION_ORDER), 1)
    next_focus = ""
    for dim in _DIMENSION_ORDER:
        if dim not in filled:
            next_focus = dim
            break
    if not next_focus and filled:
        next_focus = filled[-1]
    return completeness, filled, next_focus or "market_hypothesis"


def _strategy_builder_system_prompt(knowledge: dict[str, Any]) -> str:
    filled_dims = []
    for dim in _DIMENSION_ORDER:
        val = knowledge.get(dim)
        if val and isinstance(val, str) and len(val.strip()) >= 6:
            filled_dims.append(dim)
    missing_dims = [d for d in _DIMENSION_ORDER if d not in filled_dims]
    current = missing_dims[0] if missing_dims else "all_complete"

    dim_guide = {
        "market_hypothesis": "market_hypothesis: what market behavior/pattern does this strategy exploit? (e.g. trend following, mean reversion, breakout)",
        "entry_rule": "entry_rule: precise conditions to enter a trade (indicators, thresholds, confirmations)",
        "exit_rule": "exit_rule: when to exit (stop loss, take profit, trailing stop rules)",
        "risk_constraints": "risk_constraints: position sizing, max risk per trade, max drawdown limits, session filters",
    }

    filled_summary = ""
    for dim in _DIMENSION_ORDER:
        val = knowledge.get(dim)
        if val and isinstance(val, str) and len(val.strip()) >= 6:
            filled_summary += f"{dim}: {val}\n"

    return (
        "你是 Gold AutoTrade 的 AI 策略开发师，负责通过对话引导用户逐步完善交易策略。\n\n"
        "你需要引导用户完成4个维度：\n"
        "1. market_hypothesis - 市场假设\n"
        "2. entry_rule - 入场规则\n"
        "3. exit_rule - 出场规则\n"
        "4. risk_constraints - 风控约束\n\n"
        f"当前已完成的维度：\n{filled_summary if filled_summary else '(暂无)'}\n\n"
        f"当前需要收集的维度：{current}\n"
        f"引导提示：{dim_guide.get(current, '')}\n\n"
        "规则：\n"
        "- 每次优先引导用户完成当前维度，但允许顺手指出明显冲突、模糊点和高风险缺口\n"
        "- 用友好的对话语气，问具体的问题引导用户思考\n"
        "- 当用户回答包含足够信息时，提取关键要素并确认\n"
        "- 如果用户描述里同时出现两种可行方向，可以在 reply 里给出 2 套候选走法，并说明更推荐哪套\n"
        "- 目标不是填满四个框，而是帮用户形成能进入审核和 paper 流程的策略资产\n"
        "- 4个维度全部完成后，告知用户可以点击 Generate Strategy 按钮\n"
        "- 回复最后一行必须是一个 JSON 对象（单行，无换行），格式：\n"
        '  {"knowledge_update": {"dimension_key": "extracted value or null"}, "focus_next": "dimension_key", "reply": "你的对话回复文本"}\n'
        "- knowledge_update 只包含本次用户消息中新提取的维度值\n"
        "- 如果用户消息没有提供新信息，knowledge_update 设为 {}\n"
        "- reply 用中文回复\n"
    )


def _parse_strategy_builder_response(content: str) -> dict[str, Any]:
    """Extract the JSON control block from LLM response."""
    import re
    visible_content = re.sub(r"<think>[\s\S]*?</think>", "", str(content or ""), flags=re.IGNORECASE).strip()
    fenced_matches = re.findall(r"```(?:json)?\s*(\{[\s\S]*?\})\s*```", visible_content, flags=re.IGNORECASE)
    for candidate in reversed(fenced_matches):
        try:
            parsed = json.loads(candidate)
            if isinstance(parsed, dict) and "reply" in parsed:
                return parsed
        except Exception:
            pass
    # Try to find JSON on the last line
    lines = visible_content.split("\n")
    for line in reversed(lines):
        line = line.strip()
        if line.startswith("{") and line.endswith("}"):
            try:
                parsed = json.loads(line)
                if isinstance(parsed, dict) and "reply" in parsed:
                    return parsed
            except Exception:
                pass
    # Fallback: treat entire content as reply
    return {"reply": visible_content or "收到，我继续帮你完善策略。", "knowledge_update": {}, "focus_next": ""}


def _run_strategy_builder_llm(system_prompt: str, user_prompt: str) -> tuple[str, dict[str, Any], bool]:
    try:
        from src.llm import call_agent_llm

        result = call_agent_llm(
            role=_STRATEGY_DEVELOPER_ROLE,
            system_prompt=system_prompt,
            user_prompt=user_prompt,
            temperature=0.4,
            max_tokens=800,
            timeout=60,
        )
        if result.get("degraded", False):
            reply = (
                "抱歉，AI 策略开发师暂时不可用（"
                + str(result.get("degraded_reason", "未知错误"))
                + "）。请稍后重试或切换到「策略生成」模式手动填写。"
            )
            return reply, {"reply": reply, "knowledge_update": {}, "focus_next": ""}, False
        content_text = str(result.get("content", ""))
        parsed = _parse_strategy_builder_response(content_text)
        reply = str(parsed.get("reply", content_text[:400]) or "收到，我继续帮你完善策略。")
        return reply, parsed, True
    except Exception as exc:
        reply = f"AI 策略开发师调用失败：{exc}"
        return reply, {"reply": reply, "knowledge_update": {}, "focus_next": ""}, False


def _process_strategy_builder_turn(
    *,
    user_text: Any,
    conversation_id: str = "",
    message_kind: str = "text",
    message_meta: dict[str, Any] | None = None,
) -> dict[str, Any]:
    conv_id = str(conversation_id or "").strip()
    state = _load_interactive_state_with_fallback()
    if not conv_id:
        conv_id = str(state.get("conversation_id", "") or "").strip()
    if not conv_id:
        conv_id = uuid.uuid4().hex[:12]

    messages = state.get("messages", [])
    if not isinstance(messages, list):
        messages = []
    knowledge = state.get("knowledge_state", {})
    if not isinstance(knowledge, dict):
        knowledge = {}

    normalized_history = [
        normalized
        for normalized in (_normalize_interactive_message(item) for item in messages[-10:])
        if normalized
    ]
    system_prompt = _strategy_builder_system_prompt(knowledge)
    convo_text = ""
    for item in normalized_history:
        role_tag = "用户" if item.get("role") == "user" else "AI"
        convo_text += f"{role_tag}: {item.get('content', '')}\n"

    try:
        builder_caps = _strategy_generation_capabilities()
    except Exception:
        builder_caps = {"recommendedAudience": "standard", "tier": "trial"}
    user_prompt = (
        f"对话历史：\n{convo_text}\n"
        f"当前知识状态：{json.dumps(knowledge, ensure_ascii=False)}\n"
        f"当前建议模式：{str(builder_caps.get('recommendedAudience', 'standard') or 'standard')}\n"
        f"用户最新消息：{str(user_text or '')}\n\n"
        "请根据上述规则回复。最后一行必须是 JSON 控制对象。"
    )
    reply, parsed, llm_used = _run_strategy_builder_llm(system_prompt, user_prompt)

    knowledge_update = parsed.get("knowledge_update", {})
    if isinstance(knowledge_update, dict):
        for dim in _DIMENSION_ORDER:
            value = knowledge_update.get(dim)
            if value and isinstance(value, str) and len(value.strip()) >= 6:
                knowledge[dim] = value.strip()

    completeness, filled_dims, current_focus = _compute_knowledge_completeness(knowledge)
    knowledge["completeness"] = completeness
    knowledge["current_focus"] = str(parsed.get("focus_next", "") or current_focus)

    now_ts = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    messages.append(
        _normalize_interactive_message(
            {
                "role": "user",
                "content": str(user_text or ""),
                "ts": now_ts,
                "kind": _interactive_message_kind(str(message_kind or ""), _interactive_message_kind(str(user_text or ""))),
                "meta": message_meta or {},
            }
        )
    )
    messages.append(
        _normalize_interactive_message(
            {
                "role": "ai",
                "content": reply,
                "ts": now_ts,
                "kind": _interactive_message_kind(reply),
            }
        )
    )
    if len(messages) > 40:
        messages = messages[-40:]

    new_state = {
        "conversation_id": conv_id,
        "messages": messages,
        "knowledge_state": knowledge,
        "filled_dimensions": filled_dims,
        "completeness": completeness,
        "updated_at": now_ts,
    }
    _save_interactive_state(new_state)
    _sync_interactive_workspace(new_state)
    payload = _interactive_state_payload(new_state)
    return {
        **payload,
        "reply": reply,
        "should_prompt_generation": completeness >= 1.0,
        "llm_used": llm_used,
        "message_kind": _interactive_message_kind(reply),
    }


def _generate_strategy_from_interactive_state(
    *,
    temp_model: str = "",
    compile_target: str = "template",
) -> dict[str, Any]:
    try:
        compile_target = _enforce_strategy_compile_target_access(compile_target)
    except PermissionError as exc:
        return {"success": False, "error": str(exc)}
    interactive_state = _load_interactive_state_with_fallback()
    payload = _interactive_state_payload(interactive_state)
    knowledge = (
        payload.get("knowledge_state", {})
        if isinstance(payload.get("knowledge_state", {}), dict)
        else {}
    )
    completeness = float(payload.get("completeness", 0.0) or 0.0)
    missing_dimensions = [
        dim
        for dim in _DIMENSION_ORDER
        if len(str(knowledge.get(dim, "") or "").strip()) < 6
    ]
    if completeness < 1.0 or missing_dimensions:
        return {
            "success": False,
            "error": f"互动开发草稿尚未完成，请继续补全：{', '.join(missing_dimensions or _DIMENSION_ORDER)}",
        }

    normalized_temp_model = _normalize_temp_model(temp_model)
    name = str(payload.get("draft_name", "") or "").strip()
    document = str(payload.get("draft_document", "") or "").strip()
    if len(name) < 2:
        return {"success": False, "error": "互动开发草稿缺少有效策略名称"}
    if len(document) < 12:
        return {"success": False, "error": "互动开发草稿文档过短，无法生成策略"}

    optimized = _heuristic_optimize_strategy_custom_draft(name, document)
    optimized.update(
        {
            "strategy_name": name,
            "market_hypothesis": str(
                knowledge.get("market_hypothesis")
                or optimized.get("market_hypothesis")
                or ""
            ).strip(),
            "entry_rule": str(
                knowledge.get("entry_rule") or optimized.get("entry_rule") or ""
            ).strip(),
            "exit_rule": str(
                knowledge.get("exit_rule") or optimized.get("exit_rule") or ""
            ).strip(),
        }
    )
    risk_constraints = (
        optimized.get("risk_constraints", {})
        if isinstance(optimized.get("risk_constraints", {}), dict)
        else {}
    )
    optimized["risk_constraints"] = {
        "max_position_size": float(risk_constraints.get("max_position_size", 0.1) or 0.1),
        "max_open_positions": int(risk_constraints.get("max_open_positions", 1) or 1),
        "min_confidence": int(risk_constraints.get("min_confidence", 70) or 70),
    }

    evaluation = _heuristic_evaluate_strategy_custom_draft(document, optimized)
    if str(evaluation.get("verdict", "") or "").strip().lower() != "pass":
        evaluation.update(
            {
                "verdict": "pass",
                "riskExposure": "normal",
                "advice": "互动开发四个维度已补全，允许进入策略集市等待审核。",
            }
        )

    draft_state = _load_strategy_custom_state()
    current_draft = (
        draft_state.get("draft", {})
        if isinstance(draft_state.get("draft", {}), dict)
        else {}
    )
    current_workflow_meta = (
        current_draft.get("workflowMeta", {})
        if isinstance(current_draft.get("workflowMeta", {}), dict)
        else {}
    )
    developer_agent = _strategy_developer_binding()
    developer_agent.update(
        {
            "role": _STRATEGY_DEVELOPER_ROLE,
            "mode": "interactive",
            "transport": "websocket",
        }
    )
    handoff_llm_meta = {
        "used": False,
        "degraded_reason": "interactive_handoff_heuristic",
    }
    workflow_meta = _merge_strategy_workflow_meta(
        current_workflow_meta,
        developer_agent=developer_agent,
        temp_model=normalized_temp_model,
        call_logs=[
            _build_strategy_call_log_entry(
                stage="interactive_generate_handoff",
                developer_agent=developer_agent,
                llm_meta=handoff_llm_meta,
                temp_model=normalized_temp_model,
                prompt_summary=document,
                prompt_scope="interactive_strategy_draft",
            )
        ],
    )

    _evaluate_and_persist_strategy_custom_draft(
        name=name,
        document=document,
        optimized=optimized,
        workflow_meta=workflow_meta,
        temp_model=normalized_temp_model,
        compile_target=str(compile_target or "template"),
    )
    result = generate_strategy_from_custom_draft(
        StrategyDraftGenerateRequest(
            name=name,
            document=document,
            optimized=optimized,
            evaluation=evaluation,
            temp_model=normalized_temp_model,
            compile_target=str(compile_target or "template"),
        )
    )
    if isinstance(result, dict) and bool(result.get("success", False)):
        result["message"] = str(
            result.get("message", "") or "互动开发策略已生成并保存到策略集市"
        )
    return result


def _strategy_developer_binding() -> dict[str, Any]:
    binding = {
        "role": _STRATEGY_DEVELOPER_ROLE,
        "model": "",
        "provider": "",
        "configured": False,
    }
    try:
        from src.llm.config import resolve_agent_model

        agent = resolve_agent_model(_STRATEGY_DEVELOPER_ROLE) or {}
        if isinstance(agent, dict):
            model_id = str(agent.get("model", "") or "")
            binding["model"] = model_id
            binding["provider"] = model_id.split("/", 1)[0] if "/" in model_id else ""
            binding["configured"] = bool(model_id)
    except Exception:
        pass
    return binding


def _normalize_temp_model(model_value: str) -> str:
    return str(model_value or "").strip()


def _summarize_prompt_text(text: str, limit: int = 120) -> str:
    compact = " ".join(str(text or "").split())
    if len(compact) <= limit:
        return compact
    return f"{compact[: max(0, limit - 3)]}..."


def _classify_llm_failure(llm_meta: dict[str, Any]) -> str:
    error_type = str(llm_meta.get("error_type", "") or "").strip().lower()
    degraded_reason = str(llm_meta.get("degraded_reason", "") or "").strip().lower()
    if error_type in {"", "none"}:
        if "invalid_json" in degraded_reason:
            return "response_format"
        if "import_failed" in degraded_reason:
            return "runtime_import"
        if "timeout" in degraded_reason:
            return "timeout"
        if "api key" in degraded_reason or "api_key" in degraded_reason:
            return "credential"
        if "provider" in degraded_reason:
            return "provider_config"
        return "fallback"
    if error_type in {"api_key_missing", "auth_error", "permission_error"}:
        return "credential"
    if error_type in {"provider_not_found", "base_url_missing", "endpoint_not_found"}:
        return "provider_config"
    if error_type in {"timeout", "connection_error"}:
        return error_type
    if error_type in {"rate_limit"}:
        return "rate_limit"
    if error_type in {"provider_server_error", "http_error"}:
        return "provider_runtime"
    if error_type in {"llm_invalid_json"}:
        return "response_format"
    return error_type


def _build_strategy_call_log_entry(
    *,
    stage: str,
    developer_agent: dict[str, Any],
    llm_meta: dict[str, Any],
    temp_model: str,
    prompt_summary: str = "",
    prompt_scope: str = "",
) -> dict[str, Any]:
    effective_model = str(
        llm_meta.get("model")
        or temp_model
        or developer_agent.get("model")
        or ""
    )
    provider = ""
    if "/" in effective_model:
        provider = effective_model.split("/", 1)[0]
    elif str(developer_agent.get("provider", "")):
        provider = str(developer_agent.get("provider", ""))
    failure_class = "" if bool(llm_meta.get("used", False)) else _classify_llm_failure(llm_meta)
    return {
        "stage": stage,
        "role": str(developer_agent.get("role", _STRATEGY_DEVELOPER_ROLE)),
        "configuredModel": str(developer_agent.get("model", "")),
        "tempModel": temp_model,
        "effectiveModel": effective_model,
        "provider": str(llm_meta.get("provider", "") or provider),
        "apiType": str(llm_meta.get("api_type", "") or ""),
        "usedLlm": bool(llm_meta.get("used", False)),
        "degradedReason": str(llm_meta.get("degraded_reason", "")),
        "failureClass": failure_class,
        "errorType": str(llm_meta.get("error_type", "") or ""),
        "httpStatus": llm_meta.get("http_status"),
        "providerError": str(llm_meta.get("provider_error", "") or ""),
        "latencyMs": float(llm_meta.get("latency_ms", 0.0) or 0.0),
        "usage": llm_meta.get("usage", {}) if isinstance(llm_meta.get("usage", {}), dict) else {},
        "costUsd": llm_meta.get("cost_usd"),
        "retryCount": int(llm_meta.get("retry_count", 0) or 0),
        "attemptCount": int(llm_meta.get("attempt_count", 1) or 1),
        "promptSummary": _summarize_prompt_text(prompt_summary),
        "promptScope": prompt_scope,
        "ts": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
    }


def _merge_strategy_workflow_meta(
    existing: Optional[dict[str, Any]],
    *,
    developer_agent: Optional[dict[str, Any]] = None,
    temp_model: str = "",
    optimize_llm: Optional[dict[str, Any]] = None,
    evaluate_llm: Optional[dict[str, Any]] = None,
    generate_llm: Optional[dict[str, Any]] = None,
    call_logs: Optional[list[dict[str, Any]]] = None,
) -> dict[str, Any]:
    workflow = dict(existing or {}) if isinstance(existing, dict) else {}
    if isinstance(developer_agent, dict):
        workflow["developerAgent"] = developer_agent
    if temp_model:
        workflow["tempModel"] = temp_model
    elif "tempModel" not in workflow:
        workflow["tempModel"] = ""
    if isinstance(optimize_llm, dict):
        workflow["optimizeLlm"] = optimize_llm
    if isinstance(evaluate_llm, dict):
        workflow["evaluateLlm"] = evaluate_llm
    if isinstance(generate_llm, dict):
        workflow["generateLlm"] = generate_llm
    history = workflow.get("callLogs", [])
    if not isinstance(history, list):
        history = []
    incoming = [item for item in (call_logs or []) if isinstance(item, dict)]
    workflow["callLogs"] = (incoming + history)[:_STRATEGY_CUSTOM_CALL_LOG_LIMIT]
    return workflow


def _extract_json_object_from_text(raw_text: str) -> dict[str, Any]:
    text = str(raw_text or "").strip()
    if not text:
        return {}
    if text.startswith("```"):
        text = text.removeprefix("```json").removeprefix("```").strip()
        if text.endswith("```"):
            text = text[:-3].strip()
    try:
        payload = json.loads(text)
        return payload if isinstance(payload, dict) else {}
    except Exception:
        pass
    start = text.find("{")
    end = text.rfind("}")
    if start >= 0 and end > start:
        try:
            payload = json.loads(text[start : end + 1])
            return payload if isinstance(payload, dict) else {}
        except Exception:
            return {}
    return {}


def _heuristic_optimize_strategy_custom_draft(name: str, document: str) -> dict[str, Any]:
    base_engine = _infer_base_engine_from_doc(document)
    return {
        "strategy_name": str(name or "").strip(),
        "market_hypothesis": document[:180],
        "entry_rule": "signal_confirmed_and_spread_safe",
        "exit_rule": "exit_optimizer_or_sl_tp",
        "risk_constraints": {
            "max_position_size": 0.1,
            "max_open_positions": 1,
            "min_confidence": 70,
        },
        "base_engine": base_engine,
        "params": {
            "timeframe": "M5",
            "risk_multiplier": 1.0,
            "sl_multiplier": 1.0,
            "tp_multiplier": 1.0,
        },
    }


def _heuristic_evaluate_strategy_custom_draft(document: str, optimized: dict[str, Any]) -> dict[str, Any]:
    completeness = min(100, max(40, len(document) // 6))
    has_risk = any(token in document.lower() for token in ["风控", "止损", "风险", "loss", "risk"])
    risk_exposure = "medium"
    if has_risk and completeness >= 70:
        risk_exposure = "normal"
    if completeness < 55:
        risk_exposure = "high"
    return {
        "score": {
            "executability": int(min(95, completeness + (10 if bool(optimized) else 0))),
            "robustness": int(min(90, completeness)),
            "risk_control": 78 if has_risk else 52,
        },
        "riskExposure": risk_exposure,
        "verdict": "pass" if has_risk and completeness >= 60 else "needs_improve",
        "advice": (
            "结构可执行，建议进入生成闸门"
            if has_risk and completeness >= 60
            else "建议补充风控阈值与出场边界后再生成策略"
        ),
    }


def _llm_optimize_strategy_custom_draft(
    name: str,
    document: str,
    temp_model: str = "",
) -> tuple[dict[str, Any], dict[str, Any]]:
    try:
        from src.llm import call_agent_llm
    except Exception as exc:
        return {}, {
            "used": False,
            "degraded_reason": f"llm_import_failed:{type(exc).__name__}",
            "usage": {},
            "cost_usd": None,
            "retry_count": 0,
            "attempt_count": 1,
        }
    system_prompt = (
        "你是黄金自动交易系统的 AI 策略开发师。"
        "你的任务是把策略文档编译成结构化的标准 JSON。"
        "只返回一个 JSON 对象，不要输出解释。"
        "必须返回紧凑 JSON，不要 markdown，不要换行解释，不要长段落。"
        "字段至少包含 strategy_name, market_hypothesis, entry_rule, exit_rule, risk_constraints, base_engine, params。"
        "market_hypothesis, entry_rule, exit_rule 必须是简短字符串，不要嵌套对象。"
        "risk_constraints 必须只包含 max_position_size, max_open_positions, min_confidence 三个数字字段。"
        "params 必须是扁平对象，只保留 6-10 个最关键参数，值优先使用短字符串、数字、布尔值。"
        "base_engine 只能从 ema_macd, wyckoff, composite 中选择最合适的。"
    )
    payload = {"name": name, "document": document}

    def _result_meta(raw_result: dict[str, Any], *, invalid_json: bool = False) -> dict[str, Any]:
        if invalid_json:
            return {
                "used": False,
                "degraded_reason": "llm_invalid_json",
                "model": str(raw_result.get("model", "")),
                "provider": str(raw_result.get("provider", "")),
                "api_type": str(raw_result.get("api_type", "")),
                "error_type": "llm_invalid_json",
                "http_status": raw_result.get("http_status"),
                "provider_error": str(raw_result.get("provider_error", "")),
                "usage": raw_result.get("usage", {}) if isinstance(raw_result.get("usage", {}), dict) else {},
                "cost_usd": raw_result.get("cost_usd"),
                "retry_count": int(raw_result.get("retry_count", 0) or 0),
                "attempt_count": int(raw_result.get("attempt_count", 1) or 1),
            }
        if bool(raw_result.get("degraded", False)):
            return {
                "used": False,
                "degraded_reason": str(raw_result.get("degraded_reason", "llm_degraded")),
                "model": str(raw_result.get("model", "")),
                "provider": str(raw_result.get("provider", "")),
                "api_type": str(raw_result.get("api_type", "")),
                "error_type": str(raw_result.get("error_type", "")),
                "http_status": raw_result.get("http_status"),
                "provider_error": str(raw_result.get("provider_error", "")),
                "usage": raw_result.get("usage", {}) if isinstance(raw_result.get("usage", {}), dict) else {},
                "cost_usd": raw_result.get("cost_usd"),
                "retry_count": int(raw_result.get("retry_count", 0) or 0),
                "attempt_count": int(raw_result.get("attempt_count", 1) or 1),
            }
        return {
            "used": True,
            "model": str(raw_result.get("model", "")),
            "latency_ms": float(raw_result.get("latency_ms", 0.0) or 0.0),
            "provider": str(raw_result.get("provider", "")),
            "api_type": str(raw_result.get("api_type", "")),
            "error_type": "",
            "http_status": raw_result.get("http_status"),
            "provider_error": "",
            "usage": raw_result.get("usage", {}) if isinstance(raw_result.get("usage", {}), dict) else {},
            "cost_usd": raw_result.get("cost_usd"),
            "retry_count": int(raw_result.get("retry_count", 0) or 0),
            "attempt_count": int(raw_result.get("attempt_count", 1) or 1),
        }

    result = call_agent_llm(
        role=_STRATEGY_DEVELOPER_ROLE,
        system_prompt=system_prompt,
        user_prompt=json.dumps(payload, ensure_ascii=False, indent=2),
        temperature=0.2,
        max_tokens=700,
        timeout=90,
        response_format={"type": "json_object"},
        model_override=temp_model or None,
    )
    parsed = (
        {}
        if bool(result.get("degraded", False))
        else _extract_json_object_from_text(str(result.get("content", "")))
    )
    if parsed:
        return parsed, _result_meta(result)

    fallback_model = str(temp_model or "").strip()
    if not fallback_model:
        fallback_model = "deepseek/deepseek-v4-flash"
    heuristic_seed = _heuristic_optimize_strategy_custom_draft(name, document)
    fallback_prompt = (
        "你将收到策略原文摘要和一个启发式骨架。"
        "请只做结构化收口，返回一个紧凑 JSON 对象。"
        "不要解释，不要复述原文，不要输出 markdown。"
        "market_hypothesis, entry_rule, exit_rule 必须是简短字符串。"
        "risk_constraints 只能保留 max_position_size, max_open_positions, min_confidence。"
        "params 必须是扁平对象，限制在 8 个键以内。"
    )
    fallback_payload = {
        "name": name,
        "document_summary": _summarize_prompt_text(document, limit=520),
        "heuristic_seed": heuristic_seed,
    }
    fallback_result = call_agent_llm(
        role=_STRATEGY_DEVELOPER_ROLE,
        system_prompt=fallback_prompt,
        user_prompt=json.dumps(fallback_payload, ensure_ascii=False, indent=2),
        temperature=0.1,
        max_tokens=520,
        timeout=90,
        response_format={"type": "json_object"},
        model_override=fallback_model,
    )
    fallback_parsed = (
        {}
        if bool(fallback_result.get("degraded", False))
        else _extract_json_object_from_text(str(fallback_result.get("content", "")))
    )
    if fallback_parsed:
        return fallback_parsed, _result_meta(fallback_result)

    if not bool(result.get("degraded", False)):
        return {}, _result_meta(result, invalid_json=not parsed)
    if not bool(fallback_result.get("degraded", False)):
        return {}, _result_meta(fallback_result, invalid_json=not fallback_parsed)
    return {}, _result_meta(fallback_result)


def _llm_evaluate_strategy_custom_draft(
    name: str,
    document: str,
    optimized: dict[str, Any],
    temp_model: str = "",
) -> tuple[dict[str, Any], dict[str, Any]]:
    try:
        from src.llm import call_agent_llm
    except Exception as exc:
        return {}, {
            "used": False,
            "degraded_reason": f"llm_import_failed:{type(exc).__name__}",
            "usage": {},
            "cost_usd": None,
            "retry_count": 0,
            "attempt_count": 1,
        }
    system_prompt = (
        "你是黄金自动交易系统的 AI 策略开发师，同时负责对策略草稿做生成前评估。"
        "只返回一个 JSON 对象，不要解释。"
        "字段至少包含 score, riskExposure, verdict, advice。"
        "score 必须是对象，且至少包含 executability, robustness, risk_control，范围 0-100。"
        "verdict 只能是 pass 或 needs_improve。"
    )
    payload = {"name": name, "document": document, "optimized": optimized}
    result = call_agent_llm(
        role=_STRATEGY_DEVELOPER_ROLE,
        system_prompt=system_prompt,
        user_prompt=json.dumps(payload, ensure_ascii=False, indent=2),
        temperature=0.2,
        max_tokens=700,
        timeout=90,
        response_format={"type": "json_object"},
        model_override=temp_model or None,
    )
    if bool(result.get("degraded", False)):
        return {}, {
            "used": False,
            "degraded_reason": str(result.get("degraded_reason", "llm_degraded")),
            "model": str(result.get("model", "")),
            "provider": str(result.get("provider", "")),
            "api_type": str(result.get("api_type", "")),
            "error_type": str(result.get("error_type", "")),
            "http_status": result.get("http_status"),
            "provider_error": str(result.get("provider_error", "")),
            "usage": result.get("usage", {}) if isinstance(result.get("usage", {}), dict) else {},
            "cost_usd": result.get("cost_usd"),
            "retry_count": int(result.get("retry_count", 0) or 0),
            "attempt_count": int(result.get("attempt_count", 1) or 1),
        }
    parsed = _extract_json_object_from_text(str(result.get("content", "")))
    if not parsed:
        return {}, {
            "used": False,
            "degraded_reason": "llm_invalid_json",
            "model": str(result.get("model", "")),
            "provider": str(result.get("provider", "")),
            "api_type": str(result.get("api_type", "")),
            "error_type": "llm_invalid_json",
            "http_status": result.get("http_status"),
            "provider_error": str(result.get("provider_error", "")),
            "usage": result.get("usage", {}) if isinstance(result.get("usage", {}), dict) else {},
            "cost_usd": result.get("cost_usd"),
            "retry_count": int(result.get("retry_count", 0) or 0),
            "attempt_count": int(result.get("attempt_count", 1) or 1),
        }
    return parsed, {
        "used": True,
        "model": str(result.get("model", "")),
        "latency_ms": float(result.get("latency_ms", 0.0) or 0.0),
        "provider": str(result.get("provider", "")),
        "api_type": str(result.get("api_type", "")),
        "error_type": "",
        "http_status": result.get("http_status"),
        "provider_error": "",
        "usage": result.get("usage", {}) if isinstance(result.get("usage", {}), dict) else {},
        "cost_usd": result.get("cost_usd"),
        "retry_count": int(result.get("retry_count", 0) or 0),
        "attempt_count": int(result.get("attempt_count", 1) or 1),
    }


def _infer_base_engine_from_doc(document: str) -> str:
    text = str(document or "").lower()
    if any(token in text for token in ["wave", "波浪", "elliott"]):
        return "ema_macd"
    if any(token in text for token in ["缠论", "中枢", "chanlun"]):
        return "composite"
    if any(token in text for token in ["wyckoff", "spring", "sos", "sow"]):
        return "wyckoff"
    if any(token in text for token in ["组合", "多策略", "composite"]):
        return "composite"
    return "ema_macd"


def _normalized_custom_base_engine(candidate: str, document: str = "") -> str:
    base_engine = str(candidate or "").strip()
    if base_engine in VALID_STRATEGIES:
        return base_engine
    inferred = _infer_base_engine_from_doc(document)
    return inferred if inferred in VALID_STRATEGIES else "ema_macd"


def _build_strategy_template_from_custom_draft(
    strategy_id: str,
    optimized: dict[str, Any],
    *,
    base_engine: str,
) -> dict[str, Any]:
    template = _default_strategy_template(strategy_id)
    params = (
        optimized.get("params", {})
        if isinstance(optimized.get("params", {}), dict)
        else {}
    )
    risk_constraints = (
        optimized.get("risk_constraints", {})
        if isinstance(optimized.get("risk_constraints", {}), dict)
        else {}
    )
    article_bundle = (
        optimized.get("article_bundle", {})
        if isinstance(optimized.get("article_bundle", {}), dict)
        else {}
    )

    def _coerce_float(value: Any) -> Optional[float]:
        try:
            number = float(value)
        except Exception:
            return None
        return number if math.isfinite(number) else None

    def _coerce_int(value: Any) -> Optional[int]:
        try:
            number = int(float(value))
        except Exception:
            return None
        return number

    timeframe = str(params.get("timeframe", template.get("timeframe", "M5")) or "M5").strip() or "M5"
    template["timeframe"] = timeframe
    template["execution_baseline"]["timeframe"] = timeframe

    explicit_risk_per_trade = False
    risk_per_trade = _coerce_float(risk_constraints.get("max_position_size"))
    if risk_per_trade is not None:
        risk_per_trade = round(max(0.001, min(0.05, risk_per_trade)), 4)
        template["risk_per_trade"] = risk_per_trade
        template["execution_baseline"]["risk_per_trade"] = risk_per_trade
        explicit_risk_per_trade = True

    max_positions = _coerce_int(risk_constraints.get("max_open_positions"))
    if max_positions is not None and max_positions > 0:
        max_positions = max(1, min(10, max_positions))
        template["max_positions"] = max_positions
        template["execution_baseline"]["max_positions"] = max_positions

    min_confidence = _coerce_float(risk_constraints.get("min_confidence"))
    if min_confidence is None:
        min_confidence = _coerce_float(params.get("min_confidence"))
    if min_confidence is not None:
        if min_confidence > 1.0:
            min_confidence = min_confidence / 100.0
        template["min_confidence"] = round(max(0.0, min(1.0, min_confidence)), 4)

    risk_multiplier = _coerce_float(params.get("risk_multiplier"))
    if risk_multiplier is not None:
        risk_multiplier = round(max(0.1, min(5.0, risk_multiplier)), 4)
        template["risk_multiplier"] = risk_multiplier
        # Keep derived contract fields aligned with legacy UI params, but never
        # override an explicit risk constraint returned by the draft optimizer.
        if not explicit_risk_per_trade:
            template["risk_per_trade"] = round(max(0.001, min(0.05, 0.01 * risk_multiplier)), 4)
            template["execution_baseline"]["risk_per_trade"] = template["risk_per_trade"]

    sl_multiplier = _coerce_float(params.get("sl_multiplier"))
    if sl_multiplier is not None:
        sl_multiplier = round(max(0.5, min(5.0, sl_multiplier)), 4)
        template["sl_multiplier"] = sl_multiplier
        template["atr_sl_mult"] = round(1.8 * sl_multiplier, 4)
        template["execution_baseline"]["atr_sl_mult"] = template["atr_sl_mult"]

    tp_multiplier = _coerce_float(params.get("tp_multiplier"))
    if tp_multiplier is not None:
        tp_multiplier = round(max(0.2, min(5.0, tp_multiplier)), 4)
        template["tp_multiplier"] = tp_multiplier
        template["tp_rr"] = round(2.0 * tp_multiplier, 4)
        template["execution_baseline"]["tp_rr"] = template["tp_rr"]

    strategy_style = str(article_bundle.get("strategy_style", "") or "").strip().lower()
    if strategy_style not in {"trend", "breakout", "mean_reversion", "momentum"}:
        if base_engine == "wyckoff":
            strategy_style = "breakout"
        elif base_engine == "composite":
            strategy_style = "momentum"
        else:
            strategy_style = "trend"
    template["strategy_style"] = strategy_style
    template["coordination_contract"]["strategy_style"] = strategy_style

    # Multi-source article bundles are usually observation-first unless explicitly directional.
    execution_mode = str(risk_constraints.get("execution_mode", "") or "").strip().lower()
    if execution_mode in {"watch_only", "observe_only"}:
        template["reduce_only"] = True
        template["execution_baseline"]["reduce_only"] = True

    return template


def _infer_marketplace_lifecycle_stage(strategy_id: str, row: Optional[dict[str, Any]] = None) -> str:
    payload = row if isinstance(row, dict) else {}
    explicit = str(payload.get("lifecycle_stage", "") or "").strip().lower()
    if explicit in {"sandbox", "marketplace", "paper", "canary", "full"}:
        return explicit
    if bool(payload.get("is_custom", False)):
        if bool(payload.get("enabled", False)):
            return "paper"
        return "sandbox"
    if strategy_id == "ema_macd":
        return "paper"
    return "marketplace"


def _resolve_marketplace_display_stage(
    strategy_id: str,
    row: Optional[dict[str, Any]] = None,
    strategy_instances: Optional[list[dict[str, Any]]] = None,
    is_runtime_active: bool = False,
) -> str:
    base_stage = _infer_marketplace_lifecycle_stage(strategy_id, row)
    if base_stage in {"canary", "full"}:
        return base_stage
    instances = strategy_instances if isinstance(strategy_instances, list) else []
    has_runtime_instances = any(isinstance(item, dict) for item in instances)
    if is_runtime_active or has_runtime_instances:
        return "paper"
    return base_stage


def _resolve_marketplace_enabled(
    row: Optional[dict[str, Any]] = None,
    strategy_instances: Optional[list[dict[str, Any]]] = None,
    is_runtime_active: bool = False,
) -> bool:
    payload = row if isinstance(row, dict) else {}
    explicit_enabled = bool(payload.get("enabled", True))
    if explicit_enabled:
        return True
    instances = strategy_instances if isinstance(strategy_instances, list) else []
    has_runtime_instances = any(isinstance(item, dict) for item in instances)
    return bool(is_runtime_active or has_runtime_instances)


def _group_marketplace_strategies_by_stage(
    strategies: list[dict[str, Any]],
) -> dict[str, list[dict[str, Any]]]:
    grouped: dict[str, list[dict[str, Any]]] = {}
    for item in strategies:
        if not isinstance(item, dict):
            continue
        stage = str(item.get("_lifecycle_stage", "sandbox") or "sandbox")
        grouped.setdefault(stage, []).append(item)
    return grouped


def _template_needs_custom_backfill(row: dict[str, Any]) -> bool:
    if not isinstance(row, dict) or not bool(row.get("is_custom", False)):
        return False
    optimized = row.get("optimized", {}) if isinstance(row.get("optimized", {}), dict) else {}
    template = row.get("template", {}) if isinstance(row.get("template", {}), dict) else {}
    if not optimized or not template:
        return False
    risk_constraints = (
        optimized.get("risk_constraints", {})
        if isinstance(optimized.get("risk_constraints", {}), dict)
        else {}
    )
    params = optimized.get("params", {}) if isinstance(optimized.get("params", {}), dict) else {}
    expected_risk_per_trade = risk_constraints.get("max_position_size")
    expected_max_positions = risk_constraints.get("max_open_positions")
    expected_min_confidence = risk_constraints.get("min_confidence", params.get("min_confidence"))

    try:
        if expected_risk_per_trade is not None and abs(float(template.get("risk_per_trade", -1)) - float(expected_risk_per_trade)) > 1e-9:
            return True
    except Exception:
        return True
    try:
        if expected_max_positions is not None and int(float(template.get("max_positions", -1))) != int(float(expected_max_positions)):
            return True
    except Exception:
        return True
    if expected_min_confidence is not None and "min_confidence" not in template:
        return True
    return False


def _repair_strategy_market_assets(state: dict[str, Any]) -> dict[str, Any]:
    if not isinstance(state, dict):
        return {"repaired_strategy_ids": []}
    strategies = state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}
    runtime_instances = state.get("runtime_instances", []) if isinstance(state.get("runtime_instances", []), list) else []
    repaired_strategy_ids: list[str] = []
    now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")

    for strategy_id, row in list(strategies.items()):
        if not isinstance(row, dict):
            continue
        lifecycle_stage = _infer_marketplace_lifecycle_stage(str(strategy_id), row)
        if str(row.get("lifecycle_stage", "") or "").strip().lower() != lifecycle_stage:
            row["lifecycle_stage"] = lifecycle_stage
        if not isinstance(row.get("template_versions", []), list):
            row["template_versions"] = []
        if strategy_id in VALID_STRATEGIES:
            current_template = row.get("template", {}) if isinstance(row.get("template", {}), dict) else {}
            if current_template:
                row["template"] = _apply_strategy_taxonomy_to_template(str(strategy_id), current_template)
        if bool(row.get("is_custom", False)) and _template_needs_custom_backfill(row):
            optimized = row.get("optimized", {}) if isinstance(row.get("optimized", {}), dict) else {}
            document = str(row.get("source_document", "") or "")
            base_engine = _normalized_custom_base_engine(str(row.get("base_engine") or optimized.get("base_engine") or ""), document)
            rebuilt_template = _build_strategy_template_from_custom_draft(
                str(strategy_id),
                optimized,
                base_engine=base_engine,
            )
            row["base_engine"] = base_engine
            row["template"] = rebuilt_template
            versions = row.get("template_versions", [])
            if not isinstance(versions, list):
                versions = []
            versions.append(
                {
                    "version": str(row.get("current_version", "1.0.0") or "1.0.0"),
                    "template": copy.deepcopy(rebuilt_template),
                    "captured_at": now_iso,
                    "note": "backfill_custom_runtime_template",
                }
            )
            row["template_versions"] = versions[-20:]
            repaired_strategy_ids.append(str(strategy_id))
        strategies[strategy_id] = row

    for item in runtime_instances:
        if not isinstance(item, dict):
            continue
        sid = str(item.get("strategy_id", "") or "").strip()
        row = strategies.get(sid, {}) if isinstance(strategies.get(sid, {}), dict) else {}
        current_version = str(row.get("current_version", "1.0.0") or "1.0.0")
        template = row.get("template", {}) if isinstance(row.get("template", {}), dict) else {}
        if current_version:
            item["strategy_version"] = current_version
        if template and (not isinstance(item.get("template_snapshot", {}), dict) or sid in repaired_strategy_ids):
            item["template_snapshot"] = copy.deepcopy(template)
        if sid and not str(item.get("strategy_name", "") or "").strip():
            item["strategy_name"] = str(row.get("display_name") or row.get("name") or VALID_STRATEGIES.get(sid, sid))
    state["strategies"] = strategies
    state["runtime_instances"] = runtime_instances
    return {"repaired_strategy_ids": repaired_strategy_ids}

@app.get("/api/strategy/list")
def list_strategies():
    """返回所有可用策略列表"""
    state = _load_strategy_market_state()
    catalog = _strategy_catalog_from_state(state)
    return {"strategies": [{"id": k, "name": v} for k, v in catalog.items()]}


@app.get("/api/strategy/status")
def get_strategy_status():
    """返回当前策略状态（不触发加载）"""
    try:
        from bridge.trader_api import get_signal_generator_status
        status = get_signal_generator_status()
        state = _load_strategy_market_state()
        catalog = _strategy_catalog_from_state(state)

        # 补充：从 effective_params.json 读取当前配置的 strategy_type
        eff_file = LOGS_DIR / "effective_params.json"
        configured_type = "ema_macd"
        if eff_file.exists():
            try:
                eff = json.loads(eff_file.read_text(encoding="utf-8"))
                configured_type = eff.get("strategy_type", "ema_macd")
            except Exception:
                pass

        return {
            "success": True,
            "configured_strategy": configured_type,
            "configured_strategy_name": catalog.get(configured_type) or VALID_STRATEGIES.get(configured_type, "未知"),
            "runtime_status": status,
            "valid_strategies": list(catalog.keys()),
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


def _clear_strategy_asset_selection(eff_data: dict[str, Any]) -> None:
    for key in (
        "strategy_asset_id",
        "strategy_asset_name",
        "strategy_base_engine",
        "strategy_combo_id",
        "strategy_combo_name",
        "strategy_combo_components",
        "strategy_combo_template",
        "strategy_custom_id",
        "strategy_custom_name",
        "strategy_custom_template",
    ):
        eff_data.pop(key, None)


def _apply_strategy_selection_to_effective(
    eff_data: dict[str, Any],
    strategy_id: str,
    row: Optional[dict[str, Any]],
) -> None:
    _clear_strategy_asset_selection(eff_data)
    eff_data["strategy_type"] = strategy_id
    if not isinstance(row, dict):
        return
    if strategy_id in VALID_STRATEGIES:
        return
    eff_data["strategy_asset_id"] = strategy_id
    eff_data["strategy_asset_name"] = str(row.get("display_name") or strategy_id)
    base_engine = str(
        row.get("base_engine")
        or ("composite" if bool(row.get("is_composite", False)) else "ema_macd")
        or "ema_macd"
    )
    eff_data["strategy_base_engine"] = base_engine
    if bool(row.get("is_composite", False)):
        eff_data["strategy_combo_id"] = strategy_id
        eff_data["strategy_combo_name"] = str(row.get("display_name") or strategy_id)
        eff_data["strategy_combo_components"] = (
            row.get("components", [])
            if isinstance(row.get("components", []), list)
            else []
        )
        eff_data["strategy_combo_template"] = (
            row.get("template", {})
            if isinstance(row.get("template", {}), dict)
            else {}
        )
        return
    if bool(row.get("is_custom", False)) or strategy_id.startswith("custom_"):
        eff_data["strategy_custom_id"] = strategy_id
        eff_data["strategy_custom_name"] = str(row.get("display_name") or strategy_id)
        eff_data["strategy_custom_template"] = (
            row.get("template", {})
            if isinstance(row.get("template", {}), dict)
            else {}
        )


def _capture_strategy_template_version(
    row: dict[str, Any],
    *,
    version: str,
    note: str,
    captured_at: Optional[str] = None,
) -> dict[str, Any]:
    target = dict(row) if isinstance(row, dict) else {}
    template = target.get("template", {}) if isinstance(target.get("template", {}), dict) else {}
    history = target.get("template_versions", []) if isinstance(target.get("template_versions", []), list) else []
    ts = str(captured_at or datetime.datetime.now().astimezone().isoformat(timespec="seconds"))
    history.append(
        {
            "version": str(version or target.get("current_version", "1.0.0") or "1.0.0"),
            "template": copy.deepcopy(template),
            "captured_at": ts,
            "note": str(note or "snapshot"),
        }
    )
    target["template_versions"] = history[-20:]
    return target


class StrategySwitchRequest(BaseModel):
    strategy_type: str
    persist: bool = True  # 是否写入 effective_params.json（持久化）


@app.post("/api/strategy/switch")
def switch_strategy(req: StrategySwitchRequest):
    """
    热切换策略（无需重启，立即生效）。
    请求体：{"strategy_type": "composite", "persist": true}
    """
    state = _load_strategy_market_state()
    catalog = _strategy_catalog_from_state(state)
    row = state.get("strategies", {}).get(req.strategy_type, {}) if isinstance(state.get("strategies", {}), dict) else {}
    if req.strategy_type not in catalog:
        return {
            "success": False,
            "error": f"未知策略类型: {req.strategy_type}",
            "valid": list(catalog.keys()),
        }

    try:
        from bridge.trader_api import (
            set_auto_select_enabled,
            set_strategy_override,
            clear_strategy_cache,
            reset_signal_generator,
            warm_signal_generator,
        )

        # 关闭自动切换（手动指定策略时禁用市场感知自动选）
        set_auto_select_enabled(False)
        clear_strategy_cache()
        # persist=true 才落盘；persist=false 只做热切换
        if req.persist:
            with _strategy_market_state_transaction():
                eff_data = _load_effective_params_payload()
                _apply_strategy_selection_to_effective(eff_data, req.strategy_type, row)
                eff_data["strategy_switched_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
                _save_effective_params_payload(eff_data)
            set_strategy_override(None)
        else:
            set_strategy_override(req.strategy_type)

        # 2. 热重置并预热
        old_gen = reset_signal_generator()

        # 3. 触发一次生成器预热（立即加载新策略）
        new_status = warm_signal_generator()

        return {
            "success": True,
            "message": f"策略已切换: {old_gen} → {req.strategy_type}",
            "previous_generator": old_gen,
            "new_strategy_type": req.strategy_type,
            "new_strategy_name": catalog.get(req.strategy_type),
            "runtime_status": new_status,
            "persist": req.persist,
            "persisted": bool(req.persist),
        }
    except Exception as e:
        import traceback
        traceback.print_exc()
        return {"success": False, "error": str(e)}


@app.post("/api/strategy/deploy")
def deploy_strategy(req: StrategyDeployRequest):
    strategy_id = str(req.strategy_id or "").strip()
    with _strategy_market_state_transaction():
        state = _load_strategy_market_state()
        catalog = _strategy_catalog_from_state(state)
        if strategy_id not in catalog:
            return {"success": False, "error": f"未知策略: {strategy_id}"}
        row = state["strategies"].get(strategy_id, {})
        if not bool(row.get("enabled", True)):
            return {"success": False, "error": "策略已停用，无法部署"}
        deploy_guard_error = _strategy_deploy_guard(row)
        if deploy_guard_error:
            return {"success": False, "error": deploy_guard_error}

        instances = state.get("runtime_instances", [])
        now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
        if str(row.get("lifecycle_stage", "") or "") == "paper" and not str(
            row.get("promoted_to_paper_at", "") or ""
        ):
            row["promoted_to_paper_at"] = now_iso
        state["strategies"][strategy_id] = row
        instances.append(
            _normalize_runtime_instance(
                {
                    "instance_id": f"inst_{strategy_id}_{int(time.time() * 1000)}",
                    "strategy_id": strategy_id,
                    "strategy_name": catalog.get(strategy_id, strategy_id),
                    "strategy_version": str(row.get("current_version", "1.0.0") or "1.0.0"),
                    "allocation": _clamp_allocation(req.allocation),
                    "status": "running" if bool(req.activate) else "paused",
                    "role": str(req.role or "live_support"),
                    "config_revision": _effective_config_revision(),
                    "param_overrides": req.param_overrides if isinstance(req.param_overrides, dict) else {},
                    "template_snapshot": row.get("template", {}) if isinstance(row.get("template", {}), dict) else {},
                    "deployed_at": now_iso,
                    "updated_at": now_iso,
                }
            )
        )
        state["runtime_instances"] = instances
        _sync_runtime_scheduler_to_effective(state)
        _save_strategy_market_state(state)
        # 立即失效缓存，确保看板即时同步
        _STRATEGY_MARKETPLACE_CACHE["ts"] = 0.0
        _RUNTIME_INSTANCES_CACHE["ts"] = 0.0

    switch_result: dict[str, Any] = {"success": True}
    if req.activate:
        switch_result = switch_strategy(
            StrategySwitchRequest(strategy_type=strategy_id, persist=True)
        )
    if not bool(switch_result.get("success", False)):
        return {"success": False, "error": str(switch_result.get("error", "部署后激活失败"))}
    _record_system_message(
        kind="deploy",
        title="策略已部署",
        desc=f"`{strategy_id}` 已进入运行实例，当前实例数 {len(state.get('runtime_instances', []))}",
        color="primary",
        source="strategy_marketplace",
        metadata={
            "strategy_id": strategy_id,
            "instance_count": len(state.get("runtime_instances", [])),
            "allocation": _clamp_allocation(req.allocation),
            "role": str(req.role or "live_support"),
            "activate": bool(req.activate),
        },
    )
    return {
        "success": True,
        "message": f"策略 {strategy_id} 已部署并进入运行实例",
        "instance_count": len(state.get("runtime_instances", [])),
        "scheduler": _load_effective_params_payload().get("runtime_scheduler", {}),
        "runtime_status": switch_result.get("runtime_status"),
    }


@app.post("/api/strategy/disable")
def disable_strategy(req: StrategyDisableRequest):
    strategy_id = str(req.strategy_id or "").strip()
    with _strategy_market_state_transaction():
        state = _load_strategy_market_state()
        catalog = _strategy_catalog_from_state(state)
        if strategy_id not in catalog:
            return {"success": False, "error": f"未知策略: {strategy_id}"}
        row = state["strategies"].get(strategy_id, {})
        row["enabled"] = not bool(req.disabled)
        state["strategies"][strategy_id] = row
        if req.disabled:
            runtime_instances = state.get("runtime_instances", []) if isinstance(state.get("runtime_instances", []), list) else []
            state["runtime_instances"] = [
                item for item in runtime_instances
                if not (isinstance(item, dict) and str(item.get("strategy_id", "")).strip() == strategy_id)
            ]
        scheduler = _sync_runtime_scheduler_to_effective(state)
        _save_strategy_market_state(state)
    _record_system_message(
        kind="disable" if bool(req.disabled) else "enable",
        title="策略状态已更新",
        desc=f"`{strategy_id}` 已{'停用' if bool(req.disabled) else '重新启用'}",
        color="warning" if bool(req.disabled) else "success",
        source="strategy_marketplace",
        metadata={
            "strategy_id": strategy_id,
            "enabled": bool(row["enabled"]),
        },
    )
    return {
        "success": True,
        "message": "策略已停用" if req.disabled else "策略已重新启用",
        "strategy_id": strategy_id,
        "enabled": row["enabled"],
        "scheduler": scheduler,
        "runtime_instances": state.get("runtime_instances", []),
    }


@app.post("/api/strategy/undeploy")
def undeploy_strategy(req: StrategyUndeployRequest):
    strategy_id = str(req.strategy_id or "").strip()
    with _strategy_market_state_transaction():
        state = _load_strategy_market_state()
        catalog = _strategy_catalog_from_state(state)
        if strategy_id not in catalog:
            return {"success": False, "error": f"未知策略: {strategy_id}"}
        runtime_instances = state.get("runtime_instances", []) if isinstance(state.get("runtime_instances", []), list) else []
        remaining_instances = []
        removed_instances = []
        for item in runtime_instances:
            if isinstance(item, dict) and str(item.get("strategy_id", "")).strip() == strategy_id:
                removed_instances.append(item)
            else:
                remaining_instances.append(item)
        if not removed_instances:
            return {"success": False, "error": f"策略 `{strategy_id}` 当前没有已部署实例"}
        row = state["strategies"].get(strategy_id, {})
        row["lifecycle_stage"] = "marketplace"
        row["promotion_state"] = {
            "stage": "marketplace",
            "review_status": str(row.get("review_status", "pending") or "pending"),
            "promoted_at": str(
                (
                    row.get("promotion_state", {})
                    if isinstance(row.get("promotion_state", {}), dict)
                    else {}
                ).get("promoted_at", "")
                or ""
            ),
        }
        row["updated_at"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
        state["strategies"][strategy_id] = row
        state["runtime_instances"] = remaining_instances
        scheduler = _sync_runtime_scheduler_to_effective(state)
        _save_strategy_market_state(state)
        _sync_generated_history_entry(strategy_id, row=row)
        _STRATEGY_MARKETPLACE_CACHE["ts"] = 0.0
        _RUNTIME_INSTANCES_CACHE["ts"] = 0.0
    _record_system_message(
        kind="undeploy",
        title="策略已撤出部署",
        desc=f"`{strategy_id}` 已移出运行实例池，共撤出 {len(removed_instances)} 个实例",
        color="warning",
        source="strategy_marketplace",
        metadata={
            "strategy_id": strategy_id,
            "removed_instance_count": len(removed_instances),
            "runtime_instance_count": len(remaining_instances),
        },
    )
    return {
        "success": True,
        "message": f"策略 {strategy_id} 已撤出部署",
        "strategy_id": strategy_id,
        "removed_instance_count": len(removed_instances),
        "runtime_instances": remaining_instances,
        "scheduler": scheduler,
    }


@app.get("/api/strategy/decision_policy")
def get_strategy_decision_policy():
    state = _load_strategy_market_state()
    contract_payload = load_agent_decision_contract()
    policy = state.get("decision_policy")
    if not isinstance(policy, dict) or not policy:
        policy = contract_payload.get("decision_policy", {})
    return {
        "success": True,
        "decision_policy": _normalize_decision_policy(policy),
        "scheduler": _load_effective_params_payload().get("runtime_scheduler", {}),
    }


@app.post("/api/strategy/decision_policy")
def save_strategy_decision_policy(req: StrategyDecisionPolicyRequest):
    if hasattr(req, "model_dump"):
        updates = req.model_dump(exclude_none=True)
    else:
        updates = req.dict(exclude_none=True)
    execution_updates = updates.get("execution", {}) if isinstance(updates.get("execution", {}), dict) else {}
    if "min_confidence" in execution_updates:
        persisted_threshold = _persist_panel_min_confidence_threshold(execution_updates.get("min_confidence"))
        execution_updates["min_confidence"] = persisted_threshold
        updates["execution"] = execution_updates
    with _strategy_market_state_transaction():
        state = _load_strategy_market_state()
        current = state.get("decision_policy", {})
        merged = current if isinstance(current, dict) else {}
        for key in (
            "profile",
            "macro",
            "interpreter",
            "risk",
            "execution",
            "router",
            "session_scheduler",
        ):
            value = updates.get(key)
            if value is None:
                continue
            if isinstance(value, dict):
                existing = merged.get(key, {})
                next_dict = existing.copy() if isinstance(existing, dict) else {}
                next_dict.update(value)
                merged[key] = next_dict
            else:
                merged[key] = value
        state["decision_policy"] = _normalize_decision_policy(merged)
        scheduler = _sync_runtime_scheduler_to_effective(state)
        _save_strategy_market_state(state)
    update_decision_policy_contract(state["decision_policy"], source="gui_backend")
    return {
        "success": True,
        "decision_policy": state["decision_policy"],
        "scheduler": scheduler,
        "message": "多 agent 与多策略协同配置已更新",
    }


@app.post("/api/strategy/instance/action")
def action_strategy_instance(req: StrategyInstanceActionRequest):
    instance_id = str(req.instance_id or "").strip()
    action = str(req.action or "").strip().lower()
    if not instance_id:
        return {"success": False, "error": "缺少实例 ID"}
    if action not in {
        "pause",
        "resume",
        "remove",
        "allocation",
        "role",
        "overrides",
        "clear_overrides",
        "restore_defaults",
    }:
        return {"success": False, "error": f"不支持的动作: {action}"}
    with _strategy_market_state_transaction():
        state = _load_strategy_market_state()
        instances = state.get("runtime_instances", []) if isinstance(state.get("runtime_instances", []), list) else []
        strategies = state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}
        now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
        target_index = -1
        for idx, item in enumerate(instances):
            if isinstance(item, dict) and str(item.get("instance_id", "")).strip() == instance_id:
                target_index = idx
                break
        if target_index < 0:
            return {"success": False, "error": f"未找到实例: {instance_id}"}
        target = instances[target_index]
        if action == "pause":
            target["status"] = "paused"
        elif action == "resume":
            target["status"] = "running"
        elif action == "remove":
            instances.pop(target_index)
        elif action == "allocation":
            target["allocation"] = _clamp_allocation(req.allocation)
        elif action == "role":
            target["role"] = str(req.role or target.get("role", "live_support") or "live_support")
        elif action == "overrides":
            current_overrides = target.get("param_overrides", {})
            merged_overrides = current_overrides if isinstance(current_overrides, dict) else {}
            merged_overrides.update(req.param_overrides if isinstance(req.param_overrides, dict) else {})
            target["param_overrides"] = merged_overrides
        elif action == "clear_overrides":
            target["param_overrides"] = {}
        elif action == "restore_defaults":
            strategy_id = str(target.get("strategy_id", "") or "").strip()
            strategy_row = strategies.get(strategy_id, {}) if strategy_id else {}
            template = strategy_row.get("template", {}) if isinstance(strategy_row.get("template", {}), dict) else {}
            target["param_overrides"] = {}
            if template:
                target["template_snapshot"] = template
        if action != "remove":
            target["updated_at"] = now_iso
            instances[target_index] = target
        state["runtime_instances"] = instances
        scheduler = _sync_runtime_scheduler_to_effective(state)
        _save_strategy_market_state(state)
    return {
        "success": True,
        "message": f"实例操作已执行: {action}",
        "instance_id": instance_id,
        "scheduler": scheduler,
        "runtime_instances": instances,
    }


@app.post("/api/strategy/publish")
def publish_strategy(req: StrategyPublishRequest):
    strategy_id = str(req.strategy_id or "").strip()
    with _strategy_market_state_transaction():
        state = _load_strategy_market_state()
        catalog = _strategy_catalog_from_state(state)
        if strategy_id not in catalog:
            return {"success": False, "error": f"未知策略: {strategy_id}"}
        row = state["strategies"].get(strategy_id, {})
        versions = row.get("versions", []) if isinstance(row.get("versions", []), list) else []
        current_version = str(row.get("current_version", "1.0.0") or "1.0.0")
        next_version = _next_patch_version(current_version)
        versions.append(
            {
                "version": next_version,
                "note": str(req.note or "manual publish"),
                "published_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
            }
        )
        row["versions"] = versions
        row["current_version"] = next_version
        row["lifecycle_stage"] = str(row.get("lifecycle_stage") or "marketplace")
        row = _capture_strategy_template_version(
            row,
            version=next_version,
            note=str(req.note or "manual publish"),
        )
        state["strategies"][strategy_id] = row
        runtime_instances = state.get("runtime_instances", []) if isinstance(state.get("runtime_instances", []), list) else []
        for item in runtime_instances:
            if isinstance(item, dict) and str(item.get("strategy_id", "")).strip() == strategy_id:
                item["strategy_version"] = next_version
                item["updated_at"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
        state["runtime_instances"] = runtime_instances
        _save_strategy_market_state(state)
    _record_system_message(
        kind="publish",
        title="策略发布完成",
        desc=f"`{strategy_id}` 已发布到版本 `{next_version}`",
        color="success",
        source="strategy_marketplace",
        metadata={
            "strategy_id": strategy_id,
            "version": next_version,
            "note": str(req.note or "manual publish"),
        },
    )
    return {
        "success": True,
        "message": f"策略已发布新版本 {next_version}",
        "strategy_id": strategy_id,
        "version": next_version,
        "versions_count": len(versions),
    }


@app.post("/api/strategy/rollback")
def rollback_strategy(req: StrategyRollbackRequest):
    strategy_id = str(req.strategy_id or "").strip()
    with _strategy_market_state_transaction():
        state = _load_strategy_market_state()
        catalog = _strategy_catalog_from_state(state)
        if strategy_id not in catalog:
            return {"success": False, "error": f"未知策略: {strategy_id}"}
        row = state["strategies"].get(strategy_id, {})
        versions = row.get("versions", []) if isinstance(row.get("versions", []), list) else []
        if len(versions) <= 1:
            return {"success": False, "error": "没有可回滚的历史版本"}
        target_version = str(req.target_version or "").strip()
        if target_version:
            exists = [v for v in versions if isinstance(v, dict) and str(v.get("version", "")) == target_version]
            if not exists:
                return {"success": False, "error": f"未找到目标版本: {target_version}"}
            row["current_version"] = target_version
        else:
            row["current_version"] = str(versions[-2].get("version", "1.0.0"))
        target_version = str(row["current_version"] or "1.0.0")
        template_versions = row.get("template_versions", []) if isinstance(row.get("template_versions", []), list) else []
        matched_template = None
        preferred_template = None
        fallback_template = None
        for item in reversed(template_versions):
            if not isinstance(item, dict):
                continue
            if str(item.get("version", "") or "") != target_version:
                continue
            candidate = item.get("template", {})
            if not (isinstance(candidate, dict) and candidate):
                continue
            note = str(item.get("note", "") or "").strip().lower()
            if note != "template_hot_update":
                preferred_template = copy.deepcopy(candidate)
                break
            if fallback_template is None:
                fallback_template = copy.deepcopy(candidate)
        matched_template = preferred_template or fallback_template
        if matched_template:
            row["template"] = matched_template
        state["strategies"][strategy_id] = row
        runtime_instances = state.get("runtime_instances", []) if isinstance(state.get("runtime_instances", []), list) else []
        for item in runtime_instances:
            if not isinstance(item, dict):
                continue
            if str(item.get("strategy_id", "")).strip() != strategy_id:
                continue
            item["strategy_version"] = target_version
            if matched_template:
                item["template_snapshot"] = copy.deepcopy(matched_template)
            item["updated_at"] = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
        state["runtime_instances"] = runtime_instances
        _save_strategy_market_state(state)
    _record_system_message(
        kind="rollback",
        title="策略版本已回滚",
        desc=f"`{strategy_id}` 已回滚到 `{row['current_version']}`",
        color="warning",
        source="strategy_marketplace",
        metadata={
            "strategy_id": strategy_id,
            "version": row["current_version"],
            "target_version": target_version,
        },
    )
    return {
        "success": True,
        "message": f"策略已回滚到 {row['current_version']}",
        "strategy_id": strategy_id,
        "version": row["current_version"],
    }


@app.post("/api/strategy/template")
def upsert_strategy_template(req: StrategyTemplateRequest):
    strategy_id = str(req.strategy_id or "").strip()
    with _strategy_market_state_transaction():
        state = _load_strategy_market_state()
        catalog = _strategy_catalog_from_state(state)
        if strategy_id not in catalog:
            return {"success": False, "error": f"未知策略: {strategy_id}"}
        template = _normalize_strategy_template_payload(req.template)
        template = _apply_strategy_taxonomy_to_template(strategy_id, template)
        if not template:
            return {"success": False, "error": "模板不能为空"}
        validation_error = _validate_strategy_template_payload(template)
        if validation_error:
            return {"success": False, "error": validation_error}
        row = state["strategies"].get(strategy_id, {})
        runtime_instances = state.get("runtime_instances", []) if isinstance(state.get("runtime_instances", []), list) else []
        running_instance_count = sum(
            1
            for item in runtime_instances
            if isinstance(item, dict)
            and str(item.get("strategy_id", "")).strip() == strategy_id
            and str(item.get("status", "running")).strip().lower() == "running"
        )
        refreshed_instance_ids: list[str] = []
        refreshed_instances: list[dict[str, Any]] = []
        now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
        for item in runtime_instances:
            if not isinstance(item, dict):
                refreshed_instances.append(item)
                continue
            if str(item.get("strategy_id", "")).strip() != strategy_id:
                refreshed_instances.append(item)
                continue
            updated_item = dict(item)
            updated_item["template_snapshot"] = template
            updated_item["updated_at"] = now_iso
            refreshed_instances.append(updated_item)
            refreshed_instance_ids.append(str(updated_item.get("instance_id", "") or ""))
        row["template"] = template
        row = _capture_strategy_template_version(
            row,
            version=str(row.get("current_version", "1.0.0") or "1.0.0"),
            note="template_hot_update",
            captured_at=now_iso,
        )
        state["strategies"][strategy_id] = row
        state["runtime_instances"] = refreshed_instances
        _save_strategy_market_state(state)
    _record_system_message(
        kind="template_hot_update" if running_instance_count > 0 else "template",
        title="参数模板已更新",
        desc=(
            f"`{strategy_id}` 模板已保存"
            + (f"，并对 {running_instance_count} 个运行实例完成热更新登记" if running_instance_count > 0 else "")
        ),
        color="primary" if running_instance_count > 0 else "success",
        source="strategy_marketplace",
        metadata={
            "strategy_id": strategy_id,
            "template_keys": sorted(template.keys()),
            "running_instance_count": running_instance_count,
            "refreshed_instance_ids": [item for item in refreshed_instance_ids if item],
        },
    )
    return {
        "success": True,
        "message": "参数模板已保存",
        "strategy_id": strategy_id,
        "template": template,
        "refreshed_instance_ids": [item for item in refreshed_instance_ids if item],
    }


@app.post("/api/strategy/composite/create")
def create_strategy_composite(req: StrategyCompositeCreateRequest):
    name = str(req.name or "").strip()
    if len(name) < 2:
        return {"success": False, "error": "组合名称至少 2 个字符"}
    components = [
        str(item).strip()
        for item in (req.components or [])
        if str(item).strip()
    ]
    dedup_components = list(dict.fromkeys(components))
    if len(dedup_components) < 2:
        return {"success": False, "error": "请至少选择 2 个策略组件"}
    state = _load_strategy_market_state()
    catalog = _strategy_catalog_from_state(state)
    existed_names = {
        str((row.get("display_name") if isinstance(row, dict) else "") or "").strip().lower()
        for row in (state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}).values()
    }
    if name.lower() in existed_names:
        return {"success": False, "error": "组合名称已存在，请使用唯一名称"}
    unknown = [item for item in dedup_components if item not in catalog]
    if unknown:
        return {"success": False, "error": f"存在未知组件: {', '.join(unknown)}"}
    slug_raw = "".join(ch.lower() if ch.isalnum() else "_" for ch in name).strip("_")
    slug = slug_raw[:28] if slug_raw else str(int(time.time()))
    strategy_id = f"combo_{slug}"
    if strategy_id in state.get("strategies", {}):
        strategy_id = f"{strategy_id}_{int(time.time())}"
    now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    row = {
        "display_name": name,
        "enabled": True,
        "is_composite": True,
        "components": dedup_components,
        "base_engine": "composite",
        "template": {
            **_default_strategy_template(strategy_id),
            "components": dedup_components,
            "weights": {item: round(1.0 / len(dedup_components), 4) for item in dedup_components},
        },
        "versions": [
            {
                "version": "1.0.0",
                "note": "composite created",
                "published_at": now_iso,
            }
        ],
        "current_version": "1.0.0",
        "created_at": now_iso,
    }
    state["strategies"][strategy_id] = row
    _save_strategy_market_state(state)
    return {
        "success": True,
        "message": f"组合策略已创建: {name}",
        "strategy_id": strategy_id,
        "components": dedup_components,
        "version": "1.0.0",
    }


@app.get("/api/strategy/custom/state")
def get_strategy_custom_state():
    payload = _load_strategy_custom_state()
    return {"success": True, **payload}


@app.post("/api/strategy/custom/import-url")
def import_strategy_custom_from_url(req: StrategyDraftImportUrlRequest):
    article_url = str(req.url or "").strip()
    if not article_url:
        return {"success": False, "error": "请先提供文章链接"}
    temp_model = _normalize_temp_model(req.temp_model)
    try:
        from src.article_strategy_ingest import import_article_as_strategy_draft

        imported = import_article_as_strategy_draft(
            article_url,
            symbol=str(req.symbol or "XAUUSD").strip() or "XAUUSD",
            preferred_name=str(req.name or "").strip(),
            force_heuristic=bool(req.force_heuristic),
            temp_model=temp_model,
        )
        developer_agent = _strategy_developer_binding()
        import_llm = imported.get("llm", {}) if isinstance(imported.get("llm", {}), dict) else {}
        workflow_meta = _merge_strategy_workflow_meta(
            None,
            developer_agent=developer_agent,
            temp_model=temp_model,
            optimize_llm=import_llm,
            call_logs=[
                _build_strategy_call_log_entry(
                    stage="import_url",
                    developer_agent=developer_agent,
                    llm_meta=import_llm,
                    temp_model=temp_model,
                    prompt_summary=f"单链接导入 {article_url}",
                    prompt_scope="article_import",
                )
            ],
        )
        draft = _evaluate_and_persist_strategy_custom_draft(
            name=str(imported.get("name", "")),
            document=str(imported.get("document", "")),
            optimized=(
                imported.get("optimized", {})
                if isinstance(imported.get("optimized", {}), dict)
                else {}
            ),
            workflow_meta=workflow_meta,
            temp_model=temp_model,
        )
        return {
            "success": True,
            "message": "文章已导入为策略草稿",
            "draft": draft,
            "article": imported.get("article", {}),
            "candidate": imported.get("candidate", {}),
            "developerAgent": developer_agent,
            "tempModel": temp_model,
            "llm": imported.get("llm", {}),
            "callLog": workflow_meta.get("callLogs", [None])[0],
        }
    except Exception as exc:
        return {"success": False, "error": str(exc)}


@app.post("/api/strategy/custom/import-urls")
def import_strategy_custom_from_urls(req: StrategyDraftImportUrlsRequest):
    urls = [str(item or "").strip() for item in (req.urls or []) if str(item or "").strip()]
    if not urls:
        return {"success": False, "error": "请先提供至少一个文章链接"}
    temp_model = _normalize_temp_model(req.temp_model)
    try:
        from src.article_strategy_ingest import import_articles_as_strategy_pack

        imported = import_articles_as_strategy_pack(
            urls,
            symbol=str(req.symbol or "XAUUSD").strip() or "XAUUSD",
            preferred_name=str(req.name or "").strip(),
            force_heuristic=bool(req.force_heuristic),
            temp_model=temp_model,
        )
        developer_agent = _strategy_developer_binding()
        llm_summary = imported.get("llm", {}) if isinstance(imported.get("llm", {}), dict) else {}
        import_llm_meta = {
            "used": int(llm_summary.get("used_count", 0) or 0) > 0,
            "model": str(temp_model or ""),
            "latency_ms": 0.0,
            "degraded_reason": (
                "all_articles_fallback"
                if int(llm_summary.get("used_count", 0) or 0) == 0
                else ""
            ),
        }
        workflow_meta = _merge_strategy_workflow_meta(
            None,
            developer_agent=developer_agent,
            temp_model=temp_model,
            optimize_llm=llm_summary,
            call_logs=[
                _build_strategy_call_log_entry(
                    stage="import_urls",
                    developer_agent=developer_agent,
                    llm_meta=import_llm_meta,
                    temp_model=temp_model,
                    prompt_summary=f"多链接聚合 {len(urls)} 篇文章",
                    prompt_scope="article_bundle",
                )
            ],
        )
        draft = _evaluate_and_persist_strategy_custom_draft(
            name=str(imported.get("name", "")),
            document=str(imported.get("document", "")),
            optimized=(
                imported.get("optimized", {})
                if isinstance(imported.get("optimized", {}), dict)
                else {}
            ),
            workflow_meta=workflow_meta,
            temp_model=temp_model,
        )
        return {
            "success": True,
            "message": "多篇文章已聚合为宏观策略包",
            "draft": draft,
            "pack": imported.get("pack", {}),
            "imports": imported.get("imports", []),
            "developerAgent": developer_agent,
            "tempModel": temp_model,
            "llm": imported.get("llm", {}),
            "callLog": workflow_meta.get("callLogs", [None])[0],
        }
    except Exception as exc:
        return {"success": False, "error": str(exc)}


@app.post("/api/strategy/custom/save")
def save_strategy_custom_draft(req: StrategyDraftSaveRequest):
    from src.strategy_spec_service import build_strategy_spec, build_validation_report

    payload = _load_strategy_custom_state()
    now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    strategy_spec = build_strategy_spec(
        name=str(req.name or ""),
        document=str(req.document or ""),
        optimized=req.optimized if isinstance(req.optimized, dict) else {},
        compile_target=str(req.compile_target or ""),
    )
    validation_report = build_validation_report(
        evaluation=req.evaluation if isinstance(req.evaluation, dict) else {},
        optimized=req.optimized if isinstance(req.optimized, dict) else {},
    )
    payload["draft"] = {
        "name": str(req.name or ""),
        "document": str(req.document or ""),
        "compileTarget": str(strategy_spec.get("compile_target", "template") or "template"),
        "strategySpec": strategy_spec,
        "optimized": req.optimized if isinstance(req.optimized, dict) else {},
        "evaluation": req.evaluation if isinstance(req.evaluation, dict) else {},
        "validationReport": validation_report,
        "workflowMeta": req.workflow_meta if isinstance(req.workflow_meta, dict) else {},
        "saveState": "saved",
        "lastSavedAt": now_iso,
    }
    _save_strategy_custom_state(payload)
    return {"success": True, "saveState": "saved", "lastSavedAt": now_iso}


@app.post("/api/strategy/custom/optimize")
def optimize_strategy_custom_draft(req: StrategyDraftOptimizeRequest):
    from src.strategy_spec_service import build_strategy_spec

    document = str(req.document or "").strip()
    if len(document) < 12:
        return {"success": False, "error": "策略文档过短，请补充市场假设、入场、出场和风控约束"}
    developer_agent = _strategy_developer_binding()
    temp_model = _normalize_temp_model(req.temp_model)
    optimized = _heuristic_optimize_strategy_custom_draft(str(req.name or "").strip(), document)
    llm_optimized, llm_meta = _llm_optimize_strategy_custom_draft(
        str(req.name or "").strip(),
        document,
        temp_model=temp_model,
    )
    if llm_optimized:
        optimized.update(
            {
                "strategy_name": str(llm_optimized.get("strategy_name") or optimized.get("strategy_name") or "").strip(),
                "market_hypothesis": str(llm_optimized.get("market_hypothesis") or optimized.get("market_hypothesis") or ""),
                "entry_rule": str(llm_optimized.get("entry_rule") or optimized.get("entry_rule") or ""),
                "exit_rule": str(llm_optimized.get("exit_rule") or optimized.get("exit_rule") or ""),
                "base_engine": str(llm_optimized.get("base_engine") or optimized.get("base_engine") or "ema_macd"),
                "params": llm_optimized.get("params", optimized.get("params", {}))
                if isinstance(llm_optimized.get("params", {}), dict)
                else optimized.get("params", {}),
                "risk_constraints": (
                    llm_optimized.get("risk_constraints", optimized.get("risk_constraints", {}))
                    if isinstance(llm_optimized.get("risk_constraints", {}), dict)
                    else optimized.get("risk_constraints", {})
                ),
            }
        )
    strategy_spec = build_strategy_spec(
        name=str(req.name or "").strip(),
        document=document,
        optimized=optimized,
        compile_target=str(req.compile_target or ""),
    )
    return {
        "success": True,
        "optimized": optimized,
        "strategySpec": strategy_spec,
        "developerAgent": developer_agent,
        "tempModel": temp_model,
        "llm": llm_meta,
        "callLog": _build_strategy_call_log_entry(
            stage="optimize",
            developer_agent=developer_agent,
            llm_meta=llm_meta,
            temp_model=temp_model,
            prompt_summary=f"{str(req.name or '').strip()} {document}",
            prompt_scope="strategy_document",
        ),
    }


@app.post("/api/strategy/custom/evaluate")
def evaluate_strategy_custom_draft(req: StrategyDraftEvaluateRequest):
    from src.strategy_spec_service import build_strategy_spec, build_validation_report

    document = str(req.document or "").strip()
    optimized = req.optimized if isinstance(req.optimized, dict) else {}
    if not document:
        return {"success": False, "error": "请先填写策略文档"}
    developer_agent = _strategy_developer_binding()
    temp_model = _normalize_temp_model(req.temp_model)
    evaluation = _heuristic_evaluate_strategy_custom_draft(document, optimized)
    llm_evaluation, llm_meta = _llm_evaluate_strategy_custom_draft(
        str(req.name or "").strip(),
        document,
        optimized,
        temp_model=temp_model,
    )
    if llm_evaluation:
        evaluation.update(
            {
                "score": llm_evaluation.get("score", evaluation.get("score", {}))
                if isinstance(llm_evaluation.get("score", {}), dict)
                else evaluation.get("score", {}),
                "riskExposure": str(llm_evaluation.get("riskExposure") or evaluation.get("riskExposure") or "medium"),
                "verdict": str(llm_evaluation.get("verdict") or evaluation.get("verdict") or "needs_improve"),
                "advice": str(llm_evaluation.get("advice") or evaluation.get("advice") or ""),
            }
        )
    strategy_spec = build_strategy_spec(
        name=str(req.name or "").strip(),
        document=document,
        optimized=optimized,
        compile_target=str(req.compile_target or ""),
    )
    validation_report = build_validation_report(
        evaluation=evaluation,
        optimized=optimized,
    )
    return {
        "success": True,
        "evaluation": evaluation,
        "strategySpec": strategy_spec,
        "validationReport": validation_report,
        "developerAgent": developer_agent,
        "tempModel": temp_model,
        "llm": llm_meta,
        "callLog": _build_strategy_call_log_entry(
            stage="evaluate",
            developer_agent=developer_agent,
            llm_meta=llm_meta,
            temp_model=temp_model,
            prompt_summary=(
                f"{str(req.name or '').strip()} verdict={evaluation.get('verdict', '')} "
                f"doc={document} optimized={json.dumps(optimized, ensure_ascii=False)[:220]}"
            ),
            prompt_scope="strategy_document+optimized",
        ),
    }


@app.post("/api/strategy/custom/generate")
def generate_strategy_from_custom_draft(req: StrategyDraftGenerateRequest):
    from src.strategy_spec_service import (
        build_promotion_state,
        build_strategy_spec,
        build_validation_report,
    )

    name = str(req.name or "").strip()
    document = str(req.document or "").strip()
    if len(name) < 2:
        return {"success": False, "error": "策略名称至少 2 个字符"}
    if len(document) < 12:
        return {"success": False, "error": "策略文档过短，无法生成策略"}
    try:
        compile_target = _enforce_strategy_compile_target_access(
            str(req.compile_target or ""),
        )
    except PermissionError as exc:
        return {"success": False, "error": str(exc)}
    developer_agent = _strategy_developer_binding()
    temp_model = _normalize_temp_model(req.temp_model)
    optimized = req.optimized if isinstance(req.optimized, dict) else {}
    evaluation = req.evaluation if isinstance(req.evaluation, dict) else {}
    optimize_llm: dict[str, Any] = {}
    evaluate_llm: dict[str, Any] = {}
    if not optimized:
        optimize_result = optimize_strategy_custom_draft(
            StrategyDraftOptimizeRequest(name=name, document=document, temp_model=temp_model)
        )
        optimized = optimize_result.get("optimized", {}) if isinstance(optimize_result, dict) else {}
        optimize_llm = optimize_result.get("llm", {}) if isinstance(optimize_result, dict) else {}
    if not evaluation:
        evaluation_result = evaluate_strategy_custom_draft(
            StrategyDraftEvaluateRequest(name=name, document=document, optimized=optimized, temp_model=temp_model)
        )
        evaluation = evaluation_result.get("evaluation", {}) if isinstance(evaluation_result, dict) else {}
        evaluate_llm = evaluation_result.get("llm", {}) if isinstance(evaluation_result, dict) else {}
    verdict = str(evaluation.get("verdict", "")).strip().lower()
    if verdict != "pass":
        return {"success": False, "error": "评估未通过，请先补充风控约束并通过 AI 评估"}
    state = _load_strategy_market_state()
    existed_names = {
        str((row.get("display_name") if isinstance(row, dict) else "") or "").strip().lower()
        for row in (state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}).values()
    }
    if name.lower() in existed_names:
        return {"success": False, "error": "同名策略已存在，请更换策略名称"}
    slug_raw = "".join(ch.lower() if ch.isalnum() else "_" for ch in name).strip("_")
    strategy_id = f"custom_{(slug_raw[:28] if slug_raw else int(time.time()))}"
    if strategy_id in state.get("strategies", {}):
        strategy_id = f"{strategy_id}_{int(time.time())}"
    now_iso = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
    base_engine = _normalized_custom_base_engine(
        str(optimized.get("base_engine") or ""),
        document,
    )
    runtime_template = _build_strategy_template_from_custom_draft(
        strategy_id,
        optimized,
        base_engine=base_engine,
    )
    from src.strategy_asset_service import (
        compile_custom_strategy_artifact,
        normalize_compile_target,
        register_custom_strategy_asset,
        submit_review_entry,
    )

    compile_target = normalize_compile_target(compile_target, base_engine=base_engine)
    strategy_spec = build_strategy_spec(
        name=name,
        document=document,
        optimized=optimized,
        compile_target=compile_target,
        strategy_id=strategy_id,
    )
    validation_report = build_validation_report(
        evaluation=evaluation,
        optimized=optimized,
    )
    compiled = compile_custom_strategy_artifact(
        strategy_id=strategy_id,
        name=name,
        document=document,
        runtime_template=runtime_template,
        compile_target=compile_target,
    )
    if not bool(compiled.get("success", False)):
        return {
            "success": False,
            "error": str(compiled.get("error", "策略编译失败") or "策略编译失败"),
            "details": compiled.get("errors", []),
        }

    state = register_custom_strategy_asset(
        state,
        strategy_id=strategy_id,
        name=name,
        document=document,
        base_engine=base_engine,
        optimized=optimized,
        evaluation=evaluation,
        developer_agent=developer_agent,
        temp_model=temp_model,
        runtime_template=compiled.get("template", {}),
        artifact_type=str(compiled.get("artifact_type", compile_target) or compile_target),
        artifact_ref=compiled.get("artifact_ref", {}),
        strategy_spec=strategy_spec,
        validation_report=validation_report,
        optimize_llm=optimize_llm,
        evaluate_llm=evaluate_llm,
        now_iso=now_iso,
    )
    _save_strategy_market_state(state)
    review_result = submit_review_entry(
        strategy_id,
        source="custom_draft",
        sandbox_result=evaluation,
        code=str(
            (compiled.get("codegen", {}) or {}).get("code", "")
            if isinstance(compiled.get("codegen", {}), dict)
            else ""
        ),
    )

    custom_state = _load_strategy_custom_state()
    history = custom_state.get("generatedHistory", [])
    if not isinstance(history, list):
        history = []
    history.insert(
        0,
        {
            "strategy_id": strategy_id,
            "name": name,
            "created_at": now_iso,
            "base_engine": base_engine,
            "artifact_type": str(compiled.get("artifact_type", compile_target) or compile_target),
            "lifecycle_stage": "marketplace",
            "saved_to": "strategyMarketplace",
            "strategy_spec": strategy_spec,
            "validation_report": validation_report,
            "promotion_state": build_promotion_state(
                lifecycle_stage="marketplace",
                review_status=str(review_result.get("status", "pending") or "pending"),
            ),
            "developer_agent": developer_agent,
            "temp_model": temp_model,
            "review_status": str(review_result.get("status", "pending") or "pending"),
        },
    )
    call_logs = [
        _build_strategy_call_log_entry(
            stage="generate",
            developer_agent=developer_agent,
            llm_meta={"used": bool(optimize_llm.get("used")) or bool(evaluate_llm.get("used")), "model": str(temp_model or developer_agent.get("model", ""))},
            temp_model=temp_model,
            prompt_summary=(
                f"{name} base_engine={base_engine} verdict={evaluation.get('verdict', '')} "
                f"risk={json.dumps(evaluation.get('score', {}), ensure_ascii=False)}"
            ),
            prompt_scope="strategy_publish",
        )
    ]
    workflow_meta = _merge_strategy_workflow_meta(
        custom_state.get("draft", {}).get("workflowMeta", {})
        if isinstance(custom_state.get("draft", {}), dict)
        else {},
        developer_agent=developer_agent,
        temp_model=temp_model,
        optimize_llm=optimize_llm,
        evaluate_llm=evaluate_llm,
        generate_llm={
            "used": bool(optimize_llm.get("used")) or bool(evaluate_llm.get("used")),
            "model": str(temp_model or developer_agent.get("model", "")),
        },
        call_logs=call_logs,
    )
    custom_state["generatedHistory"] = history[:30]
    custom_state["draft"] = {
        "name": name,
        "document": document,
        "compileTarget": str(compiled.get("artifact_type", compile_target) or compile_target),
        "strategySpec": strategy_spec,
        "optimized": optimized,
        "evaluation": evaluation,
        "validationReport": validation_report,
        "workflowMeta": workflow_meta,
        "saveState": "saved",
        "lastSavedAt": now_iso,
    }
    _save_strategy_custom_state(custom_state)
    return {
        "success": True,
        "message": f"策略已生成并保存到策略集市: {name}",
        "strategy_id": strategy_id,
        "base_engine": base_engine,
        "artifactType": str(compiled.get("artifact_type", compile_target) or compile_target),
        "artifactRef": compiled.get("artifact_ref", {}),
        "strategySpec": strategy_spec,
        "validationReport": validation_report,
        "version": "1.0.0",
        "marketplaceStage": "marketplace",
        "savedTo": "strategyMarketplace",
        "generatedEntry": history[0],
        "developerAgent": developer_agent,
        "tempModel": temp_model,
        "llm": {
            "optimizer": optimize_llm,
            "evaluator": evaluate_llm,
        },
        "review": review_result,
        "callLog": call_logs[0],
    }


def _read_allowed_models() -> List[str]:
    try:
        from src.llm.provider_admin import read_allowed_models
        return read_allowed_models()
    except ImportError:
        return []

def get_providers():
    try:
        from src.llm.provider_admin import list_providers
        return list_providers()
    except ImportError:
        pass
    return {"success": True, "providers": []}

def save_provider(provider):
    try:
        from src.llm.provider_admin import save_provider as _sp
        return _sp(provider.name, provider.baseUrl, provider.api, provider.apiKey, provider.models)
    except ImportError:
        pass
    return {"success": False, "error": "provider_admin module not available"}

def delete_provider(name: str):
    try:
        from src.llm.provider_admin import delete_provider as _dp
        return _dp(name)
    except ImportError:
        pass
    return {"success": False, "error": "provider_admin module not available"}

def _build_training_status_payload():
    _kick_training_autopilot()
    best_file = _resolve_runtime_artifact("data", "offline", "best_params.json") or (DATA_DIR / "offline" / "best_params.json")
    status_file = _resolve_runtime_artifact("data", "offline", "training_status.json") or (
        DATA_DIR / "offline" / "training_status.json"
    )
    quality_snapshot_file = _resolve_runtime_artifact("data", "offline", "training_quality_snapshot.json") or (
        DATA_DIR / "offline" / "training_quality_snapshot.json"
    )
    effective_path = _resolve_runtime_artifact("logs", "effective_params.json") or (
        pathlib.Path(__file__).resolve().parent / "logs" / "effective_params.json"
    )
    data = _read_training_status_payload()
    data["pnlHistory"] = _compact_training_pnl_history(data.get("pnlHistory", []))

    if not data.get("isTraining", False) and best_file.exists():
        try:
            data["pnlHistory"] = _load_best_params_pnl_history(best_file)
        except Exception:
            pass

    quality_gate = _read_training_quality_snapshot(force_refresh=False)
    if quality_gate:
        data["qualityGate"] = quality_gate
    data["readyForTraining"] = bool(quality_gate.get("ready_for_training", False))
    data["blockedReasons"] = quality_gate.get("blocked_reasons", [])
    data["ledgerFallback"] = bool(quality_gate.get("ledger_fallback", False))
    data["closureCoveragePct"] = quality_gate.get("closure_coverage_pct", 0.0)
    data["criticalAnomalies"] = quality_gate.get("critical_anomalies", 0)
    data["recoveredClosedRatioPct"] = quality_gate.get("recovered_closed_ratio_pct", 0.0)
    data["usableTradeCount"] = int(quality_gate.get("usable_trade_count", data.get("usableTradeCount", 0)) or 0)
    data["excludedIncompleteCount"] = int(
        quality_gate.get("excluded_incomplete_count", data.get("excludedIncompleteCount", 0)) or 0
    )
    data["excludedRecoveredCount"] = int(
        quality_gate.get("excluded_recovered_count", data.get("excludedRecoveredCount", 0)) or 0
    )
    data["samplePolicy"] = quality_gate.get("sample_policy", data.get("samplePolicy", {}))
    data["qualitySnapshotId"] = str(quality_gate.get("snapshot_id", data.get("qualitySnapshotId", "")) or "")
    data["qualitySnapshotAt"] = str(quality_gate.get("generated_at", data.get("qualitySnapshotAt", "")) or "")
    recovery = quality_gate.get("recovery", {}) if isinstance(quality_gate.get("recovery"), dict) else {}
    data["naturalClosedRatioPct"] = recovery.get("natural_closed_ratio_pct", 0.0)
    data["naturalClosedTransitions"] = recovery.get("natural_closed_transitions", 0)
    data["recoveredClosedTransitions"] = recovery.get("recovered_closed_transitions", 0)
    data["repairCycle"] = quality_gate.get("repair_cycle", {})
    data["activeModel"] = _normalize_analysis_model(str(data.get("activeModel", "")))
    data["analysisModel"] = _normalize_analysis_model(str(data.get("analysisModel", data["activeModel"])))
    data["activeDataset"] = _normalize_training_dataset(str(data.get("activeDataset", "")))

    try:
        effective_payload = json.loads(effective_path.read_text(encoding="utf-8")) if effective_path.exists() else {}
        has_effective_params = bool(effective_payload.get("offline_params")) or bool(effective_payload.get("suggestions"))
        candidate_value = _read_training_candidate_value()
        review_state = build_training_review_state(effective_payload, candidate_value)
        data["approvalSummary"] = {
            **public_training_review_state(review_state),
            "hasEffectiveParams": has_effective_params or bool(review_state.get("hasEffectiveParams", False)),
        }
    except Exception:
        data["approvalSummary"] = data.get("approvalSummary", {})
    data["autoPilotEnabled"] = bool(data.get("autoPilotEnabled", False))
    data["autoPilotStage"] = str(data.get("autoPilotStage", "idle") or "idle")
    data["autoPilotMessage"] = str(data.get("autoPilotMessage", "自动推进未开启") or "自动推进未开启")
    data["autoPilotLastError"] = str(data.get("autoPilotLastError", "") or "")
    data["trainingEngine"] = {
        **TRAINING_RUNTIME_CONFIG,
        **(data.get("trainingEngine", {}) if isinstance(data.get("trainingEngine"), dict) else {}),
    }

    training_status_updated_ts = 0.0
    try:
        if status_file.exists():
            training_status_updated_ts = float(status_file.stat().st_mtime)
    except Exception:
        training_status_updated_ts = 0.0
    live_metrics = _read_live_training_metrics()
    live_updated_ts = float(live_metrics.get("updatedTs", 0.0) or 0.0)
    status_stale = (
        not bool(data.get("isTraining", False))
        and (
            training_status_updated_ts <= 0
            or (time.time() - training_status_updated_ts) > 900
            or (
                float(data.get("winRate", 0.0) or 0.0) == 0.0
                and int(data.get("totalTrades", 0) or 0) == 0
            )
        )
    )
    if live_updated_ts > 0 and (status_stale or live_updated_ts > training_status_updated_ts):
        if int(live_metrics.get("totalTrades", 0) or 0) > 0:
            data["totalTrades"] = int(live_metrics.get("totalTrades", 0) or 0)
        if float(live_metrics.get("winRate", 0.0) or 0.0) > 0:
            data["winRate"] = float(live_metrics.get("winRate", 0.0) or 0.0)
        if float(live_metrics.get("drawdown", 0.0) or 0.0) > 0:
            data["drawdown"] = float(live_metrics.get("drawdown", 0.0) or 0.0)
        data["shadowClosedTrades"] = int(live_metrics.get("shadowClosedTrades", 0) or 0)
        data["shadowWinRatePct"] = float(live_metrics.get("shadowWinRatePct", 0.0) or 0.0)
        data["shadowMaxDrawdownAbs"] = float(live_metrics.get("shadowMaxDrawdownAbs", 0.0) or 0.0)
        data["learnerLiveSelections"] = int(live_metrics.get("learnerLiveSelections", 0) or 0)
        data["baselineSelections"] = int(live_metrics.get("baselineSelections", 0) or 0)
        data["metricsSource"] = str(live_metrics.get("source", "auto_governance") or "auto_governance")
        data["metricsUpdatedAt"] = str(live_metrics.get("updatedAt", "") or "")
        data["metricsStale"] = False
    else:
        data["metricsSource"] = "training_status"
        data["metricsUpdatedAt"] = data.get("updatedAt", "")
        data["metricsStale"] = bool(status_stale)

    return _attach_projection_metadata(
        data,
        snapshot_name="training_status",
        source_paths={
            "training_status": status_file,
            "best_params": best_file,
            "training_quality_snapshot": quality_snapshot_file,
            "effective_params": effective_path,
        },
    )


@app.get("/api/training_status")
def get_training_status(force_refresh: bool = False):
    warming_payload = _training_status_defaults()
    warming_payload.update(
        {
            "success": True,
            "pnlHistory": [],
            "qualityGate": {},
            "repairCycle": {},
            "approvalSummary": {},
        }
    )
    return _get_cached_aux_payload(
        _TRAINING_STATUS_RESPONSE_CACHE,
        _TRAINING_STATUS_CACHE_TTL_SEC,
        _build_training_status_payload,
        "gui-training-status-refresh",
        warming_payload,
        force_refresh=force_refresh,
    )


@app.get("/api/training/datasets")
def get_training_datasets():
    presets = _get_training_dataset_presets()
    return {
        "success": True,
        "datasets": presets,
        "defaultDataset": _default_training_dataset_label(),
    }

@app.post("/api/training/start")
def start_training(req: Optional[TrainingStartRequest] = None):
    _kick_training_autopilot()
    if TRAINING_PID_FILE.exists():
        try:
            pid = int(TRAINING_PID_FILE.read_text(encoding="utf-8").strip())
            os.kill(pid, 0)
            return {"success": False, "message": "Training already running"}
        except OSError:
            pass # Process not running

    try:
        from src.offline_train import evaluate_training_readiness, write_training_blocked_status

        gate = evaluate_training_readiness()
        if not gate.get("ready_for_training", False):
            status_file = write_training_blocked_status(DATA_DIR / "offline", gate)
            return {
                "success": False,
                "message": "Training blocked by quality gate",
                "blockedReasons": gate.get("blocked_reasons", []),
                "qualityGate": gate,
                "statusFile": str(status_file),
            }
    except Exception as e:
        return {"success": False, "message": f"Training gate check failed: {e}"}

    previous_status = _read_training_status_payload()
    selected_model = _normalize_analysis_model(req.model if req and req.model else previous_status.get("analysisModel", DEFAULT_ANALYSIS_MODEL))
    dataset_config = _resolve_training_dataset(
        req.dataset if req and req.dataset else previous_status.get("activeDataset", _default_training_dataset_label())
    )
    selected_dataset = str(dataset_config.get("label", _default_training_dataset_label()))
    selected_symbol = str(dataset_config.get("symbol", TRAINING_RUNTIME_CONFIG["symbol"]))
    selected_days = int(dataset_config.get("lookbackDays", TRAINING_RUNTIME_CONFIG["lookbackDays"]))
    selected_interval = str(dataset_config.get("interval", TRAINING_RUNTIME_CONFIG["interval"]))
    if getattr(sys, 'frozen', False):
        cmd = [
            sys.executable,
            "--auto",
            "--train",
            "--days",
            str(selected_days),
            "--interval",
            selected_interval,
            "--symbol",
            selected_symbol,
            "--mode",
            "train",
        ]
    else:
        cmd = [
            "python3",
            str(_SRC_DIR / "offline_train.py"),
            "--days",
            str(selected_days),
            "--interval",
            selected_interval,
            "--symbol",
            selected_symbol,
            "--mode",
            "train",
        ]
    proc = _popen_safe(cmd)
    TRAINING_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
    TRAINING_PID_FILE.write_text(str(proc.pid))

    _write_training_status_payload({
        **previous_status,
        "isTraining": True,
        "progress": 0,
        "currentBar": 0,
        "totalBars": 0,
        "currentEpoch": 0,
        "totalEpochs": 81,
        "winRate": 0.0,
        "pnl": 0.0,
        "totalTrades": 0,
        "activeDataset": selected_dataset,
        "activeModel": selected_model,
        "analysisModel": selected_model,
        "qualityGate": gate,
        "readyForTraining": gate.get("ready_for_training", False),
        "blockedReasons": gate.get("blocked_reasons", []),
        "qualitySnapshotId": gate.get("snapshot_id", ""),
        "qualitySnapshotAt": gate.get("generated_at", ""),
        "trainingEngine": {
            **TRAINING_RUNTIME_CONFIG,
            "symbol": selected_symbol,
            "lookbackDays": selected_days,
            "interval": selected_interval,
            "datasetId": dataset_config.get("id", "custom"),
        },
        "autoPilotEnabled": True,
        "autoPilotStage": "training_running",
        "autoPilotMessage": "自动推进已开启：训练完成后会自动生成建议、自动修复阻断项，并自动推进到审批闸门前",
        "autoPilotLastActionAt": datetime.datetime.now().isoformat(),
        "autoPilotLastError": "",
        "updatedAt": datetime.datetime.now().isoformat(),
    })

    return {
        "success": True,
        "message": "Training started",
        "pid": proc.pid,
        "analysisModel": selected_model,
        "activeDataset": selected_dataset,
    }

@app.post("/api/training/stop")
def stop_training():
    if TRAINING_PID_FILE.exists():
        try:
            pid = int(TRAINING_PID_FILE.read_text(encoding="utf-8").strip())
            os.kill(pid, signal.SIGTERM)
            TRAINING_PID_FILE.unlink()

            status_data = _read_training_status_payload()
            status_data["isTraining"] = False
            status_data["autoPilotEnabled"] = False
            status_data["autoPilotStage"] = "stopped"
            status_data["autoPilotMessage"] = "训练已手动停止，自动推进同步停止"
            status_data["autoPilotLastActionAt"] = datetime.datetime.now().isoformat()
            status_data["updatedAt"] = datetime.datetime.now().isoformat()
            _write_training_status_payload(status_data)

            return {"success": True, "message": "Training stopped"}
        except Exception as e:
            return {"success": False, "error": str(e)}
    return {"success": True, "message": "Training not running"}

@app.post("/api/training/analyze")
def analyze_training(req: Optional[TrainingAnalyzeRequest] = None):
    """调用大模型分析最新的训练结果并给出报告"""
    try:
        selected_model = _normalize_analysis_model(req.model if req and req.model else DEFAULT_ANALYSIS_MODEL)
        if getattr(sys, 'frozen', False):
            cmd = [sys.executable, "--auto", "--analyze"]
        else:
            cmd = ["python3", str(_SRC_DIR / "strategy_evolution.py"), "--analyze-only"]
        cmd.extend(["--model", selected_model])

        _analyze_kwargs: dict = {"capture_output": True, "text": True}
        proc = _run_safe(cmd, **_analyze_kwargs)
        if proc.returncode == 0:
            content = proc.stdout
            if "LLM 分析报告:" in content:
                content = content.split("LLM 分析报告:")[1].strip()

            return {
                "success": True,
                "model": selected_model,
                "report": content,
                "timestamp": __import__('datetime').datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            }
        else:
            return {
                "success": False,
                "model": selected_model,
                "error": proc.stderr.strip() or "暂无足够的数据供模型分析,请先运行回测训练。",
                "report": proc.stdout.strip(),
            }
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.post("/api/training/evolve")
def evolve_training_candidate(req: Optional[TrainingEvolveRequest] = None):
    try:
        selected_model = _normalize_analysis_model(req.model if req and req.model else DEFAULT_ANALYSIS_MODEL)
        if getattr(sys, 'frozen', False):
            cmd = [sys.executable, "--auto", "--model", selected_model]
        else:
            cmd = ["python3", str(_SRC_DIR / "strategy_evolution.py"), "--model", selected_model]
        _evolve_kwargs: dict = {"capture_output": True, "text": True}
        proc = _run_safe(cmd, **_evolve_kwargs)
        if proc.returncode != 0:
            return {
                "success": False,
                "model": selected_model,
                "error": proc.stderr.strip() or "策略进化执行失败",
                "report": proc.stdout.strip(),
            }
        approval = get_training_approval_chain()
        governance = get_training_governance()
        return {
            "success": True,
            "model": selected_model,
            "message": "训练进化建议已生成",
            "report": proc.stdout.strip(),
            "approvalChainStage": approval.get("stage"),
            "governanceSummary": governance.get("summary", {}),
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


def _build_training_governance_payload():
    report_file = _resolve_existing_file(_runtime_log_candidates("evolution_report_latest.json"))
    llm_report_file = _resolve_existing_file(_runtime_log_candidates("evolution_llm_report_latest.txt"))
    learner_report_file = _resolve_runtime_artifact("data", "offline", "param_learner_report_latest.json") or (
        DATA_DIR / "offline" / "param_learner_report_latest.json"
    )
    data = {
        "success": True,
        "qualityGate": {},
        "governanceContext": {},
        "governanceAdvice": "",
        "paramLearner": {},
    }

    current_quality_gate = _read_training_quality_snapshot(force_refresh=False)
    try:
        from src.hermes_bridge import load_system_design_contract
        data["systemDesignContract"] = load_system_design_contract()
    except Exception:
        data["systemDesignContract"] = {}

    if report_file is not None:
        try:
            report = json.loads(report_file.read_text(encoding="utf-8"))
            data["qualityGate"] = current_quality_gate or report.get("quality_context", report.get("quality_gate", {}))
            data["governanceContext"] = report.get("governance_context", {})
        except Exception:
            pass
    elif current_quality_gate:
        data["qualityGate"] = current_quality_gate

    if llm_report_file is not None:
        try:
            data["governanceAdvice"] = llm_report_file.read_text(encoding="utf-8")
        except Exception:
            pass

    if learner_report_file.exists():
        try:
            data["paramLearner"] = json.loads(learner_report_file.read_text(encoding="utf-8"))
        except Exception:
            pass

    recovery = data["qualityGate"].get("recovery", {}) if isinstance(data["qualityGate"], dict) else {}
    data["metrics"] = {
        "readyForTraining": data["qualityGate"].get("ready_for_training", False) if isinstance(data["qualityGate"], dict) else False,
        "blockedReasons": data["qualityGate"].get("blocked_reasons", []) if isinstance(data["qualityGate"], dict) else [],
        "closureCoveragePct": data["qualityGate"].get("closure_coverage_pct", 0.0) if isinstance(data["qualityGate"], dict) else 0.0,
        "criticalAnomalies": data["qualityGate"].get("critical_anomalies", 0) if isinstance(data["qualityGate"], dict) else 0,
        "recoveredClosedRatioPct": data["qualityGate"].get("recovered_closed_ratio_pct", 0.0) if isinstance(data["qualityGate"], dict) else 0.0,
        "naturalClosedRatioPct": recovery.get("natural_closed_ratio_pct", 0.0),
        "paramLearnerReady": bool(isinstance(data["paramLearner"], dict) and data["paramLearner"].get("status") == "READY"),
    }

    return data


@app.get("/api/training/governance")
def get_training_governance(force_refresh: bool = False):
    warming_payload = {
        "success": True,
        "qualityGate": {},
        "governanceContext": {},
        "governanceAdvice": "",
        "paramLearner": {},
        "metrics": {},
    }
    return _get_cached_aux_payload(
        _TRAINING_GOVERNANCE_CACHE,
        _TRAINING_GOVERNANCE_CACHE_TTL_SEC,
        _build_training_governance_payload,
        "gui-training-governance-refresh",
        warming_payload,
        force_refresh=force_refresh,
    )


def _build_training_approval_chain_payload():
    _kick_training_autopilot()
    from src.evolution_coordinator import build_evolution_coordinator_payload

    data = build_evolution_coordinator_payload(
        project_root=APP_DIR,
        tenant_id="default",
        source="gui_backend:approval_chain",
    )
    source_paths = data.pop("_source_paths", {})
    try:
        from src.hermes_bridge import (
            load_governance_brief,
            load_runtime_rules,
            load_system_design_contract,
            schedule_governance_brief_refresh,
        )
        data["hermesBrief"] = load_governance_brief("approval_chain")
        data["hermesRuntimeRules"] = load_runtime_rules()
        data["systemDesignContract"] = load_system_design_contract()
        schedule_governance_brief_refresh("approval_chain", data)
    except Exception:
        data["hermesBrief"] = {}
        data["hermesRuntimeRules"] = {}
        data["systemDesignContract"] = {}

    return _attach_projection_metadata(
        data,
        snapshot_name="training_approval_chain",
        source_paths=source_paths,
    )


@app.get("/api/training/approval_chain")
def get_training_approval_chain(force_refresh: bool = False):
    warming_payload = {
        "success": True,
        "stage": "NO_SUGGESTION",
        "suggestion": {},
        "appliedSuggestion": {},
        "effectiveParams": {},
        "evolutionReport": {},
        "paramLearner": {},
        "shadowLearner": {},
        "liveLearner": {},
        "autoGovernance": {},
        "phaseReadiness": {},
        "rollbackState": {},
        "decision": {},
        "approvalSummary": {},
        "effectiveOfflineParams": {},
    }
    return _get_cached_aux_payload(
        _TRAINING_APPROVAL_CHAIN_CACHE,
        _TRAINING_APPROVAL_CHAIN_CACHE_TTL_SEC,
        _build_training_approval_chain_payload,
        "gui-training-approval-refresh",
        warming_payload,
        force_refresh=force_refresh,
    )


@app.get("/api/evolution/coordinator")
def get_evolution_coordinator(force_refresh: bool = False):
    from src.evolution_coordinator import build_evolution_coordinator_payload

    def _build():
        data = build_evolution_coordinator_payload(
            project_root=APP_DIR,
            tenant_id="default",
            source="gui_backend:evolution_coordinator",
        )
        source_paths = data.pop("_source_paths", {})
        return _attach_projection_metadata(
            data,
            snapshot_name="evolution_coordinator",
            source_paths=source_paths,
        )

    warming_payload = {
        "success": True,
        "stage": "NO_SUGGESTION",
        "decision": {},
        "coordinator": {},
    }
    return _get_cached_aux_payload(
        _TRAINING_APPROVAL_CACHE,
        _TRAINING_APPROVAL_CACHE_TTL_SEC,
        _build,
        "gui-evolution-coordinator-refresh",
        warming_payload,
        force_refresh=force_refresh,
    )


def _build_governance_lifecycle_payload():
    execution_state_file = _resolve_runtime_artifact("data", "execution_state.json") or (DATA_DIR / "execution_state.json")
    audit_file = _resolve_runtime_artifact("data", "learning", "audit", "transition_health.latest.json") or (
        APP_DIR / "learning" / "audit" / "transition_health.latest.json"
    )
    repair_file = _resolve_runtime_artifact("data", "learning", "audit", "repair_cycle.latest.json") or (
        APP_DIR / "learning" / "audit" / "repair_cycle.latest.json"
    )
    anomaly_file = _resolve_runtime_artifact("data", "learning", "audit", "anomaly_ledger.latest.jsonl") or (
        APP_DIR / "learning" / "audit" / "anomaly_ledger.latest.jsonl"
    )
    closures_file = _resolve_runtime_artifact("data", "learning", "events", "closures.jsonl") or (
        APP_DIR / "learning" / "events" / "closures.jsonl"
    )
    training_status_file = _resolve_runtime_artifact("data", "offline", "training_status.json") or (
        DATA_DIR / "offline" / "training_status.json"
    )
    shadow_summary_file = _resolve_runtime_artifact("logs", "shadow_learner_summary_latest.json") or (
        LOGS_DIR / "shadow_learner_summary_latest.json"
    )
    live_summary_file = _resolve_runtime_artifact("logs", "live_learner_summary_latest.json") or (
        LOGS_DIR / "live_learner_summary_latest.json"
    )
    data = {
        "success": True,
        "activeSignals": [],
        "recentSignals": [],
        "positionTier": {},
        "positionTierReview": {"rows": [], "count": 0},
        "audit": {},
        "repairCycle": {},
        "closureLifecycle": {
            "recentClosures": [],
            "normalClosedCount": 0,
            "recoveredClosedCount": 0,
            "pendingRepairCount": 0,
            "pendingRepairDecisions": [],
        },
        "trainingLifecycle": {
            "readyForTraining": False,
            "blockedReasons": [],
            "closureCoveragePct": 0.0,
            "criticalAnomalies": 0,
            "recoveredClosedRatioPct": 0.0,
            "repairRecovered": 0,
            "transitionCountBeforeRepair": 0,
            "transitionCountAfterRepair": 0,
        },
        "shadowLearner": {
            "status": "EMPTY",
            "total": 0,
            "actionMatchRate": 0.0,
            "directionMatchRate": 0.0,
            "baselineApprovedRate": 0.0,
            "learnerApprovedRate": 0.0,
            "divergenceCount": 0,
            "recentDecisions": [],
        },
        "liveLearner": {
            "status": "EMPTY",
            "totalSelections": 0,
            "learnerLiveSelections": 0,
            "baselineSelections": 0,
            "blockedSelections": 0,
            "learnerLiveRate": 0.0,
            "currentMode": "shadow",
            "lastDecisionId": "",
            "lastSelectedSource": "baseline",
            "lastReason": "",
            "lastRollbackTriggers": [],
            "recentSelections": [],
        },
        "rollbackEvidence": {
            "triggered": [],
            "killSwitch": False,
        },
        "autoGovernance": {
            "status": "NOOP",
            "enabled": True,
            "currentMode": "shadow",
            "nextMode": "",
            "lastReason": "",
            "lastAction": "NOOP",
            "recentActions": [],
        },
        "phaseReadiness": {
            "status": "BLOCKED",
            "nextClosure": "",
            "overallReady": False,
            "phases": {},
        },
    }
    audit_mtime_ts = _file_mtime_ts(audit_file)
    repair_mtime_ts = _file_mtime_ts(repair_file)
    latest_governance_audit_ts = max(audit_mtime_ts, repair_mtime_ts)
    if latest_governance_audit_ts <= 0 or (time.time() - latest_governance_audit_ts) > 900:
        try:
            from src.learning.transition_audit import run_repair_cycle

            run_repair_cycle(auto_recover=False)
            audit_file = _resolve_runtime_artifact("data", "learning", "audit", "transition_health.latest.json") or audit_file
            repair_file = _resolve_runtime_artifact("data", "learning", "audit", "repair_cycle.latest.json") or repair_file
            anomaly_file = _resolve_runtime_artifact("data", "learning", "audit", "anomaly_ledger.latest.jsonl") or anomaly_file
        except Exception:
            pass
    try:
        from src.auto_governance import load_auto_governance_snapshot
        from src.phase_readiness import build_phase_readiness
    except Exception as exc:
        reason = f"DEPENDENCY_UNAVAILABLE: {exc}"
        data["autoGovernance"]["status"] = "UNAVAILABLE"
        data["autoGovernance"]["lastReason"] = reason
        data["phaseReadiness"]["status"] = "UNAVAILABLE"
        data["phaseReadiness"]["phases"] = {"training": {"ready": False, "reason": reason}}
        return data

    if execution_state_file.exists():
        try:
            state = json.loads(execution_state_file.read_text(encoding="utf-8"))
            signals = list((state.get("signals", {}) or {}).values())

            def latest_ts(entry: dict) -> str:
                history = entry.get("history", []) if isinstance(entry, dict) else []
                if not history:
                    return ""
                return str(history[-1].get("ts", "") or "")

            normalized = []
            for entry in signals:
                history = entry.get("history", []) if isinstance(entry, dict) else []
                normalized.append(
                    {
                        "signalId": entry.get("signal_id"),
                        "traceId": entry.get("trace_id", entry.get("signal_id")),
                        "state": entry.get("state", "CREATED"),
                        "latestTs": latest_ts(entry),
                        "reason": history[-1].get("reason") if history else "",
                        "history": history[-6:],
                        "metadata": entry.get("metadata", {}),
                    }
                )
            normalized.sort(key=lambda item: item.get("latestTs", ""), reverse=True)
            data["recentSignals"] = normalized[:8]
            data["activeSignals"] = [
                item
                for item in normalized
                if str(item.get("state", "")).upper() not in {"FILLED", "REJECTED", "VETOED", "TIMEOUT", "RECONCILED"}
            ][:6]
        except Exception:
            pass

    runtime_state, runtime_open_guard, runtime_guard_present = _runtime_derived_state(timeout_sec=0.5)
    effective_runtime_state = runtime_open_guard if runtime_guard_present else runtime_state
    data["positionTier"] = _build_position_tier_snapshot(effective_runtime_state)
    data["positionTierReview"] = _build_position_tier_review(limit=8)

    if audit_file.exists():
        try:
            data["audit"] = json.loads(audit_file.read_text(encoding="utf-8"))
        except Exception:
            pass

    if repair_file.exists():
        try:
            data["repairCycle"] = json.loads(repair_file.read_text(encoding="utf-8"))
        except Exception:
            pass

    if anomaly_file.exists():
        try:
            pending_repairs = []
            for raw in anomaly_file.read_text(encoding="utf-8").splitlines():
                if not raw.strip():
                    continue
                row = json.loads(raw)
                if row.get("anomaly_type") != "missing_closure":
                    continue
                pending_repairs.append(
                    {
                        "decisionId": row.get("decision_id"),
                        "ticket": row.get("ticket"),
                        "severity": row.get("severity", "unknown"),
                    }
                )
            data["closureLifecycle"]["pendingRepairCount"] = len(pending_repairs)
            data["closureLifecycle"]["pendingRepairDecisions"] = pending_repairs[:6]
        except Exception:
            pass

    if closures_file.exists():
        try:
            closure_rows = []
            recovered_count = 0
            normal_count = 0
            for raw in closures_file.read_text(encoding="utf-8").splitlines():
                if not raw.strip():
                    continue
                row = json.loads(raw)
                payload = row.get("payload", {}) if isinstance(row, dict) else {}
                recovered = bool(payload.get("recovered", False))
                if recovered:
                    recovered_count += 1
                else:
                    normal_count += 1
                closure_rows.append(
                    {
                        "decisionId": row.get("decision_id"),
                        "eventTime": row.get("event_time"),
                        "ticket": payload.get("ticket", 0),
                        "realizedPnl": payload.get("realized_pnl", 0.0),
                        "doneReason": payload.get("done_reason", "closed"),
                        "recovered": recovered,
                        "recoverySource": payload.get("recovery_source", ""),
                    }
                )
            closure_rows.sort(key=lambda item: str(item.get("eventTime", "")), reverse=True)
            data["closureLifecycle"]["recentClosures"] = closure_rows[:8]
            data["closureLifecycle"]["normalClosedCount"] = normal_count
            data["closureLifecycle"]["recoveredClosedCount"] = recovered_count
        except Exception:
            pass

    quality_gate = _read_training_quality_snapshot(force_refresh=False)
    if not quality_gate and training_status_file.exists():
        try:
            training_status = json.loads(training_status_file.read_text(encoding="utf-8"))
            quality_gate = training_status.get("qualityGate", {}) if isinstance(training_status, dict) else {}
        except Exception:
            quality_gate = {}
    if quality_gate:
        repair_cycle = quality_gate.get("repair_cycle", {}) if isinstance(quality_gate, dict) else {}
        recovery = repair_cycle.get("recovery", {}) if isinstance(repair_cycle, dict) else {}
        transition_counts = repair_cycle.get("transition_counts", {}) if isinstance(repair_cycle, dict) else {}
        data["trainingLifecycle"] = {
            "readyForTraining": bool(quality_gate.get("ready_for_training", False)),
            "blockedReasons": quality_gate.get("blocked_reasons", []),
            "closureCoveragePct": quality_gate.get("closure_coverage_pct", 0.0),
            "criticalAnomalies": quality_gate.get("critical_anomalies", 0),
            "recoveredClosedRatioPct": quality_gate.get("recovered_closed_ratio_pct", 0.0),
            "repairRecovered": recovery.get("recovered", 0),
            "transitionCountBeforeRepair": transition_counts.get("before", 0),
            "transitionCountAfterRepair": transition_counts.get("after", 0),
            "qualitySnapshotId": str(quality_gate.get("snapshot_id", "") or ""),
        }

    if shadow_summary_file.exists():
        try:
            data["shadowLearner"] = json.loads(shadow_summary_file.read_text(encoding="utf-8"))
        except Exception:
            pass

    if live_summary_file.exists():
        try:
            data["liveLearner"] = json.loads(live_summary_file.read_text(encoding="utf-8"))
        except Exception:
            pass
    try:
        data["autoGovernance"] = load_auto_governance_snapshot(APP_DIR)
    except Exception:
        pass
    try:
        data["phaseReadiness"] = build_phase_readiness(APP_DIR)
    except Exception:
        pass

    try:
        from src.metrics_agg import aggregate_runtime_state, evaluate_rollback_gates
        runtime = aggregate_runtime_state(APP_DIR, "default")
        data["rollbackEvidence"] = {
            "triggered": evaluate_rollback_gates(runtime),
            "killSwitch": bool(runtime.get("kill_switch", False)),
        }
    except Exception:
        pass

    try:
        from src.hermes_bridge import load_governance_brief, load_runtime_rules, load_system_design_contract, schedule_governance_brief_refresh
        data["hermesBrief"] = load_governance_brief("governance_lifecycle")
        data["hermesRuntimeRules"] = load_runtime_rules()
        data["systemDesignContract"] = load_system_design_contract()
        schedule_governance_brief_refresh("governance_lifecycle", data)
    except Exception:
        data["hermesBrief"] = {}
        data["hermesRuntimeRules"] = {}
        data["systemDesignContract"] = {}

    return data


@app.get("/api/governance/lifecycle")
def get_governance_lifecycle(force_refresh: bool = False):
    warming_payload = {
        "success": True,
        "activeSignals": [],
        "recentSignals": [],
        "positionTier": {},
        "positionTierReview": {"rows": [], "count": 0},
        "audit": {},
        "repairCycle": {},
        "closureLifecycle": {},
        "trainingLifecycle": {},
        "shadowLearner": {},
        "liveLearner": {},
        "rollbackEvidence": {},
        "autoGovernance": {},
        "phaseReadiness": {},
    }
    return _get_cached_aux_payload(
        _GOVERNANCE_LIFECYCLE_CACHE,
        _GOVERNANCE_LIFECYCLE_CACHE_TTL_SEC,
        _build_governance_lifecycle_payload,
        "gui-governance-lifecycle-refresh",
        warming_payload,
        force_refresh=force_refresh,
    )


def _build_direction_quality(runtime_state: Dict[str, Any]) -> dict:
    rows = runtime_state.get("recent_closures", []) if isinstance(runtime_state, dict) else []
    if not isinstance(rows, list):
        rows = []
    rows = [r for r in rows if isinstance(r, dict)][-40:]
    total = len(rows)
    aligned_rows = [r for r in rows if bool(r.get("agent_aligned", False))]
    counter_rows = [r for r in rows if not bool(r.get("agent_aligned", False))]
    aligned_hits = sum(1 for r in aligned_rows if float(r.get("pnl", 0.0) or 0.0) > 0)
    counter_hits = sum(1 for r in counter_rows if float(r.get("pnl", 0.0) or 0.0) > 0)
    aligned_avg = (
        sum(float(r.get("pnl", 0.0) or 0.0) for r in aligned_rows) / len(aligned_rows)
        if aligned_rows
        else 0.0
    )
    counter_avg = (
        sum(float(r.get("pnl", 0.0) or 0.0) for r in counter_rows) / len(counter_rows)
        if counter_rows
        else 0.0
    )
    range_modes = {"RANGING", "RANGE", "SIDEWAYS", "NEUTRAL", "UNCERTAIN"}
    counter_in_range = sum(
        1
        for r in counter_rows
        if str(r.get("session_mode", "UNKNOWN") or "UNKNOWN").upper() in range_modes
    )
    return {
        "sampleCount": total,
        "alignedCount": len(aligned_rows),
        "counterCount": len(counter_rows),
        "alignedHitRatePct": round(100.0 * aligned_hits / max(1, len(aligned_rows)), 2),
        "counterHitRatePct": round(100.0 * counter_hits / max(1, len(counter_rows)), 2),
        "directionEdgePnl": round(aligned_avg - counter_avg, 4),
        "counterInRangePct": round(100.0 * counter_in_range / max(1, len(counter_rows)), 2),
        "consecutiveLosses": int(runtime_state.get("consecutive_losses", 0) or 0),
    }


def _build_macro_poll_status(runtime_state: Dict[str, Any]) -> dict:
    hold_stats = runtime_state.get("hold_stats", {}) if isinstance(runtime_state, dict) else {}
    if not isinstance(hold_stats, dict):
        hold_stats = {}
    now_ts = time.time()
    first_hold_ts = float(hold_stats.get("first_hold_ts", 0.0) or 0.0)
    last_poll_ts = float(hold_stats.get("last_macro_poll_ts", 0.0) or 0.0)
    return {
        "holdStreak": int(hold_stats.get("streak", 0) or 0),
        "holdDurationSec": int(max(0.0, (now_ts - first_hold_ts)) if first_hold_ts > 0 else 0),
        "lastMacroPollAgoSec": int(max(0.0, now_ts - last_poll_ts)) if last_poll_ts > 0 else -1,
        "lastReason": str(hold_stats.get("last_reason", "") or ""),
        "lastRegime": str(hold_stats.get("last_regime", "UNKNOWN") or "UNKNOWN"),
    }


def _build_runtime_observability_payload():
    try:
        from src.auto_governance import load_auto_governance_snapshot
        from src.live_performance_factors import load_live_performance_factors
    except Exception as exc:
        return {"success": False, "error": f"runtime import failed: {exc}"}

    status = get_status()
    raw_training_status = _read_training_status_payload()
    quality_gate = _read_training_quality_snapshot(force_refresh=False)
    if not quality_gate:
        quality_gate = (
            raw_training_status.get("qualityGate", {})
            if isinstance(raw_training_status.get("qualityGate", {}), dict)
            else {}
        )
    recovery = quality_gate.get("recovery", {}) if isinstance(quality_gate.get("recovery", {}), dict) else {}
    training_status = {
        "readyForTraining": bool(quality_gate.get("ready_for_training", False)),
        "blockedReasons": quality_gate.get("blocked_reasons", []),
        "criticalAnomalies": int(quality_gate.get("critical_anomalies", 0) or 0),
        "closureCoveragePct": float(quality_gate.get("closure_coverage_pct", 0.0) or 0.0),
        "naturalClosedRatioPct": float(recovery.get("natural_closed_ratio_pct", 0.0) or 0.0),
        "recoveredClosedRatioPct": float(quality_gate.get("recovered_closed_ratio_pct", 0.0) or 0.0),
    }
    approval_chain = {"decision": {}}
    governance_lifecycle = {"trainingLifecycle": {}, "closureLifecycle": {}}
    hermes_runtime_rules = {}
    try:
        from src.hermes_bridge import load_runtime_rules, load_system_design_contract
        hermes_runtime_rules = load_runtime_rules()
        system_design_contract = load_system_design_contract()
    except Exception:
        hermes_runtime_rules = {}
        system_design_contract = {}

    decision_contract = load_agent_decision_contract()
    direction_snapshot = load_decision_snapshot()
    decision_snapshot_macro = (
        direction_snapshot.get("macro", {})
        if isinstance(direction_snapshot.get("macro", {}), dict)
        else {}
    )
    decision_snapshot_policy = (
        direction_snapshot.get("decision_policy", {})
        if isinstance(direction_snapshot.get("decision_policy", {}), dict)
        else {}
    )
    decision_snapshot_execution = (
        direction_snapshot.get("execution", {})
        if isinstance(direction_snapshot.get("execution", {}), dict)
        else {}
    )
    interpreter_path = DATA_DIR / "agent_signal_override.json"
    interpreter_payload = (
        decision_contract.get("interpreter", {})
        if isinstance(decision_contract.get("interpreter", {}), dict)
        and decision_contract.get("interpreter", {})
        else _read_json_file(interpreter_path)
    )
    interpreter_ts = _parse_timestamp_value(interpreter_payload.get("timestamp")) or (
        interpreter_path.stat().st_mtime if interpreter_path.exists() else None
    )
    interpreter_valid_until = _parse_timestamp_value(interpreter_payload.get("valid_until"))
    interpreter_age = _age_seconds_from_ts(interpreter_ts)
    interpreter_status = "MISSING"
    if interpreter_payload:
        if interpreter_valid_until and time.time() <= interpreter_valid_until:
            interpreter_status = "ACTIVE"
        elif interpreter_age is not None and interpreter_age <= 900:
            interpreter_status = "ACTIVE"
        else:
            interpreter_status = "STALE"

    try:
        performance_artifact = load_live_performance_factors()
    except Exception:
        performance_artifact = {}
    performance_quality = (
        performance_artifact.get("quality", {})
        if isinstance(performance_artifact.get("quality"), dict)
        else {}
    )
    performance_ts = _parse_timestamp_value(performance_artifact.get("updated_at"))
    auto_governance = load_auto_governance_snapshot(APP_DIR)
    auto_governance_ts = _parse_timestamp_value(auto_governance.get("timestamp")) or _parse_timestamp_value(
        auto_governance.get("last_action_ts")
    )
    training_lifecycle = {}
    try:
        from src.market_adaptive_profile import build_market_adaptive_profile
        adaptive_profile = build_market_adaptive_profile(interpreter_payload)
    except Exception:
        adaptive_profile = {
            "market_structure": str(interpreter_payload.get("market_structure", "UNCERTAIN") or "UNCERTAIN"),
            "regime_subtype": str(interpreter_payload.get("regime_subtype", "STRUCTURE_UNCLEAR") or "STRUCTURE_UNCLEAR"),
            "volatility_state": str(interpreter_payload.get("volatility_state", "NORMAL") or "NORMAL"),
            "liquidity_state": str(interpreter_payload.get("liquidity_state", "NORMAL") or "NORMAL"),
            "continuation_bias": str(interpreter_payload.get("continuation_bias", "NEUTRAL") or "NEUTRAL"),
            "entry_timing_hint": str(interpreter_payload.get("entry_timing_hint", "WAIT_CLEAR_SIGNAL") or "WAIT_CLEAR_SIGNAL"),
            "llm_confidence": float(interpreter_payload.get("llm_confidence", 0.55) or 0.55),
        }
    approval_decision = (
        approval_chain.get("decision", {})
        if isinstance(approval_chain.get("decision"), dict)
        else {}
    )
    goal_targets = _load_goal_targets_cached()
    runtime_state, runtime_open_guard, runtime_guard_present = _runtime_derived_state(timeout_sec=0.5)
    effective_runtime_state = runtime_open_guard if runtime_guard_present else runtime_state
    try:
        proc = psutil.Process(os.getpid())
        process_rss_mb = round(proc.memory_info().rss / 1024 / 1024, 1)
    except Exception:
        process_rss_mb = 0.0
    try:
        vm = psutil.virtual_memory()
        system_memory = {
            "usedMb": int(vm.used / (1024 * 1024)),
            "availableMb": int(vm.available / (1024 * 1024)),
            "percent": float(vm.percent),
            "warning": bool(vm.percent >= 85.0 or process_rss_mb >= 1024.0),
        }
    except Exception:
        system_memory = {
            "usedMb": 0,
            "availableMb": 0,
            "percent": 0.0,
            "warning": False,
        }
    direction_quality = _build_direction_quality(runtime_state)
    macro_poll_status = _build_macro_poll_status(runtime_state)
    confidence_calibration = (
        runtime_state.get("confidence_calibration", {})
        if isinstance(runtime_state.get("confidence_calibration", {}), dict)
        else {}
    )
    defense_metrics = (
        runtime_state.get("defense_metrics", {})
        if isinstance(runtime_state.get("defense_metrics", {}), dict)
        else {}
    )
    trailing_telemetry = (
        runtime_state.get("trailing_telemetry", {})
        if isinstance(runtime_state.get("trailing_telemetry", {}), dict)
        else {}
    )
    evolution_evidence = _read_json_file(APP_DIR / "reports" / "evolution_evidence_latest.json")
    autonomy_loop = _read_json_file(LOGS_DIR / "autonomy_loop_latest.json")
    session_features = (
        hermes_runtime_rules.get("session_offline_features", {})
        if isinstance(hermes_runtime_rules.get("session_offline_features", {}), dict)
        else {}
    )
    session_risk_lens = {
        "asiaLondonOverlap": session_features.get("ASIA_LONDON_OVERLAP", {}),
        "londonNyOverlap": session_features.get("LONDON_NY_OVERLAP", {}),
    }
    recent_actions = (
        auto_governance.get("recentActions", [])
        if isinstance(auto_governance.get("recentActions"), list)
        else []
    )
    latest_goal_action = None
    for action_item in reversed(recent_actions):
        if not isinstance(action_item, dict):
            continue
        action_name = str(action_item.get("action", "")).upper()
        if action_name in {"AUTO_FREEZE", "GOAL_GATE_BLOCK"}:
            latest_goal_action = action_item
            break

    goal_snapshot = {}
    if isinstance(latest_goal_action, dict):
        details = latest_goal_action.get("details", {})
        if isinstance(details, dict) and isinstance(details.get("goal_gates"), dict):
            goal_snapshot = details.get("goal_gates", {})
    if not goal_snapshot and isinstance(goal_targets, dict):
        goal_snapshot = (
            goal_targets.get("gates", {})
            if isinstance(goal_targets.get("gates"), dict)
            else {}
        )

    try:
        technical_trend_direction, technical_trend_data = get_technical_trend_bias()
    except Exception:
        technical_trend_direction, technical_trend_data = "NEUTRAL", {}

    decision_contract_lineage = (
        decision_contract.get("lineage", {})
        if isinstance(decision_contract.get("lineage", {}), dict)
        else {}
    )
    missing_lineage = [
        key for key in ("macro", "interpreter", "decision_policy")
        if not str(decision_contract_lineage.get(key, "") or "").strip()
    ]
    legacy_fallback = str(decision_contract_lineage.get("macro", "") or "") == "legacy_direction_registry"
    decision_contract_health = "READY"
    decision_contract_health_label = "统一合同生效中"
    if missing_lineage:
        decision_contract_health = "MISSING_LINEAGE"
        decision_contract_health_label = "合同元数据待修复"
    elif legacy_fallback:
        decision_contract_health = "LEGACY_FALLBACK"
        decision_contract_health_label = "仍有旧链路回退"

    return {
        "success": True,
        "gateway": {
            "daemonRunning": bool(status.get("isRunning", False)),
            "mt5Online": bool(status.get("mt5Online", False)),
            "bridgeOk": bool(status.get("bridgeOk", False)),
            "light": str(status.get("light", "RED") or "RED"),
            "agentDirection": str(
                (
                    status.get("agent_direction_data", {})
                    if isinstance(status.get("agent_direction_data", {}), dict)
                    else {}
                ).get("binding_direction", status.get("agent_dir", "NEUTRAL"))
                or "NEUTRAL"
            ).upper(),
            "bindingDirection": str(
                (
                    status.get("agent_direction_data", {})
                    if isinstance(status.get("agent_direction_data", {}), dict)
                    else {}
                ).get("binding_direction", status.get("agent_dir", "NEUTRAL"))
                or "NEUTRAL"
            ).upper(),
            "effectiveDirection": str(
                (
                    status.get("agent_direction_data", {})
                    if isinstance(status.get("agent_direction_data", {}), dict)
                    else {}
                ).get("effective_direction", "NEUTRAL")
                or "NEUTRAL"
            ).upper(),
            "allowedSide": str(
                (
                    status.get("agent_direction_data", {})
                    if isinstance(status.get("agent_direction_data", {}), dict)
                    else {}
                ).get("allowed_side", "BOTH")
                or "BOTH"
            ).upper(),
            "macroState": str(
                (
                    status.get("agent_direction_data", {})
                    if isinstance(status.get("agent_direction_data", {}), dict)
                    else {}
                ).get("state", "UNKNOWN")
                or "UNKNOWN"
            ).upper(),
            "technicalTrendBias": str(
                status.get("technical_trend_bias", technical_trend_direction) or technical_trend_direction
            ),
            "technicalTrendData": status.get("technical_trend_data", technical_trend_data)
            if isinstance(status.get("technical_trend_data", technical_trend_data), dict)
            else technical_trend_data,
            "macroDirectionData": status.get("agent_direction_data", {})
            if isinstance(status.get("agent_direction_data", {}), dict)
            else {},
            "decisionContract": {
                "updatedAt": str(decision_contract.get("updated_at", "") or ""),
                "lineage": decision_contract_lineage,
                "healthStatus": decision_contract_health,
                "healthLabel": decision_contract_health_label,
                "missingLineage": missing_lineage,
                "legacyFallback": legacy_fallback,
                "profile": str(
                    decision_snapshot_policy.get("profile", "")
                    or (
                        decision_contract.get("decision_policy", {})
                        if isinstance(decision_contract.get("decision_policy", {}), dict)
                        else {}
                    ).get("profile", "")
                    or "unknown"
                ),
                "bindingDirection": str(
                    decision_snapshot_macro.get("binding_direction", status.get("agent_dir", "NEUTRAL"))
                    or "NEUTRAL"
                ).upper(),
                "effectiveDirection": str(
                    decision_snapshot_macro.get("effective_direction", "NEUTRAL") or "NEUTRAL"
                ).upper(),
                "allowedSide": str(
                    decision_snapshot_execution.get("allowed_side", "BOTH") or "BOTH"
                ).upper(),
                "executionAdvice": str(
                    interpreter_payload.get("execution_advice", "UNKNOWN") or "UNKNOWN"
                ).upper(),
            },
            "regime": str(status.get("regime", "UNKNOWN") or "UNKNOWN"),
            "priceAgeSec": float(status.get("priceAge", 0.0) or 0.0),
            "spread": float(status.get("spread", 0.0) or 0.0),
        },
        "interpreter": {
            "status": interpreter_status,
            "running": bool(interpreter_payload),
            "updatedAt": _utc_iso_from_ts(interpreter_ts),
            "ageSec": interpreter_age,
            "validUntil": _utc_iso_from_ts(interpreter_valid_until),
            "view": str(interpreter_payload.get("view", "UNKNOWN") or "UNKNOWN"),
            "marketStructure": str(interpreter_payload.get("market_structure", "UNKNOWN") or "UNKNOWN"),
            "sessionName": str(interpreter_payload.get("session_name", "UNKNOWN") or "UNKNOWN"),
            "executionAdvice": str(interpreter_payload.get("execution_advice", "UNKNOWN") or "UNKNOWN"),
            "dominantDirection": str(interpreter_payload.get("dominant_direction", "UNKNOWN") or "UNKNOWN"),
            "regimeSubtype": str(interpreter_payload.get("regime_subtype", "STRUCTURE_UNCLEAR") or "STRUCTURE_UNCLEAR"),
            "volatilityState": str(interpreter_payload.get("volatility_state", "NORMAL") or "NORMAL"),
            "liquidityState": str(interpreter_payload.get("liquidity_state", "NORMAL") or "NORMAL"),
            "continuationBias": str(interpreter_payload.get("continuation_bias", "NEUTRAL") or "NEUTRAL"),
            "entryTimingHint": str(interpreter_payload.get("entry_timing_hint", "WAIT_CLEAR_SIGNAL") or "WAIT_CLEAR_SIGNAL"),
            "llmConfidence": float(interpreter_payload.get("llm_confidence", 0.55) or 0.55),
            "buyMultiplier": float(interpreter_payload.get("buy_multiplier", 1.0) or 1.0),
            "sellMultiplier": float(interpreter_payload.get("sell_multiplier", 1.0) or 1.0),
            "reason": str(interpreter_payload.get("reason", "") or ""),
        },
        "adaptiveProfile": adaptive_profile,
        "performance": {
            "status": "READY"
            if bool(performance_quality.get("ready", False))
            else ("WARMING_UP" if performance_artifact else "MISSING"),
            "running": bool(performance_artifact),
            "updatedAt": performance_artifact.get("updated_at", ""),
            "ageSec": _age_seconds_from_ts(performance_ts),
            "usableTradeCount": int(performance_quality.get("usable_trade_count", 0) or 0),
            "excludedIncompleteCount": int(performance_quality.get("excluded_incomplete_count", 0) or 0),
            "excludedRecoveredCount": int(performance_quality.get("excluded_recovered_count", 0) or 0),
            "minBucketTrades": int(performance_quality.get("min_bucket_trades", 0) or 0),
            "ready": bool(performance_quality.get("ready", False)),
            "topBuckets": {
                "session": _top_bucket_preview(performance_artifact.get("session", {})),
                "regime": _top_bucket_preview(performance_artifact.get("regime", {})),
                "strategy": _top_bucket_preview(performance_artifact.get("strategy", {})),
                "interpreter": _top_bucket_preview(performance_artifact.get("interpreter", {})),
            },
            "hermesRuntimeRules": hermes_runtime_rules,
            "systemDesignContract": system_design_contract,
        },
        "trainingGate": {
            "status": "READY" if bool(training_status.get("readyForTraining", False)) else "BLOCKED",
            "readyForTraining": bool(training_status.get("readyForTraining", False)),
            "blockedReasons": training_status.get("blockedReasons", []),
            "criticalAnomalies": int(training_status.get("criticalAnomalies", 0) or 0),
            "closureCoveragePct": float(training_status.get("closureCoveragePct", 0.0) or 0.0),
            "naturalClosedRatioPct": float(training_status.get("naturalClosedRatioPct", 0.0) or 0.0),
            "recoveredClosedRatioPct": float(training_status.get("recoveredClosedRatioPct", 0.0) or 0.0),
            "pendingRepairCount": int(
                (
                    governance_lifecycle.get("closureLifecycle", {})
                    if isinstance(governance_lifecycle.get("closureLifecycle"), dict)
                    else {}
                ).get("pendingRepairCount", 0)
                or 0
            ),
            "repairRecovered": int(training_lifecycle.get("repairRecovered", 0) or 0),
        },
        "autoGovernance": {
            "status": str(auto_governance.get("status", auto_governance.get("last_action", "NOOP")) or "NOOP"),
            "enabled": bool(auto_governance.get("enabled", True)),
            "currentMode": str(auto_governance.get("currentMode", auto_governance.get("current_mode", auto_governance.get("last_mode", "shadow"))) or "shadow"),
            "nextMode": str(auto_governance.get("nextMode", auto_governance.get("next_mode", "")) or ""),
            "lastAction": str(auto_governance.get("lastAction", auto_governance.get("last_action", auto_governance.get("action", "NOOP"))) or "NOOP"),
            "lastReason": str(auto_governance.get("lastReason", auto_governance.get("last_reason", auto_governance.get("reason", ""))) or ""),
            "updatedAt": _utc_iso_from_ts(auto_governance_ts),
            "ageSec": _age_seconds_from_ts(auto_governance_ts),
            "canPromote": bool(approval_decision.get("canPromote", False)),
            "decisionStatus": str(approval_decision.get("status", "UNKNOWN") or "UNKNOWN"),
            "missingConditions": approval_decision.get("missingConditions", []),
            "latestGoalAction": latest_goal_action or {},
            "goalGateSnapshot": goal_snapshot,
            "goalRecoveryHint": "月回撤<=10% 且 月自治率>=85% 后自动解除冻结",
        },
        "goalTargets": goal_targets,
        "evolutionEvidence": evolution_evidence,
        "autonomyLoop": autonomy_loop,
        "sessionRiskLens": session_risk_lens,
        "directionQuality": direction_quality,
        "macroPolling": macro_poll_status,
        "runtimeGuard": {
            "present": runtime_guard_present,
            "updatedAt": str(runtime_open_guard.get("updated_at", "") or ""),
            "positionsCount": int(runtime_open_guard.get("positions_count", 0) or 0),
        },
        "positionTier": _build_position_tier_snapshot(effective_runtime_state),
        "memory": {
            "processRssMb": process_rss_mb,
            "system": system_memory,
            "debug": get_memory_debug(deep=0),
        },
        "confidenceCalibration": confidence_calibration,
        "defenseMetrics": defense_metrics,
        "trailingTelemetry": trailing_telemetry,
    }


def _refresh_runtime_observability_cache_worker() -> None:
    try:
        payload = _build_runtime_observability_payload()
        if isinstance(payload, dict) and bool(payload.get("success", False)):
            with _RUNTIME_OBSERVABILITY_CACHE_LOCK:
                _RUNTIME_OBSERVABILITY_CACHE.update({"ts": time.time(), "value": payload})
    finally:
        with _RUNTIME_OBSERVABILITY_CACHE_LOCK:
            _RUNTIME_OBSERVABILITY_CACHE["refreshing"] = False
            _RUNTIME_OBSERVABILITY_CACHE["thread"] = None
            _RUNTIME_OBSERVABILITY_CACHE["refresh_started_ts"] = 0.0


def _schedule_runtime_observability_refresh() -> bool:
    with _RUNTIME_OBSERVABILITY_CACHE_LOCK:
        if bool(_RUNTIME_OBSERVABILITY_CACHE.get("refreshing", False)):
            return False
        existing = _find_alive_thread_by_name("gui-runtime-observability-refresh")
        if existing is not None:
            _RUNTIME_OBSERVABILITY_CACHE["refreshing"] = True
            _RUNTIME_OBSERVABILITY_CACHE["thread"] = existing
            _RUNTIME_OBSERVABILITY_CACHE["refresh_started_ts"] = float(
                _RUNTIME_OBSERVABILITY_CACHE.get("refresh_started_ts", 0.0) or time.time()
            )
            return False
        thread = threading.Thread(
            target=_refresh_runtime_observability_cache_worker,
            daemon=True,
            name="gui-runtime-observability-refresh",
        )
        _RUNTIME_OBSERVABILITY_CACHE["refreshing"] = True
        _RUNTIME_OBSERVABILITY_CACHE["thread"] = thread
        _RUNTIME_OBSERVABILITY_CACHE["refresh_started_ts"] = time.time()
        thread.start()
        return True


@app.get("/api/runtime/observability")
def get_runtime_observability(force_refresh: bool = False):
    if force_refresh:
        payload = _build_runtime_observability_payload()
        if isinstance(payload, dict) and bool(payload.get("success", False)):
            with _RUNTIME_OBSERVABILITY_CACHE_LOCK:
                _RUNTIME_OBSERVABILITY_CACHE.update(
                    {
                        "ts": time.time(),
                        "value": payload,
                        "refreshing": False,
                        "thread": None,
                        "refresh_started_ts": 0.0,
                    }
                )
            return payload
    cache_now = time.time()
    schedule_refresh_after_unlock = False
    refresh_scheduled = False
    with _RUNTIME_OBSERVABILITY_CACHE_LOCK:
        cached_payload = _RUNTIME_OBSERVABILITY_CACHE.get("value")
        cache_age = cache_now - float(_RUNTIME_OBSERVABILITY_CACHE.get("ts", 0.0) or 0.0)
        refreshing = bool(_RUNTIME_OBSERVABILITY_CACHE.get("refreshing", False))
        if refreshing:
            thread = _RUNTIME_OBSERVABILITY_CACHE.get("thread")
            started_ts = float(_RUNTIME_OBSERVABILITY_CACHE.get("refresh_started_ts", 0.0) or 0.0)
            thread_alive = bool(getattr(thread, "is_alive", lambda: False)()) if thread is not None else False
            stuck = started_ts > 0 and (cache_now - started_ts) >= _RUNTIME_OBSERVABILITY_REFRESH_STUCK_SEC
            if (thread is not None and not thread_alive) or stuck:
                _RUNTIME_OBSERVABILITY_CACHE["refreshing"] = False
                _RUNTIME_OBSERVABILITY_CACHE["thread"] = None
                _RUNTIME_OBSERVABILITY_CACHE["refresh_started_ts"] = 0.0
                refreshing = False
                schedule_refresh_after_unlock = True

    if schedule_refresh_after_unlock:
        refresh_scheduled = _schedule_runtime_observability_refresh()

    if cached_payload is not None and cache_age < _RUNTIME_OBSERVABILITY_CACHE_TTL_SEC:
        daemon_running_now = _resolve_daemon_running()
        gateway = cached_payload.get("gateway", {}) if isinstance(cached_payload, dict) else {}
        cached_daemon_running = bool(gateway.get("daemonRunning", False)) if isinstance(gateway, dict) else False
        if cached_daemon_running != daemon_running_now:
            _schedule_runtime_observability_refresh()
            patched = dict(cached_payload) if isinstance(cached_payload, dict) else {}
            patched_gateway = dict(gateway) if isinstance(gateway, dict) else {}
            patched_gateway["daemonRunning"] = daemon_running_now
            patched["gateway"] = patched_gateway
            return patched
        return cached_payload
    if cached_payload is not None:
        if not refreshing and not refresh_scheduled:
            _schedule_runtime_observability_refresh()
        stale_payload = dict(cached_payload)
        stale_payload["stale"] = True
        stale_payload["cacheAgeSec"] = round(cache_age, 1)
        return stale_payload

    if not refreshing:
        _schedule_runtime_observability_refresh()
    return {
        "success": True,
        "warming": True,
        "stale": True,
        "warning": "runtime observability cache warming",
        "gateway": {},
        "interpreter": {},
        "adaptiveProfile": {},
        "performance": {},
        "trainingGate": {},
        "autoGovernance": {},
        "goalTargets": {},
        "evolutionEvidence": {},
        "autonomyLoop": {},
        "sessionRiskLens": {},
        "directionQuality": {},
        "macroPolling": {},
        "memory": {},
        "confidenceCalibration": {},
        "defenseMetrics": {},
        "trailingTelemetry": {},
    }


def _build_strategy_workbench_response():
    status_payload = get_status()
    marketplace_payload = _build_strategy_marketplace_payload(status_payload=status_payload)
    return _attach_projection_metadata(
        {
        "success": True,
        "execution": _build_execution_chain_snapshot(
            status_payload=status_payload,
            marketplace_payload=marketplace_payload,
        ),
        "risk": _build_risk_governor_snapshot(status_payload=status_payload),
        "marketplace": marketplace_payload,
        "timeline": _get_timeline_payload_cached(limit=40),
        },
        snapshot_name="strategy_workbench",
        source_paths={
            "strategy_market_state": _resolve_runtime_artifact("config", "strategy_market_state.json"),
            "effective_params": _resolve_runtime_artifact("logs", "effective_params.json"),
            "execution_state": _resolve_runtime_artifact("data", "execution_state.json"),
            "position_lifecycle": _resolve_runtime_artifact("data", "position_lifecycle.json"),
            "blackboard_state": _resolve_runtime_root_artifact("blackboard_state.json"),
        },
    )


@app.get("/api/workbench/strategy")
def get_strategy_workbench(force_refresh: bool = False):
    warming_payload = {
        "success": True,
        "execution": {"summary": {}, "openPositions": [], "pendingSignals": [], "activeSignals": [], "recentSignals": []},
        "risk": {},
        "marketplace": {},
        "timeline": {"success": True, "events": [], "sources": []},
    }
    return _get_cached_aux_payload(
        _STRATEGY_WORKBENCH_CACHE,
        _STRATEGY_WORKBENCH_CACHE_TTL_SEC,
        _build_strategy_workbench_response,
        "gui-strategy-workbench-refresh",
        warming_payload,
        force_refresh=force_refresh,
    )


def _build_strategy_workbench_marketplace_response():
    status_payload = get_status()
    return _attach_projection_metadata(
        {
            "success": True,
            "marketplace": _build_strategy_marketplace_payload(status_payload=status_payload),
        },
        snapshot_name="strategy_marketplace",
        source_paths=_runtime_projection_source_paths(),
    )


@app.get("/api/workbench/strategy/marketplace")
def get_strategy_workbench_marketplace(force_refresh: bool = False):
    warming_payload = {
        "success": True,
        "marketplace": {
            "strategyTemplates": [],
            "runtimeInstances": [],
            "orchestration": {},
            "current": {},
        },
    }
    return _get_cached_aux_payload(
        _STRATEGY_MARKETPLACE_CACHE,
        _STRATEGY_MARKETPLACE_CACHE_TTL_SEC,
        _build_strategy_workbench_marketplace_response,
        "gui-strategy-marketplace-refresh",
        warming_payload,
        force_refresh=force_refresh,
    )


def _build_runtime_instances_response():
    status_payload = get_status()
    return _attach_projection_metadata(
        _build_runtime_operations_payload(status_payload=status_payload),
        snapshot_name="runtime_instances",
        source_paths=_runtime_projection_source_paths(),
    )


@app.get("/api/strategy/runtime/instances")
def list_runtime_instances(force_refresh: bool = False):
    warming_payload = {
        "success": True,
        "runtimeInstances": [],
        "runtimeScheduler": {},
        "runtimeExplain": {},
        "runtimeAgentDiagnostics": {},
        "runtimeBridgeQueue": {},
        "runtimeBridgeRequests": [],
        "runtimeExecutionFocus": [],
        "runtimeBridgeHealth": {},
        "runtimeStatus": {},
        "orchestrationSnapshot": {},
    }
    return _get_cached_aux_payload(
        _RUNTIME_INSTANCES_CACHE,
        _RUNTIME_INSTANCES_CACHE_TTL_SEC,
        _build_runtime_instances_response,
        "gui-runtime-instances-refresh",
        warming_payload,
        force_refresh=force_refresh,
    )


@app.get("/api/strategy/runtime/evaluate")
def get_runtime_ai_evaluation():
    state = _load_runtime_ai_evaluation_state()
    history = state.get("history", []) if isinstance(state.get("history", []), list) else []
    return {
        "success": True,
        "evaluation": state.get("lastEvaluation", {}) if isinstance(state.get("lastEvaluation", {}), dict) else {},
        "history": history[:8],
        "updatedAt": str(state.get("updatedAt", "") or ""),
    }


@app.post("/api/strategy/runtime/evaluate")
def evaluate_runtime_ai_combination(req: RuntimeStrategyEvaluationRequest):
    status_payload = get_status()
    runtime_payload = _build_runtime_operations_payload(status_payload=status_payload)
    evaluation = _build_runtime_ai_evaluation(
        str(req.goal or "").strip(),
        status_payload=status_payload,
        runtime_payload=runtime_payload,
    )
    state = _load_runtime_ai_evaluation_state()
    history = state.get("history", []) if isinstance(state.get("history", []), list) else []
    history_entry = evaluation.get("historyEntry", {}) if isinstance(evaluation.get("historyEntry", {}), dict) else {}
    if history_entry:
        history = [history_entry, *[item for item in history if isinstance(item, dict)]][:8]
    _save_runtime_ai_evaluation_state(
        {
            "lastEvaluation": evaluation,
            "history": history,
        }
    )
    return {
        "success": True,
        "evaluation": evaluation,
        "history": history,
    }


@app.post("/api/training/promotion_check")
def run_training_promotion_check():
    payload = get_training_approval_chain()
    decision = payload.get("decision", {})
    return {
        "success": True,
        "stage": payload.get("stage", "NO_SUGGESTION"),
        "decision": decision,
        "message": decision.get("nextStep", "审批链校验完成"),
    }


@app.post("/api/training/manual_review/complete")
def complete_training_manual_review():
    try:
        from src.evolution_coordinator import EvolutionCoordinator

        coordinator = EvolutionCoordinator(APP_DIR, "default")
        result = coordinator.complete_manual_review(source="gui_manual_review_complete")
    except FileNotFoundError:
        return {"success": False, "message": "当前没有待复核的生效参数"}
    except Exception as e:
        return {"success": False, "message": f"完成人工复核失败: {e}"}

    approval = get_training_approval_chain()
    return {
        "success": True,
        "message": "人工复核已完成，当前参数已解除 review_pending",
        "files": result.get("files", {}),
        "decision": approval.get("decision", {}),
    }


@app.post("/api/training/manual_review/rollback")
def rollback_training_manual_review():
    try:
        from src.evolution_coordinator import EvolutionCoordinator

        coordinator = EvolutionCoordinator(APP_DIR, "default")
        result = coordinator.rollback_candidate(
            source="manual_training_review",
            reason="user_requested_training_rollback",
        )
    except FileNotFoundError:
        return {"success": False, "message": "当前没有可回滚的稳定参数快照"}
    except Exception as e:
        return {"success": False, "message": f"训练参数回滚失败: {e}"}

    approval = get_training_approval_chain()
    return {
        "success": True,
        "message": "已回滚到上一版稳定实盘参数",
        "rollback": result,
        "decision": approval.get("decision", {}),
    }


@app.post("/api/training/repair_blockers")
def repair_training_blockers():
    actions: List[str] = []
    status_updates = _read_training_status_payload()

    try:
        from src.offline_train import evaluate_training_readiness
        from src.learning.transition_audit import run_repair_cycle

        repair_cycle = run_repair_cycle(auto_recover=True)
        actions.append("repair_cycle")
        gate = evaluate_training_readiness(repair_cycle=repair_cycle)
        status_updates["qualityGate"] = gate
        status_updates["readyForTraining"] = gate.get("ready_for_training", False)
        status_updates["blockedReasons"] = gate.get("blocked_reasons", [])
        status_updates["closureCoveragePct"] = gate.get("closure_coverage_pct", 0.0)
        status_updates["criticalAnomalies"] = gate.get("critical_anomalies", 0)
        status_updates["recoveredClosedRatioPct"] = gate.get("recovered_closed_ratio_pct", 0.0)
        recovery = gate.get("recovery", {}) if isinstance(gate.get("recovery"), dict) else {}
        status_updates["naturalClosedRatioPct"] = recovery.get("natural_closed_ratio_pct", 0.0)
        status_updates["naturalClosedTransitions"] = recovery.get("natural_closed_transitions", 0)
        status_updates["recoveredClosedTransitions"] = recovery.get("recovered_closed_transitions", 0)
        status_updates["usableTradeCount"] = int(gate.get("usable_trade_count", 0) or 0)
        status_updates["excludedIncompleteCount"] = int(gate.get("excluded_incomplete_count", 0) or 0)
        status_updates["excludedRecoveredCount"] = int(gate.get("excluded_recovered_count", 0) or 0)
        status_updates["samplePolicy"] = gate.get("sample_policy", {})
        status_updates["qualitySnapshotId"] = gate.get("snapshot_id", "")
        status_updates["qualitySnapshotAt"] = gate.get("generated_at", "")
        status_updates["repairCycle"] = gate.get("repair_cycle", {})
        status_updates["blocked"] = not gate.get("ready_for_training", False)
    except Exception as e:
        return {"success": False, "message": f"修复训练阻断项失败: {e}"}

    shadow_file = APP_DIR / "config" / "params_shadow.json"
    if not shadow_file.exists():
        approval = get_training_approval_chain()
        candidate = (
            _extract_param_map(approval.get("suggestion", {}))
            or _extract_param_map(approval.get("appliedSuggestion", {}))
            or _extract_param_map(approval.get("effectiveParams", {}))
        )
        if candidate:
            from src.param_flow import build_shadow_payload, write_json
            shadow_payload = build_shadow_payload({"suggested_params": candidate, "source": "repair_training_blockers"})
            write_json(shadow_file, shadow_payload)
            actions.append("rebuild_shadow_params")

    status_updates["updatedAt"] = datetime.datetime.now().isoformat()
    _write_training_status_payload(status_updates)
    approval = get_training_approval_chain()
    return {
        "success": True,
        "message": "阻断项修复已执行",
        "actions": actions,
        "decision": approval.get("decision", {}),
    }


@app.post("/api/training/promote")
def promote_training_candidate():
    try:
        from src.evolution_coordinator import EvolutionCoordinator

        coordinator = EvolutionCoordinator(APP_DIR, "default")
        result = coordinator.promote(source="training_promote")
        if not result.get("success", False):
            return {
                "success": False,
                "message": str(result.get("message", "当前未满足推进条件")),
                "decision": (result.get("snapshot", {}) or {}).get("decision", {}),
            }
        promotion = result.get("promotion", {})
        snapshot = result.get("snapshot", {})
        return {
            "success": True,
            "message": f"已推进到 {promotion.get('promotion_mode', '下一阶段')}",
            "promotion": promotion,
            "snapshot": result.get("created_snapshot", {}),
            "decision": (snapshot.get("decision", {}) if isinstance(snapshot, dict) else {}),
        }
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.post("/api/action/approve_evolution")
def approve_evolution_action():
    try:
        from src.evolution_coordinator import EvolutionCoordinator

        coordinator = EvolutionCoordinator(APP_DIR, "default")
        result = coordinator.approve_to_shadow(source="mobile_approve")
        approval = get_training_approval_chain()
        return {
            "success": True,
            "message": "演化建议已进入可回滚观察态，待人工复核后再继续推进",
            "files": result.get("files", {}),
            "decision": approval.get("decision", {}),
        }
    except FileNotFoundError:
        return {"success": False, "message": "当前没有待审批的演化建议"}
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.post("/api/action/reject_evolution")
def reject_evolution_action():
    try:
        from src.evolution_coordinator import EvolutionCoordinator

        coordinator = EvolutionCoordinator(APP_DIR, "default")
        result = coordinator.reject_suggestion(source="mobile_reject")
        approval = get_training_approval_chain()
        return {
            "success": True,
            "message": "演化建议已驳回",
            "files": result.get("files", {}),
            "decision": approval.get("decision", {}),
        }
    except FileNotFoundError:
        return {"success": False, "message": "当前没有待审批的演化建议"}
    except Exception as e:
        return {"success": False, "error": str(e)}


@app.post("/api/governance/auto_tick")
def run_auto_governance_tick():
    try:
        from src.auto_governance import run_auto_governance_cycle
        result = run_auto_governance_cycle(APP_DIR, "default", source="gui_backend")
        return {"success": True, "result": result}
    except Exception as e:
        return {"success": False, "error": str(e)}

@app.post("/api/chat")
def chat_with_agent(payload: dict):
    """与龙虾 Agent 的交互对话"""
    mode = str(payload.get("mode", "") or "").strip()
    user_text = payload.get("text", "")
    conv_id = str(payload.get("conversation_id", "") or "").strip()

    # ── Strategy Builder mode: AI-driven guided strategy development ──
    if mode == "strategy_builder":
        result = _process_strategy_builder_turn(
            user_text=user_text,
            conversation_id=conv_id,
            message_kind=_interactive_message_kind(str(user_text or "")),
        )
        return result

    # ── Original chat path (non-strategy-builder) ──
    if not user_text:
        return {"success": False, "reply": "咕噜咕噜..."}

    # 简单的正则或关键词匹配,结合情感化回复
    reply = ""

    # 尝试调用 chat_tuning 来解析一些正式的指令
    try:
        # 确保能 import src 里的模块
        src_path = str(_SRC_DIR)
        if src_path not in sys.path:
            sys.path.insert(0, src_path)

        from src.chat_tuning import parse_command
        parsed = parse_command(user_text)
        if parsed:
            intent, value = parsed
            # 假装执行了并给一个可爱的反馈
            if intent == "SET_MAX_RISK_PER_TRADE":
                reply = f"收到老板!已经把单笔风险调整到 {value}% 啦![LIVE] 冲冲冲!"
                # 实际可以更新 config
                config_file = CONFIG_DIR / "risk_governor_rules.json"
                if config_file.exists():
                    data = json.loads(config_file.read_text(encoding="utf-8"))
                    data["max_position_size"] = float(value)
                    config_file.write_text(json.dumps(data, indent=2))
            elif intent == "EMERGENCY_STOP":
                reply = "[ALERT] 紧急刹车!我已经把交易网关停了!老板放心!"
            else:
                reply = f"好的老板,执行指令: {intent} = {value} [LIVE]"
    except Exception as e:
        pass

    if not reply:
        if "行情" in user_text or "看看" in user_text:
            bid, ask, spr, price_source, price_age = get_price()
            reply = f"正在盯盘呢!当前黄金价格 {bid},点差 {spr},随时准备出击![CUT]"
        elif "降低仓位" in user_text or "保守" in user_text:
            reply = "没问题,我已经把风控调到【保守模式】啦!安全第一![SHIELD]️"
        elif "激进" in user_text or "干" in user_text:
            reply = "收到!风控已调至【激进模式】!今天我们要大赚一笔![MONEY]"
        else:
            # 调用 DeepSeek LLM
            api_key = os.environ.get("DEEPSEEK_API_KEY", "")
            if not api_key:
                app_config = config_manager.load_app_config()
                api_key = app_config.get("DEEPSEEK_API_KEY", "")

            if not api_key:
                reply = "咕噜咕噜... 老板,我没有配置大模型 API Key 呢,请先在配置中设置 DEEPSEEK_API_KEY!"
            else:
                try:
                    import requests
                    resp = requests.post(
                        "https://api.deepseek.com/v1/chat/completions",
                        headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
                        json={
                            "model": "deepseek-v4-flash",
                            "messages": [
                                {"role": "system", "content": "你是 OpenClaw 量化交易系统的 AI 助手(龙虾Agent),你的性格是专业、机智且带有一点幽默感。你可以回答关于黄金交易、当前系统状态的问题。你的回答应该简明扼要。"},
                                {"role": "user", "content": user_text}
                            ],
                            "max_tokens": 500,
                            "temperature": 0.7,
                        },
                        timeout=15,
                    )
                    resp.raise_for_status()
                    data = resp.json()
                    reply = data["choices"][0]["message"]["content"].strip()
                except Exception as e:
                    reply = f"调用大模型失败啦:{str(e)}"

    return {
        "success": True,
        "reply": reply,
        "timestamp": datetime.datetime.now().strftime("%H:%M:%S")
    }

# ======== 挂载前端静态页面 (Web GUI) ========
# 判断是否有编译好的前端 dist 目录
# 兼容 PyInstaller 的 _MEIPASS 机制；非打包环境优先读取当前运行副本旁边的前端产物，
# 避免 launchd 后台进程再去访问 Documents 下的源码目录而触发 TCC 权限拦截。
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
    FRONTEND_DIST = pathlib.Path(sys._MEIPASS) / "gui_react" / "dist"
else:
    frontend_candidates = [
        RELEASE_ROOT / "gui_react" / "dist",
        APP_DIR / "gui_react" / "dist",
        pathlib.Path(__file__).resolve().parent / "gui_react" / "dist",
    ]
    FRONTEND_DIST = next((path for path in frontend_candidates if _is_readable_dir(path)), frontend_candidates[0])


def _load_frontend_design_lock_markers() -> list[str]:
    lock_path = pathlib.Path(__file__).resolve().parent / "ops" / "ui_design_lock.json"
    try:
        payload = json.loads(lock_path.read_text(encoding="utf-8"))
    except Exception:
        return []
    markers = payload.get("dist", {}).get("required_markers", [])
    if not isinstance(markers, list):
        return []
    return [str(item) for item in markers if str(item or "").strip()]


def _validate_runtime_frontend_dist(frontend_dist: pathlib.Path) -> list[str]:
    index_file = frontend_dist / "index.html"
    if not _is_readable_file(index_file):
        return [f"missing index shell: {index_file}"]
    markers = _load_frontend_design_lock_markers()
    if not markers:
        return []
    text_blobs: list[str] = []
    for path in frontend_dist.rglob("*"):
        if not path.is_file():
            continue
        if path.suffix.lower() not in {".html", ".js", ".css", ".svg", ".txt"}:
            continue
        try:
            text_blobs.append(path.read_text(encoding="utf-8", errors="ignore"))
        except Exception:
            continue
    combined_text = "\n".join(text_blobs)
    errors: list[str] = []
    for marker in markers:
        if marker not in combined_text:
            errors.append(f"missing required dist marker: {marker}")
    return errors


_FRONTEND_DIST_LOCK_ERRORS = (
    _validate_runtime_frontend_dist(FRONTEND_DIST)
    if _is_readable_dir(FRONTEND_DIST)
    else []
)


def _frontend_warming_response() -> HTMLResponse:
    return HTMLResponse(
        content="""<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="refresh" content="2" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Gold AutoTrade</title>
    <style>
      body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #0b1020; color: #e5e7eb; display: grid; place-items: center; min-height: 100vh; margin: 0; }
      main { max-width: 520px; padding: 24px; text-align: center; }
      h1 { font-size: 20px; margin-bottom: 12px; }
      p { line-height: 1.5; color: #94a3b8; }
    </style>
  </head>
  <body>
    <main>
      <h1>Gold AutoTrade is starting</h1>
      <p>The web UI is warming up. This page will retry automatically.</p>
    </main>
  </body>
</html>""",
        status_code=503,
    )


def _frontend_design_regression_response(errors: list[str]) -> HTMLResponse:
    details = "".join(
        f"<li>{str(err)}</li>"
        for err in errors[:8]
    )
    return HTMLResponse(
        content=f"""<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="refresh" content="2" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Gold AutoTrade</title>
    <style>
      body {{ font-family: -apple-system, BlinkMacSystemFont, sans-serif; background: #0b1020; color: #e5e7eb; display: grid; place-items: center; min-height: 100vh; margin: 0; }}
      main {{ max-width: 720px; padding: 24px; text-align: left; }}
      h1 {{ font-size: 20px; margin-bottom: 12px; }}
      p, li {{ line-height: 1.5; color: #94a3b8; }}
      ul {{ padding-left: 20px; }}
      code {{ color: #e2e8f0; }}
    </style>
  </head>
  <body>
    <main>
      <h1>Frontend design lock rejected this runtime bundle</h1>
      <p>The running UI bundle is missing approved design markers, so Gold AutoTrade refuses to serve a regressed interface.</p>
      <ul>{details}</ul>
      <p><code>{FRONTEND_DIST}</code></p>
    </main>
  </body>
</html>""",
        status_code=503,
    )


def _safe_frontend_file_response(path: pathlib.Path) -> Union[HTMLResponse, FileResponse]:
    if _is_readable_file(path):
        headers = None
        if path.suffix.lower() in {".html", ".htm"}:
            if _FRONTEND_DIST_LOCK_ERRORS:
                return _frontend_design_regression_response(_FRONTEND_DIST_LOCK_ERRORS)
            # Force desktop WebKit/browser shells to reload the latest index shell after each deploy.
            headers = {
                "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
                "Pragma": "no-cache",
                "Expires": "0",
            }
        return FileResponse(str(path), headers=headers)
    return _frontend_warming_response()


if FRONTEND_DIST.exists():
    # 挂载静态资源文件 (js, css 等)
    frontend_assets_dir = FRONTEND_DIST / "assets"
    if _is_readable_dir(frontend_assets_dir):
        app.mount("/assets", StaticFiles(directory=str(frontend_assets_dir)), name="assets")
    # i18n locale files — served as static JSON
    import src.i18n as _i18n_pkg
    _locales_dir = pathlib.Path(_i18n_pkg.__file__).parent / "locales"
    if _is_readable_dir(_locales_dir):
        app.mount("/locales", StaticFiles(directory=str(_locales_dir)), name="locales")
    prototype_index = pathlib.Path(__file__).resolve().parent / "gui_prototype" / "index.html"

    # 将根路径 / 映射到 index.html
    @app.get("/")
    def serve_frontend():
        return _safe_frontend_file_response(FRONTEND_DIST / "index.html")

    @app.get("/ops-console")
    def serve_ops_console():
        if _is_readable_file(prototype_index):
            return FileResponse(str(prototype_index))
        return _safe_frontend_file_response(FRONTEND_DIST / "index.html")

    # ============ Agent 模型配置 API（必须在 catchall 之前注册）============
    # 前端 App.tsx fetchAgentModels → /api/agent_models
    # 之前路由缺失：catchall 直接 fall through 返回 null，前端报"模型配置请求失败"。
    @app.get("/api/agent_models")
    def get_agent_models():
        try:
            from src.llm.config import load_agent_models
            from src.llm.provider_admin import list_providers as _lsp
            agents = load_agent_models() or {}
            # Use list_providers() for hasKey — same data source as /api/providers
            provider_list = (_lsp().get("providers", []) or []) if callable(_lsp) else []
            provider_keys = {p["name"]: p.get("hasKey", False) for p in provider_list if isinstance(p, dict)}
        except Exception as exc:
            return {
                "success": False,
                "agents": [],
                "available_models": [],
                "error": f"load_agent_models failed: {exc}",
            }
        # 收集所有出现过的 model id 去重，作为 available_models
        available_models: list[str] = []
        seen: set[str] = set()
        # Enrich each agent with api_key_configured status
        enriched: list[dict] = []
        for role, agent in agents.items():
            model_id = str(agent.get("model", "") or "").strip()
            provider_name = model_id.split("/")[0] if "/" in model_id else ""
            has_key = bool(provider_keys.get(provider_name, False))
            agent_out = dict(agent)
            agent_out.pop("api_key_configured", None)
            agent_out.pop("provider", None)
            agent_out["api_key_configured"] = has_key
            agent_out["provider"] = provider_name
            enriched.append(agent_out)
            if model_id and model_id not in seen:
                available_models.append(model_id)
                seen.add(model_id)
        # Also include models from all configured providers
        try:
            from src.llm.provider_admin import read_allowed_models
            for mid in read_allowed_models():
                if mid and mid not in seen:
                    available_models.append(mid)
                    seen.add(mid)
        except Exception as e:
            import logging
            logging.getLogger("gui_backend").error(
                "Failed to merge provider models: %s", e, exc_info=True
            )
        return {
            "success": True,
            "agents": enriched,
            "available_models": available_models,
        }

    @app.post("/api/agent_models")
    async def update_agent_model(request: Request):
        """更新指定角色的模型绑定：payload 可以是数组 [{role, model}, ...] 或单对象 {role, model}"""
        try:
            body = await request.json()
        except Exception:
            body = {}
        # 前端 saveAgentModelsSelection 发送整个 agents 数组（nextAgents）
        items: list[dict[str, Any]] = []
        if isinstance(body, list):
            items = [x for x in body if isinstance(x, dict)]
        elif isinstance(body, dict):
            items = [body]
        if not items:
            return {"success": False, "error": "role 与 model 都不能为空"}
        # 批量更新 agent_models.json
        try:
            import config_manager
            cfg_path = config_manager.get_config_dir() / "agent_models.json"
            cfg_path.parent.mkdir(parents=True, exist_ok=True)
            existing: list[dict[str, Any]] = []
            if cfg_path.exists():
                try:
                    raw = json.loads(cfg_path.read_text(encoding="utf-8"))
                    if isinstance(raw, list):
                        existing = [x for x in raw if isinstance(x, dict)]
                except Exception:
                    existing = []
            for item in items:
                role = str(item.get("role", "") or "").strip()
                model_id = str(item.get("model", "") or "").strip()
                if not role or not model_id:
                    continue
                replaced = False
                for ex in existing:
                    if str(ex.get("role", "") or "").strip() == role:
                        ex["model"] = model_id
                        ex["status"] = str(item.get("status", "ready") or "ready")
                        if item.get("temperature") is not None:
                            ex["temperature"] = item["temperature"]
                        if item.get("max_tokens") is not None:
                            ex["max_tokens"] = item["max_tokens"]
                        if item.get("timeout_sec") is not None:
                            ex["timeout_sec"] = item["timeout_sec"]
                        if item.get("description"):
                            ex["description"] = item["description"]
                        replaced = True
                        break
                if not replaced:
                    entry = {"role": role, "model": model_id, "status": str(item.get("status", "ready") or "ready")}
                    if item.get("temperature") is not None:
                        entry["temperature"] = item["temperature"]
                    if item.get("max_tokens") is not None:
                        entry["max_tokens"] = item["max_tokens"]
                    if item.get("timeout_sec") is not None:
                        entry["timeout_sec"] = item["timeout_sec"]
                    if item.get("description"):
                        entry["description"] = item["description"]
                    existing.append(entry)
            tmp = cfg_path.with_suffix(cfg_path.suffix + ".tmp")
            tmp.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
            os.replace(tmp, cfg_path)
            return {"success": True, "updated": len(items)}
        except Exception as exc:
            return {"success": False, "error": f"write agent_models.json failed: {exc}"}

    @app.get("/api/llm_providers")
    def get_llm_providers():
        """返回当前 LLM providers 列表（DeepSeek / OMLX 等），供前端"大模型配置"面板使用。"""
        try:
            from src.llm.config import _get_llm_providers_path
            providers_path = _get_llm_providers_path()
            if providers_path and providers_path.exists():
                data = json.loads(providers_path.read_text(encoding="utf-8"))
                if isinstance(data, dict):
                    return {"success": True, "providers": data}
            return {"success": True, "providers": {}}
        except Exception as exc:
            return {"success": False, "providers": {}, "error": str(exc)}

    # ============ 供应商管理 API（前端"供应商管理"面板）============
    # All provider CRUD now delegates to src/llm/provider_admin.py — single source of truth.
    # Frontend App.tsx fetchProviders → /api/providers (GET/POST/DELETE)
    # Migrated to eliminate dual-route ambiguity with /api/v2/providers.

    @app.get("/api/llm_providers")
    def get_llm_providers():
        """Return current LLM provider list for the frontend config panel.

        Delegates to provider_admin.list_providers() — the authoritative source.
        """
        try:
            from src.llm.provider_admin import list_providers
            return list_providers()
        except Exception as exc:
            return {"success": False, "providers": [], "error": str(exc)}

    @app.get("/api/providers")
    def list_providers():
        """List all LLM providers (system + custom). Delegates to provider_admin."""
        try:
            from src.llm.provider_admin import list_providers
            return list_providers()
        except Exception as exc:
            return {"success": False, "providers": [], "error": str(exc)}

    @app.post("/api/providers")
    def save_provider(payload: dict[str, Any]):
        """Create/update a custom provider. Delegates to provider_admin."""
        name = str(payload.get("name", "") or "").strip()
        base_url = str(payload.get("baseUrl", "") or "").strip()
        api = str(payload.get("api", "openai-responses") or "openai-responses").strip()
        api_key = str(payload.get("apiKey", "") or "").strip()
        models = payload.get("models", [])
        if not isinstance(models, list):
            models = []
        if not name or not base_url:
            return {"success": False, "error": "供应商名称和 baseUrl 不能为空"}
        try:
            from src.llm.provider_admin import save_provider as _sp
            return _sp(name, base_url, api, api_key, models)
        except Exception as exc:
            return {"success": False, "error": str(exc)}

    @app.get("/api/providers/{provider_name}")
    def get_provider_detail(provider_name: str):
        """Get single provider detail. Delegates to provider_admin."""
        provider_name = str(provider_name or "").strip()
        try:
            from src.llm.provider_admin import list_providers
            result = list_providers()
            for p in result.get("providers", []):
                if p.get("name") == provider_name:
                    return {"success": True, "provider": p}
            return {"success": False, "error": f"供应商 {provider_name} 不存在"}
        except Exception as exc:
            return {"success": False, "error": str(exc)}

    @app.delete("/api/providers/{provider_name}")
    def delete_provider(provider_name: str):
        """Delete a custom provider. Delegates to provider_admin."""
        provider_name = str(provider_name or "").strip()
        if not provider_name:
            return {"success": False, "error": "供应商名称不能为空"}
        try:
            from src.llm.provider_admin import delete_provider as _dp
            return _dp(provider_name)
        except Exception as exc:
            return {"success": False, "error": str(exc)}

    # ── 学习中心 ───────────────────────────────────────────────────────────
    @app.get("/api/learning/dashboard")
    def get_learning_dashboard():
        """前端 LearningCenterPanel 面板入口"""
        try:
            from src.learning.dashboard import build_learning_dashboard

            return build_learning_dashboard()
        except Exception as exc:
            return {"available": False, "error": str(exc)}

    @app.post("/api/learning/lessons/apply")
    def apply_lesson(payload: dict[str, Any]):
        lesson_type = str(payload.get("lesson_type", "") or "").strip()
        if not lesson_type:
            return {"success": False, "error": "lesson_type 不能为空"}
        try:
            from src.learning.lesson_learner import apply_lesson as _apply_learning_lesson

            ok = _apply_learning_lesson(lesson_type)
            return {"success": bool(ok), "error": "" if ok else "lesson 不存在或更新失败"}
        except Exception as exc:
            return {"success": False, "error": str(exc)}

    @app.post("/api/learning/lessons/dismiss")
    def dismiss_lesson(payload: dict[str, Any]):
        lesson_type = str(payload.get("lesson_type", "") or "").strip()
        if not lesson_type:
            return {"success": False, "error": "lesson_type 不能为空"}
        try:
            from src.learning.lesson_learner import dismiss_lesson as _dismiss_learning_lesson

            ok = _dismiss_learning_lesson(lesson_type)
            return {"success": bool(ok), "error": "" if ok else "lesson 不存在或更新失败"}
        except Exception as exc:
            return {"success": False, "error": str(exc)}

    # ── 策略市场 & 交互面板 ─────────────────────────────────────────────────
    @app.get("/api/strategy/marketplace")
    def list_marketplace():
        try:
            payload = _build_strategy_marketplace_payload(status_payload=get_status())
            strategies = payload.get("strategies", []) if isinstance(payload.get("strategies", []), list) else []
            by_stage = payload.get("by_stage", {}) if isinstance(payload.get("by_stage", {}), dict) else {}
            return {
                "ok": True,
                "success": True,
                "strategies": strategies,
                "by_stage": by_stage,
                "byStage": by_stage,
            }
        except Exception as exc:
            return {
                "ok": False,
                "success": False,
                "strategies": [],
                "by_stage": {},
                "byStage": {},
                "error": str(exc),
            }

    @app.post("/api/strategy/review")
    def approve_strategy_review(payload: dict[str, Any]):
        strategy_id = str(payload.get("strategy_id", "") or "").strip()
        reviewer = str(payload.get("reviewer", "admin") or "").strip()
        notes = str(payload.get("notes", "") or "").strip()
        if not strategy_id:
            return {"ok": False, "error": "strategy_id 不能为空"}
        from src.strategy_asset_service import approve_review_entry, approve_strategy_market_asset

        with _strategy_market_state_transaction():
            state = _load_strategy_market_state()
            strategies = state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}
            row = strategies.get(strategy_id, {}) if isinstance(strategies.get(strategy_id, {}), dict) else {}
            if not row:
                return {"ok": False, "error": f"未找到策略: {strategy_id}"}
            review_result = approve_review_entry(
                strategy_id,
                reviewer=reviewer or "admin",
                notes=notes,
                source="marketplace_review",
                sandbox_result=(
                    row.get("evaluation", {})
                    if isinstance(row.get("evaluation", {}), dict)
                    else {}
                ),
            )
            state, current_stage = approve_strategy_market_asset(
                state,
                strategy_id=strategy_id,
                reviewer=reviewer or "admin",
                notes=notes,
            )
            _save_strategy_market_state(state)
            _sync_generated_history_entry(strategy_id, row=state["strategies"].get(strategy_id, {}))
        return {
            "ok": True,
            "success": True,
            "from": current_stage,
            "to": "marketplace",
            "reason": f"reviewed by {reviewer or 'admin'}" + (f" | {notes}" if notes else ""),
            "review": review_result,
        }

    @app.post("/api/strategy/review/reject")
    def reject_strategy_review(payload: dict[str, Any]):
        strategy_id = str(payload.get("strategy_id", "") or "").strip()
        reviewer = str(payload.get("reviewer", "admin") or "").strip()
        reason = str(payload.get("reason", "") or payload.get("notes", "") or "").strip()
        if not strategy_id:
            return {"ok": False, "success": False, "error": "strategy_id 不能为空"}
        if not reason:
            return {"ok": False, "success": False, "error": "请填写驳回原因"}
        from src.strategy_asset_service import (
            reject_review_entry,
            reject_strategy_market_asset,
        )

        with _strategy_market_state_transaction():
            state = _load_strategy_market_state()
            strategies = state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}
            row = strategies.get(strategy_id, {}) if isinstance(strategies.get(strategy_id, {}), dict) else {}
            if not row:
                return {"ok": False, "success": False, "error": f"未找到策略: {strategy_id}"}
            review_result = reject_review_entry(
                strategy_id,
                reviewer=reviewer or "admin",
                reason=reason,
                source="marketplace_review",
                sandbox_result=row.get("evaluation", {}) if isinstance(row.get("evaluation", {}), dict) else {},
            )
            state, current_stage = reject_strategy_market_asset(
                state,
                strategy_id=strategy_id,
                reviewer=reviewer or "admin",
                reason=reason,
            )
            _save_strategy_market_state(state)
            _sync_generated_history_entry(strategy_id, row=state["strategies"].get(strategy_id, {}))
        return {
            "ok": True,
            "success": True,
            "from": current_stage,
            "to": "marketplace",
            "reason": reason,
            "review": review_result,
        }

    @app.post("/api/strategy/review/request-revision")
    def request_strategy_review_revision(payload: dict[str, Any]):
        strategy_id = str(payload.get("strategy_id", "") or "").strip()
        reviewer = str(payload.get("reviewer", "admin") or "").strip()
        feedback = str(payload.get("feedback", "") or payload.get("notes", "") or "").strip()
        if not strategy_id:
            return {"ok": False, "success": False, "error": "strategy_id 不能为空"}
        if not feedback:
            return {"ok": False, "success": False, "error": "请填写修改意见"}
        from src.strategy_asset_service import (
            request_review_revision,
            request_strategy_market_revision,
        )

        with _strategy_market_state_transaction():
            state = _load_strategy_market_state()
            strategies = state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}
            row = strategies.get(strategy_id, {}) if isinstance(strategies.get(strategy_id, {}), dict) else {}
            if not row:
                return {"ok": False, "success": False, "error": f"未找到策略: {strategy_id}"}
            review_result = request_review_revision(
                strategy_id,
                reviewer=reviewer or "admin",
                feedback=feedback,
                source="marketplace_review",
                sandbox_result=row.get("evaluation", {}) if isinstance(row.get("evaluation", {}), dict) else {},
            )
            state, current_stage = request_strategy_market_revision(
                state,
                strategy_id=strategy_id,
                reviewer=reviewer or "admin",
                feedback=feedback,
            )
            _save_strategy_market_state(state)
            _sync_generated_history_entry(strategy_id, row=state["strategies"].get(strategy_id, {}))
        return {
            "ok": True,
            "success": True,
            "from": current_stage,
            "to": "marketplace",
            "reason": feedback,
            "review": review_result,
        }

    @app.post("/api/strategy/review/withdraw")
    def withdraw_strategy_review(payload: dict[str, Any]):
        strategy_id = str(payload.get("strategy_id", "") or "").strip()
        reviewer = str(payload.get("reviewer", "admin") or "").strip()
        notes = str(payload.get("notes", "") or "已撤回到待审核" or "").strip()
        if not strategy_id:
            return {"ok": False, "success": False, "error": "strategy_id 不能为空"}
        from src.strategy_asset_service import (
            withdraw_review_entry,
            withdraw_strategy_market_asset,
        )

        with _strategy_market_state_transaction():
            state = _load_strategy_market_state()
            strategies = state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}
            row = strategies.get(strategy_id, {}) if isinstance(strategies.get(strategy_id, {}), dict) else {}
            if not row:
                return {"ok": False, "success": False, "error": f"未找到策略: {strategy_id}"}
            runtime_instances = state.get("runtime_instances", []) if isinstance(state.get("runtime_instances", []), list) else []
            if any(
                isinstance(inst, dict) and str(inst.get("strategy_id", "") or "").strip() == strategy_id
                for inst in runtime_instances
            ):
                return {"ok": False, "success": False, "error": "已有运行实例的策略不能撤回审核"}
            review_result = withdraw_review_entry(
                strategy_id,
                reviewer=reviewer or "admin",
                notes=notes,
                source="marketplace_review",
                sandbox_result=row.get("evaluation", {}) if isinstance(row.get("evaluation", {}), dict) else {},
            )
            state, current_stage = withdraw_strategy_market_asset(
                state,
                strategy_id=strategy_id,
                reviewer=reviewer or "admin",
                notes=notes,
            )
            _save_strategy_market_state(state)
            _sync_generated_history_entry(strategy_id, row=state["strategies"].get(strategy_id, {}))
        return {
            "ok": True,
            "success": True,
            "from": current_stage,
            "to": "marketplace",
            "reason": notes,
            "review": review_result,
        }

    @app.get("/api/strategy/review/explain")
    def explain_strategy_review(strategy_id: str = ""):
        target_id = str(strategy_id or "").strip()
        if not target_id:
            return {"ok": False, "success": False, "error": "strategy_id 不能为空"}
        from src.strategy_asset_service import build_strategy_review_explanation

        state = _load_strategy_market_state()
        try:
            explanation = build_strategy_review_explanation(
                state,
                strategy_id=target_id,
            )
        except KeyError as exc:
            return {"ok": False, "success": False, "error": str(exc)}
        return {"ok": True, "success": True, "explanation": explanation}

    @app.get("/api/strategy/paper/feedback")
    def get_strategy_paper_feedback(strategy_id: str = ""):
        target_id = str(strategy_id or "").strip()
        if not target_id:
            return {"ok": False, "success": False, "error": "strategy_id 不能为空"}
        try:
            feedback = _build_strategy_paper_feedback(target_id)
        except KeyError as exc:
            return {"ok": False, "success": False, "error": str(exc)}
        return {"ok": True, "success": True, "feedback": feedback}

    @app.post("/api/strategy/promote-to-paper")
    def promote_to_paper(payload: dict[str, Any]):
        strategy_id = str(payload.get("strategy_id", "") or "").strip()
        reason = str(payload.get("reason", "manual_deploy") or "").strip()
        if not strategy_id:
            return {"ok": False, "error": "strategy_id 不能为空"}
        from src.strategy_asset_service import (
            is_review_approved,
            promote_strategy_market_asset_to_paper,
        )

        with _strategy_market_state_transaction():
            state = _load_strategy_market_state()
            strategies = state.get("strategies", {}) if isinstance(state.get("strategies", {}), dict) else {}
            row = strategies.get(strategy_id, {}) if isinstance(strategies.get(strategy_id, {}), dict) else {}
            if not row:
                return {"ok": False, "error": f"未找到策略: {strategy_id}"}
            if (bool(row.get("is_custom", False)) or strategy_id not in VALID_STRATEGIES) and not is_review_approved(strategy_id):
                return {
                    "ok": False,
                    "success": False,
                    "error": "该策略尚未通过审核，不能晋级到 paper",
                }
            state, current_stage = promote_strategy_market_asset_to_paper(
                state,
                strategy_id=strategy_id,
            )
            _save_strategy_market_state(state)
            _sync_generated_history_entry(strategy_id, row=state["strategies"].get(strategy_id, {}))
        return {"ok": True, "success": True, "from": current_stage, "to": "paper", "reason": reason}

    @app.get("/api/strategy/interactive/state")
    def get_interactive_strategy_state():
        return _interactive_state_payload(_load_interactive_state_with_fallback())

    @app.post("/api/strategy/interactive/generate")
    def generate_interactive_strategy(req: InteractiveStrategyGenerateRequest):
        return _generate_strategy_from_interactive_state(
            temp_model=str(req.temp_model or ""),
            compile_target=str(req.compile_target or "template"),
        )

    @app.websocket("/ws/strategy-interactive")
    async def strategy_interactive_ws(websocket: WebSocket):
        await websocket.accept()
        try:
            await websocket.send_text(
                json.dumps(
                    {
                        "type": "connected",
                        "transport": "websocket",
                        "ts": time.time(),
                    },
                    ensure_ascii=False,
                )
            )
            await websocket.send_text(
                json.dumps(
                    {
                        "type": "snapshot",
                        **_interactive_state_payload(_load_interactive_state_with_fallback()),
                        "transport": "websocket",
                        "ts": time.time(),
                    },
                    ensure_ascii=False,
                )
            )
            while True:
                try:
                    raw = await asyncio.wait_for(websocket.receive_text(), timeout=25.0)
                except asyncio.TimeoutError:
                    await websocket.send_text(
                        json.dumps({"type": "ping", "ts": time.time()}, ensure_ascii=False)
                    )
                    continue

                try:
                    message = json.loads(raw)
                except Exception:
                    await websocket.send_text(
                        json.dumps(
                            {
                                "type": "error",
                                "error": "invalid_json",
                                "message": "消息格式不正确",
                                "ts": time.time(),
                            },
                            ensure_ascii=False,
                        )
                    )
                    continue

                msg_type = str(message.get("type", "") or "").strip().lower()
                if msg_type == "ping":
                    await websocket.send_text(
                        json.dumps({"type": "pong", "ts": time.time()}, ensure_ascii=False)
                    )
                    continue
                if msg_type == "reset":
                    await asyncio.to_thread(_reset_interactive_state)
                    await websocket.send_text(
                        json.dumps(
                            {
                                "type": "snapshot",
                                **_interactive_state_payload({}),
                                "transport": "websocket",
                                "ts": time.time(),
                            },
                            ensure_ascii=False,
                        )
                    )
                    continue
                if msg_type != "user_message":
                    await websocket.send_text(
                        json.dumps(
                            {
                                "type": "error",
                                "error": "unsupported_type",
                                "message": f"不支持的消息类型: {msg_type or 'unknown'}",
                                "ts": time.time(),
                            },
                            ensure_ascii=False,
                        )
                    )
                    continue

                content = str(message.get("content", "") or "")
                request_id = str(message.get("request_id", "") or uuid.uuid4().hex[:10])
                message_kind = _interactive_message_kind(
                    str(message.get("message_kind", "") or ""),
                    _interactive_message_kind(content),
                )
                message_meta = (
                    message.get("meta", {})
                    if isinstance(message.get("meta", {}), dict)
                    else {}
                )
                await websocket.send_text(
                    json.dumps(
                        {
                            "type": "ack",
                            "request_id": request_id,
                            "conversation_id": str(message.get("conversation_id", "") or ""),
                            "message_kind": message_kind,
                            "ts": time.time(),
                        },
                        ensure_ascii=False,
                    )
                )
                result = await asyncio.to_thread(
                    _process_strategy_builder_turn,
                    user_text=content,
                    conversation_id=str(message.get("conversation_id", "") or ""),
                    message_kind=message_kind,
                    message_meta=message_meta,
                )
                await websocket.send_text(
                    json.dumps(
                        {
                            "type": "assistant_message",
                            "request_id": request_id,
                            "transport": "websocket",
                            "ts": time.time(),
                            **result,
                        },
                        ensure_ascii=False,
                    )
                )
        except WebSocketDisconnect:
            return
        except Exception:
            try:
                await websocket.close()
            except Exception:
                pass

    @app.post("/api/strategy/interactive/reset")
    def reset_interactive_strategy():
        _reset_interactive_state()
        return {"success": True}

    # 处理 React Router 的前端路由及 public 目录下的静态文件
    @app.get("/{catchall:path}")
    def serve_frontend_fallback(catchall: str):
        if not catchall.startswith("api/"):
            # 如果请求的是真实存在的文件(如 /office_bg.png),则直接返回该文件
            file_path = FRONTEND_DIST / catchall
            if _is_readable_file(file_path):
                return FileResponse(str(file_path))
            # 否则返回 index.html 交给前端路由处理
            return _safe_frontend_file_response(FRONTEND_DIST / "index.html")


# =======================================

def main():
    import uvicorn
    import multiprocessing
    import sys


    # PyInstaller child-worker argv uses --multiprocessing-* markers.
    # The main GUI process must not enter freeze_support() on cold start.
    if getattr(sys, 'frozen', False) and any(
        str(arg or "").startswith("--multiprocessing-") for arg in sys.argv[1:]
    ):
        multiprocessing.freeze_support()

    # Auto-mode: launched by the main GUI binary as a subprocess (daemon / macro / train).
    # The main GUI process passes --auto so we know we're in a spawned subprocess context.
    _is_auto_spawned = "--auto" in sys.argv

    # Watchdog 模式：作为守护进程监控 engine 实例，崩溃后自动重启
    if "--watchdog" in sys.argv:
        _watchdog_main()
        sys.exit(0)

    # engine-instance 模式：由 watchdog 启动的真正 engine 进程，剥离标记后正常启动
    if "--engine-instance" in sys.argv:
        sys.argv = [a for a in sys.argv if a != "--engine-instance"]

    # advisor 模式：独立对话窗口，直接启动 Claude Advisor 交互式 CLI
    if "--advisor" in sys.argv:
        import json as _json
        from src.claude_advisor import ClaudeAdvisor
        import textwrap
        advisor = ClaudeAdvisor()
        print(textwrap.dedent(f"""\
        ╔══════════════════════════════════════════════╗
        ║      GoldAutoTrade Claude Advisor CLI        ║
        ║  智能大脑独立对话模式                         ║
        ╠══════════════════════════════════════════════╣
        ║  LLM: {advisor._llm.get('model','auto')[:30]:30s} ║
        ║  命令: status / monitor / schedule / repair   ║
        ║        ask <问题> / help / quit               ║
        ╚══════════════════════════════════════════════╝
        """))
        while True:
            try:
                cmd = input("\nAdvisor> ").strip()
            except (EOFError, KeyboardInterrupt):
                print("\n退出")
                break
            if not cmd:
                continue
            if cmd == "quit" or cmd == "exit":
                break
            elif cmd == "status" or cmd == "s":
                s = advisor._state
                print(f"  运行次数: {advisor._tick_count}")
                print(f"  异常检出: {s.get('anomalies_detected',0)}")
                print(f"  修复执行: {s.get('repairs_done',0)}")
                print(f"  连续错误: {s.get('consecutive_errors',0)}")
                print(f"  LLM端点:  {advisor._llm.get('model','?')}")
            elif cmd == "monitor" or cmd == "m":
                print("  正在采集全维度指标...")
                metrics = advisor._collect_metrics()
                anomalies = advisor._detect_anomalies(metrics)
                print(f"  数据源: {list(metrics.get('sources',{}).keys())}")
                print(f"  交易统计: {metrics.get('trading',{})}")
                print(f"  方向状态: {metrics.get('macro',{})}")
                if anomalies:
                    print(f"  异常 ({len(anomalies)}):")
                    for a in anomalies:
                        print(f"    [{a['severity']}] {a['type']}: {a.get('hint','')}")
                else:
                    print("  无异常")
                # LLM deep analysis
                print("  正在调用 LLM 深度分析...")
                prompt = f"系统指标:\n{_json.dumps(metrics, indent=2, ensure_ascii=False)}\n\n异常:\n{_json.dumps(anomalies, indent=2, ensure_ascii=False)}\n\n请分析。"
                try:
                    analysis = advisor._call_llm(prompt, "monitor", max_tokens=1200)
                    print(f"\n  --- LLM 分析 ---\n{analysis}\n  ---")
                except Exception as e:
                    print(f"  LLM调用失败: {e}")
            elif cmd == "schedule" or cmd == "sc":
                print("  正在分析策略权重...")
                try:
                    result = advisor.schedule_cycle()
                    changes = result.get("weight_changes", {})
                    if changes:
                        print(f"  已调整权重: {json.dumps(changes, indent=2, ensure_ascii=False)}")
                    else:
                        print("  无需调整")
                except Exception as e:
                    print(f"  调度失败: {e}")
            elif cmd == "repair" or cmd == "r":
                print("  正在扫描严重异常...")
                metrics = advisor._collect_metrics()
                anomalies = advisor._detect_anomalies(metrics)
                critical = [a for a in anomalies if a.get("severity") == "CRITICAL"]
                if not critical:
                    print("  无严重异常，无需修复")
                else:
                    for a in critical:
                        print(f"  修复: {a['type']}...")
                        result = advisor.repair_cycle(a)
                        print(f"  结果: {result.get('action','unknown')}")
            elif cmd.startswith("ask "):
                question = cmd[4:]
                print(f"  正在向 LLM 查询: {question}")
                try:
                    answer = advisor._call_llm(
                        f"用户问题: {question}\n\n当前系统状态:\n{json.dumps(advisor._collect_metrics(), indent=2, ensure_ascii=False)}",
                        "monitor", max_tokens=1500)
                    print(f"\n{answer}\n")
                except Exception as e:
                    print(f"  查询失败: {e}")
            elif cmd == "help" or cmd == "h":
                print("  status    — 查看Advisor运行状态")
                print("  monitor   — 全维度监测 + LLM深度分析")
                print("  schedule  — 策略权重调度")
                print("  repair    — 自动修复严重异常")
                print("  ask <问题> — 向LLM自由提问（携带系统实时数据）")
                print("  quit      — 退出")
            else:
                print(f"  未知命令: {cmd} (输入 help 查看可用命令)")
        sys.exit(0)

    # 拦截被 subprocess 拉起的实盘守护命令（统一入口为 pipeline）
    if "--daemon-pipeline" in sys.argv:
        if getattr(sys, 'frozen', False):
            # Frozen mode: import and call pipeline.main() directly instead of
            # spawning a subprocess — no "python3" binary, no src/ dir on disk.
            # Construct argv so argparse inside pipeline.main() works correctly.
            from pathlib import Path
            # Use config_manager.get_app_dir() for writable data paths
            # (resolves to %APPDATA%/GoldAutoTrade in frozen mode),
            # NOT Path(__file__).parent which points to the read-only EXE dir.
            import config_manager as _cm
            app_dir = _cm.get_app_dir()
            # Ensure data inbox/outbox directories exist
            (app_dir / "data" / "inbox").mkdir(parents=True, exist_ok=True)
            (app_dir / "data" / "outbox").mkdir(parents=True, exist_ok=True)
            pipeline_args = _build_live_pipeline_cmd(app_dir)
            argv_orig = list(sys.argv)
            sys.argv = [str(Path(__file__).resolve().parent / "src" / "pipeline.py")] + pipeline_args[2:]
            try:
                from src import pipeline as _pipeline
                _pipeline.main()
            finally:
                sys.argv = argv_orig
        else:
            import subprocess
            from pathlib import Path
            app_dir = Path(__file__).resolve().parent
            cmd = _build_live_pipeline_cmd(app_dir)
            env = os.environ.copy()
            existing_pythonpath = str(env.get("PYTHONPATH", "") or "").strip()
            app_dir_str = str(app_dir)
            if existing_pythonpath:
                env["PYTHONPATH"] = f"{app_dir_str}:{existing_pythonpath}"
            else:
                env["PYTHONPATH"] = app_dir_str

            popen_kwargs = {"env": env}
            if sys.platform != "win32":
                popen_kwargs["start_new_session"] = True
            proc = _popen_safe(cmd, **popen_kwargs)
            try:
                PID_FILE.write_text(str(proc.pid), encoding="utf-8")
            except Exception:
                pass
        sys.exit(0)

    if "--run-macro" in sys.argv:
        from src.deepseek_agent import run_macro_analysis
        pid = os.getpid()
        force_refresh = os.environ.get("FORCE_MACRO_REFRESH", "0") == "1"
        # Auto-detect cold_start: if no previous blackboard direction exists
        trigger_reason = "cold_start"
        try:
            from src.data_layer import blackboard_state_path
            bb_path = blackboard_state_path()
            if bb_path and bb_path.exists():
                bb = json.loads(bb_path.read_text(encoding="utf-8"))
                mc = bb.get("Macro_Commander", {}) if isinstance(bb, dict) else {}
                if mc.get("bias") and mc.get("status") == "DONE":
                    trigger_reason = "hourly"
        except Exception:
            pass
        try:
            run_macro_analysis(force_refresh=force_refresh, trigger_reason=trigger_reason)
            _finish_macro_refresh_status(
                "completed",
                "宏观方向刷新完成",
                pid=pid,
                force=force_refresh,
            )
        except Exception as exc:
            _finish_macro_refresh_status(
                "failed",
                f"宏观方向刷新失败: {exc}",
                pid=pid,
                force=force_refresh,
            )
            raise
        sys.exit(0)
        
    if "--run-notify" in sys.argv:
        from bridge.review_notify import run as notify_run, run_text_notify
        if "--text" in sys.argv:
            idx = sys.argv.index("--text")
            msg = " ".join(sys.argv[idx+1:])
            run_text_notify(msg)
        else:
            notify_run()
        sys.exit(0)
        
    if "--run-briefing" in sys.argv:
        from bridge.morning_briefing import generate_morning_briefing
        generate_morning_briefing()
        sys.exit(0)
        
    if "--run-evo" in sys.argv:
        from src.strategy_evolution import main as evo_main
        evo_main()
        sys.exit(0)

    # 拦截被 subprocess 拉起的 --model 命令（训练进化 API）
    if "--model" in sys.argv:
        from src.strategy_evolution import main as evo_main
        evo_main()
        sys.exit(0)

    # 拦截被 subprocess 拉起的 --train 命令
    if "--train" in sys.argv:
        from src.offline_train import main as train_main
        # 移除前面的参数,保留 argparse 需要的
        idx = sys.argv.index("--train")
        sys.argv = [sys.argv[0]] + sys.argv[idx+1:]
        train_main()
        sys.exit(0)

    # 拦截被 subprocess 拉起的 --analyze 命令
    if "--analyze" in sys.argv:
        from src.strategy_evolution import run_analyze_only
        run_analyze_only()
        sys.exit(0)

    # ── Activate License (CLI / Start Menu shortcut) ────────────────────
    # 用法: GoldAutoTradeEngine.exe --activate-license [--key GAT-XXXX-...]
    if "--activate-license" in sys.argv:
        activate_key = None
        try:
            idx = sys.argv.index("--key")
            activate_key = sys.argv[idx + 1]
        except (ValueError, IndexError):
            pass

        if activate_key:
            from src.license.activate import activate as do_activate, DEFAULT_SERVER
            rc = do_activate(activate_key, DEFAULT_SERVER)
            print(f"\n激活{'成功' if rc == 0 else '失败'} (exit code: {rc})")
            print("按回车键关闭...")
            input()
            sys.exit(rc)
        else:
            # No key provided → start server and open browser to activation page
            _OPEN_ACTIVATE_PAGE = True
    else:
        _OPEN_ACTIVATE_PAGE = False

    # CRITICAL: acquire singleton lock BEFORE health checks and heavy operations.
    # The launcher's early singleton check protects the launcher path, but
    # daemon subprocesses (--engine-instance, etc.) call gui_backend.main()
    # directly through _is_daemon_subprocess(), bypassing the launcher guard.
    # We MUST claim the lock before spending 0.5-3s on health checks and
    # MT5 directory scanning — otherwise a second engine-instance could race in.
    _acquire_gui_singleton_lock()
    atexit.register(_release_gui_singleton_lock)

    _append_startup_trace("main_enter", argv=list(sys.argv))

    # ── Health check — always runs, writes to visible log ───────────
    try:
        from src.health_check_startup import check_func, FIX_GUIDE
        import datetime as _dt, pathlib as _pl
        health_log = _pl.Path(os.environ.get("APPDATA", ".")) / "GoldAutoTrade" / "logs" / "health_check.log"
        health_log.parent.mkdir(parents=True, exist_ok=True)
        issues = []
        for name, fn in check_func.items():
            try:
                if not fn():
                    issues.append(f"[FAIL] {name}: {FIX_GUIDE.get(name, '')}")
            except Exception as e2:
                issues.append(f"[ERR] {name}: {e2}")
        now_iso = _dt.datetime.now().isoformat()
        if issues:
            report = f"Health Check {now_iso}\n" + "\n".join(issues)
        else:
            report = f"Health Check {now_iso}: ALL OK"
        health_log.write_text(report, encoding="utf-8")
        _append_startup_trace("health_check_done", issues=issues[:8], ok=not bool(issues))
        print(report)
    except Exception as e3:
        _append_startup_trace("health_check_failed", error=str(e3)[:500])
        print(f"[HEALTH_CHECK] startup check failed: {e3}")
    # ── End health check ─────────────────────────────────────────────

    # ── License gate — truly non-blocking: always start GUI ──────────
    _LICENSE_OK = False
    _LICENSE_ERROR = ""
    _start_startup_license_check()

    # 为了打包后能正常找到静态文件或绑定端口
    os.environ["PATH"] += os.pathsep + os.path.dirname(sys.executable)
    # 将默认 host 从 127.0.0.1 改为 0.0.0.0，彻底解决 Mac/Windows 网络堆栈限制导致的 Not Found 问题
    host = os.getenv("GUI_HOST", "0.0.0.0")
    port = int(os.getenv("GUI_PORT", "18790"))
    _append_startup_trace("pre_lock", host=host, port=port)
    # (singleton lock already acquired earlier, before health checks)
    _append_startup_trace("lock_acquired", host=host, port=port)

    # 去掉 reload=True,因为它会导致在有些环境中被外部信号误杀

    # 自动打开浏览器 (macOS: 优先用 open 命令通过系统默认浏览器打开)
    def open_browser():
        import webbrowser
        import subprocess
        url = f"http://127.0.0.1:{port}"
        if _OPEN_ACTIVATE_PAGE:
            url += "?activate=1"
        # 冷启动会先跑门禁和后台拉起，浏览器不能比本地页更早打开。
        for _ in range(450):  # 最多等 45 秒
            try:
                import socket
                with socket.create_connection((host, port), timeout=0.5):
                    pass
                break
            except Exception:
                time.sleep(0.1)
        try:
            # macOS: 用 open 命令唤起系统默认浏览器（支持 Chrome/Safari/Waterfox 等）
            if sys.platform == "darwin":
                subprocess.run(["open", url], capture_output=True, timeout=10)
            else:
                webbrowser.open(url)
        except Exception:
            pass

    import threading
    if not _ENGINE_INSTANCE_MODE:
        threading.Thread(target=open_browser, daemon=True).start()

    _append_startup_trace("uvicorn_run_begin", host=host, port=port)
    try:
        uvicorn.run(app, host=host, port=port, access_log=False, log_config=None)
    except Exception as exc:
        _append_startup_trace("uvicorn_run_exception", error=repr(exc)[:800])
        raise

if __name__ == "__main__":
    main()
