feat: 添加期货数据播放器及相关测试和文档
新增期货数据动态播放器功能,包括基础版和增强版实现,添加测试脚本和详细文档说明。主要变更包括: 1. 实现买卖盘深度可视化播放功能 2. 添加播放控制、速度调节和跳转功能 3. 提供统一价格轴显示优化版本 4. 添加测试脚本验证功能 5. 编写详细使用文档和README说明
This commit is contained in:
parent
d5974f1e7c
commit
7f4f88e853
155
README_futures_player.md
Normal file
155
README_futures_player.md
Normal 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
177
README_unified_final.md
Normal 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
202
demo_unified_player.py
Normal 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
482
futures_data_player.py
Normal 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
539
futures_player_enhanced.py
Normal 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
625
futures_player_unified.py
Normal 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()
|
||||||
@ -13,7 +13,7 @@ def analyze_large_orders_extended():
|
|||||||
|
|
||||||
print("正在读取数据文件...")
|
print("正在读取数据文件...")
|
||||||
# 读取数据(从上级目录的data文件夹)
|
# 读取数据(从上级目录的data文件夹)
|
||||||
df = pd.read_parquet('../data/au2512_20251013.parquet')
|
df = pd.read_parquet('data/au2512_20251013.parquet')
|
||||||
|
|
||||||
print(f"数据总行数: {len(df)}")
|
print(f"数据总行数: {len(df)}")
|
||||||
print(f"数据列名: {df.columns.tolist()}")
|
print(f"数据列名: {df.columns.tolist()}")
|
||||||
|
|||||||
@ -13,7 +13,7 @@ def analyze_total_orders_extended():
|
|||||||
|
|
||||||
print("正在读取数据文件...")
|
print("正在读取数据文件...")
|
||||||
# 读取数据(从上级目录的data文件夹)
|
# 读取数据(从上级目录的data文件夹)
|
||||||
df = pd.read_parquet('../data/au2512_20251013.parquet')
|
df = pd.read_parquet('data/au2512_20251013.parquet')
|
||||||
|
|
||||||
print(f"数据总行数: {len(df)}")
|
print(f"数据总行数: {len(df)}")
|
||||||
print(f"数据列名: {df.columns.tolist()}")
|
print(f"数据列名: {df.columns.tolist()}")
|
||||||
|
|||||||
60
simple_test.py
Normal file
60
simple_test.py
Normal 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
127
test_player.py
Normal 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
154
test_unified_player.py
Normal 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)
|
||||||
Loading…
Reference in New Issue
Block a user