第三章:数据获取与处理
做T0交易,说白了就是跟市场抢时间。你策略再牛,数据跟不上,一切都是白搭。我个人习惯把数据层比作「地基」——地基不稳,楼盖得再高也得塌。
这一章,咱们就聊聊怎么打好这个地基。从Level-2行情获取,到数据清洗,再到构建Tick级数据表,最后落地存储。每一步我都会把实战中踩过的坑、用过的技巧分享出来。
3.1 Level-2行情数据获取
先说说数据源。A股的Level-2行情,目前主流渠道就那么几家:券商、第三方数据商(比如万得、聚宽、恒生),还有交易所直连。我个人建议,起步阶段用券商的Level-2接口就够了,成本低,接入也快。
为什么非要用Level-2?普通行情3秒一笔,Level-2是逐笔成交,3秒一次快照。你想想看,T0交易拼的就是毫秒级反应,用普通行情做T0,就像用望远镜打靶——看得见,打不准。
核心数据字段:
- 逐笔成交:成交时间、价格、数量、买卖方向
- 十档行情:买一到买十、卖一到卖十的挂单量和价格
- 委托队列:前50笔委托的明细
- 快照数据:每3秒一次的全市场快照
我在项目中遇到过一个问题:券商接口返回的数据格式五花八门。有的用JSON,有的用Protobuf,还有的直接给你二进制流。嗯,这里要注意,一定要先做格式统一,不然后面处理起来会疯掉。
3.2 数据清洗与预处理
数据拿到手,别急着用。Level-2行情里「脏数据」不少,我总结了几类常见问题:
- 时间戳错乱:网络延迟导致数据包乱序,后发的先到
- 重复数据:同一个成交记录被推送了两次
- 异常值:价格突然变成0,或者成交量暴增100倍
- 缺失数据:某个时间窗口的数据没收到
我曾经因为没处理好重复数据,导致策略在回测里年化收益50%,实盘一跑直接亏钱。后来排查了两天才发现,是重复的成交记录让策略误以为流动性很好,频繁开仓。
避坑指南:数据清洗一定要做「去重+排序+校验」三步走。我曾经偷懒只做了去重,结果时间戳乱序导致策略信号全部错位,那叫一个惨。
清洗流程我一般这么设计:
- 按时间戳排序,丢弃明显异常的数据包
- 对同一毫秒内的重复数据,保留第一条
- 价格校验:超出当日涨跌停板的价格直接丢弃
- 成交量校验:单笔成交超过流通盘0.1%的,标记为异常
3.3 构建Tick级数据表
清洗完的数据,需要组织成结构化的Tick表。我习惯用Pandas的DataFrame来存,结构大概是这样的:
| 字段名 | 类型 | 说明 |
|---|---|---|
| ts | datetime64[ns] | 时间戳,精确到毫秒 |
| code | string | 股票代码 |
| price | float64 | 成交价格 |
| volume | int64 | 成交量(股) |
| side | int8 | 买卖方向:1买 -1卖 |
| bid_px_1 | float64 | 买一价 |
| ask_px_1 | float64 | 卖一价 |
代码实现其实不复杂,核心逻辑就几行:
import pandas as pd
import numpy as np
def build_tick_table(raw_data):
# 原始数据解析
df = pd.DataFrame(raw_data)
# 时间戳处理
df['ts'] = pd.to_datetime(df['timestamp'], unit='ms')
# 排序去重
df = df.sort_values('ts').drop_duplicates(subset=['ts', 'code'])
# 价格校验
df = df[(df['price'] > 0) & (df['price'] < 1000)]
return df
你想想看,一个股票一天大概产生几万笔成交,全市场几千只股票,数据量其实不小。所以Tick表的设计要兼顾查询效率和存储成本。我个人习惯把每只股票的数据单独存一个文件,按日期分目录,这样查询时只需要加载少量数据。
3.4 数据存储方案
存储方案这块,我踩过不少坑。最开始用CSV存,结果一个月的Tick数据就有几十个G,查询慢得要命。后来试过MySQL,写入速度跟不上。最后选定了Parquet格式,压缩率高,查询也快。
我的推荐方案:
- 实时数据:用Redis缓存,TTL设5分钟
- 历史数据:用Parquet文件存储,按日期分区
- 高频查询:用DuckDB做本地分析,比Pandas快10倍
存储目录结构我一般这么组织:
data/
├── 2024/
│ ├── 01/
│ │ ├── 600519.parquet
│ │ ├── 000001.parquet
│ │ └── ...
│ ├── 02/
│ └── ...
└── realtime/
├── 600519.parquet
└── ...
写入代码示例:
def save_tick_data(df, code, date):
path = f'data/{date[:4]}/{date[5:7]}/{code}.parquet'
df.to_parquet(path, compression='snappy')
读取时,我一般只加载当天数据,配合DuckDB做聚合查询,速度非常快。嗯,这里要注意,Parquet文件不支持追加写入,所以实时数据要攒够一批再写,比如每5秒批量写入一次。
3.5 本章知识体系
为了让你更直观地理解整个数据流程,我画了一张图:
这张图把整个流程串起来了:从行情源出发,经过清洗、建表,最后落地到不同的存储层。每个环节都有坑,但只要你按这个流程走,基本不会出大问题。
好了,数据获取与处理这块就聊到这儿。记住一句话:数据质量决定策略上限。你花80%的时间在数据处理上,一点都不亏。