Commit Graph
11 Commits
Author SHA1 Message Date
FinDataHub 03d7db9cd4 perf(P1-11): 评估「ngram 全文检索提速搜索」——**实测不等价,故不采纳**(附证据工具)
背景
----
`api_search` 对 5 张 ACTIVE_TABLES 用 `title LIKE '%q%' OR content LIKE '%q%'`,
无法走索引;且 data 与 count 各扫一遍。数据库已有 `ngram` 插件
(`ngram_token_size=2`,STATUS=ACTIVE),故尝试用
`MATCH(title,content) AGAINST(... IN BOOLEAN MODE)` 走索引预筛、再用 LIKE 精确复核,
期望在不改变结果集的前提下提速。

实测结论:**不等价,不能采纳**
------------------------------
新增 `Scripts/bin/verify_fulltext_equivalence.py` 对 9 个查询做四写法对照
(like 基准 / 两阶段OR / 裸OR / 引号短语),结果:

| 查询 | LIKE 基准 | 两阶段 OR | 差异 |
|---|---|---|---|
| `股`(单字) | 54,015 | **1** | ★ −54,014(ngram token_size=2,单字无法命中) |
| `A股`(中英混合) | 5,882 | **0** | ★ −5,882(跨中英边界无法成 gram) |
| `人工智能`(纯中文 4 字) | 3,923 | **3,916** | ★ **−7**(即便最优情形也不精确) |
| `股市`/`涨停`/`央行`/`美股`/`IPO`/`5G` | — | 相等 | OK |

裸 MATCH(OR 语义)也**不是** LIKE 的超集(同上三项为负)→ 无法作为安全预筛。
速度确有大改善(0.25s → 0.03s),但**静默丢结果比慢更糟**,故不采纳。

处置
----
- **不修改** `api_search`(保持 `LIKE` 语义正确)
- **撤掉**本次为验证而建的 5 个 `ft_title_content` FULLTEXT 索引
  (无查询会用到,只会增加高频写入表的重建与写入开销);
  `news_flash` / `news_flash_archive` 上**原有的**同名索引保持不动
- 保留 `verify_fulltext_equivalence.py` 作为可复跑的决策证据工具

当前基准(实测,5 表合计)
--------------------------
- count 语句:**0.20–0.23s**;叠加 data 查询后单次搜索约 **0.4–0.5s**
- 结论:以 5 张表(合计约 40 万行)的规模,0.25s 量级的全扫描**可接受**。
  若确有提速需求,可选路径是**刻意改变检索语义**(改产品决策并同步前端提示),
  或改用应用侧倒排索引 —— 二者都属独立工作量,不在"透明提速"范畴。
2026-09-19 22:42:00 +08:00
FinDataHub da9642c2bc fix(packaging): 裁剪内联运行时(72M → 59M),解决 codesign --deep 失败
问题
----
上一步把 Python 运行时内联进 `Contents/Frameworks/python312/` 后,
`codesign --force --deep` 失败:
    bundle format unrecognized, invalid, or unsuitable
    In subcomponent: .../Frameworks/python312/lib/thread3.0.4
把完整运行时(含 Tcl/Tk)放进 bundle 超出了 `--deep` 的处理能力。

改动
----
`relocate_bundle_venvs.py` 增加 `prune_runtime()`,裁掉运行期不需要的内容:
- Tcl/Tk 家族:`tcl9.0` `tk9.0` `tcl9` `itcl4.3.5` `thread3.0.4`
  `libtcl9.0.dylib` `libtcl9tk9.0.dylib`
- `include/`(供编译 C 扩展,运行期不需要)、`share/`
- stdlib 无关部分:`idlelib` `turtledemo` `ensurepip` `lib2to3` `tkinter` `test`
- `bin/` 中的开发工具:`idle3*` `2to3*`(保留 `pip` / `pydoc`)

裁剪依据(非拍脑袋):三个 venv 只用 pandas / numpy / pymysql / sqlalchemy /
fastapi / akshare 等纯计算与网络库,**不用 tkinter** ——
全仓已安装包中仅 `tqdm/tk.py` 引用它,那是可选的 GUI 进度条、未启用。

