#!/usr/bin/env python3
"""
strategy_evolution.py -- Anti-overfitting strategy evolution engine.
Reads offline training results + real trading ledger -> outputs parameter change suggestions.
"""
import json, sys, pathlib, os
from datetime import datetime
from collections import defaultdict

# ROOT now resolved via frozen_path
from frozen_path import APP_ROOT as ROOT
sys.path.insert(0, str(ROOT))
import config_manager
from src.agent_role_router import ROLE_M2_BRAIN

APP_DIR = config_manager.get_app_dir()
DATA_DIR = config_manager.get_data_dir()
LOGS_DIR = config_manager.get_logs_dir()

CONFIG_FILE  = APP_DIR / "config" / "strategy_rules_v3.json"
STATS_FILE   = DATA_DIR / "offline" / "offline_stats.json"
LEDGER_FILE  = LOGS_DIR / "trading_ledger.ndjson"
PAPER_FILE   = LOGS_DIR / "paper_mode.json"
EVOLUTION_APPLIED = LOGS_DIR / "evolution_applied.json"
EVOLUTION_REPORT  = LOGS_DIR / "evolution_report_latest.json"


# =====================================================================
# Anti-overfitting configuration
# =====================================================================

def _load_overfit_guards() -> dict:
    """Load overfit protection thresholds from strategy_rules_v3.json.
    Falls back to hardcoded defaults if config file is unavailable."""
    defaults = {
        "min_trades_to_disable_direction": 50,
        "min_days_to_disable_direction": 30,
        "min_trades_to_change_confidence": 20,
        "min_days_to_change_confidence": 15,
        "min_trades_to_change_sl": 30,
        "min_improvement_pct": 2.0,
        "max_confidence_change": 0.05,
        "max_lots_change": 0.05,
        "max_sl_atr_change": 0.3,
        "max_tp_atr_change": 0.3,
        "max_params_per_change": 2,
        "cooldown_days": 7,
        "paper_mode_days": 3,
        "paper_pass_winrate": 45,
    }
    try:
        from config_manager import get_app_dir as _get_app_dir
        cfg_path = _get_app_dir() / "config" / "strategy_rules_v3.json"
        if cfg_path.exists():
            import json as _json
            cfg = _json.loads(cfg_path.read_text(encoding="utf-8"))
            json_guards = cfg.get("overfit_guards", {})
            if isinstance(json_guards, dict):
                defaults.update({k: v for k, v in json_guards.items() if k in defaults})
    except Exception:
        pass
    return defaults


OVERFIT_GUARDS = _load_overfit_guards()


# =====================================================================
# Utilities
# =====================================================================

def _now():
    return datetime.now().strftime("%Y-%m-%dT%H:%M:%S+08:00")

