fix开仓价卖回价与真盈亏一次冻结,复盘同步不再反复改写。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-13 10:56:53 +08:00
parent 1aa279f191
commit aa8b16ce27
5 changed files with 106 additions and 64 deletions
+10 -6
View File
@@ -42,15 +42,19 @@
含义:**计入权利金的 USDT 波动**;**忽略现货多买缓冲**。
有开/平手续费时:开仓费按开仓价、平仓费按卖回价折 U 后扣除。
**多笔成交(VWAP):**
**固定量 vs 一次冻结价:**
| 价格 | 定义 |
| 字段 | 性质 |
|------|------|
| 开仓价 | 买币多笔加权:`Σ买币花费USDT / Σ买入币量`(缓冲买单计入均价,公式仍只乘 `premium_paid`) |
| 卖回价 | 平仓后卖回多笔加权:`Σ收回USDT / Σ卖出币量`(**不含**开仓多余卖回) |
| `premium_paid`(权利金币) | 开仓后**固定**,不再改 |
| `realized_pnl`(期权腿币盈亏) | 平仓后**固定**,不再改 |
| 开仓价 | 买币完成时算一次 VWAP 写入 `open_index_px`,**冻住** |
| 卖回价 | 平仓卖回全部完成时算一次 VWAP 写入 `close_index_px`,**冻住** |
| `realized_pnl_usdt` | 卖回完成后按上式算一次,`pnl_settled=1` 后**只读** |
- 卖回未完成(仍有可卖)→ 复盘标「待卖回」,不冻结真盈亏
- 无 VWAP 时回退冻结指数;仍缺则「缺价未核」
- 卖回未完成 → 「待卖回」,不算真盈亏
- 缺价 → 「缺价未核」
- 复盘刷新**不得**改已冻结的开仓价/卖回价/真盈亏/权利金币量/币盈亏
- **消费者**:期权复盘「期权交易记录」盈亏列、复盘 KPI、`realized_pnl_usdt` 冻结
- **不含**:现货多买差分当盈亏 / 账户 after−before
+2 -1
View File
@@ -308,12 +308,13 @@ def settle_closed_trades_from_trading_account(
)
if pnl_u is None or src in ("none", "pending_sell"):
continue
# 卖回价一次冻结为卖回 VWAP;开仓价已在开仓时冻住,仅缺省时补
conn.execute(
"""
UPDATE options_trades
SET trading_usdt_after = COALESCE(?, trading_usdt_after),
realized_pnl_usdt = ?,
open_index_px = COALESCE(?, open_index_px),
open_index_px = COALESCE(open_index_px, ?),
close_index_px = COALESCE(?, close_index_px),
pnl_settled = 1
WHERE id = ? AND COALESCE(pnl_settled, 0)=0
+9 -1
View File
@@ -520,6 +520,14 @@ def open_coin_option_buy_full(
message="期权持仓中",
)
# 开仓价一次冻结:优先买币 VWAP(之后不再改);权利金币量 premium_paid 固定
open_px_freeze = idx_open
try:
bv = _spot_bridge.bridge_buy_vwap(conn, bridge_id)
if bv is not None and float(bv) > 0:
open_px_freeze = float(bv)
except Exception:
pass
trade_id = _insert_coin_trade(
conn,
inst_id=inst_id,
@@ -537,7 +545,7 @@ def open_coin_option_buy_full(
budget_usdt=buy_usdt,
premium_ccy=premium_ccy,
open_fee=open_fee,
open_index_px=idx_open,
open_index_px=open_px_freeze,
trading_usdt_before=trading_usdt_before,
profit_exit_enabled=profit_exit_enabled,
profit_exit_mult=profit_exit_mult,
+33 -52
View File
@@ -556,6 +556,10 @@ def sync_options_from_local_trades(
if not ccy:
ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly)
frozen_usdt = _safe_float(r["realized_pnl_usdt"])
try:
settled = int(r["pnl_settled"] or 0) == 1
except Exception:
settled = False
close_idx = _safe_float(r["close_index_px"])
open_idx = _safe_float(r["open_index_px"])
idx = None
@@ -576,43 +580,35 @@ def sync_options_from_local_trades(
close_fee=cfee,
premium_ccy=ccy,
)
# 初始权利金可用开仓价,缺则兜底指数;真盈亏必须用冻结的开仓价+卖回价,禁止实时价冒充
# 权利金币量/币盈亏固定;开仓价与卖回价各自冻一次后真盈亏只读
open_idx_for_prem = open_idx if (open_idx is not None and open_idx > 0) else idx
sell_idx = close_idx if (close_idx is not None and close_idx > 0) else None
from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade
pnl_u, pnl_src = resolve_true_pnl_usdt_from_trade(
premium_paid=prem,
premium_received=r["premium_received"],
realized_pnl=r["realized_pnl"],
open_index_px=open_idx if (open_idx is not None and open_idx > 0) else None,
close_index_px=sell_idx,
open_fee=ofee,
close_fee=cfee,
premium_ccy=ccy,
fee_ccy=str(r["premium_ccy"] or ccy or ""),
conn=conn,
bridge_id=r["bridge_id"],
)
# 能算新公式则写回;卖回未完成→待卖回;算不出→缺价未核
status_label = "已平"
if pnl_src == "pending_sell":
status_label = "待卖回"
pnl_u = None
# 清掉曾用指数提前冻上的假真盈亏
try:
conn.execute(
"""
UPDATE options_trades
SET realized_pnl_usdt = NULL, pnl_settled = 0
WHERE id = ? AND COALESCE(pnl_settled, 0) = 1
""",
(trade_id,),
)
except Exception:
pass
elif pnl_u is not None and pnl_src not in ("none", "pending_sell"):
if frozen_usdt is None or abs(float(frozen_usdt) - float(pnl_u)) > 0.05:
pnl_u = None
if settled and frozen_usdt is not None:
# 已冻结:只读展示,禁止同步反复改写
pnl_u = round(float(frozen_usdt), 4)
else:
pnl_u, pnl_src = resolve_true_pnl_usdt_from_trade(
premium_paid=prem,
premium_received=r["premium_received"],
realized_pnl=r["realized_pnl"],
open_index_px=open_idx if (open_idx is not None and open_idx > 0) else None,
close_index_px=sell_idx,
open_fee=ofee,
close_fee=cfee,
premium_ccy=ccy,
fee_ccy=str(r["premium_ccy"] or ccy or ""),
conn=conn,
bridge_id=r["bridge_id"],
)
if pnl_src == "pending_sell":
status_label = "待卖回"
pnl_u = None
elif pnl_u is not None and pnl_src not in ("none", "pending_sell"):
# 未冻结且可算:只写一次
try:
from lib.options.options_pnl_settle_lib import resolve_spot_vwap_prices
@@ -626,33 +622,18 @@ def sync_options_from_local_trades(
"""
UPDATE options_trades
SET realized_pnl_usdt = ?,
open_index_px = COALESCE(?, open_index_px),
open_index_px = COALESCE(open_index_px, ?),
close_index_px = COALESCE(?, close_index_px),
pnl_settled = 1
WHERE id = ?
WHERE id = ? AND COALESCE(pnl_settled, 0)=0
""",
(pnl_u, opx, cpx, trade_id),
)
except Exception:
pass
else:
status_label = "缺价未核"
pnl_u = None
try:
conn.execute(
"""
UPDATE options_trades
SET realized_pnl_usdt = NULL, pnl_settled = 0
WHERE id = ?
AND (
COALESCE(pnl_settled, 0) = 1
OR realized_pnl_usdt IS NOT NULL
)
""",
(trade_id,),
)
except Exception:
pass
else:
status_label = "缺价未核"
pnl_u = None
# 初始权利金(U)=开仓指数×权利金币量;不含多买
stored_initial = _safe_float(r["initial_premium_usdt"])
prem_u = stored_initial
+52 -4
View File
@@ -6,6 +6,54 @@ import unittest
class ReviewLocalSyncFreezeTests(unittest.TestCase):
def test_settled_true_pnl_not_rewritten_on_resync(self):
"""已冻结真盈亏只读;同步不得因桥/指数再改写."""
from lib.options.options_db import init_options_tables
from lib.options.options_review_lib import sync_options_from_local_trades
from lib.sim.mode_lib import MODE_SIM, TRADING_MODE_KEY
from lib.instance.runtime_settings_lib import ensure_runtime_settings_table, runtime_set
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
init_options_tables(conn)
ensure_runtime_settings_table(conn)
runtime_set(conn, TRADING_MODE_KEY, MODE_SIM)
conn.execute(
"""
INSERT INTO options_trades(
inst_id, underlying, opt_type, sheets, eth_amount,
premium_paid, premium_received, realized_pnl, premium_ccy,
open_fee, close_fee, status, realized_pnl_usdt,
open_index_px, close_index_px, pnl_settled,
created_at, closed_at
) VALUES (
'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01,
0.01, 0.02, 0.01, 'BTC',
0, 0, 'closed', 700.0,
90000.0, 80000.0, 1,
'2026-10-05 05:11:30', '2026-10-07 10:03:53'
)
"""
)
conn.commit()
sync_options_from_local_trades(conn, None)
sync_options_from_local_trades(conn, None)
trade = conn.execute(
"SELECT realized_pnl_usdt, open_index_px, close_index_px, pnl_settled, realized_pnl, premium_paid FROM options_trades WHERE id=1"
).fetchone()
self.assertAlmostEqual(float(trade["realized_pnl_usdt"]), 700.0, places=2)
self.assertAlmostEqual(float(trade["open_index_px"]), 90000.0, places=2)
self.assertAlmostEqual(float(trade["close_index_px"]), 80000.0, places=2)
self.assertEqual(int(trade["pnl_settled"]), 1)
self.assertAlmostEqual(float(trade["realized_pnl"]), 0.01, places=8)
self.assertAlmostEqual(float(trade["premium_paid"]), 0.01, places=8)
row = conn.execute(
"SELECT realized_pnl_total FROM options_review_trades WHERE history_key=?",
("local_opt:1",),
).fetchone()
self.assertAlmostEqual(float(row["realized_pnl_total"]), 700.0, places=2)
conn.close()
def test_premium_usdt_move_formula(self):
from lib.options.options_db import init_options_tables
from lib.options.options_review_lib import sync_options_from_local_trades
@@ -32,9 +80,9 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
) VALUES (
'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01,
0.01, 0.02, 0.01, 'BTC',
0, 0, 'closed', -51.0,
0, 0, 'closed', NULL,
90000.0, 80000.0,
1000.0, 2506.0, 1,
1000.0, 2506.0, 0,
'2026-10-05 05:11:30', '2026-10-07 10:03:53'
)
"""
@@ -65,7 +113,7 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
init_options_tables(conn)
ensure_runtime_settings_table(conn)
runtime_set(conn, TRADING_MODE_KEY, MODE_SIM)
# 无开仓/卖回价 → 无法按新公式更新
# 未冻结且无开仓/卖回价 → 缺价未核(不瞎写)
conn.execute(
"""
INSERT INTO options_trades(
@@ -76,7 +124,7 @@ class ReviewLocalSyncFreezeTests(unittest.TestCase):
) VALUES (
'BTC-USD-261008-86500-P', 'BTC', 'P', 1, 0.01,
0.01, 0.02, 0.01, 'BTC',
'closed', -51.0, 1,
'closed', NULL, 0,
'2026-10-05 05:11:30', '2026-10-07 10:03:53'
)
"""