Portfolio Analytics

Quant Portfolio Analytics

Holdings (Ticker, Avg Price, Qty)

Initializing Python environment in browser...

Total Value
--
Max Drawdown
--
95% Daily VaR
--

Quantitative Risk & Performance Summary

Asset Correlation Matrix

Measures how closely assets move together. +1.0 means they move in lockstep, 0.0 means no relationship, and -1.0 means they move in opposite directions.

packages = ["pandas", "numpy", "pyodide-http"] import pyodide_http import json import asyncio from datetime import datetime import pandas as pd import numpy as np from js import Plotly, JSON, document from urllib.parse import quote # Patch HTTP requests so Python can fetch financial data directly in browser pyodide_http.patch_all() MAIN_TEAL = "#00d4aa" ACCENT_RED = "#ff4b4b" CHART_COLORS = ["#00d4aa", "#008a73", "#004d40", "#7ef4da", "#b2fcf0"] document.getElementById("status").innerText = "Environment ready! Click Run Analytics." async def fetch_yahoo_data(ticker): target_url = f"https://query1.finance.yahoo.com/v8/finance/chart/{ticker}?range=3y&interval=1d" url = f"https://api.allorigins.win/raw?url={quote(target_url, safe='')}" import pyodide.http response = await pyodide.http.pyfetch(url) data = await response.json() result = data["chart"]["result"][0] timestamps = result["timestamp"] prices = result["indicators"]["quote"][0]["close"] dates = [datetime.fromtimestamp(ts) for ts in timestamps] df = pd.DataFrame({"Close": prices}, index=dates) return df["Close"].ffill() async def run_analytics(event): document.getElementById("status").innerText = "Fetching market data..." raw_input = document.getElementById("holdings_input").value holdings = {} for line in raw_input.strip().split("\n"): if "," in line: parts = [p.strip() for p in line.split(",")] if len(parts) == 3: holdings[parts[0].upper()] = [float(parts[1]), float(parts[2])] tickers = list(holdings.keys()) price_series = {} for t in tickers: try: series = await fetch_yahoo_data(t) if not series.empty: price_series[t] = series except Exception as e: print(f"Error fetching {t}: {e}") if not price_series: document.getElementById("status").innerText = "[X] Failed to fetch market data. Check ticker symbols." return df = pd.DataFrame(price_series).ffill().dropna() if df.empty: document.getElementById("status").innerText = "[X] No overlapping market data found for tickers." return latest_prices = df.iloc[-1].copy() valid_tickers = df.columns.tolist() for t in valid_tickers: if t.endswith('.L'): latest_prices[t] /= 100.0 values = pd.Series({t: latest_prices[t] * holdings[t][1] for t in valid_tickers}) total_value = float(values.sum()) weights = values / total_value returns = df[valid_tickers].pct_change().dropna() port_returns = returns.dot(weights) # Performance Math cum_returns = (1 + port_returns).cumprod() window = 63 rf_annual = 0.04 rolling_mu = port_returns.rolling(window).mean() * 252 rolling_std = port_returns.rolling(window).std() * np.sqrt(252) rolling_sharpe = (rolling_mu - rf_annual) / rolling_std # Sortino Math downside_returns = port_returns.copy() downside_returns[downside_returns > 0] = 0 rolling_downside_std = downside_returns.rolling(window).std() * np.sqrt(252) rolling_sortino = (rolling_mu - rf_annual) / rolling_downside_std # Drawdown & VaR running_max = cum_returns.cummax() drawdown = (cum_returns - running_max) / running_max var_95 = float(np.percentile(port_returns, 5)) mdd_val = float(drawdown.min()) # Update Summary Cards document.getElementById("val-total").innerText = f"GBP {total_value:,.2f}" document.getElementById("val-mdd").innerText = f"{mdd_val:.2%}" document.getElementById("val-var").innerText = f"{var_95:.2%}" plotly_config = JSON.parse(json.dumps({"responsive": True, "displayModeBar": False})) # 1. Cumulative Growth Chart growth_data = [{ "x": [d.strftime("%Y-%m-%d") for d in cum_returns.index], "y": [float(v) for v in cum_returns.values], "type": "scatter", "mode": "lines", "line": {"color": MAIN_TEAL, "width": 2.5}, "name": "Cumulative Growth" }] growth_layout = { "title": "Portfolio Cumulative Growth (Value of GBP 1)", "paper_bgcolor": "rgba(0,0,0,0)", "plot_bgcolor": "rgba(0,0,0,0)", "font": {"color": "#ffffff"}, "xaxis": {"gridcolor": "#232a35"}, "yaxis": {"gridcolor": "#232a35"} } Plotly.newPlot("growth-chart", JSON.parse(json.dumps(growth_data)), JSON.parse(json.dumps(growth_layout)), plotly_config) # 2. Drawdown Chart dd_data = [{ "x": [d.strftime("%Y-%m-%d") for d in drawdown.index], "y": [float(v * 100) for v in drawdown.values], "type": "scatter", "fill": "tozeroy", "line": {"color": ACCENT_RED, "width": 1.5}, "name": "Drawdown %" }] dd_layout = { "title": "Portfolio Underwater Analysis (Drawdown %)", "paper_bgcolor": "rgba(0,0,0,0)", "plot_bgcolor": "rgba(0,0,0,0)", "font": {"color": "#ffffff"}, "xaxis": {"gridcolor": "#232a35"}, "yaxis": {"gridcolor": "#232a35", "ticksuffix": "%"} } Plotly.newPlot("drawdown-chart", JSON.parse(json.dumps(dd_data)), JSON.parse(json.dumps(dd_layout)), plotly_config) # 3. Asset Allocation Donut Chart alloc_data = [{ "values": [float(v) for v in values.values], "labels": [str(k) for k in values.index], "type": "pie", "hole": 0.5, "marker": {"colors": CHART_COLORS} }] alloc_layout = { "title": "Asset Allocation", "paper_bgcolor": "rgba(0,0,0,0)", "font": {"color": "#ffffff"} } Plotly.newPlot("allocation-chart", JSON.parse(json.dumps(alloc_data)), JSON.parse(json.dumps(alloc_layout)), plotly_config) # 4. Rolling Sharpe Ratio Chart clean_sharpe = rolling_sharpe.dropna() sharpe_data = [{ "x": [d.strftime("%Y-%m-%d") for d in clean_sharpe.index], "y": [float(v) for v in clean_sharpe.values], "type": "scatter", "mode": "lines", "line": {"color": MAIN_TEAL, "width": 2} }] sharpe_layout = { "title": "Rolling Ann. Sharpe Ratio", "paper_bgcolor": "rgba(0,0,0,0)", "plot_bgcolor": "rgba(0,0,0,0)", "font": {"color": "#ffffff"}, "xaxis": {"gridcolor": "#232a35"}, "yaxis": {"gridcolor": "#232a35"} } Plotly.newPlot("sharpe-chart", JSON.parse(json.dumps(sharpe_data)), JSON.parse(json.dumps(sharpe_layout)), plotly_config) # 5. Daily Returns Distribution Histogram hist_data = [{ "x": [float(v) for v in port_returns.values], "type": "histogram", "nbinsx": 50, "marker": {"color": MAIN_TEAL, "opacity": 0.75}, "name": "Daily Returns" }] hist_layout = { "title": "Daily Returns Distribution", "paper_bgcolor": "rgba(0,0,0,0)", "plot_bgcolor": "rgba(0,0,0,0)", "font": {"color": "#ffffff"}, "xaxis": {"title": "Daily Return", "gridcolor": "#232a35", "tickformat": ".1%"}, "yaxis": {"title": "Frequency", "gridcolor": "#232a35"}, "shapes": [{ "type": "line", "x0": var_95, "x1": var_95, "y0": 0, "y1": 1, "yref": "paper", "line": {"color": ACCENT_RED, "width": 2, "dash": "dash"} }], "annotations": [{ "x": var_95, "y": 1, "yref": "paper", "text": f"95% VaR ({var_95:.2%})", "showarrow": False, "font": {"color": ACCENT_RED}, "xanchor": "right" }] } Plotly.newPlot("returns-dist-chart", JSON.parse(json.dumps(hist_data)), JSON.parse(json.dumps(hist_layout)), plotly_config) # 6. Safe Risk & Performance Summary Table Calculation try: is_underwater = drawdown < 0 groups = (drawdown == 0).cumsum() recovery_days = int(is_underwater.groupby(groups).sum().max()) except: recovery_days = 0 cur_sharpe = float(clean_sharpe.iloc[-1]) if len(clean_sharpe) > 0 else 0.0 clean_sortino = rolling_sortino.dropna() cur_sortino = float(clean_sortino.iloc[-1]) if len(clean_sortino) > 0 else 0.0 ann_vol = float(port_returns.std() * np.sqrt(252)) tot_ret = float(cum_returns.iloc[-1] - 1) ann_ret = float((1 + tot_ret) ** (1 / (len(df) / 252)) - 1) calmar_val = float(ann_ret / abs(mdd_val)) if mdd_val != 0 else 0.0 hit_ratio_val = float((port_returns > 0).sum() / len(port_returns)) tail_returns = port_returns[port_returns <= var_95] cvar_val = float(tail_returns.mean()) if len(tail_returns) > 0 else 0.0 metrics_list = [ ("Current Sharpe", f"{cur_sharpe:.2f}"), ("Current Sortino", f"{cur_sortino:.2f}"), ("Calmar Ratio", f"{calmar_val:.2f}"), ("Ann. Volatility", f"{ann_vol:.2%}"), ("95% VaR", f"{var_95:.2%}"), ("Expected Shortfall", f"{cvar_val:.2%}"), ("Max Drawdown", f"{mdd_val:.2%}"), ("Max Recovery", f"{recovery_days} Days"), ("Hit Ratio", f"{hit_ratio_val:.2%}") ] grid_html = """
Metric
Value
""" for label, val in metrics_list: grid_html += f"""
{label}
{val}
""" document.getElementById("summary-table-container").innerHTML = grid_html # 7. Asset Correlation Matrix Heatmap corr_matrix = returns.corr() corr_z = [[float(val) for val in row] for row in corr_matrix.values] corr_data = [{ "z": corr_z, "x": [str(c) for c in corr_matrix.columns], "y": [str(i) for i in corr_matrix.index], "type": "heatmap", "colorscale": "RdYlGn", "zmin": -1, "zmax": 1 }] corr_layout = { "title": "Asset Correlation Matrix", "paper_bgcolor": "rgba(0,0,0,0)", "plot_bgcolor": "rgba(0,0,0,0)", "font": {"color": "#ffffff"}, "margin": {"t": 40, "b": 40, "l": 40, "r": 40} } Plotly.newPlot("correlation-chart", JSON.parse(json.dumps(corr_data)), JSON.parse(json.dumps(corr_layout)), plotly_config) document.getElementById("last-updated").innerText = f"Last Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" document.getElementById("status").innerText = "Analysis Complete!"
Scroll to Top