"""期权目标位委托:指数目标价仅用于监控触发;触发后按买一限价平仓(无止损,到期结算).""" from __future__ import annotations import sqlite3 import time from typing import Any, Callable from lib.options.options_db import init_options_tables from lib.options.options_pricing_lib import close_ref_prices, fetch_option_mark_px 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 _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None = None) -> tuple[float | None, float | None]: from lib.exchange.okx_options_lib import option_fields_from_inst_id inst_id = str(pos.get("instId") or pos.get("inst_id") or "") mark = _safe_float(pos.get("markPx")) or _safe_float((quote or {}).get("mark_px") or (quote or {}).get("mark")) if mark is None: mark = fetch_option_mark_px(ex, inst_id) opt_type = pos.get("optType") or (quote or {}).get("opt_type") strike = _safe_float(pos.get("stk")) or _safe_float((quote or {}).get("strike")) if not opt_type or strike is None: pt, ps = option_fields_from_inst_id(inst_id) opt_type = opt_type or pt if strike is None: strike = ps idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px")) return close_ref_prices(mark_px=mark, opt_type=str(opt_type or ""), strike=strike, index_px=idx) def ensure_target_tables(conn: sqlite3.Connection) -> None: init_options_tables(conn) conn.execute( """ CREATE TABLE IF NOT EXISTS options_target_monitors ( id INTEGER PRIMARY KEY AUTOINCREMENT, inst_id TEXT NOT NULL, underlying TEXT, opt_type TEXT, target_index REAL NOT NULL, trade_id INTEGER, sheets INTEGER, status TEXT DEFAULT 'active', trigger_idx REAL, close_ord_id TEXT, message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, triggered_at TIMESTAMP ) """ ) conn.execute( """ CREATE INDEX IF NOT EXISTS idx_options_target_monitors_status ON options_target_monitors(status) """ ) def target_hit(*, opt_type: str | None, index_px: float, target_index: float) -> bool: """Call:指数涨到/超过目标平仓;Put:指数跌到/低于目标平仓.""" ot = (opt_type or "").strip().upper() if ot == "P": return index_px <= target_index return index_px >= target_index def upsert_target_monitor( conn: sqlite3.Connection, *, inst_id: str, target_index: float, underlying: str | None = None, opt_type: str | None = None, trade_id: int | None = None, sheets: int | None = None, ) -> dict[str, Any]: ensure_target_tables(conn) inst_id = (inst_id or "").strip() if not inst_id: return {"ok": False, "msg": "缺少 inst_id"} if target_index is None or float(target_index) <= 0: return {"ok": False, "msg": "目标位无效"} target_index = float(target_index) row = conn.execute( """ SELECT id FROM options_target_monitors WHERE inst_id = ? AND status IN ('active', 'closing') ORDER BY CASE status WHEN 'active' THEN 0 WHEN 'closing' THEN 1 ELSE 2 END, id DESC LIMIT 1 """, (inst_id,), ).fetchone() if row: conn.execute( """ UPDATE options_target_monitors SET target_index = ?, underlying = COALESCE(?, underlying), opt_type = COALESCE(?, opt_type), trade_id = COALESCE(?, trade_id), sheets = COALESCE(?, sheets), status = 'active', trigger_idx = NULL, close_ord_id = NULL, message = NULL, triggered_at = NULL WHERE id = ? """, (target_index, underlying, opt_type, trade_id, sheets, int(row["id"])), ) mon_id = int(row["id"]) # 同一合约其他进行中的委托取消,避免双轨触发重复推送 conn.execute( """ UPDATE options_target_monitors SET status = 'cancelled', message = '被新目标位覆盖' WHERE inst_id = ? AND id != ? AND status IN ('active', 'closing') """, (inst_id, mon_id), ) else: cur = conn.execute( """ INSERT INTO options_target_monitors (inst_id, underlying, opt_type, target_index, trade_id, sheets, status) VALUES (?, ?, ?, ?, ?, ?, 'active') """, (inst_id, underlying, opt_type, target_index, trade_id, sheets), ) mon_id = int(cur.lastrowid) return {"ok": True, "id": mon_id, "inst_id": inst_id, "target_index": target_index} def cancel_target_monitor(conn: sqlite3.Connection, *, inst_id: str | None = None, monitor_id: int | None = None) -> int: ensure_target_tables(conn) if monitor_id is not None: cur = conn.execute( """ UPDATE options_target_monitors SET status = 'cancelled', message = '手动取消' WHERE id = ? AND status IN ('active', 'closing') """, (int(monitor_id),), ) return int(cur.rowcount or 0) if inst_id: cur = conn.execute( """ UPDATE options_target_monitors SET status = 'cancelled', message = '手动取消' WHERE inst_id = ? AND status IN ('active', 'closing') """, (inst_id.strip(),), ) return int(cur.rowcount or 0) return 0 def _row_to_target(r: sqlite3.Row) -> dict[str, Any]: return { "id": int(r["id"]), "inst_id": r["inst_id"], "underlying": r["underlying"], "opt_type": r["opt_type"], "target_index": _safe_float(r["target_index"]), "trade_id": r["trade_id"], "sheets": r["sheets"], "status": r["status"], "message": r["message"], "created_at": r["created_at"], } def list_active_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]: ensure_target_tables(conn) rows = conn.execute( """ SELECT id, inst_id, underlying, opt_type, target_index, trade_id, sheets, status, message, created_at FROM options_target_monitors WHERE status = 'active' ORDER BY id DESC """ ).fetchall() return [_row_to_target(r) for r in rows] def list_closing_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]: """已挂出平仓单、等待成交的目标(不再重复推送微信).""" ensure_target_tables(conn) rows = conn.execute( """ SELECT id, inst_id, underlying, opt_type, target_index, trade_id, sheets, status, message, created_at FROM options_target_monitors WHERE status = 'closing' ORDER BY id DESC """ ).fetchall() return [_row_to_target(r) for r in rows] def targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[str, Any]]: """UI/持仓挂载:active 与 closing 都算进行中.""" out: dict[str, dict[str, Any]] = {} for t in list_closing_targets(conn) + list_active_targets(conn): inst = str(t.get("inst_id") or "") if inst and inst not in out: out[inst] = t return out def mark_monitor( conn: sqlite3.Connection, monitor_id: int, *, status: str, trigger_idx: float | None = None, close_ord_id: str | None = None, message: str | None = None, ) -> None: conn.execute( """ UPDATE options_target_monitors SET status = ?, trigger_idx = COALESCE(?, trigger_idx), close_ord_id = COALESCE(?, close_ord_id), message = COALESCE(?, message), triggered_at = CASE WHEN ? IN ('triggered', 'expired', 'closing') THEN COALESCE(triggered_at, CURRENT_TIMESTAMP) ELSE triggered_at END WHERE id = ? """, (status, trigger_idx, close_ord_id, message, status, int(monitor_id)), ) def cancel_orphans_without_position( conn: sqlite3.Connection, *, live_inst_ids: set[str], ) -> int: """持仓已消失的目标委托标记为 expired(到期/已平),不挂止损.""" ensure_target_tables(conn) rows = list_active_targets(conn) + list_closing_targets(conn) n = 0 for t in rows: inst = str(t.get("inst_id") or "") if inst and inst not in live_inst_ids: mark_monitor(conn, int(t["id"]), status="expired", message="持仓已平/到期,委托结束") n += 1 return n def _commit_monitor(conn: sqlite3.Connection) -> None: """状态变更立刻落库,避免后续 sync 异常回滚后重复触发/推送.""" try: conn.commit() except Exception: pass def close_option_by_bid_depth( cfg: dict[str, Any], ex: Any, inst_id: str, *, sheets: int | None = None, ) -> dict[str, Any]: """目标触发后只锁买一限价卖出;需过 2×门控(通过后同仓续批只验流动性).""" from lib.options.options_close_exec_lib import close_option_by_bid1 return close_option_by_bid1( cfg, ex, inst_id, sheets=sheets, require_recycle_gate=True, signal_note="目标位平仓", ) def _notify_target_close( send_wechat: Callable[[str], None] | None, *, account_label: str, inst_id: str, target: float, idx: float, result: dict[str, Any], ) -> None: if not send_wechat: return try: send_wechat( "\n".join( [ "【OKX期权·目标位平仓】", f"账户:{account_label}", f"合约:{inst_id}", f"目标指数:{target:g}", f"触发指数:{idx:g}", f"提交张数:{result.get('submitted_sheets') or '—'}", f"预估收回:{result.get('premium_received') if result.get('premium_received') is not None else '—'} USDC", ] ) ) except Exception: pass def _result_fully_done(result: dict[str, Any]) -> bool: if result.get("already_flat"): return True if result.get("fully_closed"): return True remaining = result.get("remaining_sheets") if remaining is not None and int(remaining) <= 0 and result.get("ok"): return True return False def run_options_target_closes( conn: sqlite3.Connection, positions: list[dict[str, Any]], *, close_fn: Callable[[str], dict[str, Any]], index_fn: Callable[[dict[str, Any]], float | None] | None = None, send_wechat: Callable[[str], None] | None = None, account_label: str = "OKX期权", ) -> int: """ 扫描 active 目标委托;指数到位后限价平仓. 状态先 commit 再推微信,避免 sync 失败回滚导致同一笔反复推送. 未完全成交进入 closing,仅重试平仓不再推送. 返回本次新触发(并推送)的条数. """ ensure_target_tables(conn) pos_by_inst = {str(p.get("inst_id") or p.get("instId") or ""): p for p in positions} live_ids = {k for k in pos_by_inst if k} cancel_orphans_without_position(conn, live_inst_ids=live_ids) _commit_monitor(conn) # 先处理已挂单等待成交的,绝不再发微信 for mon in list_closing_targets(conn): inst_id = str(mon.get("inst_id") or "") if not inst_id: continue if inst_id not in pos_by_inst: mark_monitor(conn, int(mon["id"]), status="expired", message="持仓已平") _commit_monitor(conn) continue result = close_fn(inst_id) idx = _safe_float(pos_by_inst[inst_id].get("idx_px") or pos_by_inst[inst_id].get("idxPx")) if result.get("already_flat") or _result_fully_done(result): mark_monitor( conn, int(mon["id"]), status="triggered", trigger_idx=idx, close_ord_id=result.get("close_ord_id"), message="目标位限价平仓完成", ) _commit_monitor(conn) continue mark_monitor( conn, int(mon["id"]), status="closing", trigger_idx=idx, close_ord_id=result.get("close_ord_id"), message=str(result.get("msg") or result.get("stopped_reason") or "等待买一成交"), ) _commit_monitor(conn) triggered = 0 for mon in list_active_targets(conn): inst_id = str(mon.get("inst_id") or "") target = _safe_float(mon.get("target_index")) if not inst_id or target is None: continue pos = pos_by_inst.get(inst_id) if not pos: continue if index_fn is not None: idx = index_fn(pos) else: idx = _safe_float(pos.get("idx_px") or pos.get("idxPx")) if idx is None: continue opt_type = mon.get("opt_type") or pos.get("opt_type") or pos.get("optType") if not target_hit(opt_type=str(opt_type) if opt_type else None, index_px=idx, target_index=target): continue result = close_fn(inst_id) if result.get("already_flat"): mark_monitor(conn, int(mon["id"]), status="expired", trigger_idx=idx, message="持仓已平") _commit_monitor(conn) continue if not result.get("ok"): mark_monitor( conn, int(mon["id"]), status="active", trigger_idx=idx, message=str(result.get("msg") or result.get("stopped_reason") or "平仓未完成,将重试"), ) _commit_monitor(conn) continue done = _result_fully_done(result) status = "triggered" if done else "closing" mark_monitor( conn, int(mon["id"]), status=status, trigger_idx=idx, close_ord_id=result.get("close_ord_id"), message="目标位触发限价平仓" if done else "目标位已挂买一限价,等待成交", ) # 关键:先落库,再推送——否则后续 sync 异常回滚会让同一笔反复推微信 _commit_monitor(conn) triggered += 1 _notify_target_close( send_wechat, account_label=account_label, inst_id=inst_id, target=target, idx=idx, result=result, ) return triggered