3. 数据获取与清洗:用Python搞定Level-2数据

做订单流分析,数据就是你的弹药库。弹药不行,枪法再好也白搭。

我刚开始接触Level-2数据时,踩过不少坑。有一次回测策略表现特别好,我差点就实盘上了。结果仔细一查,发现是时间戳没对齐,把未来的数据当成了历史信号。嗯,那感觉就像考试抄答案抄错了行。

今天咱们就聊聊,怎么用Python把交易所的Level-2数据拿下来,再把它收拾得服服帖帖。

3.1 Level-2数据长什么样?

先搞清楚我们要处理什么。Level-2数据比普通行情丰富得多,主要包括这几类:

数据类型 字段示例 更新频率
逐笔成交 时间、价格、成交量、买卖方向 每笔成交即更新
十档行情 买1~买10价量、卖1~卖10价量 约3秒一次快照
委托队列 买1/卖1的前50笔委托明细 随盘口变化更新
逐笔委托 委托时间、价格、数量、委托编号 每笔委托即更新

我个人习惯把逐笔成交和十档行情作为核心数据源。为什么?因为订单流分析最依赖的就是这两类数据——成交数据告诉你资金在动,盘口数据告诉你资金在哪儿等着。

3.2 数据获取:从交易所到本地

获取Level-2数据主要有两条路:

  • 直接接入交易所:上交所用L1/L2行情网关,深交所用MDGW。这条路门槛高,适合机构。
  • 通过数据服务商:比如万得、聚宽、Tushare、AkShare等。个人开发者首选。

我用AkShare比较多,免费且接口丰富。下面是一个获取逐笔成交数据的例子:

import akshare as ak
import pandas as pd

# 获取某只股票的逐笔成交数据
# 注意:这里需要你有Level-2权限
stock_code = "600519"  # 贵州茅台
trade_date = "2024-01-15"

# 获取逐笔成交
df_tick = ak.stock_individual_tick_em(
    symbol=stock_code,
    start_date=trade_date,
    end_date=trade_date
)

print(df_tick.head())
print(f"数据量:{len(df_tick)} 条")

输出大概长这样:

        时间        价格    成交量  买卖方向
0  09:30:00.123  1680.00   100     买
1  09:30:00.456  1680.02   200     卖
2  09:30:00.789  1680.01   150     买
...
注意:不同数据源的字段名和格式可能不一样。我曾经遇到过某家服务商把"买卖方向"字段写成了"BSFlag",害我排查了半天。拿到数据后,第一件事就是打印列名,确认字段含义。

3.3 时间戳对齐:最容易被忽视的坑

为什么时间戳对齐这么重要?

你想想看,订单流分析本质上是在做"时间序列上的因果推断"。如果逐笔成交的时间戳和盘口快照的时间戳对不上,你可能会把成交归到错误的盘口状态上。

举个例子:

  • 09:30:01.000 盘口显示买1是1000手
  • 09:30:01.500 成交了500手
  • 09:30:02.000 盘口显示买1变成了500手

如果时间戳没对齐,你可能会以为成交发生在盘口更新之后,那就完全搞反了因果关系。

对齐的方法其实不复杂:

# 假设 df_trade 是逐笔成交,df_quote 是盘口快照
# 统一时间戳精度到毫秒

# 1. 把时间列转为datetime类型
df_trade['time'] = pd.to_datetime(df_trade['时间'], format='%H:%M:%S.%f')
df_quote['time'] = pd.to_datetime(df_quote['时间'], format='%H:%M:%S.%f')

# 2. 设置时间索引
df_trade.set_index('time', inplace=True)
df_quote.set_index('time', inplace=True)

# 3. 使用merge_asof进行"最近邻"对齐
# 注意:direction='backward'表示成交对齐到最近的盘口快照
df_aligned = pd.merge_asof(
    df_trade,
    df_quote,
    left_index=True,
    right_index=True,
    direction='backward',
    tolerance=pd.Timedelta('3s')  # 最多允许3秒偏差
)

print(f"对齐后数据量:{len(df_aligned)} 条")
print(f"缺失值数量:{df_aligned.isnull().sum().sum()}")
小技巧:我建议把时间戳统一到毫秒级。有些数据源给的是微秒甚至纳秒,但盘口数据通常只有毫秒精度。统一到毫秒可以减少很多对齐问题。

3.4 去重:别让重复数据毁了你的分析

Level-2数据在传输过程中,偶尔会出现重复推送。尤其是网络不稳定的时候,同一个成交记录可能收到两次。

去重的方法很简单,但要注意选对去重字段:

