Space49 / app.py
QuantumLearner's picture
Update app.py
f6522a9 verified
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)