免费数据源最危险的地方不是不能用,而是错了你不知道

免费数据源最危险的地方不是不能用,而是错了你不知道

作者:量化专家 | 迪雅数据量化学院 适合读者:正在用免费数据源做研究、或准备把研究代码转成长期运行系统的开发者

前两篇讲的是"怎么发现数据问题"。这一篇讲一个更麻烦的层次:问题发现不了的时候会发生什么

你可能觉得夸张。但先看一个真实的形态:

[
  {"date": "2016-10-12", "open": 9.07, "close": 11.07, "high": 11.07, "low": 9.07, "amount": 355.87},
  {"date": "2016-10-13", "open": 12.27, "close": 12.27, "high": 12.27, "low": 12.27, "amount": 73},
  {"date": "2016-10-14", "open": 13.6, "close": 13.6, "high": 13.6, "low": 13.6, "amount": 128.89}
]

结构完整,类型正确,字段齐全。看起来完全正常。

但仔细看第二行和第三行:open == close == high == low一整天的四个价格完全相同。

这在 A 股意味着什么?意味着这一天这只股票只有一个价格。只有两种情况会这样:一字涨跌停,或者这笔数据是假的

这就是免费数据源最危险的地方——它不报错。


一、错误有三种,只有一种你能立刻发现

错误类型表现你能发现吗
接口挂了超时、连接失败、HTTP 500立刻发现,程序直接报错
返回空[]null容易发现,但要你写了判断
返回错的数据结构完整、值也"像那么回事"静默通过,进入你的计算链路

前两种是"好错误"——它们吵闹、显眼,逼你立刻处理。

第三种是"坏错误"——它安静、体面,穿着正确格式的外衣,出现在你的回测结果里。你可能两周后才从"这个策略收益怎么这么高"的疑虑中发现它。

而且方向往往是让你的结果变好看:缺失的交易日被跳过、错误的复权让收益曲线更平滑、异常值被当成真实波动。你没有动力去怀疑一个表现良好的回测。


二、错误是从哪来的

理解来源,才能设计检查。免费数据源的错误主要来自四个环节:

1. 上游页面结构变化

多数免费源的本质是爬取公开财经页面。上游改一次 HTML 结构、加一次反爬、调整一次分页规则,抓取逻辑就可能失效。

失效不总是"彻底抓不到"。更常见的是部分失效:原来一页 100 条,现在一页 80 条——于是你拿到不完整的数据,但每一行都是真的。

这类错误最隐蔽:字段对、格式对、值也对,只是少了。

2. 字段位置漂移

如果解析逻辑依赖"第 43 个位置是市净率"这种位置约定,上游插入一个新字段,你拿到的就全错位了。

注意:错位后的值仍然是合法的数字。 你不会看到 NaN,你会看到"市净率 = 2.35"——它其实是振幅。

3. 复权口径未声明

免费源常常直接给一个 close,不说是什么口径。你自己按前复权理解,它可能是未复权。

在除权日,这个差异会让你的信号完全反向。

4. 时间基准混乱

有的源用交易日,有的用自然日。非交易日是缺行、补前收、还是补 NaN——不同源处理不同。

一旦不一致,你的"持有 N 天"就变成了不确定的 N 天。


三、四个能自动跑的检查

核心思路是:把"人眼才能发现的错误"变成"程序能自动发现的错误"

检查 1:OHLC 逻辑自洽

这一条能挡掉大量脏数据,且零成本:

def check_ohlc(bar: dict) -> list[str]:
    """返回问题列表,空列表表示通过"""
    errs = []
    o, c, h, l = bar['open'], bar['close'], bar['high'], bar['low']

    # 最高价必须 >= 所有其他价格
    if h < max(o, c, l):
        errs.append(f"{bar['date']} 最高价 {h} 低于 open/close/low")
    # 最低价必须 <= 所有其他价格
    if l > min(o, c, h):
        errs.append(f"{bar['date']} 最低价 {l} 高于 open/close/high")
    # 价格必须为正
    if min(o, c, h, l) <= 0:
        errs.append(f"{bar['date']} 存在非正价格")

    return errs


