diff --git a/docs/规格-期权交易账户盈亏结算.md b/docs/规格-期权交易账户盈亏结算.md index 43e7661..1a59a46 100644 --- a/docs/规格-期权交易账户盈亏结算.md +++ b/docs/规格-期权交易账户盈亏结算.md @@ -31,35 +31,36 @@ ## 2. 双口径定义 -### 2.1 口径 A — 期权复盘(交易账户) +### 2.1 口径 A — 交易账户对账(内部冻结) ``` 本单已实现盈亏(U) = trading_usdt_after − trading_usdt_before ``` -- **消费者**:期权复盘「交易记录」「复盘记录」、复盘 KPI -- **刻意含多买缓冲 / 滑点 / 手续费**:便于交易账户对账 +- **用途**:交易账户对账、卖回后冻结(`pnl_settled`) +- **刻意含多买缓冲 / 滑点 / 现货侧成本** - **门禁**:全部期权仓位已平 **且** 标的币无可卖量(粉尘可忽略)后才拍 `after` 并冻结 +- **不**作为复盘「期权交易记录」列表盈亏展示 -### 2.2 口径 B — 期权数据统计(历史 × 卖回价) +### 2.2 口径 B — 数据统计 / 复盘交易记录(历史 × 卖回价) ``` -统计已实现盈亏(U) = 期权历史.realized_pnl(币) × 平仓卖回价 +展示已实现盈亏(U) = 期权历史.realized_pnl(币) × 平仓卖回价 ``` - **平仓卖回价**:优先卖回成交均价 / 冻结的 `close_index_px`;缺失时再用当时指数一次性冻结 -- **消费者**:期权面板「数据统计」合计/胜率/盈亏比、数据统计页 KPI、顶栏期权窗口统计 -- **不含**账户多买缓冲差分;与复盘 A **独立**,禁止混用 +- **消费者**:期权面板「数据统计」、顶栏窗口统计、**期权复盘「期权交易记录」盈亏列**(与数据统计同数) +- **不含**账户多买缓冲差分 -### 2.3 初始权利金与盈利率 +### 2.3 初始权利金、盈利率、期权手续费 | 字段 | 定义 | |------|------| -| 初始权利金(U) | `open_index_px × premium_paid`(`premium_paid` 为开仓权利金币量;**不含** `budget_usdt` 多买) | -| 盈利率(复盘) | `realized_pnl_usdt(A) / initial_premium_usdt` | -| 盈利率(统计) | `统计盈亏(B) / initial_premium_usdt`(展示层可算,不强制落库) | +| 初始权利金(U) | `open_index_px × premium_paid`(不含 `budget_usdt` 多买) | +| 盈利率 | `口径B盈亏 / initial_premium_usdt` | +| 期权手续费(U) | 仅期权开/平手续费:`open_fee×开仓指数 + close_fee×卖回价`(**不含**现货买币/卖回手续费) | -复盘列表与复盘详情须展示:初始权利金(U)、盈利率。 +复盘「期权交易记录」须展示:初始权利金(U)、盈亏(B)、盈利率、期权手续费(U)。 --- @@ -261,3 +262,4 @@ IDLE |------|------| | 2026-10-09 | 初版:交易账户快照差分 + 一次一仓 + 持仓禁划转 + 卖回后冻结 | | 2026-10-13 | 修订:双口径 A/B;卖光再结算;初始权利金/盈利率;状态机;历史修复脚本 | +| 2026-10-13 | 复盘「期权交易记录」盈亏与数据统计同口径 B;增加仅期权手续费展示 | diff --git a/lib/common/static/options_review.js b/lib/common/static/options_review.js index 67df14b..3acf341 100644 --- a/lib/common/static/options_review.js +++ b/lib/common/static/options_review.js @@ -59,6 +59,13 @@ return n.toFixed(2) + "U"; } + function fmtOptionFee(v) { + if (v == null || v === "") return "—"; + var n = Number(v); + if (Number.isNaN(n)) return "—"; + return n.toFixed(2) + "U"; + } + function fmtProfitRate(v) { if (v == null || v === "") return "—"; var n = Number(v); @@ -398,7 +405,7 @@ if (!tbody) return; var wrap = beginListLoad("or-trades-wrap", soft); if (!soft) { - tbody.innerHTML = '加载中…'; + tbody.innerHTML = '加载中…'; } var p = baseQs(); // 交易记录保留已复盘条目,不再只显示待复盘 @@ -412,7 +419,7 @@ .then(function (data) { if (doSync) setSyncStatus("本地记录已加载"); if (!data.ok) { - tbody.innerHTML = '加载失败'; + tbody.innerHTML = '加载失败'; endListLoad(wrap); return; } @@ -424,7 +431,7 @@ tradesCache = {}; if (!rows.length) { tbody.innerHTML = - '暂无交易记录'; + '暂无交易记录'; endListLoad(wrap); return; } @@ -471,6 +478,9 @@ '">' + fmtProfitRate(pr) + "" + + "" + + fmtOptionFee(t.option_fee_usdt) + + "" + '' + escapeHtml(t.opened_at || "—") + "" + @@ -508,7 +518,7 @@ }) .catch(function () { if (doSync) setSyncStatus("加载失败"); - tbody.innerHTML = '加载失败'; + tbody.innerHTML = '加载失败'; endListLoad(wrap); }); } @@ -885,6 +895,7 @@ ["初始权利金", fmtPremiumU(t.initial_premium_usdt)], ["盈亏", fmtPnl(t.realized_pnl_total)], ["盈利率", fmtProfitRate(resolveProfitRate(t))], + ["期权手续费", fmtOptionFee(t.option_fee_usdt)], ["持有", fmtHold(t.hold_seconds)], ["开仓时间", t.opened_at || "—"], ["平仓时间", t.closed_at || "—"], @@ -1226,6 +1237,7 @@ ($("or-f-pnl") || {}).value = fmtPnl(t.realized_pnl_total); ($("or-f-initial-prem") || {}).value = fmtPremiumU(t.initial_premium_usdt); ($("or-f-profit-rate") || {}).value = fmtProfitRate(resolveProfitRate(t)); + ($("or-f-option-fee") || {}).value = fmtOptionFee(t.option_fee_usdt); ($("or-f-hold") || {}).value = fmtHold(t.hold_seconds); setSelectValue($("or-f-strategy"), e.strategy_tag || ""); setSelectValue($("or-f-direction"), e.direction_view || autoDirection(t)); diff --git a/lib/options/options_review_db.py b/lib/options/options_review_db.py index bf19a0d..345ab06 100644 --- a/lib/options/options_review_db.py +++ b/lib/options/options_review_db.py @@ -133,6 +133,8 @@ def init_options_review_tables(conn: sqlite3.Connection) -> None: _ensure_column(conn, "options_review_trades", "profit_rr", "REAL") _ensure_column(conn, "options_review_trades", "initial_premium_usdt", "REAL") _ensure_column(conn, "options_review_trades", "profit_rate", "REAL") + # 仅期权开/平手续费折 U(不含现货买币/卖回) + _ensure_column(conn, "options_review_trades", "option_fee_usdt", "REAL") _ensure_column(conn, "options_review_entries", "prev_entry_tf", "TEXT") diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index e94d2e0..4f5669f 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -148,6 +148,7 @@ def upsert_option_history_row(conn: sqlite3.Connection, row: dict[str, Any]) -> "realized_pnl": pnl, "initial_premium_usdt": _safe_float(row.get("initial_premium_usdt")), "profit_rate": _safe_float(row.get("profit_rate")), + "option_fee_usdt": _safe_float(row.get("option_fee_usdt")), } cols = list(fields.keys()) if existing: @@ -319,7 +320,8 @@ def sync_options_from_local_trades( open_quote, close_quote, premium_paid, premium_received, realized_pnl, realized_pnl_usdt, close_index_px, open_index_px, budget_usdt, premium_ccy, created_at, closed_at, signal_note, status, - trading_usdt_before, pnl_settled, initial_premium_usdt + trading_usdt_before, pnl_settled, initial_premium_usdt, + open_fee, close_fee FROM options_trades WHERE status = 'closed' ORDER BY id DESC @@ -411,16 +413,9 @@ def sync_options_from_local_trades( ccy = str(r["premium_ccy"] or "").strip().upper() 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 - # 账户快照路径:未结算不写 U,避免指数假数进复盘 - acct_pending = _safe_float(r["trading_usdt_before"]) is not None and not settled close_idx = _safe_float(r["close_index_px"]) open_idx = _safe_float(r["open_index_px"]) - # 折算指数:平仓冻结 > 开仓 > 实时(仅无冻结时兜底) + # 折算指数:平仓卖回价 > 开仓 > 实时(仅无冻结时兜底) idx = None if close_idx is not None and close_idx > 0: idx = close_idx @@ -435,22 +430,36 @@ def sync_options_from_local_trades( if paid0 is not None and recv0 is not None: pnl = recv0 - paid0 prem = _safe_float(r["premium_paid"]) - if acct_pending: - pnl_u = None - elif frozen_usdt is not None and (settled or _safe_float(r["trading_usdt_before"]) is None): - pnl_u = round(float(frozen_usdt), 4) - else: - pnl_u = _to_usdt(pnl, ccy=ccy, idx=idx) + # 期权交易记录盈亏与数据统计同口径 B:币盈亏 × 平仓卖回价(不用账户差分 A) + pnl_u = _to_usdt(pnl, ccy=ccy, idx=idx) # 初始权利金(U)=开仓指数×权利金币量;不含多买 stored_initial = _safe_float(r["initial_premium_usdt"]) open_idx_for_prem = open_idx if (open_idx is not None and open_idx > 0) else idx prem_u = stored_initial if prem_u is None: prem_u = _to_usdt(prem, ccy=ccy, idx=open_idx_for_prem) - if ccy in ("ETH", "BTC") and pnl_u is None and not acct_pending: - pnl_u = pnl + if ccy in ("ETH", "BTC") and pnl_u is None: + pnl_u = None if ccy in ("ETH", "BTC") and prem_u is None: prem_u = prem + # 仅期权开/平手续费折 U(不含现货) + ofee = abs(_safe_float(r["open_fee"]) or 0.0) + cfee = abs(_safe_float(r["close_fee"]) or 0.0) + fee_u = None + if ccy in ("ETH", "BTC"): + fee_parts = 0.0 + has_fee = False + if ofee > 0 and open_idx_for_prem is not None and open_idx_for_prem > 0: + fee_parts += ofee * float(open_idx_for_prem) + has_fee = True + sell_idx = close_idx if (close_idx is not None and close_idx > 0) else idx + if cfee > 0 and sell_idx is not None and sell_idx > 0: + fee_parts += cfee * float(sell_idx) + has_fee = True + if has_fee: + fee_u = round(fee_parts, 4) + elif ofee > 0 or cfee > 0: + fee_u = round(ofee + cfee, 4) profit_rate = None if pnl_u is not None and prem_u is not None and abs(float(prem_u)) > 1e-12: profit_rate = round(float(pnl_u) / float(prem_u), 6) @@ -470,12 +479,13 @@ def sync_options_from_local_trades( "open_avg_px": r["open_quote"], "close_avg_px": r["close_quote"], "premium_paid": prem_u if prem_u is not None else prem, - "realized_pnl": pnl_u if pnl_u is not None else (None if acct_pending else pnl), + "realized_pnl": pnl_u, "initial_premium_usdt": prem_u, "profit_rate": profit_rate, + "option_fee_usdt": fee_u, "created_at": opened_at, "closed_at": closed_at, - "status_label": "待结算" if acct_pending else "已平", + "status_label": "已平", }, ) if action == "inserted": diff --git a/lib/options/templates/options_review_panel.html b/lib/options/templates/options_review_panel.html index 55a7b49..7d56e14 100644 --- a/lib/options/templates/options_review_panel.html +++ b/lib/options/templates/options_review_panel.html @@ -77,7 +77,7 @@ .or-list-loading{opacity:.55;pointer-events:none;transition:opacity .12s ease} .or-trades-table-wrap,.or-reviewed-table-wrap{min-height:9.5rem;overflow-x:auto} .or-reviewed-table{min-width:1180px} - .or-trades-table{min-width:960px} + .or-trades-table{min-width:1080px} .or-kpi-row{ display:grid;grid-template-columns:repeat(6,minmax(0,1fr)); gap:8px;margin-bottom:12px; @@ -241,6 +241,7 @@ 初始权利金 盈亏 盈利率 + 手续费 开仓时间 平仓时间 持有 @@ -248,7 +249,7 @@ - 加载中… + 加载中… @@ -285,6 +286,7 @@ autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other"> +