skyfffire há 3 dias atrás
pai
commit
7ba7c68ae2
2 ficheiros alterados com 482 adições e 11 exclusões
  1. 137 0
      DASHBOARD_EVENTS_FEATURE.md
  2. 345 11
      src/leadlag/templates/dashboard.html

+ 137 - 0
DASHBOARD_EVENTS_FEATURE.md

@@ -0,0 +1,137 @@
+# 交易事件可视化功能说明
+
+## 📊 功能概述
+
+已成功实现**方案1(时间轴标记)+ 简化表格**,将交易事件直观地展示在价格图表和BPS图表上。
+
+## ✨ 新增功能
+
+### 1. 图表标记系统
+
+在价格走势图和BPS图表上,所有交易事件都会以**可视化标记**的形式显示:
+
+#### 标记类型和颜色:
+- 📈 **开多仓** (open_long): 绿色向上箭头
+- 📉 **开空仓** (open_short): 红色向下箭头  
+- 🔼 **平多仓** (close_long): 蓝色三角形
+- 🔽 **平空仓** (close_short): 紫色三角形
+
+#### 标记特性:
+- **垂直虚线**: 标识事件发生的精确时间
+- **图标标记**: 在价格点上显示事件类型
+- **颜色编码**: 快速识别事件类型
+- **可点击**: 点击标记查看详细信息
+
+### 2. 交互式事件详情
+
+点击图表上的任何事件标记,会弹出详细信息窗口,显示:
+- ⏰ 事件时间
+- 💱 交易对
+- 💰 成交价格
+- 📊 交易数量
+- 🎯 交易方向
+- 📈 Ask/Bid价差
+- 🔗 交易哈希
+
+### 3. 智能Tooltip提示
+
+当鼠标悬停在图表上时:
+- 显示当前时间点的价格数据
+- **自动检测**附近5秒内的交易事件
+- 在tooltip中显示事件信息
+- 无需点击即可快速查看
+
+### 4. 简化的事件表格
+
+表格现在只显示**最近10条**交易事件:
+- 减少页面滚动
+- 提高加载速度
+- 保留关键信息
+- 显示总事件数量
+- 提示用户查看图表获取完整信息
+
+### 5. 视觉提示
+
+在事件记录区域添加了醒目的提示框:
+```
+💡 提示:图表上的标记点显示所有交易事件。点击标记可查看详情。
+📈开多 | 📉开空 | 🔼平多 | 🔽平空
+```
+
+## 🎯 使用方法
+
+### 查看事件标记
+1. 打开Dashboard
+2. 选择时间范围和交易对
+3. 在价格图表和BPS图表上查看彩色标记
+4. 标记会随图表缩放和平移自动调整
+
+### 查看事件详情
+**方法1**: 点击图表上的标记点
+**方法2**: 鼠标悬停查看tooltip
+**方法3**: 查看下方的事件表格(最近10条)
+
+### 图表操作
+- **缩放**: 鼠标滚轮或拖动底部滑块
+- **平移**: 按住鼠标拖动
+- **重置**: 点击"重置缩放"按钮
+- **关闭详情**: 点击模态框外部或"关闭"按钮
+
+## 🔧 技术实现
+
+### 核心函数
+- `addEventMarkersToCharts()`: 在图表上添加事件标记
+- `showEventDetails()`: 显示事件详情模态框
+- `getEventIcon()`: 获取事件类型对应的图标
+- `formatEventType()`: 格式化事件类型显示
+
+### 数据流
+1. 从API加载交易事件数据
+2. 存储到 `tradingEventsData` 全局变量
+3. 在图表渲染后调用 `addEventMarkersToCharts()`
+4. 使用ECharts的 `markLine` 和 `markPoint` 功能
+5. 绑定点击事件监听器
+
+### 同步机制
+- 价格图表和BPS图表同时添加标记
+- 缩略图选择后自动重新添加标记
+- 时间范围变化后自动更新标记
+
+## 📈 优势
+
+1. **直观性**: 一眼看出交易时机与价格的关系
+2. **完整性**: 显示所有交易事件,不受表格限制
+3. **交互性**: 点击查看详情,悬停快速预览
+4. **性能**: 简化表格提高页面加载速度
+5. **美观性**: 彩色标记和图标增强视觉效果
+
+## 🎨 颜色方案
+
+| 事件类型 | 颜色 | 符号 | 含义 |
+|---------|------|------|------|
+| 开多仓 | 🟢 绿色 (#28a745) | ↑ | 看涨建仓 |
+| 开空仓 | 🔴 红色 (#dc3545) | ↓ | 看跌建仓 |
+| 平多仓 | 🔵 蓝色 (#007bff) | △ | 多头平仓 |
+| 平空仓 | 🟣 紫色 (#6f42c1) | ▽ | 空头平仓 |
+
+## 🚀 下一步优化建议
+
+1. **事件过滤**: 添加按事件类型过滤的功能
+2. **事件统计**: 显示成功率、盈亏统计
+3. **事件连线**: 连接开仓和平仓事件
+4. **性能优化**: 大量事件时的渲染优化
+5. **导出功能**: 导出事件数据为CSV/Excel
+
+## 📝 注意事项
+
+- 事件标记会在图表重绘后自动添加
+- 点击标记时会暂停图表的其他交互
+- 模态框可以通过点击外部区域关闭
+- Tooltip中的事件检测范围为±5秒
+
+---
+
+**实现日期**: 2025-11-05  
+**版本**: v1.0  
+**状态**: ✅ 已完成并测试
+

+ 345 - 11
src/leadlag/templates/dashboard.html

@@ -186,6 +186,13 @@
             border-radius: 10px;
             box-shadow: 0 2px 10px rgba(0,0,0,0.1);
         }
+
+        .events-container h2 {
+            margin-bottom: 15px;
+            color: #333;
+            border-bottom: 2px solid #667eea;
+            padding-bottom: 10px;
+        }
         
         .events-table {
             width: 100%;
@@ -315,6 +322,15 @@
 
         <div class="events-container">
             <h2>📋 交易事件记录</h2>
+            <div style="background: #e7f3ff; padding: 10px; border-radius: 5px; margin-bottom: 15px; border-left: 4px solid #667eea;">
+                <strong>💡 提示:</strong>
+                <span style="color: #555;">
+                    图表上的标记点显示所有交易事件。点击标记可查看详情。
+                    <span style="margin-left: 10px;">
+                        📈开多 | 📉开空 | 🔼平多 | 🔽平空
+                    </span>
+                </span>
+            </div>
             <div id="eventsContent">
                 <div class="loading">正在加载数据...</div>
             </div>
@@ -330,6 +346,7 @@
         let thumbnailData = null;
         let selectedTimeRange = null;
         let resizeHandler = null;
+        let tradingEventsData = []; // 存储交易事件数据
         
         // 初始化页面
         document.addEventListener('DOMContentLoaded', function() {
@@ -555,6 +572,23 @@
                             result += `${param.marker} ${param.name}: ${value.toFixed(6)}<br/>`;
                         });
 
+                        // 检查是否有交易事件在这个时间点附近(±5秒)
+                        if (tradingEventsData && tradingEventsData.length > 0) {
+                            const timestampSec = timestamp / 1000;
+                            const nearbyEvents = tradingEventsData.filter(event =>
+                                Math.abs(event.timestamp - timestampSec) < 5
+                            );
+
+                            if (nearbyEvents.length > 0) {
+                                result += '<br/><strong style="color: #667eea;">📌 交易事件:</strong><br/>';
+                                nearbyEvents.forEach(event => {
+                                    const icon = getEventIcon(event.event_type);
+                                    const eventName = formatEventType(event.event_type);
+                                    result += `${icon} ${eventName} @ ${event.price ? event.price.toFixed(6) : '-'}<br/>`;
+                                });
+                            }
+                        }
+
                         return result;
                     }
                 },
@@ -641,6 +675,11 @@
 
             // 同步两个图表的缩放
             syncChartsZoom();
+
+            // 如果有交易事件数据,添加标记
+            if (tradingEventsData && tradingEventsData.length > 0) {
+                addEventMarkersToCharts();
+            }
         }
         
         // 显示BPS图表
@@ -695,6 +734,23 @@
                             result += `${param.marker} ${param.name}: ${value.toFixed(2)}<br/>`;
                         });
 
