#!/usr/bin/env python3
"""
GoldRiskManager - 风险管理层
职责：接收 Signal Layer 输出 → 执行单层总仲裁 → 输出风控决策

仲裁优先级（从上到下，高优先级拦截后不再检查低优先级）：
  1. circuit_breaker      - 熔断机制（连亏/极端行情）
  2. daily_loss_limit     - 日亏损上限
  3. contextual_arbiter   - 方向/时段/结构总仲裁
  4. performance_gate     - 表现因子门控
  5. spread_filter        - 点差阈值
  6. position_limit       - 仓位约束
  7. confidence_threshold - 信心阈值

契约版本：1.4.0
"""
import sys
import pathlib
import json
import time
import sqlite3
from typing import Optional

# ============ 路径配置（与 gold_trader.py 一致）============
# ROOT now resolved via frozen_path (PyInstaller-safe)
from frozen_path import APP_ROOT as ROOT
sys.path.insert(0, str(ROOT))
import config_manager
try:
    from src.market_context import (
        is_defensive_regime,
        is_range_regime,
        is_trend_regime,
        normalize_session_bucket,
        normalize_regime_label,
    )
except Exception:
    def normalize_session_bucket(value):
        return str(value or "NORMAL").upper()

    def normalize_regime_label(value):
        return str(value or "UNKNOWN").upper()

    def is_trend_regime(value):
        return normalize_regime_label(value) in {"STRONG_TREND", "WEAK_TREND", "STEALTH_TREND"}

    def is_range_regime(value):
        return normalize_regime_label(value) == "RANGING"

    def is_defensive_regime(value):
        return normalize_regime_label(value) in {"TRANSITION", "VOLATILITY_SPIKE", "FAKE_TREND", "EXTREME"}

DATA_DIR = config_manager.get_data_dir()
LOGS_DIR = config_manager.get_logs_dir()
DB_PATH = DATA_DIR / "bridge" / "state" / "agent_state.db"

# ============ 参数缓存（双通道：effective > suggested）============
_PARAMS_CACHE = None
_PARAMS_CACHE_TTL = 300  # 5分钟 (可通过 strategy_sync 配置调整)


def _load_params_suggestion():
    """
    加载参数（双通道：effective_params.json > params_suggestion.json）
    与 ARCHITECTURE.md "参数治理双通道" 设计一致。
    """
    global _PARAMS_CACHE
    now = time.time()
    if _PARAMS_CACHE and (now - _PARAMS_CACHE.get("_loaded_at", 0)) < _PARAMS_CACHE_TTL:
        return _PARAMS_CACHE

    try:
        from src.param_flow import load_runtime_param_payload
        data = load_runtime_param_payload()
    except Exception:
        return {}
    if not data:
        return {}
    data["_loaded_at"] = now
    data["_source"] = data.get("_source_file", "")
    _PARAMS_CACHE = data
    return data


from config_manager import get_effective_param as _get_effective_param


# ============ GARCH 波动率（复用现有模块）============
def _get_vol_multiplier() -> dict:
    """获取 GARCH 波动率乘数"""
    try:
        _bp = str(ROOT / "bridge")
        if _bp not in sys.path:
            sys.path.insert(0, _bp)
        from garch_volatility import get_vol_multiplier
        return get_vol_multiplier()
    except Exception:
        return {"multiplier": 1.0, "vol_ratio": 1.0, "regime": "UNKNOWN"}


# ============ 交易时段判断（复用 session_analyzer）============
def _get_session_info() -> dict:
    """获取当前时段信息"""
    try:
        _bp = str(ROOT / "bridge")
        if _bp not in sys.path:
            sys.path.insert(0, _bp)
        from session_analyzer import analyze_all_sessions, get_strategy_recommendation
        analysis = analyze_all_sessions()
        rec = get_strategy_recommendation(analysis)
        current_analysis = analysis.get("current_analysis", {}) if isinstance(analysis, dict) else {}
        session_profile = current_analysis.get("session_profile", analysis.get("current_session_profile", {})) if isinstance(analysis, dict) else {}
        return {
            "session_name": analysis.get("current_session", "UNKNOWN") if isinstance(analysis, dict) else "UNKNOWN",
            "session_label": analysis.get("current_session_name", "未知时段") if isinstance(analysis, dict) else "未知时段",
            "analysis": analysis,
            "recommendation": rec.get("recommendation", "CAUTIOUS") if rec else "CAUTIOUS",
            "current_direction": rec.get("current_direction", "NEUTRAL") if rec else "NEUTRAL",
            "session_mode": rec.get("session_mode", session_profile.get("mode", "NORMAL")) if rec else session_profile.get("mode", "NORMAL"),
            "session_profile": session_profile,
            "entry_style": rec.get("entry_style", session_profile.get("entry_style", "MARKET")) if rec else session_profile.get("entry_style", "MARKET"),
            "risk_multiplier": rec.get("risk_multiplier", session_profile.get("risk_multiplier", 1.0)) if rec else session_profile.get("risk_multiplier", 1.0),
            "reason": rec.get("reason", "") if rec else "",
        }
    except Exception:
        return {
            "session_name": "UNKNOWN",
            "session_label": "未知时段",
            "recommendation": "CAUTIOUS",
            "current_direction": "NEUTRAL",
            "session_mode": "NORMAL",
            "session_profile": {},
            "entry_style": "MARKET",
            "risk_multiplier": 1.0,
            "reason": "",
        }


# ============ 连亏与日亏统计（从 SQLite 读取）============
def _get_daily_stats(state: dict) -> dict:
    """
    从 SQLite state 中提取日级别风险统计
    返回: {consecutive_losses, daily_loss_pct, total_trades}
    """
    return {
        "consecutive_losses": state.get("consecutive_losses", 0),
        "daily_loss_pct": state.get("daily_loss_pct", 0.0),
        "total_trades": state.get("total_trades", 0),
        "last_trade_date": state.get("last_trade_date", ""),
    }


