← 返回文章列表

AKShare+DuckDB:先检查数据,再开始回测

作者:杨轩

拿到一份行情表,先查哪三处?

一个真实接口小测试,加一份故意带错的离线样本:检查重复、价格口径和日期缺口,附代码、依赖锁和预期结果。

拿到一张行情表,先别急着计算收益。先回答三个问题:有没有重复日期,价格是不是同一种口径,缺少的日期该怎样解释?这一步做好,换回测框架时才能知道结果差异来自哪里。

这一篇给出一个能在本地运行的小练习:用 AKShare 获取少量历史日线,用 DuckDB 保存和查询数据,再用几条检查找出问题。下载包中的数据是虚构的,代码默认只跑离线样本;联网取数是单独的可选命令。这里验证数据处理流程,不展示策略收益。

先分清两个工具的工作

AKShare 负责连接数据接口;DuckDB 负责查询、整理和保存本地数据。两者配合,适合个人研究中“先拉一小段数据,再用 SQL 看清楚”的工作。

它们也有各自的边界:接口能返回数据,不等于长期完整稳定;数据库能读进一列数字,不等于数字的含义正确。复权口径、字段单位、采集时间,仍要由研究者记录。

如果你先想了解交易配置会怎样改变结果,可以读AKQuant 费用与 T+1 小实验。这一篇补上它前面的数据检查环节。

本次实验的范围

离线样本使用虚构代码 DEMO001,窗口为 2025-01-02 至 2025-01-08。十行输入故意包含重复、错误和不同价格口径,方便把检查结果看清楚。它不是任何股票或基金的历史行情。

可选联网步骤调用 fund_etf_hist_em,查询 510300 在相同窗口内的不复权日线。这个代码只用于演示接口参数,不作标的推荐。联网结果、采集时间和版本单独记录,不把它混进虚构样本。

本次实际运行的版本与联网状态,见下方“运行记录”。

一张最小数据说明表

项目 这份练习采用的规则
标的 离线为虚构 DEMO001;联网为显式参数 510300,不能混在一起
日期 YYYY-MM-DD,日线按中国市场本地交易日期理解,不是 UTC 时间戳
开、高、低、收 正数,最多六位小数;进入数据库后为 DECIMAL(12,6)
价格口径 none 表示本例要求的不复权;qfq 不允许混入同一条处理链
字段范围 只处理价格;成交量单位没有在本例逐源核实,因此不做成交量聚合
缺失值 标记并剔除无效行,不补零、不向前填充、不插值
重复 完全一致的有效记录去重;同日、同标的、同口径但价格冲突则报错
来源 保留接口名、参数、抓取时间、输入文件 SHA256 和软件版本

这里没有处理复权因子,也没有把不复权价格变化当作含分红收益。想改成前复权或后复权,先调整整份数据说明和验证方法,不能只替换一个字符串。

跑一次,看看实际留下什么

解压数据检查复现包,进入文件夹。使用 Python 3.12 建立独立环境,按附件 README 安装锁定依赖,然后运行:

python pipeline.py --out run-01
python -m unittest -v \
  test_pipeline.py

每次使用新的输出目录,旧结果不会被覆盖。脚本生成 clean.parquet 和 report.json。

处理结果 行数或日期
原始输入 10 行
通过检查、去重后 4 行
去掉完全相同的重复 1 行
剔除的问题行 5 行:复权口径、OHLC 范围、空价格、非法日期、超出窗口各 1 行
缺少的预期观测日期 2025-01-06

清理后的四个收盘价为 10.2、10.4、10.6、10.8。这里的递增只是练习设计,不是市场结果。脚本重新读取 Parquet,核对日期、精确小数和记录是否保持一致。

三类检查,分别解决什么问题

1. 重复日期:先找出来,再决定怎么处理

同一只标的同一天出现两条记录,可能是重复抓取,也可能是后一次修订。不能看到重复就盲目保留最后一行。

SELECT symbol, date, adjust,
       COUNT(*) AS n
FROM read_csv(
  'fixture.csv',
  all_varchar = true)
GROUP BY symbol, date, adjust
HAVING COUNT(*) > 1;

