"""
中控子代理:拉取交易所挂单/条件单并规范化展示;撤销单笔或按合约批量撤销;挂止盈止损(先撤条件单再挂).
"""
from __future__ import annotations

import os
import time
from typing import Any

from lib.exchange.okx_orders_lib import fetch_okx_all_open_orders
from lib.hub.hub_symbol_lib import symbols_match


def _coerce_float(*values) -> float | None:
    for v in values:
        if v is None or v == "":
            continue
        try:
            return float(v)
        except (TypeError, ValueError):
            continue
    return None


def _symbol_base_coin(symbol: str) -> str:
    """ZEC/USDT:USDT,ZEC-USDT-SWAP 等统一为标的币 ZEC."""
    s = (symbol or "").strip().upper()
    if not s:
        return ""
    if "-SWAP" in s:
        s = s.replace("-SWAP", "")
    if "-" in s:
        return s.split("-", 1)[0]
    if "/" in s:
        return s.split("/", 1)[0]
    if ":" in s:
        return s.split(":", 1)[0]
    return s


def _order_type_str(order: dict) -> str:
    info = order.get("info") or {}
    if isinstance(info, dict):
        for key in ("orderType", "type", "origType", "algoType", "ordType"):
            val = info.get(key)
            if val:
                return str(val).upper()
    return str(order.get("type") or "").upper()


def _is_conditional_type(typ: str) -> bool:
    t = (typ or "").upper()
    if not t:
        return False
    keys = ("STOP", "TAKE_PROFIT", "TRAIL", "TRIGGER", "CONDITIONAL", "OCO")
    return any(k in t for k in keys)


def _order_label(typ: str, side: str, reduce_only: bool | None) -> str:
    t = (typ or "").upper()
    side_l = (side or "").lower()
    parts = []
    if "TAKE_PROFIT" in t:
        parts.append("止盈")
    elif "STOP" in t:
        parts.append("止损")
    elif "LIMIT" in t:
        parts.append("限价")
    elif "MARKET" in t:
        parts.append("市价")
    else:
        parts.append(typ or "委托")
    if side_l == "buy":
        parts.append("买入")
    elif side_l == "sell":
        parts.append("卖出")
    if reduce_only:
        parts.append("·只减仓")
    return " ".join(parts)


def _normalize_raw_order(order: dict, *, channel: str) -> dict[str, Any] | None:
    if not isinstance(order, dict):
        return None
    info = order.get("info") or {}
    if not isinstance(info, dict):
        info = {}
    oid = order.get("id") or info.get("algoId") or info.get("orderId") or info.get("ordId")
    if oid is None:
        return None
    sym = str(order.get("symbol") or info.get("symbol") or info.get("instId") or "")
    typ = _order_type_str(order)
    side = str(order.get("side") or info.get("side") or "").lower()
    reduce_only = order.get("reduceOnly")
    if reduce_only is None:
        reduce_only = info.get("reduceOnly")
    try:
        reduce_only = bool(reduce_only) if reduce_only is not None else None
    except (TypeError, ValueError):
        reduce_only = None
    sl_trig = _coerce_float(info.get("slTriggerPx"), order.get("stopLossPrice"))
    tp_trig = _coerce_float(info.get("tpTriggerPx"), order.get("takeProfitPrice"))
    trig = _coerce_float(
        order.get("stopPrice"),
        order.get("triggerPrice"),
        info.get("triggerPrice"),
        info.get("stopPrice"),
        info.get("triggerPx"),
        sl_trig,
        tp_trig,
    )
    price = _coerce_float(order.get("price"), info.get("price"), info.get("ordPx"))
    amt = _coerce_float(order.get("amount"), order.get("remaining"), info.get("quantity"), info.get("origQty"), info.get("sz"))
    category = "conditional" if _is_conditional_type(typ) or channel == "algo" else "limit"
    label = _order_label(typ, side, reduce_only)
    if sl_trig is not None and tp_trig is not None:
        label = f"止盈止损 SL={sl_trig:g} TP={tp_trig:g}"
    elif sl_trig is not None:
        label = f"止损 {sl_trig:g}"
    elif tp_trig is not None:
        label = f"止盈 {tp_trig:g}"
    return {
        "id": str(oid),
        "symbol": sym,
        "channel": channel,
        "category": category,
        "label": label,
        "type": typ,
        "side": side,
        "amount": amt,
        "trigger_price": trig,
        "price": price,
        "reduce_only": reduce_only,
        "status": str(order.get("status") or info.get("status") or "open"),
    }


