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!"