feat: add price information cover page to analyst PDF reports
- Page 1 now shows price statistics (growth rate, duration, start/end prices) - Page 1 also includes last 5 trading days data - Page 2+ contains the original analyst insights - Updates pdf_generator.py, download_service.py, and routes.py
This commit is contained in:
parent
11cda2acaf
commit
9ceff4cf9b
|
|
@ -245,6 +245,10 @@ async def download_reports(request: DownloadRequest):
|
|||
if not reports_to_download:
|
||||
raise HTTPException(status_code=404, detail="No reports found for selected analysts")
|
||||
|
||||
# Extract price data for cover page
|
||||
price_data = result.get("price_data")
|
||||
price_stats = result.get("price_stats")
|
||||
|
||||
# Single report - return PDF
|
||||
if len(reports_to_download) == 1:
|
||||
pdf_bytes, filename = download_service.create_single_pdf(
|
||||
|
|
@ -252,6 +256,8 @@ async def download_reports(request: DownloadRequest):
|
|||
ticker=request.ticker,
|
||||
analysis_date=request.analysis_date,
|
||||
report_content=reports_to_download[0]["report_content"],
|
||||
price_data=price_data,
|
||||
price_stats=price_stats,
|
||||
)
|
||||
|
||||
return Response(
|
||||
|
|
@ -267,6 +273,8 @@ async def download_reports(request: DownloadRequest):
|
|||
ticker=request.ticker,
|
||||
analysis_date=request.analysis_date,
|
||||
reports=reports_to_download,
|
||||
price_data=price_data,
|
||||
price_stats=price_stats,
|
||||
)
|
||||
|
||||
return Response(
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ class DownloadService:
|
|||
ticker: str,
|
||||
analysis_date: str,
|
||||
report_content: str,
|
||||
price_data: list = None,
|
||||
price_stats: dict = None,
|
||||
) -> tuple[bytes, str]:
|
||||
"""
|
||||
Create a PDF for a single analyst report
|
||||
|
|
@ -71,6 +73,8 @@ class DownloadService:
|
|||
ticker: Stock ticker symbol
|
||||
analysis_date: Date of analysis (YYYY-MM-DD)
|
||||
report_content: Markdown formatted report content
|
||||
price_data: Optional list of price data for cover page
|
||||
price_stats: Optional price statistics for cover page
|
||||
|
||||
Returns:
|
||||
Tuple of (PDF bytes, filename)
|
||||
|
|
@ -81,6 +85,8 @@ class DownloadService:
|
|||
ticker=ticker,
|
||||
analysis_date=analysis_date,
|
||||
report_content=report_content,
|
||||
price_data=price_data,
|
||||
price_stats=price_stats,
|
||||
)
|
||||
|
||||
# Generate filename with English name: TICKER_English_Name_DATE.pdf
|
||||
|
|
@ -94,6 +100,8 @@ class DownloadService:
|
|||
ticker: str,
|
||||
analysis_date: str,
|
||||
reports: List[Dict[str, str]],
|
||||
price_data: list = None,
|
||||
price_stats: dict = None,
|
||||
) -> tuple[bytes, str]:
|
||||
"""
|
||||
Create a ZIP file containing multiple analyst report PDFs
|
||||
|
|
@ -102,6 +110,8 @@ class DownloadService:
|
|||
ticker: Stock ticker symbol
|
||||
analysis_date: Date of analysis (YYYY-MM-DD)
|
||||
reports: List of dicts with keys 'analyst_name' and 'report_content'
|
||||
price_data: Optional list of price data for cover page
|
||||
price_stats: Optional price statistics for cover page
|
||||
|
||||
Returns:
|
||||
Tuple of (ZIP bytes, filename)
|
||||
|
|
@ -124,6 +134,8 @@ class DownloadService:
|
|||
ticker=ticker,
|
||||
analysis_date=analysis_date,
|
||||
report_content=report_content,
|
||||
price_data=price_data,
|
||||
price_stats=price_stats,
|
||||
)
|
||||
|
||||
# Add to ZIP with English filename
|
||||
|
|
|
|||
|
|
@ -168,6 +168,8 @@ class PDFGenerator:
|
|||
ticker: str,
|
||||
analysis_date: str,
|
||||
report_content: str,
|
||||
price_data: list = None,
|
||||
price_stats: dict = None,
|
||||
) -> bytes:
|
||||
"""
|
||||
Generate a PDF from analyst report content
|
||||
|
|
@ -177,6 +179,8 @@ class PDFGenerator:
|
|||
ticker: Stock ticker symbol
|
||||
analysis_date: Date of analysis
|
||||
report_content: Markdown formatted report content
|
||||
price_data: Optional list of price data dicts with Date, Open, High, Low, Close, Volume
|
||||
price_stats: Optional dict with growth_rate, duration_days, start_date, end_date, start_price, end_price
|
||||
|
||||
Returns:
|
||||
PDF file content as bytes
|
||||
|
|
@ -247,6 +251,115 @@ class PDFGenerator:
|
|||
allowWidows=0,
|
||||
)
|
||||
|
||||
# === PAGE 1: Price Information (if price data is provided) ===
|
||||
if price_stats and price_data:
|
||||
# Page 1 Title
|
||||
price_title = f"{ticker} 價格資訊"
|
||||
elements.append(Paragraph(price_title, title_style))
|
||||
elements.append(Spacer(1, 0.3*cm))
|
||||
|
||||
# Analysis date
|
||||
elements.append(Paragraph(f"分析日期:{analysis_date}", subtitle_style))
|
||||
elements.append(Spacer(1, 0.8*cm))
|
||||
|
||||
# Price statistics style
|
||||
stat_style = ParagraphStyle(
|
||||
'StatStyle',
|
||||
parent=styles['Normal'],
|
||||
fontName=self.primary_font,
|
||||
fontSize=12,
|
||||
leading=18,
|
||||
textColor=HexColor('#333333'),
|
||||
spaceAfter=6,
|
||||
wordWrap='CJK',
|
||||
)
|
||||
|
||||
stat_label_style = ParagraphStyle(
|
||||
'StatLabelStyle',
|
||||
parent=styles['Normal'],
|
||||
fontName=self.primary_font,
|
||||
fontSize=10,
|
||||
textColor=HexColor('#666666'),
|
||||
spaceAfter=2,
|
||||
wordWrap='CJK',
|
||||
)
|
||||
|
||||
stat_value_style = ParagraphStyle(
|
||||
'StatValueStyle',
|
||||
parent=styles['Normal'],
|
||||
fontName=self.primary_font,
|
||||
fontSize=16,
|
||||
textColor=HexColor('#1a1a1a'),
|
||||
spaceAfter=12,
|
||||
wordWrap='CJK',
|
||||
)
|
||||
|
||||
# Growth rate with color
|
||||
growth_rate = price_stats.get('growth_rate', 0)
|
||||
growth_color = '#22c55e' if growth_rate >= 0 else '#ef4444' # green/red
|
||||
growth_text = f"+{growth_rate:.2f}%" if growth_rate >= 0 else f"{growth_rate:.2f}%"
|
||||
|
||||
growth_value_style = ParagraphStyle(
|
||||
'GrowthValueStyle',
|
||||
parent=stat_value_style,
|
||||
fontSize=20,
|
||||
textColor=HexColor(growth_color),
|
||||
)
|
||||
|
||||
# Add price statistics
|
||||
elements.append(Paragraph("總報酬率", stat_label_style))
|
||||
elements.append(Paragraph(growth_text, growth_value_style))
|
||||
elements.append(Spacer(1, 0.3*cm))
|
||||
|
||||
duration_days = price_stats.get('duration_days', 0)
|
||||
elements.append(Paragraph("分析期間", stat_label_style))
|
||||
elements.append(Paragraph(f"{duration_days} 天", stat_value_style))
|
||||
|
||||
start_date = price_stats.get('start_date', 'N/A')
|
||||
end_date = price_stats.get('end_date', 'N/A')
|
||||
elements.append(Paragraph("日期區間", stat_label_style))
|
||||
elements.append(Paragraph(f"{start_date} ~ {end_date}", stat_style))
|
||||
elements.append(Spacer(1, 0.3*cm))
|
||||
|
||||
start_price = price_stats.get('start_price', 0)
|
||||
end_price = price_stats.get('end_price', 0)
|
||||
elements.append(Paragraph("起始價格", stat_label_style))
|
||||
elements.append(Paragraph(f"${start_price:.2f}", stat_value_style))
|
||||
|
||||
elements.append(Paragraph("結束價格", stat_label_style))
|
||||
elements.append(Paragraph(f"${end_price:.2f}", stat_value_style))
|
||||
|
||||
# Add latest price data summary if available
|
||||
if price_data and len(price_data) > 0:
|
||||
elements.append(Spacer(1, 0.5*cm))
|
||||
elements.append(Paragraph("最近交易數據", heading_style))
|
||||
elements.append(Spacer(1, 0.2*cm))
|
||||
|
||||
# Show last 5 trading days
|
||||
recent_data = price_data[-5:] if len(price_data) >= 5 else price_data
|
||||
for day in reversed(recent_data):
|
||||
date = day.get('Date', 'N/A')
|
||||
close = day.get('Close', 0)
|
||||
adj_close = day.get('Adj Close', close)
|
||||
volume = day.get('Volume', 0)
|
||||
|
||||
# Format volume
|
||||
if volume >= 1000000000:
|
||||
vol_str = f"{volume/1000000000:.2f}B"
|
||||
elif volume >= 1000000:
|
||||
vol_str = f"{volume/1000000:.2f}M"
|
||||
elif volume >= 1000:
|
||||
vol_str = f"{volume/1000:.2f}K"
|
||||
else:
|
||||
vol_str = str(volume)
|
||||
|
||||
day_text = f"{date}:收盤 ${adj_close:.2f},成交量 {vol_str}"
|
||||
elements.append(Paragraph(day_text, stat_style))
|
||||
|
||||
# Page break before analyst content
|
||||
elements.append(PageBreak())
|
||||
|
||||
# === PAGE 2+: Analyst Report Content ===
|
||||
# Add title
|
||||
title = f"{analyst_name}"
|
||||
elements.append(Paragraph(title, title_style))
|
||||
|
|
|
|||
Loading…
Reference in New Issue