| | import gradio as gr |
| | import pandas as pd |
| | import numpy as np |
| | import plotly.graph_objects as go |
| | from datetime import datetime |
| | import yfinance as yf |
| |
|
| | |
| | |
| | |
| | BG_CARD = "#ffffff" |
| | BORDER = "#e2e8f0" |
| | BLUE_PRIMARY = "#2563eb" |
| | BLUE_DARK = "#1e3a8a" |
| | BLUE_LIGHT = "#eff6ff" |
| | GREEN = "#059669" |
| | RED = "#dc2626" |
| | GOLD = "#d97706" |
| | TEXT_DARK = "#0f172a" |
| | TEXT_MED = "#475569" |
| | TEXT_LIGHT = "#94a3b8" |
| |
|
| | PLOTLY_THEME = dict( |
| | paper_bgcolor="#ffffff", |
| | plot_bgcolor="#f8faff", |
| | font=dict(family="DM Sans, sans-serif", color=TEXT_DARK, size=12), |
| | legend=dict(bgcolor="rgba(255,255,255,0.95)", bordercolor=BORDER, |
| | borderwidth=1, font=dict(color=TEXT_DARK)), |
| | margin=dict(l=55, r=30, t=55, b=45), |
| | ) |
| | AXIS_STYLE = dict( |
| | gridcolor="#e8f0fb", zerolinecolor="#cbd5e1", |
| | tickfont=dict(color=TEXT_LIGHT), linecolor=BORDER, |
| | ) |
| |
|
| | |
| | |
| | |
| | CUSTOM_CSS = """ |
| | @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'); |
| | |
| | *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } |
| | |
| | body, .gradio-container, .gradio-container * { |
| | font-family: 'DM Sans', sans-serif !important; |
| | } |
| | |
| | /* ββ Page background ββ */ |
| | .gradio-container { |
| | background: #f1f5f9 !important; |
| | max-width: 100% !important; |
| | padding: 0 !important; |
| | } |
| | |
| | /* ββ Hero header ββ */ |
| | .pf-hero { |
| | background: linear-gradient(135deg, #0f172a 0%, #1e3a8a 50%, #1d4ed8 100%); |
| | padding: 40px 48px 32px; |
| | position: relative; |
| | overflow: hidden; |
| | border-bottom: 3px solid #f59e0b; |
| | } |
| | .pf-hero::before { |
| | content: ''; |
| | position: absolute; |
| | top: -60px; right: -60px; |
| | width: 320px; height: 320px; |
| | background: radial-gradient(circle, rgba(99,102,241,0.15) 0%, transparent 70%); |
| | border-radius: 50%; |
| | } |
| | .pf-hero::after { |
| | content: ''; |
| | position: absolute; |
| | bottom: -40px; left: 10%; |
| | width: 200px; height: 200px; |
| | background: radial-gradient(circle, rgba(251,191,36,0.08) 0%, transparent 70%); |
| | border-radius: 50%; |
| | } |
| | .pf-logo { |
| | display: inline-flex; |
| | align-items: center; |
| | gap: 12px; |
| | margin-bottom: 14px; |
| | } |
| | .pf-logo-hex { |
| | width: 44px; height: 44px; |
| | background: linear-gradient(135deg, #f59e0b, #fbbf24); |
| | clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); |
| | display: flex; align-items: center; justify-content: center; |
| | font-size: 20px; font-weight: 800; color: #0f172a; |
| | } |
| | .pf-title { |
| | font-family: 'Syne', sans-serif !important; |
| | font-size: 1.9rem; |
| | font-weight: 800; |
| | color: #ffffff !important; |
| | letter-spacing: -0.5px; |
| | line-height: 1.1; |
| | } |
| | .pf-title span { color: #fbbf24; } |
| | .pf-sub { |
| | font-size: 0.78rem; |
| | color: rgba(255,255,255,0.55) !important; |
| | letter-spacing: 2.5px; |
| | text-transform: uppercase; |
| | margin-top: 10px; |
| | } |
| | .pf-pills { |
| | display: flex; |
| | gap: 8px; |
| | margin-top: 16px; |
| | flex-wrap: wrap; |
| | } |
| | .pf-pill { |
| | padding: 4px 12px; |
| | border-radius: 20px; |
| | font-size: 0.68rem; |
| | font-weight: 600; |
| | letter-spacing: 0.8px; |
| | text-transform: uppercase; |
| | border: 1px solid rgba(255,255,255,0.15); |
| | color: rgba(255,255,255,0.75) !important; |
| | background: rgba(255,255,255,0.07); |
| | } |
| | |
| | /* ββ Control bar ββ */ |
| | .pf-controls { |
| | background: #ffffff; |
| | border-bottom: 1px solid #e2e8f0; |
| | padding: 14px 24px; |
| | box-shadow: 0 2px 8px rgba(15,23,42,0.06); |
| | } |
| | |
| | /* ββ Tabs ββ */ |
| | div[role="tablist"] { |
| | background: #ffffff !important; |
| | border-bottom: 2px solid #e2e8f0 !important; |
| | padding: 0 20px !important; |
| | gap: 0 !important; |
| | } |
| | div[role="tab"] { |
| | font-family: 'DM Sans', sans-serif !important; |
| | font-size: 0.81rem !important; |
| | font-weight: 500 !important; |
| | color: #64748b !important; |
| | border: none !important; |
| | border-bottom: 3px solid transparent !important; |
| | padding: 14px 18px !important; |
| | background: transparent !important; |
| | border-radius: 0 !important; |
| | transition: all 0.2s ease !important; |
| | white-space: nowrap !important; |
| | } |
| | div[role="tab"]:hover { |
| | color: #2563eb !important; |
| | background: #eff6ff !important; |
| | } |
| | div[role="tab"][aria-selected="true"] { |
| | color: #2563eb !important; |
| | border-bottom: 3px solid #2563eb !important; |
| | font-weight: 700 !important; |
| | background: #eff6ff !important; |
| | } |
| | |
| | /* ββ Tab content wrapper ββ */ |
| | .tab-content-wrap { |
| | padding: 24px; |
| | background: #f1f5f9; |
| | min-height: 400px; |
| | } |
| | |
| | /* ββ KPI cards ββ */ |
| | .kpi-row { |
| | display: grid; |
| | gap: 14px; |
| | margin-bottom: 20px; |
| | } |
| | .kpi-row-5 { grid-template-columns: repeat(5, 1fr); } |
| | .kpi-row-4 { grid-template-columns: repeat(4, 1fr); } |
| | .kpi-row-3 { grid-template-columns: repeat(3, 1fr); } |
| | |
| | .kpi { |
| | background: #ffffff; |
| | border: 1px solid #e2e8f0; |
| | border-radius: 14px; |
| | padding: 18px 20px 16px; |
| | position: relative; |
| | overflow: hidden; |
| | box-shadow: 0 1px 6px rgba(15,23,42,0.05); |
| | transition: box-shadow 0.2s, transform 0.2s; |
| | } |
| | .kpi:hover { |
| | box-shadow: 0 6px 20px rgba(15,23,42,0.10); |
| | transform: translateY(-2px); |
| | } |
| | .kpi-accent { |
| | position: absolute; |
| | top: 0; left: 0; right: 0; |
| | height: 3px; |
| | background: linear-gradient(90deg, #2563eb, #60a5fa); |
| | border-radius: 14px 14px 0 0; |
| | } |
| | .kpi-accent.g { background: linear-gradient(90deg, #059669, #34d399); } |
| | .kpi-accent.r { background: linear-gradient(90deg, #dc2626, #f87171); } |
| | .kpi-accent.o { background: linear-gradient(90deg, #d97706, #fbbf24); } |
| | |
| | .kpi-label { |
| | font-size: 0.68rem; |
| | font-weight: 600; |
| | color: #94a3b8; |
| | text-transform: uppercase; |
| | letter-spacing: 1.2px; |
| | margin-bottom: 8px; |
| | } |
| | .kpi-val { |
| | font-family: 'Syne', sans-serif !important; |
| | font-size: 1.55rem; |
| | font-weight: 700; |
| | color: #0f172a; |
| | line-height: 1; |
| | } |
| | .kpi-val.g { color: #059669; } |
| | .kpi-val.r { color: #dc2626; } |
| | .kpi-val.o { color: #d97706; } |
| | .kpi-sub { |
| | font-size: 0.71rem; |
| | color: #94a3b8; |
| | margin-top: 5px; |
| | font-weight: 500; |
| | } |
| | .kpi-sub.g { color: #059669; } |
| | .kpi-sub.r { color: #dc2626; } |
| | |
| | /* ββ Section header ββ */ |
| | .sec-hdr { |
| | display: flex; |
| | align-items: center; |
| | gap: 10px; |
| | margin-bottom: 16px; |
| | padding-bottom: 12px; |
| | border-bottom: 1px solid #e2e8f0; |
| | } |
| | .sec-hdr-icon { |
| | width: 34px; height: 34px; |
| | background: #eff6ff; |
| | border-radius: 8px; |
| | display: flex; align-items: center; justify-content: center; |
| | font-size: 16px; |
| | } |
| | .sec-hdr-text { flex: 1; } |
| | .sec-hdr-title { |
| | font-family: 'Syne', sans-serif !important; |
| | font-size: 0.95rem; |
| | font-weight: 700; |
| | color: #0f172a; |
| | } |
| | .sec-hdr-sub { |
| | font-size: 0.72rem; |
| | color: #94a3b8; |
| | margin-top: 2px; |
| | } |
| | |
| | /* ββ Banners ββ */ |
| | .bn { |
| | border-radius: 10px; |
| | padding: 11px 16px; |
| | font-size: 0.81rem; |
| | font-weight: 500; |
| | margin-bottom: 14px; |
| | display: flex; |
| | align-items: center; |
| | gap: 8px; |
| | } |
| | .bn-ok { background: #ecfdf5; border: 1px solid #6ee7b7; color: #065f46; border-left: 4px solid #059669; } |
| | .bn-warn { background: #fffbeb; border: 1px solid #fcd34d; color: #78350f; border-left: 4px solid #d97706; } |
| | .bn-err { background: #fef2f2; border: 1px solid #fca5a5; color: #7f1d1d; border-left: 4px solid #dc2626; } |
| | .bn-info { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e3a8a; border-left: 4px solid #2563eb; } |
| | |
| | /* ββ Badge ββ */ |
| | .bdg { |
| | display: inline-flex; align-items: center; gap: 5px; |
| | padding: 5px 14px; |
| | border-radius: 20px; |
| | font-size: 0.72rem; font-weight: 700; |
| | letter-spacing: 0.6px; text-transform: uppercase; |
| | margin-bottom: 14px; |
| | } |
| | .bdg-ok { background: #ecfdf5; color: #065f46; border: 1px solid #6ee7b7; } |
| | .bdg-warn { background: #fef2f2; color: #991b1b; border: 1px solid #fca5a5; } |
| | |
| | /* ββ Chart wrapper ββ */ |
| | .chart-card { |
| | background: #ffffff; |
| | border: 1px solid #e2e8f0; |
| | border-radius: 14px; |
| | overflow: hidden; |
| | box-shadow: 0 1px 6px rgba(15,23,42,0.04); |
| | margin-bottom: 16px; |
| | } |
| | |
| | /* ββ Buttons ββ */ |
| | button.primary, .gr-button-primary { |
| | font-family: 'DM Sans', sans-serif !important; |
| | font-weight: 700 !important; |
| | font-size: 0.83rem !important; |
| | letter-spacing: 0.3px !important; |
| | background: linear-gradient(135deg, #1d4ed8, #2563eb) !important; |
| | color: #ffffff !important; |
| | border: none !important; |
| | border-radius: 10px !important; |
| | box-shadow: 0 4px 14px rgba(37,99,235,0.30) !important; |
| | transition: all 0.2s !important; |
| | padding: 10px 24px !important; |
| | } |
| | button.primary:hover { |
| | background: linear-gradient(135deg, #1e40af, #1d4ed8) !important; |
| | box-shadow: 0 6px 20px rgba(37,99,235,0.40) !important; |
| | transform: translateY(-1px) !important; |
| | } |
| | |
| | /* ββ Inputs / dropdowns ββ */ |
| | input, select, textarea, .gr-dropdown { |
| | font-family: 'DM Sans', sans-serif !important; |
| | background: #ffffff !important; |
| | border: 1.5px solid #e2e8f0 !important; |
| | border-radius: 10px !important; |
| | color: #0f172a !important; |
| | font-size: 0.88rem !important; |
| | transition: border-color 0.2s, box-shadow 0.2s !important; |
| | } |
| | input:focus, select:focus { |
| | border-color: #2563eb !important; |
| | box-shadow: 0 0 0 3px rgba(37,99,235,0.10) !important; |
| | outline: none !important; |
| | } |
| | label { |
| | font-family: 'DM Sans', sans-serif !important; |
| | font-size: 0.73rem !important; |
| | font-weight: 600 !important; |
| | color: #475569 !important; |
| | letter-spacing: 0.6px !important; |
| | text-transform: uppercase !important; |
| | } |
| | |
| | /* ββ Tables ββ */ |
| | table { |
| | border-collapse: separate !important; |
| | border-spacing: 0 !important; |
| | width: 100% !important; |
| | background: #ffffff !important; |
| | border-radius: 12px !important; |
| | overflow: hidden !important; |
| | border: 1px solid #e2e8f0 !important; |
| | font-size: 0.82rem !important; |
| | box-shadow: 0 1px 6px rgba(15,23,42,0.04) !important; |
| | } |
| | th { |
| | background: #1e3a8a !important; |
| | color: #ffffff !important; |
| | font-size: 0.70rem !important; |
| | font-weight: 700 !important; |
| | letter-spacing: 1px !important; |
| | text-transform: uppercase !important; |
| | padding: 12px 16px !important; |
| | white-space: nowrap !important; |
| | } |
| | td { |
| | color: #0f172a !important; |
| | padding: 10px 16px !important; |
| | border-bottom: 1px solid #f1f5f9 !important; |
| | background: #ffffff !important; |
| | font-family: 'DM Mono', monospace !important; |
| | font-size: 0.80rem !important; |
| | } |
| | tr:nth-child(even) td { background: #f8fafc !important; } |
| | tr:hover td { background: #eff6ff !important; } |
| | |
| | /* ββ Slider ββ */ |
| | input[type="range"] { accent-color: #2563eb !important; } |
| | |
| | /* ββ Footer ββ */ |
| | .pf-footer { |
| | background: #0f172a; |
| | color: rgba(255,255,255,0.4); |
| | text-align: center; |
| | padding: 20px; |
| | font-size: 0.70rem; |
| | letter-spacing: 1.8px; |
| | text-transform: uppercase; |
| | margin-top: 32px; |
| | border-top: 2px solid #1e3a8a; |
| | } |
| | |
| | /* ββ Scrollbar ββ */ |
| | ::-webkit-scrollbar { width: 5px; height: 5px; } |
| | ::-webkit-scrollbar-track { background: #f1f5f9; } |
| | ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } |
| | ::-webkit-scrollbar-thumb:hover { background: #2563eb; } |
| | |
| | /* ββ Accordion ββ */ |
| | .gr-accordion { |
| | background: #ffffff !important; |
| | border: 1px solid #e2e8f0 !important; |
| | border-radius: 12px !important; |
| | overflow: hidden !important; |
| | } |
| | |
| | /* ββ Responsive ββ */ |
| | @media (max-width: 768px) { |
| | .kpi-row-5 { grid-template-columns: repeat(2, 1fr) !important; } |
| | .kpi-row-4 { grid-template-columns: repeat(2, 1fr) !important; } |
| | .pf-title { font-size: 1.4rem !important; } |
| | .pf-hero { padding: 24px 20px !important; } |
| | } |
| | """ |
| |
|
| | |
| | |
| | |
| | def kpi(label, value, sub="", accent="b"): |
| | ac = {"g": "g", "r": "r", "o": "o"}.get(accent, "") |
| | vc = ac |
| | sc = "g" if ("β²" in sub or "+" in sub) else "r" if ("βΌ" in sub or (sub.startswith("-") and sub != "-")) else "" |
| | s_html = f'<div class="kpi-sub {sc}">{sub}</div>' if sub else "" |
| | return f"""<div class="kpi"> |
| | <div class="kpi-accent {ac}"></div> |
| | <div class="kpi-label">{label}</div> |
| | <div class="kpi-val {vc}">{value}</div> |
| | {s_html} |
| | </div>""" |
| |
|
| | def sec(icon, title, sub=""): |
| | s = f'<div class="sec-hdr-sub">{sub}</div>' if sub else "" |
| | return f"""<div class="sec-hdr"> |
| | <div class="sec-hdr-icon">{icon}</div> |
| | <div class="sec-hdr-text"> |
| | <div class="sec-hdr-title">{title}</div> |
| | {s} |
| | </div> |
| | </div>""" |
| |
|
| | def banner(msg, kind="info"): |
| | icons = {"ok": "β
", "warn": "β οΈ", "err": "π«", "info": "βΉοΈ"} |
| | css = {"ok": "bn-ok", "warn": "bn-warn", "err": "bn-err", "info": "bn-info"} |
| | return f'<div class="bn {css.get(kind,"bn-info")}">{icons.get(kind,"βΉοΈ")} {msg}</div>' |
| |
|
| | SYMBOLS = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN', 'RELIANCE.NS'] |
| |
|
| | |
| | |
| | |
| | def get_realtime(symbols): |
| | results = {} |
| | for sym in symbols: |
| | try: |
| | info = yf.Ticker(sym).info |
| | price = info.get('currentPrice') or info.get('regularMarketPrice') or 0 |
| | open_ = info.get('regularMarketOpen') or price |
| | results[sym] = { |
| | 'symbol': sym, 'price': price, 'open': open_, |
| | 'high': info.get('dayHigh', 0), 'low': info.get('dayLow', 0), |
| | 'volume': info.get('volume', 0), 'market_cap': info.get('marketCap', 0), |
| | 'pe_ratio': info.get('trailingPE', 0), |
| | 'change_pct': ((price - open_) / open_ * 100) if open_ else 0, |
| | } |
| | except Exception: |
| | results[sym] = {k: 0 for k in ['price','open','high','low','volume','market_cap','pe_ratio','change_pct']} |
| | results[sym]['symbol'] = sym |
| | return results |
| |
|
| | def get_historical(symbol, period="1y"): |
| | try: |
| | df = yf.Ticker(symbol).history(period=period) |
| | return df if not df.empty else pd.DataFrame() |
| | except Exception: |
| | return pd.DataFrame() |
| |
|
| | def get_returns(symbol, period="1y"): |
| | df = get_historical(symbol, period) |
| | if df.empty: |
| | return pd.Series(dtype=float) |
| | return df['Close'].pct_change().dropna() |
| |
|
| | def compute_risk(returns, portfolio_value, rf=0.04): |
| | if returns.empty: |
| | return {} |
| | daily_vol = returns.std() |
| | annual_vol = daily_vol * np.sqrt(252) |
| | total_ret = (1 + returns).prod() - 1 |
| | years = max(len(returns) / 252, 0.01) |
| | ann_ret = (1 + total_ret) ** (1 / years) - 1 |
| | sharpe = (ann_ret - rf) / annual_vol if annual_vol else 0 |
| | var_95 = abs(np.percentile(returns, 5)) |
| | var_99 = abs(np.percentile(returns, 1)) |
| | tail = returns[returns <= -var_95] |
| | cvar_95 = abs(tail.mean()) if len(tail) else var_95 |
| | cum = (1 + returns).cumprod() |
| | peak = cum.cummax() |
| | dd = (cum - peak) / peak |
| | max_dd = abs(dd.min()) |
| | return dict( |
| | annual_vol=annual_vol, daily_vol=daily_vol, ann_ret=ann_ret, sharpe=sharpe, |
| | var_95=var_95, var_99=var_99, cvar_95=cvar_95, |
| | var_95_usd=var_95*portfolio_value, var_99_usd=var_99*portfolio_value, |
| | cvar_95_usd=cvar_95*portfolio_value, max_dd=max_dd, drawdown=dd, returns=returns, |
| | ) |
| |
|
| | def sharpe_label(s): |
| | if s > 3: return "Exceptional", "g" |
| | if s > 2: return "Very Good", "g" |
| | if s > 1: return "Good", "b" |
| | if s > 0.5: return "Acceptable", "o" |
| | if s > 0: return "Poor", "o" |
| | return "Losing Money", "r" |
| |
|
| | def apply_theme(fig, title_text=None, yaxis_title=None, xaxis_title=None, extra=None): |
| | layout = dict(**PLOTLY_THEME) |
| | layout['xaxis'] = dict(**AXIS_STYLE) |
| | layout['yaxis'] = dict(**AXIS_STYLE) |
| | if title_text: |
| | layout['title'] = dict(text=title_text, font=dict(color="#0f172a", size=13, family="Syne, sans-serif")) |
| | if yaxis_title: layout['yaxis']['title'] = dict(text=yaxis_title, font=dict(color=TEXT_MED)) |
| | if xaxis_title: layout['xaxis']['title'] = dict(text=xaxis_title, font=dict(color=TEXT_MED)) |
| | if extra: layout.update(extra) |
| | fig.update_layout(**layout) |
| | return fig |
| |
|
| | |
| | |
| | |
| |
|
| | def render_market_overview(): |
| | data = get_realtime(SYMBOLS) |
| | ts = datetime.now().strftime('%d %b %Y %H:%M:%S') |
| |
|
| | |
| | cards = '<div class="kpi-row kpi-row-5" style="margin-bottom:20px">' |
| | for sym, d in data.items(): |
| | chg = d.get('change_pct', 0) |
| | sign = "β²" if chg >= 0 else "βΌ" |
| | acc = "g" if chg >= 0 else "r" |
| | display_sym = sym.replace('.NS', '') |
| | cards += kpi(display_sym, f"${d['price']:.2f}" if d['price'] else "β", |
| | f"{sign} {abs(chg):.2f}%", acc) |
| | cards += "</div>" |
| |
|
| | prices = {s.replace('.NS',''): d['price'] for s, d in data.items() if d['price']} |
| | changes = {s.replace('.NS',''): d['change_pct'] for s, d in data.items()} |
| | bcolors = [GREEN if changes.get(s,0) >= 0 else RED for s in prices] |
| |
|
| | fig_p = go.Figure() |
| | fig_p.add_trace(go.Bar( |
| | x=list(prices.keys()), y=list(prices.values()), |
| | marker=dict(color=bcolors, line=dict(color='white', width=1.5), |
| | opacity=0.9), |
| | text=[f"${v:.2f}" for v in prices.values()], |
| | textposition='outside', textfont=dict(size=11, color=TEXT_DARK, family="DM Mono"), |
| | hovertemplate="<b>%{x}</b><br>Price: $%{y:.2f}<extra></extra>", |
| | )) |
| | apply_theme(fig_p, title_text="Current Stock Prices (USD)", yaxis_title="Price ($)", |
| | extra={"showlegend": False, "bargap": 0.3}) |
| |
|
| | vols = {s.replace('.NS',''): d.get('volume', 0) for s, d in data.items()} |
| | fig_v = go.Figure() |
| | fig_v.add_trace(go.Bar( |
| | x=list(vols.keys()), y=list(vols.values()), |
| | marker=dict(color=list(vols.values()), |
| | colorscale=[[0,"#bfdbfe"],[1,"#1d4ed8"]], |
| | showscale=False, line=dict(color='white', width=1.5), opacity=0.9), |
| | text=[f"{v/1e6:.1f}M" for v in vols.values()], |
| | textposition='outside', textfont=dict(size=11, color=TEXT_DARK, family="DM Mono"), |
| | hovertemplate="<b>%{x}</b><br>Volume: %{y:,.0f}<extra></extra>", |
| | )) |
| | apply_theme(fig_v, title_text="Trading Volume", yaxis_title="Volume", |
| | extra={"showlegend": False, "bargap": 0.3}) |
| |
|
| | rows = [] |
| | for s, d in data.items(): |
| | chg = d.get('change_pct', 0) |
| | rows.append({ |
| | 'Symbol': s.replace('.NS',''), |
| | 'Price ($)': f"${d['price']:.2f}" if d['price'] else "β", |
| | 'Open ($)': f"${d['open']:.2f}" if d['open'] else "β", |
| | 'High ($)': f"${d['high']:.2f}" if d['high'] else "β", |
| | 'Low ($)': f"${d['low']:.2f}" if d['low'] else "β", |
| | 'Volume': f"{d['volume']/1e6:.1f}M" if d['volume'] else "β", |
| | 'Mkt Cap': f"${d['market_cap']/1e12:.2f}T" if d.get('market_cap') else "β", |
| | 'P/E': f"{d['pe_ratio']:.1f}" if d.get('pe_ratio') else "β", |
| | 'Change': f"{'β²' if chg >= 0 else 'βΌ'} {abs(chg):.2f}%", |
| | }) |
| |
|
| | return ( |
| | cards, fig_p, fig_v, pd.DataFrame(rows), |
| | banner(f"Data refreshed at {ts}", "ok") |
| | ) |
| |
|
| |
|
| | def render_historical(symbol, period): |
| | df = get_historical(symbol, period) |
| | if df.empty: |
| | return None, None, None, banner("No data available for this symbol/period.", "err") |
| |
|
| | fig_c = go.Figure() |
| | fig_c.add_trace(go.Candlestick( |
| | x=df.index, open=df['Open'], high=df['High'], low=df['Low'], close=df['Close'], |
| | increasing=dict(line=dict(color=GREEN, width=1.2), fillcolor="rgba(5,150,105,0.20)"), |
| | decreasing=dict(line=dict(color=RED, width=1.2), fillcolor="rgba(220,38,38,0.20)"), |
| | name="OHLC", |
| | )) |
| | ma20 = df['Close'].rolling(20).mean() |
| | fig_c.add_trace(go.Scatter( |
| | x=df.index, y=ma20, name="MA 20", |
| | line=dict(color=BLUE_PRIMARY, width=1.8, dash='dot'), |
| | )) |
| | apply_theme(fig_c, title_text=f"{symbol} β Price Chart with MA20 ({period})", |
| | yaxis_title="Price (USD)", extra={"xaxis_rangeslider_visible": False}) |
| |
|
| | returns = df['Close'].pct_change().dropna() |
| | cum_ret = (1 + returns).cumprod() - 1 |
| | col_ret = GREEN if cum_ret.iloc[-1] >= 0 else RED |
| | fig_r = go.Figure() |
| | fig_r.add_trace(go.Scatter( |
| | x=cum_ret.index, y=cum_ret * 100, fill='tozeroy', |
| | fillcolor="rgba(5,150,105,0.08)" if cum_ret.iloc[-1] >= 0 else "rgba(220,38,38,0.08)", |
| | line=dict(color=col_ret, width=2.2), name="Cumulative Return", |
| | hovertemplate="%{x|%b %d, %Y}<br>Return: %{y:.2f}%<extra></extra>", |
| | )) |
| | fig_r.add_hline(y=0, line=dict(color=TEXT_LIGHT, dash='dash', width=1)) |
| | apply_theme(fig_r, title_text="Cumulative Return (%)", yaxis_title="Return (%)") |
| |
|
| | vcols = [GREEN if c >= o else RED for c, o in zip(df['Close'], df['Open'])] |
| | fig_v = go.Figure() |
| | fig_v.add_trace(go.Bar( |
| | x=df.index, y=df['Volume'], marker_color=vcols, name="Volume", |
| | opacity=0.8, |
| | hovertemplate="%{x|%b %d}<br>Vol: %{y:,.0f}<extra></extra>", |
| | )) |
| | apply_theme(fig_v, title_text="Volume (Green = Up Day Β· Red = Down Day)", yaxis_title="Volume") |
| |
|
| | total = cum_ret.iloc[-1] * 100 |
| | sign = "β²" if total >= 0 else "βΌ" |
| | acc = "g" if total >= 0 else "r" |
| | stats = f""" |
| | {sec("π", f"Historical Analysis β {symbol}", f"Period: {period} Β· {len(df)} trading days")} |
| | <div class="kpi-row kpi-row-4"> |
| | {kpi("Current Price", f"${df['Close'].iloc[-1]:.2f}", "", "b")} |
| | {kpi("Period High", f"${df['High'].max():.2f}", "", "g")} |
| | {kpi("Period Low", f"${df['Low'].min():.2f}", "", "r")} |
| | {kpi("Total Return", f"{sign} {abs(total):.2f}%", "", acc)} |
| | </div>""" |
| |
|
| | return fig_c, fig_r, fig_v, stats |
| |
|
| |
|
| | def render_risk(symbol, portfolio_value): |
| | returns = get_returns(symbol, "1y") |
| | if returns.empty: |
| | return banner("Could not fetch data for this symbol.", "err"), None, None, None, None |
| |
|
| | m = compute_risk(returns, portfolio_value) |
| | slabel, scol = sharpe_label(m['sharpe']) |
| | risk_ok = m['annual_vol'] < 0.30 and m['max_dd'] < 0.20 and m['sharpe'] > 1.0 |
| | badge = f'<span class="bdg {"bdg-ok" if risk_ok else "bdg-warn"}">{"β Within Limits" if risk_ok else "β Risk Alert"}</span>' |
| |
|
| | kpi_html = f""" |
| | {sec("π‘οΈ", f"Risk Assessment β {symbol}", f"Portfolio: ${portfolio_value:,.0f} Β· {len(returns)} trading days")} |
| | {badge} |
| | <div class="kpi-row kpi-row-4" style="margin-top:12px"> |
| | {kpi("VaR 95%", f"{m['var_95']:.2%}", f"β${m['var_95_usd']:,.0f} / day", "r")} |
| | {kpi("VaR 99%", f"{m['var_99']:.2%}", f"β${m['var_99_usd']:,.0f} / day", "r")} |
| | {kpi("CVaR 95%", f"{m['cvar_95']:.2%}", "Expected Shortfall", "r")} |
| | {kpi("Annual Vol", f"{m['annual_vol']:.2%}", f"Daily: {m['daily_vol']:.2%}", |
| | "o" if m['annual_vol'] > 0.25 else "b")} |
| | </div> |
| | <div class="kpi-row kpi-row-4"> |
| | {kpi("Max Drawdown", f"{m['max_dd']:.2%}", "Peak-to-Trough", |
| | "r" if m['max_dd'] > 0.20 else "o")} |
| | {kpi("Sharpe Ratio", f"{m['sharpe']:.2f}", slabel, scol)} |
| | {kpi("Annual Return", f"{m['ann_ret']:.2%}", |
| | "β² Positive" if m['ann_ret'] >= 0 else "βΌ Negative", |
| | "g" if m['ann_ret'] >= 0 else "r")} |
| | {kpi("Data Points", str(len(returns)), "Trading Days", "b")} |
| | </div>""" |
| |
|
| | |
| | fig_dist = go.Figure() |
| | fig_dist.add_trace(go.Histogram( |
| | x=returns * 100, nbinsx=55, |
| | marker=dict(color=BLUE_PRIMARY, opacity=0.75, line=dict(color='white', width=0.5)), |
| | name="Daily Returns", |
| | hovertemplate="Return: %{x:.2f}%<br>Count: %{y}<extra></extra>", |
| | )) |
| | fig_dist.add_vline(x=-m['var_95']*100, line=dict(color=GOLD, dash='dash', width=2), |
| | annotation=dict(text="VaR 95%", font=dict(color=GOLD, size=10))) |
| | fig_dist.add_vline(x=-m['var_99']*100, line=dict(color=RED, dash='dash', width=2), |
| | annotation=dict(text="VaR 99%", font=dict(color=RED, size=10))) |
| | apply_theme(fig_dist, title_text="Return Distribution + VaR Lines", |
| | xaxis_title="Daily Return (%)", yaxis_title="Frequency") |
| |
|
| | |
| | dd = m['drawdown'] |
| | fig_dd = go.Figure() |
| | fig_dd.add_trace(go.Scatter( |
| | x=dd.index, y=dd * 100, fill='tozeroy', |
| | fillcolor="rgba(220,38,38,0.10)", |
| | line=dict(color=RED, width=1.8), name="Drawdown %", |
| | hovertemplate="%{x|%b %d, %Y}<br>Drawdown: %{y:.2f}%<extra></extra>", |
| | )) |
| | fig_dd.add_hline(y=-m['max_dd']*100, line=dict(color=GOLD, dash='dot', width=1.5), |
| | annotation=dict(text=f"Max DD {m['max_dd']:.2%}", font=dict(color=GOLD, size=10))) |
| | apply_theme(fig_dd, title_text="Underwater Drawdown Chart", yaxis_title="Drawdown (%)") |
| |
|
| | |
| | rv = returns.rolling(21).std() * np.sqrt(252) * 100 |
| | fig_rv = go.Figure() |
| | fig_rv.add_trace(go.Scatter( |
| | x=rv.index, y=rv, fill='tozeroy', fillcolor="rgba(37,99,235,0.08)", |
| | line=dict(color=BLUE_PRIMARY, width=2), name="21-day Vol", |
| | hovertemplate="%{x|%b %d, %Y}<br>Vol: %{y:.2f}%<extra></extra>", |
| | )) |
| | fig_rv.add_hline(y=30, line=dict(color=RED, dash='dash', width=1.3), |
| | annotation=dict(text="Risk Limit 30%", font=dict(color=RED, size=10))) |
| | apply_theme(fig_rv, title_text="Rolling 21-Day Annualised Volatility", yaxis_title="Volatility (%)") |
| |
|
| | |
| | risk_score = min(100, m['annual_vol']/0.5*40 + m['max_dd']/0.5*40 + max(0,1-m['sharpe'])*20) |
| | gcol = GREEN if risk_score < 40 else GOLD if risk_score < 70 else RED |
| | fig_g = go.Figure(go.Indicator( |
| | mode="gauge+number", |
| | value=risk_score, |
| | title=dict(text="COMPOSITE RISK SCORE", font=dict(family="DM Sans", size=11, color=TEXT_MED)), |
| | number=dict(font=dict(family="Syne", size=36, color=gcol)), |
| | gauge=dict( |
| | axis=dict(range=[0,100], tickwidth=1, tickcolor=TEXT_LIGHT, |
| | tickfont=dict(family="DM Sans", size=10, color=TEXT_LIGHT)), |
| | bar=dict(color=gcol, thickness=0.22), |
| | bgcolor="#ffffff", borderwidth=1, bordercolor=BORDER, |
| | steps=[dict(range=[0,40], color="rgba(5,150,105,0.07)"), |
| | dict(range=[40,70], color="rgba(217,119,6,0.07)"), |
| | dict(range=[70,100],color="rgba(220,38,38,0.07)")], |
| | threshold=dict(line=dict(color=gcol, width=3), thickness=0.75, value=risk_score), |
| | ), |
| | )) |
| | fig_g.update_layout(paper_bgcolor="#ffffff", font=dict(family="DM Sans", color=TEXT_DARK), |
| | height=260, margin=dict(l=30,r=30,t=60,b=20)) |
| |
|
| | return kpi_html, fig_dist, fig_dd, fig_rv, fig_g |
| |
|
| |
|
| | SCENARIOS = { |
| | "Moderate β5%": -0.05, |
| | "Correction β10%": -0.10, |
| | "Bear Market β20%": -0.20, |
| | "Severe β30%": -0.30, |
| | "2008 Crisis β50%": -0.50, |
| | "COVID β35%": -0.35, |
| | "Flash Crash β10%": -0.10, |
| | "Rate Shock β15%": -0.15, |
| | } |
| |
|
| | def render_stress(symbol, portfolio_value): |
| | returns = get_returns(symbol, "1y") |
| | avg_daily = returns.mean() if not returns.empty else 0.0003 |
| |
|
| | rows, pcts, dloss, labels = [], [], [], [] |
| | for name, shock in SCENARIOS.items(): |
| | shocked = portfolio_value * (1 + shock) |
| | loss = portfolio_value - shocked |
| | days_rec = abs(shock) / avg_daily if avg_daily > 0 else float('inf') |
| | yrs_rec = round(days_rec/252, 1) if days_rec != float('inf') else None |
| | rows.append({'Scenario': name, 'Market Shock': f"{shock:.0%}", |
| | 'Portfolio After': f"${shocked:,.0f}", |
| | 'Loss Amount': f"${loss:,.0f}", |
| | 'Recovery (yrs)': str(yrs_rec) if yrs_rec else "N/A"}) |
| | pcts.append(shock * 100) |
| | dloss.append(loss) |
| | labels.append(name) |
| |
|
| | def sev(l): |
| | if l < -30: return RED |
| | if l < -15: return GOLD |
| | return BLUE_PRIMARY |
| |
|
| | fig_pct = go.Figure() |
| | fig_pct.add_trace(go.Bar( |
| | x=labels, y=pcts, |
| | marker=dict(color=[sev(l) for l in pcts], line=dict(color='white', width=1), |
| | opacity=0.85), |
| | text=[f"{l:.0f}%" for l in pcts], textposition='outside', |
| | textfont=dict(size=10, color=TEXT_DARK, family="DM Mono"), |
| | hovertemplate="<b>%{x}</b><br>Loss: %{y:.1f}%<extra></extra>", |
| | )) |
| | apply_theme(fig_pct, title_text="Portfolio Loss % by Scenario", yaxis_title="Loss (%)", |
| | extra={"yaxis": dict(**AXIS_STYLE, range=[min(pcts)*1.3, 5]), "bargap": 0.3}) |
| |
|
| | fig_usd = go.Figure() |
| | fig_usd.add_trace(go.Bar( |
| | x=labels, y=dloss, |
| | marker=dict(color=dloss, colorscale=[[0,"#bfdbfe"],[0.5,GOLD],[1,RED]], |
| | showscale=False, line=dict(color='white', width=1), opacity=0.85), |
| | text=[f"${l:,.0f}" for l in dloss], textposition='outside', |
| | textfont=dict(size=10, color=TEXT_DARK, family="DM Mono"), |
| | hovertemplate="<b>%{x}</b><br>Loss: $%{y:,.0f}<extra></extra>", |
| | )) |
| | apply_theme(fig_usd, title_text="Dollar Loss by Scenario", yaxis_title="Loss ($)", |
| | extra={"bargap": 0.3}) |
| |
|
| | return fig_pct, fig_usd, pd.DataFrame(rows) |
| |
|
| |
|
| | def render_correlation(symbols_str): |
| | syms = [s.strip().upper() for s in symbols_str.split(',') if s.strip()] |
| | if len(syms) < 2: |
| | return None, banner("Enter at least 2 comma-separated symbols.", "warn"), None |
| |
|
| | all_ret = {} |
| | for s in syms: |
| | r = get_returns(s, "1y") |
| | if not r.empty: |
| | all_ret[s] = r |
| |
|
| | if len(all_ret) < 2: |
| | return None, banner("Could not fetch data for enough symbols.", "err"), None |
| |
|
| | df_ret = pd.DataFrame(all_ret).dropna() |
| | corr = df_ret.corr() |
| |
|
| | fig_h = go.Figure(go.Heatmap( |
| | z=corr.values, x=corr.columns.tolist(), y=corr.index.tolist(), |
| | colorscale=[[0, RED],[0.5,"#f8fafc"],[1, BLUE_PRIMARY]], |
| | zmid=0, zmin=-1, zmax=1, |
| | text=corr.values.round(2), texttemplate="%{text}", |
| | textfont=dict(family="DM Mono", size=12, color=TEXT_DARK), |
| | hovertemplate="<b>%{x} vs %{y}</b><br>r = %{z:.3f}<extra></extra>", |
| | colorbar=dict(tickfont=dict(family="DM Sans", color=TEXT_MED), |
| | title=dict(text="r", font=dict(color=TEXT_MED))), |
| | )) |
| | apply_theme(fig_h, title_text="Correlation Matrix β 1Y Daily Returns") |
| |
|
| | cum_df = (1 + df_ret).cumprod() - 1 |
| | palette = [BLUE_PRIMARY, GREEN, GOLD, RED, "#7c3aed", "#db2777", "#0891b2"] |
| | fig_cr = go.Figure() |
| | for i, col in enumerate(cum_df.columns): |
| | fig_cr.add_trace(go.Scatter( |
| | x=cum_df.index, y=cum_df[col]*100, name=col, |
| | line=dict(color=palette[i % len(palette)], width=2.2), |
| | hovertemplate=f"<b>{col}</b><br>%{{x|%b %d}}<br>Return: %{{y:.2f}}%<extra></extra>", |
| | )) |
| | apply_theme(fig_cr, title_text="Cumulative Returns Comparison (%)", yaxis_title="Return (%)") |
| |
|
| | avg_corr = corr.values[np.triu_indices_from(corr.values, k=1)].mean() |
| | if avg_corr < 0.5: |
| | msg, kind = f"Well Diversified β avg pairwise correlation: {avg_corr:.3f}", "ok" |
| | elif avg_corr < 0.7: |
| | msg, kind = f"Moderately Correlated β avg correlation: {avg_corr:.3f}", "warn" |
| | else: |
| | msg, kind = f"Highly Correlated β low diversification benefit (r = {avg_corr:.3f})", "err" |
| |
|
| | return fig_h, banner(msg, kind), fig_cr |
| |
|
| |
|
| | def render_monte_carlo(symbol, portfolio_value, days, sims): |
| | days, sims = int(days), int(sims) |
| | returns = get_returns(symbol, "1y") |
| | if returns.empty: |
| | return None, banner("Could not fetch data.", "err") |
| |
|
| | mu, sigma = returns.mean(), returns.std() |
| | np.random.seed(42) |
| | sim_rets = np.random.normal(mu, sigma, (days, sims)) |
| | sim_paths = portfolio_value * np.exp(np.cumsum(np.log(1 + sim_rets), axis=0)) |
| | final_vals = sim_paths[-1] |
| |
|
| | fig = go.Figure() |
| | x_ax = list(range(days)) |
| | for i in range(min(200, sims)): |
| | col = "rgba(5,150,105,0.10)" if sim_paths[-1,i] >= portfolio_value else "rgba(220,38,38,0.08)" |
| | fig.add_trace(go.Scatter(x=x_ax, y=sim_paths[:,i], mode='lines', |
| | line=dict(color=col, width=0.5), |
| | showlegend=False, hoverinfo='skip')) |
| |
|
| | med_path = np.median(sim_paths, axis=1) |
| | fig.add_trace(go.Scatter(x=x_ax, y=med_path, mode='lines', |
| | line=dict(color=BLUE_PRIMARY, width=2.8), name="Median Path")) |
| |
|
| | p5 = np.percentile(sim_paths, 5, axis=1) |
| | p95 = np.percentile(sim_paths, 95, axis=1) |
| | fig.add_trace(go.Scatter( |
| | x=x_ax + x_ax[::-1], y=list(p95)+list(p5[::-1]), |
| | fill='toself', fillcolor="rgba(37,99,235,0.06)", |
| | line=dict(color='rgba(0,0,0,0)'), name="90% Confidence Band", |
| | )) |
| | fig.add_hline(y=portfolio_value, line=dict(color=TEXT_LIGHT, dash='dash', width=1.5), |
| | annotation=dict(text="Initial Value", font=dict(color=TEXT_LIGHT, size=10))) |
| | apply_theme(fig, title_text=f"Monte Carlo Simulation β {sims:,} Paths Β· {days} Trading Days", |
| | yaxis_title="Portfolio Value ($)", xaxis_title="Trading Day") |
| |
|
| | med_fin = np.median(final_vals) |
| | p5_fin = np.percentile(final_vals, 5) |
| | p95_fin = np.percentile(final_vals, 95) |
| | pct_profit = (final_vals >= portfolio_value).mean() * 100 |
| | med_ret = (med_fin / portfolio_value - 1) * 100 |
| | sign = "β²" if med_ret >= 0 else "βΌ" |
| |
|
| | stats = f""" |
| | {sec("π²", f"Monte Carlo Results β {symbol}", f"{sims:,} simulations Β· {days} trading days")} |
| | <div class="kpi-row kpi-row-4"> |
| | {kpi("Median Outcome", f"${med_fin:,.0f}", |
| | f"{sign} {abs(med_ret):.1f}%", "g" if med_ret >= 0 else "r")} |
| | {kpi("Best Case (95th)", f"${p95_fin:,.0f}", |
| | f"+{(p95_fin/portfolio_value-1)*100:.1f}%", "g")} |
| | {kpi("Worst Case (5th)", f"${p5_fin:,.0f}", |
| | f"{(p5_fin/portfolio_value-1)*100:.1f}%", "r")} |
| | {kpi("% Profitable", f"{pct_profit:.1f}%", |
| | f"of {sims:,} simulations", "g" if pct_profit >= 50 else "r")} |
| | </div>""" |
| |
|
| | return fig, stats |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | HEADER_HTML = """ |
| | <div class="pf-hero"> |
| | <div class="pf-logo"> |
| | <div class="pf-logo-hex">⬑</div> |
| | <div> |
| | <div class="pf-title">Portfolio <span>Intelligence</span> System</div> |
| | </div> |
| | </div> |
| | <div class="pf-sub">Multi-Agent Risk & Market Analytics Platform Β· v3.0</div> |
| | <div class="pf-pills"> |
| | <span class="pf-pill">π‘ Real-Time Data</span> |
| | <span class="pf-pill">π‘οΈ Risk Metrics</span> |
| | <span class="pf-pill">π² Monte Carlo</span> |
| | <span class="pf-pill">π Stress Testing</span> |
| | <span class="pf-pill">π Correlation</span> |
| | <span class="pf-pill">IIT Madras 2026</span> |
| | </div> |
| | </div> |
| | """ |
| |
|
| | FOOTER_HTML = """ |
| | <div class="pf-footer"> |
| | ⬑ Portfolio Intelligence System · IIT Madras 2026 · |
| | Ashwini Β· Dibyendu Sarkar Β· Jyoti Ranjan Sethi Β· |
| | Data: Yahoo Finance Β· Educational Use Only |
| | </div> |
| | """ |
| |
|
| | with gr.Blocks(title="Portfolio Intelligence System", css=CUSTOM_CSS) as demo: |
| |
|
| | gr.HTML(HEADER_HTML) |
| |
|
| | |
| | with gr.Row(elem_classes=["pf-controls"]): |
| | shared_symbol = gr.Dropdown( |
| | choices=['AAPL','GOOGL','MSFT','TSLA','AMZN','RELIANCE.NS'], |
| | value="AAPL", label="π Stock Symbol", scale=2, |
| | ) |
| | shared_period = gr.Dropdown( |
| | choices=["1mo","3mo","6mo","1y","2y","5y"], |
| | value="1y", label="π
Period", scale=1, |
| | ) |
| | shared_portfolio = gr.Number( |
| | value=100_000, label="π° Portfolio Value ($)", |
| | minimum=1000, scale=2, |
| | ) |
| |
|
| | with gr.Tabs(): |
| |
|
| | |
| | with gr.Tab("π‘ Market Overview"): |
| | with gr.Row(): |
| | overview_btn = gr.Button("π Refresh Market Data", variant="primary", size="lg", scale=1) |
| | status_out = gr.HTML() |
| | cards_out = gr.HTML() |
| | with gr.Row(): |
| | price_out = gr.Plot(label="Stock Prices", show_label=False) |
| | vol_out = gr.Plot(label="Trading Volume", show_label=False) |
| | with gr.Accordion("π Full Price Table", open=False): |
| | table_out = gr.Dataframe(interactive=False) |
| | overview_btn.click( |
| | fn=render_market_overview, |
| | outputs=[cards_out, price_out, vol_out, table_out, status_out], |
| | ) |
| |
|
| | |
| | with gr.Tab("π Historical Analysis"): |
| | hist_btn = gr.Button("π Load Historical Data", variant="primary", size="lg") |
| | hist_stat = gr.HTML() |
| | with gr.Row(): |
| | candle = gr.Plot(label="Candlestick", show_label=False) |
| | ret_chart = gr.Plot(label="Cumulative Return", show_label=False) |
| | vol_hist = gr.Plot(label="Volume", show_label=False) |
| | hist_btn.click( |
| | fn=render_historical, |
| | inputs=[shared_symbol, shared_period], |
| | outputs=[candle, ret_chart, vol_hist, hist_stat], |
| | ) |
| |
|
| | |
| | with gr.Tab("π‘οΈ Risk Assessment"): |
| | risk_btn = gr.Button("π Calculate Risk Metrics", variant="primary", size="lg") |
| | risk_kpi = gr.HTML() |
| | with gr.Row(): |
| | gauge_out = gr.Plot(label="Risk Gauge", show_label=False) |
| | dist_out = gr.Plot(label="Distribution", show_label=False) |
| | with gr.Row(): |
| | dd_out = gr.Plot(label="Drawdown", show_label=False) |
| | rv_out = gr.Plot(label="Rolling Vol", show_label=False) |
| | risk_btn.click( |
| | fn=render_risk, |
| | inputs=[shared_symbol, shared_portfolio], |
| | outputs=[risk_kpi, dist_out, dd_out, rv_out, gauge_out], |
| | ) |
| |
|
| | |
| | with gr.Tab("π₯ Stress Testing"): |
| | stress_btn = gr.Button("π₯ Run Stress Tests", variant="primary", size="lg") |
| | with gr.Row(): |
| | spct = gr.Plot(label="Loss %", show_label=False) |
| | susd = gr.Plot(label="Dollar Loss", show_label=False) |
| | with gr.Accordion("π Full Stress Test Table", open=True): |
| | stbl = gr.Dataframe(interactive=False) |
| | stress_btn.click( |
| | fn=render_stress, |
| | inputs=[shared_symbol, shared_portfolio], |
| | outputs=[spct, susd, stbl], |
| | ) |
| |
|
| | |
| | with gr.Tab("π Correlation"): |
| | with gr.Row(): |
| | sym_in = gr.Textbox( |
| | value="AAPL,GOOGL,MSFT,TSLA,AMZN", |
| | label="Symbols (comma-separated)", scale=4, |
| | ) |
| | corr_btn = gr.Button("π Compute", variant="primary", scale=1) |
| | corr_inf = gr.HTML() |
| | with gr.Row(): |
| | heat_out = gr.Plot(label="Heatmap", show_label=False) |
| | cmp_out = gr.Plot(label="Returns Comparison", show_label=False) |
| | corr_btn.click( |
| | fn=render_correlation, |
| | inputs=[sym_in], |
| | outputs=[heat_out, corr_inf, cmp_out], |
| | ) |
| |
|
| | |
| | with gr.Tab("π² Monte Carlo"): |
| | with gr.Row(): |
| | mc_days = gr.Slider(21, 504, value=252, step=21, label="π
Simulation Days") |
| | mc_sims = gr.Slider(100, 1000, value=500, step=100, label="π’ Simulations") |
| | mc_btn = gr.Button("π² Run Simulation", variant="primary", size="lg") |
| | mc_stats = gr.HTML() |
| | mc_chart = gr.Plot(label="Simulation Paths", show_label=False) |
| | mc_btn.click( |
| | fn=render_monte_carlo, |
| | inputs=[shared_symbol, shared_portfolio, mc_days, mc_sims], |
| | outputs=[mc_chart, mc_stats], |
| | ) |
| |
|
| | |
| | with gr.Tab("βΉοΈ About"): |
| | gr.Markdown(""" |
| | ## ⬑ Portfolio Intelligence System |
| | |
| | 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. |
| | |
| | --- |
| | |
| | ### π§© Modules |
| | |
| | | Module | Description | |
| | |---|---| |
| | | **π‘ Market Overview** | Real-time prices, volume, P/E, market cap for 6 stocks | |
| | | **π Historical Analysis** | Candlestick + MA20, cumulative returns, volume | |
| | | **π‘οΈ Risk Assessment** | VaR, CVaR, Sharpe, Max Drawdown, Rolling Vol, Risk Gauge | |
| | | **π₯ Stress Testing** | 8 crash scenarios β loss % and dollar impact | |
| | | **π Correlation** | Correlation heatmap + cumulative return comparison | |
| | | **π² Monte Carlo** | Up to 1000 simulation paths with confidence bands | |
| | |
| | --- |
| | |
| | ### ποΈ Architecture |
| | |
| | ``` |
| | Yahoo Finance API |
| | β |
| | Market Data Agent β SQLite DB |
| | β |
| | Risk Management Agent (RiskIQ) |
| | β |
| | Gradio Dashboard UI |
| | ``` |
| | |
| | --- |
| | |
| | ### π Risk Metrics Reference |
| | |
| | | Metric | Good Value | Description | |
| | |---|---|---| |
| | | VaR 95% | < 2% | Max 1-day loss with 95% confidence | |
| | | CVaR 95% | < 3% | Avg loss when VaR is exceeded | |
| | | Sharpe Ratio | > 1.0 | Return per unit of risk | |
| | | Max Drawdown | < 20% | Worst peak-to-trough decline | |
| | | Annual Volatility | < 25% | Annualised return fluctuation | |
| | |
| | --- |
| | |
| | **Team:** Ashwini Β· Dibyendu Sarkar Β· Jyoti Ranjan Sethi |
| | **Program:** Multi-Agent Systems Β· IIT Madras Β· Week 3 of 16 Β· 2026 |
| | |
| | > β οΈ *For educational purposes only. Not financial advice. Data sourced from Yahoo Finance.* |
| | """) |
| |
|
| | gr.HTML(FOOTER_HTML) |
| |
|
| |
|
| | if __name__ == "__main__": |
| | demo.launch() |