def _load_performance_factors(force_refresh: bool = False) -> dict:
    try:
        from src.live_performance_factors import load_live_performance_factors
        return load_live_performance_factors(force_refresh=force_refresh)
    except Exception:
        return {}


def _load_hermes_runtime_rules(force_refresh: bool = False) -> dict:
    try:
        from src.hermes_bridge import load_runtime_rules
        return load_runtime_rules(force_refresh=force_refresh)
    except Exception:
        return {}


# ============ GoldRiskManager 核心类 =============
class GoldRiskManager:
    """
    风险管理器

    使用方式：
        risk_mgr = GoldRiskManager()
        decision = risk_mgr.evaluate(signal_dict, context_dict)
    """

    SCHEMA_VERSION = "1.4.0"

    # 仲裁优先级（顺序即优先级，从高到低）
    RISK_CHECK_ORDER = [
        "circuit_breaker",
        "daily_loss_limit",
        "session_filter",
        "learned_avoidance",
        "contextual_arbiter",
        "performance_gate",
        "spread_filter",
        "position_limit",
        "confidence_threshold",
    ]

    def __init__(self):
        self._params = None  # 延迟加载

    # ---- 参数获取（延迟加载，缓存5分钟）----
    def _load_params(self) -> dict:
        """加载 effective_params，缓存5分钟"""
        if self._params is None:
            self._params = _load_params_suggestion()
        return self._params

    # ── GUI parameter name mapping (risk_governor_rules.json → internal names) ──
    _PARAM_ALIASES = {
        "max_position_size": ["max_position_size", "max_lots"],
        "max_positions": ["max_open_positions", "max_positions"],
        "max_same_side_positions": ["max_same_side_positions"],
        "min_confidence_threshold": ["min_confidence_threshold", "min_confidence"],
        "max_consecutive_losses": ["max_consecutive_losses"],
        "spread_kill": ["spread_kill"],
    }

    def _get_param(self, name: str, default):
        """从 config_manager（GUI risk_governor_rules.json）读取参数，回退到参数建议系统"""
        # 1. config_manager.get_effective_param() — 读 GUI 配置 + effective_params.json
        aliases = self._PARAM_ALIASES.get(name, [name])
        for alias in aliases:
            val = _get_effective_param(alias, None)
            if val is not None:
                try:
                    result = float(val) if not isinstance(val, (int, float)) else val
                    # min_confidence_threshold 如果以百分比存储（>1），转换为小数
                    if name == "min_confidence_threshold" and result > 1.0:
                        result = result / 100.0
                    return result
                except (TypeError, ValueError):
                    pass
        # 2. _load_params_suggestion — 读 AI 建议系统
        data = self._load_params()
        for sug in data.get("suggestions", []):
            if sug.get("param") == name and sug.get("auto_apply"):
                return sug.get("suggested_value", default)
        return default

    # ---- 6项风控检查（按优先级顺序）----

    def _check_circuit_breaker(self, context: dict) -> tuple:
        """
        优先级1：熔断机制
        检查：kill_switch / 连亏次数
        """
        state = context.get("state", {})
        params = context.get("params", {})

        # 连亏超过上限 — kill_switch 已降级为置信度惩罚，不再硬阻断
        max_consec = int(self._get_param("max_consecutive_losses", 4))
        consecutive_losses = state.get("consecutive_losses", 0)
        if consecutive_losses >= max_consec:
            return False, f"LOSS_LIMIT_consecutive_{consecutive_losses}>={max_consec}"

        return True, None

    def _check_daily_loss_limit(self, context: dict) -> tuple:
        """
        优先级2：日亏损上限
        检查：单日亏损达 2% 禁止新开仓，达 5% 强平所有
        注意：强平逻辑在 Execution Layer，此处只负责拦截新开仓
        """
        state = context.get("state", {})
        daily_loss_pct = abs(state.get("daily_loss_pct", 0.0))  # 存的是负数

        # 日亏损阈值从配置读取，默认 2.0%
        max_daily_loss = float(self._get_param("max_daily_loss_pct", 2.0) or 2.0)
        # Staleness guard: if daily_loss_pct hasn't been refreshed in >600s,
        # the value may be stale from an earlier session; recalculate from recent_closures.
        import time as _t
        _last_pnl_ts = float(state.get("_daily_loss_updated_at", 0) or 0)
        if _t.time() - _last_pnl_ts > 600.0 and daily_loss_pct >= max_daily_loss:
            # Stale block — force recalculate rather than permanently blocking
            try:
                from bridge.gold_trader import load_state as _rls
                _fresh = _rls()
                _fresh_pct = abs(float(_fresh.get("daily_loss_pct", 0.0) or 0.0))
                if _fresh_pct < max_daily_loss:
                    return True, None  # stale value was too high, fresh shows recovery
            except Exception:
                pass
        if daily_loss_pct >= max_daily_loss:
            return False, f"DAILY_LOSS_LIMIT_{daily_loss_pct:.2f}%>={max_daily_loss}%"

        return True, None

    def _check_session_filter(self, context: dict) -> tuple:
        """
        优先级3：动态时区策略 — 自适应而非硬拦截
        根据时段调整仓位大小和信心度门槛，不直接拦截。
        仅在极端条件（周末/硬性禁止）下拦截。
        """
        session_info = context.get("session_info") or _get_session_info()
        session_mode = session_info.get("session_mode", "NORMAL")
        session_profile = session_info.get("session_profile", {})

        # Only hard-block in truly dead sessions (weekend, holiday)
        if session_mode in ("WEEKEND", "HOLIDAY"):
            return False, f"SESSION_HARD_BLOCK_{session_mode}"

        # ASIA_NOISE / ASIA_DEFENSIVE: allow but with session-native adjustments
        # (risk_multiplier, confidence_penalty, LIMIT entry style already applied
        #  by _get_session_info → _calc_risk_params and _build_contextual_policy)
        return True, None

    def _check_structure_guard(self, context: dict, signal: dict) -> tuple:
        interpreter = context.get("interpreter", {})
        if not isinstance(interpreter, dict):
            return True, None
        side = str(signal.get("side", "")).upper()
        shock_direction = str(interpreter.get("shock_direction", "NONE")).upper()
        pullback_phase = str(interpreter.get("pullback_phase", "NONE")).upper()
        counter_trend_risk = str(interpreter.get("counter_trend_risk", "LOW")).upper()
        execution_advice = str(interpreter.get("execution_advice", "ALLOW_FOLLOW")).upper()
        market_structure = str(interpreter.get("market_structure", "UNCERTAIN")).upper()
        dominant_direction = str(interpreter.get("dominant_direction", "MIXED")).upper()
        confirmation_state = str(interpreter.get("confirmation_state", "NONE")).upper()
        pullback_quality = str(interpreter.get("pullback_quality", "UNKNOWN")).upper()
        failed_breakout = str(interpreter.get("failed_breakout", "NONE")).upper()
        liquidity_sweep = str(interpreter.get("liquidity_sweep", "NONE")).upper()
        opposite = (
            (shock_direction == "BULLISH" and side == "SELL")
            or (shock_direction == "BEARISH" and side == "BUY")
        )
        if not opposite:
            return True, None
        if market_structure in ("FAILED_BREAKOUT", "IMPULSE_EXHAUSTION") and confirmation_state != "CONFIRMED":
            return False, f"STRUCTURE_REVERSAL_GUARD_{market_structure}"
        if liquidity_sweep in ("BULLISH", "BEARISH") and execution_advice == "LIMIT_ONLY":
            return False, f"LIQUIDITY_SWEEP_GUARD_{liquidity_sweep}"
        if execution_advice == "BLOCK_COUNTER_TREND" and counter_trend_risk == "HIGH":
            return False, f"STRUCTURE_BLOCK_{shock_direction}_{pullback_phase or 'NONE'}"
        if market_structure in ("FIRST_PULLBACK", "SHOCK_REPRICING") and counter_trend_risk in ("HIGH", "MEDIUM"):
            return False, f"STRUCTURE_GUARD_{shock_direction}_{market_structure}"
        if failed_breakout in ("BULLISH", "BEARISH") and pullback_quality != "HEALTHY":
            return False, f"FAILED_BREAKOUT_GUARD_{failed_breakout}"
        if dominant_direction == shock_direction and counter_trend_risk == "HIGH":
            return False, f"COUNTER_TREND_HIGH_RISK_{shock_direction}"
        return True, None

    def _check_learned_avoidance(self, context: dict) -> tuple:
        """
        优先级2.5：学习避让
        检查：learning_brain 中的 loss_lessons 是否匹配当前信号特征
        """
        try:
            from src.learning.lesson_learner import get_active_avoidance_rules
            rules = get_active_avoidance_rules()
            if not rules:
                return True, None

            signal_ctx = context.get("signal_indicators", context)
            side = str(signal_ctx.get("side", "") or "").upper()
            regime = str(context.get("selection_regime", "") or "").upper()
            session = str(context.get("session", "") or "").upper()
            confidence = float(context.get("confidence", 0) or 0)
            agent_dir = str(context.get("agent_direction", "") or "").upper()

            for rule in rules:
                try:
                    conditions = __import__("json").loads(str(rule.get("conditions", "[]")))
                except Exception:
                    continue
                matches = 0
                for c in conditions:
                    feat = str(c.get("feature", ""))
                    op = str(c.get("op", "="))
                    val = c.get("value")

                    current_val = None
                    if feat == "side":
                        current_val = side
                    elif feat == "regime":
                        current_val = regime
                    elif feat == "session":
                        current_val = session
                    elif feat == "agent_aligned":
                        aligned = (side == "BUY" and agent_dir == "LONG") or (side == "SELL" and agent_dir == "SHORT")
                        current_val = 1 if aligned else 0
                    elif feat == "confidence":
                        current_val = confidence
                    elif feat == "agent_direction":
                        current_val = agent_dir

                    if current_val is None:
                        continue

                    if op == "=":
                        if str(current_val) == str(val):
                            matches += 1
                    elif op == ">=":
                        if float(current_val) >= float(val):
                            matches += 1
                    elif op == "<":
                        if float(current_val) < float(val):
                            matches += 1

                if matches == len(conditions):
                    action = str(rule.get("action", ""))
                    return False, f"LEARNED_AVOIDANCE:{rule.get('lesson_type','')}:{action}"

            return True, None
        except Exception:
            return True, None

    def _check_spread_filter(self, context: dict) -> tuple:
        """
        优先级4：点差阈值
        检查：当前点差超过上限则拦截
        """
        spread = context.get("spread")
        if spread is None:
            return False, "SPREAD_UNAVAILABLE"
        spread_kill = float(self._get_param("spread_kill", 70))

        if float(spread) > spread_kill:
            return False, f"WIDE_SPREAD_{spread:.1f}>{spread_kill}"

        return True, None

    def _check_performance_gate(self, context: dict) -> tuple:
        performance_profile = context.get("performance_profile", {})
        if not isinstance(performance_profile, dict):
            return True, None
        if bool(performance_profile.get("block", False)):
            return False, f"PERF_GATE_{performance_profile.get('block_reason', 'underperforming')}"
        return True, None

    def _check_position_limit(self, context: dict, signal: dict) -> tuple:
        """
        优先级5：仓位约束
        检查：持仓数 / 持仓上限 / GARCH波动率调整
        """
        state = context.get("state", {})
        params = context.get("params", {})

        # 已有持仓数 — fallback to position_open flag if positions_all missing
        positions_all = state.get("positions_all", [])
        current_positions = len(positions_all)
        if current_positions == 0 and bool(state.get("position_open", False)):
            current_positions = 1  # Unknown count but position is open
        max_positions = int(self._get_param("max_positions", 3) or 3)
        if current_positions >= max_positions:
            return False, f"MAX_POSITIONS_{current_positions}>={max_positions}"

        # 最大手数限制
        max_lots = float(self._get_param("max_position_size", 0.20))

        # GARCH 波动率动态调整
        vol_info = _get_vol_multiplier()
        vol_mult = vol_info.get("multiplier", 1.0)
        vol_regime = vol_info.get("regime", "UNKNOWN")

        if vol_mult != 1.0:
            max_lots = round(max_lots * vol_mult, 4)

        # 信号手数不能超过上限
        sig_vol = signal.get("volume_lots", 0.10)
        if sig_vol > max_lots:
            # 注意：这里不拦截，只记录，最终手数以 max_lots 为准
            pass

        return True, None

    def _check_confidence_threshold(self, context: dict, confidence: float) -> tuple:
        """
        优先级6：信号置信度阈值
        检查：信心低于最低门槛时建议拦截
        """
        threshold_profile = context.get("confidence_threshold_profile") or self._get_dynamic_confidence_threshold(context)
        min_conf = float(threshold_profile.get("target", 0.80))
        hard_floor = float(threshold_profile.get("hard_floor", min_conf))
        threshold_meta = str(threshold_profile.get("meta", "") or "")

        if confidence < hard_floor:
            return False, (
                f"LOW_CONFIDENCE_{confidence:.2f}<{hard_floor:.2f}"
                f"[{threshold_meta}]"
            )

        return True, None

    def _get_dynamic_confidence_threshold(self, context: dict) -> dict:
        base_conf = float(self._get_param("min_confidence_threshold", 0.80))
        target = base_conf
        parts = [f"base={base_conf:.2f}"]

        policy = context.get("contextual_policy", {})
        if not isinstance(policy, dict):
            policy = {}
        session_info = context.get("session_info", {})
        if not isinstance(session_info, dict):
            session_info = {}
        interpreter = context.get("interpreter", {})
        if not isinstance(interpreter, dict):
            interpreter = {}
        runtime_rules = context.get("hermes_runtime_rules", {})
        if not isinstance(runtime_rules, dict):
            runtime_rules = {}

        score = int(policy.get("score", 0) or 0)
        if score > 0:
            discount = min(0.15, score * 0.04)
            target -= discount
            parts.append(f"score-{discount:.2f}")

        selection_regime = normalize_regime_label(
            policy.get("selection_regime", context.get("selection_regime", "UNKNOWN")) or "UNKNOWN"
        )
        if is_trend_regime(selection_regime):
            target -= 0.06
            parts.append(f"regime={selection_regime}-0.06")
        elif is_range_regime(selection_regime) or is_defensive_regime(selection_regime):
            target += 0.12
            parts.append(f"regime={selection_regime}+0.12")

        advisories = policy.get("advisories", [])
        if isinstance(advisories, list) and any(str(item).startswith("agent_bias_aligned:") for item in advisories):
            target -= 0.02
            parts.append("agent_aligned-0.02")

        session_mode = str(policy.get("session_mode", session_info.get("session_mode", "NORMAL")) or "NORMAL").upper()
        session_bucket = normalize_session_bucket(session_mode)
        session_label = str(session_info.get("session_label", "") or "")
        if session_mode == "NORMAL" and ("欧美重叠" in session_label or "LONDON" in session_label or "NY" in session_label):
            target -= 0.03
            parts.append("session_overlap-0.03")
        elif session_bucket == "ASIA":
            target += 0.03
            parts.append("session_asia_defense+0.03")

        indicators = context.get("signal_indicators", {})
        if not isinstance(indicators, dict):
            indicators = {}
        adx_val = float(indicators.get("adx", 0.0) or 0.0)
        trend_strength = str(indicators.get("trend_strength", "") or "").upper()
        if adx_val >= 30 or is_trend_regime(trend_strength):
            target -= 0.03
            parts.append("trend_strength-0.03")

        execution_advice = str(interpreter.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper()
        if execution_advice == "BLOCK_COUNTER_TREND":
            target += 0.03
            parts.append("structure_block+0.03")

        session_name = str((context.get("session_info", {}) or {}).get("session_name", "UNKNOWN") or "UNKNOWN").upper()
        session_rule = (
            (runtime_rules.get("session_rules", {}) or {}).get(session_name, {})
            if isinstance(runtime_rules.get("session_rules", {}), dict)
            else {}
        )
        if isinstance(session_rule, dict) and session_rule:
            delta = float(session_rule.get("confidence_delta", 0.0) or 0.0)
            target -= delta
            parts.append(f"hermes_session={session_name}:{delta:+.2f}")

        regime_rule = (
            (runtime_rules.get("regime_rules", {}) or {}).get(selection_regime, {})
            if isinstance(runtime_rules.get("regime_rules", {}), dict)
            else {}
        )
        if isinstance(regime_rule, dict) and regime_rule:
            delta = float(regime_rule.get("confidence_floor_delta", 0.0) or 0.0)
            target -= delta
            parts.append(f"hermes_regime={selection_regime}:{delta:+.2f}")

        target = max(0.52, min(0.95, target))
        conservative_regime = is_range_regime(selection_regime) or is_defensive_regime(selection_regime)
        hard_floor = max(0.68 if conservative_regime else 0.45, target - (0.10 if conservative_regime else 0.08))
        soft_size_multiplier = 0.55 if conservative_regime else 0.75
        parts.append(f"target={target:.2f}")
        parts.append(f"floor={hard_floor:.2f}")
        return {
            "target": round(target, 4),
            "hard_floor": round(hard_floor, 4),
            "soft_size_multiplier": soft_size_multiplier,
            "meta": " | ".join(parts),
        }

    def _build_contextual_policy(self, context: dict, signal: dict, confidence: float) -> dict:
        session_info = context.get("session_info") or _get_session_info()
        session_profile = (
            session_info.get("session_profile", {})
            if isinstance(session_info.get("session_profile", {}), dict)
            else {}
        )
        interpreter = context.get("interpreter", {})
        if not isinstance(interpreter, dict):
            interpreter = {}
        runtime_rules = context.get("hermes_runtime_rules", {})
        if not isinstance(runtime_rules, dict):
            runtime_rules = {}

        side = str(signal.get("side", "") or "").upper()
        regime = normalize_regime_label(signal.get("regime", signal.get("indicators", {}).get("regime", "UNKNOWN")) or "UNKNOWN")
        agent_direction = str(context.get("agent_direction", "NEUTRAL") or "NEUTRAL").upper()
        selection_regime = normalize_regime_label(context.get("selection_regime", regime) or regime)
        selected_strategy = str(context.get("selected_strategy", signal.get("strategy_id", "UNKNOWN")) or "UNKNOWN")
        consecutive_losses = int((context.get("state") or {}).get("consecutive_losses", 0) or 0)

        session_mode = str(
            session_info.get("session_mode", session_profile.get("mode", "NORMAL")) or "NORMAL"
        )
        session_bucket = normalize_session_bucket(session_mode)
        entry_style = str(session_info.get("entry_style", session_profile.get("entry_style", "MARKET")) or "MARKET").upper()
        recommendation = str(session_info.get("recommendation", "CAUTIOUS") or "CAUTIOUS").upper()

        score = 0
        reasons: list[str] = []
        advisories: list[str] = []
        hard_blockers: list[str] = []
        limit_only = entry_style == "LIMIT"

        if is_defensive_regime(selection_regime) and selected_strategy not in {"composite", "composite_v1", "rsi_extreme", "boll_band", "key_level", "grid_osc"}:
            limit_only = True
            entry_style = "LIMIT"
            advisories.append(f"selection_defensive:{selected_strategy}")
        if is_range_regime(selection_regime) and selected_strategy in {"ema_macd", "adx_breakout", "pullback_fade", "wyckoff", "custom_macdcross"}:
            hard_blockers.append(f"range_trend_strategy_block:{selected_strategy}")
        if selection_regime in {"RANGING", "TRANSITION"} and consecutive_losses >= 2:
            hard_blockers.append("consecutive_losses_in_range")

        if session_profile.get("block_new_positions") or recommendation == "AVOID":
            limit_only = True
            entry_style = "LIMIT"
            score -= 1
            advisories.append(f"session_caution:{session_mode}")
        elif session_mode == "ASIA_NOISE":
            limit_only = True
            entry_style = "LIMIT"
            score -= 1
            advisories.append("session_noise:asia")
        elif session_mode in {"ASIA_DEFENSIVE", "ASIA_TREND"}:
            score -= 1
            limit_only = True
            entry_style = "LIMIT"
            advisories.append(f"session_defensive:{session_mode}")
        elif session_bucket == "ASIA" and selected_strategy in {"ema_macd", "adx_breakout", "pullback_fade"}:
            limit_only = True
            entry_style = "LIMIT"
            score -= 1
            advisories.append(f"session_asia_trend_limit:{selected_strategy}")

        session_rule = (
            (runtime_rules.get("session_rules", {}) or {}).get(str(session_info.get("session_name", "UNKNOWN") or "UNKNOWN").upper(), {})
            if isinstance(runtime_rules.get("session_rules", {}), dict)
            else {}
        )
        if isinstance(session_rule, dict) and session_rule:
            action = str(session_rule.get("action", "neutral") or "neutral")
            if action == "favor":
                score += 1
                advisories.append(f"hermes_session_favor:{session_info.get('session_name', 'UNKNOWN')}")
            elif action == "avoid":
                score -= 1
                limit_only = True
                entry_style = "LIMIT"
                advisories.append(f"hermes_session_avoid:{session_info.get('session_name', 'UNKNOWN')}")
            if bool(session_rule.get("block", False)):
                hard_blockers.append(f"hermes_session_block:{session_info.get('session_name', 'UNKNOWN')}")

        regime_rule = (
            (runtime_rules.get("regime_rules", {}) or {}).get(selection_regime, {})
            if isinstance(runtime_rules.get("regime_rules", {}), dict)
            else {}
        )
        if isinstance(regime_rule, dict) and regime_rule:
            action = str(regime_rule.get("action", "neutral") or "neutral")
            if action == "favor":
                score += 1
                advisories.append(f"hermes_regime_favor:{selection_regime}")
            elif action == "avoid":
                score -= 1
                advisories.append(f"hermes_regime_avoid:{selection_regime}")
            if bool(regime_rule.get("block", False)):
                hard_blockers.append(f"hermes_regime_block:{selection_regime}")

        shock_direction = str(interpreter.get("shock_direction", "NONE") or "NONE").upper()
        execution_advice = str(interpreter.get("execution_advice", "ALLOW_FOLLOW") or "ALLOW_FOLLOW").upper()
        market_structure = str(interpreter.get("market_structure", "UNCERTAIN") or "UNCERTAIN").upper()
        confirmation_state = str(interpreter.get("confirmation_state", "NONE") or "NONE").upper()
        pullback_quality = str(interpreter.get("pullback_quality", "UNKNOWN") or "UNKNOWN").upper()
        counter_trend_risk = str(interpreter.get("counter_trend_risk", "LOW") or "LOW").upper()
        structure_side = "BUY" if shock_direction == "BULLISH" else "SELL" if shock_direction == "BEARISH" else None

        if structure_side and structure_side == side and execution_advice in {"LIMIT_ONLY", "WAIT_CONFIRMATION", "BLOCK_COUNTER_TREND"}:
            limit_only = True
            entry_style = "LIMIT"
            score -= 1
            advisories.append(f"structure_advice:{execution_advice}")
        if market_structure in {"FAILED_BREAKOUT", "IMPULSE_EXHAUSTION"} and structure_side == side:
            limit_only = True
            entry_style = "LIMIT"
            score -= 1
            advisories.append(f"structure_reversal:{market_structure}")
        if confirmation_state == "CONFIRMED" and pullback_quality == "HEALTHY" and structure_side == side:
            score += 1
            advisories.append("structure_confirmed")
        if market_structure == "SHOCK_REPRICING" and structure_side == side:
            score += 1
            advisories.append("shock_repricing")
        if counter_trend_risk == "HIGH" and structure_side and structure_side != side:
            score -= 1
            reasons.append("counter_trend_high_risk")

        opposes_agent = (
            (agent_direction == "LONG" and side == "SELL")
            or (agent_direction == "SHORT" and side == "BUY")
        )
        if opposes_agent:
            if is_trend_regime(regime) and confidence >= 0.85:
                score -= 1
                advisories.append(f"agent_bias_opposed:{agent_direction}")
            else:
                score -= 1
                limit_only = True
                entry_style = "LIMIT"
                advisories.append(f"agent_bias_opposed:{agent_direction}")
        elif agent_direction in {"LONG", "SHORT"} and side:
            score += 1
            advisories.append(f"agent_bias_aligned:{agent_direction}")

        confidence_multiplier = 1.0
        if session_mode in {"ASIA_DEFENSIVE", "ASIA_TREND"}:
            confidence_multiplier *= 0.95
        if session_mode == "ASIA_NOISE":
            confidence_multiplier *= 0.88
        if limit_only:
            confidence_multiplier *= 0.97
        if opposes_agent:
            confidence_multiplier *= 0.93
        adjusted_confidence = max(0.0, min(0.99, confidence * confidence_multiplier))

        if (
            execution_advice == "BLOCK_COUNTER_TREND"
            and structure_side
            and structure_side == side
            and counter_trend_risk == "HIGH"
        ):
            hard_blockers.append("structure_counter_trend_high_risk")

        block = len(hard_blockers) > 0
        drop_reason = f"CONTEXT_ARB_{hard_blockers[0]}" if hard_blockers else None

        return {
            "score": score,
            "block": block,
            "drop_reason": drop_reason,
            "reasons": reasons,
            "advisories": advisories,
            "limit_only": limit_only,
            "entry_style": entry_style,
            "session_mode": session_mode,
            "agent_direction": agent_direction,
            "selection_regime": selection_regime,
            "selected_strategy": selected_strategy,
            "adjusted_confidence": round(adjusted_confidence, 4),
            "hard_blockers": hard_blockers,
        }

    def _check_contextual_arbiter(self, context: dict) -> tuple:
        policy = context.get("contextual_policy", {})
        if not isinstance(policy, dict):
            return True, None
        if bool(policy.get("block", False)):
            return False, str(policy.get("drop_reason", "CONTEXT_ARB_BLOCK") or "CONTEXT_ARB_BLOCK")
        return True, None

    # ---- 止损止盈计算 ----

    def _calc_risk_params(self, signal: dict, context: dict) -> dict:
        """
        计算精确的止损止盈价格和手数
        输出: {lots, sl_price, tp_price, sl_points, tp_points}
        """
        state = context.get("state", {})
        params = context.get("params", {})

        # 手数计算
        max_lots = float(self._get_param("max_position_size", 0.20))
        vol_mult = _get_vol_multiplier().get("multiplier", 1.0)
        if vol_mult != 1.0:
            max_lots = round(max_lots * vol_mult, 4)
        session_info = context.get("session_info") or _get_session_info()
        session_mult = float(session_info.get("risk_multiplier", 1.0) or 1.0)
        performance_profile = context.get("performance_profile", {}) if isinstance(context.get("performance_profile", {}), dict) else {}
        performance_mult = float(performance_profile.get("live_multiplier", 1.0) or 1.0)

        # 止盈/止损价格（直接使用 Signal Layer 输出，不二次计算）
        payload = signal.get("payload", signal)
        entry = payload.get("entry", 0)
        sl = payload.get("sl", 0)
        tp = payload.get("tp", 0)
        sig_vol = payload.get("volume_lots", signal.get("volume_lots", 0.10))
        lots = min(sig_vol, max_lots)
        if session_mult != 1.0 and lots > 0:
            lots = round(max(0.01, lots * session_mult), 4)
        if performance_mult != 1.0 and lots > 0:
            lots = round(max(0.01, lots * performance_mult), 4)

        # ── Edge-based sizing (replaces confidence-based soft_mult) ──
        # Lot size = f(strategy×regime historical edge), NOT signal confidence.
        # Confidence is a gate (threshold) only. This ensures symmetric sizing
        # between wins and losses within each strategy×regime pair.
        try:
            strategy_id = str(payload.get("strategy_id", payload.get("_strategy_type", "")) or "").strip()
            regime = str(payload.get("regime", payload.get("indicators", {}).get("regime", "UNKNOWN")) or "UNKNOWN").upper()
            if strategy_id:
                from src.learning.edge_sizer import get_strategy_regime_lot
                edge_lot = get_strategy_regime_lot(strategy_id, regime, default_lot=lots)
                lots = round(max(0.01, edge_lot), 4)
        except Exception:
            pass

        return {
            "lots": lots,
            "sl_price": round(sl, 3),
            "tp_price": round(tp, 3),
            "sl_points": round(abs(entry - sl), 1) if entry and sl else 0,
            "tp_points": round(abs(tp - entry), 1) if tp and entry else 0,
        }

    # ---- 主评估方法 ----


    # To avoid blackboard spam from high-frequency blocked signals
    _last_block_logs = {}

    def _log_block_to_blackboard(self, reason: str, signal: dict):
        """将拦截信息上报到决策大厅，带节流防刷屏"""
        try:
            import time
            from src.swarm.blackboard import global_blackboard
            
            # Simple category for throttle
            cat = reason.split("_")[0] if "_" in reason else reason
            now = time.time()
            last_time = self._last_block_logs.get(cat, 0)
            
            if now - last_time > 120:  # 120秒内同类错误只报一次
                side_zh = "做多" if signal.get("side") == "BUY" else "做空"
                msg = f"已驳回 {side_zh} 请求。原因：{reason}。继续潜伏。"
                global_blackboard.add_log("Risk_Commissar", "WARN", msg)
                self._last_block_logs[cat] = now
        except Exception:
            pass

    def evaluate(self, signal: dict, context: dict, trace_id: str = None) -> dict:
        """
        核心评估方法：接收 Signal Layer 输出，执行6项仲裁，返回风控决策

        输入：
            signal: Signal Layer 输出的标准 dict
                {
                    "schema_version": "2.1.0",
                    "trace_id": "20260322-140100-abc123",
                    "strategy_id": "ema_macd_v2",
                    "payload": {
                        "action": "BUY",
                        "confidence": 0.75,
                        "side": "BUY",
                        "entry": 2342.5,
                        "sl": 2338.0,
                        "tp": 2358.0,
                        "indicators": {...}
                    }
                }
            context: 执行上下文
                {
                    "price_bid": float,
                    "price_ask": float,
                    "spread": float,
                    "state": dict,   # SQLite 运行时状态
                    "params": dict    # effective_params
                }
            trace_id: 可选，手动传入 trace_id（从 signal 中提取）

        输出：
            标准 Risk Layer 输出 dict（带 schema_version + trace_id）
        """
        # 提取 trace_id
        if trace_id is None:
            trace_id = signal.get("trace_id", f"rm_{int(time.time()*1000)}")

        # 提取 payload（兼容新旧两种 signal 格式）
        if "payload" in signal:
            payload = signal["payload"]
            action = payload.get("action", "HOLD")
            confidence = payload.get("confidence", 0)
            side = payload.get("side", "BUY")
            entry = payload.get("entry", 0)
            sl = payload.get("sl", 0)
            tp = payload.get("tp", 0)
            indicators = payload.get("indicators", {})
        else:
            # 兼容旧格式（无 payload 包装）
            action = signal.get("action", "HOLD")
            confidence = signal.get("confidence", 0)
            side = signal.get("side", "BUY")
            entry = signal.get("entry", 0)
            sl = signal.get("sl", 0)
            tp = signal.get("tp", 0)
            indicators = signal.get("indicators", {})

        action = str(action or "HOLD").upper()
        if action in {"BUY", "SELL"}:
            side = action
            action = "OPEN"
        elif action == "OPEN":
            side = str(side or "BUY").upper()
        else:
            action = "HOLD"

        # 如果信号本身就是 HOLD，直接返回
        if action == "HOLD":
            return {
                "schema_version": self.SCHEMA_VERSION,
                "trace_id": trace_id,
                "parent_schema_version": signal.get("schema_version", "unknown"),
                "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
                "payload": {
                    "action": "HOLD",
                    "drop_reason": signal.get("reason", "signal_hold"),
                    "lots": 0,
                    "sl_price": 0,
                    "tp_price": 0,
                    "risk_checks": [],
                }
            }

        # 初始化检查结果
        risk_checks = []
        final_action = action
        drop_reason = None
        session_info = context.get("session_info") or _get_session_info()
        if context.get("session_info") is None:
            context = {**context, "session_info": session_info}
        performance_factors = context.get("performance_factors")
        if not isinstance(performance_factors, dict):
            performance_factors = _load_performance_factors()
            context = {**context, "performance_factors": performance_factors}
        hermes_runtime_rules = context.get("hermes_runtime_rules")
        if not isinstance(hermes_runtime_rules, dict):
            hermes_runtime_rules = _load_hermes_runtime_rules()
            context = {**context, "hermes_runtime_rules": hermes_runtime_rules}
        try:
            from src.live_performance_factors import build_pretrade_performance_profile
            performance_profile = build_pretrade_performance_profile(signal, context, performance_factors)
        except Exception:
            performance_profile = {}
        context = {**context, "performance_profile": performance_profile}
        confidence_bias = float(performance_profile.get("confidence_bias", 0.0) or 0.0)
        consecutive_losses = int((context.get("state") or {}).get("consecutive_losses", 0) or 0)
        if consecutive_losses >= 2:
            confidence_bias = min(confidence_bias, 0.0)
        if confidence_bias:
            confidence = max(0.0, min(0.99, confidence + confidence_bias))
        payload_context = {
            **payload,
            "side": side,
            "regime": indicators.get("regime", context.get("selection_regime", "UNKNOWN")),
            "strategy_id": signal.get("strategy_id", ""),
            "indicators": indicators,
        }
        context = {**context, "signal_indicators": indicators}
        contextual_policy = self._build_contextual_policy(context, payload_context, confidence)
        context = {**context, "contextual_policy": contextual_policy}
        confidence = float(contextual_policy.get("adjusted_confidence", confidence) or confidence)
        threshold_profile = self._get_dynamic_confidence_threshold(context)
        context = {**context, "confidence_threshold_profile": threshold_profile}

        # ---- 按优先级执行7项检查(含学习避让) ----
        check_map = {
            "circuit_breaker": self._check_circuit_breaker,
            "daily_loss_limit": self._check_daily_loss_limit,
            "session_filter": self._check_session_filter,
            "learned_avoidance": self._check_learned_avoidance,
            "contextual_arbiter": self._check_contextual_arbiter,
            "performance_gate": self._check_performance_gate,
            "spread_filter": self._check_spread_filter,
            "position_limit": self._check_position_limit,
            "confidence_threshold": self._check_confidence_threshold,
        }

        for check_name in self.RISK_CHECK_ORDER:
            check_func = check_map[check_name]

            # position_limit 需要 payload（取 volume_lots），confidence_threshold 需要 confidence 值
            if check_name == "position_limit":
                passed, reason = check_func(context, payload)
            elif check_name == "structure_guard":
                passed, reason = check_func(context, payload)
            elif check_name == "confidence_threshold":
                passed, reason = check_func(context, confidence)
            else:
                passed, reason = check_func(context)

            risk_checks.append({
                "check": check_name,
                "result": "PASS" if passed else "BLOCK",
                "detail": reason or "ok"
            })

            if not passed:
                # 高优先级拦截，后续不再检查
                final_action = "HOLD"
                drop_reason = reason
                break

        # ---- 计算风控参数（止损/止盈/手数）----
        if final_action != "HOLD":
            risk_params = self._calc_risk_params(signal, context)
        else:
            risk_params = {
                "lots": 0, "sl_price": 0, "tp_price": 0,
                "sl_points": 0, "tp_points": 0
            }

        return {
            "schema_version": self.SCHEMA_VERSION,
            "trace_id": trace_id,
            "parent_schema_version": signal.get("schema_version", "unknown"),
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
            "payload": {
                "action": final_action,
                "side": side,
                "drop_reason": drop_reason,
                "confidence": confidence,
                "entry": entry,
                "sl_price": risk_params["sl_price"],
                "tp_price": risk_params["tp_price"],
                "lots": risk_params["lots"],
                "sl_points": risk_params["sl_points"],
                "tp_points": risk_params["tp_points"],
                "risk_checks": risk_checks,
                "indicators": indicators,
                "session_info": session_info,
                "performance_profile": performance_profile,
                "contextual_policy": contextual_policy,
                "interpreter": context.get("interpreter", {}),
            }
        }

    # ---- GUI 风险状态查询 ----

    def get_risk_exposure(self, state: dict = None) -> dict:
        """
        供 GUI 显示当前风险状态
        返回当前风险敞口的快照
        """
        if state is None:
            state = {}

        daily_loss_pct = abs(state.get("daily_loss_pct", 0.0))
        consecutive_losses = state.get("consecutive_losses", 0)
        max_consec = int(self._get_param("max_consecutive_losses", 4))
        max_lots = float(self._get_param("max_position_size", 0.20))
        spread_kill = float(self._get_param("spread_kill", 70))
        vol_info = _get_vol_multiplier()
        session_info = _get_session_info()

        # 日亏损状态
        if daily_loss_pct >= 5.0:
            daily_loss_status = "CRITICAL"  # 需强平
        elif daily_loss_pct >= 2.0:
            daily_loss_status = "WARNING"   # 禁新开仓
        elif daily_loss_pct >= 1.0:
            daily_loss_status = "CAUTION"   # 警示
        else:
            daily_loss_status = "NORMAL"

        return {
            "schema_version": self.SCHEMA_VERSION,
            "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S+08:00"),
            "daily_loss_pct": round(daily_loss_pct, 3),
            "daily_loss_status": daily_loss_status,  # NORMAL / CAUTION / WARNING / CRITICAL
            "consecutive_losses": consecutive_losses,
            "max_consecutive_losses": max_consec,
            "circuit_breaker_status": "TRIPPED" if consecutive_losses >= max_consec else "OK",
            "spread_kill": spread_kill,
            "max_position_size": max_lots,
            "vol_multiplier": vol_info.get("multiplier", 1.0),
            "vol_regime": vol_info.get("regime", "UNKNOWN"),
            "session_recommendation": session_info.get("recommendation", "CAUTIOUS"),
            "session_mode": session_info.get("session_mode", "NORMAL"),
            "session_entry_style": session_info.get("entry_style", "MARKET"),
            "session_risk_multiplier": session_info.get("risk_multiplier", 1.0),
            "session_status": "OK" if not session_info.get("session_profile", {}).get("block_new_positions") and session_info.get("recommendation") != "AVOID" else "BLOCKED",
        }


