Cai
8 days ago 2fbc2b9ee0dfcf211f57b04769b7694d539d7312
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
import argparse
import csv
import os
from collections import Counter, defaultdict
from datetime import datetime, timezone, timedelta
from pathlib import Path
 
 
def read_csv(path):
    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        return list(csv.DictReader(handle))
 
 
def top(counter, n=10):
    return counter.most_common(n)
 
 
def safe_cell(value):
    return (value or "").replace("\n", " ").strip()
 
 
def relative_link(target, base_dir):
    return Path(os.path.relpath(target, base_dir)).as_posix()
 
 
COMPANY_STOPWORDS = {
    "有色金属",
    "能源金属",
    "工业金属",
    "基本金属",
    "贵金属",
    "小金属",
    "金属新材料",
    "中重稀土",
    "矿产资源",
    "自然资源",
    "稀土资源",
    "COMEX黄金",
    "LME铜",
    "LME铝",
    "国家能源",
    "中国新能源",
    "基础化工",
    "稀土冶炼",
    "年全球稀土",
    "全球稀土",
}
 
 
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--project-root", default=".")
    parser.add_argument("--outputs-dir", required=True)
    parser.add_argument("--result-index", required=True)
    parser.add_argument("--fact-files", nargs="+", required=True)
    parser.add_argument("--company-files", nargs="+", required=True)
    parser.add_argument("--snapshot", nargs="+", required=True)
    args = parser.parse_args()
 
    project_root = Path(args.project_root).resolve()
    outputs_dir = project_root / args.outputs_dir
    outputs_dir.mkdir(parents=True, exist_ok=True)
    result_index = project_root / args.result_index
    created_at = datetime.now(timezone(timedelta(hours=8))).isoformat(timespec="seconds")
 
    facts = []
    for file_name in args.fact_files:
        path = project_root / file_name
        if path.exists():
            facts.extend(read_csv(path))
 
    companies = []
    for file_name in args.company_files:
        path = project_root / file_name
        if path.exists():
            companies.extend(read_csv(path))
 
    snapshot = []
    for snapshot_file in args.snapshot:
        path = project_root / snapshot_file
        if path.exists():
            snapshot.extend(read_csv(path))
 
    metal_counter = Counter()
    theme_counter = Counter()
    metal_theme = defaultdict(Counter)
    example_by_metal = defaultdict(list)
    for row in facts:
        metals = [tag for tag in row.get("metal_tags", "").split("|") if tag]
        themes = [tag for tag in row.get("theme_tags", "").split("|") if tag]
        for metal in metals:
            metal_counter[metal] += 1
            if len(example_by_metal[metal]) < 4:
                example_by_metal[metal].append(row)
            for theme in themes:
                metal_theme[metal][theme] += 1
        for theme in themes:
            theme_counter[theme] += 1
 
    company_counter = Counter()
    company_examples = {}
    for row in companies:
        name = row.get("company_name", "")
        if (
            not name
            or len(name) > 14
            or name in COMPANY_STOPWORDS
            or name.isdigit()
            or "本报告" in name
            or "所有材料" in name
        ):
            continue
        company_counter[name] += 1
        company_examples.setdefault(name, row)
 
    by_metal_snapshot = defaultdict(list)
    for row in snapshot:
        by_metal_snapshot[row.get("metal", "")].append(row)
 
    industry_lines = [
        "# 有色行业视图草稿",
        "",
        "状态:DRAFT_FOR_REVIEW",
        f"生成时间:{created_at}",
        "",
        "## 证据边界",
        "",
        "本文档由 BATCH-001 与 BATCH-003 的规则抽取事实句、BATCH-003 转换/抽取 manifest、外部价格库存快照共同生成。当前尚未完成关键事实段落、表格定位和人工复核,因此只作为核心文档草稿,不输出正式行业结论或交易建议。",
        "",
        "## 材料覆盖",
        "",
        f"- 规则抽取事实句:{len(facts)} 条",
        f"- 公司提及草表:{len(companies)} 条",
        "- BATCH-003 转换状态:329 个补登记 raw 中 309 个已转换、20 个进入转换缺口。",
        "",
        "## 金属主题分布",
        "",
        "| 金属 | 事实句数量 | 主要主题 |",
        "|---|---:|---|",
    ]
    for metal, count in top(metal_counter, 12):
        themes = ", ".join([f"{name}:{cnt}" for name, cnt in top(metal_theme[metal], 5)])
        industry_lines.append(f"| {metal} | {count} | {themes} |")
    industry_lines.extend(["", "## 初步行业读法", ""])
    for metal, _ in top(metal_counter, 8):
        themes = [name for name, _ in top(metal_theme[metal], 4)]
        industry_lines.append(f"### {metal}")
        industry_lines.append("")
        industry_lines.append(f"- 抽取材料中,{metal}相关句子主要集中在:{', '.join(themes) if themes else '待复核'}。")
        if example_by_metal[metal]:
            industry_lines.append("- 待复核样例:")
            for row in example_by_metal[metal][:2]:
                industry_lines.append(f"  - {safe_cell(row.get('evidence_text'))}({row.get('doc_id')},{row.get('source_location')})")
        industry_lines.append("")
    industry_lines.extend(
        [
            "## 下一步复核点",
            "",
            "1. 对价格、库存、供给、项目投产和政策相关事实补段落或表格定位。",
            "2. 将低置信度公司提及草表收敛为公司证据卡,补业务占比、资源/产能、成本位置和触发/失效条件。",
            "3. 外部价格库存快照需要按来源继续补 SHFE、COMEX、SMM 等可复核数值。",
        ]
    )
 
    market_lines = [
        "# 有色市场视图草稿",
        "",
        "状态:DRAFT_FOR_REVIEW",
        f"生成时间:{created_at}",
        "",
        "## 证据边界",
        "",
        "本文档仅汇总公开外部快照和研报抽取中的市场相关句子。涉及实时行情、新闻、市场表现或外部信息的内容按 supplement/evidence 分账,不作为正式交易判断。",
        "",
        "## 外部价格库存快照",
        "",
        "| 金属 | 指标 | 数值 | 单位 | 市场/地区 | 来源 |",
        "|---|---|---:|---|---|---|",
    ]
    for row in snapshot:
        market_lines.append(
            f"| {row.get('metal')} | {row.get('indicator')} | {row.get('value')} | {row.get('unit')} | {row.get('market_or_region')} | {row.get('source_name')} |"
        )
    market_lines.extend(["", "## 市场变量分布", "", "| 主题 | 事实句数量 |", "|---|---:|"])
    for theme, count in top(theme_counter, 12):
        market_lines.append(f"| {theme} | {count} |")
    market_lines.extend(
        [
            "",
            "## 观察框架",
            "",
            "- 工业金属:优先跟踪价格、库存、冶炼/矿端供应、下游开工和宏观需求预期之间是否一致。",
            "- 贵金属:区分金银价格的宏观利率/美元逻辑与材料端白银需求逻辑。",
            "- 能源金属:锂、镍、钴需要把价格方向与库存、排产、产能释放和政策扰动分开验证。",
            "- 稀土/小金属:重点核对供给管制、出口、库存和高端制造需求,不用单一价格点推导结论。",
            "",
            "## 缺口",
            "",
            "1. SHFE 库存与国内现货价格仍需补可复核数值入口。",
            "2. 锂和稀土的部分价格为补充口径,仍需 SMM 或交易所/行业协会直接数值复核。",
            "3. 市场异常表现、新闻事件和政策事件需要后续按 darkline 分流形成独立证据链。",
        ]
    )
 
    company_lines = [
        "# 有色公司视图草稿",
        "",
        "状态:DRAFT_FOR_REVIEW",
        f"生成时间:{created_at}",
        "",
        "## 证据边界",
        "",
        "公司视图当前来自规则公司名扫描,置信度较低,只用于确定后续证据卡优先级。不得据此输出交易指令、收益承诺或无证据强结论。",
        "",
        "## 公司提及频次",
        "",
        "| 公司名片段 | 提及次数 | 样例定位 | 样例上下文 |",
        "|---|---:|---|---|",
    ]
    for name, count in top(company_counter, 40):
        row = company_examples[name]
        company_lines.append(
            f"| {name} | {count} | {row.get('doc_id')} / {row.get('source_location')} | {safe_cell(row.get('context_snippet'))[:120]} |"
        )
    company_lines.extend(
        [
            "",
            "## 后续公司证据卡字段",
            "",
            "1. 公司简称、证券代码、主营金属、核心资产或业务环节。",
            "2. 资源量、产能、成本位置、扩产/投产节奏、价格敏感性。",
            "3. 观察条件、触发条件、失效条件和主要风险。",
            "4. 原文页码/段落/表格定位、外部公告或数据来源、review_status。",
        ]
    )
 
    industry_path = outputs_dir / "nonferrous_industry_view_draft.md"
    market_path = outputs_dir / "nonferrous_market_view_draft.md"
    company_path = outputs_dir / "nonferrous_company_view_draft.md"
    industry_path.write_text("\n".join(industry_lines) + "\n", encoding="utf-8")
    market_path.write_text("\n".join(market_lines) + "\n", encoding="utf-8")
    company_path.write_text("\n".join(company_lines) + "\n", encoding="utf-8")
 
    result_lines = [
        "# ANA-YS-INDUSTRY-001 结果入口",
        "",
        "状态:DRAFT_FOR_REVIEW",
        f"更新时间:{created_at}",
        "",
        "## 核心文档",
        "",
        f"1. [有色行业视图草稿]({relative_link(industry_path, result_index.parent)})",
        f"2. [有色市场视图草稿]({relative_link(market_path, result_index.parent)})",
        f"3. [有色公司视图草稿]({relative_link(company_path, result_index.parent)})",
        "",
        "## 关键过程产物",
        "",
        "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/batch003_conversion_summary.md`",
        "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/batch003_extract001_summary.md`",
        "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/key_fact_review_batch003_pass001_summary.md`",
        "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/company_evidence_card_batch003_pass001_summary.md`",
        "- `ana-data/cases/有色案例/ANA-YS-INDUSTRY-001/outputs/price_inventory_supplement_pass002_summary.md`",
        "- `ana-data/cases/有色案例/supplement/nonferrous_price_inventory_snapshot_20260625.csv`",
        "- `ana-data/cases/有色案例/supplement/nonferrous_price_inventory_supplement_pass002_20260625.csv`",
        "- `ana-data/cases/有色案例/manifest/conversion_gap_batch003.csv`",
        "",
        "## 限制",
        "",
        "BATCH-001 与 BATCH-003 关键事实尚未完成段落/表格定位和人工复核。对外正式结论、公司投资读法或交付前,必须提交执行审核或输出审核。",
    ]
    result_index.parent.mkdir(parents=True, exist_ok=True)
    result_index.write_text("\n".join(result_lines) + "\n", encoding="utf-8")
 
    print(f"industry={industry_path.relative_to(project_root).as_posix()}")
    print(f"market={market_path.relative_to(project_root).as_posix()}")
    print(f"company={company_path.relative_to(project_root).as_posix()}")
    print(f"result_index={result_index.relative_to(project_root).as_posix()}")
 
 
if __name__ == "__main__":
    main()