MB-X Bilibili Pipeline
6 days ago 8b94574583bb5d33faf4d3cec465e3fbcdcf40d3
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
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
#!/usr/bin/env python3
"""Rebuild current valuation snapshots after the 2026 interim-report refresh.
 
Judgment (business classification, scenario profit bands and multiples) lives
here; all numeric valuation formulae are executed by the frozen V1 engine.
Files are written to one stable ``当前估值`` tree and are overwritten on rerun.
"""
 
from __future__ import annotations
 
import argparse
import csv
import hashlib
import json
import math
import re
import statistics
import subprocess
import sys
import time
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from pathlib import Path
from typing import Any
 
 
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "dev/ana-dev"))
sys.path.insert(0, str(Path(__file__).resolve().parent))
from stock_valuation_pipeline.core import run_pipeline  # noqa: E402
import full_midyear_refresh_20260826 as fetch  # noqa: E402
 
 
BATCH_ID = fetch.BATCH_ID
AS_OF = fetch.AS_OF
PRICE_DATE = fetch.PRICE_DATE
TMP_ROOT = fetch.TMP_ROOT
RAW_ROOT = fetch.RAW_ROOT
CASE_ROOT = fetch.CASE_ROOT
CURRENT_ROOT = ROOT / "ana-data/result/股票估值/当前估值"
MASTER_ROOT = ROOT / "ana-data/result/股票估值/全量中报重估"
REGISTRY = ROOT / "dev/ana-dev/stock_valuation_pipeline/source_registry.json"
SCAN_PATH = ROOT / "ana-data/cases/股票估值/BATCH-STOCK-VALUATION-QINGFENGPU-TIMELINE-20260825-001/估值口径异常扫描.csv"
 
 
TIER_RULES: dict[str, dict[str, Any]] = {
    "bank": {"name": "商业银行", "method": "pb", "bands": ((0.45, 0.70), (0.70, 1.05), (1.05, 1.40))},
    "financial": {"name": "证券、保险或综合金融", "method": "pb", "bands": ((0.55, 0.90), (0.90, 1.45), (1.45, 2.10))},
    "resource": {"name": "资源开采与有色金属", "method": "pe", "bands": ((6, 10), (10, 16), (16, 24))},
    "coal_energy": {"name": "煤炭、油气与传统能源", "method": "pe", "bands": ((5, 8), (8, 13), (13, 20))},
    "chemical": {"name": "化工、氟化工或化学材料", "method": "pe", "bands": ((8, 13), (13, 21), (21, 30))},
    "agriculture": {"name": "农业、农资、养殖或食品原料", "method": "pe", "bands": ((8, 13), (13, 21), (21, 30))},
    "memory_module": {"name": "存储模组、存储控制器或存储产品(强周期)", "method": "pe", "bands": ((5, 8), (10, 15), (15, 20))},
    "semi_equipment": {"name": "半导体设备、厂务或EDA", "method": "pe", "bands": ((22, 32), (32, 48), (48, 68))},
    "semi_material": {"name": "半导体材料与电子化学品", "method": "pe", "bands": ((18, 28), (28, 42), (42, 60))},
    "semi_foundry": {"name": "晶圆制造、硅片或化合物半导体", "method": "pe", "bands": ((20, 30), (30, 48), (48, 68))},
    "semi_packaging": {"name": "半导体封装测试", "method": "pe", "bands": ((15, 23), (23, 34), (34, 48))},
    "semi_design": {"name": "集成电路设计与芯片产品", "method": "pe", "bands": ((20, 30), (30, 45), (45, 65))},
    "optical_ai": {"name": "AI算力、服务器、通信或光通信", "method": "pe", "bands": ((18, 28), (28, 42), (42, 60))},
    "software": {"name": "软件、数据服务、安全或信创", "method": "pe", "bands": ((18, 28), (28, 42), (42, 60))},
    "robotics": {"name": "机器人核心零部件、自动化或工业控制", "method": "pe", "bands": ((15, 23), (23, 35), (35, 50))},
    "military": {"name": "军工电子、航空航天或特种装备", "method": "pe", "bands": ((18, 28), (28, 42), (42, 60))},
    "battery_solar": {"name": "光伏、锂电、储能材料或设备", "method": "pe", "bands": ((8, 13), (13, 21), (21, 30))},
    "wind_power": {"name": "风电、核电、电网或电力设备", "method": "pe", "bands": ((12, 18), (18, 27), (27, 38))},
    "pharma": {"name": "医药、医疗器械或生命科学服务", "method": "pe", "bands": ((15, 23), (23, 35), (35, 50))},
    "consumer": {"name": "消费品、商业服务或文旅", "method": "pe", "bands": ((10, 16), (16, 25), (25, 36))},
    "property": {"name": "地产、建筑或基础设施", "method": "pb", "bands": ((0.45, 0.75), (0.75, 1.20), (1.20, 1.80))},
    "transport": {"name": "交通运输、物流或公用运营", "method": "pe", "bands": ((9, 14), (14, 22), (22, 32))},
    "industrial": {"name": "通用制造、机械设备或工业材料", "method": "pe", "bands": ((10, 16), (16, 25), (25, 36))},
}
 
 
OLD_TYPE_KEYWORDS: dict[str, tuple[str, ...]] = {
    "集成电路设计与芯片产品": ("芯片", "集成电路", "半导体", "处理器", "fpga", "soc"),
    "AI算力、网络、光通信或散热基础设施": ("服务器", "算力", "通信", "网络", "光模块", "光通信", "散热", "数据中心"),
    "机器人核心零部件、自动化或关联制造": ("机器人", "自动化", "伺服", "减速", "机床", "工业控制", "传感器", "电机"),
    "风电、核电设备或运营": ("风电", "核电", "风机", "叶片", "电力", "发电"),
    "光伏或锂电材料、设备、组件与系统": ("光伏", "锂电", "电池", "储能", "逆变器", "硅片", "组件"),
    "农业、养殖、化肥或化工": ("农业", "种业", "养殖", "饲料", "农药", "化肥", "化工"),
    "AI应用、软件、安全或信创": ("软件", "信息安全", "网络安全", "数据服务", "云计算", "信息技术"),
    "消费、地产、旅游、影视或事件驱动公司": ("消费", "食品", "饮料", "旅游", "酒店", "影视", "地产", "商业"),
    "医药、创新药、疫苗或医疗器械": ("医药", "药品", "疫苗", "医疗", "生物制品", "诊断"),
    "军工电子、航空航天或特种装备": ("军工", "航空", "航天", "雷达", "特种装备", "军用", "无人机"),
}
 
 
def read_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))
 
 
def write_json(path: Path, value: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
 
 
def normalize_pipeline_report(calc_dir: Path) -> None:
    """Remove generated Markdown trailing spaces and keep its manifest exact."""
    report_path = calc_dir / "valuation_report.md"
    manifest_path = calc_dir / "run_manifest.json"
    content = report_path.read_text(encoding="utf-8")
    normalized = "\n".join(line.rstrip() for line in content.splitlines()) + "\n"
    report_path.write_text(normalized, encoding="utf-8")
    manifest = read_json(manifest_path)
    payload = report_path.read_bytes()
    manifest["artifacts"]["valuation_report.md"] = {
        "bytes": len(payload),
        "sha256": hashlib.sha256(payload).hexdigest(),
    }
    write_json(manifest_path, manifest)
 
 
def safe_name(value: str) -> str:
    return re.sub(r'[<>:"/\\|?*]', "", value).strip() or "公司"
 
 
def valuation_position_pct(price: float, base_low: float, base_high: float) -> float:
    """Comparable valuation-position score requested by the operating manual.
 
    Below the range it is the discount to the lower bound; inside the range it
    is the deviation from the midpoint; above the range it is the premium to
    the upper bound.  Ascending order therefore consistently means cheaper.
    """
    if price < base_low:
        return price / base_low - 1
    if price <= base_high:
        midpoint = (base_low + base_high) / 2
        return price / midpoint - 1
    return price / base_high - 1
 
 
def number(row: dict[str, Any], *keys: str, default: float | None = None) -> float | None:
    for key in keys:
        value = row.get(key)
        if value not in (None, ""):
            try:
                return float(value)
            except (TypeError, ValueError):
                pass
    return default
 
 
def force_reclass_tickers() -> set[str]:
    if not SCAN_PATH.exists():
        return set()
    with SCAN_PATH.open(encoding="utf-8", newline="") as handle:
        return {
            row["ticker"]
            for row in csv.DictReader(handle)
            if "RESEARCH_TAG_MODEL_ROUTE" in row.get("flags", "")
        }
 
 
def classify(business: dict[str, str]) -> str:
    text = " ".join(business.values()).lower()
    rules = (
        ("bank", r"银行业务|发放贷款|公司金融业务|个人金融业务"),
        ("financial", r"证券经纪|证券投资|保险业务|期货经纪|信托业务|融资租赁"),
        ("military", r"军工|军用|航空装备|航天|雷达|导弹|弹药|火工品|特种装备|无人机"),
        ("memory_module", r"存储器|存储控制|固态硬盘|内存条|移动存储|存储卡模组|存储盘模组"),
        ("semi_equipment", r"半导体设备|刻蚀设备|薄膜沉积|离子注入|光刻设备|晶圆制造设备|eda|掩膜版"),
        ("semi_packaging", r"封装测试|封装、测试|集成电路封测"),
        ("semi_foundry", r"晶圆代工|晶圆制造|硅片|外延片|化合物半导体|砷化镓|氮化镓"),
        ("semi_material", r"半导体材料|电子化学品|光刻胶|抛光液|靶材|电子特气|高纯气体"),
        ("semi_design", r"集成电路设计|芯片设计|芯片研发|芯片产品|处理器|fpga|soc芯片|存储芯片"),
        ("optical_ai", r"光模块|光通信|服务器|数据中心|算力|网络设备|通信设备|射频器件|散热"),
        ("software", r"软件开发|软件产品|网络安全|信息安全|数据服务|云计算|信息技术服务|系统集成"),
        ("robotics", r"机器人|自动化设备|工业自动化|伺服|减速器|减速机|工业控制|数控机床|传感器|机器视觉"),
        ("battery_solar", r"光伏|太阳能|锂电|动力电池|储能电池|电池材料|正极材料|负极材料|隔膜|电解液|逆变器"),
        ("wind_power", r"风电|风力发电|核电|电网|输配电|电力设备|变压器|电气设备"),
        ("resource", r"金矿|银矿|铜矿|铝矿|锌矿|铅矿|锂矿|稀土|钨矿|钼矿|矿产开采|有色金属采选"),
        ("coal_energy", r"煤炭开采|煤炭生产|油气开采|石油开采|天然气开采|煤化工"),
        ("pharma", r"药品|医药|原料药|制剂|疫苗|医疗器械|体外诊断|生命科学|cro|cdmo"),
        ("agriculture", r"种业|种子|养殖|饲料|农药|化肥|复合肥|农业服务|农产品"),
        ("chemical", r"化工产品|化学品|氟化物|农化|涂料|树脂|橡胶|塑料助剂|化学材料"),
        ("property", r"房地产开发|建筑施工|工程承包|基础设施建设|园林工程"),
        ("transport", r"航空运输|港口|高速公路|铁路运输|物流服务|水务|燃气供应"),
        ("consumer", r"食品|饮料|酒类|服装|家居|家电|旅游|酒店|影视|零售|珠宝"),
    )
    for tier, pattern in rules:
        if re.search(pattern, text, re.I):
            return tier
    return "industrial"
 
 
def old_type_valid(old_type: str, business: dict[str, str], forced: bool) -> bool:
    if forced:
        return False
    keywords = OLD_TYPE_KEYWORDS.get(old_type)
    if keywords is None:
        return True
    text = " ".join(business.values()).lower()
    return any(keyword.lower() in text for keyword in keywords)
 
 
def financial_period(row: dict[str, Any], cash: dict[str, Any] | None, period_end: str) -> dict[str, Any]:
    attributable = number(row, "PARENT_NETPROFIT", "PARENTNETPROFIT", default=0.0) or 0.0
    deduct = number(row, "DEDUCT_PARENT_NETPROFIT", "KCFJCXSYJLR", default=attributable)
    return {
        "period_end": period_end,
        "basis": "audited" if period_end.endswith("12-31") else "interim_report_unaudited",
        "revenue": number(row, "TOTAL_OPERATE_INCOME", "TOTALOPERATEREVE", default=0.0) or 0.0,
        "attributable_profit": attributable,
        "deduct_profit": deduct if deduct is not None else attributable,
        "cfo": number(cash or {}, "NETCASH_OPERATE"),
        "capex": abs(number(cash or {}, "CONSTRUCT_LONG_ASSET")) if number(cash or {}, "CONSTRUCT_LONG_ASSET") is not None else None,
    }
 
 
def scenario_bands(old: dict[str, Any], tier: str, preserve: bool, method: str) -> tuple[tuple[float, float], ...]:
    if preserve:
        scenarios = sorted(old["valuation"]["scenarios"], key=lambda x: {"pessimistic": 0, "base": 1, "optimistic": 2}.get(x["role"], 9))
        if len(scenarios) >= 3 and all(item.get("method") == method for item in scenarios[:3]):
            return tuple((float(item["multiple_low"]), float(item["multiple_high"])) for item in scenarios[:3])
    rule = TIER_RULES[tier]
    if method == rule["method"]:
        return rule["bands"]
    if method == "pb":
        return ((0.5, 0.9), (0.9, 1.5), (1.5, 2.4))
    return ((10, 16), (16, 25), (25, 36))
 
 
def build_scenarios(
    *, method: str, bands: tuple[tuple[float, float], ...], normalized: float,
    consensus: float | None, consensus_count: int, tier: str,
) -> list[dict[str, Any]]:
    roles = (("悲观", "pessimistic"), ("基准", "base"), ("乐观", "optimistic"))
    if method == "pb":
        assumptions = (
            "盈利或资产回报继续承压,按净资产折价情景定价。",
            "盈利、资产质量和回报率处于中性路径,按基准PB区间定价。",
            "盈利修复、资产周转或ROE明显改善,按乐观PB区间定价。",
        )
        return [
            {"name": name, "role": role, "method": "pb", "multiple_low": band[0], "multiple_high": band[1], "assumption": assumption}
            for (name, role), band, assumption in zip(roles, bands, assumptions)
        ]
    anchor = consensus if consensus is not None and consensus > 0 else normalized
    anchor = max(anchor, 1.0)
    base_low = max(anchor * 0.78, normalized * 0.85 if normalized > 0 else 0.0, 1.0)
    base_high = max(anchor * (1.18 if consensus_count >= 2 else 1.25), normalized * 1.15 if normalized > 0 else 0.0, base_low)
    profits = ((base_low * 0.62, base_low * 0.88), (base_low, base_high), (base_high * 1.10, base_high * 1.38))
    assumptions = (
        "需求、价格、订单或验收低于预期,利润与估值倍数同步回落。",
        f"以最新TTM扣非利润和{consensus_count}家有效机构预测为锚,按{TIER_RULES[tier]['name']}的周期与质量定价。",
        "经营兑现显著超预期,但避免利润和估值倍数无限双重上调。",
    )
    return [
        {
            "name": name, "role": role, "method": "pe",
            "profit_low": profit[0], "profit_high": profit[1],
            "multiple_low": band[0], "multiple_high": band[1], "assumption": assumption,
        }
        for (name, role), profit, band, assumption in zip(roles, profits, bands, assumptions)
    ]
 
 
def source_url(code: str, kind: str) -> str:
    if kind == "finance":
        return "https://datacenter-web.eastmoney.com/api/data/v1/get"
    if kind == "forecast":
        return f"https://basic.10jqka.com.cn/{code}/worth.html"
    if kind == "business":
        return f"https://basic.10jqka.com.cn/{code}/operate.html"
    return ""
 
 
def included_forecasts(raw: dict[str, Any], notice_date: str, current_profit: float, shares: float) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
    included: list[dict[str, Any]] = []
    excluded: list[dict[str, Any]] = []
    for item in raw.get("forecasts", []):
        profit = number(item, "profit_2026")
        is_current = item.get("report_date", "") >= notice_date
        clears_achieved = profit is not None and (current_profit <= 0 or profit >= current_profit * 0.95)
        use = bool(is_current and clears_achieved and profit is not None and profit > 0)
        estimates = {}
        for year in (2026, 2027, 2028):
            year_profit = number(item, f"profit_{year}")
            eps = number(item, f"eps_{year}")
            if year_profit is not None and year_profit > 0:
                estimates[str(year)] = {"profit": year_profit, "eps": eps if eps is not None else year_profit / shares}
        record = {
            "institution": item.get("institution") or "未命名机构",
            "report_date": item["report_date"],
            "include": use,
            "core_assumption": "研报日期不早于最新经营披露,且全年利润不低于已实现累计利润。" if use else "早于最新披露、全年利润低于已实现累计利润或关键值缺失,仅保留作预期差证据。",
            "source_id": "SRC-THS-FORECAST-DETAIL-20260826",
            "estimates": estimates,
        }
        if estimates:
            (included if use else excluded).append(record)
    return included, excluded
 
 
def balance_sheet(raw: dict[str, Any], old: dict[str, Any]) -> dict[str, Any]:
    bal = raw["selected"].get("current_balance") or {}
    main = raw["selected"].get("current_main") or {}
    old_bal = old["balance_sheet"]
    parent_equity = number(bal, "TOTAL_EQUITY_PARENT")
    if parent_equity is None:
        total_equity = number(bal, "TOTAL_EQUITY")
        minority = number(bal, "MINORITY_EQUITY", default=0.0) or 0.0
        parent_equity = total_equity - minority if total_equity is not None else number(main, "TOTAL_EQUITY_PARENT", default=float(old_bal["equity"]))
    equity = parent_equity
    monetary = max(0.0, number(bal, "MONETARYFUNDS", default=float(old_bal["cash_available"])) or 0.0)
    old_cash = max(0.0, float(old_bal["cash_available"]))
    cash_available = min(monetary, old_cash) if old_cash < monetary else monetary
    fv_keys = (
        "TRADE_FINASSET_NOTFVTPL", "TRADE_FINASSET", "FVTPL_FINASSET", "APPOINT_FVTPL_FINASSET",
        "AVAILABLE_SALE_FINASSET", "DERIVE_FINASSET", "BUY_RESALE_FINASSET",
    )
    debt_keys = ("SHORT_LOAN", "NONCURRENT_LIAB_1YEAR", "LONG_LOAN", "BOND_PAYABLE", "LEASE_LIAB", "SHORT_BOND_PAYABLE")
    financial_assets = sum(max(0.0, number(bal, key, default=0.0) or 0.0) for key in fv_keys)
    debt = sum(max(0.0, number(bal, key, default=0.0) or 0.0) for key in debt_keys)
    return {
        "period_end": raw["periods"]["current"],
        "equity": equity if equity is not None else float(old_bal["equity"]),
        "cash_available": cash_available,
        "non_operating_financial_assets": financial_assets,
        "interest_bearing_debt": debt,
        "minority_interest": max(0.0, number(bal, "MINORITY_EQUITY", default=float(old_bal["minority_interest"])) or 0.0),
    }
 
 
def build_one(item: dict[str, Any], forced: set[str]) -> dict[str, Any]:
    ticker = item["ticker"]
    if ticker.endswith(".HK"):
        return {"ticker": ticker, "company": item["company"], "status": "UNSUPPORTED_HK"}
    old = read_json(ROOT / item["snapshot_path"])
    if item.get("close") is None:
        out_dir = CURRENT_ROOT / ticker
        out_dir.mkdir(parents=True, exist_ok=True)
        last_price = float(old["market"]["price"])
        last_price_date = str(old["market"].get("price_timestamp") or "")[:10]
        gap_snapshot = {
            "status": "PRICE_GAP",
            "batch_id": BATCH_ID,
            "meta": {"company": item["company"], "code": ticker, "as_of_date": AS_OF},
            "required_price_date": PRICE_DATE,
            "last_valid_price": last_price,
            "last_valid_price_date": last_price_date,
            "retained_valuation_id": item["valuation_id"],
            "retained_base_low": item["base_low"],
            "retained_base_high": item["base_high"],
        }
        write_json(out_dir / f"{safe_name(item['company'])}估值快照_当前.json", gap_snapshot)
        calc_dir = out_dir / "calculation"
        calc_dir.mkdir(parents=True, exist_ok=True)
        for stale_name in ("valuation_results.json", "valuation_report.md", "run_manifest.json"):
            stale_path = calc_dir / stale_name
            if stale_path.exists():
                stale_path.unlink()
        formal_path = out_dir / f"{safe_name(item['company'])}价格合理性评估_当前.md"
        formal_path.write_text(
            "\n".join(
                [
                    f"# {item['company']}当前价格合理性评估", "", "## 结论", "",
                    f"- {PRICE_DATE}是本轮统一价格日,但前复权专表没有该股当日日K,因此**不生成或伪造当日价格判定**。",
                    f"- 最近可核实价格为{last_price:.2f}元({last_price_date});原基准合理区间{float(item['base_low']):.2f}—{float(item['base_high']):.2f}元继续作为最近有效价值版本,但不能冒充{PRICE_DATE}的新判定。",
                    "- 后续前复权专表补齐复牌/有效交易日数据后,再按每日流程恢复价格判定。", "",
                    "## 边界", "",
                    "- 本报告是数据缺口说明;不构成交易指令或收益承诺。", "",
                ]
            ),
            encoding="utf-8",
        )
        write_json(
            out_dir / "source_evidence_manifest.json",
            {"batch_id": BATCH_ID, "ticker": ticker, "company": item["company"], "as_of": AS_OF,
             "status": "PRICE_GAP", "required_price_date": PRICE_DATE, "last_valid_price_date": last_price_date,
             "retained_valuation_id": item["valuation_id"]},
        )
        return {
            "ticker": ticker, "company": item["company"], "status": "PRICE_GAP",
            "errors": [f"{PRICE_DATE}前复权专表无当日日K,不回退旧价"],
            "last_price": last_price, "last_price_date": last_price_date,
            "report_path": formal_path.relative_to(ROOT).as_posix(),
        }
    raw = read_json(RAW_ROOT / f"{ticker.replace('.', '_')}.json")
    if not raw.get("selected"):
        return {"ticker": ticker, "company": item["company"], "status": "DATA_GAP", "errors": raw.get("errors", [])}
    annual_end = raw["periods"]["annual"]
    current_end = raw["periods"]["current"]
    prior_end = raw["periods"]["prior"]
    selected = raw["selected"]
    financials = {
        "annual": financial_period(selected["annual_income"], selected.get("annual_cashflow"), annual_end),
        "current_cumulative": financial_period(selected["current_income"], selected.get("current_cashflow"), current_end),
        "prior_year_same_period": financial_period(selected["prior_income"], selected.get("prior_cashflow"), prior_end),
    }
    annual = financials["annual"]
    current = financials["current_cumulative"]
    prior = financials["prior_year_same_period"]
    ttm_attr = annual["attributable_profit"] + current["attributable_profit"] - prior["attributable_profit"]
    ttm_deduct = annual["deduct_profit"] + current["deduct_profit"] - prior["deduct_profit"]
    ttm_revenue = annual["revenue"] + current["revenue"] - prior["revenue"]
    # Latest statutory report equity units take precedence over the older
    # instrument-static snapshot.  This matters after bonus issues, splits,
    # conversions and other H1 capital changes.
    reported_shares = number(selected.get("current_main") or {}, "TOTAL_SHARE")
    shares = reported_shares or item.get("static_total_shares") or float(old["market"]["diluted_shares"])
    shares = float(shares)
    if shares < 1_000_000 and float(old["market"]["diluted_shares"]) > 10_000_000:
        shares *= 10000
    shares_date = current_end if reported_shares else PRICE_DATE
    price = item.get("close") or float(old["market"]["price"])
    notice_date = str(selected["current_income"].get("NOTICE_DATE") or selected["current_income"].get("UPDATE_DATE"))[:10]
    included, excluded = included_forecasts(raw, notice_date, current["attributable_profit"], shares)
    consensus_values = [forecast["estimates"]["2026"]["profit"] for forecast in included if "2026" in forecast["estimates"]]
    consensus = statistics.median(consensus_values) if consensus_values else None
    tier = classify(raw["business"])
    old_type = str(old.get("analysis", {}).get("company_type") or "")
    preserve = old_type_valid(old_type, raw["business"], ticker in forced)
    if tier == "memory_module" and "强周期" not in old_type:
        preserve = False
    old_method = str(old["valuation"]["scenarios"][0]["method"])
    default_method = str(TIER_RULES[tier]["method"])
    method = old_method if preserve else default_method
    quality_override_reason = ""
    ttm_margin = ttm_deduct / ttm_revenue if ttm_revenue > 0 else None
    if method == "pe" and ttm_deduct <= 0:
        method = "pb"
        preserve = False
        quality_override_reason = "TTM扣非亏损,停止用尚未兑现的远期预测机械套PE,改用资产口径"
    elif method == "pe" and ttm_margin is not None and ttm_margin < 0.01 and len(consensus_values) < 3:
        method = "pb"
        preserve = False
        quality_override_reason = "TTM扣非净利率低于1%且机构覆盖不足,PE对微小利润过度敏感"
    elif method == "pe" and ttm_margin is not None and ttm_margin > 1.0:
        method = "pb"
        preserve = False
        quality_override_reason = "TTM扣非利润超过TTM营业收入,盈利锚异常,改用资产口径并下调置信度"
    normalized_anchor = ttm_deduct
    if (
        method == "pe" and consensus is not None and len(consensus_values) >= 3
        and ttm_deduct > consensus * 1.5
    ):
        normalized_anchor = consensus
        quality_override_reason = (
            f"TTM扣非利润显著高于最新{len(consensus_values)}家机构预测中位数,"
            "按最新一致预期压低正常化盈利,避免一次性高利润外推"
        )
    old_info_for_forecast = str(old["meta"]["latest_operating_info_date"])
    old_current_profit = float(old["financials"]["current_cumulative"]["attributable_profit"])
    old_included = {
        (
            str(row.get("institution") or ""), str(row.get("report_date") or ""),
            round(float(((row.get("estimates") or {}).get("2026") or {}).get("profit") or 0.0), 2),
        )
        for row in old.get("institutions", {}).get("forecasts", [])
        if row.get("include", True)
        and str(row.get("report_date") or "") >= old_info_for_forecast
        and float(((row.get("estimates") or {}).get("2026") or {}).get("profit") or 0.0) >= (old_current_profit * 0.95 if old_current_profit > 0 else 0.0)
    }
    new_included = {
        (
            str(row.get("institution") or ""), str(row.get("report_date") or ""),
            round(float(((row.get("estimates") or {}).get("2026") or {}).get("profit") or 0.0), 2),
        )
        for row in included
    }
    financial_update = notice_date > str(old["meta"]["latest_operating_info_date"])
    reuse_prior_judgment = bool(preserve and not financial_update and old_included == new_included)
    if reuse_prior_judgment:
        scenarios = json.loads(json.dumps(old["valuation"]["scenarios"], ensure_ascii=False))
        method = str(scenarios[0]["method"])
    else:
        bands = scenario_bands(old, tier, preserve, method)
        scenarios = build_scenarios(method=method, bands=bands, normalized=normalized_anchor, consensus=consensus, consensus_count=len(consensus_values), tier=tier)
    bal = balance_sheet(raw, old)
    if method == "pb" and bal["equity"] <= 0:
        bal["equity"] = 0.0
    company_type = old_type if preserve and old_type else TIER_RULES[tier]["name"]
    code = ticker.split(".")[0]
    sources = [
        {
            "id": "SRC-EM-FINANCE-ANNUAL-20260826", "source_type": "financial_mirror",
            "title": f"东方财富结构化财务:{annual_end}年度/比较基础", "publish_date": str(selected["annual_income"].get("NOTICE_DATE") or selected["annual_income"].get("UPDATE_DATE"))[:10],
            "period_end": annual_end, "url": source_url(code, "finance"),
            "supports": ["financials.annual"], "revision_status": "current",
        },
        {
            "id": "SRC-EM-FINANCE-CURRENT-20260826", "source_type": "financial_mirror",
            "title": f"东方财富结构化财务:{current_end}最新累计期", "publish_date": notice_date,
            "period_end": current_end, "url": source_url(code, "finance"),
            "supports": ["financials.current_cumulative", "balance_sheet.equity"], "revision_status": "current",
        },
        {
            "id": "SRC-EM-FINANCE-PRIOR-20260826", "source_type": "financial_mirror",
            "title": f"东方财富结构化财务:{prior_end}同期比较", "publish_date": notice_date,
            "period_end": prior_end, "url": source_url(code, "finance"),
            "supports": ["financials.prior_year_same_period"], "revision_status": "current",
        },
        {
            "id": "SRC-LOCAL-FRONT-CLOSE-20260825", "source_type": "quote_provider",
            "title": "trading_xuntou前复权完整交易日收盘与当前股本勾稽", "publish_date": PRICE_DATE,
            "period_end": PRICE_DATE, "url": None,
            "supports": ["market.price", "market.diluted_shares", "market.platform_market_cap"], "revision_status": "current",
        },
    ]
    if included or excluded:
        sources.append(
            {
                "id": "SRC-THS-FORECAST-DETAIL-20260826", "source_type": "institution_aggregator",
                "title": "同花顺盈利预测逐家明细(使用真实研报日期)", "publish_date": max(item["report_date"] for item in included + excluded),
                "period_end": max(item["report_date"] for item in included + excluded), "url": source_url(code, "forecast"),
                "supports": ["institutions"], "revision_status": "current",
            }
        )
    old_info_date = str(old["meta"]["latest_operating_info_date"])
    reclassified = not preserve
    snapshot = {
        "snapshot_id": f"{ticker.replace('.', '-')}-midyear-current",
        "meta": {
            "company": item["company"], "code": ticker, "market": item["market"], "as_of_date": AS_OF,
            "currency": item["currency"], "report_period_end": current_end, "latest_operating_info_date": notice_date,
        },
        "market": {
            "price": price, "price_type": "前复权完整交易日收盘价", "price_timestamp": f"{PRICE_DATE}T15:00:00+08:00",
            "diluted_shares": shares, "shares_date": shares_date, "platform_market_cap": price * shares,
        },
        "financials": financials,
        "balance_sheet": bal,
        "normalization": {
            "adjustments": [{
                "description": (
                    "以最新机构一致预期对异常高TTM扣非利润做正常化约束"
                    if normalized_anchor != ttm_deduct
                    else "以最新TTM扣非归母利润替代TTM归母利润作为核心盈利代理"
                ),
                "amount": normalized_anchor - ttm_attr,
                "basis": f"{annual_end}+{current_end}-{prior_end}",
            }],
        },
        "valuation": {
            "scenarios": scenarios,
            "reverse_pe_multiples": [6, 8, 10, 12, 15, 18, 20, 25, 30, 35, 40, 50, 60, 75, 100],
            "exit_pe_multiples": [8, 10, 12, 15, 18, 20, 25, 30, 35, 40, 50],
            "holding_period": {"years": 5, "required_return": 0.10, "cumulative_dividend_per_share": 0},
        },
        "institutions": {"coverage_status": "available" if included else "no_usable_forecasts", "forecasts": included},
        "sources": sources,
        "analysis": {
            "company_type": company_type,
            "business_identity": raw["business"],
            "business_identity_source": source_url(code, "business"),
            "primary_model": f"{method.upper()}三情景;最新TTM扣非、真实日期机构预测与资产负债交叉验证",
            "cross_checks": ["最新TTM扣非利润", "机构预测真实日期", "PB/PS", "经营现金流", "反向估值"],
            "profit_sources": [f"主营业务:{raw['business'].get('main_business') or '公开经营分析未返回'}", f"产品类型:{raw['business'].get('product_types') or '未披露'}"],
            "profit_source_quality": [f"TTM归母{ttm_attr/1e8:.2f}亿元,TTM扣非{ttm_deduct/1e8:.2f}亿元。", f"有效机构2026预测{len(consensus_values)}家;剔除滞后或低于已实现利润的预测{len(excluded)}家。"],
            "risks": ["中报仍可能未经审计,全年季节性、价格周期和一次性因素可能令TTM失真。", "结构化财务为C1公告镜像,正式复核时优先回到交易所/巨潮原文。", "合理区间是条件化估值,不是目标价。"],
            "upgrade_triggers": ["后续财报扣非利润和经营现金流同时高于基准路径。", "机构在最新披露后上调盈利且经营数据验证。"],
            "downgrade_triggers": ["单季利润显著回落、扣非与现金流背离或机构预测再次下修。", "主营身份、股本、会计口径或重大事项改变。"],
            "executive_conclusion": "由冻结V1内核计算后生成。",
            "old_valuation_date": item["valuation_date"], "old_latest_operating_info_date": old_info_date,
            "financial_update": financial_update, "model_reclassified": reclassified, "old_company_type": old_type,
            "reused_prior_judgment": reuse_prior_judgment,
            "classification_tier": tier, "classification_confidence": "中低" if reclassified else "中",
            "quality_model_override_reason": quality_override_reason,
            "excluded_institution_forecasts": excluded, "information_cutoff": f"{AS_OF}T10:19:24+08:00",
            "raw_sha256": raw.get("raw_sha256", {}),
        },
    }
    out_dir = CURRENT_ROOT / ticker
    out_dir.mkdir(parents=True, exist_ok=True)
    snapshot_path = out_dir / f"{safe_name(item['company'])}估值快照_当前.json"
    write_json(snapshot_path, snapshot)
    calc_dir = out_dir / "calculation"
    if method == "pb" and bal["equity"] <= 0:
        calc_dir.mkdir(parents=True, exist_ok=True)
        for stale_name in ("valuation_results.json", "valuation_report.md", "run_manifest.json"):
            stale_path = calc_dir / stale_name
            if stale_path.exists():
                stale_path.unlink()
        formal_path = out_dir / f"{safe_name(item['company'])}价格合理性评估_当前.md"
        formal_path.write_text(
            "\n".join(
                [
                    f"# {item['company']}当前价格合理性评估",
                    "",
                    "## 结论",
                    "",
                    f"- 当前价格:{price:.2f}元({PRICE_DATE}前复权完整交易日收盘)。",
                    f"- 最新经营期:{current_end};TTM归母/扣非:{ttm_attr/1e8:.2f}/{ttm_deduct/1e8:.2f}亿元。",
                    "- **本轮不输出虚假的数值合理区间。** 公司最新归母净资产不为正,同时TTM扣非利润为负,冻结V1内核支持的PE与PB都失去有效锚点。",
                    "- 当前状态:估值模型缺口;旧合理区间停止作为当前判断依据。若后续净资产转正、形成可持续盈利或完成可核实资产重组,再重新建立区间。",
                    "",
                    "## 风险与泡沫解释",
                    "",
                    "- 在负净资产、持续亏损条件下,价格主要反映保壳、重整、资产注入或经营扭转的期权价值;这些预期无法由当前盈利和净资产验证。",
                    "- 这不是‘低估’,也不能因旧PB区间很低而机械判成‘明显偏贵’;应单列为高不确定性、无法可靠数值估值。",
                    "",
                    "## 证据与边界",
                    "",
                    f"- 财务镜像更新日:{notice_date};信息截止:{AS_OF}。",
                    "- 财务数字来自登记C1结构化公告镜像,正式决策应回到交易所/巨潮原文复核。",
                    "- 不构成交易指令或收益承诺。",
                    "",
                ]
            ),
            encoding="utf-8",
        )
        write_json(
            out_dir / "source_evidence_manifest.json",
            {
                "batch_id": BATCH_ID,
                "ticker": ticker,
                "company": item["company"],
                "as_of": AS_OF,
                "price_date": PRICE_DATE,
                "status": "VALUATION_MODEL_GAP",
                "reason": "latest parent equity <= 0 and TTM deduct profit <= 0; frozen V1 PE/PB cannot form a valid range",
                "raw_sha256": raw.get("raw_sha256", {}),
            },
        )
        return {
            "ticker": ticker,
            "company": item["company"],
            "status": "VALUATION_MODEL_GAP",
            "errors": ["最新归母净资产不为正且TTM扣非亏损,PE/PB均无有效锚点,旧区间停止使用"],
            "report_path": formal_path.relative_to(ROOT).as_posix(),
        }
    run = run_pipeline(snapshot_path, calc_dir, REGISTRY)
    normalize_pipeline_report(calc_dir)
    results = read_json(calc_dir / "valuation_results.json")
    if int(results.get("qa", {}).get("error_count") or 0) > 0:
        issues = results.get("qa", {}).get("issues") or []
        raise RuntimeError(f"V1 QA blocked: {issues[:3]}")
    base_result = next(row for row in results["scenarios"] if row["role"] == "base")
    optimistic_result = next(row for row in results["scenarios"] if row["role"] == "optimistic")
    base_low = float(base_result["price_low"])
    base_high = float(base_result["price_high"])
    optimistic_high = float(optimistic_result["price_high"])
    if price < base_low:
        label = "偏低"
    elif price <= base_high:
        label = "基本合理"
    elif price <= optimistic_high:
        label = "偏贵"
    else:
        label = "明显偏贵"
    premium = price / base_high - 1 if base_high > 0 else math.inf
    vs_optimistic = price / optimistic_high - 1 if optimistic_high > 0 else math.inf
    if premium <= 0:
        bubble = "未识别估值泡沫"
    elif vs_optimistic > 0:
        bubble = "泡沫-极端"
    elif premium <= 0.15:
        bubble = "泡沫-轻"
    elif premium <= 0.50:
        bubble = "泡沫-中"
    else:
        bubble = "泡沫-高"
    yoy_profit = None
    if prior["attributable_profit"] not in (None, 0):
        yoy_profit = current["attributable_profit"] / prior["attributable_profit"] - 1
    if bubble == "未识别估值泡沫":
        bubble_reason = "当前价格未高于基准合理区间上沿,按统一规则不认定估值泡沫。"
    else:
        signals = [
            f"价格高于基准上沿{premium*100:.1f}%",
            f"市场提前交易{company_type}的增长、修复或份额提升预期",
        ]
        if vs_optimistic > 0:
            signals.append(f"且高于乐观上沿{vs_optimistic*100:.1f}%,包含模型之外的额外叙事溢价")
        if yoy_profit is not None:
            signals.append(f"最新累计归母利润同比{'增长' if yoy_profit >= 0 else '下降'}{abs(yoy_profit)*100:.1f}%")
        if current.get("cfo") is not None and float(current["cfo"]) < 0:
            signals.append("经营现金流为负,利润兑现质量尚未完全验证")
        if not consensus_values:
            signals.append("最新披露后没有可用机构盈利预测,乐观预期缺少外部预测交叉验证")
        elif len(consensus_values) <= 2:
            signals.append(f"有效机构预测仅{len(consensus_values)}家,样本偏少")
        if reclassified:
            signals.append("原估值模型与真实主营不完全匹配,本轮已重分类,历史题材溢价需折价看待")
        bubble_reason = ";".join(signals) + "。"
    position_pct = valuation_position_pct(price, base_low, base_high)
    calc_text = (calc_dir / "valuation_report.md").read_text(encoding="utf-8")
    intro = (
        "\n## 本轮中报更新\n\n"
        f"- 当前价格:{price:.2f}元({PRICE_DATE}前复权完整交易日收盘);信息截止:{AS_OF} 10:19:24。\n"
        f"- 最新经营期:{current_end},披露/镜像更新日:{notice_date};上次经营信息日:{old_info_date}。\n"
        f"- 主营身份:{company_type};原模型:{old_type or '未记录'};模型{'已重分类' if reclassified else '延续已核实模型'}。\n"
        f"- TTM归母/扣非:{ttm_attr/1e8:.2f}/{ttm_deduct/1e8:.2f}亿元;有效机构2026预测:{len(consensus_values)}家,中位数:{('—' if consensus is None else f'{consensus/1e8:.2f}亿元')}。\n"
        f"- 盈利质量模型修正:{quality_override_reason or '未触发异常盈利锚修正'}。\n"
        f"- 基准合理区间:**{base_low:.2f}—{base_high:.2f}元**;乐观上沿:{optimistic_high:.2f}元;当前判断:**{label}**;泡沫状态:**{bubble}**。\n"
        f"- 相对基准上沿:{premium*100:.2f}%;相对乐观上沿:{vs_optimistic*100:.2f}%。\n"
        "- 财务核心数字来自登记C1结构化公告镜像;机构预测只使用真实研报日期,早于最新披露或低于已实现利润的记录已排除。\n"
        "\n## 泡沫识别与原因\n\n"
        f"- 判定:**{bubble}**。{bubble_reason}\n"
        "- 原因性质:除价格位置外均为基于最新财务、真实主营与机构覆盖的解释性推断,不等同于已证实的资金流因果。\n"
    )
    formal = calc_text.replace("\n## 1. 结论先行", intro + "\n## 1. 结论先行", 1)
    formal = formal.replace("价格合理性评估(流水线生成底稿)", "当前价格合理性评估", 1)
    formal_path = out_dir / f"{safe_name(item['company'])}价格合理性评估_当前.md"
    formal_path.write_text("\n".join(line.rstrip() for line in formal.splitlines()) + "\n", encoding="utf-8")
    evidence = {
        "batch_id": BATCH_ID, "ticker": ticker, "company": item["company"], "as_of": AS_OF,
        "price_date": PRICE_DATE, "old_valuation_id": item["valuation_id"], "old_valuation_date": item["valuation_date"],
        "financial_update": financial_update, "model_reclassified": reclassified, "preserved_old_multiples": preserve,
        "reused_prior_judgment": reuse_prior_judgment,
        "raw_sha256": raw.get("raw_sha256", {}), "qa": results["qa"], "run_status": run["status"],
        "excluded_forecast_count": len(excluded), "included_forecast_count": len(included),
    }
    write_json(out_dir / "source_evidence_manifest.json", evidence)
    source_hash = hashlib.sha256(snapshot_path.read_bytes()).hexdigest().upper()
    valuation_id = "VAL-" + hashlib.sha256(f"{ticker}|{AS_OF}|{source_hash}".encode()).hexdigest()[:24]
    return {
        "ticker": ticker, "company": item["company"], "market": item["market"], "currency": item["currency"],
        "status": "COMPLETE", "valuation_id": valuation_id, "valuation_date": AS_OF, "price_date": PRICE_DATE,
        "close": price, "shares": shares, "market_cap": price * shares, "latest_period": current_end,
        "latest_notice_date": notice_date, "old_valuation_date": item["valuation_date"], "financial_update": financial_update,
        "model_reclassified": reclassified, "old_company_type": old_type, "company_type": company_type,
        "method": method.upper(), "normalized_profit": float(results["metrics"]["normalized_profit"]),
        "normalized_pe": float(results["metrics"]["normalized_pe"]) if results["metrics"].get("normalized_pe") is not None else None,
        "pb": float(results["metrics"]["pb"]) if results["metrics"].get("pb") is not None else None,
        "ps": float(results["metrics"]["ps"]) if results["metrics"].get("ps") is not None else None,
        "consensus_year": 2026 if consensus is not None else None, "consensus_profit": consensus,
        "consensus_count": len(consensus_values), "excluded_forecast_count": len(excluded),
        "pessimistic_low": float(next(row for row in results["scenarios"] if row["role"] == "pessimistic")["price_low"]),
        "pessimistic_high": float(next(row for row in results["scenarios"] if row["role"] == "pessimistic")["price_high"]),
        "base_low": base_low, "base_high": base_high,
        "optimistic_low": float(optimistic_result["price_low"]), "optimistic_high": optimistic_high,
        "label": label, "bubble_status": bubble, "bubble_reason": bubble_reason,
        "valuation_position_pct": position_pct,
        "premium_to_base_high": premium, "vs_optimistic_high": vs_optimistic,
        "confidence": "中低" if reclassified or raw["status"] != "OK" or quality_override_reason else ("中高" if current_end == "2026-06-30" and len(consensus_values) >= 2 else "中"),
        "qa_status": results["qa"]["status"], "qa_warning_count": results["qa"]["warning_count"],
        "report_path": formal_path.relative_to(ROOT).as_posix(), "snapshot_path": snapshot_path.relative_to(ROOT).as_posix(),
        "source_hash": source_hash,
    }
 
 
def load_universe() -> list[dict[str, Any]]:
    with (TMP_ROOT / "universe.csv").open(encoding="utf-8", newline="") as handle:
        rows = list(csv.DictReader(handle))
    for row in rows:
        for key in ("base_low", "base_high", "optimistic_low", "optimistic_high", "normalized_profit", "normalized_pe", "pb", "ps", "consensus_profit", "close", "static_total_shares"):
            row[key] = float(row[key]) if row.get(key) not in (None, "") else None
        row["consensus_count"] = int(row["consensus_count"]) if row.get("consensus_count") else 0
    return rows
 
 
def write_outputs(rows: list[dict[str, Any]], failures: list[dict[str, Any]], wall_seconds: float) -> None:
    MASTER_ROOT.mkdir(parents=True, exist_ok=True)
    columns = list(rows[0])
    csv_path = MASTER_ROOT / "全部已评估公司最新估值.csv"
    with csv_path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=columns)
        writer.writeheader()
        writer.writerows(sorted(rows, key=lambda row: (row["valuation_position_pct"], row["ticker"])))
    labels = Counter(row["label"] for row in rows)
    updates = Counter("半年报" if row["latest_period"] == "2026-06-30" else "一季报" for row in rows)
    lines = [
        "# 全部已评估公司中报期最新估值",
        "",
        f"- 信息截止:{AS_OF} 10:19:24;价格:{PRICE_DATE}前复权完整交易日收盘。",
        f"- 有效数值估值:{len(rows)}只;显式缺口:{len(failures)}只;本轮计算墙钟:{wall_seconds:.1f}秒。",
        f"- 最新财务覆盖:半年报{updates['半年报']}只、一季报{updates['一季报']}只;相对旧经营信息新增披露{sum(row['financial_update'] for row in rows)}只。",
        f"- 主营模型重分类:{sum(row['model_reclassified'] for row in rows)}只;机构预测使用真实研报日期,旧预测不会以抓取日代替。",
        f"- 判断分布:偏低{labels['偏低']}、基本合理{labels['基本合理']}、偏贵{labels['偏贵']}、明显偏贵{labels['明显偏贵']}。",
        "- 当前稳定逐股报告位于`../当前估值/<ticker>/`,每次覆盖同名文件,不再新增日期版当前报告。",
        "- 合理价值是条件化区间,不构成交易指令或收益承诺。",
        "",
        "## 估值由低到高",
        "",
        "| 公司 | 代码 | 收盘价 | 基准区间 | 估值位置 | 判断 | 泡沫判定 | 最新财务 | 有效机构 | 模型变化 | 置信度 | 报告 |",
        "|---|---|---:|---:|---:|---|---|---|---:|---|---|---|",
    ]
    for row in sorted(rows, key=lambda item: (item["valuation_position_pct"], item["ticker"])):
        report = "../" + row["report_path"].split("ana-data/result/股票估值/", 1)[1]
        lines.append(
            f"| {row['company']} | {row['ticker']} | {row['close']:.2f} | {row['base_low']:.2f}—{row['base_high']:.2f} | "
            f"{row['valuation_position_pct']*100:.2f}% | {row['label']} | {row['bubble_status']} | {row['latest_period']} | {row['consensus_count']} | "
            f"{'重分类' if row['model_reclassified'] else '延续'} | {row['confidence']} | [报告]({report}) |"
        )
    if failures:
        lines += ["", "## 显式缺口", "", "| 公司 | 代码 | 状态 | 原因 |", "|---|---|---|---|"]
        for item in failures:
            lines.append(f"| {item.get('company','')} | {item['ticker']} | {item['status']} | {';'.join(item.get('errors', [])) or '公开源不支持'} |")
    md_path = MASTER_ROOT / "全部已评估公司最新估值.md"
    md_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
    write_json(
        CASE_ROOT / "revaluation_manifest.json",
        {
            "batch_id": BATCH_ID, "as_of": AS_OF, "price_date": PRICE_DATE,
            "completed_at": datetime.now().astimezone().isoformat(), "wall_seconds": round(wall_seconds, 3),
            "success": len(rows), "failures": failures, "label_counts": dict(labels),
            "financial_update_count": sum(row["financial_update"] for row in rows),
            "reclassified_count": sum(row["model_reclassified"] for row in rows),
            "csv_sha256": hashlib.sha256(csv_path.read_bytes()).hexdigest().upper(),
            "md_sha256": hashlib.sha256(md_path.read_bytes()).hexdigest().upper(),
        },
    )
 
 
