Spaces:
Sleeping
Sleeping
| 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 | |
| ) | |