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