skills/financial-report-writing/scripts/generate_report.py

314 lines
12 KiB
Python
Raw Normal View History

"""
金融研究报告生成器
整合数据获取分析和文档生成功能一键生成完整报告
"""
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()