3、订单簿数据获取:用Python搞定Level 1与Level 2数据
做外汇交易这么多年,我越来越觉得——订单簿就是市场的X光片。Level 1数据告诉你价格在哪,Level 2数据告诉你价格为什么在那。今天咱们就聊聊怎么用Python把这些数据抓到手,再洗干净。
3.1 Level 1 vs Level 2:到底差在哪?
先别急着写代码,咱们得搞清楚要拿什么数据。
Level 1数据,说白了就是「最优报价」。你打开MT4看到的Bid/Ask,就是Level 1。它告诉你:现在市场上最好的买单在哪,最好的卖单在哪。
Level 2数据,也叫市场深度(Market Depth)。它把订单簿一层层剥开给你看——距离当前价格5个点的买单有多少,10个点的卖单又有多少。我刚开始做量化时,只盯着Level 1看,结果策略回测漂亮,实盘一跑就崩。后来才发现,流动性深度才是决定滑点的关键。
| 维度 | Level 1 | Level 2 |
|---|---|---|
| 数据内容 | 最优Bid/Ask + 成交量 | 多档位挂单 + 每档深度 |
| 更新频率 | 毫秒级 | 毫秒级(数据量更大) |
| 典型用途 | 实时报价、简单策略 | 订单簿分析、做市策略 |
| 数据量 | 小 | 大(每笔含10-20档) |
我的经验:做日内交易,Level 1就够了。但如果你做高频或者分析流动性拐点,Level 2是必须的。我见过有人用Level 1数据做订单簿重建,那误差大得离谱。
3.2 主流数据源API对接
市面上数据源不少,但我重点讲三个:Dukascopy、FXCM、OANDA。为什么?因为我都踩过坑。
3.2.1 Dukascopy:免费但有点「糙」
Dukascopy的JForex平台提供免费的历史数据,但API文档写得...嗯,你懂的。我个人习惯用它的Tick数据,精度到0.1 pip。
import requests
import pandas as pd
from datetime import datetime
def get_dukascopy_tick(symbol, date):
"""
获取Dukascopy的Tick数据
注意:日期格式必须为YYYY/MM/DD
"""
url = f"https://datafeed.dukascopy.com/datafeed/{symbol}/"
url += f"{date.year}/{date.month:02d}/{date.day:02d}/"
url += "ticks.h5"
# 我记得第一次用这个接口时,忘了处理时区问题
# 结果拿到的数据全是错的
response = requests.get(url)
if response.status_code == 200:
# 解析二进制数据
# 这里省略具体解析逻辑
pass
else:
print(f"数据获取失败: {response.status_code}")
避坑指南:我曾经因为Dukascopy的数据时间戳是UTC+2,直接拿来做回测,结果策略在非农数据发布时表现异常。后来才发现是时区没对齐。切记:所有时间戳统一转成UTC。
3.2.2 FXCM:稳定但收费
FXCM的REST API做得比较规范。它提供Level 2数据,但需要申请权限。我建议你直接用它的Python SDK——fxcmpy,省去很多麻烦。
import fxcmpy
# 初始化连接
# 注意:token需要从FXCM后台获取
con = fxcmpy.fxcmpy(access_token='YOUR_TOKEN',
log_level='error')
# 获取Level 2数据
# 返回的是DataFrame,包含多档Bid/Ask
def get_fxcm_level2(symbol='EUR/USD', depth=10):
"""
获取FXCM的Level 2市场深度
depth参数控制返回的档位数
"""
try:
data = con.get_market_depth(symbol)
# 只取前depth档
return data.head(depth)
except Exception as e:
print(f"获取Level 2失败: {e}")
return None
小技巧:FXCM的Level 2数据更新频率是100ms一次。如果你需要更高频率,可以考虑用WebSocket连接。我做过测试,WebSocket模式下延迟能降到20ms以内。
3.2.3 OANDA:文档最友好
OANDA的API文档是我见过最清晰的。它的v20 API支持REST和Stream两种模式。我个人偏爱Stream模式——数据实时推送,不用轮询。
import oandapyV20
import oandapyV20.endpoints.orders as orders
from oandapyV20.contrib.requests import MarketOrderRequest
# 配置OANDA连接
client = oandapyV20.API(access_token="YOUR_TOKEN",
environment="practice")
# 获取订单簿数据
def get_oanda_orderbook(instrument="EUR_USD"):
"""
OANDA的订单簿数据包含:
- price: 价格
- longCount: 多头挂单数
- shortCount: 空头挂单数
"""
# 注意:OANDA的订单簿是聚合数据
# 不是原始挂单,而是按价格聚合的
r = orders.OrderBook(instrument)
response = client.request(r)
# 解析返回数据
orderbook = response['orderBook']['buckets']
return pd.DataFrame(orderbook)
对比总结:
- Dukascopy:免费,但数据格式奇葩,适合有耐心的开发者
- FXCM:稳定,Level 2数据质量高,但收费
- OANDA:文档好,API设计现代,适合快速开发
3.3 数据清洗与预处理
数据拿到手,别急着用。我敢说,80%的量化策略问题出在数据质量上。下面是我总结的清洗流程。
3.3.1 时间戳对齐
不同数据源的时间戳格式五花八门。Dukascopy用毫秒时间戳,FXCM用ISO 8601字符串,OANDA用RFC 3339格式。统一转成datetime对象是第一步。
def normalize_timestamp(ts, source):
"""
统一时间戳格式
"""
if source == 'dukascopy':
# Dukascopy: 毫秒时间戳
return pd.to_datetime(ts, unit='ms')
elif source == 'fxcm':
# FXCM: ISO 8601字符串
return pd.to_datetime(ts)
elif source == 'oanda':
# OANDA: RFC 3339格式
return pd.to_datetime(ts)
else:
raise ValueError(f"未知数据源: {source}")
3.3.2 异常值处理
订单簿数据里经常出现「幽灵挂单」——价格异常高或低的挂单。这些通常是数据错误或者测试单。我一般用Z-score方法过滤。
def filter_outliers(df, column='price', threshold=3):
"""
用Z-score过滤异常价格
threshold=3表示超过3个标准差视为异常
"""
z_scores = (df[column] - df[column].mean()) / df[column].std()
return df[abs(z_scores) < threshold]
我曾经踩过的坑:有一次用FXCM的Level 2数据做策略,发现回测收益高得离谱。查了半天,原来是数据里包含了「冰山订单」——大单被拆成小单显示。如果不做聚合处理,策略会误以为流动性很好。后来我加了一步:对相邻档位的挂单量做合并。
3.3.3 缺失值填充
Level 2数据经常有空档——某个价格区间没有挂单。直接填0会扭曲流动性分析。我建议用插值法。
def fill_missing_depth(df, method='linear'):
"""
填充订单簿中的空档
method: linear(线性插值) / ffill(前向填充)
"""
# 先按价格排序
df = df.sort_values('price')
# 对挂单量做插值
df['volume'] = df['volume'].interpolate(method=method)
return df
3.4 知识体系总览
说了这么多,咱们用一张图把整个流程串起来。这是我做数据获取时脑子里始终绷着的一根弦。
这张图我每次做数据项目都会贴在墙上。你想想看,从数据源到最终可用的DataFrame,中间任何一个环节出问题,后面的策略分析都是白搭。
最后说一句:数据获取和清洗,看起来是脏活累活,但恰恰是量化交易中最值得花时间的地方。我见过太多人把精力花在策略优化上,结果数据本身就有问题。记住:垃圾进,垃圾出。