"""
策略参数定制 schema — 定义所有可调参数的元数据、分组、边界与验证规则。
作为整个策略定制模块（API / UI / 校验）的单一数据源。
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Optional

# ═══════════════════════════════════════════════════════════════════════════════
# Parameter type & metadata
# ═══════════════════════════════════════════════════════════════════════════════


class ParamType(str, Enum):
    FLOAT = "float"
    INT = "int"
    BOOL = "bool"
    STRING_CHOICE = "string_choice"
    FLOAT_RANGE = "float_range"


@dataclass
class ParamDef:
    """单个参数的完整元数据。"""

    key: str
    label: str
    description: str
    type: ParamType
    default: Any
    group: str  # logical group key
    subgroup: str = ""
    order: int = 0
    # Numeric bounds
    min_val: Optional[float] = None
    max_val: Optional[float] = None
    step: Optional[float] = None
    # Choices
    choices: list[str] = field(default_factory=list)
    # Unit label shown in UI
    unit: str = ""
    # When this param only applies if a condition is met
    visible_when: Optional[str] = None  # python expression evaluated in context
    # Risk level of changing this param
    risk_level: str = "low"  # "low" | "medium" | "high" | "critical"
    # Whether this requires paper mode before live
    requires_paper_mode: bool = False
    # Advanced params hidden by default
    advanced: bool = False
    # Runtime safety: "hot" = can change anytime, "warm" = pause new trades,
    # "cold" = must be flat (no open positions)
    runtime_safe: str = "hot"
    # Suggested favorite — shown in quick-access panel; user overrides via localStorage
    favorite: bool = False


@dataclass
class CrossParamRule:
    """A cross-parameter consistency rule checked during batch validation."""

    id: str
    description: str
    condition: str  # Python expression evaluated with params dict as locals
    severity: str = "error"  # "error" (block save) or "warning" (allow with confirmation)
    fix_hint: str = ""


# ═══════════════════════════════════════════════════════════════════════════════
# Cross-parameter consistency rules
# ═══════════════════════════════════════════════════════════════════════════════

CROSS_PARAM_RULES: list[CrossParamRule] = [
    CrossParamRule(
        id="CPR01",
        description="震荡模式已禁用，但震荡过滤器仍为 ALLOW",
        condition=(
            "updates.get('entry_direction_filter.disable_range_mode') == True "
            "and (updates.get('entry_direction_filter.range_mode_long_filter') == 'ALLOW' "
            "or updates.get('entry_direction_filter.range_mode_short_filter') == 'ALLOW')"
        ),
        severity="warning",
        fix_hint="将 range_mode_long_filter 和 range_mode_short_filter 设为 BLOCK",
    ),
    CrossParamRule(
        id="CPR02",
        description="趋势模式已禁用，但趋势过滤器仍为 ALLOW",
        condition=(
            "updates.get('entry_direction_filter.disable_trend_mode') == True "
            "and (updates.get('entry_direction_filter.trend_mode_long_filter') == 'ALLOW' "
            "or updates.get('entry_direction_filter.trend_mode_short_filter') == 'ALLOW')"
        ),
        severity="warning",
        fix_hint="将 trend_mode_long_filter 和 trend_mode_short_filter 设为 BLOCK",
    ),
    CrossParamRule(
        id="CPR03",
        description="最大仓位小于基础仓位",
        condition=(
            "float(updates.get('position_sizing.max_lots', 0)) < "
            "float(updates.get('position_sizing.base_lots', 0))"
        ),
        severity="error",
        fix_hint="max_lots 必须 >= base_lots",
    ),
    CrossParamRule(
        id="CPR04",
        description="最低置信度(趋势) > 最低置信度(震荡)，震荡通常需要更高门槛",
        condition=(
            "float(updates.get('risk_governor.min_confidence', 0)) > "
            "float(updates.get('risk_governor.min_confidence_ranging', 0))"
        ),
        severity="warning",
        fix_hint="震荡市场假信号更多，建议 min_confidence_ranging >= min_confidence",
    ),
    CrossParamRule(
        id="CPR05",
        description="连亏4笔后的置信度门槛不应低于连亏3笔后",
        condition=(
            "float(updates.get('risk_governor.min_confidence_loss4', 0)) < "
            "float(updates.get('risk_governor.min_confidence_loss3', 0))"
        ),
        severity="error",
        fix_hint="min_confidence_loss4 应 >= min_confidence_loss3",
    ),
    CrossParamRule(
        id="CPR06",
        description="追踪止损激活阈值不应高于追踪基准距离",
        condition=(
            "float(updates.get('exit_strategy.trailing_stop.trend.min_profit_atr', 0)) > "
            "float(updates.get('exit_strategy.trailing_stop.trend.trail_base_atr', 0))"
        ),
        severity="warning",
        fix_hint="激活阈值应 <= 追踪距离，否则永远无法激活",
    ),
    CrossParamRule(
        id="CPR07",
        description="追踪最小 ATR 不应大于最大 ATR",
        condition=(
            "float(updates.get('exit_strategy.trailing_stop.trend.min_trail_atr', 0)) > "
            "float(updates.get('exit_strategy.trailing_stop.trend.max_trail_atr', 0))"
        ),
        severity="error",
        fix_hint="min_trail_atr 必须 <= max_trail_atr",
    ),
    CrossParamRule(
        id="CPR08",
        description="正常点差 SL 倍率不应大于宽点差 SL 倍率",
        condition=(
            "float(updates.get('risk_governor.sl_atr_multiplier_normal', 0)) > "
            "float(updates.get('risk_governor.sl_atr_multiplier_wide_spread', 0))"
        ),
        severity="warning",
        fix_hint="宽点差应使用更宽的止损",
    ),
]


# ═══════════════════════════════════════════════════════════════════════════════
# Logical parameter groups (for UI grouping)
# ═══════════════════════════════════════════════════════════════════════════════

GROUPS: dict[str, dict[str, Any]] = {
    "entry_trend": {
        "label": "入场规则 — 趋势",
        "icon": "trending_up",
        "description": "趋势市场中的做多/做空入场条件",
        "order": 1,
    },
    "entry_range": {
        "label": "入场规则 — 震荡",
        "icon": "swap_horiz",
        "description": "震荡市场中的高低位反转入场条件",
        "order": 2,
    },
    "entry_filter": {
        "label": "入场过滤",
        "icon": "filter_alt",
        "description": "全局入场方向过滤器与风控阻挡规则",
        "order": 3,
    },
    "exit_general": {
        "label": "出场规则",
        "icon": "exit_to_app",
        "description": "止损止盈、保本线、价格跟踪",
        "order": 4,
    },
    "exit_partial": {
        "label": "分批出场",
        "icon": "call_split",
        "description": "部分止盈与尾仓奔跑策略",
        "order": 5,
    },
    "exit_advanced": {
        "label": "高级出场条件",
        "icon": "tune",
        "description": "RSI 极值退出、时间止损、波动退出",
        "order": 6,
    },
    "risk": {
        "label": "风控参数",
        "icon": "shield",
        "description": "置信度门槛、点差保护、连续亏损规则",
        "order": 7,
    },
    "position": {
        "label": "仓位管理",
        "icon": "pie_chart",
        "description": "基础仓位、最大仓位、亏损缩仓",
        "order": 8,
    },
    "trend_vote": {
        "label": "趋势投票模型",
        "icon": "how_to_vote",
        "description": "EMA / MACD / ADX / RSI 多因子投票权重",
        "order": 9,
    },
    "regime": {
        "label": "市况检测",
        "icon": "landscape",
        "description": "ADX 趋势/震荡判定阈值",
        "order": 10,
    },
    "macro": {
        "label": "宏观方向门",
        "icon": "gavel",
        "description": "Agent 方向门配置、逆势缩放与亏损保护",
        "order": 11,
    },
    "router": {
        "label": "策略路由",
        "icon": "alt_route",
        "description": "不同市况下的策略优先级与权重分配",
        "order": 12,
    },
}


# ═══════════════════════════════════════════════════════════════════════════════
# Master parameter definitions — SINGLE SOURCE OF TRUTH
# ═══════════════════════════════════════════════════════════════════════════════

PARAMS: list[ParamDef] = [
    # ── 入场趋势 ────────────────────────────────────────────────────────
    ParamDef(
        key="trend_mode.long.conditions",
        label="趋势做多条件",
        description="趋势中触发做多的技术条件组合",
        type=ParamType.STRING_CHOICE,
        default=["ema9 > ema21", "rsi14 < 70", "price >= ema9"],
        choices=["ema9 > ema21", "rsi14 < 70", "price >= ema9", "macd > 0", "adx > 25"],
        group="entry_trend",
        order=1,
        risk_level="critical",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    ParamDef(
        key="trend_mode.short.conditions",
        label="趋势做空条件",
        description="趋势中触发做空的技术条件组合",
        type=ParamType.STRING_CHOICE,
        default=["ema9 < ema21", "rsi14 > 30", "price <= ema9"],
        choices=["ema9 < ema21", "rsi14 > 30", "price <= ema9", "macd < 0", "adx > 25"],
        group="entry_trend",
        order=2,
        risk_level="critical",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    ParamDef(
        key="trend_mode.long.min_adx",
        label="趋势做多最小 ADX",
        description="趋势做多所需的最低 ADX 值",
        type=ParamType.INT,
        default=25,
        min_val=15,
        max_val=60,
        step=1,
        group="entry_trend",
        order=3,
        risk_level="high",
        runtime_safe="cold",
    ),
    ParamDef(
        key="trend_mode.short.min_adx",
        label="趋势做空最小 ADX",
        description="趋势做空所需的最低 ADX 值",
        type=ParamType.INT,
        default=25,
        min_val=15,
        max_val=60,
        step=1,
        group="entry_trend",
        order=4,
        risk_level="high",
        runtime_safe="cold",
    ),
    ParamDef(
        key="trend_mode.long.confidence_boost",
        label="趋势做多置信度加成",
        description="符合趋势做多条件时额外增加的置信度",
        type=ParamType.FLOAT,
        default=0.05,
        min_val=0.0,
        max_val=0.20,
        step=0.01,
        group="entry_trend",
        order=5,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="trend_mode.short.confidence_boost",
        label="趋势做空置信度加成",
        description="符合趋势做空条件时额外增加的置信度",
        type=ParamType.FLOAT,
        default=0.05,
        min_val=0.0,
        max_val=0.20,
        step=0.01,
        group="entry_trend",
        order=6,
        risk_level="medium",
        runtime_safe="warm",
    ),
    # ── 入场震荡 ────────────────────────────────────────────────────────
    ParamDef(
        key="range_mode.long.conditions",
        label="震荡做多条件",
        description="震荡市场中触发做多的技术条件组合",
        type=ParamType.STRING_CHOICE,
        default=["rsi14 <= 35", "price <= bb_lower"],
        choices=["rsi14 <= 35", "price <= bb_lower", "rsi14 <= 30", "stoch_k < 20"],
        group="entry_range",
        order=1,
        risk_level="high",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    ParamDef(
        key="range_mode.short.conditions",
        label="震荡做空条件",
        description="震荡市场中触发做空的技术条件组合",
        type=ParamType.STRING_CHOICE,
        default=["rsi14 >= 65", "price >= bb_upper"],
        choices=["rsi14 >= 65", "price >= bb_upper", "rsi14 >= 70", "stoch_k > 80"],
        group="entry_range",
        order=2,
        risk_level="high",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    ParamDef(
        key="range_mode.long.confidence_penalty",
        label="震荡做多置信度惩罚",
        description="震荡中做多需扣除的置信度（震荡信号可靠性较低）",
        type=ParamType.FLOAT,
        default=0.10,
        min_val=0.0,
        max_val=0.30,
        step=0.01,
        group="entry_range",
        order=3,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="range_mode.short.confidence_penalty",
        label="震荡做空置信度惩罚",
        description="震荡中做空需扣除的置信度",
        type=ParamType.FLOAT,
        default=0.10,
        min_val=0.0,
        max_val=0.30,
        step=0.01,
        group="entry_range",
        order=4,
        risk_level="medium",
        runtime_safe="warm",
    ),
    # ── 入场过滤 ────────────────────────────────────────────────────────
    ParamDef(
        key="entry_direction_filter.trend_adx_threshold",
        label="趋势 ADX 判定阈值",
        description="ADX 超过此值判定为趋势市",
        type=ParamType.INT,
        default=25,
        min_val=15,
        max_val=50,
        step=1,
        group="entry_filter",
        order=1,
        risk_level="high",
        runtime_safe="warm",
    ),
    ParamDef(
        key="entry_direction_filter.trend_mode_long_filter",
        label="趋势做多过滤",
        description="趋势市中是否允许做多",
        type=ParamType.STRING_CHOICE,
        default="ALLOW",
        choices=["ALLOW", "BLOCK"],
        group="entry_filter",
        order=2,
        risk_level="critical",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    ParamDef(
        key="entry_direction_filter.trend_mode_short_filter",
        label="趋势做空过滤",
        description="趋势市中是否允许做空",
        type=ParamType.STRING_CHOICE,
        default="ALLOW",
        choices=["ALLOW", "BLOCK"],
        group="entry_filter",
        order=3,
        risk_level="critical",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    ParamDef(
        key="entry_direction_filter.range_mode_long_filter",
        label="震荡做多过滤",
        description="震荡市中是否允许做多",
        type=ParamType.STRING_CHOICE,
        default="ALLOW",
        choices=["ALLOW", "BLOCK"],
        group="entry_filter",
        order=4,
        risk_level="critical",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    ParamDef(
        key="entry_direction_filter.range_mode_short_filter",
        label="震荡做空过滤",
        description="震荡市中是否允许做空",
        type=ParamType.STRING_CHOICE,
        default="ALLOW",
        choices=["ALLOW", "BLOCK"],
        group="entry_filter",
        order=5,
        risk_level="critical",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    ParamDef(
        key="entry_direction_filter.disable_trend_mode",
        label="禁用趋势模式",
        description="全局禁用趋势策略（仅震荡策略生效）",
        type=ParamType.BOOL,
        default=False,
        group="entry_filter",
        order=6,
        risk_level="critical",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    ParamDef(
        key="entry_direction_filter.disable_range_mode",
        label="禁用震荡模式",
        description="全局禁用震荡策略（仅趋势策略生效）",
        type=ParamType.BOOL,
        default=False,
        group="entry_filter",
        order=7,
        risk_level="critical",
        requires_paper_mode=True,
        runtime_safe="cold",
    ),
    # ── 出场规则 ────────────────────────────────────────────────────────
    ParamDef(
        key="exit_strategy.break_even.enabled",
        label="保本线启用",
        description="浮盈达到触发条件后移动止损至开仓价",
        type=ParamType.BOOL,
        default=True,
        group="exit_general",
        order=1,
        risk_level="low",
        runtime_safe="hot",
    ),
    ParamDef(
        key="exit_strategy.break_even.trigger_atr",
        label="保本触发 ATR 倍数",
        description="浮盈超过此 ATR 倍数后触发保本止损",
        type=ParamType.FLOAT,
        default=0.5,
        min_val=0.2,
        max_val=1.5,
        step=0.1,
        group="exit_general",
        order=2,
        risk_level="medium",
        runtime_safe="hot",
    ),
    ParamDef(
        key="exit_strategy.trailing_stop.trend.enabled",
        label="趋势追踪止损启用",
        description="趋势中启用 ATR 动态追踪止损",
        type=ParamType.BOOL,
        default=True,
        group="exit_general",
        order=3,
        risk_level="low",
        runtime_safe="warm",
    ),
    ParamDef(
        key="exit_strategy.trailing_stop.trend.min_profit_atr",
        label="追踪止损激活阈值",
        description="浮盈超过此 ATR 倍数后激活追踪止损",
        type=ParamType.FLOAT,
        default=0.8,
        min_val=0.3,
        max_val=2.0,
        step=0.1,
        group="exit_general",
        order=4,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="exit_strategy.trailing_stop.trend.trail_base_atr",
        label="追踪基准 ATR 倍数",
        description="追踪止损的基础 ATR 距离",
        type=ParamType.FLOAT,
        default=2.0,
        min_val=0.5,
        max_val=5.0,
        step=0.1,
        group="exit_general",
        order=5,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="exit_strategy.trailing_stop.trend.min_trail_atr",
        label="追踪最小 ATR",
        description="追踪距离的最小 ATR 下限",
        type=ParamType.FLOAT,
        default=1.0,
        min_val=0.3,
        max_val=3.0,
        step=0.1,
        group="exit_general",
        order=6,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="exit_strategy.trailing_stop.trend.max_trail_atr",
        label="追踪最大 ATR",
        description="追踪距离的最大 ATR 上限",
        type=ParamType.FLOAT,
        default=3.5,
        min_val=1.0,
        max_val=8.0,
        step=0.1,
        group="exit_general",
        order=7,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="exit_strategy.trailing_stop.range.enabled",
        label="震荡追踪止损启用",
        description="震荡中启用追踪止损（通常不推荐）",
        type=ParamType.BOOL,
        default=False,
        group="exit_general",
        order=8,
        risk_level="high",
        runtime_safe="warm",
    ),
    # ── 分批出场 ────────────────────────────────────────────────────────
    ParamDef(
        key="exit_strategy.partial_close.trend.enabled",
        label="趋势分批止盈启用",
        description="趋势中到达部分止盈位后平掉部分仓位",
        type=ParamType.BOOL,
        default=True,
        group="exit_partial",
        order=1,
        visible_when="exit_strategy.partial_close.trend.enabled == True",
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="exit_strategy.partial_close.trend.partial_tp_atr",
        label="部分止盈 ATR 倍数",
        description="到达此 ATR 倍数时触发部分平仓",
        type=ParamType.FLOAT,
        default=1.5,
        min_val=0.5,
        max_val=5.0,
        step=0.1,
        group="exit_partial",
        order=2,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="exit_strategy.partial_close.trend.partial_close_pct",
        label="部分平仓比例",
        description="触发部分止盈时平仓的仓位比例",
        type=ParamType.FLOAT,
        default=0.4,
        min_val=0.1,
        max_val=0.8,
        step=0.05,
        unit="%",
        group="exit_partial",
        order=3,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="exit_strategy.partial_close.trend.runner_trail",
        label="尾仓追踪启用",
        description="部分止盈后，剩余仓位使用 ATR 追踪",
        type=ParamType.BOOL,
        default=True,
        group="exit_partial",
        order=4,
        risk_level="low",
        runtime_safe="warm",
    ),
    # ── 高级出场 ────────────────────────────────────────────────────────
    ParamDef(
        key="exit_strategy.rsi_exit.trend.rsi_long_exit",
        label="RSI 多头极限退出",
        description="趋势多头中 RSI 达到此值触发退出",
        type=ParamType.INT,
        default=75,
        min_val=65,
        max_val=90,
        step=1,
        group="exit_advanced",
        order=1,
        advanced=True,
        risk_level="medium",
        runtime_safe="hot",
    ),
    ParamDef(
        key="exit_strategy.rsi_exit.trend.rsi_short_exit",
        label="RSI 空头极限退出",
        description="趋势空头中 RSI 达到此值触发退出",
        type=ParamType.INT,
        default=25,
        min_val=10,
        max_val=35,
        step=1,
        group="exit_advanced",
        order=2,
        advanced=True,
        risk_level="medium",
        runtime_safe="hot",
    ),
    ParamDef(
        key="exit_strategy.rsi_exit.trend.profit_required_atr",
        label="RSI 退出盈利要求",
        description="触发 RSI 退出所需的最小浮盈 ATR",
        type=ParamType.FLOAT,
        default=0.3,
        min_val=0.0,
        max_val=1.0,
        step=0.1,
        group="exit_advanced",
        order=3,
        advanced=True,
        risk_level="medium",
        runtime_safe="hot",
    ),
    ParamDef(
        key="exit_strategy.time_exit.range.max_hold_secs",
        label="震荡持仓超时（秒）",
        description="震荡中持仓超过此时间强制退出",
        type=ParamType.INT,
        default=3600,
        min_val=600,
        max_val=28800,
        step=300,
        group="exit_advanced",
        order=4,
        advanced=True,
        risk_level="low",
        runtime_safe="hot",
    ),
    # ── 风控参数 ────────────────────────────────────────────────────────
    ParamDef(
        key="risk_governor.min_confidence",
        label="最低置信度 (趋势)",
        description="趋势市场中开仓所需的最低综合置信度",
        type=ParamType.FLOAT,
        default=0.68,
        min_val=0.50,
        max_val=0.90,
        step=0.01,
        group="risk",
        order=1,
        risk_level="high",
        runtime_safe="warm",
        favorite=True,
    ),
    ParamDef(
        key="risk_governor.min_confidence_ranging",
        runtime_safe="warm",
        label="最低置信度 (震荡)",
        description="震荡市场中开仓所需的最低综合置信度",
        type=ParamType.FLOAT,
        default=0.75,
        min_val=0.50,
        max_val=0.90,
        step=0.01,
        group="risk",
        order=2,
        risk_level="high",
    ),
    ParamDef(
        key="risk_governor.min_confidence_loss3",
        label="3 连亏后最低置信度",
        description="连续亏损 3 笔后提高置信度门槛",
        type=ParamType.FLOAT,
        default=0.78,
        min_val=0.60,
        max_val=0.95,
        step=0.01,
        group="risk",
        order=3,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="risk_governor.min_confidence_loss4",
        label="4 连亏后最低置信度",
        description="连续亏损 4 笔后进一步提高置信度门槛",
        type=ParamType.FLOAT,
        default=0.82,
        min_val=0.60,
        max_val=0.95,
        step=0.01,
        group="risk",
        order=4,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="risk_governor.max_consecutive_losses",
        label="最大连续亏损笔数",
        description="连续亏损达到此数后暂停开仓",
        type=ParamType.INT,
        default=4,
        min_val=2,
        max_val=8,
        step=1,
        group="risk",
        order=5,
        risk_level="high",
        runtime_safe="warm",
        favorite=True,
    ),
    ParamDef(
        key="risk_governor.consecutive_loss_halve",
        label="连亏减半阈值",
        description="连续亏损达到此数后仓位减半",
        type=ParamType.INT,
        default=3,
        min_val=2,
        max_val=6,
        step=1,
        group="risk",
        order=6,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="risk_governor.spread_kill",
        label="点差熔断",
        description="点差超过此值（pips）立即禁止开仓",
        type=ParamType.FLOAT,
        default=70,
        min_val=30,
        max_val=150,
        step=5,
        group="risk",
        order=7,
        risk_level="high",
        runtime_safe="warm",
        favorite=True,
    ),
    ParamDef(
        key="risk_governor.spread_wide_sl_expand",
        label="宽点差 SL 扩展阈值",
        description="点差超过此值触发止损扩展保护",
        type=ParamType.FLOAT,
        default=50,
        min_val=20,
        max_val=100,
        step=5,
        group="risk",
        order=8,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="risk_governor.sl_atr_multiplier_normal",
        label="SL ATR 倍率（正常）",
        description="正常点差下的止损 ATR 倍率",
        type=ParamType.FLOAT,
        default=1.5,
        min_val=0.5,
        max_val=3.0,
        step=0.1,
        group="risk",
        order=9,
        risk_level="high",
        runtime_safe="warm",
        favorite=True,
    ),
    ParamDef(
        key="risk_governor.sl_atr_multiplier_wide_spread",
        label="SL ATR 倍率（宽点差）",
        description="宽点差时的止损 ATR 倍率（适当放宽防扫损）",
        type=ParamType.FLOAT,
        default=1.8,
        min_val=0.8,
        max_val=4.0,
        step=0.1,
        group="risk",
        order=10,
        risk_level="medium",
        runtime_safe="warm",
    ),
    ParamDef(
        key="risk_governor.max_positions",
        label="最大持仓数",
        description="同时持有的最大持仓笔数",
        type=ParamType.INT,
        default=4,
        min_val=1,
        max_val=8,
        step=1,
        group="risk",
        order=11,
        risk_level="high",
        runtime_safe="cold",
    ),
    # ── 仓位管理 ────────────────────────────────────────────────────────
    ParamDef(
        key="position_sizing.base_lots",
        runtime_safe="cold",
        label="基础仓位 (lots)",
        description="每笔交易的基准手数",
        type=ParamType.FLOAT,
        default=0.05,
        min_val=0.01,
        max_val=0.50,
        step=0.01,
        group="position",
        order=1,
        risk_level="high",
        favorite=True,
    ),
    ParamDef(
        key="position_sizing.max_lots",
        runtime_safe="cold",
        label="最大仓位 (lots)",
        description="单笔交易允许的最大手数",
        type=ParamType.FLOAT,
        default=0.05,
        min_val=0.01,
        max_val=1.0,
        step=0.01,
        group="position",
        order=2,
        risk_level="high",
        favorite=True,
    ),
    ParamDef(
        key="position_sizing.sim_stage_max",
        label="模拟阶段最大仓位",
        description="Paper/Shadow 模式下的最大仓位",
        type=ParamType.FLOAT,
        default=0.05,
        min_val=0.01,
        max_val=0.20,
        step=0.01,
        group="position",
        order=3,
        risk_level="low",
        runtime_safe="cold",
    ),
    ParamDef(
        key="position_sizing.loss_rate_threshold",
        label="亏损率缩仓阈值",
        description="胜率低于此值时自动缩仓",
        type=ParamType.FLOAT,
        default=0.4,
        min_val=0.2,
        max_val=0.6,
        step=0.05,
        group="position",
        order=4,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    # ── 趋势投票 ────────────────────────────────────────────────────────
    ParamDef(
        key="trend_vote_model.enabled",
        label="趋势投票启用",
        description="启用多因子投票判定趋势方向",
        type=ParamType.BOOL,
        default=True,
        group="trend_vote",
        order=1,
        risk_level="high",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="trend_vote_model.weights.ema_alignment",
        label="EMA 对齐权重",
        description="EMA 排列方向在投票中的权重",
        type=ParamType.FLOAT,
        default=0.35,
        min_val=0.0,
        max_val=0.6,
        step=0.05,
        group="trend_vote",
        order=2,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="trend_vote_model.weights.macd_histogram",
        label="MACD 柱权重",
        description="MACD 柱方向在投票中的权重",
        type=ParamType.FLOAT,
        default=0.25,
        min_val=0.0,
        max_val=0.5,
        step=0.05,
        group="trend_vote",
        order=3,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="trend_vote_model.weights.adx_strength",
        label="ADX 强度权重",
        description="ADX 趋势强度在投票中的权重",
        type=ParamType.FLOAT,
        default=0.20,
        min_val=0.0,
        max_val=0.4,
        step=0.05,
        group="trend_vote",
        order=4,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="trend_vote_model.weights.rsi_bias",
        label="RSI 偏向权重",
        description="RSI 方向偏向在投票中的权重",
        type=ParamType.FLOAT,
        default=0.20,
        min_val=0.0,
        max_val=0.4,
        step=0.05,
        group="trend_vote",
        order=5,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="trend_vote_model.min_vote_score",
        label="最低投票分数",
        description="判定为趋势所需的最低综合投票分数",
        type=ParamType.FLOAT,
        default=0.60,
        min_val=0.40,
        max_val=0.80,
        step=0.05,
        group="trend_vote",
        order=6,
        risk_level="high",
        runtime_safe="warm",
        favorite=False,
    ),
    # ── 市况检测 ────────────────────────────────────────────────────────
    ParamDef(
        key="entry_rules.regime_detection.TRENDING.adx_threshold",
        label="趋势判定 ADX 阈值",
        description="ADX >= 此值判定为趋势市",
        type=ParamType.INT,
        default=18,
        min_val=12,
        max_val=40,
        step=1,
        group="regime",
        order=1,
        risk_level="high",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="entry_rules.regime_detection.TRENDING.sl_atr_mult",
        label="趋势 SL ATR 倍率",
        description="趋势市中止损的 ATR 倍率",
        type=ParamType.FLOAT,
        default=1.0,
        min_val=0.5,
        max_val=3.0,
        step=0.1,
        group="regime",
        order=2,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="entry_rules.regime_detection.TRENDING.tp_atr_mult",
        label="趋势 TP ATR 倍率",
        description="趋势市中止盈的 ATR 倍率",
        type=ParamType.FLOAT,
        default=2.0,
        min_val=1.0,
        max_val=5.0,
        step=0.1,
        group="regime",
        order=3,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="entry_rules.regime_detection.RANGING.sl_atr_mult",
        label="震荡 SL ATR 倍率",
        description="震荡市中止损的 ATR 倍率（较紧）",
        type=ParamType.FLOAT,
        default=0.5,
        min_val=0.2,
        max_val=1.5,
        step=0.1,
        group="regime",
        order=4,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="entry_rules.regime_detection.RANGING.tp_atr_mult",
        label="震荡 TP ATR 倍率",
        description="震荡市中止盈的 ATR 倍率（较快止盈）",
        type=ParamType.FLOAT,
        default=1.0,
        min_val=0.3,
        max_val=2.0,
        step=0.1,
        group="regime",
        order=5,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    # ── 宏观方向门 ──────────────────────────────────────────────────────
    ParamDef(
        key="decision_policy.macro.aligned_scale",
        label="顺势仓位倍率",
        description="与 Agent 方向一致时的仓位放大系数",
        type=ParamType.FLOAT,
        default=1.1,
        min_val=0.8,
        max_val=1.5,
        step=0.05,
        group="macro",
        order=1,
        risk_level="high",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="decision_policy.macro.countertrend_scale",
        label="逆势仓位倍率",
        description="与 Agent 方向相反时的仓位缩小系数",
        type=ParamType.FLOAT,
        default=0.35,
        min_val=0.0,
        max_val=0.8,
        step=0.05,
        group="macro",
        order=2,
        risk_level="high",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="decision_policy.macro.neutral_scale",
        label="中性仓位倍率",
        description="Agent 方向为 NEUTRAL 时的仓位系数",
        type=ParamType.FLOAT,
        default=1.0,
        min_val=0.5,
        max_val=1.2,
        step=0.05,
        group="macro",
        order=3,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="decision_policy.macro.direction_loss_guard.enabled",
        label="方向亏损保护启用",
        description="Agent 方向连亏时自动收紧",
        type=ParamType.BOOL,
        default=True,
        group="macro",
        order=4,
        risk_level="high",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="decision_policy.macro.direction_loss_guard.aligned_loss_streak_soft",
        label="顺势软限制阈值",
        description="顺势连亏此数后触发仓位软缩放",
        type=ParamType.INT,
        default=2,
        min_val=1,
        max_val=5,
        step=1,
        group="macro",
        order=5,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    ParamDef(
        key="decision_policy.macro.direction_loss_guard.aligned_loss_streak_hard",
        label="顺势硬限制阈值",
        description="顺势连亏此数后触发仓位硬缩放/暂停",
        type=ParamType.INT,
        default=3,
        min_val=2,
        max_val=6,
        step=1,
        group="macro",
        order=6,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
    # ── 策略路由 ────────────────────────────────────────────────────────
    ParamDef(
        key="decision_policy.execution.min_confidence",
        label="路由最低置信度",
        description="策略路由层面的最低综合置信度门槛",
        type=ParamType.FLOAT,
        default=0.15,
        min_val=0.05,
        max_val=0.40,
        step=0.05,
        group="router",
        order=1,
        risk_level="medium",
        runtime_safe="warm",
        favorite=False,
    ),
]


# ═══════════════════════════════════════════════════════════════════════════════
# Utility functions
# ═══════════════════════════════════════════════════════════════════════════════

def get_param_def(key: str) -> Optional[ParamDef]:
    for p in PARAMS:
        if p.key == key:
            return p
    return None


def get_params_by_group() -> dict[str, list[ParamDef]]:
    result: dict[str, list[ParamDef]] = {}
    for p in sorted(PARAMS, key=lambda x: (x.order, x.key)):
        result.setdefault(p.group, []).append(p)
    return result


def get_group_meta(group_key: str) -> dict[str, Any]:
    return GROUPS.get(group_key, {"label": group_key, "icon": "settings", "description": "", "order": 99})


def get_all_groups_sorted() -> list[dict[str, Any]]:
    items = [
        {**meta, "key": key}
        for key, meta in GROUPS.items()
    ]
    items.sort(key=lambda x: x.get("order", 99))
    return items


def get_schema_defaults_flat() -> dict[str, Any]:
    """Return all schema defaults as a flat {param_key: default_value} dict."""
    return {p.key: p.default for p in PARAMS}


def export_schema_defaults_to_v3(v3_path: Optional[str] = None) -> dict[str, Any]:
    """
    Write schema defaults into strategy_rules_v3.json under a _schema_defaults
    section.  Existing rules values are NOT overwritten — this is additive so the
    runtime can still read its own structure while config_manager can consult
    _schema_defaults as the authoritative fallback.

    Returns the defaults dict that was written.
    """
    import json
    from pathlib import Path

    defaults = get_schema_defaults_flat()
    target = Path(v3_path) if v3_path else (
        Path(__file__).resolve().parent.parent / "config" / "strategy_rules_v3.json"
    )

    if target.exists():
        try:
            payload = json.loads(target.read_text(encoding="utf-8"))
        except Exception:
            payload = {}
    else:
        payload = {}

    payload["_schema_defaults"] = defaults
    target.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
    return defaults


def validate_param_value(param_def: ParamDef, value: Any) -> tuple[bool, str]:
    """校验单个参数值是否合法。返回 (ok, reason)。"""
    if param_def.type == ParamType.FLOAT:
        try:
            v = float(value)
        except (TypeError, ValueError):
            return False, f"必须是数字，收到: {value}"
        if param_def.min_val is not None and v < param_def.min_val:
            return False, f"最小值 {param_def.min_val}，收到: {v}"
        if param_def.max_val is not None and v > param_def.max_val:
            return False, f"最大值 {param_def.max_val}，收到: {v}"
        return True, "ok"

    if param_def.type == ParamType.INT:
        try:
            v = int(value)
        except (TypeError, ValueError):
            return False, f"必须是整数，收到: {value}"
        if param_def.min_val is not None and v < param_def.min_val:
            return False, f"最小值 {param_def.min_val}，收到: {v}"
        if param_def.max_val is not None and v > param_def.max_val:
            return False, f"最大值 {param_def.max_val}，收到: {v}"
        return True, "ok"

    if param_def.type == ParamType.BOOL:
        if not isinstance(value, bool):
            return False, f"必须是布尔值，收到: {type(value).__name__}"
        return True, "ok"

    if param_def.type == ParamType.STRING_CHOICE:
        if not isinstance(value, str):
            return False, f"必须是字符串，收到: {type(value).__name__}"
        if param_def.choices and value not in param_def.choices:
            return False, f"无效选项 '{value}'，可选: {param_def.choices}"
        return True, "ok"

    return True, "ok"


def validate_params_batch(updates: dict[str, Any]) -> dict[str, list[str]]:
    """批量校验参数（包含跨参数约束规则）。返回 {param_key / rule_id: [error_msgs]}。"""
    errors: dict[str, list[str]] = {}
    # Phase 1: per-param value validation
    for key, value in updates.items():
        pd = get_param_def(key)
        if pd is None:
            continue
        ok, reason = validate_param_value(pd, value)
        if not ok:
            errors.setdefault(key, []).append(reason)
    # Phase 2: cross-parameter consistency rules
    cross_errors = validate_cross_param_rules(updates)
    for rule_id, msgs in cross_errors.items():
        errors.setdefault(rule_id, []).extend(msgs)
    return errors


def validate_cross_param_rules(updates: dict[str, Any]) -> dict[str, list[str]]:
    """Run cross-parameter consistency rules. Returns {rule_id: [messages]}."""
    _safe_builtins = {
        "float": float, "int": int, "bool": bool, "str": str,
        "True": True, "False": False, "abs": abs, "max": max, "min": min,
    }
    results: dict[str, list[str]] = {}
    for rule in CROSS_PARAM_RULES:
        try:
            triggered = eval(rule.condition, {"__builtins__": _safe_builtins}, {"updates": updates})
        except Exception:
            continue
        if triggered:
            results.setdefault(rule.id, []).append(
                f"[{rule.severity}] {rule.description}. {rule.fix_hint}"
            )
    return results


def compute_params_summary(params: dict[str, Any]) -> dict[str, Any]:
    """生成参数摘要，包含变更风险等级统计。"""
    low = medium = high = critical = 0
    groups_touched: set[str] = set()
    for key in params:
        pd = get_param_def(key)
        if pd is None:
            continue
        groups_touched.add(pd.group)
        if pd.risk_level == "low":
            low += 1
        elif pd.risk_level == "medium":
            medium += 1
        elif pd.risk_level == "high":
            high += 1
        elif pd.risk_level == "critical":
            critical += 1
    needs_paper = any(
        get_param_def(k) and get_param_def(k).requires_paper_mode  # type: ignore
        for k in params
    )
    return {
        "total_params": len(params),
        "low_risk": low,
        "medium_risk": medium,
        "high_risk": high,
        "critical_risk": critical,
        "groups_touched": sorted(groups_touched),
        "requires_paper_mode": needs_paper,
    }


def runtime_safety_check(updates: dict[str, Any], engine_busy: bool, has_positions: bool) -> dict[str, list[str]]:
    """Check whether pending param changes are safe given engine state.

    Returns {block_level: [reason_strings]} — empty dict = all clear.
    block_level is one of "block" (cannot apply), "warn" (can apply but risky).
    """
    result: dict[str, list[str]] = {}
    for key in updates:
        pd = get_param_def(key)
        if pd is None:
            continue
        level = getattr(pd, "runtime_safe", "hot")
        if level == "cold" and (engine_busy or has_positions):
            result.setdefault("block", []).append(
                f"{pd.label} ({key}) 需要平仓后才能修改 (runtime_safe=cold)"
            )
        elif level == "warm" and engine_busy:
            result.setdefault("warn", []).append(
                f"{pd.label} ({key}) 将在下次开仓时生效 (runtime_safe=warm)"
            )
    return result


def export_schema_for_frontend() -> list[dict[str, Any]]:
    """导出参数 schema 供前端消费（生成 TypeScript 兼容 JSON）。"""
    return [
        {
            "key": p.key,
            "label": p.label,
            "description": p.description,
            "type": p.type.value,
            "default": p.default,
            "group": p.group,
            "subgroup": p.subgroup,
            "order": p.order,
            "min_val": p.min_val,
            "max_val": p.max_val,
            "step": p.step,
            "choices": p.choices,
            "unit": p.unit,
            "visible_when": p.visible_when,
            "risk_level": p.risk_level,
            "requires_paper_mode": p.requires_paper_mode,
            "advanced": p.advanced,
            "runtime_safe": p.runtime_safe,
            "favorite": p.favorite,
        }
        for p in PARAMS
    ]
