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) { 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); } 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); } 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');