A股和港股一起做回测,收益变高了?先检查这件事
作者: TickDB Research · 发布: 2026/7/8 · 阅读: 4
标签: QL-OLD-003, 知乎A001
做 A 股和港股的跨市场回测,策略没改,回测曲线却更好看了。多出来的那截平滑收益,可能不是策略选股能力强,而是交易日历、join 方式和填充规则替你做了决定。本文用 TickDB 实际拉取一段 A/H 交易日历和日 K 数据,拆解一套五步排查框架,帮你把回测里那些“数据替你做的决定”一个一个找出来。
如果你自己跑过 A 股和港股的跨市场回测,你可能经历过这种困惑。同一个策略逻辑,优化了参数、换了样本池、调整了因子权重,曲线变化不大。但你改了一个不起眼的数据处理选项:从“只保留两地都有交易的日期”改成“用上一个交易日的价格填充休市日”。曲线立刻变好看了。
这不一定是你找到了新的 Alpha,也可能只是你改了一个填充规则。
如果你正在维护团队的回测数据管道,你的同事可能正在用完全不同的交易日历对齐方式跑同一个策略。他跑出来的夏普比率比你高出一截,不一定是因为参数更好,也可能是 join 方式、缺失值处理和交易日历标记不一样。这种差异常常藏在“数据预处理”步骤里,而这一步在交接文档里往往只有一句话:价格数据已对齐。
这篇文章不讲策略,只讲一件事:跨市场回测前,先确认数据到底是怎么对齐的。
一、先用 TickDB 实测一段交易日历
为了避免只讲概念,我用 TickDB 实际拉了一段日期:2026-02-10 到 2026-02-28。选择这个窗口,是因为 TickDB 返回结果里能直接看到两地交易节奏错位:部分日期 HK 已开市,而 CN 仍未开市。
调用的是 TickDB 的交易日历接口:
GET https://api.tickdb.ai/v1/market/trade-daysmarket=CNmarket=HKbeg_day=20260210end_day=20260228
本次实测结果如下:
| 项目 | CN | HK |
|---|---|---|
| 开市日数量 | 8 | 11 |
| 单边开市日数量 | 0 | 3 |
| 半日市 | 无 | 2026-02-16 |
HK-only 日期是:
2026-02-162026-02-202026-02-23
也就是说,在这个样本窗口里,如果你把 A 股和港股放在一张回测表里,交易日历已经不是天然一致的。 你后面选择 inner join、outer join、dropna,还是 forward fill,都会改变回测样本。
再看成一张图会更直观:
图下说明:这张图只展示本次实测窗口里的交易日历差异,不外推全年,也不替代你的回测区间核算。
这组数据不能推出“每一年一定差多少天”。它只能证明一件更实际的事:跨市场回测前,不能默认两地交易日历一致。 具体差异要按你回测的年份、市场和样本窗口重新拉取。
二、实际调用代码:先对齐交易日历,再进入回测
下面这段代码是本次实测使用的核心逻辑。它直接调用 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-10 到 2026-02-28:
- CN 开市日:8 天
- HK 开市日:11 天
- HK-only:3 天
- HK 半日市:
2026-02-16
在日历上找差异,比在价格数据里找异常快得多。
第 2 步:标记,不要直接删除
单边交易日最容易的处理是删掉。但删除同时会删掉那一侧市场独有的信息。更稳妥的做法是标记:
cn_onlyhk_onlybothhalf_day
以后任何时候复查回测结果,你都能追溯到原始属性。半年后有人问你这几天的数据去哪了,标记就是证据。
第 3 步:检查 join 方式在替你假设什么
三种 join 路径,每一条都在替策略做不同的假设。
| 处理方式 | 隐含假设 | 主要风险 |
|---|---|---|
| inner join | 策略只在两地同时开市的日期运行 | 单边交易日被系统性丢弃 |
| outer join + 删除含 NaN 行 | 缺失行不参与回测 | 以“清理缺失值”的名义完成样本截断 |
| outer join + 前向填充 | 休市期间沿用上一交易日状态 | 把无新成交的填充值混入普通交易日统计 |
每一种 join 方式都在替你做一个关于“策略何时持有、何时空仓、何时不更新价格”的假设。这个假设应该由你来做,不应该由数据处理的默认选项来做。
图下说明:三条路径没有绝对优劣,关键是把隐含假设写清楚,并在回测指标里分区间复核。
第 4 步:确认“同一日期”在两个市场是否真的可比
同一天能不能直接比较,不是只看日期标签一样不一样,还要看交易时段、时间戳语义和市场状态是否可比。
本次我也调用了 TickDB 的交易时段接口:
GET https://api.tickdb.ai/v1/market/trading-sessions?symbol=600519.SHGET https://api.tickdb.ai/v1/market/trading-sessions?symbol=700.HK
接口返回的市场交易时段中,CN 包含 09:30-11:30、13:00-14:57,HK 包含 09:30-12:00、13:00-16:00。
这里要注意两点:
第一,交易时段和交易日历是两类信息。半日市要用交易日历里的 half_trade_days 或交易所日历确认,不能只靠常规交易时段推断。
第二,日 K 接口返回的是时间戳。跨市场对齐时,不要把 UTC 日期、本地交易日和回测标签混成一件事。 做 A/H 回测时,交易日历判断应以市场日历为准,再进入价格序列对齐。
第 5 步:分区间看指标,不要只看全区间汇总
把回测时间段至少切成三块:
- 两地重叠交易日
- CN-only 日期
- HK-only 日期
分别看收益、回撤、换手、信号触发次数。如果全区间表现主要来自单边交易日或填充区间,就要回头检查数据处理逻辑。
不要只用全区间汇总来判断策略好不好。分区间看,数据处理留下的痕迹才会浮出来。
五、这套排查框架在真实数据里的位置
用真实 API 做这件事时,我会把流程拆成四步:
图下说明:真实数据进入回测前,至少把请求参数、返回日期、差集列表、填充来源和分区间指标留下来。
第一步,先用交易日历接口拿到 CN/HK 的开市日和半日市。
第二步,再用 K 线接口拉取具体标的的价格序列。本次我用 600519.SH 和 700.HK 做了日 K 拉取验证。
第三步,把交易日历标签写入价格数据,而不是直接让价格表自己决定 join 方式。
第四步,记录每一次删除、填充和保留的原因。
这样做的好处是,回测结论不再只是一条曲线,而是一条可以追溯的数据路径:哪一天是两地都开市,哪一天只有一边开市,哪一天是半日市,哪一行价格来自真实返回,哪一行来自填充规则。
六、TickDB 在这套框架里的位置
TickDB 在这里不是替你决定回测规则,而是提供统一的数据入口,让你能把规则说清楚、跑出来、记录下来。
本次实测中,TickDB 提供了三类信息:
| 接口 | 本文用途 |
|---|---|
/v1/market/trade-days | 拉取 CN/HK 交易日和半日市,识别单边交易日 |
/v1/market/kline | 拉取 600519.SH 与 700.HK 的日 K 数据,验证价格序列可获取 |
/v1/market/trading-sessions | 核验不同市场常规交易时段并提醒时间语义差异 |
但接口把数据给你,不等于替你完成回测假设。交易日历的差异、单边交易日的标记、join 方式的选择、fill 规则的记录,仍然是研究者自己要完成的工作。数据源可以帮你把数据拉进来,但不能替你判断每一行该保留、删除还是填充。
七、五步检查卡
| 步骤 | 检查项 | 通过标准 |
|---|---|---|
| 1 | 两地交易日历是否分别获取 | 目标窗口内 CN/HK 开市日、半日市已拉取 |
| 2 | 单边交易日是否标记 | 每个交易日都有 both、cn_only、hk_only 或 half_day 标签 |
| 3 | join 方式是否明确 | inner、outer、dropna、forward fill 的选择可追溯 |
| 4 | timestamp 语义是否确认 | UTC 时间戳、本地交易日、回测日期标签不混用 |
| 5 | 回测指标是否分区间计算 | 重叠区间和单边区间分别统计 |
八、FAQ
Q1:是不是不用前向填充,问题就解决了?
不完全是。前向填充只是最容易发现的一种处理方式。关键不是找到“唯一正确”的填充方法,而是知道你的填充方法做了什么假设,并记录在回测文档里。
Q2:inner join 和 outer join 加删除 NaN 行结果一样,为什么还要区分?
因为可追溯性不同。主动选择 inner join 的人知道自己在丢弃数据。被动清理缺失值的人可能在三个月后完全忘了这一步,找不到任何丢弃记录。
Q3:TickDB 能自动处理交易日历对齐吗?
不能。TickDB 可以提供交易日历、K 线和交易时段等结构化数据,但交易日历差异判断、join 方式选择、fill 规则记录,仍然需要研究者主动完成。
Q4:本文这次实测能证明所有年份的 A/H 交易日历差异吗?
不能。本文只证明 2026-02-10 到 2026-02-28 这个样本窗口存在差异。真正回测时,要按你的回测区间重新拉取交易日历。
你跑跨市场回测时,是先对齐交易日历再算收益,还是先跑出曲线再回头看日历差异?
下次看到一条突然变平滑的跨市场回测曲线,可以先问一句:这真的是策略变好了,还是数据处理替我做了决定?
本文内容仅为跨市场回测中交易日历错位问题的数据质量讨论,不构成任何投资建议。文中 TickDB 调用基于 2026-07-08 的实际接口返回结果;不同日期、不同市场、不同标的的返回结果需以实际调用为准。
通过 TickDB API 获取实时行情数据
一个 API 接入外汇、加密货币、美股、港股、A股、贵金属和全球指数的实时行情。支持 WebSocket 低延迟推送,免费开始使用。
免费领取 API Key查看 API 文档