fix多笔买/卖回用VWAP计入真盈亏,卖完再结算并标记待卖回。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -32,7 +32,7 @@ USDT(交易户)
|
||||
▼
|
||||
卖光可卖标的币(禁止按开仓权利金封顶)→ 快照 trading_usdt_after
|
||||
▼
|
||||
口径 A(复盘真盈亏): 币盈亏×卖回价 − 权利金×(开仓价−卖回价);含权利金U波动,忽略多买
|
||||
口径 A(复盘真盈亏): 币盈亏×卖回VWAP − 权利金×(买VWAP−卖回VWAP);卖完再结算
|
||||
口径 B(期权历史/数据统计): 仅期权腿币盈亏(×卖回价折 U);不含现货/权利金U波动修正
|
||||
```
|
||||
|
||||
|
||||
+12
-1
@@ -42,8 +42,18 @@
|
||||
含义:**计入权利金的 USDT 波动**;**忽略现货多买缓冲**。
|
||||
有开/平手续费时:开仓费按开仓价、平仓费按卖回价折 U 后扣除。
|
||||
|
||||
**多笔成交(VWAP):**
|
||||
|
||||
| 价格 | 定义 |
|
||||
|------|------|
|
||||
| 开仓价 | 买币多笔加权:`Σ买币花费USDT / Σ买入币量`(缓冲买单计入均价,公式仍只乘 `premium_paid`) |
|
||||
| 卖回价 | 平仓后卖回多笔加权:`Σ收回USDT / Σ卖出币量`(**不含**开仓多余卖回) |
|
||||
|
||||
- 卖回未完成(仍有可卖)→ 复盘标「待卖回」,不冻结真盈亏
|
||||
- 无 VWAP 时回退冻结指数;仍缺则「缺价未核」
|
||||
|
||||
- **消费者**:期权复盘「期权交易记录」盈亏列、复盘 KPI、`realized_pnl_usdt` 冻结
|
||||
- **不含**:现货多买 / 账户 after−before / 桥 `usdt_recovered−budget`
|
||||
- **不含**:现货多买差分当盈亏 / 账户 after−before
|
||||
- 账户 `trading_usdt_before/after` 仅审计用,**不**作复盘真盈亏
|
||||
|
||||
### 2.2 口径 B — 期权历史 / 数据统计(仅期权腿)
|
||||
@@ -271,3 +281,4 @@ IDLE
|
||||
| 2026-10-13 | 交易记录改回真盈亏 A;期权历史/统计仅期权腿 B,不含现货 |
|
||||
| 2026-10-13 | 真盈亏明确为卖光收回−买币;复盘禁止指数折 U 冒充;桥可回退 |
|
||||
| 2026-10-13 | 真盈亏改回权利金 USDT 波动式:币盈亏×卖回价−权利金×(开−平);忽略多买 |
|
||||
| 2026-10-13 | 多笔买/卖回用 VWAP;卖完再结算;待卖回/缺价未核标记 |
|
||||
|
||||
@@ -451,11 +451,17 @@
|
||||
var badgeExtra = reviewed
|
||||
? ' <span class="or-badge" style="background:rgba(61,214,140,.2)">已复盘</span>'
|
||||
: "";
|
||||
var needsMark = String(t.status_raw || "") === "缺价未核";
|
||||
var stRaw = String(t.status_raw || "");
|
||||
var needsMark = stRaw === "缺价未核";
|
||||
var pendingSell = stRaw === "待卖回";
|
||||
if (needsMark) {
|
||||
badgeExtra +=
|
||||
' <span class="or-badge" style="background:rgba(255,170,60,.28)" title="缺开仓价或卖回价,未能按新真盈亏公式更新">缺价未核</span>';
|
||||
}
|
||||
if (pendingSell) {
|
||||
badgeExtra +=
|
||||
' <span class="or-badge" style="background:rgba(120,180,255,.28)" title="平仓卖回未完成(可能多笔),完成后按卖回VWAP结算真盈亏">待卖回</span>';
|
||||
}
|
||||
return (
|
||||
'<tr class="or-trade-row' +
|
||||
active +
|
||||
@@ -476,7 +482,11 @@
|
||||
'<td class="' +
|
||||
pnlClass(t.realized_pnl_total) +
|
||||
'">' +
|
||||
(needsMark ? '<span class="muted" title="未能按新公式更新">—</span>' : fmtPnl(t.realized_pnl_total)) +
|
||||
(needsMark || pendingSell
|
||||
? '<span class="muted" title="' +
|
||||
(pendingSell ? "卖回未完成" : "未能按新公式更新") +
|
||||
'">—</span>'
|
||||
: fmtPnl(t.realized_pnl_total)) +
|
||||
"</td>" +
|
||||
'<td class="' +
|
||||
pnlClass(pr) +
|
||||
|
||||
@@ -263,10 +263,13 @@ def settle_closed_trades_from_trading_account(
|
||||
params.append(int(bridge_id))
|
||||
else:
|
||||
return 0
|
||||
from lib.options.options_pnl_settle_lib import resolve_spot_vwap_prices
|
||||
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT id, trading_usdt_before, premium_paid, premium_received, realized_pnl,
|
||||
open_index_px, close_index_px, open_fee, close_fee, premium_ccy, fee_ccy
|
||||
open_index_px, close_index_px, open_fee, close_fee, premium_ccy, fee_ccy,
|
||||
bridge_id
|
||||
FROM options_trades
|
||||
WHERE {" AND ".join(clauses)}
|
||||
""",
|
||||
@@ -280,30 +283,42 @@ def settle_closed_trades_from_trading_account(
|
||||
)
|
||||
after_use = after
|
||||
if after_use is None and before is not None:
|
||||
# 无 after 读数时不编造;仍可用权利金公式冻结
|
||||
after_use = None
|
||||
row_bid = row["bridge_id"] if hasattr(row, "keys") else row[11]
|
||||
if row_bid is None and bridge_id is not None:
|
||||
row_bid = bridge_id
|
||||
open_px, close_px, _gate = resolve_spot_vwap_prices(
|
||||
conn,
|
||||
bridge_id=row_bid,
|
||||
open_index_px=row["open_index_px"] if hasattr(row, "keys") else row[5],
|
||||
close_index_px=row["close_index_px"] if hasattr(row, "keys") else row[6],
|
||||
)
|
||||
pnl_u, src = resolve_true_pnl_usdt_from_trade(
|
||||
premium_paid=row["premium_paid"] if hasattr(row, "keys") else row[2],
|
||||
premium_received=row["premium_received"] if hasattr(row, "keys") else row[3],
|
||||
realized_pnl=row["realized_pnl"] if hasattr(row, "keys") else row[4],
|
||||
open_index_px=row["open_index_px"] if hasattr(row, "keys") else row[5],
|
||||
close_index_px=row["close_index_px"] if hasattr(row, "keys") else row[6],
|
||||
open_index_px=open_px,
|
||||
close_index_px=close_px,
|
||||
open_fee=row["open_fee"] if hasattr(row, "keys") else row[7],
|
||||
close_fee=row["close_fee"] if hasattr(row, "keys") else row[8],
|
||||
premium_ccy=row["premium_ccy"] if hasattr(row, "keys") else row[9],
|
||||
fee_ccy=row["fee_ccy"] if hasattr(row, "keys") else row[10],
|
||||
conn=conn,
|
||||
bridge_id=row_bid,
|
||||
)
|
||||
if pnl_u is None or src == "none":
|
||||
if pnl_u is None or src in ("none", "pending_sell"):
|
||||
continue
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_trades
|
||||
SET trading_usdt_after = COALESCE(?, trading_usdt_after),
|
||||
realized_pnl_usdt = ?,
|
||||
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
|
||||
""",
|
||||
(after_use, pnl_u, tid),
|
||||
(after_use, pnl_u, open_px, close_px, tid),
|
||||
)
|
||||
updated += 1
|
||||
return updated
|
||||
|
||||
@@ -306,6 +306,20 @@ def open_coin_option_buy_full(
|
||||
"budget": budget_info,
|
||||
}
|
||||
_spot_bridge.update_bridge(conn, bridge_id, coin_bought=float(coin_bought))
|
||||
# 买侧 VWAP 累加(多笔买同理);USDT 优先成交回报,否则用计划买额
|
||||
try:
|
||||
usdt_spent = float(buy.get("usdt_spent") or buy_usdt)
|
||||
except (TypeError, ValueError):
|
||||
usdt_spent = float(buy_usdt)
|
||||
try:
|
||||
_spot_bridge.add_bridge_buy_fill(
|
||||
conn,
|
||||
bridge_id,
|
||||
usdt_spent=usdt_spent,
|
||||
coin_bought=float(coin_bought),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
conn.commit()
|
||||
except Exception:
|
||||
|
||||
@@ -202,6 +202,62 @@ def true_pnl_usdt_premium_move(
|
||||
return round(float(pnl) * float(close_px) - prem_move, 4)
|
||||
|
||||
|
||||
def resolve_spot_vwap_prices(
|
||||
conn: Any = None,
|
||||
*,
|
||||
bridge_id: Any = None,
|
||||
open_index_px: Any = None,
|
||||
close_index_px: Any = None,
|
||||
) -> tuple[float | None, float | None, str]:
|
||||
"""开仓价/卖回价:优先桥上多笔 VWAP,否则冻结指数.
|
||||
|
||||
返回 (open_px, close_px, gate):
|
||||
gate='' 可算; 'pending_sell' 卖回未完成; 'missing' 缺价
|
||||
"""
|
||||
open_px = _safe_float(open_index_px)
|
||||
close_px = _safe_float(close_index_px)
|
||||
bid = None
|
||||
try:
|
||||
if bridge_id is not None and str(bridge_id).strip() != "":
|
||||
bid = int(bridge_id)
|
||||
except (TypeError, ValueError):
|
||||
bid = None
|
||||
if conn is not None and bid is not None:
|
||||
try:
|
||||
from lib.options.options_spot_bridge_lib import (
|
||||
BRIDGE_CLOSED,
|
||||
BRIDGE_PENDING_SELL,
|
||||
bridge_buy_vwap,
|
||||
bridge_sell_vwap,
|
||||
ensure_bridge_table,
|
||||
)
|
||||
|
||||
ensure_bridge_table(conn)
|
||||
bv = bridge_buy_vwap(conn, bid)
|
||||
if bv is not None and bv > 0:
|
||||
open_px = bv
|
||||
row = conn.execute(
|
||||
"SELECT status FROM options_spot_bridge WHERE id=?",
|
||||
(bid,),
|
||||
).fetchone()
|
||||
st = str(
|
||||
row["status"] if row and hasattr(row, "keys") else (row[0] if row else "") or ""
|
||||
)
|
||||
if st == BRIDGE_PENDING_SELL:
|
||||
return open_px, None, "pending_sell"
|
||||
sv = bridge_sell_vwap(conn, bid)
|
||||
if sv is not None and sv > 0:
|
||||
close_px = sv
|
||||
elif st == BRIDGE_CLOSED and (close_px is None or close_px <= 0):
|
||||
# 已关桥但无 VWAP/冻结卖回价
|
||||
return open_px, None, "missing"
|
||||
except Exception:
|
||||
pass
|
||||
if open_px is None or open_px <= 0 or close_px is None or close_px <= 0:
|
||||
return open_px, close_px, "missing"
|
||||
return open_px, close_px, ""
|
||||
|
||||
|
||||
def resolve_true_pnl_usdt_from_trade(
|
||||
*,
|
||||
premium_paid: Any = None,
|
||||
@@ -213,10 +269,12 @@ def resolve_true_pnl_usdt_from_trade(
|
||||
close_fee: Any = 0,
|
||||
premium_ccy: str | None = None,
|
||||
fee_ccy: str | None = None,
|
||||
conn: Any = None,
|
||||
bridge_id: Any = None,
|
||||
) -> tuple[float | None, str]:
|
||||
"""由成交字段算复盘真盈亏;不含现货多买/账户差分.
|
||||
"""由成交字段算复盘真盈亏;开/卖回价优先多笔 VWAP.
|
||||
|
||||
返回 (pnl_u, source): premium_move | stable | none
|
||||
返回 (pnl_u, source): premium_move | stable | pending_sell | none
|
||||
"""
|
||||
del fee_ccy # 费率折 U 已体现在历史币盈亏里;本函数不重扣
|
||||
pc = str(premium_ccy or "").strip().upper()
|
||||
@@ -233,8 +291,14 @@ def resolve_true_pnl_usdt_from_trade(
|
||||
return round(float(coin), 4), "stable"
|
||||
return None, "none"
|
||||
|
||||
open_px = _safe_float(open_index_px)
|
||||
close_px = _safe_float(close_index_px)
|
||||
open_px, close_px, gate = resolve_spot_vwap_prices(
|
||||
conn,
|
||||
bridge_id=bridge_id,
|
||||
open_index_px=open_index_px,
|
||||
close_index_px=close_index_px,
|
||||
)
|
||||
if gate == "pending_sell":
|
||||
return None, "pending_sell"
|
||||
# 优先严格按用户式:历史币盈亏×卖回价 − 权利金×(开仓价−卖回价)
|
||||
usdt = true_pnl_usdt_premium_move(
|
||||
coin_pnl=coin,
|
||||
|
||||
@@ -591,19 +591,35 @@ def sync_options_from_local_trades(
|
||||
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_u is not None and pnl_src != "none":
|
||||
if pnl_src == "pending_sell":
|
||||
status_label = "待卖回"
|
||||
pnl_u = None
|
||||
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:
|
||||
try:
|
||||
from lib.options.options_pnl_settle_lib import resolve_spot_vwap_prices
|
||||
|
||||
opx, cpx, _g = resolve_spot_vwap_prices(
|
||||
conn,
|
||||
bridge_id=r["bridge_id"],
|
||||
open_index_px=open_idx,
|
||||
close_index_px=sell_idx,
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_trades
|
||||
SET realized_pnl_usdt = ?, pnl_settled = 1
|
||||
SET realized_pnl_usdt = ?,
|
||||
open_index_px = COALESCE(?, open_index_px),
|
||||
close_index_px = COALESCE(?, close_index_px),
|
||||
pnl_settled = 1
|
||||
WHERE id = ?
|
||||
""",
|
||||
(pnl_u, trade_id),
|
||||
(pnl_u, opx, cpx, trade_id),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -96,6 +96,153 @@ def ensure_bridge_table(conn: sqlite3.Connection) -> None:
|
||||
ON options_spot_bridge(status)
|
||||
"""
|
||||
)
|
||||
# 多笔买卖 VWAP:买侧累加(含缓冲);平仓后卖回单独累加(不含开仓多余卖回)
|
||||
for ddl in (
|
||||
"ALTER TABLE options_spot_bridge ADD COLUMN buy_usdt_spent REAL",
|
||||
"ALTER TABLE options_spot_bridge ADD COLUMN buy_coin_accum REAL",
|
||||
"ALTER TABLE options_spot_bridge ADD COLUMN close_sell_coin REAL",
|
||||
"ALTER TABLE options_spot_bridge ADD COLUMN close_sell_usdt REAL",
|
||||
):
|
||||
try:
|
||||
conn.execute(ddl)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def add_bridge_buy_fill(
|
||||
conn: sqlite3.Connection,
|
||||
bridge_id: int,
|
||||
*,
|
||||
usdt_spent: float | None,
|
||||
coin_bought: float | None,
|
||||
) -> None:
|
||||
"""累加开仓买币成交,供买侧 VWAP=ΣUSDT/Σ币."""
|
||||
ensure_bridge_table(conn)
|
||||
u = _safe_float(usdt_spent)
|
||||
c = _safe_float(coin_bought)
|
||||
if bridge_id is None or u is None or c is None or u <= 0 or c <= 0:
|
||||
return
|
||||
row = conn.execute(
|
||||
"SELECT buy_usdt_spent, buy_coin_accum FROM options_spot_bridge WHERE id=?",
|
||||
(int(bridge_id),),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return
|
||||
prev_u = _safe_float(row["buy_usdt_spent"] if hasattr(row, "keys") else row[0]) or 0.0
|
||||
prev_c = _safe_float(row["buy_coin_accum"] if hasattr(row, "keys") else row[1]) or 0.0
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_spot_bridge
|
||||
SET buy_usdt_spent = ?, buy_coin_accum = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""",
|
||||
(round(prev_u + float(u), 8), round(prev_c + float(c), 8), int(bridge_id)),
|
||||
)
|
||||
|
||||
|
||||
def add_bridge_close_sell_fill(
|
||||
conn: sqlite3.Connection,
|
||||
bridge_id: int,
|
||||
*,
|
||||
usdt_recovered: float | None,
|
||||
coin_sold: float | None,
|
||||
) -> None:
|
||||
"""累加平仓后卖回成交(多笔),供卖回 VWAP=ΣUSDT/Σ币;同时累加 usdt_recovered."""
|
||||
ensure_bridge_table(conn)
|
||||
u = _safe_float(usdt_recovered)
|
||||
c = _safe_float(coin_sold)
|
||||
if bridge_id is None:
|
||||
return
|
||||
if u is not None and u > 0:
|
||||
add_bridge_usdt_recovered(conn, int(bridge_id), u)
|
||||
if (u is None or u <= 0) and (c is None or c <= 0):
|
||||
return
|
||||
row = conn.execute(
|
||||
"""
|
||||
SELECT close_sell_usdt, close_sell_coin, coin_sold
|
||||
FROM options_spot_bridge WHERE id=?
|
||||
""",
|
||||
(int(bridge_id),),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return
|
||||
prev_u = _safe_float(row["close_sell_usdt"] if hasattr(row, "keys") else row[0]) or 0.0
|
||||
prev_c = _safe_float(row["close_sell_coin"] if hasattr(row, "keys") else row[1]) or 0.0
|
||||
prev_sold = _safe_float(row["coin_sold"] if hasattr(row, "keys") else row[2]) or 0.0
|
||||
new_u = prev_u + (float(u) if u is not None and u > 0 else 0.0)
|
||||
new_c = prev_c + (float(c) if c is not None and c > 0 else 0.0)
|
||||
new_sold = prev_sold + (float(c) if c is not None and c > 0 else 0.0)
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_spot_bridge
|
||||
SET close_sell_usdt = ?, close_sell_coin = ?, coin_sold = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""",
|
||||
(round(new_u, 8), round(new_c, 8), round(new_sold, 8), int(bridge_id)),
|
||||
)
|
||||
|
||||
|
||||
def bridge_buy_vwap(conn: sqlite3.Connection, bridge_id: int | None) -> float | None:
|
||||
"""开仓买币 VWAP(USDT/币);多笔加权.忽略后续把 coin_bought 改成权利金量."""
|
||||
if bridge_id is None:
|
||||
return None
|
||||
ensure_bridge_table(conn)
|
||||
row = conn.execute(
|
||||
"SELECT buy_usdt_spent, buy_coin_accum, budget_usdt, coin_bought FROM options_spot_bridge WHERE id=?",
|
||||
(int(bridge_id),),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
spent = _safe_float(row["buy_usdt_spent"] if hasattr(row, "keys") else row[0])
|
||||
coins = _safe_float(row["buy_coin_accum"] if hasattr(row, "keys") else row[1])
|
||||
if spent is not None and coins is not None and spent > 0 and coins > 0:
|
||||
return round(float(spent) / float(coins), 8)
|
||||
# 旧桥无累加字段:用预算/买入量近似单笔
|
||||
budget = _safe_float(row["budget_usdt"] if hasattr(row, "keys") else row[2])
|
||||
bought = _safe_float(row["coin_bought"] if hasattr(row, "keys") else row[3])
|
||||
if budget is not None and bought is not None and budget > 0 and bought > 0:
|
||||
return round(float(budget) / float(bought), 8)
|
||||
return None
|
||||
|
||||
|
||||
def bridge_sell_vwap(conn: sqlite3.Connection, bridge_id: int | None) -> float | None:
|
||||
"""平仓后卖回 VWAP(USDT/币);仅 close_sell_* 多笔加权,不含开仓多余卖回."""
|
||||
if bridge_id is None:
|
||||
return None
|
||||
ensure_bridge_table(conn)
|
||||
row = conn.execute(
|
||||
"SELECT close_sell_usdt, close_sell_coin, usdt_recovered, coin_sold, status FROM options_spot_bridge WHERE id=?",
|
||||
(int(bridge_id),),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
u = _safe_float(row["close_sell_usdt"] if hasattr(row, "keys") else row[0])
|
||||
c = _safe_float(row["close_sell_coin"] if hasattr(row, "keys") else row[1])
|
||||
if u is not None and c is not None and u > 0 and c > 0:
|
||||
return round(float(u) / float(c), 8)
|
||||
# 旧桥无分拆字段且已关桥:用总收回/总卖出近似(可能含开仓多余卖回)
|
||||
st = str(row["status"] if hasattr(row, "keys") else row[4] or "")
|
||||
if st == BRIDGE_CLOSED:
|
||||
ru = _safe_float(row["usdt_recovered"] if hasattr(row, "keys") else row[2])
|
||||
sc = _safe_float(row["coin_sold"] if hasattr(row, "keys") else row[3])
|
||||
if ru is not None and sc is not None and ru > 0 and sc > 0:
|
||||
return round(float(ru) / float(sc), 8)
|
||||
return None
|
||||
|
||||
|
||||
def bridge_sell_complete(conn: sqlite3.Connection, bridge_id: int | None) -> bool:
|
||||
"""平仓卖回是否已完成(桥已关闭)."""
|
||||
if bridge_id is None:
|
||||
return False
|
||||
ensure_bridge_table(conn)
|
||||
row = conn.execute(
|
||||
"SELECT status FROM options_spot_bridge WHERE id=?",
|
||||
(int(bridge_id),),
|
||||
).fetchone()
|
||||
if not row:
|
||||
return False
|
||||
return str(row["status"] if hasattr(row, "keys") else row[0] or "") == BRIDGE_CLOSED
|
||||
|
||||
|
||||
def list_open_bridges(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
||||
@@ -546,11 +693,37 @@ def sell_residual_after_option_flat(
|
||||
|
||||
# 一次一仓空仓后卖光可用(含盈利币);禁止用 coin_bought/权利金封顶
|
||||
# 此处无业务连接:模拟盘 convert 可安全另开写钱包
|
||||
usdt_before_sell = None
|
||||
try:
|
||||
from lib.options.options_account_pnl_lib import fetch_trading_usdt
|
||||
|
||||
usdt_before_sell = fetch_trading_usdt({"fetch_options_balances": None}, ex)
|
||||
except Exception:
|
||||
usdt_before_sell = None
|
||||
sell = spot_market_sell_coin_to_usdt(
|
||||
ex,
|
||||
underlying=underlying,
|
||||
coin_amount=None,
|
||||
)
|
||||
# 实盘订单回报常无 usdt_recovered:用交易账户 USDT 增量补
|
||||
if sell.get("ok") and _safe_float(sell.get("usdt_recovered")) is None:
|
||||
try:
|
||||
from lib.exchange.okx_options_lib import invalidate_options_balance_cache
|
||||
from lib.options.options_account_pnl_lib import fetch_trading_usdt
|
||||
|
||||
invalidate_options_balance_cache()
|
||||
usdt_after_sell = fetch_trading_usdt({"fetch_options_balances": None}, ex)
|
||||
if (
|
||||
usdt_before_sell is not None
|
||||
and usdt_after_sell is not None
|
||||
and float(usdt_after_sell) > float(usdt_before_sell)
|
||||
):
|
||||
sell = dict(sell)
|
||||
sell["usdt_recovered"] = round(
|
||||
float(usdt_after_sell) - float(usdt_before_sell), 8
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
if bid is None:
|
||||
if not sell.get("ok"):
|
||||
msg = str(sell.get("msg") or "")
|
||||
@@ -609,8 +782,14 @@ def sell_residual_after_option_flat(
|
||||
"sell_complete": False,
|
||||
}
|
||||
recovered = _safe_float(sell.get("usdt_recovered"))
|
||||
if recovered is not None and recovered > 0:
|
||||
add_bridge_usdt_recovered(c, bid, recovered)
|
||||
sold_amt = _safe_float(sell.get("coin_sold"))
|
||||
# 平仓后卖回(可多笔):累加 close_sell_* 供卖回 VWAP;开仓多余卖回不走这里
|
||||
add_bridge_close_sell_fill(
|
||||
c,
|
||||
bid,
|
||||
usdt_recovered=recovered,
|
||||
coin_sold=sold_amt,
|
||||
)
|
||||
# 卖后仍有可卖 → 不关桥、不结算(盈利币未卖回时曾误冻亏损)
|
||||
try:
|
||||
left = fetch_trading_coin_available(ex, underlying)
|
||||
@@ -623,7 +802,6 @@ def sell_residual_after_option_flat(
|
||||
bid,
|
||||
status=BRIDGE_PENDING_SELL,
|
||||
sell_ord_id=str(sell.get("ord_id") or ""),
|
||||
coin_sold=_safe_float(sell.get("coin_sold")),
|
||||
inst_id=inst_id,
|
||||
message=left_msg,
|
||||
)
|
||||
@@ -641,7 +819,6 @@ def sell_residual_after_option_flat(
|
||||
bid,
|
||||
status=BRIDGE_CLOSED,
|
||||
sell_ord_id=str(sell.get("ord_id") or ""),
|
||||
coin_sold=_safe_float(sell.get("coin_sold")),
|
||||
inst_id=inst_id,
|
||||
message="期权已平,币已卖回 USDT",
|
||||
close=True,
|
||||
@@ -652,6 +829,7 @@ def sell_residual_after_option_flat(
|
||||
"bridge_id": bid,
|
||||
"bridge_status": BRIDGE_CLOSED,
|
||||
"sell_complete": True,
|
||||
"sell_vwap": bridge_sell_vwap(c, bid),
|
||||
}
|
||||
|
||||
if get_db is not None:
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
<span class="or-step" aria-hidden="true">1</span>
|
||||
<div>
|
||||
<div class="or-section-title" id="or-list-title">期权交易记录</div>
|
||||
<p class="or-section-desc">真盈亏=历史币盈亏×卖回价 − 权利金×(开仓价−卖回价)。缺开仓/卖回价的旧单标记「缺价未核」,不沿用旧冻结。期权历史仅记币收益。</p>
|
||||
<p class="or-section-desc">真盈亏=历史币盈亏×卖回价 − 权利金×(开仓价−卖回价);开/卖回价为多笔 VWAP。卖回未完标「待卖回」,缺价标「缺价未核」。期权历史仅记币收益。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-strike-table-wrap or-trades-table-wrap" id="or-trades-wrap">
|
||||
|
||||
@@ -91,8 +91,7 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]:
|
||||
elif coin_pnl is not None and open_idx is not None and open_idx > 0:
|
||||
stats_u = round(coin_pnl * open_idx, 4)
|
||||
|
||||
# 口径 A:权利金 USDT 波动式(忽略现货多买/账户差分)
|
||||
# 真盈亏必须同时有开仓价与卖回价;缺一则标记缺价未核(不用开仓价冒充卖回价)
|
||||
# 口径 A:权利金 USDT 波动式;开/卖回价优先桥多笔 VWAP
|
||||
true_a, src = resolve_true_pnl_usdt_from_trade(
|
||||
premium_paid=paid,
|
||||
premium_received=recv,
|
||||
@@ -102,6 +101,8 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]:
|
||||
open_fee=r["open_fee"],
|
||||
close_fee=r["close_fee"],
|
||||
premium_ccy=ccy,
|
||||
conn=conn,
|
||||
bridge_id=r["bridge_id"],
|
||||
)
|
||||
|
||||
action = "ok"
|
||||
@@ -109,7 +110,12 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]:
|
||||
new_pnl_settled = 1 if settled else 0
|
||||
|
||||
status_label = "已平"
|
||||
if true_a is not None and src != "none":
|
||||
if src == "pending_sell":
|
||||
action = "mark_pending_sell"
|
||||
new_pnl_settled = 0
|
||||
new_settled_u = None
|
||||
status_label = "待卖回"
|
||||
elif true_a is not None and src != "none":
|
||||
if settled_u is None or abs(float(settled_u) - float(true_a)) > 0.05:
|
||||
action = "rewrite_A_premium_move"
|
||||
new_settled_u = round(float(true_a), 4)
|
||||
@@ -119,7 +125,6 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]:
|
||||
new_settled_u = round(float(true_a), 4)
|
||||
new_pnl_settled = 1
|
||||
else:
|
||||
# 缺开仓价/卖回价/币盈亏等:无法按新公式更新 → 标记「缺价未核」
|
||||
action = "mark_uncomputable_A"
|
||||
new_pnl_settled = 0
|
||||
new_settled_u = None
|
||||
@@ -158,7 +163,7 @@ def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]:
|
||||
"UPDATE options_trades SET initial_premium_usdt=? WHERE id=?",
|
||||
(new_initial, tid),
|
||||
)
|
||||
if action == "mark_uncomputable_A":
|
||||
if action in ("mark_uncomputable_A", "mark_pending_sell"):
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE options_trades
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""多笔买/卖回 VWAP 与真盈亏."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import unittest
|
||||
|
||||
|
||||
class SpotVwapBridgeTests(unittest.TestCase):
|
||||
def test_buy_and_close_sell_vwap_multi_fill(self):
|
||||
from lib.options.options_spot_bridge_lib import (
|
||||
BRIDGE_CLOSED,
|
||||
add_bridge_buy_fill,
|
||||
add_bridge_close_sell_fill,
|
||||
bridge_buy_vwap,
|
||||
bridge_sell_vwap,
|
||||
ensure_bridge_table,
|
||||
insert_bridge,
|
||||
update_bridge,
|
||||
)
|
||||
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.row_factory = sqlite3.Row
|
||||
ensure_bridge_table(conn)
|
||||
bid = insert_bridge(conn, underlying="BTC", status="holding", budget_usdt=2200)
|
||||
add_bridge_buy_fill(conn, bid, usdt_spent=1000.0, coin_bought=0.5)
|
||||
add_bridge_buy_fill(conn, bid, usdt_spent=1200.0, coin_bought=0.6)
|
||||
# 2200 / 1.1 = 2000
|
||||
self.assertAlmostEqual(float(bridge_buy_vwap(conn, bid)), 2000.0, places=4)
|
||||
|
||||
add_bridge_close_sell_fill(conn, bid, usdt_recovered=800.0, coin_sold=0.4)
|
||||
add_bridge_close_sell_fill(conn, bid, usdt_recovered=1200.0, coin_sold=0.6)
|
||||
# 2000 / 1.0 = 2000
|
||||
self.assertAlmostEqual(float(bridge_sell_vwap(conn, bid)), 2000.0, places=4)
|
||||
update_bridge(conn, bid, status=BRIDGE_CLOSED, close=True)
|
||||
self.assertAlmostEqual(float(bridge_sell_vwap(conn, bid)), 2000.0, places=4)
|
||||
conn.close()
|
||||
|
||||
def test_true_pnl_uses_bridge_vwap_not_index(self):
|
||||
from lib.options.options_db import init_options_tables
|
||||
from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade
|
||||
from lib.options.options_spot_bridge_lib import (
|
||||
BRIDGE_CLOSED,
|
||||
add_bridge_buy_fill,
|
||||
add_bridge_close_sell_fill,
|
||||
ensure_bridge_table,
|
||||
insert_bridge,
|
||||
update_bridge,
|
||||
)
|
||||
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.row_factory = sqlite3.Row
|
||||
init_options_tables(conn)
|
||||
ensure_bridge_table(conn)
|
||||
bid = insert_bridge(conn, underlying="BTC", status="holding", budget_usdt=2200)
|
||||
add_bridge_buy_fill(conn, bid, usdt_spent=2200.0, coin_bought=1.1)
|
||||
add_bridge_close_sell_fill(conn, bid, usdt_recovered=1600.0, coin_sold=0.8)
|
||||
add_bridge_close_sell_fill(conn, bid, usdt_recovered=400.0, coin_sold=0.2)
|
||||
update_bridge(conn, bid, status=BRIDGE_CLOSED, close=True)
|
||||
# buy_vwap=2000, sell_vwap=2000; coin_pnl=0.01
|
||||
# A = 0.01*2000 - (0.01*2000 - 0.01*2000) = 20
|
||||
# 若误用指数 90000/80000 会得到 700
|
||||
pnl, src = resolve_true_pnl_usdt_from_trade(
|
||||
premium_paid=0.01,
|
||||
premium_received=0.02,
|
||||
realized_pnl=0.01,
|
||||
open_index_px=90000.0,
|
||||
close_index_px=80000.0,
|
||||
premium_ccy="BTC",
|
||||
conn=conn,
|
||||
bridge_id=bid,
|
||||
)
|
||||
self.assertEqual(src, "premium_move")
|
||||
self.assertAlmostEqual(float(pnl), 20.0, places=2)
|
||||
conn.close()
|
||||
|
||||
def test_pending_sell_blocks_true_pnl(self):
|
||||
from lib.options.options_pnl_settle_lib import resolve_true_pnl_usdt_from_trade
|
||||
from lib.options.options_spot_bridge_lib import (
|
||||
BRIDGE_PENDING_SELL,
|
||||
add_bridge_buy_fill,
|
||||
add_bridge_close_sell_fill,
|
||||
ensure_bridge_table,
|
||||
insert_bridge,
|
||||
update_bridge,
|
||||
)
|
||||
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.row_factory = sqlite3.Row
|
||||
ensure_bridge_table(conn)
|
||||
bid = insert_bridge(conn, underlying="ETH", status="holding", budget_usdt=3000)
|
||||
add_bridge_buy_fill(conn, bid, usdt_spent=3000.0, coin_bought=1.0)
|
||||
add_bridge_close_sell_fill(conn, bid, usdt_recovered=1500.0, coin_sold=0.5)
|
||||
update_bridge(conn, bid, status=BRIDGE_PENDING_SELL)
|
||||
pnl, src = resolve_true_pnl_usdt_from_trade(
|
||||
premium_paid=0.01,
|
||||
premium_received=0.02,
|
||||
realized_pnl=0.01,
|
||||
open_index_px=3000.0,
|
||||
close_index_px=3000.0,
|
||||
premium_ccy="ETH",
|
||||
conn=conn,
|
||||
bridge_id=bid,
|
||||
)
|
||||
self.assertEqual(src, "pending_sell")
|
||||
self.assertIsNone(pnl)
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user