""" 智策报告PDF导出模块 """ from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY from datetime import datetime import os import tempfile class SectorStrategyPDFGenerator: """智策报告PDF生成器""" def __init__(self): """初始化PDF生成器""" self.setup_fonts() def setup_fonts(self): """设置中文字体""" try: # 尝试注册常见的中文字体 font_paths = [ 'C:/Windows/Fonts/msyh.ttc', # 微软雅黑 'C:/Windows/Fonts/simsun.ttc', # 宋体 'C:/Windows/Fonts/simhei.ttf', # 黑体 ] for font_path in font_paths: if os.path.exists(font_path): try: pdfmetrics.registerFont(TTFont('ChineseFont', font_path)) self.chinese_font = 'ChineseFont' print(f"[PDF] 成功加载字体: {font_path}") return except: continue # 如果都失败,使用默认字体 self.chinese_font = 'Helvetica' print("[PDF] 警告: 未找到中文字体,使用默认字体") except Exception as e: print(f"[PDF] 字体设置失败: {e}") self.chinese_font = 'Helvetica' def generate_pdf(self, result_data: dict, output_path: str = None) -> str: """ 生成智策分析PDF报告 Args: result_data: 分析结果数据 output_path: 输出路径,如果为None则生成临时文件 Returns: PDF文件路径 """ try: # 如果没有指定输出路径,创建临时文件 if output_path is None: temp_dir = tempfile.gettempdir() timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") output_path = os.path.join(temp_dir, f"智策报告_{timestamp}.pdf") # 创建PDF文档 doc = SimpleDocTemplate( output_path, pagesize=A4, rightMargin=0.5*inch, leftMargin=0.5*inch, topMargin=0.5*inch, bottomMargin=0.5*inch ) # 构建内容 story = [] # 添加标题页 story.extend(self._create_title_page(result_data)) story.append(PageBreak()) # 添加市场概况 story.extend(self._create_market_overview(result_data)) story.append(PageBreak()) # 添加核心预测 story.extend(self._create_predictions_section(result_data)) story.append(PageBreak()) # 添加智能体分析摘要 story.extend(self._create_agents_summary(result_data)) story.append(PageBreak()) # 添加综合研判 story.extend(self._create_comprehensive_report(result_data)) # 生成PDF doc.build(story) print(f"[PDF] 报告生成成功: {output_path}") return output_path except Exception as e: print(f"[PDF] 生成失败: {e}") import traceback traceback.print_exc() raise def _create_title_page(self, data: dict) -> list: """创建标题页""" styles = self._get_styles() elements = [] # 添加空白 elements.append(Spacer(1, 2*inch)) # 主标题 title = Paragraph("智策板块策略分析报告", styles['Title']) elements.append(title) elements.append(Spacer(1, 0.5*inch)) # 副标题 subtitle = Paragraph("AI驱动的多维度板块投资决策支持系统", styles['Heading2']) elements.append(subtitle) elements.append(Spacer(1, 1*inch)) # 报告信息 timestamp = data.get('timestamp', datetime.now().strftime('%Y-%m-%d %H:%M:%S')) info_text = f""" 生成时间: {timestamp}
分析周期: 当日市场数据
AI模型: DeepSeek Multi-Agent System
分析维度: 宏观·板块·资金·情绪
""" info = Paragraph(info_text, styles['Normal']) elements.append(info) elements.append(Spacer(1, 1*inch)) # 免责声明 disclaimer = Paragraph( "本报告由AI系统自动生成,仅供参考,不构成投资建议。
" "投资有风险,决策需谨慎。
", styles['Normal'] ) elements.append(disclaimer) return elements def _create_market_overview(self, data: dict) -> list: """创建市场概况部分""" styles = self._get_styles() elements = [] # 标题 elements.append(Paragraph("一、市场概况", styles['Heading1'])) elements.append(Spacer(1, 0.2*inch)) # 这里需要从原始数据中提取市场概况 # 由于result_data中可能没有直接的市场数据,我们从agents_analysis中提取 overview_text = f""" 本报告基于{data.get('timestamp', 'N/A')}的实时市场数据, 通过四位AI智能体的多维度分析,为您提供板块投资策略建议。 """ elements.append(Paragraph(overview_text, styles['Normal'])) elements.append(Spacer(1, 0.2*inch)) # 分析师团队 team_text = """ 分析师团队:
• 宏观策略师 - 分析宏观经济、政策导向、新闻事件
• 板块诊断师 - 分析板块走势、估值水平、轮动特征
• 资金流向分析师 - 分析主力资金、北向资金流向
• 市场情绪解码员 - 分析市场情绪、热度、赚钱效应
""" elements.append(Paragraph(team_text, styles['Normal'])) return elements def _create_predictions_section(self, data: dict) -> list: """创建核心预测部分""" styles = self._get_styles() elements = [] predictions = data.get('final_predictions', {}) if predictions.get('prediction_text'): # 文本格式 elements.append(Paragraph("二、核心预测", styles['Heading1'])) elements.append(Spacer(1, 0.2*inch)) elements.append(Paragraph(predictions['prediction_text'], styles['Normal'])) return elements # JSON格式预测 elements.append(Paragraph("二、核心预测", styles['Heading1'])) elements.append(Spacer(1, 0.2*inch)) # 1. 板块多空 elements.extend(self._create_long_short_section(predictions, styles)) elements.append(Spacer(1, 0.3*inch)) # 2. 板块轮动 elements.extend(self._create_rotation_section(predictions, styles)) elements.append(Spacer(1, 0.3*inch)) # 3. 板块热度 elements.extend(self._create_heat_section(predictions, styles)) elements.append(Spacer(1, 0.3*inch)) # 4. 策略总结 elements.extend(self._create_summary_section(predictions, styles)) return elements def _create_long_short_section(self, predictions: dict, styles: dict) -> list: """创建板块多空部分""" elements = [] elements.append(Paragraph("2.1 板块多空预测", styles['Heading2'])) elements.append(Spacer(1, 0.1*inch)) long_short = predictions.get('long_short', {}) # 看多板块 bullish = long_short.get('bullish', []) if bullish: elements.append(Paragraph("看多板块:", styles['Normal'])) for idx, item in enumerate(bullish, 1): text = f""" {idx}. {item.get('sector', 'N/A')} (信心度: {item.get('confidence', 0)}/10)
理由: {item.get('reason', 'N/A')}
风险: {item.get('risk', 'N/A')} """ elements.append(Paragraph(text, styles['Small'])) elements.append(Spacer(1, 0.05*inch)) # 看空板块 bearish = long_short.get('bearish', []) if bearish: elements.append(Spacer(1, 0.1*inch)) elements.append(Paragraph("看空板块:", styles['Normal'])) for idx, item in enumerate(bearish, 1): text = f""" {idx}. {item.get('sector', 'N/A')} (信心度: {item.get('confidence', 0)}/10)
理由: {item.get('reason', 'N/A')}
风险: {item.get('risk', 'N/A')} """ elements.append(Paragraph(text, styles['Small'])) elements.append(Spacer(1, 0.05*inch)) return elements def _create_rotation_section(self, predictions: dict, styles: dict) -> list: """创建板块轮动部分""" elements = [] elements.append(Paragraph("2.2 板块轮动预测", styles['Heading2'])) elements.append(Spacer(1, 0.1*inch)) rotation = predictions.get('rotation', {}) # 当前强势 current_strong = rotation.get('current_strong', []) if current_strong: elements.append(Paragraph("当前强势板块:", styles['Normal'])) for item in current_strong: text = f""" • {item.get('sector', 'N/A')}
轮动逻辑: {item.get('logic', 'N/A')[:100]}...
时间窗口: {item.get('time_window', 'N/A')}
操作建议: {item.get('advice', 'N/A')} """ elements.append(Paragraph(text, styles['Small'])) elements.append(Spacer(1, 0.05*inch)) # 潜力接力 potential = rotation.get('potential', []) if potential: elements.append(Spacer(1, 0.1*inch)) elements.append(Paragraph("潜力接力板块:", styles['Normal'])) for item in potential: text = f""" • {item.get('sector', 'N/A')}
轮动逻辑: {item.get('logic', 'N/A')[:100]}...
时间窗口: {item.get('time_window', 'N/A')}
操作建议: {item.get('advice', 'N/A')} """ elements.append(Paragraph(text, styles['Small'])) elements.append(Spacer(1, 0.05*inch)) return elements def _create_heat_section(self, predictions: dict, styles: dict) -> list: """创建板块热度部分""" elements = [] elements.append(Paragraph("2.3 板块热度排行", styles['Heading2'])) elements.append(Spacer(1, 0.1*inch)) heat = predictions.get('heat', {}) # 创建表格数据 table_data = [['排名', '板块', '热度评分', '趋势', '持续性']] # 最热板块 hottest = heat.get('hottest', []) for idx, item in enumerate(hottest[:5], 1): table_data.append([ str(idx), item.get('sector', 'N/A'), str(item.get('score', 0)), item.get('trend', 'N/A'), item.get('sustainability', 'N/A') ]) if len(table_data) > 1: # 创建表格 table = Table(table_data, colWidths=[0.8*inch, 2*inch, 1*inch, 1*inch, 1*inch]) 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), self.chinese_font), ('FONTSIZE', (0, 0), (-1, 0), 10), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), colors.beige), ('GRID', (0, 0), (-1, -1), 1, colors.black), ('FONTNAME', (0, 1), (-1, -1), self.chinese_font), ('FONTSIZE', (0, 1), (-1, -1), 9), ])) elements.append(table) return elements def _create_summary_section(self, predictions: dict, styles: dict) -> list: """创建策略总结部分""" elements = [] summary = predictions.get('summary', {}) if not summary: return elements elements.append(Paragraph("2.4 策略总结", styles['Heading2'])) elements.append(Spacer(1, 0.1*inch)) # 市场观点 if summary.get('market_view'): elements.append(Paragraph("市场观点:", styles['Normal'])) elements.append(Paragraph(summary['market_view'], styles['Small'])) elements.append(Spacer(1, 0.1*inch)) # 核心机会 if summary.get('key_opportunity'): elements.append(Paragraph("核心机会:", styles['Normal'])) elements.append(Paragraph(summary['key_opportunity'], styles['Small'])) elements.append(Spacer(1, 0.1*inch)) # 主要风险 if summary.get('major_risk'): elements.append(Paragraph("主要风险:", styles['Normal'])) elements.append(Paragraph(summary['major_risk'], styles['Small'])) elements.append(Spacer(1, 0.1*inch)) # 整体策略 if summary.get('strategy'): elements.append(Paragraph("整体策略:", styles['Normal'])) elements.append(Paragraph(summary['strategy'], styles['Small'])) return elements def _create_agents_summary(self, data: dict) -> list: """创建智能体分析摘要""" styles = self._get_styles() elements = [] elements.append(Paragraph("三、AI智能体分析摘要", styles['Heading1'])) elements.append(Spacer(1, 0.2*inch)) agents_analysis = data.get('agents_analysis', {}) for key, agent_data in agents_analysis.items(): agent_name = agent_data.get('agent_name', '未知分析师') agent_role = agent_data.get('agent_role', '') analysis = agent_data.get('analysis', '') # 分析师名称和职责 elements.append(Paragraph(f"{agent_name}", styles['Heading2'])) elements.append(Paragraph(f"{agent_role}", styles['Small'])) elements.append(Spacer(1, 0.1*inch)) # 分析内容(截取前500字) analysis_preview = analysis[:500] + "..." if len(analysis) > 500 else analysis elements.append(Paragraph(analysis_preview, styles['Small'])) elements.append(Spacer(1, 0.2*inch)) return elements def _create_comprehensive_report(self, data: dict) -> list: """创建综合研判部分""" styles = self._get_styles() elements = [] elements.append(Paragraph("四、综合研判", styles['Heading1'])) elements.append(Spacer(1, 0.2*inch)) report = data.get('comprehensive_report', '') if report: # 截取前1000字 report_preview = report[:1000] + "..." if len(report) > 1000 else report elements.append(Paragraph(report_preview, styles['Small'])) else: elements.append(Paragraph("暂无综合研判数据", styles['Normal'])) elements.append(Spacer(1, 0.3*inch)) # 添加结束语 ending = Paragraph( "--- 报告结束 ---
" "本报告由智策AI系统自动生成
", styles['Normal'] ) elements.append(ending) return elements def _get_styles(self) -> dict: """获取样式""" styles = getSampleStyleSheet() # 自定义样式 custom_styles = { 'Title': ParagraphStyle( 'CustomTitle', parent=styles['Title'], fontName=self.chinese_font, fontSize=24, textColor=colors.HexColor('#667eea'), spaceAfter=30, alignment=TA_CENTER ), 'Heading1': ParagraphStyle( 'CustomHeading1', parent=styles['Heading1'], fontName=self.chinese_font, fontSize=16, textColor=colors.HexColor('#667eea'), spaceAfter=12, spaceBefore=12 ), 'Heading2': ParagraphStyle( 'CustomHeading2', parent=styles['Heading2'], fontName=self.chinese_font, fontSize=14, textColor=colors.HexColor('#764ba2'), spaceAfter=10, spaceBefore=10 ), 'Normal': ParagraphStyle( 'CustomNormal', parent=styles['Normal'], fontName=self.chinese_font, fontSize=11, leading=16, alignment=TA_JUSTIFY ), 'Small': ParagraphStyle( 'CustomSmall', parent=styles['Normal'], fontName=self.chinese_font, fontSize=9, leading=14, alignment=TA_LEFT ) } return custom_styles # 测试函数 if __name__ == "__main__": # 创建测试数据 test_data = { "success": True, "timestamp": "2024-01-15 10:30:00", "final_predictions": { "long_short": { "bullish": [ { "sector": "电子", "confidence": 8, "reason": "政策支持,资金持续流入", "risk": "估值偏高,注意回调风险" } ], "bearish": [] }, "rotation": { "current_strong": [], "potential": [], "declining": [] }, "heat": { "hottest": [ {"sector": "电子", "score": 95, "trend": "升温", "sustainability": "强"} ] }, "summary": { "market_view": "市场整体向好", "key_opportunity": "科技板块", "major_risk": "估值风险", "strategy": "积极配置科技" } }, "agents_analysis": {}, "comprehensive_report": "综合研判内容..." } generator = SectorStrategyPDFGenerator() output_path = generator.generate_pdf(test_data) print(f"测试PDF生成: {output_path}")