def _okx_normalize_orders(raw: dict, channel: str) -> list[dict[str, Any]]:
    """OKX 算法单常一笔同时含 SL+TP,拆成两条供中控「交易所止盈止损」展示."""
    n = _normalize_raw_order(dict(raw), channel=channel)
    if not n:
        return []
    info = raw.get("info") or {}
    if not isinstance(info, dict):
        info = {}
    sl_trig = _coerce_float(info.get("slTriggerPx"), raw.get("stopLossPrice"))
    tp_trig = _coerce_float(info.get("tpTriggerPx"), raw.get("takeProfitPrice"))
    if sl_trig is None or tp_trig is None or sl_trig == tp_trig:
        return [n]
    base_id = n["id"]
    rows: list[dict[str, Any]] = []
    for role, px, lbl in (
        ("sl", sl_trig, f"止损 {sl_trig:g}"),
        ("tp", tp_trig, f"止盈 {tp_trig:g}"),
    ):
        row = dict(n)
        row["id"] = f"{base_id}:{role}"
        row["algo_id"] = base_id
        row["label"] = lbl
        row["trigger_price"] = px
        row["category"] = "conditional"
        row["channel"] = channel
        rows.append(row)
    return rows


def _okx_algo_order_id(order_id: str) -> str:
    oid = str(order_id or "")
    if ":" in oid:
        return oid.split(":", 1)[0]
    return oid


def _binance_list(ex: Any, symbol: str | None) -> list[dict]:
    ex.load_markets()
    out: list[dict] = []
    symbols: list[str] = []
    if symbol:
        try:
            symbols = [ex.market(symbol)["symbol"]]
        except Exception:
            symbols = [symbol]
    else:
        symbols = []
        try:
            for p in ex.fetch_positions() or []:
                sym = p.get("symbol")
                if sym:
                    symbols.append(sym)
        except Exception:
            pass
    if symbol and not symbols:
        symbols = [symbol]

    def collect(ex_sym: str) -> None:
        market = ex.market(ex_sym)
        contract_id = market.get("id")
        try:
            for o in ex.fetch_open_orders(ex_sym) or []:
                item = dict(o)
                item["_channel"] = "regular"
                n = _normalize_raw_order(item, channel="regular")
                if n:
                    out.append(n)
        except Exception:
            pass
        try:
            if contract_id and hasattr(ex, "fapiPrivateGetOpenAlgoOrders"):
                raw = ex.fapiPrivateGetOpenAlgoOrders({"symbol": contract_id})
                items = raw if isinstance(raw, list) else (raw.get("orders") or raw.get("data") or [])
                for info in items or []:
                    if not isinstance(info, dict):
                        continue
                    wrapped = {
                        "id": info.get("algoId") or info.get("orderId"),
                        "symbol": ex_sym,
                        "info": info,
                        "type": info.get("orderType") or info.get("type"),
                        "side": (info.get("side") or "").lower(),
                        "amount": info.get("quantity") or info.get("origQty"),
                        "stopPrice": info.get("triggerPrice") or info.get("stopPrice"),
                        "reduceOnly": info.get("reduceOnly"),
                    }
                    n = _normalize_raw_order(wrapped, channel="algo")
                    if n:
                        out.append(n)
        except Exception:
            pass

    if symbols:
        seen = set()
        for s in symbols:
            if s in seen:
                continue
            seen.add(s)
            collect(s)
    return out


