跳转至

报告生成器 API

HtmlReporter

Bases: Reporter


              flowchart TD
              fund_cli.core.reporters.html_reporter.HtmlReporter[HtmlReporter]
              fund_cli.core.reporter.Reporter[Reporter]

                              fund_cli.core.reporter.Reporter --> fund_cli.core.reporters.html_reporter.HtmlReporter
                


              click fund_cli.core.reporters.html_reporter.HtmlReporter href "" "fund_cli.core.reporters.html_reporter.HtmlReporter"
              click fund_cli.core.reporter.Reporter href "" "fund_cli.core.reporter.Reporter"
            

HTML报告生成器 (FUND-ANALYZE-011)

源代码位于: src/fund_cli/core/reporters/html_reporter.py
class HtmlReporter(Reporter):
    """HTML报告生成器 (FUND-ANALYZE-011)"""

    def generate(self, fund_code: str, metrics: dict[str, Any], nav_data: Any = None, benchmark_data: Any = None, **kwargs) -> str:  # type: ignore[override]
        kwargs.get("nav_data")
        html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>{fund_code} 分析报告</title>
<style>
body {{ font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; color: #333; }}
h1 {{ color: #1a5276; border-bottom: 2px solid #2980b9; padding-bottom: 10px; }}
h2 {{ color: #2c3e50; margin-top: 30px; }}
table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
th {{ background: #2980b9; color: white; }}
tr:nth-child(even) {{ background: #f2f2f2; }}
.positive {{ color: #27ae60; }}
.negative {{ color: #e74c3c; }}
.footer {{ margin-top: 30px; font-size: 12px; color: #999; border-top: 1px solid #eee; padding-top: 10px; }}
</style></head><body>
<h1>{fund_code} 基金分析报告</h1>
<p>报告日期: {date.today().strftime('%Y-%m-%d')}</p>
<h2>核心指标</h2>
<table><tr><th>指标</th><th>值</th></tr>"""

        key_metrics = [
            ("总收益率", metrics.get("total_return", "N/A")),
            ("年化收益率", metrics.get("annualized_return", "N/A")),
            ("波动率", metrics.get("volatility", "N/A")),
            ("夏普比率", metrics.get("sharpe_ratio", "N/A")),
            ("最大回撤", metrics.get("max_drawdown", "N/A")),
            ("索提诺比率", metrics.get("sortino_ratio", "N/A")),
            ("Alpha", metrics.get("alpha", "N/A")),
            ("Beta", metrics.get("beta", "N/A")),
        ]
        for name, value in key_metrics:
            if isinstance(value, float):
                cls = "positive" if value > 0 else "negative" if value < 0 else ""
                html += f"<tr><td>{name}</td><td class='{cls}'>{value:.4f}</td></tr>"
            else:
                html += f"<tr><td>{name}</td><td>{value}</td></tr>"

        html += "</table>"
        html += "<div class='footer'>本报告由 Fund CLI 自动生成,仅供参考,不构成投资建议。</div>"
        html += "</body></html>"
        return html

    def save(self, content: str, output_path: str) -> None:
        from pathlib import Path

        Path(output_path).write_text(content, encoding="utf-8")

    def get_formats(self) -> list[str]:
        return ["html"]

MarkdownReporter

Bases: Reporter


              flowchart TD
              fund_cli.core.reporters.markdown_reporter.MarkdownReporter[MarkdownReporter]
              fund_cli.core.reporter.Reporter[Reporter]

                              fund_cli.core.reporter.Reporter --> fund_cli.core.reporters.markdown_reporter.MarkdownReporter
                


              click fund_cli.core.reporters.markdown_reporter.MarkdownReporter href "" "fund_cli.core.reporters.markdown_reporter.MarkdownReporter"
              click fund_cli.core.reporter.Reporter href "" "fund_cli.core.reporter.Reporter"
            

Markdown报告生成器

源代码位于: src/fund_cli/core/reporters/markdown_reporter.py
class MarkdownReporter(Reporter):
    """Markdown报告生成器"""

    def generate(self, fund_code: str, metrics: dict[str, Any], nav_data: Any = None, benchmark_data: Any = None, **kwargs) -> str:  # type: ignore[override]
        md = f"# {fund_code} 基金分析报告\n\n"
        md += f"报告日期: {date.today().strftime('%Y-%m-%d')}\n\n"
        md += "## 核心指标\n\n"
        md += "| 指标 | 值 |\n|------|-----|\n"

        key_metrics = [
            ("总收益率", metrics.get("total_return")),
            ("年化收益率", metrics.get("annualized_return")),
            ("波动率", metrics.get("volatility")),
            ("夏普比率", metrics.get("sharpe_ratio")),
            ("最大回撤", metrics.get("max_drawdown")),
        ]
        for name, value in key_metrics:
            if isinstance(value, float):
                md += f"| {name} | {value:.4f} |\n"
            else:
                md += f"| {name} | {value or 'N/A'} |\n"

        md += "\n---\n*本报告由 Fund CLI 自动生成,仅供参考。*\n"
        return md

    def save(self, content: str, output_path: str) -> None:
        from pathlib import Path

        Path(output_path).write_text(content, encoding="utf-8")

    def get_formats(self) -> list[str]:
        return ["markdown"]

PdfReporter

Bases: Reporter


              flowchart TD
              fund_cli.core.reporters.pdf_reporter.PdfReporter[PdfReporter]
              fund_cli.core.reporter.Reporter[Reporter]

                              fund_cli.core.reporter.Reporter --> fund_cli.core.reporters.pdf_reporter.PdfReporter
                


              click fund_cli.core.reporters.pdf_reporter.PdfReporter href "" "fund_cli.core.reporters.pdf_reporter.PdfReporter"
              click fund_cli.core.reporter.Reporter href "" "fund_cli.core.reporter.Reporter"
            

PDF报告生成器.

源代码位于: src/fund_cli/core/reporters/pdf_reporter.py
class PdfReporter(Reporter):
    """PDF报告生成器."""

    def generate(self, fund_code: str, metrics: dict[str, Any], nav_data: Any = None, benchmark_data: Any = None, **kwargs) -> str:  # type: ignore[override]
        """生成HTML内容(PDF基于HTML转换)."""
        from datetime import date

        _ = kwargs.get("nav_data")  # 保留参数以保持接口兼容性

        html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{fund_code} 基金分析报告</title>
<style>
@page {{ size: A4; margin: 2cm; }}
body {{ font-family: "SimSun", "Noto Sans CJK SC", sans-serif; max-width: 100%; margin: 0; padding: 20px; color: #333; font-size: 12px; }}
h1 {{ color: #1a5276; border-bottom: 3px solid #2980b9; padding-bottom: 10px; font-size: 22px; }}
h2 {{ color: #2c3e50; margin-top: 25px; font-size: 16px; }}
table {{ border-collapse: collapse; width: 100%; margin: 10px 0; font-size: 11px; }}
th, td {{ border: 1px solid #ddd; padding: 6px 10px; text-align: left; }}
th {{ background: #2980b9; color: white; }}
tr:nth-child(even) {{ background: #f5f5f5; }}
.positive {{ color: #27ae60; font-weight: bold; }}
.negative {{ color: #e74c3c; font-weight: bold; }}
.summary-box {{ background: #eaf2f8; border-left: 4px solid #2980b9; padding: 15px; margin: 15px 0; }}
.footer {{ margin-top: 30px; font-size: 10px; color: #999; border-top: 1px solid #eee; padding-top: 10px; }}
.page-break {{ page-break-before: always; }}
</style>
</head>
<body>
<h1>{fund_code} 基金分析报告</h1>
<p>报告日期: {date.today().strftime('%Y年%m月%d日')}</p>

<div class="summary-box">
<strong>投资摘要:</strong>
总收益率 <span class="{'positive' if float(metrics.get('total_return', 0)) > 0 else 'negative'}">{metrics.get('total_return', 'N/A')}</span>,
夏普比率 {metrics.get('sharpe_ratio', 'N/A')}
最大回撤 <span class="negative">{metrics.get('max_drawdown', 'N/A')}</span>
</div>

<h2>核心绩效指标</h2>
<table>
<tr><th>指标</th><th>值</th></tr>"""

        key_metrics = [
            ("总收益率", metrics.get("total_return", "N/A")),
            ("年化收益率", metrics.get("annualized_return", "N/A")),
            ("波动率", metrics.get("volatility", "N/A")),
            ("夏普比率", metrics.get("sharpe_ratio", "N/A")),
            ("最大回撤", metrics.get("max_drawdown", "N/A")),
            ("索提诺比率", metrics.get("sortino_ratio", "N/A")),
            ("Alpha", metrics.get("alpha", "N/A")),
            ("Beta", metrics.get("beta", "N/A")),
            ("信息比率", metrics.get("information_ratio", "N/A")),
            ("Calmar比率", metrics.get("calmar_ratio", "N/A")),
        ]
        for name, value in key_metrics:
            if isinstance(value, (int, float)):
                cls = "positive" if value > 0 else "negative" if value < 0 else ""
                html += f"<tr><td>{name}</td><td class='{cls}'>{value:.4f}</td></tr>"
            else:
                html += f"<tr><td>{name}</td><td>{value}</td></tr>"

        html += """</table>
<div class="footer">
<p>本报告由 Fund CLI v3.1 自动生成,仅供参考,不构成投资建议。</p>
</div>
</body></html>"""
        return html

    def save(self, content: str, output_path: str) -> None:
        """保存为PDF文件."""
        self.export_pdf(content, output_path)

    def get_formats(self) -> list[str]:
        return ["pdf"]

generate

generate(
    fund_code: str,
    metrics: dict[str, Any],
    nav_data: Any = None,
    benchmark_data: Any = None,
    **kwargs,
) -> str

生成HTML内容(PDF基于HTML转换).

源代码位于: src/fund_cli/core/reporters/pdf_reporter.py
    def generate(self, fund_code: str, metrics: dict[str, Any], nav_data: Any = None, benchmark_data: Any = None, **kwargs) -> str:  # type: ignore[override]
        """生成HTML内容(PDF基于HTML转换)."""
        from datetime import date

        _ = kwargs.get("nav_data")  # 保留参数以保持接口兼容性

        html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{fund_code} 基金分析报告</title>
<style>
@page {{ size: A4; margin: 2cm; }}
body {{ font-family: "SimSun", "Noto Sans CJK SC", sans-serif; max-width: 100%; margin: 0; padding: 20px; color: #333; font-size: 12px; }}
h1 {{ color: #1a5276; border-bottom: 3px solid #2980b9; padding-bottom: 10px; font-size: 22px; }}
h2 {{ color: #2c3e50; margin-top: 25px; font-size: 16px; }}
table {{ border-collapse: collapse; width: 100%; margin: 10px 0; font-size: 11px; }}
th, td {{ border: 1px solid #ddd; padding: 6px 10px; text-align: left; }}
th {{ background: #2980b9; color: white; }}
tr:nth-child(even) {{ background: #f5f5f5; }}
.positive {{ color: #27ae60; font-weight: bold; }}
.negative {{ color: #e74c3c; font-weight: bold; }}
.summary-box {{ background: #eaf2f8; border-left: 4px solid #2980b9; padding: 15px; margin: 15px 0; }}
.footer {{ margin-top: 30px; font-size: 10px; color: #999; border-top: 1px solid #eee; padding-top: 10px; }}
.page-break {{ page-break-before: always; }}
</style>
</head>
<body>
<h1>{fund_code} 基金分析报告</h1>
<p>报告日期: {date.today().strftime('%Y年%m月%d日')}</p>

<div class="summary-box">
<strong>投资摘要:</strong>
总收益率 <span class="{'positive' if float(metrics.get('total_return', 0)) > 0 else 'negative'}">{metrics.get('total_return', 'N/A')}</span>,
夏普比率 {metrics.get('sharpe_ratio', 'N/A')}
最大回撤 <span class="negative">{metrics.get('max_drawdown', 'N/A')}</span>
</div>

<h2>核心绩效指标</h2>
<table>
<tr><th>指标</th><th>值</th></tr>"""

        key_metrics = [
            ("总收益率", metrics.get("total_return", "N/A")),
            ("年化收益率", metrics.get("annualized_return", "N/A")),
            ("波动率", metrics.get("volatility", "N/A")),
            ("夏普比率", metrics.get("sharpe_ratio", "N/A")),
            ("最大回撤", metrics.get("max_drawdown", "N/A")),
            ("索提诺比率", metrics.get("sortino_ratio", "N/A")),
            ("Alpha", metrics.get("alpha", "N/A")),
            ("Beta", metrics.get("beta", "N/A")),
            ("信息比率", metrics.get("information_ratio", "N/A")),
            ("Calmar比率", metrics.get("calmar_ratio", "N/A")),
        ]
        for name, value in key_metrics:
            if isinstance(value, (int, float)):
                cls = "positive" if value > 0 else "negative" if value < 0 else ""
                html += f"<tr><td>{name}</td><td class='{cls}'>{value:.4f}</td></tr>"
            else:
                html += f"<tr><td>{name}</td><td>{value}</td></tr>"

        html += """</table>
<div class="footer">
<p>本报告由 Fund CLI v3.1 自动生成,仅供参考,不构成投资建议。</p>
</div>
</body></html>"""
        return html

save

save(content: str, output_path: str) -> None

保存为PDF文件.

源代码位于: src/fund_cli/core/reporters/pdf_reporter.py
def save(self, content: str, output_path: str) -> None:
    """保存为PDF文件."""
    self.export_pdf(content, output_path)

DocxReporter

Bases: Reporter


              flowchart TD
              fund_cli.core.reporters.docx_reporter.DocxReporter[DocxReporter]
              fund_cli.core.reporter.Reporter[Reporter]

                              fund_cli.core.reporter.Reporter --> fund_cli.core.reporters.docx_reporter.DocxReporter
                


              click fund_cli.core.reporters.docx_reporter.DocxReporter href "" "fund_cli.core.reporters.docx_reporter.DocxReporter"
              click fund_cli.core.reporter.Reporter href "" "fund_cli.core.reporter.Reporter"
            

Word报告生成器.

源代码位于: src/fund_cli/core/reporters/docx_reporter.py
class DocxReporter(Reporter):
    """Word报告生成器."""

    def generate(self, fund_code: str, metrics: dict[str, Any], nav_data: Any = None, benchmark_data: Any = None, **kwargs) -> str:  # type: ignore[override]
        """生成Word文档内容(返回临时文件路径)."""
        try:
            from docx import Document
            from docx.enum.table import WD_TABLE_ALIGNMENT
            from docx.enum.text import WD_ALIGN_PARAGRAPH
            from docx.shared import Pt, RGBColor  # Cm, Inches 未使用
        except ImportError as exc:
            raise RuntimeError("python-docx 未安装,请运行: pip install python-docx") from exc

        doc = Document()

        # 标题
        title = doc.add_heading(f'{fund_code} 基金分析报告', level=0)
        title.alignment = WD_ALIGN_PARAGRAPH.CENTER

        # 日期
        from datetime import date
        p = doc.add_paragraph(f'报告日期: {date.today().strftime("%Y年%m月%d日")}')
        p.alignment = WD_ALIGN_PARAGRAPH.RIGHT

        # 投资摘要
        doc.add_heading('投资摘要', level=1)
        summary = doc.add_paragraph()
        total_return = metrics.get('total_return', 0)
        run = summary.add_run(f'总收益率: {total_return:.2%}')
        run.font.color.rgb = RGBColor(0x27, 0xAE, 0x60) if total_return > 0 else RGBColor(0xE7, 0x4C, 0x3C)
        summary.add_run(f'  |  夏普比率: {metrics.get("sharpe_ratio", "N/A")}')
        summary.add_run(f'  |  最大回撤: {metrics.get("max_drawdown", "N/A")}')

        # 核心绩效指标
        doc.add_heading('核心绩效指标', level=1)
        table = doc.add_table(rows=1, cols=2)
        table.style = 'Light Grid Accent 1'
        table.alignment = WD_TABLE_ALIGNMENT.CENTER
        hdr_cells = table.rows[0].cells
        hdr_cells[0].text = '指标'
        hdr_cells[1].text = '值'

        key_metrics = [
            ("总收益率", metrics.get("total_return", "N/A")),
            ("年化收益率", metrics.get("annualized_return", "N/A")),
            ("波动率", metrics.get("volatility", "N/A")),
            ("夏普比率", metrics.get("sharpe_ratio", "N/A")),
            ("最大回撤", metrics.get("max_drawdown", "N/A")),
            ("Alpha", metrics.get("alpha", "N/A")),
            ("Beta", metrics.get("beta", "N/A")),
        ]
        for name, value in key_metrics:
            row_cells = table.add_row().cells
            row_cells[0].text = name
            row_cells[1].text = f"{value:.4f}" if isinstance(value, (int, float)) else str(value)

        # 页脚
        doc.add_paragraph()
        footer = doc.add_paragraph('本报告由 Fund CLI v3.1 自动生成,仅供参考,不构成投资建议。')
        footer.style.font.size = Pt(9)

        # 保存到临时文件
        temp_path = Path(tempfile.gettempdir()) / f"{fund_code}_report.docx"
        doc.save(str(temp_path))
        return str(temp_path)

    def save(self, content: str, output_path: str) -> None:
        """保存Word文档."""
        import shutil
        shutil.copy2(content, output_path)

    def get_formats(self) -> list[str]:
        return ["docx"]

generate

generate(
    fund_code: str,
    metrics: dict[str, Any],
    nav_data: Any = None,
    benchmark_data: Any = None,
    **kwargs,
) -> str

生成Word文档内容(返回临时文件路径).

源代码位于: src/fund_cli/core/reporters/docx_reporter.py
def generate(self, fund_code: str, metrics: dict[str, Any], nav_data: Any = None, benchmark_data: Any = None, **kwargs) -> str:  # type: ignore[override]
    """生成Word文档内容(返回临时文件路径)."""
    try:
        from docx import Document
        from docx.enum.table import WD_TABLE_ALIGNMENT
        from docx.enum.text import WD_ALIGN_PARAGRAPH
        from docx.shared import Pt, RGBColor  # Cm, Inches 未使用
    except ImportError as exc:
        raise RuntimeError("python-docx 未安装,请运行: pip install python-docx") from exc

    doc = Document()

    # 标题
    title = doc.add_heading(f'{fund_code} 基金分析报告', level=0)
    title.alignment = WD_ALIGN_PARAGRAPH.CENTER

    # 日期
    from datetime import date
    p = doc.add_paragraph(f'报告日期: {date.today().strftime("%Y年%m月%d日")}')
    p.alignment = WD_ALIGN_PARAGRAPH.RIGHT

    # 投资摘要
    doc.add_heading('投资摘要', level=1)
    summary = doc.add_paragraph()
    total_return = metrics.get('total_return', 0)
    run = summary.add_run(f'总收益率: {total_return:.2%}')
    run.font.color.rgb = RGBColor(0x27, 0xAE, 0x60) if total_return > 0 else RGBColor(0xE7, 0x4C, 0x3C)
    summary.add_run(f'  |  夏普比率: {metrics.get("sharpe_ratio", "N/A")}')
    summary.add_run(f'  |  最大回撤: {metrics.get("max_drawdown", "N/A")}')

    # 核心绩效指标
    doc.add_heading('核心绩效指标', level=1)
    table = doc.add_table(rows=1, cols=2)
    table.style = 'Light Grid Accent 1'
    table.alignment = WD_TABLE_ALIGNMENT.CENTER
    hdr_cells = table.rows[0].cells
    hdr_cells[0].text = '指标'
    hdr_cells[1].text = '值'

    key_metrics = [
        ("总收益率", metrics.get("total_return", "N/A")),
        ("年化收益率", metrics.get("annualized_return", "N/A")),
        ("波动率", metrics.get("volatility", "N/A")),
        ("夏普比率", metrics.get("sharpe_ratio", "N/A")),
        ("最大回撤", metrics.get("max_drawdown", "N/A")),
        ("Alpha", metrics.get("alpha", "N/A")),
        ("Beta", metrics.get("beta", "N/A")),
    ]
    for name, value in key_metrics:
        row_cells = table.add_row().cells
        row_cells[0].text = name
        row_cells[1].text = f"{value:.4f}" if isinstance(value, (int, float)) else str(value)

    # 页脚
    doc.add_paragraph()
    footer = doc.add_paragraph('本报告由 Fund CLI v3.1 自动生成,仅供参考,不构成投资建议。')
    footer.style.font.size = Pt(9)

    # 保存到临时文件
    temp_path = Path(tempfile.gettempdir()) / f"{fund_code}_report.docx"
    doc.save(str(temp_path))
    return str(temp_path)

save

save(content: str, output_path: str) -> None

保存Word文档.

源代码位于: src/fund_cli/core/reporters/docx_reporter.py
def save(self, content: str, output_path: str) -> None:
    """保存Word文档."""
    import shutil
    shutil.copy2(content, output_path)

PptxReporter

Bases: Reporter


              flowchart TD
              fund_cli.core.reporters.pptx_reporter.PptxReporter[PptxReporter]
              fund_cli.core.reporter.Reporter[Reporter]

                              fund_cli.core.reporter.Reporter --> fund_cli.core.reporters.pptx_reporter.PptxReporter
                


              click fund_cli.core.reporters.pptx_reporter.PptxReporter href "" "fund_cli.core.reporters.pptx_reporter.PptxReporter"
              click fund_cli.core.reporter.Reporter href "" "fund_cli.core.reporter.Reporter"
            

PPT报告生成器.

源代码位于: src/fund_cli/core/reporters/pptx_reporter.py
class PptxReporter(Reporter):
    """PPT报告生成器."""

    def generate(self, fund_code: str, metrics: dict[str, Any], nav_data: Any = None, benchmark_data: Any = None, **kwargs) -> str:  # type: ignore[override]
        """生成PPT内容(返回临时文件路径)."""
        try:
            from pptx import Presentation
            from pptx.dml.color import RGBColor
            from pptx.enum.text import PP_ALIGN
            from pptx.util import Inches, Pt
        except ImportError as exc:
            raise RuntimeError("python-pptx 未安装,请运行: pip install python-pptx") from exc

        prs = Presentation()
        prs.slide_width = Inches(13.333)
        prs.slide_height = Inches(7.5)

        # 幻灯片1:封面
        slide_layout = prs.slide_layouts[6]  # blank
        slide = prs.slides.add_slide(slide_layout)

        # 标题
        txBox = slide.shapes.add_textbox(Inches(1), Inches(2), Inches(11.333), Inches(2))
        tf = txBox.text_frame
        p = tf.paragraphs[0]
        p.text = f"{fund_code} 基金分析报告"
        p.font.size = Pt(44)
        p.font.bold = True
        p.font.color.rgb = RGBColor(0x1A, 0x52, 0x76)
        p.alignment = PP_ALIGN.CENTER

        # 日期
        from datetime import date
        p2 = tf.add_paragraph()
        p2.text = date.today().strftime("%Y年%m月%d日")
        p2.font.size = Pt(20)
        p2.font.color.rgb = RGBColor(0x7F, 0x8C, 0x8D)
        p2.alignment = PP_ALIGN.CENTER

        # 幻灯片2:核心指标
        slide2 = prs.slides.add_slide(slide_layout)

        txBox2 = slide2.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12), Inches(0.8))
        tf2 = txBox2.text_frame
        p3 = tf2.paragraphs[0]
        p3.text = "核心绩效指标"
        p3.font.size = Pt(32)
        p3.font.bold = True
        p3.font.color.rgb = RGBColor(0x2C, 0x3E, 0x50)

        # 指标表格
        rows, cols = 6, 4
        table_shape = slide2.shapes.add_table(rows, cols, Inches(0.5), Inches(1.5), Inches(12), Inches(4))
        table = table_shape.table

        headers = ["指标", "值", "指标", "值"]
        for i, h in enumerate(headers):
            cell = table.cell(0, i)
            cell.text = h
            for paragraph in cell.text_frame.paragraphs:
                paragraph.font.size = Pt(14)
                paragraph.font.bold = True

        metrics_pairs = [
            ("总收益率", metrics.get("total_return", "N/A"), "年化收益率", metrics.get("annualized_return", "N/A")),
            ("波动率", metrics.get("volatility", "N/A"), "夏普比率", metrics.get("sharpe_ratio", "N/A")),
            ("最大回撤", metrics.get("max_drawdown", "N/A"), "Alpha", metrics.get("alpha", "N/A")),
        ]
        for r, (n1, v1, n2, v2) in enumerate(metrics_pairs, 1):
            for c, val in enumerate([n1, v1, n2, v2]):
                cell = table.cell(r, c)
                if isinstance(val, (int, float)):
                    cell.text = f"{val:.4f}"
                else:
                    cell.text = str(val)
                for paragraph in cell.text_frame.paragraphs:
                    paragraph.font.size = Pt(13)

        # 幻灯片3:投资建议
        slide3 = prs.slides.add_slide(slide_layout)
        txBox3 = slide3.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12), Inches(0.8))
        tf3 = txBox3.text_frame
        p4 = tf3.paragraphs[0]
        p4.text = "投资摘要"
        p4.font.size = Pt(32)
        p4.font.bold = True

        summary_box = slide3.shapes.add_textbox(Inches(0.5), Inches(1.5), Inches(12), Inches(4))
        stf = summary_box.text_frame
        stf.word_wrap = True
        sp = stf.paragraphs[0]
        total_return = metrics.get('total_return', 0)
        sp.text = f"总收益率 {total_return:.2%}"
        sp.font.size = Pt(18)
        sp.font.color.rgb = RGBColor(0x27, 0xAE, 0x60) if total_return > 0 else RGBColor(0xE7, 0x4C, 0x3C)

        sp2 = stf.add_paragraph()
        sp2.text = f"夏普比率 {metrics.get('sharpe_ratio', 'N/A')},最大回撤 {metrics.get('max_drawdown', 'N/A')}"
        sp2.font.size = Pt(16)
        sp2.space_before = Pt(12)

        # 保存到临时文件
        temp_path = Path(tempfile.gettempdir()) / f"{fund_code}_report.pptx"
        prs.save(str(temp_path))
        return str(temp_path)

    def save(self, content: str, output_path: str) -> None:
        """保存PPT文件."""
        import shutil
        shutil.copy2(content, output_path)

    def get_formats(self) -> list[str]:
        return ["pptx"]

generate

generate(
    fund_code: str,
    metrics: dict[str, Any],
    nav_data: Any = None,
    benchmark_data: Any = None,
    **kwargs,
) -> str

生成PPT内容(返回临时文件路径).

源代码位于: src/fund_cli/core/reporters/pptx_reporter.py
def generate(self, fund_code: str, metrics: dict[str, Any], nav_data: Any = None, benchmark_data: Any = None, **kwargs) -> str:  # type: ignore[override]
    """生成PPT内容(返回临时文件路径)."""
    try:
        from pptx import Presentation
        from pptx.dml.color import RGBColor
        from pptx.enum.text import PP_ALIGN
        from pptx.util import Inches, Pt
    except ImportError as exc:
        raise RuntimeError("python-pptx 未安装,请运行: pip install python-pptx") from exc

    prs = Presentation()
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)

    # 幻灯片1:封面
    slide_layout = prs.slide_layouts[6]  # blank
    slide = prs.slides.add_slide(slide_layout)

    # 标题
    txBox = slide.shapes.add_textbox(Inches(1), Inches(2), Inches(11.333), Inches(2))
    tf = txBox.text_frame
    p = tf.paragraphs[0]
    p.text = f"{fund_code} 基金分析报告"
    p.font.size = Pt(44)
    p.font.bold = True
    p.font.color.rgb = RGBColor(0x1A, 0x52, 0x76)
    p.alignment = PP_ALIGN.CENTER

    # 日期
    from datetime import date
    p2 = tf.add_paragraph()
    p2.text = date.today().strftime("%Y年%m月%d日")
    p2.font.size = Pt(20)
    p2.font.color.rgb = RGBColor(0x7F, 0x8C, 0x8D)
    p2.alignment = PP_ALIGN.CENTER

    # 幻灯片2:核心指标
    slide2 = prs.slides.add_slide(slide_layout)

    txBox2 = slide2.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12), Inches(0.8))
    tf2 = txBox2.text_frame
    p3 = tf2.paragraphs[0]
    p3.text = "核心绩效指标"
    p3.font.size = Pt(32)
    p3.font.bold = True
    p3.font.color.rgb = RGBColor(0x2C, 0x3E, 0x50)

    # 指标表格
    rows, cols = 6, 4
    table_shape = slide2.shapes.add_table(rows, cols, Inches(0.5), Inches(1.5), Inches(12), Inches(4))
    table = table_shape.table

    headers = ["指标", "值", "指标", "值"]
    for i, h in enumerate(headers):
        cell = table.cell(0, i)
        cell.text = h
        for paragraph in cell.text_frame.paragraphs:
            paragraph.font.size = Pt(14)
            paragraph.font.bold = True

    metrics_pairs = [
        ("总收益率", metrics.get("total_return", "N/A"), "年化收益率", metrics.get("annualized_return", "N/A")),
        ("波动率", metrics.get("volatility", "N/A"), "夏普比率", metrics.get("sharpe_ratio", "N/A")),
        ("最大回撤", metrics.get("max_drawdown", "N/A"), "Alpha", metrics.get("alpha", "N/A")),
    ]
    for r, (n1, v1, n2, v2) in enumerate(metrics_pairs, 1):
        for c, val in enumerate([n1, v1, n2, v2]):
            cell = table.cell(r, c)
            if isinstance(val, (int, float)):
                cell.text = f"{val:.4f}"
            else:
                cell.text = str(val)
            for paragraph in cell.text_frame.paragraphs:
                paragraph.font.size = Pt(13)

    # 幻灯片3:投资建议
    slide3 = prs.slides.add_slide(slide_layout)
    txBox3 = slide3.shapes.add_textbox(Inches(0.5), Inches(0.3), Inches(12), Inches(0.8))
    tf3 = txBox3.text_frame
    p4 = tf3.paragraphs[0]
    p4.text = "投资摘要"
    p4.font.size = Pt(32)
    p4.font.bold = True

    summary_box = slide3.shapes.add_textbox(Inches(0.5), Inches(1.5), Inches(12), Inches(4))
    stf = summary_box.text_frame
    stf.word_wrap = True
    sp = stf.paragraphs[0]
    total_return = metrics.get('total_return', 0)
    sp.text = f"总收益率 {total_return:.2%}"
    sp.font.size = Pt(18)
    sp.font.color.rgb = RGBColor(0x27, 0xAE, 0x60) if total_return > 0 else RGBColor(0xE7, 0x4C, 0x3C)

    sp2 = stf.add_paragraph()
    sp2.text = f"夏普比率 {metrics.get('sharpe_ratio', 'N/A')},最大回撤 {metrics.get('max_drawdown', 'N/A')}"
    sp2.font.size = Pt(16)
    sp2.space_before = Pt(12)

    # 保存到临时文件
    temp_path = Path(tempfile.gettempdir()) / f"{fund_code}_report.pptx"
    prs.save(str(temp_path))
    return str(temp_path)

save

save(content: str, output_path: str) -> None

保存PPT文件.

源代码位于: src/fund_cli/core/reporters/pptx_reporter.py
def save(self, content: str, output_path: str) -> None:
    """保存PPT文件."""
    import shutil
    shutil.copy2(content, output_path)