"""期权限价挂单:展示 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, }