"""
ExitOptimizer - 智能平仓引擎 v2.0
核心理念：不让盈利单变亏损

三层保护：
  L1 保本：profit >= 0.5ATR → SL 移到 entry（保本）
  L2 锁利：profit >= 1.0ATR → 止盈40%，SL移到 entry+0.3ATR
  L3 奔跑：profit >= 1.5ATR → 再止盈30%，SL追踪0.6ATR

动量退出（不等SL）：
  BUY: RSI > 72 强制止盈（超买）
  SELL: RSI < 28 强制止盈（超卖）
  MACD柱收缩 > 50% 强制检查

时间退出：
  持仓 > 25分钟（震荡市场）→ 强制检查
"""

import time
import json
import pathlib
import os
from typing import Tuple, Optional

ROOT = pathlib.Path(__file__).parent.parent.absolute()
_PEAK_FILE = ROOT / "logs" / "exit_tp_peak.json"


def _write_request_atomic(target_path: pathlib.Path, payload: dict):
    tmp_path = target_path.with_suffix(target_path.suffix + ".tmp")
    tmp_path.write_text(json.dumps(payload, indent=2))
    os.replace(tmp_path, target_path)

def _load_peak_file():
    if not _PEAK_FILE.exists():
        return {}
    try:
        return json.loads(_PEAK_FILE.read_text())
    except:
        return {}

def _save_peak_file(data):
    try:
        _PEAK_FILE.write_text(json.dumps(data, indent=2))
    except:
        pass

# ─── TP 追踪配置 ────────────────────────────────────────────
TP_TRAIL_MIN_PROFIT_ATR = 0.8   # 浮盈达到多少 ATR 后激活 TP 追踪
# ==========================================
# 核心配置：动态呼吸空间参数
# ==========================================
# 黄金波动剧烈，追踪止损如果跟得太紧，极易被盘中次级回调扫出，导致错失大波段
TP_TRAIL_BASE_ATR       = 2.2   # 基础追踪距离：进一步放宽到 2.2 ATR，避免趋势初期被正常回踩洗掉
MIN_TRAIL_ATR           = 1.2   # 最小追踪距离兜底：继续放宽，避免小盈利阶段止损贴脸
MAX_TRAIL_ATR           = 3.8   # 最大追踪距离天花板：保留高波动时的呼吸空间
L3_TIGHT_MULTIPLIER     = 0.8   # 狂飙阶段也避免收得过急，防止强趋势尾段被小回撤扫掉
TP_TRAIL_STEP_ATR       = 0.5   # TP 必须朝有利方向移动至少这么多 ATR 才触发修改（避免频繁小幅修改）

# ==========================================
# 核心配置：各级防守阈值 (过滤碎单，拉大间距)
# ==========================================
L1_PROFIT_ATR = 1.5          # L1 保本触发线：推迟到 1.5 ATR，先放过 1.2~1.4 ATR 的正常首波利润
L2_PROFIT_ATR = 2.2          # L2 锁利触发线：至少跑出 2.2 ATR 再进入趋势保护
L3_PROFIT_ATR = 3.4          # L3 狂飙触发线：进一步后移，避免趋势中段就进入极紧追踪
L1_LOCKED_PROFIT_ATR = 0.1   # L1 仅做轻微盈利保护，不在趋势初段过度锁利
L2_MIN_LOCKED_PROFIT_ATR = 0.2  # L2 最低锁盈 floor 放松，优先保留呼吸空间
L2_PARTIAL_MIN_PROFIT_ATR = 2.8  # L2 分批止盈至少要达到更厚的利润垫
L3_PARTIAL_MIN_PROFIT_ATR = 3.8  # L3 分批止盈进一步后移，避免尾仓过早切掉

