"""
量化交易回测系统 - 优化版
功能：基于包住形态的交易信号识别和回测分析
作者：量化交易团队
版本：2.1（修复15分钟窗口、统计键名、费用口径、边界处理）
"""

import datetime
from typing import List, Dict, Tuple, Optional, Any
from dataclasses import dataclass
from loguru import logger
from models.weex import Weex15, Weex1
import os

try:
    # 可视化依赖（若未安装，将在运行时给出提示）
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    _PLOTLY_AVAILABLE = True
except Exception as _e:
    _PLOTLY_AVAILABLE = False
    _PLOTLY_IMPORT_ERROR = _e


# ===============================================================
# 📊 配置管理类
# ===============================================================

@dataclass
class BacktestConfig:
    """回测配置类"""
    # 交易参数（注意：tp/sl单位与K线报价同单位，diff = 价格差值）
    take_profit: float = 3.0          # 止盈点数（价格差）- 优化：从8.0缩小到3.0
    stop_loss: float = -2.0           # 止损点数（价格差，负数）- 优化：从-1.0调整到-2.0
    contract_size: float = 10000      # 合约规模（每1点价格差对应的盈亏金额 = diff * contract_size）
    open_fee: float = 3.0             # 开仓固定手续费（金额）- 优化：从5.0降低到3.0
    close_fee_rate: float = 0.0003    # 平仓按成交额比例的手续费（金额 = rate * exit_price * contract_size）- 优化：从0.0005降低到0.0003

    # 新增：动态止盈止损参数
    dynamic_tp: bool = True           # 是否启用动态止盈
    dynamic_sl: bool = True           # 是否启用动态止损
    tp_ratio: float = 1.5             # 动态止盈倍数（基于ATR或价格波动）
    sl_ratio: float = 1.0             # 动态止损倍数（基于ATR或价格波动）
    min_tp: float = 2.0               # 最小止盈点数
    max_tp: float = 8.0               # 最大止盈点数
    min_sl: float = -3.0              # 最小止损点数（负数）
    max_sl: float = -1.0              # 最大止损点数（负数）

    # 回测日期范围（仅示例：若不传dates则使用）
    start_date: str = "2025-07-01"
    end_date: str = "2025-07-31"

    # 信号参数（保留扩展位）
    enable_bear_bull_engulf: bool = True  # 涨包跌信号（前跌后涨包住 -> 做多）
    enable_bull_bear_engulf: bool = True  # 跌包涨信号（前涨后跌包住 -> 做空）

    # 新增：信号过滤参数
    min_engulf_ratio: float = 0.6     # 最小包住比例（当前K线实体与前一K线实体的比例）
    min_volume_ratio: float = 1.2     # 最小成交量比例（当前K线成交量与前一K线成交量的比例）
    enable_volume_filter: bool = False # 是否启用成交量过滤（需要数据支持）

    def __post_init__(self):
        """验证配置参数"""
        if self.take_profit <= 0:
            raise ValueError("止盈点数必须大于0")
        if self.stop_loss >= 0:
            raise ValueError("止损点数必须小于0")
        if self.min_tp <= 0 or self.max_tp <= 0:
            raise ValueError("最小/最大止盈点数必须大于0")
        if self.min_sl >= 0 or self.max_sl >= 0:
            raise ValueError("最小/最大止损点数必须小于0")
        if self.tp_ratio <= 0 or self.sl_ratio <= 0:
            raise ValueError("动态止盈止损倍数必须大于0")
        if self.min_engulf_ratio <= 0 or self.min_engulf_ratio > 1:
            raise ValueError("最小包住比例必须在(0,1]范围内")


# ===============================================================
# 📊 数据获取模块
# ===============================================================

def get_data_by_date(model, date_str: str) -> List[Dict[str, Any]]:
    """按天获取指定表的数据；要求 model.id 为毫秒时间戳"""
    try:
        target_date = datetime.datetime.strptime(date_str, '%Y-%m-%d')
    except ValueError:
        logger.error("日期格式不正确，请使用 YYYY-MM-DD 格式。")
        return []

    start_ts = int(target_date.timestamp() * 1000)
    end_ts = int((target_date + datetime.timedelta(days=1)).timestamp() * 1000) - 1

    query = (model
             .select()
             .where(model.id.between(start_ts, end_ts))
             .order_by(model.id.asc()))

    return [
        {'id': i.id, 'open': i.open, 'high': i.high, 'low': i.low, 'close': i.close}
        for i in query
    ]


def get_future_data_1min(start_ts: int, end_ts: int) -> List[Dict[str, Any]]:
    """获取指定时间范围内的 1 分钟数据（闭区间）"""
    if end_ts < start_ts:
        return []
    query = (Weex1
             .select()
             .where(Weex1.id.between(start_ts, end_ts))
             .order_by(Weex1.id.asc()))
    return [{'id': i.id, 'open': i.open, 'high': i.high, 'low': i.low, 'close': i.close} for i in query]


