余额
${fmt(ag.balance_usdt, 2)} U
浮盈合计
${fmt(ag.total_unrealized_pnl, 2)}
@@ -1140,7 +1245,8 @@
row.key || row.id,
p,
findMonitorOrder(orders, p.symbol, p.side),
- findTrendPlan(trends, p.symbol, p.side)
+ findTrendPlan(trends, p.symbol, p.side),
+ tickMap
)
)
.join("");
@@ -1150,7 +1256,8 @@
if (orders.length) {
inner += `
下单监控 · ${orders.length}
`;
orders.forEach((o) => {
- inner += `
${esc(o.symbol || o.exchange_symbol)} · ${renderDirectionHtml(o.direction)} · 触发 ${fmt(o.trigger_price, 4)}
`;
+ const sym = o.exchange_symbol || o.symbol || "";
+ inner += `
${esc(o.symbol || o.exchange_symbol)} · ${renderDirectionHtml(o.direction)} · 触发 ${fmtSymbolPrice(o.trigger_price, sym, tickMap)}
`;
});
}
if ((row.capabilities || []).includes("key")) {
@@ -1183,7 +1290,10 @@
if ((row.capabilities || []).includes("trend") && trends.length) {
inner += `
趋势回调 · ${trends.length}
`;
trends.forEach((t) => {
- inner += `
#${t.id} ${esc(t.symbol)} ${renderDirectionHtml(t.direction)} · SL ${t.stop_loss} · TP ${t.take_profit}
`;
+ const sym = t.exchange_symbol || t.symbol || "";
+ const sl = t.stop_loss_display || fmtSymbolPrice(t.stop_loss, sym, tickMap);
+ const tp = t.take_profit_display || fmtSymbolPrice(t.take_profit, sym, tickMap);
+ inner += `
#${t.id} ${esc(t.symbol)} ${renderDirectionHtml(t.direction)} · 均价 ${esc(t.avg_entry_price_display || fmtSymbolPrice(t.avg_entry_price, sym, tickMap))} · SL ${esc(sl)} · TP ${esc(tp)}${trendAddSummaryHtml(t, tickMap)}
`;
});
}
if (rolls.length) {
@@ -1197,6 +1307,7 @@
}
function renderFullscreenExchange(row) {
+ const tickMap = buildPriceTickMap(row);
const ag = row.agent || {};
const pos = Array.isArray(ag.positions) ? ag.positions : [];
const hm = row.hub_monitor || {};
@@ -1241,7 +1352,8 @@
row.key || row.id,
p,
findMonitorOrder(orders, p.symbol, p.side),
- findTrendPlan(trends, p.symbol, p.side)
+ findTrendPlan(trends, p.symbol, p.side),
+ tickMap
);
});
} else {
@@ -1259,11 +1371,11 @@
);
}
}
- html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders), "暂无运行中的下单监控");
+ html += renderHubSectionCard("下单监控", renderOrderMonitorSection(orders, tickMap), "暂无运行中的下单监控");
if ((row.capabilities || []).includes("trend")) {
- html += renderHubSectionCard("趋势回调", renderTrendSection(trends), "暂无运行中的趋势回调计划");
+ html += renderHubSectionCard("趋势回调", renderTrendSection(trends, tickMap), "暂无运行中的趋势回调计划");
}
- html += renderHubSectionCard("顺势加仓", renderRollSection(rolls), "暂无运行中的顺势加仓组");
+ html += renderHubSectionCard("顺势加仓", renderRollSection(rolls, tickMap), "暂无运行中的顺势加仓组");
return html;
}
diff --git a/strategy_trend_register.py b/strategy_trend_register.py
index b0327da..3990ad9 100644
--- a/strategy_trend_register.py
+++ b/strategy_trend_register.py
@@ -52,6 +52,7 @@ def install_strategy_trend(app: Flask, repo_root: str, app_module: Any = None, *
cfg = build_trend_config(app_module, **build_kw)
app.extensions["strategy_trend_cfg"] = cfg
register_trend_routes(app, cfg)
+ _patch_hub_monitor_enrich(app, cfg)
@app.context_processor
def _trend_ctx():
@@ -268,9 +269,95 @@ def _insert_preview_snapshot(conn, preview_id: str, created: str, exp_ms: int, p
)
+def _format_trend_price(cfg: dict, symbol: str, value) -> str:
+ if value in (None, ""):
+ return "—"
+ m = _m(cfg)
+ sym = symbol or ""
+ norm = getattr(m, "normalize_exchange_symbol", None)
+ if callable(norm):
+ try:
+ sym = norm(sym) or sym
+ except Exception:
+ pass
+ try:
+ m.ensure_markets_loaded()
+ return str(m.exchange.price_to_precision(sym, float(value)))
+ except Exception:
+ fn = getattr(m, "format_price_for_symbol", None)
+ if callable(fn):
+ return fn(symbol, value)
+ return str(value)
+
+
+def _trend_add_leg_fields(cfg: dict, d: dict) -> dict:
+ """解析已补仓次数与已触达网格价(供策略页与中控 monitor 共用)。"""
+ import json
+
+ out = dict(d)
+ try:
+ legs_done = int(out.get("legs_done") or 0)
+ except (TypeError, ValueError):
+ legs_done = 0
+ try:
+ dca_legs = int(out.get("dca_legs") or 0)
+ except (TypeError, ValueError):
+ dca_legs = 0
+ try:
+ grid = json.loads(out.get("grid_prices_json") or "[]")
+ if not isinstance(grid, list):
+ grid = []
+ except Exception:
+ grid = []
+ add_prices: list[float] = []
+ for x in grid[:legs_done]:
+ try:
+ add_prices.append(float(x))
+ except (TypeError, ValueError):
+ pass
+ sym = out.get("exchange_symbol") or out.get("symbol") or ""
+ out["add_count"] = legs_done
+ out["add_count_total"] = dca_legs
+ out["add_prices"] = add_prices
+ out["add_prices_display"] = [_format_trend_price(cfg, sym, p) for p in add_prices]
+ for field in ("stop_loss", "take_profit", "add_upper", "avg_entry_price"):
+ if out.get(field) not in (None, ""):
+ out[f"{field}_display"] = _format_trend_price(cfg, sym, out.get(field))
+ return out
+
+
+def enrich_trend_plan_for_hub(cfg: dict, raw: dict) -> dict:
+ """中控 /api/hub/monitor:补仓次数、加仓价(交易所精度)。"""
+ return _trend_add_leg_fields(cfg, dict(raw or {}))
+
+
+def _patch_hub_monitor_enrich(app: Flask, cfg: dict) -> None:
+ ctx = dict(app.config.get("HUB_CTX") or {})
+ prev = ctx.get("enrich_monitor")
+
+ def enrich_monitor(keys=None, orders=None, trends=None, rolls=None):
+ payload: dict[str, Any] = {}
+ if callable(prev):
+ try:
+ prev_out = prev(keys=keys, orders=orders, trends=trends, rolls=rolls)
+ if isinstance(prev_out, dict):
+ payload.update(prev_out)
+ except Exception:
+ pass
+ if trends:
+ payload["trends"] = [
+ enrich_trend_plan_for_hub(cfg, t) for t in trends if isinstance(t, dict)
+ ]
+ return payload
+
+ ctx["enrich_monitor"] = enrich_monitor
+ app.config["HUB_CTX"] = ctx
+
+
def enrich_trend_plan(cfg: dict, row) -> dict:
m = _m(cfg)
d = _row(cfg, row)
+ d = _trend_add_leg_fields(cfg, d)
try:
d["breakeven_applied"] = int(d.get("breakeven_applied") or 0) != 0
except Exception:
diff --git a/tests/test_hub_agent_entry_price.py b/tests/test_hub_agent_entry_price.py
new file mode 100644
index 0000000..6102b51
--- /dev/null
+++ b/tests/test_hub_agent_entry_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_entry_price # noqa: E402
+
+
+class TestHubAgentEntryPrice(unittest.TestCase):
+ def test_binance_entry_price(self):
+ px = _position_entry_price({"entryPrice": 65851.6, "info": {}})
+ self.assertAlmostEqual(px, 65851.6)
+
+ def test_okx_avg_px(self):
+ px = _position_entry_price({"info": {"avgPx": "72.731"}})
+ self.assertAlmostEqual(px, 72.731)
+
+ def test_gate_info_entry(self):
+ px = _position_entry_price({"info": {"entry_price": "0.2232"}})
+ self.assertAlmostEqual(px, 0.2232)
+
+ def test_missing_returns_none(self):
+ self.assertIsNone(_position_entry_price({"info": {}}))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_trend_hub_enrich.py b/tests/test_trend_hub_enrich.py
new file mode 100644
index 0000000..a2c30a0
--- /dev/null
+++ b/tests/test_trend_hub_enrich.py
@@ -0,0 +1,44 @@
+"""趋势回调中控 enrich:补仓次数与加仓价。"""
+from __future__ import annotations
+
+import json
+import sys
+import unittest
+from pathlib import Path
+from unittest.mock import MagicMock
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+
+from strategy_trend_register import _trend_add_leg_fields # noqa: E402
+
+
+class TestTrendHubEnrich(unittest.TestCase):
+ def test_add_count_and_prices(self):
+ mock_ex = MagicMock()
+ mock_ex.price_to_precision = lambda sym, px: f"{float(px):.4f}"
+ app_mod = MagicMock()
+ app_mod.exchange = mock_ex
+ app_mod.ensure_markets_loaded = MagicMock()
+ app_mod.normalize_exchange_symbol = lambda s: s
+ cfg = {"app_module": app_mod}
+ raw = {
+ "symbol": "ETH/USDT",
+ "exchange_symbol": "ETH/USDT:USDT",
+ "legs_done": 2,
+ "dca_legs": 5,
+ "grid_prices_json": json.dumps([1800.1, 1750.2, 1700.3]),
+ "stop_loss": 1600,
+ "take_profit": 2000,
+ "avg_entry_price": 1820.5,
+ }
+ out = _trend_add_leg_fields(cfg, raw)
+ self.assertEqual(out["add_count"], 2)
+ self.assertEqual(out["add_count_total"], 5)
+ self.assertEqual(out["add_prices"], [1800.1, 1750.2])
+ self.assertEqual(len(out["add_prices_display"]), 2)
+ self.assertEqual(out["stop_loss_display"], "1600.0000")
+
+
+if __name__ == "__main__":
+ unittest.main()