CoolFace
Apppublic

Mallikarjun1009/DataVizFinal

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
streamlit_app.py989 linesDownload Raw Back to src
1 2import streamlit as st3 4import pandas as pd5import altair as alt6import plotly.express as px7import plotly.graph_objects as go8 9from pathlib import Path10import numpy as np11 12st.set_page_config(layout="wide")13 14# ── inject custom CSS ────────────────────────────────────────────────────15css = """16<style>17/* 1️⃣  reduce global page padding */18.stApp {19    padding: 0rem 1rem 2rem;     /* top right/left bottom */20}21 22/* 2️⃣  keep title from clipping under top browser bar */23h1 { 24    margin-top: 1.2rem;          /* push title a bit down */25}26 27/* 3️⃣  remove unnecessary side gutter (Streamlit adds it for sidebar) */28.css-18e3th9 { padding: 0; }     /* main block */29.css-1d391kg { padding: 0; }     /* wide-mode block */30</style>31"""32st.markdown(css, unsafe_allow_html=True)33 34# ─────────────────────────────────────────────────────────────────────────────35# Data loader (cached) 36# ─────────────────────────────────────────────────────────────────────────────37@st.cache_data38def load_data(path):39    df_raw = pd.read_csv(path)40 41    # Separate IVV and others42    df_ivv = df_raw[df_raw["ETF"] == "IVV"].copy()43    df_others = df_raw[df_raw["ETF"] != "IVV"].copy()44 45    # Try standard parsing for IVV46    df_ivv["Date"] = pd.to_datetime(df_ivv["Date"], errors="coerce", format="%Y-%m-%d")47    48    # If still all NaT, try Excel-style origin fallback49    if df_ivv["Date"].isna().sum() == len(df_ivv):50        df_ivv["Date"] = pd.to_datetime(df_ivv["Date"], errors="coerce", unit="D", origin="1899-12-30")51 52    # Parse rest normally53    df_others["Date"] = pd.to_datetime(df_others["Date"], errors="coerce")54 55    # Recombine56    df = pd.concat([df_ivv, df_others], ignore_index=True)57    df = df.sort_values(["ETF", "Date"])58 59    # Engineer fields60    df["Daily_Return"] = df.groupby("ETF")["Close"].pct_change()61    df["Cumulative_Return"] = df.groupby("ETF")["Daily_Return"].transform(lambda r: (1 + r).cumprod() - 1)62    df["MA_200"] = df.groupby("ETF")["Adj Close"].transform(lambda s: s.rolling(200, min_periods=1).mean())63    df["Growth10k"] = df.groupby("ETF")["Daily_Return"].transform(lambda r: (1 + r).cumprod() * 10_000)64    peak = df.groupby("ETF")["Adj Close"].transform("cummax")65    df["Drawdown"] = df["Adj Close"] / peak - 166    df["Year"] = df["Date"].dt.year67 68    return df69 70csv_path = Path(__file__).parent / "combined_etf_data (1).csv"   # ensure file name71 72# manual refresh73if st.button("🔄 Refresh data"):74    load_data.clear()75data = load_data(csv_path)76 77st.title("ETF Observatory – ETF Performance Dashboard: Risk, Return, and Investment Insights Across Global Markets")78 79st.caption("A comprehensive interactive dashboard for analyzing historical performance, volatility, drawdowns, and risk-adjusted returns across major global ETFs.")80 81 82st.subheader("Dataset Information")83 84st.markdown(85    """86Data for all five ETFs was sourced directly from [Yahoo Finance](https://finance.yahoo.com/) using the `yfinance` Python library.87"""88)89 90col1, col2 = st.columns(2)91 92with col1:93    st.subheader("1. ETF Universe")94    st.markdown(95        """96| Ticker | Fund Name                                  |97|:------:|:-------------------------------------------|98| **EEM** | iShares MSCI Emerging Markets             |99| **EFA** | iShares MSCI EAFE (Europe, Australasia & Far East) |100| **IVV** | iShares Core S&P 500                      |101| **IWM** | iShares Russell 2000                      |102| **TLT** | iShares 20+ Year Treasury Bond Fund       |103""",104        unsafe_allow_html=True,105    )106 107with col2:108    st.subheader("2. Core Metrics & Notation")109    st.markdown(110        """111| Symbol               | Meaning                                                                                                              |112|:--------------------:|:---------------------------------------------------------------------------------------------------------------------|113| `rₜ`                 | **Daily Return:** `(Closeₜ / Closeₜ₋₁) - 1`                                                                          |114| **Cumulative Return**| Compound growth over time: `(1+r₁) × (1+r₂) × … – 1`                                                                  |115| `μ` (mu)             | Average return over your selected period                                                                             |116| `σ` (sigma)          | Volatility (standard deviation of returns)                                                                           |117| **Sharpe Ratio**     | `(μ – r_f) / σ`, where `r_f` is the risk-free rate                                                                   |118| **Max Drawdown**     | Largest peak-to-trough percentage decline in **Adj Close**                                                            |119| **CAGR**             | Compound Annual Growth Rate – annualised return assuming reinvestment                                                 |120""",121        unsafe_allow_html=True,122    )123 124st.subheader("3. What we can do")125st.markdown(126    """127- Explore miniature price charts for each ETF and quickly spot trends.128- Filter by time window, sampling frequency (daily/monthly/yearly), and custom risk-free rate.129- Simulate the growth of any initial investment ($1 – $100) and see its current value.130- Compare ETFs on risk vs return with an interactive scatter and a dedicated volatility gauge.131- Dive into monthly performance with heatmaps and drill-down bar charts by year.132- Snapshot annual metrics in a radar chart and a clean, formatted table.133    """134)135 136st.subheader("ETF Dataset")137st.dataframe(data)138with st.expander("About the Dataset"):139    st.markdown("""140This dataset provides a daily historical record of five major **Exchange-Traded Funds (ETFs)** across global markets, capturing their performance from inception to the present. It serves as the foundational data for all visualizations and calculations in this dashboard.141 142### Key Features:143- **Date**: Trading date for each observation.144- **Adj Close / Close / Open / High / Low**: Standard price fields, where `Adj Close` accounts for splits and dividends.145- **Volume**: Total trading volume for the day.146- **ETF**: The ETF ticker symbol, identifying the fund:147    - `EEM`: iShares MSCI Emerging Markets148    - `EFA`: iShares MSCI EAFE (Europe, Australasia & Far East)149    - `IVV`: iShares Core S&P 500150    - `IWM`: iShares Russell 2000151    - `TLT`: iShares 20+ Year Treasury Bond Fund152 153### Engineered Fields:154- **Daily_Return**: Percentage change from previous close.155- **Cumulative_Return**: Compounded return from the start of the period.156- **Rolling_30d_Vol**: 30-day rolling volatility (standard deviation).157- **Month / Year**: Date-based partitioning for visual summaries.158- **MA_200**: 200-day moving average of the Adjusted Close.159- **Growth10k**: Simulated growth of a $10,000 investment over time.160- **Drawdown**: Maximum decline from previous price peaks.161 162These engineered metrics allow us to evaluate both **short-term dynamics** and **long-term investment performance**, enabling side-by-side risk-return comparisons, volatility analysis, and simulated portfolio growth.163 164This dataset has been cleaned, standardized, and transformed for interactive exploration within this dashboard.165    """)166 167 168 169# =============================================================================170# ROW 0 ▸ four mini closing‑price charts (one per ETF)171# =============================================================================172st.subheader("Mini Close‑Price Trends")173 174# choose which ETFs to show as minis (first four by default)175etfs_to_show = data["ETF"].unique()          #  ← show every ticker, not just [:4]176cols = st.columns(len(etfs_to_show), gap="medium")   # narrow gap (Streamlit ≥1.24)177 178for col, etf in zip(cols, etfs_to_show):179    mini_df = data[data["ETF"] == etf]180    fig = px.line(181        mini_df,182        x="Date",183        y="Close",184        title=etf,185        template="plotly_dark",186        height=250,187    )188    fig.update_layout(189        margin=dict(l=10, r=10, t=30, b=10),190        showlegend=False,191        xaxis=dict(title=None),192        yaxis=dict(title=None),193    )194    col.plotly_chart(fig, use_container_width=True)195 196    with col.expander(f" {etf} Insight"):197        if etf == "EEM":198            st.markdown("""199**EEM (iShares MSCI Emerging Markets)** tracks stocks from countries like China, Brazil, and India.  200This chart shows volatile performance with noticeable peaks around 2007 and 2021, reflecting the sensitivity of emerging markets to global risk sentiment.201            """)202        elif etf == "EFA":203            st.markdown("""204**EFA (iShares MSCI EAFE)** captures developed markets outside North America — Europe, Australasia, and the Far East.  205The chart shows long-term recovery post-2008, but relatively flatter growth compared to U.S.-based ETFs.206            """)207        elif etf == "IVV":208            st.markdown("""209**IVV (iShares Core S&P 500)** reflects the U.S. large-cap equity market.  210A consistent upward trend with strong growth after 2012, underlining the dominance of U.S. tech and large-cap sectors.211            """)212        elif etf == "IWM":213            st.markdown("""214**IWM (iShares Russell 2000)** covers small-cap U.S. stocks.  215More fluctuation than IVV, highlighting sensitivity to domestic economic conditions and investor sentiment.216            """)217        elif etf == "TLT":218            st.markdown("""219**TLT (iShares 20+ Year Treasury)** tracks long-term U.S. government bonds.  220You can observe spikes during periods of market stress (e.g., 2020) followed by declines as interest rates rise.221            """)222 223# =============================================================================224# ROW 1 ▸ full‑width Plotly overviews225# =============================================================================226st.subheader("Closing Prices (All ETFs)")227with st.expander("Interpretation: Price Trends and Window Metrics"):228    st.markdown("""229This section presents the historical **closing prices** of selected ETFs over a custom time range. You can choose from:230 231- **1Y** — captures recent market conditions and volatility232- **5Y** — balances short-term noise with longer trends233- **MAX** — shows long-term compounded growth and resilience234 235By adjusting this window, you can uncover how different ETFs behave across economic cycles — from recent turbulence to multi-decade trajectories.236 237### What We’re Comparing:238Each ETF line reflects raw market prices — not adjusted for dividends — which helps visualize:239- Relative growth rates240- Crash/recovery patterns241- Stability or cyclicality242 243### Understanding the Metrics Below:244We compute essential summary stats for the selected window:245 246- **Best Performer**: The ETF with the highest return over the window.247- **Avg Period Return**: Mean return across all ETFs during the selected range.248- **Avg Annual Volatility**: Standard deviation of returns, scaled to yearly — higher means more fluctuation.249- **Avg Max Drawdown**: The average peak-to-trough decline for each ETF — shows downside exposure during crashes.250 251These metrics help distinguish **return efficiency**, **risk behavior**, and **drawdown resilience**, providing a well-rounded snapshot of ETF performance over your chosen timeframe.252    """)253 254 255# ① dropdown for year‑span256window = st.selectbox("Show period:",257                      options=["1 Y", "5 Y", "MAX"],258                      index=2,            # default = MAX259                      key="price_range")260 261# ② compute slice262min_date = data["Date"].min()263if window == "1 Y":264    end_date = min_date + pd.DateOffset(years=1)265    subset = data[(data["Date"] >= min_date) & (data["Date"] <= end_date)]266elif window == "5 Y":267    end_date = min_date + pd.DateOffset(years=5)268    subset = data[(data["Date"] >= min_date) & (data["Date"] <= end_date)]269else:  # MAX270    subset = data.copy()271 272# ③ plot273st.plotly_chart(274    px.line(275        subset,276        x="Date",277        y="Close",278        color="ETF",279        title=f"ETF Closing Prices — {window}",280        template="plotly_white",281    ),282    use_container_width=True,283)284 285summary = (286    subset.groupby("ETF")287    .agg(288        first_close=("Close", "first"),289        last_close=("Close", "last"),290        daily_std=("Daily_Return", "std"),291    )292)293summary["Period_Return"] = summary["last_close"] / summary["first_close"] - 1294summary["Ann_Vol"] = summary["daily_std"] * (252**0.5)295 296# Max‑drawdown requires rolling cumulative max297def max_dd(group):298    peak = group["Adj Close"].cummax()299    dd = group["Adj Close"] / peak - 1300    return dd.min()301 302maxdd = subset.groupby("ETF").apply(max_dd).rename("Max_Drawdown")303summary = summary.join(maxdd)304 305# Which ETF won?306winner = summary["Period_Return"].idxmax()307winner_ret = summary.loc[winner, "Period_Return"]308 309# Show metrics side‑by‑side310st.subheader("Key Metrics for Selected Window")311 312m1, m2, m3, m4 = st.columns(4)313 314m1.metric("Best Performer", winner, f"{winner_ret:.2%}")315m2.metric("Avg Period Return", f"{summary['Period_Return'].mean():.2%}")316m3.metric("Avg Ann. Volatility", f"{summary['Ann_Vol'].mean():.2%}")317m4.metric("Avg Max Drawdown", f"{summary['Max_Drawdown'].mean():.2%}")318 319 320st.subheader("Cumulative Returns (All ETFs)")321with st.expander("Interpretation: Cumulative Returns and Long-Term Growth"):322    st.markdown("""323This section visualizes the **cumulative return** for each ETF from 2000 up to the year selected in the slider. It highlights how an investment compounds over time, assuming all returns are reinvested.324 325### What We’re Seeing:326- **Cumulative Return** tracks total growth:  327  \[(1 + r₁) × (1 + r₂) × ... × (1 + rₙ) - 1\]  328  It's a strong signal of long-term performance, ignoring short-term noise.329  330- **Best Performing ETF**: The asset that achieved the highest return over the selected range.331- **Underperforming ETF**: The one with the lowest return.332- **Average Cumulative Return**: Mean return across all ETFs for the selected period.333- **CAGR (Compound Annual Growth Rate)**: Indicates average yearly return assuming reinvestment. Smooths out volatility and makes comparisons fair across timeframes.334 335### Why This Matters:336Cumulative returns reveal which ETFs have sustained growth and resilience over multiple market cycles. For example:337- **IWM** (U.S. small-cap) shows strong compounding despite volatility.338- **TLT** (U.S. Treasury bonds) may lag in returns, but often acts defensively during market stress.339 340You can move the slider to examine how different years (e.g., pre-2008, post-COVID) affect overall ETF rankings and long-term wealth accumulation.341    """)342 343# ① End‑year slider: from first year to last year in the dataset344yr_min, yr_max = int(data["Year"].min()), int(data["Year"].max())345end_year = st.slider(346    "Display data up to year:",347    yr_min,348    yr_max,349    yr_max,350    key="cum_slider",351)352 353# ② Subset the data: keep rows up to 31‑Dec of the chosen year354window_mask = data["Date"] <= pd.Timestamp(year=end_year, month=12, day=31)355cur = data[window_mask]356 357# ③ Plot cumulative returns358fig_cum = px.line(359    cur,360    x="Date",361    y="Cumulative_Return",362    color="ETF",363    title=f"Cumulative Returns (up to {end_year})",364    template="plotly_white",365)366st.plotly_chart(fig_cum, use_container_width=True)367 368# ④ Live metrics ----------------------------------------------------------369last_day = cur["Date"].max()370last_vals = (371    cur[cur["Date"] == last_day]372    .set_index("ETF")["Cumulative_Return"]373)374 375best_etf = last_vals.idxmax()376worst_etf = last_vals.idxmin()377best_ret = last_vals.max()378worst_ret = last_vals.min()379avg_ret = last_vals.mean()380 381years_span = (last_day - cur["Date"].min()).days / 365.25382cagr_best = (1 + best_ret) ** (1 / years_span) - 1383 384m1, m2, m3, m4 = st.columns(4)385m1.metric("Best Performing ETF", best_etf, f"{best_ret:.2%}")386m2.metric("Underperforming ETF", worst_etf, f"{worst_ret:.2%}")387m3.metric("Avg Cum Return", f"{avg_ret:.2%}")388m4.metric("CAGR (Best)", f"{cagr_best:.2%}")389 390# =============================================================================391# ROW 2 ▸ Growth of $10 000392# =============================================================================393st.header("Growth of an Investment")394with st.expander("About Growth of an Investment"):395    st.markdown("""396This section visualizes how a one-time investment would have grown over time in a selected ETF. Use the dropdown to choose among **EEM**, **EFA**, **IVV**, **IWM**, or **TLT**, and set your initial investment using the slider.397 398The chart reflects the compounding effect of returns, based on daily price changes. It offers a visual and quantitative sense of how each fund performs over the long term—highlighting both growth potential and volatility.399 400Key points:401- The graph tracks **portfolio value** over time, simulating reinvested returns.402- The final metric below the chart displays the **current value** of your selected investment.403- Ideal for comparing historical performance and assessing how different asset classes react to market cycles.404 405This tool is especially useful for exploring how different ETFs align with your long-term financial goals and risk tolerance.406    """)407 408# ① choose ETF409choice = st.selectbox("Choose ETF", data["ETF"].unique())410 411# ② choose starting amount (slider $1 – $100)412start_amt = st.slider("Initial Amount ($)", 1, 100, 10, step=1)413 414# ③ build dataframe for the chosen ETF415gdf = data[data["ETF"] == choice].copy()416gdf["Wealth"] = (1 + gdf["Cumulative_Return"]) * start_amt417 418# ④ plot with Altair419growth_chart = (420    alt.Chart(gdf)421    .mark_line(strokeWidth=2, color="#1f77b4")422    .encode(423        x="Date:T",424        y=alt.Y("Wealth:Q", title="Portfolio Value ($)"),425        tooltip=["Date:T", alt.Tooltip("Wealth:Q", format="$.2f")],426    )427    .properties(height=350, width=900)428)429st.altair_chart(growth_chart, use_container_width=True)430 431# ⑤ show current value as a metric432final_val = gdf["Wealth"].iloc[-1]433st.metric(434    label=f"Value today of ${start_amt} invested in {choice}",435    value=f"${final_val:,.2f}",436)437 438# =============================================================================439# ROW 3 ▸ Risk vs Return scatter440# =============================================================================441# =============================================================================442# ROW 3 ▸ Risk vs Return443# =============================================================================444import numpy as np445import altair as alt446import plotly.graph_objects as go447 448st.header("Risk vs Return")449with st.expander("About Risk vs Return Analysis"):450    st.markdown("""451This section provides a detailed comparison of **risk-adjusted performance** for various ETFs using standard financial metrics like **Volatility**, **Return**, and the **Sharpe Ratio**. Let’s break it down:452 453---454 455### Controls (Left Panel)456 457- **Year Window**:  458  Adjust the date range to select the historical time period for analysis. This helps to study how ETFs performed in different market cycles (e.g., bull markets, crashes, recoveries).459 460- **Frequency (Daily / Monthly / Yearly)**:  461  Choose the time scale for return and volatility calculation.  462  - **Daily**: Most granular but noisier.463  - **Monthly**: Good balance between detail and smoothness.464  - **Yearly**: Smoothest, best for long-term performance view.465 466- **Risk-Free Rate (%)**:  467  Used in Sharpe Ratio calculations. This represents the return from a "riskless" investment like U.S. Treasuries. Adjust this slider based on macroeconomic assumptions or personal benchmarks.468 469- **Highlight ETFs**:  470  Optionally emphasize specific ETFs in the scatter plot. Helpful for visual isolation of desired funds.471 472- **ETF to Gauge**:  473  Select an ETF to inspect its **Volatility Gauge**, Sharpe Ratio, and annualized return in more detail.474 475---476 477### Scatter Plot (Center)478 479Each dot represents an ETF, plotted using:480- **X-Axis (Volatility σ)**:  481  Annualized standard deviation of returns — a measure of risk or price fluctuation.482- **Y-Axis (Return μ)**:  483  Annualized average return during the selected period.484 485This visual shows the risk-return tradeoff. Ideally, we want funds in the **top-left quadrant** — high return, low volatility.486 487---488 489### Volatility Gauge (Right Panel)490 491- This semi-circular gauge shows the **annualized volatility** of the ETF selected in the dropdown.492- Color zones:493  - Green = Low volatility (more stable)494  - Yellow = Medium volatility495  - Red = High volatility (riskier)496- The black marker shows where your selected ETF stands.497 498This helps investors **visually assess the ETF’s stability** and compare it to historical volatility thresholds.499 500---501 502### Risk–Return Snapshot (Below Gauge)503 504This dynamic text highlights:505- **Risk Category** (Low/Med/High) based on volatility506- Arrow indicating return direction (↑ for positive, ↓ for negative)507- **Annualized Return** (percentage return normalized to yearly scale)508 509Together, this gives a clean, instant overview of risk vs reward for a specific ETF.510 511---512 513### Sharpe Ratio Metrics (Bottom Panel)514 515- **Best Sharpe**: ETF with the highest Sharpe Ratio — offers the most efficient return per unit of risk.516- **Worst Sharpe**: ETF with the lowest Sharpe — least efficient.517- **Avg Sharpe**: Mean Sharpe Ratio across all ETFs.518- **Quadrant Metrics**:519  - ⬆ Return / ⬇ Vol: ETFs with high return and low volatility — desirable zone.520  - ⬇ Return / ⬆ Vol: Undesirable zone — low returns with high risk.521 522---523 524### Why this matters?525 526This section lets users:527- Explore **risk-adjusted return** in a quantitative and visual way.528- Compare ETFs not just on returns, but on **efficiency**.529- Make informed decisions aligned with their **risk tolerance** and **investment horizon**.530 531Use this to evaluate if a higher return is worth the additional volatility, and discover which ETFs strike the best balance.532 533    """)534 535 536# 3‑column layout: controls | scatter | gauge537ctrl_col, scat_col, gauge_col = st.columns([1, 2.5, 1.2])538 539# ─────────────────────────  controls  ──────────────────────────540with ctrl_col:541    start_year, end_year = st.slider(542        "Year window:",543        int(data.Year.min()), int(data.Year.max()),544        (int(data.Year.min()), int(data.Year.max())),545        step=1, key="risk_years"546    )547 548    freq = st.radio(549        "Frequency:", ["Daily", "Monthly", "Yearly"],550        index=0, horizontal=True551    )552 553    rf = st.slider(554        "Risk‑free rate (%)", 0.0, 5.0, 0.0, 0.25,555        key="rf"556    )557 558    hi_etfs = st.multiselect(559        "Highlight ETFs (optional)",560        options=data["ETF"].unique(),561        default=[]562    )563 564    # NEW — gauge ETF selector lives with the other controls565    g_etf = st.selectbox(566        "ETF to gauge:",567        options=data["ETF"].unique(),568        key="gauge_etf"569    )570 571# ────────────────────  data window & stats  ───────────────────572mask = (573    (data["Date"] >= f"{start_year}-01-01") &574    (data["Date"] <= f"{end_year}-12-31")575)576win = data[mask].copy()577 578if freq == "Monthly":579    win["Period_Return"] = win.groupby("ETF")["Close"].pct_change()580    win = (581        win.set_index("Date")582           .groupby("ETF")["Period_Return"]583           .resample("M").sum()584           .unstack("ETF").stack()585           .reset_index()586           .rename(columns={0: "Period_Return"})587    )588elif freq == "Yearly":589    win["Period_Return"] = win.groupby("ETF")["Close"].pct_change()590    win = (591        win.set_index("Date")592           .groupby("ETF")["Period_Return"]593           .resample("Y").sum()594           .unstack("ETF").stack()595           .reset_index()596           .rename(columns={0: "Period_Return"})597    )598else:                                # Daily599    win["Period_Return"] = win["Daily_Return"]600 601stats = (602    win.groupby("ETF")["Period_Return"]603       .agg(mu="mean", sigma="std")604       .assign(605           ann_mu    = lambda d: d.mu    * (252 if freq=="Daily" else 12 if freq=="Monthly" else 1),606           ann_sigma = lambda d: d.sigma * (np.sqrt(252) if freq=="Daily" else np.sqrt(12) if freq=="Monthly" else 1),607       )608)609stats["Sharpe"] = (stats.ann_mu - rf/100) / stats.ann_sigma610 611# quadrant counts612median_sigma = stats.ann_sigma.median()613quad = (stats.ann_mu > 0).astype(str) + "-" + (stats.ann_sigma > median_sigma).astype(str)614qcounts = quad.value_counts().reindex(615    ["True-False","True-True","False-False","False-True"], fill_value=0616)617names = {618    "True-False": "⬆ Return / ⬇ Vol",619    "True-True" : "⬆ Return / ⬆ Vol",620    "False-False":"⬇ Return / ⬇ Vol",621    "False-True" : "⬇ Return / ⬆ Vol",622}623 624plot_df = stats.reset_index()625if hi_etfs:626    plot_df = plot_df[plot_df.ETF.isin(hi_etfs)]627 628# ─────────────────────  scatter plot  ──────────────────────629with scat_col:630    scatter = (631        alt.Chart(plot_df)632           .mark_circle(size=200)633           .encode(634               x=alt.X("ann_sigma:Q", title="Volatility σ (annualised)"),635               y=alt.Y("ann_mu:Q",    title="Return μ (annualised)"),636               color="ETF",637               tooltip=[638                   "ETF",639                   alt.Tooltip("ann_mu:Q",    title="Ann Return", format=".2%"),640                   alt.Tooltip("ann_sigma:Q", title="Ann Vol",    format=".2%"),641                   alt.Tooltip("Sharpe:Q",    format=".2f"),642               ],643           )644           .properties(height=420)645    )646    st.altair_chart(scatter, use_container_width=True)647 648# ─────────────────────  gauge only  ───────────────────────649with gauge_col:650    st.subheader("Volatility Gauge")651 652    # thresholds653    vals        = stats.ann_sigma.sort_values()654    low, high   = np.percentile(vals, [33, 66])655    val         = stats.loc[g_etf, "ann_sigma"]656 657    fig_g = go.Figure(go.Indicator(658        mode="gauge+number",659        value=val,660        number={"valueformat": ".3f"},661        title={"text": f"Ann σ for {g_etf}"},662        gauge={663            "axis":   {"range": [0, vals.max()*1.1]},664            "steps": [665                {"range":[0,    low],  "color":"#4CAF50"},666                {"range":[low, high],  "color":"#FFC107"},667                {"range":[high, vals.max()*1.1], "color":"#F44336"},668            ],669            "threshold": {670                "value":     val,671                "line":      {"color":"black","width":4},672                "thickness": 0.75,673            },674        },675    ))676    fig_g.update_layout(margin=dict(l=0,r=0,t=25,b=0), height=420)677    st.plotly_chart(fig_g, use_container_width=True)678 679    level     = "Low Risk" if val < low else "Med Risk" if val < high else "High Risk"680    arrow     = "⬆️" if stats.loc[g_etf,"ann_mu"] > 0 else "⬇️"681    ann_ret   = stats.loc[g_etf,"ann_mu"]682    st.metric(683        "Risk–Return Snapshot",684        f"{level} / {arrow} {ann_ret:.2%} ann return"685    )686 687# ────────────────────  Sharpe metric strip  ───────────────────688best  = stats.Sharpe.idxmax()689worst = stats.Sharpe.idxmin()690m1, m2, m3, m4, m5 = st.columns(5)691m1.metric("Best Sharpe",  best,  f"{stats.loc[best,'Sharpe']:.2f}")692m2.metric("Worst Sharpe", worst, f"{stats.loc[worst,'Sharpe']:.2f}")693m3.metric("Avg Sharpe",          f"{stats.Sharpe.mean():.2f}")694m4.metric(names["True-False"], int(qcounts["True-False"]))695m5.metric(names["False-True"], int(qcounts["False-True"]))696 697 698# =============================================================================699# ROW 4 ▸ Calendar heatmap700# =============================================================================701import calendar702 703# ── Monthly x Year Heatmap ────────────────────────────────────────────────704st.header("Monthly Returns Heatmap")705with st.expander("About Monthly Returns Visualization"):706    st.markdown("""707This section provides a detailed month-by-month breakdown of ETF performance over multiple years using two complementary visualizations.708 709---710 711### Monthly Returns Heatmap (Top Chart)712 713- **What It Shows**:  714  A heatmap matrix where:715  - **Rows** represent years (within the selected range)716  - **Columns** represent months (January to December)717  - **Cell colors** represent the average daily return for each ETF in that month718 719- **Color Coding**:720  - Blue tones indicate positive average returns (darker blues signal stronger gains)721  - Red tones indicate negative returns722  - Near-white means close to zero or neutral returns723 724- **Purpose**:  725  Enables quick visual detection of:726  - Seasonal return trends (e.g., consistent December rallies)727  - Crisis years or prolonged underperformance728  - Monthly volatility patterns across the years729 730- **User Controls**:731  - ETF Selector: Choose the fund to analyze732  - Year Range Slider: Narrow or expand the time horizon733 734---735 736### Monthly Returns Breakdown (Bottom Bar Chart)737 738- **What It Shows**:  739  A bar chart for a selected year from the heatmap:740  - **X-axis** shows months741  - **Y-axis** displays average daily returns in each month742 743- **Purpose**:744  - Offers a zoomed-in view of monthly behavior within a specific year745  - Clarifies which months drove annual performance746  - Highlights inconsistencies or standout months in otherwise stable years747 748---749 750### How the Two Charts Work Together751 752- The bar chart is linked to the heatmap:753  - Selecting a year updates the bar chart to show monthly detail754  - Provides seamless transition from a multi-year overview to annual resolution755 756- **Example**:757  - A dark red October cell in the heatmap for 2008 suggests a drawdown758  - Select 2008 in the bar chart to confirm that October was indeed a steep decline759 760---761 762### Significance763 764Understanding ETF performance at the monthly level is useful for:765- Timing market entries and exits more effectively766- Detecting seasonal or cyclical behavior in ETF returns767- Adjusting portfolio rebalancing strategies768- Enhancing historical awareness of volatility and market anomalies769 770This dual-chart setup enables both high-level pattern recognition and granular month-by-month analysis—supporting more informed, data-driven decisions.771    """)772 773 774# choose ETF & year range775etf_sel = st.selectbox("ETF:", data.ETF.unique())776yr_min, yr_max = int(data.Date.dt.year.min()), int(data.Date.dt.year.max())777year_range = st.slider("Year range:", yr_min, yr_max, (yr_min, yr_max), step=1)778 779# prepare monthly avg returns780df_month = (781    data[(data.ETF==etf_sel)]782      .assign(Year=lambda d: d.Date.dt.year,783              Month=lambda d: d.Date.dt.month)784      .query("Year >= @year_range[0] and Year <= @year_range[1]")785      .groupby(["Year","Month"])["Daily_Return"]786      .mean()787      .reset_index()788)789 790# pivot into heatmap-friendly791heat_df = df_month.pivot(index="Year", columns="Month", values="Daily_Return").fillna(0)792 793# altair heatmap794heatmap = (795    alt.Chart(df_month)796       .mark_rect()797       .encode(798           x=alt.X("Month:O", title="Month",799                   axis=alt.Axis(labelFlush=True, labelAngle=0, tickCount=12,800                                 labelExpr="datum.value>0 ? datum.value : ''",801                                 labelFontSize=10)),802           y=alt.Y("Year:O", title="Year",803                   axis=alt.Axis(labelFontSize=10)),804           color=alt.Color("Daily_Return:Q",805                           title="Avg Daily Return",806                           scale=alt.Scale(scheme="redblue", domainMid=0)),807           tooltip=[808             alt.Tooltip("Year:O"), 809             alt.Tooltip("Month:O", title="Mon"),810             alt.Tooltip("Daily_Return:Q", format=".2%")811           ]812       )813       .properties(width=700, height=300)814)815st.altair_chart(heatmap, use_container_width=True)816 817 818# ── Drill-down: Monthly Bar Chart ─────────────────────────────────────────819st.subheader(f"{etf_sel} Monthly Returns Breakdown")820 821# pick a single year for bar chart822bar_year = st.selectbox("Select year:", list(range(year_range[0], year_range[1]+1)))823df_bar = (824    df_month[df_month.Year==bar_year]825      .sort_values("Month")826)827 828bar = (829    alt.Chart(df_bar)830       .mark_bar()831       .encode(832           x=alt.X("Month:O", title="Month",833                   axis=alt.Axis(labelAngle=0, tickCount=12)),834           y=alt.Y("Daily_Return:Q", title="Avg Daily Return", axis=alt.Axis(format=".2%")),835           tooltip=[alt.Tooltip("Daily_Return:Q", format=".2%"),836                    "Month:O"]837       )838       .properties(width=700, height=200)839)840st.altair_chart(bar, use_container_width=True)841 842# =============================================================================843# NEW SECTION ▸ ETF Radar Comparison844# =============================================================================845import plotly.express as px846 847st.header("ETF Radar Comparison")848with st.expander("About ETF Radar Comparison"):849    st.markdown("""850The **ETF Radar Comparison** module enables side-by-side analysis of two selected ETFs using both a normalized radar chart and actual metric values.851 852---853 854### Radar Chart (Top)855 856- **Purpose**:  857  Provides a visual snapshot of how two ETFs stack up across key performance and risk metrics for a selected year.858 859- **Metrics Compared**:860  - **Annual Return**: Total percentage gain over the selected year.861  - **Annual Volatility**: Standard deviation of daily returns annualized—higher values imply greater risk.862  - **Max Drawdown**: Worst peak-to-trough decline in adjusted closing price—used to assess downside risk.863  - **Sharpe Ratio**: Risk-adjusted return; calculated as \((\mu - r_f) / \sigma\), where:864    - \( \mu \) = average return865    - \( r_f \) = risk-free rate (e.g., treasury yield)866    - \( \sigma \) = volatility867  - **CAGR (Compound Annual Growth Rate)**: Measures the geometric growth rate assuming reinvestment and smooth compounding.868 869- **Normalization**:870  Each metric is scaled from 0 to 1 across the two ETFs to fit the radar format, allowing quick visual comparison even if the raw values differ in magnitude.871 872- **Visual Interpretation**:873  - A larger, more filled-in polygon suggests stronger performance across the board.874  - For instance, if one ETF has higher annual return, better Sharpe, and lower drawdown, it will dominate the radar plot.875 876---877 878### Metric Table (Bottom)879 880- **Purpose**:  881  Complements the radar chart by showing actual, unscaled values for precision.882 883- **Columns**:884  - Each row corresponds to an ETF885  - Columns include Annual Return, Volatility, Max Drawdown, Sharpe Ratio, and CAGR886  - Values are formatted clearly with percent signs and decimals for easy reference887 888---889 890### Interactivity891 892- **ETF Selector**: Choose any two ETFs from the dropdown893- **Year Slider**: Adjust the year to compare ETF performance in different market regimes (e.g., crisis years, bull runs)894 895---896 897### Use Cases898 899- Compare growth-oriented vs. defensive ETFs in volatile years900- Understand which ETF offers better risk-adjusted returns901- Make informed allocation decisions based on historical behavior902- Spot years when one ETF clearly outperformed across most dimensions903 904This comparison tool allows both quick pattern recognition (radar chart) and precision evaluation (data table), helping users make nuanced judgments about ETF performance.905    """)906 907 908# ── Controls ────────────────────────────────────────────────────────────────909etfs_radar = st.multiselect(910    "Select up to 2 ETFs to compare:",911    options=data.ETF.unique(),912    default=["EEM", "IVV"],913    max_selections=2914)915radar_year = st.slider(916    "Choose year for snapshot:",917    int(data.Date.dt.year.min()),918    int(data.Date.dt.year.max()),919    int(data.Date.dt.year.max())920)921 922# ── Compute snapshot stats at annual frequency ───────────────────────────────923df_year = (924    data.assign(Year=data.Date.dt.year)925        .query("Year == @radar_year")926        .groupby("ETF")927        .agg(928            Annual_Return=("Daily_Return", lambda x: (1 + x).prod() - 1),929            Annual_Volatility=("Daily_Return", "std"),930            Max_Drawdown=("Drawdown", "min"),931            Sharpe=("Daily_Return", lambda x: (x.mean() * 252) / (x.std() * np.sqrt(252))),932            CAGR=("Daily_Return", lambda x: (1 + x).prod() ** (1 / 1) - 1)933        )934        .reset_index()935        .query("ETF in @etfs_radar")936)937 938# ── Melt for polar plot ───────────────────────────────────────────────────────939radar_df = df_year.melt(940    id_vars="ETF",941    var_name="Metric",942    value_name="Value"943)944 945# ── Normalize metrics to [0,1] for display ───────────────────────────────────946# (so that different scales can be seen relative to each other)947norms = {}948for m in radar_df.Metric.unique():949    mn, mx = radar_df.query("Metric == @m").Value.min(), radar_df.query("Metric == @m").Value.max()950    norms[m] = (mn, mx)951radar_df["NormValue"] = radar_df.apply(952    lambda row: (row.Value - norms[row.Metric][0]) / (norms[row.Metric][1] - norms[row.Metric][0] + 1e-9),953    axis=1954)955 956# ── Build radar chart ─────────────────────────────────────────────────────────957fig_radar = px.line_polar(958    radar_df,959    r="NormValue",960    theta="Metric",961    color="ETF",962    line_close=True,963    template="plotly_dark",964    title=f"ETF Metrics Radar — {radar_year}"965)966fig_radar.update_traces(fill="toself", opacity=0.6)967fig_radar.update_layout(968    polar=dict(969        radialaxis=dict(range=[0,1], visible=True, tickvals=[0,0.5,1], ticktext=["Low","Med","High"])970    ),971    legend=dict(title=None),972    margin=dict(l=20,r=20,t=40,b=20),973    height=500974)975st.plotly_chart(fig_radar, use_container_width=True)976 977# ── Show actual numbers in table ─────────────────────────────────────────────978st.subheader("Actual Metric Values")979st.dataframe(980    df_year.set_index("ETF").style.format({981        "Annual_Return": "{:.1%}",982        "Annual_Volatility": "{:.2%}",983        "Max_Drawdown": "{:.1%}",984        "Sharpe": "{:.2f}",985        "CAGR": "{:.1%}",986    }),987    use_container_width=True988)989