QuantumLearner commited on
Commit
d1d2c07
·
verified ·
1 Parent(s): 98f1c8f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +178 -133
app.py CHANGED
@@ -5,14 +5,13 @@ import yfinance as yf
5
  import datetime
6
  import plotly.graph_objs as go
7
 
8
- # ---------- Page config (must be the first Streamlit call) ----------
9
  st.set_page_config(layout="wide")
10
 
11
- # ---------- Stable CSS for wider sidebar (avoid fragile class names) ----------
12
  st.markdown(
13
  """
14
  <style>
15
- /* Make the sidebar wider in a stable way */
16
  [data-testid="stSidebar"] {
17
  width: 350px;
18
  min-width: 350px;
@@ -22,11 +21,15 @@ st.markdown(
22
  unsafe_allow_html=True,
23
  )
24
 
25
- # ---------- Session state for persistent "Run Analysis" ----------
26
  if "run_analysis" not in st.session_state:
27
  st.session_state.run_analysis = False
 
 
 
 
28
 
29
- # ---------- App title and description (kept as-is) ----------
30
  st.title("Key Economic Recession Indicators")
31
  st.markdown("""
32
  This tool allows you to visualize and analyze various recession indicators over time.
@@ -34,7 +37,7 @@ This tool allows you to visualize and analyze various recession indicators over
34
  - Use the checkboxes in the sidebar to choose the indicators you'd like to explore.
35
  """)
36
 
37
- # ---------- Sidebar controls ----------
38
  with st.sidebar.expander("How to Use", expanded=False):
39
  st.write("""
40
  **How to use this app:**
@@ -50,8 +53,8 @@ with st.sidebar.expander("Indicators", expanded=True):
50
  'Sahm Recession Indicator': 'SAHMREALTIME',
51
  'U.S. Recession Probabilities': 'RECPROUSM156N',
52
  'Yield Spread (10Y - 2Y)': 'Yield_Spread', # Calculated, not fetched
53
- 'Stock Market (S&P 500)': 'SP500', # Fetched from yfinance
54
- 'VIX': 'VIX', # Fetched from yfinance
55
  'Treasury Rates': ('GS10', 'DGS2', 'DGS1MO', 'TB3MS'),
56
  'Federal Funds Rate': 'FEDFUNDS',
57
  'Unemployment Rate': 'UNRATE',
@@ -65,7 +68,7 @@ with st.sidebar.expander("Indicators", expanded=True):
65
  }
66
  selected_indicators = {key: st.checkbox(key, value=True) for key in indicators.keys()}
67
 
68
- # Single Run button (no clear buttons)
69
  if st.sidebar.button("Run Analysis"):
70
  st.session_state.run_analysis = True
71
 
@@ -73,7 +76,7 @@ if st.sidebar.button("Run Analysis"):
73
  start_date = datetime.datetime(1920, 1, 1)
74
  end_date = datetime.datetime.today()
75
 
