QuantumLearner commited on
Commit
0364e04
·
verified ·
1 Parent(s): 8c4bf24

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +518 -1
app.py CHANGED
@@ -1 +1,518 @@
1
- API_KEY = os.getenv("FMP_API_KEY")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py — Macro Cycle Composite (2 sections, production style)
2
+
3
+ import math
4
+ from datetime import datetime, date, timedelta
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import pandas_datareader.data as web
9
+ import yfinance as yf
10
+
11
+ import streamlit as st
12
+ import plotly.graph_objects as go
13
+ from plotly.subplots import make_subplots
14
+ import plotly.express as px
15
+
16
+ # --------------------------- Page config ---------------------------
17
+ st.set_page_config(
18
+ page_title="Market Cycle Composite",
19
+ layout="wide",
20
+ initial_sidebar_state="expanded"
21
+ )
22
+
23
+ st.title("Market Cycle Phases")
24
+
25
+ st.markdown(
26
+ "Classifies the market cycle each period: **Early**, **Mid-Late**, **Decline**, or **Uncertain**. "
27
+ "The solid line is the composite score; the dashed line is S&P 500 YoY. "
28
+ "Colored bands show the active phase over time. "
29
+ "Use the sidebar to set the sample start, smoothing, slope lookback, thresholds, and minimum run. "
30
+ "Click **Run Analysis** to update the charts."
31
+ )
32
+
33
+ # --------------------------- Constants -----------------------------
34
+ TODAY_PLUS_1 = (date.today() + timedelta(days=1))
35
+ DEFAULT_START = date(2000, 1, 1)
36
+
37
+ FRED_MAP = {
38
+ 'LEI' : 'USSLIND',
39
+ 'Philly Manuf Diff' : 'GACDFSA066MSFRBPHI',
40
+ 'Texas Serv Diff' : 'TSSOSBACTUAMFRBDAL',
41
+ 'Capacity Util' : 'CUMFNS',
42
+ 'BBK Leading' : 'BBKMLEIX',
43
+ 'CFNAI 3MMA' : 'CFNAIMA3',
44
+ 'Core CPI' : 'CPILFESL',
45
+ 'Core PCE' : 'PCEPILFE',
46
+ 'Hourly Wage' : 'CES0500000003',
47
+ 'PPI' : 'PPIACO',
48
+ 'Commodities' : 'PALLFNFINDEXM',
49
+ '10Y' : 'DGS10',
50
+ 'HY OAS' : 'BAMLH0A0HYM2',
51
+ 'StLouis FSI' : 'STLFSI4',
52
+ }
53
+
54
+ PHASE_COLORS = {
55
+ 'Early' : '#54d62c',
56
+ 'Mid-Late' : '#3fa1ff',
57
+ 'Decline' : '#ff4c4c',
58
+ 'Uncertain' : '#ffd400'
59
+ }
60
+
61
+ # --------------------------- Caching -------------------------------
62
+ @st.cache_data(show_spinner=False)
63
+ def load_macro_series(start: date, end: date) -> pd.DataFrame:
64
+ raw = {}
65
+ for k, tkr in FRED_MAP.items():
66
+ s = web.DataReader(tkr, 'fred', start, end).squeeze()
67
+ raw[k] = s
68
+ df = pd.DataFrame(raw)
69
+ return df
70
+
71
+ @st.cache_data(show_spinner=False)
72
+ def load_spx(start: date, end: date) -> pd.Series:
73
+ data = yf.download('^GSPC', start=start, end=end, auto_adjust=False, progress=False)
74
+ data.columns = data.columns.get_level_values(0)
75
+ spx = data['Adj Close'].resample('ME').last().ffill()
76
+ return spx
77
+
78
+ # --------------------------- Sidebar -------------------------------
79
+ with st.sidebar:
80
+ st.title("Parameters")
81
+
82
+ with st.expander("Data Range", expanded=False):
83
+ start_date = st.date_input(
84
+ "Start Date",
85
+ value=DEFAULT_START,
86
+ min_value=date(1950, 1, 1),
87
+ max_value=TODAY_PLUS_1,
88
+ help=(
89
+ "Start date for all series. "
90
+ "Earlier = more history, heavier load. "
91
+ "Later = less history."
92
+ )
93
+ )
94
+ st.caption(f"End Date: {TODAY_PLUS_1.isoformat()} (fixed to today + 1)")
95
+
96
+ freq = st.selectbox(
97
+ "Resample Frequency",
98
+ options=["ME", "QE"],
99
+ index=0,
100
+ help=(
101
+ "Aggregation for all series. "
102
+ "ME = month end. QE = quarter end. "
103
+ "Higher aggregation smooths high-frequency noise."
104
+ )
105
+ )
106
+
107
+ with st.expander("Composite & Phase Parameters", expanded=False):
108
+ smooth_window = st.number_input(
109
+ "Composite Smoothing Window (months)",
110
+ value=2, min_value=1, max_value=12, step=1,
111
+ help=(
112
+ "Moving average on the composite. "
113
+ "Increase to smooth more (slower). "
114
+ "Decrease to react faster."
115
+ )
116
+ )
117
+ slope_window = st.number_input(
118
+ "Slope Lookback (months)",
119
+ value=3, min_value=1, max_value=12, step=1,
120
+ help=(
121
+ "Difference span used for slope. "
122
+ "Higher = coarser trend. "
123
+ "Lower = more sensitive."
124
+ )
125
+ )
126
+ comp_thr = st.number_input(
127
+ "Composite Threshold",
128
+ value=0.15, step=0.05, format="%.2f",
129
+ help=(
130
+ "Band around zero for phase changes. "
131
+ "Increase = fewer flips. "
132
+ "Decrease = earlier shifts."
133
+ )
134
+ )
135
+ slope_thr = st.number_input(
136
+ "Slope Threshold",
137
+ value=0.005, step=0.001, format="%.3f",
138
+ help=(
139
+ "Minimum slope for Early/Decline. "
140
+ "Increase to demand stronger momentum. "
141
+ "Decrease to catch weaker turns."
142
+ )
143
+ )
144
+ min_run = st.number_input(
145
+ "Minimum Phase Run (months)",
146
+ value=6, min_value=1, max_value=24, step=1,
147
+ help=(
148
+ "Median/mode filter on phases. "
149
+ "Higher enforces longer runs. "
150
+ "Lower allows quicker switches."
151
+ )
152
+ )
153
+
154
+ run = st.button("Run Analysis", help="Compute the composite and render both sections.")
155
+
156
+ # --------------------------- Run pipeline --------------------------
157
+ if run:
158
+ prog = st.progress(0, text="Loading data...")
159
+ try:
160
+ # Load & resample
161
+ with st.spinner("Loading macro series..."):
162
+ df_raw_daily = load_macro_series(start_date, TODAY_PLUS_1)
163
+ prog.progress(20, text="Resampling...")
164
+
165
+ df = df_raw_daily.resample(freq).last().ffill()
166
+
167
+ # ====== TRANSFORMS & COMPOSITE ======
168
+ yoy = lambda s: s.pct_change(12) * 100
169
+ delta12 = lambda s: s.diff(12)
170
+ invert = lambda s: -s
171
+
172
+ tr = df.copy()
173
+ tr['LEI'] = yoy(tr['LEI'])
174
+ tr['Capacity Util'] = yoy(tr['Capacity Util'])
175
+ tr['BBK Leading'] = yoy(tr['BBK Leading'])
176
+ for c in ['Core CPI','Core PCE','Hourly Wage','PPI','Commodities']:
177
+ tr[c] = yoy(tr[c])
178
+ tr['10Y'] = invert(delta12(tr['10Y']))
179
+ tr['HY OAS'] = invert(delta12(tr['HY OAS']))
180
+ tr['StLouis FSI'] = invert(tr['StLouis FSI'])
181
+
182
+ infl_cols = ['Core CPI','Core PCE','Hourly Wage','PPI','Commodities']
183
+ tr['Inflation'] = tr[infl_cols].mean(axis=1)
184
+
185
+ inputs = [
186
+ 'LEI','Philly Manuf Diff','Texas Serv Diff',
187
+ 'Capacity Util','BBK Leading','CFNAI 3MMA',
188
+ 'Inflation','10Y','HY OAS','StLouis FSI'
189
+ ]
190
+
191
+ z = tr[inputs].apply(lambda s: (s - s.mean())/s.std())
192
+ comp = z.mean(axis=1)
193
+ comp_sm = comp.rolling(int(smooth_window), min_periods=1).mean()
194
+ slope = comp.diff(int(slope_window))
195
+
196
+ # Phase classification
197
+ cond_early = (comp < -comp_thr) & (slope > slope_thr)
198
+ cond_midlate = comp > comp_thr
199
+ cond_decline = (comp < -comp_thr) & (slope < -slope_thr)
200
+ cond_unc = comp.abs() <= comp_thr
201
+
202
+ phase = pd.Series('Mid-Late', index=comp.index)
203
+ phase[cond_early] = 'Early'
204
+ phase[cond_decline] = 'Decline'
205
+ phase[cond_unc] = 'Uncertain'
206
+
207
+ code_map = {'Early':0, 'Mid-Late':1, 'Decline':2, 'Uncertain':3}
208
+ inv_map = {v:k for k,v in code_map.items()}
209
+ codes = phase.map(code_map)
210
+
211
+ smoothed_codes = codes.rolling(
212
+ window=int(min_run), center=True, min_periods=1
213
+ ).apply(lambda x: pd.Series(x).value_counts().idxmax(), raw=False)
214
+
215
+ phase = smoothed_codes.round().astype(int).map(inv_map)
216
+
217
+ prog.progress(55, text="Loading equity series...")
218
+ spx = load_spx(start_date, TODAY_PLUS_1)
219
+ spx_yoy = spx.pct_change(12) * 100
220
+ if freq != 'ME':
221
+ spx_yoy = spx_yoy.resample(freq).last().ffill()
222
+
223
+ # ================== SECTION 1 — COMPOSITE ==================
224
+ st.header("Market Cycle Composite Indicator")
225
+
226
+ with st.expander("Methodology", expanded=False):
227
+ st.markdown("#### What you’re looking at")
228
+ st.write(
229
+ "One score that summarizes many macro and market series. "
230
+ "We also show the S&P 500 YoY for context. "
231
+ "Colored bands mark the phase we infer at each date."
232
+ )
233
+
234
+ st.markdown("#### How we build the score")
235
+ st.write("1) Put all series on the same timeline.")
236
+ st.write("We keep the last value each period and fill gaps with the last known value.")
237
+ st.latex(r"X^{(F)}_{t} = X_{\tau(t)},\;\; \tau(t)=\text{last timestamp in period }t")
238
+ st.latex(r"\tilde{X}_t = \begin{cases} X_t, & \text{if observed}\\ \tilde{X}_{t-1}, & \text{if missing}\end{cases}")
239
+
240
+ st.write("2) Convert each series to a comparable change.")
241
+ st.write("Most series use year-over-year percent change:")
242
+ st.latex(r"\mathrm{YoY}(X_t)=100\left(\frac{X_t}{X_{t-12}}-1\right)")
243
+ st.write("Rates and spreads use 12-month differences (and we flip the sign when higher is worse):")
244
+ st.latex(r"\Delta_{12}(X_t)=X_t-X_{t-12},\qquad \tilde{X}_t=-\Delta_{12}(X_t)")
245
+ st.write("Stress metrics are inverted so higher stress lowers the score:")
246
+ st.latex(r"\tilde{X}_t=-X_t")
247
+
248
+ st.write("3) Build an inflation block from five series and average them:")
249
+ st.latex(r"\mathrm{Inflation}_t=\frac{1}{5}\sum_{i=1}^{5}X_{i,t}")
250
+
251
+ st.write("4) Standardize each input so all have the same scale.")
252
+ st.latex(r"Z_{i,t}=\frac{X_{i,t}-\mu_i}{\sigma_i}")
253
+
254
+ st.write("5) Average the standardized inputs to get the composite.")
255
+ st.latex(r"C_t=\frac{1}{N}\sum_{i=1}^{N}Z_{i,t}")
256
+
257
+ st.write("6) Smooth the composite to cut noise.")
258
+ st.latex(r"\bar{C}_t=\frac{1}{W}\sum_{k=0}^{W-1}C_{t-k}")
259
+
260
+ st.write("7) Measure recent direction with a simple slope.")
261
+ st.latex(r"S_t=C_t-C_{t-W_s}")
262
+
263
+ st.write("8) Assign a phase using two thresholds.")
264
+ st.latex(r"\text{Early: } C_t<-\theta_c \ \wedge\ S_t>+\theta_s")
265
+ st.latex(r"\text{Decline: } C_t<-\theta_c \ \wedge\ S_t<-\theta_s")
266
+ st.latex(r"\text{Mid\text{-}Late: } C_t>+\theta_c")
267
+ st.latex(r"\text{Uncertain: } |C_t|\le\theta_c")
268
+
269
+ st.write("9) Stabilize phases with a centered majority vote over a short window.")
270
+ st.latex(r"\hat{P}_t=\operatorname{mode}\{P_{t-k},\ldots,P_{t+k}\},\;\; m=2k+1")
271
+
272
+ st.markdown("#### How the sidebar settings change results")
273
+ st.write("- **Resample Frequency**: Month-end or quarter-end. Higher aggregation is smoother.")
274
+ st.write("- **Composite Smoothing Window**: Larger = smoother, slower to react.")
275
+ st.write("- **Slope Lookback**: Larger = slower slope, fewer flips.")
276
+ st.write("- **Composite Threshold**: Larger = fewer phase changes.")
277
+ st.write("- **Slope Threshold**: Larger = only strong turns count as Early/Decline.")
278
+ st.write("- **Minimum Phase Run**: Larger = longer required runs, fewer whipsaws.")
279
+
280
+ # Composite plot
281
+ try:
282
+ fig_comp = make_subplots(specs=[[{"secondary_y": True}]])
283
+ fig_comp.update_layout(
284
+ template='plotly_dark',
285
+ height=520,
286
+ margin=dict(l=60, r=20, t=60, b=40),
287
+ title_text='Market-Cycle Composite Indicator',
288
+ xaxis_rangeslider_visible=False,
289
+ paper_bgcolor='rgba(0,0,0,1)',
290
+ plot_bgcolor='rgba(0,0,0,1)',
291
+ font=dict(color='white'),
292
+ legend=dict(
293
+ bgcolor="rgba(0,0,0,0)",
294
+ font=dict(color="white"),
295
+ title_font=dict(color="white")
296
+ )
297
+ )
298
+
299
+ # Phase shading
300
+ mask = phase.copy()
301
+ grp = (mask != mask.shift()).cumsum()
302
+ for ph, color in PHASE_COLORS.items():
303
+ for _, span in mask[mask == ph].groupby(grp):
304
+ x0 = pd.Timestamp(span.index[0]).to_pydatetime()
305
+ x1 = pd.Timestamp(span.index[-1]).to_pydatetime()
306
+ fig_comp.add_shape(
307
+ type="rect",
308
+ x0=x0, x1=x1, y0=0, y1=1,
309
+ xref="x1", yref="paper",
310
+ fillcolor=color, opacity=0.22,
311
+ layer="below", line_width=0
312
+ )
313
+
314
+ # Composite and zero
315
+ fig_comp.add_trace(
316
+ go.Scatter(
317
+ x=comp_sm.index, y=comp_sm, mode='lines',
318
+ line=dict(width=2), name=f'Cycle Composite ({int(smooth_window)}m MA)'
319
+ ),
320
+ secondary_y=False
321
+ )
322
+ fig_comp.add_hline(y=0, line_color='rgba(255,255,255,0.6)', line_width=1)
323
+
324
+ # SPX YoY on secondary axis
325
+ fig_comp.add_trace(
326
+ go.Scatter(
327
+ x=spx_yoy.index, y=spx_yoy,
328
+ mode='lines', line=dict(width=2, dash='dash', color='#00d084'),
329
+ name='S&P 500 YoY %'
330
+ ),
331
+ secondary_y=True
332
+ )
333
+
334
+ # Legend keys for phases
335
+ for ph, color in PHASE_COLORS.items():
336
+ fig_comp.add_trace(
337
+ go.Scatter(
338
+ x=[None], y=[None], mode='lines',
339
+ line=dict(color=color, width=10),
340
+ name=ph, showlegend=True
341
+ ),
342
+ secondary_y=False
343
+ )
344
+
345
+ # Axes styling
346
+ fig_comp.update_xaxes(
347
+ tickformat='%Y',
348
+ dtick="M12",
349
+ title_font=dict(color="white"),
350
+ tickfont=dict(color="white"),
351
+ tickcolor="white",
352
+ gridcolor="rgba(255,255,255,0.10)",
353
+ zerolinecolor="rgba(255,255,255,0.15)",
354
+ linecolor="rgba(255,255,255,0.15)",
355
+ ticks="outside"
356
+ )
357
+ fig_comp.update_yaxes(
358
+ title_text='Composite Z-Score',
359
+ secondary_y=False,
360
+ title_font=dict(color="white"),
361
+ tickfont=dict(color="white"),
362
+ tickcolor="white",
363
+ gridcolor="rgba(255,255,255,0.10)",
364
+ zerolinecolor="rgba(255,255,255,0.15)",
365
+ linecolor="rgba(255,255,255,0.15)",
366
+ ticks="outside"
367
+ )
368
+ fig_comp.update_yaxes(
369
+ title_text='S&P 500 YoY %',
370
+ secondary_y=True,
371
+ range=[-80, 80],
372
+ title_font=dict(color="white"),
373
+ tickfont=dict(color="white"),
374
+ tickcolor="white"
375
+ )
376
+
377
+ st.plotly_chart(fig_comp, use_container_width=True)
378
+ except Exception:
379
+ st.error("Failed to render the composite chart.")
380
+
381
+ prog.progress(85, text="Computing interpretation...")
382
+
383
+ # Dynamic Interpretation (same logic as raw)
384
+ try:
385
+ phase_series = pd.Series(phase, index=comp.index)
386
+ current_date = phase_series.index[-1]
387
+ current_phase = phase_series.iloc[-1]
388
+ current_comp = comp_sm.loc[current_date]
389
+ current_slope = slope.loc[current_date]
390
+ current_spx_yoy = spx_yoy.loc[current_date]
391
+
392
+ interp = ""
393
+ if current_phase == 'Early':
394
+ interp = (
395
+ f"As of {current_date.date()}, the composite is {current_comp:.2f} "
396
+ f"(+{current_slope:.3f} over {int(slope_window)}m) and S&P YoY is {current_spx_yoy:.1f}%. "
397
+ "Early-phase conditions indicate nascent expansion. "
398
+ "Leading activity indicators have bottomed and credit spreads are narrowing. "
399
+ "Selective exposure to cyclical sectors may capture emerging growth while risks remain contained."
400
+ )
401
+ elif current_phase == 'Mid-Late':
402
+ interp = (
403
+ f"As of {current_date.date()}, the composite stands at {current_comp:.2f} "
404
+ f"(slope {current_slope:.3f}) with S&P YoY {current_spx_yoy:.1f}%. "
405
+ "Mid-to-late cycle signals peak growth. "
406
+ "Inflationary pressures and monetary tightening typically intensify in this phase. "
407
+ "Shift allocation toward high-quality equities and defensive sectors to protect gains."
408
+ )
409
+ elif current_phase == 'Decline':
410
+ interp = (
411
+ f"As of {current_date.date()}, the composite reads {current_comp:.2f} "
412
+ f"(slope {current_slope:.3f}) and S&P YoY is {current_spx_yoy:.1f}%. "
413
+ "Decline-phase patterns signal contraction. "
414
+ "Risk sentiment deteriorates and liquidity tightens. "
415
+ "Consider de-risking portfolios: increase fixed income, cash buffers, and low-volatility assets."
416
+ )
417
+ elif current_phase == 'Uncertain':
418
+ interp = (
419
+ f"As of {current_date.date()}, the composite is neutral at {current_comp:.2f} "
420
+ f"(slope {current_slope:.3f}) with S&P YoY {current_spx_yoy:.1f}%. "
421
+ "Signals conflict and volatility often rises. "
422
+ "Maintain balanced allocations, await clearer directional cues, and manage risk with disciplined stops."
423
+ )
424
+ else:
425
+ interp = "Phase classification unavailable."
426
+
427
+ with st.expander("Dynamic Interpretation", expanded=False):
428
+ st.write(interp)
429
+ except Exception:
430
+ st.error("Failed to produce the interpretation.")
431
+
432
+ # ================== SECTION 2 — RAW SERIES ==================
433
+ prog.progress(95, text="Rendering input grid...")
434
+
435
+ st.header("Macro Input Series (Resampled)")
436
+
437
+ with st.expander("Methodology", expanded=False):
438
+ st.write("Resample each series to the selected frequency (period end).")
439
+ st.latex(r"X^{(F)}_{t} = X_{\tau(t)} \quad \text{with } \tau(t) = \text{last timestamp in period } t")
440
+ st.write("Forward-fill missing observations to avoid gaps in aligned panels.")
441
+ st.latex(r"\tilde{X}_t = \begin{cases} X_t, & \text{if observed} \\ \tilde{X}_{t-1}, & \text{otherwise} \end{cases}")
442
+ st.write("No transforms are applied in this section. It is a clean view of inputs after resampling.")
443
+
444
+ try:
445
+ df_view = df.copy()
446
+ n_series = len(df_view.columns)
447
+ ncols = 3
448
+ nrows = math.ceil(n_series / ncols)
449
+
450
+ fig_grid = make_subplots(
451
+ rows=nrows, cols=ncols,
452
+ shared_xaxes=False,
453
+ subplot_titles=list(df_view.columns)
454
+ )
455
+
456
+ xmin, xmax = df_view.index.min(), df_view.index.max()
457
+ for i, col in enumerate(df_view.columns, start=1):
458
+ r = (i-1)//ncols + 1
459
+ c = (i-1)%ncols + 1
460
+ fig_grid.add_trace(
461
+ go.Scatter(
462
+ x=df_view.index, y=df_view[col],
463
+ mode='lines',
464
+ line=dict(width=1.5),
465
+ name=col, showlegend=False
466
+ ),
467
+ row=r, col=c
468
+ )
469
+
470
+ for i in range(1, nrows*ncols + 1):
471
+ xaxis_key = f'xaxis{i}' if i > 1 else 'xaxis'
472
+ if xaxis_key in fig_grid.layout:
473
+ fig_grid.layout[xaxis_key].update(
474
+ range=[xmin, xmax],
475
+ tickformat='%Y',
476
+ tickfont=dict(color="white"),
477
+ tickcolor="white"
478
+ )
479
+
480
+ fig_grid.update_layout(
481
+ template='plotly_dark',
482
+ height=max(360, nrows*260),
483
+ title_text='All Input Series',
484
+ margin=dict(l=40, r=10, t=50, b=40),
485
+ paper_bgcolor='rgba(0,0,0,1)',
486
+ plot_bgcolor='rgba(0,0,0,1)',
487
+ font=dict(color='white')
488
+ )
489
+
490
+ for i in range(1, nrows*ncols + 1):
491
+ yaxis_key = f'yaxis{i}' if i > 1 else 'yaxis'
492
+ if yaxis_key in fig_grid.layout:
493
+ fig_grid.layout[yaxis_key].update(
494
+ tickfont=dict(color="white"),
495
+ tickcolor="white"
496
+ )
497
+
498
+ st.plotly_chart(fig_grid, use_container_width=True)
499
+ except Exception:
500
+ st.error("Failed to render the input grid.")
501
+
502
+ prog.progress(100, text="Done.")
503
+ except Exception:
504
+ st.error("Processing failed. Adjust parameters and try again.")
505
+ else:
506
+ st.info("Set parameters in the sidebar and click **Run Analysis**.")
507
+
508
+
509
+ # Hide default Streamlit style
510
+ st.markdown(
511
+ """
512
+ <style>
513
+ #MainMenu {visibility: hidden;}
514
+ footer {visibility: hidden;}
515
+ </style>
516
+ """,
517
+ unsafe_allow_html=True
518
+ )