历史深度数据探查

This commit is contained in:
zlt 2026-07-27 10:03:45 +08:00
parent 642934f58d
commit a7ec17bccd
4 changed files with 162 additions and 4 deletions

View File

@ -41,8 +41,22 @@ akg-factor-bridge读视图+热度 → 四路日截面变换 → 【漏斗合
## 用法(全程 Docker不在宿主机直跑
```bash
# 0) 基座视图建一次/改一次——经 astock-kg 的 postgres 容器(容器名 akg-postgres库/用户均 akg
docker exec -i akg-postgres psql -U akg -d akg < sql/astock_kg_slot_views.sql
# 0) 基座视图建一次/改一次。**按你在哪台机器上选一条**——桥是跨机部署的独立单元,
# 部署它的服务器上通常没有 astock-kg 的容器。
# (a) 在桥这边(推荐,无需 psql 客户端,用桥自己的 AKG_PG_* 连接):
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
# ⚠️ 需要 DDL 权限。日常只读账号会报 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 前面只影响宿主机进程)
# (b) 在桥这边、想用原生 psql一次性容器不装客户端
# 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
# (c) 在 astock-kg 那台机器上(把本文件带过去):
# docker exec -i akg-postgres psql -U akg -d akg < sql/astock_kg_slot_views.sql
# 1) 配连接(地址填「桥容器可达」的:跨机=LAN IP与基座同机同网=服务名,见下「网络」)
cp .env.example .env && vim .env

124
apply_views.py Normal file
View File

@ -0,0 +1,124 @@
"""把插槽视图 DDL 应用到基座 PG —— 用桥自己的 psycopg 连接,不依赖 psql 客户端。
**为什么需要它**桥是跨机部署的独立单元设计 §4部署它的服务器上通常
**没有** astock-kg `akg-postgres` 容器README 里那条
`docker exec -i akg-postgres psql ...` 只在基座那台机器上成立
而桥容器是 python:3.11-slim也没有 psql 客户端
本模块用桥已有的 `AKG_PG_*` 连接执行 DDL零新增凭据全程 Docker
**边界说明**视图定义文件本来就随桥仓库版本控制sql/astock_kg_slot_views.sql
所以由桥来应用它并不越界桥定义插槽接口基座只是接口的宿主
但注意**权限**日常运行只需只读账号应用 DDL 需要能 CREATE VIEW / ALTER TABLE
的账号通常是基座 owner若报 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
注意必须用 `docker compose exec -e`在命令前面写 `AKG_PG_USER=... docker compose exec`
只会设置宿主机上 compose 客户端进程的环境变量传不进容器
逐语句执行 + 逐语句报错一条失败比如 ALTER 缺权限不影响其余语句
这样你能准确看到是哪一条没过而不是整个脚本回滚
"""
from __future__ import annotations
from pathlib import Path
import psycopg
import config
DEFAULT_SQL = "sql/astock_kg_slot_views.sql"
def split_statements(sql: str) -> list[str]:
"""按分号切分 SQL 语句。剥掉 -- 行注释,尊重单引号字符串。
本文件的 DDL 里没有 $$ 引用块与多行注释故不做处理
若将来加了函数体这里要先升级再用
"""
out, buf = [], []
in_str = in_comment = False
i, n = 0, len(sql)
while i < n:
c = sql[i]
nxt = sql[i + 1] if i + 1 < n else ""
if in_comment:
if c == "\n":
in_comment = False
buf.append(c)
i += 1
continue
if in_str:
buf.append(c)
if c == "'":
if nxt == "'": # 转义的单引号 ''
buf.append(nxt)
i += 2
continue
in_str = False
i += 1
continue
if c == "-" and nxt == "-":
in_comment = True
i += 2
continue
if c == "'":
in_str = True
buf.append(c)
i += 1
continue
if c == ";":
stmt = "".join(buf).strip()
if stmt:
out.append(stmt)
buf = []
i += 1
continue
buf.append(c)
i += 1
tail = "".join(buf).strip()
if tail:
out.append(tail)
return out
def _label(stmt: str) -> str:
"""给语句起个人能读的名字,用于逐条报告。"""
head = " ".join(stmt.split())[:78]
return head + ("" if len(" ".join(stmt.split())) > 78 else "")
def apply(path: str = DEFAULT_SQL, dry_run: bool = False) -> int:
p = Path(path)
if not p.exists():
p = Path("/app") / path # 容器内工作目录兜底
if not p.exists():
raise SystemExit(f"找不到 SQL 文件: {path}")
stmts = split_statements(p.read_text(encoding="utf-8"))
c = config.akg_pg()
print(f"应用 {p}{len(stmts)} 条语句)→ {c.user}@{c.host}:{c.port}/{c.db}")
if dry_run:
for i, s in enumerate(stmts, 1):
print(f" [{i:>2}] {_label(s)}")
print("\n(dry-run未执行)")
return 0
ok = fail = 0
# autocommit逐条独立提交一条失败不毒化后续DDL 各自独立、且都幂等)
with psycopg.connect(host=c.host, port=c.port, user=c.user,
password=c.password, dbname=c.db, autocommit=True) as conn:
for i, s in enumerate(stmts, 1):
try:
conn.execute(s)
ok += 1
print(f" ✅ [{i:>2}] {_label(s)}")
except Exception as e: # noqa: BLE001 —— 逐条报错才看得出是哪条缺权限
fail += 1
print(f" ❌ [{i:>2}] {_label(s)}\n {e!r}")
print(f"\n完成:成功 {ok} 条,失败 {fail}")
if fail:
print("提示CREATE VIEW / ALTER TABLE 需要 DDL 权限;只读账号会报 "
"permission denied —— 用基座 owner 账号跑一次即可(见本文件 docstring")
return fail

View File

@ -42,11 +42,24 @@ git commit -m "fix: 交叉评审第一批——传导口径/事件回填/年报
## 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 exec -i akg-postgres psql -U akg -d akg < sql/astock_kg_slot_views.sql
# 推荐:在桥这边跑,零新增凭据、全程 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 前面只设置宿主机进程的环境变量,进不去
```
**预期**`CREATE VIEW` ×4、`ALTER TABLE`、`CREATE INDEX`,无 ERROR。
**预期**6 条语句逐条 ✅4 个 CREATE OR REPLACE VIEW + 1 个 ALTER TABLE + 1 个 CREATE INDEX
**不动任何一行数据**`claims` / `documents` 一个字节不变。
---