def check_flat_days(bars: list[dict], max_ratio: float = 0.05):
    """检测「四价相同」的异常日占比
    
    正常股票偶有一字涨跌停,但占比不应超过 5%
    """
    flat = [b for b in bars
            if b['open'] == b['close'] == b['high'] == b['low']]
    ratio = len(flat) / len(bars) if bars else 0
    if ratio > max_ratio:
        return {
            "ratio": round(ratio, 4),
            "count": len(flat),
            "sample": [b['date'] for b in flat[:5]],
            "verdict": "异常:占比过高,数据源可能返回了填充值",
        }
    return {"ratio": round(ratio, 4), "count": len(flat), "verdict": "正常"}

为什么 max_ratio 是 5%? 因为正常股票确实会有极少数一字板。这个阈值不是精确科学,是"能筛出批量填充"的量级判断。关键是它自动跑,不用你看图。

检查 2:交易日连续性

from datetime import datetime, timedelta

def check_trading_days(bars: list[dict]):
    """检测缺失的交易日(用粗略的工作日规则,精确版应查交易日历)"""
    dates = sorted(datetime.strptime(b['date'], '%Y-%m-%d') for b in bars)
    if len(dates) < 2:
        return {"verdict": "数据过少"}

    gaps = []
    for i in range(1, len(dates)):
        delta = (dates[i] - dates[i-1]).days
        # 超过 5 天(含周末+节假日容差)视为可疑缺口
        if delta > 5:
            gaps.append({
                "from": dates[i-1].strftime('%Y-%m-%d'),
                "to": dates[i].strftime('%Y-%m-%d'),
                "days": delta,
            })
    return {
        "total_bars": len(bars),
        "range": f"{dates[0]:%Y-%m-%d} ~ {dates[-1]:%Y-%m-%d}",
        "gaps": gaps,
        "verdict": "发现缺口,需核对" if gaps else "连续",
    }

注意这个检查只能发现"明显缺口"(比如整月缺失)。精确判断需要真实交易日历——春节休市和普通周一的区别,只有日历知道。但粗略检查已经能抓出大部分"抓取中断"的情况。

检查 3:价格跳变

def check_jumps(bars: list[dict], threshold: float = 0.11):
    """检测相邻交易日超过 ±11% 的跳变
    A股涨跌停是 ±10%(ST 为 ±5%),超过 11% 基本可判定为数据问题
    排除条件:用户需自行标注的除权日
    """
    jumps = []
    for i in range(1, len(bars)):
        prev_c = bars[i-1]['close']
        cur_c = bars[i]['close']
        if prev_c <= 0:
            continue
        chg = (cur_c - prev_c) / prev_c
        if abs(chg) > threshold:
            jumps.append({
                "date": bars[i]['date'],
                "prev_close": prev_c,
                "close": cur_c,
                "chg_pct": round(chg * 100, 2),
            })
    return jumps

这个检查的产出要人工看一眼——因为除权日确实会出现大幅跳变,那是正常的。所以:

  • 跳变集中在少数几天 → 大概率是除权,检查你的数据源是否已复权
  • 跳变分散在很多天 → 数据源有问题

「异常检测」的产出永远需要人判断,但程序能帮你把 2400 个交易日缩小到 5 个可疑点。 这就是它的价值。

检查 4:和一个独立源交叉验证

这是最强的一条,也是成本最高的一条:

def cross_validate(bars_a: list[dict], bars_b: list[dict], tol: float = 0.001):
    """两个独立源比对收盘价,容差 0.1%"""
    map_b = {b['date']: b['close'] for b in bars_b}
    diffs = []
    for a in bars_a:
        b_close = map_b.get(a['date'])
        if b_close is None:
            continue
        if a['close'] <= 0:
            continue
        dev = abs(a['close'] - b_close) / a['close']
        if dev > tol:
            diffs.append({
                "date": a['date'],
                "a": a['close'],
                "b": b_close,
                "dev_pct": round(dev * 100, 3),
            })
    return diffs

