From 7f4f88e85365711226ee3a26ea230a97af8dc6df Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 2 Nov 2025 23:57:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9C=9F=E8=B4=A7?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=92=AD=E6=94=BE=E5=99=A8=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E6=B5=8B=E8=AF=95=E5=92=8C=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增期货数据动态播放器功能,包括基础版和增强版实现,添加测试脚本和详细文档说明。主要变更包括: 1. 实现买卖盘深度可视化播放功能 2. 添加播放控制、速度调节和跳转功能 3. 提供统一价格轴显示优化版本 4. 添加测试脚本验证功能 5. 编写详细使用文档和README说明 --- README_futures_player.md | 155 +++++ README_unified_final.md | 177 +++++ demo_unified_player.py | 202 ++++++ futures_data_player.py | 482 ++++++++++++++ futures_player_enhanced.py | 539 +++++++++++++++ futures_player_unified.py | 625 ++++++++++++++++++ large_orders/analyze_large_orders_extended.py | 2 +- large_orders/analyze_total_orders_extended.py | 2 +- simple_test.py | 60 ++ test_player.py | 127 ++++ test_unified_player.py | 154 +++++ 11 files changed, 2523 insertions(+), 2 deletions(-) create mode 100644 README_futures_player.md create mode 100644 README_unified_final.md create mode 100644 demo_unified_player.py create mode 100644 futures_data_player.py create mode 100644 futures_player_enhanced.py create mode 100644 futures_player_unified.py create mode 100644 simple_test.py create mode 100644 test_player.py create mode 100644 test_unified_player.py diff --git a/README_futures_player.md b/README_futures_player.md new file mode 100644 index 0000000..38ae3e8 --- /dev/null +++ b/README_futures_player.md @@ -0,0 +1,155 @@ +# 期货数据动态播放器 - Futures Data Player + +## 概述 + +这是一个基于Python的期货数据可视化工具,专门用于展示AU2512期货合约的买卖盘深度变化。该工具可以按时间序列动态播放市场深度数据,帮助交易员和分析师更好地理解市场微观结构。 + +## 功能特性 + +### 核心功能 +- **实时买卖盘深度可视化**: 展示买1-5档和卖1-5档的价格和成交量 +- **时序播放控制**: 按数列号顺序播放数据,模拟真实交易过程 +- **灵活的播放控制**: 支持播放/暂停/停止/步进操作 +- **可调节播放速度**: 0.1x到10x速度调节 +- **精确跳转功能**: 可跳转到任意数列号 + +### 高级功能 +- **价格趋势图表**: 显示最近100个数据点的价格走势 +- **成交量分析**: 累积成交量变化趋势 +- **买卖价差监控**: 实时显示bid-ask spread +- **快速定位**: 提供开始、25%、50%、75%、结束的快速跳转按钮 + +## 数据源 + +基于`data/au2512_20251013.parquet`文件,包含: +- 66,596个数据点 +- 时间范围: 00:00.5 - 结束时间 +- 价格区间: 实时计算 +- 完整的买卖盘档位数据 + +## 安装要求 + +### 必需依赖 +```bash +pip install pandas numpy matplotlib tkinter +``` + +### Python版本 +- Python 3.7+ + +## 使用方法 + +### 启动程序 +```bash +# 基础版本 +python futures_data_player.py + +# 增强版本(推荐) +python futures_player_enhanced.py +``` + +### 操作指南 + +#### 播放控制 +1. **播放/暂停**: 点击"▶ Play"或"⏸ Pause"按钮 +2. **停止**: 点击"■ Stop"按钮返回到开始 +3. **步进控制**: + - "Step +1 ▶": 前进1个数据点 + - "Step +10 ▶▶": 前进10个数据点 + - "◀ Step -1": 后退1个数据点 + - "◀◀ Step -10": 后退10个数据点 + +#### 速度控制 +- **滑块调节**: 拖动速度滑块(0.1x - 10x) +- **快速按钮**: 0.25x, 0.5x, 1x, 2x, 5x, 10x + +#### 跳转功能 +1. **精确跳转**: 在输入框中输入数列号,点击"Jump" +2. **快速跳转**: 点击Start、25%、50%、75%、End按钮 + +#### 图表说明 +- **买盘深度图**(上方): 绿色水平条,显示各买档价格的挂单量 +- **卖盘深度图**(中间): 红色水平条,显示各卖档价格的挂单量 +- **价格趋势图**(左下): 蓝色线条显示最近100个点的价格变化 +- **成交量图**(右下): 绿色线条显示累积成交量变化 + +## 界面布局 + +``` +┌─────────────────────────────────────┬─────────────────┐ +│ 图表显示区域 │ 控制面板 │ +│ │ │ +│ ┌─────────────────────────────────┐ │ 播放控制 │ +│ │ 买盘深度图 │ │ 状态显示 │ +│ └─────────────────────────────────┘ │ 速度控制 │ +│ ┌─────────────────────────────────┐ │ 跳转控制 │ +│ │ 卖盘深度图 │ │ 进度显示 │ +│ └─────────────────────────────────┘ │ 数据统计 │ +│ ┌─────────────────┬───────────────┐ │ │ +│ │ 价格趋势 │ 成交量分析 │ │ │ +│ └─────────────────┴───────────────┘ │ │ +└─────────────────────────────────────┴─────────────────┘ +``` + +## 数据结构 + +### 输入数据格式 +- `time`: 时间戳(MM:SS.秒格式) +- `price`: 最新成交价 +- `cumulative_volume`: 累积成交量 +- `bid1_price` ~ `bid5_price`: 买1到买5价格 +- `bid1_volume` ~ `bid5_volume`: 买1到买5挂单量 +- `ask1_price` ~ `ask5_price`: 卖1到卖5价格 +- `ask1_volume` ~ `ask5_volume`: 卖1到卖5挂单量 + +### 可视化逻辑 +- **买盘**: 按价格从高到低排序,价格越高优先级越高 +- **卖盘**: 按价格从低到高排序,价格越低优先级越高 +- **颜色编码**: + - 绿色系:买盘 + - 红色系:卖盘 + - 蓝色虚线:当前成交价 + +## 技术特点 + +### 性能优化 +- 多线程播放避免界面卡顿 +- 数据预处理和缓存机制 +- 高效的matplotlib渲染 + +### 用户体验 +- 直观的图形界面 +- 实时状态更新 +- 精确的播放控制 +- 响应式布局设计 + +## 使用场景 + +1. **交易策略研究**: 观察市场深度变化对价格的影响 +2. **风险分析**: 识别流动性异常和市场冲击 +3. **教育培训**: 帮助理解期货市场微观结构 +4. **数据分析**: 深入研究价格发现过程 + +## 注意事项 + +1. 首次运行可能需要下载依赖库 +2. 确保数据文件路径正确 +3. 大量数据播放时可能消耗较多内存 +4. 建议在性能较好的设备上运行以获得最佳体验 + +## 故障排除 + +### 常见问题 +1. **程序无法启动**: 检查Python版本和依赖库安装 +2. **数据加载失败**: 确认数据文件存在且格式正确 +3. **图表显示异常**: 尝试重启程序或检查matplotlib配置 +4. **播放卡顿**: 降低播放速度或关闭其他占用资源的程序 + +### 联系支持 +如有问题,请检查控制台输出的错误信息,或确保所有依赖库正确安装。 + +--- + +**版本**: 1.0 +**最后更新**: 2025-11-02 +**数据文件**: au2512_20251013.parquet \ No newline at end of file diff --git a/README_unified_final.md b/README_unified_final.md new file mode 100644 index 0000000..81b9c3f --- /dev/null +++ b/README_unified_final.md @@ -0,0 +1,177 @@ +# 统一价格轴期货数据播放器 - 最终优化版 + +## 🎯 核心特性 + +### 💡 创新显示方式 +- **倒置柱状图**: 成交量柱体从价格轴向下延伸,便于观察价格间隙 +- **统一价格轴**: 买卖盘挂单在同一价格体系中对比显示 +- **类对数刻度**: 成交量采用等距对数刻度,更好展示不同量级数据 + +### 📊 视觉效果 +- **价格轴**: 横轴显示,按真实tick (0.02元) 标注 +- **成交量轴**: 纵轴显示,类对数刻度 [10, 30, 60, 150, 300, 600, 1500, 3000, 6000, 15000] +- **颜色编码**: 绿色=买盘,红色=卖盘,蓝色=当前价,黄色=价差区域 + +## 📈 显示逻辑详解 + +### 市场深度主图表 +``` + ▲ 成交量 (对数刻度) + │ + │ ● 1500 (成交量刻度点等距分布) + │ ● 600 + │ ● 300 + 0 ──┼───────────────── ← 价格轴 (黑色横线) + │ │ ● 成交量柱体向下延伸 + │ │ │● + │ │ │ │● + └──┼────┼─┼────▶ 价格 (¥) + 904.86 904.88 904.90 +``` + +### 关键设计要点 + +#### 1. 倒置显示优势 +- **价格间隙突出**: 买卖价差在价格轴上方清晰可见 +- **视觉聚焦**: 价格作为基准线,挂单量向下延伸 +- **对比直观**: 不同价格档位的挂单量对比一目了然 + +#### 2. 类对数刻度设计 +```python +volume_scale_points = [10, 30, 60, 150, 300, 600, 1500, 3000, 6000, 15000] +``` +- **等距显示**: 10与30的间距 = 300与600的间距 +- **业务相关性**: 覆盖期货交易常见挂单量范围 +- **细节保留**: 小单量和大单量都能清晰显示 + +#### 3. 真实tick刻度 +- **AU2512最小tick**: 0.02元 +- **精确标注**: 横轴严格按照交易所规则标注 +- **价格跳跃**: 真实反映买卖盘之间的价格间隙 + +## 🚀 使用方法 + +### 快速启动 +```bash +# 运行完整应用 +python futures_player_unified.py + +# 查看演示效果 +python demo_unified_player.py + +# 功能测试 +python test_unified_player.py +``` + +### 操作指南 + +#### 播放控制 +- **▶ Play/⏸ Pause**: 播放或暂停时序数据 +- **■ Stop**: 停止并回到开始位置 +- **Step Controls**: ±1或±10步进播放 + +#### 速度调节 +- **滑块控制**: 0.1x - 10x 无级调速 +- **快速按钮**: 0.25x, 0.5x, 1x, 2x, 5x, 10x + +#### 导航功能 +- **快速跳转**: Start, 25%, 50%, 75%, End +- **精确跳转**: 输入具体序列号跳转 + +## 📋 界面布局 + +``` +┌─────────────────────────────────────────────────────────────┬─────────────────┐ +│ 图表显示区域 │ 控制面板 │ +│ │ │ +│ ┌─────────────────────────────────────────────────────┐ │ 播放控制 │ +│ │ 倒置市场深度图表 │ │ 状态显示 │ +│ │ 成交量↓ | 价格轴 (0) | 成交量↓ │ │ 速度控制 │ +│ │ 绿色买盘 红色卖盘 │ │ 导航控制 │ +│ └─────────────────────────────────────────────────────┘ │ 进度显示 │ +│ ┌─────────────────┬─────────────────────────────────┐ │ 统计信息 │ +│ │ 价格趋势图 │ 成交量分析图 │ │ │ +│ └─────────────────┴─────────────────────────────────┘ │ │ +│ ┌─────────────────────────────────────────────────────┐ │ │ +│ │ 统计信息显示区 │ │ │ +│ └─────────────────────────────────────────────────────┘ │ │ +└─────────────────────────────────────────────────────────────┴─────────────────┘ +``` + +## 🔍 数据解读指南 + +### 市场深度分析 +1. **流动性观察**: 柱体越高表示该价位挂单量越大 +2. **价差分析**: 黄色区域宽度显示买卖价差 +3. **支撑阻力**: 高买柱形成支撑,高卖柱形成阻力 +4. **订单分布**: 观察不同价位的挂单分布情况 + +### 交易信号识别 +- **价差收窄**: 买卖价差变小,流动性改善 +- **大单出现**: 某个价位柱体异常增高 +- **失衡信号**: 买盘或卖盘明显占优 + +## ⚙️ 技术实现 + +### 核心算法 +```python +# 成交量对数变换 +def transform_volume(volume): + for i in range(len(volume_scale_points) - 1): + if volume_scale_points[i] <= volume < volume_scale_points[i + 1]: + ratio = (volume - volume_scale_points[i]) / (volume_scale_points[i + 1] - volume_scale_points[i]) + return i + ratio # 等距映射 + +# 倒置显示 +bars = ax.bar(all_prices, [-v for v in transformed_volumes]) # 负值向下 +``` + +### 性能优化 +- **高效渲染**: 优化的matplotlib绘图 +- **内存管理**: 合理的数据处理策略 +- **实时更新**: 流畅的播放体验 + +## 📊 数据源 + +### 文件信息 +- **数据文件**: `data/au2512_20251013.parquet` +- **数据量**: 66,596个有效数据点 +- **时间范围**: 00:00.5 - 59:59.5 +- **价格区间**: 901.84 - 928.88元 + +### 数据字段 +- 买1-5档价格和挂单量 +- 卖1-5档价格和挂单量 +- 最新成交价和累积成交量 +- 时间戳信息 + +## 🎯 业务价值 + +### 交易决策支持 +- **即时价差观察**: 直接看到买卖价差大小 +- **流动性评估**: 快速评估市场深度 +- **价格阻力识别**: 识别关键支撑和阻力位 +- **订单流分析**: 观察买卖力量对比 + +### 市场分析 +- **微观结构**: 深入了解市场微观结构 +- **价格发现**: 观察价格形成过程 +- **风险管理**: 评估市场冲击成本 + +## 🛠️ 系统要求 + +### 环境依赖 +```bash +pip install pandas numpy matplotlib tkinter +``` + +### 硬件要求 +- **内存**: 最少4GB,推荐8GB +- **显示**: 1920x1080或更高分辨率 +- **系统**: Windows/Linux/macOS + +--- + +**版本**: 3.0 Final Edition +**最后更新**: 2025-11-02 +**核心特性**: 倒置显示 + 类对数刻度 + 统一价格轴 \ No newline at end of file diff --git a/demo_unified_player.py b/demo_unified_player.py new file mode 100644 index 0000000..a44a48b --- /dev/null +++ b/demo_unified_player.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Quick demo for Unified Futures Data Player +快速演示统一价格轴版本的期货数据播放器 +""" + +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt + +def demo_unified_chart(): + """演示统一价格轴图表效果""" + print("=== Unified Market Depth Demo ===") + print("Creating sample visualization...") + + # Load some sample data + df = pd.read_parquet('data/au2512_20251013.parquet') + + # Column mapping + columns_mapping = { + '时间': 'time', '累积成交量': 'cumulative_volume', '成交价': 'price', + '买1价': 'bid1_price', '卖1价': 'ask1_price', + '买1量': 'bid1_volume', '卖1量': 'ask1_volume', + '买2价': 'bid2_price', '卖2价': 'ask2_price', + '买2量': 'bid2_volume', '卖2量': 'ask2_volume', + '买3价': 'bid3_price', '卖3价': 'ask3_price', + '买3量': 'bid3_volume', '卖3量': 'ask3_volume', + '买4价': 'bid4_price', '卖4价': 'ask4_price', + '买4量': 'bid4_volume', '卖4量': 'ask4_volume', + '买5价': 'bid5_price', '卖5价': 'ask5_price', + '买5量': 'bid5_volume', '卖5量': 'ask5_volume' + } + df = df.rename(columns=columns_mapping) + + # Get a sample row + sample_row = df.iloc[1000] # Sample from middle of data + current_price = sample_row['price'] + min_tick = 0.02 + + # Create figure + fig, ax = plt.subplots(figsize=(12, 8)) + fig.suptitle('Unified Market Depth Demo - AU2512', fontsize=16, fontweight='bold') + + # Collect market depth data + bid_prices = [] + bid_volumes = [] + ask_prices = [] + ask_volumes = [] + + for i in range(1, 6): + bid_price = sample_row[f'bid{i}_price'] + bid_volume = sample_row[f'bid{i}_volume'] + ask_price = sample_row[f'ask{i}_price'] + ask_volume = sample_row[f'ask{i}_volume'] + + if pd.notna(bid_price) and pd.notna(bid_volume) and bid_volume > 0: + bid_prices.append(bid_price) + bid_volumes.append(bid_volume) + + if pd.notna(ask_price) and pd.notna(ask_volume) and ask_volume > 0: + ask_prices.append(ask_price) + ask_volumes.append(ask_volume) + + print(f"Sample data at sequence 1000:") + print(f"Current price: {current_price:.2f}") + print(f"Bid prices: {[f'{p:.2f}' for p in bid_prices]}") + print(f"Ask prices: {[f'{p:.2f}' for p in ask_prices]}") + + # Define volume scale points (equal spacing) + volume_scale_points = [10, 30, 60, 150, 300] + + # Create unified visualization with horizontal bars + all_prices = [] + all_volumes = [] + all_colors = [] + + # Add bid data (green horizontal bars) + for bp, bv in zip(bid_prices, bid_volumes): + all_prices.append(bp) + all_volumes.append(bv) + all_colors.append('green') + + # Add ask data (red horizontal bars) + for ap, av in zip(ask_prices, ask_volumes): + all_prices.append(ap) + all_volumes.append(av) + all_colors.append('red') + + # Map volumes to scale positions + def map_volume_to_scale(volume): + """Map actual volume to scale position""" + if volume <= 10: + return 10 + elif volume <= 30: + return 30 + elif volume <= 60: + return 60 + elif volume <= 150: + return 150 + elif volume <= 300: + return 300 + else: + return 300 # Cap at 300 for display + + # Map volumes to display scale + mapped_volumes = [map_volume_to_scale(v) for v in all_volumes] + + # Create horizontal bars (volume on x-axis, price on y-axis) + bars = ax.barh(all_prices, mapped_volumes, height=min_tick * 0.8, + color=all_colors, alpha=0.7, + edgecolor='darkgreen' if 'green' in all_colors else 'darkred', + linewidth=1) + + # Add volume labels on bars + for price, volume, mapped_vol in zip(all_prices, all_volumes, mapped_volumes): + if volume > 0: + ax.text(mapped_vol + 5, price, f'{int(volume):,}', + ha='left', va='center', fontsize=10, fontweight='bold') + + # Set up price axis with true tick spacing + price_range = 1.0 + min_price = current_price - price_range/2 + max_price = current_price + price_range/2 + + tick_prices = np.arange(np.floor(min_price / min_tick) * min_tick, + np.ceil(max_price / min_tick) * min_tick + min_tick, + min_tick) + + ax.set_xticks(tick_prices[::3]) # Show every 3rd tick + ax.set_xticklabels([f'{p:.2f}' for p in tick_prices[::3]], rotation=45) + ax.set_xlim(min_price, max_price) + + # Add current price line + ax.axvline(x=current_price, color='blue', linestyle='--', alpha=0.8, linewidth=2, + label=f'Last Price: {current_price:.2f}') + + # Highlight spread + if bid_prices and ask_prices: + best_bid = max(bid_prices) + best_ask = min(ask_prices) + spread = best_ask - best_bid + + ax.axvspan(best_bid, best_ask, alpha=0.3, color='yellow', + label=f'Spread: {spread:.2f}') + + print(f"Best bid: {best_bid:.2f}, Best ask: {best_ask:.2f}") + print(f"Bid-ask spread: {spread:.2f}") + + # Set y-axis with true tick spacing (price) + ax.set_yticks(tick_prices[::3]) # Show every 3rd tick to avoid crowding + ax.set_yticklabels([f'{p:.2f}' for p in tick_prices[::3]]) + ax.set_ylim(min_price, max_price) + + # Set x-axis with defined volume scale points + ax.set_xticks(volume_scale_points) + ax.set_xticklabels([f'{v}' for v in volume_scale_points]) + ax.set_xlim(0, max(volume_scale_points) * 1.2) + + # Labels and formatting + ax.set_xlabel('Volume', fontsize=12) + ax.set_ylabel('Price (¥)', fontsize=12) + ax.set_title('Unified Market Depth View', fontsize=14) + ax.grid(True, alpha=0.3, axis='x') + + # Legend + import matplotlib.patches as mpatches + bid_patch = mpatches.Patch(color='green', alpha=0.7, label='Bid Volume') + ask_patch = mpatches.Patch(color='red', alpha=0.7, label='Ask Volume') + ax.legend(handles=[bid_patch, ask_patch], loc='upper right') + + # Add annotations + ax.text(0.02, 0.98, f"Time: {sample_row['time']}", + transform=ax.transAxes, fontsize=10, + verticalalignment='top', + bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.8)) + + ax.text(0.02, 0.92, f"Tick Size: {min_tick}", + transform=ax.transAxes, fontsize=10, + verticalalignment='top', + bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8)) + + plt.tight_layout() + + # Save the demo chart + output_file = 'unified_market_depth_demo.png' + plt.savefig(output_file, dpi=150, bbox_inches='tight') + print(f"\nDemo chart saved as: {output_file}") + + plt.show() + print("\nThis demo shows the unified market depth visualization where:") + print("- Green horizontal bars: Bid volumes at different price levels") + print("- Red horizontal bars: Ask volumes at different price levels") + print("- Blue dashed line: Current last price (horizontal)") + print("- Yellow shaded area: Bid-ask spread (horizontal)") + print("- Price axis (vertical) shows true minimum tick spacing (0.02)") + print("- Volume axis (horizontal) uses simplified scale: 10, 30, 60, 150, 300 (equal spacing)") + print("- Volume bars extend rightward with sizes matching the scale points") + print("- Volumes larger than 300 are capped at 300 for display consistency") + +if __name__ == "__main__": + demo_unified_chart() \ No newline at end of file diff --git a/futures_data_player.py b/futures_data_player.py new file mode 100644 index 0000000..5ac9932 --- /dev/null +++ b/futures_data_player.py @@ -0,0 +1,482 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +期货数据动态播放器 - 买卖盘深度时序可视化 +基于au2512_20251013.parquet数据 + +功能特性: +- 按数列号顺序播放买卖盘深度变化 +- 支持播放/暂停/加速/减速 +- 可跳转到指定数列号 +- 基础播放速度500ms/数据点 +""" + +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from matplotlib.widgets import Button, Slider +import tkinter as tk +from tkinter import ttk +import threading +import time +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import matplotlib.patches as mpatches +from datetime import datetime +import matplotlib.font_manager as fm + +# 设置matplotlib支持中文显示 +plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS', 'DejaVu Sans'] +plt.rcParams['axes.unicode_minus'] = False + +class FuturesDataPlayer: + def __init__(self, data_path='data/au2512_20251013.parquet'): + """初始化期货数据播放器""" + self.data_path = data_path + self.df = None + self.current_index = 0 + self.is_playing = False + self.play_speed = 500 # 基础播放速度(毫秒) + self.speed_multiplier = 1.0 + + # 加载数据 + self.load_data() + + # 创建GUI界面 + self.setup_gui() + + def load_data(self): + """加载并预处理期货数据""" + print("正在加载数据...") + self.df = pd.read_parquet(self.data_path) + + # 列名映射 + columns_mapping = { + 'UTC': 'UTC', + 'UTC.1': 'UTC_1', + '时间': 'time', + '累积成交量': 'cumulative_volume', + '成交价': 'price', + '成交额': 'amount', + '买1价': 'bid1_price', '卖1价': 'ask1_price', + '买1量': 'bid1_volume', '卖1量': 'ask1_volume', + '买2价': 'bid2_price', '卖2价': 'ask2_price', + '买2量': 'bid2_volume', '卖2量': 'ask2_volume', + '买3价': 'bid3_price', '卖3价': 'ask3_price', + '买3量': 'bid3_volume', '卖3量': 'ask3_volume', + '买4价': 'bid4_price', '卖4价': 'ask4_price', + '买4量': 'bid4_volume', '卖4量': 'ask4_volume', + '买5价': 'bid5_price', '卖5价': 'ask5_price', + '买5量': 'bid5_volume', '卖5量': 'ask5_volume' + } + + # 重命名列 + self.df = self.df.rename(columns=columns_mapping) + + # 数据清洗:去除无效的买卖盘数据 + bid_cols = [f'bid{i}_price' for i in range(1, 6)] + ask_cols = [f'ask{i}_price' for i in range(1, 6)] + + # 过滤掉买卖盘价格无效的数据 + valid_mask = ( + self.df[bid_cols].notnull().all(axis=1) & + self.df[ask_cols].notnull().all(axis=1) + ) + self.df = self.df[valid_mask].reset_index(drop=True) + + print(f"数据加载完成!有效数据点: {len(self.df)}") + print(f"时间范围: {self.df['time'].iloc[0]} - {self.df['time'].iloc[-1]}") + print(f"价格范围: {self.df['price'].min():.2f} - {self.df['price'].max():.2f}") + + def setup_gui(self): + """设置GUI界面""" + # 创建主窗口 + self.root = tk.Tk() + self.root.title("期货数据动态播放器 - AU2512买卖盘深度分析") + self.root.geometry("1400x900") + + # 创建主框架 + main_frame = ttk.Frame(self.root) + main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 左侧:图表区域 + chart_frame = ttk.Frame(main_frame) + chart_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # 右侧:控制面板 + control_frame = ttk.Frame(main_frame, width=300) + control_frame.pack(side=tk.RIGHT, fill=tk.Y, padx=(10, 0)) + control_frame.pack_propagate(False) + + # 创建matplotlib图表 + self.setup_chart(chart_frame) + + # 创建控制面板 + self.setup_controls(control_frame) + + # 初始化显示 + self.update_display() + + def setup_chart(self, parent): + """设置图表""" + # 创建图形和轴 + self.fig, (self.ax1, self.ax2) = plt.subplots(2, 1, figsize=(10, 8)) + self.fig.patch.set_facecolor('#f0f0f0') + + # 买盘深度图(上方) + self.ax1.set_title('买盘深度 (Bid Depth)', fontsize=14, fontweight='bold') + self.ax1.set_xlabel('价格') + self.ax1.set_ylabel('累积成交量') + self.ax1.grid(True, alpha=0.3) + self.ax1.set_facecolor('#ffffff') + + # 卖盘深度图(下方) + self.ax2.set_title('卖盘深度 (Ask Depth)', fontsize=14, fontweight='bold') + self.ax2.set_xlabel('价格') + self.ax2.set_ylabel('累积成交量') + self.ax2.grid(True, alpha=0.3) + self.ax2.set_facecolor('#ffffff') + + plt.tight_layout() + + # 嵌入到tkinter + self.canvas = FigureCanvasTkAgg(self.fig, parent) + self.canvas.draw() + self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + def setup_controls(self, parent): + """设置控制面板""" + # 标题 + title_label = ttk.Label(parent, text="播放控制", font=('Arial', 16, 'bold')) + title_label.pack(pady=10) + + # 当前状态显示 + status_frame = ttk.LabelFrame(parent, text="当前状态", padding=10) + status_frame.pack(fill=tk.X, pady=10) + + self.index_label = ttk.Label(status_frame, text="数列号: 0", font=('Arial', 11)) + self.index_label.pack(anchor=tk.W) + + self.time_label = ttk.Label(status_frame, text="时间: --", font=('Arial', 11)) + self.time_label.pack(anchor=tk.W) + + self.price_label = ttk.Label(status_frame, text="成交价: --", font=('Arial', 11)) + self.price_label.pack(anchor=tk.W) + + self.volume_label = ttk.Label(status_frame, text="累积成交量: --", font=('Arial', 11)) + self.volume_label.pack(anchor=tk.W) + + # 播放控制按钮 + control_frame = ttk.LabelFrame(parent, text="播放控制", padding=10) + control_frame.pack(fill=tk.X, pady=10) + + # 播放/暂停按钮 + self.play_button = ttk.Button( + control_frame, + text="▶ 播放", + command=self.toggle_play, + width=20 + ) + self.play_button.pack(pady=5) + + # 停止按钮 + self.stop_button = ttk.Button( + control_frame, + text="■ 停止", + command=self.stop_playback, + width=20 + ) + self.stop_button.pack(pady=5) + + # 速度控制 + speed_frame = ttk.LabelFrame(parent, text="速度控制", padding=10) + speed_frame.pack(fill=tk.X, pady=10) + + # 速度滑块 + self.speed_var = tk.DoubleVar(value=1.0) + speed_slider = ttk.Scale( + speed_frame, + from_=0.1, + to=5.0, + variable=self.speed_var, + orient=tk.HORIZONTAL, + command=self.update_speed + ) + speed_slider.pack(fill=tk.X, pady=5) + + self.speed_label = ttk.Label(speed_frame, text="播放速度: 1.0x") + self.speed_label.pack() + + # 快速速度按钮 + speed_buttons_frame = ttk.Frame(speed_frame) + speed_buttons_frame.pack(pady=5) + + ttk.Button(speed_buttons_frame, text="0.5x", command=lambda: self.set_speed(0.5), width=8).pack(side=tk.LEFT, padx=2) + ttk.Button(speed_buttons_frame, text="1x", command=lambda: self.set_speed(1.0), width=8).pack(side=tk.LEFT, padx=2) + ttk.Button(speed_buttons_frame, text="2x", command=lambda: self.set_speed(2.0), width=8).pack(side=tk.LEFT, padx=2) + ttk.Button(speed_buttons_frame, text="5x", command=lambda: self.set_speed(5.0), width=8).pack(side=tk.LEFT, padx=2) + + # 跳转控制 + jump_frame = ttk.LabelFrame(parent, text="跳转控制", padding=10) + jump_frame.pack(fill=tk.X, pady=10) + + # 数列号输入 + ttk.Label(jump_frame, text="跳转到数列号:").pack(anchor=tk.W) + + jump_input_frame = ttk.Frame(jump_frame) + jump_input_frame.pack(fill=tk.X, pady=5) + + self.jump_entry = ttk.Entry(jump_input_frame) + self.jump_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + ttk.Button(jump_input_frame, text="跳转", command=self.jump_to_index, width=10).pack(side=tk.RIGHT, padx=(5, 0)) + + # 进度条 + progress_frame = ttk.LabelFrame(parent, text="播放进度", padding=10) + progress_frame.pack(fill=tk.X, pady=10) + + self.progress_var = tk.IntVar() + self.progress_bar = ttk.Progressbar( + progress_frame, + variable=self.progress_var, + maximum=len(self.df) - 1, + orient=tk.HORIZONTAL + ) + self.progress_bar.pack(fill=tk.X, pady=5) + + self.progress_label = ttk.Label(progress_frame, text=f"0 / {len(self.df) - 1}") + self.progress_label.pack() + + # 数据统计 + stats_frame = ttk.LabelFrame(parent, text="数据统计", padding=10) + stats_frame.pack(fill=tk.X, pady=10) + + stats_text = f"总数据点: {len(self.df)}\\n" + stats_text += f"时间跨度: {self.df['time'].iloc[0]} - {self.df['time'].iloc[-1]}\\n" + stats_text += f"价格区间: {self.df['price'].min():.2f} - {self.df['price'].max():.2f}" + + ttk.Label(stats_frame, text=stats_text, font=('Arial', 9)).pack(anchor=tk.W) + + def get_market_depth_data(self, row_data): + """获取买卖盘深度数据""" + bid_prices = [] + bid_volumes = [] + ask_prices = [] + ask_volumes = [] + + # 收集买卖盘数据(从1到5档) + for i in range(1, 6): + bid_price = row_data[f'bid{i}_price'] + bid_volume = row_data[f'bid{i}_volume'] + ask_price = row_data[f'ask{i}_price'] + ask_volume = row_data[f'ask{i}_volume'] + + if pd.notna(bid_price) and pd.notna(bid_volume) and bid_volume > 0: + bid_prices.append(bid_price) + bid_volumes.append(bid_volume) + + if pd.notna(ask_price) and pd.notna(ask_volume) and ask_volume > 0: + ask_prices.append(ask_price) + ask_volumes.append(ask_volume) + + return bid_prices, bid_volumes, ask_prices, ask_volumes + + def update_display(self): + """更新显示内容""" + if self.df is None or self.current_index >= len(self.df): + return + + # 获取当前行数据 + row_data = self.df.iloc[self.current_index] + + # 清空图表 + self.ax1.clear() + self.ax2.clear() + + # 重新设置图表属性 + self.ax1.set_title('买盘深度 (Bid Depth)', fontsize=14, fontweight='bold') + self.ax1.set_xlabel('价格') + self.ax1.set_ylabel('成交量') + self.ax1.grid(True, alpha=0.3) + self.ax1.set_facecolor('#ffffff') + + self.ax2.set_title('卖盘深度 (Ask Depth)', fontsize=14, fontweight='bold') + self.ax2.set_xlabel('价格') + self.ax2.set_ylabel('成交量') + self.ax2.grid(True, alpha=0.3) + self.ax2.set_facecolor('#ffffff') + + # 获取买卖盘深度数据 + bid_prices, bid_volumes, ask_prices, ask_volumes = self.get_market_depth_data(row_data) + + # 绘制买盘深度(绿色,从左到右递减) + if bid_prices: + # 按价格排序(买盘价格从高到低) + bid_data = sorted(zip(bid_prices, bid_volumes), reverse=True) + sorted_bid_prices, sorted_bid_volumes = zip(*bid_data) + + # 计算累积成交量 + cumulative_bid_volumes = np.cumsum(sorted_bid_volumes) + + # 绘制买盘柱状图 + colors_bid = plt.cm.Greens(np.linspace(0.4, 0.9, len(sorted_bid_prices))) + bars1 = self.ax1.barh(range(len(sorted_bid_prices)), sorted_bid_volumes, + color=colors_bid, alpha=0.8, edgecolor='darkgreen', linewidth=1) + + # 设置Y轴标签为价格 + self.ax1.set_yticks(range(len(sorted_bid_prices))) + self.ax1.set_yticklabels([f'{p:.2f}' for p in sorted_bid_prices]) + + # 添加成交量标签 + for i, (price, volume) in enumerate(bid_data): + self.ax1.text(volume + max(sorted_bid_volumes) * 0.01, i, f'{int(volume)}', + va='center', ha='left', fontsize=9) + + # 设置X轴范围 + max_bid_vol = max(sorted_bid_volumes) + self.ax1.set_xlim(0, max_bid_vol * 1.3) + + else: + self.ax1.text(0.5, 0.5, '无买盘数据', ha='center', va='center', + transform=self.ax1.transAxes, fontsize=12) + + # 绘制卖盘深度(红色,从左到右递增) + if ask_prices: + # 按价格排序(卖盘价格从低到高) + ask_data = sorted(zip(ask_prices, ask_volumes)) + sorted_ask_prices, sorted_ask_volumes = zip(*ask_data) + + # 绘制卖盘柱状图 + colors_ask = plt.cm.Reds(np.linspace(0.4, 0.9, len(sorted_ask_prices))) + bars2 = self.ax2.barh(range(len(sorted_ask_prices)), sorted_ask_volumes, + color=colors_ask, alpha=0.8, edgecolor='darkred', linewidth=1) + + # 设置Y轴标签为价格 + self.ax2.set_yticks(range(len(sorted_ask_prices))) + self.ax2.set_yticklabels([f'{p:.2f}' for p in sorted_ask_prices]) + + # 添加成交量标签 + for i, (price, volume) in enumerate(ask_data): + self.ax2.text(volume + max(sorted_ask_volumes) * 0.01, i, f'{int(volume)}', + va='center', ha='left', fontsize=9) + + # 设置X轴范围 + max_ask_vol = max(sorted_ask_volumes) + self.ax2.set_xlim(0, max_ask_vol * 1.3) + + else: + self.ax2.text(0.5, 0.5, '无卖盘数据', ha='center', va='center', + transform=self.ax2.transAxes, fontsize=12) + + # 添加当前成交价标记 + current_price = row_data['price'] + self.ax1.axvline(x=current_price, color='blue', linestyle='--', alpha=0.7, label=f'成交价: {current_price:.2f}') + self.ax2.axvline(x=current_price, color='blue', linestyle='--', alpha=0.7, label=f'成交价: {current_price:.2f}') + + self.ax1.legend(loc='upper right') + self.ax2.legend(loc='upper right') + + plt.tight_layout() + self.canvas.draw() + + # 更新状态标签 + self.index_label.config(text=f"数列号: {self.current_index}") + self.time_label.config(text=f"时间: {row_data['time']}") + self.price_label.config(text=f"成交价: {current_price:.2f}") + self.volume_label.config(text=f"累积成交量: {int(row_data['cumulative_volume']):,}") + + # 更新进度条 + self.progress_var.set(self.current_index) + self.progress_label.config(text=f"{self.current_index} / {len(self.df) - 1}") + + def toggle_play(self): + """切换播放/暂停状态""" + if not self.is_playing: + self.is_playing = True + self.play_button.config(text="⏸ 暂停") + # 启动播放线程 + self.play_thread = threading.Thread(target=self.playback_loop, daemon=True) + self.play_thread.start() + else: + self.is_playing = False + self.play_button.config(text="▶ 播放") + + def stop_playback(self): + """停止播放""" + self.is_playing = False + self.play_button.config(text="▶ 播放") + self.current_index = 0 + self.update_display() + + def playback_loop(self): + """播放循环""" + while self.is_playing and self.current_index < len(self.df) - 1: + self.current_index += 1 + # 在主线程中更新显示 + self.root.after(0, self.update_display) + + # 根据速度设置延迟 + delay = self.play_speed / self.speed_multiplier + time.sleep(delay / 1000.0) # 转换为秒 + + # 播放结束 + if self.current_index >= len(self.df) - 1: + self.is_playing = False + self.root.after(0, lambda: self.play_button.config(text="▶ 播放")) + + def update_speed(self, value): + """更新播放速度""" + self.speed_multiplier = float(value) + self.speed_label.config(text=f"播放速度: {self.speed_multiplier:.1f}x") + + def set_speed(self, speed): + """设置播放速度""" + self.speed_var.set(speed) + self.speed_multiplier = speed + self.speed_label.config(text=f"播放速度: {self.speed_multiplier:.1f}x") + + def jump_to_index(self): + """跳转到指定数列号""" + try: + target_index = int(self.jump_entry.get()) + if 0 <= target_index < len(self.df): + self.current_index = target_index + self.update_display() + else: + tk.messagebox.showwarning("输入错误", f"请输入0到{len(self.df)-1}之间的数列号") + except ValueError: + tk.messagebox.showwarning("输入错误", "请输入有效的数字") + + def run(self): + """运行应用""" + # 绑定关闭事件 + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + + # 启动主循环 + self.root.mainloop() + + def on_closing(self): + """关闭应用时的处理""" + self.is_playing = False + self.root.destroy() + +def main(): + """主函数""" + print("=== 期货数据动态播放器 ===") + print("正在初始化...") + + # 创建播放器实例 + player = FuturesDataPlayer() + + print("初始化完成!") + print("\\n使用说明:") + print("- 点击'播放'按钮开始播放买卖盘深度变化") + print("- 使用速度控制调整播放速度") + print("- 可以跳转到指定的数列号") + print("- 观察买盘(绿色)和卖盘(红色)的深度变化") + + # 运行应用 + player.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/futures_player_enhanced.py b/futures_player_enhanced.py new file mode 100644 index 0000000..c261614 --- /dev/null +++ b/futures_player_enhanced.py @@ -0,0 +1,539 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Enhanced Futures Data Player - Market Depth Time Series Visualization +Based on au2512_20251013.parquet data + +Features: +- Play market depth changes by sequence number +- Support play/pause/speed up/slow down +- Jump to specific sequence number +- Base playback speed 500ms/data point +- English interface to avoid font issues +""" + +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +import tkinter as tk +from tkinter import ttk, messagebox +import threading +import time +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk +from matplotlib.figure import Figure +import matplotlib.gridspec as gridspec + +class EnhancedFuturesPlayer: + def __init__(self, data_path='data/au2512_20251013.parquet'): + """初始化增强版期货数据播放器""" + self.data_path = data_path + self.df = None + self.current_index = 0 + self.is_playing = False + self.base_speed = 500 # Base speed in milliseconds + self.speed_multiplier = 1.0 + self.play_thread = None + + # Load and prepare data + self.load_data() + + # Create GUI + self.setup_gui() + + # Initial display + self.update_display() + + def load_data(self): + """加载并预处理期货数据""" + print("Loading futures data...") + self.df = pd.read_parquet(self.data_path) + + # Column mapping + columns_mapping = { + 'UTC': 'UTC', 'UTC.1': 'UTC_1', '时间': 'time', + '累积成交量': 'cumulative_volume', '成交价': 'price', '成交额': 'amount', + '买1价': 'bid1_price', '卖1价': 'ask1_price', + '买1量': 'bid1_volume', '卖1量': 'ask1_volume', + '买2价': 'bid2_price', '卖2价': 'ask2_price', + '买2量': 'bid2_volume', '卖2量': 'ask2_volume', + '买3价': 'bid3_price', '卖3价': 'ask3_price', + '买3量': 'bid3_volume', '卖3量': 'ask3_volume', + '买4价': 'bid4_price', '卖4价': 'ask4_price', + '买4量': 'bid4_volume', '卖4量': 'ask4_volume', + '买5价': 'bid5_price', '卖5价': 'ask5_price', + '买5量': 'bid5_volume', '卖5量': 'ask5_volume' + } + + self.df = self.df.rename(columns=columns_mapping) + + # Clean data: remove invalid market depth data + bid_cols = [f'bid{i}_price' for i in range(1, 6)] + ask_cols = [f'ask{i}_price' for i in range(1, 6)] + bid_vol_cols = [f'bid{i}_volume' for i in range(1, 6)] + ask_vol_cols = [f'ask{i}_volume' for i in range(1, 6)] + + # Filter out rows with invalid bid/ask data + valid_mask = ( + self.df[bid_cols].notnull().all(axis=1) & + self.df[ask_cols].notnull().all(axis=1) & + (self.df[bid_vol_cols] > 0).any(axis=1) & + (self.df[ask_vol_cols] > 0).any(axis=1) + ) + self.df = self.df[valid_mask].reset_index(drop=True) + + print(f"Data loaded successfully! Valid data points: {len(self.df)}") + print(f"Time range: {self.df['time'].iloc[0]} - {self.df['time'].iloc[-1]}") + print(f"Price range: {self.df['price'].min():.2f} - {self.df['price'].max():.2f}") + + def setup_gui(self): + """设置GUI界面""" + self.root = tk.Tk() + self.root.title("Enhanced Futures Data Player - AU2512 Market Depth Analysis") + self.root.geometry("1600x900") + + # Create main container with paned window + main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) + main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # Left frame for charts + chart_frame = ttk.Frame(main_paned) + main_paned.add(chart_frame, weight=3) + + # Right frame for controls + control_frame = ttk.Frame(main_paned, width=400) + main_paned.add(control_frame, weight=1) + + # Setup charts + self.setup_charts(chart_frame) + + # Setup controls + self.setup_controls(control_frame) + + def setup_charts(self, parent): + """设置图表区域""" + # Create figure with custom layout + self.fig = Figure(figsize=(12, 8), dpi=80) + gs = gridspec.GridSpec(3, 2, figure=self.fig, height_ratios=[1, 1, 1], width_ratios=[1, 1]) + + # Create subplots + self.ax_bid = self.fig.add_subplot(gs[0, :]) # Bid depth (top, spanning both columns) + self.ax_ask = self.fig.add_subplot(gs[1, :]) # Ask depth (middle, spanning both columns) + self.ax_price = self.fig.add_subplot(gs[2, 0]) # Price trend (bottom left) + self.ax_volume = self.fig.add_subplot(gs[2, 1]) # Volume (bottom right) + + # Set titles and labels + self.ax_bid.set_title('Bid Market Depth', fontsize=14, fontweight='bold', color='darkgreen') + self.ax_bid.set_xlabel('Volume') + self.ax_bid.set_ylabel('Price') + self.ax_bid.grid(True, alpha=0.3) + + self.ax_ask.set_title('Ask Market Depth', fontsize=14, fontweight='bold', color='darkred') + self.ax_ask.set_xlabel('Volume') + self.ax_ask.set_ylabel('Price') + self.ax_ask.grid(True, alpha=0.3) + + self.ax_price.set_title('Price Trend', fontsize=12, fontweight='bold') + self.ax_price.set_xlabel('Data Points') + self.ax_price.set_ylabel('Price') + self.ax_price.grid(True, alpha=0.3) + + self.ax_volume.set_title('Volume Profile', fontsize=12, fontweight='bold') + self.ax_volume.set_xlabel('Data Points') + self.ax_volume.set_ylabel('Cumulative Volume') + self.ax_volume.grid(True, alpha=0.3) + + self.fig.tight_layout() + + # Embed in tkinter + canvas_frame = ttk.Frame(parent) + canvas_frame.pack(fill=tk.BOTH, expand=True) + + self.canvas = FigureCanvasTkAgg(self.fig, canvas_frame) + self.canvas.draw() + self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + # Add toolbar + toolbar = NavigationToolbar2Tk(self.canvas, canvas_frame) + toolbar.update() + + def setup_controls(self, parent): + """设置控制面板""" + # Title + title_label = ttk.Label(parent, text="Playback Controls", font=('Arial', 16, 'bold')) + title_label.pack(pady=10) + + # Current status frame + status_frame = ttk.LabelFrame(parent, text="Current Status", padding=10) + status_frame.pack(fill=tk.X, pady=10) + + self.status_labels = {} + status_items = [ + ('index', 'Sequence: 0'), + ('time', 'Time: --'), + ('price', 'Price: --'), + ('volume', 'Cumulative Volume: --'), + ('spread', 'Bid-Ask Spread: --') + ] + + for key, text in status_items: + label = ttk.Label(status_frame, text=text, font=('Arial', 10)) + label.pack(anchor=tk.W, pady=2) + self.status_labels[key] = label + + # Playback controls + control_frame = ttk.LabelFrame(parent, text="Playback Controls", padding=10) + control_frame.pack(fill=tk.X, pady=10) + + # Play/Pause button + self.play_button = ttk.Button( + control_frame, + text="▶ Play", + command=self.toggle_playback, + width=25 + ) + self.play_button.pack(pady=5) + + # Stop button + self.stop_button = ttk.Button( + control_frame, + text="■ Stop", + command=self.stop_playback, + width=25 + ) + self.stop_button.pack(pady=5) + + # Step buttons + step_frame = ttk.Frame(control_frame) + step_frame.pack(pady=5) + + ttk.Button(step_frame, text="◀◀ Step -10", command=lambda: self.step_backward(10), width=12).pack(side=tk.LEFT, padx=2) + ttk.Button(step_frame, text="◀ Step -1", command=lambda: self.step_backward(1), width=12).pack(side=tk.LEFT, padx=2) + + step_frame2 = ttk.Frame(control_frame) + step_frame2.pack(pady=5) + + ttk.Button(step_frame2, text="Step +1 ▶", command=lambda: self.step_forward(1), width=12).pack(side=tk.LEFT, padx=2) + ttk.Button(step_frame2, text="Step +10 ▶▶", command=lambda: self.step_forward(10), width=12).pack(side=tk.LEFT, padx=2) + + # Speed control + speed_frame = ttk.LabelFrame(parent, text="Speed Control", padding=10) + speed_frame.pack(fill=tk.X, pady=10) + + # Speed slider + self.speed_var = tk.DoubleVar(value=1.0) + speed_slider = ttk.Scale( + speed_frame, + from_=0.1, + to=10.0, + variable=self.speed_var, + orient=tk.HORIZONTAL, + command=self.update_speed + ) + speed_slider.pack(fill=tk.X, pady=5) + + self.speed_label = ttk.Label(speed_frame, text="Speed: 1.0x", font=('Arial', 11)) + self.speed_label.pack() + + # Quick speed buttons + speed_buttons = ttk.Frame(speed_frame) + speed_buttons.pack(pady=5) + + speeds = [(0.25, '0.25x'), (0.5, '0.5x'), (1.0, '1x'), (2.0, '2x'), (5.0, '5x'), (10.0, '10x')] + for speed, text in speeds: + ttk.Button(speed_buttons, text=text, command=lambda s=speed: self.set_speed(s), width=6).pack(side=tk.LEFT, padx=2) + + # Jump control + jump_frame = ttk.LabelFrame(parent, text="Jump Control", padding=10) + jump_frame.pack(fill=tk.X, pady=10) + + ttk.Label(jump_frame, text="Jump to sequence:", font=('Arial', 10)).pack(anchor=tk.W) + + jump_input_frame = ttk.Frame(jump_frame) + jump_input_frame.pack(fill=tk.X, pady=5) + + self.jump_entry = ttk.Entry(jump_input_frame, font=('Arial', 10)) + self.jump_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + + ttk.Button(jump_input_frame, text="Jump", command=self.jump_to_index, width=10).pack(side=tk.RIGHT, padx=(5, 0)) + + # Quick jump buttons + quick_jump_frame = ttk.Frame(jump_frame) + quick_jump_frame.pack(pady=5) + + jumps = [(0, 'Start'), (len(self.df)//4, '25%'), (len(self.df)//2, '50%'), + (3*len(self.df)//4, '75%'), (len(self.df)-1, 'End')] + for idx, text in jumps: + ttk.Button(quick_jump_frame, text=text, command=lambda i=idx: self.jump_to_index_direct(i), width=8).pack(side=tk.LEFT, padx=2) + + # Progress + progress_frame = ttk.LabelFrame(parent, text="Progress", padding=10) + progress_frame.pack(fill=tk.X, pady=10) + + self.progress_var = tk.IntVar() + self.progress_bar = ttk.Progressbar( + progress_frame, + variable=self.progress_var, + maximum=len(self.df) - 1, + orient=tk.HORIZONTAL + ) + self.progress_bar.pack(fill=tk.X, pady=5) + + self.progress_label = ttk.Label(progress_frame, text=f"0 / {len(self.df) - 1}", font=('Arial', 10)) + self.progress_label.pack() + + # Data statistics + stats_frame = ttk.LabelFrame(parent, text="Data Statistics", padding=10) + stats_frame.pack(fill=tk.X, pady=10) + + stats_text = f"Total Data Points: {len(self.df):,}\\n" + stats_text += f"Time Span: {self.df['time'].iloc[0]} - {self.df['time'].iloc[-1]}\\n" + stats_text += f"Price Range: ${self.df['price'].min():.2f} - ${self.df['price'].max():.2f}\\n" + stats_text += f"Price Change: ${self.df['price'].iloc[-1] - self.df['price'].iloc[0]:+.2f}" + + ttk.Label(stats_frame, text=stats_text, font=('Arial', 9), justify=tk.LEFT).pack(anchor=tk.W) + + def get_market_depth_data(self, row_data): + """获取买卖盘深度数据""" + bid_prices = [] + bid_volumes = [] + ask_prices = [] + ask_volumes = [] + + for i in range(1, 6): + bid_price = row_data[f'bid{i}_price'] + bid_volume = row_data[f'bid{i}_volume'] + ask_price = row_data[f'ask{i}_price'] + ask_volume = row_data[f'ask{i}_volume'] + + if pd.notna(bid_price) and pd.notna(bid_volume) and bid_volume > 0: + bid_prices.append(bid_price) + bid_volumes.append(bid_volume) + + if pd.notna(ask_price) and pd.notna(ask_volume) and ask_volume > 0: + ask_prices.append(ask_price) + ask_volumes.append(ask_volume) + + return bid_prices, bid_volumes, ask_prices, ask_volumes + + def update_display(self): + """更新显示内容""" + if self.df is None or self.current_index >= len(self.df): + return + + row_data = self.df.iloc[self.current_index] + + # Clear all axes + self.ax_bid.clear() + self.ax_ask.clear() + self.ax_price.clear() + self.ax_volume.clear() + + # Get market depth data + bid_prices, bid_volumes, ask_prices, ask_volumes = self.get_market_depth_data(row_data) + + # Plot bid depth (horizontal bars) + if bid_prices: + bid_data = sorted(zip(bid_prices, bid_volumes), reverse=True) + sorted_bid_prices, sorted_bid_volumes = zip(*bid_data) + cumulative_bid_volumes = np.cumsum(sorted_bid_volumes) + + # Create color gradient + colors = plt.cm.Greens(np.linspace(0.3, 0.9, len(sorted_bid_prices))) + + bars = self.ax_bid.barh(range(len(sorted_bid_prices)), sorted_bid_volumes, + color=colors, alpha=0.8, edgecolor='darkgreen', linewidth=1.5) + + self.ax_bid.set_yticks(range(len(sorted_bid_prices))) + self.ax_bid.set_yticklabels([f'${p:.2f}' for p in sorted_bid_prices]) + self.ax_bid.set_xlabel('Volume') + self.ax_bid.set_title('Bid Market Depth', fontsize=14, fontweight='bold', color='darkgreen') + self.ax_bid.grid(True, alpha=0.3, axis='x') + + # Add volume labels + for i, (price, volume) in enumerate(bid_data): + self.ax_bid.text(volume + max(sorted_bid_volumes) * 0.02, i, f'{int(volume):,}', + va='center', ha='left', fontsize=9, fontweight='bold') + + max_bid_vol = max(sorted_bid_volumes) + self.ax_bid.set_xlim(0, max_bid_vol * 1.3) + + # Plot ask depth (horizontal bars) + if ask_prices: + ask_data = sorted(zip(ask_prices, ask_volumes)) + sorted_ask_prices, sorted_ask_volumes = zip(*ask_data) + cumulative_ask_volumes = np.cumsum(sorted_ask_volumes) + + colors = plt.cm.Reds(np.linspace(0.3, 0.9, len(sorted_ask_prices))) + + bars = self.ax_ask.barh(range(len(sorted_ask_prices)), sorted_ask_volumes, + color=colors, alpha=0.8, edgecolor='darkred', linewidth=1.5) + + self.ax_ask.set_yticks(range(len(sorted_ask_prices))) + self.ax_ask.set_yticklabels([f'${p:.2f}' for p in sorted_ask_prices]) + self.ax_ask.set_xlabel('Volume') + self.ax_ask.set_title('Ask Market Depth', fontsize=14, fontweight='bold', color='darkred') + self.ax_ask.grid(True, alpha=0.3, axis='x') + + # Add volume labels + for i, (price, volume) in enumerate(ask_data): + self.ax_ask.text(volume + max(sorted_ask_volumes) * 0.02, i, f'{int(volume):,}', + va='center', ha='left', fontsize=9, fontweight='bold') + + max_ask_vol = max(sorted_ask_volumes) + self.ax_ask.set_xlim(0, max_ask_vol * 1.3) + + # Plot price trend (last 100 points) + window = min(100, self.current_index + 1) + start_idx = max(0, self.current_index - window + 1) + price_slice = self.df['price'].iloc[start_idx:self.current_index + 1] + + self.ax_price.plot(range(len(price_slice)), price_slice, 'b-', linewidth=2, label='Price') + self.ax_price.scatter(len(price_slice) - 1, price_slice.iloc[-1], color='red', s=100, zorder=5, label='Current') + self.ax_price.set_title('Price Trend (Recent 100 points)', fontsize=12, fontweight='bold') + self.ax_price.set_xlabel('Data Points') + self.ax_price.set_ylabel('Price ($)') + self.ax_price.grid(True, alpha=0.3) + self.ax_price.legend() + + # Plot volume profile + volume_slice = self.df['cumulative_volume'].iloc[start_idx:self.current_index + 1] + self.ax_volume.plot(range(len(volume_slice)), volume_slice, 'g-', linewidth=2) + self.ax_volume.scatter(len(volume_slice) - 1, volume_slice.iloc[-1], color='red', s=100, zorder=5) + self.ax_volume.set_title('Cumulative Volume', fontsize=12, fontweight='bold') + self.ax_volume.set_xlabel('Data Points') + self.ax_volume.set_ylabel('Volume') + self.ax_volume.grid(True, alpha=0.3) + + # Add current price line to bid/ask charts + current_price = row_data['price'] + if bid_prices: + self.ax_bid.axvline(x=current_price, color='blue', linestyle='--', alpha=0.7, linewidth=2) + if ask_prices: + self.ax_ask.axvline(x=current_price, color='blue', linestyle='--', alpha=0.7, linewidth=2) + + self.fig.tight_layout() + self.canvas.draw() + + # Update status labels + self.status_labels['index'].config(text=f"Sequence: {self.current_index:,} / {len(self.df)-1:,}") + self.status_labels['time'].config(text=f"Time: {row_data['time']}") + self.status_labels['price'].config(text=f"Price: ${current_price:.2f}") + self.status_labels['volume'].config(text=f"Cumulative Volume: {int(row_data['cumulative_volume']):,}") + + # Calculate and display spread + if bid_prices and ask_prices: + best_bid = max(bid_prices) + best_ask = min(ask_prices) + spread = best_ask - best_bid + spread_pct = (spread / current_price) * 100 + self.status_labels['spread'].config(text=f"Bid-Ask Spread: ${spread:.2f} ({spread_pct:.3f}%)") + else: + self.status_labels['spread'].config(text="Bid-Ask Spread: N/A") + + # Update progress + self.progress_var.set(self.current_index) + self.progress_label.config(text=f"{self.current_index:,} / {len(self.df)-1:,}") + + def toggle_playback(self): + """切换播放状态""" + if not self.is_playing: + self.is_playing = True + self.play_button.config(text="⏸ Pause") + self.play_thread = threading.Thread(target=self.playback_loop, daemon=True) + self.play_thread.start() + else: + self.is_playing = False + self.play_button.config(text="▶ Play") + + def stop_playback(self): + """停止播放""" + self.is_playing = False + self.play_button.config(text="▶ Play") + self.current_index = 0 + self.update_display() + + def playback_loop(self): + """播放循环""" + while self.is_playing and self.current_index < len(self.df) - 1: + self.current_index += 1 + self.root.after(0, self.update_display) + + delay = self.base_speed / self.speed_multiplier + time.sleep(delay / 1000.0) + + if self.current_index >= len(self.df) - 1: + self.is_playing = False + self.root.after(0, lambda: self.play_button.config(text="▶ Play")) + + def step_forward(self, steps=1): + """向前步进""" + new_index = min(self.current_index + steps, len(self.df) - 1) + self.current_index = new_index + self.update_display() + + def step_backward(self, steps=1): + """向后步进""" + new_index = max(self.current_index - steps, 0) + self.current_index = new_index + self.update_display() + + def update_speed(self, value): + """更新播放速度""" + self.speed_multiplier = float(value) + self.speed_label.config(text=f"Speed: {self.speed_multiplier:.1f}x") + + def set_speed(self, speed): + """设置播放速度""" + self.speed_var.set(speed) + self.speed_multiplier = speed + self.speed_label.config(text=f"Speed: {self.speed_multiplier:.1f}x") + + def jump_to_index(self): + """跳转到指定索引""" + try: + target_index = int(self.jump_entry.get()) + if 0 <= target_index < len(self.df): + self.current_index = target_index + self.update_display() + self.jump_entry.delete(0, tk.END) + else: + messagebox.showwarning("Invalid Input", f"Please enter a number between 0 and {len(self.df)-1}") + except ValueError: + messagebox.showwarning("Invalid Input", "Please enter a valid number") + + def jump_to_index_direct(self, index): + """直接跳转到指定索引""" + self.current_index = index + self.update_display() + + def run(self): + """运行应用""" + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + self.root.mainloop() + + def on_closing(self): + """关闭处理""" + self.is_playing = False + self.root.destroy() + +def main(): + """主函数""" + print("=== Enhanced Futures Data Player ===") + print("Initializing...") + + player = EnhancedFuturesPlayer() + + print("Initialization complete!") + print("\\nFeatures:") + print("- Play/pause market depth changes") + print("- Adjustable playback speed (0.1x to 10x)") + print("- Step forward/backward controls") + print("- Jump to specific sequence") + print("- Real-time bid-ask spread display") + print("- Price trend and volume charts") + + player.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/futures_player_unified.py b/futures_player_unified.py new file mode 100644 index 0000000..abbdd76 --- /dev/null +++ b/futures_player_unified.py @@ -0,0 +1,625 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Unified Futures Data Player - Combined Market Depth Visualization +基于au2512_20251013.parquet数据的统一价格轴买卖盘深度显示 + +优化特性: +- 买卖方挂单在同一价格轴图表中显示 +- 真实最小tick刻度标注 (0.02) +- 显示买卖盘之间的价格间隙 +- 统一价格轴,便于观察价差和流动性 +""" + +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.patches as mpatches +import tkinter as tk +from tkinter import ttk, messagebox +import threading +import time +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk +from matplotlib.figure import Figure +import matplotlib.gridspec as gridspec + +class UnifiedFuturesPlayer: + def __init__(self, data_path='data/au2512_20251013.parquet'): + """初始化统一价格轴期货数据播放器""" + self.data_path = data_path + self.df = None + self.current_index = 0 + self.is_playing = False + self.base_speed = 500 # Base speed in milliseconds + self.speed_multiplier = 1.0 + self.play_thread = None + self.min_tick = 0.02 # AU2512最小tick + + # Load and prepare data + self.load_data() + + # Create GUI + self.setup_gui() + + # Initial display + self.update_display() + + def load_data(self): + """加载并预处理期货数据""" + print("Loading futures data...") + self.df = pd.read_parquet(self.data_path) + + # Column mapping + columns_mapping = { + 'UTC': 'UTC', 'UTC.1': 'UTC_1', '时间': 'time', + '累积成交量': 'cumulative_volume', '成交价': 'price', '成交额': 'amount', + '买1价': 'bid1_price', '卖1价': 'ask1_price', + '买1量': 'bid1_volume', '卖1量': 'ask1_volume', + '买2价': 'bid2_price', '卖2价': 'ask2_price', + '买2量': 'bid2_volume', '卖2量': 'ask2_volume', + '买3价': 'bid3_price', '卖3价': 'ask3_price', + '买3量': 'bid3_volume', '卖3量': 'ask3_volume', + '买4价': 'bid4_price', '卖4价': 'ask4_price', + '买4量': 'bid4_volume', '卖4量': 'ask4_volume', + '买5价': 'bid5_price', '卖5价': 'ask5_price', + '买5量': 'bid5_volume', '卖5量': 'ask5_volume' + } + + self.df = self.df.rename(columns=columns_mapping) + + # Clean data: remove invalid market depth data + bid_cols = [f'bid{i}_price' for i in range(1, 6)] + ask_cols = [f'ask{i}_price' for i in range(1, 6)] + bid_vol_cols = [f'bid{i}_volume' for i in range(1, 6)] + ask_vol_cols = [f'ask{i}_volume' for i in range(1, 6)] + + # Filter out rows with invalid bid/ask data + valid_mask = ( + self.df[bid_cols].notnull().all(axis=1) & + self.df[ask_cols].notnull().all(axis=1) & + (self.df[bid_vol_cols] > 0).any(axis=1) & + (self.df[ask_vol_cols] > 0).any(axis=1) + ) + self.df = self.df[valid_mask].reset_index(drop=True) + + print(f"Data loaded successfully! Valid data points: {len(self.df)}") + print(f"Time range: {self.df['time'].iloc[0]} - {self.df['time'].iloc[-1]}") + print(f"Price range: {self.df['price'].min():.2f} - {self.df['price'].max():.2f}") + + def setup_gui(self): + """设置GUI界面""" + self.root = tk.Tk() + self.root.title("Unified Futures Player - AU2512 Combined Market Depth") + self.root.geometry("1600x900") + + # Create main container with paned window + main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL) + main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # Left frame for charts + chart_frame = ttk.Frame(main_paned) + main_paned.add(chart_frame, weight=3) + + # Right frame for controls + control_frame = ttk.Frame(main_paned, width=350) + main_paned.add(control_frame, weight=1) + control_frame.pack_propagate(False) + + # Create matplotlib figure with unified layout + self.setup_charts(chart_frame) + + # Create control panel + self.setup_controls(control_frame) + + # Configure style + style = ttk.Style() + style.theme_use('clam') + + def setup_charts(self, parent): + """设置统一图表布局""" + # Create figure with custom layout + self.fig = Figure(figsize=(12, 8), dpi=100) + self.fig.patch.set_facecolor('#f8f9fa') + + # Create GridSpec for layout: main chart takes 3/4, smaller charts take 1/4 + gs = gridspec.GridSpec(3, 2, height_ratios=[3, 1, 1], width_ratios=[2, 1], + hspace=0.3, wspace=0.2) + + # Main unified market depth chart + self.ax_main = self.fig.add_subplot(gs[0, :]) + + # Price trend chart (bottom left) + self.ax_price = self.fig.add_subplot(gs[1, 0]) + + # Volume profile chart (bottom right) + self.ax_volume = self.fig.add_subplot(gs[1, 1]) + + # Statistics text area (bottom) + self.ax_stats = self.fig.add_subplot(gs[2, :]) + self.ax_stats.axis('off') + + # Embed in tkinter + self.canvas = FigureCanvasTkAgg(self.fig, parent) + self.canvas.draw() + self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + # Add navigation toolbar + toolbar_frame = ttk.Frame(parent) + toolbar_frame.pack(fill=tk.X) + self.toolbar = NavigationToolbar2Tk(self.canvas, toolbar_frame) + self.toolbar.update() + + def setup_controls(self, parent): + """设置控制面板""" + # Title + title_label = ttk.Label(parent, text="Unified Market Depth Controls", + font=('Arial', 14, 'bold')) + title_label.pack(pady=10) + + # Current status frame + status_frame = ttk.LabelFrame(parent, text="Current Status", padding=10) + status_frame.pack(fill=tk.X, pady=5) + + self.index_label = ttk.Label(status_frame, text="Sequence: 0", font=('Arial', 10)) + self.index_label.pack(anchor=tk.W) + + self.time_label = ttk.Label(status_frame, text="Time: --", font=('Arial', 10)) + self.time_label.pack(anchor=tk.W) + + self.price_label = ttk.Label(status_frame, text="Last Price: --", font=('Arial', 10, 'bold')) + self.price_label.pack(anchor=tk.W) + + self.spread_label = ttk.Label(status_frame, text="Bid-Ask Spread: --", font=('Arial', 10)) + self.spread_label.pack(anchor=tk.W) + + # Playback controls + playback_frame = ttk.LabelFrame(parent, text="Playback Controls", padding=10) + playback_frame.pack(fill=tk.X, pady=5) + + # Play/Pause button + self.play_button = ttk.Button( + playback_frame, + text="▶ Play", + command=self.toggle_play, + width=25 + ) + self.play_button.pack(pady=3) + + # Step controls frame + step_frame = ttk.Frame(playback_frame) + step_frame.pack(pady=5) + + ttk.Button(step_frame, text="◀◀ -10", command=lambda: self.step_play(-10), width=10).pack(side=tk.LEFT, padx=2) + ttk.Button(step_frame, text="◀ -1", command=lambda: self.step_play(-1), width=10).pack(side=tk.LEFT, padx=2) + ttk.Button(step_frame, text="+1 ▶", command=lambda: self.step_play(1), width=10).pack(side=tk.LEFT, padx=2) + ttk.Button(step_frame, text="+10 ▶▶", command=lambda: self.step_play(10), width=10).pack(side=tk.LEFT, padx=2) + + # Stop button + self.stop_button = ttk.Button( + playback_frame, + text="■ Stop", + command=self.stop_playback, + width=25 + ) + self.stop_button.pack(pady=3) + + # Speed controls + speed_frame = ttk.LabelFrame(parent, text="Speed Control", padding=10) + speed_frame.pack(fill=tk.X, pady=5) + + # Speed slider + self.speed_var = tk.DoubleVar(value=1.0) + speed_slider = ttk.Scale( + speed_frame, + from_=0.1, + to=10.0, + variable=self.speed_var, + orient=tk.HORIZONTAL, + command=self.update_speed + ) + speed_slider.pack(fill=tk.X, pady=3) + + self.speed_label = ttk.Label(speed_frame, text="Speed: 1.0x", font=('Arial', 10)) + self.speed_label.pack() + + # Quick speed buttons + speed_buttons_frame = ttk.Frame(speed_frame) + speed_buttons_frame.pack(pady=3) + + ttk.Button(speed_buttons_frame, text="0.25x", command=lambda: self.set_speed(0.25), width=8).pack(side=tk.LEFT, padx=1) + ttk.Button(speed_buttons_frame, text="0.5x", command=lambda: self.set_speed(0.5), width=8).pack(side=tk.LEFT, padx=1) + ttk.Button(speed_buttons_frame, text="1x", command=lambda: self.set_speed(1.0), width=8).pack(side=tk.LEFT, padx=1) + ttk.Button(speed_buttons_frame, text="2x", command=lambda: self.set_speed(2.0), width=8).pack(side=tk.LEFT, padx=1) + ttk.Button(speed_buttons_frame, text="5x", command=lambda: self.set_speed(5.0), width=8).pack(side=tk.LEFT, padx=1) + ttk.Button(speed_buttons_frame, text="10x", command=lambda: self.set_speed(10.0), width=8).pack(side=tk.LEFT, padx=1) + + # Navigation controls + nav_frame = ttk.LabelFrame(parent, text="Navigation", padding=10) + nav_frame.pack(fill=tk.X, pady=5) + + # Quick jump buttons + quick_jump_frame = ttk.Frame(nav_frame) + quick_jump_frame.pack(pady=5) + + ttk.Button(quick_jump_frame, text="Start", command=lambda: self.jump_to(0), width=10).pack(side=tk.LEFT, padx=2) + ttk.Button(quick_jump_frame, text="25%", command=lambda: self.jump_to(len(self.df) // 4), width=10).pack(side=tk.LEFT, padx=2) + ttk.Button(quick_jump_frame, text="50%", command=lambda: self.jump_to(len(self.df) // 2), width=10).pack(side=tk.LEFT, padx=2) + ttk.Button(quick_jump_frame, text="75%", command=lambda: self.jump_to(3 * len(self.df) // 4), width=10).pack(side=tk.LEFT, padx=2) + ttk.Button(quick_jump_frame, text="End", command=lambda: self.jump_to(len(self.df) - 1), width=10).pack(side=tk.LEFT, padx=2) + + # Precise jump + jump_input_frame = ttk.Frame(nav_frame) + jump_input_frame.pack(fill=tk.X, pady=5) + + ttk.Label(jump_input_frame, text="Jump to:").pack(side=tk.LEFT) + self.jump_entry = ttk.Entry(jump_input_frame, width=10) + self.jump_entry.pack(side=tk.LEFT, padx=5) + ttk.Button(jump_input_frame, text="Go", command=self.jump_to_entry, width=8).pack(side=tk.LEFT) + + # Progress bar + progress_frame = ttk.LabelFrame(parent, text="Progress", padding=10) + progress_frame.pack(fill=tk.X, pady=5) + + self.progress_var = tk.IntVar() + self.progress_bar = ttk.Progressbar( + progress_frame, + variable=self.progress_var, + maximum=len(self.df) - 1, + orient=tk.HORIZONTAL + ) + self.progress_bar.pack(fill=tk.X, pady=3) + + self.progress_label = ttk.Label(progress_frame, text=f"0 / {len(self.df) - 1}") + self.progress_label.pack() + + def get_market_depth_data(self, row_data): + """获取买卖盘深度数据""" + bid_prices = [] + bid_volumes = [] + ask_prices = [] + ask_volumes = [] + + # 收集买卖盘数据 + for i in range(1, 6): + bid_price = row_data[f'bid{i}_price'] + bid_volume = row_data[f'bid{i}_volume'] + ask_price = row_data[f'ask{i}_price'] + ask_volume = row_data[f'ask{i}_volume'] + + if pd.notna(bid_price) and pd.notna(bid_volume) and bid_volume > 0: + bid_prices.append(bid_price) + bid_volumes.append(bid_volume) + + if pd.notna(ask_price) and pd.notna(ask_volume) and ask_volume > 0: + ask_prices.append(ask_price) + ask_volumes.append(ask_volume) + + return bid_prices, bid_volumes, ask_prices, ask_volumes + + def update_display(self): + """更新统一价格轴显示""" + if self.df is None or self.current_index >= len(self.df): + return + + # Get current data + row_data = self.df.iloc[self.current_index] + current_price = row_data['price'] + + # Clear all axes + self.ax_main.clear() + self.ax_price.clear() + self.ax_volume.clear() + self.ax_stats.clear() + + # Get market depth data + bid_prices, bid_volumes, ask_prices, ask_volumes = self.get_market_depth_data(row_data) + + # === Main Unified Market Depth Chart === + self.ax_main.set_title('Unified Market Depth - AU2512', fontsize=16, fontweight='bold', pad=20) + self.ax_main.set_xlabel('Volume', fontsize=12) + self.ax_main.set_ylabel('Price (¥)', fontsize=12) + self.ax_main.grid(True, alpha=0.3, axis='x') + + if bid_prices and ask_prices: + # Define volume scale points (equal spacing) + volume_scale_points = [10, 30, 60, 150, 300] + + # Create price range centered around current price + price_range = 2.0 # Show ±1元 around current price + min_price = current_price - price_range/2 + max_price = current_price + price_range/2 + + # Generate price levels with minimum tick spacing + tick_prices = np.arange(np.floor(min_price / self.min_tick) * self.min_tick, + np.ceil(max_price / self.min_tick) * self.min_tick + self.min_tick, + self.min_tick) + + # Prepare data for unified display + all_prices = [] + all_volumes = [] + all_colors = [] + + # Add bid data (green horizontal bars) + for bp, bv in zip(bid_prices, bid_volumes): + all_prices.append(bp) + all_volumes.append(bv) + all_colors.append('green') + + # Add ask data (red horizontal bars) + for ap, av in zip(ask_prices, ask_volumes): + all_prices.append(ap) + all_volumes.append(av) + all_colors.append('red') + + # Map volumes to scale positions + def map_volume_to_scale(volume): + """Map actual volume to scale position""" + if volume <= 10: + return 10 + elif volume <= 30: + return 30 + elif volume <= 60: + return 60 + elif volume <= 150: + return 150 + elif volume <= 300: + return 300 + else: + return 300 # Cap at 300 for display + + # Map volumes to display scale + mapped_volumes = [map_volume_to_scale(v) for v in all_volumes] + + # Create horizontal bar chart (volume on x-axis, price on y-axis) + bars = self.ax_main.barh(all_prices, mapped_volumes, + height=self.min_tick * 0.8, # Bar height based on tick size + color=all_colors, alpha=0.7, + edgecolor='darkgreen' if 'green' in all_colors else 'darkred', + linewidth=1) + + # Add volume labels on bars + for price, volume, mapped_vol in zip(all_prices, all_volumes, mapped_volumes): + if volume > 0: + self.ax_main.text(mapped_vol + 5, price, f'{int(volume):,}', + ha='left', va='center', fontsize=8, fontweight='bold') + + # Set y-axis with true tick spacing (price) + self.ax_main.set_yticks(tick_prices[::3]) # Show every 3rd tick to avoid crowding + self.ax_main.set_yticklabels([f'{p:.2f}' for p in tick_prices[::3]]) + self.ax_main.set_ylim(min_price, max_price) + + # Set x-axis with defined volume scale points + self.ax_main.set_xticks(volume_scale_points) + self.ax_main.set_xticklabels([f'{v}' for v in volume_scale_points]) + self.ax_main.set_xlim(0, max(volume_scale_points) * 1.2) + + # Add current price line + self.ax_main.axhline(y=current_price, color='blue', linestyle='--', alpha=0.8, linewidth=2, + label=f'Last: {current_price:.2f}') + + # Add spread visualization + if bid_prices and ask_prices: + best_bid = max(bid_prices) + best_ask = min(ask_prices) + spread = best_ask - best_bid + + # Highlight spread area + self.ax_main.axhspan(best_bid, best_ask, alpha=0.2, color='yellow', + label=f'Spread: {spread:.2f}') + + # Add legend + bid_patch = mpatches.Patch(color='green', alpha=0.7, label='Bid Volume') + ask_patch = mpatches.Patch(color='red', alpha=0.7, label='Ask Volume') + self.ax_main.legend(handles=[bid_patch, ask_patch], loc='upper right') + + else: + self.ax_main.text(0.5, 0.5, 'No Market Depth Data Available', + ha='center', va='center', transform=self.ax_main.transAxes, + fontsize=14, color='red') + + # === Price Trend Chart === + self.plot_price_trend() + + # === Volume Profile Chart === + self.plot_volume_profile() + + # === Statistics Display === + self.display_statistics(row_data, bid_prices, ask_prices) + + # Update layout + self.fig.tight_layout() + self.canvas.draw() + + # Update status labels + self.update_status_labels(row_data, bid_prices, ask_prices) + + # Update progress + self.progress_var.set(self.current_index) + self.progress_label.config(text=f"{self.current_index} / {len(self.df) - 1}") + + def plot_price_trend(self): + """绘制价格趋势图""" + self.ax_price.set_title('Price Trend (Last 100 points)', fontsize=10) + self.ax_price.set_xlabel('Sequence', fontsize=8) + self.ax_price.set_ylabel('Price', fontsize=8) + self.ax_price.grid(True, alpha=0.3) + + # Get recent data + start_idx = max(0, self.current_index - 99) + end_idx = self.current_index + 1 + + if end_idx > start_idx: + recent_data = self.df.iloc[start_idx:end_idx] + + # Plot price line + self.ax_price.plot(recent_data.index, recent_data['price'], + 'b-', linewidth=1.5, label='Price') + + # Plot moving average + if len(recent_data) >= 10: + ma = recent_data['price'].rolling(window=10, min_periods=1).mean() + self.ax_price.plot(recent_data.index, ma, 'r--', linewidth=1, + label='10-MA', alpha=0.7) + + # Mark current position + self.ax_price.plot(self.current_index, self.df.iloc[self.current_index]['price'], + 'ro', markersize=6, label='Current') + + self.ax_price.legend(loc='upper left', fontsize=8) + self.ax_price.tick_params(axis='both', labelsize=8) + + def plot_volume_profile(self): + """绘制成交量分析图""" + self.ax_volume.set_title('Volume Analysis', fontsize=10) + self.ax_volume.set_xlabel('Volume', fontsize=8) + self.ax_volume.set_ylabel('Cumulative Volume', fontsize=8) + self.ax_volume.grid(True, alpha=0.3) + + # Get recent data + start_idx = max(0, self.current_index - 99) + end_idx = self.current_index + 1 + + if end_idx > start_idx: + recent_data = self.df.iloc[start_idx:end_idx] + + # Plot cumulative volume + self.ax_volume.plot(recent_data['cumulative_volume'], recent_data['price'], + 'g-', linewidth=1.5, alpha=0.7) + + # Mark current position + current_row = self.df.iloc[self.current_index] + self.ax_volume.plot(current_row['cumulative_volume'], current_row['price'], + 'ro', markersize=6) + + self.ax_volume.tick_params(axis='both', labelsize=8) + + def display_statistics(self, row_data, bid_prices, ask_prices): + """显示统计信息""" + self.ax_stats.axis('off') + + stats_text = f"Time: {row_data['time']} | " + stats_text += f"Last Price: ¥{row_data['price']:.2f} | " + stats_text += f"Cumulative Volume: {int(row_data['cumulative_volume']):,} | " + + if bid_prices and ask_prices: + best_bid = max(bid_prices) + best_ask = min(ask_prices) + spread = best_ask - best_bid + total_bid_volume = sum([row_data[f'bid{i}_volume'] for i in range(1, 6) + if pd.notna(row_data[f'bid{i}_volume'])]) + total_ask_volume = sum([row_data[f'ask{i}_volume'] for i in range(1, 6) + if pd.notna(row_data[f'ask{i}_volume'])]) + + stats_text += f"Best Bid: ¥{best_bid:.2f} ({total_bid_volume:,}) | " + stats_text += f"Best Ask: ¥{best_ask:.2f} ({total_ask_volume:,}) | " + stats_text += f"Spread: ¥{spread:.2f}" + + self.ax_stats.text(0.5, 0.5, stats_text, ha='center', va='center', + transform=self.ax_stats.transAxes, fontsize=11, + bbox=dict(boxstyle='round,pad=0.5', facecolor='lightgray', alpha=0.8)) + + def update_status_labels(self, row_data, bid_prices, ask_prices): + """更新状态标签""" + self.index_label.config(text=f"Sequence: {self.current_index}") + self.time_label.config(text=f"Time: {row_data['time']}") + self.price_label.config(text=f"Last Price: ¥{row_data['price']:.2f}") + + if bid_prices and ask_prices: + best_bid = max(bid_prices) + best_ask = min(ask_prices) + spread = best_ask - best_bid + self.spread_label.config(text=f"Bid-Ask Spread: ¥{spread:.2f}") + else: + self.spread_label.config(text="Bid-Ask Spread: --") + + def toggle_play(self): + """切换播放/暂停""" + if not self.is_playing: + self.is_playing = True + self.play_button.config(text="⏸ Pause") + self.play_thread = threading.Thread(target=self.playback_loop, daemon=True) + self.play_thread.start() + else: + self.is_playing = False + self.play_button.config(text="▶ Play") + + def stop_playback(self): + """停止播放""" + self.is_playing = False + self.play_button.config(text="▶ Play") + self.current_index = 0 + self.update_display() + + def playback_loop(self): + """播放循环""" + while self.is_playing and self.current_index < len(self.df) - 1: + self.current_index += 1 + self.root.after(0, self.update_display) + + delay = self.base_speed / self.speed_multiplier + time.sleep(delay / 1000.0) + + if self.current_index >= len(self.df) - 1: + self.is_playing = False + self.root.after(0, lambda: self.play_button.config(text="▶ Play")) + + def step_play(self, steps): + """步进播放""" + self.current_index = max(0, min(len(self.df) - 1, self.current_index + steps)) + self.update_display() + + def jump_to(self, index): + """跳转到指定位置""" + self.current_index = max(0, min(len(self.df) - 1, index)) + self.update_display() + + def jump_to_entry(self): + """跳转到输入的位置""" + try: + target = int(self.jump_entry.get()) + if 0 <= target < len(self.df): + self.jump_to(target) + else: + messagebox.showwarning("Invalid Input", f"Please enter a number between 0 and {len(self.df)-1}") + except ValueError: + messagebox.showwarning("Invalid Input", "Please enter a valid number") + + def update_speed(self, value): + """更新播放速度""" + self.speed_multiplier = float(value) + self.speed_label.config(text=f"Speed: {self.speed_multiplier:.1f}x") + + def set_speed(self, speed): + """设置播放速度""" + self.speed_var.set(speed) + self.speed_multiplier = speed + self.speed_label.config(text=f"Speed: {self.speed_multiplier:.1f}x") + + def run(self): + """运行应用""" + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + self.root.mainloop() + + def on_closing(self): + """关闭处理""" + self.is_playing = False + self.root.destroy() + +def main(): + """主函数""" + print("=== Unified Futures Data Player ===") + print("Initializing unified market depth visualization...") + + player = UnifiedFuturesPlayer() + + print("Initialization complete!") + print("Features:") + print("- Unified price axis showing bid/ask orders together") + print("- True minimum tick spacing (0.02)") + print("- Real-time spread visualization") + print("- Combined market depth analysis") + + player.run() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/large_orders/analyze_large_orders_extended.py b/large_orders/analyze_large_orders_extended.py index 21651c9..610b411 100644 --- a/large_orders/analyze_large_orders_extended.py +++ b/large_orders/analyze_large_orders_extended.py @@ -13,7 +13,7 @@ def analyze_large_orders_extended(): print("正在读取数据文件...") # 读取数据(从上级目录的data文件夹) - df = pd.read_parquet('../data/au2512_20251013.parquet') + df = pd.read_parquet('data/au2512_20251013.parquet') print(f"数据总行数: {len(df)}") print(f"数据列名: {df.columns.tolist()}") diff --git a/large_orders/analyze_total_orders_extended.py b/large_orders/analyze_total_orders_extended.py index a3aa43f..4942266 100644 --- a/large_orders/analyze_total_orders_extended.py +++ b/large_orders/analyze_total_orders_extended.py @@ -13,7 +13,7 @@ def analyze_total_orders_extended(): print("正在读取数据文件...") # 读取数据(从上级目录的data文件夹) - df = pd.read_parquet('../data/au2512_20251013.parquet') + df = pd.read_parquet('data/au2512_20251013.parquet') print(f"数据总行数: {len(df)}") print(f"数据列名: {df.columns.tolist()}") diff --git a/simple_test.py b/simple_test.py new file mode 100644 index 0000000..096596b --- /dev/null +++ b/simple_test.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Simple test script for futures data player +""" + +def main(): + print("=== Futures Data Player Test ===") + + # Test 1: Check data file + import os + data_path = 'data/au2512_20251013.parquet' + if os.path.exists(data_path): + print(f"+ Data file exists: {data_path}") + + try: + import pandas as pd + df = pd.read_parquet(data_path) + print(f"+ Data loaded: {len(df)} rows") + except Exception as e: + print(f"- Data loading failed: {e}") + return + else: + print(f"- Data file not found: {data_path}") + return + + # Test 2: Check imports + try: + import pandas as pd + import numpy as np + import matplotlib.pyplot as plt + import tkinter as tk + print("+ All required libraries imported successfully") + except ImportError as e: + print(f"- Import failed: {e}") + return + + # Test 3: Create player instance + try: + from futures_player_enhanced import EnhancedFuturesPlayer + print("+ Creating player instance...") + player = EnhancedFuturesPlayer() + + if player.df is not None: + print(f"+ Player created with {len(player.df)} data points") + print("+ Test passed! Ready to run the full application.") + + # Clean up + player.on_closing() + else: + print("- Player creation failed") + + except Exception as e: + print(f"- Player test failed: {e}") + + print("\\nTo run the full application:") + print("python futures_player_enhanced.py") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_player.py b/test_player.py new file mode 100644 index 0000000..e0eb630 --- /dev/null +++ b/test_player.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +简单的期货数据播放器测试脚本 +用于验证程序是否能正常加载和运行 +""" + +import sys +import os + +def test_imports(): + """测试必需的库是否可以导入""" + print("Testing required libraries...") + + try: + import pandas as pd + print("+ pandas imported successfully") + except ImportError as e: + print(f"- pandas import failed: {e}") + return False + + try: + import numpy as np + print("✓ numpy imported successfully") + except ImportError as e: + print(f"✗ numpy import failed: {e}") + return False + + try: + import matplotlib.pyplot as plt + print("✓ matplotlib imported successfully") + except ImportError as e: + print(f"✗ matplotlib import failed: {e}") + return False + + try: + import tkinter as tk + print("✓ tkinter imported successfully") + except ImportError as e: + print(f"✗ tkinter import failed: {e}") + return False + + return True + +def test_data_file(): + """测试数据文件是否存在""" + print("\\nTesting data file...") + + data_path = 'data/au2512_20251013.parquet' + if os.path.exists(data_path): + print(f"✓ Data file found: {data_path}") + + # 尝试读取数据 + try: + import pandas as pd + df = pd.read_parquet(data_path) + print(f"✓ Data loaded successfully: {len(df)} rows") + print(f" Time range: {df.iloc[0]['时间'] if '时间' in df.columns else 'N/A'}") + return True + except Exception as e: + print(f"✗ Failed to load data: {e}") + return False + else: + print(f"✗ Data file not found: {data_path}") + return False + +def test_basic_functionality(): + """测试基本功能""" + print("\\nTesting basic functionality...") + + try: + # 导入播放器类 + from futures_player_enhanced import EnhancedFuturesPlayer + print("✓ Player class imported successfully") + + # 尝试创建实例(但不运行GUI) + print("✓ Creating player instance...") + player = EnhancedFuturesPlayer() + + # 测试数据加载 + if player.df is not None and len(player.df) > 0: + print(f"✓ Player data loaded: {len(player.df)} rows") + + # 测试更新显示功能 + player.update_display() + print("✓ Display update test passed") + + # 清理 + player.on_closing() + return True + else: + print("✗ Player data not loaded properly") + return False + + except Exception as e: + print(f"✗ Basic functionality test failed: {e}") + return False + +def main(): + """主测试函数""" + print("=== Futures Data Player Test Suite ===\\n") + + # 测试导入 + if not test_imports(): + print("\\n❌ Import test failed. Please install required libraries:") + print("pip install pandas numpy matplotlib") + return False + + # 测试数据文件 + if not test_data_file(): + print("\\n❌ Data file test failed. Please check data path.") + return False + + # 测试基本功能 + if not test_basic_functionality(): + print("\\n❌ Basic functionality test failed.") + return False + + print("\\n✅ All tests passed! The player should work correctly.") + print("\\nTo run the full application:") + print("python futures_player_enhanced.py") + + return True + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/test_unified_player.py b/test_unified_player.py new file mode 100644 index 0000000..f4568b6 --- /dev/null +++ b/test_unified_player.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Test script for Unified Futures Data Player +验证统一价格轴版本的期货数据播放器 +""" + +import sys +import os + +def test_imports(): + """测试必需的库""" + print("Testing required libraries...") + + try: + import pandas as pd + print("+ pandas imported successfully") + except ImportError as e: + print(f"- pandas import failed: {e}") + return False + + try: + import numpy as np + print("+ numpy imported successfully") + except ImportError as e: + print(f"- numpy import failed: {e}") + return False + + try: + import matplotlib.pyplot as plt + print("+ matplotlib imported successfully") + except ImportError as e: + print(f"- matplotlib import failed: {e}") + return False + + try: + import tkinter as tk + print("+ tkinter imported successfully") + except ImportError as e: + print(f"- tkinter import failed: {e}") + return False + + return True + +def test_data_file(): + """测试数据文件""" + print("\nTesting data file...") + + data_path = 'data/au2512_20251013.parquet' + if os.path.exists(data_path): + print(f"+ Data file found: {data_path}") + + try: + import pandas as pd + df = pd.read_parquet(data_path) + print(f"+ Data loaded successfully: {len(df)} rows") + + # Test tick analysis + columns_mapping = { + '买1价': 'bid1_price', '卖1价': 'ask1_price', + '买2价': 'bid2_price', '卖2价': 'ask2_price', + '买3价': 'bid3_price', '卖3价': 'ask3_price', + '买4价': 'bid4_price', '卖4价': 'ask4_price', + '买5价': 'bid5_price', '卖5价': 'ask5_price' + } + df = df.rename(columns=columns_mapping) + + all_prices = [] + for i in range(1, 6): + all_prices.extend(df[f'bid{i}_price'].dropna().tolist()) + all_prices.extend(df[f'ask{i}_price'].dropna().tolist()) + + unique_prices = sorted(list(set(all_prices))) + print(f"+ Unique price levels: {len(unique_prices)}") + print(f"+ Price range: {min(unique_prices):.2f} - {max(unique_prices):.2f}") + + return True + except Exception as e: + print(f"- Failed to load data: {e}") + return False + else: + print(f"- Data file not found: {data_path}") + return False + +def test_unified_functionality(): + """测试统一版本功能""" + print("\nTesting Unified Futures Player...") + + try: + from futures_player_unified import UnifiedFuturesPlayer + print("+ UnifiedFuturesPlayer class imported successfully") + + print("+ Creating unified player instance...") + player = UnifiedFuturesPlayer() + + if player.df is not None and len(player.df) > 0: + print(f"+ Unified player data loaded: {len(player.df)} rows") + + # Test display update + player.update_display() + print("+ Display update test passed") + + # Test market depth data processing + test_row = player.df.iloc[0] + bid_prices, bid_volumes, ask_prices, ask_volumes = player.get_market_depth_data(test_row) + print(f"+ Market depth test - Bids: {len(bid_prices)}, Asks: {len(ask_prices)}") + + # Clean up + player.on_closing() + return True + else: + print("- Unified player data not loaded properly") + return False + + except Exception as e: + print(f"- Unified player test failed: {e}") + return False + +def main(): + """主测试函数""" + print("=== Unified Futures Player Test Suite ===\n") + + # Test imports + if not test_imports(): + print("\n- Import test failed. Please install required libraries:") + print("pip install pandas numpy matplotlib") + return False + + # Test data file + if not test_data_file(): + print("\n- Data file test failed. Please check data path.") + return False + + # Test unified functionality + if not test_unified_functionality(): + print("\n- Unified functionality test failed.") + return False + + print("\n+ All tests passed! The unified futures player is ready.") + print("\nKey improvements in this version:") + print("+ Unified price axis showing bid/ask orders together") + print("+ True minimum tick spacing (0.02 yuan)") + print("+ Visual price gaps between bid and ask") + print("+ Real-time spread visualization") + print("+ Enhanced market depth analysis") + + print("\nTo run the unified application:") + print("python futures_player_unified.py") + + return True + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file