"""中控条件开仓单:客户端本地监控/开仓/出场(中控只下发不盯盘).""" from __future__ import annotations import json import sqlite3 import time from datetime import datetime, timedelta, timezone from typing import Any # armed:待命穿越; triggering:已穿越等偏离区内开仓; open:持仓中; # done:终态(leave_up/leave_down/cancel/timeout/closed) ACTIVE_STATUSES = ("armed", "triggering", "open") 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 ensure_cond_order_table(conn: sqlite3.Connection) -> None: conn.execute( """ CREATE TABLE IF NOT EXISTS options_cond_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, hub_order_id TEXT NOT NULL UNIQUE, underlying TEXT NOT NULL, opt_type TEXT NOT NULL, trigger_side TEXT NOT NULL, trigger_px REAL NOT NULL, deviation_pct REAL NOT NULL, min_expiry_hours REAL NOT NULL, target_index REAL, profit_exit_enabled INTEGER DEFAULT 0, profit_exit_mult REAL DEFAULT 1.0, pending_target_index REAL, pending_profit_exit_enabled INTEGER, pending_profit_exit_mult REAL, exit_update_pending INTEGER DEFAULT 0, status TEXT NOT NULL DEFAULT 'armed', end_reason TEXT, last_index REAL, crossed_at TEXT, opened_inst_id TEXT, opened_trade_id INTEGER, opened_at TEXT, message TEXT, created_at TEXT, updated_at TEXT, ended_at TEXT ) """ ) try: conn.execute("ALTER TABLE options_cond_orders ADD COLUMN opened_at TEXT") except Exception: pass conn.execute( """ CREATE INDEX IF NOT EXISTS idx_options_cond_orders_status ON options_cond_orders(status) """ ) try: conn.execute( """ UPDATE options_cond_orders SET opened_at = updated_at WHERE opened_at IS NULL AND status = 'open' AND updated_at IS NOT NULL AND TRIM(updated_at) != '' """ ) except Exception: pass def _now() -> str: try: from lib.options.options_time_lib import options_app_now_str return options_app_now_str() except Exception: return time.strftime("%Y-%m-%d %H:%M:%S") def _notify_cond( send_wechat: Any, *, title: str, order: dict[str, Any], account_label: str = "", extra: list[str] | None = None, ) -> None: if not callable(send_wechat) or not order: return try: lines = [ title, f"账户:{account_label or 'OKX期权'}", f"单号:{order.get('hub_order_id') or '—'}", f"标的:{order.get('underlying') or '—'} {order.get('opt_type') or '—'}", f"触发价:{order.get('trigger_px') if order.get('trigger_px') is not None else '—'}", ] for line in extra or []: if line: lines.append(line) send_wechat("\n".join(lines)) except Exception: pass def next_thursday_2300(created_at: Any) -> datetime | None: """下发后第一个周四 23:00(业务时区).周四 23:00 之后下发的顺延到下周四.""" from lib.options.options_time_lib import options_app_tz, options_wall_clock_to_ms ms = options_wall_clock_to_ms(created_at) if ms is None: return None tz = options_app_tz() dt = datetime.fromtimestamp(ms / 1000.0, tz=timezone.utc).astimezone(tz) days = (3 - dt.weekday()) % 7 cand = dt.replace(hour=23, minute=0, second=0, microsecond=0) + timedelta(days=days) if cand <= dt: cand += timedelta(days=7) return cand def expire_untriggered_cond_orders( conn: sqlite3.Connection, *, send_wechat: Any = None, account_label: str = "", now: datetime | None = None, ) -> int: """仅取消仍待命(未触发)的条件单;持仓/触发中不动.""" from lib.options.options_time_lib import options_app_tz now_dt = now or datetime.now(options_app_tz()) n = 0 for order in list_cond_orders(conn, active_only=True): if str(order.get("status") or "") != "armed": continue deadline = next_thursday_2300(order.get("created_at")) if deadline is None or now_dt < deadline: continue _end_order( conn, order, end_reason="timeout", message="周四23:00未触发自动取消", send_wechat=send_wechat, account_label=account_label, ) n += 1 return n def _row_to_dict(row: sqlite3.Row | None) -> dict[str, Any] | None: if row is None: return None d = dict(row) d["profit_exit_enabled"] = bool(int(d.get("profit_exit_enabled") or 0)) d["exit_update_pending"] = bool(int(d.get("exit_update_pending") or 0)) if d.get("pending_profit_exit_enabled") is not None: d["pending_profit_exit_enabled"] = bool(int(d["pending_profit_exit_enabled"])) return d def list_cond_orders(conn: sqlite3.Connection, *, active_only: bool = False) -> list[dict[str, Any]]: ensure_cond_order_table(conn) if active_only: rows = conn.execute( f""" SELECT * FROM options_cond_orders WHERE status IN ({",".join("?" * len(ACTIVE_STATUSES))}) ORDER BY id DESC """, ACTIVE_STATUSES, ).fetchall() else: rows = conn.execute( "SELECT * FROM options_cond_orders ORDER BY id DESC LIMIT 100" ).fetchall() return [_row_to_dict(r) for r in rows if r] def get_cond_order_by_hub_id(conn: sqlite3.Connection, hub_order_id: str) -> dict[str, Any] | None: ensure_cond_order_table(conn) row = conn.execute( "SELECT * FROM options_cond_orders WHERE hub_order_id = ?", ((hub_order_id or "").strip(),), ).fetchone() return _row_to_dict(row) def has_active_cond_order(conn: sqlite3.Connection) -> bool: ensure_cond_order_table(conn) row = conn.execute( f""" SELECT 1 FROM options_cond_orders WHERE status IN ({",".join("?" * len(ACTIVE_STATUSES))}) LIMIT 1 """, ACTIVE_STATUSES, ).fetchone() return row is not None def cond_order_blocks_manual_open_msg(conn: sqlite3.Connection) -> str | None: if has_active_cond_order(conn): return "中控条件单占用中,禁止手动开仓" return None def machine_has_option_position(cfg: dict[str, Any], ex: Any) -> bool: try: raw = cfg["fetch_option_positions"](ex) except Exception: return False if not raw: return False from lib.options.options_position_limit_lib import count_live_option_positions return count_live_option_positions(raw) > 0 def create_cond_order( conn: sqlite3.Connection, payload: dict[str, Any], *, send_wechat: Any = None, account_label: str = "", ) -> dict[str, Any]: """接收中控下发;同机仅允许一张活跃条件单.""" ensure_cond_order_table(conn) hub_order_id = (payload.get("hub_order_id") or "").strip() if not hub_order_id: return {"ok": False, "msg": "缺少 hub_order_id"} existing = get_cond_order_by_hub_id(conn, hub_order_id) if existing: return {"ok": True, "msg": "条件单已存在", "order": existing, "deduped": True} if has_active_cond_order(conn): return {"ok": False, "msg": "本机已有活跃条件单"} underlying = (payload.get("underlying") or "ETH").strip().upper() or "ETH" opt_type = (payload.get("opt_type") or "P").strip().upper() if opt_type not in ("P", "C"): return {"ok": False, "msg": "opt_type 须为 P 或 C"} # 左侧单:看跌等上穿、看涨等下穿;忽略中控传入的 trigger_side trigger_side = "gte" if opt_type == "P" else "lte" trigger_px = _safe_float(payload.get("trigger_px")) if trigger_px is None or trigger_px <= 0: return {"ok": False, "msg": "trigger_px 无效"} deviation_pct = _safe_float(payload.get("deviation_pct")) if deviation_pct is None or deviation_pct <= 0: return {"ok": False, "msg": "deviation_pct 须 > 0"} min_expiry_hours = _safe_float(payload.get("min_expiry_hours")) if min_expiry_hours is None or min_expiry_hours <= 0: return {"ok": False, "msg": "min_expiry_hours 须 > 0"} exit_cfg = payload.get("exit") if isinstance(payload.get("exit"), dict) else {} target_index = _safe_float(exit_cfg.get("target_index") or payload.get("target_index")) pe_en = bool(exit_cfg.get("profit_exit_enabled") or payload.get("profit_exit_enabled")) from lib.options.options_profit_exit_lib import normalize_profit_exit_mult pe_mult = normalize_profit_exit_mult( exit_cfg.get("profit_exit_mult", payload.get("profit_exit_mult")), default=1.0 ) if target_index is not None and target_index <= 0: return {"ok": False, "msg": "target_index 无效"} if not pe_en and target_index is None: return {"ok": False, "msg": "请至少配置目标位或翻倍出场"} now = _now() conn.execute( """ INSERT INTO options_cond_orders ( hub_order_id, underlying, opt_type, trigger_side, trigger_px, deviation_pct, min_expiry_hours, target_index, profit_exit_enabled, profit_exit_mult, status, message, created_at, updated_at ) VALUES (?,?,?,?,?,?,?,?,?,?, 'armed', ?, ?, ?) """, ( hub_order_id, underlying, opt_type, trigger_side, float(trigger_px), float(deviation_pct), float(min_expiry_hours), float(target_index) if target_index is not None else None, 1 if pe_en else 0, float(pe_mult), "已接收,待命穿越", now, now, ), ) conn.commit() order = get_cond_order_by_hub_id(conn, hub_order_id) _notify_cond( send_wechat, title="【OKX期权·条件单下发】", order=order or {}, account_label=account_label, extra=[ f"方向:{'Put 上穿' if opt_type == 'P' else 'Call 下穿'}", f"偏离:{deviation_pct:g}%", f"最短到期:{float(min_expiry_hours):g}h", "状态:待命", ], ) return {"ok": True, "msg": "条件单已 armed", "order": order} def cancel_cond_order( conn: sqlite3.Connection, hub_order_id: str, *, send_wechat: Any = None, account_label: str = "", ) -> dict[str, Any]: order = get_cond_order_by_hub_id(conn, hub_order_id) if not order: return {"ok": False, "msg": "条件单不存在"} if order.get("status") not in ACTIVE_STATUSES: return {"ok": True, "msg": "条件单已结束", "order": order} if order.get("status") == "open": return {"ok": False, "msg": "已开仓,请用平仓/出场结束;取消仅适用于未成交条件单"} _end_order( conn, order, end_reason="cancel", message="中控取消", send_wechat=send_wechat, account_label=account_label, ) return {"ok": True, "msg": "已取消", "order": get_cond_order_by_hub_id(conn, hub_order_id)} def update_cond_order_exit( conn: sqlite3.Connection, hub_order_id: str, *, target_index: Any = None, profit_exit_enabled: Any = None, profit_exit_mult: Any = None, ) -> dict[str, Any]: """成交后改出场;若正在平仓挂单则先记 pending,本轮结束后再套用.""" order = get_cond_order_by_hub_id(conn, hub_order_id) if not order: return {"ok": False, "msg": "条件单不存在"} if order.get("status") != "open": return {"ok": False, "msg": "仅持仓中的条件单可改出场"} from lib.options.options_profit_exit_lib import normalize_profit_exit_mult new_target = _safe_float(target_index) if target_index is not None else order.get("target_index") if target_index is not None and str(target_index).strip() == "": new_target = None if new_target is not None and new_target <= 0: return {"ok": False, "msg": "target_index 无效"} if profit_exit_enabled is None: pe_en = bool(order.get("profit_exit_enabled")) else: pe_en = bool(profit_exit_enabled) pe_mult = normalize_profit_exit_mult( profit_exit_mult if profit_exit_mult is not None else order.get("profit_exit_mult"), default=1.0, ) if not pe_en and new_target is None: return {"ok": False, "msg": "请至少配置目标位或翻倍出场"} closing = _exit_close_in_progress(conn, str(order.get("opened_inst_id") or "")) now = _now() if closing: conn.execute( """ UPDATE options_cond_orders SET pending_target_index = ?, pending_profit_exit_enabled = ?, pending_profit_exit_mult = ?, exit_update_pending = 1, message = ?, updated_at = ? WHERE hub_order_id = ? """, ( float(new_target) if new_target is not None else None, 1 if pe_en else 0, float(pe_mult), "出场已更新,等待本轮平仓挂单结束后生效", now, hub_order_id, ), ) conn.commit() return { "ok": True, "msg": "已记录,本轮平仓结束后生效", "deferred": True, "order": get_cond_order_by_hub_id(conn, hub_order_id), } _apply_exit_to_position( conn, order, target_index=new_target, profit_exit_enabled=pe_en, profit_exit_mult=pe_mult, ) conn.execute( """ UPDATE options_cond_orders SET target_index = ?, profit_exit_enabled = ?, profit_exit_mult = ?, exit_update_pending = 0, pending_target_index = NULL, pending_profit_exit_enabled = NULL, pending_profit_exit_mult = NULL, message = ?, updated_at = ? WHERE hub_order_id = ? """, ( float(new_target) if new_target is not None else None, 1 if pe_en else 0, float(pe_mult), "出场已更新并生效", now, hub_order_id, ), ) conn.commit() return {"ok": True, "msg": "出场已更新", "order": get_cond_order_by_hub_id(conn, hub_order_id)} def _exit_close_in_progress(conn: sqlite3.Connection, inst_id: str) -> bool: if not inst_id: return False try: from lib.options.options_target_lib import list_closing_targets for t in list_closing_targets(conn): if str(t.get("inst_id") or "") == inst_id: return True except Exception: pass try: row = conn.execute( """ SELECT profit_exit_state FROM options_trades WHERE inst_id = ? AND status = 'open' ORDER BY id ASC LIMIT 1 """, (inst_id,), ).fetchone() if row and str(row["profit_exit_state"] or "") == "closing": return True except Exception: pass return False def _apply_exit_to_position( conn: sqlite3.Connection, order: dict[str, Any], *, target_index: float | None, profit_exit_enabled: bool, profit_exit_mult: float, ) -> None: inst_id = str(order.get("opened_inst_id") or "").strip() if not inst_id: return from lib.options.options_profit_exit_lib import set_profit_exit from lib.options.options_target_lib import cancel_target_monitor, upsert_target_monitor set_profit_exit(conn, inst_id=inst_id, enabled=profit_exit_enabled, mult=profit_exit_mult) if target_index is not None: upsert_target_monitor( conn, inst_id=inst_id, target_index=float(target_index), underlying=str(order.get("underlying") or ""), opt_type=str(order.get("opt_type") or ""), ) else: cancel_target_monitor(conn, inst_id=inst_id) conn.commit() def _end_order( conn: sqlite3.Connection, order: dict[str, Any], *, end_reason: str, message: str, send_wechat: Any = None, account_label: str = "", index_px: float | None = None, ) -> None: now = _now() conn.execute( """ UPDATE options_cond_orders SET status = 'done', end_reason = ?, message = ?, updated_at = ?, ended_at = ? WHERE hub_order_id = ? """, (end_reason, message, now, now, order["hub_order_id"]), ) conn.commit() if callable(send_wechat): try: reason_map = { "leave_up": "上破偏离区结束", "leave_down": "下破偏离区结束", "cancel": "中控取消", "timeout": "周四23:00未触发自动取消", "closed": "出场完成", } _notify_cond( send_wechat, title="【OKX期权·条件单结束】", order=order, account_label=account_label, extra=[ f"原因:{reason_map.get(end_reason, end_reason)}", f"说明:{message}", ] + ([f"当前指数:{index_px:g}"] if index_px is not None else []), ) except Exception: pass def band_bounds(trigger_px: float, deviation_pct: float) -> tuple[float, float]: d = max(0.0, float(deviation_pct)) / 100.0 lo = float(trigger_px) * (1.0 - d) hi = float(trigger_px) * (1.0 + d) return lo, hi def pick_nearest_itm_contract( cfg: dict[str, Any], ex: Any, *, underlying: str, opt_type: str, index_px: float, min_expiry_hours: float, ) -> dict[str, Any] | None: """按当前指数选最近一档实值;到期 ≥ min_expiry_hours.""" from lib.exchange.okx_options_lib import build_option_chain, option_buy_liquidity_ok from lib.options.options_pricing_lib import option_moneyness max_dte_days = max(float(min_expiry_hours) / 24.0 + 14.0, 14.0) # 放宽实值距离:取全部 ITM,再按距离排序 chain = build_option_chain( ex, underlying, max_dte_days=max_dte_days, itm_only=False, itm_max_dist_usd=1e12, index_px=index_px, ) min_ms = time.time() * 1000 + float(min_expiry_hours) * 3600 * 1000 want = (opt_type or "P").upper() candidates: list[tuple[float, dict[str, Any]]] = [] expiries = chain.get("expiries") or [] # build_option_chain 返回 list[{exp_time, contracts}] if isinstance(expiries, dict): exp_iter = [{"exp_time": k, "contracts": v} for k, v in expiries.items()] else: exp_iter = list(expiries) for bucket in exp_iter: try: exp_ms = int(bucket.get("exp_time") or 0) except (TypeError, ValueError): exp_ms = 0 if exp_ms and exp_ms < min_ms: continue for c in bucket.get("contracts") or []: if str(c.get("opt_type") or "").upper() != want: continue strike = _safe_float(c.get("strike")) if strike is None: continue mny = option_moneyness(opt_type=want, strike=strike, index_px=index_px) if mny != "itm": continue dist = abs(float(strike) - float(index_px)) candidates.append((dist, c)) if not candidates: return None candidates.sort(key=lambda x: (x[0], str(x[1].get("inst_id") or ""))) # 优先有卖一深度的最近档 for _dist, c in candidates[:12]: inst = str(c.get("inst_id") or "") if not inst: continue try: q = cfg["quote_option_contract"](ex, inst) except Exception: continue ask = q.get("ask") ask_sz = q.get("ask_sz") ok, _ = option_buy_liquidity_ok(ask, ask_sz) if ok: out = dict(c) out["quote"] = q return out # 深度都不够时仍返回最近档,由上层继续等 best = dict(candidates[0][1]) return best def _fetch_index(cfg: dict[str, Any], ex: Any, underlying: str) -> float | None: from lib.exchange.okx_options_lib import fetch_index_price uly = f"{(underlying or 'ETH').upper()}-USD" try: return fetch_index_price(ex, uly) except Exception: return None def _try_open_full( cfg: dict[str, Any], ex: Any, *, inst_id: str, order: dict[str, Any], ) -> dict[str, Any]: from lib.options.options_margin_mode_lib import is_coin_margin_mode from lib.options.options_coin_open_lib import open_coin_option_buy_full note = f"中控条件单:{order.get('hub_order_id')}" target_index = _safe_float(order.get("target_index")) pe_en = bool(order.get("profit_exit_enabled")) pe_mult = float(order.get("profit_exit_mult") or 1.0) if is_coin_margin_mode(): return open_coin_option_buy_full( cfg, ex, inst_id=inst_id, signal_note=note, target_index=target_index, profit_exit_enabled=pe_en, profit_exit_mult=pe_mult, target_sheets=None, ) return { "ok": False, "msg": "条件单开仓当前仅支持币本位客户端规则(买币→开仓)", "need_depth": False, } def run_cond_orders_tick( conn_or_get_db: Any, cfg: dict[str, Any], *, send_wechat: Any = None, account_label: str = "", ) -> None: """监控循环单步:周四未触发取消 / 穿越 / 偏离区 / 开仓 / 出场收尾. 推荐传 get_db:开仓前会关闭连接,避免与买币/等待成交嵌套锁库。 """ get_db = conn_or_get_db if callable(conn_or_get_db) else None if get_db is None: conn = conn_or_get_db ensure_cond_order_table(conn) expire_untriggered_cond_orders( conn, send_wechat=send_wechat, account_label=account_label ) ex = cfg.get("exchange_options") if ex is None: return for order in list_cond_orders(conn, active_only=True): try: _tick_one( conn, cfg, ex, order, send_wechat=send_wechat, account_label=account_label, get_db=None, ) except Exception as e: try: conn.execute( "UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?", (f"监控异常: {e}"[:200], _now(), order["hub_order_id"]), ) conn.commit() except Exception: pass return ex: Any = None orders: list[dict[str, Any]] = [] conn = get_db() try: ensure_cond_order_table(conn) expire_untriggered_cond_orders( conn, send_wechat=send_wechat, account_label=account_label ) ex = cfg.get("exchange_options") if ex is not None: orders = list_cond_orders(conn, active_only=True) try: conn.commit() except Exception: pass finally: try: conn.close() except Exception: pass if ex is None or not orders: return for order in orders: conn = get_db() try: _tick_one( conn, cfg, ex, order, send_wechat=send_wechat, account_label=account_label, get_db=get_db, ) try: conn.commit() except Exception: pass except Exception as e: try: conn.execute( "UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?", (f"监控异常: {e}"[:200], _now(), order["hub_order_id"]), ) conn.commit() except Exception: pass finally: try: conn.close() except Exception: pass def _tick_one( conn: sqlite3.Connection, cfg: dict[str, Any], ex: Any, order: dict[str, Any], *, send_wechat: Any, account_label: str, get_db: Any = None, ) -> None: status = str(order.get("status") or "") underlying = str(order.get("underlying") or "ETH") idx = _fetch_index(cfg, ex, underlying) if idx is None or idx <= 0: return last = _safe_float(order.get("last_index")) conn.execute( "UPDATE options_cond_orders SET last_index=?, updated_at=? WHERE hub_order_id=?", (float(idx), _now(), order["hub_order_id"]), ) conn.commit() trigger_px = float(order["trigger_px"]) lo, hi = band_bounds(trigger_px, float(order["deviation_pct"])) side = str(order.get("trigger_side") or "gte") if status == "armed": crossed = False if side == "gte" and last is not None and last < trigger_px and idx >= trigger_px: crossed = True elif side == "lte" and last is not None and last > trigger_px and idx <= trigger_px: crossed = True # 首次无 last:只记录,等下一跳再判穿越(避免挂单时已在上方立刻开) if last is None: return if crossed: conn.execute( """ UPDATE options_cond_orders SET status='triggering', crossed_at=?, message=?, updated_at=? WHERE hub_order_id=? """, (_now(), f"已穿越 {trigger_px:g},等待偏离区内开仓", _now(), order["hub_order_id"]), ) conn.commit() order["status"] = "triggering" status = "triggering" else: return if status == "triggering": if idx > hi: _end_order( conn, order, end_reason="leave_up", message=f"指数 {idx:g} 上破偏离上沿 {hi:g}", send_wechat=send_wechat, account_label=account_label, index_px=idx, ) return if idx < lo: _end_order( conn, order, end_reason="leave_down", message=f"指数 {idx:g} 下破偏离下沿 {lo:g}", send_wechat=send_wechat, account_label=account_label, index_px=idx, ) return # 区内:有仓则不再开 if machine_has_option_position(cfg, ex): conn.execute( "UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?", ("偏离区内但已有持仓,等待", _now(), order["hub_order_id"]), ) conn.commit() return picked = pick_nearest_itm_contract( cfg, ex, underlying=underlying, opt_type=str(order.get("opt_type") or "P"), index_px=idx, min_expiry_hours=float(order.get("min_expiry_hours") or 24), ) if not picked or not picked.get("inst_id"): conn.execute( "UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?", ("偏离区内暂无合适实值合约", _now(), order["hub_order_id"]), ) conn.commit() return inst_id = str(picked["inst_id"]) # 深度检查 from lib.exchange.okx_options_lib import option_buy_liquidity_ok q = picked.get("quote") or cfg["quote_option_contract"](ex, inst_id) ok_liq, msg_liq = option_buy_liquidity_ok(q.get("ask"), q.get("ask_sz")) if not ok_liq: conn.execute( "UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?", (f"深度不足继续等待: {msg_liq or '无卖一'}", _now(), order["hub_order_id"]), ) conn.commit() return # 开仓(买币/等成交)前释放本连接,避免嵌套锁库 try: conn.commit() except Exception: pass hub_id = order["hub_order_id"] if get_db is not None: try: conn.close() except Exception: pass conn = None result = _try_open_full(cfg, ex, inst_id=inst_id, order=order) if get_db is not None: conn = get_db() ensure_cond_order_table(conn) assert conn is not None if not result.get("ok"): conn.execute( "UPDATE options_cond_orders SET message=?, updated_at=? WHERE hub_order_id=?", (f"开仓未成继续等: {result.get('msg') or '失败'}"[:180], _now(), hub_id), ) conn.commit() return trade_id = result.get("trade_id") conn.execute( """ UPDATE options_cond_orders SET status='open', opened_inst_id=?, opened_trade_id=?, opened_at=COALESCE(opened_at, ?), message=?, updated_at=? WHERE hub_order_id=? """, ( inst_id, int(trade_id) if trade_id is not None else None, _now(), f"已开仓 {inst_id}", _now(), hub_id, ), ) conn.commit() opened = get_cond_order_by_hub_id(conn, order["hub_order_id"]) or order _notify_cond( send_wechat, title="【OKX期权·条件单开仓】", order=opened, account_label=account_label, extra=[ f"合约:{inst_id}", f"说明:已开仓 {inst_id}", ], ) order2 = opened order2["opened_inst_id"] = inst_id _apply_exit_to_position( conn, order2, target_index=_safe_float(order2.get("target_index")), profit_exit_enabled=bool(order2.get("profit_exit_enabled")), profit_exit_mult=float(order2.get("profit_exit_mult") or 1.0), ) return if status == "open": # 延迟出场更新 if order.get("exit_update_pending") and not _exit_close_in_progress( conn, str(order.get("opened_inst_id") or "") ): pe_en = order.get("pending_profit_exit_enabled") if pe_en is None: pe_en = order.get("profit_exit_enabled") _apply_exit_to_position( conn, order, target_index=_safe_float(order.get("pending_target_index")), profit_exit_enabled=bool(pe_en), profit_exit_mult=float( order.get("pending_profit_exit_mult") if order.get("pending_profit_exit_mult") is not None else order.get("profit_exit_mult") or 1.0 ), ) conn.execute( """ UPDATE options_cond_orders SET target_index = pending_target_index, profit_exit_enabled = COALESCE(pending_profit_exit_enabled, profit_exit_enabled), profit_exit_mult = COALESCE(pending_profit_exit_mult, profit_exit_mult), exit_update_pending = 0, pending_target_index = NULL, pending_profit_exit_enabled = NULL, pending_profit_exit_mult = NULL, message = ?, updated_at = ? WHERE hub_order_id = ? """, ("延迟出场已生效", _now(), order["hub_order_id"]), ) conn.commit() inst_id = str(order.get("opened_inst_id") or "") if not inst_id: return if not machine_has_option_position(cfg, ex): # 确认该合约已平 raw = cfg["fetch_option_positions"](ex) or [] still = any(str(p.get("instId") or "") == inst_id for p in raw) if still: return _end_order( conn, order, end_reason="closed", message="持仓已平,条件单完成", send_wechat=send_wechat, account_label=account_label, index_px=idx, )