from __future__ import annotations from pathlib import Path from math import pow from docx import Document from docx.enum.section import WD_SECTION from docx.enum.table import WD_ALIGN_VERTICAL, WD_CELL_VERTICAL_ALIGNMENT from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.style import WD_STYLE_TYPE from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Inches, Pt, RGBColor ROOT = Path(__file__).resolve().parents[1] OUTPUT = ROOT / "股票价格合理性评估操作手册_v1.0.docx" # compact_reference_guide preset + named Chinese readability override. PAGE_W_DXA = 12240 PAGE_H_DXA = 15840 CONTENT_W_DXA = 9360 TABLE_INDENT_DXA = 120 FONT_LATIN = "Calibri" FONT_EAST_ASIA = "Microsoft YaHei" BLUE = "2E74B5" DARK_BLUE = "1F4D78" INK = "0B2545" MUTED = "5F6B7A" LIGHT_BLUE = "E8EEF5" LIGHT_GRAY = "F2F4F7" CALLOUT = "F4F6F9" PALE_GOLD = "FFF6D8" GOLD = "7A5A00" PALE_RED = "FDECEC" RED = "9B1C1C" PALE_GREEN = "EAF4EC" GREEN = "245B36" WHITE = "FFFFFF" BLACK = "000000" def set_run_font(run, size=None, bold=None, italic=None, color=None, latin=FONT_LATIN, east_asia=FONT_EAST_ASIA): run.font.name = latin if run._element.rPr is None: run._element.get_or_add_rPr() rfonts = run._element.rPr.rFonts if rfonts is None: rfonts = OxmlElement("w:rFonts") run._element.rPr.insert(0, rfonts) rfonts.set(qn("w:ascii"), latin) rfonts.set(qn("w:hAnsi"), latin) rfonts.set(qn("w:eastAsia"), east_asia) if size is not None: run.font.size = Pt(size) if bold is not None: run.bold = bold if italic is not None: run.italic = italic if color is not None: run.font.color.rgb = RGBColor.from_string(color) def set_style_font(style, size, color=BLACK, bold=False): style.font.name = FONT_LATIN style.font.size = Pt(size) style.font.bold = bold style.font.color.rgb = RGBColor.from_string(color) rpr = style.element.get_or_add_rPr() rfonts = rpr.rFonts if rfonts is None: rfonts = OxmlElement("w:rFonts") rpr.insert(0, rfonts) rfonts.set(qn("w:ascii"), FONT_LATIN) rfonts.set(qn("w:hAnsi"), FONT_LATIN) rfonts.set(qn("w:eastAsia"), FONT_EAST_ASIA) def set_cell_shading(cell, fill): tc_pr = cell._tc.get_or_add_tcPr() shd = tc_pr.find(qn("w:shd")) if shd is None: shd = OxmlElement("w:shd") tc_pr.append(shd) shd.set(qn("w:fill"), fill) shd.set(qn("w:val"), "clear") def set_cell_margins(cell, top=80, start=120, bottom=80, end=120): tc_pr = cell._tc.get_or_add_tcPr() tc_mar = tc_pr.first_child_found_in("w:tcMar") if tc_mar is None: tc_mar = OxmlElement("w:tcMar") tc_pr.append(tc_mar) for tag, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)): node = tc_mar.find(qn(f"w:{tag}")) if node is None: node = OxmlElement(f"w:{tag}") tc_mar.append(node) node.set(qn("w:w"), str(value)) node.set(qn("w:type"), "dxa") def set_repeat_table_header(row): tr_pr = row._tr.get_or_add_trPr() tbl_header = tr_pr.find(qn("w:tblHeader")) if tbl_header is None: tbl_header = OxmlElement("w:tblHeader") tr_pr.append(tbl_header) tbl_header.set(qn("w:val"), "true") def set_table_geometry(table, widths_dxa, indent_dxa=TABLE_INDENT_DXA): assert sum(widths_dxa) == CONTENT_W_DXA, (widths_dxa, sum(widths_dxa)) table.autofit = False tbl_pr = table._tbl.tblPr tbl_w = tbl_pr.find(qn("w:tblW")) if tbl_w is None: tbl_w = OxmlElement("w:tblW") tbl_pr.append(tbl_w) tbl_w.set(qn("w:w"), str(CONTENT_W_DXA)) tbl_w.set(qn("w:type"), "dxa") tbl_ind = tbl_pr.find(qn("w:tblInd")) if tbl_ind is None: tbl_ind = OxmlElement("w:tblInd") tbl_pr.append(tbl_ind) tbl_ind.set(qn("w:w"), str(indent_dxa)) tbl_ind.set(qn("w:type"), "dxa") layout = tbl_pr.find(qn("w:tblLayout")) if layout is None: layout = OxmlElement("w:tblLayout") tbl_pr.append(layout) layout.set(qn("w:type"), "fixed") grid = table._tbl.tblGrid for child in list(grid): grid.remove(child) for width in widths_dxa: col = OxmlElement("w:gridCol") col.set(qn("w:w"), str(width)) grid.append(col) for row in table.rows: for idx, cell in enumerate(row.cells): tc_pr = cell._tc.get_or_add_tcPr() tc_w = tc_pr.find(qn("w:tcW")) if tc_w is None: tc_w = OxmlElement("w:tcW") tc_pr.append(tc_w) tc_w.set(qn("w:w"), str(widths_dxa[idx])) tc_w.set(qn("w:type"), "dxa") cell.width = Inches(widths_dxa[idx] / 1440) set_cell_margins(cell) cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER def set_cell_borders(cell, color="D9DEE5", size="4"): tc_pr = cell._tc.get_or_add_tcPr() borders = tc_pr.find(qn("w:tcBorders")) if borders is None: borders = OxmlElement("w:tcBorders") tc_pr.append(borders) for edge in ("top", "start", "bottom", "end", "insideH", "insideV"): node = borders.find(qn(f"w:{edge}")) if node is None: node = OxmlElement(f"w:{edge}") borders.append(node) node.set(qn("w:val"), "single") node.set(qn("w:sz"), size) node.set(qn("w:color"), color) def add_table(doc, headers, rows, widths_dxa, header_fill=LIGHT_BLUE, font_size=9.2, aligns=None, first_col_bold=False): table = doc.add_table(rows=1, cols=len(headers)) table.style = "Table Grid" set_table_geometry(table, widths_dxa) header = table.rows[0] set_repeat_table_header(header) for idx, text in enumerate(headers): cell = header.cells[idx] cell.text = str(text) set_cell_shading(cell, header_fill) set_cell_borders(cell) for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(0) p.paragraph_format.line_spacing = 1.12 p.alignment = aligns[idx] if aligns else WD_ALIGN_PARAGRAPH.CENTER for run in p.runs: set_run_font(run, size=font_size, bold=True, color=INK) for r_index, row_data in enumerate(rows): cells = table.add_row().cells for idx, value in enumerate(row_data): cells[idx].text = "" if value is None else str(value) if r_index % 2 == 1: set_cell_shading(cells[idx], "FAFBFC") set_cell_borders(cells[idx]) for p in cells[idx].paragraphs: p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(0) p.paragraph_format.line_spacing = 1.15 p.alignment = aligns[idx] if aligns else (WD_ALIGN_PARAGRAPH.LEFT if idx else WD_ALIGN_PARAGRAPH.CENTER) for run in p.runs: set_run_font(run, size=font_size, bold=(first_col_bold and idx == 0), color=BLACK) doc.add_paragraph().paragraph_format.space_after = Pt(0) return table def shade_paragraph(paragraph, fill=CALLOUT, border_color="D8E0E8"): p_pr = paragraph._p.get_or_add_pPr() shd = p_pr.find(qn("w:shd")) if shd is None: shd = OxmlElement("w:shd") p_pr.append(shd) shd.set(qn("w:fill"), fill) borders = p_pr.find(qn("w:pBdr")) if borders is None: borders = OxmlElement("w:pBdr") p_pr.append(borders) left = borders.find(qn("w:left")) if left is None: left = OxmlElement("w:left") borders.append(left) left.set(qn("w:val"), "single") left.set(qn("w:sz"), "18") left.set(qn("w:space"), "8") left.set(qn("w:color"), border_color) def add_callout(doc, label, text, kind="info"): colors = { "info": (CALLOUT, BLUE, INK), "caution": (PALE_GOLD, GOLD, GOLD), "risk": (PALE_RED, RED, RED), "positive": (PALE_GREEN, GREEN, GREEN), } fill, border, text_color = colors[kind] p = doc.add_paragraph() p.paragraph_format.left_indent = Inches(0.08) p.paragraph_format.right_indent = Inches(0.03) p.paragraph_format.space_before = Pt(4) p.paragraph_format.space_after = Pt(8) p.paragraph_format.line_spacing = 1.2 shade_paragraph(p, fill, border) r = p.add_run(f"{label} ") set_run_font(r, size=10.5, bold=True, color=text_color) r = p.add_run(text) set_run_font(r, size=10.5, color=BLACK) return p def add_formula(doc, *lines): p = doc.add_paragraph() p.style = doc.styles["Formula"] shade_paragraph(p, "F7F9FB", "AAB7C4") for idx, line in enumerate(lines): if idx: p.add_run("\n") r = p.add_run(line) set_run_font(r, size=10.2, bold=(idx == 0), color=INK, latin="Consolas", east_asia=FONT_EAST_ASIA) return p def add_body(doc, text, bold_lead=None): p = doc.add_paragraph() if bold_lead and text.startswith(bold_lead): r = p.add_run(bold_lead) set_run_font(r, size=11, bold=True, color=INK) r = p.add_run(text[len(bold_lead):]) set_run_font(r, size=11, color=BLACK) else: r = p.add_run(text) set_run_font(r, size=11, color=BLACK) return p def add_bullet_list(doc, items, level=0): num_id = create_numbering(doc, kind="bullet") for text in items: p = doc.add_paragraph() apply_num(p, num_id, level) r = p.add_run(text) set_run_font(r, size=11, color=BLACK) def add_numbered_list(doc, items): num_id = create_numbering(doc, kind="decimal") for text in items: p = doc.add_paragraph() apply_num(p, num_id, 0) r = p.add_run(text) set_run_font(r, size=11, color=BLACK) def create_numbering(doc, kind="bullet"): numbering = doc.part.numbering_part.element abstract_ids = [int(x.get(qn("w:abstractNumId"))) for x in numbering.findall(qn("w:abstractNum"))] num_ids = [int(x.get(qn("w:numId"))) for x in numbering.findall(qn("w:num"))] abstract_id = (max(abstract_ids) + 1) if abstract_ids else 1 num_id = (max(num_ids) + 1) if num_ids else 1 abstract = OxmlElement("w:abstractNum") abstract.set(qn("w:abstractNumId"), str(abstract_id)) nsid = OxmlElement("w:nsid") nsid.set(qn("w:val"), f"{abstract_id:08X}") abstract.append(nsid) multi = OxmlElement("w:multiLevelType") multi.set(qn("w:val"), "singleLevel") abstract.append(multi) lvl = OxmlElement("w:lvl") lvl.set(qn("w:ilvl"), "0") start = OxmlElement("w:start") start.set(qn("w:val"), "1") lvl.append(start) num_fmt = OxmlElement("w:numFmt") num_fmt.set(qn("w:val"), "bullet" if kind == "bullet" else "decimal") lvl.append(num_fmt) lvl_text = OxmlElement("w:lvlText") lvl_text.set(qn("w:val"), "•" if kind == "bullet" else "%1.") lvl.append(lvl_text) suff = OxmlElement("w:suff") suff.set(qn("w:val"), "tab") lvl.append(suff) p_pr = OxmlElement("w:pPr") tabs = OxmlElement("w:tabs") tab = OxmlElement("w:tab") tab.set(qn("w:val"), "num") tab.set(qn("w:pos"), "540") tabs.append(tab) p_pr.append(tabs) ind = OxmlElement("w:ind") ind.set(qn("w:left"), "540") ind.set(qn("w:hanging"), "270") p_pr.append(ind) spacing = OxmlElement("w:spacing") spacing.set(qn("w:after"), "80") spacing.set(qn("w:line"), "300") spacing.set(qn("w:lineRule"), "auto") p_pr.append(spacing) lvl.append(p_pr) if kind == "bullet": r_pr = OxmlElement("w:rPr") rfonts = OxmlElement("w:rFonts") rfonts.set(qn("w:ascii"), "Arial") rfonts.set(qn("w:hAnsi"), "Arial") r_pr.append(rfonts) lvl.append(r_pr) abstract.append(lvl) numbering.append(abstract) num = OxmlElement("w:num") num.set(qn("w:numId"), str(num_id)) abs_id = OxmlElement("w:abstractNumId") abs_id.set(qn("w:val"), str(abstract_id)) num.append(abs_id) numbering.append(num) return num_id def apply_num(paragraph, num_id, level): p_pr = paragraph._p.get_or_add_pPr() num_pr = p_pr.find(qn("w:numPr")) if num_pr is None: num_pr = OxmlElement("w:numPr") p_pr.append(num_pr) ilvl = OxmlElement("w:ilvl") ilvl.set(qn("w:val"), str(level)) num_id_el = OxmlElement("w:numId") num_id_el.set(qn("w:val"), str(num_id)) num_pr.append(ilvl) num_pr.append(num_id_el) def add_hyperlink(paragraph, text, url): part = paragraph.part r_id = part.relate_to(url, "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", is_external=True) hyperlink = OxmlElement("w:hyperlink") hyperlink.set(qn("r:id"), r_id) new_run = OxmlElement("w:r") r_pr = OxmlElement("w:rPr") r_style = OxmlElement("w:rStyle") r_style.set(qn("w:val"), "Hyperlink") r_pr.append(r_style) r_fonts = OxmlElement("w:rFonts") r_fonts.set(qn("w:ascii"), FONT_LATIN) r_fonts.set(qn("w:hAnsi"), FONT_LATIN) r_fonts.set(qn("w:eastAsia"), FONT_EAST_ASIA) r_pr.append(r_fonts) new_run.append(r_pr) t = OxmlElement("w:t") t.text = text new_run.append(t) hyperlink.append(new_run) paragraph._p.append(hyperlink) return hyperlink def add_page_field(paragraph): r = paragraph.add_run("第 ") set_run_font(r, size=9, color=MUTED) run = paragraph.add_run() fld_char1 = OxmlElement("w:fldChar") fld_char1.set(qn("w:fldCharType"), "begin") instr_text = OxmlElement("w:instrText") instr_text.set(qn("xml:space"), "preserve") instr_text.text = " PAGE " fld_char2 = OxmlElement("w:fldChar") fld_char2.set(qn("w:fldCharType"), "end") run._r.append(fld_char1) run._r.append(instr_text) run._r.append(fld_char2) set_run_font(run, size=9, color=MUTED) r = paragraph.add_run(" 页") set_run_font(r, size=9, color=MUTED) def add_section_title(doc, text, level=1): p = doc.add_paragraph(style=f"Heading {level}") p.paragraph_format.keep_with_next = True r = p.add_run(text) set_run_font(r, size={1: 16, 2: 13, 3: 12}[level], bold=True, color={1: BLUE, 2: BLUE, 3: DARK_BLUE}[level]) return p def page_break(doc): doc.add_page_break() def configure_document(doc): section = doc.sections[0] section.page_width = Inches(8.5) section.page_height = Inches(11) section.top_margin = Inches(1) section.right_margin = Inches(1) section.bottom_margin = Inches(1) section.left_margin = Inches(1) section.header_distance = Inches(0.492) section.footer_distance = Inches(0.492) normal = doc.styles["Normal"] set_style_font(normal, 11, BLACK, False) normal.paragraph_format.space_before = Pt(0) normal.paragraph_format.space_after = Pt(6) normal.paragraph_format.line_spacing = 1.25 for name, size, color, before, after in ( ("Heading 1", 16, BLUE, 18, 10), ("Heading 2", 13, BLUE, 14, 7), ("Heading 3", 12, DARK_BLUE, 10, 5), ): style = doc.styles[name] set_style_font(style, size, color, True) style.paragraph_format.space_before = Pt(before) style.paragraph_format.space_after = Pt(after) style.paragraph_format.keep_with_next = True style.paragraph_format.keep_together = True formula = doc.styles.add_style("Formula", WD_STYLE_TYPE.PARAGRAPH) set_style_font(formula, 10.2, INK, False) formula.paragraph_format.left_indent = Inches(0.12) formula.paragraph_format.right_indent = Inches(0.05) formula.paragraph_format.space_before = Pt(4) formula.paragraph_format.space_after = Pt(8) formula.paragraph_format.line_spacing = 1.2 # Running header and footer: quiet furniture from compact_reference_guide. header = section.header hp = header.paragraphs[0] hp.alignment = WD_ALIGN_PARAGRAPH.LEFT hp.paragraph_format.space_after = Pt(0) r = hp.add_run("股票价格合理性评估操作手册") set_run_font(r, size=8.5, color=MUTED) footer = section.footer fp = footer.paragraphs[0] fp.alignment = WD_ALIGN_PARAGRAPH.RIGHT fp.paragraph_format.space_before = Pt(0) add_page_field(fp) def build_document(): doc = Document() configure_document(doc) # Cover - editorial_cover pattern. p = doc.add_paragraph() p.paragraph_format.space_before = Pt(78) p.paragraph_format.space_after = Pt(16) p.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p.add_run("股票估值 · 操作手册") set_run_font(r, size=10.5, bold=True, color=GOLD) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(10) r = p.add_run("如何判断一只股票的价格是否合理") set_run_font(r, size=29, bold=True, color=INK) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(22) r = p.add_run("从信息取数、利润还原、质量校验,到估值区间与情景收益率") set_run_font(r, size=14, color=DARK_BLUE) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_before = Pt(18) p.paragraph_format.space_after = Pt(96) r = p.add_run("一份可以直接照着执行、复算和更新的通用框架") set_run_font(r, size=10.5, italic=True, color=MUTED) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p.add_run("版本 1.0 | 2026-07-30") set_run_font(r, size=10.5, bold=True, color=INK) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(10) r = p.add_run("适用范围:A股为主,兼顾港股、美股;不针对特定证券") set_run_font(r, size=9.5, color=MUTED) add_callout(doc, "重要说明", "本文档用于研究与估值训练,不构成投资建议。估值结果是条件判断,不是价格保证。", "caution") page_break(doc) add_section_title(doc, "先看这一页:最终要算出什么", 1) add_body(doc, "判断价格是否合理,不是找一个精确目标价,而是同时回答四个问题:企业能持续赚多少钱、合理估值是多少、当前价格隐含了什么假设、未来收益和下行风险是否匹配。") add_table( doc, ["最终输出", "计算结果", "用途"], [ ["正常化利润", "历史事实经一次性、周期性和会计口径调整后的可持续利润", "决定估值的利润底座"], ["合理价值区间", "悲观 / 基准 / 乐观价值,不用单点目标价", "判断当前价格落在哪个区间"], ["预期年化收益率", "基于未来利润、退出估值与分红计算 IRR", "与最低要求回报比较"], ["反向估值假设", "当前价格隐含的收入增长、利润率、ROIC 或持续年限", "判断市场预期是否现实"], ["悲观损失", "悲观价值相对当前价格的跌幅", "确定风险是否可承受"], ], [1800, 4320, 3240], aligns=[WD_ALIGN_PARAGRAPH.CENTER, WD_ALIGN_PARAGRAPH.LEFT, WD_ALIGN_PARAGRAPH.LEFT], first_col_bold=True, ) add_callout(doc, "核心判定", "只有当基准回报达标、当前价格位于合理区间、隐含假设可实现、悲观损失可承受时,才称为“价格合理”。合理不等于一定值得买;买入通常还需要安全边际。", "positive") add_section_title(doc, "30分钟快速路径", 2) add_numbered_list(doc, [ "锁定估值日期、当前股价和完全稀释后股数,计算总市值。", "从最新年报、季报和业绩预告得到归母净利润、扣非归母净利润和未来利润范围。", "剔除一次性损益,检查现金流、负债和周期位置,得到正常化利润。", "按行业选一个主估值模型,再选一个交叉验证模型。", "计算悲观、基准、乐观三种情景的价值和5年预期收益率。", "做反向估值,写清楚当前价格要求公司未来做到什么。", "按本手册的判定矩阵得出:低估、合理、偏贵或暂时无法判断。", ]) page_break(doc) add_section_title(doc, "第一部分 建立统一口径", 1) add_section_title(doc, "1.1 先确定估值任务", 2) add_body(doc, "在取数前先写下四个约束,否则同一家公司会被算出互相冲突的结果。") add_table(doc, ["项目", "必须填写", "示例"], [ ["估值基准日", "股价、资产负债和已知公告截至哪一天", "2026-07-30收盘"], ["持有期限", "用多长时间检验价值兑现", "5年"], ["最低要求回报 R", "投资者对该风险要求的年化收益", "示例使用10%,须自行替换"], ["风险预算", "悲观情景可接受的永久亏损", "不超过20%"], ["估值币种", "股价、现金流、折现率必须使用一致币种", "人民币名义口径"], ], [1800, 4440, 3120], first_col_bold=True) add_section_title(doc, "1.2 股权价值、企业价值和每股价值", 2) add_formula(doc, "总市值 = 当前股价 × 完全稀释后总股数", "企业价值 EV = 总市值 + 有息负债 + 优先股 + 少数股东权益 - 可支配现金 - 非经营性金融资产", "每股价值 = 普通股股权价值 ÷ 完全稀释后总股数") add_bullet_list(doc, [ "不要用流通市值代替总市值。限售股同样属于股权价值。", "完全稀释股数应考虑已授予期权、限制性股票、可转债和潜在增发。", "受限资金、客户备付金或维持经营必需的现金,不应全部当作可加回现金。", "使用 EV/EBITDA、EV/EBIT 或 FCFF-DCF 时必须先算企业价值;使用 P/E、P/B、FCFE 时直接对应股权价值。", ]) add_callout(doc, "常见错误", "把企业价值除以股数直接当每股价值,会漏减债务和少数股东权益,通常高估股票价值。", "risk") add_section_title(doc, "1.3 最低要求回报怎么定", 2) add_formula(doc, "要求回报 R = 同币种长期无风险利率 + 股票风险溢价 + 公司特有风险调整", "公司特有风险调整考虑:规模、杠杆、周期、治理、流动性、国家与汇率风险") add_body(doc, "如果没有能力估计各项风险溢价,可以制定固定投资政策,例如低风险、一般风险、高风险分别用不同的要求回报,并对折现率上下浮动2个百分点做敏感性分析。本文示例用10%,只用于演示计算。") add_section_title(doc, "第二部分 信息从哪里获取,以及谁优先", 1) add_section_title(doc, "2.1 可靠性和时效性必须分开排序", 2) add_body(doc, "审计年报可靠性高,但可能已经过时;业绩预告更新,却只是区间估计。正确做法是:历史事实以正式披露为准,未来判断用最新信息更新,并保留每个数据的日期、期间和口径。") add_table(doc, ["等级", "信息类型", "主要用途", "处理规则"], [ ["A1", "交易所披露的审计年报及审计意见", "历史事实、会计政策、附注、分部", "同期间最高优先级;检查非标审计意见"], ["A2", "正式半年报、季报、财务报表", "最近已实现业绩和资产负债", "优先于快报、预告和机构预测"], ["A3", "业绩快报、预告及修正公告", "尚未出正式财报的近端业绩", "快报通常高于预告;修正公告覆盖原公告"], ["A4", "经营数据、重大合同、产销、价格和资本运作公告", "预测销量、价格、产能和股数", "只使用已生效或高概率事项,避免把合同额当收入"], ["B1", "业绩说明会、投资者关系活动记录、公司官网", "解释经营驱动和管理层指引", "只能补充,不能替代法定披露"], ["B2", "政府、监管机构、海关、行业协会原始数据", "行业需求、价格、渗透率、宏观变量", "优先于二手媒体转述"], ["C1", "机构一致预期、数据库聚合", "市场基准和预测分歧", "用最新中位数;核对更新时间和覆盖机构数"], ["C2", "单篇券商报告", "详细业务拆分和预测逻辑", "复算关键假设;不把目标价当证据"], ["D", "媒体、自媒体、论坛、所谓渠道消息", "发现线索", "不直接进入估值;必须回到原始来源验证"], ], [900, 2520, 2520, 3420], font_size=8.8, first_col_bold=True) add_section_title(doc, "2.2 同一数据发生冲突时的规则", 2) add_numbered_list(doc, [ "先确认是否属于同一报告期、同一会计口径和同一币种。", "同期间优先使用公司在交易所披露的最新正式文件;修正公告覆盖原公告。", "原始文件优先于数据库摘录,数据库只用于检索和批量整理。", "实际数优先于预测数;预测不可与实际重复相加。", "归母、扣非归母、净利润、EBITDA 和自由现金流不可互换。", "不能消除的冲突保留为范围,并降低估值置信度。", ]) add_section_title(doc, "2.3 主要入口", 2) source_rows = [ ["A股公告", "巨潮资讯、上交所、深交所、北交所", "年报、季报、快报、预告、问询回复、投资者关系记录"], ["港股公告", "披露易 HKEXnews", "年报、中报、公告、股本变化"], ["美股公告", "SEC EDGAR", "10-K、10-Q、8-K、20-F、6-K"], ["行业与宏观", "国家统计局、海关总署、央行、行业监管及协会", "销量、价格、产量、利率、进出口"], ["机构预期", "Wind、Choice、iFinD、Bloomberg、FactSet、LSEG等", "一致预期、历史预测、估值可比"], ["市场价格", "交易所、公司公告、可信行情终端", "基准日股价、总股本、复权与公司行动"], ] add_table(doc, ["市场/用途", "入口", "获取内容"], source_rows, [1440, 3000, 4920], first_col_bold=True) add_callout(doc, "留痕要求", "每个关键输入至少记录:数值、单位、报告期、发布日期、来源文件、页码或章节、实际/预测标记、是否经审计、调整说明。", "info") add_section_title(doc, "2.4 最小数据包", 2) add_bullet_list(doc, [ "最近5个完整年度的利润表、资产负债表、现金流量表及分部数据。", "最近8个季度的收入、归母利润、扣非归母、经营现金流及关键经营指标。", "估值日的完全稀释股数、现金、有息负债、少数股东权益和潜在融资。", "未来2至3年的公司指引、机构一致预期及支撑预测的行业量价数据。", ]) add_section_title(doc, "第三部分 算清利润", 1) add_section_title(doc, "3.1 财报利润链条", 2) add_formula(doc, "营业收入 = 销量 × 售价(或客户数 × 单客收入)", "毛利润 = 营业收入 - 营业成本", "核心经营利润 ≈ 毛利润 - 销售费用 - 管理费用 - 研发费用 - 正常财务费用", "归母净利润 = 净利润 - 少数股东损益") add_body(doc, "直接从利润表取得营业收入、营业利润、利润总额、净利润和归母净利润;再从附注、管理层讨论与分析和分部信息中找到各业务的收入、成本、毛利率及同比变化原因。") add_section_title(doc, "3.2 估值要保留的四个利润", 2) add_table(doc, ["利润口径", "计算方法", "作用"], [ ["报表归母利润", "利润表或主要财务指标中的归母净利润", "与市场常见P/E对齐"], ["扣非归母利润", "公司披露的扣除非经常性损益后归母净利润", "正常化利润的起点"], ["正常化利润", "扣非归母 + 应加回的真正一次性损失 - 未扣掉的不可持续收益 ± 周期调整", "估计持续盈利能力"], ["未来12个月利润 NTM", "未来连续四个季度预计归母或正常化利润之和", "计算前瞻P/E和预期收益"], ], [1800, 4860, 2700], first_col_bold=True) add_section_title(doc, "3.3 TTM和单季度利润", 2) add_formula(doc, "TTM利润 = 最近完整年度利润 + 本年累计利润 - 上年同期累计利润", "单季度利润 = 本期累计利润 - 上一期累计利润", "预告隐含季度利润 = 本期累计预告区间 - 已披露季度实际利润") add_body(doc, "例:第一季度归母净利润3亿元,半年度预告8至10亿元,则第二季度隐含利润为5至7亿元。基准情景可先用中值6亿元,悲观情景接近5亿元,并检查预告是否包含一次性项目。") add_section_title(doc, "3.4 正常化利润调整", 2) add_table(doc, ["调整项目", "常见处理", "证据位置"], [ ["资产出售、债务重组、诉讼赔偿", "通常剔除;若商业模式本来依赖资产交易则单独判断", "非经常性损益明细、附注"], ["政府补助和税收优惠", "判断与主营业务相关性、标准是否明确、能否持续", "其他收益、政府补助、所得税附注"], ["公允价值和投资收益", "经营性金融业务可保留;非主营投资单独估值", "投资收益、公允价值附注"], ["资产减值", "真正一次性且资产经济价值仍在时才考虑加回;重复减值属于经营成本", "减值明细、资产附注"], ["股权激励", "属于股东承担的经济成本,不宜简单加回;同时考虑稀释", "股份支付、股本变动"], ["周期性利润", "不用峰值利润;按中周期价格、产能利用率和成本计算", "3至10年历史、行业价格"], ["汇兑损益", "经常发生且与经营/负债结构相关时保留正常水平", "财务费用附注"], ["少数股东损益", "必须扣除不属于上市公司普通股股东的部分", "利润表、子公司权益结构"], ], [1740, 4620, 3000], font_size=8.8, first_col_bold=True) add_formula(doc, "正常化归母利润 = 扣非归母利润 + 合理的一次性损失加回 - 未被扣除的不可持续收益 ± 周期和会计口径调整") add_callout(doc, "审慎原则", "不能证明某项损失真正一次性,就不要加回;不能证明某项收益可持续,就不要把它资本化为高估值。", "caution") add_section_title(doc, "第四部分 追踪利润来源", 1) add_section_title(doc, "4.1 用利润桥解释变化", 2) add_body(doc, "利润增长只有被拆成业务驱动后才可用于预测。建议把本期相对上期的利润变化拆成以下七项。") add_formula(doc, "利润变化 = 销量贡献 + 售价贡献 + 产品/客户结构贡献 + 单位成本贡献", " + 期间费用贡献 + 财务与税率贡献 + 一次性项目贡献") add_table(doc, ["驱动", "计算思路", "优先信息源"], [ ["销量", "销量变化 × 上期单位毛利", "公司产销公告、分部数据、行业销量"], ["售价", "售价变化 × 本期销量", "公司披露、产品价格、行业价格"], ["产品结构", "高毛利产品占比变化造成的毛利变化", "分产品收入和毛利率"], ["单位成本", "原材料、人工、折旧和制造费用的单位变化", "成本附注、原材料价格、产能利用率"], ["期间费用", "各费用率变化 × 收入", "利润表、费用附注"], ["融资与税率", "净利息、汇兑、实际税率变化", "财务费用、所得税附注"], ["一次性项目", "非经常性损益的税后归母影响", "非经常性损益明细"], ], [1380, 4260, 3720], first_col_bold=True) add_section_title(doc, "4.2 未来利润怎么预测", 2) add_formula(doc, "未来收入 = 市场规模 × 市占率;或 销量 × 单价;或 客户数 × 单客收入", "未来毛利 = 未来收入 × 预计毛利率", "未来归母利润 = 毛利 - 期间费用 ± 其他持续项目 - 税 - 少数股东损益") add_numbered_list(doc, [ "先预测业务量,不从历史利润增速直接外推。", "将价格、产品结构、单位成本和费用率分别设定,不把所有变化塞进一个净利率。", "完整反映增长所需的资本开支、营运资金和股份稀释。", "把已发生的季度作为实际数,只预测剩余季度,确保不重复计算。", "机构预测使用最近更新的中位数,并记录预测分歧和上调/下调方向。", ]) add_section_title(doc, "4.3 不同类型公司的利润底座", 2) add_table(doc, ["公司类型", "建议利润底座", "避免"], [ ["成熟稳定企业", "TTM正常化利润 + NTM利润", "过度依赖遥远增长"], ["高增长盈利企业", "NTM利润和3至5年业务驱动预测", "直接把最近高增速永久外推"], ["强周期企业", "中周期商品价格、成本和产量下的利润", "用景气顶峰P/E判断便宜"], ["亏损成长企业", "收入、毛利、单位经济性和达到正现金流所需资金", "用负P/E或远期极端利润"], ["金融企业", "正常信用成本、ROE、资本充足率和可分配利润", "机械套用EV/EBITDA"], ], [1800, 4500, 3060], first_col_bold=True) add_section_title(doc, "第五部分 利润质量与生存能力", 1) add_section_title(doc, "5.1 现金流校验", 2) add_formula(doc, "经营现金含金量 = 3年累计经营现金流 ÷ 3年累计归母净利润", "简化自由现金流 = 经营现金流 - 资本开支", "FCFE = 经营现金流 - 资本开支 + 可持续净借款", "自由现金流收益率 = 正常化FCFE ÷ 总市值") add_body(doc, "现金含金量要看多年累计,避免单季营运资金波动。大于1通常说明利润现金兑现较好,但预收款、金融企业、地产和高速扩张企业需要结合商业模式解释。FCFE中的净借款只能使用与目标资本结构一致的可持续水平,不能用一次性举债美化现金流。") add_section_title(doc, "5.2 回报率与增长质量", 2) add_formula(doc, "NOPAT = EBIT × (1 - 正常税率)", "ROIC = NOPAT ÷ 平均投入资本", "投入资本 ≈ 平均(股东权益 + 有息负债 - 可支配现金)", "可持续增长 g ≈ 再投资率 × 新增资本回报率") add_body(doc, "增长只有在新增资本回报率长期高于资本成本时才创造价值。若收入增长依赖大量应收、存货、资本开支或并购,必须降低增长的价值权重。") add_section_title(doc, "5.3 负债与稀释", 2) add_formula(doc, "净负债 = 有息负债 - 可支配现金", "净负债/EBITDA = 净负债 ÷ 正常化EBITDA", "利息保障倍数 = EBIT ÷ 利息费用", "股数稀释率 = 完全稀释股数同比增长率") add_callout(doc, "硬门槛", "若公司未来12至24个月存在明显偿债、再融资或持续经营风险,普通相对估值可能失效。此时先做困境概率、债务到期和融资稀释分析,再谈便宜。", "risk") add_section_title(doc, "5.4 估值前排雷清单", 2) add_bullet_list(doc, [ "经营现金流长期显著低于利润,应收、存货或合同资产异常增长。", "毛利率显著高于同行却缺乏业务解释,或收入与税费、现金不匹配。", "资本化研发、利息资本化或在建工程快速增长,美化当期利润。", "频繁并购、商誉过高、反复减值或依赖出售资产维持利润。", "关联交易、资金占用、担保、非标审计意见或管理层无法保证财报真实完整。", "股份支付、增发、可转债和员工期权导致持续稀释。", "周期顶部利润、高景气价格或异常低信用成本被当成永久水平。", ]) add_section_title(doc, "第六部分 选择正确的估值模型", 1) add_section_title(doc, "6.1 模型选择总表", 2) add_table(doc, ["企业类型", "主模型", "交叉验证", "关键输入"], [ ["一般非金融盈利企业", "P/E或FCFF-DCF", "EV/EBIT、FCF收益率、反向DCF", "正常化利润、增长、ROIC、负债"], ["资本开支较重企业", "FCFF-DCF、EV/EBIT", "FCF收益率、重置价值", "维持性资本开支、周期利用率"], ["银行", "P/B-ROE、股利/剩余收益模型", "P/E、股息率", "正常ROE、信用成本、资本充足率"], ["保险", "P/EV或剩余收益", "P/B、股息率", "内含价值、新业务价值、偿付能力"], ["REIT/物业", "NAV、P/FFO", "股息率、资本化率", "可持续租金、空置率、净负债"], ["资源/强周期", "中周期利润、NAV", "EV/EBITDA、股息率", "中周期价格、储量、成本曲线"], ["亏损成长企业", "情景DCF、EV/收入", "单位经济性、资金缺口", "毛利率、获客成本、留存、稀释"], ["多业务控股公司", "SOTP分部加总", "合并DCF、控股折价", "各分部价值、净负债、税费"], ], [1740, 2340, 2520, 2760], font_size=8.7, first_col_bold=True) add_callout(doc, "模型纪律", "至少用一个主模型和一个不同逻辑的交叉验证。若两个结果相差超过25%,先解释口径、周期、负债、资本开支或终值假设,不要直接平均。", "caution") add_section_title(doc, "6.2 相对估值的可比对象", 2) add_body(doc, "同行可比不是业务名字相似就够了。至少匹配收入来源、增长、利润率、ROIC、负债、会计口径、地区风险和生命周期。建议用中位数,并说明公司相对同行应该溢价还是折价。") add_section_title(doc, "第七部分 估值标准与计算公式", 1) add_section_title(doc, "7.1 P/E与利润收益率", 2) add_formula(doc, "TTM P/E = 总市值 ÷ TTM归母净利润", "正常化P/E = 总市值 ÷ 正常化归母利润", "前瞻P/E = 总市值 ÷ NTM归母利润", "利润收益率 = 正常化利润 ÷ 总市值 = 1 ÷ 正常化P/E") add_body(doc, "合理P/E必须与利润增长、回报率、风险和资本需求匹配。低P/E可能来自周期顶部、利润下滑或高负债;高P/E只有在高质量增长可持续时才合理。") add_table(doc, ["检查项", "计算/观察", "通过标准"], [ ["历史位置", "当前正常化P/E对比自身5至10年分位", "解释经营结构变化后再比较"], ["同行位置", "对比可比公司中位数", "溢折价能由增长、ROIC、风险解释"], ["增长匹配", "前瞻P/E与3至5年利润CAGR", "不依赖不现实的长期高增速"], ["现金匹配", "利润收益率与FCF收益率", "差异可由营运资金和资本开支解释"], ], [1800, 3960, 3600], first_col_bold=True) add_section_title(doc, "7.2 EV/EBIT、EV/EBITDA和FCF收益率", 2) add_formula(doc, "EBITDA = EBIT + 折旧摊销", "EV/EBIT = 企业价值 ÷ 正常化EBIT", "EV/EBITDA = 企业价值 ÷ 正常化EBITDA", "企业自由现金流收益率 = FCFF ÷ EV", "股权自由现金流收益率 = FCFE ÷ 总市值") add_bullet_list(doc, [ "EV倍数适合资本结构不同的企业比较。", "EBITDA忽略资本开支;重资产企业不能只看EV/EBITDA。", "FCF收益率最接近股东实际可获得现金,但要区分维持性和增长性资本开支。", "同一分析中不得用股权现金流除以EV,也不得用企业现金流除以总市值。", ]) add_section_title(doc, "7.3 P/B与ROE", 2) add_formula(doc, "P/B = 总市值 ÷ 归母净资产", "ROE = 归母净利润 ÷ 平均归母净资产", "稳定期理论P/B ≈ (ROE - g) ÷ (要求回报R - g)") add_body(doc, "P/B主要适合资产与盈利能力关系紧密的银行、保险和部分资产型公司。低P/B若对应低ROE、资产减值或资本约束,并不代表低估。") add_section_title(doc, "7.4 股东收益率", 2) add_formula(doc, "股东收益率 = (现金分红 + 净回购) ÷ 总市值", "净回购 = 回购注销金额 - 新股发行及股权融资金额") add_body(doc, "回购只有在未被股权激励和新增发行抵消、且回购价格合理时,才真正增加每股价值。") add_section_title(doc, "第八部分 DCF与反向估值", 1) add_section_title(doc, "8.1 FCFF-DCF", 2) add_formula(doc, "FCFF = EBIT × (1 - 税率) + 折旧摊销 - 资本开支 - 营运资金增加", "股权成本 Ke = 无风险利率 + Beta × 股票风险溢价 + 特有风险调整", "WACC = Ke × E/(D+E) + 税前债务成本Kd × (1-税率) × D/(D+E)", "终值 TV = FCFF(n+1) ÷ (WACC - 永续增长率g)", "企业价值 = Σ[FCFF(t) ÷ (1 + WACC)^t] + TV ÷ (1 + WACC)^n", "股权价值 = 企业价值 + 现金及非经营资产 - 债务 - 优先股 - 少数股东权益") add_body(doc, "预测期通常覆盖企业从当前状态过渡到稳定状态的时间。终值增长率必须低于折现率,并与估值币种下的长期经济增长相容。WACC中的E和D使用市场价值权重。现金流和折现率必须匹配:FCFF用WACC,FCFE用股权成本。") add_section_title(doc, "8.2 DCF必做的敏感性分析", 2) add_table(doc, ["变量", "基准", "悲观", "乐观"], [ ["收入增长", "业务驱动中值", "低2至5个百分点", "高2至5个百分点"], ["稳态利润率", "历史/同行可实现水平", "成本或竞争恶化", "接近优秀同行但不超常"], ["WACC/股权成本", "要求回报对应", "提高1至2个百分点", "降低1个百分点"], ["永续增长率", "低于长期名义经济增长", "降低0.5至1个百分点", "最多小幅提高"], ["终值占比", "单独披露", "若高于70%降低置信度", "不可用乐观终值掩盖近端弱现金流"], ], [1800, 2880, 2340, 2340], first_col_bold=True) add_section_title(doc, "8.3 反向DCF", 2) add_body(doc, "反向估值把当前市值设为已知数,反求能使模型成立的收入增长率、利润率、ROIC或竞争优势持续年限。它不是第三个目标价,而是检查市场正在要求公司做到什么。") add_numbered_list(doc, [ "将当前总市值转换为企业价值。", "锁定合理的折现率、终值增长率和资本需求。", "反求未来5至10年的收入增长或稳态利润率,使DCF价值等于当前EV。", "把隐含假设与历史最好水平、同行、行业空间和产能约束比较。", "若当前价格只有在乐观假设同时成立时才合理,应判为偏贵。", ]) add_callout(doc, "最重要的问题", "不要只问“我算出的价值是多少”,还要问“市场价格要求未来发生什么,而这些事情发生的概率有多大”。", "positive") add_section_title(doc, "第九部分 预期收益率与最终判定", 1) add_section_title(doc, "9.1 五年持有收益率", 2) add_formula(doc, "第N年EPS = 当前基准EPS × (1 + 可持续增长率)^N", "第N年价格 = 第N年EPS × 合理退出P/E", "简化年化回报IRR = [(第N年价格 + 累计分红) ÷ 当前价格]^(1/N) - 1") add_body(doc, "更精确时应把每年分红按实际时点计入XIRR。退出P/E应使用企业进入更成熟阶段后的正常倍数,通常不应高于当前高增长期倍数。") add_section_title(doc, "9.2 安全边际和下行风险", 2) add_formula(doc, "安全边际 = (基准合理价值 - 当前价格) ÷ 基准合理价值", "悲观下行 = (当前价格 - 悲观价值) ÷ 当前价格") add_body(doc, "安全边际需要随不确定性增加。稳定、高透明度企业可以要求较小折价;周期、杠杆、治理或技术不确定性高的企业应要求更大折价。不要把固定20%或30%当作普遍真理。") add_section_title(doc, "9.3 默认判定矩阵", 2) add_table(doc, ["结论", "默认量化标准", "必要条件"], [ ["明显低估", "基准IRR ≥ R+3个百分点,且价格低于基准价值下沿", "隐含假设不激进;悲观损失在风险预算内"], ["基本合理", "基准IRR在R上下3个百分点内,且价格位于合理区间", "业务假设可实现;至少两种模型方向一致"], ["偏贵", "基准IRR < R-3个百分点,或价格高于合理价值上沿", "只有乐观情景才能达标,或当前价格隐含超常经营"], ["无法判断", "主模型与验证模型差异>25%,或利润无法正常化", "信息不足、会计可信度低、重大二元事件未决"], ], [1560, 4260, 3540], first_col_bold=True) add_callout(doc, "阈值说明", "3个百分点和25%是便于执行的默认值,可按个人投资政策调整;一旦选定,应在不同股票之间保持一致。", "info") add_section_title(doc, "9.4 最终结论必须写成条件句", 2) add_formula(doc, "结论模板:在收入增速__%、正常化净利率__%、要求回报__%、退出P/E__倍的基准假设下,", "合理价值为__至__元,当前价格__元,5年预期IRR为__%,悲观下行__%。", "因此判定为__;若__指标跌破/超过__,需重新估值。") add_section_title(doc, "第十部分 完整示例:甲公司", 1) add_body(doc, "以下为虚构公司,仅用于演示。单位为亿元,股数为亿股。估值日价格22.50元,持有期5年,最低要求回报10%。") add_section_title(doc, "10.1 资本结构与利润", 2) add_table(doc, ["项目", "数值", "计算/说明"], [ ["当前股价", "22.50元", "估值日收盘价"], ["完全稀释股数", "10亿股", "含潜在稀释"], ["总市值", "225亿元", "22.50 × 10"], ["现金", "40亿元", "假设均可支配"], ["有息负债", "60亿元", "短债+长债"], ["少数股东权益", "5亿元", "企业价值口径需加"], ["企业价值EV", "250亿元", "225 + 60 + 5 - 40"], ["TTM归母利润", "18亿元", "报表数"], ["一次性资产出售收益", "3亿元", "税后归母影响"], ["正常化利润", "15亿元", "18 - 3"], ["未来12个月基准利润", "16.5亿元", "业务驱动预测"], ["正常化FCFE", "13亿元", "用于FCF收益率"], ], [2880, 1800, 4680], first_col_bold=True) add_section_title(doc, "10.2 相对估值", 2) add_formula(doc, "报表TTM P/E = 225 ÷ 18 = 12.5倍", "正常化P/E = 225 ÷ 15 = 15.0倍", "前瞻P/E = 225 ÷ 16.5 = 13.6倍", "FCF收益率 = 13 ÷ 225 = 5.8%") add_body(doc, "假设公司历史、同行和经营质量支持基准15倍前瞻P/E,则相对估值为16.5 × 15 = 247.5亿元,即每股24.75元。") add_section_title(doc, "10.3 简化DCF", 2) add_body(doc, "假设第一年FCFF为16亿元,未来5年增长7%,WACC为10%,永续增长率3%,净债务及少数股东权益合计25亿元。") dcf_rows = [ ["1", "16.00", "14.55"], ["2", "17.12", "14.15"], ["3", "18.32", "13.76"], ["4", "19.60", "13.39"], ["5", "20.97", "13.02"], ["终值", "308.60", "191.62"], ] add_table(doc, ["年度", "FCFF/终值", "折现现值"], dcf_rows, [1800, 3240, 4320], first_col_bold=True) add_formula(doc, "企业价值 = 68.87 + 191.62 = 260.49亿元", "股权价值 = 260.49 - 25 = 235.49亿元", "每股价值 = 235.49 ÷ 10 = 23.55元") add_callout(doc, "解释", "相对估值24.75元、DCF 23.55元,差异约5%,方向一致。DCF终值占企业价值约74%,因此仍须对WACC和永续增长率做敏感性分析。", "info") add_section_title(doc, "10.4 情景收益率", 2) scenario_data = [] for name, growth, exit_pe, div in [("悲观", 0.02, 12, 1.80), ("基准", 0.07, 15, 2.25), ("乐观", 0.11, 18, 2.70)]: eps5 = 1.65 * pow(1 + growth, 5) total5 = eps5 * exit_pe + div irr = pow(total5 / 22.5, 1 / 5) - 1 scenario_data.append([name, f"{growth:.0%}", f"{eps5:.2f}元", f"{exit_pe}倍", f"{div:.2f}元", f"{irr:.1%}"]) add_table(doc, ["情景", "EPS增长", "第5年EPS", "退出P/E", "累计分红", "5年IRR"], scenario_data, [1200, 1440, 1680, 1440, 1680, 1920], font_size=9.0, aligns=[WD_ALIGN_PARAGRAPH.CENTER] * 6, first_col_bold=True) add_body(doc, "基准情景IRR约10.4%,刚超过10%的最低要求;悲观情景仅约1.0%。用基准情景未来总价值36.96元按10%折现,当前合理价格约22.95元。") add_section_title(doc, "10.5 反向估值与结论", 2) add_body(doc, "当前22.50元大致要求公司未来5年EPS增长约7%、退出P/E约15倍并累计分红2.25元。这些假设若与历史增长、行业空间、ROIC和资本需求相符,则价格可以被经营事实支撑。") add_callout(doc, "示例结论", "综合DCF 23.55元、前瞻P/E价值24.75元、基准IRR 10.4%,甲公司当前22.50元处于合理区间偏低位置,但安全边际不大,因此判定“基本合理”,而非“明显低估”。若正常化利润下调超过10%或合理退出P/E降至12倍,应重新估值。", "positive") page_break(doc) add_section_title(doc, "附录A 一页式计算模板", 1) add_body(doc, "复制本页填写。所有金额必须使用同一币种和单位。") add_table(doc, ["模块", "输入/结果", "数值", "来源/说明"], [ ["任务", "估值日 / 持有期 / 要求回报 / 风险预算", "", ""], ["价格", "当前股价 / 稀释股数 / 总市值", "", ""], ["资本", "现金 / 有息负债 / 少数股东权益 / EV", "", ""], ["利润", "TTM归母 / 扣非归母 / 正常化利润 / NTM利润", "", ""], ["现金流", "CFO / Capex / FCFE / FCF收益率", "", ""], ["质量", "现金含金量 / ROIC / 净负债EBITDA / 稀释率", "", ""], ["主估值", "模型 / 关键假设 / 价值区间", "", ""], ["验证估值", "模型 / 关键假设 / 价值区间", "", ""], ["情景", "悲观价值 / 基准价值 / 乐观价值", "", ""], ["收益", "悲观IRR / 基准IRR / 乐观IRR", "", ""], ["反向", "当前价格隐含增长 / 利润率 / 退出倍数", "", ""], ["结论", "低估 / 合理 / 偏贵 / 无法判断", "", "触发重新估值的条件:"], ], [1260, 3540, 1740, 2820], font_size=8.8, first_col_bold=True) add_section_title(doc, "一页式判定", 2) add_bullet_list(doc, [ "基准IRR是否达到最低要求回报?", "当前价格是否位于至少两种模型共同支持的价值区间?", "当前价格隐含的增长、利润率和ROIC是否可实现?", "悲观下行是否处于风险预算内?", "利润是否真实、可持续,并能转换成现金?", "未来12至24个月是否存在偿债、稀释或持续经营风险?", ]) page_break(doc) add_section_title(doc, "附录B 利润正常化与预测工作表", 1) add_section_title(doc, "B1 正常化利润", 2) add_table(doc, ["项目", "金额", "加/减", "税后归母影响", "依据与来源"], [ ["报表归母净利润", "", "起点", "", ""], ["公司披露非经常性损益", "", "减", "", ""], ["未扣除的不可持续收益", "", "减", "", ""], ["可合理加回的一次性损失", "", "加", "", ""], ["周期调整", "", "加/减", "", ""], ["会计政策和其他调整", "", "加/减", "", ""], ["正常化归母利润", "", "结果", "", ""], ], [2520, 1320, 1080, 1800, 2640], font_size=8.8, first_col_bold=True) add_section_title(doc, "B2 未来利润驱动", 2) add_table(doc, ["驱动", "最近实际", "悲观", "基准", "乐观", "证据"], [ ["销量/客户数", "", "", "", "", ""], ["售价/单客收入", "", "", "", "", ""], ["营业收入", "", "", "", "", ""], ["毛利率", "", "", "", "", ""], ["销售费用率", "", "", "", "", ""], ["管理费用率", "", "", "", "", ""], ["研发费用率", "", "", "", "", ""], ["财务费用", "", "", "", "", ""], ["正常税率", "", "", "", "", ""], ["少数股东损益", "", "", "", "", ""], ["正常化归母利润", "", "", "", "", ""], ], [1800, 1320, 1320, 1320, 1320, 2280], font_size=8.5, first_col_bold=True) page_break(doc) add_section_title(doc, "附录C 信息证据登记表", 1) add_table(doc, ["数据项", "数值", "期间", "发布日期", "来源文件/章节", "实际/预测", "等级", "调整说明"], [ ["当前股价", "", "", "", "", "实际", "A", ""], ["完全稀释股数", "", "", "", "", "实际/估计", "", ""], ["归母利润", "", "", "", "", "实际", "", ""], ["扣非归母", "", "", "", "", "实际", "", ""], ["业绩预告", "", "", "", "", "公司估计", "", ""], ["机构一致预期", "", "", "", "", "预测", "", ""], ["销量/价格", "", "", "", "", "", "", ""], ["现金与负债", "", "", "", "", "实际", "", ""], ["资本开支", "", "", "", "", "实际/预测", "", ""], ["估值参数", "", "", "", "", "假设", "", ""], ], [1080, 900, 900, 1080, 1980, 1080, 720, 1620], font_size=7.8, aligns=[WD_ALIGN_PARAGRAPH.CENTER] * 4 + [WD_ALIGN_PARAGRAPH.LEFT] + [WD_ALIGN_PARAGRAPH.CENTER] * 2 + [WD_ALIGN_PARAGRAPH.LEFT], first_col_bold=True) add_callout(doc, "使用规则", "每次财报、预告或重大经营信息发布后,仅更新受影响的输入和情景;保留旧版本,以便检查预测偏差和机构修正。", "info") add_section_title(doc, "附录D 无法正常估值的情形", 1) add_bullet_list(doc, [ "财务真实性存在重大疑问,或审计意见、监管问询尚未消除。", "公司价值主要取决于单一诉讼、审批、勘探或临床试验等二元事件。", "未来融资缺口巨大,稀释比例无法合理估计。", "商业模式尚未验证,单位经济性为负且没有可信的改善路径。", "主模型与验证模型差异持续超过25%,且不能由周期、负债或口径解释。", "行业规则、价格机制或资产权属发生重大变化,历史利润失去参考意义。", ]) add_callout(doc, "正确结论", "“无法判断”是一种有效结论。信息不足时强行给出目标价,会把不确定性伪装成精确数字。", "caution") page_break(doc) add_section_title(doc, "附录E 权威规则与估值参考", 1) add_body(doc, "以下链接用于查找原始披露规则和估值方法。规则可能修订,使用时应以估值日最新版本为准。") references = [ ("巨潮资讯网(深交所法定信息披露平台)", "https://www.cninfo.com.cn/"), ("上海证券交易所", "https://www.sse.com.cn/"), ("深圳证券交易所", "https://www.szse.cn/"), ("北京证券交易所", "https://www.bse.cn/"), ("香港交易所披露易", "https://www.hkexnews.hk/"), ("美国SEC EDGAR", "https://www.sec.gov/edgar/search/"), ("证监会《上市公司信息披露管理办法》(2025-07-01起施行)", "https://www.csrc.gov.cn/shanghai/c105565/c7549909/content.shtml"), ("证监会《非经常性损益(2023年修订)》", "https://www.csrc.gov.cn/csrc/c101954/c7451398/content.shtml"), ("上交所《股票上市规则(2026年4月修订)》", "https://www.sse.com.cn/lawandrules/sselawsrules2025/stocks/mainipo/c/c_20260424_10816589.shtml"), ("Damodaran:估值框架与DCF", "https://pages.stern.nyu.edu/~adamodar/New_Home_Page/lectures/val.html"), ("Damodaran:每股股权价值的调整", "https://pages.stern.nyu.edu/adamodar/New_Home_Page/littlebook/valuepershare.htm"), ("Investor.gov:P/E定义", "https://www.investor.gov/introduction-investing/investing-basics/glossary/price-earnings-pe-ratio"), ] num_id = create_numbering(doc, kind="decimal") for title, url in references: p = doc.add_paragraph() apply_num(p, num_id, 0) add_hyperlink(p, title, url) r = p.add_run(f" {url}") set_run_font(r, size=8.5, color=MUTED) add_section_title(doc, "最后检查:交付前的十个问题", 2) add_numbered_list(doc, [ "估值日期、币种和持有期是否明确?", "总市值是否使用完全稀释后总股数?", "利润是否区分报表、扣非、正常化和未来12个月?", "一次性和周期性项目是否有证据支持调整?", "利润增长是否拆成销量、价格、结构、成本和费用?", "利润是否通过现金流、ROIC、负债和稀释校验?", "是否选对行业模型,并使用不同逻辑交叉验证?", "是否完成悲观、基准、乐观情景和敏感性分析?", "当前价格隐含的经营假设是否明确且可实现?", "结论是否包含合理价值区间、IRR、悲观下行和重新估值触发条件?", ]) # Core properties. doc.core_properties.title = "如何判断一只股票的价格是否合理" doc.core_properties.subject = "股票估值操作手册" doc.core_properties.author = "Codex" doc.core_properties.keywords = "股票估值, 正常化利润, DCF, 反向DCF, 预期收益率, 信息源" doc.core_properties.comments = "版本1.0;通用研究框架;不构成投资建议。" doc.save(OUTPUT) return OUTPUT if __name__ == "__main__": path = build_document() print(path)