diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 1fdcdf1..48cdf7c 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -1320,139 +1320,222 @@ body.market-chart-fs-open { border-color: rgba(0, 255, 157, 0.38); } -.hub-trend-plan-list { +/* 趋势回调:与四所实例 strategy_trend_panel 同款卡片 */ +.hub-trend-running-title { + margin: 0 0 10px; + font-size: 0.95rem; + color: #b8c4ff; + font-weight: 600; +} + +.hub-trend-plan-list.running-plans-stack { display: flex; flex-direction: column; gap: 12px; } -.hub-trend-plan-card { +.hub-trend-plan-card.plan-position-card { + background: #141a2a; + border: 1px solid #2a3150; + border-radius: 12px; padding: 12px 14px; - background: rgba(0, 0, 0, 0.28); - border: 1px solid var(--border-soft); - border-radius: 10px; } -.hub-trend-plan-head { +.hub-trend-plan-card .plan-card-head { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; - gap: 8px; + gap: 10px; + flex-wrap: wrap; 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 { +.hub-trend-plan-card .plan-card-title { display: flex; + align-items: center; + gap: 8px; flex-wrap: wrap; - gap: 8px 14px; - font-size: 12px; - color: var(--muted); + font-size: 1rem; + font-weight: 700; + color: #f0f2ff; +} + +.hub-trend-plan-card .plan-card-meta { + font-size: 0.76rem; + color: #8892b0; + line-height: 1.55; margin-bottom: 10px; } -.hub-trend-plan-meta .pos-meta-accent, -.hub-trend-plan-meta strong { +.hub-trend-plan-card .plan-card-meta .accent { + color: #6ab8ff; +} + +.hub-trend-plan-card .plan-card-meta strong { color: var(--accent); } -.hub-trend-plan-foot { - margin-top: 10px; - font-size: 11px; - color: var(--muted); +.hub-trend-plan-card .plan-card-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px 14px; + margin-bottom: 10px; } -.pos-value.pos-tp-program { - color: #8fc8ff; -} - -.hub-trend-plan-card--horizontal { +.hub-trend-plan-card .plan-cell { display: flex; - flex-wrap: wrap; - gap: 14px 18px; - align-items: flex-start; - background: linear-gradient(145deg, rgba(12, 18, 32, 0.92), rgba(8, 12, 22, 0.88)); - border-color: rgba(0, 212, 255, 0.22); - box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35); + flex-direction: column; + gap: 3px; } -.hub-trend-plan-body { - flex: 1 1 320px; - min-width: 0; +.hub-trend-plan-card .plan-cell .lbl { + font-size: 0.72rem; + color: #8b95b8; } -.hub-trend-plan-side { - flex: 1 1 220px; - min-width: 200px; - max-width: 100%; +.hub-trend-plan-card .plan-cell .val { + color: #f0f2ff; + font-size: 0.88rem; + font-weight: 500; } -.hub-trend-dca-block { - padding: 8px 10px; - background: rgba(0, 0, 0, 0.22); - border: 1px solid var(--border-soft); +.hub-trend-plan-card .plan-cell .val.pnl-profit { + color: #4cd97f; +} + +.hub-trend-plan-card .plan-cell .val.pnl-loss { + color: #ff6666; +} + +.hub-trend-plan-card .plan-cell .val.pnl-neutral { + color: #cfd3ef; +} + +.hub-trend-plan-card .btn-close-plan { + padding: 7px 14px; + background: #5c1e2a; + color: #ffb4b4; + border: none; border-radius: 8px; + cursor: pointer; + font-size: 0.82rem; + font-weight: 600; + text-decoration: none; + white-space: nowrap; + display: inline-block; } -.hub-trend-dca-title { - font-size: 11px; - color: var(--muted); - margin-bottom: 6px; +.hub-trend-plan-card .btn-close-plan:hover { + filter: brightness(1.08); } -.hub-trend-dca-table { +.hub-trend-plan-card .plan-dca-block { + margin-top: 12px; + padding-top: 10px; + border-top: 1px dashed #2a3558; +} + +.hub-trend-plan-card .plan-dca-title { + font-size: 0.74rem; + color: #8b95b8; + margin-bottom: 8px; +} + +.hub-trend-plan-card .plan-dca-table { width: 100%; border-collapse: collapse; - font-size: 11px; + font-size: 0.76rem; } -.hub-trend-dca-table th, -.hub-trend-dca-table td { - padding: 4px 6px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); +.hub-trend-plan-card .plan-dca-table th, +.hub-trend-plan-card .plan-dca-table td { + padding: 6px 8px; + border-bottom: 1px solid #243050; text-align: left; } -.hub-trend-dca-table th { - color: var(--muted); +.hub-trend-plan-card .plan-dca-table th { + color: #6a7598; font-weight: 600; } -.hub-trend-dca-table .dca-done { - color: var(--green); +.hub-trend-plan-card .plan-dca-table .st-done { + color: #4cd97f; } -.hub-trend-dca-table .dca-pending { - color: var(--muted); +.hub-trend-plan-card .plan-dca-table .st-pending { + color: #9aa3c4; } -.exchange-fullscreen .hub-trend-plan-list { - display: block; +.hub-trend-plan-card .hub-plan-breakeven-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; + margin-top: 8px; +} + +.hub-trend-plan-card .hub-plan-be-label { + font-size: 0.78rem; + color: #cfd3ef; + display: flex; + align-items: center; + gap: 6px; +} + +.hub-trend-plan-card .hub-plan-be-input { + width: 72px; + padding: 4px 8px; + border-radius: 6px; + border: 1px solid #304164; + background: #0f1424; + color: #cfd3ef; + opacity: 0.85; +} + +.hub-trend-plan-card .hub-plan-be-btn { + padding: 6px 12px; + background: #1f4a3a; + color: #8fc8ff; + border-radius: 8px; + font-size: 0.78rem; + text-decoration: none; + cursor: pointer; + white-space: nowrap; +} + +.hub-trend-plan-card .hub-plan-be-btn--static { + cursor: default; +} + +.hub-trend-plan-card .hub-plan-be-done { + color: #6ab88a; + font-size: 0.75rem; +} + +.hub-trend-plan-card .hub-plan-account-foot { + margin-bottom: 0; +} + +.hub-trend-plan-card .badge.direction-long { + color: #4cd97f; + border-color: rgba(76, 217, 127, 0.45); +} + +.hub-trend-plan-card .badge.direction-short { + color: #ff6666; + border-color: rgba(255, 102, 102, 0.45); +} + +.exchange-fullscreen .hub-trend-plan-card.plan-position-card { + width: 100%; max-width: 100%; } -.exchange-fullscreen .hub-trend-plan-card--horizontal { - width: 100%; -} - -.exchange-fullscreen .hub-trend-plan-metrics-row { - display: flex; - flex-wrap: wrap; - align-items: flex-end; - gap: 10px 16px; -} - -.exchange-fullscreen .hub-trend-plan-grid { - flex: 1 1 280px; +@media (max-width: 720px) { + .hub-trend-plan-card .plan-card-grid { + grid-template-columns: 1fr; + } } /* 顺势加仓 */ diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index f95e033..79a84d0 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -345,6 +345,76 @@ return { text: pnlText, cls: pnlCls(upnl) }; } + /** 与实例策略页一致:浮盈亏 % = 浮盈亏 / 计划保证金 */ + function formatTrendPlanFloatingPnl(upnl, planMargin) { + if (upnl == null || !Number.isFinite(Number(upnl))) { + return { text: "—", cls: "" }; + } + let pnlText = fmt(upnl, 2) + "U"; + const margin = Number(planMargin); + if (Number.isFinite(margin) && margin > 0) { + const pct = (Number(upnl) / margin) * 100; + pnlText += ` (${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%)`; + } + const n = Number(upnl); + let cls = "pnl-neutral"; + if (n > 0) cls = "pnl-profit"; + else if (n < 0) cls = "pnl-loss"; + return { text: pnlText, cls }; + } + + function renderDirectionBadge(side) { + const s = normSide(side); + const label = sideDirLabel(side); + const cls = s === "long" ? "direction-long" : s === "short" ? "direction-short" : ""; + if (!cls) return esc(String(label)); + return `${esc(label)}`; + } + + function resolveTrendDcaLevels(t) { + if (Array.isArray(t.dca_levels) && t.dca_levels.length) return t.dca_levels; + const plan = t || {}; + let grid = []; + let legAmounts = []; + try { + grid = JSON.parse(plan.grid_prices_json || "[]"); + if (!Array.isArray(grid)) grid = []; + } catch (_e) { + grid = []; + } + try { + legAmounts = JSON.parse(plan.leg_amounts_json || "[]"); + if (!Array.isArray(legAmounts)) legAmounts = []; + } catch (_e2) { + legAmounts = []; + } + const legsDone = Number(plan.legs_done) || 0; + const dcaLegs = Number(plan.dca_legs) || 0; + const firstDone = Number(plan.first_order_done) !== 0; + const out = [ + { + label: "首仓", + price: null, + contracts: plan.first_order_amount, + status: firstDone ? "done" : "pending", + status_label: firstDone ? "已开仓" : "待开仓", + }, + ]; + const n = Math.max(grid.length, legAmounts.length, dcaLegs); + for (let idx = 0; idx < n; idx += 1) { + const legI = idx + 1; + const done = legI <= legsDone; + out.push({ + label: `补仓${legI}`, + price: idx < grid.length ? grid[idx] : null, + contracts: idx < legAmounts.length ? legAmounts[idx] : null, + status: done ? "done" : "pending", + status_label: done ? "已补仓" : "待补仓", + }); + } + return out; + } + function pnlCls(v) { const n = Number(v); if (!Number.isFinite(n) || n === 0) return ""; @@ -1333,6 +1403,8 @@ btn.onclick = (ev) => { ev.preventDefault(); ev.stopPropagation(); + const msg = (btn.dataset.confirm || "").trim(); + if (msg && !confirm(msg)) return; openInstance(btn.dataset.exId, btn.dataset.next || "/", { newTab: ev.ctrlKey || ev.metaKey, }); @@ -1514,7 +1586,7 @@ } function renderTrendDcaTable(t, tickMap) { - const levels = Array.isArray(t.dca_levels) ? t.dca_levels : []; + const levels = resolveTrendDcaLevels(t); if (!levels.length) return ""; const sym = t.exchange_symbol || t.symbol || ""; const rows = levels @@ -1525,7 +1597,7 @@ : "—"; const amt = lv.contracts != null && lv.contracts !== "" ? esc(String(lv.contracts)) : "—"; - const stCls = lv.status === "done" ? "dca-done" : "dca-pending"; + const stCls = lv.status === "done" ? "st-done" : "st-pending"; const label = lv.status_label || (lv.status === "done" ? "已补仓" : "待补仓"); return ` ${esc(lv.label || lv.leg_key || "—")} @@ -1535,16 +1607,16 @@ `; }) .join(""); - return `
-
补仓计划明细
- - - ${rows} + return `
+
补仓计划明细
+
档位触发价张数状态
+ + ${rows}
档位触发价张数状态
`; } - function renderTrendPlanCard(t, tickMap, pos) { + function renderTrendPlanCard(t, tickMap, pos, exchangeRow) { 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); @@ -1564,66 +1636,79 @@ ? esc(legsDone) : "—"; const upnlTrend = resolveTrendFloatingPnl(pos, t); - const notional = - pos && pos.notional_usdt != null - ? pos.notional_usdt - : t.plan_margin_capital != null - ? Number(t.plan_margin_capital) * Number(t.leverage || 1) - : null; - const pnlFmt = formatFloatingPnlText(upnlTrend, notional); - const pnlInner = + const pnlFmt = formatTrendPlanFloatingPnl(upnlTrend, t.plan_margin_capital); + const pnlVal = pnlFmt.text === "—" ? "—" - : `${esc(pnlFmt.text)}`; - const sizing = resolveTrendSizingFooter({}, t, true); - const levTxt = - sizing.leverage != null && sizing.leverage !== "" ? `${esc(sizing.leverage)}x` : "—"; - const baseTxt = - sizing.planBase != null && sizing.planBase !== "" ? `${fmt(sizing.planBase, 2)}U` : "—"; - const ratioTxt = - sizing.positionRatio != null && sizing.positionRatio !== "" - ? `${fmt(sizing.positionRatio, 2)}%` - : "—"; + : `${esc(pnlFmt.text)}`; const riskTxt = t.risk_percent != null && t.risk_percent !== "" ? `${esc(t.risk_percent)}%` : "—"; - const footHtml = ``; + const snapTxt = + t.snapshot_available_usdt != null && t.snapshot_available_usdt !== "" + ? `${fmt(t.snapshot_available_usdt, 2)}U` + : "—"; + const marginTxt = + t.plan_margin_capital != null && t.plan_margin_capital !== "" + ? `≈${fmt(t.plan_margin_capital, 2)}U` + : "—"; + const levTxt = t.leverage != null && t.leverage !== "" ? `${esc(t.leverage)}x` : "—"; + const bePct = + t.breakeven_offset_pct != null && t.breakeven_offset_pct !== "" + ? esc(t.breakeven_offset_pct) + : "0.3"; + const exId = exchangeRow && exchangeRow.id != null ? esc(exchangeRow.id) : ""; + const canOpen = !!(exchangeRow && (exchangeRow.flask_url_browser || exchangeRow.flask_url)); + const endBtn = canOpen + ? `结束计划` + : ""; + const beBtn = canOpen + ? `保本移交下单监控` + : `保本移交下单监控`; + const beApplied = + t.breakeven_applied + ? `已保本 ${esc(String(t.breakeven_applied_at || "").slice(0, 16))}` + : ""; const dcaHtml = renderTrendDcaTable(t, tickMap); - return `
-
-
- #${esc(t.id)} ${esc(sym)} ${renderDirectionHtml(t.direction)} - ${esc(t.status || "active")} -
-
- 来源: 趋势回调计划 - 风险: ${riskTxt} - ${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)} - 已补仓 ${legsTxt} -
-
-
-
均价${esc(avg)}
-
止损${esc(sl)}
-
止盈程序监控 · ${esc(tp)}
-
盈亏比${esc(rrTxt)}
-
标记价${esc(mark)}
-
浮盈亏${pnlInner}
-
- ${footHtml} + return `
+
+
+ #${esc(t.id)} ${esc(sym)} + ${renderDirectionBadge(t.direction)}
+ ${endBtn} +
+
+ 来源: 趋势回调计划 | 风险: ${riskTxt} + | ${esc(trendAddZoneLabel(t.direction))} ${esc(addZone)} + | 已补仓 ${legsTxt} +
+
+
均价${esc(avg)}
+
止损${esc(sl)}
+
止盈${esc(tp)}
+
盈亏比${esc(rrTxt)}
+
标记价${esc(mark)}
+
浮盈亏${pnlVal}
+
+ ${dcaHtml} +
+ + ${beBtn} + ${beApplied} +
+ - ${dcaHtml ? `
${dcaHtml}
` : ""}
`; } - function renderTrendSection(trends, tickMap, positions) { + function renderTrendSection(trends, tickMap, positions, exchangeRow) { if (!trends || !trends.length) return ""; const posList = Array.isArray(positions) ? positions : []; - return `
${trends + const cards = trends .map((t) => { const sym = t.exchange_symbol || t.symbol || ""; const side = (t.direction || "long").toLowerCase(); @@ -1636,9 +1721,13 @@ break; } } - return renderTrendPlanCard(t, tickMap, matched); + return renderTrendPlanCard(t, tickMap, matched, exchangeRow); }) - .join("")}
`; + .join(""); + return `
+
运行中的计划
+
${cards}
+
`; } function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan, tickMap) { @@ -2052,7 +2141,7 @@ if ((row.capabilities || []).includes("trend")) { html += renderHubSectionCard( "趋势回调", - renderTrendSection(trends, tickMap, pos), + renderTrendSection(trends, tickMap, pos, row), "暂无运行中的趋势回调计划" ); } diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 5680c4c..63a7ab3 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -14,7 +14,7 @@ - + @@ -235,6 +235,6 @@
- +