---
title: 自动缺口巡查
doc_type: operation-runbook
domain: data-sync
keywords:
  - 自动缺口巡查
  - startup sync
  - 补数
  - 缺口判定
summary: 说明运行时自动缺口巡查如何在启动后和后台周期内检查历史缺口、按正式同步口径补齐数据，以及它和正式调度任务的分工。
when_to_read:
  - 要修改启动补数逻辑
  - 想确认缺口判定和任务边界
read_next:
  - ../business/data-fetch-tasks.md
  - ../business/notice-market-pipeline.md
---
# 自动缺口巡查

这篇文档只说明一件事：桌面端如何在启动后和后台周期巡查里，按当前正式同步口径检查缺口数据，并在后台自动补齐。

当前范围只覆盖已经登记为桌面端正式业务同步链路的数据，不把原后端更宽的脚本检查集直接搬进来。

## 适用范围

- `tushare.trade_calendar.sync`
- `tushare.stock_basic_daily_basic.sync` 的历史快照语义
- `tushare.stk_factor_pro.sync`
- `tushare.limit_list_d.sync`
- `tushare.suspend_d.sync`
- `cninfo.notice.sync`
- `notice.filter`

明确不纳入这次自动缺口巡查的内容：

- `daily_basic`、独立 `adj_factor` 补数任务
- 港股 / 美股行情补数
- `notice.ai.analysis`
- 当日盘中今日行情刷新

## 当前正式语义

1. 自动缺口巡查在应用启动后立即执行一次；首轮恢复执行结束后，运行时每 `30` 分钟自动巡查一次。
2. 首轮失败时，后台只会在当前进程内按 `1 / 3 / 10` 分钟退避最多再试 3 次；任一成功即停止；恢复逻辑与周期巡查复用同一套实现。
3. 它不新增 provider 拉数逻辑，只复用现有 Rust 服务层已存在的按任务或按交易日同步能力。
4. 启动首轮以 `STARTUP` 作为任务触发来源写入运行轨迹；启动期自动恢复重试统一记为 `RECOVERY`；后台周期巡查统一记为 `SCHEDULE`。
5. 自动缺口巡查受 `sys_config.sync.auto_enabled` 控制；关闭后直接跳过。
6. 自动缺口巡查自身依赖的布尔配置必须是合法布尔值；`sync.auto_enabled` 或 `sync.notice.cninfo.enabled` 缺失、为空或写成非法字符串时直接 fail-fast，不再静默套默认值。

## 正式命名口径

从这篇 runbook 开始，行情补缺相关术语统一按“任务键 + API 名 + 落库表”表达，不再混用 `daily`、`stock basic`、`provider-daily`、`provider 日线`、`统一日线` 这类简称。

| 语义 | 正式任务键 | 单日快照状态键 | 正式落库表 |
| --- | --- | --- | --- |
| 股票基本信息 + `daily_basic` 市值标签单日快照 | `tushare.stock_basic_daily_basic.sync` | `tushare.stock_basic_daily_basic.snapshot:<market>:<trade_date>` | `stock_basic_info` + `api_tushare_daily_basic` |
| `stk_factor_pro` 单日快照 | `tushare.stk_factor_pro.sync` | `tushare.stk_factor_pro.snapshot:<market>:<trade_date>` | `api_tushare_stk_factor_pro` |
| `limit_list_d` 单日快照 | `tushare.limit_list_d.sync` | `tushare.limit_list_d.snapshot:<market>:<trade_date>` | `tushare_limit_list` |
| `suspend_d` 单日快照 | `tushare.suspend_d.sync` | `tushare.suspend_d.snapshot:<market>:<trade_date>` | `tushare_suspend_d` |

## 缺口判定规则

### 1. 交易日历

- 检查范围：仍按现有任务语义检查 `A / H / U` 交易日历是否存在整窗历史缺口。
- 检查市场：`A / H / U`。
- 判定方式：
  - 本地 `stock_trade_calendar` 在窗口内的最小日期必须等于窗口起点
  - 最大日期必须等于窗口终点
  - 行数必须等于自然日天数
- 只要任一市场不满足，就视为交易日历存在历史缺口，并按现有任务语义补跑整窗同步。

### 2. A 股股票基本信息 / 日线 / 涨停 / 停牌

