搭量化系统的第一课不是写策略——你的 K 线可能每天都在“幽灵跳空“
先抛一个会让很多人后背发凉的工程事实——
你正在用的某个免费看盘软件,它的 K 线在每个除权日都会出现一根"幽灵跳空"。你拿这条没复权的价格序列去算涨跌、跑回测,账户明明盈利,你却算出亏损——而你完全不知道差错出在哪里。
这是量化选股系统开发公开课的第 1 课——行情数据层应该长什么样。它要回答的核心问题只有一个——为什么"接口返 200"和"数据可信"是两件完全不同的事。
一、为什么这一课必须排在第一节
量化选股系统由三大块组成——数据层、策略层、输出层。在工程上,三者的重要性看似是 1:1:1,但在真实项目里几乎是 7:2:1。
数据层是整个系统站立的地面。地面歪一寸,上层每一栋楼都跟着歪。
绝大多数初学者上手就直奔策略和回测,跳过数据层这一关,等到实盘表现远低于回测预期,才回头追问问题——而问题十有八九出在数据。
这一课不教你具体怎么写代码,而是建立一种认知——数据先于策略。你拿到的每一份数据,背后都有可能的失真路径;做策略的人,必须首先成为一个挑剔的数据消费者。
二、数据可信的"两层校验"
绝大多数初学者校验数据只看一件事——调用是否成功、是否返回了数据。这是远远不够的。数据可信由两层共同决定——
def fetch(endpoint: str, dtype: str) -> dict | None:
# ===== 第一层:网络层校验 =====
# 接口是否真的活着、有没有降级、是否在合理时延内返回
try:
data = request(endpoint, timeout=REASONABLE)
except Exception as e:
record_failure(endpoint, e)
return None
# ===== 第二层:数据层校验 =====
# 返回内容的时间戳是否最新、字段口径是否一致、有没有"静默退化"
last_ts = extract_last_timestamp(data)
if not is_fresh(last_ts, dtype):
log_fossil(endpoint, last_ts) # 化石数据,直接丢弃
return None
return data
两层校验必须并行存在——
- 只校验网络层 → 你会接收"接口活着但数据是化石"的结果
- 只校验数据层 → 你会在接口已死时仍然不停重试
三、三件大事之一:复权——把"除权日的一刀"补回去
除权除息日,会导致股价发生跳变。如果你拿没有复权的价格序列去计算涨跌、回测策略、比较收益,会出现明显的数值失真。
最常见的失真表现是——账户实际是盈利,但你算出来是亏损;或者反过来。
绝大多数免费看盘软件默认不开启自动复权,因此它们的 K 线在每个除权日都会出现"幽灵跳空"。
# ❌ 直接用原始价格比较——跨除权日时数值不可比
ret = (price[t] - price[t-1]) / price[t-1] # 除权日这里会算出一根假大跌
# ✅ 数据进入策略之前先复权,确保"任意两天价格可被等价比较"
adj_price = apply_adjust(raw_price, dividend_events, mode='backward')
ret = (adj_price[t] - adj_price[t-1]) / adj_price[t-1]
专业量化系统必须在数据进入策略之前完成复权处理。前复权或后复权都可以,但必须确保——任意两天的价格可以被等价比较。
四、三件大事之二:时效性——“活着"不等于"新鲜”
接口返回 200,意味着网络通路正常;但不能保证返回内容是"今天的"。
真实事故里出现过两类——
- 接口正常返回数据,但末行时间戳停留在两年前
- 接口稳定运转,但中间某一段时间字段口径悄悄变了
一个合格的数据层会在每次拿到数据后,再做一次时间戳校验。这一步不可省略,也不能被"接口活着"替代。
五、三件大事之三:单位归一——不同源的"元/万/亿"必须收拢
多数据源系统中,不同接口返回的数值单位常常不同。如果在策略代码里依靠**“启发式判断”**决定单位,迟早会在边界数据上出错。
# ❌ 启发式判断单位——"这个数看起来很大,应该是元吧?"
amount = raw if raw > 1e8 else raw * 1e4 # 边界数据上必翻车
# ✅ 在数据入口处一次性归一为约定的"基础单位",下游一律假定已归一
amount = normalize_unit(raw, source_unit=SOURCE_UNIT_TABLE[source])
正确的做法是——在数据入口处一次性把所有单位归一为预先约定的"基础单位",下游所有模块都假定输入已经归一。
这是一类"不会让程序崩,但会让你某天上头条"的隐形 bug,必须在架构层面处理掉,不能依赖事后排查。
六、架构分层:四层缺一不可
一个能支撑实盘的行情数据层,大致可以分为四层。这里只讲每一层"应该做什么",不展开"怎么做"——
| 层 | 应该做什么 | 缺了会怎样 |
|---|---|---|
| 取数层(含冗余) | 多源接入、降级备援 | 任意上游故障,系统直接哑火 |
| 复权层 | 除权除息归位 | 盈亏与账户实际表现对不上 |
| 时效层 | 时间戳校验 | 在不知情下用过时数据做今日决策 |
| 归一层 | 单位收拢 | 因单位错位推出明显错误结论 |
七、三个最常见的误区
误区一:“数据能接通就够了”
只看连通性、不看数据本身,是初学者最常见的错觉。一份"接得通但已经过期"的数据,对策略的伤害大于"接不通"——后者会让系统报错停机,前者则让你在错误的输入上"正常运行",决策结果几乎不可能正确。
误区二:“我只用一个数据源就够”
量化系统里没有"完全可靠的数据源"。所有商业数据接口都会经历故障、降级、停止维护、口径调整。单数据源 = 单点失败,且故障到来时通常没有预警。系统层面必须从一开始就预留至少一条备援链路。
误区三:“复权是细节问题,对策略影响不大”
这是一个会被实测打脸的认知。对发生过除权除息的标的,未复权价格序列与真实收益曲线之间,可以差出十几个甚至几十个百分点。一旦回测或盈亏曲线与账户记账对不上号,你将无法判断问题出在哪里。
八、一个脱敏案例:一根伪信号是怎么诞生的
某次复盘中,我们注意到一只标的的策略评分曲线在某一天剧烈跳变,但当日并没有任何已知事件。
深入排查后发现——当天是该标的的除权日,而上游一份历史数据接口在该次维护中"丢失"了复权标记。系统读到的是未复权的价格,于是产生了一个伪信号。
如果这条伪信号当时进入了推送链路,用户看到的会是"系统认为该票上涨",而实际持有人的账户记录显示的是下跌。这种"系统与现实对不上号"的体验,是用户信任崩塌的开始。
这次事件没有造成对外影响,是因为时效校验和复权校验同时存在;任何一层缺位,都会让伪信号穿过整个系统。
九、思考题
- 打开你在用的看盘软件,挑一只过去半年除权过的标的,对比"除权前一天"和"除权当天"的显示价格,看是否连续。
- 列出你当前系统的全部数据来源,标注每个是否有备援。如果存在单点依赖,估算"它今天挂掉,你的系统停摆多久"。
- 找出你代码里所有以"亿"为单位的金额字段,逐个检查上游是不是真的就是亿。
总结
- 数据层是整个系统的地基,重要性长期被严重低估(真实权重 7:2:1)
- 数据可信 = 网络层 + 数据层两道校验,缺一不可
- 复权、时效、归一是数据层的三件大事,每一件都对应过历史教训
- 先把四层分清楚,再谈策略——顺序反了,前面的工作大概率要返工
更多推荐



所有评论(0)