A股和港股数据合表前,先别急着跑回测 摘要跨市场回测里很多问题不是出在策略代码而是出在数据合表之前。A 股和港股的交易日历不完全一致inner join、dropna、forward fill会把不同的数据假设写进样本里。本文用 TickDB 实测一段 CN/HK 交易日历和日 K 数据把“交易日历 - 日期标签 - join/fill - 留痕”拆成一个工程检查流程重点不是判断策略好不好而是避免数据处理默认值替你做决定。先说结论如果你要把 A 股和港股放进同一张回测表里不能只看date字段能不能对上。至少要先做四件事分别拉 CN/HK 交易日历标记both、cn_only、hk_only、half_day明确选择inner join、outer join dropna还是outer join forward fill把每一次删除、填充、保留的原因写进日志或数据表。这篇文章不讨论投资结论也不评价交易方法。它只解决一个工程问题跨市场行情数据进入回测框架之前怎样把交易日历错位和填充规则显性化一、用一段实测数据看问题我用 TickDB 拉了2026-02-10到2026-02-28的 CN/HK 交易日历。这个窗口里HK 有几个交易日 CN 没有开市。调用接口GET https://api.tickdb.ai/v1/market/trade-days参数marketCN marketHK beg_day20260210 end_day20260228实测结果项目CNHK开市日数量811单边开市日数量03半日市无2026-02-16HK-only 日期2026-02-162026-02-202026-02-23图下说明终端截图展示的是本次实测窗口里的 CN/HK 交易日历差异。API Key 已脱敏这只证明本窗口存在差异不外推所有年份。把这件事画出来更直观图下说明先分出both、cn_only、hk_only、half_day再决定怎么合表。二、一个最小可运行检查脚本安装依赖python3-mpipinstallrequests配置 API KeyexportTICKDB_API_KEY你的 API Key代码importjsonimportosimportrequests BASE_URLhttps://api.tickdb.aiHEADERS{X-API-Key:os.environ[TICKDB_API_KEY]}defnormalize_day(day:str)-str:returnf{day[:4]}-{day[4:6]}-{day[6:8]}defget_trade_days(market:str)-dict:resprequests.get(f{BASE_URL}/v1/market/trade-days,headersHEADERS,params{market:market,beg_day:20260210,end_day:20260228,},timeout20,)resp.raise_for_status()payloadresp.json()ifpayload.get(code)!0:raiseRuntimeError(payload)returnpayload[data]defopen_days(calendar:dict)-set[str]:trade_dayscalendar.get(trade_days)or[]half_trade_dayscalendar.get(half_trade_days)or[]return{normalize_day(d)fordintrade_dayshalf_trade_days}cnopen_days(get_trade_days(CN))hkopen_days(get_trade_days(HK))report{both:sorted(cnhk),cn_only:sorted(cn-hk),hk_only:sorted(hk-cn),cn_open_count:len(cn),hk_open_count:len(hk),}print(json.dumps(report,ensure_asciiFalse,indent2))这段代码的作用不是替你决定怎么对齐而是在合表前先告诉你哪些日期两边都开市哪些日期只有一边开市。如果这一步都没有做后面的join和fill就很容易变成“默认值说了算”。三、join 方式其实是在写业务假设跨市场数据合表时常见三种写法写法它在假设什么风险inner join只在两地都开市的日期运行样本单边交易日被直接丢掉outer join dropna有缺失就删看起来是清理数据实际可能是截断样本outer join forward fill休市市场沿用上一交易日状态填充值可能被当成真实交易日数据这里没有绝对正确的写法。问题在于你有没有意识到它们在替你做假设。图下说明关键不是选哪条路径而是把路径对应的假设、影响行数和填充来源写清楚。四、建议先加日期标签再合价格表很多回测表只保留价格字段date close_cn close_hk这对单市场可能还够用跨市场就太薄了。建议至少加一层交易日历标签date cn_is_open hk_is_open market_day_type is_half_day fill_source fill_reasonmarket_day_type可以是both cn_only hk_only closedfill_source可以是raw forward_fill empty这样以后排查时能直接问某段指标主要来自both日期还是来自hk_only日期某个价格是原始返回还是前向填充这几行为什么被删哪个市场在那天其实没有交易这比事后翻 pandas 合并代码靠谱得多。五、日期一样不代表交易语义一样我还用 TickDB 拉了两个标的的常规交易时段GET https://api.tickdb.ai/v1/market/trading-sessions?symbol600519.SH GET https://api.tickdb.ai/v1/market/trading-sessions?symbol700.HK本次返回里CN 包含09:30-11:30、13:00-14:57HK 包含09:30-12:00、13:00-16:00这一步不是为了用交易时段替代交易日历。恰好相反它提醒我们交易日历、交易时段、时间戳是三类信息。尤其要注意半日市要看交易日历里的half_trade_days或交易所日历不能只靠常规交易时段猜。TickDB 文档里的时间戳口径是毫秒级 UTC 时间戳跨市场对齐时不要把 UTC 日期、本地交易日、回测标签混成一个字段。六、放进数据管道时我会这样拆图下说明真实数据进入回测前先记录请求参数、返回日期、差集列表、填充来源和分区间指标。工程上可以拆成四步calendar_loader拉 CN/HK 交易日历price_loader拉600519.SH、700.HK等标的日 Kcalendar_tagger给每个日期打both/cn_only/hk_only/half_day标签alignment_logger记录 join 方式、删除行、填充行和原因。最后再把整理后的数据交给回测框架。这个顺序很重要。不要让回测框架在没有上下文的情况下直接吞两张价格表。否则它只会按你给的 DataFrame 计算不会提醒你某几行其实是填出来的。七、TickDB 在这里做什么不做什么本文用 TickDB 做三件事接口用途/v1/market/trade-days拉 CN/HK 交易日和半日市/v1/market/kline拉600519.SH和700.HK的日 K 样本/v1/market/trading-sessions核验不同市场常规交易时段TickDB 提供的是统一行情数据 API 入口让交易日历、K 线、交易时段这些数据可以进入同一套工程流程。它不替你决定用inner join还是outer join缺失值要不要填填充值能不能参与某个统计口径回测结论是否成立。这些仍然是数据工程和研究流程自己的责任。另外本次实测只证明样本窗口和样本标的在采样时刻的接口返回结果。不要外推成所有年份、所有市场、所有标的都一样也不要外推成延迟、SLA 或策略效果。最后给一张检查表检查项要留什么证据两地交易日历CN/HK 各自开市日、半日市单边交易日cn_only、hk_only列表join 方式inner、outer、dropna、forward_fill填充来源哪些行来自 raw哪些行来自 fill时间语义UTC 时间戳、本地交易日、回测标签的对应关系分区间指标both和单边日期分开统计跨市场回测里真正危险的不是代码报错。报错至少会让你停下来。更危险的是数据看起来已经对齐曲线也能跑出来但 join 和填充规则在你没注意的时候改了样本。所以在 A 股和港股数据合表前先把交易日历和填充规则写清楚。它不酷但很管用。本文只讨论跨市场回测中的交易日历、数据对齐和工程留痕方法不构成任何投资建议。文中 TickDB 调用基于 2026-07-08 的实际接口返回结果不同日期、不同市场、不同标的的返回结果需以实际调用为准。标签建议Python、后端、数据工程、量化开发、TickDB