Spaces:
Sleeping
Sleeping
| # modules/analysis_pipeline.py | |
| import os | |
| import asyncio | |
| import pandas as pd | |
| from datetime import datetime | |
| import google.generativeai as genai | |
| from dotenv import load_dotenv | |
| from .api_clients import AlphaVantageClient, NewsAPIClient, MarketauxClient, get_price_history | |
| import time | |
| # Load environment variables and configure AI | |
| load_dotenv() | |
| genai.configure(api_key=os.getenv("GEMINI_API_KEY")) | |
| MODEL_NAME = os.getenv("GEMINI_MODEL", "gemini-2.5-flash") | |
| # Define the analysis pipeline class | |
| class StockAnalysisPipeline: | |
| """Pipeline for generating comprehensive stock analysis reports""" | |
| def __init__(self, symbol): | |
| """Initialize the pipeline with a stock symbol""" | |
| self.symbol = symbol.upper() # Convert to uppercase | |
| self.company_data = {} | |
| self.analysis_results = {} | |
| self.ai_model = genai.GenerativeModel(model_name=MODEL_NAME) | |
| async def run_analysis(self): | |
| """Run the full analysis pipeline in an interleaved pattern""" | |
| print(f"Starting analysis pipeline for {self.symbol}...") | |
| # 1. Get company overview and financial statements first | |
| await self._get_company_overview() | |
| if hasattr(self, 'company_name'): | |
| print(f"Analyzing {self.symbol} ({self.company_name})") | |
| else: | |
| self.company_name = self.symbol | |
| print(f"Analyzing {self.symbol}") | |
| # 2. Get and analyze financial statements | |
| print("Getting financial data...") | |
| await self._get_financial_statements() | |
| # 3. Run financial health analysis with Gemini | |
| print("Analyzing financial health...") | |
| self.analysis_results['financial_health'] = await self._analyze_financial_health() | |
| # 4. Get and analyze market news and sentiment | |
| print("Getting news and sentiment data...") | |
| await self._get_market_sentiment_and_news() | |
| # 5. Run news sentiment analysis with Gemini | |
| print("Analyzing news and sentiment...") | |
| self.analysis_results['news_sentiment'] = await self._analyze_news_sentiment() | |
| # 6. Get quote data and price history | |
| print("Getting quote and price data...") | |
| await self._get_analyst_ratings() | |
| await self._get_price_data() | |
| # 7. Run expert opinion analysis with Gemini | |
| print("Analyzing market data...") | |
| self.analysis_results['expert_opinion'] = await self._analyze_expert_opinion() | |
| # 8. Create final summary and recommendation | |
| print("Creating final summary and recommendation...") | |
| self.analysis_results['summary'] = await self._create_summary() | |
| # 9. Return the complete analysis | |
| return { | |
| 'symbol': self.symbol, | |
| 'company_name': self.company_name, | |
| 'analysis': self.analysis_results, | |
| 'price_data': self.company_data.get('price_data', {}), | |
| 'overview': self.company_data.get('overview', {}) | |
| } | |
| async def _get_company_overview(self): | |
| """Get company overview information""" | |
| self.company_data['overview'] = await AlphaVantageClient.get_company_overview(self.symbol) | |
| if self.company_data['overview'] and 'Name' in self.company_data['overview']: | |
| self.company_name = self.company_data['overview']['Name'] | |
| else: | |
| self.company_name = self.symbol | |
| print(f"Retrieved company overview for {self.symbol}") | |
| async def _get_financial_statements(self): | |
| """Get company financial statements""" | |
| # Run these in parallel | |
| income_stmt_task = AlphaVantageClient.get_income_statement(self.symbol) | |
| balance_sheet_task = AlphaVantageClient.get_balance_sheet(self.symbol) | |
| cash_flow_task = AlphaVantageClient.get_cash_flow(self.symbol) | |
| # Wait for all tasks to complete | |
| results = await asyncio.gather( | |
| income_stmt_task, | |
| balance_sheet_task, | |
| cash_flow_task | |
| ) | |
| # Store results | |
| self.company_data['income_statement'] = results[0] | |
| self.company_data['balance_sheet'] = results[1] | |
| self.company_data['cash_flow'] = results[2] | |
| print(f"Retrieved financial statements for {self.symbol}") | |
| async def _get_market_sentiment_and_news(self): | |
| """Get market sentiment and news about the company""" | |
| # Get news from multiple sources in parallel | |
| alpha_news_task = AlphaVantageClient.get_news_sentiment(self.symbol) | |
| news_api_task = NewsAPIClient.get_company_news(self.company_name if hasattr(self, 'company_name') else self.symbol) | |
| marketaux_task = MarketauxClient.get_company_news(self.symbol) | |
| # Wait for all tasks to complete | |
| results = await asyncio.gather( | |
| alpha_news_task, | |
| news_api_task, | |
| marketaux_task | |
| ) | |
| # Store results | |
| self.company_data['alpha_news'] = results[0] | |
| self.company_data['news_api'] = results[1] | |
| self.company_data['marketaux'] = results[2] | |
| print(f"Retrieved news and sentiment for {self.symbol}") | |
| async def _get_analyst_ratings(self): | |
| """Get current stock quotes instead of analyst ratings""" | |
| self.company_data['quote_data'] = await AlphaVantageClient.get_global_quote(self.symbol) | |
| print(f"Retrieved quote data for {self.symbol}") | |
| async def _get_price_data(self): | |
| """Get historical price data""" | |
| # Get price data for different time periods | |
| periods = ['1_month', '3_months', '1_year'] | |
| price_data = {} | |
| # Sử dụng phương thức đồng bộ thông thường vì get_price_history không còn async | |
| for period in periods: | |
| price_data[period] = get_price_history(self.symbol, period) | |
| self.company_data['price_data'] = price_data | |
| print(f"Retrieved price history for {self.symbol}") | |
| async def _analyze_financial_health(self): | |
| """Analyze company's financial health using AI""" | |
| # Add a small delay before API call to Gemini to avoid rate limiting | |
| await asyncio.sleep(1) | |
| # Prepare financial data for the AI | |
| financial_data = { | |
| 'overview': self.company_data.get('overview', {}), | |
| 'income_statement': self.company_data.get('income_statement', {}), | |
| 'balance_sheet': self.company_data.get('balance_sheet', {}), | |
| 'cash_flow': self.company_data.get('cash_flow', {}) | |
| } | |
| # Create prompt for financial analysis | |
| prompt = f""" | |
| You are a senior financial analyst. Analyze the financial health of {self.symbol} based on the following data: | |
| {financial_data} | |
| Provide a detailed analysis covering: | |
| 1. Overall financial condition overview | |
| 2. Key financial ratios analysis (P/E, ROE, Debt/Equity, etc.) | |
| 3. Revenue and profit growth assessment | |
| 4. Cash flow and liquidity assessment | |
| 5. Key financial strengths and weaknesses | |
| Format requirements: | |
| - Write in professional, concise financial reporting style | |
| - Use Markdown formatting with appropriate headers and bullet points | |
| - DO NOT include any introductory phrases like "Hello," "I'm happy to provide," etc. | |
| - DO NOT include any concluding phrases | |
| - Present only factual analysis based on the data | |
| - Present the information directly and objectively | |
| - Prefer using the correct currency text instead of the symbol. For example, use USD instead of $ | |
| """ | |
| # Get AI response | |
| response = self.ai_model.generate_content(prompt) | |
| return response.text | |
| async def _analyze_news_sentiment(self): | |
| """Analyze news and market sentiment using AI""" | |
| # Add a small delay before API call to Gemini to avoid rate limiting | |
| await asyncio.sleep(1) | |
| # Prepare news data for the AI | |
| news_data = { | |
| 'alpha_news': self.company_data.get('alpha_news', {}), | |
| 'news_api': self.company_data.get('news_api', {}), | |
| 'marketaux': self.company_data.get('marketaux', {}) | |
| } | |
| # Create prompt for news analysis | |
| prompt = f""" | |
| You are a market analyst. Analyze news and market sentiment about {self.symbol} based on the following data: | |
| {news_data} | |
| Provide a detailed analysis covering: | |
| 1. Summary of key recent news about the company | |
| 2. Important events that could impact stock price | |
| 3. Overall market sentiment analysis (positive/negative/neutral) | |
| 4. Risk factors identified in news | |
| Format requirements: | |
| - Write in professional, concise financial reporting style | |
| - Use Markdown formatting with appropriate headers and bullet points | |
| - DO NOT include any introductory phrases like "Hello," "I'm happy to provide," etc. | |
| - DO NOT include any concluding phrases | |
| - Present only factual analysis based on the data | |
| - Present the information directly and objectively | |
| - Prefer using the correct currency text instead of the symbol. For example, use USD instead of $ | |
| """ | |
| # Get AI response | |
| response = self.ai_model.generate_content(prompt) | |
| return response.text | |
| async def _analyze_expert_opinion(self): | |
| """Analyze current stock quote and price data""" | |
| # Add a small delay before API call to Gemini to avoid rate limiting | |
| await asyncio.sleep(1) | |
| # Prepare data for the AI | |
| quote_data = self.company_data.get('quote_data', {}) | |
| price_data = self.company_data.get('price_data', {}) | |
| overview = self.company_data.get('overview', {}) | |
| # Create prompt for market analysis with chart descriptions | |
| chart_descriptions = [] | |
| # Add descriptions for each timeframe chart | |
| for period, period_name in [('1_month', 'last month'), ('3_months', 'last 3 months'), ('1_year', 'last year')]: | |
| if period in price_data and 'values' in price_data[period] and price_data[period]['values']: | |
| values = price_data[period]['values'] | |
| # Get first and last price for the period | |
| first_price = float(values[-1]['close']) # Reversed order in the API | |
| last_price = float(values[0]['close']) | |
| price_change = ((last_price - first_price) / first_price) * 100 | |
| # Calculate volatility (standard deviation) | |
| if len(values) > 1: | |
| closes = [float(day['close']) for day in values] | |
| volatility = pd.Series(closes).pct_change().std() * 100 # Convert to percentage | |
| else: | |
| volatility = 0.0 | |
| # Detect trend (simple linear regression slope) | |
| if len(values) > 2: | |
| closes = [float(day['close']) for day in values] | |
| dates = list(range(len(closes))) | |
| slope = pd.Series(closes).corr(pd.Series(dates)) | |
| trend = "strong upward" if slope > 0.7 else \ | |
| "upward" if slope > 0.3 else \ | |
| "relatively flat" if slope > -0.3 else \ | |
| "downward" if slope > -0.7 else \ | |
| "strong downward" | |
| else: | |
| trend = "insufficient data to determine" | |
| # Get price range | |
| prices = [float(day['close']) for day in values] | |
| min_price = min(prices) if prices else 0 | |
| max_price = max(prices) if prices else 0 | |
| price_range = max_price - min_price | |
| # Find significant price movements | |
| significant_changes = [] | |
| if len(values) > 5: | |
| for i in range(1, len(values)): | |
| prev_close = float(values[i]['close']) | |
| curr_close = float(values[i-1]['close']) | |
| daily_change = ((curr_close - prev_close) / prev_close) * 100 | |
| if abs(daily_change) > 2.0: # More than 2% daily change | |
| date = values[i-1]['datetime'] | |
| significant_changes.append(f"On {date}, there was a {daily_change:.2f}% {'increase' if daily_change > 0 else 'decrease'}") | |
| # Limit to 3 most significant changes | |
| significant_changes = significant_changes[:3] | |
| # Create chart description | |
| description = f""" | |
| Chart for {period_name}: | |
| - Overall trend: {trend} | |
| - Price change: {price_change:.2f}% ({first_price:.2f} to {last_price:.2f}) | |
| - Volatility: {volatility:.2f}% | |
| - Price range: {min_price:.2f} to {max_price:.2f} (range: {price_range:.2f}) | |
| """ | |
| # Add significant changes if any | |
| if significant_changes: | |
| description += "- Significant price movements:\n * " + "\n * ".join(significant_changes) | |
| chart_descriptions.append(description) | |
| # Create prompt for market analysis | |
| prompt = f""" | |
| You are a stock market analyst. Analyze the current stock data for {self.symbol} based on the following information: | |
| Current Quote Data: {quote_data} | |
| Company Overview: {overview} | |
| Chart Analysis: | |
| {chr(10).join(chart_descriptions)} | |
| Provide a detailed analysis covering: | |
| 1. Current stock performance overview | |
| 2. Price trends and technical indicators based on the charts | |
| 3. Price comparison with sector averages and benchmarks | |
| 4. Potential price movement factors | |
| 5. Technical analysis of support and resistance levels | |
| 6. Trading volume patterns and their significance | |
| Format requirements: | |
| - Write in professional, concise financial reporting style | |
| - Use Markdown formatting with appropriate headers and bullet points | |
| - DO NOT include any introductory phrases like "Hello," "I'm happy to provide," etc. | |
| - DO NOT include any concluding phrases | |
| - Present only factual analysis based on the data | |
| - Present the information directly and objectively | |
| - Prefer using the correct currency text instead of the symbol. For example, use USD instead of $ | |
| """ | |
| # Get AI response | |
| response = self.ai_model.generate_content(prompt) | |
| return response.text | |
| async def _create_summary(self): | |
| """Create a comprehensive summary and investment recommendation""" | |
| # Add a small delay before API call to Gemini to avoid rate limiting | |
| await asyncio.sleep(1) | |
| # Combine all analyses | |
| combined_analysis = { | |
| 'financial_health': self.analysis_results.get('financial_health', ''), | |
| 'news_sentiment': self.analysis_results.get('news_sentiment', ''), | |
| 'expert_opinion': self.analysis_results.get('expert_opinion', '') | |
| } | |
| # Add overview data | |
| overview = self.company_data.get('overview', {}) | |
| # Create prompt for final summary | |
| prompt = f""" | |
| You are an investment advisor. Based on the detailed analyses below for {self.symbol} ({overview.get('Name', '')}), | |
| synthesize a final report and investment recommendation: | |
| === Company Basic Information === | |
| {overview} | |
| === Financial Health Analysis === | |
| {combined_analysis['financial_health']} | |
| === News and Market Sentiment Analysis === | |
| {combined_analysis['news_sentiment']} | |
| === Market Analysis === | |
| {combined_analysis['expert_opinion']} | |
| Provide: | |
| 1. Brief company and industry overview | |
| 2. Summary of key strengths and weaknesses from the analyses above | |
| 3. Risk and opportunity assessment | |
| 4. Investment recommendation (BULLISH/BEARISH/NEUTRAL) with rationale | |
| 5. Key factors to monitor going forward | |
| Format requirements: | |
| - Write in professional, concise financial reporting style | |
| - Use Markdown formatting with appropriate headers and bullet points | |
| - DO NOT include any introductory phrases like "Hello," "I'm happy to provide," etc. | |
| - DO NOT include any concluding phrases or sign-offs | |
| - Present the report directly and objectively | |
| - The report should be comprehensive but concise | |
| - Prefer using the correct currency text instead of the symbol. For example, use USD instead of $ | |
| """ | |
| # Get AI response | |
| response = self.ai_model.generate_content(prompt) | |
| return response.text | |
| # Main function to run the pipeline | |
| async def run_analysis_pipeline(symbol): | |
| """Run the complete stock analysis pipeline for a given symbol""" | |
| pipeline = StockAnalysisPipeline(symbol) | |
| return await pipeline.run_analysis() | |
| # Function to generate HTML report from analysis results | |
| import altair as alt | |
| import base64 | |
| import io | |
| from PIL import Image | |
| # Function to convert Altair chart to base64 image | |
| def chart_to_base64(chart): | |
| """Convert Altair chart to base64-encoded PNG image""" | |
| # Save chart as PNG | |
| import io | |
| import base64 | |
| from PIL import Image | |
| try: | |
| # Sử dụng Altair's save method | |
| import tempfile | |
| # Tạo file tạm thời để lưu chart | |
| with tempfile.NamedTemporaryFile(suffix='.png') as tmpfile: | |
| # Lưu biểu đồ dưới dạng PNG | |
| chart.save(tmpfile.name) | |
| # Đọc file PNG và mã hóa base64 | |
| with open(tmpfile.name, 'rb') as f: | |
| image_bytes = f.read() | |
| base64_image = base64.b64encode(image_bytes).decode('utf-8') | |
| return base64_image | |
| except Exception as e: | |
| # Backup method - tạo hình ảnh đơn giản với thông tin chart | |
| try: | |
| print(f"Chart rendering failed: {str(e)}") | |
| # Tạo một hình ảnh thay thế đơn giản | |
| width, height = 800, 400 | |
| # Tạo hình ảnh trắng | |
| image = Image.new("RGB", (width, height), (255, 255, 255)) | |
| # Lưu hình ảnh vào buffer | |
| buffer = io.BytesIO() | |
| image.save(buffer, format="PNG") | |
| image_bytes = buffer.getvalue() | |
| # Mã hóa base64 | |
| base64_image = base64.b64encode(image_bytes).decode('utf-8') | |
| return base64_image | |
| except: | |
| return None | |
| # Function to create price chart from price data | |
| def create_price_chart(price_data, period, symbol): | |
| """Create a price chart from the price data""" | |
| if 'values' not in price_data: | |
| return None | |
| df = pd.DataFrame(price_data['values']) | |
| if df.empty: | |
| return None | |
| df['datetime'] = pd.to_datetime(df['datetime']) | |
| df['close'] = pd.to_numeric(df['close']) | |
| # Map period to title | |
| title_map = { | |
| '1_month': f'{symbol} - Price over the last month', | |
| '3_months': f'{symbol} - Price over the last 3 months', | |
| '1_year': f'{symbol} - Price over the last year' | |
| } | |
| # Create the Altair chart | |
| chart = alt.Chart(df).mark_line(color='#3498db').encode( | |
| x=alt.X('datetime:T', title='Time'), | |
| y=alt.Y('close:Q', title='Closing Price', scale=alt.Scale(zero=False)), | |
| ).properties( | |
| title=title_map.get(period, f'Stock price ({period})'), | |
| width=800, | |
| height=400 | |
| ) | |
| # Add a point for the last day | |
| last_point = alt.Chart(df.iloc[[-1]]).mark_circle(size=100, color='red').encode( | |
| x='datetime:T', | |
| y='close:Q', | |
| tooltip=[ | |
| alt.Tooltip('datetime:T', title='Date', format='%d/%m/%Y'), | |
| alt.Tooltip('close:Q', title='Closing Price', format=',.2f'), | |
| alt.Tooltip('volume:Q', title='Volume', format=',.0f') | |
| ] | |
| ) | |
| # Combine the line and point charts | |
| final_chart = chart + last_point | |
| return final_chart | |
| # Sửa function generate_html_report để thêm biểu đồ | |
| def generate_html_report(analysis_results): | |
| """Generate HTML report from analysis results""" | |
| # Import markdown module | |
| import markdown | |
| import re | |
| from markdown.extensions.tables import TableExtension | |
| from markdown.extensions.fenced_code import FencedCodeExtension | |
| # Get current date for the report | |
| current_date = datetime.now().strftime("%d/%m/%Y") | |
| symbol = analysis_results['symbol'] | |
| company_name = analysis_results['company_name'] | |
| import json | |
| json.dump(analysis_results['analysis'], open('analysis_results_before.json', 'w'), ensure_ascii=False, indent=4) | |
| # Pre-process markdown text to fix bullet point styling | |
| def process_markdown_text(text): | |
| # First, properly format bullet points with '*' | |
| # Pattern: "\n* Item" -> "\n\n- Item" | |
| text = re.sub(r'\n\*\s+(.*?)$', r'\n\n- \1', text, flags=re.MULTILINE) | |
| # Pattern: Replace $ with USD | |
| text = text.replace('$', 'USD ') | |
| return text | |
| # Process and convert markdown to HTML | |
| summary_text = process_markdown_text(analysis_results['analysis']['summary']) | |
| financial_text = process_markdown_text(analysis_results['analysis']['financial_health']) | |
| news_text = process_markdown_text(analysis_results['analysis']['news_sentiment']) | |
| expert_text = process_markdown_text(analysis_results['analysis']['expert_opinion']) | |
| import json | |
| json.dump({'summary': summary_text, 'financial': financial_text, 'news': news_text, 'expert': expert_text}, open('analysis_results.json', 'w'), ensure_ascii=False, indent=4) | |
| # Convert to HTML | |
| summary_html = markdown.markdown( | |
| summary_text, | |
| extensions=['tables', 'fenced_code'] | |
| ) | |
| financial_html = markdown.markdown( | |
| financial_text, | |
| extensions=['tables', 'fenced_code'] | |
| ) | |
| news_html = markdown.markdown( | |
| news_text, | |
| extensions=['tables', 'fenced_code'] | |
| ) | |
| expert_html = markdown.markdown( | |
| expert_text, | |
| extensions=['tables', 'fenced_code'] | |
| ) | |
| # Generate chart images | |
| price_charts_html = "" | |
| if 'price_data' in analysis_results: | |
| price_data = analysis_results['price_data'] | |
| periods = ['1_month', '3_months', '1_year'] | |
| for period in periods: | |
| if period in price_data: | |
| chart = create_price_chart(price_data[period], period, symbol) | |
| if chart: | |
| try: | |
| base64_image = chart_to_base64(chart) | |
| if base64_image: | |
| price_charts_html += f""" | |
| <div class="chart-container"> | |
| <h3>Price Chart - {period.replace('_', ' ').title()}</h3> | |
| <img src="data:image/png;base64,{base64_image}" alt="{symbol} {period} chart" | |
| style="width: 100%; max-width: 800px; margin: 0 auto; display: block;"> | |
| </div> | |
| """ | |
| except Exception as e: | |
| print(f"Error generating chart image: {e}") | |
| # Create HTML content | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Stock Analysis Report {symbol}</title> | |
| <style> | |
| body {{ | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| line-height: 1.6; | |
| color: #333; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background-color: #f9f9f9; | |
| }} | |
| .report-header {{ | |
| background-color: #2c3e50; | |
| color: white; | |
| padding: 20px; | |
| border-radius: 5px 5px 0 0; | |
| position: relative; | |
| }} | |
| .report-date {{ | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| font-size: 14px; | |
| }} | |
| .report-title {{ | |
| margin: 0; | |
| padding: 0; | |
| font-size: 24px; | |
| color: white; | |
| }} | |
| .report-subtitle {{ | |
| margin: 5px 0 0; | |
| padding: 0; | |
| font-size: 16px; | |
| font-weight: normal; | |
| color: white; | |
| }} | |
| .report-body {{ | |
| background-color: white; | |
| padding: 20px; | |
| border-radius: 0 0 5px 5px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| }} | |
| .section {{ | |
| margin-bottom: 20px; | |
| border-bottom: 1px solid #eee; | |
| padding-bottom: 20px; | |
| }} | |
| h1, h2, h3, h4, h5, h6 {{ | |
| color: #2c3e50; | |
| margin-top: 1.5em; | |
| margin-bottom: 0.5em; | |
| }} | |
| h1 {{ font-size: 24px; }} | |
| h2 {{ | |
| font-size: 20px; | |
| border-bottom: 2px solid #3498db; | |
| padding-bottom: 5px; | |
| color: #2c3e50 !important; | |
| }} | |
| h3 {{ font-size: 18px; color: #3498db; }} | |
| h4 {{ font-size: 16px; }} | |
| p {{ margin: 0.8em 0; }} | |
| ul, ol {{ | |
| margin: 1em 0 1em 2em; | |
| padding-left: 0; | |
| }} | |
| li {{ | |
| margin-bottom: 0.8em; | |
| line-height: 1.5; | |
| }} | |
| li strong {{ | |
| color: #2c3e50; | |
| }} | |
| table {{ | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin: 15px 0; | |
| }} | |
| th, td {{ | |
| padding: 12px; | |
| border: 1px solid #ddd; | |
| text-align: left; | |
| }} | |
| th {{ | |
| background-color: #f2f2f2; | |
| font-weight: bold; | |
| }} | |
| tr:nth-child(even) {{ | |
| background-color: #f9f9f9; | |
| }} | |
| .bullish {{ | |
| color: #27ae60; | |
| font-weight: bold; | |
| }} | |
| .bearish {{ | |
| color: #e74c3c; | |
| font-weight: bold; | |
| }} | |
| .neutral {{ | |
| color: #f39c12; | |
| font-weight: bold; | |
| }} | |
| code {{ | |
| background: #f8f8f8; | |
| border: 1px solid #ddd; | |
| border-radius: 3px; | |
| padding: 0 3px; | |
| font-family: Consolas, monospace; | |
| }} | |
| pre {{ | |
| background: #f8f8f8; | |
| border: 1px solid #ddd; | |
| border-radius: 3px; | |
| padding: 10px; | |
| overflow-x: auto; | |
| }} | |
| blockquote {{ | |
| margin: 1em 0; | |
| padding: 0 1em; | |
| color: #666; | |
| border-left: 4px solid #ddd; | |
| }} | |
| hr {{ | |
| border: 0; | |
| border-top: 1px solid #eee; | |
| margin: 20px 0; | |
| }} | |
| .footer {{ | |
| text-align: center; | |
| margin-top: 40px; | |
| padding-top: 20px; | |
| font-size: 12px; | |
| color: #777; | |
| border-top: 1px solid #eee; | |
| }} | |
| /* Custom styling for bullet points */ | |
| ul {{ | |
| list-style-type: disc; | |
| }} | |
| ul ul {{ | |
| list-style-type: circle; | |
| }} | |
| ul ul ul {{ | |
| list-style-type: square; | |
| }} | |
| /* Fix for section headers to ensure they're black */ | |
| .section h2 {{ | |
| color: #2c3e50 !important; | |
| }} | |
| /* Fix for investment report headers */ | |
| strong {{ | |
| color: inherit; | |
| }} | |
| /* Chart container styling */ | |
| .chart-container {{ | |
| margin: 30px 0; | |
| text-align: center; | |
| }} | |
| .chart-container h3 {{ | |
| text-align: center; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="report-header"> | |
| <div class="report-date">Date: {current_date}</div> | |
| <h1 class="report-title">Stock Analysis Report: {symbol}</h1> | |
| <h2 class="report-subtitle">{company_name}</h2> | |
| </div> | |
| <div class="report-body"> | |
| <div class="section"> | |
| <h2>Summary & Recommendation</h2> | |
| {summary_html} | |
| </div> | |
| <div class="section"> | |
| <h2>Financial Health Analysis</h2> | |
| {financial_html} | |
| </div> | |
| <div class="section"> | |
| <h2>News & Market Sentiment Analysis</h2> | |
| {news_html} | |
| </div> | |
| <div class="section"> | |
| <h2>Market Analysis</h2> | |
| {expert_html} | |
| </div> | |
| <div class="section"> | |
| <h2>Price Charts</h2> | |
| {price_charts_html} | |
| </div> | |
| <div class="footer"> | |
| This report was automatically generated by AI Financial Dashboard. Information is for reference only. | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| return html_content | |
| # Function to generate and save PDF report | |
| def generate_pdf_report(analysis_results, output_path): | |
| """Generate and save PDF report directly""" | |
| from weasyprint import HTML | |
| # Generate HTML content | |
| html_content = generate_html_report(analysis_results) | |
| # Save HTML preview for debugging | |
| with open("report_preview.html", "w", encoding="utf-8") as f: | |
| f.write(html_content) | |
| # Generate PDF | |
| try: | |
| HTML(string=html_content).write_pdf(output_path) | |
| print(f"PDF report saved successfully at: {output_path}") | |
| return True | |
| except Exception as e: | |
| print(f"Error generating PDF report: {e}") | |
| return False |