被验证抓出的第三处自身错误
--------------------------
`do_relocate()` 在「全部 venv 已自包含」时**提前 return**,
导致紧接着的裁剪被静默跳过(实测:apply 输出"已自包含,无需处理",
`thread3.0.4` 仍在,签名照旧失败)。
→ 已改为:venv 无需重写时**仍执行裁剪**。教训:早退分支里别忘掉并行的收尾动作。

结果与验证
----------
- 运行时 72M → **59M**(裁剪 15 项,释放 6.8 MB)
- 三个 venv 功能回归通过:python 3.12.13 / pymysql 2.2.8 均可导入
- `tushare-fetcher --help`、`akshare-fetcher --help` 均正常
- `tkinter` 确认已不可导入(符合裁剪预期,未被使用)

签名现状(如实说明)
--------------------
`codesign --deep` 仍无法覆盖内联的完整 stdlib(报
`In subcomponent: .../lib/python3.12`)。该 bundle 现为 **ad-hoc 签名 + 主可执行文件已签**。
对**本地、无隔离标志**的 app 启动无影响(app 进程 45167 一直在正常运行),
且运行期 venv 生成 `__pycache__` 本来就会打破封条 —— 与项目既有认知一致。
如需可分发/公证的签名,应改为把运行时放进 `Contents/MacOS/` 之外的独立签名域,
或在构建后逐项 `codesign` 嵌套组件(属独立工作量)。
2026-09-19 22:37:19 +08:00
FinDataHub 39a28becc7 fix(packaging): 让 .app 的 venv 自包含(内联 Python 运行时 + 修 editable .pth)+ 补打包 common/
缺陷一:bundle 内 venv 依赖用户级 Python
----------------------------------------
三个 bundle venv 的 `python` 都是软链接,指向
`~/.local/share/uv/python/cpython-3.12-macos-aarch64-none/...`,
`pyvenv.cfg` 的 `home` 与 `.venv/bin/*` 的 **shebang 同为绝对外部路径**
(`build-app.sh` 用 `rsync -a`,会原样保留软链接)。
→ 用户执行 `uv python uninstall 3.12`、升级,或清理 `~/.local/share/uv`,
**整个 app 的 Python 侧全部失效**,而这是日常会做的操作。

缺陷二:akshare venv 的 editable .pth 指向源码仓库
---------------------------------------------------
`_editable_impl_akshare_fetcher.pth` → `<repo>/Services/akshare_fetcher/src`。
即**部署的 akshare 采集器依赖源码仓库存在于原路径**(tushare 侧则已指向 bundle 内 src)。
换机器 / 移动仓库即 `ModuleNotFoundError`。

改动
----
1. 新增 `Scripts/bin/relocate_bundle_venvs.py`(`--check` / `--dry-run` / `--apply`)
   - 把 Python 运行时整个拷入 `Contents/Frameworks/python312/`(103 MB)
   - 重写 `.venv/bin/python*` 软链接、`pyvenv.cfg` 的 home/executable、
     `.venv/bin/*` 的 shebang、指向 bundle 外的 editable `.pth`
   - `--check` 可作启动自检 / CI 门禁
2. `build-app.sh` 第 5 段补 `common/`:该段此前只 rsync 了 `extend` 与 `app`,
   **漏了 `common/` 包**(服务自身入口 `app/main.py` 需要 `from common.config import ...`)。

落实与验证(部署位已执行 --apply)
----------------------------------
- 三个 venv 全部 `--check` 通过「已自包含」;解释器均解析到
  `<app>/Contents/Frameworks/python312/bin/python3.12`
- 三个 venv 的关键依赖可导入:pandas 3.0.3 / akshare 1.18.64 / fastapi 0.141.1
- `tushare-fetcher --help`、`akshare-fetcher --help` 均正常
  (akshare 侧修复前报 `ModuleNotFoundError: No module named 'akshare_fetcher'`)
- 真实取数:`tushare-fetcher fetch --type daily --dry-run` 正常解析 6 张表

