Spaces:
Sleeping
Sleeping
Create app.py
Browse files
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 |
+
|