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:
@@ -31,6 +31,7 @@ _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
|
||||
|
||||
import ccxt
|
||||
from fastapi import FastAPI, Header, HTTPException, Request
|
||||
@@ -409,29 +410,8 @@ def _position_entry_price(p: dict[str, Any]) -> float | None:
|
||||
|
||||
|
||||
def _position_mark_price(p: dict[str, Any]) -> float | None:
|
||||
"""四所 ccxt 持仓统一解析标记价(用于强平/浮盈计算)。"""
|
||||
info = p.get("info") or {}
|
||||
if not isinstance(info, dict):
|
||||
info = {}
|
||||
for key in (
|
||||
p.get("markPrice"),
|
||||
p.get("mark_price"),
|
||||
p.get("mark"),
|
||||
info.get("markPx"),
|
||||
info.get("mark_price"),
|
||||
info.get("markPrice"),
|
||||
info.get("last"),
|
||||
info.get("lastPrice"),
|
||||
):
|
||||
px = _finite_or_none(key)
|
||||
if px is not None and px > 0:
|
||||
return px
|
||||
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
|
||||
"""四所 ccxt 持仓统一解析标记价(与实例 parse_ccxt_position_metrics 一致)。"""
|
||||
return parse_position_mark_price(p)
|
||||
|
||||
|
||||
def _ticker_mark_price(ex: Any, symbol: str) -> float | None:
|
||||
|
||||
+106
-1
@@ -16,7 +16,6 @@ if str(_REPO_ROOT) not in sys.path:
|
||||
|
||||
from hub_kline_store import format_ohlcv_detail, resolve_chart_bars, retention_days
|
||||
from hub_ohlcv_lib import CHART_TIMEFRAME_ORDER, CHART_TIMEFRAMES, bar_limit_for_timeframe
|
||||
|
||||
from env_load import load_hub_dotenv
|
||||
|
||||
load_hub_dotenv()
|
||||
@@ -705,6 +704,106 @@ def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon:
|
||||
p["sl_breakeven_secured"] = bool(matched["sl_breakeven_secured"])
|
||||
|
||||
|
||||
def _agent_position_has_mark(p: dict) -> bool:
|
||||
try:
|
||||
v = float(p.get("mark_price"))
|
||||
return v > 0
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def _apply_agent_mark_price(p: dict, mark_price: object, mark_display: object = None) -> None:
|
||||
try:
|
||||
mpf = float(mark_price)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
if mpf <= 0:
|
||||
return
|
||||
p["mark_price"] = mpf
|
||||
disp = mark_display
|
||||
if disp is not None and str(disp).strip() not in ("", "-"):
|
||||
p["mark_price_fmt"] = str(disp)
|
||||
|
||||
|
||||
def _find_matched_order_price_op(
|
||||
p: dict,
|
||||
order_prices: list,
|
||||
hub_orders: list,
|
||||
op_by_id: dict,
|
||||
) -> dict | None:
|
||||
sym = p.get("symbol") or ""
|
||||
side = (p.get("side") or "").lower()
|
||||
for o in hub_orders:
|
||||
if not isinstance(o, dict):
|
||||
continue
|
||||
o_sym = o.get("exchange_symbol") or o.get("symbol") or ""
|
||||
if not _symbols_match(sym, o_sym):
|
||||
continue
|
||||
if (o.get("direction") or "").lower() != side:
|
||||
continue
|
||||
matched = op_by_id.get(o.get("id"))
|
||||
if isinstance(matched, dict):
|
||||
return matched
|
||||
break
|
||||
for op in order_prices:
|
||||
if not isinstance(op, dict):
|
||||
continue
|
||||
if not _symbols_match(sym, op.get("symbol") or ""):
|
||||
continue
|
||||
return op
|
||||
return None
|
||||
|
||||
|
||||
def _merge_flask_position_mark_price(
|
||||
agent_row: dict, snap: dict | None, hub_mon: dict | None
|
||||
) -> None:
|
||||
"""子代理无标记价时,用实例 price_snapshot 的交易所标记价补全中控持仓展示。"""
|
||||
ag = agent_row.get("agent")
|
||||
if not isinstance(ag, dict) or not isinstance(snap, dict):
|
||||
return
|
||||
positions = ag.get("positions")
|
||||
if not isinstance(positions, list) or not positions:
|
||||
return
|
||||
order_prices = snap.get("order_prices") or []
|
||||
hub_orders = []
|
||||
if isinstance(hub_mon, dict):
|
||||
hub_orders = hub_mon.get("orders") or []
|
||||
op_by_id = {
|
||||
op.get("id"): op
|
||||
for op in order_prices
|
||||
if isinstance(op, dict) and op.get("id") is not None
|
||||
}
|
||||
for p in positions:
|
||||
if not isinstance(p, dict) or _agent_position_has_mark(p):
|
||||
continue
|
||||
matched = _find_matched_order_price_op(p, order_prices, hub_orders, op_by_id)
|
||||
if isinstance(matched, dict):
|
||||
_apply_agent_mark_price(
|
||||
p,
|
||||
matched.get("exchange_mark_price"),
|
||||
matched.get("exchange_mark_price_display"),
|
||||
)
|
||||
position_marks = snap.get("position_marks") or []
|
||||
if not isinstance(position_marks, list):
|
||||
return
|
||||
for p in positions:
|
||||
if not isinstance(p, dict) or _agent_position_has_mark(p):
|
||||
continue
|
||||
sym = p.get("symbol") or ""
|
||||
side = (p.get("side") or "").lower()
|
||||
for pm in position_marks:
|
||||
if not isinstance(pm, dict):
|
||||
continue
|
||||
if not _symbols_match(sym, pm.get("symbol") or ""):
|
||||
continue
|
||||
if (pm.get("side") or "").lower() != side:
|
||||
continue
|
||||
_apply_agent_mark_price(
|
||||
p, pm.get("mark_price"), pm.get("mark_price_display")
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
def _merge_flask_exchange_tpsl(agent_row: dict, snap: dict | None, hub_mon: dict | None) -> None:
|
||||
"""子代理挂单为空时,用实例 Flask 已算好的 exchange_tpsl 补全展示。"""
|
||||
ag = agent_row.get("agent")
|
||||
@@ -764,6 +863,7 @@ async def _assemble_board_row(
|
||||
_merge_flask_order_price_fields(hub_mon, snap)
|
||||
_merge_flask_exchange_tpsl(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
|
||||
_merge_flask_position_breakeven(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
|
||||
_merge_flask_position_mark_price(agent_row, snap, hub_mon if isinstance(hub_mon, dict) else None)
|
||||
flask_ok = isinstance(hub_mon, dict) and hub_mon.get("ok") is not False
|
||||
raw_review = (ex.get("review_url") or "").strip()
|
||||
review_link = browser_url(raw_review) if raw_review else default_review_url(
|
||||
@@ -1088,6 +1188,11 @@ def api_ping():
|
||||
"features": ["monitor", "settings", "auth", "board_sse"],
|
||||
"board_poll_interval_sec": HUB_BOARD_POLL_INTERVAL,
|
||||
"board_version": board_store.version,
|
||||
"board_aggregating": board_store.aggregating,
|
||||
"board_updated_at": (board_store.payload or {}).get("updated_at")
|
||||
if isinstance(board_store.payload, dict)
|
||||
else None,
|
||||
"board_error": board_store.last_error,
|
||||
"password_required": password_required(),
|
||||
"env_disabled_ids": sorted(env_force_disabled_ids()),
|
||||
"hub_disabled_ids_raw": (os.getenv("HUB_DISABLED_IDS") or ""),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
let lastMonitorBoardUpdatedAt = "";
|
||||
let localBoardVersion = 0;
|
||||
let monitorBoardInFlight = false;
|
||||
let monitorBoardFetchPending = false;
|
||||
let monitorBoardSlowHintTimer = null;
|
||||
let boardEventSource = null;
|
||||
let sseReconnectTimer = null;
|
||||
@@ -384,7 +385,7 @@
|
||||
try {
|
||||
const st = JSON.parse(ev.data || "{}");
|
||||
const ver = Number(st.board_version) || 0;
|
||||
if (ver > localBoardVersion) {
|
||||
if (ver !== localBoardVersion) {
|
||||
void fetchMonitorBoardSnapshot({ background: true });
|
||||
} else if (st.aggregating && lastMonitorRows.length) {
|
||||
applyMonitorBoardUi(lastMonitorRows, st.updated_at || lastMonitorBoardUpdatedAt, {
|
||||
@@ -468,7 +469,7 @@
|
||||
if (!cached) return false;
|
||||
lastMonitorRows = cached.rows;
|
||||
lastMonitorBoardUpdatedAt = cached.updated_at || "";
|
||||
localBoardVersion = Number(cached.board_version) || 0;
|
||||
localBoardVersion = 0;
|
||||
applyMonitorBoardUi(cached.rows, lastMonitorBoardUpdatedAt, { stale: true });
|
||||
return true;
|
||||
}
|
||||
@@ -660,7 +661,10 @@
|
||||
const background = !!options.background;
|
||||
const showLoading = !!options.showLoading && !lastMonitorRows.length;
|
||||
const box = document.getElementById("monitor-grid");
|
||||
if (monitorBoardInFlight && background) return;
|
||||
if (monitorBoardInFlight) {
|
||||
if (background) monitorBoardFetchPending = true;
|
||||
else return;
|
||||
}
|
||||
if (showLoading && box) {
|
||||
box.innerHTML =
|
||||
'<div class="board-loading"><span class="board-loading-spin" aria-hidden="true"></span>正在加载监控快照…<p class="board-loading-sub"></p></div>';
|
||||
@@ -687,11 +691,14 @@
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ver >= localBoardVersion || !lastMonitorRows.length) {
|
||||
const ts = data.updated_at || "";
|
||||
const versionChanged = ver !== localBoardVersion;
|
||||
const timeChanged = ts && ts !== lastMonitorBoardUpdatedAt;
|
||||
if (versionChanged || timeChanged || !lastMonitorRows.length) {
|
||||
localBoardVersion = ver;
|
||||
lastMonitorRows = rows;
|
||||
saveMonitorBoardCache(lastMonitorRows, data.updated_at, ver);
|
||||
applyMonitorBoardUi(lastMonitorRows, data.updated_at, {
|
||||
saveMonitorBoardCache(lastMonitorRows, ts, ver);
|
||||
applyMonitorBoardUi(lastMonitorRows, ts, {
|
||||
stale: !!data.aggregating,
|
||||
});
|
||||
} else if (data.aggregating && lastMonitorRows.length) {
|
||||
@@ -715,6 +722,10 @@
|
||||
clearTimeout(fetchTimer);
|
||||
clearMonitorBoardSlowHint();
|
||||
monitorBoardInFlight = false;
|
||||
if (monitorBoardFetchPending) {
|
||||
monitorBoardFetchPending = false;
|
||||
void fetchMonitorBoardSnapshot({ background: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,6 +735,7 @@
|
||||
}
|
||||
try {
|
||||
await requestMonitorBoardRefresh();
|
||||
await fetchMonitorBoardSnapshot({ background: false });
|
||||
} catch (e) {
|
||||
showToast(String(e), true);
|
||||
}
|
||||
|
||||
@@ -245,6 +245,6 @@
|
||||
<div id="toast"></div>
|
||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||
<script src="/assets/chart.js?v=20260603-hub-binance-tick"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-mark-price2"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-board-refresh"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user