综合

A股和港股一起做回测,收益变高了?先检查这件事

作者: TickDB Research · 发布: 2026/7/8 · 阅读: 4

标签: QL-OLD-003, 知乎A001

做 A 股和港股的跨市场回测,策略没改,回测曲线却更好看了。多出来的那截平滑收益,可能不是策略选股能力强,而是交易日历、join 方式和填充规则替你做了决定。本文用 TickDB 实际拉取一段 A/H 交易日历和日 K 数据,拆解一套五步排查框架,帮你把回测里那些“数据替你做的决定”一个一个找出来。


如果你自己跑过 A 股和港股的跨市场回测,你可能经历过这种困惑。同一个策略逻辑,优化了参数、换了样本池、调整了因子权重,曲线变化不大。但你改了一个不起眼的数据处理选项:从“只保留两地都有交易的日期”改成“用上一个交易日的价格填充休市日”。曲线立刻变好看了。

这不一定是你找到了新的 Alpha,也可能只是你改了一个填充规则。

如果你正在维护团队的回测数据管道,你的同事可能正在用完全不同的交易日历对齐方式跑同一个策略。他跑出来的夏普比率比你高出一截,不一定是因为参数更好,也可能是 join 方式、缺失值处理和交易日历标记不一样。这种差异常常藏在“数据预处理”步骤里,而这一步在交接文档里往往只有一句话:价格数据已对齐。

这篇文章不讲策略,只讲一件事:跨市场回测前,先确认数据到底是怎么对齐的。


一、先用 TickDB 实测一段交易日历

为了避免只讲概念,我用 TickDB 实际拉了一段日期:2026-02-102026-02-28。选择这个窗口,是因为 TickDB 返回结果里能直接看到两地交易节奏错位:部分日期 HK 已开市,而 CN 仍未开市。

调用的是 TickDB 的交易日历接口:

  • GET https://api.tickdb.ai/v1/market/trade-days
  • market=CN
  • market=HK
  • beg_day=20260210
  • end_day=20260228

本次实测结果如下:

项目CNHK
开市日数量811
单边开市日数量03
半日市2026-02-16

HK-only 日期是:

  • 2026-02-16
  • 2026-02-20
  • 2026-02-23

也就是说,在这个样本窗口里,如果你把 A 股和港股放在一张回测表里,交易日历已经不是天然一致的。 你后面选择 inner join、outer join、dropna,还是 forward fill,都会改变回测样本。

!image.png

再看成一张图会更直观:

!image.png

图下说明:这张图只展示本次实测窗口里的交易日历差异,不外推全年,也不替代你的回测区间核算。

这组数据不能推出“每一年一定差多少天”。它只能证明一件更实际的事:跨市场回测前,不能默认两地交易日历一致。 具体差异要按你回测的年份、市场和样本窗口重新拉取。


二、实际调用代码:先对齐交易日历,再进入回测

下面这段代码是本次实测使用的核心逻辑。它直接调用 TickDB,可以在安装 requests 并配置 TICKDB_API_KEY 后运行。

import json
import os
import requests

BASE_URL = "https://api.tickdb.ai"
HEADERS = {"X-API-Key": os.environ["TICKDB_API_KEY"]}

def normalize_day(day: str) -> str:
    return f"{day[:4]}-{day[4:6]}-{day[6:8]}"

def get_trade_days(market: str) -> dict:
    response = requests.get(
        f"{BASE_URL}/v1/market/trade-days",
        headers=HEADERS,
        params={
            "market": market,
            "beg_day": "20260210",
            "end_day": "20260228",
        },
        timeout=20,
    )
    response.raise_for_status()
    payload = response.json()
    if payload.get("code") != 0:
        raise RuntimeError(payload)
    return payload["data"]

cn = get_trade_days("CN")
hk = get_trade_days("HK")

cn_open = {normalize_day(d) for d in cn["trade_days"]}
cn_open |= {normalize_day(d) for d in cn.get("half_trade_days", [])}

hk_open = {normalize_day(d) for d in hk["trade_days"]}
hk_open |= {normalize_day(d) for d in hk.get("half_trade_days", [])}

result = {
    "window": {"start": "2026-02-10", "end": "2026-02-28"},
    "cn_open_count": len(cn_open),
    "hk_open_count": len(hk_open),
    "both_dates": sorted(cn_open & hk_open),
    "cn_only_dates": sorted(cn_open - hk_open),
    "hk_only_dates": sorted(hk_open - cn_open),
    "cn_half_trade_days": [normalize_day(d) for d in cn.get("half_trade_days", [])],
    "hk_half_trade_days": [normalize_day(d) for d in hk.get("half_trade_days", [])],
}