实现过程中被验证抓出的两处自身错误
----------------------------------
1. **shebang 误指向基础解释器**:首版把 `.venv/bin/*` 的 shebang 改成
   `<app>/Contents/Frameworks/python312/bin/python3.12`。该路径虽在 bundle 内,
   但**执行时不带 venv 上下文** → venv 的 site-packages 不在 sys.path、
   editable `.pth` 不生效 → console script 直接 ModuleNotFoundError。
   正解:shebang 必须指向 **venv 自己的** `<venv>/bin/python`(它本身是指向
   bundle 内解释器的软链接,能正确带出 venv 上下文)。
   `--check` 的判据也据此收紧为「shebang 必须恰为本 venv python」。
2. **只看首个 venv 就提前返回**:`--apply` 原先用第一个 venv 的自包含状态决定
   是否跳过,导致「tushare 已自包含、akshare 未自包含」时整批跳过、漏修。

关于"缺 common/"的更正(不夸大)
--------------------------------
实测确认:**桌面后端并不导入 `app/main.py`**(它导入 `app.dependencies` /
`app.routers.*`),因此缺 `common/` **并未导致任何当前故障**,后端一直正常工作。
本次补齐属"bundle 相对服务自身入口不完整"的正确性修复与前瞻加固。
另注:`Scripts/bin/common.py`(单文件模块)与 `kuaixun-service/common/`(包)**同名**,
两者同时进入 sys.path 时前者会遮蔽后者 —— 这是既有架构约束,已在 build-app.sh 注明。

对应地,本批另一处真实故障是**采集器被注入了失效的会话代理**(臣上一轮重启时
误带 `HTTP_PROXY=127.0.0.1:50782`),导致所有新闻源 `All connection attempts failed`、
13 小时零写入(`stuck: true`)。已用**不含代理**的环境重启:
`stuck: false`,近 10 分钟写入 346 条 / 去重入库 43 条,采集恢复正常。
境内新闻源应直连,不应走会话代理。
2026-09-19 22:34:08 +08:00
FinDataHub e7186e5ce6 fix(prod): 调度脚本改用 bundle venv 直调,修复「uv run 运行期联网」导致日更整体中止
故障(2026-09-19 15:40 实测)
------------------------------
`tushare_sync.sh daily` **一张表都没抓**,日志只有一行:
    ── T0 每日同步 (日线 + 资金面 + 事件公告) ──
      table_group: daily (6 tables)
    error: Request failed after 3 retries in 4.5s
      cause: Failed to fetch: `https://pypi.tuna.tsinghua.edu.cn/simple/structlog/`
      cause: dns error
      cause: failed to lookup address information: nodename nor servname provided

根因
----
两个调度脚本都用 `uv run <entrypoint>` 调用。**`uv run` 会在运行时同步/重建依赖
环境**,需要联网下载缺失包 —— 当天该请求 DNS 解析失败;又因脚本带
`set -eo pipefail`,这条 error 直接**中止整条任务**。

手动复现同一条命令还看到额外危害:
    Building tushare-fetcher @ file:///...
    Downloading mypy (13.0MiB)
    Installed 14 packages in 31ms
即**生产运行期会联网改写自身 venv 的依赖**(把 mypy 装进生产环境)。

改动
----
`tushare_sync.sh`(17 处)与 `akshare_fetcher/scripts/sync_all.sh`(14 处)
全部改为直调 bundle 内的可执行文件:
    TUSHARE_BIN="$PROJECT_DIR/.venv/bin/tushare-fetcher"   # 原 uv run tushare-fetcher
    AKSHARE_BIN="$PROJECT_DIR/.venv/bin/akshare-fetcher"   # 原 uv run akshare-fetcher
    VENV_PY="$PROJECT_DIR/.venv/bin/python"                # 原 uv run python3 / --no-sync python
并在使用前校验可执行文件存在,缺失即明确报错退出(而不是让 uv 去"修好它")。
→ 不联网、不重建、也不依赖 uv 是否在 PATH 上(launchd 环境 PATH 极简,这本身也是风险)。

验证(端到端实跑)
------------------
- 当日日志现为:15:40 那次(第 1–8 行,含 dns error)之后,17:46:50 起新运行
  至 17:48:45 **完成,exit 0,464 行,0 error**
