Spaces:
Sleeping
Sleeping
| 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 | |
| ############################# | |
| 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']}<br>Market Value: {mv_formatted}<br>" | |
| f"Shares: {shares_formatted}<br>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 | |
| ############################# | |
| 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}<br>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 | |
| ############################# | |
| 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}<br>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 = """ | |
| <style> | |
| #MainMenu {visibility: hidden;} | |
| footer {visibility: hidden;} | |
| </style> | |
| """ | |
| st.markdown(hide_streamlit_style, unsafe_allow_html=True) | |