验证和联调
This commit is contained in:
parent
957305bd1d
commit
9c336a5037
|
|
@ -0,0 +1,57 @@
|
||||||
|
# 量化因子 · G1 实测结论与待办(2026-07-26)
|
||||||
|
|
||||||
|
> 第一批(桥侧口径修复 + 插槽视图 v2)已实机验证通过,代码已入库,不在此重复。
|
||||||
|
> 本文只记两件事:G1 实测读出的事实(后续决策的依据)、尚未完成的第二三批。
|
||||||
|
|
||||||
|
## 一、G1 实测(2026-07-24 截面,实机)
|
||||||
|
|
||||||
|
**覆盖池 2391 只**——设计文档写的 1675 已过时,316 个池在持续涌现收录。
|
||||||
|
|
||||||
|
**1. 门槛② 的损耗几乎全部来自"没有券商覆盖",不是估值**
|
||||||
|
U 2391 → 有 upside 795(33%)→ upside≥0 剩 755。
|
||||||
|
无券商覆盖挡掉 **1596 只(67%)**;θ_v=0 只挡掉 **40 只(有覆盖股的 5%)**。
|
||||||
|
⇒ §9-3 的 q 必须显著上调,否则估值那一刀形同虚设。
|
||||||
|
⇒ §9-4(无覆盖股怎么办)不是边角料,是三分之二的池。
|
||||||
|
|
||||||
|
**2. 三项正交**
|
||||||
|
corr(z_H, z_V) = −0.033;z_T 与另两项均 ≈ 0.01。
|
||||||
|
⇒ 评审中"三项加权实为两项"的担心**被证伪**,三项各带独立信息。
|
||||||
|
|
||||||
|
**3. 0.5/0.3/0.2 是严格的"传导优先",不是加权混合**
|
||||||
|
w_T=0.50 → top10 **10/10**、top20 **20/20**、top30 **30/30**、top50 **39/39**。
|
||||||
|
39 只传导票精确占满前 39 名,0.3/0.2 只在组内排序、跨组不起作用。
|
||||||
|
实测要真混合需 w_T ≈ 0.20(此时 top20 只有 6/20 是传导票)。
|
||||||
|
|
||||||
|
**4. 传导票 39 只(P 的 5.2%),组合可行性没问题**
|
||||||
|
top20 可以整篮子都是传导票,不存在"只剩 1~3 只"的塌缩。
|
||||||
|
|
||||||
|
**5. 硬伤1 真实 binding**
|
||||||
|
当日 12 个候选里 **6 个** quiet 存满 12 条。
|
||||||
|
|
||||||
|
## 二、第二批 · 基座四处(未开工)
|
||||||
|
|
||||||
|
不触碰 claims/documents,不需 replay,不需重抽。
|
||||||
|
|
||||||
|
1. `graph_store.topic_context` 的 Cypher 加 `ORDER BY`,cap 30 → 200
|
||||||
|
(现无 ORDER BY,同日重跑 moved_ratio 会变)
|
||||||
|
2. `transmission.scan` 去掉 `quiet[:12]`(那 12 是给旁批用的,却把因子锁在 144 行/日);
|
||||||
|
落库带上 `mkt_trade_date`
|
||||||
|
3. `hotspot._latest_mkt` / `_movers_set` 加 `trade_date` 参数(现恒取最新快照)
|
||||||
|
4. `sync_consensus` 加 `asof` 参数(§9-6 甲案,约 5 行)
|
||||||
|
★ **必须同时补 `report_date <= ref` 上界**,否则历史回填是前视
|
||||||
|
|
||||||
|
**已定**:新鲜度不中止扫描,只告警 + 记 `mkt_trade_date`。
|
||||||
|
|
||||||
|
## 三、第三批 · 待决策
|
||||||
|
|
||||||
|
1. **传导组内部怎么排**——当前按传导强度(连续 z_T),两段式按"冷+便宜"。
|
||||||
|
两种写法都是"传导票全部在前",差别只在这 39 只内部(top20 重合 16/20)。
|
||||||
|
2. **赛道门槛 C 的数据源**——`industry_pools` 成员级无 segment/layer,图谱在 Neo4j。
|
||||||
|
建议:claims 口径先用于覆盖体检,基座投影视图(第五/六插槽)作 S1 正解。
|
||||||
|
3. **θ_v 的 q**——按上面第 1 条读数重定。
|
||||||
|
4. **无券商覆盖的 1596 只**怎么处置。
|
||||||
|
|
||||||
|
## 四、下一步建议
|
||||||
|
|
||||||
|
`akg_gate`(0/1,全池出行)与 `akg_score` 一起注册——否则平台的 IC/分层只看得到
|
||||||
|
池内排序,完全看不到两道门槛的价值,而门槛才是这套逻辑的主体。
|
||||||
|
|
@ -1,691 +0,0 @@
|
||||||
# 评审问题的修复方案(2026-07-26)
|
|
||||||
|
|
||||||
> 按依赖顺序分三批。**第一批**(桥侧 + 视图)不依赖任何决策、不动基座,今天就能跑;
|
|
||||||
> **第二批**(基座侧)是确定性缺陷修复,需要动 astock-kg 代码;
|
|
||||||
> **第三批**(赛道门槛 C 与权重结构)需要你先拍板,我给出两种拍法各自的代码形态。
|
|
||||||
>
|
|
||||||
> 附件:`astock_kg_slot_views_v2.sql`(完整替换)、`freeze.py`(新文件,直接放桥根目录)。
|
|
||||||
> 所有改动都保持"桥只连三库、基座不算因子"的边界。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. 数据安全性核对:已抽取的成果一份都不会白费
|
|
||||||
|
|
||||||
已逐表核实(`db/postgres/init.sql` + 全仓 grep),结论先行:
|
|
||||||
**本方案的全部改动都不触碰 `claims` / `documents`,不需要 replay,不需要重抽。**
|
|
||||||
|
|
||||||
### 0.1 每一处改动写了什么
|
|
||||||
|
|
||||||
| 改动 | 写入对象 | 对已有数据的影响 |
|
|
||||||
|---|---|---|
|
|
||||||
| 1.1 视图替换 | 视图(不含数据) | 无 |
|
|
||||||
| 1.1 `ALTER ... ADD COLUMN IF NOT EXISTS mkt_trade_date` | `transmission_candidates` 加可空列 | 无(已存行取 NULL) |
|
|
||||||
| 1.1 `CREATE INDEX IF NOT EXISTS` | 索引 | 无 |
|
|
||||||
| 1.2–1.8 桥侧补丁 | 平台 MySQL `t_factor_*` | 只动平台侧派生表,可随时重算 |
|
|
||||||
| 1.9 `freeze.py` | 桥工程 `data/frozen/` | 纯新增文件 |
|
|
||||||
| 2.1 `topic_context` 加 `ORDER BY` | **只读 Cypher** | 无 |
|
|
||||||
| 2.2 `transmission.scan` | `transmission_candidates`(按 `scan_date` 删后插的日台账) | 只影响新扫描日 |
|
|
||||||
| 2.3 `hotspot._latest_mkt` 加参数 | `hotspot_candidates`(同为日台账) | 只影响新扫描日 |
|
|
||||||
| 2.4 `sync_consensus(asof)` | `consensus_daily`(主键 `ts_code, asof_date`) | 回填是**新增** asof 行;不覆盖历史 |
|
|
||||||
| 2.5 `industry_pools_history` | 新表 | 纯新增 |
|
|
||||||
|
|
||||||
**没有一处 DELETE / UPDATE / TRUNCATE 落在抽取产物上。** 全仓 grep 也确认:
|
|
||||||
`claims` / `documents` / `mkt_daily` / `company_master` 全库没有任何删除或清空语句
|
|
||||||
(`claims` 的 DDL 注释就写着 `---- 断言表:只追加 ----`,并有
|
|
||||||
`CONSTRAINT uq_claim_dedup UNIQUE (dedup_key)` 保证幂等)。
|
|
||||||
|
|
||||||
### 0.2 为什么架构上就不会白费
|
|
||||||
|
|
||||||
抽取的产物是 `claims` + `documents`,它们是**事实源**;Neo4j 里的 belief 图是**投影**。
|
|
||||||
`replay_all()` 的 docstring 说得很明白:"从 claim 层全量重建 belief 图:
|
|
||||||
只跑融合规则,**不重新抽取、不调用任何 LLM**。因此确定、便宜、稳定。"
|
|
||||||
|
|
||||||
所以就算将来真要动本体/闸门/融合规则,代价也只是一次 replay(12297 条约 80 秒),
|
|
||||||
claims 无损。真正昂贵的 re-extraction 是另一件事,`claims.extractor_model` 这一列
|
|
||||||
就是专门为"按模型版本圈定重抽范围"留的——架构本来就是为这种演进设计的。
|
|
||||||
|
|
||||||
**而本方案连 replay 都不需要**:2.1 是只读查询,2.2–2.5 全部写 PG 应用层表,
|
|
||||||
没有一处改动落在融合规则或 Neo4j 投影路径上。
|
|
||||||
|
|
||||||
### 0.3 需要你知道的三点(不是"白费",但要说清)
|
|
||||||
|
|
||||||
1. **已存的 3 天传导台账修不"好",但也不用修。**
|
|
||||||
`transmission_candidates` 现有的 07-11~07-23 三天,`quiet` 是被 `[:12]` 截断的、
|
|
||||||
`moved_ratio` 建立在任意 ≤30 抽样上。修法上线后新扫描才正确。
|
|
||||||
**好消息**:`mkt_daily` 是持久快照表(`trade_date` 在主键里、全库无删除),
|
|
||||||
所以历史 movers 其实**是可重建的** —— 2.2/2.3 改完之后,
|
|
||||||
`transmission.scan(scan_date=D, allow_stale=True)` 能对任意有 mkt 快照的 D 重扫。
|
|
||||||
(这也顺带修正了 v1.1 文档里"历史 movers 难重建"的判断——难的不是 movers,
|
|
||||||
是当时的**图谱状态**。)
|
|
||||||
**但我建议不要重扫这三天**:会用今天的图谱去套那三天,而 44079 份公告正在
|
|
||||||
以 200 份/30 分钟入库,图谱这两周变化不小 —— 那就是标准的成员性前视。
|
|
||||||
这三天目前也没被任何东西消费,直接从修好之后重新起算最干净。
|
|
||||||
|
|
||||||
2. **新鲜度断言会让传导"少产出",这是有意的。**
|
|
||||||
2.2 加的断言在 `mkt_daily` 最新 stock 快照 ≠ `scan_date` 时**中止扫描**。
|
|
||||||
在公告批量入库期间,如果 beat 被饿到(就是 `announcement-corpus-bulk-load`
|
|
||||||
记的那个单队列风险),17:30 快照晚点 → 当天传导直接没有产出。
|
|
||||||
这比"用 T−1 的 movers 生成带今日戳的候选"好,但你要预期到它会偶发触发,
|
|
||||||
也意味着 live 传导史积累得慢一点。队列分离那次修复的实机验证,
|
|
||||||
现在多了一个理由要尽快做完。
|
|
||||||
|
|
||||||
3. **唯一可能产生 LLM 花费的是"提环节覆盖",但它也不浪费任何东西。**
|
|
||||||
如果 G1 体检发现赛道覆盖太薄,对策是 `segment_backfill`(环节专项遍存量重抽)。
|
|
||||||
它零新语料、`dedup_key` 幂等、只追加不删旧 claims —— 花的是 brain 调用,
|
|
||||||
不是把已有成果作废。
|
|
||||||
|
|
||||||
### 0.4 落地前后的自查(跑一遍留个数,最稳)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 改动前后各跑一次,两次输出应完全一致(除 transmission_candidates 的列数 +1)
|
|
||||||
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 'industry_pools', count(*), max(refreshed_at)::date FROM industry_pools;
|
|
||||||
|
|
||||||
-- 顺带回答一个对回填很关键的问题:mkt_daily 的 stock 切片到底存了多少天?
|
|
||||||
-- (它决定了传导"理论上"能重扫到哪一天,也决定了 §7 回填的真实边界)
|
|
||||||
SELECT kind, min(trade_date), max(trade_date), count(DISTINCT trade_date) days
|
|
||||||
FROM mkt_daily GROUP BY kind ORDER BY kind;
|
|
||||||
SQL
|
|
||||||
```
|
|
||||||
|
|
||||||
最后那条查询的结果请一并发我 —— 如果 `kind='stock'` 的天数明显多于 3 天,
|
|
||||||
设计文档 §7 里"传导只 live 累积"这一条的**边界可以往前推**(图谱漂移仍在,
|
|
||||||
但至少多了一个"用当时 movers + 今天图谱"的、带明确标注的近似区间可选)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第一批 · 桥侧 + 视图(零决策,今天可落)
|
|
||||||
|
|
||||||
### 1.1 `sql/astock_kg_slot_views.sql` → 用附件 `astock_kg_slot_views_v2.sql` 整体替换
|
|
||||||
|
|
||||||
三处变更:`n_paths` → `n_sources`(修重复计数);暴露 `target/members_total/moved/
|
|
||||||
n_quiet_stored/mkt_trade_date`(让桥能自检上游截断与快照新鲜度);`v_factor_events`
|
|
||||||
补 `doc_id/source_type/tier`(供年报封顶)。文件里带一段幂等 `ALTER TABLE`,一起跑即可。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -i akg-postgres psql -U akg -d akg < sql/astock_kg_slot_views.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
**数据安全性**:只建视图 + 加一个可空列 + 加索引,**不写不改不删任何一行数据**。
|
|
||||||
`claims` / `documents` 完全不动,无需 replay、无需重抽。
|
|
||||||
|
|
||||||
✅ 列名已核实(不用再探):`documents.source_type`,取值
|
|
||||||
`annual_report / research_report / announcement / news / interactive_qa`
|
|
||||||
(`db/postgres/init.sql:12`)。`claims` 有 `tier / confidence / dedup_key / doc_id /
|
|
||||||
subject_norm`(`init.sql:22-58, 263`),`entity_links(alias_norm, canonical_id, entity_type)`、
|
|
||||||
`company_master(ts_code, short_name)` 均存在(`init.sql:230-251`)——
|
|
||||||
第三批 3.1 的救急 SQL 依赖的列全部对得上。
|
|
||||||
|
|
||||||
### 1.2 `common.py` — 交易日历改用行情表(**修硬伤 3**)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def trading_days(start: str, end: str) -> list:
|
|
||||||
"""目标区间交易日历。
|
|
||||||
|
|
||||||
⚠️ 曾用热度表(stock_fund_heat_scores) —— 它最早只有 2026-03-26 前后,
|
|
||||||
导致 `build akg_event --mode history --start 2024-01-01` 静默返回空
|
|
||||||
(cal 为空 → _EMPTY → 只打印"无数据(跳过)",不报错)。
|
|
||||||
改用行情表 gp_day_data(5584 天,覆盖全历史)。"""
|
|
||||||
df = db.read_mysql(
|
|
||||||
"price",
|
|
||||||
"SELECT DISTINCT `timestamp` AS trade_date FROM gp_day_data "
|
|
||||||
"WHERE `timestamp` BETWEEN %s AND %s ORDER BY `timestamp`",
|
|
||||||
(start, end))
|
|
||||||
if df.empty:
|
|
||||||
print(f" ⚠️ 交易日历为空(gp_day_data 在 {start}~{end} 无数据)"
|
|
||||||
f"——上层会跳过该区间,请核对区间与行情表覆盖")
|
|
||||||
return list(pd.to_datetime(df["trade_date"]))
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 `common.py` — 分块提交(**修放量会炸**)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def write_factor(table: str, df: pd.DataFrame, mode: str = "daily") -> None:
|
|
||||||
"""df[trade_date, stock_code, factor_value] → 幂等写因子表。
|
|
||||||
|
|
||||||
分块提交(评审):热度全史约 322×1674≈54 万行,原来单事务 executemany
|
|
||||||
走 ShardingSphere 代理有风险。改成 DELETE 一个事务 + INSERT 按块提交。
|
|
||||||
代价是中途失败会留下部分区间——但整个写入按区间幂等,重跑即修复。"""
|
|
||||||
if df is None or df.empty:
|
|
||||||
print(f" {table}: 无数据(跳过)")
|
|
||||||
return
|
|
||||||
df = df.dropna(subset=["trade_date", "stock_code", "factor_value"]).copy()
|
|
||||||
df["stock_code"] = df["stock_code"].map(to_prefix)
|
|
||||||
df["trade_date"] = pd.to_datetime(df["trade_date"]).dt.date
|
|
||||||
df = df.drop_duplicates(["trade_date", "stock_code"], keep="last")
|
|
||||||
if df.empty:
|
|
||||||
print(f" {table}: 清洗后无数据")
|
|
||||||
return
|
|
||||||
dmin, dmax = df["trade_date"].min(), df["trade_date"].max()
|
|
||||||
ensure_table(table)
|
|
||||||
rows = list(df[["trade_date", "stock_code", "factor_value"]]
|
|
||||||
.itertuples(index=False, name=None))
|
|
||||||
chunk = config.WRITE_CHUNK_ROWS
|
|
||||||
with db.factor_conn() as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(f"DELETE FROM {table} WHERE trade_date BETWEEN %s AND %s",
|
|
||||||
(dmin, dmax))
|
|
||||||
conn.commit()
|
|
||||||
for i in range(0, len(rows), chunk):
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.executemany(
|
|
||||||
f"INSERT INTO {table} (trade_date, stock_code, factor_value) "
|
|
||||||
f"VALUES (%s,%s,%s)", rows[i:i + chunk])
|
|
||||||
conn.commit()
|
|
||||||
if len(rows) > chunk:
|
|
||||||
print(f" ...{min(i + chunk, len(rows))}/{len(rows)}")
|
|
||||||
print(f" {table}: 写入 {len(rows)} 行, 日期 {dmin}~{dmax}")
|
|
||||||
```
|
|
||||||
|
|
||||||
顶部加 `import config`。
|
|
||||||
|
|
||||||
### 1.4 `common.py` — universe 过滤改成可切换(**评审 §4**)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def universe_filter(df: pd.DataFrame, col: str = "stock_code",
|
|
||||||
as_prefix: bool = True) -> pd.DataFrame:
|
|
||||||
"""按 config.SUBFACTOR_UNIVERSE 决定子因子是否受覆盖池限制。
|
|
||||||
|
|
||||||
默认 'pool'(保持现状)。**建议改 'market'**:industry_pools 只存最新态、
|
|
||||||
每周一 refresh_pools 自动长大,用它过滤历史子因子会引入成员性前视
|
|
||||||
(§2.2 批传导用的同一条论证,上升一层),且 z 统计量每周一结构性跳变。
|
|
||||||
子因子表的定位是"可独立观察的仪表",过滤该发生在消费端(akg_score)而不是这里。"""
|
|
||||||
if config.SUBFACTOR_UNIVERSE != "pool":
|
|
||||||
return df
|
|
||||||
uni = load_universe()
|
|
||||||
if as_prefix:
|
|
||||||
uni = {to_prefix(x) for x in uni}
|
|
||||||
return df[df[col].astype(str).str.strip().isin(uni)]
|
|
||||||
```
|
|
||||||
|
|
||||||
`build_heat` / `build_upside` / `build_event` 里的 `uni = ...` + `isin(uni)` 都换成调它。
|
|
||||||
`build_transmission` **不换** —— 传导天生就是池内语义。
|
|
||||||
|
|
||||||
### 1.5 `config.py` — 追加四个旋钮
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 子因子是否受覆盖池限制:pool(现状)| market(评审建议,避免成员性前视)
|
|
||||||
SUBFACTOR_UNIVERSE = os.environ.get("SUBFACTOR_UNIVERSE", "pool").lower()
|
|
||||||
# 行情读取按月分块(防 `--start 2006` 把千万行拉进 pandas)
|
|
||||||
PRICE_CHUNK_DAYS = int(os.environ.get("PRICE_CHUNK_DAYS", "31"))
|
|
||||||
# 因子表写入分块行数
|
|
||||||
WRITE_CHUNK_ROWS = int(os.environ.get("WRITE_CHUNK_ROWS", "50000"))
|
|
||||||
# 单文档最多贡献几条事件(年报能抽十几条,见评审 §6.5)
|
|
||||||
EVENT_MAX_PER_DOC = int(os.environ.get("EVENT_MAX_PER_DOC", "3"))
|
|
||||||
# 事件只认哪些来源(documents.source_type)。默认只认公告——年报里的"历史诉讼"
|
|
||||||
# 会被记成披露日的当日负面事件,是 S2 否决闸最危险的假信号来源。
|
|
||||||
# 想放宽就填 "announcement,annual_report"。
|
|
||||||
EVENT_SOURCE_TYPES = {
|
|
||||||
s.strip() for s in
|
|
||||||
os.environ.get("EVENT_SOURCE_TYPES", "announcement").split(",") if s.strip()}
|
|
||||||
```
|
|
||||||
|
|
||||||
`.env.example` 同步加这四行(都给默认值,可不填)。
|
|
||||||
|
|
||||||
### 1.6 `factors.py` — 行情按月分块(**修放量会炸**)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _read_gp_price(start, end):
|
|
||||||
"""gp_day_data 现价。两处修正(评审):
|
|
||||||
① 按月分块 —— 原来一次拉全区间,`--mode history --start 2006-01-01`
|
|
||||||
会把千万级行拉进 pandas;
|
|
||||||
② 不在 SQL 里按代码过滤 —— 代码形态(600000.SH / SH600000 / 600000)
|
|
||||||
两边不一致,SQL 过滤容易全空且难排查,改在 pandas 侧折前缀后过滤。"""
|
|
||||||
cands = [config.PRICE_CODE_COL] + [c for c in ("symbol", "ts_code")
|
|
||||||
if c != config.PRICE_CODE_COL]
|
|
||||||
col, last = None, None
|
|
||||||
for c in cands: # 先用 LIMIT 1 探列名,别用全区间去试错
|
|
||||||
try:
|
|
||||||
db.read_mysql("price", f"SELECT `{c}` FROM gp_day_data LIMIT 1")
|
|
||||||
col = c
|
|
||||||
break
|
|
||||||
except Exception as e: # noqa: BLE001
|
|
||||||
last = e
|
|
||||||
if col is None:
|
|
||||||
raise RuntimeError(f"gp_day_data 代码列都不行(试了 {cands}): {last!r}")
|
|
||||||
print(f" (upside 现价用 gp_day_data.{col})")
|
|
||||||
|
|
||||||
parts, cur = [], pd.Timestamp(start)
|
|
||||||
endts = pd.Timestamp(end)
|
|
||||||
while cur <= endts:
|
|
||||||
hi = min(cur + pd.Timedelta(days=config.PRICE_CHUNK_DAYS - 1), endts)
|
|
||||||
parts.append(db.read_mysql(
|
|
||||||
"price",
|
|
||||||
f"SELECT `timestamp` AS trade_date, `{col}` AS ts_code, close "
|
|
||||||
f"FROM gp_day_data WHERE `timestamp` BETWEEN %s AND %s",
|
|
||||||
(cur.date().isoformat(), hi.date().isoformat())))
|
|
||||||
cur = hi + pd.Timedelta(days=1)
|
|
||||||
return (pd.concat(parts, ignore_index=True) if parts
|
|
||||||
else pd.DataFrame(columns=["trade_date", "ts_code", "close"]))
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.7 `factors.py` — 传导改用 `n_sources` + 上游截断自检(**修硬伤 2**)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def build_transmission(start, end):
|
|
||||||
"""传导分 = 指向该股所在环节的 **distinct 源数** ×(1 − 已动比例);
|
|
||||||
同股同日多候选取最大。
|
|
||||||
|
|
||||||
口径修正(评审 硬伤2):原用 n_paths = jsonb_array_length(paths),
|
|
||||||
但 cascade() 的变长边 *1..N 会把 A→B 与 A→X→B 各返回一条,
|
|
||||||
transmission_targets() 的 updown/supply/drives 三桶之间也不去重,
|
|
||||||
于是同一 source 对同一 target 重复计入。改用 distinct source 数,
|
|
||||||
也更贴合传导模块自述的"多源汇聚 = 传导逻辑更硬"。"""
|
|
||||||
tr = db.read_pg(
|
|
||||||
"SELECT scan_date, target, ts_code, n_sources, n_paths_raw, moved_ratio, "
|
|
||||||
" members_total, n_quiet_stored, mkt_trade_date "
|
|
||||||
"FROM v_factor_transmission WHERE scan_date BETWEEN %s AND %s",
|
|
||||||
(start, end))
|
|
||||||
if tr.empty:
|
|
||||||
return _EMPTY
|
|
||||||
|
|
||||||
# ---- 上游失真自检:把三个"静默失真"变成显式告警(评审 硬伤1、§6.2)----
|
|
||||||
if tr["mkt_trade_date"].notna().any():
|
|
||||||
bad = tr[tr["mkt_trade_date"].astype(str) != tr["scan_date"].astype(str)]
|
|
||||||
if not bad.empty:
|
|
||||||
print(f" ⚠️ {bad['scan_date'].nunique()} 个 scan_date 的 movers 快照日"
|
|
||||||
f"与 scan_date 不符(17:30 快照晚点)——这些日的传导项不可信")
|
|
||||||
if (tr["members_total"] >= 30).any():
|
|
||||||
n = tr.loc[tr["members_total"] >= 30, "target"].nunique()
|
|
||||||
print(f" ⚠️ {n} 个环节 members_total>=30,撞上游 topic_context cap"
|
|
||||||
f"——moved_ratio 建立在任意 ≤30 抽样上(先修基座再放量)")
|
|
||||||
if (tr["n_quiet_stored"] >= 12).any():
|
|
||||||
n = tr.loc[tr["n_quiet_stored"] >= 12, "target"].nunique()
|
|
||||||
print(f" ⚠️ {n} 个环节 quiet 存满 12 条,撞 quiet[:12] 截断"
|
|
||||||
f"——真实未动成员更多,因子覆盖被展示逻辑锁住")
|
|
||||||
|
|
||||||
tr["factor_value"] = (tr["n_sources"].astype(float)
|
|
||||||
* (1.0 - pd.to_numeric(tr["moved_ratio"],
|
|
||||||
errors="coerce").fillna(0.0)))
|
|
||||||
g = (tr.groupby(["scan_date", "ts_code"])["factor_value"].max().reset_index()
|
|
||||||
.rename(columns={"scan_date": "trade_date", "ts_code": "stock_code"}))
|
|
||||||
return g[["trade_date", "stock_code", "factor_value"]]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.8 `factors.py` — 事件的年报污染防护(**S2 前必修,现在顺手做**)
|
|
||||||
|
|
||||||
`build_event` 里读完 `ev` 之后、算 `pol` 之前插入:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# ---- 年报污染防护(评审 §6.5)----
|
|
||||||
# v_factor_events 的锚 documents.meta->>'company_ts_code' 年报也有,而一份年报
|
|
||||||
# 能抽十几条 EVENT,且含**历史**诉讼/处罚 —— 会在年报披露日形成巨大负值尖峰。
|
|
||||||
# hotspot._pick_event_anomalies 为此专门做了防刷屏(other 不进 / 同主体同类型
|
|
||||||
# 去重 / 单主体≤2),桥侧原来零保护。三道,从强到弱:
|
|
||||||
if "source_type" in ev.columns:
|
|
||||||
# documents.source_type ∈ annual_report / research_report / announcement
|
|
||||||
# / news / interactive_qa (init.sql:12 已核实)
|
|
||||||
keep = ev["source_type"].astype(str).isin(config.EVENT_SOURCE_TYPES)
|
|
||||||
if (~keep).any():
|
|
||||||
drop_by = ev.loc[~keep, "source_type"].value_counts().to_dict()
|
|
||||||
print(f" (事件:按 source_type 剔除 {int((~keep).sum())} 条 {drop_by})")
|
|
||||||
ev = ev[keep]
|
|
||||||
# 同主体+同类型+同披露日只留置信度最高的一条
|
|
||||||
if "confidence" in ev.columns:
|
|
||||||
ev = ev.sort_values("confidence", ascending=False, na_position="last")
|
|
||||||
ev = ev.drop_duplicates(["ts_code", "event_type", "direction", "disclosure_date"])
|
|
||||||
# 单文档封顶:一份文档最多贡献 EVENT_MAX_PER_DOC 条
|
|
||||||
if "doc_id" in ev.columns:
|
|
||||||
ev = ev.groupby("doc_id", group_keys=False).head(config.EVENT_MAX_PER_DOC)
|
|
||||||
if ev.empty:
|
|
||||||
return _EMPTY
|
|
||||||
```
|
|
||||||
|
|
||||||
`build_event` 的 SQL 同步改成 `SELECT ts_code, disclosure_date, event_type, direction,
|
|
||||||
confidence, doc_id, doc_type FROM v_factor_events WHERE ...`。
|
|
||||||
|
|
||||||
另外 `cal` 为空时别静默返回(1.2 已在 `trading_days` 里加了告警,这里再补一句):
|
|
||||||
|
|
||||||
```python
|
|
||||||
cal = common.trading_days(start, end)
|
|
||||||
if not cal:
|
|
||||||
print(f" ⚠️ {start}~{end} 无交易日 → 事件因子空转(不是"没有事件")")
|
|
||||||
return _EMPTY
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.9 `freeze.py` — 新文件,直接放桥根目录(**评审 §5,最高优先级**)
|
|
||||||
|
|
||||||
见附件。这一条是**唯一有时间不可逆性的**:传导、池、Segment 边、前复权 close
|
|
||||||
都是过期即不可复原,`§10` 里"GRU 复活需 ≥1 年 live 传导史"这个计时器只在
|
|
||||||
开始存快照那天启动。它同时一次性满足判收标准里的"重跑不漂移"与"赛道成员可审计"。
|
|
||||||
|
|
||||||
`run.py` 三处小改:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# ① 顶部 subparser 加
|
|
||||||
sub.add_parser("freeze").add_argument("--date")
|
|
||||||
|
|
||||||
# ② cmd_build 收集产物并在末尾冻结
|
|
||||||
def cmd_build(which, mode, start, end, date, do_freeze=True):
|
|
||||||
...
|
|
||||||
frames = {}
|
|
||||||
for code in codes:
|
|
||||||
...
|
|
||||||
df = factors.BUILDERS[code](start, end)
|
|
||||||
frames[f"factor_{code}"] = df
|
|
||||||
common.write_factor(factors.FACTORS[code], df, mode)
|
|
||||||
if do_freeze and mode == "daily":
|
|
||||||
import freeze
|
|
||||||
freeze.snapshot(start, extra_frames=frames)
|
|
||||||
|
|
||||||
# ③ dispatch
|
|
||||||
elif a.cmd == "freeze":
|
|
||||||
import freeze
|
|
||||||
freeze.snapshot(a.date)
|
|
||||||
```
|
|
||||||
|
|
||||||
`build` 子命令加一个 `--no-freeze`(回填时不必每天冻结)。
|
|
||||||
`docker-compose.yml` 的 `volumes` 已经挂了 `.:/app`,`data/frozen/` 天然落在宿主仓库里;
|
|
||||||
记得 `.gitignore` 里决定要不要跟踪(我建议**跟踪 manifest.json、忽略数据文件**,
|
|
||||||
manifest 才是可 diff 的审计线)。
|
|
||||||
|
|
||||||
### 1.10 `probe.py` — 加两节(G1 需要)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def probe_corr():
|
|
||||||
"""池内三项相关矩阵(评审 §2)——若 corr(z_H, z_V) > 0.5,
|
|
||||||
"三项加权"实际是两项,§9-8 的权重讨论要重开。"""
|
|
||||||
_sec("corr · 池内 (z_T, z_H, z_V) 相关矩阵")
|
|
||||||
# 取最新可算日:传导有值的最近 scan_date
|
|
||||||
d = db.read_pg("SELECT max(scan_date) FROM v_factor_transmission").iloc[0, 0]
|
|
||||||
... # 三路各取当日截面 → 外连接 → 标准化 → .corr(method="spearman")
|
|
||||||
# 同时打印:|P| 规模、|P ∩ 传导>0|、传导为 0 的占比
|
|
||||||
```
|
|
||||||
|
|
||||||
`SECTIONS` 里注册 `"corr": probe_corr`,`run.py` 的 `--section` choices 加 `corr`。
|
|
||||||
(`tracks` 节等第三批的 yml 有草案了再加。)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第二批 · 基座侧(确定性缺陷修复,不算"新增因子计算")
|
|
||||||
|
|
||||||
> 这四处都是"让上游可复现/不撒谎",不涉及打分,不破 §4 铁律。
|
|
||||||
> `graph_store` 那处我是通过子代理读到的 Cypher,落地前请你对一眼实际代码。
|
|
||||||
|
|
||||||
### 2.1 `graph_store.topic_context` — 加 `ORDER BY`、cap 参数化(**修硬伤 1 的根**)
|
|
||||||
|
|
||||||
```cypher
|
|
||||||
MATCH (c)-[m:IN_SEGMENT {status:'active'}]->(t:Segment {segment_name:$id})
|
|
||||||
RETURN coalesce(c.name, c.ts_code, c.entity_key) AS name, c.ts_code AS ts_code
|
|
||||||
ORDER BY coalesce(c.ts_code, c.entity_key) -- ★ 新增:无序 → 同日重跑结果会变
|
|
||||||
LIMIT $cap
|
|
||||||
```
|
|
||||||
|
|
||||||
`cap` 默认从 30 提到 200(或让调用方传)。**这一条不改,硬伤 1 修不掉** ——
|
|
||||||
`transmission` 的 `members_total / moved_ratio` 会一直建立在任意 ≤30 抽样上。
|
|
||||||
|
|
||||||
### 2.2 `transmission.scan` — 存全量 quiet + 新鲜度断言 + 记快照日
|
|
||||||
|
|
||||||
```python
|
|
||||||
def scan(scan_date=None, annotate=True, max_candidates=12, member_cap=200):
|
|
||||||
from app.analysis import hotspot
|
|
||||||
from app.store import claim_store, graph_store
|
|
||||||
|
|
||||||
sd = date.fromisoformat(scan_date) if scan_date else date.today()
|
|
||||||
|
|
||||||
# ---- 0. 新鲜度记账(评审 §6.2)----
|
|
||||||
# ★ 用户决定(2026-07-26):**照常落库,只打告警 + 记 mkt_trade_date**,不中止。
|
|
||||||
# 理由:保覆盖。传导每日只有几十行,公告批量入库期间 beat 一旦被饿就整天空白,
|
|
||||||
# 代价太大;改由桥侧按 mkt_trade_date 自行判断要不要采信(factors.py 已实现告警)。
|
|
||||||
# 背景:movers/quiet 来自 mkt_daily 的 **最新** 快照,不是 sd 的快照。若 17:30
|
|
||||||
# sync_market 晚点,会用 T−1 的 movers 写成 scan_date=T;反过来重跑历史日
|
|
||||||
# 会用今天的快照 = 直接前视。落这一列后,下游至少看得见。
|
|
||||||
with get_pool().connection() as conn:
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT max(trade_date) FROM mkt_daily WHERE kind='stock'").fetchone()
|
|
||||||
mkt_td = row[0] if row else None
|
|
||||||
if mkt_td != sd:
|
|
||||||
logger.warning(
|
|
||||||
"传导扫描:mkt_daily 最新 stock 快照 %s ≠ scan_date %s —— movers 非当日,"
|
|
||||||
"候选照常落库但已记 mkt_trade_date,下游(因子桥)会据此告警。"
|
|
||||||
"若非预期,补跑 sync_market 后重扫本日即可(同日幂等)。", mkt_td, sd)
|
|
||||||
...
|
|
||||||
for t in targets.values():
|
|
||||||
ctx = graph_store.topic_context(t["target_type"], t["target"], cap=member_cap)
|
|
||||||
members = [m for m in ctx["members"] if m.get("ts_code")]
|
|
||||||
if not members:
|
|
||||||
continue
|
|
||||||
moved = [m for m in members if m["ts_code"] in movers]
|
|
||||||
quiet = [m for m in members if m["ts_code"] not in movers]
|
|
||||||
# ★ 原来是 `for m in quiet[:12]` —— 那个 12 是给旁批/展示用的,
|
|
||||||
# 却把因子的结构上限锁在 12×12=144 行/日(实测 83 吻合)。
|
|
||||||
# 落库存全量,旁批仍只喂前 12(见下面 _annotate 的入参)。
|
|
||||||
quiet_rows, upsides, over = [], [], 0
|
|
||||||
for m in quiet: # ← 去掉 [:12]
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
`_annotate` 调用处把喂给 brain 的 quiet 截到 12:`_annotate(out, quiet_show=12)`,
|
|
||||||
prompt 里只列前 12(旁批的信息密度需求和因子的覆盖需求本来就是两件事)。
|
|
||||||
|
|
||||||
落库 INSERT 加 `mkt_trade_date`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
conn.execute("ALTER TABLE transmission_candidates "
|
|
||||||
"ADD COLUMN IF NOT EXISTS mkt_trade_date DATE") # 幂等,跟 _DDL 一起
|
|
||||||
...
|
|
||||||
"""INSERT INTO transmission_candidates
|
|
||||||
(scan_date, rank, target, target_type, paths, members_total, moved,
|
|
||||||
moved_ratio, quiet, pricing, annotation, mkt_trade_date)
|
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"""
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 `hotspot._latest_mkt / _movers_set` — 接受 `trade_date`
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _latest_mkt(kind: str, trade_date: Optional[date] = None):
|
|
||||||
"""trade_date 给定时取该日快照;不给才退回 max(trade_date)。
|
|
||||||
(评审 §6.2:原来恒取最新,导致 scan_date 只是个标签。)"""
|
|
||||||
with get_pool().connection() as conn:
|
|
||||||
if trade_date is None:
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT max(trade_date) FROM mkt_daily WHERE kind=%s", (kind,)).fetchone()
|
|
||||||
td = row[0] if row else None
|
|
||||||
else:
|
|
||||||
td = trade_date
|
|
||||||
...
|
|
||||||
|
|
||||||
def _movers_set(trade_date: Optional[date] = None) -> set[str]:
|
|
||||||
_, stocks = _latest_mkt("stock", trade_date)
|
|
||||||
return {x["code"] for x in stocks if (x.get("pct_change") or 0) >= 3}
|
|
||||||
```
|
|
||||||
|
|
||||||
`hotspot.scan` / `transmission.scan` 里的 `_movers_set()` 都传 `sd`。
|
|
||||||
这样 2.2 的断言就成了双保险(断言拦"快照缺",传参拦"取错日")。
|
|
||||||
|
|
||||||
### 2.4 `sync_consensus` — 参数化 `asof`(**§9-6 甲案,约 5 行**)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def sync_consensus(lookback_days: int = 90,
|
|
||||||
asof: Optional[str] = None) -> dict[str, Any]:
|
|
||||||
"""...(原 docstring 保留)
|
|
||||||
|
|
||||||
asof(评审 §6.4 / 设计 §9-6 甲案):给定时按该日重放同一套聚合逻辑,
|
|
||||||
live 与 history 共用一份代码、零漂移。不给 = today(原行为)。"""
|
|
||||||
ref = date.fromisoformat(asof) if asof else date.today()
|
|
||||||
pool_codes = _pool_ts_codes()
|
|
||||||
if not pool_codes:
|
|
||||||
return {"skipped": "无已注册股票池"}
|
|
||||||
since = (ref - timedelta(days=lookback_days)).isoformat()
|
|
||||||
...
|
|
||||||
cur.execute(
|
|
||||||
"""SELECT ts_code, report_date, org_name, quarter, eps, np,
|
|
||||||
max_price, min_price, rating
|
|
||||||
FROM gp_report_rc
|
|
||||||
WHERE report_date >= %s AND report_date <= %s -- ★ 上界必加
|
|
||||||
AND ts_code IN %s""",
|
|
||||||
(since, ref.isoformat(), tuple(pool_codes)))
|
|
||||||
...
|
|
||||||
# today = date.today() → 改成用 ref
|
|
||||||
```
|
|
||||||
|
|
||||||
**★ 上界 `report_date <= ref` 是这条改动里最关键的一行** —— 不加,历史回填会把
|
|
||||||
未来研报算进当时的一致预期,就是教科书式前视。live 模式下 today 本就是上界,
|
|
||||||
所以原代码没有它也对;参数化之后必须补。
|
|
||||||
|
|
||||||
两个已知残余(回填时在文档里写明,不影响本次改动):
|
|
||||||
|
|
||||||
- `_pool_ts_codes()` 用的是**今天的池** → 历史回填仍带成员性前视(同评审 §4);
|
|
||||||
- `target_mid_avg` 是 90 天内全部研报行的简单平均,不按机构去重、不按时间加权 ——
|
|
||||||
历史与 live 一致,所以不影响可比性,但要知道这个"中枢"的语义比字面弱。
|
|
||||||
|
|
||||||
回填跑法:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 逐日重放(一天约 800 行,500 天约 40 万行,consensus_daily 主键天然幂等)
|
|
||||||
docker compose exec backend python -c "
|
|
||||||
from app.store import market_snapshot as ms
|
|
||||||
import pandas as pd
|
|
||||||
for d in pd.bdate_range('2025-01-01','2026-07-24'):
|
|
||||||
print(d.date(), ms.sync_consensus(asof=d.date().isoformat()))"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.5(可选,但强烈建议)`industry_pools` 开始存历史
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE TABLE IF NOT EXISTS industry_pools_history (
|
|
||||||
snapshot_date DATE NOT NULL,
|
|
||||||
theme TEXT NOT NULL,
|
|
||||||
members JSONB NOT NULL,
|
|
||||||
stats JSONB,
|
|
||||||
PRIMARY KEY (snapshot_date, theme)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
`claim_store.upsert_pool` 末尾顺手写一行(或 `refresh_pools` 跑完整表快照一次)。
|
|
||||||
成本几乎为零,解开的是评审 §4 那一整类问题(universe 成员性前视 + z 统计量周一跳变)。
|
|
||||||
桥侧 `freeze.py` 已经在冻结当日 universe,两边互为备份。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 第三批 · 需要你拍板的两件(我给出两种拍法的代码形态)
|
|
||||||
|
|
||||||
### 3.1 赛道门槛 C 的数据源:三条路,我推荐 (c)
|
|
||||||
|
|
||||||
| | 做法 | 成本 | 代价 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| (a) | 从 `claims` 重建(一条 SQL) | 最低,今天就能跑 | 融合前口径:无 status/supersede/tier 裁决,会捞到已被顶替的历史归属;object 侧无 norm,同义环节不合并 |
|
|
||||||
| (b) | 桥直连 Neo4j | 中 | 破坏"桥只连三库",裁决逻辑复制进桥 |
|
|
||||||
| (c) | **基座投影表 + 第五/六个只读视图** | 中,但一次性 | 无。是投影不是打分,不破 §4 |
|
|
||||||
|
|
||||||
**我的建议是 (a) 先用于 G1 体检、(c) 作为 S1 的正解。** 体检只需要回答"这个赛道在图谱里
|
|
||||||
有没有料",(a) 的超集口径完全够用而且更保守(宁可高估覆盖,也不要因为口径太严把
|
|
||||||
本来有料的赛道判成空)。而一旦进公式,成员性必须走裁决后的口径。
|
|
||||||
|
|
||||||
(a) 的 SQL(**列名待核实**,先跑第一段确认再跑第二段):
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- 先确认列名
|
|
||||||
SELECT column_name FROM information_schema.columns
|
|
||||||
WHERE table_name IN ('claims','entity_links','company_master')
|
|
||||||
ORDER BY table_name, ordinal_position;
|
|
||||||
|
|
||||||
-- G1 体检用:环节 → 上市成员(融合前口径,仅供体检,不进公式)
|
|
||||||
CREATE OR REPLACE VIEW v_probe_segment_members AS
|
|
||||||
SELECT c.object_id AS segment_name,
|
|
||||||
c.qualifiers->>'chain' AS chain,
|
|
||||||
COALESCE(el.canonical_id, cm.ts_code) AS ts_code,
|
|
||||||
c.tier,
|
|
||||||
count(*) AS n_claims
|
|
||||||
FROM claims c
|
|
||||||
LEFT JOIN entity_links el ON el.alias_norm = c.subject_norm
|
|
||||||
LEFT JOIN company_master cm ON cm.short_name = c.subject_id
|
|
||||||
WHERE c.predicate = 'IN_SEGMENT' AND c.object_type = 'Segment'
|
|
||||||
GROUP BY 1,2,3,4;
|
|
||||||
|
|
||||||
-- 环节上下游边(单表零 join)
|
|
||||||
CREATE OR REPLACE VIEW v_probe_segment_edges AS
|
|
||||||
SELECT DISTINCT c.subject_id AS up, c.object_id AS down, c.tier
|
|
||||||
FROM claims c
|
|
||||||
WHERE c.predicate = 'SEGMENT_UPSTREAM_OF' AND c.object_type = 'Segment';
|
|
||||||
```
|
|
||||||
|
|
||||||
(c) 的形状:基座在 Neo4j 投影 beat 之后,把
|
|
||||||
`MATCH (co:Company)-[e:IN_SEGMENT {status:'active'}]->(s:Segment)` 与
|
|
||||||
`MATCH (a:Segment)-[e:SEGMENT_UPSTREAM_OF {status:'active'}]->(b:Segment)`
|
|
||||||
两条 Cypher 的结果物化到 PG 两张表,视图名就叫
|
|
||||||
`v_factor_segment_members(segment_name, chain, ts_code, tier, updated_at)` /
|
|
||||||
`v_factor_segment_edges(up, down, tier, updated_at)` ——
|
|
||||||
`freeze.py` 已经预留了对这两个视图的冻结(视图不存在时静默跳过,不报错)。
|
|
||||||
|
|
||||||
**顺带三件与 C 有关的定调建议:**
|
|
||||||
|
|
||||||
1. **`layer` 写在 yml 里人工指定,加一列 `layer_source`。** 四层枚举(材料→设备→
|
|
||||||
制造→应用)系统里根本不存在,且 `enums._SEGMENT_BAD_SUBSTR` 把"上游/中游/下游/
|
|
||||||
环节"列为环节名**禁用词素**(注释:"链位置由图上边表达")。S1 公式又不用 layer ——
|
|
||||||
**降为可选元数据,别让它阻塞 G2**。
|
|
||||||
2. **C 做两级并强制标 `source_rule`**:`graph`(强,可审计到具体 claim)/
|
|
||||||
`fallback`(弱,概念标签或申万行业白名单兜底)。否则核聚变/低空经济/商业航天
|
|
||||||
一旦查不到,C 要么塌成空集、要么退化成"电子+计算机"。
|
|
||||||
3. **覆盖为空的赛道直接进采集清单** —— 这就是基座「需求闭环」的正用法,
|
|
||||||
比放弃赛道有价值(`claim_store.open_demand("research_report", "Concept", <赛道>)`)。
|
|
||||||
|
|
||||||
### 3.2 权重结构:两段式(推荐)还是真混合
|
|
||||||
|
|
||||||
传导池内 95% 为 0,z-score 后 `0.5·z_T` 的组间落差压过另两项全幅
|
|
||||||
(数值验证:200 只候选 / 10 只有传导 → top20 里传导票 9.8/10)。所以现在的
|
|
||||||
0.5/0.3/0.2 事实上是"传导票优先,组内再比冷和便宜"。二选一:
|
|
||||||
|
|
||||||
**A(推荐)· 承认两段式,代码更短、行为与文字一致**
|
|
||||||
|
|
||||||
```python
|
|
||||||
def akg_score(pool: pd.DataFrame) -> pd.Series:
|
|
||||||
"""景气度漏斗的池内排序(评审 §2 方案 A)。
|
|
||||||
|
|
||||||
结构 = 传导档位(主键) + 组内「冷 & 便宜」(次键)。
|
|
||||||
这不是"降低传导权重",恰恰是把"传导第一"写实:
|
|
||||||
0.5/0.3/0.2 在 95% 稀疏下已经等价于此,写明比藏在 z-score 里好 ——
|
|
||||||
可解释、可调试、一眼看出是哪一段在起作用。
|
|
||||||
"""
|
|
||||||
t = np.log1p(pool["transmission"].fillna(0.0))
|
|
||||||
# 档位:0 = 无传导;1 = 有传导且强度在有传导组的下半;2 = 上半
|
|
||||||
tier = pd.Series(0, index=pool.index, dtype=float)
|
|
||||||
hit = t > 0
|
|
||||||
if hit.any():
|
|
||||||
tier[hit] = np.where(t[hit] >= t[hit].median(), 2.0, 1.0)
|
|
||||||
|
|
||||||
zH = -_robust_z(pool["heat"].fillna(pool["heat"].median()))
|
|
||||||
zV = _robust_z(pool["upside"])
|
|
||||||
tiebreak = 0.6 * zH + 0.4 * zV # 次序仍是「还没热」>「便宜」
|
|
||||||
|
|
||||||
# 档间不可逆(这正是"传导第一"的含义),档内连续排序
|
|
||||||
span = 10.0 # > tiebreak 的理论幅度(约 ±6)
|
|
||||||
return tier * span + tiebreak
|
|
||||||
```
|
|
||||||
|
|
||||||
好处:档位与次键各自可独立观察;调试时 `groupby(tier)` 直接看出每档的表现;
|
|
||||||
把 §9-9("还没热"该不该做条件项)一并解决了 —— 它现在天然是组内量。
|
|
||||||
|
|
||||||
**B · 真要连续混合**:`w_T` 降到 **0.10~0.15**,或对 `z_T` 做有界变换:
|
|
||||||
|
|
||||||
```python
|
|
||||||
zT = np.clip((t - t.mean()) / (t.std() or 1.0), -3, 3) / 3 * 1.5 # 压到与另两项同量级
|
|
||||||
score = 0.5 * zT + 0.3 * zH + 0.2 * zV # 实测能把 top20 命中从 9.8 拉到 7.5
|
|
||||||
```
|
|
||||||
|
|
||||||
**无论选哪个,都建议同时注册 `akg_gate`**(0/1,全覆盖池出行)。否则平台的 IC/分层
|
|
||||||
只看得到"池内排序"的价值,**完全看不到两道门槛的价值**——而门槛才是这套逻辑的主体。
|
|
||||||
两个因子、两个仪表,各答一个问题,"哪一项在拖后腿"一眼可见。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 落地顺序建议
|
|
||||||
|
|
||||||
```
|
|
||||||
今天 1.1 视图替换 → 1.2/1.3/1.5/1.6/1.7/1.8 桥侧补丁 → 1.9 freeze.py
|
|
||||||
跑一次:run.py views → run.py freeze → run.py build all --mode daily
|
|
||||||
(freeze 的 warnings 就是硬伤 1 的实测量级,正好拿来定 2.1/2.2 的 cap)
|
|
||||||
本周 2.1~2.3 基座三处(改完 replay 不需要,只影响新扫描)→ 2.5 池历史表
|
|
||||||
1.10 probe 加 corr 节 → 3.1(a) 的两个体检视图 → 跑 run.py probe
|
|
||||||
⇒ G1 体检报告(含三项相关矩阵 + 赛道命中量级)发我
|
|
||||||
再定 3.1 的 C 口径与赛道清单、3.2 的权重结构 → G2/G3
|
|
||||||
2.4 sync_consensus 参数化 + 逐日回填 → G4
|
|
||||||
```
|
|
||||||
|
|
||||||
**如果只能做一件:`freeze.py`。** 别的都能补,快照不存就永久没了。
|
|
||||||
|
|
@ -1,355 +0,0 @@
|
||||||
# 第一批 · 联调验证清单(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` 参数取消。
|
|
||||||
|
|
@ -1,197 +0,0 @@
|
||||||
# 第一批验证清单(**已作废,见下**)
|
|
||||||
|
|
||||||
> ⚠️ **本文件已被 `第一批联调验证清单_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` 的向量化改写做过逐位等值回归;
|
|
||||||
> `probe corr` 的边界情形(传导全 0、MAD=0、池内只剩 1~2 只)已跑过不炸。
|
|
||||||
> **本批不动 astock-kg 代码**,只替换基座上的四个视图(`CREATE OR REPLACE` + 一个可空列 + 一个索引)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 0. 先提交(改完再跑,出问题好回滚)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/Documents/work/project/akg-factor-bridge
|
|
||||||
git add -A
|
|
||||||
git commit -m "fix: 交叉评审第一批——传导口径/事件回填/年报污染/分块与冻结
|
|
||||||
|
|
||||||
- 视图 v2:n_paths→n_sources(distinct 源数,修 cascade 变长边与三桶重复计数);
|
|
||||||
暴露 target/members_total/n_quiet_stored/mkt_trade_date 供桥自检上游截断;
|
|
||||||
v_factor_events 补 doc_id/source_type/tier
|
|
||||||
- common.trading_days 改用 gp_day_data —— 原用热度表(最早2025-03-26)导致
|
|
||||||
事件 history 回填静默返空
|
|
||||||
- factors: 事件加年报污染防护(source_type 白名单+同键去重+单文档封顶)、
|
|
||||||
衰减向量化(逐位等值);行情按月分块;传导上游截断/快照陈旧显式告警
|
|
||||||
- common.write_factor 分块提交(热度全史约54万行)
|
|
||||||
- 新增 freeze.py:每日冻结 universe/传导原始行/一致预期/前复权close/热度+manifest
|
|
||||||
—— 上游不可复现(Neo4j无ORDER BY、前复权重写、池只存最新态),
|
|
||||||
\"可复现\"只能定义为\"能从冻结输入重算出同一个值\"
|
|
||||||
- probe 新增 corr 节:池内三项相关矩阵 + 权重体检(top-K 里几只是传导票)
|
|
||||||
- 新增 .gitignore(原来没有,.env 会被 git add -A 带进库)
|
|
||||||
- SUBFACTOR_UNIVERSE/EVENT_SOURCE_TYPES 等旋钮进 .env.example,默认保持原行为"
|
|
||||||
```
|
|
||||||
|
|
||||||
> ⚠️ 顺带发现:**这个仓库原来没有 `.gitignore`**,一旦你在开发机建了 `.env`,
|
|
||||||
> `git add -A` 会把三处数据库口令一起提交。已补上。
|
|
||||||
> 另外 `.idea/` 已经被跟踪了,`.gitignore` 对它无效——想清出去要
|
|
||||||
> `git rm -r --cached .idea && git commit -m "chore: .idea 移出版本控制"`(可选)。
|
|
||||||
|
|
||||||
`.review_backup_20260726/` 是改动前的原文件备份 + 补丁包,已在 `.gitignore` 里。
|
|
||||||
验证通过后整目录删掉即可。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 应用视图(基座侧,只建视图 + 加一个可空列 + 加索引)
|
|
||||||
|
|
||||||
> ⚠️ **勘误(2026-07-26)**:原来这里写的 `docker exec -i akg-postgres psql ...`
|
|
||||||
> 只在 **astock-kg 那台机器**上成立。桥是跨机部署的独立单元,桥的服务器上没有
|
|
||||||
> `akg-postgres` 容器,桥容器本身(python:3.11-slim)也没有 psql 客户端。
|
|
||||||
> 已新增 `run.py apply-views`,用桥自己的 `AKG_PG_*` 连接执行 DDL。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 推荐:在桥这边跑,零新增凭据、全程 Docker
|
|
||||||
docker compose exec -T akg-factor-bridge python run.py apply-views --dry-run # 先看清单
|
|
||||||
docker compose exec -T akg-factor-bridge python run.py apply-views
|
|
||||||
|
|
||||||
# 若日常账号是只读的,会逐条报 permission denied —— 用基座 owner 跑这一次:
|
|
||||||
docker compose exec -T \
|
|
||||||
-e AKG_PG_USER=<owner> -e AKG_PG_PASSWORD=<pw> \
|
|
||||||
akg-factor-bridge python run.py apply-views
|
|
||||||
# 必须用 -e 传进容器:写在 docker 前面只设置宿主机进程的环境变量,进不去
|
|
||||||
```
|
|
||||||
|
|
||||||
**预期**:6 条语句逐条 ✅(4 个 CREATE OR REPLACE VIEW + 1 个 ALTER TABLE + 1 个 CREATE INDEX)。
|
|
||||||
**不动任何一行数据**,`claims` / `documents` 一个字节不变。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 起容器 + 连通性与视图版本自检
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/Documents/work/project/akg-factor-bridge
|
|
||||||
docker compose up -d --build
|
|
||||||
docker compose exec akg-factor-bridge python run.py views
|
|
||||||
```
|
|
||||||
|
|
||||||
**要看的三段**:
|
|
||||||
|
|
||||||
1. 连通性七项全 ✅;
|
|
||||||
2. 新增的「视图版本」段——四项应该是
|
|
||||||
`✅ n_sources` / `✅ mkt_trade_date` / `✅ source_type` / `⬜ v_factor_segment_members`
|
|
||||||
(最后一个是 G2 的第五插槽视图,现在未就绪是对的);
|
|
||||||
3. 末行打印当前口径:`SUBFACTOR_UNIVERSE=pool | EVENT_SOURCE_TYPES=['announcement'] | EVENT_MAX_PER_DOC=3`。
|
|
||||||
|
|
||||||
> 若 `mkt_trade_date` 是 ⬜,说明 `ALTER TABLE` 那段没跑到,回第 1 步。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. ★ G1 体检(这一步的输出最关键,直接决定第三批怎么定)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 三节可分开跑;price 节全表聚合可能 1~3 分钟
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
**`corr` 节要回答的两个问题**(把整段输出发我):
|
|
||||||
|
|
||||||
- **① `corr(z_H, z_V)` 是多少?** 若 |值| > 0.5,「还没热」与「便宜」高度共线,
|
|
||||||
「三项加权」实际是两项,§9-8 的权重讨论要重开(而且门槛② 与 `z_V` 本就是同一变量进两次)。
|
|
||||||
- **② `w_T=0.50` 那行的 top-K 命中数。** 如果命中数 ≈ min(K, 传导票数),
|
|
||||||
就实锤了「0.5/0.3/0.2 事实上是传导优先而非加权混合」。
|
|
||||||
同一节还会打印**两段式与当前公式的 top20 重合度**——越接近 20/20,
|
|
||||||
说明改成两段式零代价,那第三批 3.2 就直接选 A。
|
|
||||||
|
|
||||||
另外 `corr` 节末尾的 `U → 有 upside → 过门槛② → 其中有传导` 这条链,
|
|
||||||
最后一个数就是真正决定榜首的量级。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 因子构建 + 冻结(daily 跑完会自动冻结)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 热度 T+1,构建日选有数据的那天(views 的「数据历史深度」段会告诉你前沿在哪)
|
|
||||||
docker compose exec akg-factor-bridge python run.py build all --mode daily --date 2026-07-24
|
|
||||||
```
|
|
||||||
|
|
||||||
**要看的**:
|
|
||||||
|
|
||||||
- 四路各自的写入行数,和上次(upside 795 / heat 1674 / event 195 / transmission 83)对比;
|
|
||||||
- **event 行数大概率会掉**——新加了 `source_type='announcement'` 白名单 + 单文档封顶,
|
|
||||||
日志会打印剔除了多少条、按来源分布是什么。**这个分布请一并发我**:
|
|
||||||
如果剔除后接近 0,说明基座里公告的 `source_type` 不是 `announcement` 这个字符串,
|
|
||||||
按实际值改 `.env` 的 `EVENT_SOURCE_TYPES` 即可;
|
|
||||||
- **transmission 那里的 ⚠️ 告警**——`quiet 存满 12 条` / `members_total >= 30` /
|
|
||||||
`mkt 快照日 ≠ scan_date` 各命中几个环节。这三个数就是硬伤 1 的实测量级,
|
|
||||||
直接决定第二批里 `topic_context` 的 cap 该提到多少;
|
|
||||||
- 末尾 `❄️ 冻结 ... → /app/data/frozen/2026-07-24`,各源行数 + warnings。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 看一眼冻结产物
|
|
||||||
docker compose exec akg-factor-bridge cat /app/data/frozen/2026-07-24/manifest.json
|
|
||||||
ls -la data/frozen/2026-07-24/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5.(可选,但很值)事件回填冒烟——验证硬伤 3 真的修好了
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 改之前这条命令会静默返回空(日历取自热度表,最早 2025-03-26)
|
|
||||||
docker compose exec akg-factor-bridge python run.py build akg_event \
|
|
||||||
--mode history --start 2024-01-01 --end 2024-03-31 --no-freeze
|
|
||||||
```
|
|
||||||
|
|
||||||
**预期**:现在应该有真实行数(2024 年的公告若已抽取),而不是「无数据(跳过)」。
|
|
||||||
若仍为空,看是不是被 `EVENT_SOURCE_TYPES` 过滤掉了(日志会说)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 顺带跑一条 SQL(回答一个对回填边界很关键的问题)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec -i akg-postgres psql -U akg -d akg <<'SQL'
|
|
||||||
SELECT kind, min(trade_date), max(trade_date), count(DISTINCT trade_date) AS days
|
|
||||||
FROM mkt_daily GROUP BY kind ORDER BY kind;
|
|
||||||
SQL
|
|
||||||
```
|
|
||||||
|
|
||||||
`mkt_daily` 是持久快照表(全库无删除),所以 `kind='stock'` 的天数决定了传导
|
|
||||||
**理论上**能重扫到哪一天。若明显多于 3 天,设计 §7 里「传导只 live 累积」的边界
|
|
||||||
可以往前推一段(图谱漂移仍在,但至少多一个带明确标注的近似区间可选)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 把这些发我
|
|
||||||
|
|
||||||
1. `run.py views` 全部输出
|
|
||||||
2. `probe --section corr` 全部输出 ← **最重要**
|
|
||||||
3. `probe --section upside` 的 q 档位表
|
|
||||||
4. `build all --mode daily` 的日志(尤其 event 的剔除分布、transmission 的 ⚠️ 三项)
|
|
||||||
5. 第 6 步那条 SQL 的结果
|
|
||||||
|
|
||||||
拿到这五样,第三批(赛道门槛 C 的口径 + 权重结构)就能用你的真实数据定,
|
|
||||||
不用再靠我的模拟。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 已记下的决定
|
|
||||||
|
|
||||||
- **传导新鲜度断言 → 照常落库,只打告警 + 记 `mkt_trade_date`**(不中止扫描)。
|
|
||||||
理由:保覆盖,传导每日只有几十行,公告批量入库期间 beat 一旦被饿就整天空白,
|
|
||||||
代价太大;改由桥侧按 `mkt_trade_date` 自行判断是否采信。
|
|
||||||
第二批的 `transmission.scan` 已按此改写(`allow_stale` 参数取消)。
|
|
||||||
|
|
@ -1,487 +0,0 @@
|
||||||
# 量化因子导出与合成设计 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` 一并提交。
|
|
||||||
Loading…
Reference in New Issue