def _okx_list(ex: Any, symbol: str | None) -> list[dict]:
    ex.load_markets()
    out: list[dict] = []
    symbols: list[str] = []
    if symbol:
        try:
            symbols = [ex.market(symbol)["symbol"]]
        except Exception:
            symbols = [symbol]
    else:
        try:
            for p in ex.fetch_positions() or []:
                sym = p.get("symbol")
                if sym:
                    symbols.append(sym)
        except Exception:
            pass
    if symbol and not symbols:
        symbols = [symbol]
    seen: set[tuple[str, str]] = set()
    for sym in symbols:
        try:
            for o in fetch_okx_all_open_orders(ex, sym):
                ch = "algo" if _is_conditional_type(_order_type_str(o)) else "regular"
                for n in _okx_normalize_orders(dict(o), channel=ch):
                    key = (n["id"], n.get("channel") or ch)
                    if key in seen:
                        continue
                    seen.add(key)
                    out.append(n)
        except Exception:
            pass
    return out


def _gate_extract_trigger_rule(info: dict) -> int | None:
    if not isinstance(info, dict):
        return None
    trig = info.get("trigger")
    if isinstance(trig, dict) and trig.get("rule") is not None:
        try:
            return int(trig["rule"])
        except (TypeError, ValueError):
            pass
    try:
        return int(info.get("rule"))
    except (TypeError, ValueError):
        return None


def _gate_tpsl_role_from_rule(rule: int | None, direction: str) -> str | None:
    if rule is None:
        return None
    d = (direction or "long").strip().lower()
    if d == "long":
        return "sl" if rule == 2 else ("tp" if rule == 1 else None)
    return "sl" if rule == 1 else ("tp" if rule == 2 else None)


def _gate_trigger_params(ex: Any) -> dict:
    p = {"type": "swap", "trigger": True}
    try:
        ex.load_unified_status()
        if ex.options.get("unifiedAccount"):
            p["unifiedAccount"] = True
    except Exception:
        pass
    return p


def _gate_list(ex: Any, symbol: str | None) -> list[dict]:
    ex.load_markets()
    out: list[dict] = []
    symbols: list[str] = []
    if symbol:
        try:
            symbols = [ex.market(symbol)["symbol"]]
        except Exception:
            symbols = [symbol]
    else:
        try:
            for p in ex.fetch_positions() or []:
                sym = p.get("symbol")
                if sym:
                    symbols.append(sym)
        except Exception:
            pass
    if symbol and not symbols:
        symbols = [symbol]
    trig_params = _gate_trigger_params(ex)
    seen = set()
    for sym in symbols:
        if sym in seen:
            continue
        seen.add(sym)
        try:
            for o in ex.fetch_open_orders(sym) or []:
                n = _normalize_raw_order(dict(o), channel="regular")
                if n:
                    out.append(n)
        except Exception:
            pass
        try:
            for o in ex.fetch_open_orders(sym, params=trig_params) or []:
                item = dict(o)
                item["type"] = item.get("type") or "trigger"
                n = _normalize_raw_order(item, channel="algo")
                if n:
                    info = o.get("info") if isinstance(o.get("info"), dict) else {}
                    rule = _gate_extract_trigger_rule(info)
                    if rule is not None:
                        n["gate_trigger_rule"] = rule
                    out.append(n)
        except Exception:
            pass
    return out


def list_open_orders(ex: Any, exchange_kind: str, symbol: str | None = None) -> list[dict]:
    kind = (exchange_kind or "binance").lower()
    if kind == "binance":
        orders = _binance_list(ex, symbol)
    elif kind == "okx":
        orders = _okx_list(ex, symbol)
    else:
        orders = _gate_list(ex, symbol)
    if symbol:
        orders = [o for o in orders if symbols_match(symbol, o.get("symbol") or "")]
    # 去重 id+channel
    seen: set[tuple[str, str]] = set()
    uniq: list[dict] = []
    for o in orders:
        key = (o["id"], o["channel"])
        if key in seen:
            continue
        seen.add(key)
        uniq.append(o)
    return uniq