def sql_text(value: Any) -> str:
    if value is None or value == "":
        return "NULL"
    return f"CONVERT(0x{str(value).encode('utf-8').hex()} USING utf8mb4)"
 
 
def sql_number(value: Any) -> str:
    if value in (None, "", "NULL"):
        return "NULL"
    result = float(value)
    if not math.isfinite(result):
        return "NULL"
    return format(result, ".12g")
 
 
def mysql_execute(sql: str) -> None:
    proc = subprocess.run(
        [
            "mysql",
            "--login-path=ana_semi_admin_preflight",
            "--default-character-set=utf8mb4",
            "--binary-mode",
        ],
        cwd=ROOT,
        input=sql,
        text=True,
        encoding="utf-8",
        errors="strict",
        capture_output=True,
        check=False,
    )
    if proc.returncode != 0:
        raise RuntimeError(f"mysql publish failed: {proc.stderr.strip()}")
 
 
def write_csv_rows(path: Path, rows: list[dict[str, Any]], fieldnames: list[str] | None = None) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    if fieldnames is None:
        fieldnames = list(rows[0]) if rows else []
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fieldnames, extrasaction="ignore", lineterminator="\n")
        writer.writeheader()
        writer.writerows(rows)
 
 
def action_publish() -> None:
    master_path = MASTER_ROOT / "全部已评估公司最新估值.csv"
    manifest_path = CASE_ROOT / "revaluation_manifest.json"
    with master_path.open(encoding="utf-8", newline="") as handle:
        rows = list(csv.DictReader(handle))
    manifest = read_json(manifest_path)
    failures = list(manifest["failures"])
    if len(rows) + len(failures) != 1229:
        raise RuntimeError(f"unexpected publish scope: rows={len(rows)}, failures={len(failures)}")
    tickers = [row["ticker"] for row in rows]
    if len(set(tickers)) != len(tickers):
        raise RuntimeError("duplicate master tickers")
    base_universe_map = {row["ticker"]: row for row in load_universe()}
    price_rows = fetch.mysql_rows(
        f"SELECT ticker,close FROM stock_valuation.daily_price WHERE trade_date='{PRICE_DATE}' ORDER BY ticker"
    )
    price_map = {row["ticker"]: float(row["close"]) for row in price_rows}
    mismatches = [
        row["ticker"] for row in rows
        if row["ticker"] not in price_map or abs(float(row["close"]) - price_map[row["ticker"]]) > 0.000001
    ]
    if mismatches:
        raise RuntimeError(f"daily price mismatch: {mismatches[:10]}")
 
    CASE_ROOT.mkdir(parents=True, exist_ok=True)
    active_before = fetch.mysql_rows(
        "SELECT * FROM stock_valuation.valuation_version WHERE active_to IS NULL ORDER BY ticker,valuation_id"
    )
    judgement_before = fetch.mysql_rows(
        f"SELECT * FROM stock_valuation.daily_judgement WHERE trade_date='{PRICE_DATE}' ORDER BY ticker"
    )
    active_backup_path = CASE_ROOT / "pre_publish_active_versions.csv"
    judgement_backup_path = CASE_ROOT / f"pre_publish_daily_judgement_{PRICE_DATE.replace('-', '')}.csv"
    if not active_backup_path.exists():
        write_csv_rows(active_backup_path, active_before)
    if not judgement_backup_path.exists():
        write_csv_rows(judgement_backup_path, judgement_before)
 
    valuation_values: list[str] = []
    judgement_values: list[str] = []
    for row in rows:
        valuation_values.append(
            "(" + ",".join(
                [
                    sql_text(row["valuation_id"]), sql_text(row["ticker"]), sql_text(AS_OF), sql_text(row["method"]),
                    sql_number(row["pessimistic_low"]), sql_number(row["pessimistic_high"]),
                    sql_number(row["base_low"]), sql_number(row["base_high"]),
                    sql_number(row["optimistic_low"]), sql_number(row["optimistic_high"]),
                    sql_number(row["normalized_profit"]), sql_number(row["normalized_pe"]),
                    sql_number(row["pb"]), sql_number(row["ps"]),
                    sql_number(row["consensus_year"]), sql_number(row["consensus_profit"]), sql_number(row["consensus_count"]),
                    sql_text(row["report_path"]), sql_text(row["snapshot_path"]), sql_text(row["source_hash"]),
                    sql_text(AS_OF), "NULL",
                ]
            ) + ")"
        )
        close = float(row["close"])
        base_low = float(row["base_low"])
        base_high = float(row["base_high"])
        judgement_values.append(
            "(" + ",".join(
                [
                    sql_text(row["ticker"]), sql_text(PRICE_DATE), sql_text(row["valuation_id"]),
                    sql_number(close), sql_number(base_low), sql_number(base_high), sql_number(row["optimistic_high"]),
                    sql_text(row["label"]), sql_number(close / base_low - 1), sql_number(close / base_high - 1),
                ]
            ) + ")"
        )
    all_a_tickers = tickers + [item["ticker"] for item in failures if not item["ticker"].endswith(".HK")]
    close_tickers = tickers + [item["ticker"] for item in failures if item["status"] == "VALUATION_MODEL_GAP"]
    ticker_sql = ",".join(sql_text(value) for value in all_a_tickers)
    close_ticker_sql = ",".join(sql_text(value) for value in close_tickers)
    valuation_id_sql = ",".join(sql_text(row["valuation_id"]) for row in rows)
    retained_gap_ids = [
        base_universe_map[item["ticker"]]["valuation_id"] for item in failures if item["status"] == "PRICE_GAP"
    ]
    reopen_gap_sql = "\n".join(
        f"UPDATE valuation_version SET active_to=NULL WHERE valuation_id={sql_text(value)};" for value in retained_gap_ids
    )
    sql = f"""
SET NAMES utf8mb4;
USE stock_valuation;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
UPDATE valuation_version
SET active_to=GREATEST(active_from,{sql_text(PRICE_DATE)})
WHERE active_to IS NULL
  AND ticker IN ({close_ticker_sql})
  AND valuation_id NOT IN ({valuation_id_sql});
INSERT INTO valuation_version
(valuation_id,ticker,valuation_date,method,pessimistic_low,pessimistic_high,base_low,base_high,
 optimistic_low,optimistic_high,normalized_profit,normalized_pe,pb,ps,consensus_year,consensus_profit,
 consensus_count,report_path,snapshot_path,source_hash,active_from,active_to)
VALUES {','.join(valuation_values)}
ON DUPLICATE KEY UPDATE
 method=VALUES(method),pessimistic_low=VALUES(pessimistic_low),pessimistic_high=VALUES(pessimistic_high),
 base_low=VALUES(base_low),base_high=VALUES(base_high),optimistic_low=VALUES(optimistic_low),
 optimistic_high=VALUES(optimistic_high),normalized_profit=VALUES(normalized_profit),normalized_pe=VALUES(normalized_pe),
 pb=VALUES(pb),ps=VALUES(ps),consensus_year=VALUES(consensus_year),consensus_profit=VALUES(consensus_profit),
 consensus_count=VALUES(consensus_count),report_path=VALUES(report_path),snapshot_path=VALUES(snapshot_path),
 active_from=VALUES(active_from),active_to=NULL;
DELETE FROM daily_judgement WHERE trade_date={sql_text(PRICE_DATE)} AND ticker IN ({ticker_sql});
INSERT INTO daily_judgement
(ticker,trade_date,valuation_id,close,base_low,base_high,optimistic_high,label,distance_to_base_low,distance_to_base_high)
VALUES {','.join(judgement_values)};
{reopen_gap_sql}
DELETE v FROM valuation_version v
LEFT JOIN daily_judgement d ON d.valuation_id=v.valuation_id
WHERE v.valuation_date={sql_text(AS_OF)}
  AND v.valuation_id NOT IN ({valuation_id_sql})
  AND d.valuation_id IS NULL;
COMMIT;
"""
    mysql_execute(sql)
 
    latest_root = ROOT / "ana-data/result/股票估值/估值台账"
    base_fields = [
        "ticker", "company", "market", "currency", "close", "trade_date", "base_low", "base_high",
        "optimistic_high", "label", "distance_to_base_low", "distance_to_base_high", "valuation_date",
        "consensus_year", "consensus_profit", "consensus_count", "report_path",
    ]
    latest_rows: list[dict[str, Any]] = []
    for row in sorted(rows, key=lambda item: (float(item["valuation_position_pct"]), item["ticker"])):
        close = float(row["close"])
        base_low = float(row["base_low"])
        base_high = float(row["base_high"])
        latest_rows.append(
            {
                "ticker": row["ticker"], "company": row["company"], "market": row["market"], "currency": row["currency"],
                "close": row["close"], "trade_date": PRICE_DATE, "base_low": row["base_low"], "base_high": row["base_high"],
                "optimistic_high": row["optimistic_high"], "label": row["label"],
                "distance_to_base_low": f"{close/base_low-1:.8f}", "distance_to_base_high": f"{close/base_high-1:.8f}",
                "valuation_date": AS_OF, "consensus_year": row["consensus_year"], "consensus_profit": row["consensus_profit"],
                "consensus_count": row["consensus_count"], "report_path": row["report_path"],
            }
        )
    write_csv_rows(latest_root / "latest.csv", latest_rows, base_fields)
    gap_fields = ["ticker", "company", "market", "currency", "status", "reason", "price_date", "close", "source"]
    gap_rows: list[dict[str, Any]] = []
    universe_map = base_universe_map
    for item in failures:
        old = universe_map[item["ticker"]]
        if item["ticker"].endswith(".HK"):
            gap_rows.append(
                {"ticker": item["ticker"], "company": item["company"], "market": old["market"], "currency": old["currency"],
                 "status": "无法自动日更", "reason": "本地前复权专表首版不覆盖港股", "price_date": "", "close": "",
                 "source": "trading_xuntou.cn_stock_kline_1d_front"}
            )
        elif item["status"] == "PRICE_GAP":
            gap_rows.append(
                {"ticker": item["ticker"], "company": item["company"], "market": old["market"], "currency": old["currency"],
                 "status": "缺少完整日K", "reason": f"{PRICE_DATE}正式价格日,但前复权专表无当日日K;不回退旧价",
                 "price_date": item.get("last_price_date", ""), "close": item.get("last_price", ""),
                 "source": "trading_xuntou.cn_stock_kline_1d_front"}
            )
        else:
            gap_rows.append(
                {"ticker": item["ticker"], "company": item["company"], "market": old["market"], "currency": old["currency"],
                 "status": "估值模型缺口", "reason": "最新归母净资产不为正且TTM扣非亏损;PE/PB均无有效锚点,旧区间停止使用",
                 "price_date": PRICE_DATE, "close": old["close"], "source": "最新中报C1公告镜像+冻结V1内核"}
            )
    write_csv_rows(latest_root / "latest_gaps.csv", gap_rows, gap_fields)
    labels = Counter(row["label"] for row in rows)
    (latest_root / "latest.md").write_text(
        "\n".join(
            [
                "# 股票估值每日台账最新总表", "", f"- 价格交易日:`{PRICE_DATE}`", f"- 估值更新日:`{AS_OF}`",
                f"- 数值判定:{len(rows)}只;显式缺口:{len(gap_rows)}只。",
                f"- 标签:偏低{labels['偏低']}、基本合理{labels['基本合理']}、偏贵{labels['偏贵']}、明显偏贵{labels['明显偏贵']}。",
                "- 泡沫原因与量价显影将在同一发布流程的解释层补齐。", "",
            ]
        ),
        encoding="utf-8",
    )
 
    audit_rows: list[dict[str, Any]] = []
    for row in sorted(rows, key=lambda item: item["ticker"]):
        raw = read_json(RAW_ROOT / f"{row['ticker'].replace('.', '_')}.json")
        audit_rows.append(
            {
                "ticker": row["ticker"], "company": row["company"], "raw_status": raw.get("status"),
                "latest_period": row["latest_period"], "latest_notice_date": row["latest_notice_date"],
                "financial_update": row["financial_update"], "model_reclassified": row["model_reclassified"],
                "consensus_count": row["consensus_count"], "excluded_forecast_count": row["excluded_forecast_count"],
                "qa_status": row["qa_status"], "qa_warning_count": row["qa_warning_count"],
                "finance_income_sha256": (raw.get("raw_sha256") or {}).get("income", ""),
                "finance_balance_sha256": (raw.get("raw_sha256") or {}).get("balance", ""),
                "finance_cashflow_sha256": (raw.get("raw_sha256") or {}).get("cashflow", ""),
                "forecast_sha256": (raw.get("raw_sha256") or {}).get("forecast", ""),
                "business_sha256": (raw.get("raw_sha256") or {}).get("business", ""),
            }
        )
    write_csv_rows(CASE_ROOT / "data_freshness_audit.csv", audit_rows)
    validation = fetch.mysql_rows(
        f"""
SELECT
 (SELECT COUNT(*) FROM stock_valuation.valuation_version WHERE valuation_date='{AS_OF}') AS new_versions,
 (SELECT COUNT(*) FROM stock_valuation.valuation_version WHERE active_to IS NULL) AS active_versions,
 (SELECT COUNT(*) FROM stock_valuation.daily_judgement WHERE trade_date='{PRICE_DATE}') AS judgements,
 (SELECT COUNT(*) FROM stock_valuation.daily_price WHERE trade_date='{PRICE_DATE}') AS prices,
 (SELECT COUNT(*) FROM stock_valuation.valuation_version WHERE ticker='603398.SH' AND active_to IS NULL) AS mubang_active
"""
    )[0]
    retained_gap_active = sum(item["status"] in {"UNSUPPORTED_HK", "PRICE_GAP"} for item in failures)
    priced_without_valuation = sum(item["status"] == "VALUATION_MODEL_GAP" for item in failures)
    expected = {
        "new_versions": len(rows), "active_versions": len(rows) + retained_gap_active,
        "judgements": len(rows), "prices": len(rows) + priced_without_valuation, "mubang_active": 0,
    }
    for key, value in expected.items():
        if int(validation[key]) != value:
            raise RuntimeError(f"post-publish validation failed: {key}={validation[key]}, expected={value}")
    write_json(
        CASE_ROOT / "publication_receipt.json",
        {
            "batch_id": BATCH_ID, "published_at": datetime.now().astimezone().isoformat(),
            "database": "stock_valuation", "validation": validation,
            "latest_csv_sha256_before_bubble_enrichment": hashlib.sha256((latest_root / "latest.csv").read_bytes()).hexdigest().upper(),
            "latest_gaps_sha256": hashlib.sha256((latest_root / "latest_gaps.csv").read_bytes()).hexdigest().upper(),
            "rollback_active_versions_sha256": hashlib.sha256(active_backup_path.read_bytes()).hexdigest().upper(),
            "rollback_daily_judgement_sha256": hashlib.sha256(judgement_backup_path.read_bytes()).hexdigest().upper(),
        },
    )
    print(json.dumps({"status": "PUBLISHED", "validation": validation, "gaps": gap_rows}, ensure_ascii=False, indent=2))
 
 
