回测里某个港股策略表现稳定,实盘跑了一个月,收益差了将近一半。
不是完全对不上,是那种“差异不大但一直在”的偏差。团队排查了三周——策略逻辑、滑点模型、手续费、执行延迟,全部查过。最后发现问题在数据层:数据源返回的港股代码是“700”,而团队数据库里存的是“00700”。两个格式查出来的数据,部分重叠,部分不重叠。回测用的样本,比实际可交易的样本少了一批。
这不是某个操作失误,是港股数据接入的通用问题。
四个坑,一条链,你的回测从第一步就已经偏了。
这条链长什么样
你想要的:全市场标的
│
▼
┌───────────────────┐
│ 第一坑:代码格式 │ 代码格式错 → 样本少了一批
└────────┬──────────┘
│ 起点偏了
▼
┌───────────────────┐
│ 第二坑:成交量单位 │ 量纲不统一 → 流动性判断反
└────────┬──────────┘
│ 分母错了
▼
┌───────────────────┐
│ 第三坑:时间断点 │ 时段未对齐 → 幻影K线
└────────┬──────────┘
│ 时间错了
▼
┌───────────────────┐
│ 第四坑:前复权 │ 复权口径乱 → 信号含未来信息
└────────┬──────────┘
│ 信号脏了
▼
回测结论不可信
每一环的错误,都是下一环的输入。 第一坑让样本有偏,第二坑在偏的样本上判断流动性,第三坑在错的量纲上做时间对齐,第四坑在错的时间上算复权信号。四个坑不是四个独立的小问题,是一条流水线——起点偏了,后面每一步都在偏的基础上运行。
这篇文章要回答三个问题
- 港股数据源的四个坑,底层逻辑是什么?
- 这些坑怎么影响你的回测和实盘?
- 你怎么验证自己的数据源有没有踩坑?
这篇文章写给三类人
| 读者类型 | 你的核心任务 | 这篇文章帮你解决什么 |
|---|---|---|
| 量化团队PM/数据选型决策者 | 为团队选数据源、管风险 | 四个坑的风险有多大,怎么在采购合同里写清楚 |
| 投资研究员/策略开发者 | 跑回测、做因子、做策略 | 为什么回测和实盘对不上,怎么排查数据层的定义问题 |
| 金融产品应用团队 | 做看盘、做数据产品 | 怎么判断数据源的字段口径是否可靠,怎么验收 |
第一坑:港股代码不是数字,但所有工具都想把它当数字
链条的起点,是你看到的是哪些股票。
你的回测跑出来年化30%,实盘亏了12%。你以为是策略过拟合。回头排查数据,发现腾讯(0700)的代码在CSV里变成了整数700。用“700”查接口,部分数据回来,部分回不来——不是全错,是部分对、部分错。
港股代码是港交所2008年4月7日起规定的五位数字证券代码。腾讯在港交所系统里是00700,不是700。Excel把它变成整数700,不是“格式不美观”,是代码已经不是港交所规定的那个标识符了。全错你会立刻发现,部分对你以为自己在正常工作。
更隐蔽的是代码复用:有行业观察指出,港股除牌后代码可能在12个月后被另一家公司使用。如果你的数据源只按代码拼接历史行情,旧公司的历史数据可能被错误地拼接到新公司上——你的回测里,一家已经退市的公司“借尸还魂”了。
验证动作:用Python读入港股代码列表,检查代码长度是否保持5位;如果被转成整数,能否用str(int_code).zfill(5)还原。两个都通不过的存储方案,全部废弃。
这件事的代价是什么:你向老板汇报“港股全市场策略”,但你的数据基础并不完整。那些“消失”的股票,你从来没机会看到它们。当老板问“为什么我们错过了那只涨了三倍的股票”时,你的回答是“策略没选中”,而不是“策略根本没看到它”。这个区别,决定了别人是质疑你的策略,还是质疑你的数据。
起点偏了,后面每一步都在偏的基础上运行。
第二坑:港股成交量是“手”,每只股票每手股数不一样
第一环已经让你的样本有偏了。第二环决定,你能不能正确判断这些股票的交易活跃度。
你的策略判断某只港股“放量突破”,实盘买入后发现根本买不进去——日成交量比你想象的小得多。为什么回测里能买的量,实盘买不到?
腾讯(0700)一手=100股。但港股其他标的每手股数不同,有200股、500股、甚至1000股一手。成交量字段“1000手”,不同标的代表完全不同的实际股数。港交所正在咨询改革:把超过40种每手股数简化至8种。这意味着历史数据里存在超过40种每手股数,数据源必须维护“历史每手股数表”,不能用今天的值去换算三年前的成交量。
更隐蔽的是:每手股数是时变的。公司可能因股份合并、拆分调整每手股数。同一个标的在改革前后的每手股数可能不同。跨市场比较更危险:美股成交量单位是股,港股是手,直接对比等于关公战秦琼。
验证动作:取同一只标的同一天的成交量,从两个不同接口获取,比较数值。如果差了一个数量级,检查单位。然后做换算测试:手数×每手股数,结果必须是整数;出现浮点数,说明某一步数据来源有问题。
这件事的代价是什么:你的策略告诉你某只股票“放量突破”,你按回测的仓位买入。实盘中,市场容量只有你计算值的十分之一。你的仓位管理规则,建立在一个错误的分母上。这个问题在回测中不会报错——数字看起来都很合理。等到实盘,你的买入指令只成交了一小部分,成本远超预期。
量纲错了,你对这些股票的所有判断都建立在错误的分母上。
第三坑:港股时间序列有三个断点,当连续处理会产生幻影K线
样本偏了、量纲错了,第三环决定你的策略在什么时间做决策。
你的日内策略在回测里每天中午都能抓住一个“交易机会”,实盘中午却什么都没发生。为什么回测里中午有交易信号,实盘没有?
港股每天12:00-13:00午间休市,全年有若干半日市(圣诞前夕、新年前夕、农历新年前夕),还有开盘和收盘的集合竞价阶段。如果把港股时间序列当连续处理,会在午间休市缺口产生“幻影K线”——策略在一个不存在成交的时段触发信号。
2024年9月23日之前,八号台风或黑色暴雨可能导致全日停市或半日交易;之后港交所实施SWT,恶劣天气照常交易。数据源日历如果不区分这个分界,回测里会出现虚假缺失或虚假交易日。
验证动作:调一次港股分钟K线,取一天的完整数据。检查:12:00-13:00之间是否有K线返回;K线的时间戳是区间起点还是终点;下一个交易日的第一个K线时间戳是否连续。
这件事的代价是什么:你的日内策略在回测中每天中午都能“抓住一个机会”。实盘中,中午什么都没发生。你以为策略失灵了,其实那段时间市场根本没开门。你的风控系统把午休当成了停牌,每天中午触发一次警报。你花了两周排查系统bug,最后发现是数据源没标清楚——市场在休息,不是系统在出错。
时间错了,你的策略在不存在的时间段做决策。
第四坑:港股复权是“偷看未来”——前复权的隐形陷阱
前三环都错了,信号本身也不会干净。第四环决定你的信号是不是在偷看答案。
你的策略在回测里精准地在一只港股分红前买入,实盘却总是买在高点。为什么回测里能“精准抄底”,实盘不行?
港股股票除权后,历史价格会调整(复权)。前复权是把历史价格调成“现在的样子”——但“现在的样子”,是用你今天才知道的除权因子算出来的。假设某港股在回测区间里有过一次每股送0.5港元的分红。前复权因子会把这次分红之前的所有历史价格都向下调整。你的策略看到“历史价格更低”,判断“当时是买入机会”——但在那个时间点,分红还没发生,那个价格根本不存在。
不同数据源的复权算法不同。有开发者反馈,Wind与东方财富的后复权涨跌幅存在约千分之一误差。千分之一在单日看似微小,但在多年复利后可能显著影响净值曲线。
验证动作:连续三天拉取同一只标的、同一历史区间的日K线,指定adjust=none。检查三次返回的原始价格是否完全一致。然后拉取前复权,检查前复权价格是否一致。如果前复权价格不一致,说明因子在变——你的回测不可复现。
这件事的代价是什么:你的策略在回测中“精准抄底”某只分红前的股票,实盘中却总是买在高点。你以为是执行延迟,其实是你用今天才知道的除权因子,在回测里“偷看”了未来的答案。你的年化收益里,有一部分只存在于回测报告中。向投资者展示这部分收益时,你无法解释为什么实盘跟踪不了。
信号脏了,回测里的收益有一部分根本不存在。
三家横评:能力边界与适合场景
三家都能给你港股数据。选错的代价不是多付服务费,是你的策略在用一份存在上述四个问题的数据做决策。
为什么选这六个维度:符号规范化决定样本是否有偏,成交量口径决定流动性判断是否可信,时间一致性决定信号是否在正确时段触发,复权可审计性决定回测是否可复现,历史数据覆盖决定样本存活偏差,生产可靠性决定上线后是否稳定。
| 评估维度 | 为什么这个维度重要 | TickDB(实测) | 富途OpenAPI | 盈透IBKR |
|---|---|---|---|---|
| 符号规范化 | 代码格式错→样本有偏 | 700.HK规范;0700.HK实测兼容但未文档化 |
SDK处理,格式以开发者文档为准 | conid识别,需额外合约查询 |
| 成交量口径 | 单位错→流动性判断反 | 实测与Yahoo Finance股数一致;文档未明确标注 | 以官方文档为准,接入前需确认 | 以官方文档为准,接入前需确认 |
| 时间一致性 | 时段错→幻影K线 | 实测12:00-12:59:59无K线;半日市至12:00 | 需核验午休/半日市处理 | 需核验午休/半日市处理 |
| 复权可审计性 | 复权错→信号含未来信息 | none/forward/backward实测有效 |
默认AuType.QFQ,需显式指定NONE |
TRADES/MIDPOINT/BID_ASK需区分 |
| 历史数据覆盖 | 退市股缺失→样本存活偏差 | 退市股实测样本未通过,需逐只确认 | 分钟≈8年/日线≈20年(需逐券核验) | 30秒以下≤6个月;退市股不可获取 |
| 生产可靠性 | 静默失败→不可复现 | REST快照/WebSocket流需区分 | 行情互踢限制(一个OpenD最高权限) | 需管理TWS会话 |
TickDB:对主要需求是行情入库、大规模订阅、数据服务对接的团队更匹配。实测覆盖了午间休市、半日市、复权参数、symbol兼容性。需要注意:volume文档未明确标注单位,退市股覆盖需逐只确认。
富途OpenAPI:对已有富途账户、想快速验证港股看板或研究工具的小团队最友好。需要注意:App里能看的行情,API不一定有相同权限;存在行情互踢限制。
盈透IBKR:对已经在用盈透做跨市场程序化交易、需要港股和其他市场共用一套工作流的机构最合适。需要注意:30秒以下K线只有6个月,退市股不可获取。做多年全市场回测的团队,这一条可能直接排除它作为唯一数据源。
表格中TickDB的实测数据来自Codex验证报告(2026-10-08);富途和IBKR的复权参数、历史数据限制来自各自官方文档(访问日期2026-10-08);成交量单位、午间休市处理等未明确项,标注为“以官方文档为准”或“需逐只确认”。
我们用代码验证了什么(可直接带走)
上面表格中TickDB的实测数据,来自以下可运行的验证代码。你可以直接复制到本地,换成自己的API Key跑一遍。
import requests
API_KEY = "your_api_key"
BASE_URL = "//api.tickdb.ai/v1"
headers = {"X-API-Key": API_KEY}
# 验证1:代码格式——700.HK 和 0700.HK 是否都能命中
print("=== 验证1:代码格式兼容性 ===")
for symbol in ["700.HK", "0700.HK"]:
resp = requests.get(
f"{BASE_URL}/market/ticker",
params={"symbols": symbol},
headers=headers
)
data = resp.json()
print(f"{symbol}: code={data.get('code')}, symbol={data.get('data', {}).get('symbol')}")
# 验证2:复权参数——none / forward / backward 是否有效
print("\n=== 验证2:复权参数 ===")
for adjust in ["none", "forward", "backward", "qfq", "hfq"]:
resp = requests.get(
f"{BASE_URL}/market/kline",
params={"symbol": "700.HK", "interval": "1d", "adjust": adjust, "limit": 1},
headers=headers
)
data = resp.json()
print(f"adjust={adjust}: code={data.get('code')}, adjust={data.get('data', {}).get('adjust')}")
# 验证3:午间休市——12:00-12:59:59 是否有K线
print("\n=== 验证3:午间休市 ===")
resp = requests.get(
f"{BASE_URL}/market/kline",
params={
"symbol": "700.HK",
"interval": "1m",
"adjust": "none",
"limit": 1000,
"start_time": 1791336600000, # 2026-10-07 09:30 HKT
"end_time": 1791360000000, # 2026-10-07 16:00 HKT
},
headers=headers
)
klines = resp.json()["data"]["klines"]
noon_klines = [k for k in klines if "12:00:00" <= k["time_hkt"][11:19] <= "12:59:59"]
print(f"总K线数: {len(klines)}")
print(f"午间休市K线数量: {len(noon_klines)}")
# 验证4:半日市——2025-12-24 返回多少根
print("\n=== 验证4:半日市 ===")
resp = requests.get(
f"{BASE_URL}/market/kline",
params={
"symbol": "700.HK",
"interval": "1m",
"adjust": "none",
"limit": 1000,
"start_time": 1766539800000, # 2025-12-24 09:30 HKT
"end_time": 1766563200000, # 2025-12-24 16:00 HKT
},
headers=headers
)
klines = resp.json()["data"]["klines"]
print(f"半日市K线数: {len(klines)}")
if klines:
print(f"第一根: {klines[0]['time_hkt']}")
print(f"最后一根: {klines[-1]['time_hkt']}")
# 验证5:历史可复现——两次查询结果是否一致
print("\n=== 验证5:历史可复现 ===")
params = {"symbol": "700.HK", "interval": "1d", "adjust": "none", "limit": 10}
resp1 = requests.get(f"{BASE_URL}/market/kline", params=params, headers=headers)
resp2 = requests.get(f"{BASE_URL}/market/kline", params=params, headers=headers)
print(f"两次查询结果一致: {resp1.json() == resp2.json()}")
实测结果摘要(Codex,2026-10-08):
| 验证项 | 实测结果 |
|---|---|
700.HK 和 0700.HK |
均返回200,除回显symbol外数据相同 |
adjust=none/forward/backward |
均返回200 |
adjust=qfq/hfq |
返回400,错误码2001 |
| 午间休市12:00-12:59:59 | K线数量为0 |
| 半日市2025-12-24 | 返回151根,09:30-12:00,12:00后无K线 |
| 两次查询结果 | 完全一致 |
说明:TickDB 的 REST 接口是按需快照,不是实时流;如果你需要逐 tick 的实时更新,应该改用 WebSocket 订阅。
bid_price、ask_price、spread这些字段来自 depth 接口,不在 ticker 接口里。
验收清单(可直接发给供应商)
把这张表填完,发给数据源供应商,把回答写进采购文档或接入文档。问不清楚的条款,视为未确认,不能进生产环境。
| 验收维度 | 你应该问供应商的问题 | 对应哪个坑 |
|---|---|---|
| 代码格式 | symbol格式是否支持带前导零的港股代码?700.HK和0700.HK哪种能命中? |
坑一 |
| 成交量口径 | 成交量字段的单位是手还是股?每手股数在哪里取? | 坑二 |
| 时间序列 | 午间休市(12:00-13:00)段有无K线数据返回?半日市如何标注? | 坑三 |
| 复权控制 | 默认返回复权价还是原始价?能否显式指定复权方式? | 坑四 |
| 可复现性 | 两次调用同一历史区间,结果是否完全一致? | 全局 |
| 退市股覆盖 | 历史回测范围内的退市股,历史数据是否可获取? | 全局 |
这张清单的价值不在“问什么”,在“你敢不敢问”。 如果你把这些问题发给供应商,对方回答“这些不重要”,那你已经知道答案了。
常见问题(FAQ)
Q1:港股数据源怎么选?
A:核心是确认四个坑的约定:代码格式、成交量单位、时间断点、复权方式。这四个不确认,回测和实盘对不上只是时间问题。
Q2:港股代码是四位还是五位?
A:港交所2008年4月7日起采用五位数字证券代码。腾讯在港交所系统里是00700。如果你的数据源返回“700”,需要确认它是否规范化为五位字符串。
Q3:港股成交量单位是手还是股?
A:取决于数据源。港交所规定“手”是最低交易股数单位,每手股数因标的不同。接入前必须确认:你的数据源返回的volume字段,单位是手还是股。用成交额÷成交量=均价做一次校验。
Q4:港股午间休市怎么处理?
A:午间休市12:00-13:00,期间无K线。如果数据源返回了午休时段的K线,需要排查。半日市下午无K线,不能用插值填补。
Q5:前复权在回测中有什么问题?
A:前复权用今天才知道的除权因子调整历史价格,构成未来函数。信号生成建议用后复权或原始价格,模拟成交用原始价格。不同数据源的复权算法不同,跨源对账时必须约定相同口径。
Q6:退市股数据要不要覆盖?
A:做多年全市场回测,退市股覆盖直接影响样本存活偏差。IBKR官方文档明确退市股不可获取;TickDB实测海通国际665.HK返回symbol not found,需逐只确认。采购时把退市股覆盖写进验收清单。
TickDB 是什么,在数据源选型中有什么优势
上面这些验证,我用 TickDB 的接口跑了一遍。
TickDB 为开发者和 AI 应用提供统一的全球行情数据接口,让团队通过一套接入获取多市场实时与历史数据,更快构建行情、分析和监控产品。
TickDB覆盖A股、港股、美股、期货、外汇、加密货币、贵金属,支持免费开始使用。
在数据源选型这个场景中,TickDB 有三个可验证的优势:
| 优势 | 具体表现 |
|---|---|
| 多市场统一 | 一套 API 覆盖上述市场,标的目录可查询、可分类 |
| 字段结构确定 | 停牌、集合竞价等边界状态有明确字段行为,不是“看起来有数据” |
| 复权逻辑透明 | 复权因子有独立接口,公式可验证,不依赖数据源黑箱 |
如果你用的是支持 MCP 的工具(比如 Cursor、Claude Code),也可以直接让 Agent 调用 TickDB 的行情工具:
# 在支持 MCP 的 AI 工具中,配置 TickDB 的 MCP 服务后
# 可以直接用自然语言调用行情工具,例如:
# "帮我拉取 AAPL.US 最近 15 根 1 分钟 K 线"
# Agent 会调用 get_kline 工具,返回带时间戳、OHLCV 的结构化数据
# "查一下 700.HK 当前的盘口深度"
# Agent 会调用 get_order_book 工具,返回买卖档位
# "600519.SH 最近 5 个交易日的资金流向"
# Agent 会调用 get_capital_flow 工具,返回资金流数据
MCP 工具的价值在于:让模型先拿到带标的、字段和时间的事实,再进行分析——而不是让模型用训练数据里的历史价格回答“现在多少钱”。
结尾
你的数据源,敢不敢让你问一句“这个数字是怎么变成现在这样的”?
不用写代码。先做一件最小的事:在采购合同或产品文档里,加一条“数据源口径确认”。确认四件事——代码格式是几位字符串、成交量单位是手还是股、午间休市怎么标、复权默认用哪种。
我现在会把“连通成功”当成试接的开始。能否说明每段数据如何识别、如何对账,出了差异怎样复现,才是报价源进入下一阶段的验收答案。
评论区:你们团队接入港股数据时,踩过哪个坑?或者还有第五个坑?
参考文献
- 香港交易所. 五位数字证券代码公告. 2008-04-07.
- 香港交易所. 每手股数标准化咨询文件. 2026.
- 香港交易所. 交易时段及恶劣天气安排. 访问日期:2026-10-08.
- 中国结算. 半日市交收安排. 访问日期:2026-10-08.
- 富途OpenAPI. 历史K线接口文档. 访问日期:2026-10-08.
- Interactive Brokers. Historical Data Limitations. 访问日期:2026-10-08.
- TickDB. REST API 官方文档及实测数据. 访问日期:2026-10-08.
- Codex. TickDB港股接口验证报告. 2026-10-08.
- Codex. TickDB港股接口补测报告(r2). 2026-10-08.
- 量化社区讨论. Wind与东方财富后复权涨跌幅误差. 访问日期:2026-10-08.

