From 474e0742f7bb5137b28492a95fb4c4ef2e2c57c0 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 6 Oct 2026 16:45:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9D=A1=E4=BB=B6=E5=8D=95=E6=9C=AA=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E8=87=B3=E5=91=A8=E5=9B=9B23:00=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8F=96=E6=B6=88=EF=BC=8C=E4=B8=8B=E5=8F=91/=E5=8F=96?= =?UTF-8?q?=E6=B6=88/=E5=BC=80=E4=BB=93=E5=9D=87=E6=8E=A8=E4=BC=81?= =?UTF-8?q?=E4=B8=9A=E5=BE=AE=E4=BF=A1=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- lib/hub/hub_bridge_register.py | 7 +- lib/options/options_cond_order_lib.py | 138 ++++++++++++++++++++++---- 2 files changed, 127 insertions(+), 18 deletions(-) diff --git a/lib/hub/hub_bridge_register.py b/lib/hub/hub_bridge_register.py index aa26165..5c4e0e7 100644 --- a/lib/hub/hub_bridge_register.py +++ b/lib/hub/hub_bridge_register.py @@ -358,7 +358,12 @@ def register_hub_bridge_routes(app: Flask, app_module: Any) -> None: conn = cfg["get_db"]() try: init_options_tables(conn) - out = create_cond_order(conn, data) + out = create_cond_order( + conn, + data, + send_wechat=cfg.get("send_wechat"), + account_label=str(cfg.get("account_label") or ""), + ) out["version"] = 1 return jsonify(out) finally: diff --git a/lib/options/options_cond_order_lib.py b/lib/options/options_cond_order_lib.py index 21d6f3b..9358839 100644 --- a/lib/options/options_cond_order_lib.py +++ b/lib/options/options_cond_order_lib.py @@ -4,10 +4,11 @@ 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/closed) +# done:终态(leave_up/leave_down/cancel/timeout/closed) ACTIVE_STATUSES = ("armed", "triggering", "open") @@ -87,6 +88,78 @@ def _now() -> str: 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 @@ -156,7 +229,13 @@ def machine_has_option_position(cfg: dict[str, Any], ex: Any) -> bool: return count_live_option_positions(raw) > 0 -def create_cond_order(conn: sqlite3.Connection, payload: dict[str, Any]) -> dict[str, Any]: +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() @@ -224,6 +303,18 @@ def create_cond_order(conn: sqlite3.Connection, payload: dict[str, Any]) -> dict ) 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} @@ -435,20 +526,20 @@ def _end_order( "leave_up": "上破偏离区结束", "leave_down": "下破偏离区结束", "cancel": "中控取消", + "timeout": "周四23:00未触发自动取消", "closed": "出场完成", } - lines = [ - "【OKX期权·条件单结束】", - f"账户:{account_label or 'OKX期权'}", - f"单号:{order.get('hub_order_id')}", - f"标的:{order.get('underlying')} {order.get('opt_type')}", - f"触发价:{order.get('trigger_px')}", - f"原因:{reason_map.get(end_reason, end_reason)}", - f"说明:{message}", - ] - if index_px is not None: - lines.append(f"当前指数:{index_px:g}") - send_wechat("\n".join(lines)) + _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 @@ -583,8 +674,11 @@ def run_cond_orders_tick( send_wechat: Any = None, account_label: str = "", ) -> None: - """监控循环单步:穿越/偏离区/开仓/出场收尾.""" + """监控循环单步:周四未触发取消 / 穿越 / 偏离区 / 开仓 / 出场收尾.""" 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 @@ -747,8 +841,18 @@ def _tick_one( ), ) conn.commit() - # 确保出场规则挂上 - order2 = get_cond_order_by_hub_id(conn, order["hub_order_id"]) or order + 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,