QuantumLearner commited on
Commit
00e5508
·
verified ·
1 Parent(s): e4bebda

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +224 -0
app.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import requests
3
+ import pandas as pd
4
+ import plotly.express as px
5
+ import os
6
+
7
+ # Global API key and default ETF symbols
8
+
9
+ API_KEY = os.getenv("FMP_API_KEY")
10
+
11
+ ETF_ASSET_SYMBOL = "SPY" # Used for asset composition
12
+
13
+ # For sector and country composition, you may adjust this as needed.
14
+ ETF_SYMBOL = "QDVE.DE"
15
+
16
+ def format_number(n):
17
+ """
18
+ Format a number with K, M, or B suffix.
19
+ """
20
+ abs_n = abs(n)
21
+ if abs_n < 1e3:
22
+ return str(round(n))
23
+ elif abs_n < 1e6:
24
+ return f"{round(n/1e3)}K"
25
+ elif abs_n < 1e9:
26
+ return f"{round(n/1e6)}M"
27
+ else:
28
+ return f"{round(n/1e9)}B"
29
+
30
+ #############################
31
+ # ETF Asset Composition Functions
32
+ #############################
33
+ @st.cache_data(show_spinner=False)
34
+ def get_etf_asset_composition(etf_symbol: str) -> pd.DataFrame:
35
+ url = f"https://financialmodelingprep.com/api/v3/etf-holder/{etf_symbol}?apikey={API_KEY}"
36
+ response = requests.get(url)
37
+ response.raise_for_status()
38
+ data = response.json()
39
+ if not data:
40
+ return pd.DataFrame()
41
+ df = pd.DataFrame(data)
42
+ df['marketValue'] = pd.to_numeric(df['marketValue'], errors='coerce')
43
+ df_sorted = df.sort_values(by="marketValue", ascending=False)
44
+ return df_sorted
45
+
46
+ def plot_etf_asset_composition(df: pd.DataFrame, etf_symbol: str):
47
+ if df.empty:
48
+ return None
49
+ date_as_of = df.iloc[0]['updated'] if ('updated' in df.columns and not df.empty) else "N/A"
50
+ tickers = df['asset'].tolist()
51
+ market_values = df['marketValue'].tolist()
52
+ labels = []
53
+ for _, row in df.iterrows():
54
+ mv_formatted = format_number(row['marketValue'])
55
+ shares_formatted = format_number(row['sharesNumber'])
56
+ weight_formatted = f"{round(row['weightPercentage'], 2)}%"
57
+ labels.append(
58
+ f"{row['asset']}<br>Market Value: {mv_formatted}<br>"
59
+ f"Shares: {shares_formatted}<br>Weight: {weight_formatted}"
60
+ )
61
+ title = f"ETF Holdings by Market Value for {etf_symbol} (as of {date_as_of})"
62
+ fig = px.bar(
63
+ x=tickers,
64
+ y=market_values,
65
+ text=labels,
66
+ title=title,
67
+ labels={"x": "Asset", "y": "Market Value (USD)"}
68
+ )
69
+ fig.update_traces(textposition='outside')
70
+ fig.update_layout(uniformtext_minsize=8, uniformtext_mode='hide')
71
+ return fig
72
+
73
+ #############################
74
+ # Sector Composition Functions
75
+ #############################
76
+ @st.cache_data(show_spinner=False)
77
+ def get_etf_sector_composition(etf_symbol: str) -> pd.DataFrame:
78
+ url = f"https://financialmodelingprep.com/api/v3/etf-sector-weightings/{etf_symbol}?apikey={API_KEY}"
79
+ response = requests.get(url)
80
+ response.raise_for_status()
81
+ data = response.json()
82
+ if not data:
83
+ return pd.DataFrame()
84
+ df = pd.DataFrame(data)
85
+ df['weightPercentage'] = df['weightPercentage'].str.rstrip('%').astype(float)
86
+ df_sorted = df.sort_values(by="weightPercentage", ascending=False)
87
+ return df_sorted
88
+
89
+ def plot_etf_sector_composition(df: pd.DataFrame, etf_symbol: str):
90
+ if df.empty:
91
+ return None
92
+ sectors = df['sector'].tolist()
93
+ weights = df['weightPercentage'].tolist()
94
+ labels = [f"{sector}<br>Weight: {weight:.2f}%" for sector, weight in zip(sectors, weights)]
95
+ title = f"ETF Sector Weighting for {etf_symbol}"
96
+ fig = px.bar(
97
+ x=sectors,
98
+ y=weights,
99
+ text=labels,
100
+ title=title,
101
+ labels={"x": "Sector", "y": "Weight Percentage"}
102
+ )
103
+ fig.update_traces(textposition='outside')
104
+ fig.update_layout(uniformtext_minsize=8, uniformtext_mode='hide')
105
+ return fig
106
+
107
+ #############################
108
+ # Country Composition Functions
109
+ #############################
110
+ @st.cache_data(show_spinner=False)
111
+ def get_etf_country_composition(etf_symbol: str) -> pd.DataFrame:
112
+ url = f"https://financialmodelingprep.com/api/v3/etf-country-weightings/{etf_symbol}?apikey={API_KEY}"
113
+ response = requests.get(url)
114
+ response.raise_for_status()
115
+ data = response.json()
116
+ if not data:
117
+ return pd.DataFrame()
118
+ df = pd.DataFrame(data)
119
+ df['weightPercentage'] = df['weightPercentage'].str.rstrip('%').astype(float)
120
+ df_sorted = df.sort_values(by="weightPercentage", ascending=False)
121
+ return df_sorted
122
+
123
+ def plot_etf_country_composition(df: pd.DataFrame, etf_symbol: str):
124
+ if df.empty:
125
+ return None
126
+ countries = df['country'].tolist()
127
+ weights = df['weightPercentage'].tolist()
128
+ labels = [f"{country}<br>Weight: {weight:.2f}%" for country, weight in zip(countries, weights)]
129
+ title = f"ETF Country Weighting for {etf_symbol}"
130
+ fig = px.bar(
131
+ x=countries,
132
+ y=weights,
133
+ text=labels,
134
+ title=title,
135
+ labels={"x": "Country", "y": "Weight Percentage"}
136
+ )
137
+ fig.update_traces(textposition='outside')
138
+ fig.update_layout(uniformtext_minsize=8, uniformtext_mode='hide')
139
+ return fig
140
+
141
+ #############################
142
+ # MAIN APP
143
+ #############################
144
+ def main():
145
+ st.set_page_config(page_title="ETF Asset Composition Dashboard", layout="wide")
146
+ st.title("ETF Asset Composition Dashboard")
147
+ st.write(
148
+ "This dashboard displays the composition of an ETF. "
149
+ "Use the side menu to enter the ETF ticker symbol and then click the 'Run ETF Composition' button. "
150
+ "The dashboard is divided into three sections: Asset Composition, Sector Composition, and Country Composition. "
151
+ "Each section includes a chart and a data table. Hover over the charts for more details."
152
+ )
153
+
154
+ # Initialize run button session state variable
155
+ if "run_etf" not in st.session_state:
156
+ st.session_state.run_etf = False
157
+
158
+ # Sidebar: Settings inside an expander
159
+ with st.sidebar.expander("Settings", expanded=True):
160
+ etf_ticker = st.text_input(
161
+ "ETF Ticker",
162
+ value="SPY",
163
+ help="Enter the ticker symbol of the ETF (e.g., SPY, QQQ)."
164
+ )
165
+ if st.button("Run ETF Composition"):
166
+ st.session_state.run_etf = True
167
+ st.session_state.etf_ticker = etf_ticker
168
+
169
+ # Main content: Only show dashboard if run button was pressed
170
+ if st.session_state.get("run_etf", False):
171
+ # Section 1: ETF Asset Composition
172
+ st.header("1. ETF Asset Composition")
173
+ st.write(
174
+ "This section shows the holdings of the ETF by asset. "
175
+ "The bar chart displays each asset's market value, shares held, and weight in the ETF. "
176
+ "The data table below shows the raw holdings data."
177
+ )
178
+ asset_df = get_etf_asset_composition(st.session_state.etf_ticker)
179
+ asset_fig = plot_etf_asset_composition(asset_df, st.session_state.etf_ticker)
180
+ if asset_fig is not None:
181
+ st.plotly_chart(asset_fig, use_container_width=True)
182
+ st.subheader("Data")
183
+ st.dataframe(asset_df, use_container_width=True)
184
+
185
+ # Section 2: Sector Composition
186
+ st.header("2. Sector Composition")
187
+ st.write(
188
+ "This section displays the ETF's sector weighting. "
189
+ "The bar chart visualizes the weight percentage of each sector represented in the ETF. "
190
+ "The data table below contains the raw sector weighting data."
191
+ )
192
+ sector_df = get_etf_sector_composition(st.session_state.etf_ticker)
193
+ sector_fig = plot_etf_sector_composition(sector_df, st.session_state.etf_ticker)
194
+ if sector_fig is not None:
195
+ st.plotly_chart(sector_fig, use_container_width=True)
196
+ st.subheader("Data")
197
+ st.dataframe(sector_df, use_container_width=True)
198
+
199
+ # Section 3: Country Composition
200
+ st.header("3. Country Composition")
201
+ st.write(
202
+ "This section shows the geographic distribution of the ETF's holdings by country. "
203
+ "The bar chart displays the weight percentage by country, and the table below lists the detailed country weighting data."
204
+ )
205
+ country_df = get_etf_country_composition(st.session_state.etf_ticker)
206
+ country_fig = plot_etf_country_composition(country_df, st.session_state.etf_ticker)
207
+ if country_fig is not None:
208
+ st.plotly_chart(country_fig, use_container_width=True)
209
+ st.subheader("Data")
210
+ st.dataframe(country_df, use_container_width=True)
211
+ else:
212
+ st.info("Please enter the ETF ticker in the sidebar and click 'Run ETF Composition'.")
213
+
214
+ if __name__ == "__main__":
215
+ main()
216
+
217
+
218
+ hide_streamlit_style = """
219
+ <style>
220
+ #MainMenu {visibility: hidden;}
221
+ footer {visibility: hidden;}
222
+ </style>
223
+ """
224
+ st.markdown(hide_streamlit_style, unsafe_allow_html=True)