Fix mark/PnL display fallback and intraday entry labels

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-06 22:19:42 +08:00
parent d4a76f05d2
commit 8e1492e4f0
7 changed files with 126 additions and 53 deletions
+19 -3
View File
@@ -7224,6 +7224,15 @@ def api_price_snapshot():
except Exception:
all_swap_positions = []
from lib.hub.price_snapshot_lib import resolve_order_snapshot_price, seed_prices_from_positions
seed_prices_from_positions(
prices,
order_rows,
all_swap_positions,
resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
)
key_prices = []
for r in key_rows:
is_fib = is_fib_key_monitor_type(r["monitor_type"])
@@ -7364,8 +7373,6 @@ def api_price_snapshot():
})
order_prices = []
from lib.hub.price_snapshot_lib import resolve_order_snapshot_price
for r in order_rows:
margin = float(r["margin_capital"] or 0)
leverage = float(r["leverage"] or 0)
@@ -7403,7 +7410,11 @@ def api_price_snapshot():
if ex_metrics.get("notional") is not None:
payload["exchange_notional"] = ex_metrics["notional"]
if ex_metrics.get("mark_price") is not None:
payload["exchange_mark_price"] = ex_metrics["mark_price"]
mp = ex_metrics["mark_price"]
payload["exchange_mark_price"] = mp
payload["exchange_mark_price_display"] = format_price_for_symbol(
r["symbol"], mp
)
if ex_metrics.get("unrealized_pnl") is not None:
payload["float_pnl"] = round(float(ex_metrics["unrealized_pnl"]), 2)
payload["pnl_source"] = "exchange"
@@ -7429,6 +7440,9 @@ def api_price_snapshot():
except Exception:
payload["price"] = px_for_fmt
payload["price_display"] = px_disp
if payload.get("exchange_mark_price") is None:
payload["exchange_mark_price"] = px_for_fmt
payload["exchange_mark_price_display"] = px_disp
else:
payload["price"] = None
payload["price_display"] = "-"
@@ -8288,6 +8302,8 @@ def add_order():
conn.close()
flash(style_err)
return redirect("/trade")
if entry_model:
trade_style = "trend"
available_usdt = get_available_trading_usdt()
live_price = get_price(symbol)
if live_price is None:
+6
View File
@@ -527,6 +527,12 @@ def register_hub_routes(app):
od = apply_order_monitor_source_labels(od)
except Exception:
pass
try:
from lib.trade.entry_model_lib import enrich_entry_model_display
enrich_entry_model_display(od)
except Exception:
pass
orders.append(od)
trends = []
if c.get("has_trend"):
+50 -1
View File
@@ -1,7 +1,7 @@
"""price_snapshot 共用:订单行情价兜底,避免 get_price 失败时整单不入 order_prices。"""
from __future__ import annotations
from typing import Any, Callable, Mapping, Optional
from typing import Any, Callable, Mapping, Optional, Sequence
from lib.hub.hub_position_metrics import parse_position_mark_price
@@ -73,3 +73,52 @@ def resolve_order_snapshot_price(
except (TypeError, ValueError):
pass
return None
def seed_prices_from_positions(
prices: dict[str, float],
order_rows: Sequence[Any],
all_positions: Sequence[dict[str, Any]],
*,
resolve_ex_sym_fn: Callable[[Any], str],
) -> None:
"""用持仓标记价补全 prices 字典(symbol 与 order_monitors 行对齐)。"""
if not all_positions or not order_rows:
return
try:
from lib.hub.hub_symbol_lib import symbols_match
except Exception:
symbols_match = None
for r in order_rows:
try:
sym = str(r["symbol"] or "").strip()
except (KeyError, TypeError, IndexError):
sym = ""
if not sym or sym in prices:
continue
try:
ex_sym = resolve_ex_sym_fn(r)
except Exception:
ex_sym = sym
try:
direction = str(r["direction"] or "long").lower()
except (KeyError, TypeError, IndexError):
direction = "long"
for p in all_positions:
if not isinstance(p, dict):
continue
ps = p.get("symbol") or ""
if not ps:
continue
matched = ps == sym or ps == ex_sym
if not matched and symbols_match is not None:
matched = symbols_match(sym, ps) or symbols_match(ex_sym, ps)
if not matched:
continue
side = (p.get("side") or "").lower()
if side and side != direction:
continue
mp = parse_position_mark_price(p)
if mp is not None and mp > 0:
prices[sym] = float(mp)
break
+36 -44
View File
@@ -891,6 +891,38 @@ function paintPriceTrend(el, key, value){
lastPriceMap[key] = value;
}
function formatOrderMarkDisplay(o){
const row = o || {};
const markRaw = row.exchange_mark_price;
const markNum = Number(markRaw);
const hasMark = markRaw !== null && markRaw !== undefined && markRaw !== "" && Number.isFinite(markNum) && markNum > 0;
if(hasMark){
if(row.exchange_mark_price_display && row.exchange_mark_price_display !== "-") return row.exchange_mark_price_display;
return markNum.toFixed(6);
}
if(row.price_display && row.price_display !== "-") return row.price_display;
const px = Number(row.price);
return Number.isFinite(px) && px > 0 ? px.toFixed(6) : "-";
}
function paintOrderMarkAndPnl(orderId, o){
const pEl = document.getElementById(`order-price-${orderId}`);
if(pEl){
const disp = formatOrderMarkDisplay(o);
pEl.innerText = disp;
const markNum = Number(o && o.exchange_mark_price);
const pxNum = Number.isFinite(markNum) && markNum > 0 ? markNum : Number(o && o.price);
paintPriceTrend(pEl, `o-${orderId}`, Number.isFinite(pxNum) ? pxNum : null);
}
const pnlEl = document.getElementById(`order-pnl-${orderId}`);
if(pnlEl){
pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`;
pnlEl.classList.remove("price-up","price-down","price-flat");
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down");
else pnlEl.classList.add("price-flat");
}
}
function refreshPriceSnapshot(){
fetch("/api/price_snapshot").then(r=>r.json()).then(data=>{
const updatedEl = document.getElementById("price-last-updated");
@@ -925,22 +957,7 @@ function refreshPriceSnapshot(){
}
});
(data.order_prices || []).forEach(o=>{
const pEl = document.getElementById(`order-price-${o.id}`);
if(pEl){
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
let disp = "";
if(hasMark && o.exchange_mark_price_display){
disp = o.exchange_mark_price_display;
} else if(o.price_display){
disp = o.price_display;
} else {
const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
disp = Number.isFinite(px) ? px.toFixed(6) : "-";
}
pEl.innerText = disp;
const pxNum = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
paintPriceTrend(pEl, `o-${o.id}`, Number.isFinite(pxNum) ? pxNum : px);
}
paintOrderMarkAndPnl(o.id, o);
const exM = document.getElementById(`order-ex-margin-${o.id}`);
if(exM){
const mv = o.exchange_initial_margin;
@@ -952,14 +969,6 @@ function refreshPriceSnapshot(){
exM.innerText = (prc === 0) ? "无仓数据" : "-";
}
}
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
if(pnlEl){
pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`;
pnlEl.classList.remove("price-up","price-down","price-flat");
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down");
else pnlEl.classList.add("price-flat");
}
const rrEl = document.getElementById(`order-rr-${o.id}`);
if(rrEl){
const rr = o.display_rr_ratio != null && o.display_rr_ratio !== "" ? o.display_rr_ratio : o.rr_ratio;
@@ -1225,19 +1234,10 @@ function refreshPriceSnapshotConditional(){
if(typeof paintKeyMonitorSummary === "function") paintKeyMonitorSummary(k.id, k);
});
}
if(page === "trade"){
const tradePage = page === "trade" || !!document.querySelector("[id^='order-price-']");
if(tradePage){
(data.order_prices || []).forEach(o=>{
const pEl = document.getElementById(`order-price-${o.id}`);
if(pEl){
const hasMark = (()=>{ const x = o.exchange_mark_price; if(x===null||x===undefined||x==="")return false; const n=Number(x); return !Number.isNaN(n); })();
let disp = "";
if(hasMark && o.exchange_mark_price_display) disp = o.exchange_mark_price_display;
else if(o.price_display) disp = o.price_display;
else { const px = hasMark ? Number(o.exchange_mark_price) : Number(o.price); disp = Number.isFinite(px) ? px.toFixed(6) : "-"; }
pEl.innerText = disp;
const pxNum = hasMark ? Number(o.exchange_mark_price) : Number(o.price);
paintPriceTrend(pEl, `o-${o.id}`, Number.isFinite(pxNum) ? pxNum : px);
}
paintOrderMarkAndPnl(o.id, o);
const exM = document.getElementById(`order-ex-margin-${o.id}`);
if(exM){
const mv = o.exchange_initial_margin;
@@ -1245,14 +1245,6 @@ function refreshPriceSnapshotConditional(){
if(!Number.isNaN(mn)) exM.innerText = `${mn.toFixed(2)}U`;
else { const prc = (typeof data.positions_raw_count === "number") ? data.positions_raw_count : null; exM.innerText = (prc === 0) ? "无仓数据" : "-"; }
}
const pnlEl = document.getElementById(`order-pnl-${o.id}`);
if(pnlEl){
pnlEl.innerText = `${formatSigned(o.float_pnl, 2)}U (${formatSigned(o.float_pct, 2)}%)`;
pnlEl.classList.remove("price-up","price-down","price-flat");
if(Number(o.float_pnl) > 0) pnlEl.classList.add("price-up");
else if(Number(o.float_pnl) < 0) pnlEl.classList.add("price-down");
else pnlEl.classList.add("price-flat");
}
const rrEl = document.getElementById(`order-rr-${o.id}`);
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
@@ -111,7 +111,7 @@
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% elif intraday_discipline %}开仓: —{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %}</span>
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
<span class="pos-meta-item {% if not intraday_discipline %}{% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}{% endif %}">
+1 -1
View File
@@ -224,7 +224,7 @@
</div>
<div class="pos-meta">
<span class="pos-meta-item">来源: {{ o.monitor_type|default('下单监控', true) }}{% if o.key_signal_type %} · {{ o.key_signal_type }}{% endif %}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">{% if o.entry_model_label %}开仓: {{ o.entry_model_label }}{% elif intraday_discipline %}开仓: —{% else %}风格: {{ '波段单' if o.trade_style == 'swing' else '趋势单' }}{% endif %}</span>
<span class="pos-meta-item">风险: {% if position_sizing_mode == 'full_margin' %}{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% else %}{{ o.risk_percent or '-' }}%≈{{ funds_fmt(o.risk_amount) if o.risk_amount is not none else '-' }}U{% endif %}</span>
<span class="pos-meta-item" id="order-latest-risk-wrap-{{ o.id }}" style="display:none">最新风险: —</span>
<span class="pos-meta-item {% if not intraday_discipline %}{% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}{% endif %}">
+13 -3
View File
@@ -1112,6 +1112,17 @@
return mt || "下单监控";
}
function monitorEntryStyleHtml(mo, intradayDiscipline) {
const o = mo || {};
if (o.entry_model_label) return `开仓: ${esc(o.entry_model_label)}`;
if (intradayDiscipline) return "开仓: —";
const ts = String(o.trade_style || "").toLowerCase();
if (ts === "swing") return "风格: 波段单";
if (ts === "trend") return "风格: 趋势单";
if (o.trade_style) return `风格: ${esc(o.trade_style)}`;
return "风格: —";
}
function monitorOrderSourceHtml(mo, trendPlan) {
if (isTrendContext(mo, trendPlan)) {
return `来源: ${esc(monitorOrderSourceLabel(mo, trendPlan))}`;
@@ -3073,8 +3084,7 @@
meta.push(`<span class="pos-meta-off">移动保本:关</span>`);
} else if (mo.monitor_type || mo.key_signal_type || mo.trend_plan_id) {
meta.push(monitorOrderSourceHtml(mo, trendPlan));
if (mo.trade_style) meta.push(`风格: ${esc(mo.trade_style)}`);
else meta.push("风格: —");
meta.push(monitorEntryStyleHtml(mo, intraday));
const riskLine = formatMonitorRiskMeta(mo, trendPlan);
if (riskLine) meta.push(riskLine);
const latestRiskLine = formatLatestRiskMeta(mo, trendPlan, pos, tpsl);
@@ -3188,7 +3198,7 @@
const fcBadge = o.force_close_enabled ? forceCloseSymbolBadgeHtml(o) : "";
return `<div class="hub-mini-card">
<div class="hub-mini-title">#${esc(o.id)} · ${esc(o.symbol || o.exchange_symbol)} ${tcBadge}${fcBadge} · ${renderDirectionHtml(o.direction)}</div>
<div class="hub-mini-line">触发 ${fmtSymbolPrice(o.trigger_price, sym, tickMap)} · SL ${fmtSymbolPrice(o.stop_loss, sym, tickMap)} · TP ${fmtSymbolPrice(o.take_profit, sym, tickMap)} · ${esc(o.trade_style || o.monitor_type || "下单监控")}</div>
<div class="hub-mini-line">触发 ${fmtSymbolPrice(o.trigger_price, sym, tickMap)} · SL ${fmtSymbolPrice(o.stop_loss, sym, tickMap)} · TP ${fmtSymbolPrice(o.take_profit, sym, tickMap)} · ${esc(o.entry_model_label || o.trade_style || o.monitor_type || "下单监控")}</div>
</div>`;
})
.join("");