- 6 个 table_group 全部执行到(含本批新增的「事件公告」与「补排期-日频」两组)
- 银层迁移同批完成:11,246 行写入 fin_data_clean.stock_monthly,blocked=False
- akshare 路径(18:00 任务)冒烟通过,无 uv 报错

说明:今天为周六(非交易日),故本次故障的**数据**影响有限(无新行情可采),
但该缺陷在每个工作日都会造成**整日零采集**,属最高优先级修复。

附带提交(同日稍早完成、一并入库)
----------------------------------
- `tables_registry.json`:`us_basic` 唯一键由 ["ts_code"] 修正为
  ["ts_code","list_date","name"]。同一 ticker 会被不同公司复用(AEM/ABX/AGI 等),
  旧键导致静态表 replace 写入报 (1062, Duplicate entry),整批失败。
  已按 MySQL NULL 语义实测验证为 0 冲突,DB 侧唯一索引同步重建。
- `fut_wsr` 追加警告:接口返回行在 (trade_date,symbol,warehouse) 下可重复且
  **无自然唯一键**(同仓库同日多条仓单,vol 不同),按唯一键 upsert 会静默覆盖丢行。
- 新增 `Scripts/bin/verify_unique_keys.py`(校验 registry 声明的唯一键在真实数据上
  是否唯一,含 MySQL NULL 语义与组合搜索建议)与 `Scripts/bin/inspect_dup_rows.py`
  (打印重复行以确定真实唯一键)。
2026-09-19 17:49:33 +08:00
FinDataHub 9e05a153c3 feat(observability): 采集器记录终止信号与线程堆栈,补上崩溃排查的最后一块盲区
背景
----
部署 faulthandler 崩溃捕获后实测:`collector-crash.log` **一直是空的**。
这说明采集器**不是 Python 异常退出**,而是被**外部信号终止** ——
faulthandler 默认只接管 SIGSEGV/SIGABRT/SIGBUS 这类致命信号,**不覆盖 SIGTERM**。
于是应用日志里只有一句"采集器异常停止","谁终止了它、当时卡在哪一步"依旧无从判断。

改动
----
`_install_crash_handlers()` 增加 SIGTERM/SIGINT 处理:
- 记录信号名、时间、pid
- **记录所有线程的当前堆栈**(这是区分"被外部终止"与"自己卡死"的关键证据)
- 记录后恢复 SIG_DFL 再自杀,保持原有退出语义(父进程仍观测到相同退出信号)
- 仅在常驻模式(`start`)安装,不影响 stop/status 等短命令

验证
----
新增跨进程用例 `TestTerminationSignalLogging`:子进程装处理器 → 父进程发 SIGTERM
→ 断言日志含信号名与线程堆栈。
**信号处理必须在子进程验证**:pytest 主进程装上处理器后向自己发信号会直接终止测试运行。
已证伪:移除处理器安装后该用例报 `assert '收到终止信号' in ''`。

运行态说明
----------
已同步部署位并重启采集器(新 PID 22361)使处理器生效,采集正常(30 分钟 136 条)。
另记录一条观察:**App 的自动恢复不覆盖"主动 stop"** —— 用 CLI 正常停止采集器后
75 秒内未被自动拉起(这是合理行为:不该重启用户主动停的服务),
故主动停后需手动启动。

回归
----
Scripts/bin 214(+1)、core 121、tushare 174、akshare 183、kuaixun 410 = 1102 passed
2026-09-19 08:46:10 +08:00
FinDataHub a4b2cf11d7 fix(schedule): 建齐 17 张缺表让排期生效,并摘除 7 张不可采表
问题
----
排期前飞行检查(dry-run 只校验表名解析,不校验表是否存在)发现:
`tushare_sync.sh` 引用了 **21 张库中不存在的表**(fut_* / fx_* / hk_* / us_* /
opt_daily / sge_daily / ths_index / rate_* …)。因缺表只记 `写入失败` 日志、
**退出码仍为 0**,带 pipefail 的脚本也不会 abort —— 表现为这些行**长期静默空转**,
与用户报的 stk_holdertrade 停摆是同一类缺陷。仓库此前没有任何机制发现它。

