#!/usr/bin/env python3
|
"""Generate the 2026-08-13 formal valuations for Deye and Sieyuan.
|
|
This is a task-specific analysis builder. It reuses the frozen V1 arithmetic
|
core, the V2 evidence package and the local front-adjusted market contract; it
|
does not duplicate V1 valuation formulas or write external databases.
|
"""
|
|
from __future__ import annotations
|
|
import hashlib
|
import json
|
import os
|
import statistics
|
from dataclasses import dataclass
|
from datetime import date, timedelta
|
from pathlib import Path
|
from typing import Any
|
|
import pymysql
|
|
from stock_valuation_pipeline.core import run_pipeline
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
AS_OF = "2026-08-13"
|
AS_OF_STAMP = "20260813"
|
RESULT_ROOT = ROOT / "ana-data" / "result" / "股票估值"
|
CASE_ROOT = ROOT / "ana-data" / "cases" / "股票估值" / "ANA-STOCK-VALUATION-DEYE-SIEYUAN-20260813-001"
|
V2_ROOT = ROOT / "ai-valuation-analyst" / "tmp" / "valuation_20260813_deye_sieyuan_v2"
|
CACHE_ROOT = ROOT / "ai-valuation-analyst" / "tmp" / "v2_cache" / "blobs" / "sha256"
|
REGISTRY = ROOT / "dev" / "ana-dev" / "stock_valuation_pipeline" / "source_registry.json"
|
|
|
@dataclass(frozen=True)
|
class Company:
|
ticker: str
|
slug: str
|
company: str
|
market: str
|
report_shares: float
|
current_shares: float
|
share_date: str
|
share_title: str
|
share_url: str
|
share_publish_date: str
|
business: str
|
profit_sources: list[str]
|
quality: list[str]
|
risks: list[str]
|
upgrade_triggers: list[str]
|
downgrade_triggers: list[str]
|
base_profit: tuple[float, float]
|
base_multiple: tuple[float, float]
|
pessimistic_profit: tuple[float, float]
|
pessimistic_multiple: tuple[float, float]
|
optimistic_profit: tuple[float, float]
|
optimistic_multiple: tuple[float, float]
|
latest_info_date: str
|
latest_info_title: str
|
latest_info_url: str
|
latest_info_note: str
|
|
|
COMPANIES = (
|
Company(
|
ticker="605117.SH",
|
slug="deye",
|
company="德业股份",
|
market="上海证券交易所",
|
report_shares=909_332_542.0,
|
current_shares=1_273_231_036.0,
|
share_date="2026-07-02",
|
share_title="关于2022年股票期权激励计划2026年第二季度自主行权结果暨股份变动的公告",
|
share_url="https://static.sse.com.cn/disclosure/listedinfo/announcement/c/new/2026-07-02/605117_20260702_MIX1.pdf",
|
share_publish_date="2026-07-02",
|
business="户用及工商业储能逆变器、储能电池包与环境电器",
|
profit_sources=[
|
"2025年储能逆变器收入52.17亿元、毛利率51.10%,是毛利贡献的第一来源;储能电池包收入38.32亿元、毛利率31.81%,是增速最快的第二增长极。",
|
"光伏逆变器收入10.54亿元,环境电器和热交换器合计约20.05亿元;2025年外销收入97.47亿元,占主营收入约80.0%,利润高度依赖海外储能需求和渠道。",
|
"2026年上半年业绩预告归母净利润26.68至27.28亿元、扣非24.93至25.53亿元,增长主要来自欧洲、中东、东南亚户储及工商业储能销售放量。",
|
],
|
quality=[
|
"2025年经营现金流39.86亿元,高于归母净利润31.71亿元;TTM简化自由现金流约40.60亿元,现金转化较好。",
|
"2025年储能电池包毛利率同比下降9.49个百分点,且逆变器、电池包期末库存分别同比增加52.55%和49.18%,高增长同时带来库存与价格压力。",
|
"2025年合同负债4.80亿元、同比增长81.43%;2026Q1升至6.05亿元,预收增长对后续收入有一定支撑。",
|
],
|
risks=[
|
"外销约占主营收入80%,欧盟、美国及其他市场的关税、贸易限制、认证和补贴规则变化会直接影响订单和利润率。",
|
"储能景气和能源价格具有周期性;机构2026年利润预期较2025年大幅增长,若下半年需求或毛利率回落,年度利润容易低于一致预期。",
|
"储能电池包毛利率已显著下降,锂电池、IGBT、铜铝及其他元器件价格波动会侵蚀利润;库存高增长增加跌价与渠道去库存风险。",
|
"汇率、海外应收与渠道回款、股权激励持续行权会影响归母利润和每股收益。",
|
],
|
upgrade_triggers=[
|
"2026年正式半年报达到预告中枢且经营现金流、库存周转同步改善。",
|
"欧洲、中东、东南亚之外的新市场持续放量,储能电池包毛利率止跌回升。",
|
],
|
downgrade_triggers=[
|
"2026年归母净利润低于60亿元或扣非利润与现金流明显背离。",
|
"贸易限制、补贴退坡、渠道去库存或产品降价使储能业务毛利率继续显著下滑。",
|
],
|
base_profit=(6.0e9, 7.4e9),
|
base_multiple=(18.0, 24.0),
|
pessimistic_profit=(4.5e9, 5.5e9),
|
pessimistic_multiple=(12.0, 17.0),
|
optimistic_profit=(7.8e9, 9.0e9),
|
optimistic_multiple=(24.0, 30.0),
|
latest_info_date="2026-07-09",
|
latest_info_title="2026年半年度业绩预增公告",
|
latest_info_url="https://static.sse.com.cn/disclosure/listedinfo/announcement/c/new/2026-07-09/605117_20260709_IHEV.pdf",
|
latest_info_note="预告口径:归母26.68至27.28亿元,扣非24.93至25.53亿元。",
|
),
|
Company(
|
ticker="002028.SZ",
|
slug="sieyuan_electric",
|
company="思源电气",
|
market="深圳证券交易所",
|
report_shares=782_451_182.0,
|
current_shares=782_573_282.0,
|
share_date="2026-06-30",
|
share_title="2025年度权益分派实施公告(现有总股本证明)",
|
share_url="https://disc.static.szse.cn/download/disc/disk03/finalpage/2026-07-01/d515dfa9-944e-45e0-adce-b7b4d6f678e0.PDF",
|
share_publish_date="2026-07-01",
|
business="输配电设备、保护自动化、电力电子、海外EPC与储能系统",
|
profit_sources=[
|
"2025年开关类收入89.35亿元、变压器类46.33亿元、保护及自动化25.42亿元,是利润的主体;三类毛利率分别为36.88%、36.39%和35.11%。",
|
"电力电子、EPC和储能系统及元件收入分别15.25亿元、23.32亿元和15.31亿元;后两类增长快但毛利率仅约11.8%,对收入增长的贡献高于对利润的贡献。",
|
"2025年海外收入58.03亿元、同比增长85.84%,占收入26.94%;新增订单288.91亿元、同比增长34.64%,订单与海外扩张是未来利润的主要驱动。",
|
],
|
quality=[
|
"2025年经营现金流22.34亿元、低于归母净利润31.50亿元;TTM经营现金流23.11亿元,简化自由现金流15.41亿元,现金流为正但转化率不算高。",
|
"2025年末应收账款81.83亿元,占总资产27.42%;合同负债29.77亿元,2026Q1升至31.50亿元。较高预收支撑订单,但高应收和海外EPC拉长回款周期。",
|
"2026年半年业绩快报归母14.87亿元、同比增长15.03%,明显慢于收入27.14%;汇兑损失及思源东芝持股比例变更压低归母利润增速。",
|
],
|
risks=[
|
"2026H1归母利润仅增长15.03%,低于当前一致预期隐含的全年约37%增速,下半年需要明显加速才能兑现43亿元左右的机构预测。",
|
"应收账款规模高、海外EPC周期1至3年,存在回款、汇率、税务、施工、合同与分包风险。",
|
"铜铝等大宗原料、半导体器件和国际物流价格上涨,以及低毛利EPC/储能占比提高,可能压制整体利润率。",
|
"海外业务扩张带来汇兑与当地运营风险;2026H1已出现美元应收等外币资产的较大汇兑损失。",
|
],
|
upgrade_triggers=[
|
"新增订单持续接近公司2026年375亿元经营目标,合同负债与现金回款同步增长。",
|
"下半年归母利润增速回升至25%以上,开关、变压器等高毛利业务占比稳定,汇兑损失收窄。",
|
],
|
downgrade_triggers=[
|
"2026年归母净利润低于38亿元,或收入增长继续快于利润而毛利率显著下行。",
|
"应收账款和海外EPC风险上升、经营现金流转弱,或思源东芝少数股权变化持续稀释归母利润。",
|
],
|
base_profit=(3.8e9, 4.5e9),
|
base_multiple=(26.0, 32.0),
|
pessimistic_profit=(3.3e9, 3.8e9),
|
pessimistic_multiple=(20.0, 25.0),
|
optimistic_profit=(4.6e9, 5.2e9),
|
optimistic_multiple=(32.0, 38.0),
|
latest_info_date="2026-07-11",
|
latest_info_title="2026年半年度业绩快报",
|
latest_info_url="https://file.finance.sina.com.cn/211.154.219.97%3A9494/MRGG/CNSESZ_STOCK/2026/2026-7/2026-07-11/12441232.PDF",
|
latest_info_note="快报口径:收入108.03亿元、归母14.87亿元、扣非14.19亿元、经营现金流2.25亿元。",
|
),
|
)
|
|
|
def load_json(path: Path) -> dict[str, Any]:
|
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
|
def failed_dir(ticker: str) -> Path:
|
matches = sorted(V2_ROOT.glob(f"{ticker.replace('.', '_')}.failed-*"))
|
if len(matches) != 1:
|
raise RuntimeError(f"Expected one V2 failure package for {ticker}, got {len(matches)}")
|
return matches[0]
|
|
|
def blob(raw_hash: str) -> bytes:
|
path = CACHE_ROOT / raw_hash[:2] / f"{raw_hash}.bin"
|
data = path.read_bytes()
|
if hashlib.sha256(data).hexdigest() != raw_hash:
|
raise RuntimeError(f"Cache hash mismatch: {raw_hash}")
|
return data
|
|
|
def consensus(provider: dict[str, Any], shares: float) -> tuple[int, dict[str, dict[str, float]], dict[str, float]]:
|
summary_row: dict[str, Any] | None = None
|
summary_hash = ""
|
for raw_hash in provider["forecast"]["raw_artifact_hashes"]:
|
try:
|
value = json.loads(blob(raw_hash).decode("utf-8"))
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
continue
|
rows = ((value.get("result") or {}).get("data") or [])
|
if rows and "RATING_ORG_NUM" in rows[0]:
|
summary_row = rows[0]
|
summary_hash = raw_hash
|
break
|
if not summary_row:
|
raise RuntimeError("No consensus summary")
|
years: dict[str, dict[str, float]] = {}
|
for idx in range(1, 5):
|
year = summary_row.get(f"YEAR{idx}")
|
eps = summary_row.get(f"EPS{idx}")
|
if year and eps is not None and str(summary_row.get(f"YEAR_MARK{idx}")) == "E":
|
years[str(year)] = {"eps": float(eps), "profit": float(eps) * shares}
|
targets = {
|
"min": float(summary_row["DEC_AIMPRICEMIN"]) if summary_row.get("DEC_AIMPRICEMIN") is not None else 0.0,
|
"max": float(summary_row["DEC_AIMPRICEMAX"]) if summary_row.get("DEC_AIMPRICEMAX") is not None else 0.0,
|
"raw_hash": summary_hash,
|
}
|
return int(summary_row.get("RATING_ORG_NUM") or 0), years, targets
|
|
|
def mysql_connection() -> 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="trading_xuntou",
|
charset="utf8mb4",
|
autocommit=True,
|
cursorclass=pymysql.cursors.DictCursor,
|
)
|
|
|
def local_market(ticker: str) -> dict[str, Any]:
|
start = date.fromisoformat(AS_OF) - timedelta(days=150)
|
with mysql_connection() as connection, connection.cursor() as cursor:
|
cursor.execute(
|
"SELECT trade_date,close,amount,source,source_batch_id,updated_at "
|
"FROM cn_stock_kline_1d_front WHERE symbol=%s AND trade_date BETWEEN %s AND %s "
|
"AND close>0 AND source='xtquant' ORDER BY trade_date",
|
(ticker, start, AS_OF),
|
)
|
rows = cursor.fetchall()
|
if not rows or str(rows[-1]["trade_date"]) != AS_OF:
|
raise RuntimeError(f"No complete {AS_OF} front-adjusted row for {ticker}")
|
closes = [float(row["close"]) for row in rows]
|
amounts = [float(row["amount"]) for row in rows]
|
return {
|
"price": closes[-1],
|
"date": AS_OF,
|
"amount": amounts[-1],
|
"source": rows[-1]["source"],
|
"source_batch_id": rows[-1]["source_batch_id"],
|
"updated_at": str(rows[-1]["updated_at"]),
|
"return_20d": closes[-1] / closes[-21] - 1,
|
"return_60d": closes[-1] / closes[-61] - 1,
|
"ma60": statistics.fmean(closes[-60:]),
|
"distance_to_ma60": closes[-1] / statistics.fmean(closes[-60:]) - 1,
|
"amount_ratio_5d_to_60d": statistics.fmean(amounts[-5:]) / statistics.fmean(amounts[-60:]),
|
"sample_count": len(rows),
|
}
|
|
|
def provider_data(company: Company) -> tuple[dict[str, Any], dict[str, Any]]:
|
directory = failed_dir(company.ticker)
|
provider = load_json(directory / "provider_results.json")
|
failure = load_json(directory / "failure.json")
|
if provider["announcements"]["identity"]["ticker"] != company.ticker:
|
raise RuntimeError("Ticker mismatch")
|
return provider, failure
|
|
|
def announcement(provider: dict[str, Any], title: str) -> dict[str, Any]:
|
def normalized(value: str) -> str:
|
return value.replace("第一季度", "一季度").replace(" ", "")
|
|
rows = [row for row in provider["announcements"]["records"] if normalized(row["title"]) == normalized(title)]
|
if len(rows) != 1:
|
raise RuntimeError(f"Expected one announcement {title}, got {len(rows)}")
|
return rows[0]
|
|
|
def scenario(name: str, role: str, profit: tuple[float, float], multiple: tuple[float, float], assumption: str) -> dict[str, Any]:
|
return {
|
"name": name,
|
"role": role,
|
"method": "pe",
|
"profit_low": profit[0],
|
"profit_high": profit[1],
|
"multiple_low": multiple[0],
|
"multiple_high": multiple[1],
|
"assumption": assumption,
|
}
|
|
|
def build(company: Company) -> dict[str, Any]:
|
provider, failure = provider_data(company)
|
financials = provider["finance"]["financials"]
|
balance = dict(provider["finance"]["balance_sheet"])
|
market = local_market(company.ticker)
|
quote_obj = None
|
quote_hash = ""
|
for raw_hash in provider["market"]["raw_artifact_hashes"]:
|
try:
|
obj = json.loads(blob(raw_hash).decode("utf-8"))
|
except (UnicodeDecodeError, json.JSONDecodeError):
|
continue
|
if (obj.get("data") or {}).get("f84"):
|
quote_obj = obj["data"]
|
quote_hash = raw_hash
|
break
|
if not quote_obj or float(quote_obj["f84"]) != company.current_shares:
|
raise RuntimeError(f"Quote/share proof mismatch for {company.ticker}")
|
if abs(market["price"] * company.current_shares - float(quote_obj["f116"])) > 1.0:
|
raise RuntimeError(f"Market-cap cross-check failed for {company.ticker}")
|
count, forecasts, targets = consensus(provider, company.current_shares)
|
annual = announcement(provider, "2025年年度报告")
|
current = announcement(provider, "2026年第一季度报告")
|
|
annual_f = financials["annual"]
|
current_f = financials["current_cumulative"]
|
prior_f = financials["prior_year_same_period"]
|
ttm_attr = annual_f["attributable_profit"] + current_f["attributable_profit"] - prior_f["attributable_profit"]
|
ttm_deduct = annual_f["deduct_profit"] + current_f["deduct_profit"] - prior_f["deduct_profit"]
|
ttm_cfo = annual_f["cfo"] + current_f["cfo"] - prior_f["cfo"]
|
ttm_capex = annual_f["capex"] + current_f["capex"] - prior_f["capex"]
|
# V2 finance equity is total equity. V1 PB uses attributable equity.
|
parent_equity = float(balance["equity"]) - float(balance["minority_interest"])
|
balance["equity"] = parent_equity
|
|
scenarios = [
|
scenario("悲观", "pessimistic", company.pessimistic_profit, company.pessimistic_multiple, "需求、订单或毛利率低于预期,利润下修且估值回归行业低位。"),
|
scenario("基准", "base", company.base_profit, company.base_multiple, "结合2026年最新业绩预告/快报、机构一致预期、订单与现金流,给予与增长持续性和风险相匹配的PE区间。"),
|
scenario("乐观", "optimistic", company.optimistic_profit, company.optimistic_multiple, "订单、海外扩张、产品结构和利润率超预期,利润与估值中枢同步上移。"),
|
]
|
base_low = company.base_profit[0] * company.base_multiple[0] / company.current_shares
|
base_high = company.base_profit[1] * company.base_multiple[1] / company.current_shares
|
optimistic_high = company.optimistic_profit[1] * company.optimistic_multiple[1] / company.current_shares
|
if market["price"] < base_low:
|
label = "偏低"
|
elif market["price"] <= base_high:
|
label = "基本合理"
|
elif market["price"] <= optimistic_high:
|
label = "偏贵"
|
else:
|
label = "明显偏贵"
|
|
forecast_rows = [{
|
"institution": f"东方财富公开一致预期汇总(覆盖池{count}家)",
|
"report_date": AS_OF,
|
"include": True,
|
"core_assumption": "汇总快照不替代逐家研报;覆盖池数量不等于每个年度的有效样本数。",
|
"source_id": "SRC-EM-CONSENSUS-20260813",
|
"estimates": forecasts,
|
}]
|
sources = [
|
{
|
"id": "SRC-ANNUAL-2025", "source_type": "cninfo", "title": annual["title"],
|
"publish_date": annual["publish_date"], "period_end": "2025-12-31", "url": annual["url"],
|
"supports": ["financials.annual", "business", "profit_sources", "risks"], "revision_status": "current",
|
},
|
{
|
"id": "SRC-CURRENT-2026Q1", "source_type": "cninfo", "title": current["title"],
|
"publish_date": current["publish_date"], "period_end": "2026-03-31", "url": current["url"],
|
"supports": ["financials.current_cumulative", "financials.prior_year_same_period", "balance_sheet", "balance_sheet.equity"], "revision_status": "current",
|
},
|
{
|
"id": "SRC-LATEST-OPERATING", "source_type": "sse" if company.ticker.endswith(".SH") else "financial_mirror",
|
"title": company.latest_info_title, "publish_date": company.latest_info_date, "period_end": "2026-06-30",
|
"url": company.latest_info_url, "supports": ["latest_operating_information", "valuation.scenarios"], "revision_status": "current",
|
},
|
{
|
"id": "SRC-SHARES-A1", "source_type": "sse" if company.ticker.endswith(".SH") else "szse",
|
"title": company.share_title, "publish_date": company.share_publish_date, "period_end": company.share_date,
|
"url": company.share_url, "supports": ["market.diluted_shares", "market.shares_date"], "revision_status": "current",
|
},
|
{
|
"id": "SRC-LOCAL-FRONT-KLINE", "source_type": "quote_provider", "title": "trading_xuntou前复权日K完整收盘价",
|
"publish_date": AS_OF, "period_end": AS_OF, "url": "mysql://trading_xuntou/cn_stock_kline_1d_front",
|
"supports": ["market.price", "market.price_timestamp", "market.amount", "analysis.kline"], "revision_status": "current",
|
},
|
{
|
"id": "SRC-EM-QUOTE-SHARES", "source_type": "quote_provider", "title": "东方财富当前总股本与总市值快照交叉核验",
|
"publish_date": AS_OF, "period_end": AS_OF, "url": "https://push2delay.eastmoney.com/api/qt/stock/get",
|
"supports": ["market.diluted_shares", "market.platform_market_cap"], "revision_status": "current",
|
},
|
{
|
"id": "SRC-EM-CONSENSUS-20260813", "source_type": "institution_aggregator", "title": "东方财富盈利预测汇总",
|
"publish_date": AS_OF, "period_end": AS_OF, "url": "https://datacenter-web.eastmoney.com/api/data/v1/get?reportName=RPT_WEB_RESPREDICT",
|
"supports": ["institutions"], "revision_status": "current",
|
},
|
]
|
|
snapshot = {
|
"snapshot_id": f"SNAPSHOT-{company.ticker[:6]}-{AS_OF_STAMP}-V1",
|
"meta": {
|
"company": company.company, "code": company.ticker, "market": company.market,
|
"as_of_date": AS_OF, "currency": "CNY", "report_period_end": "2026-03-31",
|
"latest_operating_info_date": company.latest_info_date,
|
},
|
"market": {
|
"price": market["price"], "price_type": "最近完整交易日前复权收盘价",
|
"price_timestamp": f"{AS_OF}T15:00:00+08:00", "diluted_shares": company.current_shares,
|
"shares_date": company.share_date, "platform_market_cap": float(quote_obj["f116"]),
|
},
|
"financials": financials,
|
"balance_sheet": {
|
"cash_available": balance["cash_available"], "equity": balance["equity"],
|
"interest_bearing_debt": balance["interest_bearing_debt"], "minority_interest": balance["minority_interest"],
|
"non_operating_financial_assets": balance["non_operating_financial_assets"], "period_end": balance["period_end"],
|
},
|
"normalization": {
|
"adjustments": [{
|
"description": "以TTM扣非归母利润替代TTM归母利润作为核心盈利代理",
|
"amount": ttm_deduct - ttm_attr,
|
"basis": "2025年报加2026Q1减2025Q1;最新半年预告/快报仅用于情景利润,不混入TTM法定口径",
|
}]
|
},
|
"valuation": {
|
"scenarios": scenarios,
|
"reverse_pe_multiples": [12, 15, 18, 20, 22, 25, 28, 30, 32, 35, 40],
|
"exit_pe_multiples": [12, 15, 18, 20, 22, 25, 28, 30, 32, 35, 40],
|
"holding_period": {"years": 5, "required_return": 0.10, "cumulative_dividend_per_share": 0},
|
},
|
"institutions": {"coverage_status": "available", "forecasts": forecast_rows},
|
"sources": sources,
|
"analysis": {
|
"company_type": company.business,
|
"primary_model": "2026年前瞻PE情景;TTM扣非、PB、现金流、最新半年预告/快报与反向利润交叉验证",
|
"cross_checks": ["最新半年预告/快报", "机构一致预期", "PB/ROE", "经营现金流与资本开支", "订单/合同负债/应收", "前复权量价"],
|
"profit_sources": company.profit_sources,
|
"profit_source_quality": company.quality + [
|
f"法定TTM归母{ttm_attr/1e8:.2f}亿元、TTM扣非{ttm_deduct/1e8:.2f}亿元;主模型以最新半年信息和机构预期构造2026全年情景。",
|
f"TTM经营现金流{ttm_cfo/1e8:.2f}亿元、资本开支{ttm_capex/1e8:.2f}亿元、简化自由现金流{(ttm_cfo-ttm_capex)/1e8:.2f}亿元。",
|
],
|
"risks": company.risks + ["机构汇总口径不是法定事实,且覆盖池数量不等于每个年度的有效样本数。"],
|
"upgrade_triggers": company.upgrade_triggers,
|
"downgrade_triggers": company.downgrade_triggers,
|
"executive_conclusion": f"{market['price']:.2f}元位于基准合理区间{base_low:.2f}至{base_high:.2f}元内,判断为{label}。当前结论依赖2026年利润兑现,不构成目标价或交易指令。",
|
"consensus_org_count": count,
|
"consensus_coverage_note": f"东方财富公开汇总覆盖池{count}家;2026E EPS {forecasts['2026']['eps']:.4f}元,对应利润{forecasts['2026']['profit']/1e8:.2f}亿元。",
|
"latest_operating_info_note": company.latest_info_note,
|
"consensus_target_range": f"{targets['min']:.2f}至{targets['max']:.2f}元(仅作市场预期参照,不替代本报告合理区间)",
|
"kline_return_20d": market["return_20d"], "kline_return_60d": market["return_60d"],
|
"ma60": market["ma60"], "distance_to_ma60": market["distance_to_ma60"],
|
"amount_ratio_5d_to_60d": market["amount_ratio_5d_to_60d"], "market_sample_count": market["sample_count"],
|
"market_source_batch_id": market["source_batch_id"], "market_updated_at": market["updated_at"],
|
"ttm_cfo": ttm_cfo, "ttm_capex": ttm_capex, "ttm_fcf": ttm_cfo - ttm_capex,
|
"total_equity_original": provider["finance"]["balance_sheet"]["equity"],
|
"attributable_equity_used": parent_equity, "minority_interest_original": balance["minority_interest"],
|
"report_shares": company.report_shares, "share_proof_status": "MATCH_A1_CHANGE",
|
"information_cutoff": f"{AS_OF}T23:59:00+08:00",
|
},
|
}
|
|
out_dir = RESULT_ROOT / f"{AS_OF_STAMP}_{company.slug}_valuation"
|
out_dir.mkdir(parents=True, exist_ok=True)
|
snapshot_path = out_dir / f"{company.company}估值快照_{AS_OF_STAMP}.json"
|
snapshot_path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
calc_dir = out_dir / "calculation"
|
run = run_pipeline(snapshot_path, calc_dir, REGISTRY)
|
if run["status"] not in {"GENERATED", "REUSED"}:
|
raise RuntimeError(f"Unexpected V1 status {run}")
|
results = load_json(calc_dir / "valuation_results.json")
|
calc_report_path = calc_dir / "valuation_report.md"
|
calc_report_text = calc_report_path.read_text(encoding="utf-8")
|
calc_report_text = "\n".join(line.rstrip() for line in calc_report_text.splitlines()) + "\n"
|
calc_report_path.write_text(calc_report_text, encoding="utf-8")
|
report_text = calc_report_text
|
report_text = report_text.replace("价格合理性评估(流水线生成底稿)", "价格合理性评估", 1)
|
report_text = report_text.replace(
|
"本文是研究计算底稿,不构成交易指令或收益承诺。",
|
"本文为按《股票价格合理性评估操作手册》形成的条件化研究结论,不构成交易指令或收益承诺。",
|
1,
|
)
|
report_text = report_text.replace(
|
"覆盖状态:`available`。早于最新经营信息日、或低于已实现累计利润的预测自动排除出当前一致预期。",
|
f"覆盖状态:`available`。东方财富公开一致预期覆盖池为 **{count}家**;下表按一条汇总观测进入V1,因此“机构数”列显示1,不代表仅有1家机构。早于最新经营信息日、或低于已实现累计利润的预测自动排除出当前一致预期。",
|
1,
|
)
|
market_note = (
|
"\n\n### 1.1 前复权量价显影\n\n"
|
f"- 20日涨跌:{market['return_20d']*100:.2f}%;60日涨跌:{market['return_60d']*100:.2f}%。\n"
|
f"- 60日均线:{market['ma60']:.2f}元;当前价相对MA60:{market['distance_to_ma60']*100:.2f}%。\n"
|
f"- 近5日/60日平均成交额比:{market['amount_ratio_5d_to_60d']:.2f}。量价用于显影情绪,不改变合理价值。\n"
|
)
|
report_text = report_text.replace("\n## 2. 信息来源与固定优先级", market_note + "\n## 2. 信息来源与固定优先级", 1)
|
report_text = report_text.replace(
|
"\n## 6. 利润质量、现金流与资产负债",
|
f"\n### 5.1 最新半年经营信息\n\n- {company.latest_info_note}\n- 该信息用于调整2026全年情景利润,不与法定TTM公式重复相加。\n\n## 6. 利润质量、现金流与资产负债",
|
1,
|
)
|
bubble_text = (
|
"\n## 11.1 估值泡沫判定\n\n"
|
f"- 当前价{market['price']:.2f}元未高于基准合理区间上沿{base_high:.2f}元,按手册规则**未识别估值泡沫**。\n"
|
f"- 当前价相对基准上沿为{(market['price']/base_high-1)*100:.2f}%,相对乐观上沿为{(market['price']/optimistic_high-1)*100:.2f}%。\n"
|
"- 这不等于没有经营风险;若利润情景下修,合理区间会同步下移,需要按下调触发器复评。\n"
|
)
|
report_text = report_text.replace("\n## 12. 风险、上调与下调触发器", bubble_text + "\n## 12. 风险、上调与下调触发器", 1)
|
# V1 uses Markdown hard-break spaces in its header; formal project files keep
|
# clean line endings and do not rely on trailing whitespace for rendering.
|
report_text = "\n".join(line.rstrip() for line in report_text.splitlines()) + "\n"
|
formal_path = out_dir / f"{company.company}价格合理性评估_{AS_OF_STAMP}.md"
|
formal_path.write_text(report_text, encoding="utf-8")
|
|
evidence = {
|
"case_id": "ANA-STOCK-VALUATION-DEYE-SIEYUAN-20260813-001",
|
"ticker": company.ticker, "company": company.company, "as_of": AS_OF,
|
"v2_status": "BLOCKED", "v2_failure": failure,
|
"v2_failure_package": str(failed_dir(company.ticker).relative_to(ROOT)).replace("\\", "/"),
|
"v2_raw_artifact_hashes": sorted({h for section in provider.values() for h in section.get("raw_artifact_hashes", [])}),
|
"quote_raw_hash": quote_hash, "consensus_raw_hash": targets["raw_hash"],
|
"market_contract": {
|
"table": "trading_xuntou.cn_stock_kline_1d_front", "source": market["source"],
|
"source_batch_id": market["source_batch_id"], "updated_at": market["updated_at"], "trade_date": AS_OF,
|
},
|
"share_proof": {
|
"current_shares": company.current_shares, "a1_title": company.share_title,
|
"a1_publish_date": company.share_publish_date, "a1_url": company.share_url,
|
"quote_shares": quote_obj["f84"], "quote_market_cap": quote_obj["f116"],
|
"price_times_shares": market["price"] * company.current_shares,
|
},
|
"bounded_gaps": [
|
"V2 quote f124=0,故端到端V2按历史股本时间戳门禁诚实阻断;正式估值由后续A1股本公告和估值日总股本/总市值快照补证。",
|
"机构汇总覆盖池不是逐家研报原文;本报告披露机构数、EPS和推算利润,不将其写成法定事实。",
|
"最新半年预告/快报提供利润锚,但资产负债表、现金流明细仍以2026Q1法定报告为最新完整结构口径。",
|
],
|
"checks": {
|
"v1_qa": results["qa"], "label": label, "base_low": base_low, "base_high": base_high,
|
"optimistic_high": optimistic_high, "price": market["price"],
|
},
|
}
|
(out_dir / "source_evidence_manifest.json").write_text(json.dumps(evidence, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
return {
|
"ticker": company.ticker, "company": company.company, "price": market["price"],
|
"base_low": base_low, "base_high": base_high, "optimistic_high": optimistic_high,
|
"label": label, "normalized_profit": float(results["metrics"]["normalized_profit"]),
|
"normalized_pe": float(results["metrics"]["normalized_pe"]), "pb": float(results["metrics"]["pb"]),
|
"consensus_count": count, "consensus_2026": forecasts["2026"]["profit"],
|
"forward_pe": market["price"] / forecasts["2026"]["eps"],
|
"market": market, "report_path": str(formal_path.relative_to(ROOT)).replace("\\", "/"),
|
"snapshot_path": str(snapshot_path.relative_to(ROOT)).replace("\\", "/"),
|
"qa": results["qa"],
|
}
|
|
|
def render_summary(rows: list[dict[str, Any]]) -> str:
|
lines = [
|
"# 德业股份与思源电气价格合理性评估汇总(2026-08-13)", "",
|
"- 价格基准:2026-08-13完整交易日前复权收盘价。",
|
"- 信息上限:2026-08-13;最新经营信息分别为德业股份2026H1业绩预告、思源电气2026H1业绩快报。",
|
"- 估值区间为条件化研究结论,不是目标价、交易指令或收益承诺。", "",
|
"| 公司 | 收盘价 | TTM扣非利润 | TTM扣非PE | 2026机构利润 | 前瞻PE | 基准合理区间 | 乐观上沿 | 判定 |",
|
"|---|---:|---:|---:|---:|---:|---:|---:|---|",
|
]
|
for row in rows:
|
lines.append(
|
f"| [{row['company']}](../../../../{row['report_path']}) | {row['price']:.2f}元 | "
|
f"{row['normalized_profit']/1e8:.2f}亿元 | {row['normalized_pe']:.2f}倍 | "
|
f"{row['consensus_2026']/1e8:.2f}亿元({row['consensus_count']}家覆盖池) | {row['forward_pe']:.2f}倍 | "
|
f"{row['base_low']:.2f}—{row['base_high']:.2f}元 | {row['optimistic_high']:.2f}元 | {row['label']} |"
|
)
|
lines += [
|
"", "## 核心判断", "",
|
"- 德业股份:当前价低于基准中枢,2026H1预告强、现金流好,估值看起来更便宜;但外销占比约80%,储能景气、贸易政策、汇率和电池包毛利率是主要折价来源。",
|
"- 思源电气:当前价位于基准区间中上部,订单和海外扩张具备支撑;但2026H1利润增速明显慢于收入,应收与海外EPC风险使其不宜直接套用机构目标价。",
|
"- 两家公司当前价均未高于各自基准合理区间上沿,因此按手册规则均未识别估值泡沫;若正式半年报或全年利润低于情景下限,必须下修合理区间。", "",
|
"## 量价显影", "",
|
"| 公司 | 20日涨跌 | 60日涨跌 | MA60 | 相对MA60 | 近5日/60日成交额比 |",
|
"|---|---:|---:|---:|---:|---:|",
|
]
|
for row in rows:
|
market = row["market"]
|
lines.append(
|
f"| {row['company']} | {market['return_20d']*100:.2f}% | {market['return_60d']*100:.2f}% | "
|
f"{market['ma60']:.2f}元 | {market['distance_to_ma60']*100:.2f}% | {market['amount_ratio_5d_to_60d']:.2f} |"
|
)
|
lines += ["", "量价只用于识别市场情绪和拥挤度,不反推企业价值。", ""]
|
return "\n".join(lines)
|
|
|
def main() -> None:
|
rows = [build(company) for company in COMPANIES]
|
CASE_ROOT.mkdir(parents=True, exist_ok=True)
|
(CASE_ROOT / "评估任务清单.md").write_text(
|
"# 德业股份与思源电气估值任务清单\n\n"
|
"- 案例:`ANA-STOCK-VALUATION-DEYE-SIEYUAN-20260813-001`\n"
|
"- 方法:`ana-doc/股票估值/股票价格合理性评估操作手册.md`\n"
|
"- 基准日:2026-08-13\n\n"
|
"| 代码 | 公司 | 状态 | 正式报告 |\n|---|---|---|---|\n" +
|
"\n".join(f"| {row['ticker']} | {row['company']} | {row['qa']['status']} | `{row['report_path']}` |" for row in rows) + "\n",
|
encoding="utf-8",
|
)
|
summary_dir = RESULT_ROOT / f"{AS_OF_STAMP}_deye_sieyuan_batch_valuation"
|
summary_dir.mkdir(parents=True, exist_ok=True)
|
(summary_dir / f"德业股份与思源电气估值汇总_{AS_OF_STAMP}.md").write_text(render_summary(rows), encoding="utf-8")
|
(summary_dir / "batch_metrics.json").write_text(json.dumps({"as_of": AS_OF, "rows": rows}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
print(json.dumps({"status": "PASS", "rows": rows}, ensure_ascii=False, indent=2))
|
|
|
if __name__ == "__main__":
|
main()
|