feat: 添加期货数据播放器及相关测试和文档

新增期货数据动态播放器功能,包括基础版和增强版实现,添加测试脚本和详细文档说明。主要变更包括:
1. 实现买卖盘深度可视化播放功能
2. 添加播放控制、速度调节和跳转功能
3. 提供统一价格轴显示优化版本
4. 添加测试脚本验证功能
5. 编写详细使用文档和README说明
This commit is contained in:
Your Name 2025-11-02 23:57:10 +08:00
parent d5974f1e7c
commit 7f4f88e853
11 changed files with 2523 additions and 2 deletions

155
README_futures_player.md Normal file
View File

@ -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

177
README_unified_final.md Normal file
View File

@ -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
**核心特性**: 倒置显示 + 类对数刻度 + 统一价格轴

202
demo_unified_player.py Normal file
View File

@ -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()

482
futures_data_player.py Normal file
View File

@ -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()

539
futures_player_enhanced.py Normal file
View File

@ -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()

625
futures_player_unified.py Normal file
View File

@ -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()

View File

@ -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()}")

View File

@ -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()}")

60
simple_test.py Normal file
View File

@ -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()

127
test_player.py Normal file
View File

@ -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)

154
test_unified_player.py Normal file
View File

@ -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)