feat: 持仓快照盈亏比与交易所止损已保本标识

盈亏比固定用开仓 initial_stop_loss 计算,人工改委托后不变化;轮询交易所止损触发价相对成交价判定已保本,四所实例与中控统一显示绿色标识。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-03 16:31:03 +08:00
parent e265c1b31a
commit cf3e2ee1c9
13 changed files with 486 additions and 52 deletions
+19 -12
View File
@@ -94,6 +94,10 @@ from key_monitor_lib import (
rs_break_from_direction,
run_rs_level_alert_tick,
)
from order_monitor_display_lib import (
apply_order_price_display_fields,
enrich_order_display_fields,
)
from wechat_notify_lib import build_wechat_rs_level_message, send_wechat_webhook
from hub_auth import request_allowed as hub_request_allowed
from history_window_lib import (
@@ -2270,12 +2274,7 @@ def enrich_order_item(raw_item, current_capital):
ratio = round(margin / current_capital * 100, 2) if current_capital else 0
item["notional_value"] = notional
item["position_ratio"] = ratio
item["rr_ratio"] = calc_rr_ratio(
item.get("direction") or "long",
item.get("trigger_price"),
item.get("initial_stop_loss") or item.get("stop_loss"),
item.get("take_profit"),
)
enrich_order_display_fields(item, calc_rr_ratio)
try:
be = item.get("breakeven_enabled")
item["breakeven_enabled"] = 0 if be is not None and int(be) == 0 else 1
@@ -6253,7 +6252,7 @@ def api_price_snapshot():
entry = float(r["trigger_price"] or 0)
pnl = calc_pnl(r["direction"], entry, price, margin, leverage) if entry > 0 else 0
pnl_pct = round((pnl / margin * 100), 4) if margin > 0 else 0
rr_ratio = calc_rr_ratio(r["direction"], entry, r["initial_stop_loss"] or r["stop_loss"], r["take_profit"])
exchange_tpsl = {"sl": None, "tp": None}
ex_sym = resolve_monitor_exchange_symbol(r)
prow = _select_live_position_row(all_swap_positions, ex_sym, r["direction"])
lev_row = r["leverage"] if "leverage" in r.keys() else None
@@ -6263,7 +6262,6 @@ def api_price_snapshot():
"symbol": r["symbol"],
"float_pnl": round(pnl, 2),
"float_pct": pnl_pct,
"rr_ratio": rr_ratio,
"plan_margin": round(margin, 2) if margin else None,
"exchange_initial_margin": None,
"exchange_notional": None,
@@ -6298,16 +6296,25 @@ def api_price_snapshot():
payload["price_display"] = px_disp
if exchange_private_api_configured():
try:
payload["exchange_tpsl"] = fetch_exchange_tpsl_slots(
exchange_tpsl = fetch_exchange_tpsl_slots(
ex_sym,
r["direction"],
plan_sl=r["stop_loss"],
plan_tp=r["take_profit"],
)
except Exception:
payload["exchange_tpsl"] = {"sl": None, "tp": None}
else:
payload["exchange_tpsl"] = {"sl": None, "tp": None}
exchange_tpsl = {"sl": None, "tp": None}
payload["exchange_tpsl"] = exchange_tpsl
apply_order_price_display_fields(
payload,
direction=r["direction"],
entry_price=entry,
initial_stop_loss=r["initial_stop_loss"],
stop_loss=r["stop_loss"],
take_profit=r["take_profit"],
calc_rr_ratio_fn=calc_rr_ratio,
exchange_tpsl=exchange_tpsl,
)
order_prices.append(payload)
return jsonify({
+10
View File
@@ -178,6 +178,7 @@
.pos-meta-item:not(:last-child)::after{content:'|';margin:0 8px;color:#3d4659}
.pos-meta-on{color:#6eb5ff}
.pos-meta-off{color:#7d8799}
.pos-breakeven-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:.72rem;font-weight:600;background:#1a3d2e;color:#4cd97f}
.pos-card-symbol{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
.pos-card-symbol strong{font-size:.95rem;color:#fff;font-weight:600}
.pos-side-badge{padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:500;line-height:1.2}
@@ -491,6 +492,7 @@
<span class="pos-meta-item {% if o.breakeven_enabled %}pos-meta-on{% else %}pos-meta-off{% endif %}">
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
</span>
<span class="pos-meta-item" id="order-be-wrap-{{ o.id }}" style="display:none"><span class="pos-breakeven-badge">已保本</span></span>
</div>
<div class="pos-grid">
<div class="pos-cell">
@@ -1781,6 +1783,12 @@ function formatRrRatio(rr){
return `${body}:1`;
}
function paintBreakevenBadge(orderId, secured){
const wrap = document.getElementById(`order-be-wrap-${orderId}`);
if(!wrap) return;
wrap.style.display = secured ? "inline-flex" : "none";
}
function paintPriceTrend(el, key, value){
if(!el) return;
const prev = lastPriceMap[key];
@@ -1865,6 +1873,7 @@ function refreshPriceSnapshot(){
if(rrEl){
rrEl.innerText = formatRrRatio(o.rr_ratio);
}
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
if(o.exchange_tpsl) paintExchangeTpslRow(o.id, o.exchange_tpsl);
});
}).catch(()=>{});
@@ -2074,6 +2083,7 @@ function refreshPriceSnapshotConditional(){
}
const rrEl = document.getElementById(`order-rr-${o.id}`);
if(rrEl) rrEl.innerText = formatRrRatio(o.rr_ratio);
paintBreakevenBadge(o.id, o.sl_breakeven_secured);
paintExchangeTpslRow(o.id, o.exchange_tpsl || {});
});
}