# ===============================================================
# 📈 信号判定模块
# ===============================================================

def is_bullish(c) -> bool:
    return float(c['open']) < float(c['close'])


def is_bearish(c) -> bool:
    return float(c['open']) > float(c['close'])


def check_signal(prev: Dict[str, Any], curr: Dict[str, Any], config: BacktestConfig = None) -> Tuple[Optional[str], Optional[str]]:
    """
    判断是否出现包住形态（优化版）
    返回 (方向direction, 信号键signal_key)
    signal_key: 'bear_bull_engulf' 表示 涨包跌（前跌后涨包住 -> 做多）
                'bull_bear_engulf' 表示 跌包涨（前涨后跌包住 -> 做空）
    """
    if config is None:
        config = BacktestConfig()

    p_open, p_close = float(prev['open']), float(prev['close'])
    c_open, c_close = float(curr['open']), float(curr['close'])

    # 计算实体大小
    prev_body = abs(p_close - p_open)
    curr_body = abs(c_close - c_open)

    # 过滤：当前K线实体必须足够大
    if curr_body < 0.5:  # 实体太小，忽略
        return None, None

    # 前跌后涨包住 -> 做多
    if is_bullish(curr) and is_bearish(prev) and c_open <= p_close and c_close >= p_open:
        # 新增：包住比例过滤
        if prev_body > 0:
            engulf_ratio = curr_body / prev_body
            if engulf_ratio < config.min_engulf_ratio:
                return None, None
        return "long", "bear_bull_engulf"

    # 前涨后跌包住 -> 做空
    if is_bearish(curr) and is_bullish(prev) and c_open >= p_close and c_close <= p_open:
        # 新增：包住比例过滤
        if prev_body > 0:
            engulf_ratio = curr_body / prev_body
            if engulf_ratio < config.min_engulf_ratio:
                return None, None
        return "short", "bull_bear_engulf"

    return None, None


# ===============================================================
# 💹 回测模拟模块（使用 1 分钟数据）
# ===============================================================

def calculate_dynamic_tp_sl(
    entry_price: float,
    recent_candles: List[Dict[str, Any]],
    config: BacktestConfig
) -> Tuple[float, float]:
    """
    计算动态止盈止损
    基于最近K线的波动幅度
    """
    if len(recent_candles) < 5:
        return config.take_profit, config.stop_loss

    # 计算ATR（平均真实波幅）
    true_ranges = []
    for i in range(1, min(len(recent_candles), 10)):
        prev_close = float(recent_candles[i-1]['close'])
        curr_high = float(recent_candles[i]['high'])
        curr_low = float(recent_candles[i]['low'])

        tr = max(
            curr_high - curr_low,
            abs(curr_high - prev_close),
            abs(curr_low - prev_close)
        )
        true_ranges.append(tr)

    if not true_ranges:
        return config.take_profit, config.stop_loss

    atr = sum(true_ranges) / len(true_ranges)

    # 动态计算止盈止损
    dynamic_tp = max(config.min_tp, min(config.max_tp, atr * config.tp_ratio))
    dynamic_sl = max(config.min_sl, min(config.max_sl, -atr * config.sl_ratio))

    return dynamic_tp, dynamic_sl


def simulate_trade(
    direction: str,
    entry_price: float,
    start_ts: int,
    end_ts: int,
    tp: float,
    sl: float,
    config: BacktestConfig = None
) -> Tuple[Optional[float], float, Optional[int]]:
    """
    用 1 分钟数据进行精细化止盈止损模拟（优化版）
    - direction: "long" / "short"
    - entry_price: 开仓价格
    - start_ts: 开始时间（毫秒时间戳，包含）
    - end_ts: 结束时间（毫秒时间戳，包含）
    - tp: 止盈点（价格差，正数）
    - sl: 止损点（价格差，负数）
    - config: 配置对象（用于动态止盈止损）

    返回 (exit_price, diff, exit_time)
    - exit_price: 平仓价格，None 表示区间内无数据
    - diff: 价格差 = 对多头 (exit - entry)，对空头 (entry - exit)
    - exit_time: 平仓发生时间戳
    """
    if config is None:
        config = BacktestConfig()
    future_candles = get_future_data_1min(start_ts, end_ts)
    if not future_candles:
        logger.warning(f"simulate_trade 无分钟数据: [{start_ts}, {end_ts}]，跳过该段模拟。")
        return None, 0.0, None

    # 动态止盈止损计算
    if config.dynamic_tp or config.dynamic_sl:
        # 获取最近的历史K线用于计算ATR
        history_start = start_ts - 15 * 60 * 1000 * 10  # 获取前10根15分钟K线
        history_candles = get_future_data_1min(history_start, start_ts - 1)
        if history_candles:
            dynamic_tp, dynamic_sl = calculate_dynamic_tp_sl(entry_price, history_candles, config)
            if config.dynamic_tp:
                tp = dynamic_tp
            if config.dynamic_sl:
                sl = dynamic_sl

    if direction == "long":
        tp_price = entry_price + tp
        sl_price = entry_price + sl  # sl为负
    else:
        tp_price = entry_price - tp
        sl_price = entry_price - sl  # 注意：sl为负，因此 entry - sl = entry - (-x) = entry + x

    for candle in future_candles:
        open_p = float(candle['open'])
        high = float(candle['high'])
        low = float(candle['low'])

        if direction == "long":
            # 开盘跳空优先
            if open_p >= tp_price:
                return open_p, open_p - entry_price, candle['id']
            if open_p <= sl_price:
                return open_p, open_p - entry_price, candle['id']
            # 盘中触发
            if high >= tp_price:
                return tp_price, tp, candle['id']
            if low <= sl_price:
                return sl_price, sl, candle['id']
        else:
            # 空头
            if open_p <= tp_price:
                return open_p, entry_price - open_p, candle['id']
            if open_p >= sl_price:
                return open_p, entry_price - open_p, candle['id']
            if low <= tp_price:
                return tp_price, tp, candle['id']
            if high >= sl_price:
                return sl_price, sl, candle['id']

    # 未触发止盈止损，用最后一根收盘价平仓
    final = future_candles[-1]
    final_price = float(final['close'])
    diff = (final_price - entry_price) if direction == "long" else (entry_price - final_price)
    return final_price, diff, final['id']


