Spaces:
Sleeping
Sleeping
File size: 8,569 Bytes
00e5508 f6522a9 00e5508 cde486e 00e5508 cde486e 00e5508 cde486e 00e5508 cde486e 00e5508 cde486e 00e5508 cde486e 00e5508 cde486e 00e5508 cde486e 00e5508 cde486e 00e5508 cde486e 00e5508 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | 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']}<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
#############################
@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}<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
#############################
@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}<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)
|