多资产回测如何解决跨市场行情 API 的数据格式差异

用户头像sh_****447dvu
2026-09-23 发布

概述

在构建多资产量化策略、开展跨市场回测研究时,往往需要同时获取外汇、贵金属、加密货币、股票的实时与历史行情数据。不少研究者会直接对接多家第三方数据服务,期望快速完成数据聚合,但实际开发过程中,各类接口的协议、字段、时间体系差异会产生大量适配成本,甚至会埋下回测与实盘的数据一致性隐患。

本文基于项目实践,梳理跨市场行情接入的典型问题,给出 API 的评估维度,介绍一套内部数据模型归一化的工程实现思路,并提供可用于策略原型开发的 Python WebSocket 示例代码,旨在为多资产策略研究、实盘原型开发提供可复用的技术参考。

跨市场行情接入的常见技术问题

在开展多资产监控面板开发的过程中,曾同时对接三家不同数据源分别获取外汇贵金属、加密货币、股票行情。各服务商的鉴权逻辑、标的编码、时间格式、返回字段相互独立,大量开发工作量消耗在数据格式转换,而非业务与策略逻辑本身。总结四类高频问题:

1. 标的编码体系不统一

同一交易品种,不同服务商编码规则存在差异。例如欧元兑美元存在EURUSDEUR/USD两种写法;加密货币交易对分为BTC‑USDTBTCUSDT格式;股票标的需要追加对应交易所后缀。直接将 A 服务商的编码传入 B 服务商接口,通常无法获取有效行情,会造成数据缺失。

2. 时间戳与字段数据类型混杂

时间戳存在秒级、毫秒级、时间字符串等多种输出形式;价格、成交量字段部分接口返回数值,部分返回字符串。若未做统一类型处理,在回测的时序计算、指标运算阶段会出现异常报错,干扰策略验证。

3. 推送模式、心跳与限流规则不一致

部分数据源支持 WebSocket 长连接实时推送,部分仅支持轮询拉取。各接口的心跳保活、断线恢复、访问频率限制均为独立规则,难以复用同一套客户端逻辑,增加多源并发维护成本。

4. 各市场交易时间模型存在显著差异

加密货币 7×24 小时连续交易;外汇周末休市;股票包含开盘、午休、法定节假日停盘周期。如果采用固定超时作为行情异常判定标准,休市阶段会产生大量无效告警,无法区分正常休市与真实数据源故障,不利于策略运行状态监控。

多资产行情 API 评估要点

结合实践经验,评估多资产行情 API,核心目标是降低外部接口带来的适配工作量,减少后续回测、实盘之间的数据不一致风险,重点考察如下维度:

  1. 鉴权体系:多类资产是否可复用同一 Token 完成身份校验;
  2. 协议一致性:不同品种的订阅报文结构、推送字段命名是否具备稳定性;
  3. 文档完备度:是否提供完整、可查阅的标的编码对照表;
  4. 运行约束:明确的访问配额、限流规则,同时文档对心跳、断线重连机制有清晰说明。

完全统一全品类字段的外部接口并不存在。即便是同一服务商,股票、外汇、加密货币也可能划分不同接入端点,存在局部字段差异。务实处理方式:优先选择协议标准化程度较高的数据源,在程序内部构建适配层消化剩余差异。

在原型研究阶段,可选用 AllTick API 开展测试工作,外汇、贵金属、加密货币共用一组 WebSocket 接入端点,美股、港股、A 股使用另一组端点,两套链路订阅协议保持一致,能够减少适配层的开发工作量。

工程实现:内部 Quote 模型隔离外部数据差异

应用价值:保证回测与实盘使用同一套内部数据模型,消除第三方接口变更对上层策略逻辑的影响。

无论外部数据源输出格式如何,在程序内部定义专属行情实体Quote,仅保留策略研究必需的核心字段:数据源标识、品种代码、价格、成交量、毫秒级时间戳。

所有第三方原始行情数据流入系统后,第一步执行归一化转换,映射为统一的Quote对象。策略计算、指标分析、回测模块、行情监控均依赖该内部模型,完全不感知第三方原始返回格式。

该架构带来两项关键收益:

  1. 更换底层数据源时,仅修改数据转换逻辑,策略、回测等上层代码无需改动;
  2. 新增一类交易市场,仅补充数据源配置,无需大规模重构现有业务逻辑。

此前项目中,基于这套抽象架构,完成港股、美股行情新增接入仅耗费半天调试时间。研究开发中建议遵循一条原则:策略与回测逻辑,不要直接绑定第三方 API 原始 JSON 结构。