7
run.py
View File

@ -1,6 +1,7 @@
"""akg-factor-bridge CLI。
python run.py views # 连通性自检:打印视图/表行数
python run.py apply-views [--dry-run] # 把插槽视图 DDL 应用到基座 PG
python run.py probe # G1 体检(只读,详见 probe.py
python run.py freeze [--date D] # 输入冻结G0.5,详见 freeze.py
python run.py register # 注册四子因子到 factor_metadata
@ -133,6 +134,9 @@ def main():
p.add_argument("--section", choices=["all", "pools", "price", "upside", "corr"],
default="all",
help="pools=池结构清单 price=行情年表 upside=分布与q档位 corr=三项相关矩阵")
av = sub.add_parser("apply-views")
av.add_argument("--file", default="sql/astock_kg_slot_views.sql")
av.add_argument("--dry-run", action="store_true", help="只列语句不执行")
f = sub.add_parser("freeze")
f.add_argument("--date", help="默认今天")
b = sub.add_parser("build")
@ -149,6 +153,9 @@ def main():
elif a.cmd == "probe":
import probe # 按需加载:一次性诊断命令,不影响常规链路
probe.run(a.section)
elif a.cmd == "apply-views":
import apply_views
raise SystemExit(1 if apply_views.apply(a.file, a.dry_run) else 0)
elif a.cmd == "freeze":
import freeze
freeze.snapshot(a.date)