Files
20250920-e194e889/report_generator.py
2026-04-25 19:21:28 +08:00

425 lines
17 KiB
Python

"""
报告生成器 - 生成综合分析报告
"""
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
from reportlab.lib.pagesizes import letter, A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
import os
from datetime import datetime
from typing import Dict, List
import logging
logger = logging.getLogger(__name__)
class ReportGenerator:
def __init__(self, output_dir: str = "reports"):
self.output_dir = output_dir
self.chart_dir = os.path.join(output_dir, "charts")
# 创建输出目录
os.makedirs(self.output_dir, exist_ok=True)
os.makedirs(self.chart_dir, exist_ok=True)
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
def generate_comprehensive_report(self, symbol: str, data: Dict, analysis_results: Dict) -> str:
"""生成综合分析报告"""
try:
# 生成图表
chart_files = self._generate_charts(symbol, data, analysis_results)
# 生成PDF报告
report_file = self._generate_pdf_report(symbol, data, analysis_results, chart_files)
logger.info(f"报告生成完成: {report_file}")
return report_file
except Exception as e:
logger.error(f"生成报告失败: {e}")
return ""
def _generate_charts(self, symbol: str, data: Dict, analysis_results: Dict) -> List[str]:
"""生成图表"""
chart_files = []
try:
# 股价走势图
price_chart = self._create_price_chart(symbol, data)
if price_chart:
chart_files.append(price_chart)
# 财务指标雷达图
radar_chart = self._create_radar_chart(symbol, analysis_results)
if radar_chart:
chart_files.append(radar_chart)
# 估值对比图
valuation_chart = self._create_valuation_chart(symbol, analysis_results)
if valuation_chart:
chart_files.append(valuation_chart)
# 财务健康度仪表盘
health_gauge = self._create_health_gauge(symbol, analysis_results)
if health_gauge:
chart_files.append(health_gauge)
except Exception as e:
logger.error(f"生成图表失败: {e}")
return chart_files
def _create_price_chart(self, symbol: str, data: Dict) -> str:
"""创建股价走势图"""
try:
stock_prices = data.get('stock_prices', pd.DataFrame())
if stock_prices.empty:
return ""
fig = go.Figure()
# 添加收盘价线
fig.add_trace(go.Scatter(
x=stock_prices['date'],
y=stock_prices['close'],
mode='lines',
name='收盘价',
line=dict(color='#1f77b4', width=2)
))
# 添加移动平均线
if len(stock_prices) >= 20:
stock_prices['MA20'] = stock_prices['close'].rolling(window=20).mean()
fig.add_trace(go.Scatter(
x=stock_prices['date'],
y=stock_prices['MA20'],
mode='lines',
name='20日均线',
line=dict(color='orange', width=1, dash='dash')
))
fig.update_layout(
title=f'{symbol} 股价走势图',
xaxis_title='日期',
yaxis_title='价格 (USD)',
template='plotly_white',
height=400
)
chart_file = os.path.join(self.chart_dir, f'{symbol}_price_chart.html')
fig.write_html(chart_file)
return chart_file
except Exception as e:
logger.error(f"创建股价图表失败: {e}")
return ""
def _create_radar_chart(self, symbol: str, analysis_results: Dict) -> str:
"""创建雷达图"""
try:
categories = ['估值吸引力', '财务健康度', '成长性', '风险控制']
values = [
analysis_results.get('valuation_score', 0),
analysis_results.get('financial_health_score', 0),
analysis_results.get('growth_score', 0),
analysis_results.get('risk_score', 0)
]
fig = go.Figure()
fig.add_trace(go.Scatterpolar(
r=values,
theta=categories,
fill='toself',
name=symbol,
line_color='blue'
))
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, 100]
)),
showlegend=True,
title=f'{symbol} 综合评分雷达图',
height=400
)
chart_file = os.path.join(self.chart_dir, f'{symbol}_radar_chart.html')
fig.write_html(chart_file)
return chart_file
except Exception as e:
logger.error(f"创建雷达图失败: {e}")
return ""
def _create_valuation_chart(self, symbol: str, analysis_results: Dict) -> str:
"""创建估值对比图"""
try:
valuation_data = analysis_results.get('valuation_metrics', {})
metrics = ['PE比率', 'PB比率', 'PS比率', 'PEG比率']
values = [
valuation_data.get('pe_ratio', 0),
valuation_data.get('pb_ratio', 0),
valuation_data.get('ps_ratio', 0),
valuation_data.get('peg_ratio', 0)
]
# 行业平均值(示例数据)
industry_avg = [15, 2.5, 3.0, 1.2]
fig = go.Figure()
fig.add_trace(go.Bar(
name=symbol,
x=metrics,
y=values,
marker_color='lightblue'
))
fig.add_trace(go.Bar(
name='行业平均',
x=metrics,
y=industry_avg,
marker_color='lightcoral'
))
fig.update_layout(
title=f'{symbol} 估值指标对比',
xaxis_title='指标',
yaxis_title='数值',
barmode='group',
height=400
)
chart_file = os.path.join(self.chart_dir, f'{symbol}_valuation_chart.html')
fig.write_html(chart_file)
return chart_file
except Exception as e:
logger.error(f"创建估值图表失败: {e}")
return ""
def _create_health_gauge(self, symbol: str, analysis_results: Dict) -> str:
"""创建财务健康度仪表盘"""
try:
health_score = analysis_results.get('financial_health_score', 0)
fig = go.Figure(go.Indicator(
mode = "gauge+number+delta",
value = health_score,
domain = {'x': [0, 1], 'y': [0, 1]},
title = {'text': f"{symbol} 财务健康度"},
delta = {'reference': 50},
gauge = {
'axis': {'range': [None, 100]},
'bar': {'color': "darkblue"},
'steps': [
{'range': [0, 40], 'color': "lightgray"},
{'range': [40, 70], 'color': "yellow"},
{'range': [70, 100], 'color': "green"}
],
'threshold': {
'line': {'color': "red", 'width': 4},
'thickness': 0.75,
'value': 90
}
}
))
fig.update_layout(height=300)
chart_file = os.path.join(self.chart_dir, f'{symbol}_health_gauge.html')
fig.write_html(chart_file)
return chart_file
except Exception as e:
logger.error(f"创建健康度仪表盘失败: {e}")
return ""
def _generate_pdf_report(self, symbol: str, data: Dict, analysis_results: Dict, chart_files: List[str]) -> str:
"""生成PDF报告"""
try:
report_file = os.path.join(self.output_dir, f'{symbol}_analysis_report.pdf')
doc = SimpleDocTemplate(report_file, pagesize=A4)
styles = getSampleStyleSheet()
story = []
# 标题样式
title_style = ParagraphStyle(
'CustomTitle',
parent=styles['Heading1'],
fontSize=18,
spaceAfter=30,
alignment=TA_CENTER,
textColor=colors.darkblue
)
# 添加标题
story.append(Paragraph(f"{symbol} 股票分析报告", title_style))
story.append(Spacer(1, 20))
# 基本信息
company_info = data.get('company_info', {})
story.append(Paragraph("基本信息", styles['Heading2']))
basic_info_data = [
['公司名称', company_info.get('name', 'N/A')],
['行业', company_info.get('industry', 'N/A')],
['市值', f"${company_info.get('market_cap', 0):,.0f}"],
['员工数', f"{company_info.get('employees', 0):,}"],
['分析日期', datetime.now().strftime('%Y-%m-%d %H:%M:%S')]
]
basic_info_table = Table(basic_info_data, colWidths=[2*inch, 4*inch])
basic_info_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.black)
]))
story.append(basic_info_table)
story.append(Spacer(1, 20))
# 分析结果
story.append(Paragraph("分析结果", styles['Heading2']))
# 评分表格
scores_data = [
['分析维度', '评分', '评级'],
['估值吸引力', f"{analysis_results.get('valuation_score', 0):.1f}/100", self._get_rating(analysis_results.get('valuation_score', 0))],
['财务健康度', f"{analysis_results.get('financial_health_score', 0):.1f}/100", self._get_rating(analysis_results.get('financial_health_score', 0))],
['成长性', f"{analysis_results.get('growth_score', 0):.1f}/100", self._get_rating(analysis_results.get('growth_score', 0))],
['风险控制', f"{analysis_results.get('risk_score', 0):.1f}/100", self._get_rating(analysis_results.get('risk_score', 0))],
['综合评分', f"{analysis_results.get('overall_score', 0):.1f}/100", self._get_rating(analysis_results.get('overall_score', 0))]
]
scores_table = Table(scores_data, colWidths=[2*inch, 1.5*inch, 1.5*inch])
scores_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
('GRID', (0, 0), (-1, -1), 1, colors.black)
]))
story.append(scores_table)
story.append(Spacer(1, 20))
# 投资建议
recommendation = analysis_results.get('investment_recommendation', {})
story.append(Paragraph("投资建议", styles['Heading2']))
story.append(Paragraph(f"<b>建议:</b> {recommendation.get('recommendation', 'N/A')}", styles['Normal']))
story.append(Paragraph(f"<b>信心度:</b> {recommendation.get('confidence', 'N/A')}", styles['Normal']))
story.append(Paragraph(f"<b>综合评分:</b> {recommendation.get('overall_score', 0):.1f}/100", styles['Normal']))
# 关键优势
strengths = recommendation.get('key_strengths', [])
if strengths:
story.append(Spacer(1, 10))
story.append(Paragraph("关键优势:", styles['Heading3']))
for strength in strengths:
story.append(Paragraph(f"{strength}", styles['Normal']))
# 关键担忧
concerns = recommendation.get('key_concerns', [])
if concerns:
story.append(Spacer(1, 10))
story.append(Paragraph("关键担忧:", styles['Heading3']))
for concern in concerns:
story.append(Paragraph(f"{concern}", styles['Normal']))
# 风险提示
risk_warnings = recommendation.get('risk_warnings', [])
if risk_warnings:
story.append(Spacer(1, 10))
story.append(Paragraph("风险提示:", styles['Heading3']))
for warning in risk_warnings:
story.append(Paragraph(f"⚠️ {warning}", styles['Normal']))
story.append(Spacer(1, 20))
# 免责声明
story.append(Paragraph("免责声明", styles['Heading2']))
disclaimer = """
本报告仅供投资参考,不构成投资建议。投资有风险,入市需谨慎。
投资者应根据自身情况做出投资决策,并承担相应风险。
本报告基于公开信息分析,可能存在信息滞后或不准确的情况。
"""
story.append(Paragraph(disclaimer, styles['Normal']))
# 构建PDF
doc.build(story)
return report_file
except Exception as e:
logger.error(f"生成PDF报告失败: {e}")
return ""
def _get_rating(self, score: float) -> str:
"""根据分数获取评级"""
if score >= 90:
return "优秀"
elif score >= 80:
return "良好"
elif score >= 70:
return "中等"
elif score >= 60:
return "一般"
else:
return "较差"
def generate_summary_report(self, symbol: str, analysis_results: Dict) -> str:
"""生成简要报告(文本格式)"""
try:
recommendation = analysis_results.get('investment_recommendation', {})
summary = f"""
=== {symbol} 股票分析摘要 ===
📊 综合评分: {analysis_results.get('overall_score', 0):.1f}/100
💡 投资建议: {recommendation.get('recommendation', 'N/A')}
🎯 信心度: {recommendation.get('confidence', 'N/A')}
📈 各维度评分:
• 估值吸引力: {analysis_results.get('valuation_score', 0):.1f}/100
• 财务健康度: {analysis_results.get('financial_health_score', 0):.1f}/100
• 成长性: {analysis_results.get('growth_score', 0):.1f}/100
• 风险控制: {analysis_results.get('risk_score', 0):.1f}/100
✅ 关键优势:
{chr(10).join([f"{s}" for s in recommendation.get('key_strengths', [])])}
⚠️ 关键担忧:
{chr(10).join([f"{c}" for c in recommendation.get('key_concerns', [])])}
🚨 风险提示:
{chr(10).join([f"{w}" for w in recommendation.get('risk_warnings', [])])}
分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
"""
# 保存简要报告
summary_file = os.path.join(self.output_dir, f'{symbol}_summary.txt')
with open(summary_file, 'w', encoding='utf-8') as f:
f.write(summary)
return summary_file
except Exception as e:
logger.error(f"生成简要报告失败: {e}")
return ""