tosanoob commited on
Commit
f63d7fd
·
1 Parent(s): 33062e0

feat: add stock analysis report

Browse files
.gitignore CHANGED
@@ -1,3 +1,4 @@
1
  .env
2
  PLAN.md
3
- __pycache__/
 
 
1
  .env
2
  PLAN.md
3
+ __pycache__/
4
+ .streamlit/
Home.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Home.py (Trang chủ ứng dụng)
2
+ import streamlit as st
3
+ import os
4
+ from dotenv import load_dotenv
5
+ import google.generativeai as genai
6
+
7
+ # Load environment variables
8
+ load_dotenv()
9
+
10
+ # Thiết lập trang
11
+ st.set_page_config(
12
+ page_title="AI Financial Dashboard",
13
+ page_icon="📊",
14
+ layout="wide"
15
+ )
16
+
17
+ # Tiêu đề ứng dụng
18
+ st.title("📊 AI Financial Dashboard v2.0")
19
+
20
+ # Hiển thị thông tin ứng dụng
21
+ st.markdown("""
22
+ ## Chào mừng đến với AI Financial Dashboard
23
+
24
+ Đây là ứng dụng phân tích tài chính thông minh sử dụng AI để giúp bạn đưa ra các quyết định đầu tư thông minh hơn.
25
+
26
+ ### Các chức năng chính:
27
+
28
+ 1. **💬 Chat với AI Financial Analyst**:
29
+ - Tìm kiếm thông tin cổ phiếu
30
+ - Xem biểu đồ giá
31
+ - Quy đổi tiền tệ
32
+  
33
+ 2. **📄 Báo cáo Phân tích Chuyên sâu Mã Cổ phiếu**:
34
+ - Phân tích toàn diện về một cổ phiếu cụ thể
35
+ - Thu thập dữ liệu từ nhiều nguồn khác nhau
36
+ - Tạo báo cáo chuyên sâu với đánh giá của AI
37
+  
38
+ 3. **📰 Bản tin Tổng hợp Thị trường Hàng ngày** (Sắp ra mắt):
39
+ - Tổng hợp tin tức tài chính mới nhất
40
+ - Phân loại theo chủ đề
41
+ - Cập nhật thị trường hàng ngày
42
+
43
+ ### Cách sử dụng:
44
+
45
+ Sử dụng thanh điều hướng bên trái để chuyển đổi giữa các chức năng khác nhau của ứng dụng.
46
+
47
+ """)
48
+
49
+ # Hiển thị trạng thái kết nối API
50
+ st.sidebar.title("Trạng thái kết nối")
51
+
52
+ # Kiểm tra các API key
53
+ api_keys = {
54
+ "GEMINI_API_KEY": os.getenv("GEMINI_API_KEY"),
55
+ "ALPHA_VANTAGE_API_KEY": os.getenv("ALPHA_VANTAGE_API_KEY"),
56
+ "NEWS_API_KEY": os.getenv("NEWS_API_KEY"),
57
+ "MARKETAUX_API_KEY": os.getenv("MARKETAUX_API_KEY"),
58
+ "TWELVEDATA_API_KEY": os.getenv("TWELVEDATA_API_KEY")
59
+ }
60
+
61
+ # Hiển thị trạng thái của từng API
62
+ for api_name, api_key in api_keys.items():
63
+ if api_key:
64
+ st.sidebar.success(f"✅ {api_name} đã kết nối")
65
+ else:
66
+ st.sidebar.error(f"❌ {api_name} chưa kết nối")
67
+
68
+ # Hiển thị thông tin về dự án
69
+ st.sidebar.markdown("---")
70
+ st.sidebar.markdown("""
71
+ ### Thông tin dự án
72
+ - **Phiên bản**: 2.0
73
+ - **Cập nhật**: Tính năng báo cáo chuyên sâu
74
+ """)
75
+
76
+ # Hiển thị các nút chuyển hướng nhanh
77
+ st.markdown("### Chuyển hướng nhanh")
78
+
79
+ col1, col2 = st.columns(2)
80
+
81
+ with col1:
82
+ if st.button("💬 Trò chuyện với AI Financial Analyst", use_container_width=True):
83
+ # Chuyển hướng sang trang chat
84
+ st.switch_page("pages/chat_app.py")
85
+
86
+ with col2:
87
+ if st.button("📄 Tạo Báo cáo Phân tích Cổ phiếu", use_container_width=True):
88
+ # Chuyển hướng sang trang báo cáo cổ phiếu
89
+ st.switch_page("pages/stock_report.py")
modules/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # modules/__init__.py
2
+ # File này đánh dấu thư mục modules là một package Python
modules/analysis_pipeline.py ADDED
@@ -0,0 +1,777 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/analysis_pipeline.py
2
+ import os
3
+ import asyncio
4
+ import pandas as pd
5
+ from datetime import datetime
6
+ import google.generativeai as genai
7
+ from dotenv import load_dotenv
8
+ from .api_clients import AlphaVantageClient, NewsAPIClient, MarketauxClient, get_price_history
9
+
10
+ # Load environment variables and configure AI
11
+ load_dotenv()
12
+ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
13
+ MODEL_NAME = os.getenv("GEMINI_MODEL", "gemini-2.5-flash")
14
+
15
+ # Define the analysis pipeline class
16
+ class StockAnalysisPipeline:
17
+ """Pipeline for generating comprehensive stock analysis reports"""
18
+
19
+ def __init__(self, symbol):
20
+ """Initialize the pipeline with a stock symbol"""
21
+ self.symbol = symbol.upper() # Convert to uppercase
22
+ self.company_data = {}
23
+ self.analysis_results = {}
24
+ self.ai_model = genai.GenerativeModel(model_name=MODEL_NAME)
25
+
26
+ async def gather_all_data(self):
27
+ """Gather all required data about the company from multiple sources"""
28
+ print(f"Gathering data for {self.symbol}...")
29
+
30
+ # Create tasks for all API calls to run in parallel
31
+ tasks = [
32
+ self._get_company_overview(),
33
+ self._get_financial_statements(),
34
+ self._get_market_sentiment_and_news(),
35
+ self._get_analyst_ratings(),
36
+ self._get_price_data()
37
+ ]
38
+
39
+ # Wait for all tasks to complete
40
+ await asyncio.gather(*tasks)
41
+
42
+ return self.company_data
43
+
44
+ async def _get_company_overview(self):
45
+ """Get company overview information"""
46
+ self.company_data['overview'] = await AlphaVantageClient.get_company_overview(self.symbol)
47
+ if self.company_data['overview'] and 'Name' in self.company_data['overview']:
48
+ self.company_name = self.company_data['overview']['Name']
49
+ else:
50
+ self.company_name = self.symbol
51
+ print(f"Retrieved company overview for {self.symbol}")
52
+
53
+ async def _get_financial_statements(self):
54
+ """Get company financial statements"""
55
+ # Run these in parallel
56
+ income_stmt_task = AlphaVantageClient.get_income_statement(self.symbol)
57
+ balance_sheet_task = AlphaVantageClient.get_balance_sheet(self.symbol)
58
+ cash_flow_task = AlphaVantageClient.get_cash_flow(self.symbol)
59
+
60
+ # Wait for all tasks to complete
61
+ results = await asyncio.gather(
62
+ income_stmt_task,
63
+ balance_sheet_task,
64
+ cash_flow_task
65
+ )
66
+
67
+ # Store results
68
+ self.company_data['income_statement'] = results[0]
69
+ self.company_data['balance_sheet'] = results[1]
70
+ self.company_data['cash_flow'] = results[2]
71
+ print(f"Retrieved financial statements for {self.symbol}")
72
+
73
+ async def _get_market_sentiment_and_news(self):
74
+ """Get market sentiment and news about the company"""
75
+ # Get news from multiple sources in parallel
76
+ alpha_news_task = AlphaVantageClient.get_news_sentiment(self.symbol)
77
+ news_api_task = NewsAPIClient.get_company_news(self.company_name if hasattr(self, 'company_name') else self.symbol)
78
+ marketaux_task = MarketauxClient.get_company_news(self.symbol)
79
+
80
+ # Wait for all tasks to complete
81
+ results = await asyncio.gather(
82
+ alpha_news_task,
83
+ news_api_task,
84
+ marketaux_task
85
+ )
86
+
87
+ # Store results
88
+ self.company_data['alpha_news'] = results[0]
89
+ self.company_data['news_api'] = results[1]
90
+ self.company_data['marketaux'] = results[2]
91
+ print(f"Retrieved news and sentiment for {self.symbol}")
92
+
93
+ async def _get_analyst_ratings(self):
94
+ """Get current stock quotes instead of analyst ratings"""
95
+ self.company_data['quote_data'] = await AlphaVantageClient.get_global_quote(self.symbol)
96
+ print(f"Retrieved quote data for {self.symbol}")
97
+
98
+ async def _get_price_data(self):
99
+ """Get historical price data"""
100
+ # Get price data for different time periods
101
+ periods = ['1_month', '3_months', '1_year']
102
+ price_data = {}
103
+
104
+ # Sử dụng phương thức đồng bộ thông thường vì get_price_history không còn async
105
+ for period in periods:
106
+ price_data[period] = get_price_history(self.symbol, period)
107
+
108
+ self.company_data['price_data'] = price_data
109
+ print(f"Retrieved price history for {self.symbol}")
110
+
111
+ async def run_analysis(self):
112
+ """Run the full analysis pipeline"""
113
+ # 1. Gather all data
114
+ await self.gather_all_data()
115
+
116
+ # 2. Run AI analysis in sequence
117
+ print(f"Running AI analysis for {self.symbol}...")
118
+
119
+ # Financial Health Analysis
120
+ self.analysis_results['financial_health'] = await self._analyze_financial_health()
121
+
122
+ # News & Sentiment Analysis
123
+ self.analysis_results['news_sentiment'] = await self._analyze_news_sentiment()
124
+
125
+ # Expert Opinion Analysis
126
+ self.analysis_results['expert_opinion'] = await self._analyze_expert_opinion()
127
+
128
+ # Final Summary & Recommendation
129
+ self.analysis_results['summary'] = await self._create_summary()
130
+
131
+ # 3. Return the complete analysis
132
+ return {
133
+ 'symbol': self.symbol,
134
+ 'company_name': self.company_name if hasattr(self, 'company_name') else self.symbol,
135
+ 'analysis': self.analysis_results,
136
+ 'price_data': self.company_data.get('price_data', {}),
137
+ 'overview': self.company_data.get('overview', {})
138
+ }
139
+
140
+ async def _analyze_financial_health(self):
141
+ """Analyze company's financial health using AI"""
142
+ # Prepare financial data for the AI
143
+ financial_data = {
144
+ 'overview': self.company_data.get('overview', {}),
145
+ 'income_statement': self.company_data.get('income_statement', {}),
146
+ 'balance_sheet': self.company_data.get('balance_sheet', {}),
147
+ 'cash_flow': self.company_data.get('cash_flow', {})
148
+ }
149
+
150
+ # Create prompt for financial analysis
151
+ prompt = f"""
152
+ You are a senior financial analyst. Analyze the financial health of {self.symbol} based on the following data:
153
+
154
+ {financial_data}
155
+
156
+ Provide a detailed analysis covering:
157
+ 1. Overall financial condition overview
158
+ 2. Key financial ratios analysis (P/E, ROE, Debt/Equity, etc.)
159
+ 3. Revenue and profit growth assessment
160
+ 4. Cash flow and liquidity assessment
161
+ 5. Key financial strengths and weaknesses
162
+
163
+ Format requirements:
164
+ - Write in professional, concise financial reporting style
165
+ - Use Markdown formatting with appropriate headers and bullet points
166
+ - DO NOT include any introductory phrases like "Hello," "I'm happy to provide," etc.
167
+ - DO NOT include any concluding phrases
168
+ - Present only factual analysis based on the data
169
+ - Present the information directly and objectively
170
+ """
171
+
172
+ # Get AI response - Using asyncio.to_thread instead of await
173
+ response = self.ai_model.generate_content(prompt)
174
+ return response.text
175
+
176
+ async def _analyze_news_sentiment(self):
177
+ """Analyze news and market sentiment using AI"""
178
+ # Prepare news data for the AI
179
+ news_data = {
180
+ 'alpha_news': self.company_data.get('alpha_news', {}),
181
+ 'news_api': self.company_data.get('news_api', {}),
182
+ 'marketaux': self.company_data.get('marketaux', {})
183
+ }
184
+
185
+ # Create prompt for news analysis
186
+ prompt = f"""
187
+ You are a market analyst. Analyze news and market sentiment about {self.symbol} based on the following data:
188
+
189
+ {news_data}
190
+
191
+ Provide a detailed analysis covering:
192
+ 1. Summary of key recent news about the company
193
+ 2. Important events that could impact stock price
194
+ 3. Overall market sentiment analysis (positive/negative/neutral)
195
+ 4. Risk factors identified in news
196
+
197
+ Format requirements:
198
+ - Write in professional, concise financial reporting style
199
+ - Use Markdown formatting with appropriate headers and bullet points
200
+ - DO NOT include any introductory phrases like "Hello," "I'm happy to provide," etc.
201
+ - DO NOT include any concluding phrases
202
+ - Present only factual analysis based on the data
203
+ - Present the information directly and objectively
204
+ """
205
+
206
+ # Get AI response - Using asyncio.to_thread instead of await
207
+ response = self.ai_model.generate_content(prompt)
208
+ return response.text
209
+
210
+ async def _analyze_expert_opinion(self):
211
+ """Analyze current stock quote and price data"""
212
+ # Prepare data for the AI
213
+ quote_data = self.company_data.get('quote_data', {})
214
+ price_data = self.company_data.get('price_data', {})
215
+ overview = self.company_data.get('overview', {})
216
+
217
+ # Create prompt for market analysis with chart descriptions
218
+ chart_descriptions = []
219
+
220
+ # Add descriptions for each timeframe chart
221
+ for period, period_name in [('1_month', 'last month'), ('3_months', 'last 3 months'), ('1_year', 'last year')]:
222
+ if period in price_data and 'values' in price_data[period] and price_data[period]['values']:
223
+ values = price_data[period]['values']
224
+ # Get first and last price for the period
225
+ first_price = float(values[-1]['close']) # Reversed order in the API
226
+ last_price = float(values[0]['close'])
227
+ price_change = ((last_price - first_price) / first_price) * 100
228
+
229
+ # Calculate volatility (standard deviation)
230
+ if len(values) > 1:
231
+ closes = [float(day['close']) for day in values]
232
+ volatility = pd.Series(closes).pct_change().std() * 100 # Convert to percentage
233
+ else:
234
+ volatility = 0.0
235
+
236
+ # Detect trend (simple linear regression slope)
237
+ if len(values) > 2:
238
+ closes = [float(day['close']) for day in values]
239
+ dates = list(range(len(closes)))
240
+ slope = pd.Series(closes).corr(pd.Series(dates))
241
+ trend = "strong upward" if slope > 0.7 else \
242
+ "upward" if slope > 0.3 else \
243
+ "relatively flat" if slope > -0.3 else \
244
+ "downward" if slope > -0.7 else \
245
+ "strong downward"
246
+ else:
247
+ trend = "insufficient data to determine"
248
+
249
+ # Get price range
250
+ prices = [float(day['close']) for day in values]
251
+ min_price = min(prices) if prices else 0
252
+ max_price = max(prices) if prices else 0
253
+ price_range = max_price - min_price
254
+
255
+ # Find significant price movements
256
+ significant_changes = []
257
+ if len(values) > 5:
258
+ for i in range(1, len(values)):
259
+ prev_close = float(values[i]['close'])
260
+ curr_close = float(values[i-1]['close'])
261
+ daily_change = ((curr_close - prev_close) / prev_close) * 100
262
+ if abs(daily_change) > 2.0: # More than 2% daily change
263
+ date = values[i-1]['datetime']
264
+ significant_changes.append(f"On {date}, there was a {daily_change:.2f}% {'increase' if daily_change > 0 else 'decrease'}")
265
+
266
+ # Limit to 3 most significant changes
267
+ significant_changes = significant_changes[:3]
268
+
269
+ # Create chart description
270
+ description = f"""
271
+ Chart for {period_name}:
272
+ - Overall trend: {trend}
273
+ - Price change: {price_change:.2f}% ({first_price:.2f} to {last_price:.2f})
274
+ - Volatility: {volatility:.2f}%
275
+ - Price range: {min_price:.2f} to {max_price:.2f} (range: {price_range:.2f})
276
+ """
277
+
278
+ # Add significant changes if any
279
+ if significant_changes:
280
+ description += "- Significant price movements:\n * " + "\n * ".join(significant_changes)
281
+
282
+ chart_descriptions.append(description)
283
+
284
+ # Create prompt for market analysis
285
+ prompt = f"""
286
+ You are a stock market analyst. Analyze the current stock data for {self.symbol} based on the following information:
287
+
288
+ Current Quote Data: {quote_data}
289
+ Company Overview: {overview}
290
+
291
+ Chart Analysis:
292
+ {chr(10).join(chart_descriptions)}
293
+
294
+ Provide a detailed analysis covering:
295
+ 1. Current stock performance overview
296
+ 2. Price trends and technical indicators based on the charts
297
+ 3. Price comparison with sector averages and benchmarks
298
+ 4. Potential price movement factors
299
+ 5. Technical analysis of support and resistance levels
300
+ 6. Trading volume patterns and their significance
301
+
302
+ Format requirements:
303
+ - Write in professional, concise financial reporting style
304
+ - Use Markdown formatting with appropriate headers and bullet points
305
+ - DO NOT include any introductory phrases like "Hello," "I'm happy to provide," etc.
306
+ - DO NOT include any concluding phrases
307
+ - Present only factual analysis based on the data
308
+ - Present the information directly and objectively
309
+ """
310
+
311
+ # Get AI response - Using asyncio.to_thread instead of await
312
+ response = self.ai_model.generate_content(prompt)
313
+ return response.text
314
+
315
+ async def _create_summary(self):
316
+ """Create a comprehensive summary and investment recommendation"""
317
+ # Combine all analyses
318
+ combined_analysis = {
319
+ 'financial_health': self.analysis_results.get('financial_health', ''),
320
+ 'news_sentiment': self.analysis_results.get('news_sentiment', ''),
321
+ 'expert_opinion': self.analysis_results.get('expert_opinion', '')
322
+ }
323
+
324
+ # Add overview data
325
+ overview = self.company_data.get('overview', {})
326
+
327
+ # Create prompt for final summary
328
+ prompt = f"""
329
+ You are an investment advisor. Based on the detailed analyses below for {self.symbol} ({overview.get('Name', '')}),
330
+ synthesize a final report and investment recommendation:
331
+
332
+ === Company Basic Information ===
333
+ {overview}
334
+
335
+ === Financial Health Analysis ===
336
+ {combined_analysis['financial_health']}
337
+
338
+ === News and Market Sentiment Analysis ===
339
+ {combined_analysis['news_sentiment']}
340
+
341
+ === Market Analysis ===
342
+ {combined_analysis['expert_opinion']}
343
+
344
+ Provide:
345
+ 1. Brief company and industry overview
346
+ 2. Summary of key strengths and weaknesses from the analyses above
347
+ 3. Risk and opportunity assessment
348
+ 4. Investment recommendation (BULLISH/BEARISH/NEUTRAL) with rationale
349
+ 5. Key factors to monitor going forward
350
+
351
+ Format requirements:
352
+ - Write in professional, concise financial reporting style
353
+ - Use Markdown formatting with appropriate headers and bullet points
354
+ - DO NOT include any introductory phrases like "Hello," "I'm happy to provide," etc.
355
+ - DO NOT include any concluding phrases or sign-offs
356
+ - Present the report directly and objectively
357
+ - The report should be comprehensive but concise
358
+ """
359
+
360
+ # Get AI response - Using asyncio.to_thread instead of await
361
+ response = self.ai_model.generate_content(prompt)
362
+ return response.text
363
+
364
+ # Main function to run the pipeline
365
+ async def run_analysis_pipeline(symbol):
366
+ """Run the complete stock analysis pipeline for a given symbol"""
367
+ pipeline = StockAnalysisPipeline(symbol)
368
+ return await pipeline.run_analysis()
369
+
370
+ # Function to generate HTML report from analysis results
371
+ import altair as alt
372
+ import base64
373
+ import io
374
+ from PIL import Image
375
+
376
+ # Function to convert Altair chart to base64 image
377
+ def chart_to_base64(chart):
378
+ """Convert Altair chart to base64-encoded PNG image"""
379
+ # Save chart as PNG
380
+ import io
381
+ import base64
382
+ from PIL import Image
383
+
384
+ try:
385
+ # Sử dụng Altair's save method
386
+ import tempfile
387
+
388
+ # Tạo file tạm thời để lưu chart
389
+ with tempfile.NamedTemporaryFile(suffix='.png') as tmpfile:
390
+ # Lưu biểu đồ dưới dạng PNG
391
+ chart.save(tmpfile.name)
392
+
393
+ # Đọc file PNG và mã hóa base64
394
+ with open(tmpfile.name, 'rb') as f:
395
+ image_bytes = f.read()
396
+ base64_image = base64.b64encode(image_bytes).decode('utf-8')
397
+ return base64_image
398
+ except Exception as e:
399
+ # Backup method - tạo hình ảnh đơn giản với thông tin chart
400
+ try:
401
+ print(f"Chart rendering failed: {str(e)}")
402
+ # Tạo một hình ảnh thay thế đơn giản
403
+ width, height = 800, 400
404
+
405
+ # Tạo hình ảnh trắng
406
+ image = Image.new("RGB", (width, height), (255, 255, 255))
407
+
408
+ # Lưu hình ảnh vào buffer
409
+ buffer = io.BytesIO()
410
+ image.save(buffer, format="PNG")
411
+ image_bytes = buffer.getvalue()
412
+
413
+ # Mã hóa base64
414
+ base64_image = base64.b64encode(image_bytes).decode('utf-8')
415
+ return base64_image
416
+ except:
417
+ return None
418
+
419
+ # Function to create price chart from price data
420
+ def create_price_chart(price_data, period, symbol):
421
+ """Create a price chart from the price data"""
422
+ if 'values' not in price_data:
423
+ return None
424
+
425
+ df = pd.DataFrame(price_data['values'])
426
+ if df.empty:
427
+ return None
428
+
429
+ df['datetime'] = pd.to_datetime(df['datetime'])
430
+ df['close'] = pd.to_numeric(df['close'])
431
+
432
+ # Map period to title
433
+ title_map = {
434
+ '1_month': f'{symbol} - Price over the last month',
435
+ '3_months': f'{symbol} - Price over the last 3 months',
436
+ '1_year': f'{symbol} - Price over the last year'
437
+ }
438
+
439
+ # Create the Altair chart
440
+ chart = alt.Chart(df).mark_line(color='#3498db').encode(
441
+ x=alt.X('datetime:T', title='Time'),
442
+ y=alt.Y('close:Q', title='Closing Price', scale=alt.Scale(zero=False)),
443
+ ).properties(
444
+ title=title_map.get(period, f'Stock price ({period})'),
445
+ width=800,
446
+ height=400
447
+ )
448
+
449
+ # Add a point for the last day
450
+ last_point = alt.Chart(df.iloc[[-1]]).mark_circle(size=100, color='red').encode(
451
+ x='datetime:T',
452
+ y='close:Q',
453
+ tooltip=[
454
+ alt.Tooltip('datetime:T', title='Date', format='%d/%m/%Y'),
455
+ alt.Tooltip('close:Q', title='Closing Price', format=',.2f'),
456
+ alt.Tooltip('volume:Q', title='Volume', format=',.0f')
457
+ ]
458
+ )
459
+
460
+ # Combine the line and point charts
461
+ final_chart = chart + last_point
462
+
463
+ return final_chart
464
+
465
+ # Sửa function generate_html_report để thêm biểu đồ
466
+ def generate_html_report(analysis_results):
467
+ """Generate HTML report from analysis results"""
468
+ # Import markdown module
469
+ import markdown
470
+ import re
471
+ from markdown.extensions.tables import TableExtension
472
+ from markdown.extensions.fenced_code import FencedCodeExtension
473
+
474
+ # Get current date for the report
475
+ current_date = datetime.now().strftime("%d/%m/%Y")
476
+ symbol = analysis_results['symbol']
477
+ company_name = analysis_results['company_name']
478
+
479
+ import json
480
+ json.dump(analysis_results['analysis'], open('analysis_results_before.json', 'w'), ensure_ascii=False, indent=4)
481
+
482
+ # Pre-process markdown text to fix bullet point styling
483
+ def process_markdown_text(text):
484
+ # First, properly format bullet points with '*'
485
+ # Pattern: "\n* Item" -> "\n\n- Item"
486
+ text = re.sub(r'\n\*\s+(.*?)$', r'\n\n- \1', text, flags=re.MULTILINE)
487
+
488
+ # Pattern: Replace $ with USD
489
+ text = text.replace('$', 'USD ')
490
+
491
+ return text
492
+
493
+
494
+ # Process and convert markdown to HTML
495
+ summary_text = process_markdown_text(analysis_results['analysis']['summary'])
496
+ financial_text = process_markdown_text(analysis_results['analysis']['financial_health'])
497
+ news_text = process_markdown_text(analysis_results['analysis']['news_sentiment'])
498
+ expert_text = process_markdown_text(analysis_results['analysis']['expert_opinion'])
499
+ import json
500
+
501
+ json.dump(analysis_results['analysis'], open('analysis_results.json', 'w'), ensure_ascii=False, indent=4)
502
+
503
+ # Convert to HTML
504
+ summary_html = markdown.markdown(
505
+ summary_text,
506
+ extensions=['tables', 'fenced_code']
507
+ )
508
+ financial_html = markdown.markdown(
509
+ financial_text,
510
+ extensions=['tables', 'fenced_code']
511
+ )
512
+ news_html = markdown.markdown(
513
+ news_text,
514
+ extensions=['tables', 'fenced_code']
515
+ )
516
+ expert_html = markdown.markdown(
517
+ expert_text,
518
+ extensions=['tables', 'fenced_code']
519
+ )
520
+
521
+ # Generate chart images
522
+ price_charts_html = ""
523
+ if 'price_data' in analysis_results:
524
+ price_data = analysis_results['price_data']
525
+ periods = ['1_month', '3_months', '1_year']
526
+
527
+ for period in periods:
528
+ if period in price_data:
529
+ chart = create_price_chart(price_data[period], period, symbol)
530
+ if chart:
531
+ try:
532
+ base64_image = chart_to_base64(chart)
533
+ if base64_image:
534
+ price_charts_html += f"""
535
+ <div class="chart-container">
536
+ <h3>Price Chart - {period.replace('_', ' ').title()}</h3>
537
+ <img src="data:image/png;base64,{base64_image}" alt="{symbol} {period} chart"
538
+ style="width: 100%; max-width: 800px; margin: 0 auto; display: block;">
539
+ </div>
540
+ """
541
+ except Exception as e:
542
+ print(f"Error generating chart image: {e}")
543
+
544
+ # Create HTML content
545
+ html_content = f"""
546
+ <!DOCTYPE html>
547
+ <html lang="en">
548
+ <head>
549
+ <meta charset="UTF-8">
550
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
551
+ <title>Stock Analysis Report {symbol}</title>
552
+ <style>
553
+ body {{
554
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
555
+ line-height: 1.6;
556
+ color: #333;
557
+ max-width: 1200px;
558
+ margin: 0 auto;
559
+ padding: 20px;
560
+ background-color: #f9f9f9;
561
+ }}
562
+ .report-header {{
563
+ background-color: #2c3e50;
564
+ color: white;
565
+ padding: 20px;
566
+ border-radius: 5px 5px 0 0;
567
+ position: relative;
568
+ }}
569
+ .report-date {{
570
+ position: absolute;
571
+ top: 20px;
572
+ right: 20px;
573
+ font-size: 14px;
574
+ }}
575
+ .report-title {{
576
+ margin: 0;
577
+ padding: 0;
578
+ font-size: 24px;
579
+ color: white;
580
+ }}
581
+ .report-subtitle {{
582
+ margin: 5px 0 0;
583
+ padding: 0;
584
+ font-size: 16px;
585
+ font-weight: normal;
586
+ color: white;
587
+ }}
588
+ .report-body {{
589
+ background-color: white;
590
+ padding: 20px;
591
+ border-radius: 0 0 5px 5px;
592
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
593
+ }}
594
+ .section {{
595
+ margin-bottom: 20px;
596
+ border-bottom: 1px solid #eee;
597
+ padding-bottom: 20px;
598
+ }}
599
+ h1, h2, h3, h4, h5, h6 {{
600
+ color: #2c3e50;
601
+ margin-top: 1.5em;
602
+ margin-bottom: 0.5em;
603
+ }}
604
+ h1 {{ font-size: 24px; }}
605
+ h2 {{
606
+ font-size: 20px;
607
+ border-bottom: 2px solid #3498db;
608
+ padding-bottom: 5px;
609
+ color: #2c3e50 !important;
610
+ }}
611
+ h3 {{ font-size: 18px; color: #3498db; }}
612
+ h4 {{ font-size: 16px; }}
613
+ p {{ margin: 0.8em 0; }}
614
+ ul, ol {{
615
+ margin: 1em 0 1em 2em;
616
+ padding-left: 0;
617
+ }}
618
+ li {{
619
+ margin-bottom: 0.8em;
620
+ line-height: 1.5;
621
+ }}
622
+ li strong {{
623
+ color: #2c3e50;
624
+ }}
625
+ table {{
626
+ width: 100%;
627
+ border-collapse: collapse;
628
+ margin: 15px 0;
629
+ }}
630
+ th, td {{
631
+ padding: 12px;
632
+ border: 1px solid #ddd;
633
+ text-align: left;
634
+ }}
635
+ th {{
636
+ background-color: #f2f2f2;
637
+ font-weight: bold;
638
+ }}
639
+ tr:nth-child(even) {{
640
+ background-color: #f9f9f9;
641
+ }}
642
+ .bullish {{
643
+ color: #27ae60;
644
+ font-weight: bold;
645
+ }}
646
+ .bearish {{
647
+ color: #e74c3c;
648
+ font-weight: bold;
649
+ }}
650
+ .neutral {{
651
+ color: #f39c12;
652
+ font-weight: bold;
653
+ }}
654
+ code {{
655
+ background: #f8f8f8;
656
+ border: 1px solid #ddd;
657
+ border-radius: 3px;
658
+ padding: 0 3px;
659
+ font-family: Consolas, monospace;
660
+ }}
661
+ pre {{
662
+ background: #f8f8f8;
663
+ border: 1px solid #ddd;
664
+ border-radius: 3px;
665
+ padding: 10px;
666
+ overflow-x: auto;
667
+ }}
668
+ blockquote {{
669
+ margin: 1em 0;
670
+ padding: 0 1em;
671
+ color: #666;
672
+ border-left: 4px solid #ddd;
673
+ }}
674
+ hr {{
675
+ border: 0;
676
+ border-top: 1px solid #eee;
677
+ margin: 20px 0;
678
+ }}
679
+ .footer {{
680
+ text-align: center;
681
+ margin-top: 40px;
682
+ padding-top: 20px;
683
+ font-size: 12px;
684
+ color: #777;
685
+ border-top: 1px solid #eee;
686
+ }}
687
+ /* Custom styling for bullet points */
688
+ ul {{
689
+ list-style-type: disc;
690
+ }}
691
+ ul ul {{
692
+ list-style-type: circle;
693
+ }}
694
+ ul ul ul {{
695
+ list-style-type: square;
696
+ }}
697
+ /* Fix for section headers to ensure they're black */
698
+ .section h2 {{
699
+ color: #2c3e50 !important;
700
+ }}
701
+ /* Fix for investment report headers */
702
+ strong {{
703
+ color: inherit;
704
+ }}
705
+ /* Chart container styling */
706
+ .chart-container {{
707
+ margin: 30px 0;
708
+ text-align: center;
709
+ }}
710
+ .chart-container h3 {{
711
+ text-align: center;
712
+ }}
713
+ </style>
714
+ </head>
715
+ <body>
716
+ <div class="report-header">
717
+ <div class="report-date">Date: {current_date}</div>
718
+ <h1 class="report-title">Stock Analysis Report: {symbol}</h1>
719
+ <h2 class="report-subtitle">{company_name}</h2>
720
+ </div>
721
+
722
+ <div class="report-body">
723
+ <div class="section">
724
+ <h2>Summary & Recommendation</h2>
725
+ {summary_html}
726
+ </div>
727
+
728
+ <div class="section">
729
+ <h2>Financial Health Analysis</h2>
730
+ {financial_html}
731
+ </div>
732
+
733
+ <div class="section">
734
+ <h2>News & Market Sentiment Analysis</h2>
735
+ {news_html}
736
+ </div>
737
+
738
+ <div class="section">
739
+ <h2>Market Analysis</h2>
740
+ {expert_html}
741
+ </div>
742
+
743
+ <div class="section">
744
+ <h2>Price Charts</h2>
745
+ {price_charts_html}
746
+ </div>
747
+
748
+ <div class="footer">
749
+ This report was automatically generated by AI Financial Dashboard. Information is for reference only.
750
+ </div>
751
+ </div>
752
+ </body>
753
+ </html>
754
+ """
755
+
756
+ return html_content
757
+
758
+ # Function to generate and save PDF report
759
+ def generate_pdf_report(analysis_results, output_path):
760
+ """Generate and save PDF report directly"""
761
+ from weasyprint import HTML
762
+
763
+ # Generate HTML content
764
+ html_content = generate_html_report(analysis_results)
765
+
766
+ # Save HTML preview for debugging
767
+ with open("report_preview.html", "w", encoding="utf-8") as f:
768
+ f.write(html_content)
769
+
770
+ # Generate PDF
771
+ try:
772
+ HTML(string=html_content).write_pdf(output_path)
773
+ print(f"PDF report saved successfully at: {output_path}")
774
+ return True
775
+ except Exception as e:
776
+ print(f"Error generating PDF report: {e}")
777
+ return False
modules/api_clients.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # modules/api_clients.py
2
+ import os
3
+ import aiohttp
4
+ import asyncio
5
+ from dotenv import load_dotenv
6
+ from twelvedata_api import TwelveDataAPI
7
+
8
+ # Load environment variables
9
+ load_dotenv()
10
+
11
+ # API Keys
12
+ ALPHA_VANTAGE_API_KEY = os.getenv("ALPHA_VANTAGE_API_KEY")
13
+ NEWS_API_KEY = os.getenv("NEWS_API_KEY")
14
+ MARKETAUX_API_KEY = os.getenv("MARKETAUX_API_KEY")
15
+ TWELVEDATA_API_KEY = os.getenv("TWELVEDATA_API_KEY")
16
+
17
+ # Initialize TwelveDataAPI for reuse
18
+ td_api = TwelveDataAPI(TWELVEDATA_API_KEY)
19
+
20
+ class AlphaVantageClient:
21
+ """Client for interacting with the Alpha Vantage API"""
22
+ BASE_URL = "https://www.alphavantage.co/query"
23
+
24
+ @staticmethod
25
+ async def get_company_overview(symbol):
26
+ """Get company overview information"""
27
+ params = {
28
+ 'function': 'OVERVIEW',
29
+ 'symbol': symbol,
30
+ 'apikey': ALPHA_VANTAGE_API_KEY
31
+ }
32
+ async with aiohttp.ClientSession() as session:
33
+ async with session.get(AlphaVantageClient.BASE_URL, params=params) as response:
34
+ return await response.json()
35
+
36
+ @staticmethod
37
+ async def get_income_statement(symbol):
38
+ """Get company income statement"""
39
+ params = {
40
+ 'function': 'INCOME_STATEMENT',
41
+ 'symbol': symbol,
42
+ 'apikey': ALPHA_VANTAGE_API_KEY
43
+ }
44
+ async with aiohttp.ClientSession() as session:
45
+ async with session.get(AlphaVantageClient.BASE_URL, params=params) as response:
46
+ return await response.json()
47
+
48
+ @staticmethod
49
+ async def get_balance_sheet(symbol):
50
+ """Get company balance sheet"""
51
+ params = {
52
+ 'function': 'BALANCE_SHEET',
53
+ 'symbol': symbol,
54
+ 'apikey': ALPHA_VANTAGE_API_KEY
55
+ }
56
+ async with aiohttp.ClientSession() as session:
57
+ async with session.get(AlphaVantageClient.BASE_URL, params=params) as response:
58
+ return await response.json()
59
+
60
+ @staticmethod
61
+ async def get_cash_flow(symbol):
62
+ """Get company cash flow statement"""
63
+ params = {
64
+ 'function': 'CASH_FLOW',
65
+ 'symbol': symbol,
66
+ 'apikey': ALPHA_VANTAGE_API_KEY
67
+ }
68
+ async with aiohttp.ClientSession() as session:
69
+ async with session.get(AlphaVantageClient.BASE_URL, params=params) as response:
70
+ return await response.json()
71
+
72
+ @staticmethod
73
+ async def get_news_sentiment(symbol):
74
+ """Get news sentiment for a company"""
75
+ params = {
76
+ 'function': 'NEWS_SENTIMENT',
77
+ 'tickers': symbol,
78
+ 'apikey': ALPHA_VANTAGE_API_KEY
79
+ }
80
+ async with aiohttp.ClientSession() as session:
81
+ async with session.get(AlphaVantageClient.BASE_URL, params=params) as response:
82
+ return await response.json()
83
+
84
+ @staticmethod
85
+ async def get_global_quote(symbol):
86
+ """Get real-time quote information for a company"""
87
+ params = {
88
+ 'function': 'GLOBAL_QUOTE',
89
+ 'symbol': symbol,
90
+ 'apikey': ALPHA_VANTAGE_API_KEY
91
+ }
92
+ async with aiohttp.ClientSession() as session:
93
+ async with session.get(AlphaVantageClient.BASE_URL, params=params) as response:
94
+ return await response.json()
95
+
96
+ class NewsAPIClient:
97
+ """Client for interacting with the NewsAPI"""
98
+ BASE_URL = "https://newsapi.org/v2/everything"
99
+
100
+ @staticmethod
101
+ async def get_company_news(company_name, days=7):
102
+ """Get news about a specific company from the last N days"""
103
+ params = {
104
+ 'q': company_name,
105
+ 'sortBy': 'publishedAt',
106
+ 'language': 'en',
107
+ 'pageSize': 25,
108
+ 'apiKey': NEWS_API_KEY
109
+ }
110
+ async with aiohttp.ClientSession() as session:
111
+ async with session.get(NewsAPIClient.BASE_URL, params=params) as response:
112
+ return await response.json()
113
+
114
+ @staticmethod
115
+ async def get_market_news(days=1):
116
+ """Get general financial market news from the last N days"""
117
+ params = {
118
+ 'q': 'stock market OR finance OR investing OR economy',
119
+ 'sortBy': 'publishedAt',
120
+ 'language': 'en',
121
+ 'pageSize': 30,
122
+ 'apiKey': NEWS_API_KEY
123
+ }
124
+ async with aiohttp.ClientSession() as session:
125
+ async with session.get(NewsAPIClient.BASE_URL, params=params) as response:
126
+ return await response.json()
127
+
128
+ class MarketauxClient:
129
+ """Client for interacting with the Marketaux Financial News API"""
130
+ BASE_URL = "https://api.marketaux.com/v1/news/all"
131
+
132
+ @staticmethod
133
+ async def get_company_news(symbol, days=7):
134
+ """Get news about a specific company symbol from the last N days"""
135
+ params = {
136
+ 'symbols': symbol,
137
+ 'filter_entities': 'true',
138
+ 'language': 'en',
139
+ 'api_token': MARKETAUX_API_KEY
140
+ }
141
+ async with aiohttp.ClientSession() as session:
142
+ async with session.get(MarketauxClient.BASE_URL, params=params) as response:
143
+ return await response.json()
144
+
145
+ @staticmethod
146
+ async def get_market_news(days=1):
147
+ """Get general financial market news from the last N days"""
148
+ params = {
149
+ 'industries': 'Financial Services,Technology',
150
+ 'language': 'en',
151
+ 'limit': 30,
152
+ 'api_token': MARKETAUX_API_KEY
153
+ }
154
+ async with aiohttp.ClientSession() as session:
155
+ async with session.get(MarketauxClient.BASE_URL, params=params) as response:
156
+ return await response.json()
157
+
158
+ # Helper functions that utilize TwelveDataAPI for price data
159
+ def get_price_history(symbol, time_period='1_year'):
160
+ """
161
+ Get price history using TwelveDataAPI (non-async function)
162
+ This reuses the existing TwelveDataAPI implementation
163
+ """
164
+ # Map of time periods to appropriate parameters for TwelveDataAPI
165
+ logic_map = {
166
+ 'intraday': {'interval': '15min', 'outputsize': 120},
167
+ '1_week': {'interval': '1h', 'outputsize': 40},
168
+ '1_month': {'interval': '1day', 'outputsize': 22},
169
+ '3_months': {'interval': '1day', 'outputsize': 66},
170
+ '6_months': {'interval': '1day', 'outputsize': 120},
171
+ 'year_to_date': {'interval': '1day', 'outputsize': 120},
172
+ '1_year': {'interval': '1week', 'outputsize': 52},
173
+ '5_years': {'interval': '1month', 'outputsize': 60},
174
+ 'max': {'interval': '1month', 'outputsize': 120}
175
+ }
176
+
177
+ params = logic_map.get(time_period)
178
+ if not params:
179
+ return {"error": f"Khoảng thời gian '{time_period}' không hợp lệ."}
180
+
181
+ # Call TwelveDataAPI synchronously (it's already optimized internally)
182
+ return td_api.get_time_series(symbol=symbol, **params)
pages/chat_app.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py (Phiên bản cuối cùng với Biểu đồ Nâng cao)
2
+
3
+ import streamlit as st
4
+ import pandas as pd
5
+ import altair as alt # <-- Thêm thư viện Altair
6
+ import google.generativeai as genai
7
+ import google.ai.generativelanguage as glm
8
+ from dotenv import load_dotenv
9
+ import os
10
+ from twelvedata_api import TwelveDataAPI
11
+ from collections import deque
12
+ from datetime import datetime
13
+
14
+ # --- 1. CẤU HÌNH BAN ĐẦU & KHỞI TẠO STATE ---
15
+ load_dotenv()
16
+ st.set_page_config(layout="wide", page_title="AI Financial Dashboard")
17
+ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
18
+
19
+ def initialize_state():
20
+ if "initialized" in st.session_state: return
21
+ st.session_state.initialized = True
22
+ st.session_state.td_api = TwelveDataAPI(os.getenv("TWELVEDATA_API_KEY"))
23
+ st.session_state.stock_watchlist = {}
24
+ st.session_state.timeseries_cache = {}
25
+ st.session_state.active_timeseries_period = 'intraday'
26
+ st.session_state.currency_converter_state = {'from': 'USD', 'to': 'VND', 'amount': 100.0, 'result': None}
27
+ st.session_state.chat_history = []
28
+ st.session_state.active_tab = 'Danh sách mã chứng khoán'
29
+ st.session_state.chat_session = None
30
+ initialize_state()
31
+
32
+ # --- 2. TẢI DỮ LIỆU NỀN ---
33
+ @st.cache_data(show_spinner="Đang tải và chuẩn bị dữ liệu thị trường...")
34
+ def load_market_data():
35
+ td_api = st.session_state.td_api
36
+ stocks_data = td_api.get_all_stocks()
37
+ forex_data = td_api.get_forex_pairs()
38
+ forex_graph = {}
39
+ if forex_data and 'data' in forex_data:
40
+ for pair in forex_data['data']:
41
+ base, quote = pair['symbol'].split('/'); forex_graph.setdefault(base, []); forex_graph.setdefault(quote, []); forex_graph[base].append(quote); forex_graph[quote].append(base)
42
+ country_currency_map = {}
43
+ if stocks_data and 'data' in stocks_data:
44
+ for stock in stocks_data['data']:
45
+ country, currency = stock.get('country'), stock.get('currency')
46
+ if country and currency: country_currency_map[country.lower()] = currency
47
+ all_currencies = sorted(forex_graph.keys())
48
+ return stocks_data, forex_graph, country_currency_map, all_currencies
49
+ ALL_STOCKS_CACHE, FOREX_GRAPH, COUNTRY_CURRENCY_MAP, AVAILABLE_CURRENCIES = load_market_data()
50
+
51
+ # --- 3. LOGIC THỰC THI TOOL ---
52
+ def find_and_process_stock(query: str):
53
+ print(f"Hybrid searching for stock: '{query}'...")
54
+ query_lower = query.lower()
55
+ found_data = [s for s in ALL_STOCKS_CACHE.get('data', []) if query_lower in s['symbol'].lower() or query_lower in s['name'].lower()]
56
+ if not found_data:
57
+ results = st.session_state.td_api.get_stocks(symbol=query)
58
+ found_data = results.get('data', [])
59
+ if len(found_data) == 1:
60
+ stock_info = found_data[0]; symbol = stock_info['symbol']
61
+ st.session_state.stock_watchlist[symbol] = stock_info
62
+ ts_data = get_smart_time_series(symbol=symbol, time_period='intraday')
63
+ if 'values' in ts_data:
64
+ df = pd.DataFrame(ts_data['values']); df['datetime'] = pd.to_datetime(df['datetime']); df['close'] = pd.to_numeric(df['close'])
65
+ if symbol not in st.session_state.timeseries_cache: st.session_state.timeseries_cache[symbol] = {}
66
+ st.session_state.timeseries_cache[symbol]['intraday'] = df.sort_values('datetime').set_index('datetime')
67
+ st.session_state.active_tab = 'Biểu đồ thời gian'; st.session_state.active_timeseries_period = 'intraday'
68
+ return {"status": "SINGLE_STOCK_PROCESSED", "symbol": symbol, "name": stock_info.get('name', 'N/A')}
69
+ elif len(found_data) > 1: return {"status": "MULTIPLE_STOCKS_FOUND", "data": found_data[:5]}
70
+ else: return {"status": "NO_STOCKS_FOUND"}
71
+ def get_smart_time_series(symbol: str, time_period: str):
72
+ logic_map = {'intraday': {'interval': '15min', 'outputsize': 120}, '1_week': {'interval': '1h', 'outputsize': 40}, '1_month': {'interval': '1day', 'outputsize': 22}, '6_months': {'interval': '1day', 'outputsize': 120}, '1_year': {'interval': '1week', 'outputsize': 52}}
73
+ params = logic_map.get(time_period)
74
+ if not params: return {"error": f"Khoảng thời gian '{time_period}' không hợp lệ."}
75
+ return st.session_state.td_api.get_time_series(symbol=symbol, **params)
76
+ def find_conversion_path_bfs(start, end):
77
+ if start not in FOREX_GRAPH or end not in FOREX_GRAPH: return None
78
+ q = deque([(start, [start])]); visited = {start}
79
+ while q:
80
+ curr, path = q.popleft()
81
+ if curr == end: return path
82
+ for neighbor in FOREX_GRAPH.get(curr, []):
83
+ if neighbor not in visited: visited.add(neighbor); q.append((neighbor, path + [neighbor]))
84
+ return None
85
+ def convert_currency_with_bridge(amount: float, symbol: str):
86
+ try: start_currency, end_currency = symbol.upper().split('/')
87
+ except ValueError: return {"error": "Định dạng cặp tiền tệ không hợp lệ."}
88
+ path = find_conversion_path_bfs(start_currency, end_currency)
89
+ if not path: return {"error": f"Không tìm thấy đường đi quy đổi từ {start_currency} sang {end_currency}."}
90
+ current_amount = amount; steps = []
91
+ for i in range(len(path) - 1):
92
+ step_start, step_end = path[i], path[i+1]
93
+ result = st.session_state.td_api.currency_conversion(amount=current_amount, symbol=f"{step_start}/{step_end}")
94
+ if 'rate' in result and result.get('rate') is not None:
95
+ current_amount = result['amount']; steps.append({"step": f"{i+1}. {step_start} → {step_end}", "rate": result['rate'], "intermediate_amount": current_amount})
96
+ else:
97
+ inverse_result = st.session_state.td_api.currency_conversion(amount=1, symbol=f"{step_end}/{step_start}")
98
+ if 'rate' in inverse_result and inverse_result.get('rate') and inverse_result['rate'] != 0:
99
+ rate = 1 / inverse_result['rate']; current_amount *= rate; steps.append({"step": f"{i+1}. {step_start} → {step_end} (Inverse)", "rate": rate, "intermediate_amount": current_amount})
100
+ else: return {"error": f"Lỗi ở bước quy đổi từ {step_start} sang {step_end}."}
101
+ return {"status": "Success", "original_amount": amount, "final_amount": current_amount, "path_taken": path, "conversion_steps": steps}
102
+ def perform_currency_conversion(amount: float, symbol: str):
103
+ result = convert_currency_with_bridge(amount, symbol)
104
+ st.session_state.currency_converter_state.update({'result': result, 'amount': amount})
105
+ try:
106
+ from_curr, to_curr = symbol.split('/'); st.session_state.currency_converter_state.update({'from': from_curr, 'to': to_curr})
107
+ except: pass
108
+ st.session_state.active_tab = 'Quy đổi tiền tệ'
109
+ return result
110
+
111
+ # --- 4. CẤU HÌNH GEMINI ---
112
+ SYSTEM_INSTRUCTION = """Bạn là bộ não AI điều khiển một Bảng điều khiển Tài chính Tương tác. Nhiệm vụ của bạn là hiểu yêu cầu của người dùng, gọi các công cụ phù hợp, và thông báo kết quả một cách súc tích.
113
+
114
+ QUY TẮC VÀNG:
115
+ 1. **HIỂU TRƯỚC, GỌI SAU:**
116
+ * **Tên công ty:** Khi người dùng nhập một tên công ty (ví dụ: "Tập đoàn Vingroup", "Apple"), nhiệm vụ ĐẦU TIÊN của bạn là dùng tool `find_and_process_stock` để xác định mã chứng khoán chính thức.
117
+ * **Tên quốc gia:** Khi người dùng nhập tên quốc gia cho tiền tệ (ví dụ: "tiền Việt Nam"), bạn phải tự suy luận ra mã tiền tệ 3 chữ cái ("VND") TRƯỚC KHI gọi tool `perform_currency_conversion`.
118
+ 2. **HÀNH ĐỘNG VÀ THÔNG BÁO:** Vai trò của bạn là thực thi lệnh và thông báo ngắn gọn.
119
+ * **Tìm thấy 1 mã:** "Tôi đã tìm thấy [Tên công ty] ([Mã CK]) và đã tự động thêm vào danh sách theo dõi và biểu đồ của bạn."
120
+ * **Tìm thấy nhiều mã:** "Tôi tìm thấy một vài kết quả cho '[query]'. Bạn vui lòng cho biết mã chính xác bạn muốn theo dõi?"
121
+ * **Quy đổi tiền tệ:** "Đã thực hiện. Mời bạn xem kết quả chi tiết trong tab 'Quy đổi tiền tệ'."
122
+ 3. **CẤM LIỆT KÊ DỮ LIỆU:** Bảng điều khiển đã hiển thị tất cả. TUYỆT ĐỐI không lặp lại danh sách, các con số, hay dữ liệu thô trong câu trả lời của bạn.
123
+ """
124
+ @st.cache_resource
125
+ def get_model_and_tools():
126
+ find_stock_func = glm.FunctionDeclaration(name="find_and_process_stock", description="Tìm kiếm cổ phiếu theo mã hoặc tên và tự động xử lý. Dùng tool này ĐẦU TIÊN để xác định mã CK chính thức.", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'query': glm.Schema(type=glm.Type.STRING, description="Mã hoặc tên công ty, ví dụ: 'Vingroup', 'Apple'.")}, required=['query']))
127
+ get_ts_func = glm.FunctionDeclaration(name="get_smart_time_series", description="Lấy dữ liệu lịch sử giá sau khi đã biết mã CK chính thức.", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'symbol': glm.Schema(type=glm.Type.STRING), 'time_period': glm.Schema(type=glm.Type.STRING, enum=["intraday", "1_week", "1_month", "6_months", "1_year"])}, required=['symbol', 'time_period']))
128
+ currency_func = glm.FunctionDeclaration(name="perform_currency_conversion", description="Quy đổi tiền tệ sau khi đã biết mã 3 chữ cái của cặp tiền tệ nguồn/đích, ví dụ USD/VND, JPY/EUR", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'amount': glm.Schema(type=glm.Type.NUMBER), 'symbol': glm.Schema(type=glm.Type.STRING)}, required=['amount', 'symbol']))
129
+ finance_tool = glm.Tool(function_declarations=[find_stock_func, get_ts_func, currency_func])
130
+ model = genai.GenerativeModel(model_name="gemini-1.5-pro-latest", tools=[finance_tool], system_instruction=SYSTEM_INSTRUCTION)
131
+ return model
132
+ model = get_model_and_tools()
133
+ if st.session_state.chat_session is None:
134
+ st.session_state.chat_session = model.start_chat(history=[])
135
+ AVAILABLE_FUNCTIONS = {"find_and_process_stock": find_and_process_stock, "get_smart_time_series": get_smart_time_series, "perform_currency_conversion": perform_currency_conversion}
136
+
137
+ # --- 5. LOGIC HIỂN THỊ CÁC TAB ---
138
+ def get_y_axis_domain(series: pd.Series, padding_percent: float = 0.1):
139
+ if series.empty: return None
140
+ data_min, data_max = series.min(), series.max()
141
+ if pd.isna(data_min) or pd.isna(data_max): return None
142
+ data_range = data_max - data_min
143
+ if data_range == 0:
144
+ padding = abs(data_max * (padding_percent / 2))
145
+ return [data_min - padding, data_max + padding]
146
+ padding = data_range * padding_percent
147
+ return [data_min - padding, data_max + padding]
148
+
149
+ def render_watchlist_tab():
150
+ st.subheader("Danh sách theo dõi")
151
+ if not st.session_state.stock_watchlist: st.info("Chưa có cổ phiếu nào. Hãy thử tìm kiếm một mã như 'Apple' hoặc 'VNM'."); return
152
+ for symbol, stock_info in list(st.session_state.stock_watchlist.items()):
153
+ col1, col2, col3 = st.columns([4, 4, 1])
154
+ with col1: st.markdown(f"**{symbol}**"); st.caption(stock_info.get('name', 'N/A'))
155
+ with col2: st.markdown(f"**{stock_info.get('exchange', 'N/A')}**"); st.caption(f"{stock_info.get('country', 'N/A')} - {stock_info.get('currency', 'N/A')}")
156
+ with col3:
157
+ if st.button("🗑️", key=f"delete_{symbol}", help=f"Xóa {symbol}"):
158
+ st.session_state.stock_watchlist.pop(symbol, None); st.session_state.timeseries_cache.pop(symbol, None); st.rerun()
159
+ st.divider()
160
+
161
+ def render_timeseries_tab():
162
+ st.subheader("Phân tích Biểu đồ")
163
+ if not st.session_state.stock_watchlist:
164
+ st.info("Hãy thêm ít nhất một cổ phiếu vào danh sách để xem biểu đồ."); return
165
+ time_periods = {'Trong ngày': 'intraday', '1 Tuần': '1_week', '1 Tháng': '1_month', '6 Tháng': '6_months', '1 Năm': '1_year'}
166
+ period_keys = list(time_periods.keys())
167
+ period_values = list(time_periods.values())
168
+ default_index = period_values.index(st.session_state.active_timeseries_period) if st.session_state.active_timeseries_period in period_values else 0
169
+ selected_label = st.radio("Chọn khoảng thời gian:", options=period_keys, horizontal=True, index=default_index)
170
+ selected_period = time_periods[selected_label]
171
+ if st.session_state.active_timeseries_period != selected_period:
172
+ st.session_state.active_timeseries_period = selected_period
173
+ with st.spinner(f"Đang cập nhật biểu đồ..."):
174
+ for symbol in st.session_state.stock_watchlist.keys():
175
+ ts_data = get_smart_time_series(symbol, selected_period)
176
+ if 'values' in ts_data:
177
+ df = pd.DataFrame(ts_data['values']); df['datetime'] = pd.to_datetime(df['datetime']); df['close'] = pd.to_numeric(df['close'])
178
+ if symbol not in st.session_state.timeseries_cache: st.session_state.timeseries_cache[symbol] = {}
179
+ st.session_state.timeseries_cache[symbol][selected_period] = df.sort_values('datetime').set_index('datetime')
180
+ st.rerun()
181
+ all_series_data = {symbol: st.session_state.timeseries_cache[symbol][selected_period] for symbol in st.session_state.stock_watchlist.keys() if symbol in st.session_state.timeseries_cache and selected_period in st.session_state.timeseries_cache[symbol]}
182
+ if not all_series_data:
183
+ st.warning("Không có đủ dữ liệu cho khoảng thời gian đã chọn."); return
184
+ st.markdown("##### So sánh Hiệu suất Tăng trưởng (%)")
185
+ normalized_dfs = []
186
+ for symbol, df in all_series_data.items():
187
+ if not df.empty:
188
+ normalized_series = (df['close'] / df['close'].iloc[0]) * 100
189
+ normalized_df = normalized_series.reset_index(); normalized_df.columns = ['datetime', 'value']; normalized_df['symbol'] = symbol
190
+ normalized_dfs.append(normalized_df)
191
+ if normalized_dfs:
192
+ full_normalized_df = pd.concat(normalized_dfs)
193
+ y_domain = get_y_axis_domain(full_normalized_df['value'])
194
+ chart = alt.Chart(full_normalized_df).mark_line().encode(x=alt.X('datetime:T', title='Thời gian'), y=alt.Y('value:Q', scale=alt.Scale(domain=y_domain, zero=False), title='Tăng trưởng (%)'), color=alt.Color('symbol:N', title='Mã CK'), tooltip=[alt.Tooltip('symbol:N', title='Mã'), alt.Tooltip('datetime:T', title='Thời điểm', format='%Y-%m-%d %H:%M'), alt.Tooltip('value:Q', title='Tăng trưởng', format='.2f')]).interactive()
195
+ st.altair_chart(chart, use_container_width=True)
196
+ else:
197
+ st.warning("Không có dữ liệu để vẽ biểu đồ tăng trưởng.")
198
+ st.divider()
199
+ st.markdown("##### Biểu đồ Giá Thực tế")
200
+ for symbol, df in all_series_data.items():
201
+ stock_info = st.session_state.stock_watchlist.get(symbol, {})
202
+ st.markdown(f"**{symbol}** ({stock_info.get('currency', 'N/A')})")
203
+ if not df.empty:
204
+ y_domain = get_y_axis_domain(df['close'])
205
+ data_for_chart = df.reset_index()
206
+ price_chart = alt.Chart(data_for_chart).mark_line().encode(x=alt.X('datetime:T', title='Thời gian'), y=alt.Y('close:Q', scale=alt.Scale(domain=y_domain, zero=False), title='Giá'), tooltip=[alt.Tooltip('datetime:T', title='Thời điểm', format='%Y-%m-%d %H:%M'), alt.Tooltip('close:Q', title='Giá', format=',.2f')]).interactive()
207
+ st.altair_chart(price_chart, use_container_width=True)
208
+
209
+ def render_currency_tab():
210
+ st.subheader("Công cụ quy đổi tiền tệ"); state = st.session_state.currency_converter_state
211
+ col1, col2 = st.columns(2)
212
+ amount = col1.number_input("Số tiền", value=state['amount'], min_value=0.0, format="%.2f", key="conv_amount")
213
+ from_curr = col1.selectbox("Từ", options=AVAILABLE_CURRENCIES, index=AVAILABLE_CURRENCIES.index(state['from']) if state['from'] in AVAILABLE_CURRENCIES else 0, key="conv_from")
214
+ to_curr = col2.selectbox("Sang", options=AVAILABLE_CURRENCIES, index=AVAILABLE_CURRENCIES.index(state['to']) if state['to'] in AVAILABLE_CURRENCIES else 1, key="conv_to")
215
+ if st.button("Quy đổi", use_container_width=True, key="conv_btn"):
216
+ with st.spinner("Đang quy đổi..."): result = perform_currency_conversion(amount, f"{from_curr}/{to_curr}"); st.rerun()
217
+ if state['result']:
218
+ res = state['result']
219
+ if res.get('status') == 'Success': st.success(f"**Kết quả:** `{res['original_amount']:,.2f} {res['path_taken'][0]}` = `{res['final_amount']:,.2f} {res['path_taken'][-1]}`")
220
+ else: st.error(f"Lỗi: {res.get('error', 'Không rõ')}")
221
+
222
+ # --- 6. MAIN APP LAYOUT & CONTROL FLOW ---
223
+ st.title("📈 AI Financial Dashboard")
224
+
225
+ col1, col2 = st.columns([1, 1])
226
+
227
+ with col2:
228
+ right_column_container = st.container(height=600)
229
+ with right_column_container:
230
+ tab_names = ['Danh sách mã chứng khoán', 'Biểu đồ thời gian', 'Quy đổi tiền tệ']
231
+ try: default_index = tab_names.index(st.session_state.active_tab)
232
+ except ValueError: default_index = 0
233
+ st.session_state.active_tab = tab_names[default_index]
234
+
235
+ tab1, tab2, tab3 = st.tabs(tab_names)
236
+ with tab1: render_watchlist_tab()
237
+ with tab2: render_timeseries_tab()
238
+ with tab3: render_currency_tab()
239
+
240
+ with col1:
241
+ chat_container = st.container(height=600)
242
+ with chat_container:
243
+ for message in st.session_state.chat_history:
244
+ with st.chat_message(message["role"]):
245
+ st.markdown(message["parts"])
246
+
247
+ user_prompt = st.chat_input("Hỏi AI để điều khiển bảng điều khiển...")
248
+ if user_prompt:
249
+ st.session_state.chat_history.append({"role": "user", "parts": user_prompt})
250
+ st.rerun()
251
+
252
+ if st.session_state.chat_history and st.session_state.chat_history[-1]["role"] == "user":
253
+ last_user_prompt = st.session_state.chat_history[-1]["parts"]
254
+
255
+ # ***** ĐÂY LÀ PHẦN THAY ĐỔI *****
256
+ with chat_container:
257
+ with st.chat_message("model"):
258
+ with st.spinner("🤖 AI đang thực thi lệnh..."):
259
+ response = st.session_state.chat_session.send_message(last_user_prompt)
260
+ tool_calls = [part.function_call for part in response.candidates[0].content.parts if part.function_call]
261
+ while tool_calls:
262
+ tool_responses = []
263
+ for call in tool_calls:
264
+ func_name = call.name; func_args = {k: v for k, v in call.args.items()}
265
+ if func_name in AVAILABLE_FUNCTIONS:
266
+ tool_result = AVAILABLE_FUNCTIONS[func_name](**func_args)
267
+ tool_responses.append(glm.Part(function_response=glm.FunctionResponse(name=func_name, response={'result': tool_result})))
268
+ else:
269
+ tool_responses.append(glm.Part(function_response=glm.FunctionResponse(name=func_name, response={'error': f"Function '{func_name}' not found."})))
270
+ response = st.session_state.chat_session.send_message(glm.Content(parts=tool_responses))
271
+ tool_calls = [part.function_call for part in response.candidates[0].content.parts if part.function_call]
272
+
273
+ st.session_state.chat_history.append({"role": "model", "parts": response.text})
274
+ st.rerun()
pages/stock_report.py ADDED
@@ -0,0 +1,366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pages/stock_report.py
2
+ import os
3
+ import asyncio
4
+ import streamlit as st
5
+ import pandas as pd
6
+ import altair as alt
7
+ from io import BytesIO
8
+ import base64
9
+ import tempfile
10
+ import weasyprint
11
+ import markdown
12
+ import json
13
+ from datetime import datetime
14
+ from modules.analysis_pipeline import run_analysis_pipeline, generate_html_report
15
+ from twelvedata_api import TwelveDataAPI
16
+
17
+ # Thiết lập trang
18
+ st.set_page_config(
19
+ page_title="Stock Analysis Report",
20
+ page_icon="📊",
21
+ layout="wide"
22
+ )
23
+
24
+ # Tiêu đề ứng dụng
25
+ st.title("📄 In-depth Stock Analysis Report")
26
+ st.markdown("""
27
+ This application generates a comprehensive analysis report for a stock symbol, combining data from multiple sources
28
+ and using AI to synthesize information, helping you make better investment decisions.
29
+ """)
30
+
31
+ # Hàm tạo biểu đồ giá
32
+ def create_price_chart(price_data, period):
33
+ """Tạo biểu đồ giá từ dữ liệu"""
34
+ if 'values' not in price_data:
35
+ return None
36
+
37
+ df = pd.DataFrame(price_data['values'])
38
+ if df.empty:
39
+ return None
40
+
41
+ df['datetime'] = pd.to_datetime(df['datetime'])
42
+ df['close'] = pd.to_numeric(df['close'])
43
+
44
+ # Xác định tiêu đề biểu đồ dựa vào khoảng thời gian
45
+ title_map = {
46
+ '1_month': 'Stock price over the last month',
47
+ '3_months': 'Stock price over the last 3 months',
48
+ '1_year': 'Stock price over the last year'
49
+ }
50
+
51
+ # Tạo biểu đồ với Altair
52
+ chart = alt.Chart(df).mark_line().encode(
53
+ x=alt.X('datetime:T', title='Time'),
54
+ y=alt.Y('close:Q', title='Closing Price', scale=alt.Scale(zero=False)),
55
+ tooltip=[
56
+ alt.Tooltip('datetime:T', title='Date', format='%d/%m/%Y'),
57
+ alt.Tooltip('close:Q', title='Closing Price', format=',.2f'),
58
+ alt.Tooltip('volume:Q', title='Volume', format=',.0f')
59
+ ]
60
+ ).properties(
61
+ title=title_map.get(period, f'Stock price ({period})'),
62
+ height=350
63
+ ).interactive()
64
+
65
+ return chart
66
+
67
+ # Hàm chuyển đổi kết quả phân tích thành PDF
68
+ def convert_html_to_pdf(html_content):
69
+ """Chuyển đổi HTML thành file PDF"""
70
+ with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as f:
71
+ f.write(html_content.encode())
72
+ temp_html = f.name
73
+
74
+ pdf_bytes = weasyprint.HTML(filename=temp_html).write_pdf()
75
+
76
+ # Xóa file tạm sau khi sử dụng
77
+ os.unlink(temp_html)
78
+
79
+ return pdf_bytes
80
+
81
+ # Hàm tạo nút tải xuống file PDF
82
+ def get_download_link(pdf_bytes, filename):
83
+ """Tạo link tải xuống cho file PDF"""
84
+ b64 = base64.b64encode(pdf_bytes).decode()
85
+ href = f'<a href="data:application/pdf;base64,{b64}" download="{filename}">Download Report (PDF)</a>'
86
+ return href
87
+
88
+ # Danh sách các mã chứng khoán phổ biến và thông tin
89
+ def load_stock_symbols():
90
+ """Load stock symbols from cache or create new cache"""
91
+ cache_file = "static/stock_symbols_cache.json"
92
+
93
+ # Check if cache exists
94
+ if os.path.exists(cache_file):
95
+ try:
96
+ with open(cache_file, 'r') as f:
97
+ return json.load(f)
98
+ except Exception as e:
99
+ print(f"Error loading cache: {e}")
100
+
101
+ # Default list if cache doesn't exist or fails to load
102
+ default_symbols = [
103
+ {"symbol": "AAPL", "name": "Apple Inc."},
104
+ {"symbol": "MSFT", "name": "Microsoft Corporation"},
105
+ {"symbol": "GOOGL", "name": "Alphabet Inc."},
106
+ {"symbol": "AMZN", "name": "Amazon.com Inc."},
107
+ {"symbol": "TSLA", "name": "Tesla, Inc."},
108
+ {"symbol": "META", "name": "Meta Platforms, Inc."},
109
+ {"symbol": "NVDA", "name": "NVIDIA Corporation"},
110
+ {"symbol": "JPM", "name": "JPMorgan Chase & Co."},
111
+ {"symbol": "V", "name": "Visa Inc."},
112
+ {"symbol": "JNJ", "name": "Johnson & Johnson"},
113
+ {"symbol": "WMT", "name": "Walmart Inc."},
114
+ {"symbol": "MA", "name": "Mastercard Incorporated"},
115
+ {"symbol": "PG", "name": "Procter & Gamble Co."},
116
+ {"symbol": "UNH", "name": "UnitedHealth Group Inc."},
117
+ {"symbol": "HD", "name": "Home Depot Inc."},
118
+ {"symbol": "BAC", "name": "Bank of America Corp."},
119
+ {"symbol": "XOM", "name": "Exxon Mobil Corporation"},
120
+ {"symbol": "DIS", "name": "Walt Disney Co."},
121
+ {"symbol": "CSCO", "name": "Cisco Systems, Inc."},
122
+ {"symbol": "VZ", "name": "Verizon Communications Inc."},
123
+ {"symbol": "ADBE", "name": "Adobe Inc."},
124
+ {"symbol": "NFLX", "name": "Netflix, Inc."},
125
+ {"symbol": "CMCSA", "name": "Comcast Corporation"},
126
+ {"symbol": "PFE", "name": "Pfizer Inc."},
127
+ {"symbol": "KO", "name": "Coca-Cola Company"},
128
+ {"symbol": "INTC", "name": "Intel Corporation"},
129
+ {"symbol": "PYPL", "name": "PayPal Holdings, Inc."},
130
+ {"symbol": "T", "name": "AT&T Inc."},
131
+ {"symbol": "PEP", "name": "PepsiCo, Inc."},
132
+ {"symbol": "MRK", "name": "Merck & Co., Inc."}
133
+ ]
134
+
135
+ # Try to fetch more comprehensive list if API key is available
136
+ try:
137
+ from dotenv import load_dotenv
138
+ load_dotenv()
139
+ api_key = os.getenv("TWELVEDATA_API_KEY")
140
+ if api_key:
141
+ td_api = TwelveDataAPI(api_key)
142
+ stocks_data = td_api.get_all_stocks(exchange="NASDAQ")
143
+ if stocks_data and 'data' in stocks_data:
144
+ # Convert to format we need and take first 1000 stocks
145
+ symbols = [{"symbol": stock["symbol"], "name": stock.get("name", "Unknown")}
146
+ for stock in stocks_data['data']]
147
+
148
+ # Save to cache
149
+ os.makedirs(os.path.dirname(cache_file), exist_ok=True)
150
+ with open(cache_file, 'w') as f:
151
+ json.dump(symbols, f)
152
+
153
+ return symbols
154
+ except Exception as e:
155
+ print(f"Error fetching stock symbols from API: {e}")
156
+
157
+ # If everything fails, return default list
158
+ return default_symbols
159
+
160
+ # Load stock symbols
161
+ STOCK_SYMBOLS = load_stock_symbols()
162
+
163
+ # Function to format stock options for display
164
+ def format_stock_option(stock):
165
+ return f"{stock['symbol']} - {stock['name']}"
166
+
167
+ # Tạo giao diện
168
+ col1, col2 = st.columns([3, 1])
169
+
170
+ # Phần nhập thông tin
171
+ with col2:
172
+ st.subheader("Enter Information")
173
+
174
+ # Create a list of formatted options and a mapping back to symbols
175
+ stock_options = [format_stock_option(stock) for stock in STOCK_SYMBOLS]
176
+
177
+ # Use selectbox with search functionality
178
+ selected_stock = st.selectbox(
179
+ "Select a stock symbol",
180
+ options=stock_options,
181
+ index=0 if stock_options else None,
182
+ placeholder="Search for a stock symbol...",
183
+ )
184
+
185
+ # Extract symbol from selection
186
+ if selected_stock:
187
+ stock_symbol = selected_stock.split(" - ")[0]
188
+ else:
189
+ stock_symbol = ""
190
+
191
+ if st.button("Generate Report", use_container_width=True, type="primary"):
192
+ if not stock_symbol:
193
+ st.error("Please select a stock symbol to continue.")
194
+ else:
195
+ # Lưu mã cổ phiếu vào session state để duy trì giữa các lần chạy
196
+ st.session_state.stock_symbol = stock_symbol
197
+ st.session_state.analysis_requested = True
198
+ st.rerun()
199
+
200
+ # PDF report generation section - moved from tab1
201
+ if "analysis_complete" in st.session_state and st.session_state.analysis_complete:
202
+ st.divider()
203
+ st.subheader("PDF Report")
204
+
205
+ # Lấy kết quả từ session state
206
+ analysis_results = st.session_state.analysis_results
207
+
208
+ # Tạo thư mục static nếu chưa tồn tại
209
+ os.makedirs("static", exist_ok=True)
210
+
211
+ # Tạo tên file PDF và đường dẫn
212
+ filename = f"Report_{analysis_results['symbol']}_{datetime.now().strftime('%d%m%Y')}.pdf"
213
+ pdf_path = os.path.join("static", filename)
214
+
215
+ # Hiển thị thông tin
216
+ st.markdown("Get a complete PDF report with price charts:")
217
+
218
+ # Import hàm tạo PDF
219
+ from modules.analysis_pipeline import generate_pdf_report
220
+
221
+ # Nút tạo và tải xuống PDF (gộp chung)
222
+ if st.button("📊 Generate & Download PDF Report", use_container_width=True, key="pdf_btn", type="primary"):
223
+ # Kiểm tra nếu file không tồn tại hoặc cần tạo lại
224
+ if not os.path.exists(pdf_path):
225
+ with st.spinner("Creating PDF report with charts..."):
226
+ generate_pdf_report(analysis_results, pdf_path)
227
+
228
+ if not os.path.exists(pdf_path):
229
+ st.error("Failed to create PDF report.")
230
+ st.stop()
231
+
232
+ # Đọc file PDF để tải xuống
233
+ with open(pdf_path, "rb") as pdf_file:
234
+ pdf_bytes = pdf_file.read()
235
+
236
+ # Hiển thị thông báo thành công và widget tải xuống
237
+ st.success("PDF report generated successfully!")
238
+
239
+ st.download_button(
240
+ label="⬇️ Download Report",
241
+ data=pdf_bytes,
242
+ file_name=filename,
243
+ mime="application/pdf",
244
+ use_container_width=True,
245
+ key="download_pdf_btn"
246
+ )
247
+
248
+ # Phần hiển thị báo cáo
249
+ with col1:
250
+ # Kiểm tra xem có yêu cầu phân tích không
251
+ if "analysis_requested" in st.session_state and st.session_state.analysis_requested:
252
+ symbol = st.session_state.stock_symbol
253
+
254
+ with st.spinner(f"🔍 Collecting data and analyzing {symbol} stock... (this may take a few minutes)"):
255
+ try:
256
+ # Chạy phân tích
257
+ analysis_results = asyncio.run(run_analysis_pipeline(symbol))
258
+
259
+ # Lưu kết quả vào session state
260
+ st.session_state.analysis_results = analysis_results
261
+ st.session_state.analysis_complete = True
262
+ st.session_state.analysis_requested = False
263
+
264
+ # Tự động rerun để hiển thị kết quả
265
+ st.rerun()
266
+ except Exception as e:
267
+ st.error(f"An error occurred during analysis: {str(e)}")
268
+ st.session_state.analysis_requested = False
269
+
270
+ # Kiểm tra xem phân tích đã hoàn thành chưa
271
+ if "analysis_complete" in st.session_state and st.session_state.analysis_complete:
272
+ # Lấy kết quả từ session state
273
+ analysis_results = st.session_state.analysis_results
274
+
275
+ # Tạo các tab để hiển thị nội dung
276
+ tab1, tab2, tab3, tab4, tab5 = st.tabs([
277
+ "📋 Overview",
278
+ "💰 Financial Health",
279
+ "📰 News & Sentiment",
280
+ "👨‍💼 Market Analysis",
281
+ "📊 Price Charts"
282
+ ])
283
+
284
+ with tab1:
285
+ # Hiển thị thông tin cơ bản về công ty
286
+ overview = analysis_results.get('overview', {})
287
+ if overview:
288
+ col1, col2 = st.columns([1, 1])
289
+ with col1:
290
+ st.subheader(f"{analysis_results['symbol']} - {overview.get('Name', 'N/A')}")
291
+ st.write(f"**Industry:** {overview.get('Industry', 'N/A')}")
292
+ st.write(f"**Sector:** {overview.get('Sector', 'N/A')}")
293
+ with col2:
294
+ st.write(f"**Market Cap:** {overview.get('MarketCapitalization', 'N/A')}")
295
+ st.write(f"**P/E Ratio:** {overview.get('PERatio', 'N/A')}")
296
+ st.write(f"**Dividend Yield:** {overview.get('DividendYield', 'N/A')}%")
297
+
298
+ # Hiển thị tóm tắt
299
+ st.markdown("### Summary & Recommendation")
300
+ st.markdown(analysis_results['analysis']['summary'])
301
+
302
+ with tab2:
303
+ st.markdown("### Financial Health Analysis")
304
+ st.markdown(analysis_results['analysis']['financial_health'])
305
+
306
+ with tab3:
307
+ st.markdown("### News & Market Sentiment Analysis")
308
+ st.markdown(analysis_results['analysis']['news_sentiment'])
309
+
310
+ with tab4:
311
+ st.markdown("### Market Analysis")
312
+ st.markdown(analysis_results['analysis']['expert_opinion'])
313
+
314
+ with tab5:
315
+ st.markdown("### Stock Price Charts")
316
+
317
+ # Hiển thị biểu đồ từ dữ liệu giá
318
+ price_data = analysis_results.get('price_data', {})
319
+ if price_data:
320
+ period_tabs = st.tabs(['1 Month', '3 Months', '1 Year'])
321
+
322
+ periods = ['1_month', '3_months', '1_year']
323
+ for i, period in enumerate(periods):
324
+ with period_tabs[i]:
325
+ if period in price_data:
326
+ chart = create_price_chart(price_data[period], period)
327
+ if chart:
328
+ st.altair_chart(chart, use_container_width=True)
329
+ else:
330
+ st.info(f"Insufficient data to display chart for {period} timeframe.")
331
+ else:
332
+ st.info(f"No chart data available for {period} timeframe.")
333
+ else:
334
+ st.info("No price chart data available for this stock.")
335
+ else:
336
+ # Hiển thị hướng dẫn khi không có phân tích
337
+ st.info("👈 Enter a stock symbol and click 'Generate Report' to begin.")
338
+ st.markdown("""
339
+ ### About Stock Analysis Reports
340
+
341
+ The stock analysis report includes the following information:
342
+
343
+ 1. **Overview & Investment Recommendation**: Summary of the company and general investment potential assessment.
344
+ 2. **Financial Health Analysis**: Evaluation of financial metrics, revenue growth, and profitability.
345
+ 3. **News & Market Sentiment Analysis**: Summary of notable news related to the company.
346
+ 4. **Market Analysis**: Analysis of current stock performance and market trends.
347
+ 5. **Price Charts**: Stock price charts for various timeframes.
348
+
349
+ Reports are generated based on data from multiple sources and analyzed by AI.
350
+ """)
351
+
352
+ # Hiển thị các mã cổ phiếu phổ biến
353
+ st.markdown("### Popular Stock Symbols")
354
+
355
+ # Hiển thị danh sách các mã cổ phiếu phổ biến theo lưới
356
+ # Chỉ lấy 12 mã đầu tiên để không làm rối giao diện
357
+ display_stocks = STOCK_SYMBOLS[:12]
358
+
359
+ # Tạo lưới với 4 cột
360
+ cols = st.columns(4)
361
+ for i, stock in enumerate(display_stocks):
362
+ col = cols[i % 4]
363
+ if col.button(f"{stock['symbol']} - {stock['name']}", key=f"pop_stock_{i}", use_container_width=True):
364
+ st.session_state.stock_symbol = stock['symbol']
365
+ st.session_state.analysis_requested = True
366
+ st.rerun()
requirements.txt CHANGED
@@ -1,11 +1,20 @@
 
 
 
