update
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
"""
|
||||
宏观周期分析 - PDF报告生成模块
|
||||
生成康波周期 × 美林投资时钟 × 中国政策分析的完整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, 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
|
||||
import re
|
||||
|
||||
|
||||
class MacroCyclePDFGenerator:
|
||||
"""宏观周期分析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', # 黑体
|
||||
'/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', # Linux
|
||||
'/usr/share/fonts/truetype/droid/DroidSansFallbackFull.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")
|
||||
|
||||
doc = SimpleDocTemplate(
|
||||
output_path,
|
||||
pagesize=A4,
|
||||
rightMargin=0.5*inch,
|
||||
leftMargin=0.5*inch,
|
||||
topMargin=0.5*inch,
|
||||
bottomMargin=0.5*inch
|
||||
)
|
||||
|
||||
story = []
|
||||
|
||||
# 1. 标题页
|
||||
story.extend(self._create_title_page(result_data))
|
||||
story.append(PageBreak())
|
||||
|
||||
# 2. 首席宏观策略师综合研判(最重要,放最前面)
|
||||
story.extend(self._create_chief_section(result_data))
|
||||
story.append(PageBreak())
|
||||
|
||||
# 3. 康波周期分析
|
||||
story.extend(self._create_kondratieff_section(result_data))
|
||||
story.append(PageBreak())
|
||||
|
||||
# 4. 美林投资时钟分析
|
||||
story.extend(self._create_merrill_section(result_data))
|
||||
story.append(PageBreak())
|
||||
|
||||
# 5. 中国政策分析
|
||||
story.extend(self._create_policy_section(result_data))
|
||||
|
||||
# 6. 结束语
|
||||
story.extend(self._create_ending())
|
||||
|
||||
# 生成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 _clean_markdown(self, text: str) -> str:
|
||||
"""清理Markdown标记,转换为适合PDF的纯文本/HTML"""
|
||||
if not text:
|
||||
return ""
|
||||
# 移除markdown粗体 **text** → text
|
||||
text = re.sub(r'\*\*(.*?)\*\*', r'<b>\1</b>', text)
|
||||
# 移除markdown斜体 *text* → text
|
||||
text = re.sub(r'\*(.*?)\*', r'<i>\1</i>', text)
|
||||
# 移除markdown标题 ## → 空
|
||||
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
|
||||
# 移除markdown链接 [text](url) → text
|
||||
text = re.sub(r'\[(.*?)\]\(.*?\)', r'\1', text)
|
||||
# 移除markdown表格分隔线
|
||||
text = re.sub(r'\|[-:]+\|[-:| ]+\|', '', text)
|
||||
# 替换换行
|
||||
text = text.replace('\n', '<br/>')
|
||||
return text
|
||||
|
||||
def _split_text_to_paragraphs(self, text: str, styles: dict, max_chars: int = 0) -> list:
|
||||
"""将长文本分段为多个Paragraph,避免单段过长溢出"""
|
||||
elements = []
|
||||
if not text:
|
||||
return elements
|
||||
|
||||
if max_chars > 0 and len(text) > max_chars:
|
||||
text = text[:max_chars] + "...(更多内容请查看网页版完整报告)"
|
||||
|
||||
# 按段落分割
|
||||
paragraphs = text.split('\n\n')
|
||||
for para in paragraphs:
|
||||
para = para.strip()
|
||||
if not para:
|
||||
continue
|
||||
# 清理markdown
|
||||
cleaned = self._clean_markdown(para)
|
||||
if cleaned:
|
||||
try:
|
||||
elements.append(Paragraph(cleaned, styles['Small']))
|
||||
elements.append(Spacer(1, 0.08*inch))
|
||||
except Exception:
|
||||
# 如果格式化失败,用纯文本
|
||||
plain = re.sub(r'<[^>]+>', '', cleaned)
|
||||
elements.append(Paragraph(plain, styles['Small']))
|
||||
elements.append(Spacer(1, 0.08*inch))
|
||||
|
||||
return elements
|
||||
|
||||
def _create_title_page(self, data: dict) -> list:
|
||||
"""创建标题页"""
|
||||
styles = self._get_styles()
|
||||
elements = []
|
||||
|
||||
elements.append(Spacer(1, 1.5*inch))
|
||||
|
||||
# 主标题
|
||||
elements.append(Paragraph("宏观周期分析报告", styles['Title']))
|
||||
elements.append(Spacer(1, 0.3*inch))
|
||||
|
||||
# 副标题
|
||||
elements.append(Paragraph(
|
||||
"康波周期 × 美林投资时钟 × 中国政策分析",
|
||||
styles['Heading2']
|
||||
))
|
||||
elements.append(Spacer(1, 0.8*inch))
|
||||
|
||||
# 报告信息
|
||||
timestamp = data.get('timestamp', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
||||
info_text = f"""
|
||||
<para align=center>
|
||||
<b>生成时间:</b> {timestamp}<br/>
|
||||
<b>分析框架:</b> 三维周期定位系统<br/>
|
||||
<b>AI分析师:</b> 4位专业分析师协同研判<br/>
|
||||
<b>分析维度:</b> 康波长周期 · 美林中短周期 · 中国政策环境<br/>
|
||||
<b>数据来源:</b> AKShare宏观经济数据
|
||||
</para>
|
||||
"""
|
||||
elements.append(Paragraph(info_text, styles['Normal']))
|
||||
elements.append(Spacer(1, 0.5*inch))
|
||||
|
||||
# 分析师团队
|
||||
team_text = """
|
||||
<para align=center>
|
||||
<b>AI分析师团队:</b><br/>
|
||||
康波周期分析师 · 美林时钟分析师 · 中国政策分析师 · 首席宏观策略师
|
||||
</para>
|
||||
"""
|
||||
elements.append(Paragraph(team_text, styles['Normal']))
|
||||
elements.append(Spacer(1, 0.8*inch))
|
||||
|
||||
# 免责声明
|
||||
elements.append(Paragraph(
|
||||
"<para align=center><i>本报告由AI系统自动生成,仅供学习研究参考,不构成投资建议。<br/>"
|
||||
"周期理论是认知框架而非精确预测工具。投资有风险,入市需谨慎。</i></para>",
|
||||
styles['Small']
|
||||
))
|
||||
|
||||
return elements
|
||||
|
||||
def _create_chief_section(self, data: dict) -> list:
|
||||
"""创建首席宏观策略师综合研判部分"""
|
||||
styles = self._get_styles()
|
||||
elements = []
|
||||
|
||||
elements.append(Paragraph("一、首席宏观策略师 — 综合研判", styles['Heading1']))
|
||||
elements.append(Spacer(1, 0.15*inch))
|
||||
elements.append(Paragraph(
|
||||
"<i>整合康波周期、美林投资时钟、中国政策三个维度,构建周期仪表盘,给出最终综合投资策略。</i>",
|
||||
styles['Small']
|
||||
))
|
||||
elements.append(Spacer(1, 0.15*inch))
|
||||
|
||||
chief = data.get('agents_analysis', {}).get('chief', {})
|
||||
analysis = chief.get('analysis', '暂无分析结果')
|
||||
elements.extend(self._split_text_to_paragraphs(analysis, styles, max_chars=5000))
|
||||
|
||||
return elements
|
||||
|
||||
def _create_kondratieff_section(self, data: dict) -> list:
|
||||
"""创建康波周期分析部分"""
|
||||
styles = self._get_styles()
|
||||
elements = []
|
||||
|
||||
elements.append(Paragraph("二、康波周期分析 — 60年长周期定位", styles['Heading1']))
|
||||
elements.append(Spacer(1, 0.15*inch))
|
||||
elements.append(Paragraph(
|
||||
"<i>基于康德拉季耶夫长波理论(周金涛\"人生发财靠康波\"),判断当前处于第五轮信息技术康波的阶段位置。</i>",
|
||||
styles['Small']
|
||||
))
|
||||
elements.append(Spacer(1, 0.15*inch))
|
||||
|
||||
kondratieff = data.get('agents_analysis', {}).get('kondratieff', {})
|
||||
analysis = kondratieff.get('analysis', '暂无分析结果')
|
||||
elements.extend(self._split_text_to_paragraphs(analysis, styles, max_chars=5000))
|
||||
|
||||
return elements
|
||||
|
||||
def _create_merrill_section(self, data: dict) -> list:
|
||||
"""创建美林投资时钟分析部分"""
|
||||
styles = self._get_styles()
|
||||
elements = []
|
||||
|
||||
elements.append(Paragraph("三、美林投资时钟 — 中短周期定位", styles['Heading1']))
|
||||
elements.append(Spacer(1, 0.15*inch))
|
||||
elements.append(Paragraph(
|
||||
"<i>基于经济增长与通胀两大维度,结合中国政策方向(第三维度),判断当前处于美林时钟的哪个象限。</i>",
|
||||
styles['Small']
|
||||
))
|
||||
elements.append(Spacer(1, 0.15*inch))
|
||||
|
||||
merrill = data.get('agents_analysis', {}).get('merrill', {})
|
||||
analysis = merrill.get('analysis', '暂无分析结果')
|
||||
elements.extend(self._split_text_to_paragraphs(analysis, styles, max_chars=5000))
|
||||
|
||||
return elements
|
||||
|
||||
def _create_policy_section(self, data: dict) -> list:
|
||||
"""创建中国政策分析部分"""
|
||||
styles = self._get_styles()
|
||||
elements = []
|
||||
|
||||
elements.append(Paragraph("四、中国政策环境分析", styles['Heading1']))
|
||||
elements.append(Spacer(1, 0.15*inch))
|
||||
elements.append(Paragraph(
|
||||
"<i>深度分析货币政策、财政政策、产业政策、房地产政策,评估政策对周期的影响和投资机会。</i>",
|
||||
styles['Small']
|
||||
))
|
||||
elements.append(Spacer(1, 0.15*inch))
|
||||
|
||||
policy = data.get('agents_analysis', {}).get('policy', {})
|
||||
analysis = policy.get('analysis', '暂无分析结果')
|
||||
elements.extend(self._split_text_to_paragraphs(analysis, styles, max_chars=5000))
|
||||
|
||||
return elements
|
||||
|
||||
def _create_ending(self) -> list:
|
||||
"""创建结束语"""
|
||||
styles = self._get_styles()
|
||||
elements = []
|
||||
|
||||
elements.append(Spacer(1, 0.5*inch))
|
||||
elements.append(Paragraph(
|
||||
"<para align=center><i>--- 报告结束 ---<br/>"
|
||||
"本报告由宏观周期AI分析系统自动生成<br/>"
|
||||
"康波是罗盘,美林是航海图,政策是季风<br/>"
|
||||
"愿你在经济的海洋中,驶向属于自己的财富彼岸</i></para>",
|
||||
styles['Normal']
|
||||
))
|
||||
|
||||
return elements
|
||||
|
||||
def _get_styles(self) -> dict:
|
||||
"""获取样式"""
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
custom_styles = {
|
||||
'Title': ParagraphStyle(
|
||||
'MacroTitle',
|
||||
parent=styles['Title'],
|
||||
fontName=self.chinese_font,
|
||||
fontSize=26,
|
||||
textColor=colors.HexColor('#302b63'),
|
||||
spaceAfter=30,
|
||||
alignment=TA_CENTER
|
||||
),
|
||||
'Heading1': ParagraphStyle(
|
||||
'MacroHeading1',
|
||||
parent=styles['Heading1'],
|
||||
fontName=self.chinese_font,
|
||||
fontSize=16,
|
||||
textColor=colors.HexColor('#0f0c29'),
|
||||
spaceAfter=12,
|
||||
spaceBefore=12
|
||||
),
|
||||
'Heading2': ParagraphStyle(
|
||||
'MacroHeading2',
|
||||
parent=styles['Heading2'],
|
||||
fontName=self.chinese_font,
|
||||
fontSize=14,
|
||||
textColor=colors.HexColor('#302b63'),
|
||||
spaceAfter=10,
|
||||
spaceBefore=10,
|
||||
alignment=TA_CENTER
|
||||
),
|
||||
'Normal': ParagraphStyle(
|
||||
'MacroNormal',
|
||||
parent=styles['Normal'],
|
||||
fontName=self.chinese_font,
|
||||
fontSize=11,
|
||||
leading=16,
|
||||
alignment=TA_JUSTIFY
|
||||
),
|
||||
'Small': ParagraphStyle(
|
||||
'MacroSmall',
|
||||
parent=styles['Normal'],
|
||||
fontName=self.chinese_font,
|
||||
fontSize=9,
|
||||
leading=14,
|
||||
alignment=TA_LEFT
|
||||
)
|
||||
}
|
||||
|
||||
return custom_styles
|
||||
|
||||
|
||||
def generate_macro_cycle_markdown(result_data: dict) -> str:
|
||||
"""生成宏观周期分析的Markdown报告"""
|
||||
parts = []
|
||||
timestamp = result_data.get('timestamp', datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
||||
|
||||
parts.append("# 🧭 宏观周期分析报告\n")
|
||||
parts.append(f"**生成时间**: {timestamp}\n")
|
||||
parts.append("**分析框架**: 康波周期 × 美林投资时钟 × 中国政策分析\n")
|
||||
parts.append("---\n")
|
||||
|
||||
agents = result_data.get('agents_analysis', {})
|
||||
|
||||
# 首席策略师
|
||||
chief = agents.get('chief', {})
|
||||
if chief:
|
||||
parts.append("## 👔 一、首席宏观策略师 — 综合研判\n")
|
||||
parts.append(chief.get('analysis', '暂无分析结果'))
|
||||
parts.append("\n\n---\n")
|
||||
|
||||
# 康波周期
|
||||
kondratieff = agents.get('kondratieff', {})
|
||||
if kondratieff:
|
||||
parts.append("## 🌊 二、康波周期分析 — 60年长周期定位\n")
|
||||
parts.append(kondratieff.get('analysis', '暂无分析结果'))
|
||||
parts.append("\n\n---\n")
|
||||
|
||||
# 美林时钟
|
||||
merrill = agents.get('merrill', {})
|
||||
if merrill:
|
||||
parts.append("## ⏰ 三、美林投资时钟 — 中短周期定位\n")
|
||||
parts.append(merrill.get('analysis', '暂无分析结果'))
|
||||
parts.append("\n\n---\n")
|
||||
|
||||
# 政策分析
|
||||
policy = agents.get('policy', {})
|
||||
if policy:
|
||||
parts.append("## 🏛️ 四、中国政策环境分析\n")
|
||||
parts.append(policy.get('analysis', '暂无分析结果'))
|
||||
parts.append("\n\n---\n")
|
||||
|
||||
# 免责声明
|
||||
parts.append("\n> ⚠️ **免责声明**: 本报告由AI系统自动生成,仅供学习研究参考,不构成投资建议。")
|
||||
parts.append("周期理论是认知框架而非精确预测工具。投资有风险,入市需谨慎。\n")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# 测试
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("测试宏观周期PDF生成器")
|
||||
print("=" * 60)
|
||||
|
||||
test_data = {
|
||||
"success": True,
|
||||
"timestamp": "2026-02-27 14:00:00",
|
||||
"agents_analysis": {
|
||||
"chief": {"analysis": "综合研判测试内容..."},
|
||||
"kondratieff": {"analysis": "康波分析测试内容..."},
|
||||
"merrill": {"analysis": "美林时钟测试内容..."},
|
||||
"policy": {"analysis": "政策分析测试内容..."},
|
||||
}
|
||||
}
|
||||
|
||||
generator = MacroCyclePDFGenerator()
|
||||
output_path = generator.generate_pdf(test_data)
|
||||
print(f"测试PDF生成: {output_path}")
|
||||
Reference in New Issue
Block a user