76
- # ---------- Recession periods (kept as-is) ----------
77
  crash_periods = {
78
  '1929-08-01': '1933-03-01',
79
  '1937-05-01': '1938-06-01',
@@ -92,154 +95,192 @@ crash_periods = {
92
  '2020-02-01': '2020-04-01'
93
  }
94
 
95
- # ---------- Cached data fetchers ----------
96
- @st.cache_data(ttl=6 * 60 * 60, show_spinner=False)
97
- def fetch_fred_series(series_code: str, start: datetime.datetime, end: datetime.datetime) -> pd.Series:
98
- """Fetch a single FRED series as a named Series (empty Series if fails)."""
 
 
99
  try:
100
- df = web.DataReader(series_code, 'fred', start, end)
101
- if isinstance(df, pd.DataFrame):
102
- s = df.squeeze("columns")
103
- else:
104
- s = df
105
- s = s.rename(series_code)
106
- return s
107
  except Exception as e:
108
- st.warning(f"Failed to fetch {series_code} from FRED: {e}")
109
- return pd.Series(name=series_code, dtype="float64")
110
-
111
- @st.cache_data(ttl=6 * 60 * 60, show_spinner=False)
112
- def fetch_yf_series(ticker: str, label: str, start: datetime.datetime, end: datetime.datetime) -> pd.Series:
113
- """Fetch Adj Close from Yahoo Finance as a named Series."""
 
 
 
 
 
 
 
 
 
114
  try:
115
- df = yf.download(ticker, start=start, end=end, auto_adjust=False, progress=False, threads=False)
 
 
 
 
 
116
  if isinstance(df.columns, pd.MultiIndex):
117
- df.columns = df.columns.get_level_values(0)
118
- s = df.get('Adj Close', pd.Series(dtype="float64")).rename(label)
119
- return s
 
 
 
 
 
 
 
 
 
 
 
120
  except Exception as e:
121
- st.warning(f"Failed to fetch {label} ({ticker}) from Yahoo Finance: {e}")
122
- return pd.Series(name=label, dtype="float64")
 
 
 
 
 
123
 
124
- # ---------- Build dataset ----------
125
- def build_dataset(selected: dict) -> pd.DataFrame:
126
- series_list = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
- # FRED (skip derived)
129
- for key, col in indicators.items():
130
- if not selected.get(key, False):
131
- continue
132
  if isinstance(col, tuple):
133
  for c in col:
134
- if c in ["INDPRO_PCT", "CPIAUCSL_PCT"]:
135
- continue # derived later
136
- s = fetch_fred_series(c, start_date, end_date)
137
- if not s.empty:
138
- series_list.append(s)
139
  else:
140
- if col in ["Yield_Spread", "SP500", "VIX"]:
141
- continue # handled separately / derived
142
- s = fetch_fred_series(col, start_date, end_date)
143
- if not s.empty:
144
- series_list.append(s)
145
-
146
- # YFinance
147
- if selected.get('Stock Market (S&P 500)', False):
148
- s = fetch_yf_series('^GSPC', 'SP500', start_date, end_date)
149
- if not s.empty:
150
- series_list.append(s)
151
- if selected.get('VIX', False):
152
- s = fetch_yf_series('^VIX', 'VIX', start_date, end_date)
153
- if not s.empty:
154
- series_list.append(s)
155
-
156
- if not series_list:
 
 
157
  return pd.DataFrame()
158
 
159
- combined = pd.concat(series_list, axis=1).sort_index()
160
 
161
- # Derived columns
162
- if selected.get('Industrial Production', False) and 'INDPRO' in combined.columns:
163
- combined['INDPRO_PCT'] = combined['INDPRO'].pct_change() * 100
164
- if selected.get('Inflation (CPI)', False) and 'CPIAUCSL' in combined.columns:
165
- combined['CPIAUCSL_PCT'] = combined['CPIAUCSL'].pct_change() * 100
166
 
167
- # Interpolate (time index required)
168
- combined = combined.interpolate(method='time')
169
 
170
  # Yield spread
171
- if selected.get('Yield Spread (10Y - 2Y)', False) and {'GS10', 'DGS2'}.issubset(combined.columns):
172
- combined['Yield_Spread'] = combined['GS10'] - combined['DGS2']
173
 
174
  return combined
175
 
176
- # ---------- Plotting helpers ----------
177
  def add_recession_shading(fig: go.Figure):
178
  for peak, trough in crash_periods.items():
179
  fig.add_shape(
180
- type="rect",
181
- xref="x",
182
- yref="paper",
183
- x0=peak,
184
- y0=0,
185
- x1=trough,
186
- y1=1,
187
- fillcolor="gray",
188
- opacity=0.3,
189
- layer="below",
190
- line_width=0,
191
  )
192
 
193
  def finalize_layout(fig: go.Figure, title: str, ytitle: str):
194
  fig.update_layout(
195
  title=title,
196
- xaxis_title='Date',
197
  yaxis_title=ytitle,
198
  xaxis=dict(
199
- tickformat="%Y",
200
- tickmode="linear",
201
- dtick="M36",
202
- showspikes=True,
203
- spikemode='across',
204
- spikesnap='cursor',
205
- spikethickness=1
206
  ),
207
  hovermode="x unified",
208
- hoverlabel=dict(
209
- bgcolor="white",
210
- font_size=12,
211
- font_family="Rockwell"
212
- ),
213
  legend=dict(
214
- x=0.02,
215
- y=0.95,
216
- traceorder='normal',
217
- bgcolor='rgba(255, 255, 255, 0.5)',
218
- bordercolor='rgba(0, 0, 0, 0)'
219
  ),
220
- margin=dict(l=60, r=20, t=60, b=40)
221
  )
222
  fig.update_xaxes(
223
- showgrid=True,
224
- gridwidth=1,
225
- gridcolor='LightGray',
226
- tickangle=45,
227
  tickformatstops=[
228
  dict(dtickrange=[None, "M1"], value="%b %d, %Y"),
229
- dict(dtickrange=["M1", None], value="%Y")
230
- ]
231
  )
232
- fig.update_traces(hovertemplate='%{x|%b %d, %Y}<br>%{y}<extra></extra>')
233
 
234
  # ---------- Main render ----------
235
  if st.session_state.run_analysis:
236
- with st.spinner("Fetching data and building charts..."):
237
- combined_data = build_dataset(selected_indicators)
238
 
239
- if combined_data.empty:
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  st.error("No data was successfully fetched for the selected indicators.")
241
  else:
242
- # Loop through selections and plot
243
  for key, column in indicators.items():
244
  if not selected_indicators.get(key, False):
245
  continue
@@ -248,7 +289,7 @@ if st.session_state.run_analysis:
248
  add_recession_shading(fig)
249
 
250
  if isinstance(column, tuple):
251
- # Industrial Production: level + % change on y2
252
  if column == ('INDPRO', 'INDPRO_PCT') and 'INDPRO' in combined_data.columns:
253
  fig.add_trace(go.Scatter(
254
  x=combined_data.index, y=combined_data['INDPRO'],
@@ -259,11 +300,12 @@ if st.session_state.run_analysis:
259
  x=combined_data.index, y=combined_data['INDPRO_PCT'],
260
  mode='lines', name='Industrial Production % Change', yaxis='y2'
261
  ))
262
- fig.update_layout(yaxis2=dict(title="Industrial Production % Change",
263
- overlaying='y', side='right'))
 
264
  finalize_layout(fig, key, key)
265
 
266
- # Inflation: CPI + % change on y2
267
  elif column == ('CPIAUCSL', 'CPIAUCSL_PCT') and 'CPIAUCSL' in combined_data.columns:
268
  fig.add_trace(go.Scatter(
269
  x=combined_data.index, y=combined_data['CPIAUCSL'],
@@ -274,24 +316,26 @@ if st.session_state.run_analysis:
274
  x=combined_data.index, y=combined_data['CPIAUCSL_PCT'],
275
  mode='lines', name='Inflation % Change', yaxis='y2'
276
  ))
277
- fig.update_layout(yaxis2=dict(title="Inflation % Change",
278
- overlaying='y', side='right'))
 
279
  finalize_layout(fig, key, key)
280
 
281
- # Treasury rates: plot each available
282
  elif column == ('GS10', 'DGS2', 'DGS1MO', 'TB3MS'):
283
  any_added = False
284
  for col in column:
285
  if col in combined_data.columns:
286
- any_added = True
287
  fig.add_trace(go.Scatter(
288
- x=combined_data.index, y=combined_data[col], mode='lines', name=col
 
289
  ))
 
290
  if any_added:
291
  finalize_layout(fig, key, key)
292
 
293
  else:
294
- # Generic multi-series if needed
295
  for col in column:
296
  if col in combined_data.columns:
297
  fig.add_trace(go.Scatter(
@@ -320,15 +364,16 @@ if st.session_state.run_analysis:
320
  ))
321
  finalize_layout(fig, key, key)
322
 
323
- # Only render if we actually added something beyond the shading
324
  if fig.data:
325
  st.plotly_chart(fig, use_container_width=True)
326
 
327
- # ---------- Hide default Streamlit branding ----------
328
- hide_streamlit_style = """
329
- <style>
330
- #MainMenu {visibility: hidden;}
331
- footer {visibility: hidden;}
332
- </style>
333
- """
334
- st.markdown(hide_streamlit_style, unsafe_allow_html=True)
 
 
 
5
  import datetime
6
  import plotly.graph_objs as go
7
 
8
+ # ---------- Page config (must be first) ----------
9
  st.set_page_config(layout="wide")
10
 
11
+ # ---------- Stable CSS for wider sidebar ----------
12
  st.markdown(
13
  """
14
  <style>
 
15
  [data-testid="stSidebar"] {
16
  width: 350px;
17
  min-width: 350px;
 
21
  unsafe_allow_html=True,
22
  )
23
 
24
+ # ---------- Session state ----------
25
  if "run_analysis" not in st.session_state:
26
  st.session_state.run_analysis = False
27
+ if "combined_data" not in st.session_state:
28
+ st.session_state.combined_data = None
29
+ if "selection_signature" not in st.session_state:
30
+ st.session_state.selection_signature = None
31
 
32
+ # ---------- App title / description ----------
33
  st.title("Key Economic Recession Indicators")
34
  st.markdown("""
35
  This tool allows you to visualize and analyze various recession indicators over time.
 
37
  - Use the checkboxes in the sidebar to choose the indicators you'd like to explore.
38
  """)
39
 
40
+ # ---------- Sidebar ----------
41
  with st.sidebar.expander("How to Use", expanded=False):
42
  st.write("""
43
  **How to use this app:**
 
53
  'Sahm Recession Indicator': 'SAHMREALTIME',
54
  'U.S. Recession Probabilities': 'RECPROUSM156N',
55
  'Yield Spread (10Y - 2Y)': 'Yield_Spread', # Calculated, not fetched
56
+ 'Stock Market (S&P 500)': 'SP500', # yfinance (^GSPC)
57
+ 'VIX': 'VIX', # yfinance (^VIX)
58
  'Treasury Rates': ('GS10', 'DGS2', 'DGS1MO', 'TB3MS'),
59
  'Federal Funds Rate': 'FEDFUNDS',
60
  'Unemployment Rate': 'UNRATE',
 
68
  }
69
  selected_indicators = {key: st.checkbox(key, value=True) for key in indicators.keys()}
70
 
71
+ # Single Run button
72
  if st.sidebar.button("Run Analysis"):
73
  st.session_state.run_analysis = True
74
 
 
76
  start_date = datetime.datetime(1920, 1, 1)
77
  end_date = datetime.datetime.today()
78
 
79
+ # ---------- Recession periods ----------
80
  crash_periods = {
81
  '1929-08-01': '1933-03-01',
82
  '1937-05-01': '1938-06-01',
 
95
  '2020-02-01': '2020-04-01'
96
  }
97
 
98
+ # ---------- Cached batch fetchers ----------
99
+ @st.cache_data(ttl=6*60*60, show_spinner=False)
100
+ def fetch_fred_batch(series_codes, start, end) -> pd.DataFrame:
101
+ """Batch fetch from FRED; falls back to per-series if batch fails."""
102
+ if not series_codes:
103
+ return pd.DataFrame()
104
  try:
105
+ df = web.DataReader(series_codes, "fred", start, end)
106
+ # DataReader may return a Series when series_codes has length 1
107
+ if isinstance(df, pd.Series):
108
+ df = df.to_frame(series_codes[0])
109
+ return df
 
 
110
  except Exception as e:
111
+ # Fallback: try individually, salvage what we can
112
+ frames = []
113
+ for code in series_codes:
114
+ try:
115
+ s = web.DataReader(code, "fred", start, end).squeeze("columns").rename(code)
116
+ frames.append(s)
117
+ except Exception as ie:
118
+ st.warning(f"Failed to fetch {code} from FRED: {ie}")
119
+ return pd.concat(frames, axis=1) if frames else pd.DataFrame()
120
+
121
+ @st.cache_data(ttl=6*60*60, show_spinner=False)
122
+ def fetch_yf_batch(tickers, start, end) -> pd.DataFrame:
123
+ """Batch fetch Adj Close from Yahoo Finance, rename to friendly labels."""
124
+ if not tickers:
125
+ return pd.DataFrame()
126
  try:
127
+ df = yf.download(
128
+ tickers=" ".join(tickers),
129
+ start=start, end=end,
130
+ auto_adjust=False, progress=False, threads=False,
131
+ )
132
+ # Normalize to a flat DataFrame of Adj Close columns
133
  if isinstance(df.columns, pd.MultiIndex):
134
+ # pick Adj Close for each ticker
135
+ if "Adj Close" in df.columns.get_level_values(0):
136
+ wide = df["Adj Close"].copy()
137
+ else:
138
+ # Fallback: try Close
139
+ wide = df["Close"].copy()
140
+ else:
141
+ # Single ticker -> just one column set
142
+ wide = df[["Adj Close"]] if "Adj Close" in df.columns else df[["Close"]]
143
+ wide.columns = [tickers[0]]
144
+ # Rename to app labels
145
+ rename_map = {"^GSPC": "SP500", "^VIX": "VIX"}
146
+ wide = wide.rename(columns=rename_map)
147
+ return wide
148
  except Exception as e:
149
+ st.warning(f"Failed to fetch from Yahoo Finance: {e}")
150
+ return pd.DataFrame()
151
+
152
+ # ---------- Build everything up-front ----------
153
+ def selection_signature(selected: dict) -> tuple:
154
+ """Immutable key for the current selection (used to decide if we refetch)."""
155
+ return tuple(sorted([k for k, v in selected.items() if v]))
156
 
157
+ @st.cache_data(ttl=6*60*60, show_spinner=False)
158
+ def build_dataset_all(selected_keys: tuple) -> pd.DataFrame:
159
+ """Build the full dataset for the given selection (batched)."""
160
+ # Recreate indicator mapping (cache functions need pure inputs)
161
+ indicators_local = {
162
+ 'Sahm Recession Indicator': 'SAHMREALTIME',
163
+ 'U.S. Recession Probabilities': 'RECPROUSM156N',
164
+ 'Yield Spread (10Y - 2Y)': 'Yield_Spread',
165
+ 'Stock Market (S&P 500)': 'SP500',
166
+ 'VIX': 'VIX',
167
+ 'Treasury Rates': ('GS10', 'DGS2', 'DGS1MO', 'TB3MS'),
168
+ 'Federal Funds Rate': 'FEDFUNDS',
169
+ 'Unemployment Rate': 'UNRATE',
170
+ 'Nonfarm Payrolls': 'PAYEMS',
171
+ 'Jobless Claims': 'ICSA',
172
+ 'Retail Sales': 'RSXFS',
173
+ 'Industrial Production': ('INDPRO', 'INDPRO_PCT'),
174
+ 'Housing Starts': 'HOUST',
175
+ 'Consumer Confidence': 'UMCSENT',
176
+ 'Inflation (CPI)': ('CPIAUCSL', 'CPIAUCSL_PCT')
177
+ }
178
 
179
+ fred_series = []
180
+ yf_tickers = []
181
+ for key in selected_keys:
182
+ col = indicators_local[key]
183
  if isinstance(col, tuple):
184
  for c in col:
185
+ if c in ("INDPRO_PCT", "CPIAUCSL_PCT"): # derived later
186
+ continue
187
+ fred_series.append(c)
 
 
188
  else:
189
+ if col == "SP500":
190
+ yf_tickers.append("^GSPC")
191
+ elif col == "VIX":
192
+ yf_tickers.append("^VIX")
193
+ elif col == "Yield_Spread":
194
+ pass # derived later
195
+ else:
196
+ fred_series.append(col)
197
+
198
+ fred = fetch_fred_batch(sorted(set(fred_series)), start_date, end_date)
199
+ yfdf = fetch_yf_batch(sorted(set(yf_tickers)), start_date, end_date)
200
+
201
+ frames = []
202
+ if isinstance(fred, pd.DataFrame) and not fred.empty:
203
+ frames.append(fred)
204
+ if isinstance(yfdf, pd.DataFrame) and not yfdf.empty:
205
+ frames.append(yfdf)
206
+
207
+ if not frames:
208
  return pd.DataFrame()
209
 
210
+ combined = pd.concat(frames, axis=1).sort_index()
211
 
212
+ # Derived series
213
+ if "INDPRO" in combined.columns and "Industrial Production" in selected_keys:
214
+ combined["INDPRO_PCT"] = combined["INDPRO"].pct_change() * 100
215
+ if "CPIAUCSL" in combined.columns and "Inflation (CPI)" in selected_keys:
216
+ combined["CPIAUCSL_PCT"] = combined["CPIAUCSL"].pct_change() * 100
217
 
218
+ # Interpolate for alignment
219
+ combined = combined.interpolate(method="time")
220
 
221
  # Yield spread
222
+ if "Yield Spread (10Y - 2Y)" in selected_keys and {"GS10", "DGS2"}.issubset(combined.columns):
223
+ combined["Yield_Spread"] = combined["GS10"] - combined["DGS2"]
224
 
225
  return combined
226
 
227
+ # ---------- Plot helpers ----------
228
  def add_recession_shading(fig: go.Figure):
229
  for peak, trough in crash_periods.items():
230
  fig.add_shape(
231
+ type="rect", xref="x", yref="paper",
232
+ x0=peak, y0=0, x1=trough, y1=1,
233
+ fillcolor="gray", opacity=0.3, layer="below", line_width=0,
 
 
 
 
 
 
 
 
234
  )
235
 
236
  def finalize_layout(fig: go.Figure, title: str, ytitle: str):
237
  fig.update_layout(
238
  title=title,
239
+ xaxis_title="Date",
240
  yaxis_title=ytitle,
241
  xaxis=dict(
242
+ tickformat="%Y", tickmode="linear", dtick="M36",
243
+ showspikes=True, spikemode="across", spikesnap="cursor", spikethickness=1
 
 
 
 
 
244
  ),
245
  hovermode="x unified",
246
+ hoverlabel=dict(bgcolor="white", font_size=12, font_family="Rockwell"),
 
 
 
 
247
  legend=dict(
248
+ x=0.02, y=0.95, traceorder="normal",
249
+ bgcolor="rgba(255,255,255,0.5)", bordercolor="rgba(0,0,0,0)"
 
 
 
250
  ),
251
+ margin=dict(l=60, r=20, t=40, b=40),
252
  )
253
  fig.update_xaxes(
254
+ showgrid=True, gridwidth=1, gridcolor="LightGray", tickangle=45,
 
 
 
255
  tickformatstops=[
256
  dict(dtickrange=[None, "M1"], value="%b %d, %Y"),
257
+ dict(dtickrange=["M1", None], value="%Y"),
258
+ ],
259
  )
260
+ fig.update_traces(hovertemplate="%{x|%b %d, %Y}<br>%{y}<extra></extra>")
261
 
262
  # ---------- Main render ----------
263
  if st.session_state.run_analysis:
264
+ # Compute a signature of the current selection to decide if we need to rebuild
265
+ sig = selection_signature(selected_indicators)
266
 
267
+ need_fetch = (
268
+ st.session_state.combined_data is None
269
+ or st.session_state.selection_signature != sig
270
+ )
271
+
272
+ if need_fetch:
273
+ with st.spinner("Fetching all selected data (batched) and building dataset..."):
274
+ combined_data = build_dataset_all(sig)
275
+ st.session_state.combined_data = combined_data
276
+ st.session_state.selection_signature = sig
277
+ else:
278
+ combined_data = st.session_state.combined_data
279
+
280
+ if combined_data is None or combined_data.empty:
281
  st.error("No data was successfully fetched for the selected indicators.")
282
  else:
283
+ # Plot everything AFTER data is fully ready
284
  for key, column in indicators.items():
285
  if not selected_indicators.get(key, False):
286
  continue
 
289
  add_recession_shading(fig)
290
 
291
  if isinstance(column, tuple):
292
+ # Industrial Production (level + % change on y2)
293
  if column == ('INDPRO', 'INDPRO_PCT') and 'INDPRO' in combined_data.columns:
294
  fig.add_trace(go.Scatter(
295
  x=combined_data.index, y=combined_data['INDPRO'],
 
300
  x=combined_data.index, y=combined_data['INDPRO_PCT'],
301
  mode='lines', name='Industrial Production % Change', yaxis='y2'
302
  ))
303
+ fig.update_layout(yaxis2=dict(
304
+ title="Industrial Production % Change", overlaying='y', side='right'
305
+ ))
306
  finalize_layout(fig, key, key)
307
 
308
+ # Inflation CPI (level + % change on y2)
309
  elif column == ('CPIAUCSL', 'CPIAUCSL_PCT') and 'CPIAUCSL' in combined_data.columns:
310
  fig.add_trace(go.Scatter(
311
  x=combined_data.index, y=combined_data['CPIAUCSL'],
 
316
  x=combined_data.index, y=combined_data['CPIAUCSL_PCT'],
317
  mode='lines', name='Inflation % Change', yaxis='y2'
318
  ))
319
+ fig.update_layout(yaxis2=dict(
320
+ title="Inflation % Change", overlaying='y', side='right'
321
+ ))
322
  finalize_layout(fig, key, key)
323
 
324
+ # Treasury rates
325
  elif column == ('GS10', 'DGS2', 'DGS1MO', 'TB3MS'):
326
  any_added = False
327
  for col in column:
328
  if col in combined_data.columns:
 
329
  fig.add_trace(go.Scatter(
330
+ x=combined_data.index, y=combined_data[col],
331
+ mode='lines', name=col
332
  ))
333
+ any_added = True
334
  if any_added:
335
  finalize_layout(fig, key, key)
336
 
337
  else:
338
+ # Generic multi-series, if ever needed
339
  for col in column:
340
  if col in combined_data.columns:
341
  fig.add_trace(go.Scatter(
 
364
  ))
365
  finalize_layout(fig, key, key)
366
 
 
367
  if fig.data:
368
  st.plotly_chart(fig, use_container_width=True)
369
 
370
+ # ---------- Hide Streamlit branding ----------
371
+ st.markdown(
372
+ """
373
+ <style>
374
+ #MainMenu {visibility: hidden;}
375
+ footer {visibility: hidden;}
376
+ </style>
377
+ """,
378
+ unsafe_allow_html=True,
379
+ )