1
  altair==5.5.0
2
  annotated-types==0.7.0
 
3
  attrs==25.3.0
4
  blinker==1.9.0
 
5
  cachetools==5.5.2
6
  certifi==2025.7.14
 
7
  charset-normalizer==3.4.2
8
  click==8.2.1
 
 
 
9
  gitdb==4.0.12
10
  GitPython==3.1.45
11
  google-ai-generativelanguage==0.6.15
@@ -13,30 +22,43 @@ google-api-core==2.25.1
13
  google-api-python-client==2.177.0
14
  google-auth==2.40.3
15
  google-auth-httplib2==0.2.0
 
16
  google-generativeai==0.8.5
17
  googleapis-common-protos==1.70.0
18
  grpcio==1.74.0
19
  grpcio-status==1.71.2
 
 
20
  httplib2==0.22.0
 
21
  idna==3.10
22
  Jinja2==3.1.6
23
  jsonschema==4.25.0
24
  jsonschema-specifications==2025.4.1
 
 
25
  MarkupSafe==3.0.2
 
 
26
  narwhals==1.48.1
27
  numpy==2.3.2
28
  packaging==25.0
29
  pandas==2.3.1
30
  pillow==11.3.0
 
31
  proto-plus==1.26.1
32
  protobuf==5.29.5
