Spaces:
Sleeping
Sleeping
Commit ·
a189e3e
1
Parent(s): a7d376f
initial code
Browse files- app.py +659 -0
- chart.py +208 -0
- config/agents.yaml +53 -0
- config/tasks.yaml +69 -0
- crew.py +100 -0
- tools/__pycache__/company_profile_tool.cpython-312.pyc +0 -0
- tools/__pycache__/file_read.cpython-312.pyc +0 -0
- tools/__pycache__/news_tool.cpython-312.pyc +0 -0
- tools/__pycache__/retrieve_financial_data.cpython-312.pyc +0 -0
- tools/company_profile_tool.py +46 -0
- tools/news_tool.py +135 -0
- tools/retrieve_financial_data.py +30 -0
app.py
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import sys
|
| 3 |
+
import os
|
| 4 |
+
import traceback
|
| 5 |
+
from crew import PredictingStock
|
| 6 |
+
import json
|
| 7 |
+
import re
|
| 8 |
+
import tempfile
|
| 9 |
+
import subprocess
|
| 10 |
+
import pandas as pd
|
| 11 |
+
import numpy as np
|
| 12 |
+
import matplotlib.pyplot as plt
|
| 13 |
+
import plotly.graph_objects as go
|
| 14 |
+
import plotly.express as px
|
| 15 |
+
from plotly.subplots import make_subplots
|
| 16 |
+
from typing import Dict, Any
|
| 17 |
+
from datetime import datetime, timedelta
|
| 18 |
+
|
| 19 |
+
class StockDataProcessor:
|
| 20 |
+
"""Handles stock data processing and visualization"""
|
| 21 |
+
|
| 22 |
+
def __init__(self):
|
| 23 |
+
self.data = None
|
| 24 |
+
self.df = None
|
| 25 |
+
|
| 26 |
+
def load_json_data(self, json_file_path: str) -> bool:
|
| 27 |
+
"""Load stock data from JSON file"""
|
| 28 |
+
try:
|
| 29 |
+
with open(json_file_path, 'r') as f:
|
| 30 |
+
content = f.read()
|
| 31 |
+
|
| 32 |
+
# Handle double-encoded JSON if necessary
|
| 33 |
+
try:
|
| 34 |
+
self.data = json.loads(content)
|
| 35 |
+
if isinstance(self.data, str):
|
| 36 |
+
self.data = json.loads(self.data)
|
| 37 |
+
except:
|
| 38 |
+
self.data = json.loads(content)
|
| 39 |
+
|
| 40 |
+
# Convert to DataFrame
|
| 41 |
+
self.df = pd.DataFrame.from_dict(self.data, orient='index')
|
| 42 |
+
self.df.index = pd.to_datetime(self.df.index)
|
| 43 |
+
self.df = self.df.sort_index()
|
| 44 |
+
|
| 45 |
+
# Rename columns for easier access
|
| 46 |
+
self.df.columns = ['open', 'high', 'low', 'close', 'volume', 'dividends', 'stock_splits']
|
| 47 |
+
|
| 48 |
+
# Calculate technical indicators
|
| 49 |
+
self._calculate_technical_indicators()
|
| 50 |
+
|
| 51 |
+
return True
|
| 52 |
+
except Exception as e:
|
| 53 |
+
return False
|
| 54 |
+
|
| 55 |
+
def _calculate_technical_indicators(self):
|
| 56 |
+
"""Calculate technical indicators"""
|
| 57 |
+
# Daily returns
|
| 58 |
+
self.df['daily_return'] = self.df['close'].pct_change()
|
| 59 |
+
|
| 60 |
+
# Moving averages
|
| 61 |
+
self.df['ma_20'] = self.df['close'].rolling(window=20).mean()
|
| 62 |
+
self.df['ma_50'] = self.df['close'].rolling(window=50).mean()
|
| 63 |
+
|
| 64 |
+
# RSI
|
| 65 |
+
self.df['rsi'] = self._calculate_rsi(self.df['close'])
|
| 66 |
+
|
| 67 |
+
# Bollinger Bands
|
| 68 |
+
self.df['bb_middle'] = self.df['ma_20']
|
| 69 |
+
bb_std = self.df['close'].rolling(window=20).std()
|
| 70 |
+
self.df['bb_upper'] = self.df['bb_middle'] + (bb_std * 2)
|
| 71 |
+
self.df['bb_lower'] = self.df['bb_middle'] - (bb_std * 2)
|
| 72 |
+
|
| 73 |
+
# Volume moving average
|
| 74 |
+
self.df['volume_ma'] = self.df['volume'].rolling(window=20).mean()
|
| 75 |
+
|
| 76 |
+
def _calculate_rsi(self, prices, window=14):
|
| 77 |
+
"""Calculate RSI indicator"""
|
| 78 |
+
delta = prices.diff()
|
| 79 |
+
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
|
| 80 |
+
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
|
| 81 |
+
rs = gain / loss
|
| 82 |
+
return 100 - (100 / (1 + rs))
|
| 83 |
+
|
| 84 |
+
def create_interactive_chart(self, ticker: str):
|
| 85 |
+
"""Create interactive Plotly chart with only stock price and technical analysis"""
|
| 86 |
+
if self.df is None or self.df.empty:
|
| 87 |
+
return None
|
| 88 |
+
|
| 89 |
+
# Create a single subplot for the stock price with technical analysis
|
| 90 |
+
fig = make_subplots(
|
| 91 |
+
rows=1, cols=1,
|
| 92 |
+
shared_xaxes=True,
|
| 93 |
+
subplot_titles=(f'{ticker} - Stock Price with Technical Analysis',)
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
# Price chart with technical indicators
|
| 97 |
+
fig.add_trace(
|
| 98 |
+
go.Scatter(x=self.df.index, y=self.df['close'],
|
| 99 |
+
name='Close Price', line=dict(color='#2E86C1', width=2)),
|
| 100 |
+
row=1, col=1
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
fig.add_trace(
|
| 104 |
+
go.Scatter(x=self.df.index, y=self.df['ma_20'],
|
| 105 |
+
name='20-day MA', line=dict(color='orange', width=1)),
|
| 106 |
+
row=1, col=1
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
fig.add_trace(
|
| 110 |
+
go.Scatter(x=self.df.index, y=self.df['ma_50'],
|
| 111 |
+
name='50-day MA', line=dict(color='red', width=1)),
|
| 112 |
+
row=1, col=1
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Bollinger Bands
|
| 116 |
+
fig.add_trace(
|
| 117 |
+
go.Scatter(x=self.df.index, y=self.df['bb_upper'],
|
| 118 |
+
line=dict(color='gray', width=1, dash='dash'),
|
| 119 |
+
name='BB Upper', showlegend=False),
|
| 120 |
+
row=1, col=1
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
fig.add_trace(
|
| 124 |
+
go.Scatter(x=self.df.index, y=self.df['bb_lower'],
|
| 125 |
+
line=dict(color='gray', width=1, dash='dash'),
|
| 126 |
+
fill='tonexty', fillcolor='rgba(128,128,128,0.1)',
|
| 127 |
+
name='Bollinger Bands'),
|
| 128 |
+
row=1, col=1
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
# Update layout
|
| 132 |
+
fig.update_layout(
|
| 133 |
+
height=600,
|
| 134 |
+
title_text=f"Stock Price with Technical Analysis for {ticker}",
|
| 135 |
+
showlegend=True,
|
| 136 |
+
xaxis_rangeslider_visible=True
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
# Update y-axes labels
|
| 140 |
+
fig.update_yaxes(title_text="Price ($)", row=1, col=1)
|
| 141 |
+
fig.update_xaxes(title_text="Date", row=1, col=1)
|
| 142 |
+
|
| 143 |
+
return fig
|
| 144 |
+
|
| 145 |
+
def create_default_chart(self):
|
| 146 |
+
"""Create a default placeholder chart"""
|
| 147 |
+
fig = go.Figure()
|
| 148 |
+
fig.add_trace(go.Scatter(
|
| 149 |
+
x=[0, 1, 2, 3, 4],
|
| 150 |
+
y=[100, 110, 105, 115, 120],
|
| 151 |
+
mode='lines+markers',
|
| 152 |
+
name='Sample Stock Data',
|
| 153 |
+
line=dict(color='#2E86C1', width=2)
|
| 154 |
+
))
|
| 155 |
+
|
| 156 |
+
fig.update_layout(
|
| 157 |
+
title="📈 Example Stock Chart Preview (Note: This is not the actual graph.)",
|
| 158 |
+
xaxis_title="Time Period",
|
| 159 |
+
yaxis_title="Price ($)",
|
| 160 |
+
height=400,
|
| 161 |
+
template="plotly_white"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
return fig
|
| 165 |
+
|
| 166 |
+
def get_statistics(self, ticker: str) -> str:
|
| 167 |
+
"""Generate key statistics"""
|
| 168 |
+
if self.df is None or self.df.empty:
|
| 169 |
+
return "No data available"
|
| 170 |
+
|
| 171 |
+
try:
|
| 172 |
+
current_price = self.df['close'].iloc[-1]
|
| 173 |
+
start_price = self.df['close'].iloc[0]
|
| 174 |
+
|
| 175 |
+
stats = f"""
|
| 176 |
+
📊 **Key Statistics for {ticker}**
|
| 177 |
+
|
| 178 |
+
**Price Performance:**
|
| 179 |
+
• Current Price: ${current_price:.2f}
|
| 180 |
+
|
| 181 |
+
**Trading Activity:**
|
| 182 |
+
• Data Period: {self.df.index[0].strftime('%Y-%m-%d')} to {self.df.index[-1].strftime('%Y-%m-%d')}
|
| 183 |
+
• Total Trading Days: {len(self.df)}
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
return stats.strip()
|
| 187 |
+
|
| 188 |
+
except Exception as e:
|
| 189 |
+
return f"Error calculating statistics: {e}"
|
| 190 |
+
|
| 191 |
+
def get_default_statistics(self):
|
| 192 |
+
"""Get default statistics for initial display"""
|
| 193 |
+
return """
|
| 194 |
+
📊 **Stock Statistics Preview**
|
| 195 |
+
|
| 196 |
+
Welcome to AI Stock Analysis!
|
| 197 |
+
|
| 198 |
+
**What you'll get:**
|
| 199 |
+
• Real-time stock price data
|
| 200 |
+
• Technical analysis indicators
|
| 201 |
+
• AI-powered investment recommendations
|
| 202 |
+
• Risk assessment and insights
|
| 203 |
+
|
| 204 |
+
📈 **Enter a stock ticker below to begin analysis**
|
| 205 |
+
"""
|
| 206 |
+
|
| 207 |
+
class SecureAPIHandler:
|
| 208 |
+
"""Handles API keys securely without permanent storage"""
|
| 209 |
+
|
| 210 |
+
def __init__(self):
|
| 211 |
+
self.session_keys = {}
|
| 212 |
+
# Use Hugging Face Secrets for Finnhub API key
|
| 213 |
+
self.finnhub_available = bool(os.getenv("FINNHUB_API_KEY"))
|
| 214 |
+
|
| 215 |
+
def validate_anthropic_key(self, key_value: str) -> tuple[bool, str]:
|
| 216 |
+
"""Validate Anthropic API key format"""
|
| 217 |
+
if not key_value or not key_value.strip():
|
| 218 |
+
return False, "Anthropic API key is required"
|
| 219 |
+
|
| 220 |
+
key_value = key_value.strip()
|
| 221 |
+
|
| 222 |
+
if not key_value.startswith("sk-ant-"):
|
| 223 |
+
return False, "Anthropic API key should start with 'sk-ant-'"
|
| 224 |
+
if len(key_value) < 20:
|
| 225 |
+
return False, "Anthropic API key appears too short"
|
| 226 |
+
|
| 227 |
+
return True, "Valid"
|
| 228 |
+
|
| 229 |
+
def check_finnhub_env(self) -> tuple[bool, str]:
|
| 230 |
+
"""Check if Finnhub key is properly set in environment"""
|
| 231 |
+
finnhub_key = os.getenv("FINNHUB_API_KEY")
|
| 232 |
+
if not finnhub_key:
|
| 233 |
+
return False, "Finnhub API key not found in environment variables"
|
| 234 |
+
if len(finnhub_key.strip()) < 10:
|
| 235 |
+
return False, "Finnhub API key appears invalid"
|
| 236 |
+
return True, "Finnhub API key loaded from environment"
|
| 237 |
+
|
| 238 |
+
def set_anthropic_key(self, anthropic_key: str) -> tuple[bool, str]:
|
| 239 |
+
"""Securely set Anthropic API key for the session"""
|
| 240 |
+
try:
|
| 241 |
+
is_valid, message = self.validate_anthropic_key(anthropic_key)
|
| 242 |
+
if not is_valid:
|
| 243 |
+
return False, f"Validation Error: {message}"
|
| 244 |
+
|
| 245 |
+
finnhub_valid, finnhub_message = self.check_finnhub_env()
|
| 246 |
+
if not finnhub_valid:
|
| 247 |
+
return False, f"Environment Error: {finnhub_message}"
|
| 248 |
+
|
| 249 |
+
os.environ["ANTHROPIC_API_KEY"] = anthropic_key.strip()
|
| 250 |
+
self.session_keys["ANTHROPIC_API_KEY"] = "***" + anthropic_key[-4:]
|
| 251 |
+
self.session_keys["FINNHUB_API_KEY"] = "***" + os.getenv("FINNHUB_API_KEY")[-4:]
|
| 252 |
+
|
| 253 |
+
return True, "API keys validated and configured successfully"
|
| 254 |
+
|
| 255 |
+
except Exception as e:
|
| 256 |
+
return False, f"Error configuring API keys: {str(e)}"
|
| 257 |
+
|
| 258 |
+
def clear_session_keys(self):
|
| 259 |
+
"""Clear Anthropic API key from environment"""
|
| 260 |
+
if "ANTHROPIC_API_KEY" in os.environ:
|
| 261 |
+
del os.environ["ANTHROPIC_API_KEY"]
|
| 262 |
+
self.session_keys.clear()
|
| 263 |
+
|
| 264 |
+
# Global instances
|
| 265 |
+
api_handler = SecureAPIHandler()
|
| 266 |
+
stock_processor = StockDataProcessor()
|
| 267 |
+
|
| 268 |
+
def run_chart_generation(company_ticker: str) -> tuple[bool, str]:
|
| 269 |
+
"""Run chart.py to generate JSON data"""
|
| 270 |
+
try:
|
| 271 |
+
# Run chart.py script
|
| 272 |
+
result = subprocess.run([sys.executable, 'chart.py', company_ticker],
|
| 273 |
+
capture_output=True, text=True, timeout=60)
|
| 274 |
+
|
| 275 |
+
if result.returncode == 0:
|
| 276 |
+
return True, "Chart data generated successfully"
|
| 277 |
+
else:
|
| 278 |
+
return False, f"Chart generation failed: {result.stderr}"
|
| 279 |
+
|
| 280 |
+
except subprocess.TimeoutExpired:
|
| 281 |
+
return False, "Chart generation timed out"
|
| 282 |
+
except Exception as e:
|
| 283 |
+
return False, f"Error running chart.py: {str(e)}"
|
| 284 |
+
|
| 285 |
+
def load_stock_data_and_chart(ticker_symbol: str = "AAPL", progress=gr.Progress()):
|
| 286 |
+
"""First function: Load stock data and create chart/stats"""
|
| 287 |
+
try:
|
| 288 |
+
if progress:
|
| 289 |
+
progress(0.1, desc="Generating stock data...")
|
| 290 |
+
|
| 291 |
+
ticker = ticker_symbol.strip().upper()
|
| 292 |
+
|
| 293 |
+
# Step 1: Run chart.py to generate JSON data
|
| 294 |
+
chart_success, chart_message = run_chart_generation(ticker)
|
| 295 |
+
if not chart_success:
|
| 296 |
+
return f"❌ Chart Generation Error: {chart_message}", stock_processor.get_default_statistics(), stock_processor.create_default_chart()
|
| 297 |
+
|
| 298 |
+
if progress:
|
| 299 |
+
progress(0.5, desc="Loading and processing stock data...")
|
| 300 |
+
|
| 301 |
+
# Step 2: Load the generated JSON data
|
| 302 |
+
json_file_path = f"{ticker}_financial_data.json"
|
| 303 |
+
if not os.path.exists(json_file_path):
|
| 304 |
+
return f"❌ Error: JSON file {json_file_path} not found", stock_processor.get_default_statistics(), stock_processor.create_default_chart()
|
| 305 |
+
|
| 306 |
+
# Load stock data for visualization
|
| 307 |
+
if not stock_processor.load_json_data(json_file_path):
|
| 308 |
+
return "❌ Please enter company ticker", stock_processor.get_default_statistics(), stock_processor.create_default_chart()
|
| 309 |
+
|
| 310 |
+
if progress:
|
| 311 |
+
progress(1.0, desc="Creating visualization...")
|
| 312 |
+
|
| 313 |
+
# Step 3: Create visualization and stats
|
| 314 |
+
chart_fig = stock_processor.create_interactive_chart(ticker)
|
| 315 |
+
stats = stock_processor.get_statistics(ticker)
|
| 316 |
+
|
| 317 |
+
success_msg = f"✅ Successfully loaded data for {ticker}"
|
| 318 |
+
|
| 319 |
+
return success_msg, stats, chart_fig
|
| 320 |
+
|
| 321 |
+
except Exception as e:
|
| 322 |
+
error_msg = f"❌ Error loading stock data: {str(e)}"
|
| 323 |
+
return error_msg, stock_processor.get_default_statistics(), stock_processor.create_default_chart()
|
| 324 |
+
|
| 325 |
+
def load_company_data(ticker: str):
|
| 326 |
+
"""Load data for a specific company from navigation bar"""
|
| 327 |
+
# Load the stock data
|
| 328 |
+
status, stats, chart = load_stock_data_and_chart(ticker)
|
| 329 |
+
# Return the ticker value and the loaded data
|
| 330 |
+
return ticker, status, stats, chart
|
| 331 |
+
|
| 332 |
+
def run_ai_analysis(company_ticker: str, max_amount: str, anthropic_key: str, progress=gr.Progress()):
|
| 333 |
+
"""Second function: Run AI crew analysis only"""
|
| 334 |
+
try:
|
| 335 |
+
progress(0.05, desc="Validating inputs...")
|
| 336 |
+
|
| 337 |
+
# Validate inputs
|
| 338 |
+
if not company_ticker or not company_ticker.strip():
|
| 339 |
+
return "❌ Error: Please enter a company ticker symbol"
|
| 340 |
+
|
| 341 |
+
if not max_amount or not max_amount.strip():
|
| 342 |
+
return "❌ Error: Please enter a maximum investment amount"
|
| 343 |
+
|
| 344 |
+
try:
|
| 345 |
+
float(max_amount)
|
| 346 |
+
except ValueError:
|
| 347 |
+
return "❌ Error: Maximum investment amount must be a valid number"
|
| 348 |
+
|
| 349 |
+
if not anthropic_key or not anthropic_key.strip():
|
| 350 |
+
return "❌ Error: Anthropic API key is required"
|
| 351 |
+
|
| 352 |
+
ticker = company_ticker.strip().upper()
|
| 353 |
+
|
| 354 |
+
progress(0.2, desc="Configuring API keys...")
|
| 355 |
+
|
| 356 |
+
# Set API keys securely
|
| 357 |
+
success, message = api_handler.set_anthropic_key(anthropic_key)
|
| 358 |
+
if not success:
|
| 359 |
+
return f"❌ {message}"
|
| 360 |
+
|
| 361 |
+
progress(0.3, desc="Preparing AI analysis...")
|
| 362 |
+
|
| 363 |
+
# Prepare inputs for CrewAI
|
| 364 |
+
inputs = {
|
| 365 |
+
"company_name": ticker,
|
| 366 |
+
"max_amount": max_amount
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
progress(0.4, desc="Initializing AI agents...")
|
| 370 |
+
|
| 371 |
+
# Run CrewAI analysis
|
| 372 |
+
crew_instance = PredictingStock()
|
| 373 |
+
|
| 374 |
+
progress(0.6, desc="AI agents are running now... This may take 2-5 minutes")
|
| 375 |
+
|
| 376 |
+
result = crew_instance.crew().kickoff(inputs=inputs)
|
| 377 |
+
|
| 378 |
+
progress(1.0, desc="Analysis complete!")
|
| 379 |
+
|
| 380 |
+
# Format results
|
| 381 |
+
analysis_result = f"""
|
| 382 |
+
🎯 COMPREHENSIVE STOCK ANALYSIS FOR: {ticker}
|
| 383 |
+
{'='*60}
|
| 384 |
+
|
| 385 |
+
{result.raw}
|
| 386 |
+
|
| 387 |
+
""".strip()
|
| 388 |
+
|
| 389 |
+
return analysis_result
|
| 390 |
+
|
| 391 |
+
except Exception as e:
|
| 392 |
+
error_msg = f"""
|
| 393 |
+
❌ ERROR DURING AI ANALYSIS:
|
| 394 |
+
|
| 395 |
+
{str(e)}
|
| 396 |
+
|
| 397 |
+
TROUBLESHOOTING TIPS:
|
| 398 |
+
1. Verify your Anthropic API key is correct
|
| 399 |
+
2. Check that FINNHUB_API_KEY is set in Hugging Face Secrets
|
| 400 |
+
3. Check your internet connection
|
| 401 |
+
4. Ensure the ticker symbol is valid
|
| 402 |
+
5. Try again in a few moments
|
| 403 |
+
|
| 404 |
+
Full error details:
|
| 405 |
+
{traceback.format_exc()}
|
| 406 |
+
""".strip()
|
| 407 |
+
return error_msg
|
| 408 |
+
|
| 409 |
+
def create_secure_interface():
|
| 410 |
+
"""Create the enhanced Gradio interface with rearranged layout"""
|
| 411 |
+
css = """
|
| 412 |
+
.gradio-container {
|
| 413 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 414 |
+
}
|
| 415 |
+
.header {
|
| 416 |
+
text-align: center;
|
| 417 |
+
padding: 20px;
|
| 418 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 419 |
+
color: white;
|
| 420 |
+
border-radius: 10px;
|
| 421 |
+
margin-bottom: 30px;
|
| 422 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 423 |
+
}
|
| 424 |
+
.nav-bar {
|
| 425 |
+
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
| 426 |
+
padding: 15px;
|
| 427 |
+
border-radius: 10px;
|
| 428 |
+
margin-bottom: 20px;
|
| 429 |
+
text-align: center;
|
| 430 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 431 |
+
}
|
| 432 |
+
.nav-button {
|
| 433 |
+
margin: 5px !important;
|
| 434 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
| 435 |
+
border: none !important;
|
| 436 |
+
color: white !important;
|
| 437 |
+
font-weight: bold !important;
|
| 438 |
+
border-radius: 8px !important;
|
| 439 |
+
padding: 10px 20px !important;
|
| 440 |
+
transition: all 0.3s ease !important;
|
| 441 |
+
}
|
| 442 |
+
.nav-button:hover {
|
| 443 |
+
transform: translateY(-2px) !important;
|
| 444 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important;
|
| 445 |
+
}
|
| 446 |
+
.stats-box {
|
| 447 |
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
| 448 |
+
color: white;
|
| 449 |
+
padding: 15px;
|
| 450 |
+
border-radius: 10px;
|
| 451 |
+
margin: 10px 0;
|
| 452 |
+
}
|
| 453 |
+
.secure-input {
|
| 454 |
+
border: 2px solid #28a745 !important;
|
| 455 |
+
}
|
| 456 |
+
.section-header {
|
| 457 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 458 |
+
color: white;
|
| 459 |
+
padding: 10px;
|
| 460 |
+
border-radius: 8px;
|
| 461 |
+
margin: 15px 0 10px 0;
|
| 462 |
+
text-align: center;
|
| 463 |
+
}
|
| 464 |
+
"""
|
| 465 |
+
|
| 466 |
+
with gr.Blocks(css=css, title="AI Stock Prediction with Visualization") as interface:
|
| 467 |
+
# Header
|
| 468 |
+
gr.HTML("""
|
| 469 |
+
<div class="header">
|
| 470 |
+
<h1>🚀 AI Stock Prediction & Visualization System</h1>
|
| 471 |
+
<p>Complete stock analysis with interactive charts and AI-powered insights</p>
|
| 472 |
+
</div>
|
| 473 |
+
""")
|
| 474 |
+
|
| 475 |
+
# NAVIGATION BAR
|
| 476 |
+
gr.HTML('<div class="nav-bar"><h3>🏢 Quick Access - Popular Companies</h3></div>')
|
| 477 |
+
|
| 478 |
+
with gr.Row():
|
| 479 |
+
apple_btn = gr.Button("🍎 Apple (AAPL)", elem_classes=["nav-button"], scale=1)
|
| 480 |
+
google_btn = gr.Button("🔍 Google (GOOGL)", elem_classes=["nav-button"], scale=1)
|
| 481 |
+
microsoft_btn = gr.Button("💻 Microsoft (MSFT)", elem_classes=["nav-button"], scale=1)
|
| 482 |
+
tesla_btn = gr.Button("⚡ Tesla (TSLA)", elem_classes=["nav-button"], scale=1)
|
| 483 |
+
amazon_btn = gr.Button("📦 Amazon (AMZN)", elem_classes=["nav-button"], scale=1)
|
| 484 |
+
|
| 485 |
+
# CUSTOM COMPANY SECTION
|
| 486 |
+
gr.HTML('<div class="nav-bar"><h4>✨ Add Any Company</h4></div>')
|
| 487 |
+
|
| 488 |
+
with gr.Row():
|
| 489 |
+
with gr.Column(scale=3):
|
| 490 |
+
custom_ticker_input = gr.Textbox(
|
| 491 |
+
label="Enter Any Stock Ticker",
|
| 492 |
+
placeholder="e.g., NVDA, META, NFLX, etc.",
|
| 493 |
+
value="",
|
| 494 |
+
info="Type any valid stock ticker symbol",
|
| 495 |
+
elem_classes=["secure-input"]
|
| 496 |
+
)
|
| 497 |
+
with gr.Column(scale=1):
|
| 498 |
+
custom_load_btn = gr.Button(
|
| 499 |
+
"🚀 Load Custom Stock",
|
| 500 |
+
elem_classes=["nav-button"],
|
| 501 |
+
variant="primary",
|
| 502 |
+
size="lg"
|
| 503 |
+
)
|
| 504 |
+
|
| 505 |
+
# TOP SECTION: Statistics and Chart (Displayed from start)
|
| 506 |
+
gr.HTML('<div class="section-header"><h2>📊 Live Statistics & Interactive Chart</h2></div>')
|
| 507 |
+
|
| 508 |
+
# Load default stock data (AAPL) on startup
|
| 509 |
+
default_message, default_stats, default_chart = load_stock_data_and_chart("AAPL")
|
| 510 |
+
|
| 511 |
+
with gr.Row():
|
| 512 |
+
with gr.Column(scale=1):
|
| 513 |
+
# Statistics box (shown from start with real stock data)
|
| 514 |
+
stats_output = gr.Markdown(
|
| 515 |
+
value=default_stats,
|
| 516 |
+
elem_classes=["stats-box"]
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
with gr.Column(scale=2):
|
| 520 |
+
# Interactive chart (shown from start with real stock data)
|
| 521 |
+
chart_output = gr.Plot(
|
| 522 |
+
value=default_chart,
|
| 523 |
+
label="Stock Price Chart",
|
| 524 |
+
show_label=True
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
# Status message for stock data loading
|
| 528 |
+
stock_status = gr.Markdown(value=default_message)
|
| 529 |
+
|
| 530 |
+
# MIDDLE SECTION: Input Configuration
|
| 531 |
+
gr.HTML('<div class="section-header"><h2>⚙️ Analysis Configuration</h2></div>')
|
| 532 |
+
|
| 533 |
+
with gr.Row():
|
| 534 |
+
with gr.Column(scale=1):
|
| 535 |
+
company_ticker = gr.Textbox(
|
| 536 |
+
label="Company Ticker Symbol",
|
| 537 |
+
placeholder="Here the selected company ticker will be shown",
|
| 538 |
+
value="",
|
| 539 |
+
info="Stock ticker symbol to analyze",
|
| 540 |
+
elem_classes=["secure-input"],
|
| 541 |
+
interactive=False
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
with gr.Column(scale=1):
|
| 545 |
+
max_amount = gr.Textbox(
|
| 546 |
+
label="Maximum Investment Amount (USD)",
|
| 547 |
+
placeholder="e.g., 1000, 5000.50",
|
| 548 |
+
value="",
|
| 549 |
+
info="The maximum amount you are willing to invest",
|
| 550 |
+
elem_classes=["secure-input"]
|
| 551 |
+
)
|
| 552 |
+
|
| 553 |
+
with gr.Column(scale=1):
|
| 554 |
+
anthropic_key = gr.Textbox(
|
| 555 |
+
label="Anthropic API Key (Claude)",
|
| 556 |
+
placeholder="sk-ant-api03-...",
|
| 557 |
+
type="password",
|
| 558 |
+
info="Get from console.anthropic.com - Required for AI analysis",
|
| 559 |
+
elem_classes=["secure-input"]
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
with gr.Row():
|
| 563 |
+
with gr.Column():
|
| 564 |
+
load_data_btn = gr.Button(
|
| 565 |
+
"📊 Load Stock Data",
|
| 566 |
+
variant="secondary",
|
| 567 |
+
size="lg",
|
| 568 |
+
scale=1
|
| 569 |
+
)
|
| 570 |
+
with gr.Column():
|
| 571 |
+
analyze_btn = gr.Button(
|
| 572 |
+
"🤖 Run AI Analysis",
|
| 573 |
+
variant="primary",
|
| 574 |
+
size="lg",
|
| 575 |
+
scale=2
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
# BOTTOM SECTION: AI Analysis Results
|
| 579 |
+
gr.HTML('<div class="section-header"><h2>🤖 AI Agent Analysis & Recommendations</h2></div>')
|
| 580 |
+
|
| 581 |
+
result_output = gr.TextArea(
|
| 582 |
+
label="AI Analysis Report",
|
| 583 |
+
lines=25,
|
| 584 |
+
max_lines=35,
|
| 585 |
+
interactive=False,
|
| 586 |
+
show_copy_button=True,
|
| 587 |
+
placeholder="🤖 AI analysis results and investment recommendations will appear here after running the analysis...\n\nThe AI agents will provide:\n• Comprehensive stock analysis\n• Risk assessment\n• Investment recommendations\n• Market insights\n• Technical analysis summary"
|
| 588 |
+
)
|
| 589 |
+
|
| 590 |
+
# Event handlers
|
| 591 |
+
|
| 592 |
+
# Navigation bar buttons
|
| 593 |
+
apple_btn.click(
|
| 594 |
+
fn=lambda: load_company_data("AAPL"),
|
| 595 |
+
outputs=[company_ticker, stock_status, stats_output, chart_output]
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
google_btn.click(
|
| 599 |
+
fn=lambda: load_company_data("GOOGL"),
|
| 600 |
+
outputs=[company_ticker, stock_status, stats_output, chart_output]
|
| 601 |
+
)
|
| 602 |
+
|
| 603 |
+
microsoft_btn.click(
|
| 604 |
+
fn=lambda: load_company_data("MSFT"),
|
| 605 |
+
outputs=[company_ticker, stock_status, stats_output, chart_output]
|
| 606 |
+
)
|
| 607 |
+
|
| 608 |
+
tesla_btn.click(
|
| 609 |
+
fn=lambda: load_company_data("TSLA"),
|
| 610 |
+
outputs=[company_ticker, stock_status, stats_output, chart_output]
|
| 611 |
+
)
|
| 612 |
+
|
| 613 |
+
amazon_btn.click(
|
| 614 |
+
fn=lambda: load_company_data("AMZN"),
|
| 615 |
+
outputs=[company_ticker, stock_status, stats_output, chart_output]
|
| 616 |
+
)
|
| 617 |
+
|
| 618 |
+
# Custom ticker loading functionality
|
| 619 |
+
custom_load_btn.click(
|
| 620 |
+
fn=lambda ticker: load_company_data(ticker),
|
| 621 |
+
inputs=[custom_ticker_input],
|
| 622 |
+
outputs=[company_ticker, stock_status, stats_output, chart_output]
|
| 623 |
+
)
|
| 624 |
+
|
| 625 |
+
# Load stock data when load button is clicked
|
| 626 |
+
load_data_btn.click(
|
| 627 |
+
fn=load_stock_data_and_chart,
|
| 628 |
+
inputs=[company_ticker],
|
| 629 |
+
outputs=[stock_status, stats_output, chart_output]
|
| 630 |
+
)
|
| 631 |
+
|
| 632 |
+
# Run AI analysis when button is clicked
|
| 633 |
+
analyze_btn.click(
|
| 634 |
+
fn=run_ai_analysis,
|
| 635 |
+
inputs=[company_ticker, max_amount, anthropic_key],
|
| 636 |
+
outputs=[result_output]
|
| 637 |
+
)
|
| 638 |
+
|
| 639 |
+
# Footer
|
| 640 |
+
gr.HTML("""
|
| 641 |
+
<div style="text-align: center; padding: 20px; margin-top: 30px; border-top: 1px solid #dee2e6;">
|
| 642 |
+
<p>🔐 <strong>Secure AI Stock Analysis with Interactive Visualization</strong></p>
|
| 643 |
+
<p style="font-size: 12px; color: #6c757d;">
|
| 644 |
+
Features: Real-time data • Technical indicators • AI analysis • Interactive charts
|
| 645 |
+
</p>
|
| 646 |
+
</div>
|
| 647 |
+
""")
|
| 648 |
+
|
| 649 |
+
return interface
|
| 650 |
+
|
| 651 |
+
# Launch the application
|
| 652 |
+
if __name__ == "__main__":
|
| 653 |
+
interface = create_secure_interface()
|
| 654 |
+
# Modified for Hugging Face Spaces deployment
|
| 655 |
+
interface.launch(
|
| 656 |
+
share=False,
|
| 657 |
+
debug=False,
|
| 658 |
+
show_error=True
|
| 659 |
+
)
|
chart.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yfinance as yf
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
import json
|
| 4 |
+
import sys
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
import requests
|
| 8 |
+
from requests.adapters import HTTPAdapter
|
| 9 |
+
from urllib3.util.retry import Retry
|
| 10 |
+
|
| 11 |
+
def setup_session_with_retries():
|
| 12 |
+
"""Setup a requests session with retry logic for better reliability"""
|
| 13 |
+
session = requests.Session()
|
| 14 |
+
|
| 15 |
+
retry_strategy = Retry(
|
| 16 |
+
total=3,
|
| 17 |
+
backoff_factor=1,
|
| 18 |
+
status_forcelist=[429, 500, 502, 503, 504],
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
| 22 |
+
session.mount("http://", adapter)
|
| 23 |
+
session.mount("https://", adapter)
|
| 24 |
+
|
| 25 |
+
return session
|
| 26 |
+
|
| 27 |
+
def retrieve_financial_data(company_name: str) -> str:
|
| 28 |
+
"""
|
| 29 |
+
It returns the last 6 months of daily stock Open, High, Low, Close, Volume, Dividends, Stock Splits for the specified company.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
company_name: str -> the ticker symbol of the company (e.g., 'AAPL', 'GOOGL')
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
str: JSON string containing the financial data or error message
|
| 36 |
+
"""
|
| 37 |
+
try:
|
| 38 |
+
# Validate ticker symbol
|
| 39 |
+
if not company_name or not company_name.strip():
|
| 40 |
+
return "Error: Empty ticker symbol provided"
|
| 41 |
+
|
| 42 |
+
company_name = company_name.strip().upper()
|
| 43 |
+
|
| 44 |
+
# Calculate dates for last 6 months
|
| 45 |
+
end_date = datetime.now()
|
| 46 |
+
start_date = end_date - timedelta(days=180) # Approximately 6 months
|
| 47 |
+
|
| 48 |
+
# Setup session with retries for better reliability
|
| 49 |
+
session = setup_session_with_retries()
|
| 50 |
+
|
| 51 |
+
# Get the data with custom session
|
| 52 |
+
ticker = yf.Ticker(company_name, session=session)
|
| 53 |
+
|
| 54 |
+
# Add a small delay to be respectful to the API
|
| 55 |
+
time.sleep(0.1)
|
| 56 |
+
|
| 57 |
+
# Fetch historical data
|
| 58 |
+
hist = ticker.history(
|
| 59 |
+
start=start_date.strftime("%Y-%m-%d"),
|
| 60 |
+
end=end_date.strftime("%Y-%m-%d"),
|
| 61 |
+
auto_adjust=True,
|
| 62 |
+
prepost=True,
|
| 63 |
+
threads=True
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
if hist.empty:
|
| 67 |
+
return f"Error: No data found for ticker symbol: {company_name}. Please verify the ticker symbol is correct."
|
| 68 |
+
|
| 69 |
+
# Validate that we have sufficient data
|
| 70 |
+
if len(hist) < 10:
|
| 71 |
+
return f"Error: Insufficient data found for {company_name}. Only {len(hist)} days of data available."
|
| 72 |
+
|
| 73 |
+
# Clean the data - remove any NaN values and ensure proper formatting
|
| 74 |
+
hist = hist.dropna()
|
| 75 |
+
|
| 76 |
+
if hist.empty:
|
| 77 |
+
return f"Error: No valid data remaining for {company_name} after cleaning."
|
| 78 |
+
|
| 79 |
+
# Convert to JSON and return
|
| 80 |
+
json_data = hist.to_json(date_format='iso', orient='index')
|
| 81 |
+
|
| 82 |
+
# Validate JSON was created successfully
|
| 83 |
+
try:
|
| 84 |
+
json.loads(json_data) # Test if it's valid JSON
|
| 85 |
+
except json.JSONDecodeError:
|
| 86 |
+
return f"Error: Failed to convert data to JSON format for {company_name}"
|
| 87 |
+
|
| 88 |
+
return json_data
|
| 89 |
+
|
| 90 |
+
except requests.exceptions.RequestException as e:
|
| 91 |
+
return f"Error: Network issue while retrieving data for {company_name}: {str(e)}"
|
| 92 |
+
except yf.exceptions.YFinanceException as e:
|
| 93 |
+
return f"Error: Yahoo Finance API issue for {company_name}: {str(e)}"
|
| 94 |
+
except Exception as e:
|
| 95 |
+
return f"Error: Unexpected error retrieving data for {company_name}: {str(e)}"
|
| 96 |
+
|
| 97 |
+
def save_financial_data(company_name: str, data: str) -> bool:
|
| 98 |
+
"""
|
| 99 |
+
Save financial data to a JSON file with error handling
|
| 100 |
+
|
| 101 |
+
Args:
|
| 102 |
+
company_name: str -> the ticker symbol
|
| 103 |
+
data: str -> JSON string to save
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
bool: True if successful, False otherwise
|
| 107 |
+
"""
|
| 108 |
+
try:
|
| 109 |
+
filename = f"{company_name}_financial_data.json"
|
| 110 |
+
|
| 111 |
+
# Check if data is an error message
|
| 112 |
+
if data.startswith("Error:"):
|
| 113 |
+
print(f"❌ Data retrieval failed: {data}")
|
| 114 |
+
return False
|
| 115 |
+
|
| 116 |
+
# Validate JSON before saving
|
| 117 |
+
try:
|
| 118 |
+
json.loads(data)
|
| 119 |
+
except json.JSONDecodeError:
|
| 120 |
+
print(f"❌ Invalid JSON data for {company_name}")
|
| 121 |
+
return False
|
| 122 |
+
|
| 123 |
+
# Ensure directory exists (important for cloud environments)
|
| 124 |
+
os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True)
|
| 125 |
+
|
| 126 |
+
# Save to file
|
| 127 |
+
with open(filename, "w", encoding='utf-8') as file:
|
| 128 |
+
json.dump(data, file, ensure_ascii=False, indent=2)
|
| 129 |
+
|
| 130 |
+
# Verify file was created and has content
|
| 131 |
+
if os.path.exists(filename) and os.path.getsize(filename) > 0:
|
| 132 |
+
print(f"✅ Successfully saved data for {company_name} to {filename}")
|
| 133 |
+
return True
|
| 134 |
+
else:
|
| 135 |
+
print(f"❌ Failed to save data for {company_name} - file not created or empty")
|
| 136 |
+
return False
|
| 137 |
+
|
| 138 |
+
except PermissionError:
|
| 139 |
+
print(f"❌ Permission denied: Cannot write to file for {company_name}")
|
| 140 |
+
return False
|
| 141 |
+
except OSError as e:
|
| 142 |
+
print(f"❌ OS Error saving data for {company_name}: {str(e)}")
|
| 143 |
+
return False
|
| 144 |
+
except Exception as e:
|
| 145 |
+
print(f"❌ Unexpected error saving data for {company_name}: {str(e)}")
|
| 146 |
+
return False
|
| 147 |
+
|
| 148 |
+
def validate_ticker_symbol(symbol: str) -> tuple[bool, str]:
|
| 149 |
+
"""
|
| 150 |
+
Basic validation of ticker symbol format
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
symbol: str -> ticker symbol to validate
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
tuple: (is_valid, message)
|
| 157 |
+
"""
|
| 158 |
+
if not symbol or not symbol.strip():
|
| 159 |
+
return False, "Ticker symbol cannot be empty"
|
| 160 |
+
|
| 161 |
+
symbol = symbol.strip().upper()
|
| 162 |
+
|
| 163 |
+
# Basic format validation
|
| 164 |
+
if len(symbol) < 1 or len(symbol) > 10:
|
| 165 |
+
return False, "Ticker symbol must be 1-10 characters long"
|
| 166 |
+
|
| 167 |
+
if not symbol.isalnum():
|
| 168 |
+
return False, "Ticker symbol must contain only letters and numbers"
|
| 169 |
+
|
| 170 |
+
return True, "Valid ticker symbol"
|
| 171 |
+
|
| 172 |
+
if __name__ == "__main__":
|
| 173 |
+
try:
|
| 174 |
+
# Check command line arguments
|
| 175 |
+
if len(sys.argv) != 2:
|
| 176 |
+
print("❌ Usage: python chart.py <TICKER_SYMBOL>")
|
| 177 |
+
print("Example: python chart.py AAPL")
|
| 178 |
+
sys.exit(1)
|
| 179 |
+
|
| 180 |
+
symbol = sys.argv[1].strip().upper()
|
| 181 |
+
|
| 182 |
+
# Validate ticker symbol
|
| 183 |
+
is_valid, validation_message = validate_ticker_symbol(symbol)
|
| 184 |
+
if not is_valid:
|
| 185 |
+
print(f"❌ Invalid ticker symbol: {validation_message}")
|
| 186 |
+
sys.exit(1)
|
| 187 |
+
|
| 188 |
+
print(f"📊 Retrieving financial data for {symbol}...")
|
| 189 |
+
|
| 190 |
+
# Retrieve financial data
|
| 191 |
+
financial_data = retrieve_financial_data(symbol)
|
| 192 |
+
|
| 193 |
+
# Save the data
|
| 194 |
+
success = save_financial_data(symbol, financial_data)
|
| 195 |
+
|
| 196 |
+
if success:
|
| 197 |
+
print(f"🎯 Operation completed successfully for {symbol}")
|
| 198 |
+
sys.exit(0)
|
| 199 |
+
else:
|
| 200 |
+
print(f"❌ Failed to complete operation for {symbol}")
|
| 201 |
+
sys.exit(1)
|
| 202 |
+
|
| 203 |
+
except KeyboardInterrupt:
|
| 204 |
+
print("\n❌ Operation cancelled by user")
|
| 205 |
+
sys.exit(1)
|
| 206 |
+
except Exception as e:
|
| 207 |
+
print(f"❌ Unexpected error: {e}")
|
| 208 |
+
sys.exit(1)
|
config/agents.yaml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# config/agents.yaml
|
| 2 |
+
stock_information_specialist:
|
| 3 |
+
role: >
|
| 4 |
+
Stock Information Specialist
|
| 5 |
+
goal: >
|
| 6 |
+
To provide accurate and timely stock information using the tool by gathering comprehensive historical stock Open, High, Low, Close, Volume, Dividends, Stock Splits for a specified company.
|
| 7 |
+
backstory: >
|
| 8 |
+
You are a meticulous financial data analyst with an unparalleled ability to extract,
|
| 9 |
+
process, and interpret historical stock market information. Your expertise lies
|
| 10 |
+
in identifying trends, patterns, and anomalies within vast datasets of stock prices,
|
| 11 |
+
volume, and financial statements. You are crucial for providing the foundational
|
| 12 |
+
numerical context for predicting future stock movements.
|
| 13 |
+
|
| 14 |
+
company_profile_specialist:
|
| 15 |
+
role: >
|
| 16 |
+
Company Profile Analyst
|
| 17 |
+
goal: >
|
| 18 |
+
To gather and analyze key company information including market capitalization,
|
| 19 |
+
industry classification, exchange listing, geographic location, and corporate
|
| 20 |
+
fundamentals to assess the company's market position and characteristics.
|
| 21 |
+
backstory: >
|
| 22 |
+
You are an experienced company research analyst with deep expertise in analyzing
|
| 23 |
+
corporate fundamentals and market positioning. Your strength lies in interpreting
|
| 24 |
+
company profiles, market capitalizations, industry classifications, and geographic
|
| 25 |
+
factors that influence stock performance. You excel at understanding how company
|
| 26 |
+
size, listing exchange, industry sector, and regional factors impact investment potential.
|
| 27 |
+
|
| 28 |
+
news_analyst:
|
| 29 |
+
role: >
|
| 30 |
+
News and Sentiment Analyst
|
| 31 |
+
goal: >
|
| 32 |
+
To gather, analyze, and interpret the latest news, market sentiment, and external
|
| 33 |
+
factors that could impact the stock's short-term and long-term performance.
|
| 34 |
+
backstory: >
|
| 35 |
+
You are a seasoned market news analyst with an exceptional ability to quickly
|
| 36 |
+
process and interpret financial news, earnings reports, regulatory changes, and
|
| 37 |
+
market sentiment. Your expertise lies in identifying how external events and
|
| 38 |
+
market psychology influence stock prices, providing crucial insights into timing
|
| 39 |
+
and market dynamics.
|
| 40 |
+
|
| 41 |
+
investment_advisor:
|
| 42 |
+
role: >
|
| 43 |
+
Senior Investment Advisor
|
| 44 |
+
goal: >
|
| 45 |
+
To synthesize all available information from technical analysis, fundamental
|
| 46 |
+
analysis, and market sentiment to make a well-informed investment recommendation along with how many to invest from {max_amount}
|
| 47 |
+
with clear reasoning and risk assessment.
|
| 48 |
+
backstory: >
|
| 49 |
+
You are a seasoned investment advisor with over 15 years of experience in making
|
| 50 |
+
investment decisions. Your expertise lies in combining technical analysis,
|
| 51 |
+
fundamental analysis, and market sentiment to provide comprehensive investment
|
| 52 |
+
recommendations. You have a proven track record of successful stock picks and
|
| 53 |
+
risk management, always considering both potential returns and downside risks.
|
config/tasks.yaml
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
generate_stock_information:
|
| 2 |
+
description: >
|
| 3 |
+
You are a 'Stock Information Specialist' and fetch the last 6 months of daily
|
| 4 |
+
stock prices using the retrieve_financial_data tool for the company {company_name}.
|
| 5 |
+
Analyze the data for Open, High, Low, Close, Volume, Dividends, Stock Splits.
|
| 6 |
+
Ensure the data is clean and ready for analysis. Provide insights on trends,
|
| 7 |
+
volatility, and any notable patterns in the stock performance.
|
| 8 |
+
expected_output: >
|
| 9 |
+
A comprehensive analysis of the stock data including:
|
| 10 |
+
1. Raw historical data for the last 6 months
|
| 11 |
+
2. Key statistics (average price, volatility, volume trends)
|
| 12 |
+
3. Notable patterns or anomalies in the data
|
| 13 |
+
4. Technical indicators and trends
|
| 14 |
+
5. Summary of stock performance trends
|
| 15 |
+
|
| 16 |
+
analyze_company_profile:
|
| 17 |
+
description: >
|
| 18 |
+
You are a 'Company Profile Analyst' and use the get_company_profile tool to gather
|
| 19 |
+
key company information for {company_name}. Analyze the company's market
|
| 20 |
+
capitalization, industry classification, exchange listing, geographic headquarters,
|
| 21 |
+
shares outstanding, IPO history, and other corporate fundamentals. Focus on how
|
| 22 |
+
these factors influence the company's market position and investment attractiveness.
|
| 23 |
+
expected_output: >
|
| 24 |
+
A detailed company profile analysis including:
|
| 25 |
+
1. Company overview (name, ticker, headquarters country)
|
| 26 |
+
2. Market metrics (market cap, shares outstanding, currency)
|
| 27 |
+
3. Exchange and listing information
|
| 28 |
+
4. Industry classification and sector analysis
|
| 29 |
+
5. IPO date and company maturity assessment
|
| 30 |
+
6. Geographic and regulatory environment considerations
|
| 31 |
+
7. Company size classification (large-cap, mid-cap, etc.)
|
| 32 |
+
8. Overall corporate profile assessment for investment purposes
|
| 33 |
+
gather_news_sentiment:
|
| 34 |
+
description: >
|
| 35 |
+
You are a 'News and Sentiment Analyst' and use the get_stock_news tool to gather
|
| 36 |
+
comprehensive company news for {company_name} from the last 30 days. Analyze
|
| 37 |
+
the news headlines, summaries, categories, sources, related companies, and publication
|
| 38 |
+
frequency to determine market sentiment and identify factors impacting stock performance.
|
| 39 |
+
Pay special attention to news categories, source credibility, and any mentions of
|
| 40 |
+
related companies that might affect the target stock.
|
| 41 |
+
expected_output: >
|
| 42 |
+
A comprehensive news and sentiment analysis including:
|
| 43 |
+
1. Executive summary of news sentiment (bullish/bearish/neutral)
|
| 44 |
+
2. Analysis of news frequency and intensity (high/medium/low news coverage)
|
| 45 |
+
3. Breakdown of news categories and their sentiment implications
|
| 46 |
+
4. Assessment of source credibility and news quality
|
| 47 |
+
5. Identification of key catalysts, risks, or market-moving events
|
| 48 |
+
6. Analysis of related companies/stocks mentioned and their potential impact
|
| 49 |
+
7. Overall sentiment score (1-10 scale) with directional bias
|
| 50 |
+
8. Key headlines that could significantly impact stock price
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
make_investment_decision:
|
| 54 |
+
description: >
|
| 55 |
+
You are a 'Senior Investment Advisor'. Based on the comprehensive analysis provided
|
| 56 |
+
by the Stock Information Specialist, Company Profile Analyst, and News Analyst,
|
| 57 |
+
make a final investment recommendation for {company_name} and how much to invest from {max_amount}. Consider technical
|
| 58 |
+
analysis, fundamental analysis, and market sentiment to determine whether to BUY,
|
| 59 |
+
HOLD, or SELL the stock and if BUY then how much to invest from {max_amount}. Provide clear reasoning, risk assessment, and confidence level.
|
| 60 |
+
expected_output: >
|
| 61 |
+
A final investment recommendation report including:
|
| 62 |
+
1. Clear investment decision: BUY/HOLD/SELL
|
| 63 |
+
2. Recommended investment amount from {max_amount} if BUY
|
| 64 |
+
3. Detailed reasoning combining all three analyses
|
| 65 |
+
4. Risk assessment and potential downside
|
| 66 |
+
5. Expected timeline and price targets
|
| 67 |
+
6. Confidence level (1-10 scale), Bar chart information
|
| 68 |
+
7. Alternative scenarios consideration
|
| 69 |
+
8. Executive summary for quick decision making
|
crew.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# crew.py
|
| 3 |
+
from crewai import Agent, Crew, Process, Task, LLM
|
| 4 |
+
from crewai.project import CrewBase, agent, crew, task
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
from tools.retrieve_financial_data import retrieve_financial_data
|
| 9 |
+
from tools.company_profile_tool import get_company_profile
|
| 10 |
+
from tools.news_tool import get_stock_news
|
| 11 |
+
|
| 12 |
+
# Load environment variables
|
| 13 |
+
load_dotenv()
|
| 14 |
+
|
| 15 |
+
@CrewBase
|
| 16 |
+
class PredictingStock():
|
| 17 |
+
"""PredictingStock crew"""
|
| 18 |
+
|
| 19 |
+
agents_config = 'config/agents.yaml'
|
| 20 |
+
tasks_config = 'config/tasks.yaml'
|
| 21 |
+
|
| 22 |
+
def __init__(self):
|
| 23 |
+
self.llm = LLM(
|
| 24 |
+
model="claude-3-haiku-20240307",
|
| 25 |
+
api_key=os.getenv("ANTHROPIC_API_KEY")
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
@agent
|
| 29 |
+
def stock_information_specialist(self) -> Agent:
|
| 30 |
+
return Agent(
|
| 31 |
+
config=self.agents_config['stock_information_specialist'],
|
| 32 |
+
llm=self.llm,
|
| 33 |
+
tools=[retrieve_financial_data],
|
| 34 |
+
verbose=True
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
@agent
|
| 38 |
+
def company_profile_specialist(self) -> Agent:
|
| 39 |
+
return Agent(
|
| 40 |
+
config=self.agents_config['company_profile_specialist'],
|
| 41 |
+
llm=self.llm,
|
| 42 |
+
tools=[get_company_profile],
|
| 43 |
+
verbose=True
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
@agent
|
| 47 |
+
def news_analyst(self) -> Agent:
|
| 48 |
+
return Agent(
|
| 49 |
+
config=self.agents_config['news_analyst'],
|
| 50 |
+
llm=self.llm,
|
| 51 |
+
tools=[get_stock_news],
|
| 52 |
+
verbose=True
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
@agent
|
| 56 |
+
def investment_advisor(self) -> Agent:
|
| 57 |
+
return Agent(
|
| 58 |
+
config=self.agents_config['investment_advisor'],
|
| 59 |
+
llm=self.llm,
|
| 60 |
+
verbose=True
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
@task
|
| 64 |
+
def generate_stock_information(self) -> Task:
|
| 65 |
+
return Task(
|
| 66 |
+
config=self.tasks_config['generate_stock_information'],
|
| 67 |
+
agent=self.stock_information_specialist()
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
@task
|
| 71 |
+
def analyze_company_profile(self) -> Task:
|
| 72 |
+
return Task(
|
| 73 |
+
config=self.tasks_config['analyze_company_profile'],
|
| 74 |
+
agent=self.company_profile_specialist()
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
@task
|
| 78 |
+
def gather_news_sentiment(self) -> Task:
|
| 79 |
+
return Task(
|
| 80 |
+
config=self.tasks_config['gather_news_sentiment'],
|
| 81 |
+
agent=self.news_analyst()
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
@task
|
| 85 |
+
def make_investment_decision(self) -> Task:
|
| 86 |
+
return Task(
|
| 87 |
+
config=self.tasks_config['make_investment_decision'],
|
| 88 |
+
agent=self.investment_advisor(),
|
| 89 |
+
context=[self.generate_stock_information(), self.analyze_company_profile(), self.gather_news_sentiment()]
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
@crew
|
| 93 |
+
def crew(self) -> Crew:
|
| 94 |
+
"""Creates the PredictingStock crew"""
|
| 95 |
+
return Crew(
|
| 96 |
+
agents=self.agents, # Automatically created by the @agent decorator
|
| 97 |
+
tasks=self.tasks, # Automatically created by the @task decorator
|
| 98 |
+
process=Process.sequential,
|
| 99 |
+
verbose=True,
|
| 100 |
+
)
|
tools/__pycache__/company_profile_tool.cpython-312.pyc
ADDED
|
Binary file (2.5 kB). View file
|
|
|
tools/__pycache__/file_read.cpython-312.pyc
ADDED
|
Binary file (761 Bytes). View file
|
|
|
tools/__pycache__/news_tool.cpython-312.pyc
ADDED
|
Binary file (4.71 kB). View file
|
|
|
tools/__pycache__/retrieve_financial_data.cpython-312.pyc
ADDED
|
Binary file (1.59 kB). View file
|
|
|
tools/company_profile_tool.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai.tools import tool
|
| 2 |
+
import finnhub
|
| 3 |
+
import os
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
@tool("get_company_profile")
|
| 7 |
+
def get_company_profile(company_symbol: str) -> str:
|
| 8 |
+
"""
|
| 9 |
+
Retrieves company profile information from Finnhub including country, currency,
|
| 10 |
+
exchange, industry, IPO date, market cap, name, phone, shares outstanding,
|
| 11 |
+
ticker, and website.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
company_symbol: str -> the ticker symbol of the company (e.g., 'AAPL', 'GOOGL')
|
| 15 |
+
"""
|
| 16 |
+
try:
|
| 17 |
+
# Initialize Finnhub client
|
| 18 |
+
finnhub_client = finnhub.Client(api_key=os.getenv("FINNHUB_API_KEY"))
|
| 19 |
+
|
| 20 |
+
# Get company profile
|
| 21 |
+
profile = finnhub_client.company_profile2(symbol=company_symbol)
|
| 22 |
+
|
| 23 |
+
if not profile:
|
| 24 |
+
return f"No company profile found for symbol: {company_symbol}"
|
| 25 |
+
|
| 26 |
+
# Format the profile data for better readability
|
| 27 |
+
formatted_profile = {
|
| 28 |
+
"Company Name": profile.get("name", "N/A"),
|
| 29 |
+
"Ticker Symbol": profile.get("ticker", "N/A"),
|
| 30 |
+
"Country": profile.get("country", "N/A"),
|
| 31 |
+
"Currency": profile.get("currency", "N/A"),
|
| 32 |
+
"Exchange": profile.get("exchange", "N/A"),
|
| 33 |
+
"Industry": profile.get("finnhubIndustry", "N/A"),
|
| 34 |
+
"IPO Date": profile.get("ipo", "N/A"),
|
| 35 |
+
"Market Capitalization": f"${profile.get('marketCapitalization', 0):,}" if profile.get('marketCapitalization') else "N/A",
|
| 36 |
+
"Shares Outstanding": f"{profile.get('shareOutstanding', 0):,}" if profile.get('shareOutstanding') else "N/A",
|
| 37 |
+
"Phone": profile.get("phone", "N/A"),
|
| 38 |
+
"Website": profile.get("weburl", "N/A"),
|
| 39 |
+
"Logo": profile.get("logo", "N/A")
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
# Convert to JSON string for better formatting
|
| 43 |
+
return json.dumps(formatted_profile, indent=2)
|
| 44 |
+
|
| 45 |
+
except Exception as e:
|
| 46 |
+
return f"Error retrieving company profile for {company_symbol}: {str(e)}"
|
tools/news_tool.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# from crewai.tools import tool
|
| 2 |
+
# from langchain_community.tools.yahoo_finance_news import YahooFinanceNewsTool
|
| 3 |
+
# import json
|
| 4 |
+
|
| 5 |
+
# @tool("get_stock_news")
|
| 6 |
+
# def get_stock_news(company_ticker: str) -> str:
|
| 7 |
+
# """
|
| 8 |
+
# Retrieves the latest news and market sentiment for a given stock ticker
|
| 9 |
+
# from Yahoo Finance News.
|
| 10 |
+
|
| 11 |
+
# Args:
|
| 12 |
+
# company_ticker: str -> the ticker symbol of the company (e.g., 'AAPL', 'GOOGL')
|
| 13 |
+
# """
|
| 14 |
+
# try:
|
| 15 |
+
# # Initialize Yahoo Finance News tool
|
| 16 |
+
# yahoo_news_tool = YahooFinanceNewsTool()
|
| 17 |
+
|
| 18 |
+
# # Get news for the company
|
| 19 |
+
# news_result = yahoo_news_tool.run(company_ticker)
|
| 20 |
+
|
| 21 |
+
# if not news_result:
|
| 22 |
+
# return f"No news found for ticker: {company_ticker}"
|
| 23 |
+
|
| 24 |
+
# # If the result is already a string, return it
|
| 25 |
+
# if isinstance(news_result, str):
|
| 26 |
+
# return news_result
|
| 27 |
+
|
| 28 |
+
# # If it's a dict or list, format it as JSON
|
| 29 |
+
# return json.dumps(news_result, indent=2, ensure_ascii=False)
|
| 30 |
+
|
| 31 |
+
# except Exception as e:
|
| 32 |
+
# return f"Error retrieving news for {company_ticker}: {str(e)}"
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
from crewai.tools import tool
|
| 36 |
+
import finnhub
|
| 37 |
+
import os
|
| 38 |
+
import json
|
| 39 |
+
from datetime import datetime, timedelta
|
| 40 |
+
|
| 41 |
+
@tool("get_stock_news")
|
| 42 |
+
def get_stock_news(company_ticker: str) -> str:
|
| 43 |
+
"""
|
| 44 |
+
Retrieves the latest company news for a given stock ticker from Finnhub.
|
| 45 |
+
Gets news from the last 30 days to capture recent developments and market sentiment.
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
company_ticker: str -> the ticker symbol of the company (e.g., 'AAPL', 'GOOGL')
|
| 49 |
+
"""
|
| 50 |
+
try:
|
| 51 |
+
# Initialize Finnhub client
|
| 52 |
+
finnhub_client = finnhub.Client(api_key=os.getenv("FINNHUB_API_KEY"))
|
| 53 |
+
|
| 54 |
+
# Calculate date range (last 30 days)
|
| 55 |
+
end_date = datetime.now()
|
| 56 |
+
start_date = end_date - timedelta(days=30)
|
| 57 |
+
|
| 58 |
+
# Format dates as required by Finnhub API
|
| 59 |
+
from_date = start_date.strftime("%Y-%m-%d")
|
| 60 |
+
to_date = end_date.strftime("%Y-%m-%d")
|
| 61 |
+
|
| 62 |
+
# Get company news
|
| 63 |
+
news_data = finnhub_client.company_news(
|
| 64 |
+
symbol=company_ticker,
|
| 65 |
+
_from=from_date,
|
| 66 |
+
to=to_date
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
if not news_data:
|
| 70 |
+
return f"No recent news found for ticker: {company_ticker} in the last 30 days"
|
| 71 |
+
|
| 72 |
+
# Format the news data using all available Finnhub fields
|
| 73 |
+
formatted_news = []
|
| 74 |
+
for article in news_data[:20]: # Analyze top 15 most recent articles
|
| 75 |
+
formatted_article = {
|
| 76 |
+
"id": article.get("id", "unknown"),
|
| 77 |
+
"headline": article.get("headline", "No headline available"),
|
| 78 |
+
"summary": article.get("summary", "No summary available"),
|
| 79 |
+
"category": article.get("category", "General"),
|
| 80 |
+
"source": article.get("source", "Unknown source"),
|
| 81 |
+
"url": article.get("url", ""),
|
| 82 |
+
"image": article.get("image", ""),
|
| 83 |
+
"datetime": datetime.fromtimestamp(article.get("datetime", 0)).strftime("%Y-%m-%d %H:%M:%S") if article.get("datetime") else "Unknown date",
|
| 84 |
+
"unix_timestamp": article.get("datetime", 0),
|
| 85 |
+
"related_stocks": article.get("related", []) if article.get("related") else []
|
| 86 |
+
}
|
| 87 |
+
formatted_news.append(formatted_article)
|
| 88 |
+
|
| 89 |
+
# Sort by datetime (most recent first)
|
| 90 |
+
formatted_news.sort(key=lambda x: x["unix_timestamp"], reverse=True)
|
| 91 |
+
|
| 92 |
+
# Analyze news categories and sources
|
| 93 |
+
categories = {}
|
| 94 |
+
sources = {}
|
| 95 |
+
related_companies = set()
|
| 96 |
+
|
| 97 |
+
for article in formatted_news:
|
| 98 |
+
# Count categories
|
| 99 |
+
category = article["category"]
|
| 100 |
+
categories[category] = categories.get(category, 0) + 1
|
| 101 |
+
|
| 102 |
+
# Count sources
|
| 103 |
+
source = article["source"]
|
| 104 |
+
sources[source] = sources.get(source, 0) + 1
|
| 105 |
+
|
| 106 |
+
# Collect related companies/stocks
|
| 107 |
+
if article["related_stocks"]:
|
| 108 |
+
related_companies.update(article["related_stocks"])
|
| 109 |
+
|
| 110 |
+
# Create comprehensive news analysis
|
| 111 |
+
news_analysis = {
|
| 112 |
+
"company_ticker": company_ticker,
|
| 113 |
+
"analysis_period": {
|
| 114 |
+
"from_date": from_date,
|
| 115 |
+
"to_date": to_date,
|
| 116 |
+
"days_analyzed": 30
|
| 117 |
+
},
|
| 118 |
+
"news_metrics": {
|
| 119 |
+
"total_articles_found": len(news_data),
|
| 120 |
+
"articles_analyzed": len(formatted_news),
|
| 121 |
+
"average_articles_per_day": round(len(news_data) / 30, 2)
|
| 122 |
+
},
|
| 123 |
+
"news_breakdown": {
|
| 124 |
+
"categories": dict(sorted(categories.items(), key=lambda x: x[1], reverse=True)),
|
| 125 |
+
"top_sources": dict(sorted(sources.items(), key=lambda x: x[1], reverse=True)[:5]),
|
| 126 |
+
"related_companies_mentioned": list(related_companies)[:10] if related_companies else []
|
| 127 |
+
},
|
| 128 |
+
"recent_articles": formatted_news
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
return json.dumps(news_analysis, indent=2, ensure_ascii=False)
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
return f"Error retrieving news for {company_ticker}: {str(e)}\nPlease ensure FINNHUB_API_KEY is properly set in .env file"
|
| 135 |
+
|
tools/retrieve_financial_data.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from crewai.tools import tool
|
| 2 |
+
import yfinance as yf
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
|
| 5 |
+
@tool("retrieve_financial_data")
|
| 6 |
+
def retrieve_financial_data(company_name: str) -> str:
|
| 7 |
+
"""
|
| 8 |
+
It returns the last 6 months of daily stock Open, High, Low, Close, Volume, Dividends, Stock Splits for the specified company.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
company_name: str -> the ticker symbol of the company (e.g., 'AAPL', 'GOOGL')
|
| 12 |
+
"""
|
| 13 |
+
try:
|
| 14 |
+
# Calculate dates for last 6 months
|
| 15 |
+
end_date = datetime.now()
|
| 16 |
+
start_date = end_date - timedelta(days=180) # Approximately 6 months
|
| 17 |
+
|
| 18 |
+
# Get the data
|
| 19 |
+
ticker = yf.Ticker(company_name)
|
| 20 |
+
hist = ticker.history(start=start_date.strftime("%Y-%m-%d"),
|
| 21 |
+
end=end_date.strftime("%Y-%m-%d"))
|
| 22 |
+
|
| 23 |
+
if hist.empty:
|
| 24 |
+
return f"No data found for ticker symbol: {company_name}"
|
| 25 |
+
|
| 26 |
+
# Convert to JSON and return
|
| 27 |
+
return hist.to_json(date_format='iso', orient='index')
|
| 28 |
+
|
| 29 |
+
except Exception as e:
|
| 30 |
+
return f"Error retrieving data for {company_name}: {str(e)}"
|