【Python量化数据工程实战 #01】拉下来的行情全是"脏数据"?停牌、跳空、异常值一站式清洗

本文是「Python 量化数据工程实战」系列第 1 篇。做量化的人都有这样的经历:兴冲冲拉了一年的日线,跑回测时发现某只股票中间缺了 30 天------原来那段时间停牌了;又或者某天收益率突然跳到 +900%,查了半天发现是除权没处理。数据清洗是量化工程里"最不起眼、最耗时间、最容易出 bug"的环节。这篇把最常见的三类脏数据一次性讲清楚,代码可直接跑。

本文你将得到什么

  1. 用 Python SDK 批量拉取历史 K 线的正确姿势(含分页与字段说明)
  2. 识别并处理停牌日(volume=0)的两种策略:删除 vs 前向填充
  3. 从原始收盘价计算真实涨跌幅,避免把"昨收"当"涨跌幅"的坑
  4. 3σ 法则 + IQR 检测价格异常值,防止极端行情污染模型
  5. 涨停/跌停自动标记,区分正常波动与制度性价格边界
  6. 一份可直接套用的清洗后 DataFrame,回测前 copy-paste 即可

一、数据从哪来:批量拉取与字段避坑

很多新手第一步就踩坑:stock_history 返回的字段里,pc 不是涨跌幅,而是昨收 (pre_close)。如果你直接拿 pc 当涨跌幅算指标,所有结果都会错。

python 复制代码
from mairui import Client
import pandas as pd

client = Client("LICENCE-66D8-9F96-0C7F0FBCD073")  # 演示证书,返回演示数据

# 拉取单只股票近 120 日日线
rows = client.stock_history("000001", period="d", dividend="n", lt=120)
df = pd.DataFrame(rows)
print(df[["t", "o", "h", "l", "c", "pc", "v"]].head(3))

真实输出:

text 复制代码
           t      o      h      l      c     pc       v
0  2025-11-25  11.61  11.80  11.60  11.80  11.60  1028081
1  2025-11-26  11.81  11.69  11.60  11.69  11.80   821301
2  2025-11-27  11.66  11.71  11.69  11.71  11.69  1011847

注意 pc=11.60 接近 c=11.80,这是昨收价,不是涨跌幅百分比。正确涨跌幅要手动算:

python 复制代码
df["c"] = pd.to_numeric(df["c"])
df["pc"] = pd.to_numeric(df["pc"])
df["ret_pct"] = (df["c"] - df["pc"]) / df["pc"] * 100
print(df[["t", "c", "pc", "ret_pct"]].head(3))

真实输出:

text 复制代码
           t      c     pc    ret_pct
0  2025-11-25  11.80  11.60   1.724138
1  2025-11-26  11.69  11.80  -0.932203
2  2025-11-27  11.71  11.69   0.171086

这才是真实的日涨跌幅。很多量化框架直接假设接口返回了涨跌幅字段,结果模型输入全是错的比例------这个坑排查起来极其隐蔽。

二、停牌识别:缺数据不是 bug,是真实市场状态

A股每天都有股票停牌(重组、退市整理、重大事项)。如果你拉了一年数据,某只股票中间断了 20 天,那不是接口漏了,是那只股票确实没交易。

python 复制代码
# 批量拉取 5 只股票
codes = ["000001", "000002", "600519", "600000", "000858"]
all_data = []
for code in codes:
    rows = client.stock_history(code, period="d", dividend="n", lt=120)
    df = pd.DataFrame(rows)
    df["code"] = code
    all_data.append(df)

df = pd.concat(all_data, ignore_index=True)
df["v"] = pd.to_numeric(df["v"], errors="coerce")

# 识别停牌日(成交量为 0)
suspension = df[df["v"] == 0]
print(f"停牌记录: {len(suspension)} 条")

处理策略有两种,看场景选:

场景 策略 代码
回测模型 删除停牌日 df = df[df["v"] > 0]
时序模型(LSTM/Transformer) 前向填充 df["c"] = df["c"].ffill()

删除是最安全的做法------停牌日本就没有价格发现,硬填进去反而给模型喂噪声。只有在"时间轴必须连续"的深度学习场景下,才用前向填充,且要在特征里加一个 is_suspended 标记,告诉模型"这天价格是假的"。

三、异常值检测:3σ 与 IQR 双保险

即使是正常交易的股票,偶尔也会出现异常价格------比如闪崩、乌龙指、或者数据源的脏数据。这些极端值如果不清洗,会直接扭曲均值、标准差、相关系数等所有统计量。

python 复制代码
# 为每只股票计算 3σ 异常值
df["ret_pct"] = (df["c"] - df["pc"]) / df["pc"] * 100

for code in df["code"].unique():
    sub = df[df["code"] == code]["ret_pct"].dropna()
    if len(sub) < 3:
        continue
    mean, std = sub.mean(), sub.std()
    outliers = sub[(sub - mean).abs() > 3 * std]
    if len(outliers):
        print(f"{code}: 发现 {len(outliers)} 个 3σ 异常值")

3σ 的局限 :如果股票本身波动就极大(比如次新股、ST 股),3σ 会把正常波动也标成异常。更稳健的做法是再加一层 IQR(四分位距)