def _enrich_gate_conditional_labels(cond: list[dict], side: str) -> None:
    """Gate 仓位类触发单在 ccxt 中常显示为「市价·只减仓」,按 trigger.rule 标为止盈/止损."""
    direction = (side or "long").strip().lower()
    for o in cond:
        if not isinstance(o, dict):
            continue
        if (o.get("label") or "").startswith(("止盈", "止损")):
            continue
        role = _gate_tpsl_role_from_rule(o.get("gate_trigger_rule"), direction)
        trig = o.get("trigger_price")
        if not role or trig is None:
            continue
        try:
            trig_f = float(trig)
        except (TypeError, ValueError):
            continue
        prefix = "止损" if role == "sl" else "止盈"
        o["label"] = f"{prefix} {trig_f:g}"


def attach_orders_to_positions(positions: list[dict], orders: list[dict]) -> None:
    for p in positions:
        sym = p.get("symbol") or ""
        matched = [o for o in orders if symbols_match(sym, o.get("symbol") or "")]
        cond = [o for o in matched if o.get("category") == "conditional"]
        _enrich_gate_conditional_labels(cond, p.get("side") or "long")
        from lib.hub.hub_order_sync_lib import dedupe_conditional_orders_by_role

        p["conditional_orders"] = dedupe_conditional_orders_by_role(cond)
        p["regular_orders"] = [o for o in matched if o.get("category") != "conditional"]


def cancel_order(
    ex: Any,
    exchange_kind: str,
    symbol: str,
    order_id: str,
    channel: str = "regular",
) -> None:
    kind = (exchange_kind or "binance").lower()
    ex.load_markets()
    market = ex.market(symbol)
    unified = market["symbol"]
    ch = (channel or "regular").lower()
    if kind == "binance" and ch == "algo":
        contract_id = market.get("id")
        if contract_id and hasattr(ex, "fapiPrivateDeleteAlgoOrder"):
            ex.fapiPrivateDeleteAlgoOrder({"symbol": contract_id, "algoId": str(order_id)})
            return
    params = None
    if kind == "gate" and ch == "algo":
        params = _gate_trigger_params(ex)
    elif kind == "okx" and ch == "algo":
        params = {"stop": True}
    oid = _okx_algo_order_id(order_id) if kind == "okx" else str(order_id)
    ex.cancel_order(oid, unified, params)


def cancel_orders_for_symbol(
    ex: Any,
    exchange_kind: str,
    symbol: str,
    *,
    scope: str = "all",
) -> int:
    """scope: all | conditional | limit"""
    orders = list_open_orders(ex, exchange_kind, symbol)
    if scope == "conditional":
        orders = [o for o in orders if o.get("category") == "conditional"]
    elif scope == "limit":
        orders = [o for o in orders if o.get("category") != "conditional"]
    n = 0
    for o in orders:
        try:
            cancel_order(ex, exchange_kind, symbol, o["id"], o.get("channel") or "regular")
            n += 1
        except Exception as e:
            print(
                f"[cancel_orders_for_symbol] {exchange_kind} {symbol} id={o.get('id')}: {e}",
                flush=True,
            )
    return n


def _binance_cancel_algo_open(ex: Any, symbol: str) -> None:
    try:
        market = ex.market(symbol)
        cid = market.get("id")
        if cid and hasattr(ex, "fapiPrivateDeleteAlgoOpenOrders"):
            ex.fapiPrivateDeleteAlgoOpenOrders({"symbol": cid})
    except Exception:
        pass


def _binance_trigger_params() -> dict[str, Any]:
    wt = (os.getenv("BINANCE_TRIGGER_WORKING_TYPE") or "CONTRACT_PRICE").strip().upper()
    if wt not in ("CONTRACT_PRICE", "MARK_PRICE"):
        wt = "CONTRACT_PRICE"
    return {"workingType": wt}


