From c1fda1e7d50b450af9eea4c152a6cdd77c87c1ef Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 3 Jun 2026 22:25:23 +0800 Subject: [PATCH] feat(hub): show exchange mark price on monitor positions Co-authored-by: Cursor --- manual_trading_hub/agent.py | 24 +++++++++++++++++++++ manual_trading_hub/static/app.js | 16 +++++++++++++- manual_trading_hub/static/index.html | 2 +- tests/test_hub_agent_mark_price.py | 32 ++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 tests/test_hub_agent_mark_price.py diff --git a/manual_trading_hub/agent.py b/manual_trading_hub/agent.py index 8b35169..353ab6e 100644 --- a/manual_trading_hub/agent.py +++ b/manual_trading_hub/agent.py @@ -408,6 +408,24 @@ def _position_entry_price(p: dict[str, Any]) -> float | None: return 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"), + info.get("markPx"), + info.get("mark_price"), + info.get("markPrice"), + ): + px = _finite_or_none(key) + if px is not None and px > 0: + return px + return None + + def _extract_usdt_total(balance: dict[str, Any]) -> float | None: """从 ccxt balance 结构中尽量取出 USDT 总额(与 crypto_monitor_binance 一致)。""" usdt_info = balance.get("USDT") or {} @@ -583,6 +601,10 @@ def _status_inner(x_control_token: str | None) -> Any: 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) + _, 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 positions_out.append( { "symbol": sym, @@ -593,6 +615,8 @@ def _status_inner(x_control_token: str | None) -> Any: "unrealized_pnl": _finite_or_none(upnl_f), "entry_price": entry_f, "entry_price_fmt": entry_fmt, + "mark_price": mark_f, + "mark_price_fmt": mark_fmt, "price_tick": _finite_or_none(price_tick) if price_tick is not None else None, } ) diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index e7f425a..d6e210f 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -261,6 +261,18 @@ return fmtSymbolPrice(positionEntryPrice(pos), pos && pos.symbol, tickMap); } + function positionMarkPrice(pos) { + if (!pos) return null; + const n = Number(pos.mark_price); + if (!Number.isFinite(n) || n <= 0) return null; + return n; + } + + function fmtMarkPrice(pos, tickMap) { + if (pos && pos.mark_price_fmt) return String(pos.mark_price_fmt); + return fmtSymbolPrice(positionMarkPrice(pos), pos && pos.symbol, tickMap); + } + function pnlCls(v) { const n = Number(v); if (!Number.isFinite(n) || n === 0) return ""; @@ -1301,6 +1313,7 @@
${meta.map((m) => `${m}`).join("")}
开仓价${fmtEntryPrice(pos, tickMap)}
+
标记价${fmtMarkPrice(pos, tickMap)}
止损${sl != null && sl !== "" ? fmtSymbolPrice(sl, symbol, tickMap) : "—"}
止盈${tpMonitored ? "程序监控" : tp != null && tp !== "" ? fmtSymbolPrice(tp, symbol, tickMap) : "—"}
盈亏比${tpMonitored ? "—" : rr != null ? fmt(rr, 2) + ":1" : "-:1"}
@@ -1396,11 +1409,12 @@ const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : ""; return `
- +
合约方向开仓价张数浮盈操作
+
合约方向开仓价标记价张数浮盈操作
${symBeBadge} ${renderDirectionHtml(x.side)} ${fmtEntryPrice(x, tickMap)}${fmtMarkPrice(x, tickMap)} ${fmt(x.contracts, 4)} ${fmt(x.unrealized_pnl, 2)} diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 76a9032..51429b7 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -245,6 +245,6 @@
- + diff --git a/tests/test_hub_agent_mark_price.py b/tests/test_hub_agent_mark_price.py new file mode 100644 index 0000000..a7d023d --- /dev/null +++ b/tests/test_hub_agent_mark_price.py @@ -0,0 +1,32 @@ +"""子代理持仓:四所标记价字段统一解析。""" +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "manual_trading_hub")) + +from agent import _position_mark_price # noqa: E402 + + +class TestHubAgentMarkPrice(unittest.TestCase): + def test_binance_mark_price(self): + px = _position_mark_price({"markPrice": 65880.1, "info": {}}) + self.assertAlmostEqual(px, 65880.1) + + def test_okx_mark_px(self): + px = _position_mark_price({"info": {"markPx": "72.85"}}) + self.assertAlmostEqual(px, 72.85) + + def test_gate_info_mark(self): + px = _position_mark_price({"info": {"mark_price": "0.2241"}}) + self.assertAlmostEqual(px, 0.2241) + + def test_missing_returns_none(self): + self.assertIsNone(_position_mark_price({"info": {}})) + + +if __name__ == "__main__": + unittest.main()