diff --git a/docs/期权开平仓与监控说明.md b/docs/期权开平仓与监控说明.md index bd4f9e8..7e8a97d 100644 --- a/docs/期权开平仓与监控说明.md +++ b/docs/期权开平仓与监控说明.md @@ -17,6 +17,8 @@ 开仓后写入 `options_trades`(open),并在持仓卡展示权利金、买盘深度、按买一可回收等. +**同合约加仓**:每次买入再插一条 open 记录;展示权利金 / 平仓门控 / 翻倍提醒按同合约 **SUM(premium_paid)** 汇总,不再只取最新一笔. + --- ## 2. 平仓方式 diff --git a/lib/options/options_close_exec_lib.py b/lib/options/options_close_exec_lib.py index 4c96f0d..59e2cd0 100644 --- a/lib/options/options_close_exec_lib.py +++ b/lib/options/options_close_exec_lib.py @@ -31,15 +31,10 @@ def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None: try: conn = cfg["get_db"]() try: - from lib.options.options_db import init_options_tables + from lib.options.options_db import init_options_tables, sum_open_premium_paid init_options_tables(conn) - row = conn.execute( - "SELECT premium_paid FROM options_trades WHERE inst_id = ? AND status = 'open' ORDER BY id DESC LIMIT 1", - (inst_id,), - ).fetchone() - if row and row["premium_paid"] is not None: - return float(row["premium_paid"]) + return sum_open_premium_paid(conn, inst_id) finally: conn.close() except Exception: @@ -299,16 +294,30 @@ def close_option_by_bid1( from lib.options.options_db import init_options_tables init_options_tables(conn) - row = conn.execute( - "SELECT id, premium_paid FROM options_trades WHERE inst_id = ? AND status = 'open' ORDER BY id DESC LIMIT 1", + open_rows = conn.execute( + """ + SELECT id, premium_paid FROM options_trades + WHERE inst_id = ? AND status = 'open' + ORDER BY id ASC + """, (inst_id,), - ).fetchone() - if row: + ).fetchall() + total_paid = sum(float(r["premium_paid"] or 0) for r in open_rows) + allocated = 0.0 + for i, row in enumerate(open_rows): paid = float(row["premium_paid"] or 0) - pnl = prem_recv - paid + if i == len(open_rows) - 1: + recv = round(prem_recv - allocated, 4) + elif total_paid > 0: + recv = round(prem_recv * (paid / total_paid), 4) + allocated += recv + else: + recv = round(prem_recv / len(open_rows), 4) + allocated += recv + pnl = round(recv - paid, 4) note_sql = "" - params: list[Any] = [px, prem_recv, pnl, oid or None] - if signal_note: + params: list[Any] = [px, recv, pnl, oid or None] + if signal_note and i == len(open_rows) - 1: note_sql = """, signal_note = CASE WHEN signal_note IS NULL OR TRIM(signal_note) = '' THEN ? @@ -326,7 +335,7 @@ def close_option_by_bid1( """, tuple(params), ) - conn.commit() + conn.commit() finally: conn.close() elif require_recycle_gate: diff --git a/lib/options/options_db.py b/lib/options/options_db.py index 164fbfa..f30bdb1 100644 --- a/lib/options/options_db.py +++ b/lib/options/options_db.py @@ -93,3 +93,39 @@ def init_options_tables(conn: sqlite3.Connection) -> None: ON options_target_monitors(status) """ ) + + +def sum_open_premium_paid(conn: sqlite3.Connection, inst_id: str) -> float | None: + """同合约所有 open 腿权利金合计(加仓后显示/门控用).""" + inst = (inst_id or "").strip() + if not inst: + return None + row = conn.execute( + """ + SELECT SUM(premium_paid) AS total, COUNT(*) AS n + FROM options_trades + WHERE inst_id = ? AND status = 'open' AND premium_paid IS NOT NULL + """, + (inst,), + ).fetchone() + if not row or int(row["n"] or 0) < 1: + return None + return round(float(row["total"] or 0), 4) + + +def sum_open_sheets(conn: sqlite3.Connection, inst_id: str) -> int | None: + """同合约所有 open 腿张数合计.""" + inst = (inst_id or "").strip() + if not inst: + return None + row = conn.execute( + """ + SELECT SUM(sheets) AS total, COUNT(*) AS n + FROM options_trades + WHERE inst_id = ? AND status = 'open' + """, + (inst,), + ).fetchone() + if not row or int(row["n"] or 0) < 1: + return None + return int(row["total"] or 0) diff --git a/lib/options/options_history_lib.py b/lib/options/options_history_lib.py index 6099cd4..e54d868 100644 --- a/lib/options/options_history_lib.py +++ b/lib/options/options_history_lib.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from lib.options.options_db import init_options_tables +from lib.options.options_db import init_options_tables, sum_open_premium_paid def enrich_position_row_display( @@ -50,18 +50,7 @@ def load_options_history(ex: Any, cfg: dict[str, Any]) -> list[dict[str, Any]]: } for p in raw_live: inst = str(p.get("instId") or "").strip() - premium_override = None - if inst: - rec = conn.execute( - """ - SELECT premium_paid FROM options_trades - WHERE inst_id = ? AND status = 'open' - ORDER BY id DESC LIMIT 1 - """, - (inst,), - ).fetchone() - if rec and rec["premium_paid"] is not None: - premium_override = float(rec["premium_paid"]) + premium_override = sum_open_premium_paid(conn, inst) if inst else None row = enrich_position_row_display( cfg, ex, diff --git a/lib/options/options_monitor_lib.py b/lib/options/options_monitor_lib.py index f930e82..55b3b17 100644 --- a/lib/options/options_monitor_lib.py +++ b/lib/options/options_monitor_lib.py @@ -61,18 +61,34 @@ def run_options_profit_alerts( SELECT id, inst_id, premium_paid, profit_alert_sent FROM options_trades WHERE status = 'open' + ORDER BY id ASC """ ).fetchall() + # 同合约多腿加仓:按合约汇总权利金,整仓只告警一次 + by_inst: dict[str, dict[str, Any]] = {} for row in rows: - if int(row["profit_alert_sent"] or 0): - continue inst_id = str(row["inst_id"] or "") + if not inst_id: + continue + bucket = by_inst.setdefault( + inst_id, + {"ids": [], "premium": 0.0, "all_sent": True, "has_prem": False}, + ) + bucket["ids"].append(int(row["id"])) prem = _safe_float(row["premium_paid"]) - if not inst_id or prem is None or prem <= 0: + if prem is not None: + bucket["premium"] += float(prem) + bucket["has_prem"] = True + if not int(row["profit_alert_sent"] or 0): + bucket["all_sent"] = False + + for inst_id, bucket in by_inst.items(): + if bucket["all_sent"] or not bucket["has_prem"] or bucket["premium"] <= 0: continue pos = pos_by_inst.get(inst_id) if not pos: continue + prem = float(bucket["premium"]) upl = _safe_float(pos.get("upl")) upl_ratio = _safe_float(pos.get("upl_ratio_pct")) if upl_ratio is not None: @@ -95,8 +111,8 @@ def run_options_profit_alerts( try: send_wechat(msg) conn.execute( - "UPDATE options_trades SET profit_alert_sent = 1 WHERE id = ?", - (int(row["id"]),), + f"UPDATE options_trades SET profit_alert_sent = 1 WHERE id IN ({','.join('?' * len(bucket['ids']))})", + tuple(bucket["ids"]), ) sent += 1 except Exception: diff --git a/lib/options/options_positions_lib.py b/lib/options/options_positions_lib.py index 569c853..e94ef25 100644 --- a/lib/options/options_positions_lib.py +++ b/lib/options/options_positions_lib.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from lib.options.options_db import init_options_tables +from lib.options.options_db import init_options_tables, sum_open_premium_paid from lib.options.options_history_lib import enrich_position_row_display from lib.options.options_close_gate_lib import clear_close_gate, is_close_gate_passed, update_close_gate from lib.options.options_pricing_lib import estimate_close_by_bids, intrinsic_px_per_unit @@ -140,18 +140,7 @@ def build_display_option_positions( init_options_tables(conn) for p in raw_positions: inst = str(p.get("instId") or "").strip() - premium_override = None - if inst: - rec = conn.execute( - """ - SELECT premium_paid FROM options_trades - WHERE inst_id = ? AND status = 'open' - ORDER BY id DESC LIMIT 1 - """, - (inst,), - ).fetchone() - if rec and rec["premium_paid"] is not None: - premium_override = float(rec["premium_paid"]) + premium_override = sum_open_premium_paid(conn, inst) if inst else None row = enrich_position_row_display( cfg, ex, diff --git a/lib/options/options_register.py b/lib/options/options_register.py index b248c79..ea460df 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -9,7 +9,7 @@ from typing import Any from flask import Flask, jsonify, redirect, request, url_for from jinja2 import ChoiceLoader, FileSystemLoader -from lib.options.options_db import init_options_tables +from lib.options.options_db import init_options_tables, sum_open_premium_paid, sum_open_sheets from lib.options.options_monitor_lib import options_monitor_loop from lib.options.options_pricing_lib import ( calc_order_size, @@ -170,19 +170,9 @@ def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None: conn = cfg["get_db"]() try: init_options_tables(conn) - rec = conn.execute( - """ - SELECT premium_paid FROM options_trades - WHERE inst_id = ? AND status = 'open' - ORDER BY id DESC LIMIT 1 - """, - (inst_id,), - ).fetchone() - if rec and rec["premium_paid"] is not None: - return round(float(rec["premium_paid"]), 4) + return sum_open_premium_paid(conn, inst_id) finally: conn.close() - return None def _position_avail_sheets(pos: dict[str, Any]) -> int: @@ -662,18 +652,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: rows = [] for p in raw: inst = str(p.get("instId") or "").strip() - premium_override = None - if inst: - rec = conn.execute( - """ - SELECT premium_paid FROM options_trades - WHERE inst_id = ? AND status = 'open' - ORDER BY id DESC LIMIT 1 - """, - (inst,), - ).fetchone() - if rec and rec["premium_paid"] is not None: - premium_override = float(rec["premium_paid"]) + premium_override = sum_open_premium_paid(conn, inst) if inst else None row = _enrich_position_row_display( cfg, ex, @@ -732,14 +711,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: try: trade = conn.execute( """ - SELECT id, sheets, opt_type, underlying FROM options_trades + SELECT id, opt_type, underlying FROM options_trades WHERE inst_id = ? AND status = 'open' ORDER BY id DESC LIMIT 1 """, (inst_id,), ).fetchone() trade_id = int(trade["id"]) if trade else None - sheets = int(trade["sheets"]) if trade and trade["sheets"] is not None else int(fmt.get("pos") or 0) + sheets_sum = sum_open_sheets(conn, inst_id) + sheets = sheets_sum if sheets_sum is not None else int(fmt.get("pos") or 0) opt_type = (trade["opt_type"] if trade else None) or fmt.get("opt_type") underlying = (trade["underlying"] if trade else None) or fmt.get("underlying") out = upsert_target_monitor( diff --git a/tests/test_options_add_premium.py b/tests/test_options_add_premium.py new file mode 100644 index 0000000..a4e3f27 --- /dev/null +++ b/tests/test_options_add_premium.py @@ -0,0 +1,69 @@ +"""期权加仓后权利金汇总.""" +from __future__ import annotations + +import sqlite3 + +from lib.options.options_db import init_options_tables, sum_open_premium_paid, sum_open_sheets + + +def _mem_db() -> sqlite3.Connection: + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + init_options_tables(conn) + return conn + + +def test_sum_open_premium_after_add(): + conn = _mem_db() + inst = "BTC-USD_UM-260717-65500-C" + conn.execute( + """ + INSERT INTO options_trades + (inst_id, underlying, opt_type, sheets, eth_amount, open_quote, premium_paid, status) + VALUES (?, 'BTC-USD_UM', 'C', 1, 0.01, 530, 5.3, 'open') + """, + (inst,), + ) + conn.execute( + """ + INSERT INTO options_trades + (inst_id, underlying, opt_type, sheets, eth_amount, open_quote, premium_paid, status) + VALUES (?, 'BTC-USD_UM', 'C', 1, 0.01, 140, 1.4, 'open') + """, + (inst,), + ) + conn.commit() + assert sum_open_premium_paid(conn, inst) == 6.7 + assert sum_open_sheets(conn, inst) == 2 + # 最新一笔单独是 1.4,汇总不能只取最新 + latest = conn.execute( + "SELECT premium_paid FROM options_trades WHERE inst_id=? AND status='open' ORDER BY id DESC LIMIT 1", + (inst,), + ).fetchone() + assert float(latest["premium_paid"]) == 1.4 + conn.close() + + +def test_sum_open_premium_ignores_closed(): + conn = _mem_db() + inst = "ETH-USD_UM-260101-2000-C" + conn.execute( + """ + INSERT INTO options_trades + (inst_id, underlying, opt_type, sheets, eth_amount, premium_paid, status) + VALUES (?, 'ETH-USD_UM', 'C', 1, 0.01, 2.0, 'closed') + """, + (inst,), + ) + conn.execute( + """ + INSERT INTO options_trades + (inst_id, underlying, opt_type, sheets, eth_amount, premium_paid, status) + VALUES (?, 'ETH-USD_UM', 'C', 2, 0.02, 3.5, 'open') + """, + (inst,), + ) + conn.commit() + assert sum_open_premium_paid(conn, inst) == 3.5 + assert sum_open_sheets(conn, inst) == 2 + conn.close()