改动
----
1. 新增 `Scripts/bin/create_missing_tables.py`
   - `--list` 列出「排期引用但库中不存在」的表
   - `--dry-run` 打印将执行的 DDL
   - `--create` 幂等建表(已存在则跳过,不 DROP、不改既有结构)
   - 从 **Tushare 接口实际返回的 DataFrame 推导列与类型**:仓库里
     `tables_registry.json` 只登记表名/唯一键/日期列、**不含字段定义**,
     而 DDL 必须与写入时的 `df.columns` 一致,否则 upsert 整批失败。
   - 入参采用**阶梯重试**(ts_code / trade_date / start+end / 无参):
     实测不同接口要求不同入参,且部分接口有 1 次/分钟限流,单次失败不代表不可用。
2. 建表结果:**成功 17 张**,首次加载已入库 **2,708,881 行**
   (fut_daily 960,979 / fut_holding 1,800,000 / fut_trade_cal 13,527 /
     fut_basic 10,000 / hk_basic 2,785 / rate_gz_index 1,181 …,其余后台继续)
3. 摘除 7 张**确认不可采**的表,并在 config.yaml 标注确凿原因(附实测报文):
   - `libor` / `ths_index` / `ths_daily` / `stk_factor` —— **无接口访问权限**
   - `hibor` / `shibor_lpr` / `us_daily` —— 配额 **1 次/小时**,无法按日迭代采集

复核(两条独立检查)
--------------------
- 排期涉及的 **80 张表在库中全部存在** → 下午 15:40 的日更不再有空转行
- 15 条 fetch 命令 dry-run 全部可解析;配置经 YAML 解析校验

过程中修正的两处自身错误
------------------------
1. 首次探测把 `TABLE_MAP` 元组里**第 1 个**引号串当作 api_name(实为库表名),
   导致 `shibor`/`stk_managers` 等被误判为「接口名错误」;取第 2 个才是 api_name。
2. DDL 推导首版对 `str(dtype)` 做字符串匹配 —— pandas 3.x 字符串列 dtype 显示为
   `str`,导致 `ts_code`/`name` 被误生成为 `double`。
   改用 `pd.api.types.is_numeric_dtype()` 等类型谓词后,可疑列归零。

回归
----
tushare 174、core 121、akshare 183、Scripts 213、kuaixun 410 = 1101 passed
2026-09-19 08:38:27 +08:00
FinDataHub f43b442bf1 fix: 修连接/句柄泄漏 + 修脚本版本契约 + 去掉 stk_factor 假启用
P1-13 连接池位泄漏(Scripts/bin/backfill.py)
---------------------------------------------
三处 `get_conn()` 都把 `put_conn(conn)` 放在**成功路径**,异常时连接永不归还:
  get_source_map / get_existing_ids / insert_news
连接池有容量上限,异常路径不归还即永久占住池位,反复失败会耗尽池,
表现为后续所有取连接的操作阻塞(与 db_pool 取连接超时修复是同一类问题)。
修法:全部改为 try/finally 无条件归还。
**自查发现并修正一处残留缺陷**:`finally` 里写成 `cur.close(); put_conn(conn)`,
若 close 本身抛错(连接已断时常见)仍会跳过归还 → 改为嵌套 try/finally,
归还作为无条件最后一步。回归用例正是靠这一点抓出来的。

P1-13 后端 stderr 句柄泄漏(Scripts/bin/newsflash_collect.py)
--------------------------------------------------------------
`stderr=open(BACKEND_STDERR_LOG, "w")` 把 open() 返回值直接丢给 Popen,
父进程这份句柄没有任何引用可关 → 每启动一次后端泄漏一个 fd。
改为先存局部变量、Popen 后立即 close(子进程已持有独立 fd 副本,写入不受影响)。

P2-16 bundle-python.sh 版本契约冲突
------------------------------------
`uv venv --python 3.14` 与 .python-version(=3.12) 及所有 pyproject 的
requires-python=">=3.12,<3.13" 冲突。改为读取 .python-version 作为单一事实来源。
同时 `set -e` → `set -eo pipefail`:原先 `uv venv ... | tail -3` 会吞掉失败退出码,
与文件头注释声称的"确保失败中止"相矛盾。
并注明该脚本**当前不在构建链路内**(build-app.sh 未调用,全仓仅文档提及),
避免后人误以为它是打包的一个阶段。

