tosanoob commited on
Commit
dec9e8e
·
1 Parent(s): acb1704

feat: update report format

Browse files
Files changed (4) hide show
  1. Home.py +32 -47
  2. modules/analysis_pipeline.py +69 -49
  3. pages/chat_app.py +54 -54
  4. pages/stock_report.py +40 -40
Home.py CHANGED
@@ -1,4 +1,4 @@
1
- # Home.py (Trang chủ ứng dụng)
2
  import streamlit as st
3
  import os
4
  from dotenv import load_dotenv
@@ -7,49 +7,49 @@ import google.generativeai as genai
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 ứ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"),
@@ -58,32 +58,17 @@ api_keys = {
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")
 
1
+ # Home.py (Main application homepage)
2
  import streamlit as st
3
  import os
4
  from dotenv import load_dotenv
 
7
  # Load environment variables
8
  load_dotenv()
9
 
10
+ # Page setup
11
  st.set_page_config(
12
  page_title="AI Financial Dashboard",
13
  page_icon="📊",
14
  layout="wide"
15
  )
16
 
17
+ # Application title
18
  st.title("📊 AI Financial Dashboard v2.0")
19
 
20
+ # Display application information
21
  st.markdown("""
22
+ ## Welcome to AI Financial Dashboard
23
 
24
+ This is an intelligent financial analysis application using AI to help you make smarter investment decisions.
25
 
26
+ ### Main Features:
27
 
28
+ 1. **💬 Chat with AI Financial Analyst**:
29
+ - Search for stock information
30
+ - View price charts
31
+ - Convert currencies
32
   
33
+ 2. **📄 In-depth Stock Analysis Report**:
34
+ - Comprehensive analysis of a specific stock
35
+ - Data collection from multiple sources
36
+ - Generate in-depth reports with AI evaluation
37
   
38
+ 3. **📰 Daily Market Summary Newsletter** (Coming soon):
39
+ - Compilation of latest financial news
40
+ - Categorized by topic
41
+ - Daily market updates
42
 
43
+ ### How to Use:
44
 
45
+ Use the navigation bar on the left to switch between different features of the application.
46
 
47
  """)
48
 
49
+ # Display API connection status
50
+ st.sidebar.title("Connection Status")
51
 
52
+ # Check API keys
53
  api_keys = {
54
  "GEMINI_API_KEY": os.getenv("GEMINI_API_KEY"),
55
  "ALPHA_VANTAGE_API_KEY": os.getenv("ALPHA_VANTAGE_API_KEY"),
 
58
  "TWELVEDATA_API_KEY": os.getenv("TWELVEDATA_API_KEY")
59
  }
60
 
61
+ # Display status of each API
62
  for api_name, api_key in api_keys.items():
63
  if api_key:
64
+ st.sidebar.success(f"✅ {api_name.split('_KEY')[0].replace('_', ' ')}")
65
  else:
66
+ st.sidebar.error(f"❌ {api_name.split('_KEY')[0].replace('_', ' ')}")
67
 
68
+ # Display project information
69
  st.sidebar.markdown("---")
70
  st.sidebar.markdown("""
71
+ ### Project Information
72
+ - **Version**: 2.0
73
+ - **Update**: In-depth analysis report feature
74
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modules/analysis_pipeline.py CHANGED
@@ -6,6 +6,7 @@ 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()
@@ -23,23 +24,55 @@ class StockAnalysisPipeline:
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"""
@@ -108,37 +141,11 @@ class StockAnalysisPipeline:
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', {}),
@@ -167,14 +174,18 @@ class StockAnalysisPipeline:
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', {}),
@@ -201,14 +212,18 @@ class StockAnalysisPipeline:
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', {})
@@ -306,14 +321,18 @@ class StockAnalysisPipeline:
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', ''),
@@ -355,9 +374,10 @@ class StockAnalysisPipeline:
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
 
@@ -498,7 +518,7 @@ def generate_html_report(analysis_results):
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(
 
6
  import google.generativeai as genai
7
  from dotenv import load_dotenv
8
  from .api_clients import AlphaVantageClient, NewsAPIClient, MarketauxClient, get_price_history
9
+ import time
10
 
11
  # Load environment variables and configure AI
12
  load_dotenv()
 
24
  self.analysis_results = {}
25
  self.ai_model = genai.GenerativeModel(model_name=MODEL_NAME)
26
 
27
+ async def run_analysis(self):
28
+ """Run the full analysis pipeline in an interleaved pattern"""
29
+ print(f"Starting analysis pipeline for {self.symbol}...")
 
 
 
 
 
 
 
 
 
30
 
31
+ # 1. Get company overview and financial statements first
32
+ await self._get_company_overview()
33
+ if hasattr(self, 'company_name'):
34
+ print(f"Analyzing {self.symbol} ({self.company_name})")
35
+ else:
36
+ self.company_name = self.symbol
37
+ print(f"Analyzing {self.symbol}")
38
+
39
+ # 2. Get and analyze financial statements
40
+ print("Getting financial data...")
41
+ await self._get_financial_statements()
42
+
43
+ # 3. Run financial health analysis with Gemini
44
+ print("Analyzing financial health...")
45
+ self.analysis_results['financial_health'] = await self._analyze_financial_health()
46
+
47
+ # 4. Get and analyze market news and sentiment
48
+ print("Getting news and sentiment data...")
49
+ await self._get_market_sentiment_and_news()
50
+
51
+ # 5. Run news sentiment analysis with Gemini
52
+ print("Analyzing news and sentiment...")
53
+ self.analysis_results['news_sentiment'] = await self._analyze_news_sentiment()
54
 
55
+ # 6. Get quote data and price history
56
+ print("Getting quote and price data...")
57
+ await self._get_analyst_ratings()
58
+ await self._get_price_data()
59
+
60
+ # 7. Run expert opinion analysis with Gemini
61
+ print("Analyzing market data...")
62
+ self.analysis_results['expert_opinion'] = await self._analyze_expert_opinion()
63
+
64
+ # 8. Create final summary and recommendation
65
+ print("Creating final summary and recommendation...")
66
+ self.analysis_results['summary'] = await self._create_summary()
67
+
68
+ # 9. Return the complete analysis
69
+ return {
70
+ 'symbol': self.symbol,
71
+ 'company_name': self.company_name,
72
+ 'analysis': self.analysis_results,
73
+ 'price_data': self.company_data.get('price_data', {}),
74
+ 'overview': self.company_data.get('overview', {})
75
+ }
76
 
77
  async def _get_company_overview(self):
78
  """Get company overview information"""
 
141
  self.company_data['price_data'] = price_data
142
  print(f"Retrieved price history for {self.symbol}")
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  async def _analyze_financial_health(self):
145
  """Analyze company's financial health using AI"""
146
+ # Add a small delay before API call to Gemini to avoid rate limiting
147
+ await asyncio.sleep(1)
148
+
149
  # Prepare financial data for the AI
150
  financial_data = {
151
  'overview': self.company_data.get('overview', {}),
 
174
  - DO NOT include any concluding phrases
175
  - Present only factual analysis based on the data
176
  - Present the information directly and objectively
177
+ - Prefer using the correct currency text instead of the symbol. For example, use USD instead of $
178
  """
179
 
180
+ # Get AI response
181
  response = self.ai_model.generate_content(prompt)
182
  return response.text
183
 
184
  async def _analyze_news_sentiment(self):
185
  """Analyze news and market sentiment using AI"""
186
+ # Add a small delay before API call to Gemini to avoid rate limiting
187
+ await asyncio.sleep(1)
188
+
189
  # Prepare news data for the AI
190
  news_data = {
191
  'alpha_news': self.company_data.get('alpha_news', {}),
 
212
  - DO NOT include any concluding phrases
213
  - Present only factual analysis based on the data
214
  - Present the information directly and objectively
215
+ - Prefer using the correct currency text instead of the symbol. For example, use USD instead of $
216
  """
217
 
218
+ # Get AI response
219
  response = self.ai_model.generate_content(prompt)
220
  return response.text
221
 
222
  async def _analyze_expert_opinion(self):
223
  """Analyze current stock quote and price data"""
224
+ # Add a small delay before API call to Gemini to avoid rate limiting
225
+ await asyncio.sleep(1)
226
+
227
  # Prepare data for the AI
228
  quote_data = self.company_data.get('quote_data', {})
229
  price_data = self.company_data.get('price_data', {})
 
321
  - DO NOT include any concluding phrases
322
  - Present only factual analysis based on the data
323
  - Present the information directly and objectively
324
+ - Prefer using the correct currency text instead of the symbol. For example, use USD instead of $
325
  """
326
 
327
+ # Get AI response
328
  response = self.ai_model.generate_content(prompt)
329
  return response.text
330
 
331
  async def _create_summary(self):
332
  """Create a comprehensive summary and investment recommendation"""
333
+ # Add a small delay before API call to Gemini to avoid rate limiting
334
+ await asyncio.sleep(1)
335
+
336
  # Combine all analyses
337
  combined_analysis = {
338
  'financial_health': self.analysis_results.get('financial_health', ''),
 
374
  - DO NOT include any concluding phrases or sign-offs
375
  - Present the report directly and objectively
376
  - The report should be comprehensive but concise
377
+ - Prefer using the correct currency text instead of the symbol. For example, use USD instead of $
378
  """
379
 
380
+ # Get AI response
381
  response = self.ai_model.generate_content(prompt)
382
  return response.text
383
 
 
518
  expert_text = process_markdown_text(analysis_results['analysis']['expert_opinion'])
519
  import json
520
 
521
+ json.dump({'summary': summary_text, 'financial': financial_text, 'news': news_text, 'expert': expert_text}, open('analysis_results.json', 'w'), ensure_ascii=False, indent=4)
522
 
523
  # Convert to HTML
524
  summary_html = markdown.markdown(
pages/chat_app.py CHANGED
@@ -1,8 +1,8 @@
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
@@ -11,7 +11,7 @@ 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"))
@@ -25,12 +25,12 @@ def initialize_state():
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 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()
@@ -48,7 +48,7 @@ def load_market_data():
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()
@@ -64,14 +64,14 @@ def find_and_process_stock(query: str):
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
@@ -84,9 +84,9 @@ def find_conversion_path_bfs(start, end):
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]
@@ -97,7 +97,7 @@ def convert_currency_with_bridge(amount: float, symbol: str):
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)
@@ -105,27 +105,27 @@ def perform_currency_conversion(amount: float, symbol: str):
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 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 hiểu yêu cầu của người dùng, gọi các công cụ phù hợp, 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 ( dụ: "Tập đoàn Vingroup", "Apple"), nhiệm vụ ĐẦU TIÊN của bạn dùng tool `find_and_process_stock` để xác định 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ệ ( 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 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]) đã 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 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 hoặc tên 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=" hoặc tên công ty, 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, 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
@@ -134,7 +134,7 @@ 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()
@@ -147,30 +147,30 @@ def get_y_axis_domain(series: pd.Series, padding_percent: float = 0.1):
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ổ phiếu nào. Hãy thử tìm kiếm một 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:
@@ -180,8 +180,8 @@ def render_timeseries_tab():
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 đủ 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:
@@ -191,33 +191,33 @@ def render_timeseries_tab():
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=''), 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 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")
@@ -227,7 +227,7 @@ col1, col2 = st.columns([1, 1])
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]
@@ -244,7 +244,7 @@ with col1:
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()
@@ -252,10 +252,10 @@ if user_prompt:
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 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:
 
