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 知识体系总览

说了这么多,咱们用一张图把整个流程串起来。这是我做数据获取时脑子里始终绷着的一根弦。

订单簿数据获取与处理流程 数据源层 Dukascopy (免费) FXCM (收费) OANDA (友好) 数据获取层 Level 1 (最优报价) Level 2 (市场深度) Tick数据 数据清洗层 时间戳对齐 异常值过滤 缺失值填充 去重 输出:清洗后的订单簿DataFrame

这张图我每次做数据项目都会贴在墙上。你想想看,从数据源到最终可用的DataFrame,中间任何一个环节出问题,后面的策略分析都是白搭。

最后说一句:数据获取和清洗,看起来是脏活累活,但恰恰是量化交易中最值得花时间的地方。我见过太多人把精力花在策略优化上,结果数据本身就有问题。记住:垃圾进,垃圾出

专注资料整理