判读规则

差异情况含义
0 个差异两个源一致,可信
少数几天有差异大概率是除权处理不同,检查复权口径
差异超过 5% 的交易日至少一个源有问题,不能直接用
差异系统性偏移(比如 b 总是略高)可能是复权基准不同,需要标定

交叉验证的前提是两个源真的独立。 如果它们都爬同一个上游页面,那验证的是"两边同时错"——这种验证给你的是虚假的安心。


四、把检查装进流程,而不是装在脑子里

上面四个函数,如果只是"需要的时候跑一下",大概率不会跑。要把它变成流程的一部分。

数据入库
   ↓
【自动】OHLC 自洽检查 ──→ 失败 → 拒绝入库 + 告警
   ↓
【自动】交易日连续性 ──→ 异常 → 记录待核 + 继续
   ↓
【自动】跳变检测 ──→ 输出可疑点清单
   ↓
【人工】看清单(30 秒)──→ 确认是除权还是错误
   ↓
【定期】交叉验证 ──→ 每月一次,抽样比对

关键在第二列的分级:硬性错误直接拒绝,可疑情况记录后放行。

如果所有检查都设成"失败就停",你会因为除权日的正常跳变而天天被拦,最后把检查关掉。分级是为了让它能长期跑下去。


五、免费源的隐性成本怎么算

免费源的逻辑是:你不付钱,但要自己承担维护成本。这不是坏事——如果你的场景是学习、验证、一次性的研究,这个成本完全可以接受。

要算清楚的是这笔账:

项目免费源付费源
显性成本0
接口失效后的排查服务方
上游改版的适配服务方
数据口径的确认文档应明确
出问题时的等待不确定(等上游修复)有响应时效

把时间折成钱:如果每小时算 200 元,每月花 2 小时修数据源,一年就是 4800 元。这个数很可能已经超过一个付费方案的年费。

但反过来说:如果你一个月只用一次数据、只做一个简单验证,那 4800 元的前提不成立,免费源就是最优选择。

所以问题不是"免费好不好",而是"你的时间值多少钱,以及你的数据会不会进入生产链路"。


六、写在最后

这一篇没有结论,只有三句话:

第一句:错误的形态决定它的危害。吵闹的错误是好事,安静的错误才是问题。

第二句:判断一个源可不可靠,不看它的文档写得多好,看它出问题时的表现。文档可以写得很好,边界处理却很差。

第三句:把"发现错误"从人工变成自动。你不可能每天都检查一遍 2400 个交易日的数据,但程序可以。

前面两篇讲的是"怎么让数据正确",这一篇讲的是"怎么知道数据错了"。后者是前者的前提——你不知道它错了,就永远修不好它。

本文为个人量化研究经验分享,数据接口信息以各平台官网最新公布为准。市场有风险,回测结果不代表实盘收益,不构成任何投资建议。


关于本文的写作立场

本文讨论的是方法,不是产品。文中四个检查函数与任何数据源都无关——它们作用于你已经拿到的数据,用来判断这份数据可不可信。这套检查同样适用于付费数据源(付费买的是"更少的错误"和"出问题有人负责",不是"零错误")。

如果你需要拿一个真实接口跑通这几个检查,迪雅数据(diyadata.cn)提供 50+ 个 A 股数据接口,包含实时行情、历史行情、集合竞价、板块与成分股等门类,可申请接口密钥自行验证。建议按本文的检查清单先跑一遍,再决定用哪个源。


本文是「数据源选型」系列的第三篇。前两篇:《回测十年跑得准,先过数据源这关》《集合竞价那 10 分钟,你的行情接口在"装档"吗?》《同一只股票,两个数据源给出的"成交量"差 100 倍》

评分:
在线客服
工作日 9:00 - 18:00 在线回复

产品使用、订单、售后问题
点击下方按钮在线沟通

点击开始 QQ 咨询

客服QQ:64935542

官方微信自助客服

扫一扫添加微信客服
获取专属技术支持