Spaces:
Sleeping
Sleeping
feat: change UI appearance
Browse files- .gitignore +1 -2
- .streamlit/config.toml +26 -0
- .streamlit/custom.py +130 -0
- Home.py +48 -27
- app.py +24 -0
- pages/chat_app.py +63 -47
- pages/stock_report.py +334 -320
.gitignore
CHANGED
@@ -1,4 +1,3 @@
|
|
1 |
.env
|
2 |
PLAN.md
|
3 |
-
__pycache__/
|
4 |
-
.streamlit/
|
|
|
1 |
.env
|
2 |
PLAN.md
|
3 |
+
__pycache__/
|
|
.streamlit/config.toml
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[theme]
|
2 |
+
primaryColor = "#3498db"
|
3 |
+
backgroundColor = "#f9f9f9"
|
4 |
+
secondaryBackgroundColor = "#ffffff"
|
5 |
+
textColor = "#333333"
|
6 |
+
font = "sans serif"
|
7 |
+
|
8 |
+
[server]
|
9 |
+
enableCORS = false
|
10 |
+
enableXsrfProtection = true
|
11 |
+
runOnSave = true
|
12 |
+
maxUploadSize = 200
|
13 |
+
headless = true
|
14 |
+
|
15 |
+
[browser]
|
16 |
+
gatherUsageStats = false
|
17 |
+
serverAddress = "0.0.0.0"
|
18 |
+
serverPort = 8501
|
19 |
+
|
20 |
+
[runner]
|
21 |
+
fastReruns = true
|
22 |
+
|
23 |
+
[client]
|
24 |
+
showErrorDetails = false
|
25 |
+
caching = true
|
26 |
+
displayEnabled = true
|
.streamlit/custom.py
ADDED
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
|
3 |
+
def apply_custom_css():
|
4 |
+
"""Apply custom CSS to fix UI issues on Huggingface Spaces"""
|
5 |
+
st.markdown("""
|
6 |
+
<style>
|
7 |
+
/* Fix for stacking UI issues */
|
8 |
+
.element-container {
|
9 |
+
overflow-anchor: none !important;
|
10 |
+
}
|
11 |
+
|
12 |
+
/* Fix for main content area */
|
13 |
+
.stApp {
|
14 |
+
min-height: 100vh;
|
15 |
+
overflow-y: auto;
|
16 |
+
}
|
17 |
+
|
18 |
+
/* Improve sidebar styling */
|
19 |
+
.css-1d391kg {
|
20 |
+
padding-top: 2rem;
|
21 |
+
}
|
22 |
+
|
23 |
+
/* Fix for scrolling issues */
|
24 |
+
.main .block-container {
|
25 |
+
max-width: 100%;
|
26 |
+
padding: 1rem;
|
27 |
+
}
|
28 |
+
|
29 |
+
/* Hide empty elements */
|
30 |
+
.element-container:empty {
|
31 |
+
display: none;
|
32 |
+
}
|
33 |
+
|
34 |
+
/* Custom tabs styling */
|
35 |
+
.stTabs [data-baseweb="tab-list"] {
|
36 |
+
gap: 2px;
|
37 |
+
}
|
38 |
+
|
39 |
+
.stTabs [data-baseweb="tab"] {
|
40 |
+
height: 50px;
|
41 |
+
white-space: pre-wrap;
|
42 |
+
background-color: #F0F2F6;
|
43 |
+
border-radius: 4px 4px 0 0;
|
44 |
+
gap: 1px;
|
45 |
+
padding-top: 10px;
|
46 |
+
padding-bottom: 10px;
|
47 |
+
}
|
48 |
+
|
49 |
+
.stTabs [aria-selected="true"] {
|
50 |
+
background-color: #FFFFFF !important;
|
51 |
+
border-bottom: 2px solid #4CAF50;
|
52 |
+
}
|
53 |
+
|
54 |
+
/* Chat container styling */
|
55 |
+
[data-testid="stChatMessageContent"] {
|
56 |
+
background-color: #F0F2F6;
|
57 |
+
border-radius: 10px;
|
58 |
+
padding: 8px 12px;
|
59 |
+
}
|
60 |
+
|
61 |
+
[data-testid="stChatMessage"] [data-testid="stChatMessageContent"] code {
|
62 |
+
background-color: rgba(0,0,0,0.1);
|
63 |
+
}
|
64 |
+
|
65 |
+
/* Hide duplicate header after page switches */
|
66 |
+
.duplicated-header {
|
67 |
+
display: none;
|
68 |
+
}
|
69 |
+
|
70 |
+
/* Fix for any layout issues specific to Huggingface */
|
71 |
+
@media screen and (max-width: 1200px) {
|
72 |
+
.row-widget.stButton {
|
73 |
+
width: 100%;
|
74 |
+
}
|
75 |
+
}
|
76 |
+
</style>
|
77 |
+
""", unsafe_allow_html=True)
|
78 |
+
|
79 |
+
def apply_custom_js():
|
80 |
+
"""Apply custom JavaScript to fix UI issues on Huggingface Spaces"""
|
81 |
+
st.markdown("""
|
82 |
+
<script>
|
83 |
+
// Function to clean up UI by removing duplicated elements
|
84 |
+
function cleanupUI() {
|
85 |
+
// Wait for the page to fully load
|
86 |
+
setTimeout(function() {
|
87 |
+
// Find headers that might be duplicated
|
88 |
+
const headers = document.querySelectorAll('h1, h2');
|
89 |
+
const seen = new Set();
|
90 |
+
|
91 |
+
headers.forEach((header, index) => {
|
92 |
+
const text = header.innerText;
|
93 |
+
if (seen.has(text) && index > 0) {
|
94 |
+
// Mark duplicates
|
95 |
+
header.classList.add('duplicated-header');
|
96 |
+
header.style.display = 'none';
|
97 |
+
}
|
98 |
+
seen.add(text);
|
99 |
+
});
|
100 |
+
|
101 |
+
// Fix any scrolling issues
|
102 |
+
document.querySelector('.stApp').scrollTop = 0;
|
103 |
+
}, 1000);
|
104 |
+
}
|
105 |
+
|
106 |
+
// Run cleanup when DOM is ready
|
107 |
+
document.addEventListener('DOMContentLoaded', cleanupUI);
|
108 |
+
|
109 |
+
// Also run cleanup when Streamlit's internal events happen
|
110 |
+
const observer = new MutationObserver((mutations) => {
|
111 |
+
cleanupUI();
|
112 |
+
});
|
113 |
+
|
114 |
+
// Start observing changes in the main container
|
115 |
+
window.addEventListener('load', () => {
|
116 |
+
const mainContainer = document.querySelector('.main');
|
117 |
+
if (mainContainer) {
|
118 |
+
observer.observe(mainContainer, {
|
119 |
+
childList: true,
|
120 |
+
subtree: true
|
121 |
+
});
|
122 |
+
}
|
123 |
+
});
|
124 |
+
</script>
|
125 |
+
""", unsafe_allow_html=True)
|
126 |
+
|
127 |
+
def init():
|
128 |
+
"""Initialize custom CSS and JS"""
|
129 |
+
apply_custom_css()
|
130 |
+
apply_custom_js()
|
Home.py
CHANGED
@@ -3,48 +3,69 @@ import streamlit as st
|
|
3 |
import os
|
4 |
from dotenv import load_dotenv
|
5 |
import google.generativeai as genai
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
# Load environment variables
|
8 |
load_dotenv()
|
9 |
|
10 |
-
#
|
|
|
|
|
|
|
|
|
11 |
st.set_page_config(
|
12 |
page_title="AI Financial Dashboard",
|
13 |
page_icon="📊",
|
14 |
-
layout="wide"
|
|
|
15 |
)
|
16 |
|
17 |
-
#
|
18 |
-
st
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
|
|
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 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
2. **📄 In-depth Stock Analysis Report**:
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
3. **📰 Daily Market Summary Newsletter** (Coming soon):
|
39 |
-
|
40 |
-
|
41 |
-
|
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")
|
|
|
3 |
import os
|
4 |
from dotenv import load_dotenv
|
5 |
import google.generativeai as genai
|
6 |
+
import sys
|
7 |
+
|
8 |
+
# Add project root to path for imports
|
9 |
+
sys.path.append(os.path.dirname(__file__))
|
10 |
+
|
11 |
+
# Apply custom styling for Huggingface Space compatibility
|
12 |
+
try:
|
13 |
+
from .streamlit.custom import init as apply_custom_styling
|
14 |
+
apply_custom_styling()
|
15 |
+
except (ImportError, ModuleNotFoundError):
|
16 |
+
# Fallback for when running locally or if custom styling fails
|
17 |
+
pass
|
18 |
|
19 |
# Load environment variables
|
20 |
load_dotenv()
|
21 |
|
22 |
+
# Initialize session state if needed
|
23 |
+
if "page_viewed" not in st.session_state:
|
24 |
+
st.session_state.page_viewed = True
|
25 |
+
|
26 |
+
# Page setup - set this consistently across all pages
|
27 |
st.set_page_config(
|
28 |
page_title="AI Financial Dashboard",
|
29 |
page_icon="📊",
|
30 |
+
layout="wide",
|
31 |
+
initial_sidebar_state="expanded"
|
32 |
)
|
33 |
|
34 |
+
# Clear any previous runs to avoid stacking UI elements
|
35 |
+
if hasattr(st, 'empty'):
|
36 |
+
placeholder = st.empty()
|
37 |
+
with placeholder.container():
|
38 |
+
# Application title
|
39 |
+
st.title("📊 AI Financial Dashboard v2.0")
|
40 |
+
|
41 |
+
# Display application information
|
42 |
+
st.markdown("""
|
43 |
+
## Welcome to AI Financial Dashboard
|
44 |
|
45 |
+
This is an intelligent financial analysis application using AI to help you make smarter investment decisions.
|
46 |
|
47 |
+
### Main Features:
|
48 |
|
49 |
+
1. **💬 Chat with AI Financial Analyst**:
|
50 |
+
- Search for stock information
|
51 |
+
- View price charts
|
52 |
+
- Convert currencies
|
53 |
+
|
54 |
+
2. **📄 In-depth Stock Analysis Report**:
|
55 |
+
- Comprehensive analysis of a specific stock
|
56 |
+
- Data collection from multiple sources
|
57 |
+
- Generate in-depth reports with AI evaluation
|
58 |
+
|
59 |
+
3. **📰 Daily Market Summary Newsletter** (Coming soon):
|
60 |
+
- Compilation of latest financial news
|
61 |
+
- Categorized by topic
|
62 |
+
- Daily market updates
|
63 |
|
64 |
+
### How to Use:
|
65 |
|
66 |
+
Use the navigation bar on the left to switch between different features of the application.
|
67 |
|
68 |
+
""")
|
69 |
|
70 |
# Display API connection status
|
71 |
st.sidebar.title("Connection Status")
|
app.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app.py - Entry point for Huggingface Spaces
|
2 |
+
# This file forwards to the main Home.py file
|
3 |
+
|
4 |
+
import streamlit as st
|
5 |
+
import sys
|
6 |
+
import os
|
7 |
+
|
8 |
+
# Add the current directory to the path so that imports work correctly
|
9 |
+
sys.path.append(os.path.dirname(__file__))
|
10 |
+
|
11 |
+
# Import the Home module and run it
|
12 |
+
try:
|
13 |
+
# Apply custom styling
|
14 |
+
try:
|
15 |
+
from .streamlit.custom import init as apply_custom_styling
|
16 |
+
apply_custom_styling()
|
17 |
+
except (ImportError, ModuleNotFoundError):
|
18 |
+
pass
|
19 |
+
|
20 |
+
# Import and run the Home.py module
|
21 |
+
import Home
|
22 |
+
except Exception as e:
|
23 |
+
st.error(f"Failed to load application: {str(e)}")
|
24 |
+
st.error("Please check that the application structure is correct and Home.py exists.")
|
pages/chat_app.py
CHANGED
@@ -13,7 +13,20 @@ from datetime import datetime
|
|
13 |
|
14 |
# --- 1. INITIAL CONFIGURATION & STATE INITIALIZATION ---
|
15 |
load_dotenv()
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
|
18 |
|
19 |
def initialize_state():
|
@@ -220,55 +233,58 @@ def render_currency_tab():
|
|
220 |
else: st.error(f"Error: {res.get('error', 'Unknown')}")
|
221 |
|
222 |
# --- 6. MAIN APP LAYOUT & CONTROL FLOW ---
|
223 |
-
|
|
|
224 |
|
225 |
-
|
|
|
226 |
|
227 |
-
|
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]
|
234 |
-
|
235 |
-
tab1, tab2, tab3 = st.tabs(tab_names)
|
236 |
-
with tab1: render_watchlist_tab()
|
237 |
-
with tab2: render_timeseries_tab()
|
238 |
-
with tab3: render_currency_tab()
|
239 |
|
240 |
-
with
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
246 |
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
|
|
|
|
251 |
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
func_name = call.name; func_args = {k: v for k, v in call.args.items()}
|
265 |
-
if func_name in AVAILABLE_FUNCTIONS:
|
266 |
-
tool_result = AVAILABLE_FUNCTIONS[func_name](**func_args)
|
267 |
-
tool_responses.append(glm.Part(function_response=glm.FunctionResponse(name=func_name, response={'result': tool_result})))
|
268 |
-
else:
|
269 |
-
tool_responses.append(glm.Part(function_response=glm.FunctionResponse(name=func_name, response={'error': f"Function '{func_name}' not found."})))
|
270 |
-
response = st.session_state.chat_session.send_message(glm.Content(parts=tool_responses))
|
271 |
tool_calls = [part.function_call for part in response.candidates[0].content.parts if part.function_call]
|
272 |
-
|
273 |
-
|
274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
|
14 |
# --- 1. INITIAL CONFIGURATION & STATE INITIALIZATION ---
|
15 |
load_dotenv()
|
16 |
+
|
17 |
+
# Initialize session state for this page
|
18 |
+
if "chat_app_initialized" not in st.session_state:
|
19 |
+
st.session_state.chat_app_initialized = True
|
20 |
+
|
21 |
+
# Set page config consistent with other pages
|
22 |
+
st.set_page_config(
|
23 |
+
page_title="AI Financial Dashboard",
|
24 |
+
page_icon="📊",
|
25 |
+
layout="wide",
|
26 |
+
initial_sidebar_state="expanded"
|
27 |
+
)
|
28 |
+
|
29 |
+
# Configure Gemini API
|
30 |
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
|
31 |
|
32 |
def initialize_state():
|
|
|
233 |
else: st.error(f"Error: {res.get('error', 'Unknown')}")
|
234 |
|
235 |
# --- 6. MAIN APP LAYOUT & CONTROL FLOW ---
|
236 |
+
# Use container to avoid UI stacking issues
|
237 |
+
main_container = st.container()
|
238 |
|
239 |
+
with main_container:
|
240 |
+
st.title("📈 AI Financial Dashboard")
|
241 |
|
242 |
+
col1, col2 = st.columns([1, 1])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
243 |
|
244 |
+
with col2:
|
245 |
+
right_column_container = st.container(height=600)
|
246 |
+
with right_column_container:
|
247 |
+
tab_names = ['Stock Watchlist', 'Time Charts', 'Currency Converter']
|
248 |
+
try: default_index = tab_names.index(st.session_state.active_tab)
|
249 |
+
except ValueError: default_index = 0
|
250 |
+
st.session_state.active_tab = tab_names[default_index]
|
251 |
+
|
252 |
+
tab1, tab2, tab3 = st.tabs(tab_names)
|
253 |
+
with tab1: render_watchlist_tab()
|
254 |
+
with tab2: render_timeseries_tab()
|
255 |
+
with tab3: render_currency_tab()
|
256 |
|
257 |
+
with col1:
|
258 |
+
chat_container = st.container(height=600)
|
259 |
+
with chat_container:
|
260 |
+
for message in st.session_state.chat_history:
|
261 |
+
with st.chat_message(message["role"]):
|
262 |
+
st.markdown(message["parts"])
|
263 |
|
264 |
+
user_prompt = st.chat_input("Ask AI to control the dashboard...")
|
265 |
+
if user_prompt:
|
266 |
+
st.session_state.chat_history.append({"role": "user", "parts": user_prompt})
|
267 |
+
st.rerun()
|
268 |
+
|
269 |
+
if st.session_state.chat_history and st.session_state.chat_history[-1]["role"] == "user":
|
270 |
+
last_user_prompt = st.session_state.chat_history[-1]["parts"]
|
271 |
+
|
272 |
+
with chat_container:
|
273 |
+
with st.chat_message("model"):
|
274 |
+
with st.spinner("🤖 AI executing command..."):
|
275 |
+
response = st.session_state.chat_session.send_message(last_user_prompt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
276 |
tool_calls = [part.function_call for part in response.candidates[0].content.parts if part.function_call]
|
277 |
+
while tool_calls:
|
278 |
+
tool_responses = []
|
279 |
+
for call in tool_calls:
|
280 |
+
func_name = call.name; func_args = {k: v for k, v in call.args.items()}
|
281 |
+
if func_name in AVAILABLE_FUNCTIONS:
|
282 |
+
tool_result = AVAILABLE_FUNCTIONS[func_name](**func_args)
|
283 |
+
tool_responses.append(glm.Part(function_response=glm.FunctionResponse(name=func_name, response={'result': tool_result})))
|
284 |
+
else:
|
285 |
+
tool_responses.append(glm.Part(function_response=glm.FunctionResponse(name=func_name, response={'error': f"Function '{func_name}' not found."})))
|
286 |
+
response = st.session_state.chat_session.send_message(glm.Content(parts=tool_responses))
|
287 |
+
tool_calls = [part.function_call for part in response.candidates[0].content.parts if part.function_call]
|
288 |
+
|
289 |
+
st.session_state.chat_history.append({"role": "model", "parts": response.text})
|
290 |
+
st.rerun()
|
pages/stock_report.py
CHANGED
@@ -14,353 +14,367 @@ 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.
|
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 |
-
|
33 |
-
"
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
if df.empty:
|
39 |
-
return None
|
40 |
-
|
41 |
-
df['datetime'] = pd.to_datetime(df['datetime'])
|
42 |
-
df['close'] = pd.to_numeric(df['close'])
|
43 |
-
|
44 |
-
# 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)),
|
55 |
-
tooltip=[
|
56 |
-
alt.Tooltip('datetime:T', title='Date', format='%d/%m/%Y'),
|
57 |
-
alt.Tooltip('close:Q', title='Closing Price', format=',.2f'),
|
58 |
-
alt.Tooltip('volume:Q', title='Volume', format=',.0f')
|
59 |
-
]
|
60 |
-
).properties(
|
61 |
-
title=title_map.get(period, f'Stock price ({period})'),
|
62 |
-
height=350
|
63 |
-
).interactive()
|
64 |
-
|
65 |
-
return chart
|
66 |
|
67 |
-
# Function to
|
68 |
-
def
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
|
81 |
-
# Function to
|
82 |
-
def
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
|
88 |
-
# List of popular stock symbols and information
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
try:
|
96 |
-
|
97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
except Exception as e:
|
99 |
-
print(f"Error
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
{"symbol": "AAPL", "name": "Apple Inc."},
|
104 |
-
{"symbol": "MSFT", "name": "Microsoft Corporation"},
|
105 |
-
{"symbol": "GOOGL", "name": "Alphabet Inc."},
|
106 |
-
{"symbol": "AMZN", "name": "Amazon.com Inc."},
|
107 |
-
{"symbol": "TSLA", "name": "Tesla, Inc."},
|
108 |
-
{"symbol": "META", "name": "Meta Platforms, Inc."},
|
109 |
-
{"symbol": "NVDA", "name": "NVIDIA Corporation"},
|
110 |
-
{"symbol": "JPM", "name": "JPMorgan Chase & Co."},
|
111 |
-
{"symbol": "V", "name": "Visa Inc."},
|
112 |
-
{"symbol": "JNJ", "name": "Johnson & Johnson"},
|
113 |
-
{"symbol": "WMT", "name": "Walmart Inc."},
|
114 |
-
{"symbol": "MA", "name": "Mastercard Incorporated"},
|
115 |
-
{"symbol": "PG", "name": "Procter & Gamble Co."},
|
116 |
-
{"symbol": "UNH", "name": "UnitedHealth Group Inc."},
|
117 |
-
{"symbol": "HD", "name": "Home Depot Inc."},
|
118 |
-
{"symbol": "BAC", "name": "Bank of America Corp."},
|
119 |
-
{"symbol": "XOM", "name": "Exxon Mobil Corporation"},
|
120 |
-
{"symbol": "DIS", "name": "Walt Disney Co."},
|
121 |
-
{"symbol": "CSCO", "name": "Cisco Systems, Inc."},
|
122 |
-
{"symbol": "VZ", "name": "Verizon Communications Inc."},
|
123 |
-
{"symbol": "ADBE", "name": "Adobe Inc."},
|
124 |
-
{"symbol": "NFLX", "name": "Netflix, Inc."},
|
125 |
-
{"symbol": "CMCSA", "name": "Comcast Corporation"},
|
126 |
-
{"symbol": "PFE", "name": "Pfizer Inc."},
|
127 |
-
{"symbol": "KO", "name": "Coca-Cola Company"},
|
128 |
-
{"symbol": "INTC", "name": "Intel Corporation"},
|
129 |
-
{"symbol": "PYPL", "name": "PayPal Holdings, Inc."},
|
130 |
-
{"symbol": "T", "name": "AT&T Inc."},
|
131 |
-
{"symbol": "PEP", "name": "PepsiCo, Inc."},
|
132 |
-
{"symbol": "MRK", "name": "Merck & Co., Inc."}
|
133 |
-
]
|
134 |
-
|
135 |
-
# Try to fetch more comprehensive list if API key is available
|
136 |
-
try:
|
137 |
-
from dotenv import load_dotenv
|
138 |
-
load_dotenv()
|
139 |
-
api_key = os.getenv("TWELVEDATA_API_KEY")
|
140 |
-
if api_key:
|
141 |
-
td_api = TwelveDataAPI(api_key)
|
142 |
-
stocks_data = td_api.get_all_stocks(exchange="NASDAQ")
|
143 |
-
if stocks_data and 'data' in stocks_data:
|
144 |
-
# Convert to format we need and take first 1000 stocks
|
145 |
-
symbols = [{"symbol": stock["symbol"], "name": stock.get("name", "Unknown")}
|
146 |
-
for stock in stocks_data['data']]
|
147 |
-
|
148 |
-
# Save to cache
|
149 |
-
os.makedirs(os.path.dirname(cache_file), exist_ok=True)
|
150 |
-
with open(cache_file, 'w') as f:
|
151 |
-
json.dump(symbols, f)
|
152 |
-
|
153 |
-
return symbols
|
154 |
-
except Exception as e:
|
155 |
-
print(f"Error fetching stock symbols from API: {e}")
|
156 |
-
|
157 |
-
# If everything fails, return default list
|
158 |
-
return default_symbols
|
159 |
|
160 |
-
# Load stock symbols
|
161 |
-
STOCK_SYMBOLS = load_stock_symbols()
|
162 |
|
163 |
-
# Function to format stock options for display
|
164 |
-
def format_stock_option(stock):
|
165 |
-
|
166 |
|
167 |
-
# Create interface
|
168 |
-
col1, col2 = st.columns([3, 1])
|
169 |
|
170 |
-
# Information input section
|
171 |
-
with col2:
|
172 |
-
|
173 |
-
|
174 |
-
# Create a list of formatted options and a mapping back to symbols
|
175 |
-
stock_options = [format_stock_option(stock) for stock in STOCK_SYMBOLS]
|
176 |
-
|
177 |
-
# Use selectbox with search functionality
|
178 |
-
selected_stock = st.selectbox(
|
179 |
-
"Select a stock symbol",
|
180 |
-
options=stock_options,
|
181 |
-
index=0 if stock_options else None,
|
182 |
-
placeholder="Search for a stock symbol...",
|
183 |
-
)
|
184 |
-
|
185 |
-
# Extract symbol from selection
|
186 |
-
if selected_stock:
|
187 |
-
stock_symbol = selected_stock.split(" - ")[0]
|
188 |
-
else:
|
189 |
-
stock_symbol = ""
|
190 |
-
|
191 |
-
if st.button("Generate Report", use_container_width=True, type="primary"):
|
192 |
-
if not stock_symbol:
|
193 |
-
st.error("Please select a stock symbol to continue.")
|
194 |
-
else:
|
195 |
-
# 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()
|
199 |
-
|
200 |
-
# PDF report generation section - moved from tab1
|
201 |
-
if "analysis_complete" in st.session_state and st.session_state.analysis_complete:
|
202 |
-
st.divider()
|
203 |
-
st.subheader("PDF Report")
|
204 |
|
205 |
-
#
|
206 |
-
|
207 |
|
208 |
-
#
|
209 |
-
|
|
|
|
|
|
|
|
|
|
|
210 |
|
211 |
-
#
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
|
216 |
-
st.markdown("Get a complete PDF report with price charts:")
|
217 |
|
218 |
-
|
219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
220 |
|
221 |
-
#
|
222 |
-
if
|
223 |
-
|
224 |
-
|
225 |
-
with st.spinner("Creating PDF report with charts..."):
|
226 |
-
generate_pdf_report(analysis_results, pdf_path)
|
227 |
-
|
228 |
-
if not os.path.exists(pdf_path):
|
229 |
-
st.error("Failed to create PDF report.")
|
230 |
-
st.stop()
|
231 |
|
232 |
-
#
|
233 |
-
|
234 |
-
pdf_bytes = pdf_file.read()
|
235 |
|
236 |
-
#
|
237 |
-
|
238 |
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
-
|
257 |
-
|
|
|
258 |
|
259 |
-
#
|
260 |
-
|
261 |
-
|
262 |
-
st.session_state.analysis_requested = False
|
263 |
|
264 |
-
#
|
265 |
-
st.
|
266 |
-
|
267 |
-
st.
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
"📊 Price Charts"
|
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])
|
289 |
-
with col1:
|
290 |
-
st.subheader(f"{analysis_results['symbol']} - {overview.get('Name', 'N/A')}")
|
291 |
-
st.write(f"**Industry:** {overview.get('Industry', 'N/A')}")
|
292 |
-
st.write(f"**Sector:** {overview.get('Sector', 'N/A')}")
|
293 |
-
with col2:
|
294 |
-
st.write(f"**Market Cap:** {overview.get('MarketCapitalization', 'N/A')}")
|
295 |
-
st.write(f"**P/E Ratio:** {overview.get('PERatio', 'N/A')}")
|
296 |
-
st.write(f"**Dividend Yield:** {overview.get('DividendYield', 'N/A')}%")
|
297 |
|
298 |
-
|
299 |
-
|
300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
301 |
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
st.
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
316 |
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
|
|
|
|
321 |
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
329 |
else:
|
330 |
-
st.info(f"
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
st.session_state.analysis_requested = True
|
366 |
-
st.rerun()
|
|
|
14 |
from modules.analysis_pipeline import run_analysis_pipeline, generate_html_report
|
15 |
from twelvedata_api import TwelveDataAPI
|
16 |
|
17 |
+
# Initialize session state for this page
|
18 |
+
if "stock_report_initialized" not in st.session_state:
|
19 |
+
st.session_state.stock_report_initialized = True
|
20 |
+
if "analysis_requested" not in st.session_state:
|
21 |
+
st.session_state.analysis_requested = False
|
22 |
+
if "analysis_complete" not in st.session_state:
|
23 |
+
st.session_state.analysis_complete = False
|
24 |
+
|
25 |
+
# Page setup - make sure this is consistent with Home.py
|
26 |
st.set_page_config(
|
27 |
page_title="Stock Analysis Report",
|
28 |
page_icon="📊",
|
29 |
+
layout="wide",
|
30 |
+
initial_sidebar_state="expanded"
|
31 |
)
|
32 |
|
33 |
+
# Clear the page for fresh rendering
|
34 |
+
main_container = st.container()
|
|
|
|
|
|
|
|
|
35 |
|
36 |
+
with main_container:
|
37 |
+
# Application title
|
38 |
+
st.title("📄 In-depth Stock Analysis Report")
|
39 |
+
st.markdown("""
|
40 |
+
This application generates a comprehensive analysis report for a stock symbol, combining data from multiple sources
|
41 |
+
and using AI to synthesize information, helping you make better investment decisions.
|
42 |
+
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
|
44 |
+
# Function to create price chart
|
45 |
+
def create_price_chart(price_data, period):
|
46 |
+
"""Create price chart from data"""
|
47 |
+
if 'values' not in price_data:
|
48 |
+
return None
|
49 |
+
|
50 |
+
df = pd.DataFrame(price_data['values'])
|
51 |
+
if df.empty:
|
52 |
+
return None
|
53 |
+
|
54 |
+
df['datetime'] = pd.to_datetime(df['datetime'])
|
55 |
+
df['close'] = pd.to_numeric(df['close'])
|
56 |
+
|
57 |
+
# Determine chart title based on time period
|
58 |
+
title_map = {
|
59 |
+
'1_month': 'Stock price over the last month',
|
60 |
+
'3_months': 'Stock price over the last 3 months',
|
61 |
+
'1_year': 'Stock price over the last year'
|
62 |
+
}
|
63 |
+
|
64 |
+
# Create chart with Altair
|
65 |
+
chart = alt.Chart(df).mark_line().encode(
|
66 |
+
x=alt.X('datetime:T', title='Time'),
|
67 |
+
y=alt.Y('close:Q', title='Closing Price', scale=alt.Scale(zero=False)),
|
68 |
+
tooltip=[
|
69 |
+
alt.Tooltip('datetime:T', title='Date', format='%d/%m/%Y'),
|
70 |
+
alt.Tooltip('close:Q', title='Closing Price', format=',.2f'),
|
71 |
+
alt.Tooltip('volume:Q', title='Volume', format=',.0f')
|
72 |
+
]
|
73 |
+
).properties(
|
74 |
+
title=title_map.get(period, f'Stock price ({period})'),
|
75 |
+
height=350
|
76 |
+
).interactive()
|
77 |
+
|
78 |
+
return chart
|
79 |
|
80 |
+
# Function to convert analysis results to PDF
|
81 |
+
def convert_html_to_pdf(html_content):
|
82 |
+
"""Convert HTML to PDF file"""
|
83 |
+
with tempfile.NamedTemporaryFile(suffix='.html', delete=False) as f:
|
84 |
+
f.write(html_content.encode())
|
85 |
+
temp_html = f.name
|
86 |
+
|
87 |
+
pdf_bytes = weasyprint.HTML(filename=temp_html).write_pdf()
|
88 |
+
|
89 |
+
# Delete temporary file after use
|
90 |
+
os.unlink(temp_html)
|
91 |
+
|
92 |
+
return pdf_bytes
|
93 |
+
|
94 |
+
# Function to create PDF download link
|
95 |
+
def get_download_link(pdf_bytes, filename):
|
96 |
+
"""Create download link for PDF file"""
|
97 |
+
b64 = base64.b64encode(pdf_bytes).decode()
|
98 |
+
href = f'<a href="data:application/pdf;base64,{b64}" download="{filename}">Download Report (PDF)</a>'
|
99 |
+
return href
|
100 |
|
101 |
+
# List of popular stock symbols and information
|
102 |
+
@st.cache_data(ttl=3600)
|
103 |
+
def load_stock_symbols():
|
104 |
+
"""Load stock symbols from cache or create new cache"""
|
105 |
+
cache_file = "static/stock_symbols_cache.json"
|
106 |
+
|
107 |
+
# Check if cache exists
|
108 |
+
if os.path.exists(cache_file):
|
109 |
+
try:
|
110 |
+
with open(cache_file, 'r') as f:
|
111 |
+
return json.load(f)
|
112 |
+
except Exception as e:
|
113 |
+
print(f"Error loading cache: {e}")
|
114 |
+
|
115 |
+
# Default list if cache doesn't exist or fails to load
|
116 |
+
default_symbols = [
|
117 |
+
{"symbol": "AAPL", "name": "Apple Inc."},
|
118 |
+
{"symbol": "MSFT", "name": "Microsoft Corporation"},
|
119 |
+
{"symbol": "GOOGL", "name": "Alphabet Inc."},
|
120 |
+
{"symbol": "AMZN", "name": "Amazon.com Inc."},
|
121 |
+
{"symbol": "TSLA", "name": "Tesla, Inc."},
|
122 |
+
{"symbol": "META", "name": "Meta Platforms, Inc."},
|
123 |
+
{"symbol": "NVDA", "name": "NVIDIA Corporation"},
|
124 |
+
{"symbol": "JPM", "name": "JPMorgan Chase & Co."},
|
125 |
+
{"symbol": "V", "name": "Visa Inc."},
|
126 |
+
{"symbol": "JNJ", "name": "Johnson & Johnson"},
|
127 |
+
{"symbol": "WMT", "name": "Walmart Inc."},
|
128 |
+
{"symbol": "MA", "name": "Mastercard Incorporated"},
|
129 |
+
{"symbol": "PG", "name": "Procter & Gamble Co."},
|
130 |
+
{"symbol": "UNH", "name": "UnitedHealth Group Inc."},
|
131 |
+
{"symbol": "HD", "name": "Home Depot Inc."},
|
132 |
+
{"symbol": "BAC", "name": "Bank of America Corp."},
|
133 |
+
{"symbol": "XOM", "name": "Exxon Mobil Corporation"},
|
134 |
+
{"symbol": "DIS", "name": "Walt Disney Co."},
|
135 |
+
{"symbol": "CSCO", "name": "Cisco Systems, Inc."},
|
136 |
+
{"symbol": "VZ", "name": "Verizon Communications Inc."},
|
137 |
+
{"symbol": "ADBE", "name": "Adobe Inc."},
|
138 |
+
{"symbol": "NFLX", "name": "Netflix, Inc."},
|
139 |
+
{"symbol": "CMCSA", "name": "Comcast Corporation"},
|
140 |
+
{"symbol": "PFE", "name": "Pfizer Inc."},
|
141 |
+
{"symbol": "KO", "name": "Coca-Cola Company"},
|
142 |
+
{"symbol": "INTC", "name": "Intel Corporation"},
|
143 |
+
{"symbol": "PYPL", "name": "PayPal Holdings, Inc."},
|
144 |
+
{"symbol": "T", "name": "AT&T Inc."},
|
145 |
+
{"symbol": "PEP", "name": "PepsiCo, Inc."},
|
146 |
+
{"symbol": "MRK", "name": "Merck & Co., Inc."}
|
147 |
+
]
|
148 |
+
|
149 |
+
# Try to fetch more comprehensive list if API key is available
|
150 |
try:
|
151 |
+
from dotenv import load_dotenv
|
152 |
+
load_dotenv()
|
153 |
+
api_key = os.getenv("TWELVEDATA_API_KEY")
|
154 |
+
if api_key:
|
155 |
+
td_api = TwelveDataAPI(api_key)
|
156 |
+
stocks_data = td_api.get_all_stocks(exchange="NASDAQ")
|
157 |
+
if stocks_data and 'data' in stocks_data:
|
158 |
+
# Convert to format we need and take first 1000 stocks
|
159 |
+
symbols = [{"symbol": stock["symbol"], "name": stock.get("name", "Unknown")}
|
160 |
+
for stock in stocks_data['data']]
|
161 |
+
|
162 |
+
# Save to cache
|
163 |
+
os.makedirs(os.path.dirname(cache_file), exist_ok=True)
|
164 |
+
with open(cache_file, 'w') as f:
|
165 |
+
json.dump(symbols, f)
|
166 |
+
|
167 |
+
return symbols
|
168 |
except Exception as e:
|
169 |
+
print(f"Error fetching stock symbols from API: {e}")
|
170 |
+
|
171 |
+
# If everything fails, return default list
|
172 |
+
return default_symbols
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
173 |
|
174 |
+
# Load stock symbols
|
175 |
+
STOCK_SYMBOLS = load_stock_symbols()
|
176 |
|
177 |
+
# Function to format stock options for display
|
178 |
+
def format_stock_option(stock):
|
179 |
+
return f"{stock['symbol']} - {stock['name']}"
|
180 |
|
181 |
+
# Create interface
|
182 |
+
col1, col2 = st.columns([3, 1])
|
183 |
|
184 |
+
# Information input section
|
185 |
+
with col2:
|
186 |
+
st.subheader("Enter Information")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
187 |
|
188 |
+
# Create a list of formatted options and a mapping back to symbols
|
189 |
+
stock_options = [format_stock_option(stock) for stock in STOCK_SYMBOLS]
|
190 |
|
191 |
+
# Use selectbox with search functionality
|
192 |
+
selected_stock = st.selectbox(
|
193 |
+
"Select a stock symbol",
|
194 |
+
options=stock_options,
|
195 |
+
index=0 if stock_options else None,
|
196 |
+
placeholder="Search for a stock symbol...",
|
197 |
+
)
|
198 |
|
199 |
+
# Extract symbol from selection
|
200 |
+
if selected_stock:
|
201 |
+
stock_symbol = selected_stock.split(" - ")[0]
|
202 |
+
else:
|
203 |
+
stock_symbol = ""
|
|
|
204 |
|
205 |
+
if st.button("Generate Report", use_container_width=True, type="primary"):
|
206 |
+
if not stock_symbol:
|
207 |
+
st.error("Please select a stock symbol to continue.")
|
208 |
+
else:
|
209 |
+
# Save stock symbol to session state to maintain between runs
|
210 |
+
st.session_state.stock_symbol = stock_symbol
|
211 |
+
st.session_state.analysis_requested = True
|
212 |
+
st.rerun()
|
213 |
|
214 |
+
# PDF report generation section
|
215 |
+
if "analysis_complete" in st.session_state and st.session_state.analysis_complete:
|
216 |
+
st.divider()
|
217 |
+
st.subheader("PDF Report")
|
|
|
|
|
|
|
|
|
|
|
|
|
218 |
|
219 |
+
# Get results from session state
|
220 |
+
analysis_results = st.session_state.analysis_results
|
|
|
221 |
|
222 |
+
# Create static directory if it doesn't exist
|
223 |
+
os.makedirs("static", exist_ok=True)
|
224 |
|
225 |
+
# Create PDF filename and path
|
226 |
+
filename = f"Report_{analysis_results['symbol']}_{datetime.now().strftime('%d%m%Y')}.pdf"
|
227 |
+
pdf_path = os.path.join("static", filename)
|
228 |
+
|
229 |
+
# Display information
|
230 |
+
st.markdown("Get a complete PDF report with price charts:")
|
231 |
+
|
232 |
+
# Import PDF generation function
|
233 |
+
from modules.analysis_pipeline import generate_pdf_report
|
234 |
+
|
235 |
+
# Generate and download PDF button (combined)
|
236 |
+
if st.button("📊 Generate & Download PDF Report", use_container_width=True, key="pdf_btn", type="primary"):
|
237 |
+
# Check if file doesn't exist or needs to be recreated
|
238 |
+
if not os.path.exists(pdf_path):
|
239 |
+
with st.spinner("Creating PDF report with charts..."):
|
240 |
+
generate_pdf_report(analysis_results, pdf_path)
|
241 |
+
|
242 |
+
if not os.path.exists(pdf_path):
|
243 |
+
st.error("Failed to create PDF report.")
|
244 |
+
st.stop()
|
245 |
|
246 |
+
# Read PDF file for download
|
247 |
+
with open(pdf_path, "rb") as pdf_file:
|
248 |
+
pdf_bytes = pdf_file.read()
|
|
|
249 |
|
250 |
+
# Display success message and download widget
|
251 |
+
st.success("PDF report generated successfully!")
|
252 |
+
|
253 |
+
st.download_button(
|
254 |
+
label="⬇️ Download Report",
|
255 |
+
data=pdf_bytes,
|
256 |
+
file_name=filename,
|
257 |
+
mime="application/pdf",
|
258 |
+
use_container_width=True,
|
259 |
+
key="download_pdf_btn"
|
260 |
+
)
|
261 |
+
|
262 |
+
# Report display section
|
263 |
+
with col1:
|
264 |
+
# Check if there's an analysis request
|
265 |
+
if "analysis_requested" in st.session_state and st.session_state.analysis_requested:
|
266 |
+
symbol = st.session_state.stock_symbol
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
267 |
|
268 |
+
with st.spinner(f"🔍 Collecting data and analyzing {symbol} stock... (this may take a few minutes)"):
|
269 |
+
try:
|
270 |
+
# Run analysis
|
271 |
+
analysis_results = asyncio.run(run_analysis_pipeline(symbol))
|
272 |
+
|
273 |
+
# Save results to session state
|
274 |
+
st.session_state.analysis_results = analysis_results
|
275 |
+
st.session_state.analysis_complete = True
|
276 |
+
st.session_state.analysis_requested = False
|
277 |
+
|
278 |
+
# Automatically rerun to display results
|
279 |
+
st.rerun()
|
280 |
+
except Exception as e:
|
281 |
+
st.error(f"An error occurred during analysis: {str(e)}")
|
282 |
+
st.session_state.analysis_requested = False
|
283 |
|
284 |
+
# Check if analysis is complete
|
285 |
+
if "analysis_complete" in st.session_state and st.session_state.analysis_complete:
|
286 |
+
# Get results from session state
|
287 |
+
analysis_results = st.session_state.analysis_results
|
288 |
+
|
289 |
+
# Create tabs to display content
|
290 |
+
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
291 |
+
"📋 Overview",
|
292 |
+
"💰 Financial Health",
|
293 |
+
"📰 News & Sentiment",
|
294 |
+
"👨💼 Market Analysis",
|
295 |
+
"📊 Price Charts"
|
296 |
+
])
|
297 |
+
|
298 |
+
with tab1:
|
299 |
+
# Display basic company information
|
300 |
+
overview = analysis_results.get('overview', {})
|
301 |
+
if overview:
|
302 |
+
col1, col2 = st.columns([1, 1])
|
303 |
+
with col1:
|
304 |
+
st.subheader(f"{analysis_results['symbol']} - {overview.get('Name', 'N/A')}")
|
305 |
+
st.write(f"**Industry:** {overview.get('Industry', 'N/A')}")
|
306 |
+
st.write(f"**Sector:** {overview.get('Sector', 'N/A')}")
|
307 |
+
with col2:
|
308 |
+
st.write(f"**Market Cap:** {overview.get('MarketCapitalization', 'N/A')}")
|
309 |
+
st.write(f"**P/E Ratio:** {overview.get('PERatio', 'N/A')}")
|
310 |
+
st.write(f"**Dividend Yield:** {overview.get('DividendYield', 'N/A')}%")
|
311 |
+
|
312 |
+
# Display summary
|
313 |
+
st.markdown("### Summary & Recommendation")
|
314 |
+
st.markdown(analysis_results['analysis']['summary'])
|
315 |
+
|
316 |
+
with tab2:
|
317 |
+
st.markdown("### Financial Health Analysis")
|
318 |
+
st.markdown(analysis_results['analysis']['financial_health'])
|
319 |
+
|
320 |
+
with tab3:
|
321 |
+
st.markdown("### News & Market Sentiment Analysis")
|
322 |
+
st.markdown(analysis_results['analysis']['news_sentiment'])
|
323 |
|
324 |
+
with tab4:
|
325 |
+
st.markdown("### Market Analysis")
|
326 |
+
st.markdown(analysis_results['analysis']['expert_opinion'])
|
327 |
+
|
328 |
+
with tab5:
|
329 |
+
st.markdown("### Stock Price Charts")
|
330 |
|
331 |
+
# Display charts from price data
|
332 |
+
price_data = analysis_results.get('price_data', {})
|
333 |
+
if price_data:
|
334 |
+
period_tabs = st.tabs(['1 Month', '3 Months', '1 Year'])
|
335 |
+
|
336 |
+
periods = ['1_month', '3_months', '1_year']
|
337 |
+
for i, period in enumerate(periods):
|
338 |
+
with period_tabs[i]:
|
339 |
+
if period in price_data:
|
340 |
+
chart = create_price_chart(price_data[period], period)
|
341 |
+
if chart:
|
342 |
+
st.altair_chart(chart, use_container_width=True)
|
343 |
+
else:
|
344 |
+
st.info(f"Insufficient data to display chart for {period} timeframe.")
|
345 |
else:
|
346 |
+
st.info(f"No chart data available for {period} timeframe.")
|
347 |
+
else:
|
348 |
+
st.info("No price chart data available for this stock.")
|
349 |
+
else:
|
350 |
+
# Display instructions when no analysis is present
|
351 |
+
st.info("👈 Enter a stock symbol and click 'Generate Report' to begin.")
|
352 |
+
st.markdown("""
|
353 |
+
### About Stock Analysis Reports
|
354 |
+
|
355 |
+
The stock analysis report includes the following information:
|
356 |
+
|
357 |
+
1. **Overview & Investment Recommendation**: Summary of the company and general investment potential assessment.
|
358 |
+
2. **Financial Health Analysis**: Evaluation of financial metrics, revenue growth, and profitability.
|
359 |
+
3. **News & Market Sentiment Analysis**: Summary of notable news related to the company.
|
360 |
+
4. **Market Analysis**: Analysis of current stock performance and market trends.
|
361 |
+
5. **Price Charts**: Stock price charts for various timeframes.
|
362 |
+
|
363 |
+
Reports are generated based on data from multiple sources and analyzed by AI.
|
364 |
+
""")
|
365 |
+
|
366 |
+
# Display popular stock symbols
|
367 |
+
st.markdown("### Popular Stock Symbols")
|
368 |
+
|
369 |
+
# Display list of popular stock symbols in grid
|
370 |
+
# Only take first 12 to avoid cluttering the interface
|
371 |
+
display_stocks = STOCK_SYMBOLS[:12]
|
372 |
+
|
373 |
+
# Create grid with 4 columns
|
374 |
+
cols = st.columns(4)
|
375 |
+
for i, stock in enumerate(display_stocks):
|
376 |
+
col = cols[i % 4]
|
377 |
+
if col.button(f"{stock['symbol']} - {stock['name']}", key=f"pop_stock_{i}", use_container_width=True):
|
378 |
+
st.session_state.stock_symbol = stock['symbol']
|
379 |
+
st.session_state.analysis_requested = True
|
380 |
+
st.rerun()
|
|
|
|