refactor: improve structure
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
UI Components for the Stock Trading Platform
|
||||||
|
"""
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""
|
||||||
|
Clean, professional chat interface component
|
||||||
|
"""
|
||||||
|
|
||||||
|
import streamlit as st
|
||||||
|
from core.ai_assistant import ai_assistant
|
||||||
|
from core.data_service import data_service
|
||||||
|
|
||||||
|
|
||||||
|
class ChatInterface:
|
||||||
|
"""Professional chat interface for stock analysis"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def render(symbol: str, country: str):
|
||||||
|
"""Render the chat interface"""
|
||||||
|
if not symbol:
|
||||||
|
st.warning("⚠️ Please select a stock from the sidebar.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Display chat history
|
||||||
|
if 'chat_history' not in st.session_state:
|
||||||
|
st.session_state.chat_history = []
|
||||||
|
|
||||||
|
if st.session_state.chat_history:
|
||||||
|
for message in st.session_state.chat_history:
|
||||||
|
if message['role'] == 'user':
|
||||||
|
st.chat_message("user").write(message['content'])
|
||||||
|
else:
|
||||||
|
st.chat_message("assistant").write(message['content'])
|
||||||
|
else:
|
||||||
|
# Clean welcome message
|
||||||
|
basic_info = data_service.get_basic_info(symbol, country)
|
||||||
|
company_name = basic_info.get('company_name', symbol)
|
||||||
|
welcome_msg = f"👋 Hello! I'm your AI assistant for **{company_name} ({symbol})**. Ask me anything!"
|
||||||
|
st.chat_message("assistant").write(welcome_msg)
|
||||||
|
|
||||||
|
# Chat input
|
||||||
|
user_input = st.chat_input("Ask about price, trends, analysis, trading recommendations...")
|
||||||
|
|
||||||
|
if user_input:
|
||||||
|
# Add user message
|
||||||
|
st.session_state.chat_history.append({'role': 'user', 'content': user_input})
|
||||||
|
|
||||||
|
# Generate AI response (only loads data if tools are called)
|
||||||
|
with st.spinner("Thinking..."):
|
||||||
|
ai_response = ai_assistant.generate_response(user_input, symbol, country)
|
||||||
|
|
||||||
|
# Add AI response
|
||||||
|
st.session_state.chat_history.append({'role': 'assistant', 'content': ai_response})
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
# Quick actions (collapsed by default)
|
||||||
|
ChatInterface._render_quick_actions(symbol, country)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _render_quick_actions(symbol: str, country: str):
|
||||||
|
"""Render quick action buttons"""
|
||||||
|
with st.expander("🚀 Quick Actions", expanded=False):
|
||||||
|
col1, col2, col3, col4 = st.columns(4)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
if st.button("📈 Price Info", use_container_width=True):
|
||||||
|
ChatInterface._add_price_info(symbol, country)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
if st.button("📊 30-Day Analysis", use_container_width=True):
|
||||||
|
ChatInterface._add_medium_term_analysis(symbol)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
with col3:
|
||||||
|
if st.button("💰 Trading Rec", use_container_width=True):
|
||||||
|
ChatInterface._add_trading_recommendation(symbol, country)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
with col4:
|
||||||
|
if st.button("☪️ Sharia", use_container_width=True):
|
||||||
|
ChatInterface._add_sharia_compliance(symbol, country)
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_price_info(symbol: str, country: str):
|
||||||
|
"""Add current price info to chat"""
|
||||||
|
basic_info = data_service.get_basic_info(symbol, country)
|
||||||
|
|
||||||
|
current_price = basic_info.get('current_price', 0)
|
||||||
|
market_cap = basic_info.get('market_cap', 0)
|
||||||
|
sector = basic_info.get('sector', 'N/A')
|
||||||
|
|
||||||
|
message = f"""📈 **Current Price Info for {symbol}:**
|
||||||
|
|
||||||
|
💰 **Price:** ${current_price:.2f}
|
||||||
|
🏢 **Market Cap:** ${market_cap:,.0f}
|
||||||
|
🏭 **Sector:** {sector}"""
|
||||||
|
|
||||||
|
st.session_state.chat_history.append({'role': 'assistant', 'content': message})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_medium_term_analysis(symbol: str):
|
||||||
|
"""Add 30-day analysis to chat"""
|
||||||
|
analysis = data_service.get_analysis(symbol, "1mo")
|
||||||
|
|
||||||
|
if 'error' in analysis:
|
||||||
|
message = f"❌ **30-Day Analysis:** {analysis['error']}"
|
||||||
|
else:
|
||||||
|
return_pct = analysis.get('total_return_pct', 0)
|
||||||
|
volatility = analysis.get('volatility_annualized', 0)
|
||||||
|
trend = analysis.get('trend_direction', 'neutral')
|
||||||
|
|
||||||
|
message = f"""📊 **30-Day Analysis for {symbol}:**
|
||||||
|
|
||||||
|
📈 **Return:** {return_pct:.2f}%
|
||||||
|
📉 **Volatility:** {volatility:.1f}% (annualized)
|
||||||
|
🎯 **Trend:** {trend.title()}"""
|
||||||
|
|
||||||
|
st.session_state.chat_history.append({'role': 'assistant', 'content': message})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_trading_recommendation(symbol: str, country: str):
|
||||||
|
"""Add trading recommendation to chat"""
|
||||||
|
trading = data_service.get_trading_recommendation(symbol, country)
|
||||||
|
|
||||||
|
if 'error' in trading:
|
||||||
|
message = f"❌ **Trading Recommendation:** {trading['error']}"
|
||||||
|
else:
|
||||||
|
rec = trading.get('recommendation', 'HOLD')
|
||||||
|
conf = trading.get('confidence', 0.5) * 100
|
||||||
|
reasoning = trading.get('reasoning', 'No reasoning available')
|
||||||
|
|
||||||
|
message = f"""💰 **Trading Recommendation for {symbol}:**
|
||||||
|
|
||||||
|
🎯 **Action:** {rec}
|
||||||
|
📊 **Confidence:** {conf:.0f}%
|
||||||
|
💭 **Reasoning:** {reasoning[:200]}..."""
|
||||||
|
|
||||||
|
st.session_state.chat_history.append({'role': 'assistant', 'content': message})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _add_sharia_compliance(symbol: str, country: str):
|
||||||
|
"""Add Sharia compliance to chat"""
|
||||||
|
sharia = data_service.get_sharia_compliance(symbol, country)
|
||||||
|
|
||||||
|
if 'error' in sharia:
|
||||||
|
message = f"❌ **Sharia Compliance:** {sharia['error']}"
|
||||||
|
else:
|
||||||
|
ruling = sharia.get('ruling', 'UNCERTAIN')
|
||||||
|
conf = sharia.get('confidence', 0.5) * 100
|
||||||
|
|
||||||
|
status_emoji = "✅" if ruling == "HALAL" else "❌" if ruling == "HARAM" else "⚠️"
|
||||||
|
|
||||||
|
message = f"""☪️ **Sharia Compliance for {symbol}:**
|
||||||
|
|
||||||
|
{status_emoji} **Ruling:** {ruling}
|
||||||
|
📊 **Confidence:** {conf:.0f}%"""
|
||||||
|
|
||||||
|
st.session_state.chat_history.append({'role': 'assistant', 'content': message})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Core module for AI Stock Trading Platform
|
||||||
|
"""
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import os
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from openai import OpenAI
|
||||||
|
from .data_service import data_service
|
||||||
|
|
||||||
|
|
||||||
|
class AIAssistant:
|
||||||
|
"""Enhanced AI assistant with comprehensive stock analysis tools"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
|
||||||
|
|
||||||
|
def get_enhanced_tools(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Get comprehensive tool definitions for OpenAI function calling"""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_current_price_info",
|
||||||
|
"description": "Get current price, basic metrics, and company info",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {"type": "string", "description": "Stock symbol"}
|
||||||
|
},
|
||||||
|
"required": ["symbol"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_short_term_analysis",
|
||||||
|
"description": "Get 10-day technical analysis and short-term trends",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {"type": "string", "description": "Stock symbol"}
|
||||||
|
},
|
||||||
|
"required": ["symbol"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_medium_term_analysis",
|
||||||
|
"description": "Get 30-day technical analysis and medium-term trends",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {"type": "string", "description": "Stock symbol"}
|
||||||
|
},
|
||||||
|
"required": ["symbol"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_long_term_analysis",
|
||||||
|
"description": "Get 90-day technical analysis and long-term trends",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {"type": "string", "description": "Stock symbol"}
|
||||||
|
},
|
||||||
|
"required": ["symbol"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_comprehensive_analysis",
|
||||||
|
"description": "Get full 1-year technical analysis with all indicators",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {"type": "string", "description": "Stock symbol"}
|
||||||
|
},
|
||||||
|
"required": ["symbol"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_trading_recommendation",
|
||||||
|
"description": "Get buy/hold/sell recommendation with price targets and reasoning",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {"type": "string", "description": "Stock symbol"}
|
||||||
|
},
|
||||||
|
"required": ["symbol"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_sharia_compliance",
|
||||||
|
"description": "Get Islamic finance compliance analysis",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {"type": "string", "description": "Stock symbol"}
|
||||||
|
},
|
||||||
|
"required": ["symbol"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "compare_time_periods",
|
||||||
|
"description": "Compare performance across multiple time periods (10d, 30d, 90d)",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {"type": "string", "description": "Stock symbol"}
|
||||||
|
},
|
||||||
|
"required": ["symbol"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def generate_response(self, user_input: str, symbol: str, country: str) -> str:
|
||||||
|
"""Generate AI response with enhanced tool calling"""
|
||||||
|
try:
|
||||||
|
# Get basic info without heavy loading
|
||||||
|
basic_info = data_service.get_basic_info(symbol, country)
|
||||||
|
|
||||||
|
system_msg = f"""You are a professional financial advisor assistant for {symbol}.
|
||||||
|
|
||||||
|
IMPORTANT: Only call tools when users specifically request:
|
||||||
|
- Price information or basic metrics → get_current_price_info
|
||||||
|
- Short-term analysis (10 days) → get_short_term_analysis
|
||||||
|
- Medium-term analysis (30 days) → get_medium_term_analysis
|
||||||
|
- Long-term analysis (90 days) → get_long_term_analysis
|
||||||
|
- Comprehensive analysis (1 year) → get_comprehensive_analysis
|
||||||
|
- Trading recommendations → get_trading_recommendation
|
||||||
|
- Sharia compliance → get_sharia_compliance
|
||||||
|
- Time period comparisons → compare_time_periods
|
||||||
|
|
||||||
|
For general questions about the company, market commentary, or basic information, respond directly without calling tools.
|
||||||
|
Keep responses concise and professional."""
|
||||||
|
|
||||||
|
user_msg = f"""Stock: {symbol} ({basic_info.get('company_name', 'N/A')})
|
||||||
|
Country: {country}
|
||||||
|
Sector: {basic_info.get('sector', 'N/A')}
|
||||||
|
User Question: {user_input}"""
|
||||||
|
|
||||||
|
response = self.client.chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": system_msg},
|
||||||
|
{"role": "user", "content": user_msg}
|
||||||
|
],
|
||||||
|
tools=self.get_enhanced_tools(), # type: ignore
|
||||||
|
tool_choice="auto",
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=600
|
||||||
|
)
|
||||||
|
|
||||||
|
message = response.choices[0].message
|
||||||
|
|
||||||
|
if message.tool_calls:
|
||||||
|
return self._handle_tool_calls(message.tool_calls, user_input, symbol, country)
|
||||||
|
|
||||||
|
return message.content or "I apologize, but I couldn't generate a response."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Sorry, I encountered an error: {str(e)}"
|
||||||
|
|
||||||
|
def _handle_tool_calls(self, tool_calls, user_input: str, symbol: str, country: str) -> str:
|
||||||
|
"""Handle tool calls and generate final response"""
|
||||||
|
tool_results = []
|
||||||
|
|
||||||
|
for tool_call in tool_calls:
|
||||||
|
function_name = tool_call.function.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
if function_name == "get_current_price_info":
|
||||||
|
basic_info = data_service.get_basic_info(symbol, country)
|
||||||
|
current_price = basic_info.get('current_price', 0)
|
||||||
|
market_cap = basic_info.get('market_cap', 0)
|
||||||
|
tool_results.append(f"Current Price: ${current_price:.2f}, Market Cap: ${market_cap:,.0f}")
|
||||||
|
|
||||||
|
elif function_name == "get_short_term_analysis":
|
||||||
|
analysis = data_service.get_analysis(symbol, "10d")
|
||||||
|
if 'error' not in analysis:
|
||||||
|
return_pct = analysis.get('total_return_pct', 0)
|
||||||
|
volatility = analysis.get('volatility_annualized', 0)
|
||||||
|
tool_results.append(f"10-Day Analysis: Return {return_pct:.2f}%, Volatility {volatility:.1f}%")
|
||||||
|
else:
|
||||||
|
tool_results.append("10-Day Analysis: Data unavailable")
|
||||||
|
|
||||||
|
elif function_name == "get_medium_term_analysis":
|
||||||
|
analysis = data_service.get_analysis(symbol, "1mo")
|
||||||
|
if 'error' not in analysis:
|
||||||
|
return_pct = analysis.get('total_return_pct', 0)
|
||||||
|
trend = analysis.get('trend_direction', 'neutral')
|
||||||
|
tool_results.append(f"30-Day Analysis: Return {return_pct:.2f}%, Trend {trend}")
|
||||||
|
else:
|
||||||
|
tool_results.append("30-Day Analysis: Data unavailable")
|
||||||
|
|
||||||
|
elif function_name == "get_long_term_analysis":
|
||||||
|
analysis = data_service.get_analysis(symbol, "3mo")
|
||||||
|
if 'error' not in analysis:
|
||||||
|
return_pct = analysis.get('total_return_pct', 0)
|
||||||
|
sharpe = analysis.get('sharpe_ratio', 0)
|
||||||
|
tool_results.append(f"90-Day Analysis: Return {return_pct:.2f}%, Sharpe {sharpe:.2f}")
|
||||||
|
else:
|
||||||
|
tool_results.append("90-Day Analysis: Data unavailable")
|
||||||
|
|
||||||
|
elif function_name == "get_comprehensive_analysis":
|
||||||
|
analysis = data_service.get_analysis(symbol, "1y")
|
||||||
|
if 'error' not in analysis:
|
||||||
|
return_pct = analysis.get('total_return_pct', 0)
|
||||||
|
max_drawdown = analysis.get('max_drawdown', 0)
|
||||||
|
rsi = analysis.get('rsi', 50)
|
||||||
|
tool_results.append(f"1-Year Analysis: Return {return_pct:.2f}%, Max Drawdown {max_drawdown:.1f}%, RSI {rsi:.1f}")
|
||||||
|
else:
|
||||||
|
tool_results.append("1-Year Analysis: Data unavailable")
|
||||||
|
|
||||||
|
elif function_name == "get_trading_recommendation":
|
||||||
|
trading = data_service.get_trading_recommendation(symbol, country)
|
||||||
|
if 'error' not in trading:
|
||||||
|
rec = trading.get('recommendation', 'HOLD')
|
||||||
|
conf = trading.get('confidence', 0.5) * 100
|
||||||
|
tool_results.append(f"Trading: {rec} ({conf:.0f}% confidence)")
|
||||||
|
else:
|
||||||
|
tool_results.append("Trading: Analysis unavailable")
|
||||||
|
|
||||||
|
elif function_name == "get_sharia_compliance":
|
||||||
|
sharia = data_service.get_sharia_compliance(symbol, country)
|
||||||
|
if 'error' not in sharia:
|
||||||
|
ruling = sharia.get('ruling', 'UNCERTAIN')
|
||||||
|
conf = sharia.get('confidence', 0.5) * 100
|
||||||
|
tool_results.append(f"Sharia: {ruling} ({conf:.0f}% confidence)")
|
||||||
|
else:
|
||||||
|
tool_results.append("Sharia: Analysis unavailable")
|
||||||
|
|
||||||
|
elif function_name == "compare_time_periods":
|
||||||
|
periods = ["10d", "1mo", "3mo"]
|
||||||
|
comparisons = []
|
||||||
|
for period in periods:
|
||||||
|
analysis = data_service.get_analysis(symbol, period)
|
||||||
|
if 'error' not in analysis:
|
||||||
|
return_pct = analysis.get('total_return_pct', 0)
|
||||||
|
comparisons.append(f"{period}: {return_pct:.2f}%")
|
||||||
|
tool_results.append(f"Period Comparison: {', '.join(comparisons)}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
tool_results.append(f"{function_name}: Error - {str(e)}")
|
||||||
|
|
||||||
|
# Generate final response
|
||||||
|
final_response = self.client.chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "Provide a concise, professional response based on the tool results. Focus on actionable insights."},
|
||||||
|
{"role": "user", "content": f"Question: {user_input}\n\nTool Results: {' | '.join(tool_results)}"}
|
||||||
|
],
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=500
|
||||||
|
)
|
||||||
|
|
||||||
|
return final_response.choices[0].message.content or "I couldn't generate a response."
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
ai_assistant = AIAssistant()
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import streamlit as st
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
from tools.fetching import stock_fetcher
|
||||||
|
from tools.analysis import stock_analyzer
|
||||||
|
from tools.trading_decisions import trading_engine
|
||||||
|
from tools.sharia_compliance import sharia_checker
|
||||||
|
|
||||||
|
|
||||||
|
class DataService:
|
||||||
|
"""Centralized data service for efficient stock data management"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_basic_info(symbol: str, country: str) -> Dict[str, Any]:
|
||||||
|
"""Get only basic stock info - no heavy analysis"""
|
||||||
|
cache_key = f"{symbol}_basic"
|
||||||
|
|
||||||
|
if cache_key not in st.session_state:
|
||||||
|
try:
|
||||||
|
stock_info = stock_fetcher.get_stock_info(symbol, country)
|
||||||
|
st.session_state[cache_key] = stock_info
|
||||||
|
except Exception as e:
|
||||||
|
st.session_state[cache_key] = {
|
||||||
|
'company_name': symbol,
|
||||||
|
'error': str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return st.session_state[cache_key]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_price_data(symbol: str, period: str = "1y") -> Dict[str, Any]:
|
||||||
|
"""Get price data for specific period"""
|
||||||
|
cache_key = f"{symbol}_data_{period}"
|
||||||
|
|
||||||
|
if cache_key not in st.session_state:
|
||||||
|
try:
|
||||||
|
data = stock_fetcher.fetch_stock_data(symbol, period=period)
|
||||||
|
st.session_state[cache_key] = data
|
||||||
|
except Exception as e:
|
||||||
|
st.session_state[cache_key] = None
|
||||||
|
st.error(f"Failed to load {period} data: {str(e)}")
|
||||||
|
|
||||||
|
return st.session_state[cache_key]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_analysis(symbol: str, period: str = "1y") -> Dict[str, Any]:
|
||||||
|
"""Get technical analysis for specific period"""
|
||||||
|
cache_key = f"{symbol}_analysis_{period}"
|
||||||
|
|
||||||
|
if cache_key not in st.session_state:
|
||||||
|
data = DataService.get_price_data(symbol, period)
|
||||||
|
if data is not None and hasattr(data, 'empty') and not data.empty:
|
||||||
|
try:
|
||||||
|
analysis = stock_analyzer.analyze_stock(data)
|
||||||
|
analysis['period'] = period
|
||||||
|
st.session_state[cache_key] = analysis
|
||||||
|
except Exception as e:
|
||||||
|
st.session_state[cache_key] = {'error': f"Analysis failed: {str(e)}"}
|
||||||
|
else:
|
||||||
|
st.session_state[cache_key] = {'error': 'No data available'}
|
||||||
|
|
||||||
|
return st.session_state[cache_key]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_trading_recommendation(symbol: str, country: str) -> Dict[str, Any]:
|
||||||
|
"""Get trading recommendation"""
|
||||||
|
cache_key = f"{symbol}_trading"
|
||||||
|
|
||||||
|
if cache_key not in st.session_state:
|
||||||
|
try:
|
||||||
|
analysis = DataService.get_analysis(symbol)
|
||||||
|
stock_info = DataService.get_basic_info(symbol, country)
|
||||||
|
|
||||||
|
if 'error' not in analysis and 'error' not in stock_info:
|
||||||
|
trading = trading_engine.get_trading_recommendation(symbol, analysis, stock_info)
|
||||||
|
st.session_state[cache_key] = trading
|
||||||
|
else:
|
||||||
|
st.session_state[cache_key] = {'error': 'Cannot generate recommendation'}
|
||||||
|
except Exception as e:
|
||||||
|
st.session_state[cache_key] = {'error': f"Trading analysis failed: {str(e)}"}
|
||||||
|
|
||||||
|
return st.session_state[cache_key]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_sharia_compliance(symbol: str, country: str) -> Dict[str, Any]:
|
||||||
|
"""Get Sharia compliance analysis"""
|
||||||
|
cache_key = f"{symbol}_sharia"
|
||||||
|
|
||||||
|
if cache_key not in st.session_state:
|
||||||
|
try:
|
||||||
|
stock_info = DataService.get_basic_info(symbol, country)
|
||||||
|
analysis = DataService.get_analysis(symbol)
|
||||||
|
|
||||||
|
if 'error' not in stock_info:
|
||||||
|
sharia = sharia_checker.check_sharia_compliance(symbol, stock_info, analysis)
|
||||||
|
st.session_state[cache_key] = sharia
|
||||||
|
else:
|
||||||
|
st.session_state[cache_key] = {'error': 'Cannot check compliance'}
|
||||||
|
except Exception as e:
|
||||||
|
st.session_state[cache_key] = {'error': f"Sharia check failed: {str(e)}"}
|
||||||
|
|
||||||
|
return st.session_state[cache_key]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clear_cache(symbol: Optional[str] = None):
|
||||||
|
"""Clear cached data"""
|
||||||
|
if symbol:
|
||||||
|
keys_to_remove = [key for key in st.session_state.keys() if isinstance(key, str) and key.startswith(f"{symbol}_")]
|
||||||
|
for key in keys_to_remove:
|
||||||
|
del st.session_state[key]
|
||||||
|
else:
|
||||||
|
# Clear all cache
|
||||||
|
keys_to_remove = [key for key in st.session_state.keys()
|
||||||
|
if isinstance(key, str) and ('_data_' in key or '_analysis_' in key or '_trading' in key or '_sharia' in key or '_basic' in key)]
|
||||||
|
for key in keys_to_remove:
|
||||||
|
del st.session_state[key]
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance
|
||||||
|
data_service = DataService()
|
||||||
@@ -14,6 +14,11 @@ from tools.trading_decisions import trading_engine
|
|||||||
from tools.sharia_compliance import sharia_checker
|
from tools.sharia_compliance import sharia_checker
|
||||||
from tools.charting import chart_generator
|
from tools.charting import chart_generator
|
||||||
|
|
||||||
|
# Import new modular components
|
||||||
|
from core.data_service import data_service
|
||||||
|
from core.ai_assistant import ai_assistant
|
||||||
|
from components.chat_interface import ChatInterface
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
@@ -211,264 +216,20 @@ class StockTradingApp:
|
|||||||
st.error(f"Error loading quick analysis: {str(e)}")
|
st.error(f"Error loading quick analysis: {str(e)}")
|
||||||
|
|
||||||
def load_stock_analysis(self, symbol: str):
|
def load_stock_analysis(self, symbol: str):
|
||||||
try:
|
"""Load complete analysis using data service"""
|
||||||
country = st.session_state.selected_country
|
country = st.session_state.selected_country
|
||||||
data = stock_fetcher.fetch_stock_data(symbol, period="1y")
|
# Pre-load all analysis components
|
||||||
stock_info = stock_fetcher.get_stock_info(symbol, country)
|
data_service.get_analysis(symbol)
|
||||||
analysis = stock_analyzer.analyze_stock(data)
|
data_service.get_trading_recommendation(symbol, country)
|
||||||
trading_decision = trading_engine.get_trading_recommendation(symbol, analysis, stock_info)
|
data_service.get_sharia_compliance(symbol, country)
|
||||||
sharia_compliance = sharia_checker.check_sharia_compliance(symbol, stock_info, analysis)
|
|
||||||
|
|
||||||
st.session_state.stock_data[symbol] = {
|
|
||||||
'data': data,
|
|
||||||
'stock_info': stock_info,
|
|
||||||
'analysis': analysis,
|
|
||||||
'trading_decision': trading_decision,
|
|
||||||
'sharia_compliance': sharia_compliance
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
st.error(f"Error loading analysis for {symbol}: {str(e)}")
|
|
||||||
|
|
||||||
def render_chat_page(self):
|
def render_chat_page(self):
|
||||||
st.header("💬 AI Stock Analysis Chat")
|
st.header("💬 AI Stock Analysis Chat")
|
||||||
|
|
||||||
if not st.session_state.selected_stock:
|
|
||||||
st.warning("⚠️ Please select a stock from the sidebar to start chatting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
symbol = st.session_state.selected_stock
|
symbol = st.session_state.selected_stock
|
||||||
st.info(f"💬 Chatting about: **{symbol}**")
|
|
||||||
|
|
||||||
if symbol not in st.session_state.stock_data:
|
|
||||||
with st.spinner("Loading stock data and analysis..."):
|
|
||||||
self.load_stock_analysis(symbol)
|
|
||||||
|
|
||||||
self.render_chat_interface()
|
|
||||||
|
|
||||||
def render_chat_interface(self):
|
|
||||||
symbol = st.session_state.selected_stock
|
|
||||||
|
|
||||||
if st.session_state.chat_history:
|
|
||||||
for message in st.session_state.chat_history:
|
|
||||||
if message['role'] == 'user':
|
|
||||||
st.chat_message("user").write(message['content'])
|
|
||||||
else:
|
|
||||||
st.chat_message("assistant").write(message['content'])
|
|
||||||
else:
|
|
||||||
welcome_msg = f"""
|
|
||||||
👋 Hello! I'm your AI stock analysis assistant. I can help you with:
|
|
||||||
|
|
||||||
• **Technical Analysis** of {symbol}
|
|
||||||
• **Trading Recommendations** (Buy/Hold/Sell)
|
|
||||||
• **Sharia Compliance** assessment
|
|
||||||
• **Risk Analysis** and market insights
|
|
||||||
|
|
||||||
What would you like to know about {symbol}?
|
|
||||||
"""
|
|
||||||
st.chat_message("assistant").write(welcome_msg)
|
|
||||||
|
|
||||||
user_input = st.chat_input("Ask me anything about this stock...")
|
|
||||||
|
|
||||||
if user_input:
|
|
||||||
st.session_state.chat_history.append({'role': 'user', 'content': user_input})
|
|
||||||
|
|
||||||
with st.spinner("Analyzing..."):
|
|
||||||
ai_response = self.generate_ai_response(user_input, symbol)
|
|
||||||
|
|
||||||
st.session_state.chat_history.append({'role': 'assistant', 'content': ai_response})
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
st.subheader("🚀 Quick Actions")
|
|
||||||
col1, col2, col3, col4 = st.columns(4)
|
|
||||||
|
|
||||||
with col1:
|
|
||||||
if st.button("📊 Get Analysis"):
|
|
||||||
self.add_analysis_to_chat(symbol)
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
if st.button("💰 Trading Rec"):
|
|
||||||
self.add_trading_to_chat(symbol)
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
with col3:
|
|
||||||
if st.button("☪️ Sharia Check"):
|
|
||||||
self.add_sharia_to_chat(symbol)
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
with col4:
|
|
||||||
if st.button("🎯 Price Target"):
|
|
||||||
self.add_target_to_chat(symbol)
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
def generate_ai_response(self, user_input: str, symbol: str) -> str:
|
|
||||||
try:
|
|
||||||
from openai import OpenAI
|
|
||||||
client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
|
|
||||||
|
|
||||||
# Check if user is asking about Sharia compliance
|
|
||||||
sharia_keywords = ['sharia', 'halal', 'haram', 'islamic', 'muslim', 'compliant', 'permissible', 'forbidden']
|
|
||||||
is_sharia_query = any(keyword in user_input.lower() for keyword in sharia_keywords)
|
|
||||||
|
|
||||||
stock_data = st.session_state.stock_data.get(symbol, {})
|
|
||||||
analysis = stock_data.get('analysis', {})
|
|
||||||
trading_decision = stock_data.get('trading_decision', {})
|
|
||||||
stock_info = stock_data.get('stock_info', {})
|
|
||||||
country = st.session_state.selected_country
|
|
||||||
|
|
||||||
# Format price with proper currency
|
|
||||||
current_price = analysis.get('current_price', 0)
|
|
||||||
formatted_price = stock_fetcher.format_price_with_currency(current_price, country)
|
|
||||||
|
|
||||||
# Base context without Sharia info
|
|
||||||
context = f"""
|
|
||||||
You are analyzing {symbol} ({stock_info.get('company_name', 'N/A')}).
|
|
||||||
|
|
||||||
Current Price: {formatted_price}
|
|
||||||
Return: {analysis.get('total_return_pct', 0):.2f}%
|
|
||||||
Recommendation: {trading_decision.get('recommendation', 'N/A')}
|
|
||||||
Sector: {stock_info.get('sector', 'N/A')}
|
|
||||||
|
|
||||||
User Question: {user_input}
|
|
||||||
|
|
||||||
Provide helpful analysis based on the available data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Add Sharia context only if user asks about it
|
|
||||||
if is_sharia_query:
|
|
||||||
# Load Sharia compliance if not already loaded
|
|
||||||
if symbol not in st.session_state.stock_data or 'sharia_compliance' not in st.session_state.stock_data[symbol]:
|
|
||||||
with st.spinner("Loading Sharia compliance analysis..."):
|
|
||||||
self.load_stock_analysis(symbol)
|
|
||||||
|
|
||||||
sharia_compliance = st.session_state.stock_data.get(symbol, {}).get('sharia_compliance', {})
|
|
||||||
context += f"""
|
|
||||||
|
|
||||||
SHARIA COMPLIANCE ANALYSIS:
|
|
||||||
Ruling: {sharia_compliance.get('ruling', 'N/A')}
|
|
||||||
Confidence: {sharia_compliance.get('confidence', 0)*100:.0f}%
|
|
||||||
Reasoning: {sharia_compliance.get('reasoning', 'N/A')}
|
|
||||||
|
|
||||||
Focus your response on Islamic finance principles and Sharia compliance.
|
|
||||||
"""
|
|
||||||
|
|
||||||
response = client.chat.completions.create(
|
|
||||||
model="gpt-4o-mini",
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": "You are a financial advisor and Islamic finance expert."},
|
|
||||||
{"role": "user", "content": context}
|
|
||||||
],
|
|
||||||
temperature=0.7,
|
|
||||||
max_tokens=400
|
|
||||||
)
|
|
||||||
|
|
||||||
return response.choices[0].message.content
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return f"Sorry, I'm having trouble right now. Error: {str(e)}"
|
|
||||||
|
|
||||||
def add_analysis_to_chat(self, symbol: str):
|
|
||||||
stock_data = st.session_state.stock_data.get(symbol, {})
|
|
||||||
analysis = stock_data.get('analysis', {})
|
|
||||||
|
|
||||||
if analysis:
|
|
||||||
summary = stock_analyzer.get_analysis_summary(analysis)
|
|
||||||
st.session_state.chat_history.append({
|
|
||||||
'role': 'assistant',
|
|
||||||
'content': f"📊 **Analysis Summary for {symbol}:**\n\n{summary}"
|
|
||||||
})
|
|
||||||
|
|
||||||
def add_trading_to_chat(self, symbol: str):
|
|
||||||
stock_data = st.session_state.stock_data.get(symbol, {})
|
|
||||||
trading_decision = stock_data.get('trading_decision', {})
|
|
||||||
stock_info = stock_data.get('stock_info', {})
|
|
||||||
country = st.session_state.selected_country
|
country = st.session_state.selected_country
|
||||||
|
|
||||||
if trading_decision:
|
ChatInterface.render(symbol, country)
|
||||||
rec = trading_decision.get('recommendation', 'HOLD')
|
|
||||||
conf = trading_decision.get('confidence', 0)
|
|
||||||
|
|
||||||
# Handle confidence as percentage if it's already 0-100, or as decimal if 0-1
|
|
||||||
if conf <= 1.0:
|
|
||||||
conf_pct = conf * 100
|
|
||||||
else:
|
|
||||||
conf_pct = conf
|
|
||||||
|
|
||||||
reason = trading_decision.get('reasoning', 'No reasoning available')
|
|
||||||
price_target = trading_decision.get('price_target')
|
|
||||||
stop_loss = trading_decision.get('stop_loss')
|
|
||||||
time_horizon = trading_decision.get('time_horizon', 'medium')
|
|
||||||
risk_level = trading_decision.get('risk_level', 'medium')
|
|
||||||
|
|
||||||
# Clean reasoning - remove JSON artifacts
|
|
||||||
if reason.startswith('```json') or reason.startswith('{'):
|
|
||||||
# Extract readable content from malformed JSON
|
|
||||||
if 'reasoning' in reason:
|
|
||||||
try:
|
|
||||||
import re
|
|
||||||
reasoning_match = re.search(r'"reasoning"\s*:\s*"([^"]+)"', reason)
|
|
||||||
if reasoning_match:
|
|
||||||
reason = reasoning_match.group(1)
|
|
||||||
else:
|
|
||||||
reason = "Technical analysis suggests this recommendation based on current market conditions."
|
|
||||||
except:
|
|
||||||
reason = "Technical analysis suggests this recommendation based on current market conditions."
|
|
||||||
|
|
||||||
# Format the message professionally
|
|
||||||
message_parts = [
|
|
||||||
f"💰 **Trading Recommendation: {rec}**",
|
|
||||||
f"📊 **Confidence Level:** {conf_pct:.0f}%",
|
|
||||||
f"⏱️ **Time Horizon:** {time_horizon.title()}-term",
|
|
||||||
f"⚠️ **Risk Level:** {risk_level.title()}",
|
|
||||||
"",
|
|
||||||
f"**Analysis:**",
|
|
||||||
reason
|
|
||||||
]
|
|
||||||
|
|
||||||
# Add price targets if available
|
|
||||||
if price_target:
|
|
||||||
formatted_target = stock_fetcher.format_price_with_currency(price_target, country)
|
|
||||||
message_parts.append(f"🎯 **Price Target:** {formatted_target}")
|
|
||||||
|
|
||||||
if stop_loss:
|
|
||||||
formatted_stop = stock_fetcher.format_price_with_currency(stop_loss, country)
|
|
||||||
message_parts.append(f"🛡️ **Stop Loss:** {formatted_stop}")
|
|
||||||
|
|
||||||
message_parts.append("")
|
|
||||||
message_parts.append("*This is not financial advice. Please do your own research and consult with a financial advisor.*")
|
|
||||||
|
|
||||||
message = "\n".join(message_parts)
|
|
||||||
st.session_state.chat_history.append({'role': 'assistant', 'content': message})
|
|
||||||
|
|
||||||
def add_sharia_to_chat(self, symbol: str):
|
|
||||||
stock_data = st.session_state.stock_data.get(symbol, {})
|
|
||||||
sharia_compliance = stock_data.get('sharia_compliance', {})
|
|
||||||
|
|
||||||
if sharia_compliance:
|
|
||||||
summary = sharia_checker.get_compliance_summary(sharia_compliance)
|
|
||||||
st.session_state.chat_history.append({
|
|
||||||
'role': 'assistant',
|
|
||||||
'content': f"☪️ **Sharia Compliance:**\n\n{summary}"
|
|
||||||
})
|
|
||||||
|
|
||||||
def add_target_to_chat(self, symbol: str):
|
|
||||||
stock_data = st.session_state.stock_data.get(symbol, {})
|
|
||||||
trading_decision = stock_data.get('trading_decision', {})
|
|
||||||
analysis = stock_data.get('analysis', {})
|
|
||||||
|
|
||||||
current = analysis.get('current_price', 0)
|
|
||||||
target = trading_decision.get('price_target')
|
|
||||||
stop = trading_decision.get('stop_loss')
|
|
||||||
|
|
||||||
message = f"🎯 **Current Price:** ${current:.2f}\n"
|
|
||||||
if target:
|
|
||||||
upside = ((target - current) / current) * 100
|
|
||||||
message += f"**Target:** ${target:.2f} ({upside:+.1f}%)\n"
|
|
||||||
if stop:
|
|
||||||
downside = ((stop - current) / current) * 100
|
|
||||||
message += f"**Stop Loss:** ${stop:.2f} ({downside:+.1f}%)"
|
|
||||||
|
|
||||||
st.session_state.chat_history.append({'role': 'assistant', 'content': message})
|
|
||||||
|
|
||||||
def render_dashboard_page(self):
|
def render_dashboard_page(self):
|
||||||
st.header("📊 Dashboard")
|
st.header("📊 Dashboard")
|
||||||
@@ -480,27 +241,29 @@ class StockTradingApp:
|
|||||||
symbol = st.session_state.selected_stock
|
symbol = st.session_state.selected_stock
|
||||||
country = st.session_state.selected_country
|
country = st.session_state.selected_country
|
||||||
|
|
||||||
if symbol not in st.session_state.stock_data:
|
# Load data using new data service
|
||||||
with st.spinner("Loading analysis..."):
|
with st.spinner("Loading dashboard data..."):
|
||||||
self.load_stock_analysis(symbol)
|
basic_info = data_service.get_basic_info(symbol, country)
|
||||||
|
data = data_service.get_price_data(symbol, "1y")
|
||||||
|
analysis = data_service.get_analysis(symbol, "1y")
|
||||||
|
trading_decision = data_service.get_trading_recommendation(symbol, country)
|
||||||
|
sharia_compliance = data_service.get_sharia_compliance(symbol, country)
|
||||||
|
|
||||||
stock_data = st.session_state.stock_data.get(symbol, {})
|
# Check if data loaded successfully
|
||||||
if not stock_data:
|
if data is None or analysis.get('error') or trading_decision.get('error'):
|
||||||
st.error("Failed to load data.")
|
st.error("Failed to load dashboard data. Please try again.")
|
||||||
return
|
return
|
||||||
|
|
||||||
analysis = stock_data.get('analysis', {})
|
|
||||||
trading_decision = stock_data.get('trading_decision', {})
|
|
||||||
sharia_compliance = stock_data.get('sharia_compliance', {})
|
|
||||||
data = stock_data.get('data')
|
|
||||||
|
|
||||||
# KPIs at the top
|
# KPIs at the top
|
||||||
col1, col2, col3, col4, col5 = st.columns(5)
|
col1, col2, col3, col4, col5 = st.columns(5)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
current_price = data['Close'].iloc[-1]
|
if data is not None and hasattr(data, 'iloc') and len(data) > 0:
|
||||||
formatted_price = stock_fetcher.format_price_with_currency(current_price, country)
|
current_price = data['Close'].iloc[-1]
|
||||||
st.metric("💰 Current Price", formatted_price)
|
formatted_price = stock_fetcher.format_price_with_currency(current_price, country)
|
||||||
|
st.metric("💰 Current Price", formatted_price)
|
||||||
|
else:
|
||||||
|
st.metric("💰 Current Price", "N/A")
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
total_return = analysis.get('total_return_pct', 0)
|
total_return = analysis.get('total_return_pct', 0)
|
||||||
@@ -508,36 +271,56 @@ class StockTradingApp:
|
|||||||
|
|
||||||
with col3:
|
with col3:
|
||||||
rec = trading_decision.get('recommendation', 'HOLD')
|
rec = trading_decision.get('recommendation', 'HOLD')
|
||||||
conf = trading_decision.get('confidence', 0) * 100
|
conf = trading_decision.get('confidence', 0.5)
|
||||||
st.metric("Recommendation", rec, f"{conf:.0f}% confidence")
|
if conf <= 1.0:
|
||||||
|
conf_pct = conf * 100
|
||||||
|
else:
|
||||||
|
conf_pct = conf
|
||||||
|
st.metric("Recommendation", rec, f"{conf_pct:.0f}% confidence")
|
||||||
|
|
||||||
with col4:
|
with col4:
|
||||||
ruling = sharia_compliance.get('ruling', 'UNCERTAIN')
|
ruling = sharia_compliance.get('ruling', 'UNCERTAIN')
|
||||||
sharia_conf = sharia_compliance.get('confidence', 0) * 100
|
sharia_conf = sharia_compliance.get('confidence', 0.5)
|
||||||
st.metric("Sharia Status", ruling, f"{sharia_conf:.0f}% confidence")
|
if sharia_conf <= 1.0:
|
||||||
|
sharia_conf_pct = sharia_conf * 100
|
||||||
|
else:
|
||||||
|
sharia_conf_pct = sharia_conf
|
||||||
|
st.metric("Sharia Status", ruling, f"{sharia_conf_pct:.0f}% confidence")
|
||||||
|
|
||||||
with col5:
|
with col5:
|
||||||
volatility = analysis.get('volatility_annualized', 0)
|
volatility = analysis.get('volatility_annualized', 0)
|
||||||
st.metric("Volatility", f"{volatility:.1f}%")
|
st.metric("Volatility", f"{volatility:.1f}%")
|
||||||
|
|
||||||
st.divider()
|
|
||||||
|
|
||||||
# Charts section
|
# Charts section (only if data is available)
|
||||||
|
if data is not None and hasattr(data, 'iloc') and len(data) > 0:
|
||||||
|
st.divider()
|
||||||
|
|
||||||
# First row: Risk Analysis and Trading Signals
|
# First row: Risk Analysis and Trading Signals
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
risk_fig = chart_generator.create_risk_analysis_chart(analysis, symbol)
|
try:
|
||||||
st.plotly_chart(risk_fig, use_container_width=True)
|
risk_fig = chart_generator.create_risk_analysis_chart(analysis, symbol)
|
||||||
|
st.plotly_chart(risk_fig, use_container_width=True)
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Risk chart error: {str(e)}")
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
signals_fig = chart_generator.create_trading_signals_chart(data, analysis, trading_decision, symbol)
|
try:
|
||||||
st.plotly_chart(signals_fig, use_container_width=True)
|
signals_fig = chart_generator.create_trading_signals_chart(data, analysis, trading_decision, symbol)
|
||||||
|
st.plotly_chart(signals_fig, use_container_width=True)
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Signals chart error: {str(e)}")
|
||||||
|
|
||||||
# Second row: Price Chart (full width)
|
# Second row: Price Chart (full width)
|
||||||
price_fig = chart_generator.create_price_chart(data, symbol, analysis)
|
try:
|
||||||
st.plotly_chart(price_fig, use_container_width=True)
|
price_fig = chart_generator.create_price_chart(data, symbol, analysis)
|
||||||
|
st.plotly_chart(price_fig, use_container_width=True)
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"Price chart error: {str(e)}")
|
||||||
|
else:
|
||||||
|
st.warning("📊 Charts unavailable - no price data loaded.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user