def _binance_place_tp_sl(
    ex: Any,
    symbol: str,
    direction: str,
    amount: float,
    stop_loss: float,
    take_profit: float,
    *,
    position_mode: str = "hedge",
) -> None:
    ex.load_markets()
    market = ex.market(symbol)
    if not market.get("swap"):
        raise RuntimeError("仅支持永续合约")
    close_side = "sell" if direction == "long" else "buy"
    amt = float(ex.amount_to_precision(symbol, float(amount)))
    if amt <= 0:
        raise RuntimeError("止盈止损:可平数量经精度舍入后为 0")
    sl_px = ex.price_to_precision(symbol, float(stop_loss))
    tp_px = ex.price_to_precision(symbol, float(take_profit))
    common = dict(_binance_trigger_params())
    if (position_mode or "hedge").lower() in ("hedge", "dual", "double", "hedged"):
        common["positionSide"] = "LONG" if direction == "long" else "SHORT"
    last_err: Exception | None = None
    for attempt in range(6):
        try:
            ex.create_order(
                symbol, "STOP_MARKET", close_side, amt, None, dict(common, stopPrice=sl_px)
            )
            time.sleep(0.05)
            ex.create_order(
                symbol,
                "TAKE_PROFIT_MARKET",
                close_side,
                amt,
                None,
                dict(common, stopPrice=tp_px),
            )
            return
        except Exception as e:
            last_err = e
            cancel_orders_for_symbol(ex, "binance", symbol, scope="conditional")
            _binance_cancel_algo_open(ex, symbol)
            time.sleep(0.2 * (attempt + 1))
    raise RuntimeError(f"Binance 未接受止盈/止损:{last_err}")


def _okx_order_params(
    direction: str,
    *,
    reduce_only: bool,
    pos_mode: str,
    td_mode: str,
    for_algo_tpsl: bool = False,
) -> dict:
    params: dict[str, Any] = {"tdMode": td_mode or "cross"}
    if (pos_mode or "hedge").lower() in ("hedge", "long_short_mode", "dual"):
        ps = "long" if direction == "long" else "short"
        params["posSide"] = ps
        params["positionSide"] = ps
    # OKX 条件/OCO 算法单勿带 reduceOnly,否则可能被当市价减仓立即成交
    if reduce_only and not for_algo_tpsl:
        params["reduceOnly"] = True
    return params


def _okx_place_tp_sl(
    ex: Any,
    symbol: str,
    direction: str,
    amount: float,
    stop_loss: float,
    take_profit: float,
    *,
    pos_mode: str = "hedge",
    td_mode: str = "cross",
) -> None:
    """OKX 永续:一笔 OCO 算法单挂止盈+止损(勿 reduceOnly + 分两笔 market)."""
    ex.load_markets()
    close_side = "sell" if direction == "long" else "buy"
    amt = float(ex.amount_to_precision(symbol, float(amount)))
    if amt <= 0:
        raise RuntimeError("止盈止损:可平数量经精度舍入后为 0")
    base = _okx_order_params(
        direction,
        reduce_only=False,
        pos_mode=pos_mode,
        td_mode=td_mode,
        for_algo_tpsl=True,
    )
    sl_px = ex.price_to_precision(symbol, float(stop_loss))
    tp_px = ex.price_to_precision(symbol, float(take_profit))
    order_params = {
        **base,
        "stopLossPrice": float(sl_px),
        "takeProfitPrice": float(tp_px),
        "tpOrdPx": "-1",
        "slOrdPx": "-1",
    }
    last_err: Exception | None = None
    for attempt in range(6):
        try:
            ex.create_order(symbol, "oco", close_side, amt, None, order_params)
            return
        except Exception as e:
            last_err = e
            cancel_orders_for_symbol(ex, "okx", symbol, scope="conditional")
            time.sleep(0.2 * (attempt + 1))
    raise RuntimeError(f"OKX 未接受止盈/止损条件单:{last_err}")


def _gate_tpsl_env() -> tuple[bool, int, int, str]:
    use_pos = (os.getenv("GATE_TPSL_USE_POSITION_ORDER") or "true").lower() in ("1", "true", "yes")
    exp = int(os.getenv("GATE_TPSL_TRIGGER_EXPIRATION", str(7 * 86400)))
    pt = int(os.getenv("GATE_TPSL_PRICE_TYPE", "0"))
    if pt < 0 or pt > 2:
        pt = 0
    pos_mode = (os.getenv("GATE_POS_MODE") or "hedge").strip().lower()
    return use_pos, exp, pt, pos_mode


