diff --git a/lib/options/options_monitor_lib.py b/lib/options/options_monitor_lib.py index 9657546..9b7ef64 100644 --- a/lib/options/options_monitor_lib.py +++ b/lib/options/options_monitor_lib.py @@ -371,9 +371,47 @@ def sync_open_options_trades( ) except Exception: pass + if notify_cfg is not None: + _notify_recent_unsent_expiry_closes(conn, notify_cfg) return updated +def _notify_recent_unsent_expiry_closes( + conn: sqlite3.Connection, + notify_cfg: dict[str, Any], + *, + within_hours: float = 48.0, +) -> None: + """页面同步曾漏推时,补发近期到期结算.""" + try: + from lib.options.options_notify_lib import notify_options_close_trade_ids + from lib.options.options_time_lib import options_wall_clock_to_ms + + now_ms = int(time.time() * 1000) + by_inst: dict[str, list[int]] = {} + rows = conn.execute( + """ + SELECT id, inst_id, closed_at + FROM options_trades + WHERE status = 'closed' + AND COALESCE(wechat_close_sent, 0) = 0 + AND COALESCE(signal_note, '') = '到期结算' + ORDER BY id DESC + LIMIT 20 + """ + ).fetchall() + for r in rows: + closed_ms = options_wall_clock_to_ms(r["closed_at"]) + if closed_ms is None or now_ms - int(closed_ms) > float(within_hours) * 3600 * 1000: + continue + inst = str(r["inst_id"] or "") + by_inst.setdefault(inst, []).append(int(r["id"])) + for ids in by_inst.values(): + notify_options_close_trade_ids(notify_cfg, conn, ids, reason="到期结算") + except Exception: + pass + + def reconcile_live_open_trades( conn: sqlite3.Connection, *, diff --git a/lib/options/options_notify_lib.py b/lib/options/options_notify_lib.py index e8d5db7..9babdea 100644 --- a/lib/options/options_notify_lib.py +++ b/lib/options/options_notify_lib.py @@ -58,6 +58,38 @@ def ensure_options_notify_columns(conn: sqlite3.Connection) -> None: pass +_BOUND_NOTIFY_CFG: dict[str, Any] | None = None + + +def bind_options_notify_cfg(cfg: dict[str, Any] | None) -> None: + global _BOUND_NOTIFY_CFG + _BOUND_NOTIFY_CFG = cfg + + +def active_options_notify_cfg(cfg: dict[str, Any] | None = None) -> dict[str, Any]: + if cfg is not None and callable(cfg.get("send_wechat")): + return cfg + if _BOUND_NOTIFY_CFG is not None and callable(_BOUND_NOTIFY_CFG.get("send_wechat")): + return _BOUND_NOTIFY_CFG + import os + + from lib.common.wechat_notify_lib import send_wechat_webhook + + webhook = (os.getenv("WECHAT_WEBHOOK") or "").strip() + try: + timeout = int(float(os.getenv("WECHAT_TIMEOUT_SECONDS") or "10")) + except (TypeError, ValueError): + timeout = 10 + label = ( + os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or os.getenv("OKX_ACCOUNT_LABEL") or "OKX期权" + ).strip() + + def send(content: str) -> None: + send_wechat_webhook(webhook, content, timeout=timeout) + + return {"send_wechat": send, "account_label": label} + + def notify_options_send(cfg: dict[str, Any], content: str) -> bool: send: Optional[Callable[[str], Any]] = cfg.get("send_wechat") if not callable(send): diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 8647e2a..dbd70b6 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -63,6 +63,12 @@ def install_options_trading(app: Flask, repo_root: str, app_module: Any) -> None patch_options_cfg(cfg) except Exception: pass + try: + from lib.options.options_notify_lib import bind_options_notify_cfg + + bind_options_notify_cfg(cfg) + except Exception: + pass app.extensions["options_cfg"] = cfg register_options_routes(app, cfg) _register_options_hub_bridge(app, cfg) @@ -438,7 +444,12 @@ def _sync_options_trades( try: init_options_tables(conn) reconcile_live_open_trades(conn, live_inst_ids=live_ids) - sync_open_options_trades(conn, live_inst_ids=live_ids, fetch_history_fn=_hist) + sync_open_options_trades( + conn, + live_inst_ids=live_ids, + fetch_history_fn=_hist, + notify_cfg=cfg, + ) try: hist_all = fetch_all_option_positions_history(ex, limit=200) backfill_closed_options_realized_pnl_from_history(conn, hist_all) diff --git a/lib/sim/broker_lib.py b/lib/sim/broker_lib.py index 5ee1e2d..93ef764 100644 --- a/lib/sim/broker_lib.py +++ b/lib/sim/broker_lib.py @@ -760,6 +760,13 @@ class SimBroker: conn.execute("DELETE FROM sim_option_positions WHERE inst_id=?", (inst_id,)) try: closed_at = _now() + open_ids = [ + int(r["id"]) + for r in conn.execute( + "SELECT id FROM options_trades WHERE inst_id=? AND status='open'", + (inst_id,), + ).fetchall() + ] conn.execute( """ UPDATE options_trades @@ -783,6 +790,18 @@ class SimBroker: inst_id, ), ) + if open_ids: + from lib.options.options_notify_lib import ( + active_options_notify_cfg, + notify_options_close_trade_ids, + ) + + notify_options_close_trade_ids( + active_options_notify_cfg(), + conn, + open_ids, + reason="到期结算", + ) except Exception: pass conn.commit()