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:
|
if not reports_to_download:
|
||||||
raise HTTPException(status_code=404, detail="No reports found for selected analysts")
|
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
|
# Single report - return PDF
|
||||||
if len(reports_to_download) == 1:
|
if len(reports_to_download) == 1:
|
||||||
pdf_bytes, filename = download_service.create_single_pdf(
|
pdf_bytes, filename = download_service.create_single_pdf(
|
||||||
|
|
@ -252,6 +256,8 @@ async def download_reports(request: DownloadRequest):
|
||||||
ticker=request.ticker,
|
ticker=request.ticker,
|
||||||
analysis_date=request.analysis_date,
|
analysis_date=request.analysis_date,
|
||||||
report_content=reports_to_download[0]["report_content"],
|
report_content=reports_to_download[0]["report_content"],
|
||||||
|
price_data=price_data,
|
||||||
|
price_stats=price_stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -267,6 +273,8 @@ async def download_reports(request: DownloadRequest):
|
||||||
ticker=request.ticker,
|
ticker=request.ticker,
|
||||||
analysis_date=request.analysis_date,
|
analysis_date=request.analysis_date,
|
||||||
reports=reports_to_download,
|
reports=reports_to_download,
|
||||||
|
price_data=price_data,
|
||||||
|
price_stats=price_stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,8 @@ class DownloadService:
|
||||||
ticker: str,
|
ticker: str,
|
||||||
analysis_date: str,
|
analysis_date: str,
|
||||||
report_content: str,
|
report_content: str,
|
||||||
|
price_data: list = None,
|
||||||
|
price_stats: dict = None,
|
||||||
) -> tuple[bytes, str]:
|
) -> tuple[bytes, str]:
|
||||||
"""
|
"""
|
||||||
Create a PDF for a single analyst report
|
Create a PDF for a single analyst report
|
||||||
|
|
@ -71,6 +73,8 @@ class DownloadService:
|
||||||
ticker: Stock ticker symbol
|
ticker: Stock ticker symbol
|
||||||
analysis_date: Date of analysis (YYYY-MM-DD)
|
analysis_date: Date of analysis (YYYY-MM-DD)
|
||||||
report_content: Markdown formatted report content
|
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:
|
Returns:
|
||||||
Tuple of (PDF bytes, filename)
|
Tuple of (PDF bytes, filename)
|
||||||
|
|
@ -81,6 +85,8 @@ class DownloadService:
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
analysis_date=analysis_date,
|
analysis_date=analysis_date,
|
||||||
report_content=report_content,
|
report_content=report_content,
|
||||||
|
price_data=price_data,
|
||||||
|
price_stats=price_stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate filename with English name: TICKER_English_Name_DATE.pdf
|
# Generate filename with English name: TICKER_English_Name_DATE.pdf
|
||||||
|
|
@ -94,6 +100,8 @@ class DownloadService:
|
||||||
ticker: str,
|
ticker: str,
|
||||||
analysis_date: str,
|
analysis_date: str,
|
||||||
reports: List[Dict[str, str]],
|
reports: List[Dict[str, str]],
|
||||||
|
price_data: list = None,
|
||||||
|
price_stats: dict = None,
|
||||||
) -> tuple[bytes, str]:
|
) -> tuple[bytes, str]:
|
||||||
"""
|
"""
|
||||||
Create a ZIP file containing multiple analyst report PDFs
|
Create a ZIP file containing multiple analyst report PDFs
|
||||||
|
|
@ -102,6 +110,8 @@ class DownloadService:
|
||||||
ticker: Stock ticker symbol
|
ticker: Stock ticker symbol
|
||||||
analysis_date: Date of analysis (YYYY-MM-DD)
|
analysis_date: Date of analysis (YYYY-MM-DD)
|
||||||
reports: List of dicts with keys 'analyst_name' and 'report_content'
|
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:
|
Returns:
|
||||||
Tuple of (ZIP bytes, filename)
|
Tuple of (ZIP bytes, filename)
|
||||||
|
|
@ -124,6 +134,8 @@ class DownloadService:
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
analysis_date=analysis_date,
|
analysis_date=analysis_date,
|
||||||
report_content=report_content,
|
report_content=report_content,
|
||||||
|
price_data=price_data,
|
||||||
|
price_stats=price_stats,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add to ZIP with English filename
|
# Add to ZIP with English filename
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,8 @@ class PDFGenerator:
|
||||||
ticker: str,
|
ticker: str,
|
||||||
analysis_date: str,
|
analysis_date: str,
|
||||||
report_content: str,
|
report_content: str,
|
||||||
|
price_data: list = None,
|
||||||
|
price_stats: dict = None,
|
||||||
) -> bytes:
|
) -> bytes:
|
||||||
"""
|
"""
|
||||||
Generate a PDF from analyst report content
|
Generate a PDF from analyst report content
|
||||||
|
|
@ -177,6 +179,8 @@ class PDFGenerator:
|
||||||
ticker: Stock ticker symbol
|
ticker: Stock ticker symbol
|
||||||
analysis_date: Date of analysis
|
analysis_date: Date of analysis
|
||||||
report_content: Markdown formatted report content
|
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:
|
Returns:
|
||||||
PDF file content as bytes
|
PDF file content as bytes
|
||||||
|
|
@ -247,6 +251,115 @@ class PDFGenerator:
|
||||||
allowWidows=0,
|
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
|
# Add title
|
||||||
title = f"{analyst_name}"
|
title = f"{analyst_name}"
|
||||||
elements.append(Paragraph(title, title_style))
|
elements.append(Paragraph(title, title_style))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue