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),并在持仓卡展示权利金、买盘深度、按买一可回收等.
|
开仓后写入 `options_trades`(open),并在持仓卡展示权利金、买盘深度、按买一可回收等.
|
||||||
|
|
||||||
|
**同合约加仓**:每次买入再插一条 open 记录;展示权利金 / 平仓门控 / 翻倍提醒按同合约 **SUM(premium_paid)** 汇总,不再只取最新一笔.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 平仓方式
|
## 2. 平仓方式
|
||||||
|
|||||||
@@ -31,15 +31,10 @@ def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None:
|
|||||||
try:
|
try:
|
||||||
conn = cfg["get_db"]()
|
conn = cfg["get_db"]()
|
||||||
try:
|
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)
|
init_options_tables(conn)
|
||||||
row = conn.execute(
|
return sum_open_premium_paid(conn, inst_id)
|
||||||
"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"])
|
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -299,16 +294,30 @@ def close_option_by_bid1(
|
|||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables
|
||||||
|
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
row = conn.execute(
|
open_rows = conn.execute(
|
||||||
"SELECT id, premium_paid FROM options_trades WHERE inst_id = ? AND status = 'open' ORDER BY id DESC LIMIT 1",
|
"""
|
||||||
|
SELECT id, premium_paid FROM options_trades
|
||||||
|
WHERE inst_id = ? AND status = 'open'
|
||||||
|
ORDER BY id ASC
|
||||||
|
""",
|
||||||
(inst_id,),
|
(inst_id,),
|
||||||
).fetchone()
|
).fetchall()
|
||||||
if row:
|
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)
|
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 = ""
|
note_sql = ""
|
||||||
params: list[Any] = [px, prem_recv, pnl, oid or None]
|
params: list[Any] = [px, recv, pnl, oid or None]
|
||||||
if signal_note:
|
if signal_note and i == len(open_rows) - 1:
|
||||||
note_sql = """,
|
note_sql = """,
|
||||||
signal_note = CASE
|
signal_note = CASE
|
||||||
WHEN signal_note IS NULL OR TRIM(signal_note) = '' THEN ?
|
WHEN signal_note IS NULL OR TRIM(signal_note) = '' THEN ?
|
||||||
|
|||||||
@@ -93,3 +93,39 @@ def init_options_tables(conn: sqlite3.Connection) -> None:
|
|||||||
ON options_target_monitors(status)
|
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 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(
|
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:
|
for p in raw_live:
|
||||||
inst = str(p.get("instId") or "").strip()
|
inst = str(p.get("instId") or "").strip()
|
||||||
premium_override = None
|
premium_override = sum_open_premium_paid(conn, inst) if inst else 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"])
|
|
||||||
row = enrich_position_row_display(
|
row = enrich_position_row_display(
|
||||||
cfg,
|
cfg,
|
||||||
ex,
|
ex,
|
||||||
|
|||||||
@@ -61,18 +61,34 @@ def run_options_profit_alerts(
|
|||||||
SELECT id, inst_id, premium_paid, profit_alert_sent
|
SELECT id, inst_id, premium_paid, profit_alert_sent
|
||||||
FROM options_trades
|
FROM options_trades
|
||||||
WHERE status = 'open'
|
WHERE status = 'open'
|
||||||
|
ORDER BY id ASC
|
||||||
"""
|
"""
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
# 同合约多腿加仓:按合约汇总权利金,整仓只告警一次
|
||||||
|
by_inst: dict[str, dict[str, Any]] = {}
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if int(row["profit_alert_sent"] or 0):
|
|
||||||
continue
|
|
||||||
inst_id = str(row["inst_id"] or "")
|
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"])
|
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
|
continue
|
||||||
pos = pos_by_inst.get(inst_id)
|
pos = pos_by_inst.get(inst_id)
|
||||||
if not pos:
|
if not pos:
|
||||||
continue
|
continue
|
||||||
|
prem = float(bucket["premium"])
|
||||||
upl = _safe_float(pos.get("upl"))
|
upl = _safe_float(pos.get("upl"))
|
||||||
upl_ratio = _safe_float(pos.get("upl_ratio_pct"))
|
upl_ratio = _safe_float(pos.get("upl_ratio_pct"))
|
||||||
if upl_ratio is not None:
|
if upl_ratio is not None:
|
||||||
@@ -95,8 +111,8 @@ def run_options_profit_alerts(
|
|||||||
try:
|
try:
|
||||||
send_wechat(msg)
|
send_wechat(msg)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE options_trades SET profit_alert_sent = 1 WHERE id = ?",
|
f"UPDATE options_trades SET profit_alert_sent = 1 WHERE id IN ({','.join('?' * len(bucket['ids']))})",
|
||||||
(int(row["id"]),),
|
tuple(bucket["ids"]),
|
||||||
)
|
)
|
||||||
sent += 1
|
sent += 1
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
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_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_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
|
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)
|
init_options_tables(conn)
|
||||||
for p in raw_positions:
|
for p in raw_positions:
|
||||||
inst = str(p.get("instId") or "").strip()
|
inst = str(p.get("instId") or "").strip()
|
||||||
premium_override = None
|
premium_override = sum_open_premium_paid(conn, inst) if inst else 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"])
|
|
||||||
row = enrich_position_row_display(
|
row = enrich_position_row_display(
|
||||||
cfg,
|
cfg,
|
||||||
ex,
|
ex,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Any
|
|||||||
from flask import Flask, jsonify, redirect, request, url_for
|
from flask import Flask, jsonify, redirect, request, url_for
|
||||||
from jinja2 import ChoiceLoader, FileSystemLoader
|
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_monitor_lib import options_monitor_loop
|
||||||
from lib.options.options_pricing_lib import (
|
from lib.options.options_pricing_lib import (
|
||||||
calc_order_size,
|
calc_order_size,
|
||||||
@@ -170,19 +170,9 @@ def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None:
|
|||||||
conn = cfg["get_db"]()
|
conn = cfg["get_db"]()
|
||||||
try:
|
try:
|
||||||
init_options_tables(conn)
|
init_options_tables(conn)
|
||||||
rec = conn.execute(
|
return sum_open_premium_paid(conn, inst_id)
|
||||||
"""
|
|
||||||
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)
|
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _position_avail_sheets(pos: dict[str, Any]) -> int:
|
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 = []
|
rows = []
|
||||||
for p in raw:
|
for p in raw:
|
||||||
inst = str(p.get("instId") or "").strip()
|
inst = str(p.get("instId") or "").strip()
|
||||||
premium_override = None
|
premium_override = sum_open_premium_paid(conn, inst) if inst else 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"])
|
|
||||||
row = _enrich_position_row_display(
|
row = _enrich_position_row_display(
|
||||||
cfg,
|
cfg,
|
||||||
ex,
|
ex,
|
||||||
@@ -732,14 +711,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
try:
|
try:
|
||||||
trade = conn.execute(
|
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'
|
WHERE inst_id = ? AND status = 'open'
|
||||||
ORDER BY id DESC LIMIT 1
|
ORDER BY id DESC LIMIT 1
|
||||||
""",
|
""",
|
||||||
(inst_id,),
|
(inst_id,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
trade_id = int(trade["id"]) if trade else None
|
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")
|
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")
|
underlying = (trade["underlying"] if trade else None) or fmt.get("underlying")
|
||||||
out = upsert_target_monitor(
|
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