# ============ 独立测试入口 ============
if __name__ == "__main__":
    """
    独立测试：python risk_manager.py --test
    """
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--test", action="store_true", help="运行测试")
    args = parser.parse_args()

    if args.test:
        # 构造测试数据
        test_signal = {
            "schema_version": "2.1.0",
            "trace_id": "test_001",
            "strategy_id": "ema_macd_v2",
            "payload": {
                "action": "BUY",
                "confidence": 0.75,
                "side": "BUY",
                "entry": 2342.50,
                "sl": 2338.00,
                "tp": 2358.00,
                "indicators": {
                    "ema9": 2345.2, "ema21": 2340.5,
                    "rsi14": 58.3, "atr14": 52.4
                }
            }
        }

        test_context = {
            "price_bid": 2342.00,
            "price_ask": 2342.50,
            "spread": 32.0,
            "state": {
                "consecutive_losses": 0,
                "daily_loss_pct": 0.0,
                "position_open": False,
                "positions_all": [],
            },
            "params": {}
        }

        mgr = GoldRiskManager()

        print("=== Test 1: Normal PASS ===")
        result = mgr.evaluate(test_signal, test_context)
        print(json.dumps(result, indent=2, ensure_ascii=False))

        print("\n=== Test 2: High Spread BLOCK ===")
        test_context["spread"] = 85.0
        result = mgr.evaluate(test_signal, test_context)
        print(json.dumps(result, indent=2, ensure_ascii=False))

        print("\n=== Test 3: Low Confidence BLOCK ===")
        test_signal["payload"]["confidence"] = 0.50
        test_context["spread"] = 32.0
        result = mgr.evaluate(test_signal, test_context)
        print(json.dumps(result, indent=2, ensure_ascii=False))

        print("\n=== Test 4: Daily Loss Warning ===")
        test_signal["payload"]["confidence"] = 0.75
        test_context["state"]["daily_loss_pct"] = 2.5  # 亏损2.5%
        result = mgr.evaluate(test_signal, test_context)
        print(json.dumps(result, indent=2, ensure_ascii=False))

        print("\n=== Risk Exposure ===")
        exposure = mgr.get_risk_exposure(test_context["state"])
        print(json.dumps(exposure, indent=2, ensure_ascii=False))