# 逐笔成交去重:用"时间+价格+成交量+买卖方向"作为唯一标识
df_trade_dedup = df_trade.drop_duplicates(
    subset=['时间', '价格', '成交量', '买卖方向'],
    keep='first'
)

# 盘口快照去重:用"时间"作为唯一标识
df_quote_dedup = df_quote.drop_duplicates(
    subset=['时间'],
    keep='last'  # 保留最后一条,因为盘口数据是快照
)

print(f"去重前:{len(df_trade)} 条")
print(f"去重后:{len(df_trade_dedup)} 条")
print(f"去除了 {len(df_trade) - len(df_trade_dedup)} 条重复数据")

我曾经遇到过一种特殊情况:同一个成交记录,价格和成交量都一样,但买卖方向不同。这种情况不是重复,而是真实的"多空对决"。所以去重时一定要把买卖方向也考虑进去。

3.5 缺失值处理:填还是不填?

缺失值在Level-2数据里很常见。比如盘口快照更新频率是3秒一次,但成交可能每秒发生几十笔。对齐之后,很多成交记录对应的盘口字段就是空的。

处理策略取决于你要做什么:

场景 推荐策略 原因
计算订单流不平衡 向前填充(ffill) 用最近的盘口状态来估算
回测策略信号 删除缺失行 避免使用不完整的数据做决策
实时监控 插值填充 保持数据连续性

代码实现:

# 向前填充:用上一个盘口数据填充当前缺失
df_aligned['买1价'].fillna(method='ffill', inplace=True)
df_aligned['卖1价'].fillna(method='ffill', inplace=True)

# 或者直接删除有缺失的行
df_clean = df_aligned.dropna(subset=['买1价', '卖1价', '价格'])

print(f"填充前缺失:{df_aligned.isnull().sum().sum()}")
print(f"填充后缺失:{df_aligned.fillna(method='ffill').isnull().sum().sum()}")
核心原则:缺失值处理没有银弹。我的经验是——先理解数据缺失的原因,再决定处理方式。如果是网络延迟导致的缺失,向前填充没问题。如果是数据源本身就不提供某些字段,那就要考虑换数据源了。

3.6 完整的数据清洗流水线

把上面这些步骤串起来,就是一个完整的数据清洗流水线:

def clean_level2_data(df_trade, df_quote):
    """
    清洗Level-2数据流水线
    """
    # 1. 时间戳标准化
    df_trade['time'] = pd.to_datetime(df_trade['时间'], format='%H:%M:%S.%f')
    df_quote['time'] = pd.to_datetime(df_quote['时间'], format='%H:%M:%S.%f')
    
    # 2. 去重
    df_trade = df_trade.drop_duplicates(subset=['时间', '价格', '成交量', '买卖方向'])
    df_quote = df_quote.drop_duplicates(subset=['时间'])
    
    # 3. 排序(时间戳对齐前必须排序)
    df_trade.sort_values('time', inplace=True)
    df_quote.sort_values('time', inplace=True)
    
    # 4. 时间戳对齐
    df_aligned = pd.merge_asof(
        df_trade,
        df_quote,
        left_index=True,
        right_index=True,
        direction='backward',
        tolerance=pd.Timedelta('3s')
    )
    
    # 5. 缺失值处理
    df_aligned.fillna(method='ffill', inplace=True)
    
    # 6. 删除开盘集合竞价数据(通常不需要)
    df_aligned = df_aligned[df_aligned['time'] >= '09:30:00']
    
    return df_aligned

# 使用示例
df_clean = clean_level2_data(df_trade, df_quote)
print(f"清洗完成,共 {len(df_clean)} 条有效数据")

3.7 数据清洗的核心逻辑

下面这张图展示了整个数据清洗的流程,我建议你保存下来,以后做数据清洗时对照着看:

Level-2数据清洗流程 逐笔成交数据 盘口快照数据 步骤1:时间戳标准化 步骤2:数据去重 步骤3:按时间排序 步骤4:merge_asof对齐 步骤5:缺失值处理 清洗完成的数据 注意:每一步都可能出现异常数据,建议加入异常检测

数据清洗这件事,说白了就是"垃圾进,垃圾出"。你花再多时间在策略上,如果数据本身有问题,结果都是白费。

我个人习惯在清洗完数据后,做一个简单的质量检查:统计一下缺失率、重复率、时间戳间隔分布。如果发现异常,宁可重新获取数据,也不要凑合着用。

最后一个小建议:把清洗好的数据保存成Parquet格式,比CSV快10倍以上,而且压缩率更好。我一般按日期分文件存储,方便后续回测时按需加载。

公众号:蓝海资料掘金营,微信deep3321