def _gate_place_tp_sl_position(
    ex: Any,
    symbol: str,
    direction: str,
    stop_loss: float,
    take_profit: float,
    *,
    pos_mode: str,
    price_type: int,
    expiration: int,
) -> None:
    ex.load_markets()
    market = ex.market(symbol)
    if not market.get("swap"):
        raise RuntimeError("仅支持永续合约")
    settle = market["settleId"]
    contract = market["id"]
    order_type = "close-long-position" if direction == "long" else "close-short-position"
    close_side = "sell" if direction == "long" else "buy"
    sl_rule, tp_rule = (2, 1) if close_side == "sell" else (1, 2)
    initial: dict[str, Any] = {
        "contract": contract,
        "size": 0,
        "price": "0",
        "close": True,
        "reduce_only": True,
        "tif": "ioc",
        "text": "api",
    }
    if pos_mode in ("hedge", "dual", "double"):
        initial["auto_size"] = "close_long" if direction == "long" else "close_short"
        # Gate API 1018:auto_size=close_long|close_short 时 initial.close 须为 false
        initial["close"] = False
    sl_s = ex.price_to_precision(symbol, float(stop_loss))
    tp_s = ex.price_to_precision(symbol, float(take_profit))

    def _payload(trigger_price: str, rule: int) -> dict:
        trig: dict[str, Any] = {
            "strategy_type": 0,
            "price_type": price_type,
            "price": trigger_price,
            "rule": rule,
        }
        if expiration > 0:
            trig["expiration"] = expiration
        return {
            "settle": settle,
            "initial": dict(initial),
            "trigger": trig,
            "order_type": order_type,
        }

    last_err: Exception | None = None
    for attempt in range(6):
        try:
            ex.privateFuturesPostSettlePriceOrders(_payload(sl_s, sl_rule))
            try:
                ex.privateFuturesPostSettlePriceOrders(_payload(tp_s, tp_rule))
            except Exception:
                # 保留已挂止损,仅放弃本次 TP
                raise
            return
        except Exception as e:
            last_err = e
            time.sleep(0.2 * (attempt + 1))
    raise RuntimeError(f"Gate 仓位类止盈/止损未接受:{last_err}")


def _gate_place_tp_sl_legacy(
    ex: Any,
    symbol: str,
    direction: str,
    amount: float,
    stop_loss: float,
    take_profit: float,
) -> None:
    ex.load_markets()
    close_side = "sell" if direction == "long" else "buy"
    base = {"reduceOnly": True}
    last_err: Exception | None = None
    for attempt in range(6):
        try:
            ex.create_order(
                symbol,
                "market",
                close_side,
                amount,
                None,
                dict(base, stopLossPrice=float(stop_loss)),
            )
            ex.create_order(
                symbol,
                "market",
                close_side,
                amount,
                None,
                dict(base, takeProfitPrice=float(take_profit)),
            )
            return
        except Exception as e:
            last_err = e
            time.sleep(0.2 * (attempt + 1))
    raise RuntimeError(f"Gate 条件止盈/止损未接受:{last_err}")


def _gate_td_mode_cross() -> bool:
    td = (os.getenv("GATE_TD_MODE") or "cross").strip().lower()
    return td in ("cross", "cross_margin")


def _gate_last_price(ex: Any, symbol: str) -> float | None:
    ex.load_markets()
    unified = ex.market(symbol)["symbol"]
    try:
        t = ex.fetch_ticker(unified)
    except Exception:
        return None
    if not isinstance(t, dict):
        return None
    info = t.get("info") if isinstance(t.get("info"), dict) else {}
    for key in ("last", "mark", "close", "index_price"):
        v = t.get(key) if key in t else info.get(key)
        try:
            f = float(v)
            if f > 0:
                return f
        except (TypeError, ValueError):
            continue
    return None