1
+ # app.py (Final version with Advanced Charts)
2
 
3
  import streamlit as st
4
  import pandas as pd
5
+ import altair as alt # <-- Add Altair library
6
  import google.generativeai as genai
7
  import google.ai.generativelanguage as glm
8
  from dotenv import load_dotenv
 
11
  from collections import deque
12
  from datetime import datetime
13
 
14
+ # --- 1. INITIAL CONFIGURATION & STATE INITIALIZATION ---
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"))
 
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 = 'Stock Watchlist'
29
  st.session_state.chat_session = None
30
  initialize_state()
31
 
32
+ # --- 2. LOAD BACKGROUND DATA ---
33
+ @st.cache_data(show_spinner="Loading and preparing market data...")
34
  def load_market_data():
35
  td_api = st.session_state.td_api
36
  stocks_data = td_api.get_all_stocks()
 
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. TOOL EXECUTION LOGIC ---
52
  def find_and_process_stock(query: str):
53
  print(f"Hybrid searching for stock: '{query}'...")
54
  query_lower = query.lower()
 
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 = 'Time Charts'; 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"Time period '{time_period}' is not valid."}
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
 
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": "Invalid currency pair format."}
88
  path = find_conversion_path_bfs(start_currency, end_currency)
89
+ if not path: return {"error": f"No conversion path found from {start_currency} to {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]
 
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"Error in conversion step from {step_start} to {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)
 
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 = 'Currency Converter'
109
  return result
110
 
111
+ # --- 4. GEMINI CONFIGURATION ---
112
+ SYSTEM_INSTRUCTION = """You are the AI brain controlling an Interactive Financial Dashboard. Your task is to understand user requests, call appropriate tools, and report results concisely.
113
 
114
+ GOLDEN RULES:
115
+ 1. **UNDERSTAND FIRST, CALL LATER:**
116
+ * **Company Name:** When a user enters a company name (e.g., "Vingroup Corporation", "Apple"), your FIRST task is to use the `find_and_process_stock` tool to identify the official stock symbol.
117
+ * **Country Name:** When a user enters a country name for currency (e.g., "Vietnamese currency"), you must infer the 3-letter currency code ("VND") BEFORE calling the `perform_currency_conversion` tool.
118
+ 2. **ACT AND NOTIFY:** Your role is to execute commands and report briefly.
119
+ * **Found 1 symbol:** "I've found [Company Name] ([Symbol]) and automatically added it to your watchlist and chart."
120
+ * **Found multiple symbols:** "I found several results for '[query]'. Please specify which exact symbol you want to track?"
121
+ * **Currency conversion:** "Done. Please see detailed results in the 'Currency Converter' tab."
122
+ 3. **NO DATA LISTING:** The dashboard already displays everything. ABSOLUTELY do not repeat lists, numbers, or raw data in your response.
123
  """
124
  @st.cache_resource
125
  def get_model_and_tools():
126
+ find_stock_func = glm.FunctionDeclaration(name="find_and_process_stock", description="Search for stock by symbol or name and automatically process. Use this tool FIRST to identify the official stock symbol.", parameters=glm.Schema(type=glm.Type.OBJECT, properties={'query': glm.Schema(type=glm.Type.STRING, description="Symbol or company name, e.g., 'Vingroup', 'Apple'.")}, required=['query']))
127
+ get_ts_func = glm.FunctionDeclaration(name="get_smart_time_series", description="Get price history data after knowing the official stock symbol.", 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="Convert currency after knowing the 3-letter code of source/target currency pair, e.g., 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
 
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. TAB DISPLAY LOGIC ---
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()
 
147
  return [data_min - padding, data_max + padding]
148
 
149
  def render_watchlist_tab():
150
+ st.subheader("Watchlist")
151
+ if not st.session_state.stock_watchlist: st.info("No stocks yet. Try searching for a symbol like 'Apple' or '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"Delete {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("Chart Analysis")
163
  if not st.session_state.stock_watchlist:
164
+ st.info("Please add at least one stock to the watchlist to view charts."); return
165
+ time_periods = {'Intraday': 'intraday', '1 Week': '1_week', '1 Month': '1_month', '6 Months': '6_months', '1 Year': '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("Select time period:", 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"Updating charts..."):
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:
 
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("Not enough data for the selected time period."); return
184
+ st.markdown("##### Growth Performance Comparison (%)")
185
  normalized_dfs = []
186
  for symbol, df in all_series_data.items():
187
  if not df.empty:
 
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='Time'), y=alt.Y('value:Q', scale=alt.Scale(domain=y_domain, zero=False), title='Growth (%)'), color=alt.Color('symbol:N', title='Symbol'), tooltip=[alt.Tooltip('symbol:N', title='Symbol'), alt.Tooltip('datetime:T', title='Time', format='%Y-%m-%d %H:%M'), alt.Tooltip('value:Q', title='Growth', format='.2f')]).interactive()
195
  st.altair_chart(chart, use_container_width=True)
196
  else:
197
+ st.warning("No data to draw growth chart.")
198
  st.divider()
199
+ st.markdown("##### Actual Price Charts")
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='Time'), y=alt.Y('close:Q', scale=alt.Scale(domain=y_domain, zero=False), title='Price'), tooltip=[alt.Tooltip('datetime:T', title='Time', format='%Y-%m-%d %H:%M'), alt.Tooltip('close:Q', title='Price', format=',.2f')]).interactive()
207
  st.altair_chart(price_chart, use_container_width=True)
208
 
209
  def render_currency_tab():
210
+ st.subheader("Currency Converter Tool"); state = st.session_state.currency_converter_state
211
  col1, col2 = st.columns(2)
212
+ amount = col1.number_input("Amount", value=state['amount'], min_value=0.0, format="%.2f", key="conv_amount")
213
+ from_curr = col1.selectbox("From", 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("To", options=AVAILABLE_CURRENCIES, index=AVAILABLE_CURRENCIES.index(state['to']) if state['to'] in AVAILABLE_CURRENCIES else 1, key="conv_to")
215
+ if st.button("Convert", use_container_width=True, key="conv_btn"):
216
+ with st.spinner("Converting..."): 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"**Result:** `{res['original_amount']:,.2f} {res['path_taken'][0]}` = `{res['final_amount']:,.2f} {res['path_taken'][-1]}`")
220
+ else: st.error(f"Error: {res.get('error', 'Unknown')}")
221
 
222
  # --- 6. MAIN APP LAYOUT & CONTROL FLOW ---
223
  st.title("📈 AI Financial Dashboard")
 
227
  with col2:
228
  right_column_container = st.container(height=600)
229
  with right_column_container:
230
+ tab_names = ['Stock Watchlist', 'Time Charts', 'Currency Converter']
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]
 