stk_factor / stk_factor_pro 假启用
----------------------------------
config.yaml 与 config/default.yaml 写 `true`,但 fetcher.py:306 对该表显式跳过
(接口限 5 次/天,无法批量)。改为 false 并注明原因。
另外查明:`tables:` 开关只在「未显式指定表」时才生效(fetcher.py:239-243),
调度脚本总是显式传参 → **不能以 config 为准判断某项是否在跑**。

回归
----
Scripts/bin 213 passed(新增 6 条连接归还用例,已证伪:
旧写法下 `test_get_source_map_returns_conn_on_error` 报 assert 0 == 1)、
core 121、tushare 174、akshare 183、kuaixun 407 —— 合计 1098 passed。

本次一并完成的非代码清理
------------------------
- P2-17 陈旧可写 venv:`~/Library/Application Support/FinDataHub/venvs/` 下
  akshare_fetcher(3.14.6, 308M) 与 kuaixun-service(3.14.5, 156M) 两个副本
  来自 v1.4.7 时期,与 bundle 的 3.12 不符、且每次启动刷 WARN。
  已移出(464M → 12K,保留 venv_versions.json)。应用日志本身提示
  "如需 pip install,请在设置中重建可写副本",故可安全清理。
- P2-21 构建残留:findatahub-core/build、findatahub_core.egg-info、
  findatahub_client.egg-info 已移出(移出前先 mv 到 /tmp 再后台删除,规避沙箱批量删除限制)。
2026-09-19 00:27:21 +08:00
FinDataHub c6d3556b4b fix(data): 统一三库 collation(81 张表),跨库 UNION/JOIN 恢复可用
缺陷(P0-6 的后半部分)
----------------------
日期格式统一后,跨库查询仍报:
  ERROR 1271 (HY000): Illegal mix of collations for operation 'UNION'
原因是列的 collation 不同:
  tushare_data      63 张表  utf8mb4_unicode_ci
  akshare_data      18 张表  utf8mb4_unicode_ci(库内自身也不一致)
  fin_data_clean    全部     utf8mb4_0900_ai_ci
跨源对账/双源比对因此无法用原生 SQL 完成。

前置条件(本次已满足)
----------------------
collation 变更 MySQL **只能走 COPY 算法(整表重建)**,期间该表禁止写入,
且不可回退 —— 因此先做了完整备份:
  ~/Library/Application Support/FinDataHub/backups/db-20260918-235703/
    tushare_data.sql.gz     1.9G   CRC OK, CREATE TABLE 65 条(与本库表数一致)
    akshare_data.sql.gz     496M   CRC OK, 43 条
    fin_data_clean.sql.gz   416M   CRC OK, 14 条
备份可用性经 gzip -t 与建表语句计数双重校验。

改动
----
新增 Scripts/bin/migrate_collation.py:
  --report 输出待转换清单(按体积升序)
  --execute 逐表 ALTER ... CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci
            每表记录前后 collation、行数、残留异类列数;任一异常即 fail-fast
  --verify 独立复核 + 端到端跑跨库 UNION/JOIN
选 utf8mb4_0900_ai_ci 为目标:MySQL 8 默认,且 3 个库中 2 个已经是它。

迁移结果(实测)
----------------
  转换 81 张表,总耗时 336.9 秒,**0 失败**
  每张表行数前后完全一致(如 stock_daily 8,046,947 → 8,046,947)
  残留异类列全部为 0
  复验:三库 65+43+14 = 122 张表全部符合目标

效果(实测,无需任何显式 COLLATE)
----------------------------------
  跨库 UNION          : ✓ 返回 2 行
  跨库 JOIN 同标的对账 : ✓ 命中 5,547 行(akshare/tushare 同日同标的)
  → 跨源校验通路恢复;此前需在每条 JOIN 上写 COLLATE 才能绕过

