diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 09d5aa8..2ba4d7d 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -45,6 +45,7 @@ from fib_key_monitor_lib import ( key_signal_type_for_trade_record, stored_key_signal_type, ) +from okx_orders_lib import fetch_okx_all_open_orders from key_sl_tp_lib import ( breakeven_enabled_from_row, normalize_sl_tp_mode, @@ -2467,7 +2468,7 @@ def cancel_okx_swap_open_orders(exchange_symbol): except Exception: pass try: - for o in exchange.fetch_open_orders(exchange_symbol) or []: + for o in fetch_okx_all_open_orders(exchange, exchange_symbol): oid = o.get("id") if oid is None: continue @@ -2616,26 +2617,52 @@ def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data): return stop_loss, take_profit -def _okx_tpsl_slot_from_order(order, exchange_symbol): +def _okx_tpsl_slot_build(exchange_symbol, order_id, trigger_price, order_type=""): + if trigger_price is None or order_id is None: + return None + sym = exchange_symbol.replace(":USDT", "").replace("/USDT:USDT", "") + return { + "order_id": str(order_id), + "trigger_price": float(trigger_price), + "trigger_display": format_price_for_symbol(sym, trigger_price), + "type": str(order_type or ""), + } + + +def _okx_tpsl_slots_from_order(order, exchange_symbol): + """从单笔 OKX 订单解析 SL/TP(算法单常同时带 slTriggerPx 与 tpTriggerPx)。""" + if not isinstance(order, dict): + return None, None info = order.get("info") or {} + if not isinstance(info, dict): + info = {} oid = order.get("id") or info.get("algoId") or info.get("ordId") - trig = _coerce_float( - info.get("slTriggerPx"), - info.get("tpTriggerPx"), + if oid is None: + return None, None + ord_type = str(order.get("type") or info.get("ordType") or "") + sl_px = _coerce_float( order.get("stopLossPrice"), + info.get("slTriggerPx"), + info.get("slOrdPx"), + ) + tp_px = _coerce_float( order.get("takeProfitPrice"), + info.get("tpTriggerPx"), + info.get("tpOrdPx"), + ) + sl_slot = _okx_tpsl_slot_build(exchange_symbol, oid, sl_px, ord_type) if sl_px is not None else None + tp_slot = _okx_tpsl_slot_build(exchange_symbol, oid, tp_px, ord_type) if tp_px is not None else None + if sl_slot or tp_slot: + return sl_slot, tp_slot + trig = _coerce_float( + info.get("triggerPx"), + order.get("triggerPrice"), + order.get("stopPrice"), ) if trig is None: - return None - return { - "order_id": str(oid) if oid is not None else None, - "trigger_price": float(trig), - "trigger_display": format_price_for_symbol( - exchange_symbol.replace(":USDT", "").replace("/USDT:USDT", ""), - trig, - ), - "type": str(order.get("type") or info.get("ordType") or ""), - } + return None, None + one = _okx_tpsl_slot_build(exchange_symbol, oid, trig, ord_type) + return one, None def fetch_exchange_tpsl_slots(exchange_symbol, direction, plan_sl=None, plan_tp=None): @@ -2647,35 +2674,18 @@ def fetch_exchange_tpsl_slots(exchange_symbol, direction, plan_sl=None, plan_tp= return slots try: ensure_markets_loaded() - ambiguous = [] - for order in exchange.fetch_open_orders(exchange_symbol) or []: - slot = _okx_tpsl_slot_from_order(order, exchange_symbol) - if not slot or not slot.get("order_id"): - continue - trig = slot.get("trigger_price") - if plan_sl is not None and plan_tp is not None: - try: - role = "sl" if abs(trig - float(plan_sl)) <= abs(trig - float(plan_tp)) else "tp" - except Exception: - role = None - elif plan_sl is not None: - role = "sl" - elif plan_tp is not None: - role = "tp" - else: - ambiguous.append(slot) - continue - if role in ("sl", "tp") and slots[role] is None: - slots[role] = slot - for slot in ambiguous: - trig = slot.get("trigger_price") - if trig is None: - continue - try: - plan_sl_f = float(plan_sl) if plan_sl is not None else None - plan_tp_f = float(plan_tp) if plan_tp is not None else None - except Exception: - plan_sl_f = plan_tp_f = None + plan_sl_f = plan_tp_f = None + try: + if plan_sl is not None: + plan_sl_f = float(plan_sl) + if plan_tp is not None: + plan_tp_f = float(plan_tp) + except Exception: + plan_sl_f = plan_tp_f = None + + def assign_role(trig, slot): + if trig is None or slot is None: + return if plan_sl_f is not None and plan_tp_f is not None: role = "sl" if abs(trig - plan_sl_f) <= abs(trig - plan_tp_f) else "tp" elif plan_sl_f is not None: @@ -2683,9 +2693,30 @@ def fetch_exchange_tpsl_slots(exchange_symbol, direction, plan_sl=None, plan_tp= elif plan_tp_f is not None: role = "tp" else: - continue + return if slots[role] is None: slots[role] = slot + + for order in fetch_okx_all_open_orders(exchange, exchange_symbol): + sl_slot, tp_slot = _okx_tpsl_slots_from_order(order, exchange_symbol) + if sl_slot and slots["sl"] is None: + slots["sl"] = sl_slot + if tp_slot and slots["tp"] is None: + slots["tp"] = tp_slot + if sl_slot or tp_slot: + continue + info = order.get("info") or {} + oid = order.get("id") or info.get("algoId") + trig = _coerce_float(info.get("triggerPx"), order.get("triggerPrice")) + if oid is None or trig is None: + continue + slot = _okx_tpsl_slot_build( + exchange_symbol, + oid, + trig, + str(order.get("type") or info.get("ordType") or ""), + ) + assign_role(trig, slot) except Exception: pass return slots @@ -3717,7 +3748,7 @@ def fib_limit_order_status(exchange_symbol, order_id): except Exception: pass try: - for o in exchange.fetch_open_orders(exchange_symbol) or []: + for o in fetch_okx_all_open_orders(exchange, exchange_symbol): if str(o.get("id")) == oid: return "open" except Exception: diff --git a/manual_trading_hub/exchange_orders.py b/manual_trading_hub/exchange_orders.py index 090e81e..14bb07e 100644 --- a/manual_trading_hub/exchange_orders.py +++ b/manual_trading_hub/exchange_orders.py @@ -7,6 +7,8 @@ import os import time from typing import Any +from okx_orders_lib import fetch_okx_all_open_orders + def _coerce_float(*values) -> float | None: for v in values: @@ -98,23 +100,33 @@ def _normalize_raw_order(order: dict, *, channel: str) -> dict[str, Any] | None: 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("slTriggerPx"), - info.get("tpTriggerPx"), + info.get("triggerPx"), + sl_trig, + tp_trig, ) - price = _coerce_float(order.get("price"), info.get("price")) - amt = _coerce_float(order.get("amount"), order.get("remaining"), info.get("quantity"), info.get("origQty")) + 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": _order_label(typ, side, reduce_only), + "label": label, "type": typ, "side": side, "amount": amt, @@ -210,17 +222,19 @@ def _okx_list(ex: Any, symbol: str | None) -> list[dict]: pass if symbol and not symbols: symbols = [symbol] - seen = set() + seen: set[tuple[str, str]] = set() for sym in symbols: - if sym in seen: - continue - seen.add(sym) try: - for o in ex.fetch_open_orders(sym) or []: + 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 n: - out.append(n) + if not n: + continue + key = (n["id"], n.get("channel") or ch) + if key in seen: + continue + seen.add(key) + out.append(n) except Exception: pass return out diff --git a/okx_orders_lib.py b/okx_orders_lib.py new file mode 100644 index 0000000..ffe205b --- /dev/null +++ b/okx_orders_lib.py @@ -0,0 +1,53 @@ +""" +OKX 挂单聚合:普通委托 + 算法单(conditional / oco / trigger)。 +交易所 App「止盈止损」页多为 orders-algo-pending,仅 fetch_open_orders 默认拿不到。 +""" +from __future__ import annotations + +from typing import Any + + +def _order_dedupe_key(order: dict) -> str: + info = order.get("info") or {} + if not isinstance(info, dict): + info = {} + return str(order.get("id") or info.get("algoId") or info.get("ordId") or "") + + +def fetch_okx_all_open_orders(ex, exchange_symbol: str) -> list[dict]: + """合并 OKX 普通挂单与算法挂单(去重)。""" + if not exchange_symbol: + return [] + ex.load_markets() + sym = exchange_symbol + try: + sym = ex.market(exchange_symbol)["symbol"] + except Exception: + pass + seen: set[str] = set() + out: list[dict] = [] + + def add_batch(batch: list | None) -> None: + for o in batch or []: + if not isinstance(o, dict): + continue + k = _order_dedupe_key(o) + if not k or k in seen: + continue + seen.add(k) + out.append(o) + + try: + add_batch(ex.fetch_open_orders(sym)) + except Exception: + pass + for params in ( + {"ordType": "conditional"}, + {"ordType": "oco"}, + {"trigger": True}, + ): + try: + add_batch(ex.fetch_open_orders(sym, params=dict(params))) + except Exception: + pass + return out