33
  pyarrow==21.0.0
34
  pyasn1==0.6.1
35
  pyasn1_modules==0.4.2
 
36
  pydantic==2.11.7
37
  pydantic_core==2.33.2
38
  pydeck==0.9.1
 
 
39
  pyparsing==3.2.3
 
40
  python-dateutil==2.9.0.post0
41
  python-dotenv==1.1.1
42
  pytz==2025.2
@@ -47,9 +69,13 @@ rsa==4.9.1
47
  setuptools==78.1.1
48
  six==1.17.0
49
  smmap==5.0.2
 
50
  streamlit==1.47.1
51
- tenacity==9.1.2
 
 
52
  toml==0.10.2
 
53
  tornado==6.5.1
54
  tqdm==4.67.1
55
  typing-inspection==0.4.1
@@ -57,5 +83,12 @@ typing_extensions==4.14.1
57
  tzdata==2025.2
58
  uritemplate==4.2.0
59
  urllib3==2.5.0
 
 
60
  watchdog==6.0.0
 
 
 
61
  wheel==0.45.1
 
 
 
1
+ aiohappyeyeballs==2.6.1
2
+ aiohttp==3.12.14
3
+ aiosignal==1.4.0
4
  altair==5.5.0
5
  annotated-types==0.7.0
6
+ anyio==4.9.0
7
  attrs==25.3.0
