3. 数据准备与清洗:获取Level-2行情数据、处理缺失值与异常值、数据对齐与重采样
做TWAP策略,最怕什么?
不是策略逻辑写错了,而是数据本身就有问题。我见过太多人,策略回测跑得漂漂亮亮,一上实盘就崩。为什么?数据没洗干净。
这一章,咱们就聊聊数据准备这件事。说白了,就是给你的策略喂一口干净的饭。
3.1 获取Level-2行情数据
TWAP策略对数据精度要求很高。普通的分时线,一分钟才一个价,根本不够用。你需要的是Level-2行情——也就是逐笔成交和十档盘口。
我个人习惯用Python的akshare库来拉数据。它免费,接口也稳定。当然,如果你有券商的数据源,那更好。
import akshare as ak
import pandas as pd
import numpy as np
# 获取某只股票的Level-2逐笔成交数据
# 注意:akshare的接口可能会变,建议用前先查文档
def fetch_level2_tick(symbol, date):
"""
获取Level-2逐笔成交数据
:param symbol: 股票代码,如 '600519'
:param date: 日期,如 '2024-01-15'
:return: DataFrame
"""
try:
df = ak.stock_zh_a_tick_tx(symbol=symbol, trade_date=date)
print(f"成功获取 {symbol} 在 {date} 的Level-2数据,共 {len(df)} 条")
return df
except Exception as e:
print(f"获取数据失败: {e}")
return None
# 示例调用
df_tick = fetch_level2_tick('600519', '2024-01-15')
print(df_tick.head())
这里有个坑。Level-2数据量很大,一天可能有几万甚至十几万笔。你想想看,如果直接全量加载到内存,机器很容易卡死。我建议分批读取,或者用dtype指定列类型来节省内存。
3.2 处理缺失值与异常值
数据到手了,下一步就是清洗。为什么?因为交易所的数据也不是完美的。网络抖动、服务器延迟、撮合异常,都会导致数据出问题。
缺失值处理
最常见的缺失情况是:某几秒没有成交记录。TWAP是按时间切片的,如果某段时间没数据,你的算法就不知道该怎么下单。
我的做法是:先检查缺失比例。如果缺失少于5%,用前向填充;如果超过10%,那这段数据可能有问题,建议直接丢弃。
def handle_missing(df, time_col='time', price_col='price', vol_col='volume'):
"""
处理缺失值
1. 检查时间序列是否连续
2. 对缺失的价格用前向填充
3. 对缺失的成交量填0
"""
# 生成完整的时间序列(假设是1秒间隔)
full_time = pd.date_range(start=df[time_col].min(),
end=df[time_col].max(), freq='1S')
# 对齐
df = df.set_index(time_col).reindex(full_time)
# 前向填充价格
df[price_col] = df[price_col].ffill()
# 成交量缺失填0
df[vol_col] = df[vol_col].fillna(0)
missing_rate = df[price_col].isna().sum() / len(df)
if missing_rate > 0.05:
print(f"警告:缺失率 {missing_rate:.2%},建议检查数据源")
return df.reset_index().rename(columns={'index': time_col})
异常值处理
异常值比缺失值更隐蔽。比如某笔成交价格突然比前一笔高了10%,这可能是错单,也可能是真实的大单。怎么判断?
我一般用两种方法:
- 价格突变检测: 如果单笔价格变动超过前20笔均值的3倍标准差,标记为异常。
- 成交量异常检测: 如果单笔成交量超过过去100笔均值的5倍,标记为异常。
嗯,这里要注意:不要直接删除异常值。我曾经吃过这个亏——删掉了一个看似异常的跳空,结果那其实是开盘集合竞价的真实数据。后来我改成「标记+人工复核」的方式。
def detect_outliers(df, price_col='price', vol_col='volume', window=20, z_thresh=3):
"""
检测异常值
返回异常标记列
"""
# 价格异常
df['price_ma'] = df[price_col].rolling(window=window).mean()
df['price_std'] = df[price_col].rolling(window=window).std()
df['price_z'] = (df[price_col] - df['price_ma']) / df['price_std']
df['price_outlier'] = df['price_z'].abs() > z_thresh
# 成交量异常
df['vol_ma'] = df[vol_col].rolling(window=100).mean()
df['vol_std'] = df[vol_col].rolling(window=100).std()
df['vol_z'] = (df[vol_col] - df['vol_ma']) / df['vol_std']
df['vol_outlier'] = df['vol_z'].abs() > 5
# 综合标记
df['is_outlier'] = df['price_outlier'] | df['vol_outlier']
return df
3.3 数据对齐与重采样
TWAP策略的核心逻辑是:把总订单量均匀分配到每个时间切片。所以,数据必须对齐到统一的时间轴上。
为什么需要对齐?
因为Level-2数据是事件驱动的——有成交才记录。但TWAP需要的是固定间隔的快照。比如每5秒看一眼,当前价格是多少,累计成交了多少。
重采样怎么做?
我一般用pandas的resample方法。把逐笔数据聚合成固定间隔的K线。
def resample_to_twap(df, freq='5S', price_col='price', vol_col='volume'):
"""
将逐笔数据重采样为TWAP所需的时间切片数据
:param freq: 时间切片长度,如 '5S' 表示5秒
:return: 包含 open, high, low, close, volume 的DataFrame
"""
# 确保时间列是datetime类型
df['time'] = pd.to_datetime(df['time'])
df = df.set_index('time')
# 重采样
resampled = df.resample(freq).agg({
price_col: ['first', 'max', 'min', 'last'],
vol_col: 'sum'
})
# 展平列名
resampled.columns = ['open', 'high', 'low', 'close', 'volume']
# 去掉没有成交的时间段
resampled = resampled[resampled['volume'] > 0]
return resampled
# 示例
df_5s = resample_to_twap(df_tick, freq='5S')
print(df_5s.head())
你想想看,如果切片太短(比如1秒),很多切片里没有成交,TWAP就没法执行。如果切片太长(比如1分钟),又失去了精细化的优势。我个人习惯用5秒或10秒,这个频率既能捕捉到微观波动,又不会太稀疏。
3.4 本章知识体系
下面这张图,帮你把数据准备的全流程串起来:
3.5 实战中的几个建议
最后,分享几个我在实战中踩过的坑:
- 不要迷信Level-2数据: 它也有延迟。我对比过券商终端和API数据,有时差几百毫秒。做TWAP时,建议在算法里加一个「容忍窗口」。
- 数据存储要规划好: 一天的数据可能几百MB。我习惯用Parquet格式存储,比CSV小5倍,读取也快。
- 回测和实盘的数据源要一致: 我曾经回测用A数据源,实盘用B数据源,结果策略表现完全不同。后来统一了数据源,问题才解决。
数据准备这块,说白了就是「慢工出细活」。你花在清洗上的每一分钟,都会在策略表现上得到回报。别急着跑策略,先把数据伺候好。