def load_json(path, default=None):
    if not path.exists():
        return default or {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return default or {}

def load_lines(path):
    if not path.exists():
        return []
    try:
        return [json.loads(l) for l in path.read_text(encoding="utf-8").strip().split("\n") if l.strip()]
    except Exception:
        return []

def pct(n, d):
    return round(n / max(d, 1) * 100, 2)


def normalize_offline_stats(payload: dict | None) -> dict:
    """Accept offline_stats.json or best_params.json and return flat stats."""
    if not isinstance(payload, dict):
        return {}
    if isinstance(payload.get("stats"), dict):
        stats = dict(payload.get("stats", {}))
        stats.setdefault("last_updated", payload.get("last_updated", ""))
        return stats
    return dict(payload)


def load_offline_stats() -> dict:
    """Load evolution input with fallback from offline_stats.json to best_params.json."""
    offline = normalize_offline_stats(load_json(STATS_FILE))
    if offline:
        return offline
    best_file = DATA_DIR / "offline" / "best_params.json"
    return normalize_offline_stats(load_json(best_file))


# =====================================================================
# Cooldown check
# =====================================================================

def check_cooldown(param_name):
    """检查某参数是否在冷却期（上次调整后未满7天）"""
    history = load_json(EVOLUTION_APPLIED, [])
    if isinstance(history, dict):
        history = history.get("applied", [])
    for record in reversed(history):
        for change in record.get("changes", []):
            if change.get("param") == param_name:
                from datetime import datetime as dt
                applied_at = dt.fromisoformat(change["applied_at"].replace("Z", "+00:00").replace("+08:00", "+00:00"))
                days_since = (dt.now() - applied_at).days
                if days_since < OVERFIT_GUARDS["cooldown_days"]:
                    print(f"  [cooldown] {param_name} 冷却中，{OVERFIT_GUARDS['cooldown_days'] - days_since}天后可调整")
                    return False
    return True


# =====================================================================
# Walk-Forward validation
# =====================================================================

def walkforward_validate(trades, baseline_wr):
    """
    Walk-Forward 分段验证：
    训练期(70%) / 验证期(30%)
    只在验证期胜率显著优于基准时才采纳
    """
    if len(trades) < 20:
        return {"pass": False, "reason": f"样本不足({len(trades)}笔<20)", "validated_wr": 0}

    split = int(len(trades) * 0.7)
    train, validate = trades[:split], trades[split:]

    if len(validate) < 10:
        return {"pass": False, "reason": f"验证期样本不足({len(validate)}笔<10)", "validated_wr": 0}

    wins_v = sum(1 for t in validate if t.get("pnl", 0) > 0)
    validated_wr = pct(wins_v, len(validate))
    improvement = validated_wr - baseline_wr

    return {
        "pass": improvement >= OVERFIT_GUARDS["min_improvement_pct"],
        "reason": f"验证期胜率 {validated_wr:.1f}% vs 基准 {baseline_wr:.1f}% "
                  f"(改善 {improvement:+.1f}%, 需>={OVERFIT_GUARDS['min_improvement_pct']:.1f}%)",
        "validated_wr": validated_wr,
        "train_wr": pct(sum(1 for t in train if t.get("pnl", 0) > 0), len(train)),
        "improvement": improvement,
    }


# =====================================================================
# Minimum sample requirements
# =====================================================================

def check_min_requirements(action, trades, days):
    """检查建议是否满足最小样本门槛"""
    g = OVERFIT_GUARDS
    if "DISABLE" in action:
        if len(trades) < g["min_trades_to_disable_direction"]:
            return False, f"样本不足({len(trades)}<{g['min_trades_to_disable_direction']})"
        if days < g["min_days_to_disable_direction"]:
            return False, f"天数不足({days}<{g['min_days_to_disable_direction']}天)"
    elif "CONFIDENCE" in action:
        if len(trades) < g["min_trades_to_change_confidence"]:
            return False, f"样本不足({len(trades)}<{g['min_trades_to_change_confidence']})"
    elif "SL" in action or "TP" in action:
        if len(trades) < g["min_trades_to_change_sl"]:
            return False, f"样本不足({len(trades)}<{g['min_trades_to_change_sl']})"
    return True, "ok"


# =====================================================================
# Conservative change check
# =====================================================================

def check_conservative_change(action, current_val, new_val):
    """检查参数变化幅度是否过于激进"""
    g = OVERFIT_GUARDS
    try:
        cur_f = float(current_val)
        new_f = float(new_val)
    except (TypeError, ValueError):
        return True, "ok"

    if "CONFIDENCE" in action:
        delta = abs(new_f - cur_f)
        if delta > g["max_confidence_change"]:
            return False, f"调整幅度过大({delta:.3f}>{g['max_confidence_change']})"
    elif "LOTS" in action:
        delta = abs(new_f - cur_f)
        if delta > g["max_lots_change"]:
            return False, f"调整幅度过大({delta:.3f}>{g['max_lots_change']})"
    return True, "ok"


# =====================================================================
# Paper Mode
# =====================================================================

def apply_with_paper_mode(sug):
    """
    重大调整(DISABLE方向类) -> Paper Mode 3天
    微调(置信度/仓位) -> 直接应用
    """
    paper_actions = {"DISABLE_BUY", "DISABLE_SELL", "DISABLE_TREND_MODE", "DISABLE_RANGE_MODE"}
    cautious_actions = {"RAISE_CONFIDENCE", "LOWER_CONFIDENCE"}

    if sug.get("action") in paper_actions:
        paper_cfg = {
            "mode": "PAPER",
            "param": sug["param"],
            "suggested_value": sug["suggested_value"],
            "reason": sug["reason"],
            "applied_at": _now(),
            "paper_days": OVERFIT_GUARDS["paper_mode_days"],
            "paper_pass_winrate": OVERFIT_GUARDS["paper_pass_winrate"],
            "status": "PAPER_TESTING",
        }
        PAPER_FILE.write_text(json.dumps(paper_cfg, indent=2))
        print(f"  [paper] 进入 Paper Mode: {sug['action']} {sug['param']}={sug['suggested_value']}")
        return {**sug, "_paper_mode": True, "_live_apply": False}

    return {**sug, "_paper_mode": False, "_live_apply": True}


# =====================================================================
# Core evolution logic
# =====================================================================

def generate_evolution_report(offline, real_trades):
    """
    生成策略进化建议（带六层防过拟合保护）
    """
    off_by_side = offline.get("by_side", {})
    off_by_regime = offline.get("by_regime", {})
    off_wr = offline.get("win_rate", 0)
    off_rr = offline.get("rr_ratio", 0)
    off_pnl = offline.get("total_pnl", 0)

    try:
        cfg = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
    except Exception:
        cfg = {}
    risk_cfg = cfg.get("risk_governor", {})
    pos_cfg = cfg.get("position_sizing", {})
    current_conf = risk_cfg.get("min_confidence", 0.68)
    current_max_lots = pos_cfg.get("max_lots", 0.20)

    all_trades = real_trades or []
    baseline_wr = pct(sum(1 for t in all_trades if t.get("pnl", 0) > 0), len(all_trades)) if all_trades else off_wr

    suggestions = []
    applied_record = {"timestamp": _now(), "changes": [], "paper_mode": []}

    buy_pnl  = off_by_side.get("BUY",  {}).get("pnl", 0)
    sell_pnl = off_by_side.get("SELL", {}).get("pnl", 0)
    trend_pnl = off_by_regime.get("TRENDING", {}).get("pnl", 0)
    range_pnl = off_by_regime.get("RANGING",   {}).get("pnl", 0)

    # EV001: 方向禁用 (需要足够样本)
    if buy_pnl < -5000 and sell_pnl > 5000:
        if len(all_trades) >= OVERFIT_GUARDS["min_trades_to_disable_direction"]:
            if check_cooldown("entry_direction_filter.trend_mode_long_filter"):
                sug = {
                    "id": "EV001", "priority": "HIGH", "action": "DISABLE_BUY",
                    "param": "entry_direction_filter.trend_mode_long_filter",
                    "current_value": "ALLOW", "suggested_value": "BLOCK",
                    "reason": f"BUY亏损${buy_pnl:.0f}，SELL盈利${sell_pnl:.0f}，方向严重偏科",
                    "auto_apply": False, "m2_review": True, "source": "offline",
                }
                ok, _ = check_min_requirements(sug["action"], all_trades, 30)
                if ok:
                    wf = walkforward_validate(all_trades, baseline_wr)
                    sug["_wf_pass"] = wf["pass"]
                    sug["_wf_reason"] = wf["reason"]
                    if not wf["pass"]:
                        sug["reason"] += f" | WF验证未通过: {wf['reason']}"
                    suggestions.append(sug)

    # EV002: 格局策略切换
    if trend_pnl < -2000 and range_pnl > 0 and len(all_trades) >= 80:
        if check_cooldown("entry_direction_filter.disable_trend_mode"):
            sug = {
                "id": "EV002", "priority": "HIGH", "action": "DISABLE_TREND_MODE",
                "param": "entry_direction_filter.disable_trend_mode",
                "current_value": False, "suggested_value": True,
                "reason": f"趋势策略亏损${trend_pnl:.0f}，震荡盈利${range_pnl:.0f}",
                "auto_apply": False, "m2_review": True, "source": "offline",
            }
            ok, _ = check_min_requirements(sug["action"], all_trades, 45)
            if ok:
                wf = walkforward_validate(all_trades, baseline_wr)
                sug["_wf_pass"] = wf["pass"]
                sug["_wf_reason"] = wf["reason"]
                if not wf["pass"]:
                    sug["reason"] += f" | WF验证未通过: {wf['reason']}"
                suggestions.append(sug)

    # EV003: 置信度提高 (保守调整)
    if off_wr < 40 and len(all_trades) >= OVERFIT_GUARDS["min_trades_to_change_confidence"]:
        if check_cooldown("risk_governor.min_confidence"):
            new_conf = round(min(current_conf + OVERFIT_GUARDS["max_confidence_change"], 0.85), 2)
            ok, reason = check_conservative_change("RAISE_CONFIDENCE", current_conf, new_conf)
            if ok:
                suggestions.append({
                    "id": "EV003", "priority": "HIGH", "action": "RAISE_CONFIDENCE",
                    "param": "risk_governor.min_confidence",
                    "current_value": current_conf, "suggested_value": new_conf,
                    "reason": f"胜率{off_wr:.1f}%<40%，提高门槛减少假信号",
                    "auto_apply": False, "m2_review": True, "source": "offline",
                })

    # EV004: 置信度降低 (需要 M2 审批)
    elif off_wr > 55 and len(all_trades) >= OVERFIT_GUARDS["min_trades_to_change_confidence"]:
        if check_cooldown("risk_governor.min_confidence"):
            new_conf = round(max(current_conf - OVERFIT_GUARDS["max_confidence_change"], 0.60), 2)
            ok, _ = check_conservative_change("LOWER_CONFIDENCE", current_conf, new_conf)
            if ok:
                suggestions.append({
                    "id": "EV004", "priority": "MEDIUM", "action": "LOWER_CONFIDENCE",
                    "param": "risk_governor.min_confidence",
                    "current_value": current_conf, "suggested_value": new_conf,
                    "reason": f"胜率{off_wr:.1f}%>55%，可降低门槛捕捉更多机会",
                    "auto_apply": False, "m2_review": True, "source": "offline",
                    "_paper_mode": False, "_live_apply": False,
                })

    # EV005: 市场状态感知参数切换 (regime-aware parameter switching)
    trend_wr = off_by_regime.get("TRENDING", {}).get("win_rate", 0) or 0
    range_wr = off_by_regime.get("RANGING", {}).get("win_rate", 0) or 0
    trend_trades = off_by_regime.get("TRENDING", {}).get("trades", 0) or 0
    range_trades = off_by_regime.get("RANGING", {}).get("trades", 0) or 0

    if trend_trades >= 20 and range_trades >= 20:
        if abs(trend_wr - range_wr) > 15:
            worse_name = "RANGING" if range_wr < trend_wr else "TRENDING"
            better_name = "TRENDING" if worse_name == "RANGING" else "RANGING"
            worse_wr_val = range_wr if worse_name == "RANGING" else trend_wr
            better_wr_val = trend_wr if better_name == "TRENDING" else range_wr
            param_key = f"regime_param_boost_{worse_name.lower()}"
            if check_cooldown(param_key):
                sug = {
                    "id": "EV005",
                    "priority": "MEDIUM",
                    "action": "REGIME_SWITCH_PARAMS",
                    "param": param_key,
                    "current_value": 1.0,
                    "suggested_value": 0.7,
                    "reason": (
                        f"{worse_name}胜率{worse_wr_val:.1f}%远低于{better_name}"
                        f"胜率{better_wr_val:.1f}%，建议{worse_name}参数降权30%"
                    ),
                    "auto_apply": False,
                    "m2_review": True,
                    "source": "offline",
                }
                suggestions.append(sug)

    # EV006: 策略分配权重动态调整 (allocation rebalancing)
    recent_losses = [t for t in all_trades[-20:] if float(t.get("pnl", 0) or 0) < 0]
    if len(all_trades) >= 30 and len(recent_losses) >= 8:
        param_key = "position_sizing.max_lots"
        if check_cooldown(f"{param_key}_ev006"):
            current_lots = float(pos_cfg.get("max_lots", 0.05))
            new_lots = round(max(current_lots * 0.7, 0.01), 2)
            ok, reason_msg = check_conservative_change(
                "ADJUST_ALLOCATION", current_lots, new_lots
            )
            if ok:
                suggestions.append({
                    "id": "EV006",
                    "priority": "HIGH",
                    "action": "ADJUST_ALLOCATION",
                    "param": param_key,
                    "current_value": current_lots,
                    "suggested_value": new_lots,
                    "reason": (
                        f"近20笔中{len(recent_losses)}笔亏损，"
                        f"自动缩减仓位{current_lots}→{new_lots}"
                    ),
                    "auto_apply": False,
                    "m2_review": True,
                    "source": "offline",
                    "_paper_mode": True,
                })

    # EV007: 胜率趋势微调置信度 (gradual confidence tuning)
    if len(all_trades) >= 40:
        recent_30 = all_trades[-30:]
        wr_30 = pct(
            sum(1 for t in recent_30 if float(t.get("pnl", 0) or 0) > 0),
            len(recent_30),
        )
        wr_all = pct(
            sum(1 for t in all_trades if float(t.get("pnl", 0) or 0) > 0),
            len(all_trades),
        )
        wr_delta = wr_30 - wr_all
        if abs(wr_delta) > 10 and check_cooldown("risk_governor.min_confidence_ev007"):
            direction = "RAISE" if wr_delta < -10 else "LOWER"
            current = risk_cfg.get("min_confidence", 0.68)
            delta = min(OVERFIT_GUARDS["max_confidence_change"], abs(wr_delta) / 200.0)
            new_val = round(
                current + delta if direction == "RAISE" else current - delta,
                2,
            )
            new_val = max(0.60, min(0.85, new_val))
            ok, _ = check_conservative_change(
                "GRADUAL_CONFIDENCE", current, new_val
            )
            if ok:
                suggestions.append({
                    "id": "EV007",
                    "priority": "LOW",
                    "action": "GRADUAL_CONFIDENCE",
                    "param": "risk_governor.min_confidence",
                    "current_value": current,
                    "suggested_value": new_val,
                    "reason": (
                        f"近30笔胜率{wr_30:.1f}% vs 全部{wr_all:.1f}% "
                        f"(Δ{wr_delta:+.1f}%)，{'提高' if direction == 'RAISE' else '降低'}门槛"
                    ),
                    "auto_apply": False,
                    "m2_review": True,
                    "source": "offline",
                    "_paper_mode": True,
                })

    # 限制同时调整参数数量
    auto_sug = [s for s in suggestions if s.get("auto_apply")]
    if len(auto_sug) > OVERFIT_GUARDS["max_params_per_change"]:
        for s in auto_sug[OVERFIT_GUARDS["max_params_per_change"]:]:
            s["auto_apply"] = False
            s["m2_review"] = True
            s["reason"] += " | 超过同时调整上限，延后处理"

    high   = [s for s in suggestions if s.get("priority") == "HIGH"]
    medium = [s for s in suggestions if s.get("priority") == "MEDIUM"]
    low    = [s for s in suggestions if s.get("priority") == "LOW"]
    auto   = [s for s in suggestions if s.get("auto_apply")]
    paper  = [s for s in suggestions if s.get("_paper_mode")]
    live   = [s for s in suggestions if s.get("_live_apply")]

    return {
        "timestamp": _now(),
        "baseline_win_rate": round(baseline_wr, 2),
        "offline_stats": {
            "win_rate": off_wr, "rr_ratio": off_rr, "total_pnl": off_pnl,
            "buy_pnl": buy_pnl, "sell_pnl": sell_pnl,
            "trend_pnl": trend_pnl, "range_pnl": range_pnl,
            "real_trades": len(all_trades),
        },
        "suggestions": suggestions,
        "guard_summary": {
            "total": len(suggestions),
            "high": len(high), "medium": len(medium), "low": len(low),
            "auto_apply": len(auto),
            "paper_mode": len(paper),
            "direct_live": len(live),
        },
        "_applied_record": applied_record,
    }


# =====================================================================
# Apply suggestions
# =====================================================================

def apply_suggestions(report):
    """
    保存建议报告，不再自动写入任何配置。
    所有参数调整必须经过人工审批才能生效。
    这是防止历史数据过拟合导致实盘亏损的关键安全阀。
    """
    applied_record = report.get("_applied_record", {})
    applied_record["timestamp"] = _now()
    applied_record["status"] = "PENDING_REVIEW"
    applied_record["note"] = "所有建议已保存，等待人工审批后生效"

    # evolution_applied.json 统一为 list 格式
    try:
        raw = json.loads(EVOLUTION_APPLIED.read_text(encoding="utf-8"))
    except Exception:
        raw = []
    if not isinstance(raw, list):
        raw = []
    raw.append(applied_record)
    raw = raw[-20:]
    EVOLUTION_APPLIED.write_text(json.dumps(raw, indent=2))
    print(f"  [review] 建议已保存，共 {len(report['suggestions'])} 条待审批")
    return report["suggestions"]


# =====================================================================
# DeepSeek R1 Analysis
# =====================================================================

def call_deepseek_analysis(report: dict) -> dict:
    """
    调用 LLM（通过统一模块）对策略建议（或纯离线数据）进行深度分析，生成审批报告。
    在人工审批之前调用，是关键的安全把关层。
    即使没有建议，也会分析离线数据并给出策略健康度评估。
    """
    s = report.get("offline_stats", {})
    suggestions = report.get("suggestions", [])

    # 构建分析 prompt
    prompt = f"""你是一位专业的外汇/黄金量化交易策略师，负责分析策略参数调整建议的风险和合理性。

## 当前策略表现（离线训练30天）
- 胜率: {s.get('win_rate', 0):.1f}%
- 盈亏比: {s.get('rr_ratio', 0):.2f}
- 总盈亏: ${s.get('total_pnl', 0):.2f}
- BUY 盈亏: ${s.get('buy_pnl', 0):.2f}
- SELL 盈亏: ${s.get('sell_pnl', 0):.2f}
- 趋势策略盈亏: ${s.get('trend_pnl', 0):.2f}
- 震荡策略盈亏: ${s.get('range_pnl', 0):.2f}
- 基准胜率: {report.get('baseline_win_rate', 0):.1f}%
- 实盘交易笔数: {s.get('real_trades', 0)}

## 策略建议（共 {len(suggestions)} 条）
"""

    if suggestions:
        for sug in suggestions:
            prompt += f"""
### {sug['id']} [{sug['priority']}]
- 操作: {sug['action']}
- 参数: {sug['param']}
- 当前值: {sug.get('current_value', 'N/A')} → 建议值: {sug.get('suggested_value', 'N/A')}
- 原因: {sug['reason']}
- Walk-Forward验证: {sug.get('_wf_reason', '未验证')}
"""
    else:
        prompt += "\n（暂无待审批建议，仅有离线训练数据）\n"

    prompt += """
## 分析要求

请对每条建议（或离线数据）进行以下维度的深度分析：

1. **合理性**: 这个建议是否有统计学依据？样本是否足够？
2. **风险**: 如果执行，最坏情况下会发生什么？
3. **过拟合风险**: 30天数据是否足以支撑这个结论？是否存在过拟合可能？
4. **替代方案**: 是否有更保守的应对方式？
5. **审批建议**: APPROVE / CONDITIONAL / REJECT，并给出理由

最终输出一份完整的审批/分析报告：
```
## LLM 分析报告

### 总体评估
[整体风险等级: 高/中/低，原因]

### 建议逐条分析（若无建议则为策略健康度分析）
[每条的详细分析 或 离线数据反映的问题]

### 最终审批意见
| ID | 审批结果 | 条件/理由 |
|----|---------|-----------|
| EV001 | APPROVE/REJECT/CONDITIONAL | ... |

### 风险提示
[如果执行，需要特别注意什么；如无建议，则为下一步观察建议]
```
"""

    try:
        from src.llm import call_agent_llm

        result = call_agent_llm(
            role=ROLE_M2_BRAIN,
            system_prompt="你是一位专业的外汇/黄金量化交易策略师，负责深度分析策略参数调整建议。",
            user_prompt=prompt,
            temperature=0.1,
            max_tokens=1500,
            timeout=60,
        )

        if result.get("degraded"):
            return {"model": result.get("model", ""), "analysis": f"[LLM 调用失败: {result.get('degraded_reason', 'unknown')}]", "timestamp": _now()}

        analysis = result["content"]
        return {
            "model": result.get("model", ""),
            "prompt_tokens": 0,
            "analysis": analysis,
            "timestamp": _now(),
        }
    except Exception as e:
        print(f"  [llm] 警告: {e}")
        return {"model": "", "analysis": f"[LLM API 调用失败: {e}]", "timestamp": _now()}


# =====================================================================
# Main
# =====================================================================

def main():
    print("[strategy_evolution] 启动防过拟合策略进化...")

    offline = load_offline_stats()
    if not offline:
        print("[evolution] 错误: 无法读取 offline_stats.json / best_params.json"); sys.exit(1)

    real_trades = load_lines(LEDGER_FILE)
    report = generate_evolution_report(offline, real_trades)
    suggestions = apply_suggestions(report)

    report_clean = {k: v for k, v in report.items() if not k.startswith("_")}
    EVOLUTION_REPORT.write_text(json.dumps(report_clean, indent=2, ensure_ascii=False))

    g = report["guard_summary"]
    s = report["offline_stats"]

    print(f"\n=== Strategy Evolution Report (Anti-Overfitting) ===")
    print(f"Baseline WR: {report['baseline_win_rate']:.1f}%")
    print(f"Offline: WR={s['win_rate']:.1f}% RR={s['rr_ratio']:.2f} PnL=${s['total_pnl']:.2f}")
    print(f"Real trades: {s['real_trades']} | Suggestions: {g['total']} (H:{g['high']} M:{g['medium']} L:{g['low']})")

    # DeepSeek R1 深度分析（无论是否有建议都调用）
    print(f"\n[llm] 调用 DeepSeek R1 进行深度分析...")
    llm_report = call_deepseek_analysis(report)
    analysis_text = llm_report["analysis"]

    llm_report_file = EVOLUTION_REPORT.parent / "evolution_llm_report_latest.txt"
    llm_report_file.write_text(analysis_text, encoding="utf-8")
    print(f"[llm] 分析完成，已保存到 {llm_report_file}")

    print(f"\n{'='*60}")
    print("DeepSeek R1 分析报告:")
    print(f"{'='*60}")
    print(analysis_text[:1200])
    if len(analysis_text) > 1200:
        print(f"\n... [完整报告已保存到 {llm_report_file}]")

    if report["suggestions"]:
        print(f"\n{'='*60}")
        print("待人工审批的建议:")
        print(f"{'='*60}")
        for sug in report["suggestions"]:
            wf = f" | WF:{sug.get('_wf_reason', '')}" if sug.get("_wf_reason") else ""
            print(f"  [REVIEW] {sug['id']} {sug['action']}: {sug['reason'][:70]}{wf}")
    else:
        print(f"\n无待审批建议（实盘交易笔数未达阈值，或参数处于冷却期）。")

    print(f"\n所有建议已保存，待人工审批。")
    print(f"进化报告: {EVOLUTION_REPORT}")
    print(f"LLM分析报告: {llm_report_file}")
    return report


def run_analyze_only() -> None:
    """仅执行 LLM 深度分析（被 gui_backend --analyze 子进程调用）。"""
    print("[strategy_evolution] 仅执行 LLM 深度分析...")
    report = load_json(EVOLUTION_REPORT)
    if not report:
        print("未找到基础演化报告数据。")
        sys.exit(1)
    llm_report = call_deepseek_analysis(report)
    llm_report_file = EVOLUTION_REPORT.parent / "evolution_llm_report_latest.txt"
    llm_report_file.write_text(llm_report["analysis"], encoding="utf-8")
    print(f"LLM 分析报告:\n{llm_report['analysis']}")


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--analyze-only", action="store_true", help="只进行 LLM 分析，不重新生成建议")
    args = parser.parse_args()

    if args.analyze_only:
        run_analyze_only()
    else:
        main()