# ─── 时间止损 (Time Stop) 配置 ─────────────────────────────
MAX_HOLD_SECONDS_FLAT = 5400    # 震荡无盈利状态下的最大持仓时间：从 45分钟 延长至 90分钟 (给足震荡破局的耐心)
TIME_STOP_MIN_PROFIT_ATR = 0.3  # 如果持仓超过阈值，但浮盈小于这个ATR，则强制平仓
_MIN_HOLD_FOR_TIME_STOP = 120   # 最小持仓时间（秒）：低于此视为 position_time Bug，不触发时间止损（从.bak2移植）
_MIN_HOLD_FOR_PROTECTIVE_TRAIL = 300  # 最小持仓时间：至少运行 5 分钟再进入 L1/L2 保护，避免开仓初期震荡洗掉
_MIN_HOLD_FOR_PARTIAL_CLOSE = 600  # 最小持仓时间：至少运行 10 分钟再考虑主动分批止盈

def _get_trail_distance_atr(atr: float, vol_ratio: float, base_atr: float = TP_TRAIL_BASE_ATR) -> float:
    """
    根据 GARCH 波动率动态计算追踪距离 (ATR倍数)
    高波动时拉宽 (给足呼吸空间)，低波动时收紧
    
    计算公式：trail_distance = base_atr × vol_ratio
    约束到 [MIN_TRAIL_ATR, MAX_TRAIL_ATR]
    """
    # 钳制波动率极值
    vol_ratio = max(0.3, min(2.5, vol_ratio))
    distance = base_atr * vol_ratio
    return round(max(MIN_TRAIL_ATR, min(MAX_TRAIL_ATR, distance)), 2)

_logger = None

def _log(msg, level="INFO"):
    global _logger
    if _logger:
        _logger.log(msg, level)
    else:
        ts = time.strftime("%H:%M:%S")
        print(f"[{ts}] [ExitOpt] {msg}")


def set_logger(logger_fn):
    global _logger
    _logger = logger_fn


def evaluate_exit(bid: float, ask: float, state: dict, indicators: dict, atr: float) -> Tuple[bool, str, Optional[dict]]:
    """
    判断是否应该平仓。
    返回: (should_exit, reason, exit_detail)
    exit_detail: {"action": "CLOSE"|"CLOSE_PARTIAL", "volume": float, "sl_price": float, "tp_price": float}
    """
    if not state.get("position_open"):
        return False, "", None

    side = state.get("position_side", "BUY")
    entry = state.get("position_entry", 0)
    cur_sl = state.get("position_sl", 0)
    cur_tp = state.get("position_tp", 0)
    vol = state.get("position_vol", 0.01)

    if entry <= 0 or atr <= 0:
        return False, "", None

    # ── 获取 GARCH 波动率数据（用于动态追踪距离）──
    try:
        from bridge.garch_volatility import get_vol_multiplier
        garch_result = get_vol_multiplier(refresh=False)
        vol_ratio = garch_result.get("vol_ratio", 1.0)
        garch_regime = garch_result.get("regime", "NORMAL")
    except Exception:
        vol_ratio = 1.0
        garch_regime = "NORMAL"

    # ── 计算当前浮盈 ────────────────────────────
    if side == "BUY":
        profit_pts = bid - entry
    else:
        profit_pts = entry - ask

    profit_atr = profit_pts / atr if atr > 0 else 0
    profit_per_lot = profit_pts * 100  # $ per lot (XAUUSD: $1/pt)