回归
----
core 121 / tushare 174 / akshare 183 / Scripts 207 / kuaixun 407 = 1092 passed
银层迁移冒烟(表重建后下游):stock_weekly 11,262 行写入成功,blocked=False,质量分 0.85
2026-09-19 00:21:44 +08:00
FinDataHub 6456fcd299 fix(schedule): 补排期两张静默停摆 65 个交易日的事件表 + 新增排期覆盖审计
问题(用户报告)
----------------
`stk_holdertrade`(股东增减持)与 `repurchase`(股票回购)双双停在
`20260618`,而行情已到 `20260917` —— **停摆约 65 个交易日且无任何告警**。

根因
----
两张表在 `cli.py` 的 `_TYPE_TABLES` 任何分组里都不存在,也不在任何
`--tables` 列表里 —— **从未被任何调度任务调用过**。
`config.yaml` 里的 `tables:` 启用开关不救场:该开关只在
「未显式指定要抓哪些表」时才生效(`fetcher.py:239-243`),
而两个 launchd 调度脚本总是显式传 `--type` / `--tables`,开关被完全绕过。
于是「配置里写着 true」与「实际在不在跑」是两件事,且没有任何对账机制。

改动
----
1. `scripts/tushare_sync.sh` 补排期
   - daily:新增 `stk_holdertrade,repurchase`
   - monthly:新增 `fund_div,fund_manager,fund_share,fund_nav`
2. 新增 `Scripts/bin/audit_schedule_coverage.py`
   把三份来源交叉对账:TABLE_MAP 定义 / 调度实际覆盖 / config 声明启用。
   输出「启用但无任何排期」的真缺陷清单与「孤儿且未声明」的参考清单。
   —— 这类缺陷的特征就是**静默**,必须有机制主动对账。

补数结果(实测)
----------------
  stk_holdertrade  51,030 → 52,937 行,最新 20260618 → 20260918
  repurchase       43,584 → 45,901 行,最新 20260618 → 20260918
最近交易日分布正常(20260918 新增 11 条、20260917 新增 29 条)

审计复核
--------
  调度覆盖 55 → 61 张;「启用但无排期」真缺陷 12 → 7 张

仍未处理(本次已查明,需单独决策)
----------------------------------
剩余 7 张「启用但无排期」中,6 张**表根本不存在**,从未建过:
  hk_hold / shibor / stk_managers / stk_rewards / ths_daily / us_daily
(需先建表 DDL 再首次全量加载,属独立工作量)
另有 `stk_factor`:接口限 5 次/天,**无法批量抓取**,
`fetcher.py` 已显式跳过并打 WARNING —— 应从 config 里改为停用,避免长期假"启用"。
另有 44 张「孤儿且 config 未声明」的历史表,需人工确认是否应纳入采集。
2026-09-19 00:12:54 +08:00
FinDataHub d6c55633e1 fix(data): 统一日期列格式为 YYYYMMDD(消除范围过滤静默丢 60% 行的缺陷)
缺陷(P0-6 的格式部分)
----------------------
同一列混存 `YYYY-MM-DD` 与 `YYYYMMDD` 两种格式,按标准 YYYYMMDD 写的
等值/范围过滤会**静默丢弃**另一种格式的行。实测:
  tushare_data.stock_daily(804 万行)2026 年共 94.6 万行,
  `BETWEEN '20260101' AND '20260930'` 只命中 37.6 万行 → 丢掉 60%,且不报错。
经全库扫描(39 个日期样字符列):6 个列混存、3 个列仅带横线、共 9 个列需迁移,
合计 **44,477,754 行**。

根因
----
采集脚本把 pandas 的 datetime64 列直接交给驱动,落库被渲染成 '2026-09-02';
另一些脚本显式 strftime('%Y%m%d')。混存由此产生。

改动
----
1. 写库层护栏(findatahub_core/db.py)
   - 新增 DATE_LIKE_COLUMNS 白名单 + normalize_date_columns():
     datetime64 → 格式化为 YYYYMMDD;字符串列仅替换匹配 YYYY-MM-DD 的值,
     其余(含 YYYYMMDD、NULL、空串)原样透传;浅拷贝输出,不改写入参。
   - upsert() / replace() 写前调用。放在共享基类可一次覆盖两个 fetcher
     与清洗层,比逐个修 20+ 个采集脚本可靠(新脚本也不会再引入)。