def _gate_clamp_tpsl_prices(
    ex: Any,
    symbol: str,
    direction: str,
    stop_loss: float,
    take_profit: float,
) -> tuple[float, float]:
    """
    Gate price_orders:空仓止损/多仓止盈 trigger>last;空仓止盈/多仓止损 trigger<last.
    """
    last = _gate_last_price(ex, symbol)
    if last is None or last <= 0:
        return float(stop_loss), float(take_profit)
    ex.load_markets()
    unified = ex.market(symbol)["symbol"]
    gap_pct = float(os.getenv("GATE_TPSL_LAST_PRICE_GAP_PCT", "0.05") or "0.05")
    gap = max(0.0, gap_pct) / 100.0
    if gap <= 0:
        gap = 0.0005
    sl = float(stop_loss)
    tp = float(take_profit)
    d = (direction or "long").strip().lower()
    if d == "short":
        if sl <= last:
            sl = float(ex.price_to_precision(unified, last * (1 + gap)))
        if tp >= last:
            tp = float(ex.price_to_precision(unified, last * (1 - gap)))
    else:
        if sl >= last:
            sl = float(ex.price_to_precision(unified, last * (1 - gap)))
        if tp <= last:
            tp = float(ex.price_to_precision(unified, last * (1 + gap)))
    return sl, tp


def _gate_place_tp_sl(
    ex: Any,
    symbol: str,
    direction: str,
    amount: float,
    stop_loss: float,
    take_profit: float,
) -> None:
    use_pos, exp, pt, pos_mode = _gate_tpsl_env()
    pos_err: Exception | None = None
    if use_pos:
        try:
            _gate_place_tp_sl_position(
                ex, symbol, direction, stop_loss, take_profit,
                pos_mode=pos_mode, price_type=pt, expiration=exp,
            )
            return
        except Exception as e:
            pos_err = e
            if _gate_td_mode_cross():
                raise RuntimeError(
                    f"Gate 仓位类止盈/止损未接受(全仓不支持 ccxt 条件单回退):{pos_err}"
                ) from e
    try:
        _gate_place_tp_sl_legacy(ex, symbol, direction, amount, stop_loss, take_profit)
    except Exception as legacy_err:
        if pos_err is not None:
            raise RuntimeError(
                f"Gate 仓位类止盈/止损未接受:{pos_err};条件单回退亦失败:{legacy_err}"
            ) from legacy_err
        raise


def replace_position_tpsl(
    ex: Any,
    exchange_kind: str,
    symbol: str,
    direction: str,
    amount: float,
    stop_loss: float,
    take_profit: float,
) -> dict[str, Any]:
    """
    先撤销该合约全部条件单,再挂止盈+止损.与三实例策略页逻辑对齐(读各目录 .env 中 GATE_/BINANCE_/OKX_ 参数).
    """
    kind = (exchange_kind or "binance").lower()
    direction = (direction or "long").strip().lower()
    if direction not in ("long", "short"):
        raise ValueError("direction 须为 long 或 short")
    sl = float(stop_loss)
    tp = float(take_profit)
    if sl <= 0 or tp <= 0:
        raise ValueError("止损,止盈价格须大于 0")
    ex.load_markets()
    cancelled = cancel_orders_for_symbol(ex, kind, symbol, scope="conditional")
    if kind == "binance":
        _binance_cancel_algo_open(ex, symbol)
    time.sleep(0.08)
    amt = float(amount)
    if amt <= 0:
        raise ValueError("持仓数量无效")
    if kind == "binance":
        pm = (os.getenv("BINANCE_POSITION_MODE") or "hedge").strip().lower()
        _binance_place_tp_sl(ex, symbol, direction, amt, sl, tp, position_mode=pm)
    elif kind == "okx":
        pm = (os.getenv("OKX_POS_MODE") or "hedge").strip().lower()
        td = (os.getenv("OKX_TD_MODE") or "cross").strip()
        _okx_place_tp_sl(ex, symbol, direction, amt, sl, tp, pos_mode=pm, td_mode=td)
    else:
        sl, tp = _gate_clamp_tpsl_prices(ex, symbol, direction, sl, tp)
        _gate_place_tp_sl(ex, symbol, direction, amt, sl, tp)
    return {
        "symbol": symbol,
        "direction": direction,
        "amount": amt,
        "stop_loss": sl,
        "take_profit": tp,
        "cancelled_conditional": cancelled,
    }