8
  blinker==1.9.0
9
+ Brotli==1.1.0
10
  cachetools==5.5.2
11
  certifi==2025.7.14
12
+ cffi==1.17.1
13
  charset-normalizer==3.4.2
14
  click==8.2.1
15
+ cssselect2==0.8.0
16
+ fonttools==4.59.0
17
+ frozenlist==1.7.0
18
  gitdb==4.0.12
19
  GitPython==3.1.45
20
  google-ai-generativelanguage==0.6.15
 
22
  google-api-python-client==2.177.0
23
  google-auth==2.40.3
24
  google-auth-httplib2==0.2.0
25
+ google-genai==1.27.0
26
  google-generativeai==0.8.5
27
  googleapis-common-protos==1.70.0
28
  grpcio==1.74.0
29
  grpcio-status==1.71.2
30
+ h11==0.16.0
31
+ httpcore==1.0.9
32
  httplib2==0.22.0
33
+ httpx==0.28.1
34
  idna==3.10
35
  Jinja2==3.1.6
36
  jsonschema==4.25.0
37
  jsonschema-specifications==2025.4.1
38
+ Markdown==3.8.2
39
+ markdown-it-py==3.0.0
40
  MarkupSafe==3.0.2
41
+ mdurl==0.1.2
42
+ multidict==6.6.3
43
  narwhals==1.48.1
