Spaces:
Sleeping
Sleeping
feat: update report format
Browse files- Home.py +32 -47
- modules/analysis_pipeline.py +69 -49
- pages/chat_app.py +54 -54
- pages/stock_report.py +40 -40
Home.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
# Home.py (
|
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 |
-
#
|
11 |
st.set_page_config(
|
12 |
page_title="AI Financial Dashboard",
|
13 |
page_icon="📊",
|
14 |
layout="wide"
|
15 |
)
|
16 |
|
17 |
-
#
|
18 |
st.title("📊 AI Financial Dashboard v2.0")
|
19 |
|
20 |
-
#
|
21 |
st.markdown("""
|
22 |
-
##
|
23 |
|
24 |
-
|
25 |
|
26 |
-
###
|
27 |
|
28 |
-
1. **💬 Chat
|
29 |
-
-
|
30 |
-
-
|
31 |
-
-
|
32 |
|
33 |
-
2. **📄
|
34 |
-
-
|
35 |
-
-
|
36 |
-
-
|
37 |
|
38 |
-
3. **📰
|
39 |
-
-
|
40 |
-
-
|
41 |
-
-
|
42 |
|
43 |
-
###
|
44 |
|
45 |
-
|
46 |
|
47 |
""")
|
48 |
|
49 |
-
#
|
50 |
-
st.sidebar.title("
|
51 |
|
52 |
-
#
|
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 |
-
#
|
62 |
for api_name, api_key in api_keys.items():
|
63 |
if api_key:
|
64 |
-
st.sidebar.success(f"✅ {api_name
|
65 |
else:
|
66 |
-
st.sidebar.error(f"❌ {api_name
|
67 |
|
68 |
-
#
|
69 |
st.sidebar.markdown("---")
|
70 |
st.sidebar.markdown("""
|
71 |
-
###
|
72 |
-
- **
|
73 |
-
- **
|
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
|
27 |
-
"""
|
28 |
-
print(f"
|
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 |
-
#
|
40 |
-
await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
|
42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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
|
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
|
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
|
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(
|
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 (
|
2 |
|
3 |
import streamlit as st
|
4 |
import pandas as pd
|
5 |
-
import altair as alt # <--
|
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.
|
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 = '
|
29 |
st.session_state.chat_session = None
|
30 |
initialize_state()
|
31 |
|
32 |
-
# --- 2.
|
33 |
-
@st.cache_data(show_spinner="
|
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.
|
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 = '
|
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"
|
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": "
|
88 |
path = find_conversion_path_bfs(start_currency, end_currency)
|
89 |
-
if not path: return {"error": f"
|
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"
|
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 = '
|
109 |
return result
|
110 |
|
111 |
-
# --- 4.
|
112 |
-
SYSTEM_INSTRUCTION = """
|
113 |
|
114 |
-
|
115 |
-
1. **
|
116 |
-
* **
|
117 |
-
* **
|
118 |
-
2. **
|
119 |
-
* **
|
120 |
-
* **
|
121 |
-
* **
|
122 |
-
3. **
|
123 |
"""
|
124 |
@st.cache_resource
|
125 |
def get_model_and_tools():
|
126 |
-
find_stock_func = glm.FunctionDeclaration(name="find_and_process_stock", description="
|
127 |
-
get_ts_func = glm.FunctionDeclaration(name="get_smart_time_series", description="
|
128 |
-
currency_func = glm.FunctionDeclaration(name="perform_currency_conversion", description="
|
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.
|
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("
|
151 |
-
if not st.session_state.stock_watchlist: st.info("
|
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"
|
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("
|
163 |
if not st.session_state.stock_watchlist:
|
164 |
-
st.info("
|
165 |
-
time_periods = {'
|
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("
|
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"
|
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("
|
184 |
-
st.markdown("#####
|
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='
|
195 |
st.altair_chart(chart, use_container_width=True)
|
196 |
else:
|
197 |
-
st.warning("
|
198 |
st.divider()
|
199 |
-
st.markdown("#####
|
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='
|
207 |
st.altair_chart(price_chart, use_container_width=True)
|
208 |
|
209 |
def render_currency_tab():
|
210 |
-
st.subheader("
|
211 |
col1, col2 = st.columns(2)
|
212 |
-
amount = col1.number_input("
|
213 |
-
from_curr = col1.selectbox("
|
214 |
-
to_curr = col2.selectbox("
|
215 |
-
if st.button("
|
216 |
-
with st.spinner("
|
217 |
if state['result']:
|
218 |
res = state['result']
|
219 |
-
if res.get('status') == 'Success': st.success(f"**
|
220 |
-
else: st.error(f"
|
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 = ['
|
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("
|
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 |
-
# *****
|
256 |
with chat_container:
|
257 |
with st.chat_message("model"):
|
258 |
-
with st.spinner("🤖 AI
|
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 |
-
#
|
18 |
st.set_page_config(
|
19 |
page_title="Stock Analysis Report",
|
20 |
page_icon="📊",
|
21 |
layout="wide"
|
22 |
)
|
23 |
|
24 |
-
#
|
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 |
-
#
|
32 |
def create_price_chart(price_data, period):
|
33 |
-
"""
|
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 |
-
#
|
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 |
-
#
|
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 |
-
#
|
68 |
def convert_html_to_pdf(html_content):
|
69 |
-
"""
|
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 |
-
#
|
77 |
os.unlink(temp_html)
|
78 |
|
79 |
return pdf_bytes
|
80 |
|
81 |
-
#
|
82 |
def get_download_link(pdf_bytes, filename):
|
83 |
-
"""
|
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 |
-
#
|
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 |
-
#
|
168 |
col1, col2 = st.columns([3, 1])
|
169 |
|
170 |
-
#
|
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 |
-
#
|
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 |
-
#
|
206 |
analysis_results = st.session_state.analysis_results
|
207 |
|
208 |
-
#
|
209 |
os.makedirs("static", exist_ok=True)
|
210 |
|
211 |
-
#
|
212 |
filename = f"Report_{analysis_results['symbol']}_{datetime.now().strftime('%d%m%Y')}.pdf"
|
213 |
pdf_path = os.path.join("static", filename)
|
214 |
|
215 |
-
#
|
216 |
st.markdown("Get a complete PDF report with price charts:")
|
217 |
|
218 |
-
# Import
|
219 |
from modules.analysis_pipeline import generate_pdf_report
|
220 |
|
221 |
-
#
|
222 |
if st.button("📊 Generate & Download PDF Report", use_container_width=True, key="pdf_btn", type="primary"):
|
223 |
-
#
|
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 |
-
#
|
233 |
with open(pdf_path, "rb") as pdf_file:
|
234 |
pdf_bytes = pdf_file.read()
|
235 |
|
236 |
-
#
|
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 |
-
#
|
249 |
with col1:
|
250 |
-
#
|
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 |
-
#
|
257 |
analysis_results = asyncio.run(run_analysis_pipeline(symbol))
|
258 |
|
259 |
-
#
|
260 |
st.session_state.analysis_results = analysis_results
|
261 |
st.session_state.analysis_complete = True
|
262 |
st.session_state.analysis_requested = False
|
263 |
|
264 |
-
#
|
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 |
-
#
|
271 |
if "analysis_complete" in st.session_state and st.session_state.analysis_complete:
|
272 |
-
#
|
273 |
analysis_results = st.session_state.analysis_results
|
274 |
|
275 |
-
#
|
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 |
-
#
|
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 |
-
#
|
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 |
-
#
|
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 |
-
#
|
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 |
-
#
|
353 |
st.markdown("### Popular Stock Symbols")
|
354 |
|
355 |
-
#
|
356 |
-
#
|
357 |
display_stocks = STOCK_SYMBOLS[:12]
|
358 |
|
359 |
-
#
|
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]
|