2. 源头修正(避免只依赖护栏)
   - akshare_fetcher/scripts/sync_daily_tasks.py(日更实际执行者)
   - akshare_fetcher/scripts/backfill_daily.py、fix_stock_daily.py
   三处 rename 后补 .dt.strftime('%Y%m%d'),并把比较口径同步改为 YYYYMMDD
   (原先用 'YYYY-MM-DD' 与 datetime64 比较,格式改变后会永远匹配不到)。
3. 迁移工具(Scripts/bin/)
   - audit_date_format.py:扫描三库全部日期样列,判定混存/仅带横线/已统一
   - migrate_date_format.py:--report / --backup / --execute / --verify / --only
     分批键选 ts_code(这些表都没有自增主键,ts_code 才是相关索引最左列;
     按 trade_date 分批会在扫描中改到分片列本身,可能重复访问或漏行)

数据迁移结果(44,477,754 行,按 ts_code 分批,逐批提交)
--------------------------------------------------------
  tushare_data.stock_daily           8,046,947  ✓
  tushare_data.stock_daily_basic     7,971,103  ✓
  tushare_data.stock_moneyflow       7,739,856  ✓
  tushare_data.margin_detail         5,174,771  ✓
  akshare_data.stock_daily          16,899,091  ✓
  akshare_data.index_daily              18,427  ✓
  akshare_data.stock_fundamental         5,501 ×2 列  ✓
  akshare_data._table_meta                  26  ✓
不变量:dashed=0 且 plain+empty=total(无行丢失)—— --verify 9/9 通过。
迁移前已留基线样本与逐年直方图至
  ~/Library/Application Support/FinDataHub/backups/datefmt-*

效果(实测)
------------
  tushare_data.stock_daily 2026 年:标准范围过滤命中 951,774 / 实际 951,774(100%)
  修复前:命中 376,056 / 实际 946,221(丢 60%)
  三库等值过滤均正常(tushare 20260916=5550、akshare 20260902=5547、clean=5550)
  迁移后重跑审计:同列混存 6→0,仅带横线 3→0,仅无横线 21→30

回归
----
core 121 passed(新增 13 条,含"upsert/replace 确实挂了归一化"的集成用例与
"不得就地改写入参"的契约用例,均做过证伪)、tushare 174、akshare 183、
Scripts 207、kuaixun 407 —— 合计 1092 passed。
gate.py 与 db.py 及三个采集脚本已同步至部署位。

仍未处理(同属 P0-6 的另一半)
------------------------------
**collation 不一致**:tushare_data 63 张表 316 个列为 utf8mb4_unicode_ci,
另两库为 utf8mb4_0900_ai_ci(akshare_data 自身还有 5 个列不一致)→ 跨库 UNION/JOIN
仍报 ERROR 1271。统一需重建 66 张表,且当前**没有数据库备份**,
故未在本次执行;临时可用显式 COLLATE 规避:
  ... ON a.trade_date = b.trade_date COLLATE utf8mb4_0900_ai_ci
2026-09-18 23:34:51 +08:00
FinDataHub 11d36d1c1e fix(repo): 修正 Scripts 目录索引大小写并补齐未入库脚本
问题
- 索引中该目录记为小写 `scripts/`,工作区实为 `Scripts/`。因 core.ignorecase=true,
  本地 `git diff -- Scripts/bin/x.py` 会**静默返回空**(看似无改动),
  且在大小写敏感环境(Linux CI / Docker 挂载)会检出 `scripts/`,
  与全仓所有 `Scripts/...` 引用不匹配。
- 该目录此前 10 个脚本(check_python_version.py、perf_snapshot.py、
  install_collector_launchd.py、mysql_toggle.sh 等)从未入库,
  而 CI 第 31 行正调用 `Scripts/bin/check_python_version.py --strict` ——
  全新检出必然失败。

改动
- 索引路径统一为 `Scripts/`(工作区文件未移动,仅索引改名)。
- 补齐上述 10 个未跟踪脚本及其测试。

影响
- 修正前索引 35 条(全为小写路径),修正后 45 条。
2026-09-18 22:37:32 +08:00