44
  numpy==2.3.2
45
  packaging==25.0
46
  pandas==2.3.1
47
  pillow==11.3.0
48
+ propcache==0.3.2
49
  proto-plus==1.26.1
50
  protobuf==5.29.5
51
  pyarrow==21.0.0
52
  pyasn1==0.6.1
53
  pyasn1_modules==0.4.2
54
+ pycparser==2.22
55
  pydantic==2.11.7
56
  pydantic_core==2.33.2
57
  pydeck==0.9.1
58
+ pydyf==0.11.0
59
+ Pygments==2.19.2
60
  pyparsing==3.2.3
61
+ pyphen==0.17.2
62
  python-dateutil==2.9.0.post0
63
  python-dotenv==1.1.1
64
  pytz==2025.2
 
69
  setuptools==78.1.1
70
  six==1.17.0
71
  smmap==5.0.2
72
+ sniffio==1.3.1
73
  streamlit==1.47.1
74
+ tenacity==8.5.0
75
+ tinycss2==1.4.0
76
+ tinyhtml5==2.0.0
77
  toml==0.10.2
78
+ toolz==0.12.1
79
  tornado==6.5.1
80
  tqdm==4.67.1
81
  typing-inspection==0.4.1
 
83
  tzdata==2025.2
84
  uritemplate==4.2.0
