Administrator
4 days ago 04a1828f4cca58545725aef8fb6eb6312d3d1e83
feat(stations): 添加每轮止损次数统计功能

- 在站点仪表板事件日志标签页中新增止损次数统计面板
- 实现按轮次展示多仓和空仓止损次数的功能
- 添加前端刷新按钮和数据加载逻辑
- 在后端控制器中增加 round-stop-loss API 接口
- 实现服务层按轮次统计止损次数的业务逻辑
- 支持显示进行中轮次的实时止损统计数据
4 files modified
110 ■■■■■ changed files
src/main/java/com/xcong/excoin/modules/station/controller/StationController.java 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/station/service/StationService.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/station/service/impl/StationServiceImpl.java 48 ●●●●● patch | view | raw | blame | history
src/main/resources/static/station-dashboard.html 53 ●●●●● patch | view | raw | blame | history
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');