ashwiniambastha commited on
Commit
605f2ee
Β·
verified Β·
1 Parent(s): c6a688b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1123 -929
app.py CHANGED
@@ -1,930 +1,1124 @@
1
- import gradio as gr
2
- import pandas as pd
3
- import numpy as np
4
- import plotly.graph_objects as go
5
- from datetime import datetime
6
- import yfinance as yf
7
-
8
- # ─────────────────────────────────────────────
9
- # Try importing agents; fall back gracefully
10
- # ─────────────────────────────────────────────
11
- try:
12
- from agents.market_data.agent import MarketDataAgent
13
- from agents.market_data.storage import MarketDataStorage
14
- from agents.risk_management.agent import RiskManagementAgent
15
- SYMBOLS = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN']
16
- market_agent = MarketDataAgent(SYMBOLS)
17
- storage = MarketDataStorage()
18
- risk_agent = RiskManagementAgent()
19
- AGENTS_LOADED = True
20
- except Exception:
21
- AGENTS_LOADED = False
22
-
23
- # ─────────────────────────────────────────────
24
- # Color Palette β€” Blue White Professional
25
- # ─────────────────────────────────────────────
26
- BG_PAGE = "#f0f4fb"
27
- BG_CARD = "#ffffff"
28
- BG_HEADER = "#1a3a6b"
29
- BORDER = "#d0dff0"
30
- BLUE_PRIMARY = "#1a5fca"
31
- BLUE_DARK = "#0d2d6b"
32
- BLUE_LIGHT = "#e8f0fe"
33
- GREEN = "#0d9c5b"
34
- RED = "#e03131"
35
- GOLD = "#d4940a"
36
- TEXT_DARK = "#0d1f3c"
37
- TEXT_MED = "#3a5080"
38
- TEXT_LIGHT = "#6b83a8"
39
- WHITE = "#ffffff"
40
-
41
- # ─────────────────────────────────────────────
42
- # Plotly chart theme β€” clean white/blue
43
- # ─────────────────────────────────────────────
44
- PLOTLY_THEME = dict(
45
- paper_bgcolor=BG_CARD,
46
- plot_bgcolor="#f8faff",
47
- font=dict(family="Inter, sans-serif", color=TEXT_DARK, size=12),
48
- legend=dict(bgcolor="rgba(255,255,255,0.9)", bordercolor=BORDER,
49
- borderwidth=1, font=dict(color=TEXT_DARK)),
50
- margin=dict(l=55, r=30, t=55, b=45),
51
- )
52
-
53
- AXIS_STYLE = dict(
54
- gridcolor="#e2eaf5",
55
- zerolinecolor="#c5d5ea",
56
- tickfont=dict(color=TEXT_LIGHT),
57
- linecolor=BORDER,
58
- )
59
-
60
- # ─────────────────────────────────────────────
61
- # CSS β€” Full Blue White Theme
62
- # ─────────────────────────────────────────────
63
- CUSTOM_CSS = """
64
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');
65
-
66
- *, *::before, *::after { box-sizing: border-box; }
67
-
68
- body, .gradio-container {
69
- background: #f0f4fb !important;
70
- font-family: 'Inter', sans-serif !important;
71
- color: #0d1f3c !important;
72
- }
73
-
74
- /* Header */
75
- .app-header {
76
- background: linear-gradient(135deg, #0d2d6b 0%, #1a5fca 60%, #2979e8 100%);
77
- padding: 36px 40px 28px;
78
- text-align: center;
79
- border-bottom: 4px solid #d4940a;
80
- box-shadow: 0 4px 20px rgba(26,60,107,0.18);
81
- }
82
- .app-title {
83
- font-family: 'Space Grotesk', sans-serif !important;
84
- font-size: 2rem;
85
- font-weight: 700;
86
- color: #ffffff !important;
87
- letter-spacing: 1px;
88
- margin-bottom: 6px;
89
- }
90
- .app-subtitle {
91
- font-size: 0.82rem;
92
- color: rgba(255,255,255,0.75) !important;
93
- letter-spacing: 2px;
94
- text-transform: uppercase;
95
- }
96
- .app-team {
97
- font-size: 0.75rem;
98
- color: rgba(255,255,255,0.55) !important;
99
- margin-top: 10px;
100
- letter-spacing: 1.5px;
101
- }
102
- .header-accent {
103
- width: 60px;
104
- height: 3px;
105
- background: #d4940a;
106
- margin: 12px auto;
107
- border-radius: 2px;
108
- }
109
-
110
- /* Tabs */
111
- div[role="tablist"] {
112
- background: #ffffff !important;
113
- border-bottom: 2px solid #d0dff0 !important;
114
- padding: 0 16px !important;
115
- box-shadow: 0 2px 8px rgba(26,60,107,0.06) !important;
116
- }
117
- div[role="tab"] {
118
- font-family: 'Inter', sans-serif !important;
119
- font-size: 0.82rem !important;
120
- font-weight: 500 !important;
121
- color: #6b83a8 !important;
122
- border: none !important;
123
- border-bottom: 3px solid transparent !important;
124
- padding: 13px 16px !important;
125
- background: transparent !important;
126
- transition: all 0.2s !important;
127
- }
128
- div[role="tab"]:hover { color: #1a5fca !important; background: #e8f0fe !important; }
129
- div[role="tab"][aria-selected="true"] {
130
- color: #1a5fca !important;
131
- border-bottom: 3px solid #1a5fca !important;
132
- background: #e8f0fe !important;
133
- font-weight: 600 !important;
134
- }
135
-
136
- /* Metric cards */
137
- .metric-grid { display: grid; gap: 14px; margin-bottom: 22px; }
138
- .metric-grid-5 { grid-template-columns: repeat(5, 1fr); }
139
- .metric-grid-4 { grid-template-columns: repeat(4, 1fr); }
140
-
141
- .kpi-card {
142
- background: #ffffff;
143
- border: 1px solid #d0dff0;
144
- border-radius: 12px;
145
- padding: 18px 20px;
146
- position: relative;
147
- overflow: hidden;
148
- box-shadow: 0 2px 10px rgba(26,60,107,0.07);
149
- transition: transform 0.2s, box-shadow 0.2s;
150
- }
151
- .kpi-card::before {
152
- content: '';
153
- position: absolute;
154
- top: 0; left: 0; right: 0;
155
- height: 3px;
156
- background: linear-gradient(90deg, #1a5fca, #2979e8);
157
- }
158
- .kpi-card.green::before { background: linear-gradient(90deg, #0d9c5b, #22c55e); }
159
- .kpi-card.red::before { background: linear-gradient(90deg, #e03131, #f87171); }
160
- .kpi-card.gold::before { background: linear-gradient(90deg, #d4940a, #f59e0b); }
161
- .kpi-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(26,60,107,0.12); }
162
-
163
- .kpi-label {
164
- font-size: 0.70rem;
165
- font-weight: 600;
166
- color: #6b83a8;
167
- text-transform: uppercase;
168
- letter-spacing: 1.2px;
169
- margin-bottom: 9px;
170
- }
171
- .kpi-value {
172
- font-family: 'Space Grotesk', sans-serif !important;
173
- font-size: 1.6rem;
174
- font-weight: 700;
175
- color: #0d1f3c;
176
- line-height: 1.1;
177
- }
178
- .kpi-value.up { color: #0d9c5b; }
179
- .kpi-value.down { color: #e03131; }
180
- .kpi-value.warn { color: #d4940a; }
181
- .kpi-delta {
182
- font-size: 0.74rem;
183
- font-weight: 500;
184
- margin-top: 5px;
185
- color: #6b83a8;
186
- }
187
- .kpi-delta.up { color: #0d9c5b; }
188
- .kpi-delta.down { color: #e03131; }
189
-
190
- /* Section headers */
191
- .section-header {
192
- margin-bottom: 18px;
193
- padding-bottom: 12px;
194
- border-bottom: 1px solid #d0dff0;
195
- }
196
- .section-title {
197
- font-family: 'Space Grotesk', sans-serif !important;
198
- font-size: 1rem;
199
- font-weight: 600;
200
- color: #0d2d6b;
201
- }
202
- .section-sub { font-size: 0.76rem; color: #6b83a8; margin-top: 3px; }
203
-
204
- /* Badges */
205
- .badge { display:inline-block; padding:4px 14px; border-radius:20px; font-size:0.71rem; font-weight:600; letter-spacing:.8px; text-transform:uppercase; }
206
- .badge-ok { background:#d1fae5; color:#065f46; border:1px solid #6ee7b7; }
207
- .badge-alert { background:#fee2e2; color:#991b1b; border:1px solid #fca5a5; }
208
- .badge-warn { background:#fef3c7; color:#92400e; border:1px solid #fcd34d; }
209
- .badge-info { background:#e8f0fe; color:#1a5fca; border:1px solid #a8c4f8; }
210
-
211
- /* Banners */
212
- .info-banner { background:#e8f0fe; border:1px solid #a8c4f8; border-left:4px solid #1a5fca; border-radius:8px; padding:12px 18px; font-size:.82rem; color:#1a3a6b; margin-bottom:14px; }
213
- .alert-banner { background:#fee2e2; border:1px solid #fca5a5; border-left:4px solid #e03131; border-radius:8px; padding:12px 18px; font-size:.82rem; color:#7f1d1d; margin-bottom:14px; }
214
- .success-banner { background:#d1fae5; border:1px solid #6ee7b7; border-left:4px solid #0d9c5b; border-radius:8px; padding:12px 18px; font-size:.82rem; color:#064e3b; margin-bottom:14px; }
215
-
216
- /* Buttons */
217
- button, .gr-button {
218
- font-family: 'Inter', sans-serif !important;
219
- font-weight: 600 !important;
220
- font-size: 0.83rem !important;
221
- border-radius: 8px !important;
222
- transition: all 0.2s !important;
223
- }
224
- .gr-button-primary, button.primary {
225
- background: linear-gradient(135deg, #1a5fca, #2979e8) !important;
226
- color: #ffffff !important;
227
- border: none !important;
228
- box-shadow: 0 3px 12px rgba(26,95,202,0.28) !important;
229
- }
230
- .gr-button-primary:hover, button.primary:hover {
231
- background: linear-gradient(135deg, #0d4fb5, #1a5fca) !important;
232
- box-shadow: 0 5px 18px rgba(26,95,202,0.38) !important;
233
- transform: translateY(-1px) !important;
234
- }
235
-
236
- /* Inputs */
237
- input, select, textarea {
238
- background: #ffffff !important;
239
- border: 1.5px solid #c5d5ea !important;
240
- border-radius: 8px !important;
241
- color: #0d1f3c !important;
242
- font-family: 'Inter', sans-serif !important;
243
- font-size: 0.88rem !important;
244
- }
245
- input:focus, select:focus { border-color: #1a5fca !important; box-shadow: 0 0 0 3px rgba(26,95,202,0.12) !important; outline: none !important; }
246
-
247
- label {
248
- font-family: 'Inter', sans-serif !important;
249
- font-size: 0.76rem !important;
250
- font-weight: 600 !important;
251
- color: #3a5080 !important;
252
- text-transform: uppercase !important;
253
- letter-spacing: 0.8px !important;
254
- }
255
-
256
- /* Tables */
257
- table {
258
- border-collapse: separate !important;
259
- border-spacing: 0 !important;
260
- background: #ffffff !important;
261
- border-radius: 10px !important;
262
- overflow: hidden !important;
263
- border: 1px solid #d0dff0 !important;
264
- font-size: 0.83rem !important;
265
- box-shadow: 0 2px 10px rgba(26,60,107,0.06) !important;
266
- }
267
- th {
268
- background: #1a3a6b !important;
269
- color: #ffffff !important;
270
- font-size: 0.72rem !important;
271
- font-weight: 600 !important;
272
- letter-spacing: 1px !important;
273
- text-transform: uppercase !important;
274
- padding: 12px 16px !important;
275
- }
276
- td { color: #0d1f3c !important; padding: 10px 16px !important; border-bottom: 1px solid #edf2fb !important; background: #ffffff !important; }
277
- tr:nth-child(even) td { background: #f5f8fe !important; }
278
- tr:hover td { background: #e8f0fe !important; }
279
-
280
- /* Accordion */
281
- .gr-accordion { background: #ffffff !important; border: 1px solid #d0dff0 !important; border-radius: 10px !important; }
282
-
283
- /* Markdown */
284
- .gr-markdown h1, .gr-markdown h2, .gr-markdown h3 { font-family: 'Space Grotesk', sans-serif !important; color: #0d2d6b !important; }
285
- .gr-markdown p, .gr-markdown li { color: #0d1f3c !important; line-height: 1.7 !important; }
286
- .gr-markdown strong { color: #1a5fca !important; }
287
- .gr-markdown code { background: #e8f0fe !important; color: #1a5fca !important; border-radius: 4px !important; padding: 2px 7px !important; }
288
- .gr-markdown table th { background: #1a3a6b !important; color: #fff !important; padding: 10px 14px !important; }
289
- .gr-markdown table td { color: #0d1f3c !important; padding: 9px 14px !important; }
290
-
291
- /* Slider */
292
- input[type="range"] { accent-color: #1a5fca !important; }
293
-
294
- /* Footer */
295
- .app-footer {
296
- background: #0d2d6b;
297
- color: rgba(255,255,255,0.6);
298
- text-align: center;
299
- padding: 18px;
300
- font-size: 0.72rem;
301
- letter-spacing: 1.5px;
302
- text-transform: uppercase;
303
- margin-top: 40px;
304
- }
305
-
306
- /* Scrollbar */
307
- ::-webkit-scrollbar { width: 6px; height: 6px; }
308
- ::-webkit-scrollbar-track { background: #f0f4fb; }
309
- ::-webkit-scrollbar-thumb { background: #c5d5ea; border-radius: 3px; }
310
- ::-webkit-scrollbar-thumb:hover { background: #1a5fca; }
311
- """
312
-
313
- # ─────────────────────────────────────────────
314
- # HTML helpers
315
- # ─────────────────────────────────────────────
316
- def kpi_card(label, value, delta="", color="blue"):
317
- cls_map = {"green": "up", "red": "down", "gold": "warn"}
318
- val_cls = cls_map.get(color, "")
319
- card_cls = color if color in ["green","red","gold"] else ""
320
- dlt_cls = "up" if ("β–²" in delta or "+" in delta) else "down" if ("β–Ό" in delta or "-" in delta) else ""
321
- d_html = f'<div class="kpi-delta {dlt_cls}">{delta}</div>' if delta else ""
322
- return f"""
323
- <div class="kpi-card {card_cls}">
324
- <div class="kpi-label">{label}</div>
325
- <div class="kpi-value {val_cls}">{value}</div>
326
- {d_html}
327
- </div>"""
328
-
329
- def section_header(icon, title, subtitle=""):
330
- sub = f'<div class="section-sub">{subtitle}</div>' if subtitle else ""
331
- return f'<div class="section-header"><div class="section-title">{icon} {title}</div>{sub}</div>'
332
-
333
- def banner(msg, kind="info"):
334
- return f'<div class="{kind}-banner">{msg}</div>'
335
-
336
- SYMBOLS = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN','RELIANCE']
337
-
338
- # ─────────────────────────────────────────────
339
- # Data helpers
340
- # ─────────────────────────────────────────────
341
- def get_realtime(symbols):
342
- results = {}
343
- for sym in symbols:
344
- try:
345
- info = yf.Ticker(sym).info
346
- price = info.get('currentPrice') or info.get('regularMarketPrice') or 0
347
- open_ = info.get('regularMarketOpen') or price
348
- results[sym] = {
349
- 'symbol': sym, 'price': price, 'open': open_,
350
- 'high': info.get('dayHigh', 0), 'low': info.get('dayLow', 0),
351
- 'volume': info.get('volume', 0), 'market_cap': info.get('marketCap', 0),
352
- 'pe_ratio': info.get('trailingPE', 0),
353
- 'change_pct': ((price - open_) / open_ * 100) if open_ else 0,
354
- }
355
- except Exception:
356
- results[sym] = {k: 0 for k in ['price','open','high','low','volume','market_cap','pe_ratio','change_pct']}
357
- results[sym]['symbol'] = sym
358
- return results
359
-
360
- def get_historical(symbol, period="1y"):
361
- try:
362
- df = yf.Ticker(symbol).history(period=period)
363
- return df if not df.empty else pd.DataFrame()
364
- except Exception:
365
- return pd.DataFrame()
366
-
367
- def get_returns(symbol, period="1y"):
368
- df = get_historical(symbol, period)
369
- if df.empty:
370
- return pd.Series(dtype=float)
371
- return df['Close'].pct_change().dropna()
372
-
373
- def compute_risk(returns, portfolio_value, rf=0.04):
374
- if returns.empty:
375
- return {}
376
- daily_vol = returns.std()
377
- annual_vol = daily_vol * np.sqrt(252)
378
- total_ret = (1 + returns).prod() - 1
379
- years = max(len(returns) / 252, 0.01)
380
- ann_ret = (1 + total_ret) ** (1 / years) - 1
381
- sharpe = (ann_ret - rf) / annual_vol if annual_vol else 0
382
- var_95 = abs(np.percentile(returns, 5))
383
- var_99 = abs(np.percentile(returns, 1))
384
- tail = returns[returns <= -var_95]
385
- cvar_95 = abs(tail.mean()) if len(tail) else var_95
386
- cum = (1 + returns).cumprod()
387
- peak = cum.cummax()
388
- dd = (cum - peak) / peak
389
- max_dd = abs(dd.min())
390
- return dict(
391
- annual_vol=annual_vol, daily_vol=daily_vol, ann_ret=ann_ret, sharpe=sharpe,
392
- var_95=var_95, var_99=var_99, cvar_95=cvar_95,
393
- var_95_usd=var_95*portfolio_value, var_99_usd=var_99*portfolio_value,
394
- cvar_95_usd=cvar_95*portfolio_value, max_dd=max_dd, drawdown=dd, returns=returns,
395
- )
396
-
397
- def sharpe_label(s):
398
- if s > 3: return "Exceptional", "green"
399
- if s > 2: return "Very Good", "green"
400
- if s > 1: return "Good", "blue"
401
- if s > 0.5: return "Acceptable", "gold"
402
- if s > 0: return "Poor", "gold"
403
- return "Losing Money", "red"
404
-
405
- def apply_theme(fig, title_text=None, yaxis_title=None, xaxis_title=None, extra=None):
406
- layout = dict(**PLOTLY_THEME)
407
- layout['xaxis'] = dict(**AXIS_STYLE)
408
- layout['yaxis'] = dict(**AXIS_STYLE)
409
- if title_text:
410
- layout['title'] = dict(text=title_text, font=dict(color="#1a3a6b", size=14, family="Inter, sans-serif"))
411
- if yaxis_title:
412
- layout['yaxis']['title'] = yaxis_title
413
- if xaxis_title:
414
- layout['xaxis']['title'] = xaxis_title
415
- if extra:
416
- layout.update(extra)
417
- fig.update_layout(**layout)
418
- return fig
419
-
420
- # ═══════════════════════════════════════════════
421
- # TAB RENDER FUNCTIONS
422
- # ═══════════════════════════════════════════════
423
-
424
- def render_market_overview():
425
- data = get_realtime(SYMBOLS)
426
- ts = datetime.now().strftime('%d %b %Y %H:%M:%S')
427
-
428
- cards = '<div class="metric-grid metric-grid-5">'
429
- for sym, d in data.items():
430
- chg = d.get('change_pct', 0)
431
- sign = "β–²" if chg >= 0 else "β–Ό"
432
- col = "green" if chg >= 0 else "red"
433
- cards += kpi_card(sym, f"${d['price']:.2f}" if d['price'] else "β€”",
434
- f"{sign} {abs(chg):.2f}%", col)
435
- cards += "</div>"
436
-
437
- prices = {s: d['price'] for s, d in data.items() if d['price']}
438
- changes = {s: d['change_pct'] for s, d in data.items()}
439
- bcolors = [GREEN if changes.get(s,0) >= 0 else RED for s in prices]
440
-
441
- fig_p = go.Figure()
442
- fig_p.add_trace(go.Bar(
443
- x=list(prices.keys()), y=list(prices.values()),
444
- marker=dict(color=bcolors, line=dict(color='white', width=1)),
445
- text=[f"${v:.2f}" for v in prices.values()],
446
- textposition='outside', textfont=dict(size=11, color=TEXT_DARK),
447
- hovertemplate="<b>%{x}</b><br>Price: $%{y:.2f}<extra></extra>",
448
- ))
449
- apply_theme(fig_p, title_text="Current Stock Prices (USD)", yaxis_title="Price ($)", extra={"showlegend": False})
450
-
451
- vols = {s: d.get('volume', 0) for s, d in data.items()}
452
- fig_v = go.Figure()
453
- fig_v.add_trace(go.Bar(
454
- x=list(vols.keys()), y=list(vols.values()),
455
- marker=dict(color=list(vols.values()),
456
- colorscale=[[0, BLUE_LIGHT],[1, BLUE_PRIMARY]],
457
- showscale=False, line=dict(color='white', width=1)),
458
- text=[f"{v/1e6:.1f}M" for v in vols.values()],
459
- textposition='outside', textfont=dict(size=11, color=TEXT_DARK),
460
- hovertemplate="<b>%{x}</b><br>Volume: %{y:,.0f}<extra></extra>",
461
- ))
462
- apply_theme(fig_v, title_text="Trading Volume", yaxis_title="Volume", extra={"showlegend": False})
463
-
464
- rows = []
465
- for s, d in data.items():
466
- chg = d.get('change_pct', 0)
467
- rows.append({
468
- 'Symbol': s, 'Price ($)': f"${d['price']:.2f}" if d['price'] else "β€”",
469
- 'Open ($)': f"${d['open']:.2f}" if d['open'] else "β€”",
470
- 'High ($)': f"${d['high']:.2f}" if d['high'] else "β€”",
471
- 'Low ($)': f"${d['low']:.2f}" if d['low'] else "β€”",
472
- 'Volume': f"{d['volume']/1e6:.1f}M" if d['volume'] else "β€”",
473
- 'Mkt Cap': f"${d['market_cap']/1e12:.2f}T" if d.get('market_cap') else "β€”",
474
- 'P/E': f"{d['pe_ratio']:.1f}" if d.get('pe_ratio') else "β€”",
475
- 'Change': f"{'β–²' if chg >= 0 else 'β–Ό'} {abs(chg):.2f}%",
476
- })
477
-
478
- return (cards, fig_p, fig_v, pd.DataFrame(rows),
479
- banner(f"βœ… Data refreshed at {ts}", "success"))
480
-
481
-
482
- def render_historical(symbol, period):
483
- df = get_historical(symbol, period)
484
- if df.empty:
485
- return None, None, None, banner("⚠ No data available.", "alert")
486
-
487
- fig_c = go.Figure()
488
- fig_c.add_trace(go.Candlestick(
489
- x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['Close'],
490
- increasing=dict(line=dict(color=GREEN), fillcolor="rgba(13,156,91,0.22)"),
491
- decreasing=dict(line=dict(color=RED), fillcolor="rgba(224,49,49,0.22)"),
492
- name="Price",
493
- ))
494
- ma20 = df['Close'].rolling(20).mean()
495
- fig_c.add_trace(go.Scatter(x=df.index, y=ma20, name="MA 20",
496
- line=dict(color=BLUE_PRIMARY, width=1.8, dash='dot')))
497
- apply_theme(fig_c, title_text=f"{symbol} β€” Candlestick Chart ({period})", yaxis_title="Price (USD)", extra={"xaxis_rangeslider_visible": False})
498
-
499
- returns = df['Close'].pct_change().dropna()
500
- cum_ret = (1 + returns).cumprod() - 1
501
- col_ret = GREEN if cum_ret.iloc[-1] >= 0 else RED
502
- fig_r = go.Figure()
503
- fig_r.add_trace(go.Scatter(
504
- x=cum_ret.index, y=cum_ret * 100, fill='tozeroy',
505
- fillcolor=f"rgba(13,156,91,0.10)" if cum_ret.iloc[-1] >= 0 else "rgba(224,49,49,0.10)",
506
- line=dict(color=col_ret, width=2.2), name="Cumulative Return",
507
- hovertemplate="%{x|%b %d, %Y}<br>Return: %{y:.2f}%<extra></extra>",
508
- ))
509
- fig_r.add_hline(y=0, line=dict(color=TEXT_LIGHT, dash='dash', width=1))
510
- apply_theme(fig_r, title_text="Cumulative Return (%)", yaxis_title="Return (%)")
511
-
512
- vcols = [GREEN if c >= o else RED for c, o in zip(df['Close'], df['Open'])]
513
- fig_v = go.Figure()
514
- fig_v.add_trace(go.Bar(x=df.index, y=df['Volume'], marker_color=vcols, name="Volume",
515
- hovertemplate="%{x|%b %d}<br>Vol: %{y:,.0f}<extra></extra>"))
516
- apply_theme(fig_v, title_text="Volume (Green = Up Day, Red = Down Day)", yaxis_title="Volume")
517
-
518
- total = cum_ret.iloc[-1] * 100
519
- sign = "β–²" if total >= 0 else "β–Ό"
520
- col = "green" if total >= 0 else "red"
521
- stats = f"""<div class="metric-grid metric-grid-4">
522
- {kpi_card("Current Price", f"${df['Close'].iloc[-1]:.2f}")}
523
- {kpi_card("Period High", f"${df['High'].max():.2f}", color="green")}
524
- {kpi_card("Period Low", f"${df['Low'].min():.2f}", color="red")}
525
- {kpi_card("Total Return", f"{sign} {abs(total):.2f}%", color=col)}
526
- </div>"""
527
-
528
- return fig_c, fig_r, fig_v, stats
529
-
530
-
531
- def render_risk(symbol, portfolio_value):
532
- returns = get_returns(symbol, "1y")
533
- if returns.empty:
534
- return banner("⚠ Could not fetch data.", "alert"), None, None, None, None
535
-
536
- m = compute_risk(returns, portfolio_value)
537
- slabel, scol = sharpe_label(m['sharpe'])
538
- risk_ok = m['annual_vol'] < 0.30 and m['max_dd'] < 0.20 and m['sharpe'] > 1.0
539
- badge = '<span class="badge badge-ok">βœ“ Within Limits</span>' if risk_ok else \
540
- '<span class="badge badge-alert">⚠ Risk Alert</span>'
541
-
542
- kpi_html = f"""
543
- {section_header("πŸ›‘οΈ", f"Risk Assessment β€” {symbol}",
544
- f"Portfolio Value: ${portfolio_value:,.0f} | {len(returns)} trading days")}
545
- <div style="margin-bottom:14px">{badge}</div>
546
- <div class="metric-grid metric-grid-4" style="margin-bottom:14px">
547
- {kpi_card("VaR 95%", f"{m['var_95']:.2%}", f"βˆ’${m['var_95_usd']:,.0f}/day", "red")}
548
- {kpi_card("VaR 99%", f"{m['var_99']:.2%}", f"βˆ’${m['var_99_usd']:,.0f}/day", "red")}
549
- {kpi_card("CVaR 95%", f"{m['cvar_95']:.2%}", "Expected Shortfall", "red")}
550
- {kpi_card("Annual Vol", f"{m['annual_vol']:.2%}", f"Daily: {m['daily_vol']:.2%}",
551
- "gold" if m['annual_vol'] > 0.25 else "blue")}
552
- </div>
553
- <div class="metric-grid metric-grid-4">
554
- {kpi_card("Max Drawdown", f"{m['max_dd']:.2%}", "Peak-to-Trough",
555
- "red" if m['max_dd'] > 0.20 else "gold")}
556
- {kpi_card("Sharpe Ratio", f"{m['sharpe']:.2f}", slabel, scol)}
557
- {kpi_card("Annual Return", f"{m['ann_ret']:.2%}",
558
- "β–² Positive" if m['ann_ret'] >= 0 else "β–Ό Negative",
559
- "green" if m['ann_ret'] >= 0 else "red")}
560
- {kpi_card("Data Points", str(len(returns)), "Trading Days")}
561
- </div>"""
562
-
563
- # Distribution
564
- fig_dist = go.Figure()
565
- fig_dist.add_trace(go.Histogram(
566
- x=returns * 100, nbinsx=55,
567
- marker=dict(color=BLUE_PRIMARY, opacity=0.75, line=dict(color='white', width=0.5)),
568
- name="Daily Returns",
569
- hovertemplate="Return: %{x:.2f}%<br>Count: %{y}<extra></extra>",
570
- ))
571
- fig_dist.add_vline(x=-m['var_95']*100, line=dict(color=GOLD, dash='dash', width=2),
572
- annotation=dict(text="VaR 95%", font=dict(color=GOLD, size=10)))
573
- fig_dist.add_vline(x=-m['var_99']*100, line=dict(color=RED, dash='dash', width=2),
574
- annotation=dict(text="VaR 99%", font=dict(color=RED, size=10)))
575
- apply_theme(fig_dist, title_text="Return Distribution with VaR Lines", xaxis_title="Daily Return (%)", yaxis_title="Frequency")
576
-
577
- # Drawdown
578
- dd = m['drawdown']
579
- fig_dd = go.Figure()
580
- fig_dd.add_trace(go.Scatter(
581
- x=dd.index, y=dd * 100, fill='tozeroy',
582
- fillcolor="rgba(224,49,49,0.13)",
583
- line=dict(color=RED, width=1.8), name="Drawdown %",
584
- hovertemplate="%{x|%b %d, %Y}<br>Drawdown: %{y:.2f}%<extra></extra>",
585
- ))
586
- fig_dd.add_hline(y=-m['max_dd']*100, line=dict(color=GOLD, dash='dot', width=1.5),
587
- annotation=dict(text=f"Max DD {m['max_dd']:.2%}", font=dict(color=GOLD, size=10)))
588
- apply_theme(fig_dd, title_text="Underwater Drawdown Chart", yaxis_title="Drawdown (%)")
589
-
590
- # Rolling vol
591
- rv = returns.rolling(21).std() * np.sqrt(252) * 100
592
- fig_rv = go.Figure()
593
- fig_rv.add_trace(go.Scatter(
594
- x=rv.index, y=rv, fill='tozeroy', fillcolor="rgba(26,95,202,0.09)",
595
- line=dict(color=BLUE_PRIMARY, width=2), name="21-day Vol",
596
- hovertemplate="%{x|%b %d, %Y}<br>Vol: %{y:.2f}%<extra></extra>",
597
- ))
598
- fig_rv.add_hline(y=30, line=dict(color=RED, dash='dash', width=1.3),
599
- annotation=dict(text="Risk Limit 30%", font=dict(color=RED, size=10)))
600
- apply_theme(fig_rv, title_text="Rolling 21-Day Annualised Volatility", yaxis_title="Volatility (%)")
601
-
602
- # Gauge
603
- risk_score = min(100, m['annual_vol']/0.5*40 + m['max_dd']/0.5*40 + max(0,1-m['sharpe'])*20)
604
- gcol = GREEN if risk_score < 40 else GOLD if risk_score < 70 else RED
605
- fig_g = go.Figure(go.Indicator(
606
- mode="gauge+number",
607
- value=risk_score,
608
- title=dict(text="RISK SCORE", font=dict(family="Inter", size=13, color=TEXT_MED)),
609
- number=dict(font=dict(family="Space Grotesk", size=38, color=gcol)),
610
- gauge=dict(
611
- axis=dict(range=[0,100], tickwidth=1, tickcolor=TEXT_LIGHT,
612
- tickfont=dict(family="Inter", size=10, color=TEXT_LIGHT)),
613
- bar=dict(color=gcol, thickness=0.25),
614
- bgcolor=BG_CARD, borderwidth=1, bordercolor=BORDER,
615
- steps=[dict(range=[0,40], color="rgba(13,156,91,0.08)"),
616
- dict(range=[40,70], color="rgba(212,148,10,0.08)"),
617
- dict(range=[70,100],color="rgba(224,49,49,0.08)")],
618
- threshold=dict(line=dict(color=gcol, width=3), thickness=0.75, value=risk_score),
619
- ),
620
- ))
621
- fig_g.update_layout(paper_bgcolor=BG_CARD, font=dict(family="Inter", color=TEXT_DARK),
622
- height=260, margin=dict(l=30,r=30,t=60,b=20))
623
-
624
- return kpi_html, fig_dist, fig_dd, fig_rv, fig_g
625
-
626
-
627
- SCENARIOS = {
628
- "Moderate βˆ’5%": -0.05,
629
- "Correction βˆ’10%": -0.10,
630
- "Bear Market βˆ’20%": -0.20,
631
- "Severe βˆ’30%": -0.30,
632
- "2008 Crisis βˆ’50%": -0.50,
633
- "COVID βˆ’35%": -0.35,
634
- "Flash Crash βˆ’10%": -0.10,
635
- "Rate Shock βˆ’15%": -0.15,
636
- }
637
-
638
- def render_stress(symbol, portfolio_value):
639
- returns = get_returns(symbol, "1y")
640
- avg_daily = returns.mean() if not returns.empty else 0.0003
641
-
642
- rows, pcts, dloss, labels = [], [], [], []
643
- for name, shock in SCENARIOS.items():
644
- shocked = portfolio_value * (1 + shock)
645
- loss = portfolio_value - shocked
646
- days_rec = abs(shock) / avg_daily if avg_daily > 0 else float('inf')
647
- yrs_rec = round(days_rec/252, 1) if days_rec != float('inf') else None
648
- rows.append({'Scenario': name, 'Market Shock': f"{shock:.0%}",
649
- 'Portfolio After': f"${shocked:,.0f}", 'Loss Amount': f"${loss:,.0f}",
650
- 'Recovery (yrs)': str(yrs_rec) if yrs_rec else "N/A"})
651
- pcts.append(shock * 100)
652
- dloss.append(loss)
653
- labels.append(name)
654
-
655
- def sev(l):
656
- if l < -30: return RED
657
- if l < -15: return GOLD
658
- return BLUE_PRIMARY
659
-
660
- fig_pct = go.Figure()
661
- fig_pct.add_trace(go.Bar(
662
- x=labels, y=pcts,
663
- marker=dict(color=[sev(l) for l in pcts], line=dict(color='white', width=1)),
664
- text=[f"{l:.0f}%" for l in pcts], textposition='outside',
665
- textfont=dict(size=10, color=TEXT_DARK),
666
- hovertemplate="<b>%{x}</b><br>Loss: %{y:.1f}%<extra></extra>",
667
- ))
668
- apply_theme(fig_pct, title_text="Portfolio Loss % by Scenario", yaxis_title="Loss (%)", extra={"yaxis": dict(**AXIS_STYLE, range=[min(pcts)*1.3, 5])})
669
-
670
- fig_usd = go.Figure()
671
- fig_usd.add_trace(go.Bar(
672
- x=labels, y=dloss,
673
- marker=dict(color=dloss, colorscale=[[0,BLUE_LIGHT],[0.5,GOLD],[1,RED]],
674
- showscale=False, line=dict(color='white', width=1)),
675
- text=[f"${l:,.0f}" for l in dloss], textposition='outside',
676
- textfont=dict(size=10, color=TEXT_DARK),
677
- hovertemplate="<b>%{x}</b><br>Loss: $%{y:,.0f}<extra></extra>",
678
- ))
679
- apply_theme(fig_usd, title_text="Dollar Loss by Scenario", yaxis_title="Loss ($)")
680
-
681
- return fig_pct, fig_usd, pd.DataFrame(rows)
682
-
683
-
684
- def render_correlation(symbols_str):
685
- syms = [s.strip().upper() for s in symbols_str.split(',') if s.strip()]
686
- if len(syms) < 2:
687
- return None, banner("⚠ Enter at least 2 comma-separated symbols.", "alert"), None
688
-
689
- all_ret = {}
690
- for s in syms:
691
- r = get_returns(s, "1y")
692
- if not r.empty:
693
- all_ret[s] = r
694
-
695
- if len(all_ret) < 2:
696
- return None, banner("⚠ Could not fetch data for enough symbols.", "alert"), None
697
-
698
- df_ret = pd.DataFrame(all_ret).dropna()
699
- corr = df_ret.corr()
700
-
701
- fig_h = go.Figure(go.Heatmap(
702
- z=corr.values, x=corr.columns.tolist(), y=corr.index.tolist(),
703
- colorscale=[[0, RED],[0.5,"#f0f4fb"],[1, BLUE_PRIMARY]],
704
- zmid=0, zmin=-1, zmax=1,
705
- text=corr.values.round(2), texttemplate="%{text}",
706
- textfont=dict(family="Inter", size=13, color=TEXT_DARK),
707
- hovertemplate="<b>%{x} vs %{y}</b><br>r = %{z:.3f}<extra></extra>",
708
- colorbar=dict(tickfont=dict(family="Inter", color=TEXT_MED),
709
- title=dict(text="r", font=dict(color=TEXT_MED))),
710
- ))
711
- apply_theme(fig_h, title_text="Correlation Matrix β€” 1 Year Daily Returns")
712
-
713
- cum_df = (1 + df_ret).cumprod() - 1
714
- palette = [BLUE_PRIMARY, GREEN, GOLD, RED, "#7c3aed", "#db2777", "#0891b2"]
715
- fig_cr = go.Figure()
716
- for i, col in enumerate(cum_df.columns):
717
- fig_cr.add_trace(go.Scatter(
718
- x=cum_df.index, y=cum_df[col]*100, name=col,
719
- line=dict(color=palette[i % len(palette)], width=2.2),
720
- hovertemplate=f"<b>{col}</b><br>%{{x|%b %d}}<br>Return: %{{y:.2f}}%<extra></extra>",
721
- ))
722
- apply_theme(fig_cr, title_text="Cumulative Returns Comparison (%)", yaxis_title="Return (%)")
723
-
724
- avg_corr = corr.values[np.triu_indices_from(corr.values, k=1)].mean()
725
- if avg_corr < 0.5:
726
- msg, kind = f"βœ… Well Diversified β€” avg correlation: {avg_corr:.3f}", "success"
727
- elif avg_corr < 0.7:
728
- msg, kind = f"⚠ Moderately Correlated β€” avg correlation: {avg_corr:.3f}", "info"
729
- else:
730
- msg, kind = f"πŸ”΄ Highly Correlated β€” Low Diversification Benefit (r={avg_corr:.3f})", "alert"
731
-
732
- return fig_h, banner(msg, kind), fig_cr
733
-
734
-
735
- def render_monte_carlo(symbol, portfolio_value, days, sims):
736
- days, sims = int(days), int(sims)
737
- returns = get_returns(symbol, "1y")
738
- if returns.empty:
739
- return None, banner("⚠ Could not fetch data.", "alert")
740
-
741
- mu, sigma = returns.mean(), returns.std()
742
- np.random.seed(42)
743
- sim_rets = np.random.normal(mu, sigma, (days, sims))
744
- sim_paths = portfolio_value * np.exp(np.cumsum(np.log(1 + sim_rets), axis=0))
745
- final_vals = sim_paths[-1]
746
-
747
- fig = go.Figure()
748
- x_ax = list(range(days))
749
- for i in range(min(200, sims)):
750
- col = "rgba(13,156,91,0.13)" if sim_paths[-1,i] >= portfolio_value else "rgba(224,49,49,0.10)"
751
- fig.add_trace(go.Scatter(x=x_ax, y=sim_paths[:,i], mode='lines',
752
- line=dict(color=col, width=0.5),
753
- showlegend=False, hoverinfo='skip'))
754
-
755
- med_path = np.median(sim_paths, axis=1)
756
- fig.add_trace(go.Scatter(x=x_ax, y=med_path, mode='lines',
757
- line=dict(color=BLUE_PRIMARY, width=2.8), name="Median Path"))
758
-
759
- p5 = np.percentile(sim_paths, 5, axis=1)
760
- p95 = np.percentile(sim_paths, 95, axis=1)
761
- fig.add_trace(go.Scatter(
762
- x=x_ax + x_ax[::-1], y=list(p95)+list(p5[::-1]),
763
- fill='toself', fillcolor="rgba(26,95,202,0.07)",
764
- line=dict(color='rgba(0,0,0,0)'), name="90% Confidence Band",
765
- ))
766
- fig.add_hline(y=portfolio_value, line=dict(color=TEXT_LIGHT, dash='dash', width=1.5),
767
- annotation=dict(text="Initial Value", font=dict(color=TEXT_LIGHT, size=10)))
768
- apply_theme(fig, title_text=f"Monte Carlo β€” {sims} Paths over {days} Trading Days", yaxis_title="Portfolio Value ($)", xaxis_title="Trading Day")
769
-
770
- med_fin = np.median(final_vals)
771
- p5_fin = np.percentile(final_vals, 5)
772
- p95_fin = np.percentile(final_vals, 95)
773
- pct_profit = (final_vals >= portfolio_value).mean() * 100
774
- med_ret = (med_fin / portfolio_value - 1) * 100
775
- sign = "β–²" if med_ret >= 0 else "β–Ό"
776
-
777
- stats = f"""<div class="metric-grid metric-grid-4" style="margin-top:16px">
778
- {kpi_card("Median Outcome", f"${med_fin:,.0f}",
779
- f"{sign} {abs(med_ret):.1f}%", "green" if med_ret >= 0 else "red")}
780
- {kpi_card("Best Case (95th)", f"${p95_fin:,.0f}",
781
- f"+{(p95_fin/portfolio_value-1)*100:.1f}%", "green")}
782
- {kpi_card("Worst Case (5th)", f"${p5_fin:,.0f}",
783
- f"{(p5_fin/portfolio_value-1)*100:.1f}%", "red")}
784
- {kpi_card("% Profitable", f"{pct_profit:.1f}%",
785
- f"of {sims} simulations", "green" if pct_profit >= 50 else "red")}
786
- </div>"""
787
-
788
- return fig, stats
789
-
790
- # ═══════════════════════════════════════════════
791
- # BUILD APP
792
- # ═══════════════════════════════════════════════
793
-
794
- HEADER_HTML = """
795
- <div class="app-header">
796
- <div class="app-title">⬑ Portfolio Optimization Intelligence System</div>
797
- <div class="header-accent"></div>
798
- <div class="app-subtitle">Multi-Agent Risk &amp; Market Analytics Platform &nbsp;Β·&nbsp; v3.0</div>
799
- <div class="app-team">Team: Ashwini &nbsp;|&nbsp; Dibyendu Sarkar &nbsp;|&nbsp; Jyoti Ranjan Sethi &nbsp;|&nbsp; IIT Madras 2026</div>
800
- </div>
801
- """
802
-
803
- FOOTER_HTML = """
804
- <div class="app-footer">
805
- ⬑ Portfolio Intelligence System &nbsp;|&nbsp; IIT Madras 2026 &nbsp;|&nbsp;
806
- Data Source: Yahoo Finance &nbsp;|&nbsp; For Educational Use Only
807
- </div>
808
- """
809
-
810
- with gr.Blocks(title="Portfolio Intelligence System") as demo:
811
-
812
- gr.HTML(HEADER_HTML)
813
-
814
- with gr.Row():
815
- shared_symbol = gr.Dropdown(choices=SYMBOLS, value="AAPL",
816
- label="πŸ“Œ Stock Symbol", scale=2)
817
- shared_period = gr.Dropdown(choices=["1mo","3mo","6mo","1y","2y","5y"],
818
- value="1y", label="πŸ“… Period", scale=1)
819
- shared_portfolio = gr.Number(value=100_000, label="πŸ’° Portfolio Value ($)",
820
- minimum=1000, scale=2)
821
-
822
- with gr.Tabs():
823
-
824
- with gr.Tab("πŸ“‘ Market Overview"):
825
- overview_btn = gr.Button("πŸ”„ Refresh Market Data", variant="primary", size="lg")
826
- status_out = gr.HTML()
827
- cards_out = gr.HTML()
828
- with gr.Row():
829
- price_out = gr.Plot(label="Stock Prices")
830
- vol_out = gr.Plot(label="Trading Volume")
831
- with gr.Accordion("πŸ“‹ Detailed Price Table", open=False):
832
- table_out = gr.Dataframe(interactive=False)
833
- overview_btn.click(fn=render_market_overview,
834
- outputs=[cards_out, price_out, vol_out, table_out, status_out])
835
-
836
- with gr.Tab("πŸ“ˆ Historical Analysis"):
837
- hist_btn = gr.Button("πŸ“ˆ Load Historical Data", variant="primary", size="lg")
838
- hist_stat = gr.HTML()
839
- candle = gr.Plot(label="Candlestick + MA20")
840
- ret_chart = gr.Plot(label="Cumulative Return")
841
- vol_hist = gr.Plot(label="Volume")
842
- hist_btn.click(fn=render_historical,
843
- inputs=[shared_symbol, shared_period],
844
- outputs=[candle, ret_chart, vol_hist, hist_stat])
845
-
846
- with gr.Tab("πŸ›‘οΈ Risk Assessment"):
847
- risk_btn = gr.Button("πŸ” Calculate Risk Metrics", variant="primary", size="lg")
848
- risk_kpi = gr.HTML()
849
- with gr.Row():
850
- gauge_out = gr.Plot(label="Risk Score Gauge")
851
- dist_out = gr.Plot(label="Return Distribution")
852
- dd_out = gr.Plot(label="Drawdown Chart")
853
- rv_out = gr.Plot(label="Rolling Volatility")
854
- risk_btn.click(fn=render_risk,
855
- inputs=[shared_symbol, shared_portfolio],
856
- outputs=[risk_kpi, dist_out, dd_out, rv_out, gauge_out])
857
-
858
- with gr.Tab("πŸ’₯ Stress Testing"):
859
- stress_btn = gr.Button("πŸ’₯ Run Stress Tests", variant="primary", size="lg")
860
- with gr.Row():
861
- spct = gr.Plot(label="Loss % by Scenario")
862
- susd = gr.Plot(label="Dollar Loss by Scenario")
863
- with gr.Accordion("πŸ“‹ Full Stress Test Table", open=True):
864
- stbl = gr.Dataframe(interactive=False)
865
- stress_btn.click(fn=render_stress,
866
- inputs=[shared_symbol, shared_portfolio],
867
- outputs=[spct, susd, stbl])
868
-
869
- with gr.Tab("πŸ”— Correlation"):
870
- sym_in = gr.Textbox(value="AAPL,GOOGL,MSFT,TSLA,AMZN",
871
- label="Symbols (comma-separated)")
872
- corr_btn = gr.Button("πŸ”— Compute Correlations", variant="primary", size="lg")
873
- corr_inf = gr.HTML()
874
- heat_out = gr.Plot(label="Correlation Heatmap")
875
- cmp_out = gr.Plot(label="Cumulative Return Comparison")
876
- corr_btn.click(fn=render_correlation, inputs=[sym_in],
877
- outputs=[heat_out, corr_inf, cmp_out])
878
-
879
- with gr.Tab("🎲 Monte Carlo"):
880
- with gr.Row():
881
- mc_days = gr.Slider(21, 504, value=252, step=21, label="Simulation Days")
882
- mc_sims = gr.Slider(100, 1000, value=500, step=100, label="Simulations")
883
- mc_btn = gr.Button("🎲 Run Monte Carlo Simulation", variant="primary", size="lg")
884
- mc_stats = gr.HTML()
885
- mc_chart = gr.Plot(label="Simulation Paths")
886
- mc_btn.click(fn=render_monte_carlo,
887
- inputs=[shared_symbol, shared_portfolio, mc_days, mc_sims],
888
- outputs=[mc_chart, mc_stats])
889
-
890
- with gr.Tab("ℹ️ About"):
891
- gr.Markdown("""
892
- ## ⬑ Portfolio Intelligence System
893
-
894
- A **multi-agent AI-powered platform** for comprehensive portfolio risk analysis.
895
-
896
- ---
897
-
898
- ### 🧩 Modules
899
-
900
- | Module | Description |
901
- |---|---|
902
- | **Market Overview** | Real-time prices, volume, and market cap |
903
- | **Historical Analysis** | Candlestick charts, MA20, cumulative returns |
904
- | **Risk Assessment** | VaR, CVaR, Sharpe, Drawdown, Rolling Vol, Gauge |
905
- | **Stress Testing** | 8 crash scenarios with loss & recovery analysis |
906
- | **Correlation** | Heatmap + cumulative return comparison |
907
- | **Monte Carlo** | Up to 1000-path simulation with confidence bands |
908
-
909
- ---
910
-
911
- ### πŸ—οΈ Architecture
912
- ```
913
- Yahoo Finance API β†’ Market Data Agent β†’ SQLite DB
914
- ↓
915
- Risk Management Agent
916
- ↓
917
- Gradio Dashboard UI
918
- ```
919
-
920
- ---
921
- **Team:** Ashwini Β· Dibyendu Sarkar Β· Jyoti Ranjan Sethi
922
- **Week:** 3 of 16 Β· IIT Madras Β· February 2026
923
- ⚠️ *For educational purposes only β€” not financial advice.*
924
- """)
925
-
926
- gr.HTML(FOOTER_HTML)
927
-
928
-
929
- if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
930
  demo.launch()
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ import plotly.graph_objects as go
5
+ from datetime import datetime
6
+ import yfinance as yf
7
+
8
+ # ─────────────────────────────────────────────
9
+ # Color Palette
10
+ # ─────────────────────────────────────────────
11
+ BG_CARD = "#ffffff"
12
+ BORDER = "#e2e8f0"
13
+ BLUE_PRIMARY = "#2563eb"
14
+ BLUE_DARK = "#1e3a8a"
15
+ BLUE_LIGHT = "#eff6ff"
16
+ GREEN = "#059669"
17
+ RED = "#dc2626"
18
+ GOLD = "#d97706"
19
+ TEXT_DARK = "#0f172a"
20
+ TEXT_MED = "#475569"
21
+ TEXT_LIGHT = "#94a3b8"
22
+
23
+ PLOTLY_THEME = dict(
24
+ paper_bgcolor="#ffffff",
25
+ plot_bgcolor="#f8faff",
26
+ font=dict(family="DM Sans, sans-serif", color=TEXT_DARK, size=12),
27
+ legend=dict(bgcolor="rgba(255,255,255,0.95)", bordercolor=BORDER,
28
+ borderwidth=1, font=dict(color=TEXT_DARK)),
29
+ margin=dict(l=55, r=30, t=55, b=45),
30
+ )
31
+ AXIS_STYLE = dict(
32
+ gridcolor="#e8f0fb", zerolinecolor="#cbd5e1",
33
+ tickfont=dict(color=TEXT_LIGHT), linecolor=BORDER,
34
+ )
35
+
36
+ # ─────────────────────────────────────────────
37
+ # CSS
38
+ # ─────────────────────────────────────────────
39
+ CUSTOM_CSS = """
40
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600;700&family=Syne:wght@600;700;800&family=DM+Mono:wght@400;500&display=swap');
41
+
42
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
43
+
44
+ body, .gradio-container, .gradio-container * {
45
+ font-family: 'DM Sans', sans-serif !important;
46
+ }
47
+
48
+ /* ── Page background ── */
49
+ .gradio-container {
50
+ background: #f1f5f9 !important;
51
+ max-width: 100% !important;
52
+ padding: 0 !important;
53
+ }
54
+
55
+ /* ── Hero header ── */
56
+ .pf-hero {
57
+ background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 50%, #1d4ed8 100%);
58
+ padding: 40px 48px 32px;
59
+ position: relative;
60
+ overflow: hidden;
61
+ border-bottom: 3px solid #f59e0b;
62
+ }
63
+ .pf-hero::before {
64
+ content: '';
65
+ position: absolute;
66
+ top: -60px; right: -60px;
67
+ width: 320px; height: 320px;
68
+ background: radial-gradient(circle, rgba(99,102,241,0.15) 0%, transparent 70%);
69
+ border-radius: 50%;
70
+ }
71
+ .pf-hero::after {
72
+ content: '';
73
+ position: absolute;
74
+ bottom: -40px; left: 10%;
75
+ width: 200px; height: 200px;
76
+ background: radial-gradient(circle, rgba(251,191,36,0.08) 0%, transparent 70%);
77
+ border-radius: 50%;
78
+ }
79
+ .pf-logo {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ gap: 12px;
83
+ margin-bottom: 14px;
84
+ }
85
+ .pf-logo-hex {
86
+ width: 44px; height: 44px;
87
+ background: linear-gradient(135deg, #f59e0b, #fbbf24);
88
+ clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
89
+ display: flex; align-items: center; justify-content: center;
90
+ font-size: 20px; font-weight: 800; color: #0f172a;
91
+ }
92
+ .pf-title {
93
+ font-family: 'Syne', sans-serif !important;
94
+ font-size: 1.9rem;
95
+ font-weight: 800;
96
+ color: #ffffff !important;
97
+ letter-spacing: -0.5px;
98
+ line-height: 1.1;
99
+ }
100
+ .pf-title span { color: #fbbf24; }
101
+ .pf-sub {
102
+ font-size: 0.78rem;
103
+ color: rgba(255,255,255,0.55) !important;
104
+ letter-spacing: 2.5px;
105
+ text-transform: uppercase;
106
+ margin-top: 10px;
107
+ }
108
+ .pf-pills {
109
+ display: flex;
110
+ gap: 8px;
111
+ margin-top: 16px;
112
+ flex-wrap: wrap;
113
+ }
114
+ .pf-pill {
115
+ padding: 4px 12px;
116
+ border-radius: 20px;
117
+ font-size: 0.68rem;
118
+ font-weight: 600;
119
+ letter-spacing: 0.8px;
120
+ text-transform: uppercase;
121
+ border: 1px solid rgba(255,255,255,0.15);
122
+ color: rgba(255,255,255,0.75) !important;
123
+ background: rgba(255,255,255,0.07);
124
+ }
125
+
126
+ /* ── Control bar ── */
127
+ .pf-controls {
128
+ background: #ffffff;
129
+ border-bottom: 1px solid #e2e8f0;
130
+ padding: 14px 24px;
131
+ box-shadow: 0 2px 8px rgba(15,23,42,0.06);
132
+ }
133
+
134
+ /* ── Tabs ── */
135
+ div[role="tablist"] {
136
+ background: #ffffff !important;
137
+ border-bottom: 2px solid #e2e8f0 !important;
138
+ padding: 0 20px !important;
139
+ gap: 0 !important;
140
+ }
141
+ div[role="tab"] {
142
+ font-family: 'DM Sans', sans-serif !important;
143
+ font-size: 0.81rem !important;
144
+ font-weight: 500 !important;
145
+ color: #64748b !important;
146
+ border: none !important;
147
+ border-bottom: 3px solid transparent !important;
148
+ padding: 14px 18px !important;
149
+ background: transparent !important;
150
+ border-radius: 0 !important;
151
+ transition: all 0.2s ease !important;
152
+ white-space: nowrap !important;
153
+ }
154
+ div[role="tab"]:hover {
155
+ color: #2563eb !important;
156
+ background: #eff6ff !important;
157
+ }
158
+ div[role="tab"][aria-selected="true"] {
159
+ color: #2563eb !important;
160
+ border-bottom: 3px solid #2563eb !important;
161
+ font-weight: 700 !important;
162
+ background: #eff6ff !important;
163
+ }
164
+
165
+ /* ── Tab content wrapper ── */
166
+ .tab-content-wrap {
167
+ padding: 24px;
168
+ background: #f1f5f9;
169
+ min-height: 400px;
170
+ }
171
+
172
+ /* ── KPI cards ── */
173
+ .kpi-row {
174
+ display: grid;
175
+ gap: 14px;
176
+ margin-bottom: 20px;
177
+ }
178
+ .kpi-row-5 { grid-template-columns: repeat(5, 1fr); }
179
+ .kpi-row-4 { grid-template-columns: repeat(4, 1fr); }
180
+ .kpi-row-3 { grid-template-columns: repeat(3, 1fr); }
181
+
182
+ .kpi {
183
+ background: #ffffff;
184
+ border: 1px solid #e2e8f0;
185
+ border-radius: 14px;
186
+ padding: 18px 20px 16px;
187
+ position: relative;
188
+ overflow: hidden;
189
+ box-shadow: 0 1px 6px rgba(15,23,42,0.05);
190
+ transition: box-shadow 0.2s, transform 0.2s;
191
+ }
192
+ .kpi:hover {
193
+ box-shadow: 0 6px 20px rgba(15,23,42,0.10);
194
+ transform: translateY(-2px);
195
+ }
196
+ .kpi-accent {
197
+ position: absolute;
198
+ top: 0; left: 0; right: 0;
199
+ height: 3px;
200
+ background: linear-gradient(90deg, #2563eb, #60a5fa);
201
+ border-radius: 14px 14px 0 0;
202
+ }
203
+ .kpi-accent.g { background: linear-gradient(90deg, #059669, #34d399); }
204
+ .kpi-accent.r { background: linear-gradient(90deg, #dc2626, #f87171); }
205
+ .kpi-accent.o { background: linear-gradient(90deg, #d97706, #fbbf24); }
206
+
207
+ .kpi-label {
208
+ font-size: 0.68rem;
209
+ font-weight: 600;
210
+ color: #94a3b8;
211
+ text-transform: uppercase;
212
+ letter-spacing: 1.2px;
213
+ margin-bottom: 8px;
214
+ }
215
+ .kpi-val {
216
+ font-family: 'Syne', sans-serif !important;
217
+ font-size: 1.55rem;
218
+ font-weight: 700;
219
+ color: #0f172a;
220
+ line-height: 1;
221
+ }
222
+ .kpi-val.g { color: #059669; }
223
+ .kpi-val.r { color: #dc2626; }
224
+ .kpi-val.o { color: #d97706; }
225
+ .kpi-sub {
226
+ font-size: 0.71rem;
227
+ color: #94a3b8;
228
+ margin-top: 5px;
229
+ font-weight: 500;
230
+ }
231
+ .kpi-sub.g { color: #059669; }
232
+ .kpi-sub.r { color: #dc2626; }
233
+
234
+ /* ── Section header ── */
235
+ .sec-hdr {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 10px;
239
+ margin-bottom: 16px;
240
+ padding-bottom: 12px;
241
+ border-bottom: 1px solid #e2e8f0;
242
+ }
243
+ .sec-hdr-icon {
244
+ width: 34px; height: 34px;
245
+ background: #eff6ff;
246
+ border-radius: 8px;
247
+ display: flex; align-items: center; justify-content: center;
248
+ font-size: 16px;
249
+ }
250
+ .sec-hdr-text { flex: 1; }
251
+ .sec-hdr-title {
252
+ font-family: 'Syne', sans-serif !important;
253
+ font-size: 0.95rem;
254
+ font-weight: 700;
255
+ color: #0f172a;
256
+ }
257
+ .sec-hdr-sub {
258
+ font-size: 0.72rem;
259
+ color: #94a3b8;
260
+ margin-top: 2px;
261
+ }
262
+
263
+ /* ── Banners ── */
264
+ .bn {
265
+ border-radius: 10px;
266
+ padding: 11px 16px;
267
+ font-size: 0.81rem;
268
+ font-weight: 500;
269
+ margin-bottom: 14px;
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 8px;
273
+ }
274
+ .bn-ok { background: #ecfdf5; border: 1px solid #6ee7b7; color: #065f46; border-left: 4px solid #059669; }
275
+ .bn-warn { background: #fffbeb; border: 1px solid #fcd34d; color: #78350f; border-left: 4px solid #d97706; }
276
+ .bn-err { background: #fef2f2; border: 1px solid #fca5a5; color: #7f1d1d; border-left: 4px solid #dc2626; }
277
+ .bn-info { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e3a8a; border-left: 4px solid #2563eb; }
278
+
279
+ /* ── Badge ── */
280
+ .bdg {
281
+ display: inline-flex; align-items: center; gap: 5px;
282
+ padding: 5px 14px;
283
+ border-radius: 20px;
284
+ font-size: 0.72rem; font-weight: 700;
285
+ letter-spacing: 0.6px; text-transform: uppercase;
286
+ margin-bottom: 14px;
287
+ }
288
+ .bdg-ok { background: #ecfdf5; color: #065f46; border: 1px solid #6ee7b7; }
289
+ .bdg-warn { background: #fef2f2; color: #991b1b; border: 1px solid #fca5a5; }
290
+
291
+ /* ── Chart wrapper ── */
292
+ .chart-card {
293
+ background: #ffffff;
294
+ border: 1px solid #e2e8f0;
295
+ border-radius: 14px;
296
+ overflow: hidden;
297
+ box-shadow: 0 1px 6px rgba(15,23,42,0.04);
298
+ margin-bottom: 16px;
299
+ }
300
+
301
+ /* ── Buttons ── */
302
+ button.primary, .gr-button-primary {
303
+ font-family: 'DM Sans', sans-serif !important;
304
+ font-weight: 700 !important;
305
+ font-size: 0.83rem !important;
306
+ letter-spacing: 0.3px !important;
307
+ background: linear-gradient(135deg, #1d4ed8, #2563eb) !important;
308
+ color: #ffffff !important;
309
+ border: none !important;
310
+ border-radius: 10px !important;
311
+ box-shadow: 0 4px 14px rgba(37,99,235,0.30) !important;
312
+ transition: all 0.2s !important;
313
+ padding: 10px 24px !important;
314
+ }
315
+ button.primary:hover {
316
+ background: linear-gradient(135deg, #1e40af, #1d4ed8) !important;
317
+ box-shadow: 0 6px 20px rgba(37,99,235,0.40) !important;
318
+ transform: translateY(-1px) !important;
319
+ }
320
+
321
+ /* ── Inputs / dropdowns ── */
322
+ input, select, textarea, .gr-dropdown {
323
+ font-family: 'DM Sans', sans-serif !important;
324
+ background: #ffffff !important;
325
+ border: 1.5px solid #e2e8f0 !important;
326
+ border-radius: 10px !important;
327
+ color: #0f172a !important;
328
+ font-size: 0.88rem !important;
329
+ transition: border-color 0.2s, box-shadow 0.2s !important;
330
+ }
331
+ input:focus, select:focus {
332
+ border-color: #2563eb !important;
333
+ box-shadow: 0 0 0 3px rgba(37,99,235,0.10) !important;
334
+ outline: none !important;
335
+ }
336
+ label {
337
+ font-family: 'DM Sans', sans-serif !important;
338
+ font-size: 0.73rem !important;
339
+ font-weight: 600 !important;
340
+ color: #475569 !important;
341
+ letter-spacing: 0.6px !important;
342
+ text-transform: uppercase !important;
343
+ }
344
+
345
+ /* ── Tables ── */
346
+ table {
347
+ border-collapse: separate !important;
348
+ border-spacing: 0 !important;
349
+ width: 100% !important;
350
+ background: #ffffff !important;
351
+ border-radius: 12px !important;
352
+ overflow: hidden !important;
353
+ border: 1px solid #e2e8f0 !important;
354
+ font-size: 0.82rem !important;
355
+ box-shadow: 0 1px 6px rgba(15,23,42,0.04) !important;
356
+ }
357
+ th {
358
+ background: #1e3a8a !important;
359
+ color: #ffffff !important;
360
+ font-size: 0.70rem !important;
361
+ font-weight: 700 !important;
362
+ letter-spacing: 1px !important;
363
+ text-transform: uppercase !important;
364
+ padding: 12px 16px !important;
365
+ white-space: nowrap !important;
366
+ }
367
+ td {
368
+ color: #0f172a !important;
369
+ padding: 10px 16px !important;
370
+ border-bottom: 1px solid #f1f5f9 !important;
371
+ background: #ffffff !important;
372
+ font-family: 'DM Mono', monospace !important;
373
+ font-size: 0.80rem !important;
374
+ }
375
+ tr:nth-child(even) td { background: #f8fafc !important; }
376
+ tr:hover td { background: #eff6ff !important; }
377
+
378
+ /* ── Slider ── */
379
+ input[type="range"] { accent-color: #2563eb !important; }
380
+
381
+ /* ── Footer ── */
382
+ .pf-footer {
383
+ background: #0f172a;
384
+ color: rgba(255,255,255,0.4);
385
+ text-align: center;
386
+ padding: 20px;
387
+ font-size: 0.70rem;
388
+ letter-spacing: 1.8px;
389
+ text-transform: uppercase;
390
+ margin-top: 32px;
391
+ border-top: 2px solid #1e3a8a;
392
+ }
393
+
394
+ /* ── Scrollbar ── */
395
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
396
+ ::-webkit-scrollbar-track { background: #f1f5f9; }
397
+ ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
398
+ ::-webkit-scrollbar-thumb:hover { background: #2563eb; }
399
+
400
+ /* ── Accordion ── */
401
+ .gr-accordion {
402
+ background: #ffffff !important;
403
+ border: 1px solid #e2e8f0 !important;
404
+ border-radius: 12px !important;
405
+ overflow: hidden !important;
406
+ }
407
+
408
+ /* ── Responsive ── */
409
+ @media (max-width: 768px) {
410
+ .kpi-row-5 { grid-template-columns: repeat(2, 1fr) !important; }
411
+ .kpi-row-4 { grid-template-columns: repeat(2, 1fr) !important; }
412
+ .pf-title { font-size: 1.4rem !important; }
413
+ .pf-hero { padding: 24px 20px !important; }
414
+ }
415
+ """
416
+
417
+ # ─────────────────────────────────────────────
418
+ # HTML helpers
419
+ # ─────────────────────────────────────────────
420
+ def kpi(label, value, sub="", accent="b"):
421
+ ac = {"g": "g", "r": "r", "o": "o"}.get(accent, "")
422
+ vc = ac
423
+ sc = "g" if ("β–²" in sub or "+" in sub) else "r" if ("β–Ό" in sub or (sub.startswith("-") and sub != "-")) else ""
424
+ s_html = f'<div class="kpi-sub {sc}">{sub}</div>' if sub else ""
425
+ return f"""<div class="kpi">
426
+ <div class="kpi-accent {ac}"></div>
427
+ <div class="kpi-label">{label}</div>
428
+ <div class="kpi-val {vc}">{value}</div>
429
+ {s_html}
430
+ </div>"""
431
+
432
+ def sec(icon, title, sub=""):
433
+ s = f'<div class="sec-hdr-sub">{sub}</div>' if sub else ""
434
+ return f"""<div class="sec-hdr">
435
+ <div class="sec-hdr-icon">{icon}</div>
436
+ <div class="sec-hdr-text">
437
+ <div class="sec-hdr-title">{title}</div>
438
+ {s}
439
+ </div>
440
+ </div>"""
441
+
442
+ def banner(msg, kind="info"):
443
+ icons = {"ok": "βœ…", "warn": "⚠️", "err": "🚫", "info": "ℹ️"}
444
+ css = {"ok": "bn-ok", "warn": "bn-warn", "err": "bn-err", "info": "bn-info"}
445
+ return f'<div class="bn {css.get(kind,"bn-info")}">{icons.get(kind,"ℹ️")} {msg}</div>'
446
+
447
+ SYMBOLS = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN', 'RELIANCE.NS']
448
+
449
+ # ─────────────────────���───────────────────────
450
+ # Data helpers
451
+ # ─────────────────────────────────────────────
452
+ def get_realtime(symbols):
453
+ results = {}
454
+ for sym in symbols:
455
+ try:
456
+ info = yf.Ticker(sym).info
457
+ price = info.get('currentPrice') or info.get('regularMarketPrice') or 0
458
+ open_ = info.get('regularMarketOpen') or price
459
+ results[sym] = {
460
+ 'symbol': sym, 'price': price, 'open': open_,
461
+ 'high': info.get('dayHigh', 0), 'low': info.get('dayLow', 0),
462
+ 'volume': info.get('volume', 0), 'market_cap': info.get('marketCap', 0),
463
+ 'pe_ratio': info.get('trailingPE', 0),
464
+ 'change_pct': ((price - open_) / open_ * 100) if open_ else 0,
465
+ }
466
+ except Exception:
467
+ results[sym] = {k: 0 for k in ['price','open','high','low','volume','market_cap','pe_ratio','change_pct']}
468
+ results[sym]['symbol'] = sym
469
+ return results
470
+
471
+ def get_historical(symbol, period="1y"):
472
+ try:
473
+ df = yf.Ticker(symbol).history(period=period)
474
+ return df if not df.empty else pd.DataFrame()
475
+ except Exception:
476
+ return pd.DataFrame()
477
+
478
+ def get_returns(symbol, period="1y"):
479
+ df = get_historical(symbol, period)
480
+ if df.empty:
481
+ return pd.Series(dtype=float)
482
+ return df['Close'].pct_change().dropna()
483
+
484
+ def compute_risk(returns, portfolio_value, rf=0.04):
485
+ if returns.empty:
486
+ return {}
487
+ daily_vol = returns.std()
488
+ annual_vol = daily_vol * np.sqrt(252)
489
+ total_ret = (1 + returns).prod() - 1
490
+ years = max(len(returns) / 252, 0.01)
491
+ ann_ret = (1 + total_ret) ** (1 / years) - 1
492
+ sharpe = (ann_ret - rf) / annual_vol if annual_vol else 0
493
+ var_95 = abs(np.percentile(returns, 5))
494
+ var_99 = abs(np.percentile(returns, 1))
495
+ tail = returns[returns <= -var_95]
496
+ cvar_95 = abs(tail.mean()) if len(tail) else var_95
497
+ cum = (1 + returns).cumprod()
498
+ peak = cum.cummax()
499
+ dd = (cum - peak) / peak
500
+ max_dd = abs(dd.min())
501
+ return dict(
502
+ annual_vol=annual_vol, daily_vol=daily_vol, ann_ret=ann_ret, sharpe=sharpe,
503
+ var_95=var_95, var_99=var_99, cvar_95=cvar_95,
504
+ var_95_usd=var_95*portfolio_value, var_99_usd=var_99*portfolio_value,
505
+ cvar_95_usd=cvar_95*portfolio_value, max_dd=max_dd, drawdown=dd, returns=returns,
506
+ )
507
+
508
+ def sharpe_label(s):
509
+ if s > 3: return "Exceptional", "g"
510
+ if s > 2: return "Very Good", "g"
511
+ if s > 1: return "Good", "b"
512
+ if s > 0.5: return "Acceptable", "o"
513
+ if s > 0: return "Poor", "o"
514
+ return "Losing Money", "r"
515
+
516
+ def apply_theme(fig, title_text=None, yaxis_title=None, xaxis_title=None, extra=None):
517
+ layout = dict(**PLOTLY_THEME)
518
+ layout['xaxis'] = dict(**AXIS_STYLE)
519
+ layout['yaxis'] = dict(**AXIS_STYLE)
520
+ if title_text:
521
+ layout['title'] = dict(text=title_text, font=dict(color="#0f172a", size=13, family="Syne, sans-serif"))
522
+ if yaxis_title: layout['yaxis']['title'] = dict(text=yaxis_title, font=dict(color=TEXT_MED))
523
+ if xaxis_title: layout['xaxis']['title'] = dict(text=xaxis_title, font=dict(color=TEXT_MED))
524
+ if extra: layout.update(extra)
525
+ fig.update_layout(**layout)
526
+ return fig
527
+
528
+ # ═══════════════════════════════════════════════
529
+ # TAB RENDER FUNCTIONS
530
+ # ═══════════════════════════════════════════════
531
+
532
+ def render_market_overview():
533
+ data = get_realtime(SYMBOLS)
534
+ ts = datetime.now().strftime('%d %b %Y %H:%M:%S')
535
+
536
+ # KPI cards
537
+ cards = '<div class="kpi-row kpi-row-5" style="margin-bottom:20px">'
538
+ for sym, d in data.items():
539
+ chg = d.get('change_pct', 0)
540
+ sign = "β–²" if chg >= 0 else "β–Ό"
541
+ acc = "g" if chg >= 0 else "r"
542
+ display_sym = sym.replace('.NS', '')
543
+ cards += kpi(display_sym, f"${d['price']:.2f}" if d['price'] else "β€”",
544
+ f"{sign} {abs(chg):.2f}%", acc)
545
+ cards += "</div>"
546
+
547
+ prices = {s.replace('.NS',''): d['price'] for s, d in data.items() if d['price']}
548
+ changes = {s.replace('.NS',''): d['change_pct'] for s, d in data.items()}
549
+ bcolors = [GREEN if changes.get(s,0) >= 0 else RED for s in prices]
550
+
551
+ fig_p = go.Figure()
552
+ fig_p.add_trace(go.Bar(
553
+ x=list(prices.keys()), y=list(prices.values()),
554
+ marker=dict(color=bcolors, line=dict(color='white', width=1.5),
555
+ opacity=0.9),
556
+ text=[f"${v:.2f}" for v in prices.values()],
557
+ textposition='outside', textfont=dict(size=11, color=TEXT_DARK, family="DM Mono"),
558
+ hovertemplate="<b>%{x}</b><br>Price: $%{y:.2f}<extra></extra>",
559
+ ))
560
+ apply_theme(fig_p, title_text="Current Stock Prices (USD)", yaxis_title="Price ($)",
561
+ extra={"showlegend": False, "bargap": 0.3})
562
+
563
+ vols = {s.replace('.NS',''): d.get('volume', 0) for s, d in data.items()}
564
+ fig_v = go.Figure()
565
+ fig_v.add_trace(go.Bar(
566
+ x=list(vols.keys()), y=list(vols.values()),
567
+ marker=dict(color=list(vols.values()),
568
+ colorscale=[[0,"#bfdbfe"],[1,"#1d4ed8"]],
569
+ showscale=False, line=dict(color='white', width=1.5), opacity=0.9),
570
+ text=[f"{v/1e6:.1f}M" for v in vols.values()],
571
+ textposition='outside', textfont=dict(size=11, color=TEXT_DARK, family="DM Mono"),
572
+ hovertemplate="<b>%{x}</b><br>Volume: %{y:,.0f}<extra></extra>",
573
+ ))
574
+ apply_theme(fig_v, title_text="Trading Volume", yaxis_title="Volume",
575
+ extra={"showlegend": False, "bargap": 0.3})
576
+
577
+ rows = []
578
+ for s, d in data.items():
579
+ chg = d.get('change_pct', 0)
580
+ rows.append({
581
+ 'Symbol': s.replace('.NS',''),
582
+ 'Price ($)': f"${d['price']:.2f}" if d['price'] else "β€”",
583
+ 'Open ($)': f"${d['open']:.2f}" if d['open'] else "β€”",
584
+ 'High ($)': f"${d['high']:.2f}" if d['high'] else "β€”",
585
+ 'Low ($)': f"${d['low']:.2f}" if d['low'] else "β€”",
586
+ 'Volume': f"{d['volume']/1e6:.1f}M" if d['volume'] else "β€”",
587
+ 'Mkt Cap': f"${d['market_cap']/1e12:.2f}T" if d.get('market_cap') else "β€”",
588
+ 'P/E': f"{d['pe_ratio']:.1f}" if d.get('pe_ratio') else "β€”",
589
+ 'Change': f"{'β–²' if chg >= 0 else 'β–Ό'} {abs(chg):.2f}%",
590
+ })
591
+
592
+ return (
593
+ cards, fig_p, fig_v, pd.DataFrame(rows),
594
+ banner(f"Data refreshed at {ts}", "ok")
595
+ )
596
+
597
+
598
+ def render_historical(symbol, period):
599
+ df = get_historical(symbol, period)
600
+ if df.empty:
601
+ return None, None, None, banner("No data available for this symbol/period.", "err")
602
+
603
+ fig_c = go.Figure()
604
+ fig_c.add_trace(go.Candlestick(
605
+ x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['Close'],
606
+ increasing=dict(line=dict(color=GREEN, width=1.2), fillcolor="rgba(5,150,105,0.20)"),
607
+ decreasing=dict(line=dict(color=RED, width=1.2), fillcolor="rgba(220,38,38,0.20)"),
608
+ name="OHLC",
609
+ ))
610
+ ma20 = df['Close'].rolling(20).mean()
611
+ fig_c.add_trace(go.Scatter(
612
+ x=df.index, y=ma20, name="MA 20",
613
+ line=dict(color=BLUE_PRIMARY, width=1.8, dash='dot'),
614
+ ))
615
+ apply_theme(fig_c, title_text=f"{symbol} β€” Price Chart with MA20 ({period})",
616
+ yaxis_title="Price (USD)", extra={"xaxis_rangeslider_visible": False})
617
+
618
+ returns = df['Close'].pct_change().dropna()
619
+ cum_ret = (1 + returns).cumprod() - 1
620
+ col_ret = GREEN if cum_ret.iloc[-1] >= 0 else RED
621
+ fig_r = go.Figure()
622
+ fig_r.add_trace(go.Scatter(
623
+ x=cum_ret.index, y=cum_ret * 100, fill='tozeroy',
624
+ fillcolor="rgba(5,150,105,0.08)" if cum_ret.iloc[-1] >= 0 else "rgba(220,38,38,0.08)",
625
+ line=dict(color=col_ret, width=2.2), name="Cumulative Return",
626
+ hovertemplate="%{x|%b %d, %Y}<br>Return: %{y:.2f}%<extra></extra>",
627
+ ))
628
+ fig_r.add_hline(y=0, line=dict(color=TEXT_LIGHT, dash='dash', width=1))
629
+ apply_theme(fig_r, title_text="Cumulative Return (%)", yaxis_title="Return (%)")
630
+
631
+ vcols = [GREEN if c >= o else RED for c, o in zip(df['Close'], df['Open'])]
632
+ fig_v = go.Figure()
633
+ fig_v.add_trace(go.Bar(
634
+ x=df.index, y=df['Volume'], marker_color=vcols, name="Volume",
635
+ opacity=0.8,
636
+ hovertemplate="%{x|%b %d}<br>Vol: %{y:,.0f}<extra></extra>",
637
+ ))
638
+ apply_theme(fig_v, title_text="Volume (Green = Up Day Β· Red = Down Day)", yaxis_title="Volume")
639
+
640
+ total = cum_ret.iloc[-1] * 100
641
+ sign = "β–²" if total >= 0 else "β–Ό"
642
+ acc = "g" if total >= 0 else "r"
643
+ stats = f"""
644
+ {sec("πŸ“ˆ", f"Historical Analysis β€” {symbol}", f"Period: {period} Β· {len(df)} trading days")}
645
+ <div class="kpi-row kpi-row-4">
646
+ {kpi("Current Price", f"${df['Close'].iloc[-1]:.2f}", "", "b")}
647
+ {kpi("Period High", f"${df['High'].max():.2f}", "", "g")}
648
+ {kpi("Period Low", f"${df['Low'].min():.2f}", "", "r")}
649
+ {kpi("Total Return", f"{sign} {abs(total):.2f}%", "", acc)}
650
+ </div>"""
651
+
652
+ return fig_c, fig_r, fig_v, stats
653
+
654
+
655
+ def render_risk(symbol, portfolio_value):
656
+ returns = get_returns(symbol, "1y")
657
+ if returns.empty:
658
+ return banner("Could not fetch data for this symbol.", "err"), None, None, None, None
659
+
660
+ m = compute_risk(returns, portfolio_value)
661
+ slabel, scol = sharpe_label(m['sharpe'])
662
+ risk_ok = m['annual_vol'] < 0.30 and m['max_dd'] < 0.20 and m['sharpe'] > 1.0
663
+ badge = f'<span class="bdg {"bdg-ok" if risk_ok else "bdg-warn"}">{"βœ“ Within Limits" if risk_ok else "⚠ Risk Alert"}</span>'
664
+
665
+ kpi_html = f"""
666
+ {sec("πŸ›‘οΈ", f"Risk Assessment β€” {symbol}", f"Portfolio: ${portfolio_value:,.0f} Β· {len(returns)} trading days")}
667
+ {badge}
668
+ <div class="kpi-row kpi-row-4" style="margin-top:12px">
669
+ {kpi("VaR 95%", f"{m['var_95']:.2%}", f"βˆ’${m['var_95_usd']:,.0f} / day", "r")}
670
+ {kpi("VaR 99%", f"{m['var_99']:.2%}", f"βˆ’${m['var_99_usd']:,.0f} / day", "r")}
671
+ {kpi("CVaR 95%", f"{m['cvar_95']:.2%}", "Expected Shortfall", "r")}
672
+ {kpi("Annual Vol", f"{m['annual_vol']:.2%}", f"Daily: {m['daily_vol']:.2%}",
673
+ "o" if m['annual_vol'] > 0.25 else "b")}
674
+ </div>
675
+ <div class="kpi-row kpi-row-4">
676
+ {kpi("Max Drawdown", f"{m['max_dd']:.2%}", "Peak-to-Trough",
677
+ "r" if m['max_dd'] > 0.20 else "o")}
678
+ {kpi("Sharpe Ratio", f"{m['sharpe']:.2f}", slabel, scol)}
679
+ {kpi("Annual Return", f"{m['ann_ret']:.2%}",
680
+ "β–² Positive" if m['ann_ret'] >= 0 else "β–Ό Negative",
681
+ "g" if m['ann_ret'] >= 0 else "r")}
682
+ {kpi("Data Points", str(len(returns)), "Trading Days", "b")}
683
+ </div>"""
684
+
685
+ # Distribution
686
+ fig_dist = go.Figure()
687
+ fig_dist.add_trace(go.Histogram(
688
+ x=returns * 100, nbinsx=55,
689
+ marker=dict(color=BLUE_PRIMARY, opacity=0.75, line=dict(color='white', width=0.5)),
690
+ name="Daily Returns",
691
+ hovertemplate="Return: %{x:.2f}%<br>Count: %{y}<extra></extra>",
692
+ ))
693
+ fig_dist.add_vline(x=-m['var_95']*100, line=dict(color=GOLD, dash='dash', width=2),
694
+ annotation=dict(text="VaR 95%", font=dict(color=GOLD, size=10)))
695
+ fig_dist.add_vline(x=-m['var_99']*100, line=dict(color=RED, dash='dash', width=2),
696
+ annotation=dict(text="VaR 99%", font=dict(color=RED, size=10)))
697
+ apply_theme(fig_dist, title_text="Return Distribution + VaR Lines",
698
+ xaxis_title="Daily Return (%)", yaxis_title="Frequency")
699
+
700
+ # Drawdown
701
+ dd = m['drawdown']
702
+ fig_dd = go.Figure()
703
+ fig_dd.add_trace(go.Scatter(
704
+ x=dd.index, y=dd * 100, fill='tozeroy',
705
+ fillcolor="rgba(220,38,38,0.10)",
706
+ line=dict(color=RED, width=1.8), name="Drawdown %",
707
+ hovertemplate="%{x|%b %d, %Y}<br>Drawdown: %{y:.2f}%<extra></extra>",
708
+ ))
709
+ fig_dd.add_hline(y=-m['max_dd']*100, line=dict(color=GOLD, dash='dot', width=1.5),
710
+ annotation=dict(text=f"Max DD {m['max_dd']:.2%}", font=dict(color=GOLD, size=10)))
711
+ apply_theme(fig_dd, title_text="Underwater Drawdown Chart", yaxis_title="Drawdown (%)")
712
+
713
+ # Rolling vol
714
+ rv = returns.rolling(21).std() * np.sqrt(252) * 100
715
+ fig_rv = go.Figure()
716
+ fig_rv.add_trace(go.Scatter(
717
+ x=rv.index, y=rv, fill='tozeroy', fillcolor="rgba(37,99,235,0.08)",
718
+ line=dict(color=BLUE_PRIMARY, width=2), name="21-day Vol",
719
+ hovertemplate="%{x|%b %d, %Y}<br>Vol: %{y:.2f}%<extra></extra>",
720
+ ))
721
+ fig_rv.add_hline(y=30, line=dict(color=RED, dash='dash', width=1.3),
722
+ annotation=dict(text="Risk Limit 30%", font=dict(color=RED, size=10)))
723
+ apply_theme(fig_rv, title_text="Rolling 21-Day Annualised Volatility", yaxis_title="Volatility (%)")
724
+
725
+ # Gauge
726
+ risk_score = min(100, m['annual_vol']/0.5*40 + m['max_dd']/0.5*40 + max(0,1-m['sharpe'])*20)
727
+ gcol = GREEN if risk_score < 40 else GOLD if risk_score < 70 else RED
728
+ fig_g = go.Figure(go.Indicator(
729
+ mode="gauge+number",
730
+ value=risk_score,
731
+ title=dict(text="COMPOSITE RISK SCORE", font=dict(family="DM Sans", size=11, color=TEXT_MED)),
732
+ number=dict(font=dict(family="Syne", size=36, color=gcol)),
733
+ gauge=dict(
734
+ axis=dict(range=[0,100], tickwidth=1, tickcolor=TEXT_LIGHT,
735
+ tickfont=dict(family="DM Sans", size=10, color=TEXT_LIGHT)),
736
+ bar=dict(color=gcol, thickness=0.22),
737
+ bgcolor="#ffffff", borderwidth=1, bordercolor=BORDER,
738
+ steps=[dict(range=[0,40], color="rgba(5,150,105,0.07)"),
739
+ dict(range=[40,70], color="rgba(217,119,6,0.07)"),
740
+ dict(range=[70,100],color="rgba(220,38,38,0.07)")],
741
+ threshold=dict(line=dict(color=gcol, width=3), thickness=0.75, value=risk_score),
742
+ ),
743
+ ))
744
+ fig_g.update_layout(paper_bgcolor="#ffffff", font=dict(family="DM Sans", color=TEXT_DARK),
745
+ height=260, margin=dict(l=30,r=30,t=60,b=20))
746
+
747
+ return kpi_html, fig_dist, fig_dd, fig_rv, fig_g
748
+
749
+
750
+ SCENARIOS = {
751
+ "Moderate βˆ’5%": -0.05,
752
+ "Correction βˆ’10%": -0.10,
753
+ "Bear Market βˆ’20%": -0.20,
754
+ "Severe βˆ’30%": -0.30,
755
+ "2008 Crisis βˆ’50%": -0.50,
756
+ "COVID βˆ’35%": -0.35,
757
+ "Flash Crash βˆ’10%": -0.10,
758
+ "Rate Shock βˆ’15%": -0.15,
759
+ }
760
+
761
+ def render_stress(symbol, portfolio_value):
762
+ returns = get_returns(symbol, "1y")
763
+ avg_daily = returns.mean() if not returns.empty else 0.0003
764
+
765
+ rows, pcts, dloss, labels = [], [], [], []
766
+ for name, shock in SCENARIOS.items():
767
+ shocked = portfolio_value * (1 + shock)
768
+ loss = portfolio_value - shocked
769
+ days_rec = abs(shock) / avg_daily if avg_daily > 0 else float('inf')
770
+ yrs_rec = round(days_rec/252, 1) if days_rec != float('inf') else None
771
+ rows.append({'Scenario': name, 'Market Shock': f"{shock:.0%}",
772
+ 'Portfolio After': f"${shocked:,.0f}",
773
+ 'Loss Amount': f"${loss:,.0f}",
774
+ 'Recovery (yrs)': str(yrs_rec) if yrs_rec else "N/A"})
775
+ pcts.append(shock * 100)
776
+ dloss.append(loss)
777
+ labels.append(name)
778
+
779
+ def sev(l):
780
+ if l < -30: return RED
781
+ if l < -15: return GOLD
782
+ return BLUE_PRIMARY
783
+
784
+ fig_pct = go.Figure()
785
+ fig_pct.add_trace(go.Bar(
786
+ x=labels, y=pcts,
787
+ marker=dict(color=[sev(l) for l in pcts], line=dict(color='white', width=1),
788
+ opacity=0.85),
789
+ text=[f"{l:.0f}%" for l in pcts], textposition='outside',
790
+ textfont=dict(size=10, color=TEXT_DARK, family="DM Mono"),
791
+ hovertemplate="<b>%{x}</b><br>Loss: %{y:.1f}%<extra></extra>",
792
+ ))
793
+ apply_theme(fig_pct, title_text="Portfolio Loss % by Scenario", yaxis_title="Loss (%)",
794
+ extra={"yaxis": dict(**AXIS_STYLE, range=[min(pcts)*1.3, 5]), "bargap": 0.3})
795
+
796
+ fig_usd = go.Figure()
797
+ fig_usd.add_trace(go.Bar(
798
+ x=labels, y=dloss,
799
+ marker=dict(color=dloss, colorscale=[[0,"#bfdbfe"],[0.5,GOLD],[1,RED]],
800
+ showscale=False, line=dict(color='white', width=1), opacity=0.85),
801
+ text=[f"${l:,.0f}" for l in dloss], textposition='outside',
802
+ textfont=dict(size=10, color=TEXT_DARK, family="DM Mono"),
803
+ hovertemplate="<b>%{x}</b><br>Loss: $%{y:,.0f}<extra></extra>",
804
+ ))
805
+ apply_theme(fig_usd, title_text="Dollar Loss by Scenario", yaxis_title="Loss ($)",
806
+ extra={"bargap": 0.3})
807
+
808
+ return fig_pct, fig_usd, pd.DataFrame(rows)
809
+
810
+
811
+ def render_correlation(symbols_str):
812
+ syms = [s.strip().upper() for s in symbols_str.split(',') if s.strip()]
813
+ if len(syms) < 2:
814
+ return None, banner("Enter at least 2 comma-separated symbols.", "warn"), None
815
+
816
+ all_ret = {}
817
+ for s in syms:
818
+ r = get_returns(s, "1y")
819
+ if not r.empty:
820
+ all_ret[s] = r
821
+
822
+ if len(all_ret) < 2:
823
+ return None, banner("Could not fetch data for enough symbols.", "err"), None
824
+
825
+ df_ret = pd.DataFrame(all_ret).dropna()
826
+ corr = df_ret.corr()
827
+
828
+ fig_h = go.Figure(go.Heatmap(
829
+ z=corr.values, x=corr.columns.tolist(), y=corr.index.tolist(),
830
+ colorscale=[[0, RED],[0.5,"#f8fafc"],[1, BLUE_PRIMARY]],
831
+ zmid=0, zmin=-1, zmax=1,
832
+ text=corr.values.round(2), texttemplate="%{text}",
833
+ textfont=dict(family="DM Mono", size=12, color=TEXT_DARK),
834
+ hovertemplate="<b>%{x} vs %{y}</b><br>r = %{z:.3f}<extra></extra>",
835
+ colorbar=dict(tickfont=dict(family="DM Sans", color=TEXT_MED),
836
+ title=dict(text="r", font=dict(color=TEXT_MED))),
837
+ ))
838
+ apply_theme(fig_h, title_text="Correlation Matrix β€” 1Y Daily Returns")
839
+
840
+ cum_df = (1 + df_ret).cumprod() - 1
841
+ palette = [BLUE_PRIMARY, GREEN, GOLD, RED, "#7c3aed", "#db2777", "#0891b2"]
842
+ fig_cr = go.Figure()
843
+ for i, col in enumerate(cum_df.columns):
844
+ fig_cr.add_trace(go.Scatter(
845
+ x=cum_df.index, y=cum_df[col]*100, name=col,
846
+ line=dict(color=palette[i % len(palette)], width=2.2),
847
+ hovertemplate=f"<b>{col}</b><br>%{{x|%b %d}}<br>Return: %{{y:.2f}}%<extra></extra>",
848
+ ))
849
+ apply_theme(fig_cr, title_text="Cumulative Returns Comparison (%)", yaxis_title="Return (%)")
850
+
851
+ avg_corr = corr.values[np.triu_indices_from(corr.values, k=1)].mean()
852
+ if avg_corr < 0.5:
853
+ msg, kind = f"Well Diversified β€” avg pairwise correlation: {avg_corr:.3f}", "ok"
854
+ elif avg_corr < 0.7:
855
+ msg, kind = f"Moderately Correlated β€” avg correlation: {avg_corr:.3f}", "warn"
856
+ else:
857
+ msg, kind = f"Highly Correlated β€” low diversification benefit (r = {avg_corr:.3f})", "err"
858
+
859
+ return fig_h, banner(msg, kind), fig_cr
860
+
861
+
862
+ def render_monte_carlo(symbol, portfolio_value, days, sims):
863
+ days, sims = int(days), int(sims)
864
+ returns = get_returns(symbol, "1y")
865
+ if returns.empty:
866
+ return None, banner("Could not fetch data.", "err")
867
+
868
+ mu, sigma = returns.mean(), returns.std()
869
+ np.random.seed(42)
870
+ sim_rets = np.random.normal(mu, sigma, (days, sims))
871
+ sim_paths = portfolio_value * np.exp(np.cumsum(np.log(1 + sim_rets), axis=0))
872
+ final_vals = sim_paths[-1]
873
+
874
+ fig = go.Figure()
875
+ x_ax = list(range(days))
876
+ for i in range(min(200, sims)):
877
+ col = "rgba(5,150,105,0.10)" if sim_paths[-1,i] >= portfolio_value else "rgba(220,38,38,0.08)"
878
+ fig.add_trace(go.Scatter(x=x_ax, y=sim_paths[:,i], mode='lines',
879
+ line=dict(color=col, width=0.5),
880
+ showlegend=False, hoverinfo='skip'))
881
+
882
+ med_path = np.median(sim_paths, axis=1)
883
+ fig.add_trace(go.Scatter(x=x_ax, y=med_path, mode='lines',
884
+ line=dict(color=BLUE_PRIMARY, width=2.8), name="Median Path"))
885
+
886
+ p5 = np.percentile(sim_paths, 5, axis=1)
887
+ p95 = np.percentile(sim_paths, 95, axis=1)
888
+ fig.add_trace(go.Scatter(
889
+ x=x_ax + x_ax[::-1], y=list(p95)+list(p5[::-1]),
890
+ fill='toself', fillcolor="rgba(37,99,235,0.06)",
891
+ line=dict(color='rgba(0,0,0,0)'), name="90% Confidence Band",
892
+ ))
893
+ fig.add_hline(y=portfolio_value, line=dict(color=TEXT_LIGHT, dash='dash', width=1.5),
894
+ annotation=dict(text="Initial Value", font=dict(color=TEXT_LIGHT, size=10)))
895
+ apply_theme(fig, title_text=f"Monte Carlo Simulation β€” {sims:,} Paths Β· {days} Trading Days",
896
+ yaxis_title="Portfolio Value ($)", xaxis_title="Trading Day")
897
+
898
+ med_fin = np.median(final_vals)
899
+ p5_fin = np.percentile(final_vals, 5)
900
+ p95_fin = np.percentile(final_vals, 95)
901
+ pct_profit = (final_vals >= portfolio_value).mean() * 100
902
+ med_ret = (med_fin / portfolio_value - 1) * 100
903
+ sign = "β–²" if med_ret >= 0 else "β–Ό"
904
+
905
+ stats = f"""
906
+ {sec("🎲", f"Monte Carlo Results β€” {symbol}", f"{sims:,} simulations Β· {days} trading days")}
907
+ <div class="kpi-row kpi-row-4">
908
+ {kpi("Median Outcome", f"${med_fin:,.0f}",
909
+ f"{sign} {abs(med_ret):.1f}%", "g" if med_ret >= 0 else "r")}
910
+ {kpi("Best Case (95th)", f"${p95_fin:,.0f}",
911
+ f"+{(p95_fin/portfolio_value-1)*100:.1f}%", "g")}
912
+ {kpi("Worst Case (5th)", f"${p5_fin:,.0f}",
913
+ f"{(p5_fin/portfolio_value-1)*100:.1f}%", "r")}
914
+ {kpi("% Profitable", f"{pct_profit:.1f}%",
915
+ f"of {sims:,} simulations", "g" if pct_profit >= 50 else "r")}
916
+ </div>"""
917
+
918
+ return fig, stats
919
+
920
+
921
+ # ═══════════════════════════════════════════════
922
+ # BUILD APP
923
+ # ═══════════════════════════════════════════════
924
+
925
+ HEADER_HTML = """
926
+ <div class="pf-hero">
927
+ <div class="pf-logo">
928
+ <div class="pf-logo-hex">⬑</div>
929
+ <div>
930
+ <div class="pf-title">Portfolio <span>Intelligence</span> System</div>
931
+ </div>
932
+ </div>
933
+ <div class="pf-sub">Multi-Agent Risk &amp; Market Analytics Platform Β· v3.0</div>
934
+ <div class="pf-pills">
935
+ <span class="pf-pill">πŸ“‘ Real-Time Data</span>
936
+ <span class="pf-pill">πŸ›‘οΈ Risk Metrics</span>
937
+ <span class="pf-pill">🎲 Monte Carlo</span>
938
+ <span class="pf-pill">πŸ“Š Stress Testing</span>
939
+ <span class="pf-pill">πŸ”— Correlation</span>
940
+ <span class="pf-pill">IIT Madras 2026</span>
941
+ </div>
942
+ </div>
943
+ """
944
+
945
+ FOOTER_HTML = """
946
+ <div class="pf-footer">
947
+ ⬑ Portfolio Intelligence System &nbsp;·&nbsp; IIT Madras 2026 &nbsp;·&nbsp;
948
+ Ashwini Β· Dibyendu Sarkar Β· Jyoti Ranjan Sethi &nbsp;Β·&nbsp;
949
+ Data: Yahoo Finance &nbsp;Β·&nbsp; Educational Use Only
950
+ </div>
951
+ """
952
+
953
+ with gr.Blocks(title="Portfolio Intelligence System", css=CUSTOM_CSS) as demo:
954
+
955
+ gr.HTML(HEADER_HTML)
956
+
957
+ # ── Global controls ──
958
+ with gr.Row(elem_classes=["pf-controls"]):
959
+ shared_symbol = gr.Dropdown(
960
+ choices=['AAPL','GOOGL','MSFT','TSLA','AMZN','RELIANCE.NS'],
961
+ value="AAPL", label="πŸ“Œ Stock Symbol", scale=2,
962
+ )
963
+ shared_period = gr.Dropdown(
964
+ choices=["1mo","3mo","6mo","1y","2y","5y"],
965
+ value="1y", label="πŸ“… Period", scale=1,
966
+ )
967
+ shared_portfolio = gr.Number(
968
+ value=100_000, label="πŸ’° Portfolio Value ($)",
969
+ minimum=1000, scale=2,
970
+ )
971
+
972
+ with gr.Tabs():
973
+
974
+ # ── Tab 1: Market Overview ──
975
+ with gr.Tab("πŸ“‘ Market Overview"):
976
+ with gr.Row():
977
+ overview_btn = gr.Button("πŸ”„ Refresh Market Data", variant="primary", size="lg", scale=1)
978
+ status_out = gr.HTML()
979
+ cards_out = gr.HTML()
980
+ with gr.Row():
981
+ price_out = gr.Plot(label="Stock Prices", show_label=False)
982
+ vol_out = gr.Plot(label="Trading Volume", show_label=False)
983
+ with gr.Accordion("πŸ“‹ Full Price Table", open=False):
984
+ table_out = gr.Dataframe(interactive=False)
985
+ overview_btn.click(
986
+ fn=render_market_overview,
987
+ outputs=[cards_out, price_out, vol_out, table_out, status_out],
988
+ )
989
+
990
+ # ── Tab 2: Historical Analysis ──
991
+ with gr.Tab("πŸ“ˆ Historical Analysis"):
992
+ hist_btn = gr.Button("πŸ“ˆ Load Historical Data", variant="primary", size="lg")
993
+ hist_stat = gr.HTML()
994
+ with gr.Row():
995
+ candle = gr.Plot(label="Candlestick", show_label=False)
996
+ ret_chart = gr.Plot(label="Cumulative Return", show_label=False)
997
+ vol_hist = gr.Plot(label="Volume", show_label=False)
998
+ hist_btn.click(
999
+ fn=render_historical,
1000
+ inputs=[shared_symbol, shared_period],
1001
+ outputs=[candle, ret_chart, vol_hist, hist_stat],
1002
+ )
1003
+
1004
+ # ── Tab 3: Risk Assessment ──
1005
+ with gr.Tab("πŸ›‘οΈ Risk Assessment"):
1006
+ risk_btn = gr.Button("πŸ” Calculate Risk Metrics", variant="primary", size="lg")
1007
+ risk_kpi = gr.HTML()
1008
+ with gr.Row():
1009
+ gauge_out = gr.Plot(label="Risk Gauge", show_label=False)
1010
+ dist_out = gr.Plot(label="Distribution", show_label=False)
1011
+ with gr.Row():
1012
+ dd_out = gr.Plot(label="Drawdown", show_label=False)
1013
+ rv_out = gr.Plot(label="Rolling Vol", show_label=False)
1014
+ risk_btn.click(
1015
+ fn=render_risk,
1016
+ inputs=[shared_symbol, shared_portfolio],
1017
+ outputs=[risk_kpi, dist_out, dd_out, rv_out, gauge_out],
1018
+ )
1019
+
1020
+ # ── Tab 4: Stress Testing ──
1021
+ with gr.Tab("πŸ’₯ Stress Testing"):
1022
+ stress_btn = gr.Button("πŸ’₯ Run Stress Tests", variant="primary", size="lg")
1023
+ with gr.Row():
1024
+ spct = gr.Plot(label="Loss %", show_label=False)
1025
+ susd = gr.Plot(label="Dollar Loss", show_label=False)
1026
+ with gr.Accordion("πŸ“‹ Full Stress Test Table", open=True):
1027
+ stbl = gr.Dataframe(interactive=False)
1028
+ stress_btn.click(
1029
+ fn=render_stress,
1030
+ inputs=[shared_symbol, shared_portfolio],
1031
+ outputs=[spct, susd, stbl],
1032
+ )
1033
+
1034
+ # ── Tab 5: Correlation ──
1035
+ with gr.Tab("πŸ”— Correlation"):
1036
+ with gr.Row():
1037
+ sym_in = gr.Textbox(
1038
+ value="AAPL,GOOGL,MSFT,TSLA,AMZN",
1039
+ label="Symbols (comma-separated)", scale=4,
1040
+ )
1041
+ corr_btn = gr.Button("πŸ”— Compute", variant="primary", scale=1)
1042
+ corr_inf = gr.HTML()
1043
+ with gr.Row():
1044
+ heat_out = gr.Plot(label="Heatmap", show_label=False)
1045
+ cmp_out = gr.Plot(label="Returns Comparison", show_label=False)
1046
+ corr_btn.click(
1047
+ fn=render_correlation,
1048
+ inputs=[sym_in],
1049
+ outputs=[heat_out, corr_inf, cmp_out],
1050
+ )
1051
+
1052
+ # ── Tab 6: Monte Carlo ──
1053
+ with gr.Tab("🎲 Monte Carlo"):
1054
+ with gr.Row():
1055
+ mc_days = gr.Slider(21, 504, value=252, step=21, label="πŸ“… Simulation Days")
1056
+ mc_sims = gr.Slider(100, 1000, value=500, step=100, label="πŸ”’ Simulations")
1057
+ mc_btn = gr.Button("🎲 Run Simulation", variant="primary", size="lg")
1058
+ mc_stats = gr.HTML()
1059
+ mc_chart = gr.Plot(label="Simulation Paths", show_label=False)
1060
+ mc_btn.click(
1061
+ fn=render_monte_carlo,
1062
+ inputs=[shared_symbol, shared_portfolio, mc_days, mc_sims],
1063
+ outputs=[mc_chart, mc_stats],
1064
+ )
1065
+
1066
+ # ── Tab 7: About ──
1067
+ with gr.Tab("ℹ️ About"):
1068
+ gr.Markdown("""
1069
+ ## ⬑ Portfolio Intelligence System
1070
+
1071
+ A **multi-agent AI-powered platform** for comprehensive portfolio risk analysis and market intelligence. Built as part of the IIT Madras Multi-Agent Systems curriculum.
1072
+
1073
+ ---
1074
+
1075
+ ### 🧩 Modules
1076
+
1077
+ | Module | Description |
1078
+ |---|---|
1079
+ | **πŸ“‘ Market Overview** | Real-time prices, volume, P/E, market cap for 6 stocks |
1080
+ | **πŸ“ˆ Historical Analysis** | Candlestick + MA20, cumulative returns, volume |
1081
+ | **πŸ›‘οΈ Risk Assessment** | VaR, CVaR, Sharpe, Max Drawdown, Rolling Vol, Risk Gauge |
1082
+ | **πŸ’₯ Stress Testing** | 8 crash scenarios β€” loss % and dollar impact |
1083
+ | **πŸ”— Correlation** | Correlation heatmap + cumulative return comparison |
1084
+ | **🎲 Monte Carlo** | Up to 1000 simulation paths with confidence bands |
1085
+
1086
+ ---
1087
+
1088
+ ### πŸ—οΈ Architecture
1089
+
1090
+ ```
1091
+ Yahoo Finance API
1092
+ ↓
1093
+ Market Data Agent β†’ SQLite DB
1094
+ ↓
1095
+ Risk Management Agent (RiskIQ)
1096
+ ↓
1097
+ Gradio Dashboard UI
1098
+ ```
1099
+
1100
+ ---
1101
+
1102
+ ### πŸ“ Risk Metrics Reference
1103
+
1104
+ | Metric | Good Value | Description |
1105
+ |---|---|---|
1106
+ | VaR 95% | < 2% | Max 1-day loss with 95% confidence |
1107
+ | CVaR 95% | < 3% | Avg loss when VaR is exceeded |
1108
+ | Sharpe Ratio | > 1.0 | Return per unit of risk |
1109
+ | Max Drawdown | < 20% | Worst peak-to-trough decline |
1110
+ | Annual Volatility | < 25% | Annualised return fluctuation |
1111
+
1112
+ ---
1113
+
1114
+ **Team:** Ashwini Β· Dibyendu Sarkar Β· Jyoti Ranjan Sethi
1115
+ **Program:** Multi-Agent Systems Β· IIT Madras Β· Week 3 of 16 Β· 2026
1116
+
1117
+ > ⚠️ *For educational purposes only. Not financial advice. Data sourced from Yahoo Finance.*
1118
+ """)
1119
+
1120
+ gr.HTML(FOOTER_HTML)
1121
+
1122
+
1123
+ if __name__ == "__main__":
1124
  demo.launch()