244
  with st.chat_message(message["role"]):
245
  st.markdown(message["parts"])
246
 
247
+ user_prompt = st.chat_input("Ask AI to control the dashboard...")
248
  if user_prompt:
249
  st.session_state.chat_history.append({"role": "user", "parts": user_prompt})
250
  st.rerun()
 
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
+ # ***** THIS IS THE CHANGED PART *****
256
  with chat_container:
257
  with st.chat_message("model"):
258
+ with st.spinner("🤖 AI executing command..."):
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:
pages/stock_report.py CHANGED
@@ -14,23 +14,23 @@ 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
 
@@ -41,14 +41,14 @@ def create_price_chart(price_data, period):
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)),
@@ -64,28 +64,28 @@ def create_price_chart(price_data, period):
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 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"
@@ -164,10 +164,10 @@ STOCK_SYMBOLS = load_stock_symbols()
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
 
@@ -192,7 +192,7 @@ with col2:
192
  if not stock_symbol:
193
  st.error("Please select a stock symbol to continue.")
194
  else:
195
- # Lưu 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()
@@ -202,25 +202,25 @@ with col2:
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 đườ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 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)
@@ -229,11 +229,11 @@ with col2:
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(
@@ -245,34 +245,34 @@ with col2:
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 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",
@@ -282,7 +282,7 @@ with col1:
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])
@@ -295,7 +295,7 @@ with col1:
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
 