- 目标日期：`本地交易日历 + 收盘时段 helper` 解析出的“最新已收盘交易日”。
- 巡查窗口：目标日期往前最近 `120` 个开放交易日。
- 不再使用“最早 T 日前推 3 个开放日”。
- 不再使用“空表最近 14 个开放交易日”。
- 所有行情底座数据域统一在固定窗口内巡查缺口：
  - `stock_basic + daily_basic` 单日快照：`tushare.stock_basic_daily_basic.snapshot:A:<trade_date>` + `api_tushare_daily_basic` 当日行
  - `stk_factor_pro` 单日快照：`tushare.stk_factor_pro.snapshot:A:<trade_date>` + `api_tushare_stk_factor_pro` 当日行
  - `limit_list_d` 单日快照：`tushare.limit_list_d.snapshot:A:<trade_date>` 或 `tushare_limit_list` 当日行
  - `suspend_d` 单日快照：`tushare.suspend_d.snapshot:A:<trade_date>` 或 `tushare_suspend_d` 当日行
- 缺口判定统一同时参考：
  - 本地交易日历开放日序列
  - 对应日期是否已 ready
  - 对应 `sync_status` 是否存在成功标记
- `stock_basic + daily_basic` 单日快照与 `stk_factor_pro` 单日快照现在按“目标交易日真实业务行已落库”判定 ready：如果某个交易日只有成功标记、但正式落库表里没有对应业务数据，就仍视为缺口，必须重跑。
- `stk_factor_pro` 单日快照的正式真值已收紧到 `api_tushare_stk_factor_pro`；即使同交易日存在其他来源的日线数据，也不能掩盖 `stk_factor_pro` 缺口。
- `limit_list_d / suspend_d` 单日快照因为允许某个交易日合法返回 0 行，所以按“成功标记或事件表已存在该交易日行”判定 ready；不能再只凭历史成功一次的松口径吞掉真实缺口。
- 如果某个交易日已经有成功标记、只是最近一次刷新失败，不再把它误判成“从未补齐”；但这条前提是该域 ready 判定本身仍成立。
- 如果最近 `120` 个开放交易日窗口超出本地交易日历覆盖范围，会先补 A 股交易日历，再重新解析巡查窗口；不在运行时静默降级成别的窗口。
- 如果本轮补到了 A 股 `stk_factor_pro` 单日快照，自动缺口巡查会在同一轮后台里立即重建对应窗口的 `stock_indicator_ema`，避免 `stk_factor_pro` 已补齐但自定义 EMA 仍留缺口。

### 4. CNInfo 公告

- 判定方式：读取 `sync_status.task_key = cninfo.notice.sync` 的 `last_cursor`。
- 巡查窗口：当前自然日往前最近 `30` 个自然日。
- 若 `last_cursor` 早于这 `30` 天窗口，则只补这 `30` 天窗口内的公告，不再自动追补更早历史。
- 若 `last_cursor` 已追平当前自然日，则仍重扫当前自然日，保证同一目标日内的补发公告还能被补进来。
- 若还没有成功游标，则按最近 `30` 个自然日建立公告窗口。
- 若本地状态已经标记过成功同步，但 `last_cursor` 缺失或格式非法，自动缺口巡查直接 fail-fast。
- 若最近一次同步失败，则继续保留上一次成功游标并等待下一次启动补偿 / 定时同步重试，不把窗口误判回 bootstrap。
- 若 `cninfo.enabled=true` 但 `cninfo.apiUrl / username / password` 缺任一字段，自动缺口巡查会沿用 `notice_pipeline` 的正式校验直接 fail-fast，不再带着半配置进入 CNInfo 抓取。
- 补齐入口仍走 `notice_pipeline`，保持“公告同步 -> 交易日历校验 -> t_date 回填 -> notice.filter”这一条正式后处理链。

### 5. 公告过滤重跑

- 若本次巡查只补了市场底座，没有触发公告窗口补齐，自动缺口巡查会额外重跑一次 `notice.filter`。
- 目的不是重新拉公告，而是给之前因其他缺口或历史 `SKIPPED` 停住、且仍处于待分析状态的记录一次恢复推进的机会；`2026-03-29` 起，涨停 / 停牌底座缺口本身不再新增这类 `SKIPPED`。

## 触发顺序

