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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +145 -174
app.py CHANGED
@@ -5,13 +5,14 @@ import yfinance as yf
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,15 +22,11 @@ st.markdown(
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,7 +34,7 @@ This tool allows you to visualize and analyze various recession indicators over
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,8 +50,8 @@ with st.sidebar.expander("Indicators", expanded=True):
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,7 +65,7 @@ with st.sidebar.expander("Indicators", expanded=True):
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
 
@@ -95,192 +92,166 @@ crash_periods = {
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,7 +260,7 @@ if st.session_state.run_analysis:
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'],
@@ -301,11 +272,12 @@ if st.session_state.run_analysis:
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'],
@@ -317,25 +289,25 @@ if st.session_state.run_analysis:
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(
@@ -351,7 +323,7 @@ if st.session_state.run_analysis:
351
  ))
352
  if column == 'SAHMREALTIME':
353
  fig.add_hline(
354
- y=0.5, line=dict(color="red", dash="dash"),
355
  annotation_text="Recession Threshold",
356
  annotation_position="bottom right"
357
  )
@@ -364,16 +336,15 @@ if st.session_state.run_analysis:
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
- )
 
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
  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 ----------
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
  - 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
  '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
  }
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
 
 
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
+ template='plotly_dark', # dark-friendly defaults
199
+ paper_bgcolor='rgba(0,0,0,0)', # transparent to match Streamlit theme
200
+ plot_bgcolor='rgba(0,0,0,0)', # transparent to match Streamlit theme
201
+ font=dict(color="white"),
202
  xaxis=dict(
203
+ tickformat="%Y",
204
+ tickmode="linear",
205
+ dtick="M36",
206
+ showspikes=True,
207
+ spikemode='across',
208
+ spikesnap='cursor',
209
+ spikethickness=1
210
  ),
211
  hovermode="x unified",
212
+ hoverlabel=dict(
213
+ bgcolor="rgba(14,17,23,0.95)", # blends with backgroundColor "#0e1117"
214
+ font_size=12,
215
+ font_family="Rockwell",
216
+ font_color="white"
217
+ ),
218
  legend=dict(
219
+ x=0.02,
220
+ y=0.95,
221
+ traceorder='normal',
222
+ bgcolor='rgba(0,0,0,0)', # transparent legend
223
+ bordercolor='rgba(0,0,0,0)',
224
+ font=dict(color="white"),
225
+ title_font=dict(color="white")
226
  ),
227
+ margin=dict(l=60, r=20, t=40, b=40)
228
  )
229
  fig.update_xaxes(
230
+ showgrid=True,
231
+ gridwidth=1,
232
+ gridcolor='rgba(255,255,255,0.12)', # subtle grid for dark
233
+ tickangle=45,
234
  tickformatstops=[
235
  dict(dtickrange=[None, "M1"], value="%b %d, %Y"),
236
+ dict(dtickrange=["M1", None], value="%Y")
237
+ ]
238
  )
239
+ fig.update_yaxes(
240
+ showgrid=True,
241
+ gridwidth=1,
242
+ gridcolor='rgba(255,255,255,0.12)'
243
+ )
244
+ fig.update_traces(hovertemplate='%{x|%b %d, %Y}<br>%{y}<extra></extra>')
245
 
246
  # ---------- Main render ----------
247
  if st.session_state.run_analysis:
248
+ with st.spinner("Fetching data and building charts..."):
249
+ combined_data = build_dataset(selected_indicators)
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
+ if combined_data.empty:
252
  st.error("No data was successfully fetched for the selected indicators.")
253
  else:
254
+ # Loop through selections and plot
255
  for key, column in indicators.items():
256
  if not selected_indicators.get(key, False):
257
  continue
 
260
  add_recession_shading(fig)
261
 
262
  if isinstance(column, tuple):
263
+ # Industrial Production: level + % change on y2
264
  if column == ('INDPRO', 'INDPRO_PCT') and 'INDPRO' in combined_data.columns:
265
  fig.add_trace(go.Scatter(
266
  x=combined_data.index, y=combined_data['INDPRO'],
 
272
  mode='lines', name='Industrial Production % Change', yaxis='y2'
273
  ))
274
  fig.update_layout(yaxis2=dict(
275
+ title="Industrial Production % Change",
276
+ overlaying='y', side='right'
277
  ))
278
  finalize_layout(fig, key, key)
279
 
280
+ # Inflation: CPI + % change on y2
281
  elif column == ('CPIAUCSL', 'CPIAUCSL_PCT') and 'CPIAUCSL' in combined_data.columns:
282
  fig.add_trace(go.Scatter(
283
  x=combined_data.index, y=combined_data['CPIAUCSL'],
 
289
  mode='lines', name='Inflation % Change', yaxis='y2'
290
  ))
291
  fig.update_layout(yaxis2=dict(
292
+ title="Inflation % Change",
293
+ overlaying='y', side='right'
294
  ))
295
  finalize_layout(fig, key, key)
296
 
297
+ # Treasury rates: plot each available
298
  elif column == ('GS10', 'DGS2', 'DGS1MO', 'TB3MS'):
299
  any_added = False
300
  for col in column:
301
  if col in combined_data.columns:
302
+ any_added = True
303
  fig.add_trace(go.Scatter(
304
+ x=combined_data.index, y=combined_data[col], mode='lines', name=col
 
305
  ))
 
306
  if any_added:
307
  finalize_layout(fig, key, key)
308
 
309
  else:
310
+ # Generic multi-series if needed
311
  for col in column:
312
  if col in combined_data.columns:
313
  fig.add_trace(go.Scatter(
 
323
  ))
324
  if column == 'SAHMREALTIME':
325
  fig.add_hline(
326
+ y=0.5, line=dict(color="#ff6b6b", dash="dash"),
327
  annotation_text="Recession Threshold",
328
  annotation_position="bottom right"
329
  )
 
336
  ))
337
  finalize_layout(fig, key, key)
338
 
339
+ # Only render if we actually added something beyond the shading
340
  if fig.data:
341
  st.plotly_chart(fig, use_container_width=True)
342
 
343
+ # ---------- Hide default Streamlit branding ----------
344
+ hide_streamlit_style = """
345
+ <style>
346
+ #MainMenu {visibility: hidden;}
347
+ footer {visibility: hidden;}
348
+ </style>
349
+ """
350
+ st.markdown(hide_streamlit_style, unsafe_allow_html=True)