+                        // 检查是否有交易事件在这个时间点附近(±5秒)
+                        if (tradingEventsData && tradingEventsData.length > 0) {
+                            const timestampSec = timestamp / 1000;
+                            const nearbyEvents = tradingEventsData.filter(event =>
+                                Math.abs(event.timestamp - timestampSec) < 5
+                            );
+
+                            if (nearbyEvents.length > 0) {
+                                result += '<br/><strong style="color: #667eea;">📌 交易事件:</strong><br/>';
+                                nearbyEvents.forEach(event => {
+                                    const icon = getEventIcon(event.event_type);
+                                    const eventName = formatEventType(event.event_type);
+                                    result += `${icon} ${eventName} @ ${event.price ? event.price.toFixed(6) : '-'}<br/>`;
+                                });
+                            }
+                        }
+
                         return result;
                     }
                 },
@@ -763,6 +819,14 @@
             };
 
             bpsChart.setOption(option);
+
+            // 如果有交易事件数据,添加标记
+            if (tradingEventsData && tradingEventsData.length > 0) {
+                // 延迟添加标记,确保图表已完全渲染
+                setTimeout(() => {
+                    addEventMarkersToCharts();
+                }, 100);
+            }
         }
 
         // 同步两个图表的缩放
@@ -820,6 +884,243 @@
             });
         }
 