实战代码:单进程多路 WebSocket 行情接收原型

以下 Python 代码为研究原型,单进程维护多组独立 WebSocket 连接,不同市场链路相互隔离,单条连接故障不会干扰其余行情数据流,可用于多资产策略原型的数据接收环节。

python

运行

import asyncio
import json
import os
import uuid
from dataclasses import dataclass

import websockets

BASE = "wss://quote.alltick.co"

FEEDS = {
    "multi": {  # 外汇、贵金属、加密货币
        "path": "/quote-b-ws-api",
        "codes": ["EURUSD", "GOLD", "BTCUSDT"],
    },
    "stock": {  # 美股、港股、A股
        "path": "/quote-stock-b-ws-api",
        "codes": ["AAPL.US", "700.HK", "600519.SH"],
    },
}

heartbeat = {"cmd_id": 22000, "seq_id": 1, "trace": "heartbeat", "data": {}}


@dataclass
class Quote:
    feed: str
    code: str
    price: float
    volume: float
    ts_ms: int


def normalize(feed, tick):
    """将外部原始tick归一化为内部Quote模型,供策略、回测模块调用"""
    return Quote(
        feed=feed,
        code=tick["code"],
        price=float(tick["price"]),
        volume=float(tick["volume"]),
        ts_ms=int(tick["tick_time"]),
    )


async def run_feed(name, cfg, queue):
    token = "填写申请获取的AllTick_token"
    uri = f"{BASE}{cfg['path']}?token={token}"
    subscribe = {
        "cmd_id": 22004,
        "seq_id": 1,
        "trace": str(uuid.uuid4()),
        "data": {"symbol_list": [{"code": c} for c in cfg["codes"]]},
    }
    while True:  # 断线自动重连
        try:
            async with websockets.connect(uri) as ws:
                await ws.send(json.dumps(subscribe))

                async def beat():
                    while True:
                        await asyncio.sleep(10)
                        await ws.send(json.dumps(heartbeat))

                task = asyncio.create_task(beat())
                try:
                    async for raw in ws:
                        msg = json.loads(raw)
                        if msg.get("cmd_id") == 22998:
                            await queue.put(normalize(name, msg["data"]))
                finally:
                    task.cancel()
        except (websockets.ConnectionClosed, OSError):
            await asyncio.sleep(3)


async def consumer(queue):
    while True:
        q = await queue.get()
        # 此处可扩展:写入数据库、送入策略计算模块、回测预处理
        print(q.feed, q.code, q.price, q.ts_ms)


async def main():
    queue = asyncio.Queue()
    await asyncio.gather(
        consumer(queue),
        *[run_feed(n, c, queue) for n, c in FEEDS.items()],
    )


asyncio.run(main())

原型开发注意点

  1. 各市场维持独立 WebSocket 连接,分别处理订阅、心跳、断线重连;解析后的数据推送至公共异步队列统一消费;
  2. 同一会话下重复发送订阅指令会覆盖原有标的列表,动态增减品种时,需要提交完整标的集合;
  3. 接入新数据源务必校验原始报文的时间戳单位,时间单位错误会直接造成回测时序错乱。

归一化完成后仍需要处理的工程要点

完成内部模型转换,不等于数据链路完全就绪,还有若干会影响回测可靠性的细节需要处理:

  1. 分市场实现行情异常判断逻辑

    不要使用全局统一超时阈值。加密货币监控阈值可适当缩短;外汇、股票需要结合交易日历进行判断。外汇周末无推送属于正常休市,不应判定为数据故障。

  2. 规避浮点数运算精度问题

    外汇报价多为 5 位小数,A 股为 2 位,加密货币最高可达 8 位小数。涉及净值、盈亏等金额相关计算,建议使用Decimal类型运算,避免浮点数累积误差,防止回测结果出现偏移。

  3. 历史 K 线数据同样执行归一化

    实时行情一般通过 WebSocket 获取,历史 K 线大多依赖 REST 接口,两套接口返回结构往往不一致。历史 K 线获取之后,同样转换为Quote内部模型再入库,实现回测历史数据、实盘实时数据使用同一套对象,减少实盘与回测的逻辑割裂。

总结

多资产策略研究中,跨市场行情接入不存在捷径。在数据源选型阶段优先选择协议规范、文档完整的 API;架构层面通过适配层隔离外部接口差异,保障回测、策略模块的逻辑稳定性。

开展多资产研究项目时,优先定义统一的内部行情数据模型,能够有效降低后续迭代、数据源切换带来的改造成本,提升回测结果与实盘运行的一致性。

评论