Fix options add-on premium to sum all open legs for the contract.
Display, close gate, alerts, and full-close accounting no longer use only the latest fill row. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
|
||||
开仓后写入 `options_trades`(open),并在持仓卡展示权利金、买盘深度、按买一可回收等.
|
||||
|
||||
**同合约加仓**:每次买入再插一条 open 记录;展示权利金 / 平仓门控 / 翻倍提醒按同合约 **SUM(premium_paid)** 汇总,不再只取最新一笔.
|
||||
|
||||
---
|
||||
|
||||
## 2. 平仓方式
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user