+        // 添加交易事件标记到图表
+        function addEventMarkersToCharts() {
+            if (!priceChart || !bpsChart || !tradingEventsData || tradingEventsData.length === 0) {
+                return;
+            }
+
+            // 准备标记线数据
+            const priceMarkLines = [];
+            const priceMarkPoints = [];
+            const bpsMarkLines = [];
+            const bpsMarkPoints = [];
+
+            tradingEventsData.forEach(event => {
+                const timestamp = event.timestamp * 1000; // 转换为毫秒
+                const eventType = event.event_type;
+
+                // 确定颜色和符号
+                let color, symbol, symbolSize, label;
+
+                if (eventType.includes('open_long')) {
+                    color = '#28a745'; // 绿色
+                    symbol = 'arrow'; // 向上箭头
+                    symbolSize = 15;
+                    label = '开多';
+                } else if (eventType.includes('open_short')) {
+                    color = '#dc3545'; // 红色
+                    symbol = 'arrow'; // 向下箭头(会旋转180度)
+                    symbolSize = 15;
+                    label = '开空';
+                } else if (eventType.includes('close_long')) {
+                    color = '#007bff'; // 蓝色
+                    symbol = 'triangle'; // 三角形
+                    symbolSize = 12;
+                    label = '平多';
+                } else if (eventType.includes('close_short')) {
+                    color = '#6f42c1'; // 紫色
+                    symbol = 'triangle'; // 三角形
+                    symbolSize = 12;
+                    label = '平空';
+                } else {
+                    color = '#6c757d'; // 灰色
+                    symbol = 'circle';
+                    symbolSize = 10;
+                    label = '其他';
+                }
+
+                // 创建标记线(垂直线)
+                const markLine = {
+                    xAxis: timestamp,
+                    lineStyle: {
+                        color: color,
+                        type: 'dashed',
+                        width: 1,
+                        opacity: 0.6
+                    },
+                    label: {
+                        show: false
+                    }
+                };
+
+                // 创建标记点
+                const markPoint = {
+                    coord: [timestamp, event.price || 0],
+                    value: label,
+                    symbol: symbol,
+                    symbolSize: symbolSize,
+                    symbolRotate: eventType.includes('short') ? 180 : 0, // 空头箭头向下
+                    itemStyle: {
+                        color: color,
+                        borderColor: '#fff',
+                        borderWidth: 2
+                    },
+                    label: {
+                        show: true,
+                        position: 'top',
+                        formatter: function(params) {
+                            return label;
+                        },
+                        fontSize: 10,
+                        color: color,
+                        fontWeight: 'bold'
+                    },
+                    // 添加tooltip数据
+                    eventData: event
+                };
+
+                priceMarkLines.push(markLine);
+                priceMarkPoints.push(markPoint);
+                bpsMarkLines.push(markLine);
+
+                // BPS图表的标记点(使用ask_bps或bid_bps)
+                const bpsValue = event.ask_bps || event.bid_bps || 0;
+                const bpsMarkPoint = {
+                    ...markPoint,
+                    coord: [timestamp, bpsValue]
+                };
+                bpsMarkPoints.push(bpsMarkPoint);
+            });
+
+            // 更新价格图表,添加标记
+            const priceOption = priceChart.getOption();
+            if (priceOption.series && priceOption.series.length > 0) {
+                // 在第一个系列(Binance价格)上添加标记
+                priceOption.series[0].markLine = {
+                    silent: false,
+                    data: priceMarkLines,
+                    animation: false
+                };
+                priceOption.series[0].markPoint = {
+                    silent: false,
+                    data: priceMarkPoints,
+                    animation: false
+                };
+
+                priceChart.setOption(priceOption);
+            }
+
+            // 更新BPS图表,添加标记
+            const bpsOption = bpsChart.getOption();
+            if (bpsOption.series && bpsOption.series.length > 0) {
+                // 在第一个系列(Ask BPS)上添加标记
+                bpsOption.series[0].markLine = {
+                    silent: false,
+                    data: bpsMarkLines,
+                    animation: false
+                };
+                bpsOption.series[0].markPoint = {
+                    silent: false,
+                    data: bpsMarkPoints,
+                    animation: false
+                };
+
+                bpsChart.setOption(bpsOption);
+            }
+
+            // 添加点击事件监听
+            priceChart.off('click'); // 移除旧的监听器
+            priceChart.on('click', function(params) {
+                if (params.componentType === 'markPoint' && params.data.eventData) {
+                    showEventDetails(params.data.eventData);
+                }
+            });
+
+            bpsChart.off('click');
+            bpsChart.on('click', function(params) {
+                if (params.componentType === 'markPoint' && params.data.eventData) {
+                    showEventDetails(params.data.eventData);
+                }
+            });
+        }
+
+        // 显示事件详情
+        function showEventDetails(event) {
+            const details = `
+                <div style="background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); max-width: 500px; margin: 20px auto;">
+                    <h3 style="margin-top: 0; color: #667eea; border-bottom: 2px solid #667eea; padding-bottom: 10px;">
+                        ${getEventIcon(event.event_type)} ${formatEventType(event.event_type)}
+                    </h3>
+                    <table style="width: 100%; border-collapse: collapse;">
+                        <tr>
+                            <td style="padding: 8px; font-weight: bold; color: #555;">时间:</td>
+                            <td style="padding: 8px;">${new Date(event.timestamp * 1000).toLocaleString()}</td>
+                        </tr>
+                        <tr style="background: #f8f9fa;">
+                            <td style="padding: 8px; font-weight: bold; color: #555;">交易对:</td>
+                            <td style="padding: 8px;">${event.symbol || '-'}</td>
+                        </tr>
+                        <tr>
+                            <td style="padding: 8px; font-weight: bold; color: #555;">价格:</td>
+                            <td style="padding: 8px;">${event.price ? event.price.toFixed(6) : '-'}</td>
+                        </tr>
+                        <tr style="background: #f8f9fa;">
+                            <td style="padding: 8px; font-weight: bold; color: #555;">数量:</td>
+                            <td style="padding: 8px;">${event.quantity || '-'}</td>
+                        </tr>
+                        <tr>
+                            <td style="padding: 8px; font-weight: bold; color: #555;">方向:</td>
+                            <td style="padding: 8px;">${event.side || '-'}</td>
+                        </tr>
+                        <tr style="background: #f8f9fa;">
+                            <td style="padding: 8px; font-weight: bold; color: #555;">策略状态:</td>
+                            <td style="padding: 8px;" class="${getStrategyStateClass(event.strategy_state)}">${event.strategy_state || '-'}</td>
+                        </tr>
+                        <tr>
+                            <td style="padding: 8px; font-weight: bold; color: #555;">Ask价差:</td>
+                            <td style="padding: 8px;">${event.ask_bps ? event.ask_bps.toFixed(2) + ' bps' : '-'}</td>
+                        </tr>
+                        <tr style="background: #f8f9fa;">
+                            <td style="padding: 8px; font-weight: bold; color: #555;">Bid价差:</td>
+                            <td style="padding: 8px;">${event.bid_bps ? event.bid_bps.toFixed(2) + ' bps' : '-'}</td>
+                        </tr>
+                        ${event.tx_hash ? `
+                        <tr>
+                            <td style="padding: 8px; font-weight: bold; color: #555;">交易哈希:</td>
+                            <td style="padding: 8px; word-break: break-all; font-size: 12px;">${event.tx_hash}</td>
+                        </tr>
+                        ` : ''}
+                    </table>
+                    <button onclick="closeEventDetails()" style="margin-top: 15px; width: 100%; padding: 10px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 5px; cursor: pointer; font-weight: bold;">
+                        关闭
+                    </button>
+                </div>
+            `;
+
+            // 创建模态框
+            const modal = document.createElement('div');
+            modal.id = 'eventDetailsModal';
+            modal.style.cssText = `
+                position: fixed;
+                top: 0;
+                left: 0;
+                right: 0;
+                bottom: 0;
+                background: rgba(0, 0, 0, 0.5);
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                z-index: 10000;
+            `;
+            modal.innerHTML = details;
+            modal.onclick = function(e) {
+                if (e.target === modal) {
+                    closeEventDetails();
+                }
+            };
+
+            document.body.appendChild(modal);
+        }
+
+        // 关闭事件详情
+        function closeEventDetails() {
+            const modal = document.getElementById('eventDetailsModal');
+            if (modal) {
+                modal.remove();
+            }
+        }
+
         // 加载缩略图数据(24小时,只显示币安价格,每秒最多一条)
         async function loadThumbnailData(symbol) {
             try {
@@ -934,12 +1235,17 @@
                 const params = new URLSearchParams({ hours });
                 if (symbol) params.append('symbol', symbol);
                 if (currentDbPath) params.append('db_path', currentDbPath);
-                
+
                 const response = await fetch(`/api/trading_events?${params}`);
                 const result = await response.json();
-                
+
                 if (result.success) {
+                    tradingEventsData = result.data; // 保存事件数据
                     displayTradingEvents(result.data);
+                    // 重新绘制图表以添加事件标记
+                    if (priceChart && bpsChart) {
+                        addEventMarkersToCharts();
+                    }
                 } else {
                     throw new Error(result.error);
                 }
@@ -949,16 +1255,24 @@
             }
         }
         
