币本位数据统计改为U展示,已平盈亏按指数折U与浮盈口径一致。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2340,7 +2340,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function paintPnlStat(el, value) {
|
function statsPnlUnit(d) {
|
||||||
|
if (d && d.pnl_unit) return String(d.pnl_unit);
|
||||||
|
return isCoinMarginMode() ? "U" : "USDC";
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintPnlStat(el, value, unit) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
if (value == null || value === "" || Number.isNaN(Number(value))) {
|
if (value == null || value === "" || Number.isNaN(Number(value))) {
|
||||||
el.textContent = "—";
|
el.textContent = "—";
|
||||||
@@ -2348,7 +2353,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const n = Number(value);
|
const n = Number(value);
|
||||||
el.textContent = (n > 0 ? "+" : "") + fmt(n, 2) + " USDC";
|
const u = unit || "USDC";
|
||||||
|
el.textContent = (n > 0 ? "+" : "") + fmt(n, 2) + " " + u;
|
||||||
el.classList.toggle("pos-pnl-profit", n > 0);
|
el.classList.toggle("pos-pnl-profit", n > 0);
|
||||||
el.classList.toggle("pos-pnl-loss", n < 0);
|
el.classList.toggle("pos-pnl-loss", n < 0);
|
||||||
}
|
}
|
||||||
@@ -2378,9 +2384,10 @@
|
|||||||
paintStatsCharts(null);
|
paintStatsCharts(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
paintPnlStat(totalPnlEl, d.total_pnl);
|
const unit = statsPnlUnit(d);
|
||||||
paintPnlStat(netRealizedEl, d.net_realized_pnl);
|
paintPnlStat(totalPnlEl, d.total_pnl, unit);
|
||||||
paintPnlStat(openFloatEl, d.open_float_pnl);
|
paintPnlStat(netRealizedEl, d.net_realized_pnl, unit);
|
||||||
|
paintPnlStat(openFloatEl, d.open_float_pnl, unit);
|
||||||
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) : "—";
|
||||||
@@ -2388,11 +2395,11 @@
|
|||||||
if (closedEl) closedEl.textContent = String(d.total_closed || 0);
|
if (closedEl) closedEl.textContent = String(d.total_closed || 0);
|
||||||
if (profitEl) {
|
if (profitEl) {
|
||||||
profitEl.textContent = d.avg_win != null && d.avg_win > 0
|
profitEl.textContent = d.avg_win != null && d.avg_win > 0
|
||||||
? fmt(d.avg_win, 2) + " USDC" : (d.win_count ? "0 USDC" : "—");
|
? fmt(d.avg_win, 2) + " " + unit : (d.win_count ? ("0 " + unit) : "—");
|
||||||
}
|
}
|
||||||
if (lossEl) {
|
if (lossEl) {
|
||||||
lossEl.textContent = d.avg_loss != null && d.avg_loss > 0
|
lossEl.textContent = d.avg_loss != null && d.avg_loss > 0
|
||||||
? fmt(d.avg_loss, 2) + " USDC" : (d.loss_count ? "0 USDC" : "—");
|
? fmt(d.avg_loss, 2) + " " + unit : (d.loss_count ? ("0 " + unit) : "—");
|
||||||
}
|
}
|
||||||
if (avgHoldEl) avgHoldEl.textContent = fmtDuration(d.avg_hold_sec);
|
if (avgHoldEl) avgHoldEl.textContent = fmtDuration(d.avg_hold_sec);
|
||||||
if (winHoldEl) winHoldEl.textContent = fmtDuration(d.avg_win_hold_sec);
|
if (winHoldEl) winHoldEl.textContent = fmtDuration(d.avg_win_hold_sec);
|
||||||
@@ -2455,19 +2462,20 @@
|
|||||||
if (ring) ring.style.setProperty("--win-pct", String(winRate));
|
if (ring) ring.style.setProperty("--win-pct", String(winRate));
|
||||||
if (ringLabel) ringLabel.textContent = d.total_closed ? winRate.toFixed(0) + "%" : "0%";
|
if (ringLabel) ringLabel.textContent = d.total_closed ? winRate.toFixed(0) + "%" : "0%";
|
||||||
|
|
||||||
|
const unit = statsPnlUnit(d);
|
||||||
const profit = Math.max(0, Number(d.avg_win) || 0);
|
const profit = Math.max(0, Number(d.avg_win) || 0);
|
||||||
const loss = Math.max(0, Number(d.avg_loss) || 0);
|
const loss = Math.max(0, Number(d.avg_loss) || 0);
|
||||||
const pnlTotal = profit + loss;
|
const pnlTotal = profit + loss;
|
||||||
if (pnlTotal > 0) {
|
if (pnlTotal > 0) {
|
||||||
setBarFill(profitBar, (profit / pnlTotal) * 100);
|
setBarFill(profitBar, (profit / pnlTotal) * 100);
|
||||||
setBarFill(lossBar, (loss / pnlTotal) * 100);
|
setBarFill(lossBar, (loss / pnlTotal) * 100);
|
||||||
if (profitBarLabel) profitBarLabel.textContent = fmt(profit, 2) + " USDC";
|
if (profitBarLabel) profitBarLabel.textContent = fmt(profit, 2) + " " + unit;
|
||||||
if (lossBarLabel) lossBarLabel.textContent = fmt(loss, 2) + " USDC";
|
if (lossBarLabel) lossBarLabel.textContent = fmt(loss, 2) + " " + unit;
|
||||||
} else {
|
} else {
|
||||||
setBarFill(profitBar, 0);
|
setBarFill(profitBar, 0);
|
||||||
setBarFill(lossBar, 0);
|
setBarFill(lossBar, 0);
|
||||||
if (profitBarLabel) profitBarLabel.textContent = d.win_count ? "0 USDC" : "—";
|
if (profitBarLabel) profitBarLabel.textContent = d.win_count ? ("0 " + unit) : "—";
|
||||||
if (lossBarLabel) lossBarLabel.textContent = d.loss_count ? "0 USDC" : "—";
|
if (lossBarLabel) lossBarLabel.textContent = d.loss_count ? ("0 " + unit) : "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
const winHold = Number(d.avg_win_hold_sec) || 0;
|
const winHold = Number(d.avg_win_hold_sec) || 0;
|
||||||
|
|||||||
@@ -1828,14 +1828,19 @@ 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_margin_mode_lib import is_coin_margin_mode
|
||||||
from lib.options.options_positions_lib import sum_options_net_pnl_usdc
|
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,
|
||||||
|
history_pnl_to_usdt,
|
||||||
|
)
|
||||||
|
|
||||||
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)
|
||||||
stats = compute_options_stats_from_history(history)
|
# 币本位已平盈亏按指数折 U,与持仓浮盈/合计口径一致
|
||||||
|
stats = compute_options_stats_from_history(history_pnl_to_usdt(history, ex))
|
||||||
open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live)
|
open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live)
|
||||||
net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0
|
net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0
|
||||||
total_pnl = None
|
total_pnl = None
|
||||||
@@ -1843,12 +1848,15 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
total_pnl = round(net_realized + float(open_float), 4)
|
total_pnl = round(net_realized + float(open_float), 4)
|
||||||
elif stats.get("total_closed"):
|
elif stats.get("total_closed"):
|
||||||
total_pnl = round(net_realized, 4)
|
total_pnl = round(net_realized, 4)
|
||||||
|
# 币本位统计统一标 U;USDC 模式仍标 USDC
|
||||||
|
pnl_unit = "U" if is_coin_margin_mode() else "USDC"
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"ok": True,
|
"ok": True,
|
||||||
**stats,
|
**stats,
|
||||||
"open_float_pnl": open_float,
|
"open_float_pnl": open_float,
|
||||||
"total_pnl": total_pnl,
|
"total_pnl": total_pnl,
|
||||||
|
"pnl_unit": pnl_unit,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,94 @@ from lib.instance.instance_embed_context_lib import profit_loss_ratio_from_avera
|
|||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_float(v: Any) -> float | None:
|
||||||
|
if v is None or v == "":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _underlying_index_usdt(ex: Any, underly: str) -> float | None:
|
||||||
|
"""取标的 USDT 近似指数(币本位已平盈亏折 U).优先公开 ticker,避免私钥失败."""
|
||||||
|
u = (underly or "ETH").strip().upper() or "ETH"
|
||||||
|
pubs: list[Any] = []
|
||||||
|
try:
|
||||||
|
from lib.sim.hooks import _APP_MODULE, _sim_public_exchange
|
||||||
|
|
||||||
|
pub = _sim_public_exchange(ex) if _APP_MODULE is not None else None
|
||||||
|
if pub is not None:
|
||||||
|
pubs.append(pub)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if ex is not None and ex not in pubs:
|
||||||
|
pubs.append(ex)
|
||||||
|
from lib.exchange.okx_options_lib import fetch_index_price
|
||||||
|
|
||||||
|
for pub in pubs:
|
||||||
|
try:
|
||||||
|
if hasattr(pub, "public_get_market_ticker"):
|
||||||
|
rows = (pub.public_get_market_ticker({"instId": f"{u}-USDT"}) or {}).get("data") or []
|
||||||
|
if rows:
|
||||||
|
last = _safe_float(rows[0].get("last") or rows[0].get("lastPx"))
|
||||||
|
if last is not None and last > 0:
|
||||||
|
return float(last)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
px = fetch_index_price(pub, f"{u}-USD")
|
||||||
|
if px is not None and float(px) > 0:
|
||||||
|
return float(px)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
t = pub.fetch_ticker(f"{u}/USDT") or {}
|
||||||
|
last = _safe_float(t.get("last") or t.get("close"))
|
||||||
|
if last is not None and last > 0:
|
||||||
|
return float(last)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def history_pnl_to_usdt(history: list[dict[str, Any]], ex: Any = None) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
统计用:币本位 realized_pnl(ETH/BTC) 按指数折成 U;USDC 原样.
|
||||||
|
折算失败的币仓剔除盈亏字段,避免把「币数量」当成 U.
|
||||||
|
"""
|
||||||
|
from lib.options.options_margin_mode_lib import margin_mode_from_inst_id, premium_ccy_for_mode
|
||||||
|
|
||||||
|
idx_cache: dict[str, float | None] = {}
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for row in history:
|
||||||
|
r = dict(row)
|
||||||
|
if r.get("status") == "open":
|
||||||
|
out.append(r)
|
||||||
|
continue
|
||||||
|
pnl = _safe_float(r.get("realized_pnl"))
|
||||||
|
if pnl is None:
|
||||||
|
out.append(r)
|
||||||
|
continue
|
||||||
|
inst = str(r.get("inst_id") or "")
|
||||||
|
underly = str(r.get("underlying") or (inst.split("-")[0] if inst else "ETH") or "ETH")
|
||||||
|
ccy = str(r.get("premium_ccy") or "").strip().upper()
|
||||||
|
if not ccy:
|
||||||
|
ccy = premium_ccy_for_mode(margin_mode_from_inst_id(inst), underly)
|
||||||
|
if ccy in ("ETH", "BTC"):
|
||||||
|
if underly not in idx_cache:
|
||||||
|
idx_cache[underly] = _underlying_index_usdt(ex, underly)
|
||||||
|
idx = idx_cache.get(underly)
|
||||||
|
if idx is None or idx <= 0:
|
||||||
|
r["realized_pnl"] = None
|
||||||
|
else:
|
||||||
|
r["realized_pnl"] = round(float(pnl) * float(idx), 4)
|
||||||
|
else:
|
||||||
|
r["realized_pnl"] = round(float(pnl), 4)
|
||||||
|
out.append(r)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _parse_ts(raw: Any) -> datetime | None:
|
def _parse_ts(raw: Any) -> datetime | None:
|
||||||
if raw is None or raw == "":
|
if raw is None or raw == "":
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user