import streamlit as st import requests import pandas as pd import plotly.express as px import os # Global API key and default ETF symbols API_KEY = os.getenv("FMP_API_KEY") #ETF_ASSET_SYMBOL = "SPY" # Used for asset composition #ETF_SYMBOL = "QDVE.DE" # For sector and country composition, you may adjust this as needed. def format_number(n): """ Format a number with K, M, or B suffix. """ abs_n = abs(n) if abs_n < 1e3: return str(round(n)) elif abs_n < 1e6: return f"{round(n/1e3)}K" elif abs_n < 1e9: return f"{round(n/1e6)}M" else: return f"{round(n/1e9)}B" ############################# # ETF Asset Composition Functions ############################# @st.cache_data(show_spinner=False) def get_etf_asset_composition(etf_symbol: str) -> pd.DataFrame: url = f"https://financialmodelingprep.com/api/v3/etf-holder/{etf_symbol}?apikey={API_KEY}" try: response = requests.get(url) response.raise_for_status() data = response.json() if not data: return pd.DataFrame() df = pd.DataFrame(data) df['marketValue'] = pd.to_numeric(df['marketValue'], errors='coerce') df_sorted = df.sort_values(by="marketValue", ascending=False) return df_sorted except: # Generic failover, no mention of the source return pd.DataFrame() def plot_etf_asset_composition(df: pd.DataFrame, etf_symbol: str): if df.empty: return None date_as_of = df.iloc[0]['updated'] if ('updated' in df.columns and not df.empty) else "N/A" tickers = df['asset'].tolist() market_values = df['marketValue'].tolist() labels = [] for _, row in df.iterrows(): mv_formatted = format_number(row['marketValue']) shares_formatted = format_number(row['sharesNumber']) weight_formatted = f"{round(row['weightPercentage'], 2)}%" labels.append( f"{row['asset']}
Market Value: {mv_formatted}
" f"Shares: {shares_formatted}
Weight: {weight_formatted}" ) title = f"ETF Holdings by Market Value for {etf_symbol} (as of {date_as_of})" fig = px.bar( x=tickers, y=market_values, text=labels, title=title, labels={"x": "Asset", "y": "Market Value (USD)"} ) fig.update_traces(textposition='outside') fig.update_layout(uniformtext_minsize=8, uniformtext_mode='hide') return fig ############################# # Sector Composition Functions ############################# @st.cache_data(show_spinner=False) def get_etf_sector_composition(etf_symbol: str) -> pd.DataFrame: url = f"https://financialmodelingprep.com/api/v3/etf-sector-weightings/{etf_symbol}?apikey={API_KEY}" try: response = requests.get(url) response.raise_for_status() data = response.json() if not data: return pd.DataFrame() df = pd.DataFrame(data) df['weightPercentage'] = df['weightPercentage'].str.rstrip('%').astype(float) df_sorted = df.sort_values(by="weightPercentage", ascending=False) return df_sorted except: return pd.DataFrame() def plot_etf_sector_composition(df: pd.DataFrame, etf_symbol: str): if df.empty: return None sectors = df['sector'].tolist() weights = df['weightPercentage'].tolist() labels = [f"{sector}
Weight: {weight:.2f}%" for sector, weight in zip(sectors, weights)] title = f"ETF Sector Weighting for {etf_symbol}" fig = px.bar( x=sectors, y=weights, text=labels, title=title, labels={"x": "Sector", "y": "Weight Percentage"} ) fig.update_traces(textposition='outside') fig.update_layout(uniformtext_minsize=8, uniformtext_mode='hide') return fig ############################# # Country Composition Functions ############################# @st.cache_data(show_spinner=False) def get_etf_country_composition(etf_symbol: str) -> pd.DataFrame: url = f"https://financialmodelingprep.com/api/v3/etf-country-weightings/{etf_symbol}?apikey={API_KEY}" try: response = requests.get(url) response.raise_for_status() data = response.json() if not data: return pd.DataFrame() df = pd.DataFrame(data) df['weightPercentage'] = df['weightPercentage'].str.rstrip('%').astype(float) df_sorted = df.sort_values(by="weightPercentage", ascending=False) return df_sorted except: return pd.DataFrame() def plot_etf_country_composition(df: pd.DataFrame, etf_symbol: str): if df.empty: return None countries = df['country'].tolist() weights = df['weightPercentage'].tolist() labels = [f"{country}
Weight: {weight:.2f}%" for country, weight in zip(countries, weights)] title = f"ETF Country Weighting for {etf_symbol}" fig = px.bar( x=countries, y=weights, text=labels, title=title, labels={"x": "Country", "y": "Weight Percentage"} ) fig.update_traces(textposition='outside') fig.update_layout(uniformtext_minsize=8, uniformtext_mode='hide') return fig ############################# # MAIN APP ############################# def main(): st.set_page_config(page_title="ETF Asset Composition Dashboard", layout="wide") st.title("ETF Asset Composition Dashboard") st.write( "Analyze the composition of an ETF. " "Use the side menu to enter the ETF ticker symbol and then click the 'Run ETF Composition' button. " "The analysis is divided into three sections: Asset Composition, Sector Composition, and Country Composition. " "Each section includes a chart and a data table. Hover over the charts for more details." ) # Initialize run button state if "run_etf" not in st.session_state: st.session_state.run_etf = False # Sidebar: Settings inside an expander with st.sidebar.expander("Settings", expanded=True): etf_ticker = st.text_input( "ETF Ticker", value="SPY", help="Enter the ticker symbol of the ETF (e.g., SPY, QQQ)." ) if st.button("Run ETF Composition"): st.session_state.run_etf = True st.session_state.etf_ticker = etf_ticker # Main content if st.session_state.get("run_etf", False): # Section 1: ETF Asset Composition st.header("1. ETF Asset Composition") st.write( "Shows the top holdings by market value. " "The bar chart includes each asset's market value, shares held, and weight in the ETF." ) asset_df = get_etf_asset_composition(st.session_state.etf_ticker) asset_fig = plot_etf_asset_composition(asset_df, st.session_state.etf_ticker) if asset_fig is not None: st.plotly_chart(asset_fig, use_container_width=True) st.subheader("Data") st.dataframe(asset_df, use_container_width=True) # Section 2: Sector Composition st.header("2. Sector Composition") st.write( "Displays the ETF's sector weighting. " "The bar chart visualizes each sector's percentage in the portfolio." ) sector_df = get_etf_sector_composition(st.session_state.etf_ticker) sector_fig = plot_etf_sector_composition(sector_df, st.session_state.etf_ticker) if sector_fig is not None: st.plotly_chart(sector_fig, use_container_width=True) st.subheader("Data") st.dataframe(sector_df, use_container_width=True) # Section 3: Country Composition st.header("3. Country Composition") st.write( "Shows the geographic distribution of the ETF's holdings by country. " "The bar chart displays the weight percentage by country." ) country_df = get_etf_country_composition(st.session_state.etf_ticker) country_fig = plot_etf_country_composition(country_df, st.session_state.etf_ticker) if country_fig is not None: st.plotly_chart(country_fig, use_container_width=True) st.subheader("Data") st.dataframe(country_df, use_container_width=True) else: st.info("Please enter the ETF ticker in the sidebar and click 'Run ETF Composition'.") if __name__ == "__main__": main() hide_streamlit_style = """ """ st.markdown(hide_streamlit_style, unsafe_allow_html=True)