Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Install required packages
|
2 |
+
# !pip install gradio yfinance beautifulsoup4 requests pandas numpy transformers xgboost scikit-learn python-dotenv
|
3 |
+
# !pip install python-decouple
|
4 |
+
import gradio as gr
|
5 |
+
import yfinance as yf
|
6 |
+
import requests
|
7 |
+
from bs4 import BeautifulSoup
|
8 |
+
import pandas as pd
|
9 |
+
import numpy as np
|
10 |
+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
|
11 |
+
import xgboost as xgb
|
12 |
+
from datetime import datetime, timedelta
|
13 |
+
import json
|
14 |
+
import warnings
|
15 |
+
import os
|
16 |
+
from dotenv import load_dotenv
|
17 |
+
from decouple import config
|
18 |
+
import logging
|
19 |
+
|
20 |
+
|
21 |
+
warnings.filterwarnings('ignore')
|
22 |
+
load_dotenv()
|
23 |
+
|
24 |
+
# Configure logging
|
25 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
26 |
+
|
27 |
+
# WhatsApp API Configuration
|
28 |
+
WHATSAPP_API_BASE_URL = os.getenv('WHATSAPP_API_BASE_URL')
|
29 |
+
WHATSAPP_API_KEY = os.getenv('WHATSAPP_API_KEY')
|
30 |
+
WHATSAPP_INSTANCE_NAME = os.getenv('WHATSAPP_INSTANCE_NAME')
|
31 |
+
|
32 |
+
class ConfigManager:
|
33 |
+
"""
|
34 |
+
Centralized configuration management
|
35 |
+
"""
|
36 |
+
@staticmethod
|
37 |
+
def get_api_config():
|
38 |
+
"""
|
39 |
+
Retrieve API configurations securely
|
40 |
+
"""
|
41 |
+
return {
|
42 |
+
'amfi_base_url': config('AMFI_API_URL', default='https://api.mfapi.in/mf'),
|
43 |
+
}
|
44 |
+
|
45 |
+
class WhatsAppManager:
|
46 |
+
def __init__(self, base_url, api_key):
|
47 |
+
self.base_url = base_url
|
48 |
+
self.api_key = api_key
|
49 |
+
|
50 |
+
def send_message(self, instance, phone, message):
|
51 |
+
if not self.base_url or not self.api_key or not instance:
|
52 |
+
logging.error("WhatsApp API base URL, key or instance not configured.")
|
53 |
+
return "WhatsApp API base URL, key or instance not configured."
|
54 |
+
|
55 |
+
headers = {
|
56 |
+
'Content-Type': 'application/json',
|
57 |
+
'Authorization': self.api_key
|
58 |
+
}
|
59 |
+
payload = json.dumps({
|
60 |
+
"phone": phone,
|
61 |
+
"message": message
|
62 |
+
})
|
63 |
+
try:
|
64 |
+
response = requests.post(f"{self.base_url}/message/sendText/{instance}", headers=headers, data=payload)
|
65 |
+
response.raise_for_status()
|
66 |
+
return response.json()
|
67 |
+
except requests.exceptions.RequestException as e:
|
68 |
+
logging.error(f"Error sending WhatsApp message: {str(e)}")
|
69 |
+
return f"Error sending message: {str(e)}"
|
70 |
+
|
71 |
+
|
72 |
+
class AMFIApi:
|
73 |
+
"""
|
74 |
+
Mutual Fund API Handler with real-time data fetching
|
75 |
+
"""
|
76 |
+
@staticmethod
|
77 |
+
def get_all_mutual_funds():
|
78 |
+
"""
|
79 |
+
Retrieve comprehensive mutual funds list from AMFI API
|
80 |
+
"""
|
81 |
+
config = ConfigManager.get_api_config()
|
82 |
+
try:
|
83 |
+
response = requests.get(config['amfi_base_url'])
|
84 |
+
if response.status_code == 200:
|
85 |
+
return response.json()
|
86 |
+
else:
|
87 |
+
logging.error("Failed to fetch mutual fund data.")
|
88 |
+
return "Error fetching mutual funds."
|
89 |
+
except Exception as e:
|
90 |
+
logging.error(f"API request error: {str(e)}")
|
91 |
+
return f"Error fetching mutual funds: {str(e)}"
|
92 |
+
|
93 |
+
@staticmethod
|
94 |
+
def analyze_mutual_fund(scheme_code):
|
95 |
+
"""
|
96 |
+
Fetch real-time mutual fund NAV and analyze returns.
|
97 |
+
"""
|
98 |
+
try:
|
99 |
+
config = ConfigManager.get_api_config()
|
100 |
+
amfi_url = f"{config['amfi_base_url']}/{scheme_code}"
|
101 |
+
response = requests.get(amfi_url)
|
102 |
+
|
103 |
+
if response.status_code != 200:
|
104 |
+
logging.error("Failed to fetch NAV data.")
|
105 |
+
return None, None, "Failed to fetch live NAV data."
|
106 |
+
|
107 |
+
fund_data = response.json()
|
108 |
+
if 'data' not in fund_data:
|
109 |
+
logging.error("Invalid fund data received.")
|
110 |
+
return None, None, "Invalid fund data received."
|
111 |
+
|
112 |
+
nav_data = pd.DataFrame(fund_data['data'])
|
113 |
+
|
114 |
+
# Data type conversions
|
115 |
+
nav_data['date'] = pd.to_datetime(nav_data['date'], format='%d-%m-%Y')
|
116 |
+
nav_data['nav'] = pd.to_numeric(nav_data['nav'], errors='coerce')
|
117 |
+
|
118 |
+
# Sort by date
|
119 |
+
nav_data = nav_data.sort_values('date')
|
120 |
+
|
121 |
+
# Calculate returns
|
122 |
+
latest_nav = nav_data.iloc[-1]['nav']
|
123 |
+
first_nav = nav_data.iloc[0]['nav']
|
124 |
+
|
125 |
+
returns = {
|
126 |
+
'scheme_name': fund_data.get('meta', {}).get('scheme_name', 'Unknown'),
|
127 |
+
'current_nav': latest_nav,
|
128 |
+
'initial_nav': first_nav,
|
129 |
+
'total_return': ((latest_nav - first_nav) / first_nav) * 100
|
130 |
+
}
|
131 |
+
|
132 |
+
return returns, nav_data[['date', 'nav']].rename(columns={'nav': 'NAV'}), None
|
133 |
+
|
134 |
+
except Exception as e:
|
135 |
+
logging.error(f"Analysis error: {str(e)}")
|
136 |
+
return None, None, f"Analysis error: {str(e)}"
|
137 |
+
|
138 |
+
|
139 |
+
def get_stock_data(symbol, period='3y'):
|
140 |
+
try:
|
141 |
+
logging.info(f"Fetching stock data for symbol: {symbol}")
|
142 |
+
|
143 |
+
stock = yf.Ticker(symbol)
|
144 |
+
logging.info(f"Ticker object created successfully for symbol: {symbol}")
|
145 |
+
hist = stock.history(period=period)
|
146 |
+
if hist.empty:
|
147 |
+
logging.error(f"No stock data available for symbol: {symbol} after fetching history.")
|
148 |
+
return f"No stock data available for symbol: {symbol}"
|
149 |
+
logging.info(f"Successfully fetched stock data for symbol: {symbol}")
|
150 |
+
return hist
|
151 |
+
except Exception as e:
|
152 |
+
logging.error(f"Error fetching stock data for symbol: {symbol}, error: {str(e)}")
|
153 |
+
return f"Error fetching stock data: {str(e)}"
|
154 |
+
|
155 |
+
def calculate_rsi(data, periods=14):
|
156 |
+
delta = data.diff()
|
157 |
+
gain = (delta.where(delta > 0, 0)).rolling(window=periods).mean()
|
158 |
+
loss = (-delta.where(delta < 0, 0)).rolling(window=periods).mean()
|
159 |
+
rs = gain / loss
|
160 |
+
return 100 - (100 / (1 + rs))
|
161 |
+
|
162 |
+
def predict_stock(symbol):
|
163 |
+
df = get_stock_data(symbol)
|
164 |
+
if isinstance(df, str):
|
165 |
+
return df
|
166 |
+
|
167 |
+
logging.info(f"Dataframe before feature creation for {symbol}: \n{df.head()}")
|
168 |
+
|
169 |
+
df['SMA_20'] = df['Close'].rolling(window=20).mean()
|
170 |
+
df['SMA_50'] = df['Close'].rolling(window=50).mean()
|
171 |
+
df['RSI'] = calculate_rsi(df['Close'])
|
172 |
+
|
173 |
+
features = ['SMA_20', 'SMA_50', 'RSI', 'Volume']
|
174 |
+
X = df[features].dropna()
|
175 |
+
y = df['Close'].shift(-1).dropna()
|
176 |
+
|
177 |
+
logging.info(f"Dataframe after feature creation: \nX:\n{X.head()}\ny:\n{y.head()}")
|
178 |
+
|
179 |
+
#Align X and Y
|
180 |
+
X = X.iloc[:len(y)]
|
181 |
+
|
182 |
+
split = int(len(X) * 0.8)
|
183 |
+
X_train, X_test = X[:split], X[split:]
|
184 |
+
y_train, y_test = y[:split], y[split:]
|
185 |
+
|
186 |
+
|
187 |
+
logging.info(f"Data split details: \nTrain Data size: {len(X_train)}\nTest Data Size: {len(X_test)}")
|
188 |
+
|
189 |
+
if len(X_train) == 0 or len(y_train) == 0:
|
190 |
+
logging.error(f"Insufficient training data for prediction for symbol: {symbol}")
|
191 |
+
return "Insufficient data for prediction."
|
192 |
+
|
193 |
+
model = xgb.XGBRegressor(objective='reg:squarederror', n_estimators=100)
|
194 |
+
model.fit(X_train, y_train)
|
195 |
+
|
196 |
+
if not X_test.empty:
|
197 |
+
last_data = X_test.iloc[-1:]
|
198 |
+
prediction = model.predict(last_data)[0]
|
199 |
+
return prediction
|
200 |
+
else:
|
201 |
+
logging.warning(f"No test data available for prediction for symbol: {symbol}")
|
202 |
+
return "No test data available for prediction."
|
203 |
+
|
204 |
+
def analyze_sentiment(text):
|
205 |
+
tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
|
206 |
+
model = AutoModelForSequenceClassification.from_pretrained("ProsusAI/finbert")
|
207 |
+
|
208 |
+
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)
|
209 |
+
outputs = model(**inputs)
|
210 |
+
predictions = outputs.logits.softmax(dim=-1)
|
211 |
+
|
212 |
+
labels = ['negative', 'neutral', 'positive']
|
213 |
+
return {label: float(pred) for label, pred in zip(labels, predictions[0])}
|
214 |
+
|
215 |
+
def setup_notifications(phone, stock, mf, sentiment):
|
216 |
+
if not WHATSAPP_API_BASE_URL or not WHATSAPP_API_KEY or not WHATSAPP_INSTANCE_NAME:
|
217 |
+
return "WhatsApp API credentials or instance missing."
|
218 |
+
|
219 |
+
whatsapp_manager = WhatsAppManager(WHATSAPP_API_BASE_URL, WHATSAPP_API_KEY)
|
220 |
+
|
221 |
+
try:
|
222 |
+
result = whatsapp_manager.send_message(
|
223 |
+
WHATSAPP_INSTANCE_NAME,
|
224 |
+
phone,
|
225 |
+
"🎉 Welcome to AI Finance Manager!\nYour WhatsApp notifications have been set up successfully."
|
226 |
+
)
|
227 |
+
|
228 |
+
alerts = []
|
229 |
+
if stock: alerts.append("Stock")
|
230 |
+
if mf: alerts.append("Mutual Fund")
|
231 |
+
if sentiment: alerts.append("Sentiment")
|
232 |
+
|
233 |
+
return f"WhatsApp notifications set up for: {', '.join(alerts)} - {result}"
|
234 |
+
except Exception as e:
|
235 |
+
logging.error(f"Error setting up notifications: {str(e)}")
|
236 |
+
return f"Error setting up notifications: {str(e)}"
|
237 |
+
|
238 |
+
# Chatbot Function
|
239 |
+
def chatbot_response(user_input):
|
240 |
+
user_input = user_input.lower()
|
241 |
+
|
242 |
+
if "stock" in user_input:
|
243 |
+
parts = user_input.split()
|
244 |
+
if len(parts) > 1:
|
245 |
+
symbol = parts[-1].upper()
|
246 |
+
prediction = predict_stock(symbol)
|
247 |
+
if isinstance(prediction,str):
|
248 |
+
return prediction
|
249 |
+
else:
|
250 |
+
return f"The predicted next-day closing price for {symbol} is {prediction:.2f}"
|
251 |
+
else:
|
252 |
+
return "Please provide a stock symbol."
|
253 |
+
|
254 |
+
|
255 |
+
elif "mutual fund" in user_input:
|
256 |
+
parts = user_input.split()
|
257 |
+
if len(parts) > 2 and parts[1] == "code":
|
258 |
+
scheme_code = parts[-1]
|
259 |
+
mf_returns, mf_nav_history, error = AMFIApi.analyze_mutual_fund(scheme_code)
|
260 |
+
if error:
|
261 |
+
return error
|
262 |
+
else:
|
263 |
+
return f"Mutual Fund Analysis:\nName: {mf_returns.get('scheme_name', 'Unknown')}\nCurrent NAV: {mf_returns.get('current_nav', 'N/A'):.2f}\nTotal Return: {mf_returns.get('total_return', 'N/A'):.2f}%"
|
264 |
+
else:
|
265 |
+
return "Please enter the mutual fund scheme code for analysis (e.g. 'analyze mutual fund code 123456')."
|
266 |
+
elif "sentiment" in user_input:
|
267 |
+
return "Enter the financial news text for sentiment analysis."
|
268 |
+
elif user_input.startswith("analyze sentiment"):
|
269 |
+
text = user_input[len("analyze sentiment"):].strip()
|
270 |
+
if text:
|
271 |
+
sentiment_result = analyze_sentiment(text)
|
272 |
+
if sentiment_result:
|
273 |
+
return f"Sentiment Analysis: {sentiment_result}"
|
274 |
+
else:
|
275 |
+
return "No text provided for sentiment analysis."
|
276 |
+
else:
|
277 |
+
return "Please provide text for sentiment analysis."
|
278 |
+
return "I can help with Stock Analysis, Mutual Funds, and Sentiment Analysis. Please ask your query."
|
279 |
+
|
280 |
+
|
281 |
+
# Create Gradio Interface
|
282 |
+
def create_gradio_interface():
|
283 |
+
with gr.Blocks() as app:
|
284 |
+
gr.Markdown("# AI Finance & Stock Manager with Chat and WhatsApp Alerts")
|
285 |
+
|
286 |
+
with gr.Tab("Chat"):
|
287 |
+
chat_input = gr.Textbox(label="Ask about Stocks, Mutual Funds, or Sentiment Analysis")
|
288 |
+
chat_output = gr.Textbox(label="AI Response", interactive=False)
|
289 |
+
chat_btn = gr.Button("Ask AI")
|
290 |
+
|
291 |
+
with gr.Tab("Stock Analysis"):
|
292 |
+
stock_input = gr.Textbox(label="Enter Stock Symbol (e.g., AAPL)")
|
293 |
+
stock_btn = gr.Button("Analyze Stock")
|
294 |
+
stock_output = gr.DataFrame()
|
295 |
+
prediction_output = gr.Number(label="Predicted Next Day Close Price")
|
296 |
+
|
297 |
+
with gr.Tab("Mutual Fund Analysis"):
|
298 |
+
mf_code = gr.Textbox(label="Enter Scheme Code")
|
299 |
+
mf_analyze_btn = gr.Button("Analyze Fund")
|
300 |
+
|
301 |
+
# Analysis Outputs
|
302 |
+
mf_returns = gr.JSON(label="Fund Returns")
|
303 |
+
mf_nav_history = gr.DataFrame(label="NAV History")
|
304 |
+
mf_analysis_error = gr.Textbox(label="Error Messages", visible=False)
|
305 |
+
|
306 |
+
with gr.Tab("WhatsApp Notifications"):
|
307 |
+
phone_input = gr.Textbox(label="WhatsApp Number (with country code)")
|
308 |
+
enable_stock_alerts = gr.Checkbox(label="Stock Alerts")
|
309 |
+
enable_mf_alerts = gr.Checkbox(label="Mutual Fund Alerts")
|
310 |
+
enable_sentiment_alerts = gr.Checkbox(label="Sentiment Alerts")
|
311 |
+
notification_status = gr.Textbox(label="Notification Status", interactive=False)
|
312 |
+
setup_btn = gr.Button("Setup WhatsApp Notifications")
|
313 |
+
|
314 |
+
with gr.Tab("Sentiment Analysis"):
|
315 |
+
text_input = gr.Textbox(label="Enter financial news or text")
|
316 |
+
sentiment_btn = gr.Button("Analyze Sentiment")
|
317 |
+
sentiment_output = gr.Label()
|
318 |
+
# Event Handlers
|
319 |
+
chat_btn.click(
|
320 |
+
fn=chatbot_response,
|
321 |
+
inputs=chat_input,
|
322 |
+
outputs=chat_output
|
323 |
+
)
|
324 |
+
|
325 |
+
stock_btn.click(
|
326 |
+
fn=lambda x: (get_stock_data(x), predict_stock(x)),
|
327 |
+
inputs=stock_input,
|
328 |
+
outputs=[stock_output, prediction_output]
|
329 |
+
)
|
330 |
+
mf_analyze_btn.click(
|
331 |
+
fn=AMFIApi.analyze_mutual_fund,
|
332 |
+
inputs=mf_code,
|
333 |
+
outputs=[mf_returns,mf_nav_history,mf_analysis_error]
|
334 |
+
)
|
335 |
+
|
336 |
+
sentiment_btn.click(
|
337 |
+
fn=analyze_sentiment,
|
338 |
+
inputs=text_input,
|
339 |
+
outputs=sentiment_output
|
340 |
+
)
|
341 |
+
setup_btn.click(
|
342 |
+
fn=setup_notifications,
|
343 |
+
inputs=[phone_input, enable_stock_alerts, enable_mf_alerts, enable_sentiment_alerts],
|
344 |
+
outputs=notification_status
|
345 |
+
)
|
346 |
+
return app
|
347 |
+
|
348 |
+
|
349 |
+
# Launch the app
|
350 |
+
if __name__ == "__main__":
|
351 |
+
app = create_gradio_interface()
|
352 |
+
app.launch(share=True, debug=True)
|