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 ``;
}
- 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 `
`;
}
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 @@
-
+