"""策略生成器：把"偏好参数"翻译成一套完整、可回测、可执行的多因子策略规格。

这是整个工具的核心卖点：用户只说人话级别的偏好（风险偏好 / 持有周期 /
风格倾向 / 持仓数量），生成器负责推导出全部具体参数并给出推导说明。
"""
from __future__ import annotations

import copy
import datetime
from typing import Dict, List, Optional

# ---------------- 偏好参数定义域 ----------------

RISK_LEVELS = {
    "保守": dict(target_vol=0.06, max_dd_derisk=0.08, max_positions=20, max_weight=0.08,
                 low_vol_bonus=0.20, cash_buffer=0.15, desc="低波动优先，深度分散，回撤容忍度低"),
    "稳健": dict(target_vol=0.10, max_dd_derisk=0.12, max_positions=15, max_weight=0.10,
                 low_vol_bonus=0.12, cash_buffer=0.10, desc="波动可控，适度分散"),
    "平衡": dict(target_vol=0.15, max_dd_derisk=0.18, max_positions=12, max_weight=0.12,
                 low_vol_bonus=0.05, cash_buffer=0.05, desc="收益风险均衡"),
    "进取": dict(target_vol=0.20, max_dd_derisk=0.25, max_positions=10, max_weight=0.15,
                 low_vol_bonus=0.00, cash_buffer=0.02, desc="波动换收益，适度集中"),
    "激进": dict(target_vol=0.28, max_dd_derisk=0.35, max_positions=8, max_weight=0.20,
                 low_vol_bonus=-0.05, cash_buffer=0.00, desc="高波动容忍，集中持仓"),
}

HORIZON_LEVELS = {
    "短线": dict(rebalance_days=5, momentum_lookback=20, trend_lookback=20,
                 reversal_weight=0.20, fwd_days=5, desc="周度调仓，短周期动量+反转"),
    "波段": dict(rebalance_days=10, momentum_lookback=60, trend_lookback=60,
                 reversal_weight=0.10, fwd_days=10, desc="双周调仓，中短周期"),
    "中线": dict(rebalance_days=20, momentum_lookback=120, trend_lookback=120,
                 reversal_weight=0.00, fwd_days=20, desc="月度调仓，经典中期动量"),
    "长线": dict(rebalance_days=60, momentum_lookback=180, trend_lookback=200,
                 reversal_weight=0.00, fwd_days=60, desc="季度调仓，长周期趋势"),
    "半年": dict(rebalance_days=120, momentum_lookback=250, trend_lookback=250,
                 reversal_weight=0.00, fwd_days=120, desc="半年调仓，年度级别趋势"),
}

# 默认风格权重（会在风险偏好与周期推导后做调整再归一化）
DEFAULT_STYLE = {
    "momentum": 0.30,
    "quality_momentum": 0.15,
    "low_vol": 0.20,
    "trend": 0.20,
    "reversal": 0.05,
    "high_52w": 0.05,
    "liquidity": 0.05,
}

FACTOR_LOOKBACKS = {
    "momentum": None,           # 跟随周期推导
    "quality_momentum": 120,
    "reversal": 5,
    "low_vol": 60,
    "trend": None,              # 跟随周期推导
    "high_52w": 250,
    "liquidity": 20,
}

ALLOWED_FACTORS = set(DEFAULT_STYLE)


def normalize_style(style: Dict[str, float]) -> Dict[str, float]:
    """校验并归一化风格权重。"""
    unknown = set(style) - ALLOWED_FACTORS
    if unknown:
        raise ValueError(f"未知因子: {unknown}，可选: {sorted(ALLOWED_FACTORS)}")
    vals = {k: float(v) for k, v in style.items() if float(v) > 0}
    total = sum(vals.values())
    if total <= 0:
        raise ValueError("风格权重总和必须为正")
    return {k: v / total for k, v in vals.items()}