# ===============================================================
# 📊 主回测流程（逐笔模式：每次信号只在下一根15m内评估TP/SL）
# ===============================================================

def backtest(
    dates: List[str],
    tp: float,
    sl: float,
    config: BacktestConfig = BacktestConfig()
) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Any]]]:
    """
    逐笔回测：
    - 形态出现在 idx（prev=idx-1, curr=idx），在 idx+1（下一根15mK）开盘入场
    - 仅在该入场K线的周期内（约15分钟）用1分钟数据评估TP/SL
    """
    all_data: List[Dict[str, Any]] = []
    for date_str in dates:
        all_data.extend(get_data_by_date(Weex15, date_str))
    all_data.sort(key=lambda x: x['id'])

    stats: Dict[str, Dict[str, Any]] = {
        "bear_bull_engulf": {"count": 0, "wins": 0, "total_profit": 0.0, "name": "涨包跌"},
        "bull_bear_engulf": {"count": 0, "wins": 0, "total_profit": 0.0, "name": "跌包涨"},
    }

    trades: List[Dict[str, Any]] = []

    # 至少需要 prev(0), curr(1), entry(idx+1=2), next-of-entry(idx+2=3)
    for idx in range(1, len(all_data) - 2):
        prev, curr = all_data[idx - 1], all_data[idx]
        entry_candle = all_data[idx + 1]
        next_of_entry = all_data[idx + 2]  # 用于界定入场K的结束边界

        direction, signal_key = check_signal(prev, curr, config)
        if not direction or signal_key is None:
            continue

        if (signal_key == "bear_bull_engulf" and not config.enable_bear_bull_engulf) or \
           (signal_key == "bull_bear_engulf" and not config.enable_bull_bear_engulf):
            continue

        entry_price = float(entry_candle['open'])
        entry_ts = entry_candle['id']
        # 只在入场这根15m内评估：结束边界 = 下一根15m开始时间 - 1毫秒
        end_ts = next_of_entry['id'] - 1

        exit_price, diff, exit_time = simulate_trade(
            direction=direction,
            entry_price=entry_price,
            start_ts=entry_ts,
            end_ts=end_ts,
            tp=tp,
            sl=sl,
            config=config
        )

        if exit_price is None:
            # 无分钟数据，跳过统计
            continue

        # 盈亏金额与费用
        profit_amount = diff * config.contract_size
        total_fee = config.open_fee + config.close_fee_rate * exit_price * config.contract_size
        net_profit = profit_amount - total_fee

        stats[signal_key]['count'] += 1
        stats[signal_key]['total_profit'] += net_profit
        if net_profit > 0:
            stats[signal_key]['wins'] += 1

        trades.append({
            "entry_time": datetime.datetime.fromtimestamp(entry_ts / 1000),
            "exit_time": datetime.datetime.fromtimestamp(exit_time / 1000),
            "signal_key": signal_key,
            "signal": stats[signal_key]['name'],
            "direction": "做多" if direction == "long" else "做空",
            "entry": entry_price,
            "exit": exit_price,
            "diff": diff,
            "profit_amount": profit_amount,
            "fee": total_fee,
            "net_profit": net_profit,
        })

    return trades, stats


# ===============================================================
# 📊 主回测流程（单笔持仓模式：同向忽略，反向先平后开，始终仅一笔持仓）
# ===============================================================

