diff --git a/docs/第一批联调验证清单_2026-07-26.md b/docs/第一批联调验证清单_2026-07-26.md new file mode 100644 index 0000000..9264e3a --- /dev/null +++ b/docs/第一批联调验证清单_2026-07-26.md @@ -0,0 +1,355 @@ +# 第一批 · 联调验证清单(v2,2026-07-26) + +> **本清单取代 `第一批验证清单_2026-07-26.md`**(那份把「基座机器」和「桥机器」混为一谈, +> 第 1 步的 `docker exec -i akg-postgres psql` 在桥的服务器上根本不存在这个容器)。 +> +> 这一批跨 **三个工程 / 至少两台机器**,所以每一步都标注了「在哪台机器、哪个工程、 +> 哪个容器里执行」,以及判定标准、失败去哪、怎么回滚。**不要跨节跳着做**—— +> 节与节之间有依赖顺序。 + +--- + +## §0 这一批到底在联调什么 + +| 工程 | 本批是否改动 | 改了什么 | 谁消费 | +|---|---|---|---| +| **astock-kg(基座)** | **只改 DB 里的 4 个视图 + 1 个可空列 + 1 个索引,代码零改动** | `sql/astock_kg_slot_views.sql`(文件在桥仓库,DDL 落在基座库) | 只有桥 | +| **akg-factor-bridge(桥)** | 代码全批 | 四路口径 / 事件过滤 / 分块 / 冻结 / probe corr | —— | +| **quant_factor_service(平台)** | 无改动 | 桥往 `t_factor_akg_*` + `factor_metadata` 写行 | 平台评价与下游 | + +**耦合面只有一个**:桥代码 ↔ 基座库里那 4 个视图。其余都是各自内部的事。 + +--- + +## §1 拓扑登记(**请你填,我不知道**,填完发我一份,后面所有命令按它对号入座) + +| 角色 | 主机 / 用户 | 容器名 | 本批需要的权限 | 已确认可达? | +|---|---|---|---|---| +| 基座 PG | `______` | `akg-postgres` | **DDL**(建视图/加列/建索引) | ☐ | +| 基座 backend/beat | 同上 | `akg-backend` / beat | 本批不动 | —— | +| 桥 | `factor@factorevaluation:~/project/akg-factor-bridge` | `akg_factor_bridge` | 读基座 PG / 读 153 / 写平台 MySQL | ☐ | +| 153 代理(热度) | `______` | —— | 只读 | ☐ | +| 平台因子库 + `gp_day_data` | `______` | —— | **写** `t_factor_akg_*`、`factor_metadata` | ☐ | +| 开发机 | `baobao@…/Documents/work/project/` | —— | git 源头 | —— | + +> **关于 DDL 账号**:基座 compose 里 `POSTGRES_USER: ${PG_USER:-akg}` —— 这个账号就是 +> 库的 owner,**天然有 DDL 权限**。如果桥 `.env` 的 `AKG_PG_USER` 就是它,§5 直接能跑; +> 如果你另外建了只读账号给桥用,§5 需要临时用 owner 账号。 + +--- + +## §2 已代查的前置事实(**你不需要再验证**,列出来是为了让你知道风险面有多大) + +1. **改这 4 个视图的影响面 = 0**。`grep -rn "v_factor_" backend/ frontend/src` 在 astock-kg + 里**零命中** —— 四视图目前只有桥在消费,替换它们不会波及基座任何功能。 +2. **`ALTER TABLE transmission_candidates ADD COLUMN` 安全**。全仓没有 `SELECT *` 打在这张表上: + `transmission.list_candidates` 用显式列清单、`INSERT` 也是显式列清单、 + `factor_coverage_probe` 只取 `scan_date, quiet`。加一个可空列不会撑坏任何列序假设。 +3. **不删任何数据**。§5 的 6 条语句 = 4 个 `CREATE OR REPLACE VIEW` + 1 个 + `ALTER TABLE ADD COLUMN IF NOT EXISTS` + 1 个 `CREATE INDEX IF NOT EXISTS`。 + `claims` / `documents` 一个字节不动,不需要 replay,不需要重抽。 +4. **基座 PG 是 `postgres:16.9-alpine` 且端口已发布**(`ports: "${PG_PORT:-5432}:5432"`), + 所以桥跨机连得上,§5 的一次性 psql 容器用 `postgres:16-alpine` 客户端版本也对得上。 +5. **基座 beat 时刻表**(工作日):05:30 主数据 → 17:30 行情快照 → 18:00 hotspot → + **18:15 传导扫描** → 周一 06:00 池刷新 → 07:00 日报。 + ⇒ **桥的 daily build 必须在 18:15 之后跑**,否则当日传导表是空的。 + +--- + +## §3 版本对齐(开发机 ↔ 桥服务器,**两处必须同一个 commit**) + +```bash +# 开发机 +cd ~/Documents/work/project/akg-factor-bridge && git rev-parse --short HEAD + +# 桥服务器 +cd ~/project/akg-factor-bridge && git rev-parse --short HEAD +``` + +**判定**:两个短 hash 一致 → 通过。不一致 → 开发机 `git push`、桥服务器 `git pull`, +然后**必须** `docker compose up -d --build`(代码卷挂载虽然是 `.:/app`,但依赖变了要重建)。 + +> ⚠️ 桥服务器上 `.env` 是**不进版本库的**(本批新加的 `.gitignore` 已排除它)。 +> `git pull` 不会动它。新旋钮全部有默认值,`.env` 不改也能跑。 + +--- + +## §4 基线快照(**改动前跑**,用于事后证明"抽取成果没白费") + +在**基座机器**上: + +```bash +docker exec -i akg-postgres psql -U akg -d akg <<'SQL' +SELECT 'claims' t, count(*) n, max(ingestion_date)::date latest FROM claims +UNION ALL SELECT 'documents', count(*), max(ingested_at)::date FROM documents +UNION ALL SELECT 'entity_links', count(*), max(created_at)::date FROM entity_links +UNION ALL SELECT 'company_master', count(*), max(synced_at)::date FROM company_master +UNION ALL SELECT 'consensus_daily', count(*), max(asof_date) FROM consensus_daily +UNION ALL SELECT 'mkt_daily', count(*), max(trade_date) FROM mkt_daily +UNION ALL SELECT 'transmission_candidates', count(*), max(scan_date) FROM transmission_candidates +UNION ALL SELECT 'industry_pools', count(*), max(refreshed_at)::date FROM industry_pools; + +-- 顺带回答一个对回填边界很关键的问题(mkt_daily 是持久快照表、全库无删除) +SELECT kind, min(trade_date), max(trade_date), count(DISTINCT trade_date) AS days +FROM mkt_daily GROUP BY kind ORDER BY kind; +SQL +``` + +**留档**:把输出存下来。§8 判收时再跑一次,前 4 行必须**逐字不变**。 + +--- + +## §5 应用视图(**本批唯一一次对基座库的写操作**) + +按你人在哪台机器,三选一。**只需要跑一次**,跑在基座库上,跟你从哪台机器发起无关。 + +### 5a · 在桥服务器上,用桥自己的连接(推荐;无需 psql 客户端) + +⚠️ 需要先完成 §3(`apply-views` 是本批新增的命令,旧代码没有)。 + +```bash +cd ~/project/akg-factor-bridge +docker compose exec -T akg-factor-bridge python run.py apply-views --dry-run # 先看要跑哪 6 条 +docker compose exec -T akg-factor-bridge python run.py apply-views +``` + +若 `.env` 里是只读账号,会逐条报 `permission denied`,改用 owner 账号跑这一次: + +```bash +docker compose exec -T \ + -e AKG_PG_USER=akg -e AKG_PG_PASSWORD=<基座 PG_PASSWORD> \ + akg-factor-bridge python run.py apply-views +``` + +> 必须用 `docker compose exec -e`。写成 `AKG_PG_USER=x docker compose exec …` +> 只设置宿主机上 compose 客户端进程的环境变量,**传不进容器**。 + +### 5b · 在桥服务器上,用一次性 psql 容器(§3 还没做完时的即时方案) + +```bash +cd ~/project/akg-factor-bridge +set -a; . ./.env; set +a +docker run --rm -i -e PGPASSWORD="$AKG_PG_PASSWORD" postgres:16-alpine \ + psql -h "$AKG_PG_HOST" -p "${AKG_PG_PORT:-5432}" -U "$AKG_PG_USER" -d "$AKG_PG_DB" \ + -v ON_ERROR_STOP=1 < sql/astock_kg_slot_views.sql +``` + +### 5c · 在基座机器上(需要先把 `sql/astock_kg_slot_views.sql` 带过去) + +```bash +docker exec -i akg-postgres psql -U akg -d akg < sql/astock_kg_slot_views.sql +``` + +### 判定标准(三条路一样) + +6 条语句全部成功:`CREATE VIEW` ×4 + `ALTER TABLE` + `CREATE INDEX`,无 ERROR。 +(5a 会逐条打 ✅/❌ 并在末尾汇总,缺权限时能精确定位到是哪一条。) + +### 回滚 + +```bash +# 视图退回 v1(原文件在这两处任选其一) +# 桥服务器: .review_backup_20260726/sql_astock_kg_slot_views.sql +# 或 git: git show 42659b9:sql/astock_kg_slot_views.sql > /tmp/v1.sql +docker exec -i akg-postgres psql -U akg -d akg < /tmp/v1.sql +# ALTER 加的列与索引是纯增量、无害,不需要回滚;真要清: +# ALTER TABLE transmission_candidates DROP COLUMN IF EXISTS mkt_trade_date; +# DROP INDEX IF EXISTS idx_tc_target; +``` + +--- + +## §6 兼容性矩阵(联调的核心,四个象限**现在全都是安全的**) + +`n_paths` 列在 v2 视图里**原名原义保留**(我原来改成了 `n_paths_raw`,那会让旧桥代码 +查不到列而崩 —— 已修正)。所以两个工程可以**任意顺序**升级、任意一侧独立回滚: + +| | 视图 v1(旧) | 视图 v2(新) | +|---|---|---| +| **桥 v1(旧代码)** | 原状态 | ✅ 照常工作(读 `n_paths`,旧口径) | +| **桥 v2(新代码)** | ✅ 可跑,但退回旧口径并打 ⚠️ 告警
`v_factor_transmission 还是旧版(无 n_sources)` | ✅ **目标状态**,用 `n_sources` | + +**你现在处于左下格**(桥已 pull 到 v2、视图还是 v1)——这是**合法的过渡态**, +不是故障。做完 §5 就进右下格。 + +**判定右下格已达成**: + +```bash +docker compose exec akg-factor-bridge python run.py views +``` + +看新增的「视图版本」段,前三项必须是 ✅: + +``` + ✅ v_factor_transmission.n_sources + ✅ v_factor_transmission.mkt_trade_date + ✅ v_factor_events.source_type + ⬜ v_factor_segment_members(第五插槽,G2) ← 这一项现在就该是 ⬜ +``` + +--- + +## §7 桥侧功能验证 + +### 7a · 只读(不写任何库,可反复跑) + +```bash +cd ~/project/akg-factor-bridge +docker compose exec akg-factor-bridge python run.py views +docker compose exec akg-factor-bridge python run.py probe --section corr # ★ 最重要 +docker compose exec akg-factor-bridge python run.py probe --section upside +docker compose exec akg-factor-bridge python run.py probe --section pools +docker compose exec akg-factor-bridge python run.py probe --section price # 全表聚合,1~3 分钟 +``` + +> `pools` 节直读基座 `industry_pools` 表(一次性诊断的例外)。若桥用的是只读账号 +> 且没授这张表,此节会 permission denied——**不影响其余三节**,跳过即可。 + +**`corr` 节要回答的两件事**(这一节决定第三批怎么定,请把整段原样发我): + +- **① `corr(z_H, z_V)`**:若 |值| > 0.5,「还没热」与「便宜」高度共线, + 「三项加权」实际是两项,§9-8 权重讨论要重开。 +- **② `w_T=0.50` 那行的 top-K 命中数**:若 ≈ `min(K, 传导票数)`, + 实锤「0.5/0.3/0.2 事实上是传导优先而非加权混合」。 + 同节还会打印**两段式与当前公式的 top20 重合度**——越接近 20/20, + 说明改两段式零代价,第三批 3.2 直接选 A。 + +### 7b · 写平台因子库(**这一步会往 quant_factor_service 的生产表写行**) + +```bash +# 注册(幂等,ON DUPLICATE KEY UPDATE) +docker compose exec akg-factor-bridge python run.py register + +# 构建:日期选「传导有数据」的那天,且必须在基座 18:15 传导扫描之后 +# (run.py views 的「数据历史深度」段会告诉你各源前沿在哪) +docker compose exec akg-factor-bridge python run.py build all --mode daily --date 2026-07-24 +``` + +**写入是幂等的**(按日期区间 DELETE 再 INSERT),重跑不会产生重复。 +**回滚**:`DELETE FROM t_factor_akg_* WHERE trade_date = '<日期>'`。 + +**逐路对照上一次的行数**(2026-07-23:upside 795 / heat 1674 / event 195 / transmission 83): + +| 因子 | 预期变化 | 变化原因 | 不符时看哪里 | +|---|---|---|---| +| `akg_upside` | 基本持平 | 只改了分块读取,口径未动 | 若骤降 → `gp_day_data` 代码列没对上 | +| `akg_heat` | 持平 | 未动 | —— | +| `akg_event` | **大概率下降** | 新加 `source_type='announcement'` 白名单 + 单文档封顶 | **见下** | +| `akg_transmission` | 持平或略变 | `n_paths` → `n_sources`,重复路径不再重复计数 | 值会变、行数不该变 | + +> **`akg_event` 掉到接近 0 的处置**:日志会打印 +> `(事件:按 source_type 剔除 N 条 {...分布...})`。若剔除后为空,说明基座里公告的 +> `documents.source_type` 不是 `announcement` 这个字符串。查一下真实取值: +> ```bash +> docker exec -i akg-postgres psql -U akg -d akg -c \ +> "SELECT source_type, count(*) FROM documents GROUP BY 1 ORDER BY 2 DESC;" +> ``` +> 然后在桥服务器 `.env` 里写 `EVENT_SOURCE_TYPES=<真实值>` 并重跑,**不要改代码**。 + +### 7c · 传导告警的正确读法(**容易误判,请按这个读**) + +`build akg_transmission` 会打三类 ⚠️。**它们的"没出现"含义各不相同**: + +| 告警 | 出现 = | **没出现 = ?** | +|---|---|---| +| `quiet 存满 12 条` | 撞 `transmission.py` 的 `quiet[:12]` 截断 | 真的没撞(环节未动成员本来就 <12) | +| `members_total >= 30` | 撞基座 `topic_context` 的 `LIMIT cap` | 真的没撞 | +| `mkt 快照日 ≠ scan_date` | movers 陈旧 | **⚠️ 不能判定**——`mkt_trade_date` 要等**第二批**改了基座才会被写入,本批全是 NULL,桥会跳过这项检查。**本批它不出现是必然的,不是"通过"。** | + +前两个告警各命中几个环节,请发我——那是硬伤 1 的实测量级,直接决定第二批里 +`topic_context` 的 cap 该提到多少。 + +### 7d · 输入冻结 + +daily build 跑完会自动冻结。检查: + +```bash +docker compose exec akg-factor-bridge cat /app/data/frozen/2026-07-24/manifest.json +ls -la data/frozen/2026-07-24/ +``` + +**判定**:`manifest.json` 存在,`rows` 里 universe / consensus / price_close / heat / +transmission 各有行数(heat 可能是 0——它 T+1 到达,属正常), +`segment_members` / `segment_edges` 为 0(第五/六视图 G2 才有,属正常)。 + +### 7e · 事件回填冒烟(验证硬伤 3 真的修好了) + +```bash +docker compose exec akg-factor-bridge python run.py build akg_event \ + --mode history --start 2024-01-01 --end 2024-03-31 --no-freeze +``` + +**判定**:改之前这条命令必然静默返回「无数据(跳过)」(日历取自热度表,最早 2025-03)。 +现在应该有真实行数(前提是 2024 年的公告已抽取)。 +若仍为空,先看日志是不是被 `EVENT_SOURCE_TYPES` 过滤掉了——两种"空"日志不一样。 + +--- + +## §8 判收标准(逐条可独立判定,**一条不过不判收**) + +**工程正确性** + +- [ ] §3 两处 git hash 一致 +- [ ] §5 六条 DDL 全部成功 +- [ ] §6 `views` 的视图版本段前三项 ✅、第四项 ⬜ +- [ ] §7a 四节 probe 至少 `corr` / `upside` / `price` 三节出数(`pools` 权限不足可跳) +- [ ] §7b `register` + `build all` 无 ❌,四路都有行数 +- [ ] §7d `manifest.json` 生成且各源有行数 +- [ ] §7e 2024 年区间不再静默返空 + +**数据无损**(回答"抽取成果会不会白费") + +- [ ] §4 的基线 SQL 重跑一次,`claims` / `documents` / `entity_links` / `company_master` + 四行的 `count` 与 `latest` **逐字不变** + +**幂等** + +- [ ] `build all --mode daily --date <同一天>` 连跑两次,第二次行数与第一次相同, + 平台表无重复(主键 `(trade_date, stock_code)` 本就保证,此处是验证 DELETE 区间正确) + +--- + +## §9 失败对照表 + +| 现象 | 根因 | 处置 | +|---|---|---| +| `No such container: akg-postgres` | 你在**桥服务器**上,基座容器不在这台机器 | 用 §5a 或 §5b | +| `permission denied for ...` (DDL) | 桥用的是只读账号 | §5a 的 `-e` 覆盖成 owner 账号 | +| `views` 里视图版本三项都 ⬜ | §5 没跑成功 | 回 §5,看 5a 的逐条报错 | +| 桥打 `v_factor_transmission 还是旧版` | 处于兼容矩阵左下格 | 正常过渡态,做完 §5 即消失 | +| `akg_event` 行数为 0 且日志显示剔除全部 | `source_type` 实际值不是 `announcement` | 查真实取值,改 `.env` 的 `EVENT_SOURCE_TYPES` | +| `akg_transmission` 行数为 0 | 当日基座传导扫描没产出(beat 18:15 未跑 / 被饿) | 换一个 `--date`,或先在基座跑 `transmission_scan` | +| `corr` 节报「无传导台账」 | 同上 | 同上 | +| `probe --section pools` permission denied | 只读账号没授 `industry_pools` | 跳过该节,不影响判收 | +| 平台 `gp_day_data` 查不到 / upside 为 0 | 代码列形态没对上 | `.env` 的 `PRICE_CODE_COL`(实测应为 `symbol`) | + +--- + +## §10 请回传给我的(拿到这些才能定第三批,不用再靠我的模拟) + +1. §1 拓扑登记表(填好) +2. §4 基线 SQL 的两段输出(**含 `mkt_daily` 按 kind 的天数** —— 它决定传导理论上能重扫到哪天) +3. §5 的执行输出(6 条语句的结果) +4. §6 `run.py views` 全部输出 +5. **§7a `probe --section corr` 全部输出 ← 最重要** +6. §7a `probe --section upside` 的 q 档位表 +7. §7b `build all` 的完整日志(尤其 event 的剔除分布、transmission 的两项 ⚠️ 各命中几个环节) +8. §8 判收清单的勾选情况 + +--- + +## §11 本批不做的事(避免混淆) + +- **不改 astock-kg 任何代码**。`topic_context` 加 `ORDER BY`、`transmission.scan` 去掉 + `quiet[:12]`、`_latest_mkt` 加参数、`sync_consensus` 加 `asof` —— 全在**第二批**。 +- 因此 `mkt_trade_date` 本批全是 NULL(见 §7c),`quiet[:12]` 截断依然存在, + `moved_ratio` 依然建立在任意顺序的 ≤30 抽样上。本批只是让这些问题**看得见**。 +- 赛道门槛 C、`akg_score`、`akg_gate` 都在 G2/G3,本批没有。 + +## §12 已拍板的决定(第二批据此实现) + +- **节奏**:先实机验证第一批,通过后再动基座。 +- **传导新鲜度断言**:**照常落库,只打告警 + 记 `mkt_trade_date`,不中止扫描**。 + 理由:保覆盖——传导每日只有几十行,公告批量入库期 beat 一旦被饿就整天空白; + 改由桥侧按 `mkt_trade_date` 自行判断是否采信。第二批的 `transmission.scan` 已按此改写, + `allow_stale` 参数取消。 diff --git a/docs/第一批验证清单_2026-07-26.md b/docs/第一批验证清单_2026-07-26.md index fe31898..6d33fee 100644 --- a/docs/第一批验证清单_2026-07-26.md +++ b/docs/第一批验证清单_2026-07-26.md @@ -1,4 +1,17 @@ -# 第一批实机验证清单(2026-07-26) +# 第一批验证清单(**已作废,见下**) + +> ⚠️ **本文件已被 `第一批联调验证清单_2026-07-26.md` 取代,请勿再按本文操作。** +> +> 作废原因:本文把「基座机器」和「桥机器」混为一谈——第 1 步的 +> `docker exec -i akg-postgres psql ...` 只在 astock-kg 那台机器上成立, +> 而桥是跨机部署的独立单元,它的服务器上没有 `akg-postgres` 容器。 +> 新版按「三个工程 / 至少两台机器」重写,每步标注执行位置、判定标准、 +> 失败去向与回滚方法,并补了兼容性矩阵与拓扑登记表。 +> +> 保留本文只为留下这次勘误的痕迹。原文如下(仅供对照,命令不要照抄)。 + +--- + > 已写入 `akg-factor-bridge`:改 7 个文件(+533/−121),新增 `freeze.py` / `probe.py` / `.gitignore`。 > 容器内与设备端 `py_compile` 均通过;`build_event` 的向量化改写做过逐位等值回归; diff --git a/factors.py b/factors.py index 3bdba79..a376f99 100644 --- a/factors.py +++ b/factors.py @@ -241,7 +241,7 @@ def build_transmission(start, end): 于是同一 source 对同一 target 重复计入,而 n_paths 是 factor_value 的主量级。 改用 distinct source 数,也更贴合传导模块自述的语义「多源汇聚 = 传导逻辑更硬」。 """ - cols_new = ("scan_date, target, ts_code, n_sources, n_paths_raw, moved_ratio, " + cols_new = ("scan_date, target, ts_code, n_sources, n_paths, moved_ratio, " "members_total, n_quiet_stored, mkt_trade_date") try: tr = db.read_pg( diff --git a/sql/astock_kg_slot_views.sql b/sql/astock_kg_slot_views.sql index f57b712..b4dc9eb 100644 --- a/sql/astock_kg_slot_views.sql +++ b/sql/astock_kg_slot_views.sql @@ -2,8 +2,8 @@ -- astock-kg 因子插槽接口:只读视图 v2(供 akg-factor-bridge 消费) -- ---------------------------------------------------------------------------- -- 相对 v1 的变更(2026-07-26 评审): --- ① v_factor_transmission:n_paths → n_sources(distinct 源数),修「变长边每种 --- 长度各返回一条 + 三桶间不去重」导致的路径重复计数;n_paths_raw 保留作观察。 +-- ① v_factor_transmission:新增 n_sources(distinct 源数);n_paths 原名原义保留,修「变长边每种 +-- 长度各返回一条 + 三桶间不去重」导致的路径重复计数。 -- ② v_factor_transmission:暴露 target / target_type / members_total / moved / -- mkt_trade_date,让桥能自检上游截断与快照新鲜度。 -- ③ v_factor_events:补 doc_id / source_type / tier(供年报污染防护)。 @@ -82,7 +82,9 @@ SELECT t.scan_date, q->>'ts_code' AS ts_code, (SELECT count(DISTINCT p->>'source') FROM jsonb_array_elements(COALESCE(t.paths, '[]'::jsonb)) p) AS n_sources, - jsonb_array_length(COALESCE(t.paths, '[]'::jsonb)) AS n_paths_raw, + jsonb_array_length(COALESCE(t.paths, '[]'::jsonb)) AS n_paths, + -- ↑ 原列名原语义保留不动:让 v1 桥代码在 v2 视图上照常工作(联调四象限全兼容), + -- 同时它本身也是有用的观察列(n_paths / n_sources 的比值 = 路径重复计数的倍数) t.moved_ratio, t.members_total, -- 桥用:== 上游 topic_context cap 时 moved_ratio 不可信 t.moved,