云计算百科
云计算领域专业知识百科平台

【Python量化数据清洗实战 01】回测亏钱先查数据:一条命令体检你的 K 线

本文你将得到什么:

  • 一套 K 线数据体检清单:OHLC 逻辑校验、重复行检测、价格跳变扫描、缺 bar 检测,四个函数全部可直接复制
  • 一个"确定性注入器"——手工造出 4 类坏数据,验证你的体检器真的能抓到(体检器也要自检)
  • 一组实测数字:4 类注入缺陷全部被检出,对照组唯一提示是国庆休市窗口(体检器自动分类,不误报)
  • 一份可导出的体检报告 CSV,可以直接挂进你的数据管道

  • 痛点:策略亏钱,第一嫌疑人不是策略

    很多人写完策略就急着回测,回测结果一难看就开始调参数。但更常见的真相是:你喂给回测引擎的数据本身就是坏的——停牌没剔除、除权没复权、接口偶发返回了几行脏数据,策略在垃圾上跑,参数调得再好也是白调。

    量化圈的老话是"垃圾进,垃圾出"。数据清洗是这个系列的主题,而清洗的第一步不是洗,是体检:先搞清楚数据到底哪里脏,再决定怎么洗。

    这篇先给你一套最小体检器:四项检查,五十行内搞定,跑一遍就知道你的 K 线有没有病。


    环境准备

    pip install mairui pandas numpy


    第一步:拉数据,再"故意弄脏"它

    体检器不能只报喜。为了验证它真的能抓到问题,我们先用确定性注入器往干净数据里造 4 类典型缺陷(可复现、可验证),再让体检器去抓:

    • 缺 bar:删掉某天的记录(接口漏发、本地落库失败);
    • 重复行:同一天出现两行(重试导致的重复写入);
    • OHLC 逻辑错误:最高价低于最低价(字段错位、解析错误);
    • 价格跳变:单日 +35%(主板涨跌停只有 10%,这是典型的脏数据)。

    from mairui import Client
    import pandas as pd
    import numpy as np
    import random

    client = Client("LICENCE-66D8-9F96-0C7F0FBCD073") # 演示证书,请换成你自己的正式证书

    def wiggle(bars, seed):
    """对样例数据做确定性小幅扰动,模拟真实行情起伏(同种子同结果)。"""
    rng = random.Random(seed)
    out = []
    for i, b in enumerate(bars):
    k = 1.0 + (rng.random() – 0.5) * 0.10
    nb = dict(b)
    for f in ("o", "h", "l", "c"):
    nb[f] = round(float(nb[f]) * k, 2)
    out.append(nb)
    return out

    def inject_defects(bars):
    """确定性注入 4 类缺陷,顺序从尾到头避免索引漂移。"""
    data = [dict(b) for b in bars]
    data[40]["c"] = round(float(data[40]["c"]) * 1.35, 2) # 价格跳变
    data[20]["h"], data[20]["l"] = data[20]["l"], data[20]["h"] # h < l
    del data[30] # 缺 bar
    data.insert(11, dict(data[10])) # 重复行
    return data

    SEED = 20260926 + 600519
    bars = client.stock_history(code="600519", period="d", dividend="f")
    bars = wiggle(bars, seed=SEED)
    bad_bars = inject_defects(bars)

    df = pd.DataFrame(bad_bars).rename(columns={"t": "date"}).sort_values("date").reset_index(drop=True)
    for col in ("o", "h", "l", "c", "v"):
    df[col] = df[col].astype(float)

    说明:这里用的是公开演示证书,为演示方便明文写入;实际项目请换成你自己的正式证书(申请配置见 SDK 文档 https://github.com/MaiRuiApi/mairui ),换证后代码无需修改,注入器也只是验证体检器用的教学工具。


    第二步:四项体检

    def check_ohlc(df):
    """OHLC 逻辑校验:h>=l、h>=max(o,c)、l<=min(o,c)、价格全为正。"""
    rules = {
    "h<l": df["h"] < df["l"],
    "h<max(o,c)": df["h"] < df[["o", "c"]].max(axis=1),
    "l>min(o,c)": df["l"] > df[["o", "c"]].min(axis=1),
    "价格<=0": (df[["o", "h", "l", "c"]] <= 0).any(axis=1),
    }
    mask = pd.Series(False, index=df.index)
    hit_rule = pd.Series("", index=df.index, dtype=object)
    for name, m in rules.items():
    newly = m & ~mask
    hit_rule[newly] = name
    mask = mask | m
    return df[mask].copy(), hit_rule[mask]

    def check_duplicates(df):
    return df[df["date"].duplicated(keep=False)]

    def check_jump(df, threshold=0.11):
    """主板单日涨跌停 10%,留 1% 余量。ST 股与不同板块要单独调阈值。"""
    ret = df["c"].pct_change().abs()
    return df[ret > threshold].copy(), ret

    def check_missing(df):
    """按工作日间隙检测缺 bar。1~3 个工作日=疑似缺 bar;>=4 疑似节假日窗口。"""
    dates = pd.to_datetime(df["date"]).values.astype("datetime64[D]")
    rows = []
    for i in range(1, len(dates)):
    gap = int(np.busday_count(dates[i – 1] + np.timedelta64(1, "D"), dates[i]))
    if gap > 0:
    kind = "疑似节假日窗口(需交易日历核对)" if gap >= 4 else "疑似缺bar"
    rows.append({"prev_date": str(dates[i – 1]), "next_date": str(dates[i]),
    "missing_busdays": gap, "judge": kind})
    return pd.DataFrame(rows)

    bad_ohlc, ohlc_rules = check_ohlc(df)
    bad_dup = check_duplicates(df)
    bad_jump, rets = check_jump(df)
    bad_gap = check_missing(df)

    report = pd.DataFrame({
    "检查项": ["OHLC 逻辑", "重复行", "价格跳变", "工作日间隙"],
    "检出数": [len(bad_ohlc), len(bad_dup), len(bad_jump), len(bad_gap)],
    })
    print(report.to_string(index=False))
    report.to_csv("checkup_report.csv", index=False, encoding="utf-8-sig")


    运行结果

    输出示例(演示证样例数据 + 确定性注入,仅供演示):

    检查项 检出数
    OHLC 逻辑 2
    重复行 2
    价格跳变 2
    工作日间隙 2

    明细:

    • OHLC 违例 2 处:2025-10-21: 违反 h<l(o=10.27 h=10.26 l=10.34 c=10.3);2025-11-18: 违反 h<max(o,c)(o=10.83 h=10.87 l=10.75 c=14.54)——第二处就是被注入 +35% 跳变的那行,close 14.54 高过了当天最高价,一条坏数据同时踩了两条规则;
    • 重复行:2025-09-29 出现 2 次;
    • 价格跳变 2 处:2025-11-18: 11.05 -> 14.54(+31.6%),以及次日回落 14.54 -> 11.71(+19.5%)——跳变会把相邻两天都标记出来;
    • 工作日间隙 2 处:2025-09-30 -> 2025-10-09 缺 6 个工作日(疑似节假日窗口)(这是 2025 年国庆休市,正常);2025-11-03 -> 2025-11-05 缺 1 个工作日(疑似缺bar)(这正是被删掉的那条)。

    对照组:对未注入的原始数据跑同样的体检,OHLC/重复/跳变全部 0 检出,唯一提示就是国庆休市窗口——体检器没有把正常休市当缺 bar 误报。


    结果怎么读(诚实的部分)

  • 4 类注入缺陷全部被检出,且没有误伤正常数据:唯一的"节假日窗口"提示是体检器按间隙长度自动分类的结果,这本身就是设计的一部分——缺 bar 检测只看工作日,遇到长假必须靠交易日历兜底,否则每年国庆春节你都要修一遍"假故障";
  • 一条坏数据会同时踩多条规则:注入的跳变行同时触发了跳变检查和 OHLC 检查,跳变次日的"回落"也会被标记。看到多条规则同时命中同一行,基本可以断定是数据错误而非行情;
  • 阈值要按板块调:跳变阈值 11% 只适用主板 ±10% 的涨跌停;ST 股 5%、创业板/科创板 20%,直接套用会大量漏报或误报;
  • 数字为演示证样例数据结果,仅演示体检方法;换正式证书即得真实行情下的体检报告。

  • 常见坑

  • 只用工作日判断缺 bar:np.busday_count 不知道中国节假日,长假会被判成"大量缺 bar"。正确姿势是像本文一样按间隙长度分桶,>=4 个工作日的间隙先怀疑节假日,再用交易日历(或接口返回的交易日序列)核对;
  • 重复行只查日期不查内容:如果两行日期相同但价格不同(典型的"半截写入"),duplicated() 同样能抓到,但清洗时要注意保留哪一行——通常保留最后写入的那条;
  • 体检完了不处理:体检报告的价值在于触发下一步动作(剔除、修复、重新拉取),本系列后面几篇会逐一处理这几类问题;
  • 拿前复权数据做跳变检测:复权后的价格曲线被平滑过,真实的除权跳变会被抹掉、但假跳变可能被引入——跳变检测要在复权之前做,这是下一篇要展开的话题。

  • 小结与下篇预告

    • 数据清洗的第一步是体检:OHLC 逻辑、重复行、价格跳变、缺 bar 四项检查 + 交易日历兜底;
    • 体检器本身也要自检:用确定性注入器造坏数据验证检出能力,本文 4 类缺陷全检出、对照组零误伤;
    • 体检报告能告诉你"哪里脏",但脏数据怎么处理有讲究:极端值到底是该删还是该留?下一篇讲异常值检测与处理三法(3σ / IQR / 滚动中位数),以及"极端行情保留、数据错误剔除"的判断标准。

    免责声明

    本文所有内容仅用于技术学习与交流,不构成任何投资建议。股市有风险,投资需谨慎。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Python量化数据清洗实战 01】回测亏钱先查数据:一条命令体检你的 K 线
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!