@@ -314,7 +314,7 @@ with col1:
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'])
@@ -333,7 +333,7 @@ with col1:
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
@@ -349,14 +349,14 @@ with col1:
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 cổ phiếu phổ biến theo lưới
356
- # Chỉ lấy 12 đầ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]
 
14
  from modules.analysis_pipeline import run_analysis_pipeline, generate_html_report
15
  from twelvedata_api import TwelveDataAPI
16
 
17
+ # Page setup
18
  st.set_page_config(
19
  page_title="Stock Analysis Report",
20
  page_icon="📊",
21
  layout="wide"
22
  )
23
 
24
+ # Application title
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
+ # Function to create price chart
32
  def create_price_chart(price_data, period):
33
+ """Create price chart from data"""
34
  if 'values' not in price_data:
35
  return None
36
 
 
41
  df['datetime'] = pd.to_datetime(df['datetime'])
42
  df['close'] = pd.to_numeric(df['close'])
43
 
44
+ # Determine chart title based on time period
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
+ # Create chart with 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)),
 
64
 
65
  return chart
66
 
67
+ # Function to convert analysis results to PDF
68
  def convert_html_to_pdf(html_content):
69
+ """Convert HTML to PDF file"""
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
+ # Delete temporary file after use
77
  os.unlink(temp_html)