def backtest_single_position(
    dates: List[str],
    tp: float,
    sl: float,
    config: BacktestConfig = BacktestConfig()
) -> Tuple[List[Dict[str, Any]], Dict[str, Dict[str, Any]], List[Dict[str, Any]]]:
    """
    单笔持仓回测：
    - 同向信号：忽略（不加仓）
    - 反向信号：先用分钟数据从当前持仓entry_time模拟到“新信号的入场时刻-1毫秒”完成平仓，再按新信号在其入场K开仓
    - 始终保证同一时间只有一笔持仓
    - 如果一直没有反向信号出现，最后用全部数据的最后时刻完成平仓
    """
    all_data: List[Dict[str, Any]] = []
    for date_str in dates:
        all_data.extend(get_data_by_date(Weex15, date_str))
    all_data.sort(key=lambda x: x['id'])

    stats: Dict[str, Dict[str, Any]] = {
        "bear_bull_engulf": {"count": 0, "wins": 0, "total_profit": 0.0, "name": "涨包跌"},
        "bull_bear_engulf": {"count": 0, "wins": 0, "total_profit": 0.0, "name": "跌包涨"},
    }

    trades: List[Dict[str, Any]] = []
    # 事件流：用于可视化显示“同向信号（忽略）”与“反向先平后开”
    signal_events: List[Dict[str, Any]] = []
    current_position: Optional[Dict[str, Any]] = None

    # 为了在反向信号到来时平仓，至少需要 prev(0), curr(1), entry(idx+1=2)
    for idx in range(1, len(all_data) - 1):
        prev, curr = all_data[idx - 1], all_data[idx]
        entry_candle = all_data[idx + 1]  # 新信号的入场K
        direction, signal_key = check_signal(prev, curr, config)
        if not direction or signal_key is None:
            continue

        if (signal_key == "bear_bull_engulf" and not config.enable_bear_bull_engulf) or \
           (signal_key == "bull_bear_engulf" and not config.enable_bull_bear_engulf):
            continue

        entry_price_new = float(entry_candle['open'])
        entry_ts_new = entry_candle['id']

        # 若已有持仓
        if current_position is not None:
            # 同向 -> 忽略
            if current_position['direction'] == direction:
                # 记录同向信号事件（忽略加仓）
                signal_events.append({
                    "type": "same_direction_signal",
                    "time": datetime.datetime.fromtimestamp(entry_ts_new / 1000),
                    "price": entry_price_new,
                    "direction": direction,
                    "signal_key": signal_key,
                    "signal_name": stats[signal_key]['name']
                })
                continue
            # 反向 -> 先平旧仓（分钟模拟：从旧entry_time -> 新入场时刻-1毫秒）
            exit_price, diff, exit_time = simulate_trade(
                direction=current_position['direction'],
                entry_price=current_position['entry_price'],
                start_ts=current_position['entry_time'],
                end_ts=entry_ts_new - 1,
                tp=tp,
                sl=sl,
                config=config
            )
            if exit_price is not None:
                profit_amount = diff * config.contract_size
                total_fee = config.open_fee + config.close_fee_rate * exit_price * config.contract_size
                net_profit = profit_amount - total_fee

                trades.append({
                    "entry_time": datetime.datetime.fromtimestamp(current_position['entry_time'] / 1000),
                    "exit_time": datetime.datetime.fromtimestamp(exit_time / 1000),
                    "signal_key": current_position['signal_key'],
                    "signal": current_position['signal_name'],
                    "direction": "做多" if current_position['direction'] == "long" else "做空",
                    "entry": current_position['entry_price'],
                    "exit": exit_price,
                    "diff": diff,
                    "profit_amount": profit_amount,
                    "fee": total_fee,
                    "net_profit": net_profit,
                })

                stats[current_position['signal_key']]['count'] += 1
                stats[current_position['signal_key']]['total_profit'] += net_profit
                if net_profit > 0:
                    stats[current_position['signal_key']]['wins'] += 1

                # 记录反向信号引发的平仓事件
                signal_events.append({
                    "type": "reverse_close",
                    "time": datetime.datetime.fromtimestamp(exit_time / 1000),
                    "price": exit_price,
                    "direction": current_position['direction'],
                    "signal_key": current_position['signal_key'],
                    "signal_name": current_position['signal_name']
                })

            # 清空持仓，再开新仓
            current_position = None

        # 开新仓（在新信号的入场K开盘价）
        current_position = {
            "direction": direction,
            "signal_key": signal_key,
            "signal_name": stats[signal_key]['name'],
            "entry_price": entry_price_new,
            "entry_time": entry_ts_new
        }

        # 记录反向开仓事件（若此前刚刚发生了反向平仓，则此处为接力开仓）或普通开仓事件
        signal_events.append({
            "type": "reverse_open" if any(e.get("type") == "reverse_close" and e.get("time") == datetime.datetime.fromtimestamp((entry_ts_new - 1) / 1000) for e in signal_events) else "signal_open",
            "time": datetime.datetime.fromtimestamp(entry_ts_new / 1000),
            "price": entry_price_new,
            "direction": direction,
            "signal_key": signal_key,
            "signal_name": stats[signal_key]['name']
        })

    # 数据末尾：若仍有持仓，用最后时间收盘价平仓（分钟模拟：从entry_time -> 全部数据最后时刻）
    if current_position is not None and len(all_data) > 0:
        final_ts = all_data[-1]['id']
        exit_price, diff, exit_time = simulate_trade(
            direction=current_position['direction'],
            entry_price=current_position['entry_price'],
            start_ts=current_position['entry_time'],
            end_ts=final_ts,
            tp=tp,
            sl=sl,
            config=config
        )
        if exit_price is not None:
            profit_amount = diff * config.contract_size
            total_fee = config.open_fee + config.close_fee_rate * exit_price * config.contract_size
            net_profit = profit_amount - total_fee

            trades.append({
                "entry_time": datetime.datetime.fromtimestamp(current_position['entry_time'] / 1000),
                "exit_time": datetime.datetime.fromtimestamp(exit_time / 1000),
                "signal_key": current_position['signal_key'],
                "signal": current_position['signal_name'],
                "direction": "做多" if current_position['direction'] == "long" else "做空",
                "entry": current_position['entry_price'],
                "exit": exit_price,
                "diff": diff,
                "profit_amount": profit_amount,
                "fee": total_fee,
                "net_profit": net_profit,
            })

            stats[current_position['signal_key']]['count'] += 1
            stats[current_position['signal_key']]['total_profit'] += net_profit
            if net_profit > 0:
                stats[current_position['signal_key']]['wins'] += 1

    return trades, stats, signal_events


