文档性质:可复用的技术约定(从一次真实建设中抽离、固化),不是施工日志。
适用:自建 A 股及相关市场本地库;主存 DuckDB,Parquet 作湖/缓存;源为 Tushare Pro。
配套仓库:github.com/jason9356/trader-data
真源入口:pull_modes.py · guard.py · tushare_pool.py · market.py
写在前面
本文记录的是:在本地建设一套以 Tushare 为主源的 A 股及相关市场数据库时,如何把「全量下载」做成可复用的工程,而不是一次性脚本碰运气。
常见误判有两类。第一,把吞吐问题当成网络或积分问题,忽略了更常见的根因——拉取形状选错(例如本可按报告期取全市场财务,却按证券逐只循环)。进度在走、磁盘不涨,排查就会被带偏。第二,远端接口已经返回有效数据,失败却出在本地:代理假死、日志与心跳文件互锁、SQL 标识符/类型、多进程抢同一数据库文件等。
对应的做法也简单明确:每张表先探测再登记拉取策略,未登记不得开跑;长任务配备心跳与看门狗;网络抖动开路冷却后半开重试,策略/空烧类错误才硬停;变更策略前隔离旧分区与旧水位;注册表与下载批次做覆盖对账。形状正确之后,墙钟取决于限频与数据量;形状错误时,再长的挂机也只是无效循环。
下文是抽离具体日程后的技术约定:原则、策略模型、流水线、故障模式与检查清单。表名与 VIP/限频数值因账户而异,换环境需自行探测。
技术正文
1. 范围与目标
1.1 要解决什么
把权限内、对研究有用的 日频 / 截面 / 报告期 数据可靠落到本地,并具备:
- 按表分流的拉取形状(禁止一种循环套所有 API);
- 可中断、可续跑、可熔断 的长任务运行模型;
- 湖(Parquet)+ 主库(DuckDB) 一致的布局与写入约定;
- 水位增量更新 能力(全量与日更分离)。
1.2 明确不做 / 慢做
| 类别 | 约定 |
|---|---|
1 分钟全市场(如 stk_mins) |
默认排除,不进下载批次 |
小时级限频接口(如部分 hk_daily 账户档) |
仍进计划,独立慢批次 + 拉长调用间隔 |
强制 ts_code 的重接口(如部分筹码) |
可进计划,但接受墙钟很长;勿伪装成按日截面 |
1.3 文档怎么用
- 开新表 / 改下载:先查 §3 策略模型 与 §8 检查清单。
- 出故障:先查 §5 故障模式;熔断行为见 §4.6。
- 日更:查 §7。
- 具体表名与探测结果以本仓库策略矩阵为准;换账户必须重探测。
2. 设计原则
- 形状优先于速度错误循环浪费的配额与时间,通常远大于限速偏保守。
- 对本账户探测,不死抄文档示例文档参数能调通 ≠ 本积分档下的最优拉法(例如是否有
*_vip、是否支持仅trade_date=)。 - 策略未登记 = 禁止开跑禁止「未知表默认按日扫」。
- 进度遍历 ≠ 入库成功
i/N只表示走过的日/码/期;有效性看分区、行数、首次非空。 - 长任务必须可观测、可熔断——且熔断要有恢复语义心跳 + 进程内阈值 + 外部看门狗。熔断不是「写暂停旗然后等死」:传输层故障应开路冷却、半开探测;只有逻辑错或冷却耗尽才硬停。禁止「日志停了再等一晚」。
- 改策略先清脏状态水位字段、目录布局(
code=vsperiod=)与策略绑定;切换时隔离旧分区。 - 注册表与下载计划必须对账「策略里有」≠「批次里会跑」。差集必须为空(除显式排除项)。
- 阶段完成 ≠ 项目可停批次流水线默认进入下一项;人工停靠
INGEST_PAUSE,而不是任务成功后干等。 - 远端成功 ≠ 本地成功
标识符、类型、主键、文件锁、多 writer 均可独立导致失败。
3. 拉取策略模型
3.1 模式定义
| 模式 | 含义 | 典型用途 |
|---|---|---|
snapshot |
少次调用,整表替换 | 证券主数据、交易日历、分类维表 |
by_trade_date |
一次调用 = 某一日期键下的全市场截面 | 日线、多数资金流/涨跌停;date_key 可为 trade_date / ann_date / end_date / nav_date |
by_period |
一次调用 = 某一报告期全市场 | 财务报表(优先 *_vip)、持仓截面等 |
by_code |
一次调用 = 一证券(常带起止区间) | 仅接口强制 ts_code 时 |
by_date_range |
按年/月块拉 | 新股、部分宏观利率 |
skip |
明确不做 | 无权限、产品排除 |
3.2 策略登记字段(每张表)
| 字段 | 说明 |
|---|---|
mode |
上表之一 |
api |
实际方法名(逻辑名 income → income_vip) |
date_key |
截面循环使用的参数名 |
history_start |
有意义历史起点,避免空转 |
extra_kwargs |
如业务类型、市场过滤 |
notes / 探测证据 |
为何这样定 |
真源建议单文件字典(本仓库:TABLE_STRATEGY);注册表 JSON 只作同步视图。
3.3 门禁
strategy_for(api):缺表即错。assert_strategies_cover(job_apis):任务启动前硬校验。- 财务类批次:只允许
by_period(禁止「全市场 by_code 财务」混进默认路径)。
3.4 形状判定启发式(探测清单)
对新接口,用本账户权限做抽样探测,而不是猜(密钥只放本机 env,绝不写入文档或仓库):
- 只传
trade_date=(或nav_date=/ann_date=)能否返回多证券?→ 倾向by_trade_date。 - 只传
period=/end_date=(季末)能否返回多证券?有无*_vip?→ 倾向by_period。 - 无
ts_code是否报「必须传证券代码」?→ 只能by_code。 - 无参或极少参数是否整表?→
snapshot。 - 记录首次非空的大致日期 → 写入
history_start。
3.5 常见正确形状(模式级,表名因账户而异)
- 报告期 VIP / 截面:利润表、资产负债表、现金流、财务指标、业绩快报预告、分红等(有 VIP 用 VIP)。
- 无 VIP 但可
period=:部分股东名单等。 - 按日(或等价日键)截面:日线族、多数微观结构/资金流、周线月线全市场、基金日线/净值日、指数日线全市场等。
- 强制按码:筹码分布等必须
ts_code的接口。 - 整表:
stock_basic、trade_cal、部分*_basic;注意stock_company类常可一次拉全,勿默认按码。
4. 入库流水线约定
4.1 推荐流程
账户探测 → 写入策略矩阵 → 覆盖对账(registry vs batches)
→ 冒烟(1~2 个真实截面)→ 全量/续跑(Guard + Watchdog)
→ 校验布局与行数 → 迁移/ MERGE 进 DuckDB
→ 日更只走水位
4.2 水位
| 模式 | 水位 | 注意 |
|---|---|---|
by_trade_date |
last_trade_date |
空日可推进,但应可审计 |
by_period |
done_periods |
从 by_code 切来时作废 done_codes |
by_code |
done_codes / 每码末日 |
仅强制按码表 |
snapshot |
updated_at |
整表替换成功即刷新 |
日更禁止 mode=full 重放几十年历史。
4.3 湖布局
| 布局 | 用途 |
|---|---|
year=YYYY/part-YYYYMMDD.parquet |
日截面 |
period=YYYYMMDD/part-*.parquet |
报告期截面 |
code=TS_CODE/ |
仅强制 by_code |
同一逻辑表不得长期混用互斥布局。改策略:旧目录进 quarantine/,再开新布局。
4.4 批次编排(逻辑顺序)
- Meta / 日线主链
- 财务
by_period - 市场广度截面
- 跨品种日频
- 维表 snapshot
- 结构类(周月、基金截面等)
- 强制 by_code 重表
- 小时级限频(最后,避免占满注意力)
启动前断言:
registry − scheduled − excluded == ∅
4.5 限速与网络
- 按积分档设置客户端最小间隔(高档常见约 0.13s 量级,可配置)。
- 全量优先 直连;本地代理开开关关时,很容易把「代理进程挂了」误判成 Tushare 挂了。
- 退避:频控文案走短退避;代理/DNS/连接类走熔断开路(见 §4.6),不要连打三次就永久停机。
- 小时级 API:单独配置
min_interval(例如约 3600s);看门狗 stale 阈值必须大于该间隔。
4.6 熔断语义(开路 / 半开 / 硬停)
熔断的意义是:暂时停止伤害,并在条件可能恢复后自动再试。若只有「失败 → 写暂停旗 → 进程退出」,那是急停,不是熔断。
| 类别 | 行为 | 为何 |
|---|---|---|
| 传输层(代理失败、连接重置、超时、DNS 等) | 连续失败达阈值 →开路,冷却(指数退避,有上限)→ 半开重试同一 unit;成功则合闸 | 代理重启、网络闪断是可恢复的;硬停只会逼人手工清旗 |
| 传输层耗尽 | 多次开路周期仍无成功 →IngestAbort(NETWORK) + 暂停旗 |
环境已坏,继续打只是噪音 |
| 逻辑层(首写前空烧、错误策略、连续业务错风暴、真挂死 stall) | 直接硬停 + 暂停旗 | 再试只会烧配额或空转 |
| 人工急停 | 创建暂停旗 → 下一轮检查即 abort | 与自动恢复正交 |
实现要点(本仓库):
IngestCircuitOpen:调用层捕获后sleep(cooldown),再打同一请求(半开)。- 冷却期间刷新存活时钟,避免「故意睡觉」被 stall 误杀。
IngestAbort:落告警与暂停旗;修好环境后清旗再开(续跑脚本可在重启时清掉陈旧NETWORK暂停)。- 外部看门狗仍负责:心跳过期且 PID 存活 → 强杀僵活(与开路冷却互补,不是替代)。
反模式:为了熔断而熔断——把可恢复的 ProxyError 当成终态,三次失败就永久停。正确做法是开路保护远端与本机,恢复后合闸继续水位。
5. 故障模式目录(抽象)
下列「现象 → 机制 → 约定」可迁移到任何类似管道。括号内为触发该条的经验举例,不是施工流水账。
5.1 取数形状错误
| 现象 | 机制 | 约定 |
|---|---|---|
| 很忙、磁盘几乎不涨 | 全市场本可截面,却按码循环 | 探测后登记;财务默认 by_period+VIP |
| 指数/基金/周月极慢 | 误用 by_code | 能 trade_date/nav_date 则截面 |
| 改法后数据重复或对不上 | 旧 code= 与新 period= 并存 |
quarantine;迁移只认一种布局 |
5.2 观测误判
| 现象 | 机制 | 约定 |
|---|---|---|
i/N 走完仍 0 文件 |
早于 history_start 的空日 |
设起点;看 first non-empty |
| 连续空日触发熔断 | 起点写错或权限空洞 | 用实盘校正 history_start |
| 把「慢」当故障 | 小时级限频 | 慢批次 + 调整 watchdog |
5.3 运行时假死与误杀
| 现象 | 机制 | 约定 |
|---|---|---|
| 进程在、日志停 | 代理/DNS 挂起或假死 | 开路冷却+半开;直连优先;僵活由看门狗强杀 |
| 代理开一下又挂 | 本地代理端口闪断 | 勿三次失败即永久 pause;耗尽开路周期再硬停 |
| 心跳写入报文件占用 | 多 job 抢同一心跳文件 | 每 job 独立心跳 |
| 任务莫名 exit 0 / 秒退 | Shell 管道、Tee-Object、日志 Redirect 与脚本抢同一 path |
独立进程;日志路径分离 |
| 线程超时后行为怪异 | 调用线程未回收 | 优先同步调用 + 看门狗 |
| DuckDB「文件正在使用」 | 重复启动第二 writer | 单 writer;先确认旧 PID |
5.4 本地库写入
| 现象 | 机制 | 约定 |
|---|---|---|
SQL 在 limit 等词附近报错 |
标识符未引用 | 一律双引号 |
string → DOUBLE 转换失败 |
调试数据锁死列类型 | object→VARCHAR;冲突则重建 |
| 主键冲突 | 批次内重复行或错误 merge | drop_duplicates + INSERT OR REPLACE |
| DuckDB 文件锁 | 多进程写同一库 | 单 writer;迁移与拉取串行 |
5.5 计划与流程
| 现象 | 机制 | 约定 |
|---|---|---|
| 「表很多」实际漏下 | 策略有、批次无 | 覆盖对账断言 |
| 一阶段成功后整体停摆 | 把阶段出口当项目出口 | 流水线自动下一项 |
6. 存储与 DuckDB 约定
- 主事实在 DuckDB;Parquet 为湖/缓存,更新后应可迁入或 MERGE。
- 标识符双引号(schema / table / column)。
- 类型:pandas object/string →
VARCHAR;勿用偶然数值探测污染 schema。 - upsert:主键去重 + 替换语义;类型错误可 DROP 后按真实样本重建并记日志。
- 同一时刻一个 writer 打开主库文件。
7. 日更规则(设计约定)
- 刷新日历 / 必要 snapshot。
- 对
by_trade_date:只拉水位之后的交易日。 - 对
by_period:非日频;按披露或周更最近报告期。 - 对
by_code:仅观察池或强制按码表的增量区间。 - 写入湖分区并进入 DuckDB。
- 校验:当日行数量级、主键、与上一交易日对比告警。
- 日志保留策略确认行(
CONFIRM strategy=... api=... date_key=...)。
8. 操作约定(Windows 长任务)
- 用脱离交互会话的方式启动(如
cmd /c+Start-Process)。 - 业务日志与进程 Redirect 不得指向同一文件。
- Guard:
- 传输层 →
IngestCircuitOpen(冷却后半开重试);开路周期耗尽 →IngestAbort(NETWORK); - 空烧 / 错误风暴 / stall / 暂停旗 → 直接
IngestAbort。
- 传输层 →
- Watchdog:读心跳;过期且 PID 仍在 → 强杀;阈值大于最慢 API 间隔。
- 人工急停:创建暂停旗文件(本仓库:
state/INGEST_PAUSE)。 - 恢复:先读
ingest_alert.json的code;修根因(代理、策略、水位)后再清旗续跑。NETWORK在环境已通时清旗即可,不要无脑换策略重开。 - 告警落盘供事后阅读;禁止同一错策略反复空烧。
9. 检查清单
9.1 新表入库前
- 本账户探测完成并记录证据
- 写入策略矩阵(mode / api / date_key / history_start)
- 进入某个下载批次,或写入排除名单
- 覆盖对账差集为空
- 冒烟通过
9.2 开跑前
- 无暂停旗
- Watchdog 已挂且 stale 合理
- 代理策略明确(全量倾向直连)
- 若改过策略:旧分区已 quarantine,水位已重置
9.3 跑中判读
- 心跳年龄正常,且
writes/rows在增长(或已知限频下的预期节奏) - 日志出现
circuit OPEN/ 半开重试属预期;反复合闸失败才需要查代理 - 出现
!!! ABORT/ 告警文件 → 先读 code 再处置 - 勿把
i/N当入库行数 - 勿同时再起一个写 DuckDB 的迁移/入库进程
9.4 跑后
- 布局单一、可迁入 DuckDB
- 日更路径只走水位
- 流水线进入下一批次(除非人工暂停)
10. 铁律(摘要)
- 先形状,后速度。
- 先探测,后登记,后下载。
- 未登记策略禁止开跑。
- 能截面就不要全市场 by_code。
i/N≠ 入库。- 尊重
history_start。 - 改策略先清脏。
- 长任务可观测;熔断要有开路/半开,不是一断永停。
- 日更只用水位。
- 远端成功还要过本地写入关。
- 注册表与批次对账。
- 阶段成功后继续下一批;文件与数据库不要多进程互抢。
- 传输层可恢复;逻辑层硬停。分清再动手。
附录 A. 仓库与路径索引
仓库:https://github.com/jason9356/trader-data
| 用途 | 路径(相对仓库根) |
|---|---|
| 策略矩阵 | src/trader_data/ingest/pull_modes.py |
| A 股 Loader | src/trader_data/ingest/tushare_ashare.py |
| Pool / 批次 | src/trader_data/ingest/tushare_pool.py |
| Guard / Watchdog | guard.py、watchdog.py |
| DuckDB | src/trader_data/db/market.py |
| 注册表 | docs/tushare_full_registry.json |
| 日更记忆 | docs/tushare_ingest_lessons_and_update_rules.md |
| 数据根 | 本机目录(示意:<DATA_ROOT>/,下含 parquet/ db/ state/ quarantine/ logs/;勿写真实本机路径) |
附录 B. 文档来源说明
条文由 2026-07 末至 08-05 的一次本地全量建设中归纳。文中不绑定「当前是否跑完某张表」;账户权限、VIP 有无、限频数值以你自己的探测为准。密钥与本机绝对路径不进入对外正文。
版本:2026-08-05b(补充熔断开路/半开语义;澄清 NETWORK 与逻辑硬停的边界)