356 lines
11 KiB
Python
356 lines
11 KiB
Python
"""
|
|
中控子代理:拉取交易所挂单/条件单并规范化展示;撤销单笔或按合约批量撤销。
|
|
"""
|
|
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
|