AllTick
如何清洗和预处理用于回测的 XAUUSD 历史 Tick 数据?
逐笔数据量化交易策略回测

如何清洗和预处理用于回测的 XAUUSD 历史 Tick 数据?

学习如何使用 Python 清洗历史 XAUUSD Tick 数据,验证时间戳、删除重复记录、检测异常报价,并将买卖价差纳入回测,提高回测结果的可靠性。

AllTick阅读 2 分钟

黄金交易策略在回测中可能表现出色,但在真实市场中却产生不同的结果。对于量化交易者而言,问题可能并不在策略本身,而在于用于评估策略的历史 XAUUSD Tick 数据。

重复报价可能扭曲事件统计,错误的时间戳可能改变交易信号的顺序,而忽略买卖价差则可能高估潜在收益。因此,可靠的回测应从一致的数据清洗流程开始,在剔除无效记录的同时,保留真实的市场波动。

本文将介绍关键的数据预处理步骤、可复用的 Python 示例,以及构建更可靠的 XAUUSD 回测流程时需要考虑的执行因素。

处理前先检查历史 Tick 数据

历史报价数据通常包含时间戳、买入价(Bid)和卖出价(Ask),但具体字段取决于数据来源。因此,在应用清洗规则之前,应先检查数据结构。

text
import pandas as pd
import numpy as np

df = pd.read_csv("xauusd_tick.csv")

required = {"timestamp", "bid", "ask"}
missing = required - set(df.columns)

if missing:
    raise ValueError(f"Missing columns: {missing}")

print("Original rows:", len(df))
print(df.isna().sum())

保留一份未经修改的原始文件。在更改数据之前,先确认时间戳格式、时区和价格定义。例如,Unix 毫秒时间戳与已经格式化的日期字符串,需要采用不同的解析方式。

统一时间戳并删除重复记录

当策略按顺序处理单个市场事件时,时间顺序的准确性十分重要。如果数据源使用 Unix 毫秒时间戳,可以明确指定转换方式。

text
df["timestamp"] = pd.to_datetime(
    df["timestamp"],
    unit="ms",
    utc=True,
    errors="coerce"
)

invalid_time = df["timestamp"].isna().sum()
df = df.dropna(subset=["timestamp"])

df = df.sort_values(
    "timestamp",
    kind="stable"
).reset_index(drop=True)

before = len(df)

df = df.drop_duplicates(
    subset=["timestamp", "bid", "ask"]
).copy()

print("Invalid timestamps:", invalid_time)
print("Duplicates removed:", before - len(df))

只有在数据源确认使用毫秒时间戳时,才应使用 unit="ms"。仅根据时间戳删除记录,可能会误删时间相同但实际有效的报价更新。如果数据包含事件 ID 或序列号,可以利用这些字段进一步完善重复记录识别规则。

验证报价并检查异常价格

对于常规报价数据,Bid 和 Ask 应当存在、数值有限且大于零。如果 Bid 高于 Ask,应将其标记出来进行调查,而不是直接认定数据已经损坏。

text
df["bid"] = pd.to_numeric(df["bid"], errors="coerce")
df["ask"] = pd.to_numeric(df["ask"], errors="coerce")

valid = (
    np.isfinite(df["bid"]) &
    np.isfinite(df["ask"]) &
    (df["bid"] > 0) &
    (df["ask"] > 0) &
    (df["ask"] >= df["bid"])
)

df["invalid_quote"] = ~valid
df["spread"] = df["ask"] - df["bid"]
df["mid"] = (df["ask"] + df["bid"]) / 2

print("Invalid quotes:", int((~valid).sum()))

大幅价格波动同样需要谨慎检查。经济数据公布期间出现的 XAUUSD 快速波动可能是真实行情,因此不应仅凭固定阈值自动删除异常值。

text
df["mid_return"] = df["mid"].pct_change()

df["review_move"] = (
    df["mid_return"].abs() > 0.005
)

print("Price moves flagged:", int(df["review_move"].sum()))

这里的 0.5% 仅为示例,并不是适用于所有 XAUUSD 策略的通用标准。应结合周边报价、价差变化以及可获取的原始数据证据检查被标记的记录,再决定是否将其排除。

检查缺失时间段并准备回测数据

相邻时间戳之间存在间隔,并不一定意味着数据缺失。正常的市场休市时段、报价活动变化以及数据采集过程中的中断,都可能产生时间间隔。

text
df["gap_seconds"] = (
    df["timestamp"].diff().dt.total_seconds()
)

gaps = df[df["gap_seconds"] > 60]

print("Intervals over 60 seconds:", len(gaps))

这里的 60 秒只是用于排查问题的示例阈值。应根据策略的采样频率和数据提供方公布的交易时段进行调整。不要通过插值补齐缺失的 Tick 记录,再将生成的合成价格当作真实市场事件。

部分策略直接使用单条报价更新,另一些策略则会将 Tick 数据聚合成时间 K 线。例如,下面的代码可以生成简单的一秒报价序列:

text
valid_df = df.loc[
    ~df["invalid_quote"]
].copy()

bars = (
    valid_df.set_index("timestamp")
    .resample("1s")
    .agg({
        "bid": "last",
        "ask": "last",
        "mid": "last"
    })
)

没有有效报价的时间段会继续保留为空。这可以避免将缺失观测误认为真实的市场报价。

在回测中考虑买卖价差和交易成本

使用单一价格序列,可能会高估短线策略的表现。在简化的报价模型中,多头仓位以 Ask 买入、以 Bid 卖出;空头仓位的开仓和退出方向则相反。

text
entry_ask = valid_df["ask"].iloc[0]
exit_bid = valid_df["bid"].iloc[-1]

gross_change = exit_bid - entry_ask

print("Gross price change:", gross_change)

这个示例展示了买卖价差对一笔假设多头交易的影响,但它并不是完整的交易模拟器。更真实的回测还应考虑适用的佣金、滑点、融资成本、仓位规模和订单执行规则。

策略信号只能使用决策时已经可获取的信息,否则可能产生前视偏差,使策略表现看起来优于真实交易中的结果。

保持历史研究与实时数据处理的一致性

量化交易者应记录时间戳规范、数据验证规则和记录排除条件,并在历史研究与实时数据处理流程中复用兼容的逻辑。这样更容易调查历史回测与实盘策略表现之间的差异。

AllTick 提供 REST 和 WebSocket 市场数据接口。对于需要查看黄金相关品种的开发者,可以先访问 AllTick 商品 API 产品页;API 文档 则提供接口集成参考。

需要注意,实时行情访问不等于历史 Tick 数据访问。在选择用于回测的数据源之前,应确认相关产品是否支持所需的历史字段、数据粒度和历史保留期限。

运行回测前的数据质量检查清单

在接受回测结果之前,应确认时间戳和报价字段有效,重复记录的删除规则有明确记录,异常价格已经过检查,无法解释的时间缺口已经调查。同时,还应确保执行价格反映买卖价差,并计入适用的交易成本。

高质量的数据无法保证策略盈利,但可重复的数据预处理流程能够帮助量化交易者区分真实的策略信号与数据质量问题。

如果你正在开发黄金市场相关应用,可以查看 AllTick 商品 API 及其相关文档,确认可用数据是否符合你的研究需求。

立即开始接入市场数据

几秒内生成免费 API 密钥,通过一个端点连接所有市场。