```mermaid
flowchart LR
    A["应用启动"] --> B["startup.sync.self_check 首轮"]
    B --> C["trade calendar / market foundation / notice gap check"]
    C --> D["scheduler.start()"]
    D --> E{"首轮失败?"}
    E -->|"是"| F["当前进程内 1 / 3 / 10 分钟退避恢复重试"]
    E -->|"否"| G["进入 30 分钟自动巡查 loop"]
    F --> G
    G --> H["每 30 分钟自动巡查一次"]
```

## 关键边界

- 自动缺口巡查不把页面层变成任务编排中心；全部逻辑都在 Rust 后台执行。
- 自动缺口巡查不在 notice 链里临时补拉未登记的数据域。
- 自动缺口巡查不会默认触发 `notice.ai.analysis`；公告分析仍保持现有独立任务语义。
- 普通用户不再通过设置页手动触发历史行情同步；前台只保留“系统会自动检查本地数据”的被动说明，必要时再引导检查数据源配置。
- 页面查询、交易日历读取和行情快照读取现在都只读本地正式结果，不再在读取链路里临时补拉或刷新 provider。
- `startup.sync.self_check` 现在会出现在任务中心，但它仍然是“运行时管理任务”，不是普通 cron 定时任务，也不再提供手动补数入口。
- 启动期恢复只在当前进程里生效；隐藏到托盘或重新打开窗口不会重新开启首轮，但后台 `6` 小时自动巡查仍会继续运行。
- 自动缺口巡查只补齐正式业务链路已承诺的数据，避免把原后端脚本里尚未正式接入桌面端的数据类型一起带进来。

## 与其他补偿机制的分工

| 机制 | 真实入口 | 负责什么 | 不负责什么 |
| --- | --- | --- | --- |
| `startup.sync.self_check` | 首轮：`run_task_once(..., TriggerSource::Startup)`；恢复：同任务键 + `TriggerSource::Recovery`；后台巡查：同任务键 + `TriggerSource::Schedule` | 启动后立即检查缺口；若首轮失败，当前进程内再按 `1 / 3 / 10` 分钟退避限次重试；之后每 `30` 分钟巡查一次。行情底座统一覆盖最近 `120` 个开放交易日，公告链统一覆盖最近 `30` 个自然日；若只补了市场底座但没补公告，再即时重跑一次 `notice.filter` | 不补盘中实时行情，也不替代普通 cron 的单日刷新 |
| 普通调度器 | `scheduler -> run_task_once()` | 按 cron 执行正式同步任务；当前 `stock.basic / daily / limit / suspend` 已收口为“最新已收盘交易日”的单日拉取 | 不做启动前的历史缺口扫描 |
| 手动补数入口 | 已下线 | 正式产品不再提供数据补录的手动触发路径 | 不再构成后台自动恢复模型 |

补充边界：

- `2026-03-29` 起，启动补偿和正式 cron 统一复用 `trade_calendar_utils::resolve_latest_completed_trade_date_for_datetime()` 解析 A 股目标交易日，收盘时段正式改为读取 `sys_config` 的 `market.trading.hours.a`；初始化默认值仍是 `15:00`，且“恰好等于收盘时间”继续视为前一已完成交易日。
- 原后端 T 日真值来自 `market.trading.hours.*` 配置；当前桌面端已经回到同一配置源。若 `market.trading.hours.a` 缺失或格式非法，启动补偿和依赖同一 helper 的正式 cron 会直接 fail fast，不再静默回退到硬编码时段。

## 任务通知口径

- 手动触发 `TriggerSource::Manual` 会保留完整任务完成通知，成功、跳过和失败都对用户可见，便于人工验收。
- 启动触发、定时任务、恢复任务和事件触发默认只在失败时发送系统通知，成功和跳过结果继续写入 `sys_scheduled_task_log`、`tsk_run` 与 `tsk_run_event`，但不再打断用户。
- 这条规则当前落在 [src-tauri/src/task_notification.rs](../../src-tauri/src/task_notification.rs) 的通知层，不依赖额外数据库字段，也不通过设置页单独配置。

## 验证口径

- Rust 单测覆盖：
  - 交易日历缺口判定
  - 最近 `120` 个开放交易日窗口解析
  - 窗口长度不足时的交易日历修复判定
  - 缺失 `sync_status` 与 ready 真值联合判定
  - CNInfo 最近 `30` 个自然日窗口推导
- 交付前仍按仓库统一入口执行：
  - `npm run check:constraints`
  - `npm run verify:logic`
