QuantumLearner commited on
Commit
b679dba
·
verified ·
1 Parent(s): a9d0f27

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +921 -0
app.py ADDED
@@ -0,0 +1,921 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import yfinance as yf # Internal import only; never mentioned to the user
3
+ import numpy as np
4
+ import pandas as pd
5
+ import plotly.graph_objects as go
6
+ from plotly.subplots import make_subplots
7
+ import pytz
8
+ import warnings
9
+ from datetime import datetime, timedelta
10
+ from scipy.optimize import curve_fit
11
+
12
+ warnings.filterwarnings('ignore')
13
+
14
+ # -------------------------------------------------------------------------------------
15
+ # Streamlit Configuration
16
+ # -------------------------------------------------------------------------------------
17
+ st.set_page_config(page_title="High Frequency Volatility", layout="wide")
18
+
19
+ st.title("High Frequency Volatility")
20
+
21
+ # -------------------------------------------------------------------------------------
22
+ # Sidebar Inputs
23
+ # -------------------------------------------------------------------------------------
24
+ st.sidebar.header("Inputs")
25
+
26
+ with st.sidebar.expander("Ticker & Dates", expanded=True):
27
+ ticker = st.text_input("Ticker Symbol", "TSLA", help="Enter a valid stock symbol and/or cryptocurrency pair (e.g. 'MSFT', 'BTC-USD'.)")
28
+ default_start = datetime.today() - timedelta(days=365)
29
+ default_end = datetime.today()
30
+
31
+ start_date = st.date_input(
32
+ label="Start Date (Daily Data)",
33
+ value=default_start,
34
+ help="Daily data start date."
35
+ )
36
+ end_date = st.date_input(
37
+ label="End Date (Daily Data)",
38
+ value=default_end,
39
+ help="Daily data end date."
40
+ )
41
+
42
+ run_button = st.sidebar.button("Run Analysis", help="Click to retrieve data and run all calculations.")
43
+
44
+ # -------------------------------------------------------------------------------------
45
+ # Explanation
46
+ # -------------------------------------------------------------------------------------
47
+ st.markdown("""
48
+ This tool analyzes how volatility behaves at different time scales.
49
+ It uses recent intraday and historical daily price data to estimate and visualize volatility patterns. The results help distinguish between noise and meaningful market movement. It offers insight into short-term dynamics and long-term trends.""")
50
+
51
+ st.info("""Use the sidebar to select a stock and date range. Click **Run Analysis** to begin.
52
+ """)
53
+
54
+
55
+ # -------------------------------------------------------------------------------------
56
+ # Helper Functions
57
+ # -------------------------------------------------------------------------------------
58
+ def safe_download(symbol, period=None, interval=None, start=None, end=None):
59
+ """
60
+ Safely download data. Avoid referencing external providers in errors.
61
+ """
62
+ try:
63
+ return yf.download(symbol, period=period, interval=interval, start=start, end=end)
64
+ except Exception:
65
+ st.error("Data retrieval error. Check ticker or date range.")
66
+ return None
67
+
68
+ # -------------------------------------------------------------------------------------
69
+ # Main Application
70
+ # -------------------------------------------------------------------------------------
71
+ if run_button:
72
+ # Use Streamlit progress/spinner
73
+ progress_bar = st.progress(0)
74
+ with st.spinner("Fetching data..."):
75
+
76
+ # 1) Intraday data (8d, 1m) + daily data (user date range)
77
+ intraday_data = safe_download(symbol=ticker, period="8d", interval="1m")
78
+ daily_data = safe_download(symbol=ticker, start=start_date, end=end_date, interval="1d")
79
+
80
+ progress_bar.progress(20)
81
+
82
+ if intraday_data is None or intraday_data.empty or daily_data is None or daily_data.empty:
83
+ st.error("No valid data returned for selected settings.")
84
+ st.stop()
85
+
86
+ # ================== SECTION: Volatility Signature Plot ==================
87
+ st.subheader("Volatility Signature Plot")
88
+
89
+ st.markdown(
90
+ "This section analyzes how volatility changes with sampling frequency by plotting realized volatility across intraday and long-term intervals."
91
+ )
92
+
93
+ import warnings
94
+ from scipy.optimize import curve_fit
95
+
96
+ warnings.filterwarnings('ignore')
97
+
98
+ with st.expander("Methodology", expanded=False):
99
+
100
+ st.markdown(r"""
101
+ ##### 1. Volatility Signature and Scaling Models
102
+
103
+ Examine how volatility behaves across different time intervals by building **volatility signature plots**. These plots compare empirical volatility with two models:
104
+
105
+ ---
106
+
107
+ ###### **Power-Law Scaling Model**
108
+
109
+ This model assumes that volatility follows a simple power law:
110
+
111
+ $$
112
+ \sigma(T) = c \cdot T^\alpha
113
+ $$
114
+
115
+ - $T$: sampling interval (in minutes)
116
+ - $c$: scaling constant
117
+ - $\alpha$: scaling exponent
118
+
119
+ **Interpretation of $\alpha$:**
120
+ - $\alpha = 0.5$ → volatility behaves like Brownian motion
121
+ - $\alpha < 0.5$ → noise dominates (mean reversion or microstructure effects)
122
+ - $\alpha > 0.5$ → persistence or trending behavior
123
+
124
+ ---
125
+
126
+ ###### **Two-Component Model**
127
+
128
+ This model applies only to intraday data and separates **true signal** from **market microstructure noise**:
129
+
130
+ $$
131
+ \text{Var}(r_T) = \sigma_0^2 + \frac{\eta^2}{T}
132
+ $$
133
+
134
+ - $\sigma_0^2$: genuine price variance (diffusive component)
135
+ - $\eta^2$: noise variance (dominates at short horizons)
136
+ - $T$: interval length
137
+
138
+ As $T$ increases, the noise term decays, and the model converges to the real volatility floor $\sigma_0^2$.
139
+
140
+ ---
141
+
142
+ These two models describe different aspects of how volatility scales:
143
+ - **Power-law** tells us how volatility evolves as time horizons expand.
144
+ - **Two-component** tells us how much of short-term movement is real versus noise.
145
+
146
+ Understanding these behaviors helps with signal design, execution, and model reliability.
147
+ """)
148
+
149
+ # --- Download data for long horizon inside the code (original used 5y) ---
150
+ # We'll *overwrite* daily_data with '5y' daily if you want the original approach.
151
+ # But we keep the user daily_data for this section.
152
+ # If you must strictly follow the raw code's "period='5y'", uncomment below:
153
+ # daily_data = safe_download(ticker, period='5y', interval='1d')
154
+ # However, the user specifically wants the daily_data from the date range. We'll keep that.
155
+
156
+ # Prep
157
+ intraday_data['log_return'] = np.log(intraday_data['Close'] / intraday_data['Close'].shift(1))
158
+ daily_data['log_return'] = np.log(daily_data['Close'] / daily_data['Close'].shift(1))
159
+ intraday_data.dropna(inplace=True)
160
+ daily_data.dropna(inplace=True)
161
+
162
+ # --- Parameters ---
163
+ trading_minutes_per_year = 252 * 6.5 * 60
164
+ intraday_labels = ['1m', '5m', '15m', '30m', '1h', '2h', '4h']
165
+ intraday_intervals = [1, 5, 15, 30, 60, 120, 240]
166
+ long_labels = ['1d', '1w', '1mo', '1y']
167
+ long_minutes = {'1d': 390, '1w': 1950, '1mo': 8190, '1y': 98280}
168
+
169
+ # --- Intraday Volatility ---
170
+ intra_vols = []
171
+ for interval in intraday_intervals:
172
+ resampled = intraday_data['log_return'].resample(f'{interval}min').sum()
173
+ vol = np.sqrt(np.sum(resampled**2) * (trading_minutes_per_year / interval))
174
+ intra_vols.append(vol)
175
+
176
+ T_intra = np.array(intraday_intervals)
177
+ sigma_intra = np.array(intra_vols)
178
+ var_intra = sigma_intra**2
179
+
180
+ # --- Long-Horizon Volatility ---
181
+ long_vols = []
182
+ for label in long_labels:
183
+ if label == '1d':
184
+ resampled = daily_data['log_return']
185
+ elif label == '1w':
186
+ resampled = daily_data['log_return'].resample('1W').sum()
187
+ elif label == '1mo':
188
+ # Replace '1ME' -> 'M'
189
+ resampled = daily_data['log_return'].resample('M').sum()
190
+ elif label == '1y':
191
+ # Replace '1YE' -> 'Y'
192
+ resampled = daily_data['log_return'].resample('Y').sum()
193
+ resampled = resampled.dropna()
194
+ minutes = long_minutes[label]
195
+ vol = np.sqrt(np.sum(resampled**2) * (trading_minutes_per_year / minutes))
196
+ long_vols.append(vol)
197
+
198
+ T_long = np.array(list(long_minutes.values()))
199
+ sigma_long = np.array(long_vols)
200
+ var_long = sigma_long**2
201
+
202
+ # --- Model definitions ---
203
+ def two_component_model(T, sigma0_squared, eta_squared):
204
+ return np.maximum(sigma0_squared + (eta_squared / T), 0)
205
+
206
+ def power_law(T, c, alpha):
207
+ return c * T ** alpha
208
+
209
+ # --- Fit models: Intraday ---
210
+ params_intra_2c, _ = curve_fit(two_component_model, T_intra, var_intra, bounds=(0, np.inf))
211
+ sigma0_sq_hat_intra, eta_sq_hat_intra = params_intra_2c
212
+ vol_fit_intra_2c = np.sqrt(two_component_model(T_intra, sigma0_sq_hat_intra, eta_sq_hat_intra))
213
+
214
+ params_intra_plaw, _ = curve_fit(power_law, T_intra, sigma_intra)
215
+ c_intra, alpha_intra = params_intra_plaw
216
+ vol_fit_intra_plaw = power_law(T_intra, c_intra, alpha_intra)
217
+
218
+ # --- Fit model: Long-Horizon (Power-Law Only) ---
219
+ params_long_plaw, _ = curve_fit(power_law, T_long, sigma_long)
220
+ c_long, alpha_long = params_long_plaw
221
+ vol_fit_long_plaw = power_law(T_long, c_long, alpha_long)
222
+
223
+ # --- Plot with Plotly ---
224
+ fig_sig = make_subplots(rows=1, cols=2, subplot_titles=[
225
+ "Intraday Volatility Signature",
226
+ "Long-Horizon Volatility Signature"
227
+ ])
228
+
229
+ # Intraday plot
230
+ fig_sig.add_trace(go.Scatter(
231
+ x=T_intra, y=sigma_intra, mode='lines+markers',
232
+ name='Observed Intraday Volatility'
233
+ ), row=1, col=1)
234
+
235
+ fig_sig.add_trace(go.Scatter(
236
+ x=T_intra, y=vol_fit_intra_2c, mode='lines',
237
+ name=f'2-Component Fit (σ₀ ≈ {np.sqrt(sigma0_sq_hat_intra):.2f})',
238
+ line=dict(dash='dash')
239
+ ), row=1, col=1)
240
+
241
+ fig_sig.add_trace(go.Scatter(
242
+ x=T_intra, y=vol_fit_intra_plaw, mode='lines',
243
+ name=f'Power Law Fit (α ≈ {alpha_intra:.2f})',
244
+ line=dict(dash='dot')
245
+ ), row=1, col=1)
246
+
247
+ for i, label_ in enumerate(intraday_labels):
248
+ fig_sig.add_annotation(
249
+ x=T_intra[i], y=sigma_intra[i], text=label_,
250
+ showarrow=False, yshift=10, row=1, col=1
251
+ )
252
+
253
+ # Long-horizon plot
254
+ fig_sig.add_trace(go.Scatter(
255
+ x=T_long, y=sigma_long, mode='lines+markers',
256
+ name='Observed Long-Term Volatility'
257
+ ), row=1, col=2)
258
+
259
+ fig_sig.add_trace(go.Scatter(
260
+ x=T_long, y=vol_fit_long_plaw, mode='lines',
261
+ name=f'Power Law Fit (α ≈ {alpha_long:.2f})',
262
+ line=dict(dash='dot')
263
+ ), row=1, col=2)
264
+
265
+ for i, label_ in enumerate(long_labels):
266
+ fig_sig.add_annotation(
267
+ x=T_long[i], y=sigma_long[i], text=label_,
268
+ showarrow=False, yshift=10, row=1, col=2
269
+ )
270
+
271
+ fig_sig.update_layout(
272
+ #title_text=f'Volatility Signature Plots for {ticker}',
273
+ title=dict(text=f'Volatility Signature Plots for {ticker}', font=dict(color='white')),
274
+ template='plotly_dark',
275
+ paper_bgcolor='#0e1117',
276
+ plot_bgcolor='#0e1117',
277
+ legend=dict(font=dict(color='white')),
278
+ height=500,
279
+ width=1700
280
+ )
281
+
282
+ fig_sig.update_xaxes(title_text="Sampling Interval (minutes)", row=1, col=1)
283
+ fig_sig.update_yaxes(title_text="Annualized Volatility", row=1, col=1, gridcolor='rgba(255,255,255,0.1)')
284
+ fig_sig.update_xaxes(title_text="Sampling Interval (minutes)", row=1, col=2)
285
+ fig_sig.update_yaxes(title_text="Annualized Volatility", row=1, col=2, gridcolor='rgba(255,255,255,0.1)')
286
+
287
+ st.plotly_chart(fig_sig, use_container_width=True)
288
+
289
+ # Original console output in an expander
290
+ with st.expander("Volatility Signature Plot - Dynamic Interpretation", expanded=False):
291
+ st.text("INTRADAY FITS:")
292
+ sigma0 = np.sqrt(sigma0_sq_hat_intra)
293
+ st.text(f" 2-Component: σ₀ ≈ {sigma0:.4f}, η² ≈ {eta_sq_hat_intra:.4f}")
294
+
295
+ if sigma0 > 0.01:
296
+ st.text(" → σ₀ is non-trivial. There's a persistent diffusive component in volatility even at high frequency.")
297
+ st.text(" For traders: market has underlying price movement beyond noise — high-frequency strategies need to account for this.")
298
+ else:
299
+ st.text(" → σ₀ is near zero. Most of the intraday volatility is noise-driven or transient.")
300
+ st.text(" For traders: signals at very short horizons may be unreliable — consider filtering or using coarser intervals.")
301
+
302
+ if eta_sq_hat_intra > 1e-5:
303
+ st.text(" → η² is sizable. Market microstructure noise likely distorts short-interval returns.")
304
+ st.text(" For traders: expect bid-ask bounce and slippage to dominate at sub-minute levels.")
305
+ else:
306
+ st.text(" → η² is small. Minimal microstructure noise in the observed intraday returns.")
307
+ st.text(" For traders: fine-resolution signals are cleaner — more room for high-frequency execution.")
308
+
309
+ st.text(f" Power Law: c ≈ {c_intra:.4f}, α ≈ {alpha_intra:.4f}")
310
+ if alpha_intra < 0.5:
311
+ st.text(" → α < 0.5: Volatility grows slower than √T. Suggests mean-reversion or high-frequency frictions.")
312
+ st.text(" For traders: short-term fades and reversion trades may outperform momentum strategies.")
313
+ elif np.isclose(alpha_intra, 0.5, atol=0.05):
314
+ st.text(" → α ≈ 0.5: Volatility scales close to Brownian motion. Random walk behavior.")
315
+ st.text(" For traders: short-term predictability is limited — neutrality and delta hedging make sense.")
316
+ else:
317
+ st.text(" → α > 0.5: Volatility grows faster than √T. Suggests trending or persistent order flow.")
318
+ st.text(" For traders: breakout and momentum strategies likely perform better in this regime.")
319
+
320
+ st.text("")
321
+ st.text("LONG-HORIZON FITS:")
322
+ st.text(f" Power Law: c ≈ {c_long:.4f}, α ≈ {alpha_long:.4f}")
323
+ if alpha_long < 0.5:
324
+ st.text(" → α < 0.5: Long-run volatility grows sub-linearly. Possible mean-reversion across days/weeks.")
325
+ st.text(" For traders: swing reversion setups and volatility selling may be effective.")
326
+ elif np.isclose(alpha_long, 0.5, atol=0.05):
327
+ st.text(" → α ≈ 0.5: Consistent with Brownian motion. No memory in long-term returns.")
328
+ st.text(" For traders: directional strategies offer no statistical edge — focus on volatility structures instead.")
329
+ else:
330
+ st.text(" → α > 0.5: Long-run volatility grows super-linearly. Indicates trend persistence or structural drift.")
331
+ st.text(" For traders: long-term trend-following, carry, or breakout systems are likely to work.")
332
+
333
+ progress_bar.progress(30)
334
+
335
+ # ================== SECTION: Intraday Signal-to-Noise Ratio ==================
336
+ st.subheader("Intraday Signal-to-Noise Ratio")
337
+
338
+ st.markdown(
339
+ "This section estimates how much of the intraday volatility is actual price movement versus noise from market mechanics."
340
+ )
341
+
342
+ with st.expander("Methodology", expanded=False):
343
+ st.markdown(r"""
344
+ ##### Intraday Signal-to-Noise Ratio (SNR)
345
+
346
+ This plot shows how much of the observed volatility at each intraday interval reflects true market movement versus noise introduced by high-frequency effects.
347
+
348
+ Signal-to-noise ratio is defined as:
349
+
350
+ $$
351
+ \text{SNR}(T) = \frac{\sigma_0^2}{\sigma_T^2}
352
+ $$
353
+
354
+ - $\sigma_0^2$: latent variance, estimated from the two-component model
355
+ - $\sigma_T^2$: empirical variance at sampling interval $T$
356
+
357
+
358
+ ##### Interpretation
359
+
360
+ - $\text{SNR} < 1$ → Noise dominates
361
+ - $\text{SNR} \rightarrow 1$ as $T$ increases → Signal becomes clearer as noise decays
362
+
363
+
364
+ ##### Why This Applies Only to High-Frequency Data
365
+
366
+ At short intervals, volatility is inflated by:
367
+ - bid-ask bounce
368
+ - latency
369
+ - execution frictions
370
+
371
+ As intervals widen, these distortions average out. SNR becomes useful for identifying when high-frequency signals are likely unreliable.
372
+
373
+ For longer timeframes (daily or more), microstructure effects are negligible. SNR isn't meaningful in those settings.
374
+
375
+ This diagnostic helps identify the time scales where volatility reflects genuine price discovery versus transient noise.
376
+ """)
377
+
378
+ snr_intra = sigma0_sq_hat_intra / var_intra
379
+
380
+ fig_snr = go.Figure()
381
+ fig_snr.add_trace(go.Scatter(
382
+ x=T_intra,
383
+ y=snr_intra,
384
+ mode='lines+markers',
385
+ name='σ₀² / σ²',
386
+ line=dict(color='purple', width=3)
387
+ ))
388
+
389
+ for i, label_ in enumerate(intraday_labels):
390
+ fig_snr.add_annotation(
391
+ x=T_intra[i],
392
+ y=snr_intra[i],
393
+ text=label_,
394
+ showarrow=False,
395
+ yshift=10,
396
+ font=dict(size=14)
397
+ )
398
+
399
+ fig_snr.add_shape(
400
+ type='line',
401
+ x0=min(T_intra),
402
+ x1=max(T_intra),
403
+ y0=1,
404
+ y1=1,
405
+ line=dict(color='green', dash='dash', width=3)
406
+ )
407
+
408
+ fig_snr.update_layout(
409
+ #title='Intraday Signal-to-Noise Ratio',
410
+ title=dict(text='Intraday Signal-to-Noise Ratio', font=dict(color='white')),
411
+ xaxis_title='Sampling Interval (minutes)',
412
+ yaxis_title='σ₀² / σ² (Signal-to-Noise)',
413
+ template='plotly_dark',
414
+ paper_bgcolor='#0e1117',
415
+ plot_bgcolor='#0e1117',
416
+ legend=dict(font=dict(color='white')),
417
+ height=400,
418
+ width=1000
419
+ )
420
+ fig_snr.update_yaxes(gridcolor='rgba(255,255,255,0.1)')
421
+ st.plotly_chart(fig_snr, use_container_width=True)
422
+
423
+ with st.expander("Intraday Signal-to-Noise Ratio - Dynamic Interpretation", expanded=False):
424
+ st.text("INTERPRETATION:")
425
+ for i, interval_ in enumerate(T_intra):
426
+ snr_val = snr_intra[i]
427
+ label_ = intraday_labels[i]
428
+ st.text(f"{label_} (interval = {interval_} min): σ₀² / σ² ≈ {snr_val:.2f}")
429
+ if snr_val > 0.7:
430
+ st.text(" → Signal dominates. Diffusive price movement explains most of the variance.")
431
+ st.text(" For traders: market microstructure noise is low. Alpha signals are likely more reliable.\n")
432
+ elif 0.3 < snr_val <= 0.7:
433
+ st.text(" → Mixed regime. Both signal and noise contribute materially.")
434
+ st.text(" For traders: consider robust execution filters and avoid overfitting short-term models.\n")
435
+ else:
436
+ st.text(" → Noise dominates. Most variance is from short-horizon microstructure effects.")
437
+ st.text(" For traders: avoid signals at this interval. Noise overwhelms usable price information.\n")
438
+
439
+ progress_bar.progress(40)
440
+
441
+ # ================== SECTION: Intraday Average Volatility Signature Plot ==================
442
+ st.subheader("Intraday Average Volatility Signature Plot")
443
+
444
+ st.markdown(
445
+ "This section shows how realized volatility behaves throughout the trading day, averaged across recent sessions and multiple time resolutions."
446
+ )
447
+
448
+ with st.expander("Methodology", expanded=False):
449
+ st.markdown(r"""
450
+ ##### Intraday Volatility Patterns by Time of Day
451
+
452
+ This analysis estimates average volatility at each clock time during U.S. market hours using multiple intraday windows.
453
+
454
+ Rolling realized volatility is computed using intraday log returns sampled over these intervals:
455
+ - 1 min, 5 min, 15 min
456
+ - 30 min, 1 hour, 2 hours, 4 hours
457
+
458
+ Each volatility series is then averaged by time of day (Eastern Time). This reveals typical volatility behavior across the session.
459
+
460
+ ---
461
+
462
+ ##### Common Intraday Pattern
463
+
464
+ Volatility tends to follow a U-shape across the trading day:
465
+ - High volatility after market open (9:30–10:30 AM)
466
+ - Low volatility midday (11:30 AM–2:00 PM)
467
+ - Rising volatility near close (3:00–4:00 PM)
468
+
469
+ This pattern is observed across all sampling windows. Shorter intervals capture more microstructure effects and noise. Longer intervals smooth these distortions.
470
+
471
+ ---
472
+
473
+ ##### Technical Details
474
+
475
+ Annualized volatility is computed using:
476
+
477
+ $$
478
+ \sigma_{\text{annual}} = \sqrt{\sum r^2} \cdot \sqrt{\frac{252 \times 6.5 \times 60}{\text{window size in minutes}}}
479
+ $$
480
+
481
+ The y-axis is displayed on a log scale to improve readability across different magnitudes.
482
+
483
+ This view helps identify when volatility tends to cluster during the day and informs execution timing and risk budgeting.
484
+ """)
485
+
486
+ # Original code block uses new data load for '8d' intraday
487
+ data_intra_avg = safe_download(ticker, period='8d', interval='1m')
488
+ if data_intra_avg is None or data_intra_avg.empty:
489
+ st.error("No intraday data available for the Intraday Average Volatility section.")
490
+ st.stop()
491
+
492
+ data_intra_avg.index = pd.to_datetime(data_intra_avg.index).tz_convert('America/New_York')
493
+ data_intra_avg['log_return'] = np.log(data_intra_avg['Close'] / data_intra_avg['Close'].shift(1))
494
+ data_intra_avg.dropna(inplace=True)
495
+
496
+ windows_dict = {
497
+ '1 Min': 1,
498
+ '5 Min': 5,
499
+ '15 Min': 15,
500
+ '30 Min': 30,
501
+ '1 Hour': 60,
502
+ '2 Hours': 120,
503
+ '4 Hours': 240
504
+ }
505
+ trading_minutes_per_year = 252 * 6.5 * 60
506
+ data_intra_avg['time'] = data_intra_avg.index.strftime('%H:%M')
507
+
508
+ intraday_vol = pd.DataFrame()
509
+ for label_, w_ in windows_dict.items():
510
+ data_intra_avg[f'{label_}_vol'] = (
511
+ data_intra_avg['log_return']
512
+ .rolling(w_)
513
+ .apply(lambda x: np.sqrt(np.sum(x**2) * (trading_minutes_per_year / w_)), raw=True)
514
+ )
515
+ intraday_vol[label_] = data_intra_avg.groupby('time')[f'{label_}_vol'].mean()
516
+
517
+ intraday_vol.index = intraday_vol.index.astype(str)
518
+
519
+ # Reduce x-axis labels
520
+ num_labels = 30
521
+ time_labels = np.linspace(0, len(intraday_vol.index) - 1, num_labels, dtype=int)
522
+ selected_xticks = [intraday_vol.index[i] for i in time_labels]
523
+
524
+ fig_intra_avg = go.Figure()
525
+ for label_ in windows_dict.keys():
526
+ fig_intra_avg.add_trace(go.Scatter(
527
+ x=intraday_vol.index,
528
+ y=intraday_vol[label_],
529
+ mode='lines',
530
+ name=label_,
531
+ opacity=0.8
532
+ ))
533
+
534
+ fig_intra_avg.update_layout(
535
+ #title=f'Intraday Average Volatility Signature Plot for {ticker}',
536
+ title=dict(text=f'Intraday Average Volatility Signature Plot for {ticker}', font=dict(color='white')),
537
+ xaxis_title='Time of Day (ET)',
538
+ yaxis_title='Annualized Volatility',
539
+ template='plotly_dark',
540
+ paper_bgcolor='#0e1117',
541
+ plot_bgcolor='#0e1117',
542
+ height=500,
543
+ width=1500,
544
+ legend=dict(font=dict(color='white')),
545
+ xaxis=dict(
546
+ tickmode='array',
547
+ tickvals=selected_xticks,
548
+ ticktext=selected_xticks,
549
+ tickangle=45
550
+ ),
551
+ yaxis_type='log'
552
+ )
553
+ fig_intra_avg.update_yaxes(gridcolor='rgba(255,255,255,0.1)')
554
+ st.plotly_chart(fig_intra_avg, use_container_width=True)
555
+
556
+ with st.expander("Intraday Average Volatility Signature Plot - Dynamic Interpretation", expanded=False):
557
+ st.text("INTRADAY VOLATILITY INTERPRETATION:")
558
+
559
+ ref_label = '5 Min'
560
+ vol_series = intraday_vol[ref_label]
561
+
562
+ peak_start = vol_series.iloc[:int(len(vol_series) * 0.33)].idxmax()
563
+ peak_end = vol_series.iloc[int(len(vol_series) * 0.66):].idxmax()
564
+ trough = vol_series.idxmin()
565
+
566
+ st.text(f"→ Peak volatility near open: {peak_start}")
567
+ st.text(f"→ Trough volatility mid-session: {trough}")
568
+ st.text(f"→ Peak volatility near close: {peak_end}")
569
+
570
+ early_peak = vol_series[peak_start] > vol_series[trough]
571
+ late_peak = vol_series[peak_end] > vol_series[trough]
572
+
573
+ if early_peak and late_peak:
574
+ st.text(" → U-shape pattern detected. Volatility is elevated during market open and close.")
575
+ st.text(" For traders: liquidity risk is higher early and late in the session. Expect wider spreads, faster price moves.")
576
+ st.text(" Execution near mid-day tends to carry less volatility risk — better for passive orders or size execution.")
577
+ else:
578
+ st.text(" → No clear U-shape. Volatility profile is irregular.")
579
+ st.text(" For traders: intraday behavior may be event-driven or news-sensitive in this period.")
580
+
581
+ st.text("\nSample intraday volatility (5-min window):")
582
+ sample_points = vol_series.iloc[[0, len(vol_series)//2, -1]]
583
+ st.text(str(sample_points))
584
+
585
+ progress_bar.progress(60)
586
+
587
+ # ================== SECTION: Realized vs. Implied Volatility ==================
588
+ st.subheader("Realized vs. Implied Volatility")
589
+
590
+ st.markdown(
591
+ "This section compares realized volatility over multiple horizons with implied volatility, using the VIX index as a proxy."
592
+ )
593
+
594
+ with st.expander("Methodology", expanded=False):
595
+ st.markdown(r"""
596
+ ##### Long-Term Realized vs. Implied Volatility
597
+
598
+ This comparison includes:
599
+ - **Realized volatility** estimated from historical returns
600
+ - **Implied volatility** from the VIX, which reflects market expectations over the next 30 days
601
+
602
+ ##### Realized Volatility
603
+
604
+ Computed using rolling log returns:
605
+
606
+ $$
607
+ \sigma_{\text{realized}} = \sqrt{ \sum_{i=1}^n r_i^2 \cdot \frac{\text{Annualization Factor}}{n} }
608
+ $$
609
+
610
+ - $r_i$: daily log return
611
+ - $n$: window size (1, 5, or 21 days)
612
+ - Annualization factors:
613
+ - 252 for daily
614
+ - 52 for weekly
615
+ - 12 for monthly
616
+
617
+ ##### Implied Volatility (VIX)
618
+
619
+ - Derived from S&P 500 options
620
+ - Annualized
621
+ - Represents the market’s forward-looking 30-day volatility estimate
622
+
623
+ ##### Interpretation
624
+
625
+ - Daily realized volatility is reactive and noisy
626
+ - Weekly and monthly realized volatility track broader trends
627
+ - VIX tends to exceed realized volatility due to a **volatility risk premium**
628
+
629
+ When realized volatility exceeds VIX, it signals an unexpected volatility event. Examples include earnings shocks, macro announcements, or crashes.
630
+
631
+ ##### Why This Comparison Matters
632
+
633
+ - **Volatility spreads** (VIX minus realized) may signal option overpricing or underpricing
634
+ - **Traders** can time volatility-selling or hedging strategies
635
+ - **Risk teams** can detect periods of market overreaction or complacency
636
+ """)
637
+
638
+ # Original code: data from '5y'
639
+ rv_data = safe_download(ticker, period='5y', interval='1d')
640
+ if rv_data is None or rv_data.empty:
641
+ st.error("No data available for Realized vs. Implied Volatility section.")
642
+ st.stop()
643
+
644
+ if isinstance(rv_data.columns, pd.MultiIndex):
645
+ rv_data.columns = rv_data.columns.get_level_values(0)
646
+
647
+ rv_data['log_return'] = np.log(rv_data['Close'] / rv_data['Close'].shift(1))
648
+ rv_data.dropna(inplace=True)
649
+
650
+ windows_ = {'Daily': 1, 'Weekly': 5, 'Monthly': 21}
651
+ annual_factors = {'Daily': 252, 'Weekly': 52, 'Monthly': 12}
652
+
653
+ for label_, w_ in windows_.items():
654
+ rv_data[f'{label_}_vol'] = rv_data['log_return'].rolling(w_).apply(
655
+ lambda x: np.sqrt(np.sum(x**2) * (annual_factors[label_] / w_)), raw=True
656
+ )
657
+
658
+ # Download VIX
659
+ vix_data = safe_download('^VIX', period='10y', interval='1d')
660
+ if vix_data is None or vix_data.empty:
661
+ st.error("No data for implied volatility. The plot might be empty.")
662
+ # We'll still proceed, but plot might be partial.
663
+
664
+ else:
665
+ if isinstance(vix_data.columns, pd.MultiIndex):
666
+ vix_data.columns = vix_data.columns.get_level_values(0)
667
+ vix_data = vix_data['Close'].reindex(rv_data.index, method='ffill') / 100
668
+
669
+ fig_rv_iv = go.Figure()
670
+
671
+ fig_rv_iv.add_trace(go.Scatter(
672
+ x=rv_data.index,
673
+ y=rv_data['Daily_vol'],
674
+ name='Realized Daily Volatility',
675
+ line=dict(color='orange', width=1),
676
+ opacity=0.3
677
+ ))
678
+
679
+ fig_rv_iv.add_trace(go.Scatter(
680
+ x=rv_data.index,
681
+ y=rv_data['Weekly_vol'],
682
+ name='Realized Weekly Volatility',
683
+ line=dict(color='green', width=2)
684
+ ))
685
+
686
+ fig_rv_iv.add_trace(go.Scatter(
687
+ x=rv_data.index,
688
+ y=rv_data['Monthly_vol'],
689
+ name='Realized Monthly Volatility',
690
+ line=dict(color='blue', width=2)
691
+ ))
692
+
693
+ if vix_data is not None and not vix_data.empty:
694
+ fig_rv_iv.add_trace(go.Scatter(
695
+ x=rv_data.index,
696
+ y=vix_data,
697
+ name='VIX (Implied Volatility)',
698
+ line=dict(color='red', dash='dash', width=2)
699
+ ))
700
+
701
+ # Stock price on secondary axis
702
+ fig_rv_iv.add_trace(go.Scatter(
703
+ x=rv_data.index,
704
+ y=rv_data['Close'],
705
+ name='Stock Price',
706
+ line=dict(color='white'),
707
+ opacity=0.2,
708
+ yaxis='y2',
709
+ showlegend=True
710
+ ))
711
+
712
+ fig_rv_iv.update_layout(
713
+ #title=f'Realized vs. Implied Volatility for {ticker}',
714
+ title=dict(text=f'Realized vs. Implied Volatility for {ticker}', font=dict(color='white')),
715
+ template='plotly_dark',
716
+ paper_bgcolor='#0e1117',
717
+ plot_bgcolor='#0e1117',
718
+ height=600,
719
+ width=1500,
720
+ xaxis=dict(title='Date'),
721
+ yaxis=dict(title='Annualized Volatility'),
722
+ yaxis2=dict(
723
+ title='Stock Price',
724
+ overlaying='y',
725
+ side='right',
726
+ showgrid=False
727
+ ),
728
+ legend=dict(x=0.01, y=0.99), font=dict(color='white'),
729
+ margin=dict(l=60, r=60, t=60, b=60)
730
+ )
731
+
732
+ fig_rv_iv.update_yaxes(gridcolor='rgba(255,255,255,0.1)')
733
+
734
+ st.plotly_chart(fig_rv_iv, use_container_width=True)
735
+
736
+ with st.expander("Realized vs. Implied Volatility - Dynamic Interpretation", expanded=False):
737
+ st.text("\nDYNAMIC INTERPRETATION:")
738
+ st.text("------------------------")
739
+ if (vix_data is not None and not vix_data.empty and
740
+ not rv_data.empty and 'Monthly_vol' in rv_data.columns):
741
+ latest_ = rv_data.dropna().iloc[-1]
742
+ vix_latest = vix_data.dropna().iloc[-1] if not vix_data.dropna().empty else float('nan')
743
+ realized_monthly = latest_['Monthly_vol']
744
+
745
+ st.text(f"Latest VIX (Implied 1M Vol): {vix_latest:.2%}")
746
+ st.text(f"Latest Realized Monthly Vol: {realized_monthly:.2%}\n")
747
+
748
+ if vix_latest > realized_monthly * 1.2:
749
+ st.text("→ Implied volatility is significantly higher than realized 1-month volatility.")
750
+ st.text(" Traders are demanding a risk premium — possibly due to uncertainty or expected catalysts.")
751
+ st.text(" For traders: options may be overpriced. Selling vol could outperform (e.g., short straddles with risk limits).")
752
+ elif vix_latest < realized_monthly * 0.8:
753
+ st.text("→ Implied volatility is below realized 1-month volatility.")
754
+ st.text(" Market might be underestimating future risk or recent realized vol hasn't mean-reverted.")
755
+ st.text(" For traders: long vol trades (e.g., buying calls/puts or strangles) might offer favorable asymmetry.")
756
+ else:
757
+ st.text("→ Implied and realized monthly volatility are broadly aligned.")
758
+ st.text(" Market expectations are in line with past realized movement.")
759
+ st.text(" For traders: neutral vol stance. Consider structure, skew, or relative value strategies instead.")
760
+
761
+ monthly_vol_series = rv_data['Monthly_vol'].dropna()
762
+ if len(monthly_vol_series) > 21:
763
+ vol_rolling_avg = monthly_vol_series.rolling(21).mean().iloc[-1]
764
+ if realized_monthly > vol_rolling_avg * 1.3:
765
+ st.text("\n→ Realized monthly volatility is well above its 1-month moving average.")
766
+ st.text(" For traders: regime shift likely. Could be due to macro events, earnings, or broad market repricing.")
767
+ elif realized_monthly < vol_rolling_avg * 0.7:
768
+ st.text("\n→ Realized monthly volatility is suppressed relative to recent history.")
769
+ st.text(" For traders: volatility compression phase — watch for breakout setups or sudden repricing.")
770
+
771
+ if len(rv_data) > 1:
772
+ vol_change = realized_monthly - rv_data['Monthly_vol'].iloc[-2]
773
+ if vol_change > 0.01:
774
+ st.text("→ Vol is expanding vs. previous day. Indicates rising uncertainty or event response.")
775
+ elif vol_change < -0.01:
776
+ st.text("→ Vol is compressing vs. previous day. Market calming or digesting recent moves.")
777
+ else:
778
+ st.text("Not enough data to show the Realized vs. Implied analysis or it is empty.")
779
+
780
+ progress_bar.progress(80)
781
+
782
+ # ================== SECTION: Day of the Week Effect ==================
783
+ st.subheader("Day of the Week Effect")
784
+
785
+
786
+ st.markdown(
787
+ "This section shows how realized volatility varies across weekdays using intraday return data."
788
+ )
789
+
790
+ with st.expander("Methodology", expanded=False):
791
+ st.markdown(r"""
792
+ ##### Day-of-Week Patterns in Realized Volatility
793
+
794
+ This analysis uses 5-minute intraday returns over the past 60 trading days. Realized volatility is computed daily and then averaged by weekday.
795
+
796
+ ##### Daily Volatility Calculation
797
+
798
+ Using 5-minute log returns, daily realized volatility is:
799
+
800
+ $$
801
+ \sigma_{\text{daily}} = \sqrt{ \sum_{i=1}^{n} r_i^2 }
802
+ $$
803
+
804
+ - $r_i$: 5-minute log returns
805
+ - $n$: number of 5-minute intervals in the trading day
806
+
807
+ Each day's volatility is then grouped by weekday and averaged.
808
+
809
+ ##### Interpretation
810
+
811
+ - **Mondays** often show elevated volatility, possibly due to weekend news and risk rebalancing
812
+ - **Fridays** can show rising volatility as traders adjust positions before the weekend
813
+ - **Mid-week** (Tuesday–Thursday) tends to be quieter with fewer major market events
814
+
815
+ This pattern helps identify which days tend to carry more execution or risk management impact.
816
+ """)
817
+
818
+
819
+
820
+ df_5m = safe_download(ticker, period='60d', interval='5m')
821
+ if df_5m is None or df_5m.empty:
822
+ st.error("No intraday data available for Day-of-Week analysis.")
823
+ st.stop()
824
+
825
+ if isinstance(df_5m.columns, pd.MultiIndex):
826
+ df_5m.columns = df_5m.columns.get_level_values(0)
827
+
828
+ df_5m.index = pd.to_datetime(df_5m.index)
829
+ df_5m['log_return'] = np.log(df_5m['Close'] / df_5m['Close'].shift(1))
830
+ df_5m.dropna(inplace=True)
831
+
832
+ df_5m['date'] = df_5m.index.date
833
+ df_5m['weekday'] = df_5m.index.dayofweek
834
+ df_5m = df_5m[df_5m['weekday'] < 5]
835
+
836
+ daily_vol = df_5m.groupby('date')['log_return'].apply(lambda x: np.sqrt(np.sum(x**2)))
837
+ daily_vol = daily_vol.reset_index().rename(columns={'log_return': 'realized_vol'})
838
+ daily_vol['date'] = pd.to_datetime(daily_vol['date'])
839
+ daily_vol['weekday'] = daily_vol['date'].dt.dayofweek
840
+
841
+ weekday_vol = daily_vol.groupby('weekday')['realized_vol'].mean().reset_index()
842
+ weekday_map = {0: 'Monday', 1: 'Tuesday', 2: 'Wednesday', 3: 'Thursday', 4: 'Friday'}
843
+ weekday_vol['weekday_name'] = weekday_vol['weekday'].map(weekday_map)
844
+
845
+ fig_dotw = go.Figure()
846
+ fig_dotw.add_trace(go.Bar(
847
+ x=weekday_vol['weekday_name'],
848
+ y=weekday_vol['realized_vol'],
849
+ marker_color='green'
850
+ ))
851
+
852
+ fig_dotw.update_layout(
853
+ #title=f'Day of the Week Effect for Realized Volatility ({ticker})',
854
+ title=dict(text=f'Day of the Week Effect for Realized Volatility ({ticker})', font=dict(color='white')),
855
+ xaxis_title='Day of the Week',
856
+ yaxis_title='Average Realized Volatility',
857
+ template='plotly_dark',
858
+ paper_bgcolor='#0e1117',
859
+ plot_bgcolor='#0e1117',
860
+ legend=dict(font=dict(color='white')),
861
+ height=400,
862
+ width=1200
863
+ )
864
+ fig_dotw.update_yaxes(gridcolor='rgba(255,255,255,0.1)')
865
+
866
+ st.plotly_chart(fig_dotw, use_container_width=True)
867
+
868
+ with st.expander("Day of the Week Effect - Dynamic Interpretation", expanded=False):
869
+ st.text("\nDYNAMIC INTERPRETATION:")
870
+ st.text("------------------------")
871
+
872
+ sorted_vol = weekday_vol.sort_values(by='realized_vol', ascending=False)
873
+
874
+ # Extract min and max vol days
875
+ most_volatile_day = sorted_vol.iloc[0]
876
+ least_volatile_day = sorted_vol.iloc[-1]
877
+
878
+ st.text("Average realized vol by weekday (sorted):")
879
+ for i, row in sorted_vol.iterrows():
880
+ st.text(f" {row['weekday_name']}: {row['realized_vol']:.4f}")
881
+
882
+ st.text(f"\n→ Highest average volatility: {most_volatile_day['weekday_name']} ({most_volatile_day['realized_vol']:.4f})")
883
+ st.text(f"→ Lowest average volatility: {least_volatile_day['weekday_name']} ({least_volatile_day['realized_vol']:.4f})")
884
+
885
+ mon_vol = weekday_vol.loc[weekday_vol['weekday'] == 0, 'realized_vol'].values[0]
886
+ fri_vol = weekday_vol.loc[weekday_vol['weekday'] == 4, 'realized_vol'].values[0]
887
+ wed_vol = weekday_vol.loc[weekday_vol['weekday'] == 2, 'realized_vol'].values[0]
888
+
889
+ st.text("")
890
+ if mon_vol > fri_vol and mon_vol > wed_vol:
891
+ st.text("→ Monday volatility is elevated.")
892
+ st.text(" Interpretation: markets often react to weekend news or macro events on Mondays.")
893
+ elif fri_vol > mon_vol and fri_vol > wed_vol:
894
+ st.text("→ Friday volatility is elevated.")
895
+ st.text(" Interpretation: traders adjusting risk before the weekend may cause more aggressive positioning.")
896
+ elif wed_vol < mon_vol and wed_vol < fri_vol:
897
+ st.text("→ Wednesday is the quietest.")
898
+ st.text(" Interpretation: midweek lulls are common — lower volume, fewer catalysts.")
899
+
900
+ vol_range = sorted_vol['realized_vol'].max() - sorted_vol['realized_vol'].min()
901
+ if vol_range < 0.005:
902
+ st.text("→ Volatility is fairly uniform across weekdays.")
903
+ st.text(" Interpretation: No clear day-of-week effect — intraday factors likely dominate.")
904
+ else:
905
+ st.text("→ There's a statistically meaningful difference in vol across days.")
906
+ st.text(" Interpretation: consider adjusting strategy timing to favor higher-volatility days.")
907
+
908
+ progress_bar.progress(100)
909
+ st.success("Analysis complete.")
910
+
911
+ # Hide default Streamlit style
912
+ st.markdown(
913
+ """
914
+ <style>
915
+ #MainMenu {visibility: hidden;}
916
+ footer {visibility: hidden;}
917
+ </style>
918
+ """,
919
+ unsafe_allow_html=True
920
+ )
921
+