koyelia commited on
Commit
73f46f6
·
verified ·
1 Parent(s): 17e2981

Upload app.py.py

Browse files
Files changed (1) hide show
  1. app.py.py +1230 -0
app.py.py ADDED
@@ -0,0 +1,1230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Real MCP with Gradio Agent - Stock Analysis Platform
4
+ Comprehensive implementation with MCP server and Gradio interface
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import logging
10
+ import os
11
+ from datetime import datetime, timedelta
12
+ from typing import Dict, List, Any, Optional
13
+ import traceback
14
+
15
+ # MCP and async imports
16
+ from mcp.server import Server
17
+ from mcp.server.models import InitializationOptions
18
+ from mcp.server.stdio import stdio_server
19
+ from mcp.types import Tool, TextContent
20
+ import mcp.types as types
21
+
22
+ # Data analysis imports
23
+ import yfinance as yf
24
+ import pandas as pd
25
+ import numpy as np
26
+ from dataclasses import dataclass
27
+
28
+ # Gradio for web interface
29
+ import gradio as gr
30
+ import plotly.graph_objects as go
31
+ import plotly.express as px
32
+ from plotly.subplots import make_subplots
33
+
34
+ # Setup logging
35
+ logging.basicConfig(level=logging.INFO)
36
+ logger = logging.getLogger(__name__)
37
+
38
+ @dataclass
39
+ class StockAnalysis:
40
+ """Data class for stock analysis results"""
41
+ symbol: str
42
+ company_name: str
43
+ current_price: float
44
+ ytd_return: float
45
+ volatility: float
46
+ investment_score: int
47
+ recommendation: str
48
+ risk_level: str
49
+ sector: str
50
+ market_cap: int
51
+
52
+ class StockAnalyzer:
53
+ """Advanced stock analysis engine"""
54
+
55
+ def __init__(self):
56
+ self.cache = {}
57
+ self.cache_timeout = 300 # 5 minutes
58
+
59
+ def get_stock_data(self, symbol: str, period: str = "1y") -> Optional[pd.DataFrame]:
60
+ """Get stock data with caching"""
61
+ cache_key = f"{symbol}_{period}"
62
+ current_time = datetime.now()
63
+
64
+ if cache_key in self.cache:
65
+ data, timestamp = self.cache[cache_key]
66
+ if (current_time - timestamp).seconds < self.cache_timeout:
67
+ return data
68
+
69
+ try:
70
+ stock = yf.Ticker(symbol)
71
+ data = stock.history(period=period)
72
+ self.cache[cache_key] = (data, current_time)
73
+ return data
74
+ except Exception as e:
75
+ logger.error(f"Error fetching data for {symbol}: {e}")
76
+ return None
77
+
78
+ def calculate_technical_indicators(self, data: pd.DataFrame) -> Dict:
79
+ """Calculate technical indicators"""
80
+ if data.empty:
81
+ return {}
82
+
83
+ # Moving averages
84
+ data['MA20'] = data['Close'].rolling(window=20).mean()
85
+ data['MA50'] = data['Close'].rolling(window=50).mean()
86
+ data['MA200'] = data['Close'].rolling(window=200).mean()
87
+
88
+ # RSI
89
+ delta = data['Close'].diff()
90
+ gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
91
+ loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
92
+ rs = gain / loss
93
+ data['RSI'] = 100 - (100 / (1 + rs))
94
+
95
+ # Bollinger Bands
96
+ data['BB_Middle'] = data['Close'].rolling(window=20).mean()
97
+ bb_std = data['Close'].rolling(window=20).std()
98
+ data['BB_Upper'] = data['BB_Middle'] + (bb_std * 2)
99
+ data['BB_Lower'] = data['BB_Middle'] - (bb_std * 2)
100
+
101
+ # MACD
102
+ exp1 = data['Close'].ewm(span=12).mean()
103
+ exp2 = data['Close'].ewm(span=26).mean()
104
+ data['MACD'] = exp1 - exp2
105
+ data['MACD_Signal'] = data['MACD'].ewm(span=9).mean()
106
+
107
+ return {
108
+ 'rsi': data['RSI'].iloc[-1] if not data['RSI'].empty else 0,
109
+ 'macd': data['MACD'].iloc[-1] if not data['MACD'].empty else 0,
110
+ 'macd_signal': data['MACD_Signal'].iloc[-1] if not data['MACD_Signal'].empty else 0,
111
+ 'ma20': data['MA20'].iloc[-1] if not data['MA20'].empty else 0,
112
+ 'ma50': data['MA50'].iloc[-1] if not data['MA50'].empty else 0,
113
+ 'current_price': data['Close'].iloc[-1] if not data['Close'].empty else 0
114
+ }
115
+
116
+ def calculate_investment_score(self, symbol: str) -> Dict:
117
+ """Calculate comprehensive investment score"""
118
+ try:
119
+ stock = yf.Ticker(symbol)
120
+ info = stock.info
121
+
122
+ # Get YTD data
123
+ ytd_start = datetime(2025, 1, 1)
124
+ ytd_data = stock.history(start=ytd_start.strftime("%Y-%m-%d"))
125
+
126
+ if ytd_data.empty:
127
+ return {'error': f'No YTD data available for {symbol}'}
128
+
129
+ # Calculate YTD return
130
+ ytd_return = ((ytd_data['Close'].iloc[-1] - ytd_data['Close'].iloc[0]) /
131
+ ytd_data['Close'].iloc[0]) * 100
132
+
133
+ # Get 1-year data for volatility
134
+ year_data = self.get_stock_data(symbol, "1y")
135
+ volatility = 0
136
+ max_drawdown = 0
137
+
138
+ if year_data is not None and not year_data.empty:
139
+ returns = year_data['Close'].pct_change().dropna()
140
+ volatility = returns.std() * np.sqrt(252) * 100 # Annualized volatility
141
+
142
+ # Calculate max drawdown
143
+ rolling_max = year_data['Close'].expanding().max()
144
+ drawdown = (year_data['Close'] - rolling_max) / rolling_max
145
+ max_drawdown = drawdown.min() * 100
146
+
147
+ # Technical indicators
148
+ technical = self.calculate_technical_indicators(year_data) if year_data is not None else {}
149
+
150
+ # Fundamental metrics
151
+ pe_ratio = info.get('trailingPE', 0) or 0
152
+ forward_pe = info.get('forwardPE', 0) or 0
153
+ peg_ratio = info.get('pegRatio', 0) or 0
154
+ roe = info.get('returnOnEquity', 0) or 0
155
+ profit_margin = info.get('profitMargins', 0) or 0
156
+ revenue_growth = info.get('revenueGrowth', 0) or 0
157
+
158
+ # Calculate investment score (0-100)
159
+ score = 50 # Base score
160
+
161
+ # YTD Performance (30% weight)
162
+ if ytd_return > 25:
163
+ score += 25
164
+ elif ytd_return > 15:
165
+ score += 20
166
+ elif ytd_return > 5:
167
+ score += 15
168
+ elif ytd_return > 0:
169
+ score += 10
170
+ elif ytd_return > -10:
171
+ score += 5
172
+ else:
173
+ score -= 15
174
+
175
+ # Technical indicators (25% weight)
176
+ rsi = technical.get('rsi', 50)
177
+ if 30 <= rsi <= 70: # Not oversold or overbought
178
+ score += 12
179
+ elif rsi < 30: # Oversold - potential buy
180
+ score += 8
181
+ elif rsi > 70: # Overbought - caution
182
+ score -= 5
183
+
184
+ # MACD signal
185
+ macd = technical.get('macd', 0)
186
+ macd_signal = technical.get('macd_signal', 0)
187
+ if macd > macd_signal: # Bullish signal
188
+ score += 8
189
+ else:
190
+ score -= 3
191
+
192
+ # Valuation (25% weight)
193
+ if pe_ratio and 8 < pe_ratio < 20:
194
+ score += 15
195
+ elif pe_ratio and pe_ratio < 8:
196
+ score += 20 # Very undervalued
197
+ elif pe_ratio and 20 < pe_ratio < 30:
198
+ score += 5
199
+ elif pe_ratio and pe_ratio > 35:
200
+ score -= 10
201
+
202
+ # Growth and profitability (20% weight)
203
+ if revenue_growth and revenue_growth > 0.20:
204
+ score += 15
205
+ elif revenue_growth and revenue_growth > 0.10:
206
+ score += 10
207
+ elif revenue_growth and revenue_growth > 0.05:
208
+ score += 5
209
+
210
+ if profit_margin and profit_margin > 0.15:
211
+ score += 5
212
+ elif profit_margin and profit_margin > 0.10:
213
+ score += 3
214
+
215
+ # Risk adjustment
216
+ if volatility < 15:
217
+ score += 5
218
+ elif volatility > 35:
219
+ score -= 10
220
+
221
+ if max_drawdown > -15:
222
+ score += 5
223
+ elif max_drawdown < -30:
224
+ score -= 8
225
+
226
+ # Ensure score bounds
227
+ score = max(0, min(100, score))
228
+
229
+ # Determine risk level and recommendation
230
+ if volatility < 15:
231
+ risk_level = "Low"
232
+ elif volatility < 25:
233
+ risk_level = "Medium"
234
+ else:
235
+ risk_level = "High"
236
+
237
+ if score >= 80:
238
+ recommendation = "Strong Buy"
239
+ elif score >= 70:
240
+ recommendation = "Buy"
241
+ elif score >= 60:
242
+ recommendation = "Hold"
243
+ elif score >= 50:
244
+ recommendation = "Weak Hold"
245
+ else:
246
+ recommendation = "Sell"
247
+
248
+ return {
249
+ 'symbol': symbol.upper(),
250
+ 'company_name': info.get('longName', 'N/A'),
251
+ 'current_price': ytd_data['Close'].iloc[-1],
252
+ 'ytd_return': ytd_return,
253
+ 'volatility': volatility,
254
+ 'max_drawdown': max_drawdown,
255
+ 'pe_ratio': pe_ratio,
256
+ 'forward_pe': forward_pe,
257
+ 'peg_ratio': peg_ratio,
258
+ 'roe': roe * 100 if roe else 0,
259
+ 'profit_margin': profit_margin * 100 if profit_margin else 0,
260
+ 'revenue_growth': revenue_growth * 100 if revenue_growth else 0,
261
+ 'investment_score': score,
262
+ 'recommendation': recommendation,
263
+ 'risk_level': risk_level,
264
+ 'sector': info.get('sector', 'N/A'),
265
+ 'industry': info.get('industry', 'N/A'),
266
+ 'market_cap': info.get('marketCap', 0),
267
+ 'technical_indicators': technical,
268
+ 'analysis_date': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
269
+ }
270
+
271
+ except Exception as e:
272
+ logger.error(f"Error calculating investment score for {symbol}: {e}")
273
+ return {'error': f'Error analyzing {symbol}: {str(e)}'}
274
+
275
+ # Initialize the stock analyzer
276
+ analyzer = StockAnalyzer()
277
+
278
+ # MCP Server Setup
279
+ server = Server("stock-analysis-mcp")
280
+
281
+ @server.list_tools()
282
+ async def handle_list_tools() -> List[Tool]:
283
+ """List available MCP tools"""
284
+ return [
285
+ Tool(
286
+ name="get_stock_price",
287
+ description="Get current stock price and basic info",
288
+ inputSchema={
289
+ "type": "object",
290
+ "properties": {
291
+ "symbol": {"type": "string", "description": "Stock symbol (e.g., AAPL)"}
292
+ },
293
+ "required": ["symbol"]
294
+ }
295
+ ),
296
+ Tool(
297
+ name="analyze_stock_comprehensive",
298
+ description="Comprehensive stock analysis with technical and fundamental metrics",
299
+ inputSchema={
300
+ "type": "object",
301
+ "properties": {
302
+ "symbol": {"type": "string", "description": "Stock symbol (e.g., AAPL)"}
303
+ },
304
+ "required": ["symbol"]
305
+ }
306
+ ),
307
+ Tool(
308
+ name="compare_stocks_ytd",
309
+ description="Compare multiple stocks for YTD 2025 performance",
310
+ inputSchema={
311
+ "type": "object",
312
+ "properties": {
313
+ "symbols": {
314
+ "type": "array",
315
+ "items": {"type": "string"},
316
+ "description": "List of stock symbols to compare"
317
+ }
318
+ },
319
+ "required": ["symbols"]
320
+ }
321
+ ),
322
+ Tool(
323
+ name="get_market_sector_analysis",
324
+ description="Analyze stocks by sector performance",
325
+ inputSchema={
326
+ "type": "object",
327
+ "properties": {
328
+ "symbols": {
329
+ "type": "array",
330
+ "items": {"type": "string"},
331
+ "description": "List of stock symbols to analyze by sector"
332
+ }
333
+ },
334
+ "required": ["symbols"]
335
+ }
336
+ )
337
+ ]
338
+
339
+ @server.call_tool()
340
+ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[types.TextContent]:
341
+ """Handle MCP tool calls"""
342
+ try:
343
+ if name == "get_stock_price":
344
+ symbol = arguments.get("symbol", "").upper()
345
+ if not symbol:
346
+ return [TextContent(type="text", text="Error: Symbol is required")]
347
+
348
+ stock = yf.Ticker(symbol)
349
+ info = stock.info
350
+ hist = stock.history(period="2d")
351
+
352
+ if hist.empty:
353
+ return [TextContent(type="text", text=f"Error: No data found for {symbol}")]
354
+
355
+ current_price = hist['Close'].iloc[-1]
356
+ prev_close = hist['Close'].iloc[-2] if len(hist) > 1 else current_price
357
+ change = current_price - prev_close
358
+ change_percent = (change / prev_close) * 100
359
+
360
+ result = {
361
+ "symbol": symbol,
362
+ "company_name": info.get('longName', 'N/A'),
363
+ "current_price": round(current_price, 2),
364
+ "change": round(change, 2),
365
+ "change_percent": round(change_percent, 2),
366
+ "previous_close": round(prev_close, 2),
367
+ "market_cap": info.get('marketCap', 0),
368
+ "volume": hist['Volume'].iloc[-1],
369
+ "sector": info.get('sector', 'N/A')
370
+ }
371
+
372
+ return [TextContent(type="text", text=json.dumps(result, indent=2))]
373
+
374
+ elif name == "analyze_stock_comprehensive":
375
+ symbol = arguments.get("symbol", "").upper()
376
+ if not symbol:
377
+ return [TextContent(type="text", text="Error: Symbol is required")]
378
+
379
+ analysis = analyzer.calculate_investment_score(symbol)
380
+ return [TextContent(type="text", text=json.dumps(analysis, indent=2))]
381
+
382
+ elif name == "compare_stocks_ytd":
383
+ symbols = arguments.get("symbols", [])
384
+ if not symbols:
385
+ return [TextContent(type="text", text="Error: Symbols list is required")]
386
+
387
+ comparisons = []
388
+ for symbol in symbols:
389
+ analysis = analyzer.calculate_investment_score(symbol)
390
+ if 'error' not in analysis:
391
+ comparisons.append(analysis)
392
+
393
+ # Sort by investment score
394
+ comparisons.sort(key=lambda x: x.get('investment_score', 0), reverse=True)
395
+
396
+ result = {
397
+ "comparison_results": comparisons,
398
+ "winner": comparisons[0] if comparisons else None,
399
+ "analysis_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
400
+ }
401
+
402
+ return [TextContent(type="text", text=json.dumps(result, indent=2))]
403
+
404
+ elif name == "get_market_sector_analysis":
405
+ symbols = arguments.get("symbols", [])
406
+ if not symbols:
407
+ return [TextContent(type="text", text="Error: Symbols list is required")]
408
+
409
+ sector_data = {}
410
+ for symbol in symbols:
411
+ analysis = analyzer.calculate_investment_score(symbol)
412
+ if 'error' not in analysis:
413
+ sector = analysis.get('sector', 'Unknown')
414
+ if sector not in sector_data:
415
+ sector_data[sector] = []
416
+ sector_data[sector].append(analysis)
417
+
418
+ # Calculate sector averages
419
+ sector_summary = {}
420
+ for sector, stocks in sector_data.items():
421
+ avg_score = sum(s['investment_score'] for s in stocks) / len(stocks)
422
+ avg_ytd = sum(s['ytd_return'] for s in stocks) / len(stocks)
423
+ sector_summary[sector] = {
424
+ "average_score": round(avg_score, 1),
425
+ "average_ytd_return": round(avg_ytd, 2),
426
+ "stock_count": len(stocks),
427
+ "stocks": [s['symbol'] for s in stocks]
428
+ }
429
+
430
+ result = {
431
+ "sector_analysis": sector_summary,
432
+ "detailed_stocks": sector_data,
433
+ "analysis_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
434
+ }
435
+
436
+ return [TextContent(type="text", text=json.dumps(result, indent=2))]
437
+
438
+ else:
439
+ return [TextContent(type="text", text=f"Error: Unknown tool '{name}'")]
440
+
441
+ except Exception as e:
442
+ error_msg = f"Error executing tool '{name}': {str(e)}"
443
+ logger.error(error_msg)
444
+ return [TextContent(type="text", text=error_msg)]
445
+
446
+ # Gradio Interface Functions
447
+ def create_stock_chart(symbol: str):
448
+ """Create interactive stock chart"""
449
+ try:
450
+ data = analyzer.get_stock_data(symbol, "6mo")
451
+ if data is None or data.empty:
452
+ return None
453
+
454
+ fig = make_subplots(
455
+ rows=2, cols=1,
456
+ shared_xaxes=True,
457
+ vertical_spacing=0.1,
458
+ subplot_titles=(f'{symbol.upper()} Stock Price', 'Volume'),
459
+ row_width=[0.7, 0.3]
460
+ )
461
+
462
+ # Candlestick chart
463
+ fig.add_trace(
464
+ go.Candlestick(
465
+ x=data.index,
466
+ open=data['Open'],
467
+ high=data['High'],
468
+ low=data['Low'],
469
+ close=data['Close'],
470
+ name="Price"
471
+ ),
472
+ row=1, col=1
473
+ )
474
+
475
+ # Moving averages
476
+ if len(data) >= 20:
477
+ data['MA20'] = data['Close'].rolling(window=20).mean()
478
+ fig.add_trace(
479
+ go.Scatter(x=data.index, y=data['MA20'], name='MA20', line=dict(color='orange')),
480
+ row=1, col=1
481
+ )
482
+
483
+ if len(data) >= 50:
484
+ data['MA50'] = data['Close'].rolling(window=50).mean()
485
+ fig.add_trace(
486
+ go.Scatter(x=data.index, y=data['MA50'], name='MA50', line=dict(color='blue')),
487
+ row=1, col=1
488
+ )
489
+
490
+ # Volume
491
+ fig.add_trace(
492
+ go.Bar(x=data.index, y=data['Volume'], name='Volume', marker_color='lightblue'),
493
+ row=2, col=1
494
+ )
495
+
496
+ fig.update_layout(
497
+ title=f'{symbol.upper()} - Stock Analysis',
498
+ xaxis_rangeslider_visible=False,
499
+ height=600,
500
+ showlegend=True
501
+ )
502
+
503
+ return fig
504
+ except Exception as e:
505
+ logger.error(f"Error creating chart for {symbol}: {e}")
506
+ return None
507
+
508
+ def analyze_single_stock(symbol: str) -> tuple:
509
+ """Analyze a single stock and return results"""
510
+ if not symbol:
511
+ return "Please enter a stock symbol", None, None
512
+
513
+ try:
514
+ analysis = analyzer.calculate_investment_score(symbol.upper())
515
+
516
+ if 'error' in analysis:
517
+ return f"Error: {analysis['error']}", None, None
518
+
519
+ # Create formatted analysis text
520
+ analysis_text = f"""
521
+ # 📊 Stock Analysis for {analysis['symbol']}
522
+
523
+ ## 🏢 Company Information
524
+ - **Company**: {analysis['company_name']}
525
+ - **Sector**: {analysis['sector']}
526
+ - **Industry**: {analysis['industry']}
527
+ - **Market Cap**: ${analysis['market_cap']/1e9:.2f}B
528
+
529
+ ## 💰 Current Performance
530
+ - **Current Price**: ${analysis['current_price']:.2f}
531
+ - **YTD 2025 Return**: {analysis['ytd_return']:+.2f}%
532
+ - **Investment Score**: {analysis['investment_score']}/100
533
+
534
+ ## 📈 Investment Recommendation
535
+ - **Recommendation**: {analysis['recommendation']}
536
+ - **Risk Level**: {analysis['risk_level']}
537
+ - **Volatility**: {analysis['volatility']:.1f}%
538
+
539
+ ## 🔍 Fundamental Metrics
540
+ - **P/E Ratio**: {analysis['pe_ratio']:.1f if analysis['pe_ratio'] else 'N/A'}
541
+ - **Forward P/E**: {analysis['forward_pe']:.1f if analysis['forward_pe'] else 'N/A'}
542
+ - **ROE**: {analysis['roe']:.1f}%
543
+ - **Profit Margin**: {analysis['profit_margin']:.1f}%
544
+ - **Revenue Growth**: {analysis['revenue_growth']:.1f}%
545
+
546
+ ## 📊 Technical Indicators
547
+ - **RSI**: {analysis['technical_indicators'].get('rsi', 0):.1f}
548
+ - **MACD**: {analysis['technical_indicators'].get('macd', 0):.3f}
549
+
550
+ ---
551
+ *Analysis Date: {analysis['analysis_date']}*
552
+ """
553
+
554
+ # Create chart
555
+ chart = create_stock_chart(symbol)
556
+
557
+ # Create comparison data for table
558
+ comparison_df = pd.DataFrame([{
559
+ 'Metric': 'Investment Score',
560
+ 'Value': f"{analysis['investment_score']}/100",
561
+ 'Interpretation': analysis['recommendation']
562
+ }, {
563
+ 'Metric': 'YTD Return',
564
+ 'Value': f"{analysis['ytd_return']:+.2f}%",
565
+ 'Interpretation': 'Strong' if analysis['ytd_return'] > 10 else 'Moderate' if analysis['ytd_return'] > 0 else 'Weak'
566
+ }, {
567
+ 'Metric': 'Risk Level',
568
+ 'Value': analysis['risk_level'],
569
+ 'Interpretation': f"Volatility: {analysis['volatility']:.1f}%"
570
+ }])
571
+
572
+ return analysis_text, chart, comparison_df
573
+
574
+ except Exception as e:
575
+ error_msg = f"Error analyzing {symbol}: {str(e)}"
576
+ logger.error(error_msg)
577
+ return error_msg, None, None
578
+
579
+ def compare_multiple_stocks(symbols_input: str) -> tuple:
580
+ """Compare multiple stocks"""
581
+ if not symbols_input:
582
+ return "Please enter stock symbols separated by commas", None, None
583
+
584
+ try:
585
+ symbols = [s.strip().upper() for s in symbols_input.split(',') if s.strip()]
586
+
587
+ if len(symbols) < 2:
588
+ return "Please enter at least 2 stock symbols for comparison", None, None
589
+
590
+ comparisons = []
591
+ for symbol in symbols:
592
+ analysis = analyzer.calculate_investment_score(symbol)
593
+ if 'error' not in analysis:
594
+ comparisons.append(analysis)
595
+
596
+ if not comparisons:
597
+ return "No valid stock data found for the provided symbols", None, None
598
+
599
+ # Sort by investment score
600
+ comparisons.sort(key=lambda x: x['investment_score'], reverse=True)
601
+
602
+ # Create comparison text
603
+ comparison_text = f"# 🏆 Stock Comparison Results\n\n"
604
+ comparison_text += f"**Analysis of {len(comparisons)} stocks:**\n\n"
605
+
606
+ for i, stock in enumerate(comparisons[:5]): # Top 5
607
+ rank_emoji = ["🥇", "🥈", "🥉", "4️⃣", "5️⃣"][i]
608
+ comparison_text += f"""
609
+ ## {rank_emoji} {stock['symbol']} - {stock['company_name']}
610
+ - **Score**: {stock['investment_score']}/100
611
+ - **Recommendation**: {stock['recommendation']}
612
+ - **YTD Return**: {stock['ytd_return']:+.2f}%
613
+ - **Current Price**: ${stock['current_price']:.2f}
614
+ - **Sector**: {stock['sector']}
615
+ - **Risk Level**: {stock['risk_level']}
616
+
617
+ """
618
+
619
+ # Create comparison DataFrame
620
+ comparison_df = pd.DataFrame([{
621
+ 'Rank': i+1,
622
+ 'Symbol': stock['symbol'],
623
+ 'Company': stock['company_name'][:30] + '...' if len(stock['company_name']) > 30 else stock['company_name'],
624
+ 'Score': stock['investment_score'],
625
+ 'YTD Return %': f"{stock['ytd_return']:+.2f}",
626
+ 'Price': f"${stock['current_price']:.2f}",
627
+ 'Recommendation': stock['recommendation'],
628
+ 'Sector': stock['sector']
629
+ } for i, stock in enumerate(comparisons)])
630
+
631
+ # Create comparison chart
632
+ fig = go.Figure()
633
+
634
+ fig.add_trace(go.Bar(
635
+ x=[s['symbol'] for s in comparisons],
636
+ y=[s['investment_score'] for s in comparisons],
637
+ text=[f"{s['investment_score']}" for s in comparisons],
638
+ textposition='auto',
639
+ marker_color=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd'][:len(comparisons)]
640
+ ))
641
+
642
+ fig.update_layout(
643
+ title='Investment Score Comparison',
644
+ xaxis_title='Stock Symbol',
645
+ yaxis_title='Investment Score (0-100)',
646
+ height=400
647
+ )
648
+
649
+ return comparison_text, fig, comparison_df
650
+
651
+ except Exception as e:
652
+ error_msg = f"Error comparing stocks: {str(e)}"
653
+ logger.error(error_msg)
654
+ return error_msg, None, None
655
+
656
+ # Create Gradio Interface
657
+ def create_gradio_app():
658
+ """Create the Gradio web interface"""
659
+
660
+ with gr.Blocks(title="🚀 MCP Stock Analysis Agent", theme=gr.themes.Soft()) as app:
661
+ gr.Markdown("""
662
+ # 🚀 Real MCP with Gradio Agent - Stock Analysis Platform
663
+
664
+ Advanced stock analysis powered by MCP (Model Context Protocol) with comprehensive technical and fundamental analysis.
665
+
666
+ ## Features:
667
+ - 📊 Real-time stock data analysis
668
+ - 🎯 AI-powered investment scoring
669
+ - 📈 Technical indicator analysis
670
+ - 🏆 Multi-stock comparison
671
+ - 📉 Interactive charts and visualizations
672
+ """)
673
+
674
+ with gr.Tabs():
675
+ # Single Stock Analysis Tab
676
+ with gr.Tab("📊 Single Stock Analysis"):
677
+ with gr.Row():
678
+ with gr.Column(scale=1):
679
+ stock_input = gr.Textbox(
680
+ label="Stock Symbol",
681
+ placeholder="Enter symbol (e.g., AAPL, MSFT, GOOGL)",
682
+ value="AAPL"
683
+ )
684
+ analyze_btn = gr.Button("🔍 Analyze Stock", variant="primary")
685
+
686
+ with gr.Row():
687
+ with gr.Column(scale=2):
688
+ analysis_output = gr.Markdown(label="Analysis Results")
689
+ with gr.Column(scale=1):
690
+ metrics_table = gr.Dataframe(
691
+ label="Key Metrics",
692
+ headers=["Metric", "Value", "Interpretation"]
693
+ )
694
+
695
+ stock_chart = gr.Plot(label="Stock Chart")
696
+
697
+ # Stock Comparison Tab
698
+ with gr.Tab("🏆 Stock Comparison"):
699
+ with gr.Row():
700
+ with gr.Column():
701
+ stocks_input = gr.Textbox(
702
+ label="Stock Symbols (comma-separated)",
703
+ placeholder="Enter symbols (e.g., AAPL, MSFT, GOOGL, TSLA)",
704
+ value="AAPL, MSFT, GOOGL"
705
+ )
706
+ compare_btn = gr.Button("🔍 Compare Stocks", variant="primary")
707
+
708
+ comparison_output = gr.Markdown(label="Comparison Results")
709
+ comparison_chart = gr.Plot(label="Comparison Chart")
710
+ comparison_table = gr.Dataframe(
711
+ label="Detailed Comparison",
712
+ headers=["Rank", "Symbol", "Company", "Score", "YTD Return %", "Price", "Recommendation", "Sector"]
713
+ )
714
+
715
+ # MCP Tools Tab
716
+ with gr.Tab("🛠️ MCP Tools"):
717
+ gr.Markdown("""
718
+ ## Available MCP Tools:
719
+
720
+ 1. **get_stock_price** - Get current stock price and basic info
721
+ 2. **analyze_stock_comprehensive** - Comprehensive analysis with scoring
722
+ 3. **compare_stocks_ytd** - Compare multiple stocks for YTD performance
723
+ 4. **get_market_sector_analysis** - Analyze stocks by sector
724
+
725
+ These tools can be called programmatically via the MCP protocol.
726
+ """)
727
+
728
+ with gr.Row():
729
+ mcp_tool_select = gr.Dropdown(
730
+ choices=["get_stock_price", "analyze_stock_comprehensive", "compare_stocks_ytd", "get_market_sector_analysis"],
731
+ label="Select MCP Tool",
732
+ value="get_stock_price"
733
+ )
734
+ mcp_symbol_input = gr.Textbox(
735
+ label="Symbol/Parameters",
736
+ placeholder="AAPL or AAPL,MSFT,GOOGL for comparison",
737
+ value="AAPL"
738
+ )
739
+
740
+ mcp_execute_btn = gr.Button("⚡ Execute MCP Tool", variant="secondary")
741
+ mcp_output = gr.JSON(label="MCP Tool Response")
742
+
743
+ # Event handlers
744
+ analyze_btn.click(
745
+ fn=analyze_single_stock,
746
+ inputs=[stock_input],
747
+ outputs=[analysis_output, stock_chart, metrics_table]
748
+ )
749
+
750
+ compare_btn.click(
751
+ fn=compare_multiple_stocks,
752
+ inputs=[stocks_input],
753
+ outputs=[comparison_output, comparison_chart, comparison_table]
754
+ )
755
+
756
+ def execute_mcp_tool(tool_name, params):
757
+ """Execute MCP tool from Gradio interface"""
758
+ try:
759
+ if tool_name == "get_stock_price":
760
+ arguments = {"symbol": params.strip()}
761
+ elif tool_name == "analyze_stock_comprehensive":
762
+ arguments = {"symbol": params.strip()}
763
+ elif tool_name in ["compare_stocks_ytd", "get_market_sector_analysis"]:
764
+ symbols = [s.strip() for s in params.split(',')]
765
+ arguments = {"symbols": symbols}
766
+ else:
767
+ return {"error": f"Unknown tool: {tool_name}"}
768
+
769
+ # Simulate MCP tool execution
770
+ loop = asyncio.new_event_loop()
771
+ asyncio.set_event_loop(loop)
772
+ result = loop.run_until_complete(handle_call_tool(tool_name, arguments))
773
+ loop.close()
774
+
775
+ # Parse the result
776
+ if result and len(result) > 0:
777
+ response_text = result[0].text
778
+ try:
779
+ return json.loads(response_text)
780
+ except json.JSONDecodeError:
781
+ return {"response": response_text}
782
+ else:
783
+ return {"error": "No response from MCP tool"}
784
+
785
+ except Exception as e:
786
+ return {"error": f"Error executing MCP tool: {str(e)}"}
787
+
788
+ mcp_execute_btn.click(
789
+ fn=execute_mcp_tool,
790
+ inputs=[mcp_tool_select, mcp_symbol_input],
791
+ outputs=[mcp_output]
792
+ )
793
+
794
+ # Add footer
795
+ gr.Markdown("""
796
+ ---
797
+ ### 🔧 Technical Details:
798
+ - **MCP Protocol**: Model Context Protocol for tool integration
799
+ - **Data Source**: Yahoo Finance API via yfinance
800
+ - **Analysis Engine**: Custom investment scoring algorithm
801
+ - **Visualization**: Plotly interactive charts
802
+ - **Interface**: Gradio web framework
803
+
804
+ *This platform provides educational analysis and should not be considered financial advice.*
805
+ """)
806
+
807
+ return app
808
+
809
+ # MCP Server Runner
810
+ async def run_mcp_server():
811
+ """Run the MCP server"""
812
+ logger.info("Starting MCP Stock Analysis Server...")
813
+ async with stdio_server() as (read_stream, write_stream):
814
+ await server.run(
815
+ read_stream,
816
+ write_stream,
817
+ InitializationOptions(
818
+ server_name="stock-analysis-mcp",
819
+ server_version="1.0.0",
820
+ capabilities=server.get_capabilities()
821
+ )
822
+ )
823
+
824
+ # Enhanced Portfolio Analysis
825
+ class PortfolioAnalyzer:
826
+ """Advanced portfolio analysis with risk metrics"""
827
+
828
+ def __init__(self):
829
+ self.analyzer = analyzer
830
+
831
+ def calculate_portfolio_metrics(self, symbols: List[str], weights: List[float] = None) -> Dict:
832
+ """Calculate comprehensive portfolio metrics"""
833
+ try:
834
+ if not weights:
835
+ weights = [1.0 / len(symbols)] * len(symbols) # Equal weights
836
+
837
+ portfolio_data = []
838
+ total_weight = sum(weights)
839
+ weights = [w / total_weight for w in weights] # Normalize weights
840
+
841
+ # Get data for all stocks
842
+ returns_data = []
843
+ for symbol in symbols:
844
+ data = self.analyzer.get_stock_data(symbol, "1y")
845
+ if data is not None and not data.empty:
846
+ returns = data['Close'].pct_change().dropna()
847
+ returns_data.append(returns)
848
+
849
+ # Individual stock analysis
850
+ analysis = self.analyzer.calculate_investment_score(symbol)
851
+ if 'error' not in analysis:
852
+ portfolio_data.append(analysis)
853
+
854
+ if not returns_data:
855
+ return {'error': 'No valid data for portfolio analysis'}
856
+
857
+ # Calculate portfolio returns
858
+ portfolio_returns = pd.DataFrame(returns_data).T
859
+ portfolio_returns.columns = symbols[:len(returns_data)]
860
+
861
+ # Portfolio daily returns
862
+ weighted_returns = (portfolio_returns * weights[:len(returns_data)]).sum(axis=1)
863
+
864
+ # Portfolio metrics
865
+ portfolio_return = weighted_returns.mean() * 252 * 100 # Annualized return
866
+ portfolio_volatility = weighted_returns.std() * np.sqrt(252) * 100 # Annualized volatility
867
+ sharpe_ratio = portfolio_return / portfolio_volatility if portfolio_volatility > 0 else 0
868
+
869
+ # Portfolio max drawdown
870
+ cumulative_returns = (1 + weighted_returns).cumprod()
871
+ rolling_max = cumulative_returns.expanding().max()
872
+ drawdown = (cumulative_returns - rolling_max) / rolling_max
873
+ max_drawdown = drawdown.min() * 100
874
+
875
+ # Risk metrics
876
+ var_95 = np.percentile(weighted_returns, 5) * 100 # 5% VaR
877
+
878
+ # Correlation matrix
879
+ correlation_matrix = portfolio_returns.corr().to_dict()
880
+
881
+ # Weighted portfolio score
882
+ portfolio_score = sum(stock['investment_score'] * weight
883
+ for stock, weight in zip(portfolio_data, weights[:len(portfolio_data)]))
884
+
885
+ return {
886
+ 'portfolio_return': portfolio_return,
887
+ 'portfolio_volatility': portfolio_volatility,
888
+ 'sharpe_ratio': sharpe_ratio,
889
+ 'max_drawdown': max_drawdown,
890
+ 'var_95': var_95,
891
+ 'portfolio_score': portfolio_score,
892
+ 'correlation_matrix': correlation_matrix,
893
+ 'individual_stocks': portfolio_data,
894
+ 'weights': dict(zip(symbols[:len(weights)], weights)),
895
+ 'analysis_date': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
896
+ }
897
+
898
+ except Exception as e:
899
+ logger.error(f"Error in portfolio analysis: {e}")
900
+ return {'error': f'Portfolio analysis error: {str(e)}'}
901
+
902
+ # Enhanced Gradio Interface with Portfolio Analysis
903
+ def create_enhanced_gradio_app():
904
+ """Create enhanced Gradio interface with portfolio analysis"""
905
+
906
+ portfolio_analyzer = PortfolioAnalyzer()
907
+
908
+ def analyze_portfolio(symbols_input: str, weights_input: str = "") -> tuple:
909
+ """Analyze a portfolio of stocks"""
910
+ try:
911
+ if not symbols_input:
912
+ return "Please enter stock symbols", None, None, None
913
+
914
+ symbols = [s.strip().upper() for s in symbols_input.split(',') if s.strip()]
915
+
916
+ # Parse weights if provided
917
+ weights = None
918
+ if weights_input.strip():
919
+ try:
920
+ weights = [float(w.strip()) for w in weights_input.split(',')]
921
+ if len(weights) != len(symbols):
922
+ return "Number of weights must match number of symbols", None, None, None
923
+ except ValueError:
924
+ return "Invalid weights format. Use comma-separated numbers (e.g., 0.4, 0.3, 0.3)", None, None, None
925
+
926
+ # Analyze portfolio
927
+ portfolio_analysis = portfolio_analyzer.calculate_portfolio_metrics(symbols, weights)
928
+
929
+ if 'error' in portfolio_analysis:
930
+ return f"Error: {portfolio_analysis['error']}", None, None, None
931
+
932
+ # Create analysis text
933
+ analysis_text = f"""
934
+ # 📊 Portfolio Analysis Results
935
+
936
+ ## 🏦 Portfolio Overview
937
+ - **Number of Holdings**: {len(symbols)}
938
+ - **Portfolio Score**: {portfolio_analysis['portfolio_score']:.1f}/100
939
+ - **Analysis Date**: {portfolio_analysis['analysis_date']}
940
+
941
+ ## 📈 Performance Metrics
942
+ - **Expected Annual Return**: {portfolio_analysis['portfolio_return']:+.2f}%
943
+ - **Annual Volatility**: {portfolio_analysis['portfolio_volatility']:.2f}%
944
+ - **Sharpe Ratio**: {portfolio_analysis['sharpe_ratio']:.2f}
945
+ - **Maximum Drawdown**: {portfolio_analysis['max_drawdown']:.2f}%
946
+ - **Value at Risk (95%)**: {portfolio_analysis['var_95']:.2f}%
947
+
948
+ ## 🏭 Portfolio Composition
949
+ """
950
+
951
+ for symbol, weight in portfolio_analysis['weights'].items():
952
+ analysis_text += f"- **{symbol}**: {weight:.1%}\n"
953
+
954
+ analysis_text += "\n## 📊 Individual Stock Performance\n"
955
+
956
+ for stock in portfolio_analysis['individual_stocks']:
957
+ weight = portfolio_analysis['weights'].get(stock['symbol'], 0)
958
+ analysis_text += f"""
959
+ ### {stock['symbol']} - {stock['company_name']} ({weight:.1%})
960
+ - **Score**: {stock['investment_score']}/100 | **YTD**: {stock['ytd_return']:+.2f}%
961
+ - **Price**: ${stock['current_price']:.2f} | **Sector**: {stock['sector']}
962
+ """
963
+
964
+ # Create portfolio composition chart
965
+ fig_composition = go.Figure(data=[go.Pie(
966
+ labels=list(portfolio_analysis['weights'].keys()),
967
+ values=list(portfolio_analysis['weights'].values()),
968
+ hole=0.3
969
+ )])
970
+ fig_composition.update_layout(title="Portfolio Composition", height=400)
971
+
972
+ # Create performance comparison chart
973
+ stocks_data = portfolio_analysis['individual_stocks']
974
+ fig_performance = go.Figure()
975
+
976
+ fig_performance.add_trace(go.Bar(
977
+ x=[s['symbol'] for s in stocks_data],
978
+ y=[s['ytd_return'] for s in stocks_data],
979
+ name='YTD Return %',
980
+ text=[f"{s['ytd_return']:+.1f}%" for s in stocks_data],
981
+ textposition='auto'
982
+ ))
983
+
984
+ fig_performance.update_layout(
985
+ title='Individual Stock YTD Performance',
986
+ xaxis_title='Stock Symbol',
987
+ yaxis_title='YTD Return (%)',
988
+ height=400
989
+ )
990
+
991
+ # Create portfolio metrics table
992
+ metrics_df = pd.DataFrame([
993
+ {'Metric': 'Portfolio Score', 'Value': f"{portfolio_analysis['portfolio_score']:.1f}/100"},
994
+ {'Metric': 'Expected Return', 'Value': f"{portfolio_analysis['portfolio_return']:+.2f}%"},
995
+ {'Metric': 'Volatility', 'Value': f"{portfolio_analysis['portfolio_volatility']:.2f}%"},
996
+ {'Metric': 'Sharpe Ratio', 'Value': f"{portfolio_analysis['sharpe_ratio']:.2f}"},
997
+ {'Metric': 'Max Drawdown', 'Value': f"{portfolio_analysis['max_drawdown']:.2f}%"},
998
+ {'Metric': 'VaR (95%)', 'Value': f"{portfolio_analysis['var_95']:.2f}%"}
999
+ ])
1000
+
1001
+ return analysis_text, fig_composition, fig_performance, metrics_df
1002
+
1003
+ except Exception as e:
1004
+ error_msg = f"Error analyzing portfolio: {str(e)}"
1005
+ logger.error(error_msg)
1006
+ return error_msg, None, None, None
1007
+
1008
+ with gr.Blocks(title="🚀 Advanced MCP Stock Analysis Agent", theme=gr.themes.Soft()) as app:
1009
+ gr.Markdown("""
1010
+ # 🚀 Advanced MCP Stock Analysis Agent
1011
+
1012
+ **Real Model Context Protocol (MCP) implementation with comprehensive stock analysis**
1013
+
1014
+ Features: Real-time data • AI scoring • Technical analysis • Portfolio optimization • Risk metrics
1015
+ """)
1016
+
1017
+ with gr.Tabs():
1018
+ # Single Stock Analysis Tab
1019
+ with gr.Tab("📊 Stock Analysis"):
1020
+ with gr.Row():
1021
+ with gr.Column(scale=1):
1022
+ stock_input = gr.Textbox(
1023
+ label="📈 Stock Symbol",
1024
+ placeholder="AAPL, MSFT, GOOGL, etc.",
1025
+ value="AAPL"
1026
+ )
1027
+ analyze_btn = gr.Button("🔍 Analyze Stock", variant="primary", size="lg")
1028
+
1029
+ with gr.Row():
1030
+ with gr.Column(scale=2):
1031
+ analysis_output = gr.Markdown()
1032
+ with gr.Column(scale=1):
1033
+ metrics_table = gr.Dataframe(label="📊 Key Metrics")
1034
+
1035
+ stock_chart = gr.Plot(label="📈 Interactive Chart")
1036
+
1037
+ # Portfolio Analysis Tab
1038
+ with gr.Tab("🏦 Portfolio Analysis"):
1039
+ with gr.Row():
1040
+ with gr.Column():
1041
+ portfolio_symbols = gr.Textbox(
1042
+ label="📊 Portfolio Symbols (comma-separated)",
1043
+ placeholder="AAPL, MSFT, GOOGL, TSLA, NVDA",
1044
+ value="AAPL, MSFT, GOOGL"
1045
+ )
1046
+ portfolio_weights = gr.Textbox(
1047
+ label="⚖️ Weights (optional, comma-separated)",
1048
+ placeholder="0.4, 0.3, 0.3 (leave empty for equal weights)",
1049
+ value=""
1050
+ )
1051
+ portfolio_btn = gr.Button("🔍 Analyze Portfolio", variant="primary", size="lg")
1052
+
1053
+ portfolio_output = gr.Markdown()
1054
+
1055
+ with gr.Row():
1056
+ portfolio_composition = gr.Plot(label="🥧 Portfolio Composition")
1057
+ portfolio_performance = gr.Plot(label="📊 Performance Comparison")
1058
+
1059
+ portfolio_metrics = gr.Dataframe(label="📈 Portfolio Metrics")
1060
+
1061
+ # Stock Comparison Tab
1062
+ with gr.Tab("🏆 Stock Comparison"):
1063
+ with gr.Row():
1064
+ with gr.Column():
1065
+ stocks_input = gr.Textbox(
1066
+ label="🔍 Stock Symbols (comma-separated)",
1067
+ placeholder="AAPL, MSFT, GOOGL, TSLA, NVDA",
1068
+ value="AAPL, MSFT, GOOGL"
1069
+ )
1070
+ compare_btn = gr.Button("⚡ Compare Stocks", variant="primary", size="lg")
1071
+
1072
+ comparison_output = gr.Markdown()
1073
+ comparison_chart = gr.Plot(label="📊 Comparison Chart")
1074
+ comparison_table = gr.Dataframe(label="📋 Detailed Comparison")
1075
+
1076
+ # MCP Server Tools Tab
1077
+ with gr.Tab("🛠️ MCP Server"):
1078
+ gr.Markdown("""
1079
+ ## 🔧 MCP (Model Context Protocol) Tools
1080
+
1081
+ This tab demonstrates the MCP server capabilities:
1082
+ """)
1083
+
1084
+ with gr.Row():
1085
+ with gr.Column():
1086
+ mcp_tool_select = gr.Dropdown(
1087
+ choices=[
1088
+ "get_stock_price",
1089
+ "analyze_stock_comprehensive",
1090
+ "compare_stocks_ytd",
1091
+ "get_market_sector_analysis"
1092
+ ],
1093
+ label="🛠️ Select MCP Tool",
1094
+ value="analyze_stock_comprehensive"
1095
+ )
1096
+ mcp_symbol_input = gr.Textbox(
1097
+ label="📊 Parameters",
1098
+ placeholder="AAPL or AAPL,MSFT,GOOGL",
1099
+ value="AAPL"
1100
+ )
1101
+ mcp_execute_btn = gr.Button("⚡ Execute MCP Tool", variant="secondary")
1102
+
1103
+ mcp_output = gr.JSON(label="📋 MCP Response")
1104
+
1105
+ gr.Markdown("""
1106
+ ### 📡 MCP Server Information:
1107
+ - **Server Name**: stock-analysis-mcp
1108
+ - **Version**: 1.0.0
1109
+ - **Protocol**: stdio
1110
+ - **Tools**: 4 available tools for stock analysis
1111
+ """)
1112
+
1113
+ # Event handlers
1114
+ analyze_btn.click(
1115
+ fn=analyze_single_stock,
1116
+ inputs=[stock_input],
1117
+ outputs=[analysis_output, stock_chart, metrics_table]
1118
+ )
1119
+
1120
+ portfolio_btn.click(
1121
+ fn=analyze_portfolio,
1122
+ inputs=[portfolio_symbols, portfolio_weights],
1123
+ outputs=[portfolio_output, portfolio_composition, portfolio_performance, portfolio_metrics]
1124
+ )
1125
+
1126
+ compare_btn.click(
1127
+ fn=compare_multiple_stocks,
1128
+ inputs=[stocks_input],
1129
+ outputs=[comparison_output, comparison_chart, comparison_table]
1130
+ )
1131
+
1132
+ def execute_mcp_tool(tool_name, params):
1133
+ """Execute MCP tool from Gradio interface"""
1134
+ try:
1135
+ if tool_name == "get_stock_price":
1136
+ arguments = {"symbol": params.strip()}
1137
+ elif tool_name == "analyze_stock_comprehensive":
1138
+ arguments = {"symbol": params.strip()}
1139
+ elif tool_name in ["compare_stocks_ytd", "get_market_sector_analysis"]:
1140
+ symbols = [s.strip() for s in params.split(',') if s.strip()]
1141
+ arguments = {"symbols": symbols}
1142
+ else:
1143
+ return {"error": f"Unknown tool: {tool_name}"}
1144
+
1145
+ # Execute MCP tool
1146
+ loop = asyncio.new_event_loop()
1147
+ asyncio.set_event_loop(loop)
1148
+ result = loop.run_until_complete(handle_call_tool(tool_name, arguments))
1149
+ loop.close()
1150
+
1151
+ # Parse the result
1152
+ if result and len(result) > 0:
1153
+ response_text = result[0].text
1154
+ try:
1155
+ parsed_result = json.loads(response_text)
1156
+ parsed_result["_mcp_tool"] = tool_name
1157
+ parsed_result["_execution_time"] = datetime.now().isoformat()
1158
+ return parsed_result
1159
+ except json.JSONDecodeError:
1160
+ return {
1161
+ "response": response_text,
1162
+ "_mcp_tool": tool_name,
1163
+ "_execution_time": datetime.now().isoformat()
1164
+ }
1165
+ else:
1166
+ return {"error": "No response from MCP tool"}
1167
+
1168
+ except Exception as e:
1169
+ return {
1170
+ "error": f"Error executing MCP tool: {str(e)}",
1171
+ "_mcp_tool": tool_name,
1172
+ "_execution_time": datetime.now().isoformat()
1173
+ }
1174
+
1175
+ mcp_execute_btn.click(
1176
+ fn=execute_mcp_tool,
1177
+ inputs=[mcp_tool_select, mcp_symbol_input],
1178
+ outputs=[mcp_output]
1179
+ )
1180
+
1181
+ # Footer
1182
+ gr.Markdown("""
1183
+ ---
1184
+ ## 🚀 System Architecture
1185
+
1186
+ **MCP Server**: Implements Model Context Protocol for tool integration
1187
+ **Analysis Engine**: Advanced scoring algorithm with 15+ metrics
1188
+ **Data Pipeline**: Real-time Yahoo Finance integration
1189
+ **Risk Engine**: Portfolio optimization and risk analytics
1190
+ **Visualization**: Interactive Plotly charts and dashboards
1191
+
1192
+ *Educational platform - not financial advice. Always consult professionals.*
1193
+ """)
1194
+
1195
+ return app
1196
+
1197
+ # Main execution functions
1198
+ def main():
1199
+ """Main function to run the application"""
1200
+ import argparse
1201
+
1202
+ parser = argparse.ArgumentParser(description="MCP Stock Analysis Agent")
1203
+ parser.add_argument("--mode", choices=["mcp", "gradio", "both"], default="both",
1204
+ help="Run mode: mcp (server only), gradio (web interface), or both")
1205
+ parser.add_argument("--port", type=int, default=7860, help="Gradio server port")
1206
+ parser.add_argument("--share", action="store_true", help="Share Gradio interface publicly")
1207
+
1208
+ args = parser.parse_args()
1209
+
1210
+ if args.mode == "mcp":
1211
+ # Run MCP server only
1212
+ asyncio.run(run_mcp_server())
1213
+
1214
+ elif args.mode == "gradio":
1215
+ # Run Gradio interface only
1216
+ app = create_enhanced_gradio_app()
1217
+ app.launch(server_port=args.port, share=args.share)
1218
+
1219
+ else:
1220
+ # Run both MCP server and Gradio interface
1221
+ print("🚀 Starting MCP Stock Analysis Agent...")
1222
+ print("📊 MCP Server will run in background")
1223
+ print(f"🌐 Gradio Interface will be available at http://localhost:{args.port}")
1224
+
1225
+ # Start Gradio interface (MCP server runs on-demand)
1226
+ app = create_enhanced_gradio_app()
1227
+ app.launch(server_port=args.port, share=args.share)
1228
+
1229
+ if __name__ == "__main__":
1230
+ main()