#!/usr/bin/env python3 """Fetch V2 evidence packages for uncovered industry-research securities. The target list is derived at runtime from the four governed research pools and the MySQL valuation ledger. Fetches run concurrently, while each V2 process retains its own 90-second monotonic deadline and writes only to the analyst's temporary workspace. """ from __future__ import annotations import csv import json import os import re import subprocess import sys from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path import pymysql ROOT = Path(__file__).resolve().parents[2] AS_OF = "2026-08-13" OUT = ROOT / "ai-valuation-analyst" / "tmp" / "valuation_industry_research_gaps_20260814" CACHE = ROOT / "ai-valuation-analyst" / "tmp" / "v2_cache" def mysql_connection(database: str) -> pymysql.Connection: return pymysql.connect( host=os.environ.get("MYSQL_HOST", "127.0.0.1"), port=int(os.environ.get("MYSQL_PORT", "3306")), user=os.environ.get("MYSQL_USER", "root"), password=os.environ["MYSQL_PASSWORD"], database=database, charset="utf8mb4", autocommit=True, cursorclass=pymysql.cursors.DictCursor, ) def semiconductor_tickers() -> set[str]: path = ROOT / "ana-data/cases/半导体案例/ANA-SEMI-20260722-001/outputs/核心文档/国内企业信息表.csv" market_map = {"SSE": "SH", "SSE STAR": "SH", "SZSE": "SZ", "SZSE CHINEXT": "SZ", "BSE": "BJ"} rows = csv.DictReader(path.open(encoding="utf-8-sig", newline="")) result = set() for row in rows: code = row["ticker"].strip() suffix = market_map.get(row["listed_market"].strip()) if suffix and re.fullmatch(r"\d{6}", code): result.add(f"{code}.{suffix}") # The North Exchange completed its 920-series code migration after the # research snapshot. The local formal market contract uses the new code. result.discard("835179.BJ") result.add("920179.BJ") return result def robot_tickers() -> set[str]: path = ROOT / "ana-data/cases/机器人案例/ANA-ROBOT-INDUSTRY-001/evidence/next_robot_036_market_database_company_universe_authority_20260726.csv" return {row["symbol"].strip() for row in csv.DictReader(path.open(encoding="utf-8-sig", newline=""))} def newenergy_tickers() -> set[str]: path = ROOT / "ana-data/cases/新能源案例/核心文档/全量公司横向总表.md" result = set() for code in re.findall(r"\| (\d{6}) \| \[", path.read_text(encoding="utf-8")): suffix = "SH" if code.startswith("6") else ("BJ" if code.startswith(("8", "9")) else "SZ") result.add(f"{code}.{suffix}") return result def missing_tickers() -> list[str]: universe = semiconductor_tickers() | robot_tickers() | newenergy_tickers() with mysql_connection("stock_valuation") as connection, connection.cursor() as cursor: cursor.execute("SELECT ticker FROM security") covered = {row["ticker"] for row in cursor.fetchall()} return sorted(universe - covered) def fetch(ticker: str) -> dict[str, object]: target = OUT / f"v2_{ticker.replace('.', '_')}" env = os.environ.copy() env["PYTHONPATH"] = str(ROOT / "dev/project-dev") proc = subprocess.run( [ sys.executable, "-m", "stock_valuation_pipeline_v2", "--ticker", ticker, "--as-of", AS_OF, "--cache-dir", str(CACHE), "--output-dir", str(target), ], cwd=ROOT, env=env, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=120, ) return { "ticker": ticker, "returncode": proc.returncode, "stdout": proc.stdout.strip().splitlines()[-1] if proc.stdout.strip() else "", "stderr": proc.stderr.strip()[-500:], } def latest_package_exists(ticker: str) -> bool: return any(OUT.glob(f"v2_{ticker.replace('.', '_')}.failed-*")) or (OUT / f"v2_{ticker.replace('.', '_')}").exists() def main() -> None: if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8", errors="replace") tickers = missing_tickers() if "--missing-packages-only" in sys.argv: tickers = [ticker for ticker in tickers if not latest_package_exists(ticker)] OUT.mkdir(parents=True, exist_ok=True) rows: list[dict[str, object]] = [] with ThreadPoolExecutor(max_workers=12) as pool: pending = {pool.submit(fetch, ticker): ticker for ticker in tickers} for future in as_completed(pending): ticker = pending[future] try: row = future.result() except Exception as exc: # retain every batch failure row = {"ticker": ticker, "returncode": 99, "error": repr(exc)} rows.append(row) print(f"[{len(rows):03d}/{len(tickers):03d}] {ticker} rc={row.get('returncode')}", flush=True) rows.sort(key=lambda item: str(item["ticker"])) manifest = {"as_of": AS_OF, "count": len(rows), "items": rows} (OUT / "fetch_manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"count": len(rows), "returncodes": {str(code): sum(row.get("returncode") == code for row in rows) for code in sorted({int(row.get("returncode", 99)) for row in rows})}}, ensure_ascii=False)) if __name__ == "__main__": main()