修复okx止盈止损
This commit is contained in:
+74
-43
@@ -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 []:
|
|
||||||
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:
|
try:
|
||||||
role = "sl" if abs(trig - float(plan_sl)) <= abs(trig - float(plan_tp)) else "tp"
|
if plan_sl is not None:
|
||||||
except Exception:
|
plan_sl_f = float(plan_sl)
|
||||||
role = None
|
if plan_tp is not None:
|
||||||
elif plan_sl is not None:
|
plan_tp_f = float(plan_tp)
|
||||||
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:
|
except Exception:
|
||||||
plan_sl_f = plan_tp_f = None
|
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:
|
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:
|
||||||
|
|||||||
@@ -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,16 +222,18 @@ 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:
|
||||||
|
continue
|
||||||
|
key = (n["id"], n.get("channel") or ch)
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
out.append(n)
|
out.append(n)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user