#!/usr/bin/env python3
"""
offline_train.py — 离线训练数据生成 + 参数网格搜索 (Grid Search) + 逐笔交易模拟

用法：
  python3 src/offline_train.py --days 30 --interval 5m --mode once
  python3 src/offline_train.py --days 30 --interval 5m --mode train
"""
import argparse, json, sys, time, traceback, itertools
from datetime import datetime
from pathlib import Path

SRC_DIR = Path(__file__).parent
PROJECT_ROOT = SRC_DIR.parent
sys.path.insert(0, str(PROJECT_ROOT))
import config_manager

import yfinance as yf
import pandas as pd
import numpy as np


# ═══════════════════════════════════════════════════════════════
# 工具函数
# ═══════════════════════════════════════════════════════════════

def compute_indicators(df: pd.DataFrame, ema_short=9, ema_long=21) -> pd.DataFrame:
    df = df.copy()
    close = df["Close"]
    high, low = df["High"], df["Low"]

    df["ema_short"] = close.ewm(span=ema_short, adjust=False).mean()
    df["ema_long"] = close.ewm(span=ema_long, adjust=False).mean()

    delta = close.diff()
    gain = delta.clip(lower=0).ewm(alpha=1/14, adjust=False).mean()
    loss = (-delta.clip(upper=0)).ewm(alpha=1/14, adjust=False).mean()
    rs = gain / loss.replace(0, np.nan)
    df["rsi14"] = (100 - 100 / (1 + rs)).fillna(50)

    tr1 = high - low
    tr2 = (high - close.shift(1)).abs()
    tr3 = (low - close.shift(1)).abs()
    tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
    df["atr14"] = tr.ewm(alpha=1/14, adjust=False).mean()

    dm_plus = high.diff().clip(lower=0)
    dm_minus = (-low.diff()).clip(lower=0)
    di_plus = 100 * dm_plus.ewm(alpha=1/14, adjust=False).mean() / df["atr14"].replace(0, np.nan)
    di_minus = 100 * dm_minus.ewm(alpha=1/14, adjust=False).mean() / df["atr14"].replace(0, np.nan)
    dx = 100 * (di_plus - di_minus).abs() / (di_plus + di_minus.replace(0, np.nan))
    df["adx"] = dx.ewm(alpha=1/14, adjust=False).mean()

    bb_mid = close.rolling(20).mean()
    df["bb_upper"] = bb_mid + 2 * close.rolling(20).std()
    df["bb_lower"] = bb_mid - 2 * close.rolling(20).std()

    return df


def detect_regime(adx: float) -> str:
    return "TRENDING" if adx >= 25 else "RANGING"


def detect_signal(row: pd.Series, params: dict) -> dict:
    close = float(row["Close"])
    ema_s, ema_l = float(row["ema_short"]), float(row["ema_long"])
    rsi = float(row["rsi14"])
    bb_u, bb_l = float(row.get("bb_upper", 0)), float(row.get("bb_lower", 0))
    adx = float(row.get("adx", 0))
    regime = detect_regime(adx)
    atr = float(row.get("atr14", 3.0))

    sl_mult = params.get("sl_atr", 1.0)
    tp_mult = params.get("tp_atr", 3.0)

    # === 信号逻辑 ===
    if regime == "TRENDING":
        if ema_s > ema_l and rsi > 40 and rsi < 70 and close >= ema_s:
            return {"action": "OPEN", "side": "BUY", "regime": regime,
                    "entry": round(close, 3), "sl": round(close - sl_mult*atr, 3),
                    "tp": round(close + tp_mult*atr, 3), "confidence": 0.70}
        if ema_s < ema_l and rsi < 60 and rsi > 30 and close <= ema_s:
            return {"action": "OPEN", "side": "SELL", "regime": regime,
                    "entry": round(close, 3), "sl": round(close + sl_mult*atr, 3),
                    "tp": round(close - tp_mult*atr, 3), "confidence": 0.70}
    else:
        if rsi <= 30 and close <= bb_l and bb_l > 0:
            return {"action": "OPEN", "side": "BUY", "regime": regime,
                    "entry": round(close, 3), "sl": round(close - sl_mult*atr, 3),
                    "tp": round(close + tp_mult*atr, 3), "confidence": 0.65}
        if rsi >= 70 and close >= bb_u and bb_u > 0:
            return {"action": "OPEN", "side": "SELL", "regime": regime,
                    "entry": round(close, 3), "sl": round(close + sl_mult*atr, 3),
                    "tp": round(close - tp_mult*atr, 3), "confidence": 0.65}

    return {"action": "HOLD"}


# ═══════════════════════════════════════════════════════════════
# 核心：逐笔交易模拟
# ═══════════════════════════════════════════════════════════════

