diff --git a/manual_trading_hub/exchange_orders.py b/manual_trading_hub/exchange_orders.py index 14bb07e..3983a20 100644 --- a/manual_trading_hub/exchange_orders.py +++ b/manual_trading_hub/exchange_orders.py @@ -21,6 +21,22 @@ def _coerce_float(*values) -> float | None: 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 symbols_match(position_symbol: str, order_symbol: str) -> bool: a = (position_symbol or "").strip().upper() b = (order_symbol or "").strip().upper() @@ -28,6 +44,9 @@ def symbols_match(position_symbol: str, order_symbol: str) -> bool: return False if a == b: return True + ba, bb = _symbol_base_coin(a), _symbol_base_coin(b) + if ba and bb and ba == bb: + return True for suf in (":USDT", "/USDT:USDT", "/USDT"): a2 = a.replace(suf, "") b2 = b.replace(suf, "") @@ -54,7 +73,7 @@ def _is_conditional_type(typ: str) -> bool: t = (typ or "").upper() if not t: return False - keys = ("STOP", "TAKE_PROFIT", "TRAIL", "TRIGGER", "CONDITIONAL") + keys = ("STOP", "TAKE_PROFIT", "TRAIL", "TRIGGER", "CONDITIONAL", "OCO") return any(k in t for k in keys) @@ -90,7 +109,7 @@ def _normalize_raw_order(order: dict, *, channel: str) -> dict[str, Any] | None: 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 "") + 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") @@ -137,6 +156,42 @@ def _normalize_raw_order(order: dict, *, channel: str) -> dict[str, Any] | None: } +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: + 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] = [] @@ -227,14 +282,12 @@ def _okx_list(ex: Any, symbol: str | None) -> list[dict]: try: for o in fetch_okx_all_open_orders(ex, sym): ch = "algo" if _is_conditional_type(_order_type_str(o)) else "regular" - n = _normalize_raw_order(dict(o), channel=ch) - if not n: - continue - key = (n["id"], n.get("channel") or ch) - if key in seen: - continue - seen.add(key) - out.append(n) + 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 @@ -345,7 +398,8 @@ def cancel_order( params = None if kind == "gate" and ch == "algo": params = _gate_trigger_params(ex) - ex.cancel_order(str(order_id), unified, params) + 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( diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 2425cc7..66ac374 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -43,7 +43,7 @@ HUB_BRIDGE_TOKEN = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN") _trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower() HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off") DIR = Path(__file__).resolve().parent -HUB_BUILD = "20260525-perf" +HUB_BUILD = "20260525-okx-tpsl" HUB_AGENT_TIMEOUT = float(os.getenv("HUB_AGENT_TIMEOUT", "8")) HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10")) _board_key_prices_raw = (os.getenv("HUB_BOARD_KEY_PRICES", "true") or "").strip().lower() diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 56bec96..8af5487 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -410,10 +410,27 @@ `; } + function pickExTpslOrders(cond) { + let sl = cond.find((o) => /^止损\b/.test(o.label || "")); + let tp = cond.find((o) => /^止盈\b/.test(o.label || "") && !(o.label || "").includes("止盈止损")); + if (!sl || !tp) { + const combo = cond.find((o) => (o.label || "").includes("止盈止损")); + if (combo) { + const m = (combo.label || "").match(/SL=([\d.eE+-]+).*TP=([\d.eE+-]+)/i); + if (m) { + if (!sl) sl = { ...combo, label: "止损", trigger_price: Number(m[1]) }; + if (!tp) tp = { ...combo, label: "止盈", trigger_price: Number(m[2]) }; + } + } + } + if (!sl) sl = cond.find((o) => (o.label || "").includes("止损")); + if (!tp) tp = cond.find((o) => (o.label || "").includes("止盈") && o !== sl); + return { sl, tp }; + } + function renderExTpslRows(exchangeId, symbol, cond) { const symAttr = esc(symbol || "").replace(/"/g, """); - const sl = cond.find((o) => (o.label || "").includes("止损")); - const tp = cond.find((o) => (o.label || "").includes("止盈")); + const { sl, tp } = pickExTpslOrders(cond); function row(label, o) { if (!o) { return `