修复okx止盈止损

This commit is contained in:
dekun
2026-05-25 11:14:20 +08:00
parent dccd490a46
commit 4e8498f736
3 changed files with 156 additions and 58 deletions
+77 -46
View File
@@ -45,6 +45,7 @@ from fib_key_monitor_lib import (
key_signal_type_for_trade_record, key_signal_type_for_trade_record,
stored_key_signal_type, stored_key_signal_type,
) )
from okx_orders_lib import fetch_okx_all_open_orders
from key_sl_tp_lib import ( from key_sl_tp_lib import (
breakeven_enabled_from_row, breakeven_enabled_from_row,
normalize_sl_tp_mode, normalize_sl_tp_mode,
@@ -2467,7 +2468,7 @@ def cancel_okx_swap_open_orders(exchange_symbol):
except Exception: except Exception:
pass pass
try: 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") oid = o.get("id")
if oid is None: if oid is None:
continue continue
@@ -2616,26 +2617,52 @@ def _resolve_tpsl_prices_for_manual(direction, live_price, sltp_mode, data):
return stop_loss, take_profit 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 {} info = order.get("info") or {}
if not isinstance(info, dict):
info = {}
oid = order.get("id") or info.get("algoId") or info.get("ordId") oid = order.get("id") or info.get("algoId") or info.get("ordId")
trig = _coerce_float( if oid is None:
info.get("slTriggerPx"), return None, None
info.get("tpTriggerPx"), ord_type = str(order.get("type") or info.get("ordType") or "")
sl_px = _coerce_float(
order.get("stopLossPrice"), order.get("stopLossPrice"),
info.get("slTriggerPx"),
info.get("slOrdPx"),
)
tp_px = _coerce_float(
order.get("takeProfitPrice"), 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: if trig is None:
return None return None, None
return { one = _okx_tpsl_slot_build(exchange_symbol, oid, trig, ord_type)
"order_id": str(oid) if oid is not None else None, return one, 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 ""),
}
def fetch_exchange_tpsl_slots(exchange_symbol, direction, plan_sl=None, plan_tp=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 return slots
try: try:
ensure_markets_loaded() ensure_markets_loaded()
ambiguous = [] plan_sl_f = plan_tp_f = None
for order in exchange.fetch_open_orders(exchange_symbol) or []: try:
slot = _okx_tpsl_slot_from_order(order, exchange_symbol) if plan_sl is not None:
if not slot or not slot.get("order_id"): plan_sl_f = float(plan_sl)
continue if plan_tp is not None:
trig = slot.get("trigger_price") plan_tp_f = float(plan_tp)
if plan_sl is not None and plan_tp is not None: except Exception:
try: plan_sl_f = plan_tp_f = None
role = "sl" if abs(trig - float(plan_sl)) <= abs(trig - float(plan_tp)) else "tp"
except Exception: def assign_role(trig, slot):
role = None if trig is None or slot is None:
elif plan_sl is not None: return
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
if plan_sl_f is not None and plan_tp_f is not None: 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" role = "sl" if abs(trig - plan_sl_f) <= abs(trig - plan_tp_f) else "tp"
elif plan_sl_f is not None: 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: elif plan_tp_f is not None:
role = "tp" role = "tp"
else: else:
continue return
if slots[role] is None: if slots[role] is None:
slots[role] = slot 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: except Exception:
pass pass
return slots return slots
@@ -3717,7 +3748,7 @@ def fib_limit_order_status(exchange_symbol, order_id):
except Exception: except Exception:
pass pass
try: 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: if str(o.get("id")) == oid:
return "open" return "open"
except Exception: except Exception:
+26 -12
View File
@@ -7,6 +7,8 @@ import os
import time import time
from typing import Any from typing import Any
from okx_orders_lib import fetch_okx_all_open_orders
def _coerce_float(*values) -> float | None: def _coerce_float(*values) -> float | None:
for v in values: 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 reduce_only = bool(reduce_only) if reduce_only is not None else None
except (TypeError, ValueError): except (TypeError, ValueError):
reduce_only = None 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( trig = _coerce_float(
order.get("stopPrice"), order.get("stopPrice"),
order.get("triggerPrice"), order.get("triggerPrice"),
info.get("triggerPrice"), info.get("triggerPrice"),
info.get("stopPrice"), info.get("stopPrice"),
info.get("slTriggerPx"), info.get("triggerPx"),
info.get("tpTriggerPx"), sl_trig,
tp_trig,
) )
price = _coerce_float(order.get("price"), info.get("price")) 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")) 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" 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 { return {
"id": str(oid), "id": str(oid),
"symbol": sym, "symbol": sym,
"channel": channel, "channel": channel,
"category": category, "category": category,
"label": _order_label(typ, side, reduce_only), "label": label,
"type": typ, "type": typ,
"side": side, "side": side,
"amount": amt, "amount": amt,
@@ -210,17 +222,19 @@ def _okx_list(ex: Any, symbol: str | None) -> list[dict]:
pass pass
if symbol and not symbols: if symbol and not symbols:
symbols = [symbol] symbols = [symbol]
seen = set() seen: set[tuple[str, str]] = set()
for sym in symbols: for sym in symbols:
if sym in seen:
continue
seen.add(sym)
try: 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" ch = "algo" if _is_conditional_type(_order_type_str(o)) else "regular"
n = _normalize_raw_order(dict(o), channel=ch) n = _normalize_raw_order(dict(o), channel=ch)
if n: if not n:
out.append(n) continue
key = (n["id"], n.get("channel") or ch)
if key in seen:
continue
seen.add(key)
out.append(n)
except Exception: except Exception:
pass pass
return out return out
+53
View File
@@ -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