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