# ── 获取时段信息（用于亚盘紧凑追踪）──
    try:
        from bridge.session_analyzer import get_current_session
        current_session = get_current_session()
    except Exception:
        current_session = "UNKNOWN"

    # ── 柔性武器 3：亚盘/震荡市 延迟介入 + 放宽呼吸空间 ──
    # 震荡环境里最怕“刚有点利润就收紧”，所以要更晚介入、介入后也更松。
    active_L1_PROFIT_ATR = L1_PROFIT_ATR
    active_L2_PROFIT_ATR = L2_PROFIT_ATR
    active_L3_PROFIT_ATR = L3_PROFIT_ATR
    active_TP_TRAIL_BASE_ATR = TP_TRAIL_BASE_ATR
    
    # 统一使用全局标准的时段判断，消除信息孤岛
    is_defensive_env = (garch_regime in ("RANGING", "UNCERTAIN")) or (current_session == "ASIA")
    
    if is_defensive_env:
        # 在震荡市或亚盘，必须避免过早把单子切进防守态。
        active_L1_PROFIT_ATR = 1.6  # 震荡/亚盘进一步延后防守，减少 1.x ATR 小利润被噪音扫掉
        active_L2_PROFIT_ATR = 2.4  # 只有出现更厚利润垫才开始锁盈追踪
        active_L3_PROFIT_ATR = 3.6  # 强趋势确认后再进入 L3
        active_TP_TRAIL_BASE_ATR = 2.4 # 震荡环境反而更需要宽呼吸，不能贴脸追踪
        if _logger: _logger.log(f"[游击模式] 震荡防守出场参数已激活: L1={active_L1_PROFIT_ATR}ATR, Trail={active_TP_TRAIL_BASE_ATR}ATR (Regime={garch_regime}, Session={current_session})", "DEBUG")

    # ── L0: 初始止损保护 ───────────────────────
    # 如果还没到初始止损位置，不考虑提前退出
    if side == "BUY" and bid < cur_sl:
        return True, "SL_HIT", None
    if side == "SELL" and ask > cur_sl:
        return True, "SL_HIT", None

    rsi = indicators.get("rsi", 50)
    macd_hist = indicators.get("macd_hist", 0)
    adx = indicators.get("adx", 25)
    macd_hist_prev = indicators.get("macd_hist_prev", macd_hist)

    reason_parts = []
    actions = []

    # ══ L_TIME: 智能时间止损 (Time Stop) ════════
    # 如果持仓时间过长，且盈利微弱或处于亏损，强制平仓释放资金利用率
    open_time = state.get("position_time", 0)
    if open_time > 0:
        hold_seconds = time.time() - open_time
        # 安全守卫：如果持仓时间小于最小阈值，视为 position_time Bug（残留旧值），跳过时间止损
        if hold_seconds < _MIN_HOLD_FOR_TIME_STOP:
            pass  # 持仓过短，不触发时间止损（可能是 position_time=0 或旧值残留）
        elif hold_seconds >= MAX_HOLD_SECONDS_FLAT and profit_atr < TIME_STOP_MIN_PROFIT_ATR:
            # 只有在没有触发 L1 (保本) 的情况下才进行时间止损
            actions.append({
                "action": "CLOSE",
                "volume": vol,
                "reason": f"TIME_STOP 持仓{hold_seconds/60:.0f}分钟 利润{profit_atr:.2f}ATR<{TIME_STOP_MIN_PROFIT_ATR}"
            })
            reason_parts.append(f"时间止损(>45m无趋势)")
            # 触发时间止损后直接退出，不再进行后续移动追踪判断
            reason = " | ".join(reason_parts)
            return True, reason, {"actions": actions}
    else:
        hold_seconds = 0

    # ══ L1: 保本规则 (包含点差补偿) ══════════════════════════
    # 浮盈 >= L1_PROFIT_ATR → 把SL移到 entry + 补偿空间（抵消滑点和点差）
    if (
        profit_atr >= active_L1_PROFIT_ATR
        and profit_atr < active_L2_PROFIT_ATR
        and not state.get("l1_breakeven_done", False)
        and hold_seconds >= _MIN_HOLD_FOR_PROTECTIVE_TRAIL
    ):
        # L1 只做轻量保本，优先覆盖点差/滑点而不是过早锁死小利润。
        spread = max(0.0, ask - bid)
        buffer = max(spread * 1.5, L1_LOCKED_PROFIT_ATR * atr)
        if side == "BUY":
            safe_sl = round(entry + buffer, 2)
            if cur_sl < safe_sl:
                actions.append({
                    "action": "MODIFY_SL",
                    "new_sl": safe_sl,
                    "reason": f"L1_BREAKEVEN entry={entry:.2f} safe_sl={safe_sl:.2f} profit={profit_atr:.2f}ATR"
                })
                reason_parts.append(f"L1保本@safe({safe_sl:.2f})")
                if "exit_detail" not in locals(): exit_detail = {}
                exit_detail["mark_l1_done"] = True
        elif side == "SELL":
            safe_sl = round(entry - buffer, 2)
            if cur_sl > safe_sl:
                actions.append({
                    "action": "MODIFY_SL",
                    "new_sl": safe_sl,
                    "reason": f"L1_BREAKEVEN entry={entry:.2f} safe_sl={safe_sl:.2f} profit={profit_atr:.2f}ATR"
                })
                reason_parts.append(f"L1保本@safe({safe_sl:.2f})")
                if "exit_detail" not in locals(): exit_detail = {}
                exit_detail["mark_l1_done"] = True

    # ══ L2: 动态锁利追踪 (Trailing Stop) ═══════
    # 浮盈 >= active_L2_PROFIT_ATR → 使用有数据支撑的动态距离（vol_ratio）进行跟随
    if (
        profit_atr >= active_L2_PROFIT_ATR
        and profit_atr < active_L3_PROFIT_ATR
        and hold_seconds >= _MIN_HOLD_FOR_PROTECTIVE_TRAIL
    ):
        if cur_tp and cur_tp > 0:
            actions.append({
                "action": "MODIFY_TP",
                "new_tp": 0,
                "reason": "L2取消静态TP，释放趋势利润空间"
            })
            reason_parts.append("L2取消静态TP")
            # 为避免 MT5 EA 并发修改 SL/TP 导致旧状态相互覆盖，取消 TP 后直接返回，下一周期再追踪 SL
            return actions

        # 获取具有严谨数据支撑的动态追踪距离 (基于 GARCH vol_ratio 缩放，亚盘使用 active_TP_TRAIL_BASE_ATR)
        # 不再拍脑门写死 0.3 或 0.8
        trail_dist_atr = _get_trail_distance_atr(atr, vol_ratio, active_TP_TRAIL_BASE_ATR)
        
        # L2 仅要求轻量锁盈，让大部分利润保护交给更宽的动态追踪距离。
        min_locked_profit = L2_MIN_LOCKED_PROFIT_ATR * atr
        
        if side == "BUY":
            trail_sl = round(bid - trail_dist_atr * atr, 2)
            # 兜底：至少锁定 entry + 0.3ATR
            trail_sl = max(trail_sl, entry + min_locked_profit)
        else:
            trail_sl = round(ask + trail_dist_atr * atr, 2)
            # 兜底：至少锁定 entry - 0.3ATR
            trail_sl = min(trail_sl, entry - min_locked_profit)
        
        # 只有当计算出的 trail_sl 比当前 sl 更有利时（棘轮效应），才进行移动
        if (side == "BUY" and trail_sl > cur_sl) or (side == "SELL" and trail_sl < cur_sl):
            actions.append({
                "action": "MODIFY_SL",
                "new_sl": trail_sl,
                "reason": f"L2动态追踪@{trail_sl:.2f} dist={trail_dist_atr:.1f}ATR"
            })
            reason_parts.append(f"L2止损追踪@{trail_sl:.2f}")

        # L2 阶段分批平仓逻辑（解耦追踪止损，即使已经平仓，上面的追踪止损依然生效）
        # 1. 如果是震荡市 (NORMAL/RANGING)，利润极易回吐，优先落袋为安，达标即切仓。
        # 2. 如果是趋势市 (TRENDING/EXTREME_TREND)，让利润奔跑，直到动能明显衰减才切仓。
        momentum_decay = False
        # 动能衰减过滤：要求 MACD 柱状图收缩超过 30%，过滤微小波动杂音
        if side == "BUY" and macd_hist_prev > 0 and macd_hist < (macd_hist_prev * 0.7):
            momentum_decay = True
        elif side == "SELL" and macd_hist_prev < 0 and macd_hist > (macd_hist_prev * 0.7):
            momentum_decay = True

        should_partial_close = False
        # 修复 Bug：之前把 NORMAL（正常波动）也算作震荡市强制切仓了，这会导致单边行情刚启动就被切掉。
        # 只有在明确的 RANGING (波动率低且没有单边方向) 下，才直接落袋为安。
        if garch_regime in ("RANGING", "UNCERTAIN"):
            should_partial_close = True  # 震荡市，直接落袋为安
        elif momentum_decay:
            should_partial_close = True  # 趋势市，动能衰减时切仓锁定

        if (
            not state.get("l2_partial_closed", False)
            and should_partial_close
            and profit_atr >= L2_PARTIAL_MIN_PROFIT_ATR
            and hold_seconds >= _MIN_HOLD_FOR_PARTIAL_CLOSE
        ):
            partial_vol = round(vol * 0.4, 2)
            if partial_vol >= 0.01:
                reason_msg = "L2落袋为安" if garch_regime in ("NORMAL", "RANGING", "UNCERTAIN") else "L2动能衰减"
                actions.append({
                    "action": "CLOSE_PARTIAL",
                    "volume": partial_vol,
                    "reason": f"{reason_msg}锁利40%@{profit_atr:.2f}ATR ({garch_regime})"
                })
                reason_parts.append(f"{reason_msg}止盈40%@{profit_atr:.2f}ATR")
                if "exit_detail" not in locals(): exit_detail = {}
                exit_detail["mark_l2_closed"] = True

    # ══ L3: 奔跑与极紧止损追踪 ══════════════════════════════
    # 浮盈 >= active_L3_PROFIT_ATR → 进入单边狂飙模式，使用收紧的动态距离保护绝大部分利润
    if profit_atr >= active_L3_PROFIT_ATR:
        if cur_tp and cur_tp > 0:
            actions.append({
                "action": "MODIFY_TP",
                "new_tp": 0,
                "reason": "L3取消静态TP，释放尾仓奔跑空间"
            })
            reason_parts.append("L3取消静态TP")
            # 同样防止并发覆盖
            return actions

        # 在 L3 阶段，我们缩小呼吸空间（乘以 L3_TIGHT_MULTIPLIER），因为利润已经足够丰厚，要防范暴力反转
        tight_dist_atr = max(MIN_TRAIL_ATR, _get_trail_distance_atr(atr, vol_ratio, active_TP_TRAIL_BASE_ATR) * L3_TIGHT_MULTIPLIER)
        
        trail_sl3 = round(bid - tight_dist_atr * atr, 2) if side == "BUY" else round(ask + tight_dist_atr * atr, 2)
        
        if (side == "BUY" and trail_sl3 > cur_sl) or (side == "SELL" and trail_sl3 < cur_sl):
            actions.append({
                "action": "MODIFY_SL",
                "new_sl": trail_sl3,
                "reason": f"L3奔跑追踪@{trail_sl3:.2f} dist={tight_dist_atr:.1f}ATR"
            })
            reason_parts.append(f"L3止损追踪@{trail_sl3:.2f}")

        # 同理，防连环平仓，且在 L3 阶段也要判断市场状态和动能
        momentum_decay_l3 = False
        # 动能衰减过滤：要求 MACD 柱状图收缩超过 30%，过滤微小波动杂音
        if side == "BUY" and macd_hist_prev > 0 and macd_hist < (macd_hist_prev * 0.7):
            momentum_decay_l3 = True
        elif side == "SELL" and macd_hist_prev < 0 and macd_hist > (macd_hist_prev * 0.7):
            momentum_decay_l3 = True

        should_partial_close_l3 = False
        if garch_regime in ("RANGING", "UNCERTAIN"):
            should_partial_close_l3 = True
        elif momentum_decay_l3:
            should_partial_close_l3 = True

        if (
            not state.get("l3_partial_closed", False)
            and should_partial_close_l3
            and profit_atr >= L3_PARTIAL_MIN_PROFIT_ATR
            and hold_seconds >= _MIN_HOLD_FOR_PARTIAL_CLOSE
        ):
            vol_remaining = round(vol * 0.6, 2)
            if vol_remaining >= 0.01:
                reason_msg_l3 = "L3落袋为安" if garch_regime in ("NORMAL", "RANGING", "UNCERTAIN") else "L3动能衰减"
                actions.append({
                    "action": "CLOSE_PARTIAL",
                    "volume": vol_remaining,
                    "reason": f"{reason_msg_l3}锁利60%@{profit_atr:.2f}ATR"
                })
                reason_parts.append(f"{reason_msg_l3}止盈60%@{profit_atr:.2f}ATR")
                if "exit_detail" not in locals(): exit_detail = {}
                exit_detail["mark_l3_closed"] = True

    # ══ TP 追踪（动态止盈线）已废除 ══════════════════
    # 之前这里有一段当 profit_atr >= TP_TRAIL_MIN_PROFIT_ATR 时动态修改 TP 的代码。
    # 为了实现“截断亏损，让利润奔跑”，并配合上面的“动能衰减部分平仓”逻辑，
    # 我们彻底废除了在趋势中动态拉近 TP 线的危险行为。
    # 现在的逻辑是：不设 TP 天花板，只靠上移 SL 和动能衰减来出场。

    # ══ 动量退出（不等ATR触发） ═══════════════
    # 之前这里会在 RSI>72 时直接平仓 50%，导致趋势刚启动就被腰斩。
    # 现修改为：动量指标（RSI/MACD）不再触发直接平仓，而是将这些指标作为预警，
    # 实际平仓交给 L2/L3 的动态 ATR 追踪止损来完成，保护大趋势利润。
    
    # ══ MACD 背离警告 ══════════════════════════
    # (原逻辑：MACD收缩>60%平仓50% - 已废弃，交由追踪止损处理)

    # ══ 最终决策 ══════════════════════════════
    if not actions:
        return False, "HOLD", None

    reason = " | ".join(reason_parts)
    if "exit_detail" not in locals():
        exit_detail = {}
    exit_detail["actions"] = actions
    return True, reason, exit_detail


