fix(hub): merge mark price from Flask snapshot and fix board refresh

Sync hub positions with instance price_snapshot (order_prices and position_marks).
Fix monitor board UI when hub restarts (version rewind) and queue snapshot fetches.
Expose board aggregate status on /api/ping for diagnostics.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-03 22:44:01 +08:00
parent 2a9602610e
commit d07357b98e
9 changed files with 275 additions and 31 deletions
+115
View File
@@ -0,0 +1,115 @@
"""ccxt 持仓标记价解析(实例 price_snapshot 与中控子代理共用)。"""
from __future__ import annotations
import math
from typing import Any, Callable
def _finite_or_none(x: Any) -> float | None:
try:
f = float(x)
return f if math.isfinite(f) else None
except (TypeError, ValueError):
return None
def _coerce_float(*values: Any) -> float | None:
for v in values:
if v is None or v == "":
continue
px = _finite_or_none(v)
if px is not None and px > 0:
return px
return None
def position_contracts(p: dict[str, Any]) -> float:
raw = p.get("contracts")
if raw is not None:
try:
return float(raw)
except (TypeError, ValueError):
pass
info = p.get("info") or {}
if not isinstance(info, dict):
info = {}
for k in ("positionAmt", "positionamt", "pos", "size"):
if k in info:
try:
v = float(info[k])
if v != 0:
return v
except (TypeError, ValueError):
pass
return 0.0
def position_side_from_ccxt(p: dict[str, Any], contracts: float | None = None) -> str:
s = (p.get("side") or "").lower()
if s in ("long", "short"):
return s
c = contracts if contracts is not None else position_contracts(p)
if c > 0:
return "long"
if c < 0:
return "short"
return "long"
def parse_position_mark_price(p: dict[str, Any]) -> float | None:
"""四所 ccxt 持仓统一解析标记价(与 crypto_monitor_* parse_ccxt_position_metrics 口径一致)。"""
if not isinstance(p, dict):
return None
info = p.get("info") or {}
if not isinstance(info, dict):
info = {}
mark = _coerce_float(
p.get("markPrice"),
p.get("mark_price"),
p.get("mark"),
info.get("markPx"),
info.get("mark_price"),
info.get("markPrice"),
)
if mark is not None:
return mark
contracts = position_contracts(p)
if abs(contracts) >= 1e-12:
notional = _finite_or_none(p.get("notional"))
if notional is not None and abs(notional) > 0:
return abs(notional) / abs(contracts)
return None
def build_position_marks_list(
positions: list,
*,
format_mark_display: Callable[[str, float], str] | None = None,
) -> list[dict[str, Any]]:
"""从 fetch_positions 结果生成 position_marks,供 price_snapshot / 中控合并。"""
out: list[dict[str, Any]] = []
for p in positions or []:
if not isinstance(p, dict):
continue
c = position_contracts(p)
if abs(c) < 1e-12:
continue
mark = parse_position_mark_price(p)
if mark is None or mark <= 0:
continue
sym = (p.get("symbol") or "").strip()
side = position_side_from_ccxt(p, c)
row: dict[str, Any] = {
"symbol": sym,
"side": side,
"mark_price": mark,
}
if format_mark_display and sym:
try:
row["mark_price_display"] = format_mark_display(sym, mark)
except Exception:
row["mark_price_display"] = f"{mark:g}"
else:
row["mark_price_display"] = f"{mark:g}"
out.append(row)
return out