python 复制代码
Q1 = sub.quantile(0.25)
Q3 = sub.quantile(0.75)
IQR = Q3 - Q1
lower, upper = Q1 - 1.5 * IQR, Q3 + 1.5 * IQR
iqr_outliers = sub[(sub < lower) | (sub > upper)]

实际工程中,我会同时用 3σ 和 IQR,只有两个方法都标为异常的行才删除------这能大幅降低误判率。

四、涨跌停标记:制度性边界不可忽视

A股的涨跌停是制度性价格边界,不是市场自发行为。涨停日的成交量、委买委卖结构、后续走势,都与自由交易日常态完全不同。很多策略如果不区分"涨停日",会把制度性约束误判为市场信号。

python 复制代码
# 涨跌停标记(主板 ±10%,科创/创业板 ±20%,ST ±5%)
df["limit_up"] = df["ret_pct"] >= 9.9
df["limit_down"] = df["ret_pct"] <= -9.9

# 打印涨停记录
ups = df[df["limit_up"]]
print(f"涨停记录: {len(ups)} 条")
if len(ups):
    print(ups[["code", "t", "ret_pct"]].head())

建议在特征工程阶段加入三个衍生字段:

  • is_limit_up:当日是否涨停
  • is_limit_down:当日是否跌停
  • days_since_limit:距离最近一次涨跌停的天数(用于判断"情绪冷却期")

五、完整清洗流水线

把上面四步拼起来,就是一份生产可用的清洗脚本:

python 复制代码
import pandas as pd
from mairui import Client

client = Client("LICENCE-66D8-9F96-0C7F0FBCD073")

def clean_kline(code, lt=120):
    rows = client.stock_history(code, period="d", dividend="n", lt=lt)
    df = pd.DataFrame(rows)
    df["code"] = code

    # 数值化
    for col in ["o", "h", "l", "c", "v", "pc"]:
        df[col] = pd.to_numeric(df[col], errors="coerce")

    # 1. 计算真实涨跌幅
    df["ret_pct"] = (df["c"] - df["pc"]) / df["pc"] * 100

    # 2. 去掉停牌日
    df = df[df["v"] > 0].copy()

    # 3. 3σ + IQR 异常值过滤
    sub = df["ret_pct"].dropna()
    mean, std = sub.mean(), sub.std()
    Q1, Q3 = sub.quantile(0.25), sub.quantile(0.75)
    IQR = Q3 - Q1

    mask_sigma = (df["ret_pct"] - mean).abs() <= 3 * std
    mask_iqr = (df["ret_pct"] >= Q1 - 1.5 * IQR) & (df["ret_pct"] <= Q3 + 1.5 * IQR)
    df = df[mask_sigma | mask_iqr].copy()  # 任一方法通过即保留(宽松策略)

    # 4. 涨跌停标记
    df["limit_up"] = df["ret_pct"] >= 9.9
    df["limit_down"] = df["ret_pct"] <= -9.9

    return df

# 批量清洗
codes = ["000001", "000002", "600519"]
cleaned = pd.concat([clean_kline(c) for c in codes], ignore_index=True)
print(f"清洗后: {len(cleaned)} 条")
cleaned.to_csv("cleaned_kline.csv", index=False, encoding="utf-8-sig")

六、小结

数据清洗没有"标准答案",只有"适合你的策略的答案"。但有几个原则通用:

  • 先理解字段含义 ,别凭名字猜(pc 不是涨跌幅)
  • 停牌不是 bug,处理策略取决于模型类型
  • 异常值要双保险,3σ + IQR 结合比单一方法更稳健
  • 涨跌停是制度边界,标记出来比忽略它更明智

下一篇我们讲:把清洗后的数据,变成模型能吃的"特征"------动量、波动、资金流三因子实战。

注意:本文为技术分享,所有接口调用均使用官方演示证书返回的演示数据(需替换为自己的实际证书),不构成任何投资建议。市场有风险,决策需谨慎。


代码与文档 :更多 SDK 用法与接口文档可参考 github.com/MaiRuiApi

相关推荐
专业程序开发源1 小时前
springboot篮球联赛管理系统13635-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·php·课程设计
行百里er2 小时前
轻量级 Spring 监测工具——Spring Insight 发布了
spring boot·后端·监控
程序员阿黄2 小时前
基于 Django 与 Vue 3 的智能实验室预约系统设计与实现
后端·python·mysql·django·vue·毕设
光影少年2 小时前
Koa 为什么使用洋葱模型
后端·node.js·koa
用户298698530142 小时前
Python 如何实现 Word 与 RTF 文档互转
后端·python·api
打工仔折腾 AI2 小时前
用 Shell 脚本自动部署 mysqld_exporter 并接入 Prometheus 远程抓取
人工智能·后端·python·性能优化·ai agent 实战
毅炼2 小时前
不写多语言 SDK,怎么让 Python、Go、Node 服务接入注册中心?
java·后端·系统架构·gateway
Methy2 小时前
IoTHub半个月崩了五次?都是裸rethrow惹的祸
服务器·c++·后端
我不会起名字3223 小时前
一天一道力扣Hot100(36):深度优先算法---组合总和
数据结构·c++·后端·python·算法·go