def execute_exit_actions(actions: list, state: dict, bid: float, ask: float):
    """
    执行平仓动作列表。
    返回: (success_count, total_actions)
    """
    from bridge.gold_trader import BRIDGE_TO_EA

    sig_id = f"exit_{int(time.time())}"
    executed = 0

    if not actions:
        return state

    # 从 actions 自身的 reason 字段构建日志（reason_parts/atr 在 execute_exit_actions 作用域中不可访问）
    action_reasons = [a.get("reason", a["action"]) for a in actions]
    _log(f"🚨 出场优化: {' | '.join(action_reasons)}", "WARN")
    
    executed = 0
    for act in actions:
        act_type = act["action"]
        sig_id = f"exit_{int(time.time())}_{act_type}"
        ticket = state.get("position_ticket", 0)
        vol = state.get("position_vol", 0.01)

        # ── MODIFY_TP / MODIFY_SL：EA 期望的精简格式（action 在顶层） ──
        if act_type in ("MODIFY_TP", "MODIFY_SL"):
            new_tp = act.get("new_tp", 0)
            new_sl = act.get("new_sl", 0)

            if not ticket:
                _log(f"{act_type} skipped: no ticket in state", "WARN")
                continue
            if act_type == "MODIFY_TP" and "new_tp" not in act:
                continue
            if act_type == "MODIFY_SL" and not new_sl:
                continue

            # EA 读取方式：string action = _JsonStr(body, "action"); action 必须在顶层
            req = {
                "signal_id": sig_id,
                "action": act_type,
                "symbol": "XAUUSD",
                "side": state.get("position_side", "BUY"),
                "ticket": int(ticket),
                "volume": vol,
            }
            if act_type == "MODIFY_TP":
                req["tp_price"] = float(new_tp)
            else:
                req["sl_price"] = float(new_sl)

        # ── CLOSE_PARTIAL / 其他：标准信号格式 ──
        else:
            req = {
                "signal_id": sig_id,
                "action": act_type,
                "symbol": "XAUUSD",
                "side": state.get("position_side", "BUY"),
                "ticket": int(ticket),
                "volume": float(act.get("volume", vol)),
                "sl_price": float(act.get("sl_price", 0) or 0),
                "tp_price": float(act.get("tp_price", 0) or 0),
                "signal": {
                    "side": state.get("position_side", "BUY"),
                    "symbol": "XAUUSD",
                    "entry_price": 0,
                    "sl_price": 0,
                    "tp_price": 0,
                    "reason": act.get("reason", ""),
                    "confidence": 0.95,
                    "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
                }
            }

        try:
            BRIDGE_TO_EA.mkdir(parents=True, exist_ok=True)
            req_file = BRIDGE_TO_EA / f"{sig_id}.request.json"
            _write_request_atomic(req_file, req)
            _log(f"EXIT ACTION: {act_type} | {act.get('reason','')}")
            executed += 1
        except Exception as e:
            _log(f"EXIT ACTION failed: {e}", "ERROR")

    return executed, len(actions)