def backtest_strategy(df: pd.DataFrame, params: dict, verbose=False) -> dict:
    stats = {
        "total_trades": 0, "wins": 0, "losses": 0, "total_pnl": 0.0,
        "by_regime": {"TRENDING": {"wins": 0, "losses": 0, "pnl": 0.0},
                       "RANGING":  {"wins": 0, "losses": 0, "pnl": 0.0}},
        "by_side": {"BUY": {"wins": 0, "losses": 0, "pnl": 0.0},
                     "SELL": {"wins": 0, "losses": 0, "pnl": 0.0}},
    }
    trades = []
    open_trade = None  # 限制为一次只持有一个仓位，避免重复开仓导致数据虚假

    CONTRACT_SIZE = 100
    LOTS = 0.10

    for i, (ts, row) in enumerate(df.iterrows()):
        # 1. 检查是否需要平仓
        if open_trade and i + 1 < len(df):
            next_row = df.iloc[i + 1]
            next_high, next_low = float(next_row["High"]), float(next_row["Low"])
            
            entry, sl, tp = open_trade["entry"], open_trade["sl"], open_trade["tp"]
            side = open_trade["side"]

            pnl, outcome = None, None
            if side == "BUY":
                if next_low <= sl:
                    pnl = (sl - entry) * CONTRACT_SIZE * LOTS
                    outcome = "LOSS"
                elif next_high >= tp:
                    pnl = (tp - entry) * CONTRACT_SIZE * LOTS
                    outcome = "WIN"
            else:  # SELL
                if next_high >= sl:
                    pnl = (entry - sl) * CONTRACT_SIZE * LOTS
                    outcome = "LOSS"
                elif next_low <= tp:
                    pnl = (entry - tp) * CONTRACT_SIZE * LOTS
                    outcome = "WIN"

            if outcome:
                open_trade.update({"outcome": outcome, "pnl": round(pnl, 2), "close_bar": str(ts)})
                stats["total_trades"] += 1
                stats["total_pnl"] += pnl
                if outcome == "WIN": 
                    stats["wins"] += 1
                    stats["by_side"][side]["wins"] += 1
                    stats["by_regime"][open_trade["regime"]]["wins"] += 1
                else: 
                    stats["losses"] += 1
                    stats["by_side"][side]["losses"] += 1
                    stats["by_regime"][open_trade["regime"]]["losses"] += 1
                
                stats["by_side"][side]["pnl"] += pnl
                stats["by_regime"][open_trade["regime"]]["pnl"] += pnl
                
                trades.append(open_trade)
                open_trade = None  # 清空持仓

        # 2. 如果没有持仓，尝试寻找新信号
        if not open_trade:
            sig = detect_signal(row, params)
            if sig["action"] == "OPEN":
                open_trade = {
                    "side": sig["side"],
                    "regime": sig["regime"],
                    "entry": sig["entry"],
                    "sl": sig["sl"],
                    "tp": sig["tp"],
                    "timestamp": str(ts),
                }

    # 汇总
    wr = (stats["wins"] / stats["total_trades"] * 100) if stats["total_trades"] > 0 else 0
    stats["win_rate"] = round(wr, 2)
    stats["total_pnl"] = round(stats["total_pnl"], 2)
    
    # 评分函数：鼓励高胜率和高盈利，同时惩罚交易次数过少
    if stats["total_trades"] < 10:
        stats["score"] = -9999 # 交易次数太少，不具有统计学意义
    else:
        # PnL * (WinRate/100) * log(trades)
        stats["score"] = stats["total_pnl"] * (stats["win_rate"] / 100.0) * np.log1p(stats["total_trades"])
    
    stats["trades_list"] = trades
    return stats

