问题描述
解决方案
在聚宽(JoinQuant)平台上,利用 Tick 数据绘制当天的成交量分布图(Volume Profile)主要分为三个步骤:获取 Tick 数据、计算单笔成交量、按价格聚合成交量。
由于 Tick 数据中的 volume 字段通常是累计成交量,因此核心逻辑是用当前 Tick 的累计成交量减去上一笔 Tick 的累计成交量,得出该笔交易的实际量,然后将其归类到当前的成交价格上。
以下是在**研究环境(Research)**中实现的完整代码示例。
实现代码
# -*- coding: utf-8 -*-
from jqdata import *
import pandas as pd
import matplotlib.pyplot as plt
# 设置中文字体,防止绘图乱码(根据聚宽环境调整,通常SimHei可用)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
def draw_volume_profile(security, date):
"""
绘制指定标的在指定日期的成交量分布图 (Volume Profile)
:param security: 标的代码,如 '000001.XSHE'
:param date: 日期字符串,如 '2023-05-10'
"""
# 1. 获取 Tick 数据
# start_dt 和 end_dt 覆盖全天交易时段
start_dt = f"{date} 09:30:00"
end_dt = f"{date} 15:00:00"
# 获取 tick 数据,包含时间、当前价、累计成交量
ticks = get_ticks(security, start_dt=start_dt, end_dt=end_dt,
fields=['time', 'current', 'volume'], df=True)
if ticks.empty:
print(f"未获取到 {security} 在 {date} 的Tick数据")
return
# 2. 数据处理
# Tick数据中的 volume 是累计成交量,需要计算差分得到每笔成交量
# diff() 计算当前行与上一行的差值
ticks['tick_vol'] = ticks['volume'].diff()
# 第一笔数据的 diff 结果为 NaN,通常第一笔是集合竞价产生的累积量,直接填充为原始值
ticks['tick_vol'].fillna(ticks['volume'], inplace=True)
# 过滤掉成交量为0或负数的数据(异常处理)
ticks = ticks[ticks['tick_vol'] > 0]
# 3. 按价格聚合成交量
# 将价格作为索引,对成交量求和
volume_profile = ticks.groupby('current')['tick_vol'].sum()
# 4. 绘图
plt.figure(figsize=(12, 8))
# 使用水平条形图 (barh),Y轴为价格,X轴为成交量
# width 参数控制条形的粗细,根据价格档位数量调整
plt.barh(volume_profile.index, volume_profile.values, height=0.005, color='skyblue', edgecolor='blue', alpha=0.6)
# 找出最大成交量的价格(POC - Point of Control)
poc_price = volume_profile.idxmax()
max_vol = volume_profile.max()
# 标记 POC
plt.axhline(y=poc_price, color='red', linestyle='--', label=f'POC (控制点): {poc_price}')
plt.title(f'{security} 成交量分布图 (Volume Profile) - {date}')
plt.xlabel('成交量 (股)')
plt.ylabel('价格')
plt.legend()
plt.grid(True, axis='x', linestyle='--', alpha=0.5)
plt.show()
# --- 调用示例 ---
# 请确保日期是交易日,且在Tick数据提供的范围内(股票2010年至今)
draw_volume_profile('000001.XSHE', '2023-06-01')
关键逻辑解析
-
数据获取 (
get_ticks):- 使用
get_ticks函数获取高频数据。 fields参数必须包含volume(累计成交量) 和current(当前价格)。df=True直接返回 DataFrame 格式,方便后续处理。
- 使用
-
计算增量 (
diff):- 交易所推送的 Tick 数据中,
volume字段代表“开盘截至当前的累计成交量”。 - 为了知道在特定价格成交了多少,必须计算增量:
当前Tick累计量 - 上一Tick累计量。 - 代码中使用
ticks['volume'].diff()完成此操作。
- 交易所推送的 Tick 数据中,
-
聚合 (
groupby):- Volume Profile 的本质是价格与成交量的直方图。
- 使用
groupby('current')['tick_vol'].sum()将同一价格下的所有单笔成交量累加,得到该价格档位的总成交量。
-
可视化 (
barh):- 通常 Volume Profile 是横向展示的,因此使用
plt.barh。 - Y 轴表示价格水平,X 轴表示该价格处的成交量堆积。
- POC (Point of Control):图中红线标注了当天成交量最大的价格位置,这是筹码交换最密集的区域,通常具有较强的支撑或阻力作用。
- 通常 Volume Profile 是横向展示的,因此使用
注意事项
- 数据范围: 股票 Tick 数据支持 2010-01-01 至今。如果是期货,支持 2010-01-01 至今。
- 回测环境: 在回测策略中(
initialize,handle_data等函数内),无法直接使用plt.show()弹窗绘图。上述代码主要用于研究环境进行分析。 - 策略应用: 如果要在策略中使用 Volume Profile(例如寻找支撑位),只需要执行到“第3步聚合”即可,得到
volume_profileSeries 对象后,即可通过代码逻辑判断当前价格与 POC 的关系,而无需绘图。
Q&A
Q: 为什么第一笔 Tick 的成交量处理要用 fillna?
A: diff() 函数会对第一行产生 NaN 值。对于当天的第一笔 Tick(通常是 9:25 或 9:30 的数据),其 volume 值即为开盘集合竞价的成交量,因此直接用原始 volume 填充 NaN 是正确的。
Q: 如何在策略回测中利用 Volume Profile 数据?
A: 在策略中不需要画图。你可以计算出 volume_profile 后,使用 volume_profile.idxmax() 获取 POC 价格,或者计算 70% 成交量分布的价格区间(Value Area),以此作为交易信号的过滤条件。
Q: 期货的 Volume Profile 画法有区别吗?
A: 代码逻辑完全一致。只需将 security 换成期货合约代码(如 RB2310.XSGE)。但需注意期货有夜盘,start_dt 和 end_dt 需要涵盖夜盘时间段(前一交易日 21:00 到当日 15:00)。