资讯动态

Python量化,行情数据本地落库与增量更新进阶

发布时间:2026/10/14 15:57:54 来源:尧图企业网站定制
做量化的人迟早都会遇到同一个问题行情数据到底要不要落库很多新手的做法是「每次跑策略都现拉接口」。短期没问题一旦进入回测调参、因子挖掘、多标的扫描的阶段这个习惯会立刻反噬——接口被反复调用配额按次计费的证书额度肉眼可见地往下掉网络稍微抖一下整个流程就断更糟的是不同数据源字段命名还不一致有的叫「收盘」、有的叫close代码里到处是if分支越改越乱。本文要解决的就是「把行情数据稳稳落到本地、并且只增量补齐新增部分」这件事。整套方案只用纯 HTTP GET SQLite零 SDK、零额外依赖代码已经本地跑通见下方真实运行结果你可以直接复制使用。本文你将得到什么一套可直接用的 SQLite 落库 Schema日线明细表 元数据表主键去重、不怕重复写增量更新逻辑首跑全量回填之后每次只从上次末日的次日拉起不重复消耗证书额度幂等保障同一份数据跑 N 次本地行数不变不会因为脚本重跑而翻倍缺口检测自动发现本地序列里「断档」的交易日提示你哪些日期可能漏数几个真实踩过的坑演示证书的固定样本、字段含义a是成交额、pc是前收、缺口检测必须配合交易日历。一、为什么必须落库从别人的痛点说起公开社区里关于「数据本地化」的讨论非常集中痛点高度一致每次查询都在烧额度。以 Tushare 为例社区里反复出现「每次查询消耗积分批量回测时 API 限速拖慢迭代」的抱怨。按「次/分钟、次/日」计费的证书逻辑相同——能本地解决的查询就别再打接口。网络与接口不可控。有人总结得很直白如果每次跑策略都要重新调接口、等返回、再清洗「等数据到手灵感早凉了」更麻烦的是哪天网络断了或者接口有限制你什么都做不了。多源字段对齐混乱。同一个「收盘价」A 源叫收盘B 源叫close大小写和类型还可能不一样。把数据落到自己统一的表里从源头消灭这种混乱。把数据落到本地查询走本地库既省证书额度又快——这是所有成熟量化工作流的共识。SQLite 是其中最轻量的选择一个文件就是一个数据库不用起服务几百万条历史数据完全够用个人和小团队首选。二、自验代码已本地跑通下面这段代码就是本期的完整实现。它做四件事建表、拉数、增量写库幂等 upsert、缺口检测。importsqlite3importrequestsimportdatetime BASEhttps://api.momaapi.comLICENCETEST-API-TOKEN-MOMA-836089C22111# 演示证书请换成你自己的正式证书DBmarket.dbdefinit_db(conn):建表日线明细主键 codet 去重 元数据每只标的最后更新日。conn.execute(CREATE TABLE IF NOT EXISTS daily ( code TEXT, t TEXT, o REAL, h REAL, l REAL, c REAL, v REAL, a REAL, pc REAL, sf INTEGER, PRIMARY KEY(code, t)))conn.execute(CREATE TABLE IF NOT EXISTS meta ( code TEXT PRIMARY KEY, last_trade_date TEXT))conn.commit()deffetch_history(code,st,et):拉取日线st/et 为 YYYYMMDD 区间。正式证书会按区间返回演示证书返回固定样本。urlf{BASE}/hsstock/history/{code}/d/n/{LICENCE}?st{st}et{et}rrequests.get(url,timeout20)r.raise_for_status()datar.json()returndataifisinstance(data,list)else[]defupsert(conn,code,rows):增量写库ON CONFLICT(code,t) DO UPDATE 保证幂等。返回真正新增的行数。beforeconn.execute(SELECT COUNT(*) FROM daily WHERE code?,(code,)).fetchone()[0]forbinrows:conn.execute(INSERT INTO daily(code,t,o,h,l,c,v,a,pc,sf) VALUES (?,?,?,?,?,?,?,?,?,?) ON CONFLICT(code,t) DO UPDATE SET oexcluded.o,hexcluded.h,lexcluded.l,cexcluded.c,vexcluded.v,aexcluded.a,pcexcluded.pc,sfexcluded.sf,(code,b[t],b[o],b[h],b[l],b[c],b[v],b[a],b[pc],b[sf]))conn.commit()afterconn.execute(SELECT COUNT(*) FROM daily WHERE code?,(code,)).fetchone()[0]returnafter-beforedefupdate_code(conn,code,today):增量更新单只标的无记录全量回填有记录则从上次末日次日拉起。rowconn.execute(SELECT last_trade_date FROM meta WHERE code?,(code,)).fetchone()lastrow[0]ifrowelseNoneiflast:nxt(datetime.datetime.strptime(last,%Y-%m-%d)datetime.timedelta(days1)).strftime(%Y%m%d)stnxtelse:st20200101# 首跑全量回填rowsfetch_history(code,st,today)newupsert(conn,code,rows)ifrows:max_tmax(b[t]forbinrows)conn.execute(INSERT INTO meta(code,last_trade_date) VALUES(?,?) ON CONFLICT(code) DO UPDATE SET last_trade_dateexcluded.last_trade_date,(code,max_t))conn.commit()returnlen(rows),newdefgap_report(conn,code):缺口检测在本地最小~最大交易日之间找出缺失的交易日剔除周末。rowsconn.execute(SELECT t FROM daily WHERE code? ORDER BY t,(code,)).fetchall()dates[datetime.datetime.strptime(r[0],%Y-%m-%d).date()forrinrows]ifnotdates:return[]haveset(dates)gaps[]ddates[0]whileddates[-1]:ifd.weekday()5anddnotinhave:gaps.append(d.isoformat())ddatetime.timedelta(days1)returngapsif__name____main__:today20260916connsqlite3.connect(DB)init_db(conn)code600519total,newupdate_code(conn,code,today)print(f[增量更新]{code}: 接口返回{total}行, 实际新增入库{new}行)total2,new2update_code(conn,code,today)print(f[幂等复查]{code}: 再次运行 接口返回{total2}行, 新增{new2}行 (应0))cntconn.execute(SELECT COUNT(*) FROM daily WHERE code?,(code,)).fetchone()[0]mxconn.execute(SELECT last_trade_date FROM meta WHERE code?,(code,)).fetchone()[0]print(f[本地状态]{code}: 共{cnt}行, last_trade_date{mx})gapsgap_report(conn,code)print(f[缺口检测]{code}: 连续序列中缺失交易日{len(gaps)}个)# 注入一个缺口验证检测器确实能发现断点conn.execute(DELETE FROM daily WHERE code? AND t?,(code,2025-10-10))conn.commit()gaps2gap_report(conn,code)print(f[缺口检测-注入] 删除 2025-10-10 后, 检出缺失{len(gaps2)}个:{gaps2[:3]})conn.close()三、真实运行结果用演示证书在本地直接运行不是截图、不是想象是真实跑出来的输出[增量更新] 600519: 接口返回 50 行, 实际新增入库 50 行 [幂等复查] 600519: 再次运行 接口返回 50 行, 新增 0 行 (应0) [本地状态] 600519: 共 50 行, last_trade_date2025-12-01 [缺口检测] 600519: 连续序列中缺失交易日 6 个 [缺口检测-注入] 删除 2025-10-10 后, 检出缺失 7 个: [2025-10-01, 2025-10-02, 2025-10-03]逐行解读第一行首跑把 50 行样本全部写入本地库。第二行再跑一次新增 0 行——这就是幂等。无论脚本被定时任务重复触发多少次本地数据都不会翻倍这是生产环境落库的底线。第三行本地状态显示last_trade_date2025-12-01下次增量会从 2025-12-02 起拉不会从头再拉一遍。第四、五行缺口检测生效。这里检出的 6 个「缺失日」其实对应的是国庆法定假日10-01/02/03 等——它暴露了一个重要工程事实朴素的 weekday 启发式会把法定假日误判为缺口生产环境一定要配合真实的交易日历行情服务商也提供交易日历接口来过滤否则日报会天天报警。四、几个必须说清的坑1. 演示证书返回的是固定样本。在演示证书下/hsstock/history/{code}/d/n/{licence}会忽略你传入的st/et固定返回一份 50 行的样本本文是 2025-09-15 至 2025-12-01。所以「增量窗口」在演示环境看不出差异——但这不影响本地去重、幂等、meta 跟踪逻辑的正确性这些都已用真实返回数据验证过。换成你的正式证书后接口会按你给的区间返回完整数据增量效果立刻显现。2. 字段含义别搞混。日线返回字段里t是日期o/h/l/c是开高低收v是成交量a是成交额不是成交量pc是前收前一天收盘价sf是复权标记0 表示未复权。写库时把a当成交额、pc当前收用别张冠李戴。3. 缺口检测要配合交易日历。上面已经演示没有交易日历兜底缺口检测会把国庆、春节等长假误报成数据缺失。正确做法是先把「全年交易日集合」拉到本地缺口检测只比较「交易日里哪些不在本地」这样报警才有意义。4. A 股涨红跌绿要记牢。如果你把实时快照接进来字段pc是涨跌幅%正为涨、负为跌zf是振幅%yc是昨收p是最新价cje是成交额——正值代表上涨红、负值代表下跌绿和 A 股惯例一致做展示层时别反了。五、小结与下篇预告今天我们把「行情落库 增量更新」这条最基础的流水线打通了建表去重、增量拉取、幂等写库、缺口检测四步闭环零外部依赖。下一期我们往上走一层——当你要同时维护成百上千只标的时单线程逐个拉会慢到无法接受我们要做一个配额友好的多进程批量拉数调度器令牌桶限速、指数退避重试、并发上限可控既榨干证书配额又不触发 429。本文所有代码示例与运行结果仅用于演示数据工程方法市场有风险投资需谨慎。历史行情数据不构成未来收益承诺。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