# ===============================================================
# 🔧 辅助：日期范围生成
# ===============================================================

def gen_dates(start_date: str, end_date: str) -> List[str]:
    """生成 [start_date, end_date] 闭区间的日期字符串列表，格式 YYYY-MM-DD"""
    ds = datetime.datetime.strptime(start_date, "%Y-%m-%d")
    de = datetime.datetime.strptime(end_date, "%Y-%m-%d")
    res = []
    d = ds
    while d <= de:
        res.append(d.strftime("%Y-%m-%d"))
        d += datetime.timedelta(days=1)
    return res


# ===============================================================
# 🔍 参数优化建议
# ===============================================================

def optimize_parameters(
    dates: List[str],
    config: BacktestConfig,
    tp_range: Tuple[float, float] = (1.0, 6.0),
    sl_range: Tuple[float, float] = (-4.0, -1.0),
    step: float = 0.5
) -> Dict[str, Any]:
    """
    参数优化函数：测试不同的止盈止损组合，找到最佳参数

    Args:
        dates: 回测日期列表
        config: 基础配置
        tp_range: 止盈范围 (min, max)
        sl_range: 止损范围 (min, max)
        step: 步长

    Returns:
        最佳参数组合和统计结果
    """
    logger.info("开始参数优化...")

    best_result = {
        'tp': config.take_profit,
        'sl': config.stop_loss,
        'total_net_profit': -float('inf'),
        'win_rate': 0.0,
        'total_trades': 0,
        'sharpe_ratio': -float('inf')
    }

    results = []

    # 生成参数组合
    tp_values = [round(tp_range[0] + i * step, 1) for i in range(int((tp_range[1] - tp_range[0]) / step) + 1)]
    sl_values = [round(sl_range[0] + i * step, 1) for i in range(int((sl_range[1] - sl_range[0]) / step) + 1)]

    total_combinations = len(tp_values) * len(sl_values)
    current_combination = 0

    for tp in tp_values:
        for sl in sl_values:
            if sl >= 0:  # 止损必须是负数
                continue

            current_combination += 1
            logger.info(f"测试参数组合 {current_combination}/{total_combinations}: TP={tp}, SL={sl}")

            # 创建临时配置
            temp_config = BacktestConfig(
                take_profit=tp,
                stop_loss=sl,
                contract_size=config.contract_size,
                open_fee=config.open_fee,
                close_fee_rate=config.close_fee_rate,
                enable_bear_bull_engulf=config.enable_bear_bull_engulf,
                enable_bull_bear_engulf=config.enable_bull_bear_engulf,
                min_engulf_ratio=config.min_engulf_ratio,
                dynamic_tp=False,  # 关闭动态参数以进行基础测试
                dynamic_sl=False
            )

            try:
                # 运行回测（接收 signal_events，但优化统计仅需 trades）
                trades, stats, _signal_events = backtest_single_position(dates, tp=tp, sl=sl, config=temp_config)

                if not trades:
                    continue

                # 计算统计指标
                total_trades = len(trades)
                total_net_profit = sum(t['net_profit'] for t in trades)
                winning_trades = sum(1 for t in trades if t['net_profit'] > 0)
                win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0

                # 计算夏普比率（简化版）
                profits = [t['net_profit'] for t in trades]
                if len(profits) > 1:
                    avg_profit = sum(profits) / len(profits)
                    profit_std = (sum((p - avg_profit) ** 2 for p in profits) / len(profits)) ** 0.5
                    sharpe_ratio = (avg_profit / profit_std) if profit_std > 0 else 0
                else:
                    sharpe_ratio = 0

                result = {
                    'tp': tp,
                    'sl': sl,
                    'total_net_profit': total_net_profit,
                    'win_rate': win_rate,
                    'total_trades': total_trades,
                    'sharpe_ratio': sharpe_ratio
                }
                results.append(result)

                # 更新最佳结果（优先考虑净利润，其次考虑夏普比率）
                if (total_net_profit > best_result['total_net_profit'] or
                    (total_net_profit == best_result['total_net_profit'] and sharpe_ratio > best_result['sharpe_ratio'])):
                    best_result = result.copy()

            except Exception as e:
                logger.warning(f"参数组合 TP={tp}, SL={sl} 测试失败: {e}")
                continue

    logger.info(f"参数优化完成！最佳组合: TP={best_result['tp']}, SL={best_result['sl']}")
    logger.info(f"最佳结果: 净利润={best_result['total_net_profit']:.2f}, 胜率={best_result['win_rate']:.2f}%, 交易次数={best_result['total_trades']}")

    return {
        'best_result': best_result,
        'all_results': results,
        'optimization_summary': {
            'total_combinations_tested': len(results),
            'best_tp': best_result['tp'],
            'best_sl': best_result['sl'],
            'best_net_profit': best_result['total_net_profit'],
            'best_win_rate': best_result['win_rate'],
            'best_sharpe_ratio': best_result['sharpe_ratio']
        }
    }