原始样本会找到三组重复日期:01-03、01-07、01-08。但后两组各有一条无效价格记录,所以要先验证,再比较剩余记录。只有 01-03 的有效行完全相同,安全去掉一份。

附件测试还故意制造了“同一天两份有效价格不同”的情况。脚本会停止,要求核对来源;它不会替你猜哪一个是真实价格。

2. 复权口径:同名字段,可能不是同一种价格

SELECT symbol,
       COUNT(DISTINCT adjust) AS bases
FROM read_csv(
  'fixture.csv',
  all_varchar = true)
GROUP BY symbol
HAVING COUNT(DISTINCT adjust) > 1;

这条查询会发现样本中混用了 none 与 qfq。本例将那条 qfq 记录剔除,不尝试换算成不复权价。

这是一条人为放入的错误样本,不能据此判断实际 AKShare 接口会混用口径。真实研究时,应检查请求参数、返回字段和保存文件的命名;前复权历史价格还可能随后续权益事件改变,因此输入快照比“以后再下载一次”更利于复现。参见AKShare 对该接口与复权的说明。

3. 日期缺口:找不到一行,不等于这天应该补一个价格

WITH bars AS (
  SELECT *
  FROM read_parquet(
    'run-01/clean.parquet')
), expected AS (
  SELECT CAST(date AS DATE) AS date
  FROM read_csv('expected-days.csv')
)
SELECT e.date
FROM expected e
LEFT JOIN bars b USING(date)
WHERE b.date IS NULL
ORDER BY e.date;

这份样本会列出 2025-01-06。原因是我们放入的该日记录被价格口径检查剔除了;脚本保留缺口,不制造新的行情。

附件里的日期表是教学用的预期观测表,不是已核验的交易所日历。真实数据出现缺口时,还要结合正式交易日历、上市时间、停牌及接口状态解释。这个查询只能告诉你“缺了一天”,不能自动告诉你“为什么缺”。

可选:实际获取一小段历史数据

另装联网依赖后运行:

python fetch_akshare.py --out fetch-01
python pipeline.py \
  --input fetch-01/bars.csv \
  --symbol 510300 --out real-01

第一条命令才会联网,限定一个接口、一个标的和一个短窗口;45 秒未完成会结束并记录超时。第二条只读取已经保存的本地文件。

联网失败时先看 fetch-record.json 和本机 fetch.log,核对软件版本、上游接口是否仍在维护、网络状态,以及是否触发了限流。保留失败记录,先用离线样本完成练习。不要关掉证书校验、伪造成功结果,或把旧样本重新命名成最新数据。

运行记录

2026-10-30(北京时间),在 macOS arm64、Python 3.12.14 下运行;AKShare 1.20.5、DuckDB 1.5.6。记录使用实际测试版本,不代表最新版推荐。

  • 真实接口:返回 5 条记录;本例检查没有发现重复、无效价格或预期观测缺口,Parquet 写入与回读一致。这里只验证一个标的、一个短窗口,不能推广为完整数据质量保证。
  • 离线练习:得到上表的 4 条有效记录、1 条完全重复、5 条剔除记录和 1 个观测缺口。
  • 10 项回归检查通过:包括已知结果、重复/冲突、输入顺序、非法数值、字段变化、日期表、Parquet 类型、旧输出保护与 Python 网络调用拦截。未测试其他系统和大规模性能。
  • 附件:提供虚构样本、脚本、锁定依赖、预期输出与实际取数记录;真实行情快照只用于本次本地核对,未打入下载包。

什么时候值得用这套小流程

当你有几份 CSV、反复复制粘贴和合并表格,却说不清哪一份用了什么价格口径,这套方法就有价值。先固定说明、输入快照和几条检查,再考虑自动更新。

本例不包含全市场采集、完整交易日历、自动调度、性能对比、退市覆盖或未来数据泄漏检查;它是一条可核对的起步流程。下一步把数据交给回测框架之前,再用AKQuant 实测核对费用、下单和成交规则。

更多工具的分工与限制见量化工具研究。数据库读写方法参考DuckDB Python 文档与Parquet 文档。本文为本站研究整理与 AI 辅助实验,不代表作者的交易经验或投资业绩。

本文代码与样本:资料与下载 →

让分享,成为交流的开始。

关于文章、AI 实践或阅读,有想法欢迎来信。