print(json.dumps(result, ensure_ascii=False, indent=2))

输出里的关键结果是:

{
  "cn_open_count": 8,
  "hk_open_count": 11,
  "cn_only_dates": [],
  "hk_only_dates": [
    "2026-02-16",
    "2026-02-20",
    "2026-02-23"
  ],
  "hk_half_trade_days": [
    "2026-02-16"
  ]
}

这段代码真正挡在回测入口前做的事,不是“把日期凑齐”,而是把数据处理选择显性化:哪些日期是两地都开市,哪些日期只有一边开市,哪些日期是半日市。


三、为什么交易日历错位能制造“看起来很漂亮的 Alpha”

跨市场回测和单市场回测最大的区别,不是代码更复杂,而是你的数据在做决定之前,已经有人替你做了一堆决定

A 股和港股的交易日历不完全重合。具体哪一天开市、哪一天半日市、哪一天只有一边市场交易,不能靠印象判断,要按目标年份和目标市场的交易日历核算。

如果你把两地价格表做 inner join,系统会只保留两地都有交易的日期。这样做的隐含假设是:策略只在两地同时开市的日期运行。

如果你做 outer join 之后直接删除含缺失值的行,结果可能和 inner join 接近,但过程更隐蔽。你以为自己只是在“清理缺失值”,实际上可能已经把一部分单边交易日删掉了。

如果你做 outer join 再前向填充,隐含假设就变成了:休市期间持仓状态延续,价格沿用上一交易日。 这样处理有时是合理的,但它必须被记录。因为填充出来的价格不是新成交,不应该在所有统计口径里都被当成普通交易日。

交易数据本身不会说谎。但数据被填入、被截断、被归并的方式,会把一些不存在的“规律”写进回测曲线里。


四、五步排查:把数据替你做的决定一个个找出来

第 1 步:先找出哪些日子只有一边市场在交易

先分别拉出两地交易日历,找出回测窗口里哪些日子是单边交易日。先知道差异有多大,再决定怎么处理差异。

在本次 TickDB 实测窗口里,2026-02-102026-02-28

  • CN 开市日:8 天
  • HK 开市日:11 天
  • HK-only:3 天
  • HK 半日市:2026-02-16

在日历上找差异,比在价格数据里找异常快得多。

第 2 步:标记,不要直接删除

单边交易日最容易的处理是删掉。但删除同时会删掉那一侧市场独有的信息。更稳妥的做法是标记:

  • cn_only
  • hk_only
  • both
  • half_day

以后任何时候复查回测结果,你都能追溯到原始属性。半年后有人问你这几天的数据去哪了,标记就是证据。

第 3 步:检查 join 方式在替你假设什么

三种 join 路径,每一条都在替策略做不同的假设。

处理方式隐含假设主要风险
inner join策略只在两地同时开市的日期运行单边交易日被系统性丢弃
outer join + 删除含 NaN 行缺失行不参与回测以“清理缺失值”的名义完成样本截断
outer join + 前向填充休市期间沿用上一交易日状态把无新成交的填充值混入普通交易日统计

每一种 join 方式都在替你做一个关于“策略何时持有、何时空仓、何时不更新价格”的假设。这个假设应该由你来做,不应该由数据处理的默认选项来做。

!image.png

图下说明:三条路径没有绝对优劣,关键是把隐含假设写清楚,并在回测指标里分区间复核。

第 4 步:确认“同一日期”在两个市场是否真的可比

同一天能不能直接比较,不是只看日期标签一样不一样,还要看交易时段、时间戳语义和市场状态是否可比。

本次我也调用了 TickDB 的交易时段接口:

  • GET https://api.tickdb.ai/v1/market/trading-sessions?symbol=600519.SH
  • GET https://api.tickdb.ai/v1/market/trading-sessions?symbol=700.HK

接口返回的市场交易时段中,CN 包含 09:30-11:3013:00-14:57,HK 包含 09:30-12:0013:00-16:00

这里要注意两点:

第一,交易时段和交易日历是两类信息。半日市要用交易日历里的 half_trade_days 或交易所日历确认,不能只靠常规交易时段推断。

