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:
@@ -6961,8 +6961,15 @@ def api_account_snapshot():
|
|||||||
options_unrealized_pnl = None
|
options_unrealized_pnl = None
|
||||||
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
|
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
|
||||||
try:
|
try:
|
||||||
from lib.exchange.okx_options_lib import fetch_options_unrealized_pnl_usdc
|
|
||||||
from lib.instance.instance_live_pnl_lib import merge_unrealized_pnl_components
|
from lib.instance.instance_live_pnl_lib import merge_unrealized_pnl_components
|
||||||
|
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
||||||
|
|
||||||
|
opt_cfg = app.extensions.get("options_cfg")
|
||||||
|
if opt_cfg:
|
||||||
|
# 与持仓卡「净盈亏」同口径(买一回收−权利金),不用交易所标记价 upl
|
||||||
|
options_unrealized_pnl = sum_options_net_pnl_usdc(opt_cfg, exchange_options)
|
||||||
|
else:
|
||||||
|
from lib.exchange.okx_options_lib import fetch_options_unrealized_pnl_usdc
|
||||||
|
|
||||||
options_unrealized_pnl = fetch_options_unrealized_pnl_usdc(exchange_options)
|
options_unrealized_pnl = fetch_options_unrealized_pnl_usdc(exchange_options)
|
||||||
unrealized_pnl = merge_unrealized_pnl_components(unrealized_pnl, options_unrealized_pnl)
|
unrealized_pnl = merge_unrealized_pnl_components(unrealized_pnl, options_unrealized_pnl)
|
||||||
@@ -7372,6 +7379,12 @@ def api_price_snapshot():
|
|||||||
options_unrealized_pnl = None
|
options_unrealized_pnl = None
|
||||||
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
|
if OKX_OPTIONS_ENABLED and exchange_options.apiKey:
|
||||||
try:
|
try:
|
||||||
|
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
||||||
|
|
||||||
|
opt_cfg = app.extensions.get("options_cfg")
|
||||||
|
if opt_cfg:
|
||||||
|
options_unrealized_pnl = sum_options_net_pnl_usdc(opt_cfg, exchange_options)
|
||||||
|
else:
|
||||||
from lib.exchange.okx_options_lib import fetch_options_unrealized_pnl_usdc
|
from lib.exchange.okx_options_lib import fetch_options_unrealized_pnl_usdc
|
||||||
|
|
||||||
options_unrealized_pnl = fetch_options_unrealized_pnl_usdc(exchange_options)
|
options_unrealized_pnl = fetch_options_unrealized_pnl_usdc(exchange_options)
|
||||||
|
|||||||
@@ -3929,6 +3929,24 @@ html[data-theme="light"] .options-estimate-row {
|
|||||||
.options-pos-stats-card {
|
.options-pos-stats-card {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.options-stats-pnl-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.options-stats-pnl-summary .options-stat-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(127, 127, 127, 0.12);
|
||||||
|
}
|
||||||
|
.options-stats-pnl-summary .opt-stats-net-item .v {
|
||||||
|
font-size: 1.15em;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
html[data-theme="light"] .options-stats-pnl-summary .options-stat-item {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
.options-stats-panel {
|
.options-stats-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1476,6 +1476,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function paintPnlStat(el, value) {
|
||||||
|
if (!el) return;
|
||||||
|
if (value == null || value === "" || Number.isNaN(Number(value))) {
|
||||||
|
el.textContent = "—";
|
||||||
|
el.classList.remove("pos-pnl-profit", "pos-pnl-loss");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Number(value);
|
||||||
|
el.textContent = (n > 0 ? "+" : "") + fmt(n, 2) + " USDC";
|
||||||
|
el.classList.toggle("pos-pnl-profit", n > 0);
|
||||||
|
el.classList.toggle("pos-pnl-loss", n < 0);
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshStats() {
|
async function refreshStats() {
|
||||||
const d = await apiJson("/api/options/stats");
|
const d = await apiJson("/api/options/stats");
|
||||||
const winEl = document.getElementById("opt-stats-winrate");
|
const winEl = document.getElementById("opt-stats-winrate");
|
||||||
@@ -1487,14 +1500,23 @@
|
|||||||
const winHoldEl = document.getElementById("opt-stats-win-hold");
|
const winHoldEl = document.getElementById("opt-stats-win-hold");
|
||||||
const lossHoldEl = document.getElementById("opt-stats-loss-hold");
|
const lossHoldEl = document.getElementById("opt-stats-loss-hold");
|
||||||
const openHoldEl = document.getElementById("opt-stats-open-hold");
|
const openHoldEl = document.getElementById("opt-stats-open-hold");
|
||||||
|
const totalPnlEl = document.getElementById("opt-stats-total-pnl");
|
||||||
|
const netRealizedEl = document.getElementById("opt-stats-net-realized");
|
||||||
|
const openFloatEl = document.getElementById("opt-stats-open-float");
|
||||||
const statEls = [winEl, plrEl, closedEl, profitEl, lossEl, avgHoldEl, winHoldEl, lossHoldEl, openHoldEl];
|
const statEls = [winEl, plrEl, closedEl, profitEl, lossEl, avgHoldEl, winHoldEl, lossHoldEl, openHoldEl];
|
||||||
if (!d.ok) {
|
if (!d.ok) {
|
||||||
statEls.forEach(function (el) {
|
statEls.forEach(function (el) {
|
||||||
if (el) el.textContent = "—";
|
if (el) el.textContent = "—";
|
||||||
});
|
});
|
||||||
|
paintPnlStat(totalPnlEl, null);
|
||||||
|
paintPnlStat(netRealizedEl, null);
|
||||||
|
paintPnlStat(openFloatEl, null);
|
||||||
paintStatsCharts(null);
|
paintStatsCharts(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
paintPnlStat(totalPnlEl, d.total_pnl);
|
||||||
|
paintPnlStat(netRealizedEl, d.net_realized_pnl);
|
||||||
|
paintPnlStat(openFloatEl, d.open_float_pnl);
|
||||||
if (winEl) winEl.textContent = d.total_closed ? d.win_rate + "%" : "0%";
|
if (winEl) winEl.textContent = d.total_closed ? d.win_rate + "%" : "0%";
|
||||||
if (plrEl) {
|
if (plrEl) {
|
||||||
plrEl.textContent = d.profit_loss_ratio != null ? String(d.profit_loss_ratio) : "—";
|
plrEl.textContent = d.profit_loss_ratio != null ? String(d.profit_loss_ratio) : "—";
|
||||||
|
|||||||
@@ -1161,7 +1161,11 @@ def resolve_option_close_from_history(
|
|||||||
|
|
||||||
|
|
||||||
def fetch_options_unrealized_pnl_usdc(ex: ccxt.okx) -> float | None:
|
def fetch_options_unrealized_pnl_usdc(ex: ccxt.okx) -> float | None:
|
||||||
"""期权持仓未实现盈亏合计(USDC,统计口径与 USDT 1:1)."""
|
"""
|
||||||
|
期权浮盈合计(USDC≈U).
|
||||||
|
优先返回交易所标记价 upl;实例顶栏应改用
|
||||||
|
`options_positions_lib.sum_options_net_pnl_usdc`(买一净盈亏)以与持仓卡一致.
|
||||||
|
"""
|
||||||
positions = fetch_option_positions(ex)
|
positions = fetch_option_positions(ex)
|
||||||
if positions is None:
|
if positions is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
||||||
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
||||||
<link rel="stylesheet" href="/static/instance_page.css?v=6">
|
<link rel="stylesheet" href="/static/instance_page.css?v=6">
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=90">
|
<link rel="stylesheet" href="/static/instance_theme.css?v=91">
|
||||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||||
<meta name="theme-color" content="#0b0d14">
|
<meta name="theme-color" content="#0b0d14">
|
||||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
<link rel="manifest" href="/static/icons/manifest.webmanifest">
|
||||||
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
<title>{{ exchange_display }} · 加密货币 | 交易监控复盘系统</title>
|
||||||
<link rel="stylesheet" href="/static/instance_page.css?v=3">
|
<link rel="stylesheet" href="/static/instance_page.css?v=3">
|
||||||
<link rel="stylesheet" href="/static/instance_theme.css?v=90">
|
<link rel="stylesheet" href="/static/instance_theme.css?v=91">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
|
|||||||
@@ -48,14 +48,13 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
|||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
target_monitors = []
|
target_monitors = []
|
||||||
|
from lib.options.options_positions_lib import net_pnl_from_display_row
|
||||||
|
|
||||||
upl_total = 0.0
|
upl_total = 0.0
|
||||||
has_upl = False
|
has_upl = False
|
||||||
for p in positions:
|
for p in positions:
|
||||||
# 汇总优先用买盘净盈亏,与持仓卡「净盈亏」一致
|
# 与持仓卡「净盈亏」一致(买一回收−权利金);不用交易所标记价 upl
|
||||||
preview = p.get("close_preview") or {}
|
net = net_pnl_from_display_row(p)
|
||||||
net = preview.get("estimated_pnl")
|
|
||||||
if net is None:
|
|
||||||
net = p.get("upl")
|
|
||||||
if net is None:
|
if net is None:
|
||||||
continue
|
continue
|
||||||
has_upl = True
|
has_upl = True
|
||||||
|
|||||||
@@ -81,6 +81,52 @@ def forget_close_gate_for_inst(inst_id: str) -> None:
|
|||||||
clear_close_gate(inst_id)
|
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(
|
def build_display_option_positions(
|
||||||
cfg: dict[str, Any],
|
cfg: dict[str, Any],
|
||||||
ex: Any,
|
ex: Any,
|
||||||
|
|||||||
@@ -973,13 +973,29 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
if ex is None:
|
if ex is None:
|
||||||
return jsonify({"ok": False, "msg": err})
|
return jsonify({"ok": False, "msg": err})
|
||||||
from lib.options.options_history_lib import load_options_history
|
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
|
from lib.options.options_stats_lib import compute_options_stats_from_history
|
||||||
|
|
||||||
raw_live = cfg["fetch_option_positions"](ex)
|
raw_live = cfg["fetch_option_positions"](ex)
|
||||||
if raw_live is None:
|
if raw_live is None:
|
||||||
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
||||||
history = load_options_history(ex, cfg)
|
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"])
|
@app.route("/api/options/history/<path:history_key>", methods=["DELETE"])
|
||||||
@lr
|
@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_win = sum(wins) / len(wins) if wins else None
|
||||||
avg_loss = sum(losses) / len(losses) if losses 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 {
|
return {
|
||||||
"total_closed": total_closed,
|
"total_closed": total_closed,
|
||||||
"win_count": len(wins),
|
"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),
|
"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_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,
|
"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_profit": total_profit,
|
||||||
"total_loss": round(abs(sum(losses)), 4) if losses else 0.0,
|
"total_loss": total_loss,
|
||||||
|
"net_realized_pnl": net_realized,
|
||||||
"avg_hold_sec": _avg_seconds(all_holds),
|
"avg_hold_sec": _avg_seconds(all_holds),
|
||||||
"avg_win_hold_sec": _avg_seconds(win_holds),
|
"avg_win_hold_sec": _avg_seconds(win_holds),
|
||||||
"avg_loss_hold_sec": _avg_seconds(loss_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_win = sum(wins) / len(wins) if wins else None
|
||||||
avg_loss = sum(losses) / len(losses) if losses 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 {
|
return {
|
||||||
"total_closed": total_closed,
|
"total_closed": total_closed,
|
||||||
"win_count": len(wins),
|
"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),
|
"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_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,
|
"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_profit": total_profit,
|
||||||
"total_loss": round(abs(sum(losses)), 4) if losses else 0.0,
|
"total_loss": total_loss,
|
||||||
|
"net_realized_pnl": net_realized,
|
||||||
"avg_hold_sec": _avg_seconds(all_holds),
|
"avg_hold_sec": _avg_seconds(all_holds),
|
||||||
"avg_win_hold_sec": _avg_seconds(win_holds),
|
"avg_win_hold_sec": _avg_seconds(win_holds),
|
||||||
"avg_loss_hold_sec": _avg_seconds(loss_holds),
|
"avg_loss_hold_sec": _avg_seconds(loss_holds),
|
||||||
|
|||||||
@@ -152,6 +152,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="options-pos-pane" data-opt-pos-pane="stats" role="tabpanel" aria-labelledby="opt-pos-tab-stats" hidden>
|
<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-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="options-stats-charts">
|
||||||
<div class="opt-stats-chart opt-stats-chart--ring">
|
<div class="opt-stats-chart opt-stats-chart--ring">
|
||||||
<div class="opt-stats-ring" id="opt-stats-ring" style="--win-pct: 0">
|
<div class="opt-stats-ring" id="opt-stats-ring" style="--win-pct: 0">
|
||||||
@@ -258,4 +272,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
<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>
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ class OptionsHubLibTests(TestCase):
|
|||||||
@patch("lib.options.options_positions_lib.build_display_option_positions")
|
@patch("lib.options.options_positions_lib.build_display_option_positions")
|
||||||
def test_build_options_hub_snapshot_positions(self, mock_positions, _mock_stats):
|
def test_build_options_hub_snapshot_positions(self, mock_positions, _mock_stats):
|
||||||
mock_positions.return_value = [
|
mock_positions.return_value = [
|
||||||
{"inst_id": "ETH-USD_UM-260703-1800-C", "pos": 2, "upl": 1.5, "mark_px": 0.1}
|
{
|
||||||
|
"inst_id": "ETH-USD_UM-260703-1800-C",
|
||||||
|
"pos": 2,
|
||||||
|
"upl": 9.9,
|
||||||
|
"mark_px": 0.1,
|
||||||
|
"close_preview": {"estimated_pnl": 1.5},
|
||||||
|
}
|
||||||
]
|
]
|
||||||
conn = MagicMock()
|
conn = MagicMock()
|
||||||
conn.__enter__ = MagicMock(return_value=conn)
|
conn.__enter__ = MagicMock(return_value=conn)
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""期权净盈亏汇总与持仓卡口径一致."""
|
||||||
|
from unittest import TestCase
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from lib.options.options_positions_lib import net_pnl_from_display_row, sum_options_net_pnl_usdc
|
||||||
|
|
||||||
|
|
||||||
|
class OptionsNetPnlSumTests(TestCase):
|
||||||
|
def test_net_pnl_from_display_row(self):
|
||||||
|
self.assertEqual(
|
||||||
|
net_pnl_from_display_row({"close_preview": {"estimated_pnl": -2.8}, "premium_paid": 4.95}),
|
||||||
|
-2.8,
|
||||||
|
)
|
||||||
|
self.assertIsNone(
|
||||||
|
net_pnl_from_display_row({"close_preview": {"bid_invalid": True, "estimated_pnl": -1}})
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
net_pnl_from_display_row(
|
||||||
|
{"close_preview": {"total_received": 2.15}, "premium_paid": 4.95}
|
||||||
|
),
|
||||||
|
round(2.15 - 4.95, 4),
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("lib.options.options_positions_lib.build_display_option_positions")
|
||||||
|
def test_sum_options_net_pnl_usdc(self, mock_build):
|
||||||
|
mock_build.return_value = [
|
||||||
|
{"close_preview": {"estimated_pnl": -2.8}},
|
||||||
|
{"close_preview": {"estimated_pnl": 1.0}},
|
||||||
|
{"close_preview": {"bid_invalid": True, "estimated_pnl": 9}},
|
||||||
|
]
|
||||||
|
cfg = {"fetch_option_positions": lambda ex: [{"instId": "X"}]}
|
||||||
|
self.assertEqual(sum_options_net_pnl_usdc(cfg, object()), -1.8)
|
||||||
@@ -83,3 +83,4 @@ class OptionsStatsLibTests(TestCase):
|
|||||||
self.assertAlmostEqual(out["avg_loss"], 2.66, places=2)
|
self.assertAlmostEqual(out["avg_loss"], 2.66, places=2)
|
||||||
self.assertAlmostEqual(out["profit_loss_ratio"], 0.33, places=2)
|
self.assertAlmostEqual(out["profit_loss_ratio"], 0.33, places=2)
|
||||||
self.assertEqual(out["open_count"], 1)
|
self.assertEqual(out["open_count"], 1)
|
||||||
|
self.assertAlmostEqual(out["net_realized_pnl"], round(0.87 - 3.99 - 1.33, 4), places=4)
|
||||||
|
|||||||
Reference in New Issue
Block a user