""" 中控子代理:拉取交易所挂单/条件单并规范化展示;撤销单笔或按合约批量撤销。 """ from __future__ import annotations from typing import Any 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 symbols_match(position_symbol: str, order_symbol: str) -> bool: a = (position_symbol or "").strip().upper() b = (order_symbol or "").strip().upper() if not a or not b: return False if a == b: return True for suf in (":USDT", "/USDT:USDT", "/USDT"): a2 = a.replace(suf, "") b2 = b.replace(suf, "") if f"{a2}/USDT" == b or f"{a2}/USDT:USDT" == b: return True if f"{b2}/USDT" == a or f"{b2}/USDT:USDT" == a: return True if a2 == b2: return True return False 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") 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 "") 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 trig = _coerce_float( order.get("stopPrice"), order.get("triggerPrice"), info.get("triggerPrice"), info.get("stopPrice"), info.get("slTriggerPx"), info.get("tpTriggerPx"), ) price = _coerce_float(order.get("price"), info.get("price")) amt = _coerce_float(order.get("amount"), order.get("remaining"), info.get("quantity"), info.get("origQty")) category = "conditional" if _is_conditional_type(typ) or channel == "algo" else "limit" return { "id": str(oid), "symbol": sym, "channel": channel, "category": category, "label": _order_label(typ, side, reduce_only), "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 _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() for sym in symbols: if sym in seen: continue seen.add(sym) try: for o in ex.fetch_open_orders(sym) or []: ch = "algo" if _is_conditional_type(_order_type_str(o)) else "regular" n = _normalize_raw_order(dict(o), channel=ch) if n: out.append(n) except Exception: pass return out 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: 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 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 "")] p["conditional_orders"] = [o for o in matched if o.get("category") == "conditional"] 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) ex.cancel_order(str(order_id), 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: pass return n