第二,日 K 接口返回的是时间戳。跨市场对齐时,不要把 UTC 日期、本地交易日和回测标签混成一件事。 做 A/H 回测时,交易日历判断应以市场日历为准,再进入价格序列对齐。

第 5 步:分区间看指标,不要只看全区间汇总

把回测时间段至少切成三块:

  • 两地重叠交易日
  • CN-only 日期
  • HK-only 日期

分别看收益、回撤、换手、信号触发次数。如果全区间表现主要来自单边交易日或填充区间,就要回头检查数据处理逻辑。

不要只用全区间汇总来判断策略好不好。分区间看,数据处理留下的痕迹才会浮出来。


五、这套排查框架在真实数据里的位置

用真实 API 做这件事时,我会把流程拆成四步:

!image.png

图下说明:真实数据进入回测前,至少把请求参数、返回日期、差集列表、填充来源和分区间指标留下来。

第一步,先用交易日历接口拿到 CN/HK 的开市日和半日市。

第二步,再用 K 线接口拉取具体标的的价格序列。本次我用 600519.SH700.HK 做了日 K 拉取验证。

第三步,把交易日历标签写入价格数据,而不是直接让价格表自己决定 join 方式。

第四步,记录每一次删除、填充和保留的原因。

这样做的好处是,回测结论不再只是一条曲线,而是一条可以追溯的数据路径:哪一天是两地都开市,哪一天只有一边开市,哪一天是半日市,哪一行价格来自真实返回,哪一行来自填充规则。


六、TickDB 在这套框架里的位置

TickDB 在这里不是替你决定回测规则,而是提供统一的数据入口,让你能把规则说清楚、跑出来、记录下来。

本次实测中,TickDB 提供了三类信息:

接口本文用途
/v1/market/trade-days拉取 CN/HK 交易日和半日市,识别单边交易日
/v1/market/kline拉取 600519.SH700.HK 的日 K 数据,验证价格序列可获取
/v1/market/trading-sessions核验不同市场常规交易时段并提醒时间语义差异

但接口把数据给你,不等于替你完成回测假设。交易日历的差异、单边交易日的标记、join 方式的选择、fill 规则的记录,仍然是研究者自己要完成的工作。数据源可以帮你把数据拉进来,但不能替你判断每一行该保留、删除还是填充。


七、五步检查卡

步骤检查项通过标准
1两地交易日历是否分别获取目标窗口内 CN/HK 开市日、半日市已拉取
2单边交易日是否标记每个交易日都有 bothcn_onlyhk_onlyhalf_day 标签
3join 方式是否明确inner、outer、dropna、forward fill 的选择可追溯
4timestamp 语义是否确认UTC 时间戳、本地交易日、回测日期标签不混用
5回测指标是否分区间计算重叠区间和单边区间分别统计

八、FAQ

Q1:是不是不用前向填充,问题就解决了?

不完全是。前向填充只是最容易发现的一种处理方式。关键不是找到“唯一正确”的填充方法,而是知道你的填充方法做了什么假设,并记录在回测文档里。

Q2:inner join 和 outer join 加删除 NaN 行结果一样,为什么还要区分?

因为可追溯性不同。主动选择 inner join 的人知道自己在丢弃数据。被动清理缺失值的人可能在三个月后完全忘了这一步,找不到任何丢弃记录。

Q3:TickDB 能自动处理交易日历对齐吗?

不能。TickDB 可以提供交易日历、K 线和交易时段等结构化数据,但交易日历差异判断、join 方式选择、fill 规则记录,仍然需要研究者主动完成。

Q4:本文这次实测能证明所有年份的 A/H 交易日历差异吗?

不能。本文只证明 2026-02-102026-02-28 这个样本窗口存在差异。真正回测时,要按你的回测区间重新拉取交易日历。


你跑跨市场回测时,是先对齐交易日历再算收益,还是先跑出曲线再回头看日历差异?

下次看到一条突然变平滑的跨市场回测曲线,可以先问一句:这真的是策略变好了,还是数据处理替我做了决定?

本文内容仅为跨市场回测中交易日历错位问题的数据质量讨论,不构成任何投资建议。文中 TickDB 调用基于 2026-07-08 的实际接口返回结果;不同日期、不同市场、不同标的的返回结果需以实际调用为准。

通过 TickDB API 获取实时行情数据

一个 API 接入外汇、加密货币、美股、港股、A股、贵金属和全球指数的实时行情。支持 WebSocket 低延迟推送,免费开始使用。

免费领取 API Key查看 API 文档

相关文章