# ===============================================================
# 📈 可视化：K线 + 交易点位（交互式HTML）
# ===============================================================

def _collect_15m_candles(dates: List[str]) -> List[Dict[str, Any]]:
    """收集一段日期内的15分钟K线，升序。"""
    candles: List[Dict[str, Any]] = []
    for date_str in dates:
        candles.extend(get_data_by_date(Weex15, date_str))
    candles.sort(key=lambda x: x['id'])
    return candles


def plot_trades_candlestick(
    dates: List[str],
    trades: List[Dict[str, Any]],
    signal_events: Optional[List[Dict[str, Any]]] = None,
    output_html: str = "backtest_trades.html",
    title: str = "回测交易可视化"
) -> Optional[str]:
    """
    绘制15m K线，叠加每笔交易的入场/出场标记与连线。
    - 绿色：盈利交易；红色：亏损交易
    - 做多入场标记：triangle-up；做空入场标记：triangle-down；出场标记：x
    返回生成的HTML路径，若缺少依赖则返回None。
    """
    if not _PLOTLY_AVAILABLE:
        logger.error(f"缺少可视化依赖：plotly。请先安装：pip install plotly\n原始错误：{_PLOTLY_IMPORT_ERROR}")
        return None

    candles = _collect_15m_candles(dates)
    if not candles:
        logger.warning("无15分钟K线数据，跳过可视化。")
        return None

    ts = [c['id'] for c in candles]
    opens = [float(c['open']) for c in candles]
    highs = [float(c['high']) for c in candles]
    lows = [float(c['low']) for c in candles]
    closes = [float(c['close']) for c in candles]

    fig = go.Figure()
    fig.add_trace(go.Candlestick(
        x=[datetime.datetime.fromtimestamp(t/1000) for t in ts],
        open=opens, high=highs, low=lows, close=closes,
        name="15m K线",
        increasing_line_color="#26a69a",
        decreasing_line_color="#ef5350",
        showlegend=True
    ))

    # 预计算紧凑价格范围（5%-95%分位）
    try:
        all_prices = [*lows, *highs]
        all_prices.sort()
        n = len(all_prices)
        if n >= 20:
            i5 = max(0, int(n * 0.05))
            i95 = min(n - 1, int(n * 0.95))
            compact_low = all_prices[i5]
            compact_high = all_prices[i95]
            pad = (compact_high - compact_low) * 0.02
            init_y_range = [compact_low - pad, compact_high + pad] if compact_high > compact_low else None
        else:
            init_y_range = None
    except Exception:
        init_y_range = None

    # 叠加交易点位
    for trade in trades:
        entry_time: datetime.datetime = trade['entry_time']
        exit_time: datetime.datetime = trade['exit_time']
        entry_px = float(trade['entry'])
        exit_px = float(trade['exit'])
        direction_text = str(trade['direction'])  # "做多"/"做空"
        signal_name = str(trade.get('signal', ''))
        net_profit = float(trade.get('net_profit', trade.get('diff', 0.0)))

        color = "#2ecc71" if net_profit >= 0 else "#e74c3c"
        entry_marker = "triangle-up" if direction_text == "做多" else "triangle-down"

        # 入场标记
        fig.add_trace(go.Scatter(
            x=[entry_time], y=[entry_px],
            mode="markers",
            marker=dict(symbol=entry_marker, size=12, color=color, line=dict(width=1, color="#ffffff")),
            name="入场",
            hovertemplate=(
                "信号:%{customdata[0]}<br>方向:%{customdata[1]}<br>入场:%{y:.2f}<br>时间:%{x}<extra>入场</extra>"
            ),
            customdata=[[signal_name, direction_text]],
            showlegend=False
        ))

        # 出场标记
        fig.add_trace(go.Scatter(
            x=[exit_time], y=[exit_px],
            mode="markers",
            marker=dict(symbol="x", size=10, color=color),
            name="出场",
            hovertemplate=(
                "出场:%{y:.2f}<br>时间:%{x}<br>净利润:%{customdata[0]:.2f}<extra>出场</extra>"
            ),
            customdata=[[net_profit]],
            showlegend=False
        ))

        # 连接线（入场->出场）
        fig.add_trace(go.Scatter(
            x=[entry_time, exit_time], y=[entry_px, exit_px],
            mode="lines",
            line=dict(color=color, width=2),
            name="交易",
            hoverinfo="skip",
            showlegend=False
        ))

    # 叠加信号事件（同向忽略、反向平仓、反向开仓、普通开仓）
    if signal_events:
        for ev in signal_events:
            ev_type = ev.get("type")
            ev_time: datetime.datetime = ev.get("time")
            ev_price = float(ev.get("price", 0.0))
            ev_signal_name = str(ev.get("signal_name", ""))
            ev_direction = ev.get("direction")  # "long" / "short"

            if ev_type == "same_direction_signal":
                symbol = "circle-open"
                color = "#95a5a6"  # 灰色
                name = "同向信号(忽略)"
            elif ev_type == "reverse_close":
                symbol = "x-thin"
                color = "#f39c12"  # 橙色
                name = "反向平仓"
            elif ev_type == "reverse_open":
                symbol = "triangle-up-open" if ev_direction == "long" else "triangle-down-open"
                color = "#f1c40f"  # 黄色
                name = "反向开仓"
            else:  # signal_open
                symbol = "star"
                color = "#3498db"  # 蓝色
                name = "信号开仓"

            fig.add_trace(go.Scatter(
                x=[ev_time], y=[ev_price],
                mode="markers",
                marker=dict(symbol=symbol, size=9, color=color, line=dict(width=1, color="#ffffff")),
                name=name,
                hovertemplate=(
                    "事件:%{customdata[0]}<br>信号:%{customdata[1]}<br>价格:%{y:.2f}<br>时间:%{x}<extra></extra>"
                ),
                customdata=[[name, ev_signal_name]],
                showlegend=False
            ))

    # 全局布局与交互
    fig.update_layout(
        title=title,
        template="plotly_dark",
        xaxis_title="时间",
        yaxis_title="价格",
        hovermode="x unified",
        margin=dict(l=60, r=30, t=60, b=40),
        dragmode="zoom",
        legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
        xaxis=dict(
            rangeslider=dict(visible=True),
            rangeselector=dict(
                buttons=[
                    dict(count=1, label="1h", step="hour", stepmode="backward"),
                    dict(count=4, label="4h", step="hour", stepmode="backward"),
                    dict(count=1, label="1d", step="day", stepmode="backward"),
                    dict(count=7, label="1w", step="day", stepmode="backward"),
                    dict(step="all", label="ALL")
                ]
            )
        ),
        yaxis=dict(
            fixedrange=False,
            autorange=False if init_y_range else True,
            range=init_y_range
        ),
        uirevision="keep"  # 保持用户当前视图设置
    )

    # 优化交互：十字光标、缩放细节
    fig.update_xaxes(showspikes=True, spikemode="across", spikesnap="cursor", showline=True)
    fig.update_yaxes(showspikes=True, spikemode="across", spikesnap="cursor", showline=True)

    # 输出HTML
    output_html = os.path.abspath(output_html)
    fig.write_html(
        output_html,
        include_plotlyjs='cdn',
        auto_open=False,
        full_html=True,
        config={
            "scrollZoom": True,           # 鼠标滚轮缩放
            "displaylogo": False,         # 隐藏Plotly Logo
            "doubleClick": "autosize",   # 双击自适应
            "modeBarButtonsToAdd": [      # 常用交互按钮
                "toggleSpikelines",
                "zoom2d",
                "pan2d",
                "zoomIn2d",
                "zoomOut2d",
                "zoomInY",
                "zoomOutY",
                "autoScale2d",
                "resetScale2d",
                "drawline",
                "drawopenpath",
                "eraseshape"
            ]
        }
    )
    logger.info(f"已生成可视化HTML：{output_html}")
    return output_html


