#!/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()