488 lines
27 KiB
Markdown
488 lines
27 KiB
Markdown
|
|
# 量化因子导出与合成设计 v2.0 · 交叉评审(2026-07-26)
|
|||
|
|
|
|||
|
|
> 评审范围:`akg-factor-bridge` 全部代码 + `量化因子导出与合成设计.md` v2.0 + astock-kg 供数侧
|
|||
|
|
> (`transmission.py` / `hotspot.py` / `chainmap.py` / `market_snapshot.py` / `themes.py` /
|
|||
|
|
> `graph_store.py` / `claim_store.py` / `enums.py` / `factor_coverage_probe.py`)+ 相关设计文档。
|
|||
|
|
> 结论按"必须修 / 需要决策 / 需要知道"三档排列,每条附代码出处。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 0. 总体判断
|
|||
|
|
|
|||
|
|
**方向可行,我认同 v2.0 的转向。** §2.2 的「观察 vs 推断」类型学是全文最有价值的部分:
|
|||
|
|
传导 = 当日 movers × 当前态图谱,两个输入都无耐久历史,回填必然携带成员性前视 —— 这个论证
|
|||
|
|
成立,而且是三条否决理由里唯一决定性的一条(另两条只是"效果不好",这一条是"方法上不可能")。
|
|||
|
|
把回测从生死闸降为仪表盘,在一个自己承认"无法准确估值"的领域里是正确的取舍。
|
|||
|
|
|
|||
|
|
**但 S1 现在有 3 处会让因子"看起来在跑、实际不成立"的硬伤,1 处方法论表述与实际行为不符,
|
|||
|
|
以及 1 处架构级缺口(赛道门槛 C 在当前四视图上无法实现)。** 这 5 项都在 G3 之前必须处理,
|
|||
|
|
其中 1 项(快照冻结)每晚一天就永久少一天历史。
|
|||
|
|
|
|||
|
|
一句话版本:**代码质量不错、点时纪律做得比多数量化工程好;问题全部集中在"桥以外"
|
|||
|
|
—— 桥假设上游是确定的、可复现的、可查询的,而这三条上游都不完全满足。**
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. 必须修:三处硬伤
|
|||
|
|
|
|||
|
|
### 硬伤 1 · 第一权重项不可复现,且被两道截断天花板锁住
|
|||
|
|
|
|||
|
|
`transmission.py:116` 在装配 `quiet` 落库字段时写的是:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
for m in quiet[:12]: # ← 只有前 12 个未动成员进 quiet JSONB
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
叠加 `scan()` 的 `max_candidates: int = 12`(`transmission.py:48`),
|
|||
|
|
`v_factor_transmission` 的**结构上限就是 12 × 12 = 144 行/日** —— 实测 83 行完全吻合,
|
|||
|
|
说明它不是"覆盖稀薄",而是**被一个为旁批/展示写的截断锁住了**。
|
|||
|
|
|
|||
|
|
更麻烦的是这 12 个是哪 12 个。`quiet` 来自 `graph_store.topic_context("Segment", name)`,
|
|||
|
|
其 Cypher 是:
|
|||
|
|
|
|||
|
|
```cypher
|
|||
|
|
MATCH (c)-[m:IN_SEGMENT {status:'active'}]->(t:Segment {segment_name:$id})
|
|||
|
|
RETURN ... LIMIT $cap -- cap 默认 30,且没有 ORDER BY
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
三个后果:
|
|||
|
|
|
|||
|
|
1. **同一天重跑 `transmission_scan` 可能得到不同的 `quiet` 集合与不同的 `moved_ratio`**
|
|||
|
|
—— 顺序由 Neo4j 执行计划决定。桥内写入是幂等的,但**上游不是**,所以判收标准 §10
|
|||
|
|
「重跑不产生重复或漂移」按现状无法成立。
|
|||
|
|
2. 成员数 > 30 的大环节,`members_total` / `moved_ratio` 建立在一个**任意的 ≤30 抽样**上,
|
|||
|
|
而 `moved_ratio` 直接是 `factor_value` 的一个乘数因子。
|
|||
|
|
3. 哪只股票能进因子表,事实上由数据库返回序决定。
|
|||
|
|
|
|||
|
|
**修法(基座侧,属确定性缺陷修复,不算"新增计算",不破 §4 铁律)**
|
|||
|
|
|
|||
|
|
- `topic_context` 加 `ORDER BY coalesce(c.ts_code, c.entity_key)`,`cap` 提高或参数化;
|
|||
|
|
- `transmission.scan` 落库的 `quiet` 存**全量**未动成员,旁批 prompt 仍只喂前 12
|
|||
|
|
(两个用途本来就该分开);
|
|||
|
|
- 视图额外暴露 `members_total` / `moved`,让桥能自检 `members_total == 30` 这类可疑值。
|
|||
|
|
|
|||
|
|
### 硬伤 2 · `n_paths` 被重复计数系统性抬高
|
|||
|
|
|
|||
|
|
`v_factor_transmission` 用 `jsonb_array_length(t.paths)` 当路径数
|
|||
|
|
(`sql/astock_kg_slot_views.sql:56`),而 `paths` 是**没有去重的**:
|
|||
|
|
|
|||
|
|
- `graph_store.cascade()` 主干是变长边 `-[:SEGMENT_UPSTREAM_OF*1..N]->`,
|
|||
|
|
**变长匹配会把每种长度的路径各返回一条** —— `A→B` 与 `A→X→B` 同时出现、末节点都是 B;
|
|||
|
|
去重只做了路径内去环,跨路径不去重,且 `LIMIT 60` 无 `ORDER BY`。
|
|||
|
|
- `graph_store.transmission_targets()` 三个桶(updown / supply / drives)各自 DISTINCT,
|
|||
|
|
但桶间用 `out += ...` 简单拼接,**同一 target 可以从 supply 和 drives 各来一次**。
|
|||
|
|
- `transmission._add()` 对每条 `(source, target, via)` 都 append 一条。
|
|||
|
|
|
|||
|
|
于是 `n_paths` 既不是"多少个源指向它",也不是"多少条独立路径",而是"多少次被提及"。
|
|||
|
|
而 `factor_value = n_paths × (1 − moved_ratio)`,`n_paths` 是主量级 —— 排序偏差直接传导到因子。
|
|||
|
|
|
|||
|
|
**修法(只改视图,零基座改动,成本最低的一条)** 把它改成 distinct source 数,
|
|||
|
|
这也更贴合传导模块自己写的语义"多源汇聚 = 传导逻辑更硬":
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE OR REPLACE VIEW v_factor_transmission AS
|
|||
|
|
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,
|
|||
|
|
t.moved_ratio,
|
|||
|
|
t.members_total, -- 桥用来识别 ≤30 截断
|
|||
|
|
t.moved
|
|||
|
|
FROM transmission_candidates t
|
|||
|
|
CROSS JOIN LATERAL jsonb_array_elements(COALESCE(t.quiet, '[]'::jsonb)) AS q
|
|||
|
|
WHERE q->>'ts_code' IS NOT NULL AND q->>'ts_code' <> '';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
桥侧 `build_transmission` 改用 `n_sources`,`n_paths_raw` 留着当观察列。
|
|||
|
|
|
|||
|
|
### 硬伤 3 · 事件回填会静默返回空(README 里那条示例命令现在就是无效的)
|
|||
|
|
|
|||
|
|
`factors.build_event` 用 `common.trading_days(start, end)` 当交易日历(`factors.py:137`),
|
|||
|
|
而 `common.trading_days` 查的是**热度表**:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# common.py:24-30
|
|||
|
|
"SELECT DISTINCT trade_date FROM stock_fund_heat_scores WHERE trade_date BETWEEN %s AND %s"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
热度最早只有 **2025-03-26**(文档 §6.2 自己记着)。所以:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
python run.py build akg_event --mode history --start 2024-01-01 --end 2026-07-24 # README 第 58 行
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
→ `cal` 为空 → `return _EMPTY` → 只打印「无数据(跳过)」,**不报错、不告警**。
|
|||
|
|
文档 §7 声称事件可回填到 2014 年,实际上 2025-03-26 之前一天都拿不到。
|
|||
|
|
|
|||
|
|
**修法**:日历改用 `gp_day_data` 的 `DISTINCT timestamp`(5584 天,覆盖全历史),
|
|||
|
|
热度表最多作为并集补充。顺带在 `cal` 为空时打印告警而不是静默跳过。
|
|||
|
|
|
|||
|
|
### 附带两条:放量时会炸,现在不炸
|
|||
|
|
|
|||
|
|
- **`_read_gp_price` 不带 universe 过滤也不分块**(`factors.py:64-67`):单日 ~5000 行没问题,
|
|||
|
|
但 `--mode history --start 2006-01-01` 会把千万级行拉进 pandas。加 `AND \`{col}\` IN (...)`
|
|||
|
|
+ 按月分块。
|
|||
|
|
- **`write_factor` 单事务 `executemany` 全量**(`common.py:69-75`):热度全史约
|
|||
|
|
322 × 1674 ≈ 54 万行一次提交,走 ShardingSphere 代理有风险。按日期分块提交。
|
|||
|
|
另外 `write_factor(mode)` 这个参数收了但没用到,可以删。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. 需要决策:0.5 / 0.3 / 0.2 实际上是"传导优先",不是"加权混合"
|
|||
|
|
|
|||
|
|
这是我最想让你在 G3 开工前想清楚的一条,因为它是**逻辑选择而不是回测选择**。
|
|||
|
|
|
|||
|
|
传导在池内约 95% 为 0(实测 83/1675)。对一列 95% 是 0 的数做
|
|||
|
|
`(x − mean) / std`,`std` 完全由那 5% 决定,于是**非零值的 z 落在 +4~+5,零值落在 −0.2 附近**,
|
|||
|
|
`0.5 · z_T` 的组间落差约 2.2;而 `0.3 · z_H + 0.2 · z_V` 用稳健 z 之后全幅只有约 1.7。
|
|||
|
|
落差大于全幅 → 跨组不可逆。
|
|||
|
|
|
|||
|
|
我做了数值验证(200 只候选、其中 10 只有传导、其余口径按 §3.4 实现,600 次重复):
|
|||
|
|
|
|||
|
|
| w_T | top20 中传导票 | top20 中非传导票 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| **0.50(当前)** | **9.8 / 10** | 10.2 |
|
|||
|
|
| 0.30 | 9.1 / 10 | 10.9 |
|
|||
|
|
| 0.20 | 7.8 / 10 | 12.2 |
|
|||
|
|
| 0.15 | 6.6 / 10 | 13.4 |
|
|||
|
|
| 0.10 | 4.7 / 10 | 15.3 |
|
|||
|
|
|
|||
|
|
也就是说:**当前参数下,有传导的票几乎必然全部进入前列;0.3 和 0.2 只在"有传导组内"
|
|||
|
|
和"无传导组内"各自排序,跨组不起作用。** 要让"很冷很便宜的非传导票"真有机会赢过
|
|||
|
|
"边际传导票",`w_T` 得降到 0.10~0.15。
|
|||
|
|
|
|||
|
|
两条路,选一条并写进文档:
|
|||
|
|
|
|||
|
|
- **A(我推荐,而且我认为它更贴合你原话"传导第一")**:承认它是两段式,直接写成
|
|||
|
|
`primary = 传导档位(0 / 1 / 2)`,`tiebreak = 0.6·z_H + 0.4·z_V`。
|
|||
|
|
好处:语义与实际行为一致、可解释、调试时能一眼看出是哪一段在起作用、没有隐藏非线性。
|
|||
|
|
代价:无。这同时把 §9-9 的「还没热该不该做成条件项」自然解决了 ——
|
|||
|
|
**现状在 95% 稀疏下已经等价于条件项了,不如写明**。
|
|||
|
|
- **B**:真要连续混合 → `w_T ≈ 0.12`,或对 `z_T` 做有界变换(如 `clip(z,±3)/3 × 1.5`,
|
|||
|
|
实测能把 9.8 拉到 7.5)。
|
|||
|
|
|
|||
|
|
**顺带一个共线性提醒**:门槛② (`upside ≥ θ_v`) 与 `z_V` (`upside`) 是同一个变量进两次;
|
|||
|
|
而"还没热"与"还便宜"在 A 股高度共线(涨上去了 → 又热又贵)。
|
|||
|
|
建议 G1 的 probe 直接加一节输出池内 `(z_T, z_H, z_V)` 的相关矩阵 ——
|
|||
|
|
**如果 `corr(z_H, z_V) > 0.5`,那"三项加权"实际是"两项",权重讨论要重开。** 这一节几乎零成本。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. 架构缺口:赛道门槛 C 在当前四视图上无法实现
|
|||
|
|
|
|||
|
|
**图谱在 Neo4j,不在 PostgreSQL**(`graph_store.py:17` 起全走 `neo4j` driver)。PG 侧
|
|||
|
|
`industry_pools` 的完整 DDL 只有 4 列(`claim_store.py:439-445`):
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
theme TEXT PRIMARY KEY, members JSONB NOT NULL, stats JSONB, refreshed_at TIMESTAMPTZ
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`members` 元素只有 `ts_code / name / tier / confidence / supporting` ——
|
|||
|
|
**成员级既没有 segment,也没有 layer**。环节归属是读时从 Neo4j 重算的
|
|||
|
|
(`chainmap.py:61` → `graph_store.chain_view(theme)`)。
|
|||
|
|
|
|||
|
|
所以 `frontier_tracks.yml` 里规划的 `kg_segments` / `kg_concepts` 两个映射,
|
|||
|
|
**在现有四个只读视图上没有数据源**。三条路:
|
|||
|
|
|
|||
|
|
1. **从 claims 重建**(可行,一条 SQL:`WHERE predicate='IN_SEGMENT' AND object_type='Segment'`,
|
|||
|
|
再 join `entity_links` / `company_master` 取 ts_code)。但 `claims` 是**融合前的只追加表**,
|
|||
|
|
没有 `status` / `supersede` / tier 裁决(那些只长在 Neo4j 边上),会把已被顶替的历史归属
|
|||
|
|
一起捞出来;且 object 侧没有 norm 生成列,同义环节不会合并。**可以救急,不能当正解。**
|
|||
|
|
2. **桥直连 Neo4j** —— 破坏"桥只连三库"的设计,且把裁决逻辑复制进桥。不推荐。
|
|||
|
|
3. **推荐:基座加投影表 + 第五、第六个只读视图。**
|
|||
|
|
`v_factor_segment_members(segment_name, chain, ts_code, tier, status, updated_at)`
|
|||
|
|
与 `v_factor_segment_edges(up, down)`,由基座现有的 Neo4j 投影 beat 顺手物化到 PG。
|
|||
|
|
这是**投影**不是**打分**,不破 §4 的"零新增因子计算",桥仍然只连三库。
|
|||
|
|
`probe.py:12-13` 的注释其实已经想到了这件事("G2 把池清单固化成第五个插槽视图"),
|
|||
|
|
只是低估了它的必要性 —— 它不是优化,是 C 门槛能不能实现的前提。
|
|||
|
|
|
|||
|
|
### 3.1 四层枚举(材料→设备→制造→应用)在系统里根本不存在
|
|||
|
|
|
|||
|
|
全仓唯一的层级来源是 `chainmap._topo_layers()`(`chainmap.py:310-336`)的 Kahn 拓扑 depth:
|
|||
|
|
**per-theme、相对、孤点与环上节点为 `None`**,标签由 `_layer_label` 现算成上游/中游/下游,
|
|||
|
|
不落库。而且"不存层级"是刻意的 —— `enums._SEGMENT_BAD_SUBSTR` 把
|
|||
|
|
「上游 / 中游 / 下游 / 环节 / 场景 / 领域」列为环节名**禁用词素**,注释写明
|
|||
|
|
"链位置由图上边表达"。
|
|||
|
|
|
|||
|
|
结论:`track_members.layer` 只能**在 yml 里人工指定**,并记一列 `layer_source`
|
|||
|
|
(`yml` / `graph_depth` / `unknown`)。而且 **S1 的公式压根不用 layer** ——
|
|||
|
|
建议把它降级为可选元数据,**不要让它阻塞 G2**。
|
|||
|
|
|
|||
|
|
### 3.2 赛道覆盖风险的具体量级
|
|||
|
|
|
|||
|
|
`industry_pools` 的起点是人工建的两个池(光通信 / 消费电子,见 `涌现式应用层重构设计.md:30`),
|
|||
|
|
之后靠周一 `refresh_pools` 自动收录达标涌现主题(≥3 环节且 ≥3 上市成员,
|
|||
|
|
`graph_store.EMERGE_MIN_SEGMENTS/EMERGE_MIN_LISTED`)。theme 是 `IN_SEGMENT.chain` 修饰符原文,
|
|||
|
|
过 `segment_name_ok` 卫生闸(≤12 字中文裸名)。
|
|||
|
|
|
|||
|
|
**在这个生成机制下,核聚变 / 低空经济 / 商业航空航天大概率查不到** ——
|
|||
|
|
它们需要有人先投过对应的产业链研报。文档 §3.2 已经把这列为"关键前置风险",但给的对策
|
|||
|
|
("覆盖为空的赛道宁可先不放进来")会让 C 塌成「通信 + 算力 + AI」,也就是
|
|||
|
|
**退化成"电子 + 计算机",门槛几乎不筛东西**。
|
|||
|
|
|
|||
|
|
建议 C 做成**两级并强制标注来源**:
|
|||
|
|
|
|||
|
|
| source_rule | 证据强度 | 来源 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `graph` | 强,可审计到具体 claim | `IN_SEGMENT` / `HAS_CONCEPT` 边 |
|
|||
|
|
| `fallback` | 弱,仅作占位 | 概念标签命中 / 申万行业白名单 |
|
|||
|
|
|
|||
|
|
`track_members` 里逐行记 `source_rule`,仪表盘里把两级分开看。这样 C 既不会因为图谱覆盖
|
|||
|
|
不足而变空集,也不会把弱证据伪装成强证据。**同时,覆盖为空的赛道本身就是一张采集清单**
|
|||
|
|
—— 这正是基座「需求闭环」的用法,比放弃赛道有价值。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. 逻辑一致性:v2.0 把 v1.1 一个仍然成立的开放问题删掉了
|
|||
|
|
|
|||
|
|
v1.1 §8-2 问过「**覆盖池历史快照缺失**:回填期 universe 用当前池近似(有轻微前视)
|
|||
|
|
还是曾入池并集?」—— v2.0 的 §9 十个开放问题里**没有这一条**。
|
|||
|
|
|
|||
|
|
但按 §2.2 自己的论证,这一条现在**更重要**:
|
|||
|
|
|
|||
|
|
- `v_factor_universe` = 全部 `industry_pools` 成员并集,而 `industry_pools`
|
|||
|
|
**只存最新态、每周一自动长大**(beat 表:池刷新周一 6:00)。
|
|||
|
|
- §7 判定"可直接回填"的三路(heat / event / upside)**全部经过这个 universe 过滤**。
|
|||
|
|
于是它们的**值**是点时正确的,**成员性**却是前视的 —— 用今天的池去筛 2025 年的历史,
|
|||
|
|
而今天的池里有大量主题是 2026 年的研报才长出来的。**这就是 §2.2 批评传导时用的同一条论证,
|
|||
|
|
只是上升了一层。**
|
|||
|
|
- 更直接的工程后果:`z` 统计量(mean / std / median / MAD)在**每周一发生结构性跳变**,
|
|||
|
|
IC 序列和分层读数会带一个周期性伪影。
|
|||
|
|
|
|||
|
|
**建议(两条一起做,都很便宜):**
|
|||
|
|
|
|||
|
|
1. **子因子表去掉 universe 过滤**,全市场出行。理由:子因子表的定位是"可独立观察的仪表",
|
|||
|
|
过滤只该发生在消费端。行数代价很小(heat 池内 1674 → 全市场约 5000),
|
|||
|
|
换来的是三张点时干净的面板,且平台上的子因子 IC 读数才有市场级意义。
|
|||
|
|
2. **universe 只在 `akg_score` 侧当过滤器,并每日落快照**(见下节)。
|
|||
|
|
基座侧同步加一张 `industry_pools_history(snapshot_date, theme, members)`,
|
|||
|
|
由 `refresh_pools` 顺手写。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. 今天最该做、也是唯一不可补救的一件事:开始冻结快照
|
|||
|
|
|
|||
|
|
传导、`industry_pools`、Segment 边、前复权 close —— 这四样都是"过期即不可复原":
|
|||
|
|
|
|||
|
|
- 传导:§2.2 已经论证清楚。
|
|||
|
|
- 池 / Segment 边:随研报持续生长,只存最新态。
|
|||
|
|
- **前复权 close 比文档写的更麻烦一层**:`gp_day_data` 是前复权、锚在最新日,
|
|||
|
|
所以每次有股票除权,**历史 close 会被整体重写** —— `akg_upside` 的历史值因此
|
|||
|
|
**不可复现**(今天回填的和三个月后回填的不是同一组数)。文档 §7 只提了
|
|||
|
|
"除权点系统性偏移",实际还叠加一个可复现性问题。
|
|||
|
|
|
|||
|
|
而 §10 写的「GRU 复活条件:传导积累出 ≥1 年 live 历史」——
|
|||
|
|
**这个计时器只有在开始存快照的那一天才启动。** 现在已经过去 3 天(07-11 起),
|
|||
|
|
每晚一天就永久少一天。
|
|||
|
|
|
|||
|
|
**建议在 G1 之前插一个 G0.5「冻结与修复」里程碑**,做三件事,都不依赖赛道清单:
|
|||
|
|
|
|||
|
|
1. **输入冻结**:每日把 `universe` / 四路原始值 / 门槛判定结果 / 最终分
|
|||
|
|
落一份 `data/frozen/YYYY-MM-DD.parquet`(桥自己的 `data/`,与赛道快照同一机制)。
|
|||
|
|
这一条**同时满足** §10 的「重跑不产生漂移」、「赛道成员快照可审计」、
|
|||
|
|
以及未来任何回溯需求 —— 是全文性价比最高的工程动作。
|
|||
|
|
2. 修掉第 1 节的三个硬伤。
|
|||
|
|
3. 基座开始积累 `industry_pools` / Segment 边的每日(或每周)快照。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. 供数侧需要知道的事实(会影响读数怎么解释)
|
|||
|
|
|
|||
|
|
### 6.1 传导链条的上游有两张空表 —— 这才是 83 行/日的真正原因
|
|||
|
|
|
|||
|
|
`数据源盘点.md §1a` 记着:`gp_sector_daily` **当前空表**、`gp_market_sentiment` **当前空表**、
|
|||
|
|
`t_signal_daily_results` **05-15 停更**。于是 `hotspot._pick_anomalies` 的四路异动源里:
|
|||
|
|
|
|||
|
|
| 源 | 状态 |
|
|||
|
|
|---|---|
|
|||
|
|
| ① 板块相对强度(`gp_sector_daily`) | ❌ 空表 |
|
|||
|
|
| ② 概念 / 行业资金净流入 | ✅ |
|
|||
|
|
| ③ 池内个股(`pct_change ≥ 5` **或** `total_score ≥ 0.9`) | ⚠️ 只剩涨幅门(评分停更) |
|
|||
|
|
| ④ 事件 / 盘中流雷达 | ✅ |
|
|||
|
|
|
|||
|
|
**本来最该驱动"板块传导"的板块级异动源现在是缺的。**
|
|||
|
|
所以传导稀薄不主要是"图谱覆盖不够",而是「空表 + 12×12 截断」两件事叠加。
|
|||
|
|
**要提传导覆盖,优先补 `gp_sector_daily`,比调任何参数都有用。**
|
|||
|
|
|
|||
|
|
### 6.2 movers / quiet 不看 `scan_date`
|
|||
|
|
|
|||
|
|
`hotspot._movers_set()` → `_latest_mkt("stock")` → `SELECT max(trade_date) FROM mkt_daily`
|
|||
|
|
(`hotspot.py:45-55, 86-89`)。也就是用**最新快照**,不是 `scan_date` 当天的快照。
|
|||
|
|
|
|||
|
|
live 同日运行时两者一致,没问题。但如果 17:30 的 `sync_market` 失败或晚点,
|
|||
|
|
**18:15 的传导扫描会用 T−1 的 movers,写成 `scan_date = T`,而桥完全无从察觉** ——
|
|||
|
|
第一权重项拿到一份"陈旧数据贴着新鲜标签"。
|
|||
|
|
|
|||
|
|
建议:基座加一道 freshness 断言(`mkt_daily` 无 `scan_date` 当日 stock 行则 abort scan),
|
|||
|
|
并在视图里带上实际用到的 `mkt_trade_date` 让桥能自检。
|
|||
|
|
这也是**除 §2.2 之外,传导不该回填的第二个独立的、机械层面的理由** —— 值得补进文档。
|
|||
|
|
|
|||
|
|
### 6.3 一致预期的口径细节(影响门槛②怎么理解)
|
|||
|
|
|
|||
|
|
`sync_consensus`(`market_snapshot.py:493-588`):
|
|||
|
|
|
|||
|
|
- `asof_date = date.today()`,聚合窗口 `report_date >= today − 90d`
|
|||
|
|
→ 桥的 `merge_asof(backward)` **点时正确 ✅**,这块没问题。
|
|||
|
|
- `target_mid_avg` 是**90 天内全部研报行的简单平均**(`market_snapshot.py:548`
|
|||
|
|
遍历的是 `rows` 而不是 `qrows`),**不按机构去重、不按时间加权**。
|
|||
|
|
一家券商 90 天发 5 篇就有 5 倍权重;89 天前的目标价与今天的等权。
|
|||
|
|
- 实测 `max_price` 非空仅 0.4% / `min_price` 31%(docstring 记录),所以"目标价中枢"
|
|||
|
|
**主要由单值目标价构成**,"中值"的语义比文档写的弱。
|
|||
|
|
|
|||
|
|
**含义**:θ_v = 0 这道门槛在急涨阶段会自动收紧(分母是今天的价、分子是最长 90 天前的目标价),
|
|||
|
|
它**天然带反动量色彩**。这和你的漏斗逻辑同向(涨上去了就没容错度),
|
|||
|
|
但要知道它不是一道纯"估值"门槛 —— 这也加重了第 2 节说的三项共线性问题。
|
|||
|
|
|
|||
|
|
### 6.4 §9-6(upside 历史重建 甲 / 乙):甲案,成本远低于文档估计
|
|||
|
|
|
|||
|
|
`sync_consensus` 只要把 `date.today()`(`market_snapshot.py:529`)和
|
|||
|
|
`since`(`:507`)参数化成一个 `asof: date`,就能对任意历史日重放**同一套聚合逻辑**,
|
|||
|
|
live 与 history 共用一份代码、零漂移风险。改动量大约 3 行。
|
|||
|
|
|
|||
|
|
乙案(桥侧自行聚合 `gp_report_rc`)会让聚合逻辑在基座和桥各存一份 ——
|
|||
|
|
这正是文档自己指出的漂移风险,而甲案的"工作量在基座侧"这个缺点被高估了。
|
|||
|
|
|
|||
|
|
回填时记得把用到的 `close` 一并落进 `data/frozen/`(前复权可复现性问题,见第 5 节)。
|
|||
|
|
|
|||
|
|
### 6.5 事件因子的年报污染 —— S2 上否决闸之前必须处理
|
|||
|
|
|
|||
|
|
`v_factor_events` 的 `ts_code` 取 `documents.meta->>'company_ts_code'`。公告确实 100% 可靠,
|
|||
|
|
但**年报也有这个锚**,而**一份年报能抽出十几条 EVENT** —— hotspot 为此专门做了防刷屏
|
|||
|
|
(`hotspot.py:92-99` 的注释:"实测年报副产物把台账刷满的教训",对策是 other 不进、
|
|||
|
|
同主体同类型只取最新、单主体 ≤2 条)。**桥侧没有任何等价保护。**
|
|||
|
|
|
|||
|
|
后果:年报披露日会形成一个巨大的尖峰,且年报里提到的**历史**诉讼 / 处罚会被记成
|
|||
|
|
当日的负面事件。若 S2 按计划用 `akg_event < −θ_e` 做否决闸,
|
|||
|
|
**会因为一份年报提到历史诉讼而否决掉一只好股。**
|
|||
|
|
|
|||
|
|
修法(S2 之前):按文档类型只取公告,或按 `(ts_code, event_type, 自然日)` 去重 + 单日封顶。
|
|||
|
|
极性表(§9-5)在这个问题修掉之前,调得再准也没意义。
|
|||
|
|
|
|||
|
|
### 6.6 probe 与桥的事件口径不一致
|
|||
|
|
|
|||
|
|
`factor_coverage_probe.cov_event`(`:115-128`)用 `claims.subject_id` 去匹配池成员名/码,
|
|||
|
|
而桥用文档锚 `documents.meta->>'company_ts_code'`。**两个数不会对得上** ——
|
|||
|
|
读 G1 报告时别把它们当同一个指标。建议顺手把 probe 改成同一口径,否则会误判覆盖面。
|
|||
|
|
|
|||
|
|
### 6.7 一处枚举不一致(小,但会咬人)
|
|||
|
|
|
|||
|
|
`enums.EntityKind` 只有 6 个成员,**没有 Segment**;但 `_ID_KEYS`、`ALLOWED_OBJECT_TYPES`
|
|||
|
|
都把 Segment 当一等节点。类型闸门走的是 `ALLOWED_OBJECT_TYPES` 所以不炸,
|
|||
|
|
但 `EntityKind` 已经不是节点类型的可信清单了 —— 谁下次拿它做遍历会漏 Segment。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. 判收标准的两处需要改写
|
|||
|
|
|
|||
|
|
**§10「每日调度幂等,重跑不产生重复或漂移」**
|
|||
|
|
桥内成立 ✅,上游不成立 ❌(硬伤 1 的 Neo4j 返回序、前复权重写)。建议拆成两条:
|
|||
|
|
|
|||
|
|
- 桥内写入幂等 —— 现状已满足;
|
|||
|
|
- **外部不可复现的部分靠输入冻结解决** —— 即第 5 节的 `data/frozen/`。
|
|||
|
|
把"可复现"重新定义为"可从冻结输入重算出同一个值",这是唯一诚实且可达的口径。
|
|||
|
|
|
|||
|
|
**§7-A「`akg_score` 只在当日传导表有数据的交易日出行」**
|
|||
|
|
应加强为 **当日「候选池内」传导非空**。传导 83 行/日是**全池**口径,
|
|||
|
|
过完 C ∩ V 之后池内可能一只都不剩 —— 那天照样会写出一张"第一权重项全为 0"的表,
|
|||
|
|
正是 §7-A 想避免的情况。
|
|||
|
|
|
|||
|
|
同时建议每日日志固定打两个数:`|P|`(候选池规模)与 `|P ∩ 传导>0|`。
|
|||
|
|
后者才是真正决定榜首的数量。若它长期是 0~3,那么"组合"实际上是 1~3 只票,
|
|||
|
|
且这 1~3 只是从一个 12 × 12 截断列表里出来的 —— 集中度需要在文档里明写。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. 给平台评价层的一个建议(成本极低、收益很大)
|
|||
|
|
|
|||
|
|
按现在的设计,`akg_score` 只在候选池内出行 →
|
|||
|
|
**平台的 IC / 分层只能看到"池内排序"的价值,完全看不到两道门槛的价值**,
|
|||
|
|
而门槛才是这套逻辑的主体。仪表盘会系统性低估这个因子。
|
|||
|
|
|
|||
|
|
加一个因子就解决:
|
|||
|
|
|
|||
|
|
| 因子 | 出行范围 | 回答的问题 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `akg_gate` | **全覆盖池**,0/1(1 = 过 C ∩ V) | 门槛本身有没有价值(分层直接读出门槛内外的收益差) |
|
|||
|
|
| `akg_score` | 候选池内 | 池内排序有没有价值 |
|
|||
|
|
|
|||
|
|
两个仪表各答一个问题,"哪一项在拖后腿"一眼可见。这比在一个因子上纠结 IC 高低有用得多,
|
|||
|
|
而且完全符合"回测是仪表盘不是判官"的定位。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. 开放问题(§9)我的建议答案
|
|||
|
|
|
|||
|
|
| # | 议题 | 建议 | 依据 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 1 | 热度语义翻转 | **同意取负**,但别做成独立加性项 → 做成组内 tiebreak | 第 2 节 |
|
|||
|
|
| 2 | 完整赛道清单 | **先跑 G1 体检再定清单**,不要先定清单再体检;C 做两级(graph / fallback)并强制标 `source_rule` | 3.2 |
|
|||
|
|
| 3 | θ_v 的 q | 先留 q = 0,等 G1 分布定;但注意 upside 主要由单值目标价构成,**绝对水平的可信度低于相对排序** | 6.3 |
|
|||
|
|
| 4 | 无券商覆盖股 | S1 保持"不过门槛",但**单独输出一张「赛道内 + 无覆盖」观察名单** —— 这正是你论题里最该出现的标的,别让它无声消失 | §1 研报滞后论 |
|
|||
|
|
| 5 | 事件极性 / 半衰期 | 延后到 S2 没问题,但**先修年报污染** | 6.5 |
|
|||
|
|
| 6 | upside 重建 甲 / 乙 | **甲案**,只需给 `sync_consensus` 加 `asof` 参数(约 3 行) | 6.4 |
|
|||
|
|
| 7 | score 历史 A / B | **A**,且加强为"**池内**传导非空" | 第 7 节 |
|
|||
|
|
| 8 | 权重 0.5/0.3/0.2 | 次序认可;**建议改结构(两段式)而不是改数值** | 第 2 节 |
|
|||
|
|
| 9 | 还没热是否条件项 | **S1 就上**。现状在 95% 稀疏下已经等价于条件项,不如写明 | 第 2 节 |
|
|||
|
|
| 10 | 调度时点 | **T 日 18:40 算(热度用 T−1)**。漏斗赌的是左侧位置,热度差一天的信息损失远小于因子延迟一天的机会损失;且 T+1 早间补算会让"T 日因子"依赖 T+1 数据,语义上更难向下游解释 | §3.4 末 |
|
|||
|
|
| **11(新增)** | **universe 历史快照** | v1.1 §8-2 被 v2.0 删掉了,但它现在更重要 → 子因子去掉池过滤 + 池每日快照 | 第 4 节 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. 对 G 系列里程碑的微调建议
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
G0 文档评审(含本评审的取舍)
|
|||
|
|
G0.5 ★新增★ 冻结与修复 —— 不依赖赛道清单,且第 3 项每晚一天永久少一天历史
|
|||
|
|
① data/frozen/ 输入冻结机制
|
|||
|
|
② 修硬伤 1/2/3(其中硬伤 2 只改视图)
|
|||
|
|
③ 基座开始积累 industry_pools / Segment 边快照
|
|||
|
|
G1 赛道覆盖体检 + probe 加两节:
|
|||
|
|
corr(池内三项相关矩阵)· tracks(yml 草案在图谱里做成员性试算)
|
|||
|
|
+ 原有三件(factor_coverage_probe 补跑 / upside 分布定 q / gp_day_data 起始日复核)
|
|||
|
|
G2 赛道成员表落地(layer 降级为可选元数据,不阻塞)+ 第五/第六个插槽视图
|
|||
|
|
G3 漏斗实现 + 注册 akg_score **与 akg_gate**(第 8 节)
|
|||
|
|
G4 回填与仪表盘(upside 走甲案)
|
|||
|
|
G5 每日调度上线(T 日 18:40)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. 提交提醒
|
|||
|
|
|
|||
|
|
已核对两个仓库的实际状态(`git status`):
|
|||
|
|
|
|||
|
|
**`akg-factor-bridge` 有 4 项未提交**(最后一次 commit 是 `42659b9 历史深度数据探查`):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
M README.md
|
|||
|
|
M run.py
|
|||
|
|
?? probe.py ← G1 体检脚本,全新未跟踪
|
|||
|
|
?? 量化因子导出与合成设计.md ← v2.0,全新未跟踪
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**`astock-kg` 工作区干净** —— `factor_coverage_probe.py` 已在 `32448c3` 入库,无需再提交。
|
|||
|
|
|
|||
|
|
**一处文档不同步需要处理**:`astock-kg/docs/量化因子导出与合成设计.md` 还是 **v1**
|
|||
|
|
(2026-07-24,四路对称 + 平台拟合合成),而桥仓库那份已经是 **v2.0**(自洽公式)。
|
|||
|
|
两份同名文档在两个仓库里讲相反的方案,下次翻文档一定踩坑。
|
|||
|
|
建议:**只留桥仓库这一份为唯一事实源**,`astock-kg/docs/` 那份改成一行指路
|
|||
|
|
("本设计已迁至 akg-factor-bridge 仓库,见该工程根目录同名文档"),并在提交信息里写明。
|
|||
|
|
|
|||
|
|
开发机执行(按硬规矩,push 由你做):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
cd ~/Documents/work/project/akg-factor-bridge
|
|||
|
|
git add -A
|
|||
|
|
git commit -m "docs: 量化因子设计 v2.0(推翻拟合合成,转自洽景气度漏斗)
|
|||
|
|
feat: G1 赛道覆盖体检 probe(pools/price/upside 三节,只读)
|
|||
|
|
docs: README 补 probe 用法与数据前沿自检"
|
|||
|
|
|
|||
|
|
# 文档去重后(把 astock-kg/docs 那份改成指路)
|
|||
|
|
cd ~/Documents/work/project/astock-kg
|
|||
|
|
git add -A
|
|||
|
|
git commit -m "docs: 量化因子设计迁至 akg-factor-bridge 仓库(v1 留指路,避免与 v2.0 并存)"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
本评审文档建议放 `akg-factor-bridge/评审_v2.0_2026-07-26.md` 一并提交。
|