def train_parameters(df: pd.DataFrame, output_dir: Path):
    print(f"=== 开始参数训练 (网格搜索) ===")
    
    # 定义参数空间
    param_grid = {
        "ema_short": [7, 9, 12],
        "ema_long": [21, 26, 50],
        "sl_atr": [1.0, 1.5, 2.0],
        "tp_atr": [1.5, 2.0, 3.0]
    }
    
    keys = param_grid.keys()
    combinations = list(itertools.product(*param_grid.values()))
    
    best_stats = None
    best_params = None
    
    # 模拟进度写入
    status_file = output_dir / "training_status.json"
    
    total_combs = len(combinations)
    print(f"参数组合总数: {total_combs}")
    
    for idx, vals in enumerate(combinations):
        params = dict(zip(keys, vals))
        
        # 重新计算基于 EMA 参数的指标
        df_eval = compute_indicators(df, ema_short=params["ema_short"], ema_long=params["ema_long"]).dropna()
        
        stats = backtest_strategy(df_eval, params, verbose=False)
        
        if best_stats is None or stats["score"] > best_stats["score"]:
            best_stats = stats
            best_params = params
            print(f"[{idx+1}/{total_combs}] 发现新最优参数: {params} -> PnL: ${stats['total_pnl']}, 胜率: {stats['win_rate']}%, 交易数: {stats['total_trades']}")
            
        # 进度更新
        if idx % 5 == 0 or idx == total_combs - 1:
            status_data = {
                "isTraining": True,
                "progress": round((idx + 1) / total_combs * 100, 1),
                "currentEpoch": idx + 1,
                "totalEpochs": total_combs,
                "winRate": best_stats["win_rate"] if best_stats else 0,
                "pnl": best_stats["total_pnl"] if best_stats else 0,
                "totalTrades": best_stats["total_trades"] if best_stats else 0,
                "activeDataset": "XAUUSD_5m_GridSearch"
            }
            status_file.write_text(json.dumps(status_data, indent=2))
            
    print("\n=== 训练完成 ===")
    print(f"最优参数: {best_params}")
    print(f"最优表现: 交易数 {best_stats['total_trades']}, 胜率 {best_stats['win_rate']}%, 总盈亏 ${best_stats['total_pnl']}")
    
    # 训练结束
    if status_file.exists():
        status_data = json.loads(status_file.read_text())
        status_data["isTraining"] = False
        status_data["progress"] = 100.0
        status_file.write_text(json.dumps(status_data, indent=2))
        
    # 保存最优参数和详细结果
    best_file = output_dir / "best_params.json"
    best_file.write_text(json.dumps({"best_params": best_params, "stats": best_stats}, indent=2, default=str))
    
    # 为了与之前的 GUI 兼容，输出一个 offline_train_xxx.json 格式
    out_file = output_dir / f"offline_train_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
    results = {
        "stats": {
            "total_bars": len(df),
            "total_trades": best_stats["total_trades"],
            "wins": best_stats["wins"],
            "losses": best_stats["losses"],
            "total_pnl": best_stats["total_pnl"],
            "win_rate": best_stats["win_rate"],
            "by_regime": best_stats["by_regime"],
            "by_side": best_stats["by_side"],
        },
        "trades": best_stats["trades_list"]
    }
    out_file.write_text(json.dumps(results, indent=2, default=str))

def main():
    ap = argparse.ArgumentParser(description="离线训练与参数优化")
    ap.add_argument("--days", type=int, default=30)
    ap.add_argument("--interval", type=str, default="5m")
    ap.add_argument("--symbol", type=str, default="GC=F")
    # 默认输出路径也改为走 config_manager 获取
    default_out = str(config_manager.get_data_dir() / "offline")
    ap.add_argument("--output-dir", type=str, default=default_out)
    ap.add_argument("--mode", type=str, default="train", choices=["once", "train"])
    ap.add_argument("--poll-seconds", type=float, default=60)
    args = ap.parse_args()

    out_dir = Path(args.output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    print(f"获取 {args.symbol} 近 {args.days} 天 {args.interval} 数据...")
    tk = yf.Ticker(args.symbol)
    df = None
    for attempt in range(3):
        try:
            df = tk.history(period=f"{args.days}d", interval=args.interval)
            if df is not None and len(df) > 0:
                break
        except Exception as e:
            print(f"  获取数据尝试 {attempt+1}/3 失败: {e}")
        if attempt < 2:
            import time; time.sleep(5)
    if df is None or df.empty:
        print(f"错误: 无法获取 {args.symbol} 数据"); sys.exit(1)

    print(f"有效K线: {len(df)} 根")

    if args.mode == "train":
        train_parameters(df, out_dir)
    else:
        # 默认参数单次测试
        df = compute_indicators(df).dropna()
        stats = backtest_strategy(df, {"ema_short": 9, "ema_long": 21, "sl_atr": 1.0, "tp_atr": 3.0}, verbose=True)
        print(f"\n单次测试结果:")
        print(f"交易数: {stats['total_trades']} | 胜率: {stats['win_rate']}% | 盈亏: ${stats['total_pnl']}")
        
        # 保存结果用于前端展示
        out_file = out_dir / f"offline_train_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
        results = {
            "stats": {
                "total_bars": len(df),
                "total_trades": stats["total_trades"],
                "wins": stats["wins"],
                "losses": stats["losses"],
                "total_pnl": stats["total_pnl"],
                "win_rate": stats["win_rate"],
                "by_regime": stats["by_regime"],
                "by_side": stats["by_side"],
            },
            "trades": stats["trades_list"]
        }
        out_file.write_text(json.dumps(results, indent=2, default=str))

if __name__ == "__main__":
    main()