# ===============================================================
# 🚀 启动主流程
# ===============================================================

if __name__ == '__main__':
    # 优化后的配置 - 缩小价差距离
    config = BacktestConfig(
        # 基础止盈止损参数（已优化）
        take_profit=3.0,          # 从10.0缩小到3.0
        stop_loss=-2.0,           # 从-5.0调整到-2.0
        contract_size=10000,
        open_fee=3.0,             # 从5.0降低到3.0
        close_fee_rate=0.0003,    # 从0.0005降低到0.0003

        # 动态止盈止损参数
        dynamic_tp=True,          # 启用动态止盈
        dynamic_sl=True,          # 启用动态止损
        tp_ratio=1.5,             # 动态止盈倍数
        sl_ratio=1.0,             # 动态止损倍数
        min_tp=2.0,               # 最小止盈点数
        max_tp=8.0,               # 最大止盈点数
        min_sl=-3.0,              # 最小止损点数
        max_sl=-1.0,              # 最大止损点数

        # 信号过滤参数
        min_engulf_ratio=0.6,     # 最小包住比例
        enable_volume_filter=False, # 暂不启用成交量过滤

        # 回测日期
        start_date="2025-09-01",
        end_date="2025-09-30",
        enable_bear_bull_engulf=True,
        enable_bull_bear_engulf=True
    )

    # 生成日期列表（也可以自行传入 dates）
    dates = gen_dates(config.start_date, config.end_date)

    # 可选：运行参数优化（取消注释以启用）
    # optimization_result = optimize_parameters(
    #     dates=dates,
    #     config=config,
    #     tp_range=(1.0, 6.0),    # 止盈范围
    #     sl_range=(-4.0, -1.0),  # 止损范围
    #     step=0.5                # 步长
    # )
    # print(f"\n=== 参数优化结果 ===")
    # print(f"最佳止盈: {optimization_result['optimization_summary']['best_tp']}")
    # print(f"最佳止损: {optimization_result['optimization_summary']['best_sl']}")
    # print(f"最佳净利润: {optimization_result['optimization_summary']['best_net_profit']:.2f}")
    # print(f"最佳胜率: {optimization_result['optimization_summary']['best_win_rate']:.2f}%")
    #
    # # 使用优化后的参数更新配置
    # config.take_profit = optimization_result['optimization_summary']['best_tp']
    # config.stop_loss = optimization_result['optimization_summary']['best_sl']

    # 运行"单笔持仓"模式（同向忽略，反向先平后开）
    trades, stats, signal_events = backtest_single_position(dates, tp=config.take_profit, sl=config.stop_loss, config=config)

    logger.info("===== 每笔交易详情 =====")
    for t in trades:
        logger.info(
            f"{t['entry_time']} {t['direction']}({t['signal']}) "
            f"入场={t['entry']:.2f} 出场={t['exit']:.2f} 出场时间={t['exit_time']} "
            f"差价={t['diff']:.2f} 盈亏金额={t['profit_amount']:.2f} 手续费={t['fee']:.2f} 净利润={t['net_profit']:.2f}"
        )

    total_trades = len(trades)
    total_profit_amount = sum(t['profit_amount'] for t in trades)
    total_fee = sum(t['fee'] for t in trades)
    total_net = sum(t['net_profit'] for t in trades)

    print(f"\n一共交易笔数：{total_trades}")
    print(f"总毛盈利金额：{total_profit_amount:.2f}")
    print(f"总手续费：{total_fee:.2f}")
    print(f"总净利润：{total_net:.2f}")
    print("\n===== 信号统计（基于净利润） =====")
    for k, v in stats.items():
        name = v['name']
        count = v['count']
        wins = v['wins']
        win_rate = (wins / count * 100) if count > 0 else 0.0
        total_profit = v['total_profit']  # 这里已累计为“净利润金额”
        print(f"{name}：次数={count} 胜率={win_rate:.2f}% 净利润合计={total_profit:.2f}")

    # 若需要“逐笔模式（仅入场当根15m内评估TP/SL）”，可改为：
    # trades2, stats2 = backtest(dates, tp=config.take_profit, sl=config.stop_loss, config=config)
    # ... 输出逻辑同上

    # 生成交互式可视化
    html_path = plot_trades_candlestick(dates, trades, signal_events=signal_events, output_html="backtest_trades.html")
    if html_path:
        print(f"\n可视化文件已生成：{html_path}\n请用浏览器打开进行交互分析。")

    # 输出优化建议
    print(f"\n=== 优化建议 ===")
    print(f"当前配置已优化：")
    print(f"- 止盈点数: {config.take_profit} (从原来的10.0缩小到{config.take_profit})")
    print(f"- 止损点数: {config.stop_loss} (从原来的-5.0调整到{config.stop_loss})")
    print(f"- 开仓手续费: {config.open_fee} (从原来的5.0降低到{config.open_fee})")
    print(f"- 平仓手续费率: {config.close_fee_rate} (从原来的0.0005降低到{config.close_fee_rate})")
    print(f"- 最小包住比例: {config.min_engulf_ratio} (新增过滤条件)")
    print(f"- 动态止盈止损: {'启用' if config.dynamic_tp or config.dynamic_sl else '禁用'}")
    print(f"\n如需进一步优化，请取消注释参数优化代码段。")