78
 
79
  return pdf_bytes
80
 
81
+ # Function to create PDF download link
82
  def get_download_link(pdf_bytes, filename):
83
+ """Create download link for PDF file"""
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
+ # List of popular stock symbols and information
89
  def load_stock_symbols():
90
  """Load stock symbols from cache or create new cache"""
91
  cache_file = "static/stock_symbols_cache.json"
 
164
  def format_stock_option(stock):
165
  return f"{stock['symbol']} - {stock['name']}"
166
 
167
+ # Create interface
168
  col1, col2 = st.columns([3, 1])
169
 
170
+ # Information input section
171
  with col2:
172
  st.subheader("Enter Information")
173
 
 
192
  if not stock_symbol:
193
  st.error("Please select a stock symbol to continue.")
194
  else:
195
+ # Save stock symbol to session state to maintain between runs
196
  st.session_state.stock_symbol = stock_symbol
197
  st.session_state.analysis_requested = True
198
  st.rerun()
 
202
  st.divider()
203
  st.subheader("PDF Report")
204
 
205
+ # Get results from session state
206
  analysis_results = st.session_state.analysis_results
207
 
208
+ # Create static directory if it doesn't exist
209
  os.makedirs("static", exist_ok=True)
210
 
211
+ # Create PDF filename and path
212
  filename = f"Report_{analysis_results['symbol']}_{datetime.now().strftime('%d%m%Y')}.pdf"
