Align options realtime PnL with bid-net and show totals in stats.
Header float PnL now uses bid recycle minus premium like position cards. Stats adds realized/open/total net PnL. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -48,14 +48,13 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
||||
conn.close()
|
||||
except Exception:
|
||||
target_monitors = []
|
||||
from lib.options.options_positions_lib import net_pnl_from_display_row
|
||||
|
||||
upl_total = 0.0
|
||||
has_upl = False
|
||||
for p in positions:
|
||||
# 汇总优先用买盘净盈亏,与持仓卡「净盈亏」一致
|
||||
preview = p.get("close_preview") or {}
|
||||
net = preview.get("estimated_pnl")
|
||||
if net is None:
|
||||
net = p.get("upl")
|
||||
# 与持仓卡「净盈亏」一致(买一回收−权利金);不用交易所标记价 upl
|
||||
net = net_pnl_from_display_row(p)
|
||||
if net is None:
|
||||
continue
|
||||
has_upl = True
|
||||
|
||||
@@ -81,6 +81,52 @@ def forget_close_gate_for_inst(inst_id: str) -> None:
|
||||
clear_close_gate(inst_id)
|
||||
|
||||
|
||||
def net_pnl_from_display_row(row: dict[str, Any]) -> float | None:
|
||||
"""与持仓卡「净盈亏」同口径:买一可回收 − 权利金;残档买一则无净值."""
|
||||
preview = row.get("close_preview") if isinstance(row.get("close_preview"), dict) else {}
|
||||
if preview.get("bid_invalid"):
|
||||
return None
|
||||
net = preview.get("estimated_pnl")
|
||||
if net is not None:
|
||||
try:
|
||||
return float(net)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
recv = _safe_float(preview.get("total_received"))
|
||||
paid = _safe_float(row.get("premium_paid"))
|
||||
if recv is not None and paid is not None:
|
||||
return round(recv - paid, 4)
|
||||
return None
|
||||
|
||||
|
||||
def sum_options_net_pnl_usdc(
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
raw_positions: list[dict[str, Any]] | None = None,
|
||||
) -> float | None:
|
||||
"""
|
||||
期权浮盈合计(USDC),与顶栏实时盈亏/中控口径对齐为「净盈亏」:
|
||||
各仓买一可回收 − 权利金之和.获取失败返回 None;无持仓返回 0.
|
||||
"""
|
||||
raw = raw_positions
|
||||
if raw is None:
|
||||
raw = cfg["fetch_option_positions"](ex)
|
||||
if raw is None:
|
||||
return None
|
||||
if not raw:
|
||||
return 0.0
|
||||
positions = build_display_option_positions(cfg, ex, raw)
|
||||
total = 0.0
|
||||
found = False
|
||||
for p in positions:
|
||||
net = net_pnl_from_display_row(p)
|
||||
if net is None:
|
||||
continue
|
||||
found = True
|
||||
total += float(net)
|
||||
return round(total, 4) if found else (0.0 if not positions else None)
|
||||
|
||||
|
||||
def build_display_option_positions(
|
||||
cfg: dict[str, Any],
|
||||
ex: Any,
|
||||
|
||||
@@ -973,13 +973,29 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
if ex is None:
|
||||
return jsonify({"ok": False, "msg": err})
|
||||
from lib.options.options_history_lib import load_options_history
|
||||
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
||||
from lib.options.options_stats_lib import compute_options_stats_from_history
|
||||
|
||||
raw_live = cfg["fetch_option_positions"](ex)
|
||||
if raw_live is None:
|
||||
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
||||
history = load_options_history(ex, cfg)
|
||||
return jsonify({"ok": True, **compute_options_stats_from_history(history)})
|
||||
stats = compute_options_stats_from_history(history)
|
||||
open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live)
|
||||
net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0
|
||||
total_pnl = None
|
||||
if open_float is not None:
|
||||
total_pnl = round(net_realized + float(open_float), 4)
|
||||
elif stats.get("total_closed"):
|
||||
total_pnl = round(net_realized, 4)
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
**stats,
|
||||
"open_float_pnl": open_float,
|
||||
"total_pnl": total_pnl,
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/options/history/<path:history_key>", methods=["DELETE"])
|
||||
@lr
|
||||
|
||||
@@ -75,6 +75,9 @@ def compute_options_stats_from_history(history: list[dict[str, Any]]) -> dict[st
|
||||
avg_win = sum(wins) / len(wins) if wins else None
|
||||
avg_loss = sum(losses) / len(losses) if losses else None
|
||||
|
||||
total_profit = round(sum(wins), 4) if wins else 0.0
|
||||
total_loss = round(abs(sum(losses)), 4) if losses else 0.0
|
||||
net_realized = round(sum(wins) + sum(losses), 4)
|
||||
return {
|
||||
"total_closed": total_closed,
|
||||
"win_count": len(wins),
|
||||
@@ -83,8 +86,9 @@ def compute_options_stats_from_history(history: list[dict[str, Any]]) -> dict[st
|
||||
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
|
||||
"avg_win": round(avg_win, 4) if avg_win is not None else None,
|
||||
"avg_loss": round(abs(avg_loss), 4) if avg_loss is not None else None,
|
||||
"total_profit": round(sum(wins), 4) if wins else 0.0,
|
||||
"total_loss": round(abs(sum(losses)), 4) if losses else 0.0,
|
||||
"total_profit": total_profit,
|
||||
"total_loss": total_loss,
|
||||
"net_realized_pnl": net_realized,
|
||||
"avg_hold_sec": _avg_seconds(all_holds),
|
||||
"avg_win_hold_sec": _avg_seconds(win_holds),
|
||||
"avg_loss_hold_sec": _avg_seconds(loss_holds),
|
||||
@@ -147,6 +151,9 @@ def compute_options_stats(get_db) -> dict[str, Any]:
|
||||
avg_win = sum(wins) / len(wins) if wins else None
|
||||
avg_loss = sum(losses) / len(losses) if losses else None
|
||||
|
||||
total_profit = round(sum(wins), 4) if wins else 0.0
|
||||
total_loss = round(abs(sum(losses)), 4) if losses else 0.0
|
||||
net_realized = round(sum(wins) + sum(losses), 4)
|
||||
return {
|
||||
"total_closed": total_closed,
|
||||
"win_count": len(wins),
|
||||
@@ -155,8 +162,9 @@ def compute_options_stats(get_db) -> dict[str, Any]:
|
||||
"profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss),
|
||||
"avg_win": round(avg_win, 4) if avg_win is not None else None,
|
||||
"avg_loss": round(abs(avg_loss), 4) if avg_loss is not None else None,
|
||||
"total_profit": round(sum(wins), 4) if wins else 0.0,
|
||||
"total_loss": round(abs(sum(losses)), 4) if losses else 0.0,
|
||||
"total_profit": total_profit,
|
||||
"total_loss": total_loss,
|
||||
"net_realized_pnl": net_realized,
|
||||
"avg_hold_sec": _avg_seconds(all_holds),
|
||||
"avg_win_hold_sec": _avg_seconds(win_holds),
|
||||
"avg_loss_hold_sec": _avg_seconds(loss_holds),
|
||||
|
||||
@@ -152,6 +152,20 @@
|
||||
</div>
|
||||
<div class="options-pos-pane" data-opt-pos-pane="stats" role="tabpanel" aria-labelledby="opt-pos-tab-stats" hidden>
|
||||
<div class="options-stats-panel">
|
||||
<div class="options-stats-pnl-summary" id="opt-stats-pnl-summary">
|
||||
<div class="options-stat-item opt-stats-net-item">
|
||||
<span class="k">合计盈亏</span>
|
||||
<span class="v" id="opt-stats-total-pnl">—</span>
|
||||
</div>
|
||||
<div class="options-stat-item">
|
||||
<span class="k">已平净盈亏</span>
|
||||
<span class="v" id="opt-stats-net-realized">—</span>
|
||||
</div>
|
||||
<div class="options-stat-item">
|
||||
<span class="k">持仓浮盈</span>
|
||||
<span class="v" id="opt-stats-open-float">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options-stats-charts">
|
||||
<div class="opt-stats-chart opt-stats-chart--ring">
|
||||
<div class="opt-stats-ring" id="opt-stats-ring" style="--win-pct: 0">
|
||||
@@ -258,4 +272,4 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||
<script src="/static/options_panel.js?v=29"></script>
|
||||
<script src="/static/options_panel.js?v=30"></script>
|
||||
|
||||
Reference in New Issue
Block a user