修复模拟盘期权历史分批平仓盈亏:用卖平成交重算已平记录。
加载历史前按 sim_option_orders(或钱包流水)合计各批收回,纠正只记最后一笔的旧数据。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -902,6 +902,200 @@ class SimBroker:
|
|||||||
rows.append(row)
|
rows.append(row)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sells_from_ledger(
|
||||||
|
conn: Any,
|
||||||
|
*,
|
||||||
|
inst: str,
|
||||||
|
opened: str,
|
||||||
|
closed: str,
|
||||||
|
) -> list[Any]:
|
||||||
|
"""从 sim_ledger_entries 的 option_close 备注解析卖平成交."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
try:
|
||||||
|
params: list[Any] = [f"%sell {inst} %"]
|
||||||
|
sql = """
|
||||||
|
SELECT note, ts AS created_at
|
||||||
|
FROM sim_ledger_entries
|
||||||
|
WHERE kind = 'option_close' AND note LIKE ?
|
||||||
|
"""
|
||||||
|
if opened:
|
||||||
|
sql += " AND ts >= ?"
|
||||||
|
params.append(opened[:19])
|
||||||
|
if closed:
|
||||||
|
sql += " AND ts <= ?"
|
||||||
|
params.append((closed[:16] + ":59") if len(closed) >= 16 else closed)
|
||||||
|
sql += " ORDER BY ts ASC, id ASC"
|
||||||
|
rows = conn.execute(sql, params).fetchall()
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
pat = re.compile(
|
||||||
|
rf"sell\s+{re.escape(inst)}\s+x(?P<sheets>[0-9.]+)@(?P<px>[0-9.]+)",
|
||||||
|
re.I,
|
||||||
|
)
|
||||||
|
for i, r in enumerate(rows):
|
||||||
|
note = str(r["note"] or "")
|
||||||
|
m = pat.search(note)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
sh = float(m.group("sheets"))
|
||||||
|
px = float(m.group("px"))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if sh <= 0 or px <= 0:
|
||||||
|
continue
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"ord_id": f"ledger-{i}",
|
||||||
|
"sheets": sh,
|
||||||
|
"avg_px": px,
|
||||||
|
"created_at": r["created_at"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def repair_closed_options_pnl_from_sim_orders(self, *, limit: int = 300) -> int:
|
||||||
|
"""用 sim_option_orders 卖平成交重算已平仓收回(修复分批平仓只记最后一笔).
|
||||||
|
|
||||||
|
返回更新条数.历史页/复盘合成前调用,使旧数据也能自愈.
|
||||||
|
"""
|
||||||
|
from lib.options.options_db import init_options_tables
|
||||||
|
from lib.options.options_pricing_lib import total_premium
|
||||||
|
from lib.sim.db_lib import init_sim_tables
|
||||||
|
|
||||||
|
conn = self.get_db()
|
||||||
|
updated = 0
|
||||||
|
try:
|
||||||
|
init_options_tables(conn)
|
||||||
|
init_sim_tables(conn)
|
||||||
|
# 表不存在时跳过(极旧库)
|
||||||
|
try:
|
||||||
|
conn.execute("SELECT 1 FROM sim_option_orders LIMIT 1").fetchone()
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
trades = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, inst_id, sheets, eth_amount, premium_paid, premium_received,
|
||||||
|
realized_pnl, close_quote, created_at, closed_at, close_ord_id
|
||||||
|
FROM options_trades
|
||||||
|
WHERE status = 'closed'
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT ?
|
||||||
|
""",
|
||||||
|
(int(max(1, min(int(limit), 500))),),
|
||||||
|
).fetchall()
|
||||||
|
for tr in trades:
|
||||||
|
inst = str(tr["inst_id"] or "").strip()
|
||||||
|
if not inst:
|
||||||
|
continue
|
||||||
|
want_sheets = float(tr["sheets"] or 0)
|
||||||
|
if want_sheets <= 0:
|
||||||
|
continue
|
||||||
|
eth_amt = float(tr["eth_amount"] or 0)
|
||||||
|
ct_mult = (eth_amt / want_sheets) if eth_amt > 0 else 0.01
|
||||||
|
if ct_mult <= 0:
|
||||||
|
ct_mult = 0.01
|
||||||
|
opened = str(tr["created_at"] or "").strip()
|
||||||
|
closed = str(tr["closed_at"] or "").strip()
|
||||||
|
# 时间窗:开仓后、平仓后 2 分钟内(含下单延迟)
|
||||||
|
params: list[Any] = [inst]
|
||||||
|
sql = """
|
||||||
|
SELECT ord_id, sheets, avg_px, created_at
|
||||||
|
FROM sim_option_orders
|
||||||
|
WHERE inst_id = ? AND lower(side) = 'sell'
|
||||||
|
"""
|
||||||
|
if opened:
|
||||||
|
sql += " AND created_at >= ?"
|
||||||
|
params.append(opened[:19])
|
||||||
|
if closed:
|
||||||
|
# 字符串时间比较;闭区间放宽到同分钟及之后的短尾
|
||||||
|
sql += " AND created_at <= ?"
|
||||||
|
params.append((closed[:16] + ":59") if len(closed) >= 16 else closed)
|
||||||
|
sql += " ORDER BY created_at ASC, ord_id ASC"
|
||||||
|
sells = conn.execute(sql, params).fetchall()
|
||||||
|
if not sells:
|
||||||
|
# 回退:从钱包流水 note 解析 sell INST x张数@价
|
||||||
|
sells = self._sells_from_ledger(
|
||||||
|
conn, inst=inst, opened=opened, closed=closed
|
||||||
|
)
|
||||||
|
if not sells:
|
||||||
|
continue
|
||||||
|
selected: list[Any] = []
|
||||||
|
sheets_sum = 0.0
|
||||||
|
for o in sells:
|
||||||
|
selected.append(o)
|
||||||
|
sheets_sum += float(o["sheets"] or 0)
|
||||||
|
if sheets_sum + 1e-9 >= want_sheets:
|
||||||
|
break
|
||||||
|
if sheets_sum + 1e-6 < want_sheets * 0.5:
|
||||||
|
# 成交张数明显不足,不瞎改
|
||||||
|
continue
|
||||||
|
recv = 0.0
|
||||||
|
notional = 0.0
|
||||||
|
oids: list[str] = []
|
||||||
|
for o in selected:
|
||||||
|
sh = float(o["sheets"] or 0)
|
||||||
|
px = float(o["avg_px"] or 0)
|
||||||
|
if sh <= 0 or px <= 0:
|
||||||
|
continue
|
||||||
|
recv += total_premium(px, sh * ct_mult)
|
||||||
|
notional += px * sh
|
||||||
|
oid = str(o["ord_id"] or "").strip()
|
||||||
|
if oid and oid not in oids:
|
||||||
|
oids.append(oid)
|
||||||
|
if not oids:
|
||||||
|
continue
|
||||||
|
total_recv = round(recv, 8)
|
||||||
|
paid = float(tr["premium_paid"] or 0) if tr["premium_paid"] is not None else None
|
||||||
|
if paid is None:
|
||||||
|
continue
|
||||||
|
new_pnl = round(total_recv - paid, 8)
|
||||||
|
close_px = round(notional / sheets_sum, 8) if sheets_sum > 0 else None
|
||||||
|
old_recv = (
|
||||||
|
float(tr["premium_received"])
|
||||||
|
if tr["premium_received"] is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
old_pnl = float(tr["realized_pnl"]) if tr["realized_pnl"] is not None else None
|
||||||
|
# 已与合计一致则跳过
|
||||||
|
if (
|
||||||
|
old_recv is not None
|
||||||
|
and old_pnl is not None
|
||||||
|
and abs(old_recv - total_recv) < 1e-8
|
||||||
|
and abs(old_pnl - new_pnl) < 1e-8
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
# 仅当卖笔数≥2 或合计收回明显高于本地(漏记)时覆盖
|
||||||
|
if len(selected) < 2 and old_recv is not None and total_recv <= old_recv + 1e-10:
|
||||||
|
continue
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE options_trades
|
||||||
|
SET premium_received = ?,
|
||||||
|
realized_pnl = ?,
|
||||||
|
close_quote = COALESCE(?, close_quote),
|
||||||
|
close_ord_id = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
total_recv,
|
||||||
|
new_pnl,
|
||||||
|
close_px,
|
||||||
|
",".join(oids) if oids else tr["close_ord_id"],
|
||||||
|
int(tr["id"]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
updated += 1
|
||||||
|
if updated:
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return updated
|
||||||
|
|
||||||
def option_positions_history_okx_rows(
|
def option_positions_history_okx_rows(
|
||||||
self,
|
self,
|
||||||
exchange: Any = None,
|
exchange: Any = None,
|
||||||
@@ -913,6 +1107,12 @@ class SimBroker:
|
|||||||
|
|
||||||
实盘 positions-history 在模拟模式不可用,期权历史/复盘依赖此合成数据.
|
实盘 positions-history 在模拟模式不可用,期权历史/复盘依赖此合成数据.
|
||||||
"""
|
"""
|
||||||
|
# 先按卖平成交修复分批漏计,再合成展示
|
||||||
|
try:
|
||||||
|
self.repair_closed_options_pnl_from_sim_orders(limit=max(int(limit), 200))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"""模拟盘:用分批卖平成交修复已平仓漏计盈亏."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from lib.options.options_db import init_options_tables
|
||||||
|
from lib.sim.broker_lib import SimBroker
|
||||||
|
from lib.sim.db_lib import init_sim_tables
|
||||||
|
|
||||||
|
|
||||||
|
class RepairSimPartialClosePnlTests(unittest.TestCase):
|
||||||
|
def test_repair_sums_two_sell_orders(self):
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
init_options_tables(conn)
|
||||||
|
init_sim_tables(conn)
|
||||||
|
inst = "ETH-USD-260927-2700-P"
|
||||||
|
# 开仓 228 张,权利金 0.399;旧逻辑只记了第二笔收回 0.2449 → pnl=-0.1541
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO options_trades(
|
||||||
|
inst_id, underlying, opt_type, strike, sheets, eth_amount,
|
||||||
|
open_quote, premium_paid, premium_received, realized_pnl,
|
||||||
|
status, premium_ccy, created_at, closed_at
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
inst,
|
||||||
|
"ETH",
|
||||||
|
"P",
|
||||||
|
2700,
|
||||||
|
228,
|
||||||
|
2.28,
|
||||||
|
0.175,
|
||||||
|
0.399,
|
||||||
|
0.2449,
|
||||||
|
-0.1541,
|
||||||
|
"closed",
|
||||||
|
"ETH",
|
||||||
|
"2026-09-24 07:09:19",
|
||||||
|
"2026-09-24 20:59:20",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# 两笔卖平各 114 张
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO sim_option_orders(
|
||||||
|
ord_id, inst_id, side, sheets, avg_px, state, acc_fill_sz, created_at
|
||||||
|
) VALUES (?,?,?,?,?,'filled',?,?)
|
||||||
|
""",
|
||||||
|
("sim-a", inst, "sell", 114, 0.12, 114, "2026-09-24 15:00:00"),
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO sim_option_orders(
|
||||||
|
ord_id, inst_id, side, sheets, avg_px, state, acc_fill_sz, created_at
|
||||||
|
) VALUES (?,?,?,?,?,'filled',?,?)
|
||||||
|
""",
|
||||||
|
("sim-b", inst, "sell", 114, 0.1074, 114, "2026-09-24 20:59:10"),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
return conn
|
||||||
|
|
||||||
|
# SimBroker 每次 close 自己的连接;测试里复用同一 :memory: 需禁止 close
|
||||||
|
class _Proxy:
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(conn, name)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
broker = SimBroker(lambda: _Proxy()) # type: ignore[arg-type]
|
||||||
|
n = broker.repair_closed_options_pnl_from_sim_orders()
|
||||||
|
self.assertEqual(n, 1)
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT premium_received, realized_pnl, close_ord_id FROM options_trades WHERE inst_id=?",
|
||||||
|
(inst,),
|
||||||
|
).fetchone()
|
||||||
|
# ct_mult=2.28/228=0.01; recv=114*0.12*0.01 + 114*0.1074*0.01 = 0.1368+0.122436=0.259236
|
||||||
|
self.assertAlmostEqual(float(row["premium_received"]), 0.259236, places=6)
|
||||||
|
self.assertAlmostEqual(float(row["realized_pnl"]), 0.259236 - 0.399, places=6)
|
||||||
|
self.assertIn("sim-a", str(row["close_ord_id"]))
|
||||||
|
self.assertIn("sim-b", str(row["close_ord_id"]))
|
||||||
|
|
||||||
|
# 历史合成应读到修复后的盈亏
|
||||||
|
hist = broker.option_positions_history_okx_rows(limit=10)
|
||||||
|
self.assertEqual(len(hist), 1)
|
||||||
|
self.assertAlmostEqual(float(hist[0]["realizedPnl"]), 0.259236 - 0.399, places=6)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user