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                                    |   53 ++++++++++++++++++++++++++
 src/main/java/com/xcong/excoin/modules/station/controller/StationController.java    |    6 +++
 src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java |   48 ++++++++++++++++++++++++
 src/main/java/com/xcong/excoin/modules/station/service/StationService.java          |    3 +
 4 files changed, 110 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/xcong/excoin/modules/station/controller/StationController.java b/src/main/java/com/xcong/excoin/modules/station/controller/StationController.java
index b32b786..2de3448 100644
--- a/src/main/java/com/xcong/excoin/modules/station/controller/StationController.java
+++ b/src/main/java/com/xcong/excoin/modules/station/controller/StationController.java
@@ -76,6 +76,12 @@
         return Result.ok(stationService.getEvents(apiKeyMd5));
     }
 
+    @ApiOperation("查询每轮多/空止损次数")
+    @GetMapping("/round-stop-loss")
+    public Result roundStopLoss(@RequestParam String apiKeyMd5) {
+        return Result.ok(stationService.getRoundStopLossCount(apiKeyMd5));
+    }
+
     @ApiOperation("查询策略实时状态")
     @GetMapping("/strategy-status")
     public Result strategyStatus(@RequestParam String apiKeyMd5) {
diff --git a/src/main/java/com/xcong/excoin/modules/station/service/StationService.java b/src/main/java/com/xcong/excoin/modules/station/service/StationService.java
index 8fdce89..efec324 100644
--- a/src/main/java/com/xcong/excoin/modules/station/service/StationService.java
+++ b/src/main/java/com/xcong/excoin/modules/station/service/StationService.java
@@ -55,6 +55,9 @@
     /** 查询事件日志 */
     List<StrategyEventLog> getEvents(String apiKeyMd5);
 
+    /** 查询每轮多/空止损次数(口径:数 STOP_LOSS_TRIGGERED 事件条数) */
+    List<Map<String, Object>> getRoundStopLossCount(String apiKeyMd5);
+
     /** 查询策略实时状态 */
     StrategyStatus getStrategyStatus(String apiKeyMd5);
 }
diff --git a/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java b/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java
index 9cba72e..6b29015 100644
--- a/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java
+++ b/src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java
@@ -18,7 +18,10 @@
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -166,6 +169,51 @@
     }
 
     @Override
+    public List<Map<String, Object>> getRoundStopLossCount(String apiKeyMd5) {
+        // selectByApiKeyMd5 按 event_time DESC 返回(新→旧),反转为升序(旧→新)
+        List<StrategyEventLog> events = strategyEventLogDao.selectByApiKeyMd5(apiKeyMd5);
+        List<StrategyEventLog> asc = new ArrayList<>(events);
+        Collections.reverse(asc);
+
+        List<Map<String, Object>> rounds = new ArrayList<>();
+        int longCount = 0;
+        int shortCount = 0;
+
+        for (StrategyEventLog e : asc) {
+            String type = e.getEventType();
+            if ("STOP_LOSS_TRIGGERED".equals(type)) {
+                JSONObject p = JSON.parseObject(e.getPayloadJson());
+                String direction = p.getString("direction");
+                if ("long".equals(direction)) {
+                    longCount++;
+                } else if ("short".equals(direction)) {
+                    shortCount++;
+                }
+            } else if ("ROUND_COMPLETE".equals(type)) {
+                // 轮次结束(重置前):快照当前累计的止损次数,然后清零进入下一轮
+                JSONObject p = JSON.parseObject(e.getPayloadJson());
+                Map<String, Object> row = new LinkedHashMap<>();
+                row.put("roundNum", p.getIntValue("roundNum"));
+                row.put("longLossCount", longCount);
+                row.put("shortLossCount", shortCount);
+                rounds.add(row);
+                longCount = 0;
+                shortCount = 0;
+            }
+        }
+
+        // 当前进行中的轮次(尚未 ROUND_COMPLETE):若已有止损则追加
+        if (longCount > 0 || shortCount > 0) {
+            Map<String, Object> row = new LinkedHashMap<>();
+            row.put("roundNum", null);
+            row.put("longLossCount", longCount);
+            row.put("shortLossCount", shortCount);
+            rounds.add(row);
+        }
+        return rounds;
+    }
+
+    @Override
     public StrategyStatus getStrategyStatus(String apiKeyMd5) {
         return strategyStatusDao.selectByApiKeyMd5(apiKeyMd5);
     }
diff --git a/src/main/resources/static/station-dashboard.html b/src/main/resources/static/station-dashboard.html
index b071518..bf70675 100644
--- a/src/main/resources/static/station-dashboard.html
+++ b/src/main/resources/static/station-dashboard.html
@@ -415,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>
@@ -680,6 +698,9 @@
     // 加载事件(内部会渲染事件表格 + PNL 图表)
     await loadEvents(md5);
 
+    // 加载每轮止损次数
+    loadRoundStopLoss(md5);
+
     // 事件加载完成后重算统计卡:账户总权益依赖最新 PNL_SNAPSHOT
     if (selectedMd5 === md5 && instances[md5]) {
         renderStats(instances[md5]);
@@ -835,6 +856,38 @@
     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 图表 ====================
 function renderPnlChart() {
     const canvas = document.getElementById('pnlChart');

--
Gitblit v1.9.1