fix: 中控持仓卡合并最新风险与保证金展示

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-02 22:19:07 +08:00
parent 687a34474d
commit 0b8f410fbe
2 changed files with 172 additions and 66 deletions
+132 -76
View File
@@ -1658,67 +1658,60 @@ def _exchange_tpsl_from_hub_order(hub_orders: list, symbol: str, side: str) -> d
return None
def _find_exchange_tpsl_for_position(
symbol: str,
side: str,
order_prices: list,
hub_orders: list,
) -> dict | None:
side_l = (side or "").lower()
op_by_id = {
op.get("id"): op
for op in order_prices
if isinstance(op, dict) and op.get("id") is not None
}
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(symbol, o_sym):
continue
if (o.get("direction") or "").lower() != side_l:
continue
op = op_by_id.get(o.get("id"))
if not isinstance(op, dict):
continue
et = op.get("exchange_tpsl")
if isinstance(et, dict) and (et.get("sl") or et.get("tp")):
return et
def _order_price_op_indexes(order_prices: list) -> tuple[dict, list]:
"""price_snapshot order_pricesid 可能为 int/str,需双键索引。"""
by_id: dict = {}
flat: list = []
for op in order_prices:
if not isinstance(op, dict):
continue
if not _symbols_match(symbol, op.get("symbol") or ""):
flat.append(op)
oid = op.get("id")
if oid is None:
continue
et = op.get("exchange_tpsl")
if isinstance(et, dict) and (et.get("sl") or et.get("tp")):
return et
by_id[oid] = op
by_id[str(oid)] = op
try:
by_id[int(oid)] = op
except (TypeError, ValueError):
pass
return by_id, flat
def _match_order_price_op(
order_row: dict,
by_id: dict,
order_prices: list,
) -> dict | None:
if not isinstance(order_row, dict):
return None
oid = order_row.get("id")
if oid is not None:
for key in (oid, str(oid)):
op = by_id.get(key)
if isinstance(op, dict):
return op
try:
op = by_id.get(int(oid))
if isinstance(op, dict):
return op
except (TypeError, ValueError):
pass
sym = order_row.get("exchange_symbol") or order_row.get("symbol") or ""
direction = (order_row.get("direction") or "").lower()
for op in order_prices:
if not isinstance(op, dict):
continue
if not _symbols_match(sym, op.get("symbol") or ""):
continue
op_dir = (op.get("direction") or "").lower()
if direction and op_dir and direction != op_dir:
continue
return op
return None
def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) -> None:
"""将 price_snapshot 中的快照盈亏比、已保本状态合并进 hub_monitor.orders。"""
if not isinstance(hub_mon, dict) or not isinstance(snap, dict):
return
order_prices = snap.get("order_prices") or []
op_by_id = {
op.get("id"): op
for op in order_prices
if isinstance(op, dict) and op.get("id") is not None
}
orders = hub_mon.get("orders") or []
if not isinstance(orders, list):
return
for o in orders:
if not isinstance(o, dict):
continue
op = op_by_id.get(o.get("id"))
if not isinstance(op, dict):
continue
if op.get("rr_ratio") is not None:
o["rr_ratio"] = op["rr_ratio"]
if "sl_breakeven_secured" in op:
o["sl_breakeven_secured"] = bool(op["sl_breakeven_secured"])
for key in (
_ORDER_PRICE_MERGE_KEYS = (
"stop_loss",
"take_profit",
"stop_loss_display",
@@ -1734,13 +1727,81 @@ def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) ->
"time_close_label",
"time_close_countdown",
"time_close_remaining_sec",
):
if key in op and op[key] not in (None, ""):
o[key] = op[key]
)
def _apply_order_price_op_fields(target: dict, op: dict) -> None:
if not isinstance(target, dict) or not isinstance(op, dict):
return
if op.get("rr_ratio") is not None:
target["rr_ratio"] = op["rr_ratio"]
if "sl_breakeven_secured" in op:
target["sl_breakeven_secured"] = bool(op["sl_breakeven_secured"])
for key in _ORDER_PRICE_MERGE_KEYS:
if key not in op:
continue
val = op[key]
if key == "latest_risk_amount":
if val is not None and val != "":
target[key] = val
continue
if val not in (None, ""):
target[key] = val
def _find_exchange_tpsl_for_position(
symbol: str,
side: str,
order_prices: list,
hub_orders: list,
) -> dict | None:
side_l = (side or "").lower()
by_id, flat = _order_price_op_indexes(order_prices)
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(symbol, o_sym):
continue
if (o.get("direction") or "").lower() != side_l:
continue
op = _match_order_price_op(o, by_id, flat)
if not isinstance(op, dict):
continue
et = op.get("exchange_tpsl")
if isinstance(et, dict) and (et.get("sl") or et.get("tp")):
return et
for op in flat:
if not isinstance(op, dict):
continue
if not _symbols_match(symbol, op.get("symbol") or ""):
continue
et = op.get("exchange_tpsl")
if isinstance(et, dict) and (et.get("sl") or et.get("tp")):
return et
return None
def _merge_flask_order_price_fields(hub_mon: dict | None, snap: dict | None) -> None:
"""将 price_snapshot 中的快照盈亏比、已保本状态合并进 hub_monitor.orders。"""
if not isinstance(hub_mon, dict) or not isinstance(snap, dict):
return
order_prices = snap.get("order_prices") or []
by_id, flat = _order_price_op_indexes(order_prices)
orders = hub_mon.get("orders") or []
if not isinstance(orders, list):
return
for o in orders:
if not isinstance(o, dict):
continue
op = _match_order_price_op(o, by_id, flat)
if not isinstance(op, dict):
continue
_apply_order_price_op_fields(o, op)
def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon: dict | None) -> None:
"""将 price_snapshot 的已保本状态同步到 agent 持仓,供中控首页表格展示"""
"""将 price_snapshot 的已保本、最新风险、保证金等同步到 agent 持仓。"""
ag = agent_row.get("agent")
if not isinstance(ag, dict) or not isinstance(snap, dict):
return
@@ -1748,14 +1809,10 @@ def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon:
if not isinstance(positions, list) or not positions:
return
order_prices = snap.get("order_prices") or []
by_id, flat = _order_price_op_indexes(order_prices)
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):
continue
@@ -1770,18 +1827,22 @@ def _merge_flask_position_breakeven(agent_row: dict, snap: dict | None, hub_mon:
continue
if (o.get("direction") or "").lower() != side:
continue
matched = op_by_id.get(o.get("id"))
matched = _match_order_price_op(o, by_id, flat)
if isinstance(matched, dict):
break
if o.get("latest_risk_amount") is not None or o.get("exchange_initial_margin") is not None:
matched = o
break
if matched is None:
for op in order_prices:
for op in flat:
if not isinstance(op, dict):
continue
if not _symbols_match(sym, op.get("symbol") or ""):
continue
matched = op
break
if isinstance(matched, dict) and "sl_breakeven_secured" in matched:
p["sl_breakeven_secured"] = bool(matched["sl_breakeven_secured"])
if isinstance(matched, dict):
_apply_order_price_op_fields(p, matched)
def _agent_position_has_mark(p: dict) -> bool:
@@ -1809,10 +1870,10 @@ 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()
by_id, flat = _order_price_op_indexes(order_prices)
for o in hub_orders:
if not isinstance(o, dict):
continue
@@ -1821,11 +1882,11 @@ def _find_matched_order_price_op(
continue
if (o.get("direction") or "").lower() != side:
continue
matched = op_by_id.get(o.get("id"))
matched = _match_order_price_op(o, by_id, flat)
if isinstance(matched, dict):
return matched
break
for op in order_prices:
for op in flat:
if not isinstance(op, dict):
continue
if not _symbols_match(sym, op.get("symbol") or ""):
@@ -1848,15 +1909,10 @@ def _merge_flask_position_mark_price(
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)
matched = _find_matched_order_price_op(p, order_prices, hub_orders)
if isinstance(matched, dict):
_apply_agent_mark_price(
p,
+57 -7
View File
@@ -706,11 +706,17 @@
return null;
}
function resolveTrendSizingFooter(mo, trendPlan, isTrend) {
function resolveTrendSizingFooter(mo, trendPlan, isTrend, pos) {
const m = mo || {};
const p = pos || {};
if (!isTrend || !trendPlan || !trendPlan.id) {
return {
margin: m.exchange_initial_margin ?? m.plan_margin ?? null,
margin:
m.exchange_initial_margin ??
p.exchange_initial_margin ??
m.plan_margin ??
p.plan_margin ??
null,
leverage: m.leverage,
planBase: m.margin_capital,
positionRatio: m.position_ratio,
@@ -788,15 +794,59 @@
hubHoldDurationTimer = setInterval(tickHubHoldDurations, 1000);
}
function formatLatestRiskMeta(mo, trendPlan) {
function estimateLatestRiskUsdt(side, entry, sl, pos, mo) {
const e = Number(entry);
const s = Number(sl);
if (!Number.isFinite(e) || !Number.isFinite(s) || e <= 0) return null;
const sd = (side || "long").toLowerCase();
const rf = sd === "short" ? (s - e) / e : (e - s) / e;
if (!Number.isFinite(rf)) return null;
if (rf <= 0) return 0;
const m = mo || {};
const p = pos || {};
let notional = Number(p.notional_usdt);
if (!Number.isFinite(notional) || notional <= 0) {
notional = Number(m.exchange_notional);
}
if (!Number.isFinite(notional) || notional <= 0) {
const mc = Number(m.margin_capital);
const lev = Number(m.leverage);
if (Number.isFinite(mc) && mc > 0 && Number.isFinite(lev) && lev > 0) {
notional = mc * lev;
}
}
if (!Number.isFinite(notional) || notional <= 0) {
const c = Math.abs(Number(p.contracts));
const cs = Number(p.contract_size);
const mult = Number.isFinite(cs) && cs > 0 ? cs : 1;
const px = Number(p.mark_price);
const mark = Number.isFinite(px) && px > 0 ? px : e;
if (Number.isFinite(c) && c > 0) notional = c * mult * mark;
}
if (!Number.isFinite(notional) || notional <= 0) return null;
return Math.round(notional * rf * 100) / 100;
}
function formatLatestRiskMeta(mo, trendPlan, pos, tpsl) {
const m = mo || {};
const t = trendPlan || {};
const v =
let v =
m.latest_risk_amount != null && m.latest_risk_amount !== ""
? Number(m.latest_risk_amount)
: pos && pos.latest_risk_amount != null && pos.latest_risk_amount !== ""
? Number(pos.latest_risk_amount)
: t.latest_risk_amount != null && t.latest_risk_amount !== ""
? Number(t.latest_risk_amount)
: null;
if ((v == null || !Number.isFinite(v)) && tpsl && pos) {
v = estimateLatestRiskUsdt(
pos.side || m.direction,
tpsl.entry,
tpsl.sl,
pos,
m
);
}
if (v != null && Number.isFinite(v)) {
return `最新风险: ${fmt(v, 2)}U`;
}
@@ -2837,7 +2887,7 @@
const upnl = resolveTrendFloatingPnl(pos, trendPlan);
const pnlFmt = formatFloatingPnlText(upnl, pos.notional_usdt);
const pnlText = pnlFmt.text;
const sizingFoot = resolveTrendSizingFooter(mo, trendPlan, isTrend);
const sizingFoot = resolveTrendSizingFooter(mo, trendPlan, isTrend, pos);
const openMeta = resolvePositionOpenMeta(mo, trendPlan, isTrend);
const marginText =
sizingFoot.margin != null && sizingFoot.margin !== "" && Number.isFinite(Number(sizingFoot.margin))
@@ -2855,7 +2905,7 @@
meta.push(monitorOrderSourceHtml(mo, trendPlan));
const riskLine = formatMonitorRiskMeta(mo, trendPlan);
if (riskLine) meta.push(riskLine);
const latestRiskLine = formatLatestRiskMeta(mo, trendPlan);
const latestRiskLine = formatLatestRiskMeta(mo, trendPlan, pos, tpsl);
if (latestRiskLine) meta.push(latestRiskLine);
if (trendPlan && trendPlan.id) {
const zone =
@@ -2875,7 +2925,7 @@
else meta.push("风格: —");
const riskLine = formatMonitorRiskMeta(mo, trendPlan);
if (riskLine) meta.push(riskLine);
const latestRiskLine = formatLatestRiskMeta(mo, trendPlan);
const latestRiskLine = formatLatestRiskMeta(mo, trendPlan, pos, tpsl);
if (latestRiskLine) meta.push(latestRiskLine);
const beOn = mo.breakeven_enabled === 1 || mo.breakeven_enabled === true;
meta.push(