文档性质:可复用的技术约定(从一次真实建设中抽离、固化),不是施工日志。

适用:自建 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 要解决什么

把权限内、对研究有用的 日频 / 截面 / 报告期 数据可靠落到本地,并具备:

  1. 按表分流的拉取形状(禁止一种循环套所有 API);
  2. 可中断、可续跑、可熔断 的长任务运行模型;
  3. 湖(Parquet)+ 主库(DuckDB) 一致的布局与写入约定;
  4. 水位增量更新 能力(全量与日更分离)。

1.2 明确不做 / 慢做

类别 约定
1 分钟全市场(如 stk_mins 默认排除,不进下载批次
小时级限频接口(如部分 hk_daily 账户档) 仍进计划,独立慢批次 + 拉长调用间隔
强制 ts_code 的重接口(如部分筹码) 可进计划,但接受墙钟很长;勿伪装成按日截面

1.3 文档怎么用

  • 开新表 / 改下载:先查 §3 策略模型§8 检查清单
  • 出故障:先查 §5 故障模式;熔断行为见 §4.6
  • 日更:查 §7
  • 具体表名与探测结果以本仓库策略矩阵为准;换账户必须重探测。

2. 设计原则

  1. 形状优先于速度​错误循环浪费的配额与时间,通常远大于限速偏保守。
  2. 对本账户探测,不死抄文档示例​文档参数能调通 ≠ 本积分档下的最优拉法(例如是否有 *_vip、是否支持仅 trade_date=)。
  3. 策略未登记 = 禁止开跑​禁止「未知表默认按日扫」。
  4. 进度遍历 ≠ 入库成功i/N 只表示走过的日/码/期;有效性看分区、行数、首次非空。
  5. 长任务必须可观测、可熔断——且熔断要有恢复语义​心跳 + 进程内阈值 + 外部看门狗。熔断不是「写暂停旗然后等死」:传输层故障应开路冷却、半开探测;只有逻辑错或冷却耗尽才硬停。禁止「日志停了再等一晚」。
  6. 改策略先清脏状态​水位字段、目录布局(code= vs period=)与策略绑定;切换时隔离旧分区。
  7. 注册表与下载计划必须对账​「策略里有」≠「批次里会跑」。差集必须为空(除显式排除项)。
  8. 阶段完成 ≠ 项目可停​批次流水线默认进入下一项;人工停靠 INGEST_PAUSE,而不是任务成功后干等。
  9. 远端成功 ≠ 本地成功
    标识符、类型、主键、文件锁、多 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 实际方法名(逻辑名 incomeincome_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,绝不写入文档或仓库):

  1. 只传 trade_date=(或 nav_date= / ann_date=)能否返回多证券?→ 倾向 by_trade_date
  2. 只传 period= / end_date=(季末)能否返回多证券?有无 *_vip?→ 倾向 by_period
  3. ts_code 是否报「必须传证券代码」?→ 只能 by_code
  4. 无参或极少参数是否整表?→ snapshot
  5. 记录首次非空的大致日期 → 写入 history_start

3.5 常见正确形状(模式级,表名因账户而异)

  • 报告期 VIP / 截面:利润表、资产负债表、现金流、财务指标、业绩快报预告、分红等(有 VIP 用 VIP)。
  • 无 VIP 但可 period=:部分股东名单等。
  • 按日(或等价日键)截面:日线族、多数微观结构/资金流、周线月线全市场、基金日线/净值日、指数日线全市场等。
  • 强制按码:筹码分布等必须 ts_code 的接口。
  • 整表stock_basictrade_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 批次编排(逻辑顺序)

  1. Meta / 日线主链
  2. 财务 by_period
  3. 市场广度截面
  4. 跨品种日频
  5. 维表 snapshot
  6. 结构类(周月、基金截面等)
  7. 强制 by_code 重表
  8. 小时级限频(最后,避免占满注意力)

启动前断言:

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 约定

  1. 主事实在 DuckDB;Parquet 为湖/缓存,更新后应可迁入或 MERGE。
  2. 标识符双引号(schema / table / column)。
  3. 类型:pandas object/string → VARCHAR;勿用偶然数值探测污染 schema。
  4. upsert:主键去重 + 替换语义;类型错误可 DROP 后按真实样本重建并记日志。
  5. 同一时刻一个 writer 打开主库文件。

7. 日更规则(设计约定)

  1. 刷新日历 / 必要 snapshot。
  2. by_trade_date:只拉水位之后的交易日。
  3. by_period:非日频;按披露或周更最近报告期。
  4. by_code:仅观察池或强制按码表的增量区间。
  5. 写入湖分区并进入 DuckDB。
  6. 校验:当日行数量级、主键、与上一交易日对比告警。
  7. 日志保留策略确认行(CONFIRM strategy=... api=... date_key=...)。

8. 操作约定(Windows 长任务)

  1. 用脱离交互会话的方式启动(如 cmd /c + Start-Process)。
  2. 业务日志与进程 Redirect 不得指向同一文件
  3. Guard:
    • 传输层 → IngestCircuitOpen(冷却后半开重试);开路周期耗尽 → IngestAbort(NETWORK)
    • 空烧 / 错误风暴 / stall / 暂停旗 → 直接 IngestAbort
  4. Watchdog:读心跳;过期且 PID 仍在 → 强杀;阈值大于最慢 API 间隔。
  5. 人工急停:创建暂停旗文件(本仓库:state/INGEST_PAUSE)。
  6. 恢复:先读 ingest_alert.jsoncode;修根因(代理、策略、水位)后再清旗续跑。NETWORK 在环境已通时清旗即可,不要无脑换策略重开。
  7. 告警落盘供事后阅读;禁止同一错策略反复空烧。

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. 铁律(摘要)

  1. 先形状,后速度。
  2. 先探测,后登记,后下载。
  3. 未登记策略禁止开跑。
  4. 能截面就不要全市场 by_code。
  5. i/N ≠ 入库。
  6. 尊重 history_start
  7. 改策略先清脏。
  8. 长任务可观测;熔断要有开路/半开,不是一断永停。
  9. 日更只用水位。
  10. 远端成功还要过本地写入关。
  11. 注册表与批次对账。
  12. 阶段成功后继续下一批;文件与数据库不要多进程互抢。
  13. 传输层可恢复;逻辑层硬停。分清再动手。

附录 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.pywatchdog.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 与逻辑硬停的边界)