#!/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 = pathlib.Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
import config_manager

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
# =====================================================================
OVERFIT_GUARDS = {
    "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,          # 调整后胜率需比基准高 2% 才采纳
    "max_confidence_change": 0.05,         # 每次最多 +-0.05
    "max_lots_change": 0.05,
    "max_sl_atr_change": 0.3,
    "max_tp_atr_change": 0.3,
    "max_params_per_change": 2,            # 每次最多调整 2 个参数
    "cooldown_days": 7,                   # 调整后稳定 7 天才能再改
    "paper_mode_days": 3,                 # Paper Mode 3 天
    "paper_pass_winrate": 45,             # Paper Mode 胜率需 >= 45% 才切 Live
}


# =====================================================================
# 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())
    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().strip().split("\n") if l.strip()]
    except Exception:
        return []

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


# =====================================================================
# 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)

    cfg = json.loads(CONFIG_FILE.read_text())
    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,
                })

    # 限制同时调整参数数量
    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())
    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
# =====================================================================

DEEPSEEK_MODEL = "deepseek/deepseek-reasoner"

def call_deepseek_analysis(report: dict) -> dict:
    """
    调用 DeepSeek R1 对策略建议（或纯离线数据）进行深度分析，生成审批报告。
    在人工审批之前调用，是关键的安全把关层。
    即使没有建议，也会分析离线数据并给出策略健康度评估。
    """
    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 | ... |

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

    payload = {
        "model": "deepseek-reasoner",
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": 1500,
        "temperature": 0.1,
    }

    api_key = os.environ.get("DEEPSEEK_API_KEY", "")
    if not api_key:
        import config_manager
        app_config = config_manager.load_app_config()
        api_key = app_config.get("DEEPSEEK_API_KEY", "")
        
    if not api_key:
        return {"model": DEEPSEEK_MODEL, "analysis": "[DEEPSEEK_API_KEY not set]", "timestamp": _now()}

    try:
        import requests
        resp = requests.post(
            "https://api.deepseek.com/v1/chat/completions",
            headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
            json=payload,
            timeout=60,
        )
        resp.raise_for_status()
        data = resp.json()
        msg = data["choices"][0]["message"]
        # deepseek-reasoner returns reasoning in 'reasoning_content' field
        reasoning = msg.get("reasoning_content", "") or ""
        content = msg.get("content", "") or ""
        analysis = (reasoning + "\n\n" + content).strip() if reasoning else content
    except Exception as e:
        analysis = f"[DeepSeek API 调用失败: {e}]"
        print(f"  [llm] 警告: {e}")

    return {
        "model": DEEPSEEK_MODEL,
        "prompt_tokens": 0,
        "analysis": analysis,
        "timestamp": _now(),
    }


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

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

    offline = load_json(STATS_FILE)
    if not offline:
        print("[evolution] 错误: 无法读取 offline_stats.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


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:
        print("[strategy_evolution] 仅执行 LLM 深度分析...")
        try:
            report = load_json(EVOLUTION_REPORT)
            if not report:
                print("未找到基础演化报告数据。")
                sys.exit(1)
            llm_report = call_deepseek_analysis(report)
            print(f"LLM 分析报告:\n{llm_report['analysis']}")
        except Exception as e:
            print(f"分析失败: {e}")
    else:
        main()
