File size: 38,655 Bytes
b679dba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
import streamlit as st
import yfinance as yf  # Internal import only; never mentioned to the user
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pytz
import warnings
from datetime import datetime, timedelta
from scipy.optimize import curve_fit

warnings.filterwarnings('ignore')

# -------------------------------------------------------------------------------------
# Streamlit Configuration
# -------------------------------------------------------------------------------------
st.set_page_config(page_title="High Frequency Volatility", layout="wide")

st.title("High Frequency Volatility")

# -------------------------------------------------------------------------------------
# Sidebar Inputs
# -------------------------------------------------------------------------------------
st.sidebar.header("Inputs")

with st.sidebar.expander("Ticker & Dates", expanded=True):
    ticker = st.text_input("Ticker Symbol", "TSLA", help="Enter a valid stock symbol and/or cryptocurrency pair (e.g. 'MSFT', 'BTC-USD'.)")
    default_start = datetime.today() - timedelta(days=365)
    default_end = datetime.today()

    start_date = st.date_input(
        label="Start Date (Daily Data)",
        value=default_start,
        help="Daily data start date."
    )
    end_date = st.date_input(
        label="End Date (Daily Data)",
        value=default_end,
        help="Daily data end date."
    )

run_button = st.sidebar.button("Run Analysis", help="Click to retrieve data and run all calculations.")

# -------------------------------------------------------------------------------------
# Explanation
# -------------------------------------------------------------------------------------
st.markdown("""
This tool analyzes how volatility behaves at different time scales. 
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.""")

st.info("""Use the sidebar to select a stock and date range. Click **Run Analysis** to begin.
""")


# -------------------------------------------------------------------------------------
# Helper Functions
# -------------------------------------------------------------------------------------
def safe_download(symbol, period=None, interval=None, start=None, end=None):
    """
    Safely download data. Avoid referencing external providers in errors.
    """
    try:
        return yf.download(symbol, period=period, interval=interval, start=start, end=end)
    except Exception:
        st.error("Data retrieval error. Check ticker or date range.")
        return None

