From 04a1828f4cca58545725aef8fb6eb6312d3d1e83 Mon Sep 17 00:00:00 2001
From: Administrator <15274802129@163.com>
Date: Thu, 08 Oct 2026 11:57:19 +0800
Subject: [PATCH] feat(stations): 添加每轮止损次数统计功能

---
 src/main/resources/static/station-dashboard.html |  113 +++++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 103 insertions(+), 10 deletions(-)

diff --git a/src/main/resources/static/station-dashboard.html b/src/main/resources/static/station-dashboard.html
index 2f2f7d8..bf70675 100644
--- a/src/main/resources/static/station-dashboard.html
+++ b/src/main/resources/static/station-dashboard.html
@@ -130,6 +130,10 @@
         .btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
         .btn:disabled { opacity: .4; cursor: not-allowed; }
 
+        /* ===== 手动控制面板 ===== */
+        .manual-actions { display: flex; flex-wrap: wrap; gap: 8px; }
+        .manual-actions .btn { padding: 7px 14px; }
+
         /* ===== 内容区(Tabs) ===== */
         .content { flex: 1; overflow-y: auto; padding: 16px 24px; }
         .tabs {
@@ -365,8 +369,8 @@
             <h2 style="color:var(--dim)">← 选择一个实例</h2>
         </div>
         <div class="btn-group">
-            <button class="btn btn-success" id="btnStart" disabled onclick="showConfirm('start')">▶ 启动</button>
-            <button class="btn btn-danger" id="btnStop" disabled onclick="showConfirm('stop')">⏹ 停止</button>
+            <button class="btn btn-success" id="btnStart" disabled onclick="showConfirm('start','启动')">▶ 启动</button>
+            <button class="btn btn-danger" id="btnStop" disabled onclick="showConfirm('stop','停止')">⏹ 停止</button>
         </div>
     </div>
 
@@ -392,6 +396,17 @@
     <div class="tab-content active" data-tab="overview">
         <div class="stats-grid" id="statsGrid"></div>
         <div class="chart-container">
+            <h3>⚙️ 手动控制 <span style="font-size:11px;color:var(--dim);font-weight:400">(实时下发 MQ 指令,策略运行中可用)</span></h3>
+            <div class="manual-actions">
+                <button class="btn btn-outline" id="btnCancelLongTpSl" disabled onclick="showConfirm('cancel-long-tp-sl','取消多仓止盈止损')">取消多仓止盈止损</button>
+                <button class="btn btn-outline" id="btnCancelShortTpSl" disabled onclick="showConfirm('cancel-short-tp-sl','取消空仓止盈止损')">取消空仓止盈止损</button>
+                <button class="btn btn-outline" id="btnRebuildLongTpSl" disabled onclick="showConfirm('rebuild-long-tp-sl','生成多仓止盈止损')">生成多仓止盈止损</button>
+                <button class="btn btn-outline" id="btnRebuildShortTpSl" disabled onclick="showConfirm('rebuild-short-tp-sl','生成空仓止盈止损')">生成空仓止盈止损</button>
+                <button class="btn btn-danger" id="btnCloseLong" disabled onclick="showConfirm('close-long','多单平仓')">多单平仓</button>
+                <button class="btn btn-danger" id="btnCloseShort" disabled onclick="showConfirm('close-short','空单平仓')">空单平仓</button>
+            </div>
+        </div>
+        <div class="chart-container">
             <h3>📈 盈亏趋势(最近 PNL_SNAPSHOT 事件)</h3>
             <div class="pnl-summary" id="pnlSummary"></div>
             <div class="chart-wrap"><canvas id="pnlChart"></canvas></div>
@@ -400,6 +415,24 @@
 
     <!-- 事件日志 Tab -->
     <div class="tab-content" data-tab="events">
+        <div class="events-panel" style="margin-bottom:16px">
+            <div class="events-header">
+                <h3>🎯 每轮止损次数</h3>
+                <button class="btn btn-outline" onclick="refreshRoundStopLoss()" style="font-size:11px;padding:3px 10px">🔄 刷新</button>
+            </div>
+            <div style="max-height:300px;overflow-y:auto">
+                <table class="events-table">
+                    <thead>
+                        <tr>
+                            <th style="width:120px">轮次</th>
+                            <th style="width:120px">多仓止损</th>
+                            <th>空仓止损</th>
+                        </tr>
+                    </thead>
+                    <tbody id="roundStopLossBody"></tbody>
+                </table>
+            </div>
+        </div>
         <div class="events-panel">
             <div class="events-header">
                 <h3>📋 最近 100 条事件</h3>
@@ -665,6 +698,9 @@
     // 加载事件(内部会渲染事件表格 + PNL 图表)
     await loadEvents(md5);
 
+    // 加载每轮止损次数
+    loadRoundStopLoss(md5);
+
     // 事件加载完成后重算统计卡:账户总权益依赖最新 PNL_SNAPSHOT
     if (selectedMd5 === md5 && instances[md5]) {
         renderStats(instances[md5]);
@@ -818,6 +854,38 @@
     }).join('');
 
     renderPnlChart();
