数据基座因果论断与研判结论两张只读视图(v_factor_logic、v_factor_judgement),09-03 已在基座库建成

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-03 11:35:57 +08:00
parent a2452139c6
commit ddb97a8db6
1 changed files with 254 additions and 0 deletions

View File

@ -0,0 +1,254 @@
-- 数据基座因果论断与研判结论只读视图:两张视图,供选股系统的候选卡读"证据线"。
-- 出处docs/主观量化系统方案_2026-09-03.md 第三节 3.3"数据基座"表的前两项。
--
-- 应用(在运行选股系统容器的机器上执行,容器内的 AKG_PG_* 指向数据基座的 PostgreSQL
-- docker compose exec -T akg-factor-bridge python run.py apply-views --file sql/astock_kg_logic_views.sql --dry-run
-- docker compose exec -T -e AKG_PG_USER=<属主账号> -e AKG_PG_PASSWORD=<口令> \
-- akg-factor-bridge python run.py apply-views --file sql/astock_kg_logic_views.sql
-- 本文件只用 CREATE OR REPLACE VIEW不建表、不改表、不 DROP重复应用无害。
-- 应用时避开每天 06:10 的投影表重建窗口(第一张视图要读投影表)。
-- 注意 CREATE OR REPLACE VIEW 不允许改动已有列的名字与顺序,只能在末尾追加列;
-- 将来若要调整列,需要另起视图名或由属主手工 DROP 后重建。
--
-- 一、v_factor_logic —— 因果论断,一行一条(客体是环节时展开到成员公司,一条论断多行)
--
-- 用途:回答"谁利好或利空这家公司、机制是什么、有什么前提、多长时效、出处在哪"。
-- 数据来源:断言表 claims 里谓词为 DRIVES 的行(研报因果论断的结构化,只追加、带原文与披露日),
-- 关联文档表 documents 取标题、来源类型与文档锚定公司。
--
-- 口径与数据基座后端接口 /graph/logic/{ts_code} 保持一致的部分:
-- 1. 那个接口在图上取指向公司节点的 DRIVES 边,边的方向、机制、条件、时效、强度、可信层级、
-- 置信度都来自断言的 qualifiers 与元数据;本视图直接从断言层取同一批字段,不经过图。
-- 2. 公司身份的判定仿照数据基座的实体解析器backend/app/linking/resolver.py
-- 客体原始名经 akg_norm 归一后查别名表 entity_links曾用名类别名method 为 namechange
-- 要求披露日落在有效期内,缺有效期的曾用名不采用;恰好一个候选才链接;多个候选时若
-- 文档锚定公司在候选之中则取它,否则视为歧义不链接;客体是自指词(公司、本公司、本集团、
-- 本行、发行人、上市公司)时取文档锚定公司 documents.meta->>'company_ts_code'。
-- 与接口不同的部分:
-- 3. 客体是环节object_type = 'Segment')时,接口不展开;本视图按投影表
-- segment_members_projection 展开到该环节的全部上市成员公司,每个成员一行,
-- via_segment 列写环节名link_method 列写 segment。客体是公司时 via_segment 为空。
-- 4. 接口读的是图上的边,被后续披露取代或封口的边带有 status 与 valid_to本视图逐条断言出行
-- 没有这两个字段belief 图只在 Neo4jPostgreSQL 没有 beliefs 表)。作为替代,
-- disputed 列标出该断言是否处于未结的多空分歧复核disputes 表 status = 'open'
-- review_flag 列带出逻辑评析在 logic_reviews.merged->'flags' 里对该断言的"疑似误抽"标记。
--
-- 连接键:
-- claims.doc_id = documents.doc_id
-- akg_norm(claims.object_id) = entity_links.alias_normentity_links.canonical_id 即 ts_code
-- btrim(claims.object_id) = segment_aliases.alias环节同义归一未命中用原名
-- 环节名 = COALESCE(segment_members_projection.orig_segment_name, segment_name)
-- 即投影表里的图上原名,与 sql/astock_kg_card_views.sql 的用法相同;
-- ts_code = company_master.ts_code 取简称(只做展示,不参与过滤)。
-- 代码格式ts_code 一律是点后缀式,如 600000.SH。
--
-- 已知局限:
-- a. 投影表每天 06:10 删后插,只存当前态:环节展开得到的成员集合是"今天的",历史日的论断
-- 按今天的成员展开,复盘时存在成员前视;复盘应按冻结快照重建。
-- b. 视图里的公司链接只做了解析器的精确层T1与自指、文档破歧两条规则没有做缩减形态
-- (剥公司后缀、剥括注,即 T2与主语亲和规则akg_norm 也不剥行情尾标。因此视图链接到的
-- 公司是图上链接结果的子集,未链接的论断不出现在视图里。要看覆盖率,用下面的诊断语句。
-- c. 客体是概念、行业、未上市实体或未能链接的名字时,没有 ts_code不出现在视图里。
-- d. 环节同义归一只查库表 segment_aliases解析器代码里那几条兜底字典没有复制到 SQL。
-- e. 一条论断的客体是环节时会展开成多行claim_id 在视图里不唯一;按论断计数要 DISTINCT claim_id。
-- f. 不做去重与归并:同一研报同一句话对同一公司只会有一条(断言层有 dedup_key 唯一约束),
-- 但不同研报的相同论断各算一条,归并留给逻辑评析层。
--
-- 诊断(只读,看覆盖):
-- SELECT object_type, count(*) FROM claims WHERE predicate = 'DRIVES' GROUP BY 1 ORDER BY 2 DESC;
-- SELECT link_method, count(DISTINCT claim_id), count(*) FROM v_factor_logic GROUP BY 1;
-- SELECT max(disclosure_date), count(*) FROM v_factor_logic;
CREATE OR REPLACE VIEW v_factor_logic AS
WITH drives AS (
SELECT c.claim_id,
c.subject_type,
c.subject_id,
c.object_type,
c.object_id,
c.qualifiers,
c.source_span,
c.disclosure_date,
c.confidence,
c.tier,
c.doc_id,
d.title AS doc_title,
d.source_type,
d.meta->>'company_ts_code' AS doc_company,
akg_norm(c.object_id) AS object_norm
FROM claims c
JOIN documents d ON d.doc_id = c.doc_id
WHERE c.predicate = 'DRIVES'
),
-- 公司类客体的候选身份:仿解析器的精确层,曾用名过披露日时点闸门。
company_cands AS (
SELECT x.claim_id, l.canonical_id
FROM drives x
JOIN entity_links l ON l.alias_norm = x.object_norm
WHERE x.object_type IN ('Company', 'Entity')
AND (l.method <> 'namechange'
OR ((l.valid_from IS NOT NULL OR l.valid_to IS NOT NULL)
AND (l.valid_from IS NULL OR l.valid_from <= x.disclosure_date)
AND (l.valid_to IS NULL OR x.disclosure_date <= l.valid_to)))
GROUP BY x.claim_id, l.canonical_id
),
company_cand_agg AS (
SELECT claim_id,
count(*) AS n_cands,
min(canonical_id) AS only_id,
array_agg(canonical_id) AS cand_ids
FROM company_cands
GROUP BY claim_id
),
company_link AS (
SELECT x.claim_id,
CASE
WHEN x.object_norm IN ('公司', '本公司', '本集团', '本行', '发行人', '上市公司')
THEN x.doc_company
WHEN a.n_cands = 1 THEN a.only_id
WHEN a.n_cands > 1 AND x.doc_company = ANY(a.cand_ids) THEN x.doc_company
ELSE NULL
END AS ts_code,
CASE
WHEN x.object_norm IN ('公司', '本公司', '本集团', '本行', '发行人', '上市公司')
THEN 'self_ref'
WHEN a.n_cands = 1 THEN 'dict'
WHEN a.n_cands > 1 AND x.doc_company = ANY(a.cand_ids) THEN 'tiebreak_doc'
WHEN a.n_cands > 1 THEN 'ambiguous'
ELSE 'unresolved'
END AS link_method
FROM drives x
LEFT JOIN company_cand_agg a ON a.claim_id = x.claim_id
WHERE x.object_type IN ('Company', 'Entity')
),
-- 环节类客体:同义归一后按投影表展开到上市成员。
segment_rows AS (
SELECT x.claim_id,
COALESCE(sa.canonical, btrim(x.object_id)) AS segment_canon
FROM drives x
LEFT JOIN segment_aliases sa ON sa.alias = btrim(x.object_id)
WHERE x.object_type = 'Segment'
),
segment_members AS (
SELECT DISTINCT COALESCE(orig_segment_name, segment_name) AS seg, ts_code
FROM segment_members_projection
WHERE ts_code IS NOT NULL AND ts_code <> ''
),
linked AS (
SELECT cl.claim_id, cl.ts_code, NULL::text AS via_segment, cl.link_method
FROM company_link cl
WHERE cl.ts_code IS NOT NULL
UNION ALL
SELECT sr.claim_id, m.ts_code, sr.segment_canon AS via_segment, 'segment' AS link_method
FROM segment_rows sr
JOIN segment_members m ON m.seg = sr.segment_canon
),
-- 逻辑评析对单条论断的"疑似误抽"标记(编号已在落库前映射回 claim_id
review_flags AS (
SELECT f->>'claim_id' AS claim_id_text,
min(f->>'issue') AS issue
FROM logic_reviews r
CROSS JOIN LATERAL jsonb_array_elements(
CASE WHEN jsonb_typeof(r.merged->'flags') = 'array'
THEN r.merged->'flags' ELSE '[]'::jsonb END) f
WHERE f->>'claim_id' IS NOT NULL
GROUP BY f->>'claim_id'
)
SELECT t.ts_code,
cm.short_name AS company_name,
t.via_segment,
t.link_method,
x.subject_type,
x.subject_id AS subject_name,
x.object_type,
x.object_id AS object_name,
x.qualifiers->>'direction' AS direction,
x.qualifiers->>'mechanism' AS mechanism,
x.qualifiers->>'condition' AS condition,
x.qualifiers->>'horizon' AS horizon,
x.qualifiers->>'strength' AS strength,
x.tier,
x.confidence,
x.disclosure_date,
x.source_type,
x.doc_id,
x.doc_title,
left(x.source_span, 200) AS source_span,
x.claim_id,
EXISTS (SELECT 1 FROM disputes dp
WHERE dp.status = 'open' AND x.claim_id = ANY(dp.claim_ids)) AS disputed,
rf.issue AS review_flag
FROM linked t
JOIN drives x ON x.claim_id = t.claim_id
LEFT JOIN company_master cm ON cm.ts_code = t.ts_code
LEFT JOIN review_flags rf ON rf.claim_id_text = x.claim_id::text;
-- 二、v_factor_judgement —— 研判结论,一行一个评析簇
--
-- 研判结论已经落表:产业研判面板生成的结论存在 logic_reviews 表,簇键 cluster_key 以
-- "Industry::主题" 开头(后端 /admin/review_industry 任务写入,/atlas/{theme} 与主题页读取);
-- 同一张表还存按受益方聚簇的逻辑评析("Company:代码"、"Segment:环节名"、"Concept:概念名"
-- 由 /admin/review_logic 任务写入,个股逻辑面接口的 review 段读取)。两类同表同结构,
-- 本视图一并暴露,用 scope 列区分industry 产业研判、company 个股评析、segment 环节评析、
-- concept 概念评析,其余 other。
--
-- 列的含义:
-- subject_name 是主题名或受益方原名ts_code 只在 scope = company 时有值(从簇键截取);
-- segment_name 只在 scope = segment 时有值。
-- leaning 是方向(模型四选一:偏多、偏空、中性、证据不足;产业研判与个股评析同一取值集合);
-- core_contradiction、supply_view、demand_view、expectation_shift 只有产业研判有,个股评析为空;
-- reasons、bull_points、bear_points 是依据JSON 数组,每项带 text 或 statement 与引用的 claim_ids
-- claim_ids 可回到 v_factor_logic 或 claims 表看原文;
-- verified 是第三轮自我校验结果false 时 verify_problems 列出校验指出的问题,读的一方应降权;
-- review_date 与 reviewed_at 是本版结论的生成时间input_version 是输入材料集合的指纹,
-- 材料变了指纹就变可当版本号用n_materials 是这一版评析读了多少条断言(论断与事件之和)。
--
-- 已知局限:
-- a. logic_reviews 以 cluster_key 唯一,重评时整行覆盖并把 created_at 改为当前时间,
-- 库里只有每个簇的最新一版,没有历史版本;要保留版本史需要新表,本次不做。
-- b. 产业研判的簇键是股票池主题名industry_pools.theme不是环节名主题与环节的对应
-- 关系不在库表里(池成员的 segments 字段是投影的派生),本视图不做主题到环节的映射。
-- c. 只出有第二轮结果comparison 非空)的簇;只跑完第一轮归并的簇没有方向,不出行。
-- d. 结论是模型综合生成的旁批,不是研报原文;数据基座前端对它的说明是"系统综合生成,非研报原文"。
CREATE OR REPLACE VIEW v_factor_judgement AS
SELECT CASE
WHEN r.cluster_key LIKE 'Industry::%' THEN 'industry'
WHEN r.cluster_key LIKE 'Company:%' THEN 'company'
WHEN r.cluster_key LIKE 'Segment:%' THEN 'segment'
WHEN r.cluster_key LIKE 'Concept:%' THEN 'concept'
ELSE 'other'
END AS scope,
r.beneficiary AS subject_name,
CASE WHEN r.cluster_key LIKE 'Company:%'
THEN substr(r.cluster_key, length('Company:') + 1) END AS ts_code,
CASE WHEN r.cluster_key LIKE 'Segment:%'
THEN substr(r.cluster_key, length('Segment:') + 1) END AS segment_name,
r.comparison->>'leaning' AS leaning,
r.comparison->'core_contradiction'->>'text' AS core_contradiction,
r.comparison->'supply_demand'->>'supply' AS supply_view,
r.comparison->'supply_demand'->>'demand' AS demand_view,
r.comparison->'expectation_shift'->>'text' AS expectation_shift,
r.comparison->'reasons' AS reasons,
r.comparison->'bull' AS bull_points,
r.comparison->'bear' AS bear_points,
CASE WHEN jsonb_typeof(r.comparison->'bull') = 'array'
THEN jsonb_array_length(r.comparison->'bull') ELSE 0 END AS n_bull,
CASE WHEN jsonb_typeof(r.comparison->'bear') = 'array'
THEN jsonb_array_length(r.comparison->'bear') ELSE 0 END AS n_bear,
CASE WHEN jsonb_typeof(r.merged->'flags') = 'array'
THEN jsonb_array_length(r.merged->'flags') ELSE 0 END AS n_flags,
r.verified,
r.comparison->'verify_problems' AS verify_problems,
r.model,
r.rounds,
r.created_at::date AS review_date,
r.created_at AS reviewed_at,
r.fingerprint AS input_version,
COALESCE(array_length(r.claim_ids, 1), 0) AS n_materials,
r.claim_ids,
r.review_id,
r.cluster_key
FROM logic_reviews r
WHERE r.comparison IS NOT NULL;