def generate_strategy(
    risk: str = "平衡",
    horizon: str = "波段",
    style: Optional[Dict[str, float]] = None,
    max_positions: Optional[int] = None,
    rebalance_days: Optional[int] = None,
    cash_buffer: Optional[float] = None,
    benchmark: str = "000300",
    universe: str = "sample",
    start: str = "2022-01-01",
    end: str = "2030-01-01",
    execution: Optional[Dict[str, float]] = None,
    extra_risk: Optional[Dict] = None,
    name: Optional[str] = None,
) -> Dict:
    """由偏好参数生成完整策略规格（dict，可 json 序列化）。

    Args:
        risk: 保守/稳健/平衡/进取/激进
        horizon: 短线/波段/中线/长线
        style: 因子权重覆盖（如 {"momentum":0.4,"low_vol":0.3,"trend":0.3}）
        max_positions / rebalance_days / cash_buffer: 手工覆盖项
        execution: 交易成本 {"commission":双边佣金率单边, "stamp_tax_sell":卖出印花税,
                            "slippage":单边滑点}
        extra_risk: 风控覆盖 {"trend_filter":bool, "de_risk_ratio":0.5,
                              "min_history":120, "top_select_from":None}
    """
    if risk not in RISK_LEVELS:
        raise ValueError(f"风险偏好必须为 {list(RISK_LEVELS)}")
    if horizon not in HORIZON_LEVELS:
        raise ValueError(f"持有周期必须为 {list(HORIZON_LEVELS)}")

    r = copy.deepcopy(RISK_LEVELS[risk])
    h = copy.deepcopy(HORIZON_LEVELS[horizon])

    # 风格权重：默认 -> 风险偏好微调（越保守越偏低波，越激进越带动量）-> 用户覆盖
    style_w = copy.deepcopy(DEFAULT_STYLE)
    style_w["low_vol"] = max(0.0, style_w["low_vol"] + r.pop("low_vol_bonus"))
    style_w["reversal"] = h["reversal_weight"]
    if style:
        style_w.update(style)
    style_w = normalize_style(style_w)

    lookbacks = {}
    for f, lb in FACTOR_LOOKBACKS.items():
        if f == "momentum":
            lookbacks[f] = h["momentum_lookback"]
        elif f == "trend":
            lookbacks[f] = h["trend_lookback"]
        else:
            lookbacks[f] = lb

    strat = {
        "name": name or f"{risk}{horizon}多因子",
        "generated_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
        "preferences": {
            "risk": risk,
            "risk_desc": r["desc"],
            "horizon": horizon,
            "horizon_desc": h["desc"],
            "style_request": style or "(默认风格)",
        },
        "universe": {"name": universe, "benchmark": benchmark},
        "backtest_window": {"start": start, "end": end},
        "selection": {
            "factors": style_w,
            "factor_lookbacks": lookbacks,
            "max_positions": max_positions or r["max_positions"],
            "min_history": 120,
        },
        "portfolio": {
            "rebalance_days": rebalance_days or h["rebalance_days"],
            "cash_buffer": cash_buffer if cash_buffer is not None else r["cash_buffer"],
            "max_weight": r["max_weight"],
            "fwd_days": h["fwd_days"],
        },
        "risk_control": {
            "target_vol": r["target_vol"],
            "vol_lookback": 60,
            "max_drawdown_derisk": r["max_dd_derisk"],
            "de_risk_ratio": (extra_risk or {}).get("de_risk_ratio", 0.5),
            "trend_filter": (extra_risk or {}).get("trend_filter", True),
            "trend_filter_ma": 200,
        },
        "execution": {
            "commission": (execution or {}).get("commission", 0.0003),
            "stamp_tax_sell": (execution or {}).get("stamp_tax_sell", 0.0005),
            "slippage": (execution or {}).get("slippage", 0.001),
        },
    }
    return strat


def explain(strategy: Dict) -> List[str]:
    """生成人类可读的推导说明。"""
    p, sel, port, rc = strategy["preferences"], strategy["selection"], strategy["portfolio"], strategy["risk_control"]
    lines = [
        f"策略【{strategy['name']}】生成于 {strategy['generated_at']}",
        f"1. 风险偏好「{p['risk']}」：{p['risk_desc']}",
        f"   → 目标波动率 {rc['target_vol']:.0%}，组合回撤超过 {rc['max_drawdown_derisk']:.0%} 自动降仓 "
        f"{rc['de_risk_ratio']:.0%}；单票上限 {port['max_weight']:.0%}；持仓 {sel['max_positions']} 只；现金缓冲 {port['cash_buffer']:.0%}",
        f"2. 持有周期「{p['horizon']}」：{p['horizon_desc']}",
        f"   → 每 {port['rebalance_days']} 个交易日调仓；动量回看 {sel['factor_lookbacks']['momentum']} 日；"
        f"趋势回看 {sel['factor_lookbacks']['trend']} 日",
        "3. 因子权重（截面 Z-score 加权合成）:",
        "   " + "，".join(f"{k} {v:.0%}" for k, v in sorted(sel["factors"].items(), key=lambda x: -x[1])),
    ]
    if rc["trend_filter"]:
        lines.append(f"4. 风控过滤：价格低于 {rc['trend_filter_ma']} 日均线的标的禁止入选（熊市保护）")
    ex = strategy["execution"]
    lines.append(
        f"5. 交易成本假设：佣金 {ex['commission']:.2%}/单边 + 卖出印花税 {ex['stamp_tax_sell']:.2%} + 滑点 {ex['slippage']:.2%}/单边"
    )
    return lines
