拿到一张行情表,先别急着计算收益。先回答三个问题:有没有重复日期,价格是不是同一种口径,缺少的日期该怎样解释?这一步做好,换回测框架时才能知道结果差异来自哪里。
这一篇给出一个能在本地运行的小练习:用 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 辅助实验,不代表作者的交易经验或投资业绩。