213
  pdf_path = os.path.join("static", filename)
214
 
215
+ # Display information
216
  st.markdown("Get a complete PDF report with price charts:")
217
 
218
+ # Import PDF generation function
219
  from modules.analysis_pipeline import generate_pdf_report
220
 
221
+ # Generate and download PDF button (combined)
222
  if st.button("📊 Generate & Download PDF Report", use_container_width=True, key="pdf_btn", type="primary"):
223
+ # Check if file doesn't exist or needs to be recreated
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)
 
229
  st.error("Failed to create PDF report.")
230
  st.stop()
231
 
232
+ # Read PDF file for download
233
  with open(pdf_path, "rb") as pdf_file:
234
  pdf_bytes = pdf_file.read()
235
 
236
+ # Display success message and download widget
237
  st.success("PDF report generated successfully!")
238
 
239
  st.download_button(
 
245
  key="download_pdf_btn"
246
  )
247
 
248
+ # Report display section
249
  with col1:
250
+ # Check if there's an analysis request
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
+ # Run analysis
257
  analysis_results = asyncio.run(run_analysis_pipeline(symbol))
258
 
259
+ # Save results to 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
+ # Automatically rerun to display results
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
+ # Check if analysis is complete
271
  if "analysis_complete" in st.session_state and st.session_state.analysis_complete:
272
+ # Get results from session state
273
  analysis_results = st.session_state.analysis_results
274
 
275
+ # Create tabs to display content
276
  tab1, tab2, tab3, tab4, tab5 = st.tabs([
277
  "📋 Overview",
278
  "💰 Financial Health",
 
282
  ])
283
 
284
  with tab1:
285
+ # Display basic company information
286
  overview = analysis_results.get('overview', {})
287
  if overview:
288
  col1, col2 = st.columns([1, 1])
 
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
+ # Display summary
299
  st.markdown("### Summary & Recommendation")
300
  st.markdown(analysis_results['analysis']['summary'])
301
 
 
314
  with tab5:
315
  st.markdown("### Stock Price Charts")
316
 
317
+ # Display charts from price data
318
  price_data = analysis_results.get('price_data', {})
319
  if price_data:
320
  period_tabs = st.tabs(['1 Month', '3 Months', '1 Year'])
 
333
  else:
334
  st.info("No price chart data available for this stock.")
335
  else:
336
+ # Display instructions when no analysis is present
337
  st.info("👈 Enter a stock symbol and click 'Generate Report' to begin.")
338
  st.markdown("""
339
  ### About Stock Analysis Reports
 
349
  Reports are generated based on data from multiple sources and analyzed by AI.
350
  """)
351
 
352
+ # Display popular stock symbols
353
  st.markdown("### Popular Stock Symbols")
354
 
355
+ # Display list of popular stock symbols in grid
356
+ # Only take first 12 to avoid cluttering the interface
357
  display_stocks = STOCK_SYMBOLS[:12]
358
 
359
+ # Create grid with 4 columns
360
  cols = st.columns(4)
361
  for i, stock in enumerate(display_stocks):
362
  col = cols[i % 4]