356 lines
17 KiB
Markdown
356 lines
17 KiB
Markdown
# 第一批 · 联调验证清单(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(新代码)** | ✅ 可跑,但退回旧口径并打 ⚠️ 告警<br>`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` 参数取消。
|