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