13. 订单流与K线结合:将订单流数据映射到K线周期,构建订单流K线(Volume Profile K线),Python实现
做量化交易这些年,我越来越觉得一个道理:裸K线就像人的骨架,订单流才是血肉。K线告诉你价格去了哪里,订单流告诉你价格是怎么去的。今天我们就来聊聊,怎么把这两者真正揉在一起——构建订单流K线,也叫Volume Profile K线。
说白了,就是把每一根K线内部的逐笔成交数据,按照价格档位重新聚合,形成一张「价格-成交量」的分布图。这样你一眼就能看出:这根阳线是靠哪个价位堆起来的?是主动买盘推上去的,还是被动接盘扛上去的?
13.1 数据准备:从逐笔成交到K线切片
我们先得有逐笔成交数据。我习惯用这样的结构:时间戳、价格、成交量、买卖方向(B/S)。
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 模拟逐笔成交数据
np.random.seed(42)
n_trades = 5000
base_price = 100.0
trades = pd.DataFrame({
'time': pd.date_range('2024-01-01 09:30:00', periods=n_trades, freq='100ms'),
'price': base_price + np.random.randn(n_trades).cumsum() * 0.1,
'volume': np.random.randint(1, 50, n_trades),
'side': np.random.choice(['B', 'S'], n_trades, p=[0.55, 0.45])
})
# 价格四舍五入到最小变动单位(0.01)
trades['price'] = trades['price'].round(2)
print(trades.head())
嗯,这里要注意:实际生产中,逐笔数据量非常大。我曾经在实盘环境里处理过一天几千万笔的Level-2数据,如果不用向量化操作,光循环就能跑死你。所以下面的代码全部用pandas的groupby和apply,避免逐行迭代。
13.2 核心逻辑:按K线周期切片,再按价格聚合
假设我们要做5分钟K线的订单流映射。思路很简单:
- 把逐笔数据按5分钟切块
- 在每个块内,按价格分组,统计买卖成交量
- 把结果拼成一张表,每行代表一根K线的一个价格档位
def build_volume_profile_kline(trades_df, freq='5min'):
"""
构建订单流K线(Volume Profile K线)
返回:每个K线周期内,每个价格档位的买卖成交量
"""
# 1. 按时间周期分组
trades_df = trades_df.copy()
trades_df['kline_time'] = trades_df['time'].dt.floor(freq)
# 2. 按K线周期 + 价格 + 买卖方向 聚合
vp = trades_df.groupby(['kline_time', 'price', 'side']).agg(
volume=('volume', 'sum')
).reset_index()
# 3. 透视:把买卖方向拆成两列
vp_pivot = vp.pivot_table(
index=['kline_time', 'price'],
columns='side',
values='volume',
fill_value=0
).reset_index()
# 确保列名存在
for col in ['B', 'S']:
if col not in vp_pivot.columns:
vp_pivot[col] = 0
vp_pivot.columns = ['kline_time', 'price', 'buy_vol', 'sell_vol']
vp_pivot['total_vol'] = vp_pivot['buy_vol'] + vp_pivot['sell_vol']
vp_pivot['net_vol'] = vp_pivot['buy_vol'] - vp_pivot['sell_vol']
return vp_pivot.sort_values(['kline_time', 'price'])
# 构建5分钟订单流K线
vp_kline = build_volume_profile_kline(trades, '5min')
print(vp_kline.head(10))
13.3 可视化:画出订单流K线的Volume Profile
数据有了,怎么展示?我一般用两种方式:
- 横向柱状图:每根K线右侧画一个横向的成交量分布,类似TradingView的Volume Profile
- 热力图:把多根K线的价格-成交量分布用颜色深浅表示,一眼看出资金堆积区
下面我用matplotlib画一个单根K线的Volume Profile:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
def plot_single_vp_kline(vp_data, kline_time_str):
"""
绘制单根K线的Volume Profile
vp_data: 订单流K线DataFrame
kline_time_str: 要绘制的K线时间
"""
# 筛选指定K线
kline = vp_data[vp_data['kline_time'] == kline_time_str].copy()
if kline.empty:
print(f"未找到 {kline_time_str} 的数据")
return
# 计算价格范围
prices = kline['price'].values
buy_vols = kline['buy_vol'].values
sell_vols = kline['sell_vol'].values
max_vol = max(buy_vols.max(), sell_vols.max())
fig, ax = plt.subplots(figsize=(10, 6))
# 画买盘(绿色,向右)
for i, (p, bv) in enumerate(zip(prices, buy_vols)):
ax.barh(p, bv, height=0.008, color='green', alpha=0.6, label='Buy' if i == 0 else '')
# 画卖盘(红色,向左)
for i, (p, sv) in enumerate(zip(prices, sell_vols)):
ax.barh(p, -sv, height=0.008, color='red', alpha=0.6, label='Sell' if i == 0 else '')
ax.axvline(0, color='black', linewidth=0.5)
ax.set_xlabel('Volume')
ax.set_ylabel('Price')
ax.set_title(f'Volume Profile Kline - {kline_time_str}')
ax.legend(loc='upper right')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 取第一根K线的时间
first_kline_time = vp_kline['kline_time'].iloc[0]
plot_single_vp_kline(vp_kline, first_kline_time)
13.4 实战应用:用订单流K线识别关键价位
有了订单流K线,我们能做什么?我分享三个最常用的场景:
| 场景 | 识别方法 | 交易含义 |
|---|---|---|
| 高成交量节点(HVN) | 成交量最大的价格档位 | 多空博弈最激烈的位置,容易形成支撑/阻力 |
| 低成交量节点(LVN) | 成交量明显偏小的价格档位 | 价格容易快速穿过,形成「真空区」 |
| 净成交量极值 | 净成交量(买-卖)最大的价格 | 主力资金集中介入的位置,趋势启动点 |
举个例子,我们可以找出每根K线的POC(Point of Control,即成交量最大的价格):
# 找出每根K线的POC
poc = vp_kline.loc[vp_kline.groupby('kline_time')['total_vol'].idxmax()]
poc = poc[['kline_time', 'price', 'total_vol', 'net_vol']]
poc.columns = ['kline_time', 'poc_price', 'poc_vol', 'poc_net_vol']
print(poc.head())
你想想看,如果连续三根K线的POC都在同一个价格附近,说明什么?说明这个价位有「磁铁效应」——价格离开后大概率会回来测试。我在做日内策略时,经常用这个逻辑来设置止盈止损。
13.5 进阶:多周期订单流K线聚合
实际交易中,我很少只看单一周期。我的做法是:
- 用1分钟K线做精细的Volume Profile,找日内支撑阻力
- 用30分钟K线做中期的价值区间(Value Area)
- 用日线做全局的成交量分布
代码上只需要改freq参数:
# 多周期聚合
vp_1min = build_volume_profile_kline(trades, '1min')
vp_30min = build_volume_profile_kline(trades, '30min')
vp_1day = build_volume_profile_kline(trades, '1D')
print(f"1分钟K线数: {vp_1min['kline_time'].nunique()}")
print(f"30分钟K线数: {vp_30min['kline_time'].nunique()}")
print(f"日K线数: {vp_1day['kline_time'].nunique()}")
13.6 性能优化:大数据量下的处理技巧
最后聊点实战中绕不开的问题——性能。逐笔数据量巨大,如果直接groupby,内存可能会爆。我分享几个优化技巧:
- 分块处理:按日期分块,每天单独计算,最后合并
- 使用category类型:把价格和买卖方向转为category,groupby速度提升数倍
- 提前过滤:如果只关注某个价格区间,先切片再聚合
# 性能优化示例:使用category类型
trades['price_cat'] = trades['price'].astype('category')
trades['side_cat'] = trades['side'].astype('category')
# 优化后的聚合
vp_optimized = trades.groupby(
[pd.Grouper(key='time', freq='5min'), 'price_cat', 'side_cat'],
observed=True # 只保留实际出现的组合
)['volume'].sum().reset_index()
print(f"优化前内存: {trades.memory_usage(deep=True).sum() / 1024:.2f} KB")
print(f"优化后内存: {vp_optimized.memory_usage(deep=True).sum() / 1024:.2f} KB")
嗯,这里要提醒一句:category类型虽然快,但要注意价格精度。如果价格有太多唯一值,category反而会变慢。我一般把价格四舍五入到小数点后两位再转category,效果最好。
订单流K线,说白了就是把「谁在什么价位干了多少」这件事,从逐笔数据里提炼出来,映射到K线周期上。它不像传统K线那样只告诉你结果,而是告诉你过程。我个人觉得,这是从「看图说话」到「理解市场微观结构」的关键一步。
下一节我们会继续深入,聊聊怎么用这些订单流K线数据,构建实时的交易信号。敬请期待。