diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 7209d8b..87ddd6e 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -1320,6 +1320,72 @@ body.market-chart-fs-open { border-color: rgba(0, 255, 157, 0.38); } +.hub-trend-plan-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.hub-trend-plan-card { + padding: 12px 14px; + background: rgba(0, 0, 0, 0.28); + border: 1px solid var(--border-soft); + border-radius: 10px; +} + +.hub-trend-plan-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; +} + +.hub-trend-plan-title { + font-size: 14px; + font-weight: 600; +} + +.hub-trend-plan-status { + font-size: 11px; + color: var(--muted); +} + +.hub-trend-plan-meta { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + font-size: 12px; + color: var(--muted); + margin-bottom: 10px; +} + +.hub-trend-plan-meta .pos-meta-accent, +.hub-trend-plan-meta strong { + color: var(--accent); +} + +.hub-trend-plan-foot { + margin-top: 10px; + font-size: 11px; + color: var(--muted); +} + +.pos-value.pos-tp-program { + color: #8fc8ff; +} + +.exchange-fullscreen .hub-trend-plan-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 14px; + align-items: stretch; +} + +.exchange-fullscreen .hub-trend-plan-card { + height: 100%; +} + /* 顺势加仓 */ .card-stat-chip.card-stat-roll { color: #ffb020; diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 1717890..c972298 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -303,19 +303,34 @@ return side || "—"; } - function monitorOrderSourceLabel(mo) { + function isTrendContext(monitorOrder, trendPlan) { + const mo = monitorOrder || {}; + const tp = trendPlan || {}; + if (tp.id != null && Number(tp.id) > 0) return true; + const tid = Number(mo.trend_plan_id); + if (Number.isFinite(tid) && tid > 0) return true; + const mt = String(mo.monitor_type || "").trim(); + if (mt === "趋势回调") return true; + const kst = String(mo.key_signal_type || "").trim(); + return kst === "趋势回调" || kst === "趋势回调计划"; + } + + function trendAddZoneLabel(direction) { + return (direction || "long").toLowerCase() === "short" ? "补仓下沿" : "补仓上沿"; + } + + function monitorOrderSourceLabel(mo, trendPlan) { + if (isTrendContext(mo, trendPlan)) return "趋势回调计划"; const o = mo || {}; - const tid = Number(o.trend_plan_id); - if (Number.isFinite(tid) && tid > 0) return "趋势回调"; const mt = String(o.monitor_type || "").trim(); - if (mt === "趋势回调") return "趋势回调"; - const kst = String(o.key_signal_type || "").trim(); - if (kst === "趋势回调" || kst === "趋势回调计划") return "趋势回调"; return mt || "下单监控"; } - function monitorOrderSourceHtml(mo) { - const src = monitorOrderSourceLabel(mo); + function monitorOrderSourceHtml(mo, trendPlan) { + if (isTrendContext(mo, trendPlan)) { + return `来源: ${esc(monitorOrderSourceLabel(mo, trendPlan))}`; + } + const src = monitorOrderSourceLabel(mo, trendPlan); const kst = String((mo && mo.key_signal_type) || "").trim(); let text = src; if (kst && kst !== src && !text.includes(kst)) { @@ -776,7 +791,23 @@ return reward / risk; } - function resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored) { + function resolveTrendPlanRr(trendPlan, side, entry, sl, tp) { + const t = trendPlan || {}; + if (t.planned_rr != null && t.planned_rr !== "") { + const n = Number(t.planned_rr); + if (Number.isFinite(n) && n > 0) return n; + } + const e = t.avg_entry_price != null && t.avg_entry_price !== "" ? t.avg_entry_price : entry; + const s = t.stop_loss != null && t.stop_loss !== "" ? t.stop_loss : sl; + const p = t.take_profit != null && t.take_profit !== "" ? t.take_profit : tp; + return calcRrRatio(side, e, s, p); + } + + function resolveSnapshotRr(mo, side, entry, sl, tp, tpMonitored, trendPlan) { + if (tpMonitored && isTrendContext(mo, trendPlan)) { + const rr = resolveTrendPlanRr(trendPlan, side, entry, sl, tp); + if (rr != null) return rr; + } if (tpMonitored) return null; const snap = mo && mo.rr_ratio; if (snap != null && snap !== "") { @@ -787,6 +818,17 @@ return calcRrRatio(side, entry, initSl || sl, tp); } + function formatTpCellValue(tp, tpMonitored, symbol, tickMap) { + if (tpMonitored) { + if (tp != null && tp !== "") { + return `程序监控 · ${fmtSymbolPrice(tp, symbol, tickMap)}`; + } + return "程序监控"; + } + if (tp != null && tp !== "") return fmtSymbolPrice(tp, symbol, tickMap); + return "—"; + } + function isBreakevenSecured(side, entry, monitorOrder, cond, pos) { const mo = monitorOrder || {}; const p = pos || {}; @@ -1072,10 +1114,7 @@ ? mo.trigger_price : tp.avg_entry_price; const entryN = entryRaw != null && entryRaw !== "" ? Number(entryRaw) : null; - const isTrend = - !!(trendPlan && trendPlan.id) || - String(mo.monitor_type || "").trim() === "趋势回调" || - (mo.trend_plan_id != null && Number(mo.trend_plan_id) > 0); + const isTrend = isTrendContext(mo, trendPlan); let sl = mo.stop_loss != null && mo.stop_loss !== "" ? mo.stop_loss : ""; let takeProfit = mo.take_profit != null && mo.take_profit !== "" ? mo.take_profit : ""; @@ -1083,10 +1122,14 @@ if (isTrend) { tpMonitored = true; - takeProfit = ""; if (trendPlan && trendPlan.stop_loss != null && trendPlan.stop_loss !== "") { sl = trendPlan.stop_loss; } + if (trendPlan && trendPlan.take_profit != null && trendPlan.take_profit !== "") { + takeProfit = trendPlan.take_profit; + } else { + takeProfit = ""; + } } const inferred = inferTpslFromCondOrders(pos.side, cond, entryN); @@ -1401,20 +1444,79 @@ return html; } + function renderTrendPlanCard(t, tickMap) { + const sym = t.exchange_symbol || t.symbol || ""; + const side = (t.direction || "long").toLowerCase(); + const sl = t.stop_loss_display || fmtSymbolPrice(t.stop_loss, sym, tickMap); + const tp = t.take_profit_display || fmtSymbolPrice(t.take_profit, sym, tickMap); + const avg = t.avg_entry_price_display || fmtSymbolPrice(t.avg_entry_price, sym, tickMap); + const addZone = + t.add_upper_display || fmtSymbolPrice(t.add_upper, sym, tickMap) || "—"; + const rr = resolveTrendPlanRr(t, side, t.avg_entry_price, t.stop_loss, t.take_profit); + const rrTxt = rr != null ? `${fmt(rr, 2)}:1` : "—"; + const mark = + t.floating_mark != null && t.floating_mark !== "" + ? fmtSymbolPrice(t.floating_mark, sym, tickMap) + : "—"; + const legsDone = t.add_count != null ? t.add_count : t.legs_done; + const legsTotal = t.add_count_total != null ? t.add_count_total : t.dca_legs; + const legsTxt = + legsDone != null && legsTotal != null + ? `${esc(legsDone)}/${esc(legsTotal)}` + : legsDone != null + ? esc(legsDone) + : "—"; + let pnlInner = "—"; + if (t.floating_pnl != null && t.floating_pnl !== "") { + let pnlText = `${fmt(t.floating_pnl, 2)}U`; + const margin = Number(t.plan_margin_capital); + if (Number.isFinite(margin) && margin > 0) { + const pct = (Number(t.floating_pnl) / margin) * 100; + pnlText += ` (${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%)`; + } + pnlInner = `${esc(pnlText)}`; + } + const riskTxt = + t.risk_percent != null && t.risk_percent !== "" ? `${esc(t.risk_percent)}%` : "—"; + const foot = []; + if (t.snapshot_available_usdt != null) { + foot.push(`快照可用 ${fmt(t.snapshot_available_usdt, 2)}U`); + } + if (t.plan_margin_capital != null) { + foot.push(`计划保证金≈${fmt(t.plan_margin_capital, 2)}U`); + } + if (t.leverage != null) foot.push(`杠杆 ${esc(t.leverage)}x`); + const footHtml = foot.length + ? `