fix(hub): show contract-based unrealized PnL in monitor and chart

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 20:20:36 +08:00
parent ef8656e95d
commit 806350231e
6 changed files with 267 additions and 63 deletions
+31 -25
View File
@@ -31,7 +31,12 @@ _REPO_ROOT = Path(__file__).resolve().parents[1]
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
from hub_ohlcv_lib import format_price_by_tick, price_tick_from_market
from hub_position_metrics import parse_position_mark_price, parse_position_unrealized_pnl
from hub_position_metrics import (
parse_position_entry_price,
parse_position_mark_price,
parse_position_unrealized_pnl,
resolve_position_display_upnl,
)
import ccxt
from fastapi import FastAPI, Header, HTTPException, Request
@@ -388,25 +393,16 @@ def _position_price_fmt(ex: Any, symbol: str, price: float | None) -> tuple[floa
def _position_entry_price(p: dict[str, Any]) -> float | None:
"""四所 ccxt 持仓统一解析开仓均价(Binance/OKX/Gate 字段名不一致)。"""
info = p.get("info") or {}
if not isinstance(info, dict):
info = {}
for key in (
p.get("entryPrice"),
p.get("entry_price"),
p.get("average"),
info.get("entryPrice"),
info.get("entry_price"),
info.get("avgPx"),
info.get("avgEntryPrice"),
info.get("avg_entry_price"),
info.get("avgPrice"),
info.get("openAvgPx"),
):
px = _finite_or_none(key)
if px is not None and px > 0:
return px
return None
return parse_position_entry_price(p)
def _position_contract_size(ex: Any, symbol: str) -> float:
try:
market = ex.market((symbol or "").strip())
cs = float(market.get("contractSize") or 1)
return cs if cs > 0 else 1.0
except Exception:
return 1.0
def _position_mark_price(p: dict[str, Any]) -> float | None:
@@ -602,7 +598,20 @@ def _status_inner(x_control_token: str | None) -> Any:
continue
sym = p.get("symbol") or ""
side = _position_side(p, c)
upnl_f = parse_position_unrealized_pnl(p)
entry_f = _position_entry_price(p)
mark_f = _position_mark_price(p)
if mark_f is None and sym:
mark_f = _ticker_mark_price(ex, sym)
cs = _position_contract_size(ex, sym) if sym else 1.0
exchange_upnl = parse_position_unrealized_pnl(p)
upnl_f = resolve_position_display_upnl(
side,
entry_f,
mark_f,
abs(c),
cs,
exchange_upnl,
)
if upnl_f is None:
upnl_f = 0.0
total_upnl += upnl_f
@@ -611,11 +620,7 @@ def _status_inner(x_control_token: str | None) -> Any:
notional_f = float(notional) if notional is not None else None
except (TypeError, ValueError):
notional_f = None
entry_f = _position_entry_price(p)
_, entry_fmt, price_tick = _position_price_fmt(ex, sym, entry_f)
mark_f = _position_mark_price(p)
if mark_f is None and sym:
mark_f = _ticker_mark_price(ex, sym)
_, mark_fmt, mark_tick = _position_price_fmt(ex, sym, mark_f)
if price_tick is None and mark_tick is not None:
price_tick = mark_tick
@@ -631,6 +636,7 @@ def _status_inner(x_control_token: str | None) -> Any:
"entry_price_fmt": entry_fmt,
"mark_price": mark_f,
"mark_price_fmt": mark_fmt,
"contract_size": _finite_or_none(cs),
"price_tick": _finite_or_none(price_tick) if price_tick is not None else None,
}
)