def action_revalue(workers: int, ticker: str | None, limit: int | None) -> None:
    universe = load_universe()
    if ticker:
        universe = [row for row in universe if row["ticker"] == ticker]
    if limit is not None:
        universe = universe[:limit]
    forced = force_reclass_tickers()
    rows: list[dict[str, Any]] = []
    failures: list[dict[str, Any]] = []
    started = time.monotonic()
    with ThreadPoolExecutor(max_workers=workers, thread_name_prefix="valuation-v1") as pool:
        futures = {pool.submit(build_one, item, forced): item for item in universe}
        for index, future in enumerate(as_completed(futures), 1):
            item = futures[future]
            try:
                result = future.result()
            except Exception as exc:
                result = {"ticker": item["ticker"], "company": item["company"], "status": "FAILED", "errors": [f"{type(exc).__name__}: {exc}"]}
            if result["status"] == "COMPLETE":
                rows.append(result)
            else:
                failures.append(result)
            if index % 50 == 0 or index == len(universe):
                print(f"[{index}/{len(universe)}] success={len(rows)} gaps={len(failures)} elapsed={time.monotonic()-started:.1f}s", flush=True)
    rows.sort(key=lambda row: row["ticker"])
    failures.sort(key=lambda row: row["ticker"])
    if not ticker and limit is None:
        write_outputs(rows, failures, time.monotonic() - started)
    else:
        print(json.dumps({"rows": rows, "failures": failures}, ensure_ascii=False, indent=2))
 
 
def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("action", choices=("revalue", "publish"))
    parser.add_argument("--workers", type=int, default=8)
    parser.add_argument("--ticker")
    parser.add_argument("--limit", type=int)
    args = parser.parse_args()
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    if args.action == "revalue":
        action_revalue(args.workers, args.ticker, args.limit)
    else:
        action_publish()
 
 
if __name__ == "__main__":
    main()