diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 334c8720..92bac701 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -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( diff --git a/backend/app/services/download_service.py b/backend/app/services/download_service.py index 26dd1018..7c0e23c8 100644 --- a/backend/app/services/download_service.py +++ b/backend/app/services/download_service.py @@ -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 diff --git a/backend/app/services/pdf_generator.py b/backend/app/services/pdf_generator.py index ef22afec..6300ad92 100644 --- a/backend/app/services/pdf_generator.py +++ b/backend/app/services/pdf_generator.py @@ -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))