+}
+
+// ==================== 每轮止损次数 ====================
+async function loadRoundStopLoss(md5) {
+    try {
+        const rows = await api(API + '/round-stop-loss?apiKeyMd5=' + encodeURIComponent(md5));
+        renderRoundStopLoss(rows || []);
+    } catch (e) {
+        renderRoundStopLoss([]);
+    }
+}
+
+function renderRoundStopLoss(rows) {
+    const tbody = $('roundStopLossBody');
+    if (!tbody) return;
+    if (!rows || rows.length === 0) {
+        tbody.innerHTML = `<tr><td colspan="3" style="text-align:center;padding:20px;color:var(--dim)">暂无止损记录</td></tr>`;
+        return;
+    }
+    tbody.innerHTML = rows.map(r => {
+        const roundLabel = (r.roundNum === null || r.roundNum === undefined) ? '进行中' : ('第 ' + r.roundNum + ' 轮');
+        return `
+        <tr>
+            <td>${esc(roundLabel)}</td>
+            <td>${r.longLossCount ?? 0}</td>
+            <td>${r.shortLossCount ?? 0}</td>
+        </tr>`;
+    }).join('');
+}
+
+function refreshRoundStopLoss() {
+    if (selectedMd5) loadRoundStopLoss(selectedMd5);
 }
 
 // ==================== PNL 图表 ====================
@@ -1021,8 +1089,15 @@
     addPositionQuantity: 1,
     maxPositionPerSide: 0,
     addPositionStartThreshold: 1,
-    placeExcessTakeProfit: false,
+    placeExcessTakeProfit: 'off',
     priceDriveEnabled: true,
+    dualStopLossEnabled: false,
+    longStopLossPrice: '',
+    shortStopLossPrice: '',
+    tpTriggerOppositeAddEnabled: false,
+    tpTriggerOppositeAddQuantity: 0,
+    baseTakeProfitPerGrid: 0,
+    baseTakeProfitGridCount: 0,
 };
 
 async function loadConfig(md5) {
@@ -1055,6 +1130,8 @@
     { key: 'quantity', label: '每次下单张数', type: 'number', placeholder: '2', jtype: 'string', group: 'grid' },
     { key: 'maxPositionSize', label: '最大持仓张数', type: 'number', placeholder: '4', jtype: 'int', group: 'grid' },
     { key: 'takeProfitGridSpan', label: '止盈网格跨度', type: 'number', placeholder: '2', jtype: 'int', group: 'grid' },
+    { key: 'baseTakeProfitPerGrid', label: '单格止盈张数', type: 'number', step: 'any', placeholder: '0', jtype: 'decimal', group: 'grid', tip: '基础仓位止盈:每个网格挂的止盈张数(支持小数)' },
+    { key: 'baseTakeProfitGridCount', label: '止盈网格数量', type: 'number', placeholder: '0', jtype: 'int', group: 'grid', tip: '基础仓位止盈:挂止盈的网格数量' },
 
     { key: 'expectedProfit', label: '预期收益 (USDT)', type: 'number', step: '0.01', placeholder: '0.15', jtype: 'decimal', group: 'risk' },
     { key: 'maxLoss', label: '最大亏损 (USDT)', type: 'number', step: '0.01', placeholder: '1.5', jtype: 'decimal', group: 'risk' },
@@ -1065,10 +1142,16 @@
     { key: 'addPositionQuantity', label: '加仓数量 (张)', type: 'number', placeholder: '1', jtype: 'int', group: 'addon' },
     { key: 'maxPositionPerSide', label: '单边最大仓位 (0=不限)', type: 'number', placeholder: '0', jtype: 'int', group: 'addon' },
     { key: 'addPositionStartThreshold', label: '加仓启动阈值 (次)', type: 'number', placeholder: '1', jtype: 'int', group: 'addon', tip: '前 N 次止损不触发加仓' },
+    { key: 'tpTriggerOppositeAddEnabled', label: '止盈触发对向加仓', type: 'select', options: [{ v: 'false', l: '关闭' }, { v: 'true', l: '开启' }], group: 'addon', tip: '止盈单成交后触发对向加仓' },
+    { key: 'tpTriggerOppositeAddQuantity', label: '对向加仓挂单数量 (张)', type: 'number', placeholder: '0', jtype: 'int', group: 'addon', tip: '止盈成交后在对向挂出的加仓张数' },
 
     { key: 'rounds', label: '运行轮数 (0=不限)', type: 'number', placeholder: '0', jtype: 'int', group: 'run', tip: '盈利重启达此轮数后停止' },
-    { key: 'placeExcessTakeProfit', label: '超额止盈', type: 'select', options: [{ v: 'false', l: '关闭' }, { v: 'true', l: '开启' }], group: 'run' },
+    { key: 'placeExcessTakeProfit', label: '止盈模式', type: 'select', options: [{ v: 'off', l: '关闭' }, { v: 'concentrated', l: '集中止盈' }, { v: 'dispersed', l: '分散止盈' }], group: 'run', tip: '超出基础仓位的加仓部分如何挂止盈' },
     { key: 'priceDriveEnabled', label: '价格驱动', type: 'select', options: [{ v: 'true', l: '开启' }, { v: 'false', l: '关闭' }], group: 'run' },
+
+    { key: 'dualStopLossEnabled', label: '双开止损触发', type: 'select', options: [{ v: 'false', l: '关闭' }, { v: 'true', l: '开启' }], group: 'dual-sl', tip: '开启后按下方多空止损触发价初始化双向止损单,任一触发后重挂另一侧止损' },
+    { key: 'longStopLossPrice', label: '多头止损触发价', type: 'number', step: 'any', placeholder: '如 2540', jtype: 'string', group: 'dual-sl', tip: '价格跌至该值时触发多头止损' },
+    { key: 'shortStopLossPrice', label: '空头止损触发价', type: 'number', step: 'any', placeholder: '如 2650', jtype: 'string', group: 'dual-sl', tip: '价格涨至该值时触发空头止损' },
 ];
 
 // 分组定义(渲染顺序)
@@ -1076,6 +1159,7 @@
     { id: 'grid', name: '📐 网格参数' },
     { id: 'risk', name: '🛡️ 风控参数' },
     { id: 'addon', name: '📈 加仓 & 止损' },
+    { id: 'dual-sl', name: '🎯 双开止损触发' },
     { id: 'run', name: '⚡ 运行控制' },
 ];
 
@@ -1205,24 +1289,33 @@
 function updateActionButtons(inst) {
     const startBtn = $('btnStart');
     const stopBtn = $('btnStop');
+    // 手动控制按钮(止盈止损/平仓):需策略运行中才有持仓/挂单可操作
+    const manualIds = ['btnCancelLongTpSl', 'btnCancelShortTpSl', 'btnRebuildLongTpSl',
+        'btnRebuildShortTpSl', 'btnCloseLong', 'btnCloseShort'];
+    const setManual = (disabled) => {
+        manualIds.forEach(id => { const b = $(id); if (b) b.disabled = disabled; });
+    };
     if (!inst) {
         startBtn.disabled = true;
         stopBtn.disabled = true;
+        setManual(true);
         return;
     }
     const running = inst.state === 'ACTIVE' || inst.state === 'OPENING' || inst.state === 'WAITING_KLINE';
     startBtn.disabled = running;   // 运行中不可重复启动
     stopBtn.disabled = !running;   // 未运行不可停止
+    setManual(!running);           // 仅运行中可下发手动指令
 }
 
-function showConfirm(action) {
+function showConfirm(action, label) {
     if (!selectedMd5) return;
-    pendingAction = action;
-    const verb = action === 'start' ? '启动' : '停止';
+    pendingAction = { path: action, label: label || action };
+    const verb = pendingAction.label;
+    const isDanger = action === 'stop' || action === 'close-long' || action === 'close-short';
     $('modalTitle').textContent = `确认${verb}`;
-    $('modalMsg').textContent = `确定要${verb}实例 ${selectedMd5.substring(0, 12)}... 的策略吗?`;
+    $('modalMsg').textContent = `确定要对实例 ${selectedMd5.substring(0, 12)}... 执行「${verb}」吗?`;
     $('modalConfirmBtn').textContent = verb;
-    $('modalConfirmBtn').className = 'btn ' + (action === 'start' ? 'btn-success' : 'btn-danger');
+    $('modalConfirmBtn').className = 'btn ' + (isDanger ? 'btn-danger' : 'btn-success');
     $('modalOverlay').classList.add('show');
 }
 
@@ -1272,7 +1365,7 @@
     closeModal();
 
     try {
-        const msg = await api(API + '/' + action + '?apiKeyMd5=' + encodeURIComponent(selectedMd5), { method: 'POST' });
+        const msg = await api(API + '/' + action.path + '?apiKeyMd5=' + encodeURIComponent(selectedMd5), { method: 'POST' });
         toast(msg || '指令已发送', 'success');
         // 延迟刷新等待 ACK
         setTimeout(refreshInstanceList, 2000);

--
Gitblit v1.9.1