a1abe159fa
Add deploy/manage.sh bootstrap for git.bz121.com/dekun/crypto_okx and point docs at this repo. Co-authored-by: Cursor <cursoragent@cursor.com>
125 lines
4.0 KiB
Python
125 lines
4.0 KiB
Python
"""期权限价挂单:展示 enrichment + 超时自动撤单."""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import Any
|
|
|
|
|
|
def _safe_float(v: Any) -> float | None:
|
|
if v is None or v == "":
|
|
return None
|
|
try:
|
|
return float(v)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def order_age_seconds(order: dict[str, Any], *, now_ms: float | None = None) -> float | None:
|
|
"""根据交易所 cTime(ms) 估算挂单时长(秒)."""
|
|
ct = _safe_float(order.get("c_time") or order.get("cTime"))
|
|
if ct is None or ct <= 0:
|
|
return None
|
|
# OKX 一般为毫秒时间戳
|
|
if ct < 1e12:
|
|
ct *= 1000.0
|
|
now = float(now_ms if now_ms is not None else time.time() * 1000.0)
|
|
age = (now - ct) / 1000.0
|
|
return age if age >= 0 else 0.0
|
|
|
|
|
|
def is_close_pending_order(order: dict[str, Any]) -> bool:
|
|
"""平仓向限价挂单:卖出 / reduceOnly."""
|
|
side = str(order.get("side") or "").lower()
|
|
if side == "sell":
|
|
return True
|
|
return bool(order.get("reduce_only"))
|
|
|
|
|
|
def enrich_pending_orders(
|
|
orders: list[dict[str, Any]] | None,
|
|
*,
|
|
ttl_seconds: float = 600.0,
|
|
now_ms: float | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""为 UI 附加挂单时长与自动撤倒计时."""
|
|
ttl = max(0.0, float(ttl_seconds or 0))
|
|
now = float(now_ms if now_ms is not None else time.time() * 1000.0)
|
|
out: list[dict[str, Any]] = []
|
|
for raw in orders or []:
|
|
o = dict(raw)
|
|
age = order_age_seconds(o, now_ms=now)
|
|
is_close = is_close_pending_order(o)
|
|
o["age_sec"] = round(age, 1) if age is not None else None
|
|
o["is_close_order"] = is_close
|
|
o["auto_cancel_enabled"] = bool(is_close and ttl > 0)
|
|
if age is not None and is_close and ttl > 0:
|
|
remain = max(0.0, ttl - age)
|
|
o["ttl_seconds"] = ttl
|
|
o["expire_in_sec"] = round(remain, 1)
|
|
o["stale"] = remain <= 0
|
|
else:
|
|
o["ttl_seconds"] = ttl if is_close else None
|
|
o["expire_in_sec"] = None
|
|
o["stale"] = False
|
|
out.append(o)
|
|
return out
|
|
|
|
|
|
def cancel_stale_close_pending_orders(
|
|
*,
|
|
fetch_pending: Any,
|
|
cancel_order: Any,
|
|
ttl_seconds: float = 600.0,
|
|
now_ms: float | None = None,
|
|
ex: Any = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
平仓限价挂单超过 ttl 自动撤销.
|
|
fetch_pending(ex) -> list; cancel_order(ex, inst_id=..., ord_id=...).
|
|
"""
|
|
ttl = float(ttl_seconds or 0)
|
|
if ttl <= 0:
|
|
return {"ok": True, "cancelled": 0, "checked": 0, "skipped": "ttl_disabled"}
|
|
try:
|
|
orders = fetch_pending(ex) if ex is not None else fetch_pending()
|
|
except TypeError:
|
|
orders = fetch_pending(ex)
|
|
except Exception as e:
|
|
return {"ok": False, "msg": str(e), "cancelled": 0, "checked": 0}
|
|
enriched = enrich_pending_orders(orders or [], ttl_seconds=ttl, now_ms=now_ms)
|
|
cancelled: list[dict[str, Any]] = []
|
|
errors: list[str] = []
|
|
checked = 0
|
|
for o in enriched:
|
|
if not o.get("is_close_order"):
|
|
continue
|
|
checked += 1
|
|
if not o.get("stale"):
|
|
continue
|
|
inst = str(o.get("inst_id") or "").strip()
|
|
oid = str(o.get("ord_id") or "").strip()
|
|
if not inst or not oid:
|
|
continue
|
|
try:
|
|
if ex is not None:
|
|
res = cancel_order(ex, inst_id=inst, ord_id=oid)
|
|
else:
|
|
res = cancel_order(inst_id=inst, ord_id=oid)
|
|
except TypeError:
|
|
res = cancel_order(ex, inst_id=inst, ord_id=oid)
|
|
except Exception as e:
|
|
errors.append(f"{oid}:{e}")
|
|
continue
|
|
if res.get("ok"):
|
|
cancelled.append({"inst_id": inst, "ord_id": oid, "age_sec": o.get("age_sec")})
|
|
else:
|
|
errors.append(f"{oid}:{res.get('msg') or 'cancel_failed'}")
|
|
return {
|
|
"ok": True,
|
|
"cancelled": len(cancelled),
|
|
"checked": checked,
|
|
"orders": cancelled,
|
|
"errors": errors,
|
|
"ttl_seconds": ttl,
|
|
}
|