# -------------------------------------------------------------------------------------
# Main Application
# -------------------------------------------------------------------------------------
if run_button:
    # Use Streamlit progress/spinner
    progress_bar = st.progress(0)
    with st.spinner("Fetching data..."):

        # 1) Intraday data (8d, 1m) + daily data (user date range)
        intraday_data = safe_download(symbol=ticker, period="8d", interval="1m")
        daily_data = safe_download(symbol=ticker, start=start_date, end=end_date, interval="1d")

    progress_bar.progress(20)

    if intraday_data is None or intraday_data.empty or daily_data is None or daily_data.empty:
        st.error("No valid data returned for selected settings.")
        st.stop()

    # ================== SECTION: Volatility Signature Plot ==================
    st.subheader("Volatility Signature Plot")

    st.markdown(
    "This section analyzes how volatility changes with sampling frequency by plotting realized volatility across intraday and long-term intervals."
)

    import warnings
    from scipy.optimize import curve_fit

    warnings.filterwarnings('ignore')
    
    with st.expander("Methodology", expanded=False):
        
        st.markdown(r"""
        ##### 1. Volatility Signature and Scaling Models

        Examine how volatility behaves across different time intervals by building **volatility signature plots**. These plots compare empirical volatility with two models:

        ---

        ###### **Power-Law Scaling Model**

        This model assumes that volatility follows a simple power law:

        $$
        \sigma(T) = c \cdot T^\alpha
        $$

        - $T$: sampling interval (in minutes)
        - $c$: scaling constant
        - $\alpha$: scaling exponent

        **Interpretation of $\alpha$:**
        - $\alpha = 0.5$ β†’ volatility behaves like Brownian motion
        - $\alpha < 0.5$ β†’ noise dominates (mean reversion or microstructure effects)
        - $\alpha > 0.5$ β†’ persistence or trending behavior

        ---

        ###### **Two-Component Model**

        This model applies only to intraday data and separates **true signal** from **market microstructure noise**:

        $$
        \text{Var}(r_T) = \sigma_0^2 + \frac{\eta^2}{T}
        $$

        - $\sigma_0^2$: genuine price variance (diffusive component)
        - $\eta^2$: noise variance (dominates at short horizons)
        - $T$: interval length

        As $T$ increases, the noise term decays, and the model converges to the real volatility floor $\sigma_0^2$.

        ---

        These two models describe different aspects of how volatility scales:
        - **Power-law** tells us how volatility evolves as time horizons expand.
        - **Two-component** tells us how much of short-term movement is real versus noise.

        Understanding these behaviors helps with signal design, execution, and model reliability.
            """)

    # --- Download data for long horizon inside the code (original used 5y) ---
    # We'll *overwrite* daily_data with '5y' daily if you want the original approach.
    # But we keep the user daily_data for this section. 
    # If you must strictly follow the raw code's "period='5y'", uncomment below:
    # daily_data = safe_download(ticker, period='5y', interval='1d')
    # However, the user specifically wants the daily_data from the date range. We'll keep that.

    # Prep
    intraday_data['log_return'] = np.log(intraday_data['Close'] / intraday_data['Close'].shift(1))
    daily_data['log_return'] = np.log(daily_data['Close'] / daily_data['Close'].shift(1))
    intraday_data.dropna(inplace=True)
    daily_data.dropna(inplace=True)

    # --- Parameters ---
    trading_minutes_per_year = 252 * 6.5 * 60
    intraday_labels = ['1m', '5m', '15m', '30m', '1h', '2h', '4h']
    intraday_intervals = [1, 5, 15, 30, 60, 120, 240]
    long_labels = ['1d', '1w', '1mo', '1y']
    long_minutes = {'1d': 390, '1w': 1950, '1mo': 8190, '1y': 98280}

    # --- Intraday Volatility ---
    intra_vols = []
    for interval in intraday_intervals:
        resampled = intraday_data['log_return'].resample(f'{interval}min').sum()
        vol = np.sqrt(np.sum(resampled**2) * (trading_minutes_per_year / interval))
        intra_vols.append(vol)

    T_intra = np.array(intraday_intervals)
    sigma_intra = np.array(intra_vols)
    var_intra = sigma_intra**2

    # --- Long-Horizon Volatility ---
    long_vols = []
    for label in long_labels:
        if label == '1d':
            resampled = daily_data['log_return']
        elif label == '1w':
            resampled = daily_data['log_return'].resample('1W').sum()
        elif label == '1mo':
            # Replace '1ME' -> 'M'
            resampled = daily_data['log_return'].resample('M').sum()
        elif label == '1y':
            # Replace '1YE' -> 'Y'
            resampled = daily_data['log_return'].resample('Y').sum()
        resampled = resampled.dropna()
        minutes = long_minutes[label]
        vol = np.sqrt(np.sum(resampled**2) * (trading_minutes_per_year / minutes))
        long_vols.append(vol)

    T_long = np.array(list(long_minutes.values()))
    sigma_long = np.array(long_vols)
    var_long = sigma_long**2

    # --- Model definitions ---
    def two_component_model(T, sigma0_squared, eta_squared):
        return np.maximum(sigma0_squared + (eta_squared / T), 0)

    def power_law(T, c, alpha):
        return c * T ** alpha

    # --- Fit models: Intraday ---
    params_intra_2c, _ = curve_fit(two_component_model, T_intra, var_intra, bounds=(0, np.inf))
    sigma0_sq_hat_intra, eta_sq_hat_intra = params_intra_2c
    vol_fit_intra_2c = np.sqrt(two_component_model(T_intra, sigma0_sq_hat_intra, eta_sq_hat_intra))

    params_intra_plaw, _ = curve_fit(power_law, T_intra, sigma_intra)
    c_intra, alpha_intra = params_intra_plaw
    vol_fit_intra_plaw = power_law(T_intra, c_intra, alpha_intra)

    # --- Fit model: Long-Horizon (Power-Law Only) ---
    params_long_plaw, _ = curve_fit(power_law, T_long, sigma_long)
    c_long, alpha_long = params_long_plaw
    vol_fit_long_plaw = power_law(T_long, c_long, alpha_long)

    # --- Plot with Plotly ---
    fig_sig = make_subplots(rows=1, cols=2, subplot_titles=[
        "Intraday Volatility Signature", 
        "Long-Horizon Volatility Signature"
    ])

    # Intraday plot
    fig_sig.add_trace(go.Scatter(
        x=T_intra, y=sigma_intra, mode='lines+markers',
        name='Observed Intraday Volatility'
    ), row=1, col=1)

    fig_sig.add_trace(go.Scatter(
        x=T_intra, y=vol_fit_intra_2c, mode='lines',
        name=f'2-Component Fit (Οƒβ‚€ β‰ˆ {np.sqrt(sigma0_sq_hat_intra):.2f})',
        line=dict(dash='dash')
    ), row=1, col=1)

    fig_sig.add_trace(go.Scatter(
        x=T_intra, y=vol_fit_intra_plaw, mode='lines',
        name=f'Power Law Fit (Ξ± β‰ˆ {alpha_intra:.2f})',
        line=dict(dash='dot')
    ), row=1, col=1)

    for i, label_ in enumerate(intraday_labels):
        fig_sig.add_annotation(
            x=T_intra[i], y=sigma_intra[i], text=label_,
            showarrow=False, yshift=10, row=1, col=1
        )

    # Long-horizon plot
    fig_sig.add_trace(go.Scatter(
        x=T_long, y=sigma_long, mode='lines+markers',
        name='Observed Long-Term Volatility'
    ), row=1, col=2)

    fig_sig.add_trace(go.Scatter(
        x=T_long, y=vol_fit_long_plaw, mode='lines',
        name=f'Power Law Fit (Ξ± β‰ˆ {alpha_long:.2f})',
        line=dict(dash='dot')
    ), row=1, col=2)

    for i, label_ in enumerate(long_labels):
        fig_sig.add_annotation(
            x=T_long[i], y=sigma_long[i], text=label_,
            showarrow=False, yshift=10, row=1, col=2
        )

    fig_sig.update_layout(
        #title_text=f'Volatility Signature Plots for {ticker}',
        title=dict(text=f'Volatility Signature Plots for {ticker}', font=dict(color='white')),
        template='plotly_dark',
        paper_bgcolor='#0e1117',
        plot_bgcolor='#0e1117',
        legend=dict(font=dict(color='white')),
        height=500,
        width=1700
    )

    fig_sig.update_xaxes(title_text="Sampling Interval (minutes)", row=1, col=1)
    fig_sig.update_yaxes(title_text="Annualized Volatility", row=1, col=1, gridcolor='rgba(255,255,255,0.1)')
    fig_sig.update_xaxes(title_text="Sampling Interval (minutes)", row=1, col=2)
    fig_sig.update_yaxes(title_text="Annualized Volatility", row=1, col=2, gridcolor='rgba(255,255,255,0.1)')

    st.plotly_chart(fig_sig, use_container_width=True)

    # Original console output in an expander
    with st.expander("Volatility Signature Plot - Dynamic Interpretation", expanded=False):
        st.text("INTRADAY FITS:")
        sigma0 = np.sqrt(sigma0_sq_hat_intra)
        st.text(f"  2-Component: Οƒβ‚€ β‰ˆ {sigma0:.4f}, Ξ·Β² β‰ˆ {eta_sq_hat_intra:.4f}")

        if sigma0 > 0.01:
            st.text("    β†’ Οƒβ‚€ is non-trivial. There's a persistent diffusive component in volatility even at high frequency.")
            st.text("       For traders: market has underlying price movement beyond noise β€” high-frequency strategies need to account for this.")
        else:
            st.text("    β†’ Οƒβ‚€ is near zero. Most of the intraday volatility is noise-driven or transient.")
            st.text("       For traders: signals at very short horizons may be unreliable β€” consider filtering or using coarser intervals.")

        if eta_sq_hat_intra > 1e-5:
            st.text("    β†’ Ξ·Β² is sizable. Market microstructure noise likely distorts short-interval returns.")
            st.text("       For traders: expect bid-ask bounce and slippage to dominate at sub-minute levels.")
        else:
            st.text("    β†’ Ξ·Β² is small. Minimal microstructure noise in the observed intraday returns.")
            st.text("       For traders: fine-resolution signals are cleaner β€” more room for high-frequency execution.")

        st.text(f"  Power Law:   c β‰ˆ {c_intra:.4f}, Ξ± β‰ˆ {alpha_intra:.4f}")
        if alpha_intra < 0.5:
            st.text("    β†’ Ξ± < 0.5: Volatility grows slower than √T. Suggests mean-reversion or high-frequency frictions.")
            st.text("       For traders: short-term fades and reversion trades may outperform momentum strategies.")
        elif np.isclose(alpha_intra, 0.5, atol=0.05):
            st.text("    β†’ Ξ± β‰ˆ 0.5: Volatility scales close to Brownian motion. Random walk behavior.")
            st.text("       For traders: short-term predictability is limited β€” neutrality and delta hedging make sense.")
        else:
            st.text("    β†’ Ξ± > 0.5: Volatility grows faster than √T. Suggests trending or persistent order flow.")
            st.text("       For traders: breakout and momentum strategies likely perform better in this regime.")

        st.text("")
        st.text("LONG-HORIZON FITS:")
        st.text(f"  Power Law:   c β‰ˆ {c_long:.4f}, Ξ± β‰ˆ {alpha_long:.4f}")
        if alpha_long < 0.5:
            st.text("    β†’ Ξ± < 0.5: Long-run volatility grows sub-linearly. Possible mean-reversion across days/weeks.")
            st.text("       For traders: swing reversion setups and volatility selling may be effective.")
        elif np.isclose(alpha_long, 0.5, atol=0.05):
            st.text("    β†’ Ξ± β‰ˆ 0.5: Consistent with Brownian motion. No memory in long-term returns.")
            st.text("       For traders: directional strategies offer no statistical edge β€” focus on volatility structures instead.")
        else:
            st.text("    β†’ Ξ± > 0.5: Long-run volatility grows super-linearly. Indicates trend persistence or structural drift.")
            st.text("       For traders: long-term trend-following, carry, or breakout systems are likely to work.")

    progress_bar.progress(30)

    # ================== SECTION: Intraday Signal-to-Noise Ratio ==================
    st.subheader("Intraday Signal-to-Noise Ratio")
    
    st.markdown(
    "This section estimates how much of the intraday volatility is actual price movement versus noise from market mechanics."
    )
    
    with st.expander("Methodology", expanded=False):
        st.markdown(r"""
        ##### Intraday Signal-to-Noise Ratio (SNR)

        This plot shows how much of the observed volatility at each intraday interval reflects true market movement versus noise introduced by high-frequency effects.

        Signal-to-noise ratio is defined as:

        $$
        \text{SNR}(T) = \frac{\sigma_0^2}{\sigma_T^2}
        $$

        - $\sigma_0^2$: latent variance, estimated from the two-component model  
        - $\sigma_T^2$: empirical variance at sampling interval $T$


        ##### Interpretation

        - $\text{SNR} < 1$ β†’ Noise dominates  
        - $\text{SNR} \rightarrow 1$ as $T$ increases β†’ Signal becomes clearer as noise decays


        ##### Why This Applies Only to High-Frequency Data

        At short intervals, volatility is inflated by:
        - bid-ask bounce  
        - latency  
        - execution frictions

        As intervals widen, these distortions average out. SNR becomes useful for identifying when high-frequency signals are likely unreliable.

        For longer timeframes (daily or more), microstructure effects are negligible. SNR isn't meaningful in those settings.

        This diagnostic helps identify the time scales where volatility reflects genuine price discovery versus transient noise.
        """)
    
    snr_intra = sigma0_sq_hat_intra / var_intra

    fig_snr = go.Figure()
    fig_snr.add_trace(go.Scatter(
        x=T_intra,
        y=snr_intra,
        mode='lines+markers',
        name='Οƒβ‚€Β² / σ²',
        line=dict(color='purple', width=3)
    ))

    for i, label_ in enumerate(intraday_labels):
        fig_snr.add_annotation(
            x=T_intra[i],
            y=snr_intra[i],
            text=label_,
            showarrow=False,
            yshift=10,
            font=dict(size=14)
        )

    fig_snr.add_shape(
        type='line',
        x0=min(T_intra),
        x1=max(T_intra),
        y0=1,
        y1=1,
        line=dict(color='green', dash='dash', width=3)
    )

    fig_snr.update_layout(
        #title='Intraday Signal-to-Noise Ratio',
        title=dict(text='Intraday Signal-to-Noise Ratio', font=dict(color='white')),
        xaxis_title='Sampling Interval (minutes)',
        yaxis_title='Οƒβ‚€Β² / σ² (Signal-to-Noise)',
        template='plotly_dark',
        paper_bgcolor='#0e1117',
        plot_bgcolor='#0e1117',
        legend=dict(font=dict(color='white')),
        height=400,
        width=1000
    )
    fig_snr.update_yaxes(gridcolor='rgba(255,255,255,0.1)')
    st.plotly_chart(fig_snr, use_container_width=True)

    with st.expander("Intraday Signal-to-Noise Ratio - Dynamic Interpretation", expanded=False):
        st.text("INTERPRETATION:")
        for i, interval_ in enumerate(T_intra):
            snr_val = snr_intra[i]
            label_ = intraday_labels[i]
            st.text(f"{label_} (interval = {interval_} min): Οƒβ‚€Β² / σ² β‰ˆ {snr_val:.2f}")
            if snr_val > 0.7:
                st.text("   β†’ Signal dominates. Diffusive price movement explains most of the variance.")
                st.text("     For traders: market microstructure noise is low. Alpha signals are likely more reliable.\n")
            elif 0.3 < snr_val <= 0.7:
                st.text("   β†’ Mixed regime. Both signal and noise contribute materially.")
                st.text("     For traders: consider robust execution filters and avoid overfitting short-term models.\n")
            else:
                st.text("   β†’ Noise dominates. Most variance is from short-horizon microstructure effects.")
                st.text("     For traders: avoid signals at this interval. Noise overwhelms usable price information.\n")

    progress_bar.progress(40)

    # ================== SECTION: Intraday Average Volatility Signature Plot ==================
    st.subheader("Intraday Average Volatility Signature Plot")
    
    st.markdown(
    "This section shows how realized volatility behaves throughout the trading day, averaged across recent sessions and multiple time resolutions."
    )
    
    with st.expander("Methodology", expanded=False):
        st.markdown(r"""
    ##### Intraday Volatility Patterns by Time of Day

    This analysis estimates average volatility at each clock time during U.S. market hours using multiple intraday windows.

    Rolling realized volatility is computed using intraday log returns sampled over these intervals:
    - 1 min, 5 min, 15 min  
    - 30 min, 1 hour, 2 hours, 4 hours

    Each volatility series is then averaged by time of day (Eastern Time). This reveals typical volatility behavior across the session.

    ---

    ##### Common Intraday Pattern

    Volatility tends to follow a U-shape across the trading day:
    - High volatility after market open (9:30–10:30 AM)
    - Low volatility midday (11:30 AM–2:00 PM)
    - Rising volatility near close (3:00–4:00 PM)

    This pattern is observed across all sampling windows. Shorter intervals capture more microstructure effects and noise. Longer intervals smooth these distortions.

    ---

    ##### Technical Details

    Annualized volatility is computed using:

    $$
    \sigma_{\text{annual}} = \sqrt{\sum r^2} \cdot \sqrt{\frac{252 \times 6.5 \times 60}{\text{window size in minutes}}}
    $$

    The y-axis is displayed on a log scale to improve readability across different magnitudes.

    This view helps identify when volatility tends to cluster during the day and informs execution timing and risk budgeting.
    """)

    # Original code block uses new data load for '8d' intraday
    data_intra_avg = safe_download(ticker, period='8d', interval='1m')
    if data_intra_avg is None or data_intra_avg.empty:
        st.error("No intraday data available for the Intraday Average Volatility section.")
        st.stop()

    data_intra_avg.index = pd.to_datetime(data_intra_avg.index).tz_convert('America/New_York')
    data_intra_avg['log_return'] = np.log(data_intra_avg['Close'] / data_intra_avg['Close'].shift(1))
    data_intra_avg.dropna(inplace=True)

    windows_dict = {
        '1 Min': 1,
        '5 Min': 5,
        '15 Min': 15,
        '30 Min': 30,
        '1 Hour': 60,
        '2 Hours': 120,
        '4 Hours': 240
    }
    trading_minutes_per_year = 252 * 6.5 * 60
    data_intra_avg['time'] = data_intra_avg.index.strftime('%H:%M')

    intraday_vol = pd.DataFrame()
    for label_, w_ in windows_dict.items():
        data_intra_avg[f'{label_}_vol'] = (
            data_intra_avg['log_return']
            .rolling(w_)
            .apply(lambda x: np.sqrt(np.sum(x**2) * (trading_minutes_per_year / w_)), raw=True)
        )
        intraday_vol[label_] = data_intra_avg.groupby('time')[f'{label_}_vol'].mean()

    intraday_vol.index = intraday_vol.index.astype(str)

    # Reduce x-axis labels
    num_labels = 30
    time_labels = np.linspace(0, len(intraday_vol.index) - 1, num_labels, dtype=int)
    selected_xticks = [intraday_vol.index[i] for i in time_labels]

    fig_intra_avg = go.Figure()
    for label_ in windows_dict.keys():
        fig_intra_avg.add_trace(go.Scatter(
            x=intraday_vol.index,
            y=intraday_vol[label_],
            mode='lines',
            name=label_,
            opacity=0.8
        ))

    fig_intra_avg.update_layout(
        #title=f'Intraday Average Volatility Signature Plot for {ticker}',
        title=dict(text=f'Intraday Average Volatility Signature Plot for {ticker}', font=dict(color='white')),
        xaxis_title='Time of Day (ET)',
        yaxis_title='Annualized Volatility',
        template='plotly_dark',
        paper_bgcolor='#0e1117',
        plot_bgcolor='#0e1117',
        height=500,
        width=1500,
        legend=dict(font=dict(color='white')),
        xaxis=dict(
            tickmode='array',
            tickvals=selected_xticks,
            ticktext=selected_xticks,
            tickangle=45
        ),
        yaxis_type='log'
    )
    fig_intra_avg.update_yaxes(gridcolor='rgba(255,255,255,0.1)')
    st.plotly_chart(fig_intra_avg, use_container_width=True)

    with st.expander("Intraday Average Volatility Signature Plot - Dynamic Interpretation", expanded=False):
        st.text("INTRADAY VOLATILITY INTERPRETATION:")

        ref_label = '5 Min'
        vol_series = intraday_vol[ref_label]

        peak_start = vol_series.iloc[:int(len(vol_series) * 0.33)].idxmax()
        peak_end = vol_series.iloc[int(len(vol_series) * 0.66):].idxmax()
        trough = vol_series.idxmin()

        st.text(f"β†’ Peak volatility near open:     {peak_start}")
        st.text(f"β†’ Trough volatility mid-session: {trough}")
        st.text(f"β†’ Peak volatility near close:    {peak_end}")

        early_peak = vol_series[peak_start] > vol_series[trough]
        late_peak = vol_series[peak_end] > vol_series[trough]

        if early_peak and late_peak:
            st.text("   β†’ U-shape pattern detected. Volatility is elevated during market open and close.")
            st.text("     For traders: liquidity risk is higher early and late in the session. Expect wider spreads, faster price moves.")
            st.text("     Execution near mid-day tends to carry less volatility risk β€” better for passive orders or size execution.")
        else:
            st.text("   β†’ No clear U-shape. Volatility profile is irregular.")
            st.text("     For traders: intraday behavior may be event-driven or news-sensitive in this period.")

        st.text("\nSample intraday volatility (5-min window):")
        sample_points = vol_series.iloc[[0, len(vol_series)//2, -1]]
        st.text(str(sample_points))

    progress_bar.progress(60)

    # ================== SECTION: Realized vs. Implied Volatility ==================
    st.subheader("Realized vs. Implied Volatility")
    
    st.markdown(
    "This section compares realized volatility over multiple horizons with implied volatility, using the VIX index as a proxy."
    )
    
    with st.expander("Methodology", expanded=False):
        st.markdown(r"""
    ##### Long-Term Realized vs. Implied Volatility

    This comparison includes:
    - **Realized volatility** estimated from historical returns  
    - **Implied volatility** from the VIX, which reflects market expectations over the next 30 days

    ##### Realized Volatility

    Computed using rolling log returns:

    $$
    \sigma_{\text{realized}} = \sqrt{ \sum_{i=1}^n r_i^2 \cdot \frac{\text{Annualization Factor}}{n} }
    $$

    - $r_i$: daily log return  
    - $n$: window size (1, 5, or 21 days)  
    - Annualization factors:
      - 252 for daily  
      - 52 for weekly  
      - 12 for monthly

    ##### Implied Volatility (VIX)

    - Derived from S&P 500 options  
    - Annualized  
    - Represents the market’s forward-looking 30-day volatility estimate

    ##### Interpretation

    - Daily realized volatility is reactive and noisy  
    - Weekly and monthly realized volatility track broader trends  
    - VIX tends to exceed realized volatility due to a **volatility risk premium**

    When realized volatility exceeds VIX, it signals an unexpected volatility event. Examples include earnings shocks, macro announcements, or crashes.

    ##### Why This Comparison Matters

    - **Volatility spreads** (VIX minus realized) may signal option overpricing or underpricing  
    - **Traders** can time volatility-selling or hedging strategies  
    - **Risk teams** can detect periods of market overreaction or complacency
    """)    

    # Original code: data from '5y'
    rv_data = safe_download(ticker, period='5y', interval='1d')
    if rv_data is None or rv_data.empty:
        st.error("No data available for Realized vs. Implied Volatility section.")
        st.stop()

    if isinstance(rv_data.columns, pd.MultiIndex):
        rv_data.columns = rv_data.columns.get_level_values(0)

    rv_data['log_return'] = np.log(rv_data['Close'] / rv_data['Close'].shift(1))
    rv_data.dropna(inplace=True)

    windows_ = {'Daily': 1, 'Weekly': 5, 'Monthly': 21}
    annual_factors = {'Daily': 252, 'Weekly': 52, 'Monthly': 12}

    for label_, w_ in windows_.items():
        rv_data[f'{label_}_vol'] = rv_data['log_return'].rolling(w_).apply(
            lambda x: np.sqrt(np.sum(x**2) * (annual_factors[label_] / w_)), raw=True
        )

    # Download VIX
    vix_data = safe_download('^VIX', period='10y', interval='1d')
    if vix_data is None or vix_data.empty:
        st.error("No data for implied volatility. The plot might be empty.")
        # We'll still proceed, but plot might be partial.

    else:
        if isinstance(vix_data.columns, pd.MultiIndex):
            vix_data.columns = vix_data.columns.get_level_values(0)
        vix_data = vix_data['Close'].reindex(rv_data.index, method='ffill') / 100

    fig_rv_iv = go.Figure()

    fig_rv_iv.add_trace(go.Scatter(
        x=rv_data.index,
        y=rv_data['Daily_vol'],
        name='Realized Daily Volatility',
        line=dict(color='orange', width=1),
        opacity=0.3
    ))

    fig_rv_iv.add_trace(go.Scatter(
        x=rv_data.index,
        y=rv_data['Weekly_vol'],
        name='Realized Weekly Volatility',
        line=dict(color='green', width=2)
    ))

    fig_rv_iv.add_trace(go.Scatter(
        x=rv_data.index,
        y=rv_data['Monthly_vol'],
        name='Realized Monthly Volatility',
        line=dict(color='blue', width=2)
    ))

    if vix_data is not None and not vix_data.empty:
        fig_rv_iv.add_trace(go.Scatter(
            x=rv_data.index,
            y=vix_data,
            name='VIX (Implied Volatility)',
            line=dict(color='red', dash='dash', width=2)
        ))

    # Stock price on secondary axis
    fig_rv_iv.add_trace(go.Scatter(
        x=rv_data.index,
        y=rv_data['Close'],
        name='Stock Price',
        line=dict(color='white'),
        opacity=0.2,
        yaxis='y2',
        showlegend=True
    ))

    fig_rv_iv.update_layout(
        #title=f'Realized vs. Implied Volatility for {ticker}',
        title=dict(text=f'Realized vs. Implied Volatility for {ticker}', font=dict(color='white')),
        template='plotly_dark',
        paper_bgcolor='#0e1117',
        plot_bgcolor='#0e1117',
        height=600,
        width=1500,
        xaxis=dict(title='Date'),
        yaxis=dict(title='Annualized Volatility'),
        yaxis2=dict(
            title='Stock Price',
            overlaying='y',
            side='right',
            showgrid=False
        ),
        legend=dict(x=0.01, y=0.99), font=dict(color='white'),
        margin=dict(l=60, r=60, t=60, b=60)
    )
    
    fig_rv_iv.update_yaxes(gridcolor='rgba(255,255,255,0.1)')

    st.plotly_chart(fig_rv_iv, use_container_width=True)

    with st.expander("Realized vs. Implied Volatility - Dynamic Interpretation", expanded=False):
        st.text("\nDYNAMIC INTERPRETATION:")
        st.text("------------------------")
        if (vix_data is not None and not vix_data.empty and
                not rv_data.empty and 'Monthly_vol' in rv_data.columns):
            latest_ = rv_data.dropna().iloc[-1]
            vix_latest = vix_data.dropna().iloc[-1] if not vix_data.dropna().empty else float('nan')
            realized_monthly = latest_['Monthly_vol']

            st.text(f"Latest VIX (Implied 1M Vol):     {vix_latest:.2%}")
            st.text(f"Latest Realized Monthly Vol:    {realized_monthly:.2%}\n")

            if vix_latest > realized_monthly * 1.2:
                st.text("β†’ Implied volatility is significantly higher than realized 1-month volatility.")
                st.text("   Traders are demanding a risk premium β€” possibly due to uncertainty or expected catalysts.")
                st.text("   For traders: options may be overpriced. Selling vol could outperform (e.g., short straddles with risk limits).")
            elif vix_latest < realized_monthly * 0.8:
                st.text("β†’ Implied volatility is below realized 1-month volatility.")
                st.text("   Market might be underestimating future risk or recent realized vol hasn't mean-reverted.")
                st.text("   For traders: long vol trades (e.g., buying calls/puts or strangles) might offer favorable asymmetry.")
            else:
                st.text("β†’ Implied and realized monthly volatility are broadly aligned.")
                st.text("   Market expectations are in line with past realized movement.")
                st.text("   For traders: neutral vol stance. Consider structure, skew, or relative value strategies instead.")

            monthly_vol_series = rv_data['Monthly_vol'].dropna()
            if len(monthly_vol_series) > 21:
                vol_rolling_avg = monthly_vol_series.rolling(21).mean().iloc[-1]
                if realized_monthly > vol_rolling_avg * 1.3:
                    st.text("\n→ Realized monthly volatility is well above its 1-month moving average.")
                    st.text("   For traders: regime shift likely. Could be due to macro events, earnings, or broad market repricing.")
                elif realized_monthly < vol_rolling_avg * 0.7:
                    st.text("\n→ Realized monthly volatility is suppressed relative to recent history.")
                    st.text("   For traders: volatility compression phase β€” watch for breakout setups or sudden repricing.")

            if len(rv_data) > 1:
                vol_change = realized_monthly - rv_data['Monthly_vol'].iloc[-2]
                if vol_change > 0.01:
                    st.text("β†’ Vol is expanding vs. previous day. Indicates rising uncertainty or event response.")
                elif vol_change < -0.01:
                    st.text("β†’ Vol is compressing vs. previous day. Market calming or digesting recent moves.")
        else:
            st.text("Not enough data to show the Realized vs. Implied analysis or it is empty.")

    progress_bar.progress(80)

    # ================== SECTION: Day of the Week Effect ==================
    st.subheader("Day of the Week Effect")
    

    st.markdown(
        "This section shows how realized volatility varies across weekdays using intraday return data."
    )

    with st.expander("Methodology", expanded=False):
        st.markdown(r"""
    ##### Day-of-Week Patterns in Realized Volatility

    This analysis uses 5-minute intraday returns over the past 60 trading days. Realized volatility is computed daily and then averaged by weekday.

    ##### Daily Volatility Calculation

    Using 5-minute log returns, daily realized volatility is:

    $$
    \sigma_{\text{daily}} = \sqrt{ \sum_{i=1}^{n} r_i^2 }
    $$

    - $r_i$: 5-minute log returns  
    - $n$: number of 5-minute intervals in the trading day

    Each day's volatility is then grouped by weekday and averaged.

    ##### Interpretation

    - **Mondays** often show elevated volatility, possibly due to weekend news and risk rebalancing  
    - **Fridays** can show rising volatility as traders adjust positions before the weekend  
    - **Mid-week** (Tuesday–Thursday) tends to be quieter with fewer major market events

    This pattern helps identify which days tend to carry more execution or risk management impact.
    """)

    

    df_5m = safe_download(ticker, period='60d', interval='5m')
    if df_5m is None or df_5m.empty:
        st.error("No intraday data available for Day-of-Week analysis.")
        st.stop()

    if isinstance(df_5m.columns, pd.MultiIndex):
        df_5m.columns = df_5m.columns.get_level_values(0)

    df_5m.index = pd.to_datetime(df_5m.index)
    df_5m['log_return'] = np.log(df_5m['Close'] / df_5m['Close'].shift(1))
    df_5m.dropna(inplace=True)

    df_5m['date'] = df_5m.index.date
    df_5m['weekday'] = df_5m.index.dayofweek
    df_5m = df_5m[df_5m['weekday'] < 5]

    daily_vol = df_5m.groupby('date')['log_return'].apply(lambda x: np.sqrt(np.sum(x**2)))
    daily_vol = daily_vol.reset_index().rename(columns={'log_return': 'realized_vol'})
    daily_vol['date'] = pd.to_datetime(daily_vol['date'])
    daily_vol['weekday'] = daily_vol['date'].dt.dayofweek

    weekday_vol = daily_vol.groupby('weekday')['realized_vol'].mean().reset_index()
    weekday_map = {0: 'Monday', 1: 'Tuesday', 2: 'Wednesday', 3: 'Thursday', 4: 'Friday'}
    weekday_vol['weekday_name'] = weekday_vol['weekday'].map(weekday_map)

    fig_dotw = go.Figure()
    fig_dotw.add_trace(go.Bar(
        x=weekday_vol['weekday_name'],
        y=weekday_vol['realized_vol'],
        marker_color='green'
    ))

    fig_dotw.update_layout(
        #title=f'Day of the Week Effect for Realized Volatility ({ticker})',
        title=dict(text=f'Day of the Week Effect for Realized Volatility ({ticker})', font=dict(color='white')),
        xaxis_title='Day of the Week',
        yaxis_title='Average Realized Volatility',
        template='plotly_dark',
        paper_bgcolor='#0e1117',
        plot_bgcolor='#0e1117',
        legend=dict(font=dict(color='white')),
        height=400,
        width=1200
    )
    fig_dotw.update_yaxes(gridcolor='rgba(255,255,255,0.1)')
    
    st.plotly_chart(fig_dotw, use_container_width=True)

    with st.expander("Day of the Week Effect - Dynamic Interpretation", expanded=False):
        st.text("\nDYNAMIC INTERPRETATION:")
        st.text("------------------------")

        sorted_vol = weekday_vol.sort_values(by='realized_vol', ascending=False)

        # Extract min and max vol days
        most_volatile_day = sorted_vol.iloc[0]
        least_volatile_day = sorted_vol.iloc[-1]

        st.text("Average realized vol by weekday (sorted):")
        for i, row in sorted_vol.iterrows():
            st.text(f"  {row['weekday_name']}: {row['realized_vol']:.4f}")

        st.text(f"\n→ Highest average volatility: {most_volatile_day['weekday_name']} ({most_volatile_day['realized_vol']:.4f})")
        st.text(f"β†’ Lowest average volatility:  {least_volatile_day['weekday_name']} ({least_volatile_day['realized_vol']:.4f})")

        mon_vol = weekday_vol.loc[weekday_vol['weekday'] == 0, 'realized_vol'].values[0]
        fri_vol = weekday_vol.loc[weekday_vol['weekday'] == 4, 'realized_vol'].values[0]
        wed_vol = weekday_vol.loc[weekday_vol['weekday'] == 2, 'realized_vol'].values[0]

        st.text("")
        if mon_vol > fri_vol and mon_vol > wed_vol:
            st.text("β†’ Monday volatility is elevated.")
            st.text("   Interpretation: markets often react to weekend news or macro events on Mondays.")
        elif fri_vol > mon_vol and fri_vol > wed_vol:
            st.text("β†’ Friday volatility is elevated.")
            st.text("   Interpretation: traders adjusting risk before the weekend may cause more aggressive positioning.")
        elif wed_vol < mon_vol and wed_vol < fri_vol:
            st.text("β†’ Wednesday is the quietest.")
            st.text("   Interpretation: midweek lulls are common β€” lower volume, fewer catalysts.")

        vol_range = sorted_vol['realized_vol'].max() - sorted_vol['realized_vol'].min()
        if vol_range < 0.005:
            st.text("β†’ Volatility is fairly uniform across weekdays.")
            st.text("   Interpretation: No clear day-of-week effect β€” intraday factors likely dominate.")
        else:
            st.text("β†’ There's a statistically meaningful difference in vol across days.")
            st.text("   Interpretation: consider adjusting strategy timing to favor higher-volatility days.")

    progress_bar.progress(100)
    st.success("Analysis complete.")

# Hide default Streamlit style
st.markdown(
    """
    <style>
    #MainMenu {visibility: hidden;}
    footer {visibility: hidden;}
    </style>
    """,
    unsafe_allow_html=True
)