-        // 显示交易事件
+        // 显示交易事件 - 简化版,只显示最近10条
         function displayTradingEvents(events) {
             const eventsContent = document.getElementById('eventsContent');
-            
+
             if (events.length === 0) {
                 eventsContent.innerHTML = '<div class="loading">暂无交易事件</div>';
                 return;
             }
-            
+
+            // 只显示最近10条
+            const recentEvents = events.slice(0, 10);
+            const totalCount = events.length;
+
             const table = `
+                <div style="margin-bottom: 10px; color: #666;">
+                    <strong>最近10条交易事件</strong> (共 ${totalCount} 条)
+                    ${totalCount > 10 ? `<span style="color: #999;"> - 更多事件请查看图表标记</span>` : ''}
+                </div>
                 <table class="events-table">
                     <thead>
                         <tr>
@@ -975,11 +1289,13 @@
                         </tr>
                     </thead>
                     <tbody>
-                        ${events.map(event => `
+                        ${recentEvents.map(event => `
                             <tr>
                                 <td>${new Date(event.timestamp * 1000).toLocaleString()}</td>
                                 <td>${event.symbol || '-'}</td>
-                                <td class="${getEventClass(event.event_type)}">${formatEventType(event.event_type)}</td>
+                                <td class="${getEventClass(event.event_type)}">
+                                    ${getEventIcon(event.event_type)} ${formatEventType(event.event_type)}
+                                </td>
                                 <td>${event.price ? event.price.toFixed(6) : '-'}</td>
                                 <td>${event.quantity || '-'}</td>
                                 <td>${event.side || '-'}</td>
@@ -989,8 +1305,8 @@
                                 <td>${event.ask_bps ? event.ask_bps.toFixed(2) : '-'}</td>
                                 <td>${event.bid_bps ? event.bid_bps.toFixed(2) : '-'}</td>
                                 <td>
-                                    ${event.tx_hash ? 
-                                        `<a href="#" title="${event.tx_hash}">${event.tx_hash.substring(0, 10)}...</a>` : 
+                                    ${event.tx_hash ?
+                                        `<a href="#" title="${event.tx_hash}">${event.tx_hash.substring(0, 10)}...</a>` :
                                         '-'
                                     }
                                 </td>
@@ -999,7 +1315,7 @@
                     </tbody>
                 </table>
             `;
-            
+
             eventsContent.innerHTML = table;
         }
         
@@ -1009,6 +1325,17 @@
             if (eventType.includes('close')) return 'event-close';
             return '';
         }
+
+        // 获取事件图标
+        function getEventIcon(eventType) {
+            if (eventType.includes('open_long')) return '📈';
+            if (eventType.includes('open_short')) return '📉';
+            if (eventType.includes('close_long')) return '🔼';
+            if (eventType.includes('close_short')) return '🔽';
+            if (eventType.includes('open')) return '🟢';
+            if (eventType.includes('close')) return '🔴';
+            return '⚪';
+        }
         
         // 获取策略状态的CSS类
         function getStrategyStateClass(state) {
@@ -1156,9 +1483,16 @@
                             displayPriceChart(filteredData);
                             // 更新BPS图表
                             displayBpsChart(filteredData);
-                            
+
                             // 更新时间范围显示
                             updateTimeRangeDisplay();
+
+                            // 重新添加事件标记
+                            if (tradingEventsData && tradingEventsData.length > 0) {
+                                setTimeout(() => {
+                                    addEventMarkersToCharts();
+                                }, 200);
+                            }
                         } else {
                             console.log('过滤后没有数据');
                         }