from __future__ import annotations import hashlib import importlib.util import json import os import shutil import sys from pathlib import Path from typing import Any import mysql.connector ROOT = Path(__file__).resolve().parents[2] for package_root in (ROOT / "dev" / "ana-dev", ROOT / "dev" / "project-dev"): sys.path.insert(0, str(package_root)) PREVIOUS_PATH = ROOT / "ai-valuation-analyst" / "tools" / "generate_missing_image_stocks_batch_20260805.py" SPEC = importlib.util.spec_from_file_location("valuation_batch_20260805_reuse", PREVIOUS_PATH) if SPEC is None or SPEC.loader is None: raise RuntimeError(f"cannot load {PREVIOUS_PATH}") previous = importlib.util.module_from_spec(SPEC) sys.modules[SPEC.name] = previous SPEC.loader.exec_module(previous) base = previous.base BATCH_ID = "BATCH-STOCK-VALUATION-20260806-005" AS_OF = "2026-08-05" STAMP = AS_OF.replace("-", "") V2_ROOT = ROOT / "ai-valuation-analyst" / "tmp" / "valuation_batch_20260806_005" RESULT_ROOT = ROOT / "ana-data" / "result" / "股票估值" SUMMARY_DIR = RESULT_ROOT / "20260806_batch_missing_image_stocks_valuation" CASE_ROOT = ROOT / "ana-data" / "cases" / "股票估值" / BATCH_ID REGISTRY = ROOT / "dev" / "ana-dev" / "stock_valuation_pipeline" / "source_registry.json" SCREENSHOTS = [ Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-80f8a476-6d32-4559-b424-46fe21c456ee.png"), Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-773f7cf0-7a6c-4f59-9f2f-7677922bfbcc.png"), Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-3414072f-3e97-400a-9937-d6062a731f59.png"), Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-69e7fa08-4d86-4a66-9bb8-dc9782795224.png"), Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-9661d685-8d5f-475c-b2c9-51d5abaa331a.png"), Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-2a3203c9-c044-473b-ac86-8161a9b8f70d.png"), Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-ee50b690-dfb1-4b96-9d3f-6452a4bb01e0.png"), Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-507642c0-f231-42bd-a834-5b1a66f62bf9.png"), Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-a8177a3e-e18f-4716-a404-260098312b1d.png"), Path(r"C:\Users\Cai\AppData\Local\Temp\codex-clipboard-31d336d0-a7a8-405e-8d71-a0714c0a8656.png"), ] def S(ticker: str, slug: str, business: str, driver: str, tier: str, risk: str): return base.Stock(ticker, slug, business, driver, tier, risk) STOCKS = [ S("000021.SZ", "shenzhen_kaifa", "存储半导体封测、智能制造与电子产品制造", "存储景气、封测稼动率、客户订单和产品结构", "stable_mfg", "存储周期、客户集中和汇率波动"), S("000988.SZ", "huagong_tech", "光通信器件、激光装备与传感器", "高速光模块、激光装备订单、良率和产品升级", "ai_compute", "算力资本开支、产品迭代和高估值"), S("300115.SZ", "everwin_precision", "消费电子精密结构件与新能源零部件", "大客户新品、机器人和新能源业务放量、产能利用率", "stable_mfg", "客户集中、消费电子周期和新业务兑现"), S("300258.SZ", "precision_ts", "汽车精密锻件与差速器零部件", "新能源车型销量、海外客户、单车价值和产能利用率", "auto_leader", "汽车价格战、客户集中和海外扩产"), S("300308.SZ", "innolight", "高速光模块", "AI数据中心资本开支、800G/1.6T出货、良率和客户份额", "ai_compute", "AI资本开支波动、客户集中和估值拥挤"), S("300433.SZ", "lens_technology", "消费电子玻璃、金属与精密结构件", "大客户新品、AI终端、汽车电子和产能利用率", "semi_growth", "客户集中、消费电子周期和资本开支"), S("300476.SZ", "victory_giant", "高端PCB", "AI服务器与交换机PCB、海外产能、产品结构和良率", "ai_compute", "AI需求波动、扩产爬坡和高估值"), S("300548.SZ", "changxin_bochuang", "光通信器件与高速光模块", "数通光模块需求、产品升级、良率和客户认证", "ai_compute", "客户集中、技术迭代和主题估值"), S("300757.SZ", "robotechnik", "光伏自动化与泛半导体装备", "电池片设备订单、验收节奏、海外客户和新业务", "auto_leader", "光伏资本开支下行、验收波动和订单集中"), S("300969.SZ", "hengshuai", "汽车微电机与流体控制部件", "新车型放量、单车价值、海外客户和产能爬坡", "auto_leader", "客户集中、汽车价格战和扩产执行"), S("301021.SZ", "inno_laser", "激光器与精密激光加工设备", "消费电子新品、先进封装需求、客户验证和产品结构", "semi_growth", "需求波动、客户验证和盈利修复"), S("301308.SZ", "biwin_storage", "存储器及先进封测", "存储价格、企业级与AI端侧产品、库存和封测产能", "semi_growth", "存储周期、库存价格和高估值"), S("301368.SZ", "fengli_intelligent", "精密减速器与齿轮", "机器人需求、客户导入、产能利用率和产品升级", "auto_leader", "机器人主题预期、客户验证和规模较小"), S("301529.SZ", "fosai_technology", "汽车内饰功能件", "客户车型销量、单车价值、海外工厂和原料成本", "auto_leader", "客户集中、汽车周期和海外经营"), S("600522.SH", "zhongtian_technology", "光通信、海洋电力与新能源装备", "海缆订单、光纤需求、项目交付和海外业务", "telecom_leader", "项目交付、应收回款和资本开支"), S("601012.SH", "longi", "光伏硅片、电池组件与氢能", "组件价格、BC产品放量、产能利用率和现金成本", "solar_pb", "供给过剩、持续亏损和减值压力"), S("601865.SH", "flat_glass", "光伏玻璃", "光伏装机、玻璃价格、天然气纯碱成本和产能利用率", "solar_pb", "产能过剩、价格竞争和高资本开支"), S("603119.SH", "zhejiang_rongtai", "云母绝缘材料与新能源绝缘件", "新能源车和储能需求、客户拓展、产能与产品结构", "stable_mfg", "客户集中、扩产消化和原材料波动"), S("603166.SH", "fuda", "汽车曲轴与精密锻件", "商用车与新能源零部件订单、客户结构和产能利用率", "auto_leader", "汽车周期、客户集中和新业务爬坡"), S("603228.SH", "kinwong", "高端PCB", "AI服务器、汽车电子PCB、海外产能和产品结构", "ai_compute", "AI需求、扩产爬坡和客户集中"), S("603618.SH", "hangzhou_cable", "电力电缆与导线", "电网投资、铜价传导、订单交付和回款", "stable_mfg", "铜价、低毛利、应收回款和治理风险"), S("603809.SH", "haoneng", "汽车同步器、齿毂与航空零部件", "汽车客户销量、航空业务放量、产品结构和产能", "auto_leader", "汽车周期、扩产和航空业务兑现"), S("688303.SH", "dqo_energy", "多晶硅", "多晶硅价格、产量、现金成本和行业出清", "solar_pb", "严重供给过剩、持续亏损和存货减值"), S("688327.SH", "cloudwalk", "人工智能平台与行业解决方案", "大模型项目订单、交付回款、毛利率和费用控制", "wafer_pb", "持续亏损、现金消耗、应收回款和商业化不确定性"), S("688401.SH", "freewon", "掩膜版", "半导体与显示掩膜版需求、产品升级、良率和扩产", "semi_growth", "下游周期、扩产爬坡和高估值"), S("688498.SH", "yuanjie", "光通信激光器芯片", "高速光模块需求、客户认证、良率和新产品放量", "ai_compute", "客户验证、技术迭代、盈利波动和高估值"), S("688507.SH", "suren_tech", "工程仿真软件与服务", "国产CAE渗透、订单增长、续费和研发效率", "wafer_pb", "持续亏损、回款周期和商业化规模"), S("688525.SH", "biwin_storage_sh", "存储芯片与先进封测", "存储价格、AI端侧需求、库存周转和产品结构", "semi_growth", "存储周期、库存减值和高估值"), S("688702.SH", "suya_tech", "以太网交换芯片", "数据中心交换芯片放量、客户认证、研发投入和产品迭代", "semi_growth", "客户集中、技术迭代和高研发投入"), S("688825.SH", "cxmt", "DRAM存储芯片制造", "DRAM价格、产能利用率、制程良率和产品升级", "wafer_pb", "新上市估值、存储周期、重资产折旧和技术限制"), S("920418.BJ", "suzhou_bearing", "滚针轴承与滚动体", "汽车与工业客户需求、产品升级、出口和产能利用率", "stable_mfg", "北交所流动性、客户集中和制造业周期"), ] SPECIAL_IDENTITY = { "688825.SH": {"ticker": "688825.SH", "company": "长鑫科技集团股份有限公司", "market": "上海证券交易所科创板"}, "920418.BJ": {"ticker": "920418.BJ", "company": "苏州轴承厂股份有限公司", "market": "北京证券交易所"}, } base.BATCH_ID = BATCH_ID base.AS_OF = AS_OF base.TMP = V2_ROOT base.RESULT_ROOT = RESULT_ROOT base.CASE_ROOT = CASE_ROOT base.REGISTRY = REGISTRY previous.BATCH_ID = BATCH_ID previous.AS_OF = AS_OF previous.V2_ROOT = V2_ROOT previous.RESULT_ROOT = RESULT_ROOT previous.CASE_ROOT = CASE_ROOT previous.SUMMARY_DIR = SUMMARY_DIR def db_connection(database: str = "trading_xuntou"): password = os.environ.get("STOCK_VALUATION_MYSQL_PASSWORD") if password is None: raise RuntimeError("missing STOCK_VALUATION_MYSQL_PASSWORD") return mysql.connector.connect( host=os.environ.get("STOCK_VALUATION_MYSQL_HOST", "127.0.0.1"), port=int(os.environ.get("STOCK_VALUATION_MYSQL_PORT", "3306")), user=os.environ.get("STOCK_VALUATION_MYSQL_USER", "root"), password=password, database=database, charset="utf8mb4", use_unicode=True, ) ORIGINAL_READ_JSON = base.read_json ORIGINAL_ANNOUNCEMENT = base.announcement_record ORIGINAL_PARSE_FINANCE = base.parse_finance def patched_read_json(path: Path) -> dict[str, Any]: value = ORIGINAL_READ_JSON(path) if path.name == "provider_results.json": ticker = path.parent.name.split(".failed-", 1)[0].removeprefix("v2_").replace("_", ".") if ticker in SPECIAL_IDENTITY: value["announcements"]["identity"] = SPECIAL_IDENTITY[ticker] return value def patched_announcement(provider: dict[str, Any], kind: str) -> dict[str, Any]: ticker = provider["announcements"]["identity"]["ticker"] if ticker == "688825.SH": if kind == "annual": return { "title": "长鑫科技首次公开发行招股说明书(2025年度经审计财务数据)", "publish_date": "2026-07-22", "url": "https://static.sse.com.cn/stock/disclosure/announcement/c/202605/002170_20260520_DS3Z.pdf", } return { "title": "长鑫科技科创板上市公告书(截至2026年3月31日审阅财务数据)", "publish_date": "2026-07-24", "url": "https://www.sse.com.cn/disclosure/listedinfo/announcement/", } if ticker == "920418.BJ": title = "2025年年度报告" if kind == "annual" else "2026年第一季度报告" return { "title": title, "publish_date": "2026-04-28", "url": "https://www.bse.cn/disclosure/announcement.html?companyCode=920418", } return ORIGINAL_ANNOUNCEMENT(provider, kind) def finance_total_shares(provider: dict[str, Any]) -> float: for raw_hash in provider["finance"].get("raw_artifact_hashes", []): item = base.blob(raw_hash) rows = (((item or {}).get("result") or {}).get("data") or []) if rows and rows[0].get("TOTAL_SHARE"): return float(rows[0]["TOTAL_SHARE"]) raise RuntimeError("finance TOTAL_SHARE not found") def parse_finance(provider: dict[str, Any]): financials, balance, proof = ORIGINAL_PARSE_FINANCE(provider) for period in ("annual", "current_cumulative", "prior_year_same_period"): financials[period]["capex"] = abs(float(financials[period]["capex"])) return financials, balance, proof def capital_event(ticker: str) -> dict[str, Any] | None: with db_connection() as conn: cursor = conn.cursor(dictionary=True) cursor.execute( "SELECT ann_date, report_date, total_capital, source FROM cn_stock_capital_events " "WHERE symbol=%s AND COALESCE(ann_date,report_date)<=%s " "ORDER BY COALESCE(ann_date,report_date) DESC,id DESC LIMIT 1", (ticker, AS_OF), ) return cursor.fetchone() def parse_market(provider: dict[str, Any], ticker: str) -> tuple[dict[str, Any], dict[str, Any]]: event = capital_event(ticker) shares = float(event["total_capital"]) if event else finance_total_shares(provider) with db_connection() as conn: cursor = conn.cursor(dictionary=True) cursor.execute( "SELECT trade_date,close,amount,source FROM cn_stock_kline_1d_front " "WHERE symbol=%s AND trade_date<=%s AND trade_date>=DATE_SUB(%s,INTERVAL 20 DAY) ORDER BY trade_date", (ticker, AS_OF, AS_OF), ) rows = cursor.fetchall() if not rows: raise RuntimeError(f"missing front-adjusted kline for {ticker}") latest = rows[-1] price = float(latest["close"]) shares_date = ( max(value for value in (event.get("ann_date"), event.get("report_date")) if value).isoformat() if event else AS_OF ) market = { "price": price, "date": latest["trade_date"].isoformat(), "amount": float(latest["amount"] or 0), "shares": shares, "market_cap": price * shares, "window_start": rows[0]["trade_date"].isoformat(), "window_start_close": float(rows[0]["close"]), "window_return": price / float(rows[0]["close"]) - 1, } return market, { "source": "trading_xuntou.cn_stock_kline_1d_front:xtquant:front", "shares_source": "trading_xuntou.cn_stock_capital_events:xtquant" if event else "eastmoney.finance:TOTAL_SHARE", "shares_date": shares_date, "quote_raw_hashes": [], "platform_market_cap_excluded": None, } def run_pipeline(snapshot_path: Path, calc_dir: Path, registry: Path): snapshot = previous.ORIGINAL_READ_JSON(snapshot_path) snapshot["market"]["price_type"] = "trading_xuntou前复权日K收盘价" for source in snapshot["sources"]: if source["id"] == f"SRC-EM-KLINE-{STAMP}": source.update( { "id": f"SRC-XT-FRONT-KLINE-{STAMP}", "source_type": "quote_provider", "title": f"trading_xuntou前复权专表{AS_OF}收盘价", "url": "trading_xuntou.cn_stock_kline_1d_front", } ) if source["id"].startswith("SRC-SHARES-"): source["source_type"] = "quote_provider" if snapshot["meta"]["code"] != "688825.SH" else "sse" if snapshot["meta"]["code"] == "688825.SH" and source["id"] in {"SRC-ANNUAL-2025", "SRC-CURRENT-2026"}: source["source_type"] = "sse" if snapshot["meta"]["code"] == "920418.BJ" and source["id"] in {"SRC-ANNUAL-2025", "SRC-CURRENT-2026"}: source["source_type"] = "bse" base.write_json(snapshot_path, snapshot) return previous.ORIGINAL_RUN_PIPELINE(snapshot_path, calc_dir, registry) base.read_json = patched_read_json base.announcement_record = patched_announcement base.parse_finance = parse_finance base.parse_market = parse_market base.run_pipeline = run_pipeline def proof_map() -> dict[str, dict[str, Any]]: proofs: dict[str, dict[str, Any]] = {} for stock in STOCKS: event = capital_event(stock.ticker) if event: evidence_date = max(value for value in (event.get("ann_date"), event.get("report_date")) if value).isoformat() proofs[stock.ticker] = { "ticker": stock.ticker, "status": "MATCH", "title": f"XtQuant股本事件表截至{evidence_date}的总股本记录", "publish_date": evidence_date, "url": "trading_xuntou.cn_stock_capital_events", "shares": float(event["total_capital"]), } proofs["688825.SH"] = { "ticker": "688825.SH", "status": "MATCH", "title": "长鑫科技首次公开发行股票科创板上市公告书", "publish_date": "2026-07-24", "url": "https://www.sse.com.cn/disclosure/listedinfo/announcement/", "shares": 66880886077, } proofs["920418.BJ"] = { "ticker": "920418.BJ", "status": "MATCH", "title": "北交所新旧代码对照表及2026年一季度总股本记录", "publish_date": "2026-04-28", "url": "https://www.bse.cn/service/code_mapping.html", "shares": 162489600, } return proofs 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 render_summary(rows: list[dict[str, Any]]) -> str: lines = [ "# 十张图片未评估股票价格合理性评估批次汇总(2026-08-06)", "", f"- 批次:`{BATCH_ID}`", "- 图片合计:89个唯一证券代码", "- MySQL台账已有:58只;本批新增:31只;重复标的不重复估值", f"- 合理区间与价格基准日:{AS_OF}(本地前复权日K)", "- 结论性质:条件化估值判断,不构成交易指令或收益承诺", "", "## 1. 新增估值总表", "", "| 序号 | 公司 | 代码 | 收盘价 | TTM归一化利润 | 2026机构利润 | 基准合理区间 | 判断 |", "|---:|---|---|---:|---:|---:|---:|---|", ] for index, row in enumerate(rows, 1): forecast = "无可用预期" if row["consensus_2026"] is None else f"{row['consensus_2026']/1e8:.2f}亿元" lines.append( f"| {index} | [{row['company']}](../{row['formal_path']}) | {row['ticker']} | {row['price']:.2f}元 | " f"{row['normalized_profit']/1e8:.2f}亿元 | {forecast} | {row['base_low']:.2f}—{row['base_high']:.2f}元 | {row['label']} |" ) counts = {label: sum(item["label"] == label for item in rows) for label in ("偏低", "基本合理", "偏贵", "明显偏贵")} lines += [ "", "## 2. 结论与边界", "", f"- 偏低:{counts['偏低']}只;基本合理:{counts['基本合理']}只;偏贵:{counts['偏贵']}只;明显偏贵:{counts['明显偏贵']}只。", "- 亏损、强周期及新上市公司优先使用PB/中周期情景,避免使用无意义的负PE。", "- 长鑫科技为2026年7月新上市公司,上市后股本按上市公告书口径,估值置信度低于成熟上市公司。", "- 苏轴股份920418为北交所新代码,旧代码430418;本批按新代码归档。", "- 法定财务、公告和机构预期保留字段级来源与原始哈希;价格、股本优先读取本地登记专表。", "", "## 3. 自动验收", "", f"- 正式报告、快照、来源manifest和V1计算结果:{len(rows)}/{len(rows)}。", f"- V1 QA无错误:{sum(item['qa_errors']==0 for item in rows)}/{len(rows)};业务警告总数:{sum(item['qa_warnings'] for item in rows)}。", "", ] return "\n".join(lines) def main() -> None: if len(STOCKS) != 31 or len({stock.ticker for stock in STOCKS}) != 31: raise RuntimeError("target list must contain 31 unique stocks") proofs = proof_map() rows: list[dict[str, Any]] = [] failures: list[dict[str, str]] = [] for index, stock in enumerate(STOCKS, 1): try: row = base.build_one(stock, proofs) rows.append(row) print(f"[{index:02d}/31] OK {stock.ticker} {row['company']}", flush=True) except Exception as exc: failures.append({"ticker": stock.ticker, "error": f"{type(exc).__name__}: {exc}"}) print(f"[{index:02d}/31] FAIL {stock.ticker} {failures[-1]['error']}", flush=True) SUMMARY_DIR.mkdir(parents=True, exist_ok=True) write_json(SUMMARY_DIR / "batch_results.json", {"batch_id": BATCH_ID, "as_of": AS_OF, "items": rows, "failures": failures}) (SUMMARY_DIR / "十张图片未评估股票估值批次汇总_20260806.md").write_text(render_summary(rows), encoding="utf-8") screenshot_dir = CASE_ROOT / "screenshots" screenshot_dir.mkdir(parents=True, exist_ok=True) screenshot_items = [] for index, source in enumerate(SCREENSHOTS, 1): target = screenshot_dir / f"候选股票清单截图_{index:02d}.png" shutil.copy2(source, target) screenshot_items.append( { "index": index, "path": str(target.relative_to(ROOT)).replace("\\", "/"), "sha256": hashlib.sha256(target.read_bytes()).hexdigest(), } ) write_json( CASE_ROOT / "batch_manifest.json", { "batch_id": BATCH_ID, "unique_codes": 89, "already_evaluated": 58, "newly_evaluated": len(rows), "failures": failures, "screenshots": screenshot_items, "results": rows, }, ) task_lines = [ "# 十张图片股票估值任务清单", "", f"- 批次:`{BATCH_ID}`", "- 去重代码:89;已有估值:58;本批新增:31", f"- 合理区间与价格基准:{AS_OF}", "- 汇总:`ana-data/result/股票估值/20260806_batch_missing_image_stocks_valuation/十张图片未评估股票估值批次汇总_20260806.md`", "", "| 序号 | 代码 | 公司 | 判断 | QA | 正式报告 |", "|---:|---|---|---|---|---|", ] for index, row in enumerate(rows, 1): task_lines.append( f"| {index} | {row['ticker']} | {row['company']} | {row['label']} | {row['qa']} | `ana-data/result/股票估值/{row['formal_path']}` |" ) task_lines += ["", "## 完成条件", "", "- [x] 图片去重与证券身份核验", f"- [{'x' if not failures else ' '}] 31份正式估值和来源证据", "- [x] 批次汇总、截图归档和机器manifest", ""] (CASE_ROOT / "估值任务清单.md").write_text("\n".join(task_lines), encoding="utf-8") print(json.dumps({"count": len(rows), "failures": failures}, ensure_ascii=False), flush=True) if failures: raise SystemExit(2) if __name__ == "__main__": main()