feat(hub): show exchange mark price on monitor positions
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 @@
|
||||
<div class="pos-meta">${meta.map((m) => `<span class="pos-meta-item">${m}</span>`).join("")}</div>
|
||||
<div class="pos-grid">
|
||||
<div class="pos-cell"><span class="pos-label">开仓价</span><span class="pos-value">${fmtEntryPrice(pos, tickMap)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">标记价</span><span class="pos-value">${fmtMarkPrice(pos, tickMap)}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">止损</span><span class="pos-value">${sl != null && sl !== "" ? fmtSymbolPrice(sl, symbol, tickMap) : "—"}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">止盈</span><span class="pos-value">${tpMonitored ? "程序监控" : tp != null && tp !== "" ? fmtSymbolPrice(tp, symbol, tickMap) : "—"}</span></div>
|
||||
<div class="pos-cell"><span class="pos-label">盈亏比</span><span class="pos-value">${tpMonitored ? "—" : rr != null ? fmt(rr, 2) + ":1" : "-:1"}</span></div>
|
||||
@@ -1396,11 +1409,12 @@
|
||||
const symBeBadge = beSecured ? ` ${breakevenBadgeHtml()}` : "";
|
||||
return `<div class="pos-block">
|
||||
<div class="table-scroll">
|
||||
<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>开仓价</th><th>张数</th><th>浮盈</th><th>操作</th></tr></thead><tbody>
|
||||
<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>开仓价</th><th>标记价</th><th>张数</th><th>浮盈</th><th>操作</th></tr></thead><tbody>
|
||||
<tr>
|
||||
<td class="td-symbol"><button type="button" class="btn-open-market sym-link" ${mktAttrs} title="打开行情区(含入场/止盈止损)">${esc(x.symbol)}</button>${symBeBadge}</td>
|
||||
<td class="${sideDirCls(x.side)}">${renderDirectionHtml(x.side)}</td>
|
||||
<td class="td-entry">${fmtEntryPrice(x, tickMap)}</td>
|
||||
<td>${fmtMarkPrice(x, tickMap)}</td>
|
||||
<td>${fmt(x.contracts, 4)}</td>
|
||||
<td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 2)}</td>
|
||||
<td class="td-actions">
|
||||
|
||||
@@ -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=20260528-hub-no-30m"></script>
|
||||
<script src="/assets/app.js?v=20260603-hub-board-sse"></script>
|
||||
<script src="/assets/app.js?v=20260603-hub-mark-price"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user