""" 金融研究报告生成器 整合数据获取、分析和文档生成功能,一键生成完整报告 """ import sys import argparse from datetime import datetime, timedelta from pathlib import Path from typing import Optional # 添加脚本目录到路径 sys.path.insert(0, str(Path(__file__).parent)) from docx import Document from docx.shared import Pt from docx.enum.text import WD_ALIGN_PARAGRAPH from data_fetcher import WindDataFetcher, fetch_stock_report_data from document_formatter import create_formatted_document, GBT9704Formatter from analysis_engine import perform_comprehensive_analysis class ReportGenerator: """ 金融研究报告生成器 """ def __init__(self): self.doc = None self.formatter = None self.analysis_results = None def generate_report(self, stock_code: str, stock_name: str = None, organization: str = "某某证券研究所", author: str = "", include_cover: bool = True, output_path: str = None) -> str: """ 生成完整的投资研究报告 Args: stock_code: 股票代码,如 '300866.SZ' stock_name: 股票名称(如不提供则自动获取) organization: 研究机构名称 author: 分析师姓名 include_cover: 是否包含封面 output_path: 输出文件路径 Returns: 生成的文件路径 """ print(f"开始生成 {stock_code} 的投资研究报告...") # 1. 获取数据 print("正在获取股票数据...") try: price_df, financial_data = fetch_stock_report_data(stock_code, days_back=90, weekly=True) except Exception as e: print(f"数据获取失败: {e}") raise # 如果未提供股票名称,从财务数据获取 if stock_name is None: stock_name = financial_data.get('sec_name', stock_code) print(f"获取到 {len(price_df)} 条周线数据") # 2. 执行分析 print("正在执行投资分析...") current_price = price_df['close'].iloc[-1] self.analysis_results = perform_comprehensive_analysis( price_df, financial_data, current_price ) # 3. 创建文档 print("正在生成Word文档...") self.doc, self.formatter = create_formatted_document() # 4. 添加封面(可选) if include_cover: self._add_cover_page(stock_name, organization) # 5. 添加免责声明 self._add_disclaimer() # 6. 添加核心提要 self._add_core_summary(stock_name) # 7. 添加投资评级 self._add_investment_rating(stock_name, current_price) # 8. 添加核心观点 self._add_core_views() # 9. 添加市场表现回顾 self._add_market_review(price_df) # 10. 添加页脚 self.formatter.add_page_number_footer() # 11. 保存文档 if output_path is None: date_str = datetime.now().strftime('%Y%m%d') output_path = f"{stock_name}_{stock_code.replace('.', '_')}_研究报告_{date_str}.docx" self.doc.save(output_path) print(f"报告已保存: {output_path}") return output_path def _add_cover_page(self, stock_name: str, organization: str): """添加封面页""" date_str = datetime.now().strftime('%Y年%m月%d日') self.formatter.add_cover_page( title=f"{stock_name}投资研究报告", organization=organization, date_str=date_str ) def _add_disclaimer(self): """添加免责声明""" self.formatter.add_heading('免责声明', level=1) self.formatter.add_paragraph( "本报告所载资料的来源及观点皆为公开信息,但并不能保证其准确性和完整性。本报告仅供参考," "不构成任何投资建议或承诺,投资者应审慎决策,独立判断,自行承担投资风险。" ) self.formatter.add_paragraph( "本报告版权归本公司所有,未经书面许可,任何机构和个人不得以任何形式翻版、复制、刊登、" "发表或引用。如征得本公司同意进行引用、刊发的,需在允许的范围内使用,并注明出处为" "'某某证券研究所',且不得对本报告进行任何有悖原意的引用、删节和修改。" ) def _add_core_summary(self, stock_name: str): """添加核心提要""" self.formatter.add_heading('核心提要', level=1) summary_points = self.analysis_results['summary_points'] for i, point in enumerate(summary_points, 1): # 添加项目符号 paragraph = self.doc.add_paragraph() run = paragraph.add_run(f"{i}. {point}") self.formatter.set_font(run, '仿宋', 14) paragraph.paragraph_format.line_spacing = Pt(28) paragraph.paragraph_format.line_spacing_rule = 2 paragraph.paragraph_format.space_before = Pt(0) paragraph.paragraph_format.space_after = Pt(0) def _add_investment_rating(self, stock_name: str, current_price: float): """添加投资评级""" self.formatter.add_heading('一、投资评级', level=1) # 表格数据 recommendation = self.analysis_results['recommendation'] data = [[ recommendation.rating.value, f"{recommendation.target_price:.2f}元", (datetime.now() + timedelta(days=365)).strftime('%Y年%m月%d日'), recommendation.risk_level.value ]] self.formatter.add_three_line_table( data, ['投资建议', '目标价格', '有效期', '风险等级'], font_name='仿宋', size=12 ) # 添加说明文字 self.formatter.add_paragraph( f"当前股价:{current_price:.2f}元。" f"目标价较当前价格{'上涨' if recommendation.target_price > current_price else '下跌'}" f"{abs((recommendation.target_price / current_price - 1) * 100):.2f}%。" ) def _add_core_views(self): """添加核心观点""" self.formatter.add_heading('二、核心观点', level=1) # 基本面分析 fundamental = self.analysis_results['fundamental'] self.formatter.add_heading('(一)基本面分析', level=2) self.formatter.add_paragraph( f"1. 盈利能力:公司最新净资产收益率(ROE)为{fundamental.roe}%," f"{fundamental.roe_comment}。" ) self.formatter.add_paragraph( f"2. 估值水平:当前市盈率(TTM)为{fundamental.pe_ttm}倍," f"市净率(LF)为{fundamental.pb_lf}倍。{fundamental.valuation_comment}。" ) self.formatter.add_paragraph(f"3. 结论:{fundamental.fundamental_summary}") # 技术面分析 technical = self.analysis_results['technical'] self.formatter.add_heading('(二)技术面分析', level=2) self.formatter.add_paragraph( f"1. 价格趋势:近期股价呈现{technical.trend}态势," f"{technical.ma_status}。" ) self.formatter.add_paragraph( f"2. 支撑与阻力:近期支撑位{technical.support_level}元," f"阻力位{technical.resistance_level}元。" ) self.formatter.add_paragraph( f"3. 波动率:近期年化波动率为{technical.volatility}%," f"{'属于高波动品种' if technical.volatility > 40 else '波动适中' if technical.volatility > 25 else '波动较低'}。" ) # 投资建议 recommendation = self.analysis_results['recommendation'] self.formatter.add_heading('(三)投资建议', level=2) self.formatter.add_paragraph(recommendation.recommendation_text) self.formatter.add_paragraph("主要投资逻辑:") for factor in recommendation.key_factors: paragraph = self.doc.add_paragraph() run = paragraph.add_run(f"• {factor}") self.formatter.set_font(run, '仿宋', 14) paragraph.paragraph_format.line_spacing = Pt(28) paragraph.paragraph_format.line_spacing_rule = 2 paragraph.paragraph_format.first_line_indent = Pt(28) def _add_market_review(self, price_df): """添加市场表现回顾""" self.formatter.add_heading('三、市场表现回顾', level=1) # 价格走势分析 self.formatter.add_heading('(一)价格走势分析', level=2) price_stats = self.analysis_results['price_stats'] self.formatter.add_paragraph( f"近期股价最高达到{price_stats['highest_price']}元," f"最低下探至{price_stats['lowest_price']}元," f"区间累计涨跌幅为{price_stats['total_change']:.2f}%。" f"最新收盘价为{price_stats['latest_price']}元," f"区间平均价格为{price_stats['avg_price']}元。" ) # K线数据表 self.formatter.add_heading('(二)K线数据', level=2) # 准备表格数据(最近13周) recent_data = price_df.tail(13).copy() table_data = [] for _, row in recent_data.iterrows(): date_str = row['date'].strftime('%Y-%m-%d') if hasattr(row['date'], 'strftime') else str(row['date'])[:10] table_data.append([ date_str, f"{row['open']:.2f}", f"{row['high']:.2f}", f"{row['low']:.2f}", f"{row['close']:.2f}", f"{row.get('pct_chg', 0):.2f}%", f"{row.get('volume', 0) / 10000:.0f}" ]) self.formatter.add_three_line_table( table_data, ['日期', '开盘价', '最高价', '最低价', '收盘价', '涨跌幅', '成交量(万股)'], font_name='仿宋', size=12 ) # 数据来源 self.formatter.add_data_source_footnote() def main(): """主函数""" parser = argparse.ArgumentParser(description='生成金融投资研究报告') parser.add_argument('stock_code', help='股票代码,如 300866.SZ 或 000001') parser.add_argument('--name', '-n', help='股票名称') parser.add_argument('--org', '-o', default='某某证券研究所', help='研究机构名称') parser.add_argument('--author', '-a', default='', help='分析师姓名') parser.add_argument('--no-cover', action='store_true', help='不包含封面') parser.add_argument('--output', '-p', help='输出文件路径') args = parser.parse_args() # 标准化股票代码 stock_code = args.stock_code if '.' not in stock_code: if stock_code.startswith('6'): stock_code = f"{stock_code}.SH" else: stock_code = f"{stock_code}.SZ" generator = ReportGenerator() try: output_path = generator.generate_report( stock_code=stock_code, stock_name=args.name, organization=args.org, author=args.author, include_cover=not args.no_cover, output_path=args.output ) print(f"\n报告生成成功: {output_path}") except Exception as e: print(f"\n报告生成失败: {e}") import traceback traceback.print_exc() sys.exit(1) if __name__ == '__main__': main()