85
  urllib3==2.5.0
86
+ vega-datasets==0.9.0
87
+ vl-convert-python==1.3.0
88
  watchdog==6.0.0
89
+ weasyprint==66.0
90
+ webencodings==0.5.1
91
+ websockets==15.0.1
92
  wheel==0.45.1
93
+ yarl==1.20.1
94
+ zopfli==2.0.3.post1
static/report_template.html ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Stock Analysis Report</title>
7
+ <style>
8
+ body {
9
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ margin: 0;
13
+ padding: 0;
14
+ background-color: #f9f9f9;
15
+ }
16
+ .container {
17
+ max-width: 1100px;
18
+ margin: 0 auto;
19
+ background-color: #fff;
20
+ padding: 0;
21
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
22
+ }
23
+ .report-header {
24
+ background-color: #2c3e50;
25
+ color: white;
26
+ padding: 30px;
27
+ position: relative;
28
+ }
29
+ .report-date {
30
+ position: absolute;
31
+ top: 20px;
32
+ right: 20px;
33
+ font-size: 14px;
34
+ }
35
+ .report-title {
36
+ margin: 0;
37
+ padding: 0;
38
+ font-size: 28px;
39
+ }
40
+ .report-subtitle {
41
+ margin: 5px 0 0;
42
+ padding: 0;
43
+ font-size: 18px;
44
+ font-weight: normal;
45
+ }
46
+ .report-body {
47
+ padding: 20px 30px;
48
+ }
49
+ .section {
50
+ margin-bottom: 30px;
51
+ border-bottom: 1px solid #eee;
52
+ padding-bottom: 20px;
53
+ }
54
+ h2 {
55
+ color: #2c3e50;
56
+ font-size: 22px;
57
+ margin-top: 30px;
58
+ margin-bottom: 15px;
59
+ border-bottom: 2px solid #3498db;
60
+ padding-bottom: 5px;
61
+ }
62
+ h3 {
63
+ color: #3498db;
64
+ font-size: 18px;
65
+ margin-top: 20px;
66
+ }
67
+ table {
68
+ width: 100%;
69
+ border-collapse: collapse;
70
+ margin: 15px 0;
71
+ }
72
+ th, td {
73
+ padding: 12px;
74
+ border: 1px solid #ddd;
75
+ text-align: left;
76
+ }
77
+ th {
78
+ background-color: #f2f2f2;
79
+ }
80
+ .bullish {
81
+ color: #27ae60;
82
+ font-weight: bold;
83
+ }
84
+ .bearish {
85
+ color: #e74c3c;
86
+ font-weight: bold;
87
+ }
88
+ .neutral {
89
+ color: #f39c12;
90
+ font-weight: bold;
91
+ }
92
+ ul, ol {
93
+ margin-left: 20px;
94
+ }
95
+ .kpi-grid {
96
+ display: grid;
97
+ grid-template-columns: repeat(3, 1fr);
98
+ gap: 15px;
99
+ margin: 20px 0;
100
+ }
101
+ .kpi-card {
102
+ background-color: #f8f9fa;
103
+ border-radius: 5px;
104
+ padding: 15px;
105
+ text-align: center;
106
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
107
+ }
108
+ .kpi-title {
109
+ font-size: 14px;
110
+ color: #666;
111
+ margin-bottom: 5px;
112
+ }
113
+ .kpi-value {
114
+ font-size: 20px;
115
+ font-weight: bold;
116
+ color: #2c3e50;
117
+ }
118
+ .footer {
119
+ text-align: center;
120
+ padding: 20px;
121
+ font-size: 12px;
122
+ color: #666;
123
+ border-top: 1px solid #eee;
124
+ }
125
+ </style>
126
+ </head>
127
+ <body>
128
+ <div class="container">
129
+ <div class="report-header">
130
+ <div class="report-date">{{date}}</div>
131
+ <h1 class="report-title">Stock Analysis Report: {{symbol}}</h1>
132
+ <h2 class="report-subtitle">{{company_name}}</h2>
133
+ </div>
134
+
135
+ <div class="report-body">
136
+ <div class="section">
137
+ <h2>Summary & Recommendation</h2>
138
+ {{summary}}
139
+ </div>
140
+
141
+ <div class="section">
142
+ <h2>Financial Health Analysis</h2>
143
+ {{financial_health}}
144
+ </div>
145
+
146
+ <div class="section">
147
+ <h2>News & Market Sentiment Analysis</h2>
148
+ {{news_sentiment}}
149
+ </div>
150
+
151
+ <div class="section">
152
+ <h2>Market Analysis</h2>
153
+ {{expert_opinion}}
154
+ </div>
155
+
156
+ <div class="footer">
157
+ This report was automatically generated by AI Financial Dashboard. Information is for reference only.
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </body>
162
+ </html>
static/stock_symbols_cache.json ADDED
The diff for this file is too large to render. See raw diff