Space69 / app.py
QuantumLearner's picture
Update app.py
c7bb913 verified
import streamlit as st
import datetime
import pandas as pd
import numpy as np
import plotly.graph_objs as go
from plotly.subplots import make_subplots
from scipy import stats
from statsmodels.tsa.stattools import adfuller
from scipy.stats import norm
# =====================================================================
# Streamlit Configuration
# =====================================================================
st.set_page_config(page_title="Market Inefficiency Detection", layout="wide")
st.title("Market Inefficiency Detection")
st.markdown(
"**This tool provides a comprehensive analysis of market efficiency.** "
"It uses two approaches: one examines price randomness through Runs and ADF tests, "
"and the other evaluates momentum versus mean reversion via variance ratio and autocorrelation analyses. "
"Adjust the parameters in the sidebar, press 'Run Analysis', and switch between pages to explore different insights."
)
# =====================================================================
# Sidebar - User Inputs
# =====================================================================
st.sidebar.markdown("### User Inputs")
page_selection = st.sidebar.radio(
"Select Page",
("Market Efficiency", "Mean vs Momentum"),
index=0
)
# Group inputs into expanders
with st.sidebar.expander("Main Parameters", expanded=True):
ticker = st.text_input(
label="Ticker",
value="ASML",
help="Enter the stock symbol or cryptopair (e.g.'TSLA', 'BTC-USD')."
)
default_start = datetime.date(2020, 1, 1)
default_end = datetime.date.today() + datetime.timedelta(days=1)
start_date = st.date_input(
label="Start date",
value=default_start,
help="Data start date."
)
end_date = st.date_input(
label="End date",
value=default_end,
help="Data end date."
)
with st.sidebar.expander("Market Efficiency Parameters", expanded=False):
rolling_window = st.number_input(
label="Rolling Window (days)",
min_value=10, max_value=365,
value=60,
help="Number of days in rolling calculations for Market Efficiency."
)
with st.sidebar.expander("Mean vs Momentum Parameters", expanded=False):
rolling_window_daily = st.number_input(
"Daily Rolling Window",
min_value=30, max_value=365,
value=60,
help="Rolling window for daily data."
)
rolling_window_weekly = st.number_input(
"Weekly Rolling Window",
min_value=5, max_value=52,
value=20,
help="Rolling window for weekly data."
)
rolling_window_monthly = st.number_input(
"Monthly Rolling Window",
min_value=3, max_value=24,
value=12,
help="Rolling window for monthly data."
)
max_lag = st.number_input(
"Max Lag for Autocorr",
min_value=1, max_value=10,
value=3,
help="Number of lags to average in autocorr calculations."
)
lag_val = st.number_input(
"Single Lag Value",
min_value=1, max_value=10,
value=1,
help="Lag for single-lag autocorrelation."
)
run_button = st.sidebar.button("Run Analysis")
# =====================================================================
# Caching - Data Loaders
# =====================================================================
@st.cache_data
def load_data(symbol, start, end):
"""
Loads daily close data.
Returns a DataFrame with 'Open','High','Low','Close','Volume'.
"""
import yfinance as yf # Local import to keep references minimal
try:
df = yf.download(symbol, start=start, end=end, progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
return df
except Exception:
st.error("Could not retrieve data. Please revise your inputs.")
return pd.DataFrame()
@st.cache_data
def load_data_interval(symbol, start, end, interval):
"""
Loads data for a specified interval (1d, 1wk, 1mo).
"""
import yfinance as yf
try:
df = yf.download(symbol, start=start, end=end, interval=interval, progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
return df
except Exception:
st.error("Could not retrieve data. Please revise your inputs.")
return pd.DataFrame()
# =====================================================================
# Utility Functions
# =====================================================================
def group_intervals_for_shading(df, boolean_col):
"""
Groups consecutive True rows. Returns list of (start, end) index pairs.
We shade intervals for consecutive True rows.
"""
intervals = []
df_slice = df[boolean_col].copy()
if len(df_slice) < 2:
return intervals
start_idx = None
idx_vals = df_slice.index.to_list()
for i in range(len(df_slice) - 1):
if df_slice.iloc[i]:
if start_idx is None:
start_idx = idx_vals[i]
if not df_slice.iloc[i+1]:
end_idx = idx_vals[i+1]
intervals.append((start_idx, end_idx))
start_idx = None
else:
continue
if start_idx is not None and df_slice.iloc[-1]:
intervals.append((start_idx, idx_vals[-1]))
return intervals
def add_significant_shades(fig, pval_series, fill_color):
"""
Adds vertical shading where pval < 0.05.
"""
sig_bool = pval_series < 0.05
sig_shift = sig_bool.shift(1, fill_value=False)
starts = (sig_bool & ~sig_shift)
ends = (~sig_bool & sig_shift)
start_dates = pval_series.index[starts]
end_dates = pval_series.index[ends]
if len(start_dates) > len(end_dates):
end_dates = end_dates.append(pd.Index([pval_series.index[-1]]))
for s, e in zip(start_dates, end_dates):
fig.add_shape(
type='rect',
xref='x', x0=s, x1=e,
yref='paper', y0=0, y1=1,
fillcolor=fill_color,
opacity=0.2,
layer='below',
line=dict(width=0)
)
# =====================================================================
# Functions that do the heavy computations for each page
# =====================================================================
def compute_market_efficiency(df, rolling_window):
"""
Performs the rolling runs test, rolling ADF test, and
builds the Plotly figure for the 'Market Efficiency' page.
Returns a single Plotly Figure object.
"""
df["Return"] = df["Close"].pct_change()
df["MA50"] = df["Close"].rolling(window=50).mean()
df["MA200"] = df["Close"].rolling(window=200).mean()
# 1) Rolling Runs Test
runs_results = []
for i in range(rolling_window, len(df)):
window_data = df["Return"].iloc[i - rolling_window : i].dropna()
if len(window_data) < rolling_window - 1:
continue
signs = np.where(window_data > 0, 1, 0)
n1 = np.sum(signs == 1)
n2 = len(window_data) - n1
runs = 1 + np.sum(signs[1:] != signs[:-1])
mu = 1 + (2 * n1 * n2) / (n1 + n2)
sigma = np.sqrt(
(2 * n1 * n2 * (2 * n1 * n2 - n1 - n2))
/ ((n1 + n2) ** 2 * (n1 + n2 - 1))
)
Z = (runs - mu) / sigma
p_value_runs = 2 * (1 - stats.norm.cdf(abs(Z)))
date_i = df.index[i]
efficient_bool = p_value_runs >= 0.05
runs_results.append(
{
"Date": date_i,
"p_value": p_value_runs,
"Z": Z,
"efficient": efficient_bool,
}
)
df_runs = pd.DataFrame(runs_results).set_index("Date")
# 2) Rolling ADF Test
adf_results = []
close_series = df["Close"]
for i in range(rolling_window, len(close_series)):
window_data = close_series.iloc[i - rolling_window : i]
result = adfuller(window_data, autolag="AIC")
p_value_adf = result[1]
date_i = close_series.index[i]
adf_results.append({"Date": date_i, "p_value": p_value_adf})
df_adf = pd.DataFrame(adf_results).set_index("Date")
# Build figure
fig = make_subplots(
rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.07,
subplot_titles=(
"Price with Runs & ADF Shading",
"Rolling Runs Test p-value",
"Rolling ADF Test p-value"
)
)
# Subplot 1: Price
fig.add_trace(
go.Scatter(x=df.index, y=df["Close"], mode="lines", name="Close"),
row=1, col=1
)
fig.add_trace(
go.Scatter(x=df.index, y=df["MA50"], mode="lines", name="MA 50"),
row=1, col=1
)
fig.add_trace(
go.Scatter(x=df.index, y=df["MA200"], mode="lines", name="MA 200"),
row=1, col=1
)
# Runs shading
df_runs["inefficient"] = ~df_runs["efficient"]
runs_intervals = group_intervals_for_shading(df_runs, "inefficient")
for (start, end) in runs_intervals:
fig.add_vrect(
x0=start, x1=end,
fillcolor="red", opacity=0.2, line_width=0,
row=1, col=1
)
# ADF shading
df_adf["reject"] = df_adf["p_value"] < 0.05
adf_intervals = group_intervals_for_shading(df_adf, "reject")
for (start, end) in adf_intervals:
fig.add_vrect(
x0=start, x1=end,
fillcolor="orange", opacity=0.2, line_width=0,
row=1, col=1
)
# Subplot 2: Runs p-value
fig.add_trace(
go.Scatter(x=df_runs.index, y=df_runs["p_value"], mode="lines", name="Runs p-value"),
row=2, col=1
)
fig.add_hline(y=0.05, line_dash="dash", row=2, col=1, annotation_text="0.05 threshold")
df_runs["inefficient"] = df_runs["p_value"] < 0.05
ineff_intervals = group_intervals_for_shading(df_runs, "inefficient")
for (start, end) in ineff_intervals:
fig.add_vrect(
x0=start, x1=end,
fillcolor="red", opacity=0.2, line_width=0,
row=2, col=1
)
df_runs["efficient_2"] = df_runs["p_value"] >= 0.05
eff_intervals = group_intervals_for_shading(df_runs, "efficient_2")
for (start, end) in eff_intervals:
fig.add_vrect(
x0=start, x1=end,
fillcolor="lime", opacity=0.2, line_width=0,
row=2, col=1
)
# Subplot 3: ADF p-value
fig.add_trace(
go.Scatter(x=df_adf.index, y=df_adf["p_value"], mode="lines", name="ADF p-value"),
row=3, col=1
)
fig.add_hline(y=0.05, line_dash="dash", row=3, col=1, annotation_text="0.05 threshold")
df_adf["reject"] = df_adf["p_value"] < 0.05
reject_intervals = group_intervals_for_shading(df_adf, "reject")
for (start, end) in reject_intervals:
fig.add_vrect(
x0=start, x1=end,
fillcolor="orange", opacity=0.2, line_width=0,
row=3, col=1
)
fig.update_layout(
title_text="Market Efficiency Overview",
template="plotly_dark",
paper_bgcolor='#0e1117',
plot_bgcolor='#0e1117',
height=900,
showlegend=True,
title_font_color='white',
legend_font_color='white',
font_color='white'
)
# Update all x and y axes for grid and label color
fig.for_each_xaxis(lambda axis: axis.update(gridcolor='rgba(255,255,255,0.2)', color='white'))
fig.for_each_yaxis(lambda axis: axis.update(gridcolor='rgba(255,255,255,0.2)', color='white'))
fig.update_xaxes(tickformat="%Y-%m", row=1, col=1)
fig.update_xaxes(tickformat="%Y-%m", row=2, col=1)
fig.update_xaxes(tickformat="%Y-%m", row=3, col=1)
fig.update_yaxes(title_text="Price", row=1, col=1)
fig.update_yaxes(title_text="p-value", row=2, col=1)
fig.update_yaxes(title_text="p-value", row=3, col=1)
return fig
def compute_mean_vs_momentum(daily_data, weekly_data, monthly_data,
ticker, rolling_window_daily, rolling_window_weekly,
rolling_window_monthly, max_lag, lag_val):
"""
Computes all mean vs momentum analyses.
Returns a list of five Plotly Figure objects.
"""
# Rolling Variance Ratio
def rolling_variance_ratio(returns, q, window):
vr_series = pd.Series(index=returns.index, dtype=float)
pval_series = pd.Series(index=returns.index, dtype=float)
for i in range(window + q, len(returns)):
rolling_ret = returns.iloc[i - window - q : i - q]
var_1 = rolling_ret.var()
if var_1 == 0:
continue
ret_q = rolling_ret.rolling(window=q).sum().dropna()
var_q = ret_q.var()
vr = var_q / (q * var_1)
vr_series.iloc[i] = vr
T = window
var_vr = (2 * (2*q - 1)*(q - 1)) / (3*q*T)
z_score = (vr - 1)/np.sqrt(var_vr)
p_value = 2 * (1 - norm.cdf(abs(z_score)))
pval_series.iloc[i] = p_value
return vr_series, pval_series
for df_ in [daily_data, weekly_data, monthly_data]:
pass
holding_periods = [2, 5, 10, 20, 60]
df_vr = pd.DataFrame(index=daily_data.index)
df_pval = pd.DataFrame(index=daily_data.index)
for q_ in holding_periods:
vr_, pval_ = rolling_variance_ratio(daily_data["Returns"], q_, 252)
df_vr[f"VR_{q_}"] = vr_
df_pval[f"PVal_{q_}"] = pval_
def rolling_acorr(series, window_, lag_):
return series.rolling(window_).apply(
lambda x: pd.Series(x).autocorr(lag=lag_), raw=False
)
daily_roll_ac = rolling_acorr(daily_data["Returns"], rolling_window_daily, lag_val)
weekly_roll_ac = rolling_acorr(weekly_data["Returns"], rolling_window_weekly, lag_val)
monthly_roll_ac = rolling_acorr(monthly_data["Returns"], rolling_window_monthly, lag_val)
threshold_daily = 2 / np.sqrt(rolling_window_daily)
threshold_weekly = 2 / np.sqrt(rolling_window_weekly)
threshold_monthly = 2 / np.sqrt(rolling_window_monthly)
def rolling_average_acorr(series, window_, max_lag_):
def avg_acorr(x):
acs = [pd.Series(x).autocorr(lag=i) for i in range(1, max_lag_ + 1)]
return np.mean(acs)
return series.rolling(window_).apply(avg_acorr, raw=False)
daily_roll_avg_ac = rolling_average_acorr(
daily_data["Returns"], rolling_window_daily, max_lag
)
weekly_roll_avg_ac = rolling_average_acorr(
weekly_data["Returns"], rolling_window_weekly, max_lag
)
monthly_roll_avg_ac = rolling_average_acorr(
monthly_data["Returns"], rolling_window_monthly, max_lag
)
threshold_daily_avg = threshold_daily
threshold_weekly_avg = threshold_weekly
threshold_monthly_avg = threshold_monthly
common_layout = dict(
width=1500,
template="plotly_dark",
margin=dict(l=60, r=60, t=50, b=120),
xaxis=dict(tickformat="%Y-%m", dtick="M1", tickangle=-45, gridcolor='rgba(255,255,255,0.2)', color='white'),
yaxis=dict(gridcolor='rgba(255,255,255,0.2)', color='white'),
paper_bgcolor='#0e1117',
plot_bgcolor='#0e1117',
legend=dict(
orientation="h",
yanchor="top",
y=-0.2,
xanchor="center",
x=0.5,
font=dict(color='white')
),
title_font_color='white',
font_color='white'
)
# ---- Figure 1 ----
fig1 = go.Figure()
fig1.add_trace(go.Scatter(
x=daily_data.index, y=daily_data["Close"],
mode="lines", name=f"{ticker} Stock Price",
line=dict(color="white", width=2)
))
fig1.add_trace(go.Scatter(
x=daily_data.index, y=daily_data["MA200"],
mode="lines", name="200-Day MA",
line=dict(color="cyan", dash="dash", width=2),
opacity=0.3
))
fig1.add_trace(go.Scatter(
x=daily_data.index, y=daily_data["MA7"],
mode="lines", name="7-Day MA",
line=dict(color="magenta", dash="dot", width=2),
opacity=0.8
))
fig1.add_trace(go.Scatter(
x=daily_data.index, y=daily_data["MA30"],
mode="lines", name="30-Day MA",
line=dict(color="yellow", dash="dashdot", width=2),
opacity=0.8
))
fill_colors_map = {
2: "rgba(0, 0, 255, 0.2)",
5: "rgba(255, 0, 0, 0.2)",
10: "rgba(0, 128, 0, 0.2)",
20: "rgba(255, 165, 0, 0.2)",
60: "rgba(128, 0, 128, 0.2)",
}
for q_ in holding_periods:
add_significant_shades(fig1, df_pval[f"PVal_{q_}"], fill_colors_map[q_])
fig1.update_layout(
title=f"{ticker} Price with Averages (Shading = p < 0.05 for VR)",
yaxis_title="Price",
**common_layout
)
# ---- Figure 2 ----
fig2 = go.Figure()
colors_map = {2: "blue", 5: "red", 10: "green", 20: "orange", 60: "purple"}
for q_ in holding_periods:
fig2.add_trace(go.Scatter(
x=df_vr.index, y=df_vr[f"VR_{q_}"],
mode="lines", name=f"VR {q_}-Day",
line=dict(color=colors_map[q_])
))
fig2.add_shape(
type="line",
x0=df_vr.index.min(), x1=df_vr.index.max(),
y0=1, y1=1,
line=dict(color="white", dash="dash", width=1)
)
momentum_upper = df_vr.max().max() + 0.2
momentum_lower = 1.05
meanrev_lower = df_vr.min().min() - 0.2
meanrev_upper = 0.95
x_fill = [
df_vr.index.min(),
df_vr.index.max(),
df_vr.index.max(),
df_vr.index.min()
]
y_fill_mom = [momentum_lower, momentum_lower, momentum_upper, momentum_upper]
y_fill_mean = [meanrev_lower, meanrev_lower, meanrev_upper, meanrev_upper]
fig2.add_trace(go.Scatter(
x=x_fill, y=y_fill_mom, fill="toself",
mode="lines", line=dict(color="rgba(0,0,0,0)"),
fillcolor="rgba(0,255,0,0.2)", name="Momentum Zone",
hoverinfo="skip"
))
fig2.add_trace(go.Scatter(
x=x_fill, y=y_fill_mean, fill="toself",
mode="lines", line=dict(color="rgba(0,0,0,0)"),
fillcolor="rgba(255,0,0,0.2)", name="Mean-Reversion Zone",
hoverinfo="skip"
))
x_mid = df_vr.index[len(df_vr) // 2]
fig2.add_annotation(
x=x_mid, y=(momentum_lower + momentum_upper) / 2,
text="Momentum", showarrow=False,
font=dict(color="white", size=14)
)
fig2.add_annotation(
x=x_mid, y=(meanrev_lower + meanrev_upper) / 2,
text="Mean-Reversion", showarrow=False,
font=dict(color="white", size=14)
)
fig2.update_layout(
title=f"Rolling Variance Ratio for {ticker}",
yaxis_title="Variance Ratio",
**common_layout
)
# ---- Figure 3 ----
fig3 = go.Figure()
for q_ in holding_periods:
fig3.add_trace(go.Scatter(
x=df_pval.index, y=df_pval[f"PVal_{q_}"],
mode="lines", name=f"P-value {q_}-Day",
line=dict(color=colors_map[q_])
))
fig3.add_shape(
type="line",
x0=df_pval.index.min(), x1=df_pval.index.max(),
y0=0.05, y1=0.05,
line=dict(color="white", dash="dash", width=1)
)
y_fill_signif = [0, 0, 0.05, 0.05]
y_fill_notsignif = [0.05, 0.05, 1, 1]
fig3.add_trace(go.Scatter(
x=x_fill, y=y_fill_signif, fill="toself",
mode="lines", line=dict(color="rgba(0,0,0,0)"),
fillcolor="rgba(255,0,0,0.3)", name="Significant (p < 0.05)",
hoverinfo="skip"
))
fig3.add_trace(go.Scatter(
x=x_fill, y=y_fill_notsignif, fill="toself",
mode="lines", line=dict(color="rgba(0,0,0,0)"),
fillcolor="rgba(128,128,128,0.1)", name="Not Significant",
hoverinfo="skip"
))
fig3.update_layout(
title="Rolling P-values",
yaxis_title="P-value",
**common_layout
)
# ---- Figure 4 ----
fig4 = go.Figure()
fig4.add_trace(go.Scatter(
x=daily_roll_ac.index, y=daily_roll_ac,
mode="lines", name="Daily (Lag 1)",
line=dict(color="cyan", width=2)
))
fig4.add_trace(go.Scatter(
x=weekly_roll_ac.index, y=weekly_roll_ac,
mode="lines", name="Weekly (Lag 1)",
line=dict(color="orange", width=2)
))
fig4.add_trace(go.Scatter(
x=monthly_roll_ac.index, y=monthly_roll_ac,
mode="lines", name="Monthly (Lag 1)",
line=dict(color="lime", width=2)
))
fig4.add_shape(
type="line",
x0=daily_roll_ac.index.min(), x1=daily_roll_ac.index.max(),
y0=threshold_daily, y1=threshold_daily,
line=dict(color="cyan", dash="dash", width=1)
)
fig4.add_shape(
type="line",
x0=daily_roll_ac.index.min(), x1=daily_roll_ac.index.max(),
y0=-threshold_daily, y1=-threshold_daily,
line=dict(color="cyan", dash="dash", width=1)
)
fig4.add_shape(
type="line",
x0=weekly_roll_ac.index.min(), x1=weekly_roll_ac.index.max(),
y0=threshold_weekly, y1=threshold_weekly,
line=dict(color="orange", dash="dash", width=1)
)
fig4.add_shape(
type="line",
x0=weekly_roll_ac.index.min(), x1=weekly_roll_ac.index.max(),
y0=-threshold_weekly, y1=-threshold_weekly,
line=dict(color="orange", dash="dash", width=1)
)
fig4.add_shape(
type="line",
x0=monthly_roll_ac.index.min(), x1=monthly_roll_ac.index.max(),
y0=threshold_monthly, y1=threshold_monthly,
line=dict(color="lime", dash="dash", width=1)
)
fig4.add_shape(
type="line",
x0=monthly_roll_ac.index.min(), x1=monthly_roll_ac.index.max(),
y0=-threshold_monthly, y1=-threshold_monthly,
line=dict(color="lime", dash="dash", width=1)
)
x_mid_ac = daily_roll_ac.index[len(daily_roll_ac) // 2]
y_pos_momentum_ac = max(threshold_daily, threshold_weekly, threshold_monthly) - 0.07
y_pos_reversion_ac = min(-threshold_daily, -threshold_weekly, -threshold_monthly) - 0.06
fig4.add_annotation(
x=x_mid_ac, y=y_pos_momentum_ac,
text="Momentum", showarrow=False,
font=dict(color="white", size=14)
)
fig4.add_annotation(
x=x_mid_ac, y=y_pos_reversion_ac,
text="Mean-Reversion", showarrow=False,
font=dict(color="white", size=14)
)
fig4.update_layout(
title=f"Rolling Autocorrelation at Lag {lag_val} for {ticker}",
yaxis_title="Autocorrelation",
**common_layout
)
# ---- Figure 5 ----
fig5 = go.Figure()
fig5.add_trace(go.Scatter(
x=daily_roll_avg_ac.index, y=daily_roll_avg_ac,
mode="lines", name=f"Daily Avg (Lags 1-{max_lag})",
line=dict(color="cyan", width=2)
))
fig5.add_trace(go.Scatter(
x=weekly_roll_avg_ac.index, y=weekly_roll_avg_ac,
mode="lines", name=f"Weekly Avg (Lags 1-{max_lag})",
line=dict(color="orange", width=2)
))
fig5.add_trace(go.Scatter(
x=monthly_roll_avg_ac.index, y=monthly_roll_avg_ac,
mode="lines", name=f"Monthly Avg (Lags 1-{max_lag})",
line=dict(color="lime", width=2)
))
fig5.add_shape(
type="line",
x0=daily_roll_avg_ac.index.min(), x1=daily_roll_avg_ac.index.max(),
y0=threshold_daily_avg, y1=threshold_daily_avg,
line=dict(color="cyan", dash="dash", width=1)
)
fig5.add_shape(
type="line",
x0=daily_roll_avg_ac.index.min(), x1=daily_roll_avg_ac.index.max(),
y0=-threshold_daily_avg, y1=-threshold_daily_avg,
line=dict(color="cyan", dash="dash", width=1)
)
fig5.add_shape(
type="line",
x0=weekly_roll_avg_ac.index.min(), x1=weekly_roll_avg_ac.index.max(),
y0=threshold_weekly_avg, y1=threshold_weekly_avg,
line=dict(color="orange", dash="dash", width=1)
)
fig5.add_shape(
type="line",
x0=weekly_roll_avg_ac.index.min(), x1=weekly_roll_avg_ac.index.max(),
y0=-threshold_weekly_avg, y1=-threshold_weekly_avg,
line=dict(color="orange", dash="dash", width=1)
)
fig5.add_shape(
type="line",
x0=monthly_roll_avg_ac.index.min(), x1=monthly_roll_avg_ac.index.max(),
y0=threshold_monthly_avg, y1=threshold_monthly_avg,
line=dict(color="lime", dash="dash", width=1)
)
fig5.add_shape(
type="line",
x0=monthly_roll_avg_ac.index.min(), x1=monthly_roll_avg_ac.index.max(),
y0=-threshold_monthly_avg, y1=-threshold_monthly_avg,
line=dict(color="lime", dash="dash", width=1)
)
fig5.add_annotation(
x=x_mid_ac, y=y_pos_momentum_ac,
text="Momentum", showarrow=False,
font=dict(color="white", size=14)
)
fig5.add_annotation(
x=x_mid_ac, y=y_pos_reversion_ac,
text="Mean-Reversion", showarrow=False,
font=dict(color="white", size=14)
)
fig5.update_layout(
title=f"Rolling Average Autocorrelation (Lags 1 to {max_lag}) for {ticker}",
yaxis_title="Average Autocorrelation",
xaxis_title="Date",
**common_layout
)
return [fig1, fig2, fig3, fig4, fig5]
# =====================================================================
# MAIN LOGIC
# =====================================================================
params = dict(
ticker=ticker,
start_date=start_date,
end_date=end_date,
rolling_window=rolling_window,
rolling_window_daily=rolling_window_daily,
rolling_window_weekly=rolling_window_weekly,
rolling_window_monthly=rolling_window_monthly,
max_lag=max_lag,
lag_val=lag_val
)
if "params" not in st.session_state:
st.session_state.params = None
if st.session_state.params != params:
st.session_state.params = params
st.session_state.market_efficiency_fig = None
st.session_state.mean_momentum_figs = None
st.session_state.df_main = None
st.session_state.df_daily = None
st.session_state.df_weekly = None
st.session_state.df_monthly = None
if run_button:
with st.spinner("Running analysis. Please wait..."):
progress_bar = st.progress(0)
# 1) Load daily data for Market Efficiency
df_main = load_data(ticker, start_date, end_date)
st.session_state.df_main = df_main
# 2) Market Efficiency figure
if df_main.empty or len(df_main) < rolling_window:
st.error("Not enough data for the chosen parameters.")
st.stop()
st.session_state.market_efficiency_fig = compute_market_efficiency(df_main.copy(), rolling_window)
progress_bar.progress(40)
# 3) Load daily/weekly/monthly data for Mean vs Momentum
daily_data = load_data_interval(ticker, start_date, end_date, "1d")
weekly_data = load_data_interval(ticker, start_date, end_date, "1wk")
monthly_data = load_data_interval(ticker, start_date, end_date, "1mo")
st.session_state.df_daily = daily_data
st.session_state.df_weekly = weekly_data
st.session_state.df_monthly = monthly_data
if daily_data.empty or weekly_data.empty or monthly_data.empty:
st.error("Could not load daily/weekly/monthly data with these parameters.")
st.stop()
daily_data["MA200"] = daily_data["Close"].rolling(window=200).mean()
daily_data["MA7"] = daily_data["Close"].rolling(window=7).mean()
daily_data["MA30"] = daily_data["Close"].rolling(window=30).mean()
for df_ in [daily_data, weekly_data, monthly_data]:
df_.dropna(inplace=True)
df_["Returns"] = df_["Close"].pct_change()
df_.dropna(inplace=True)
st.session_state.mean_momentum_figs = compute_mean_vs_momentum(
daily_data,
weekly_data,
monthly_data,
ticker,
rolling_window_daily,
rolling_window_weekly,
rolling_window_monthly,
max_lag,
lag_val
)
progress_bar.progress(100)
st.success("Analysis complete.")
if page_selection == "Market Efficiency":
if st.session_state.get("market_efficiency_fig") is None:
st.warning("Press 'Run Analysis' to generate results.")
st.stop()
st.subheader("Market Efficiency")
st.write(
"This page evaluates market efficiency through statistical tests. "
"The top panel displays price and moving averages with shading to indicate intervals flagged by the Runs and ADF tests. "
"The middle panel shows the rolling Runs test p-values, while the bottom panel presents the rolling ADF test p-values."
"Red shading indicates periods where the Runs test flags non-random returns. orange shading highlights intervals where the ADF test indicate stationarity."
"For further details on the methodology, please see [this article](https://entreprenerdly.com/the-market-isnt-always-random-spot-it-with-return-direction-tests/)."
)
with st.expander("Theory and Methodology", expanded=False):
st.markdown("##### Rolling Runs Test Analysis")
st.write(
"We use a 60-day rolling window to check if daily returns behave randomly. "
"Returns are computed from closing prices and converted to binary signals (1 for positive, 0 otherwise). "
"We count the number of runs (consecutive similar signals) as:"
)
st.latex(r"runs = 1 + \sum_{t=2}^{n} I(sign_t \neq sign_{t-1})")
st.write("The expected number of runs (μ) and the standard deviation (σ) are computed as:")
st.latex(r"\mu = 1 + \frac{2 n_1 n_2}{n_1+n_2}")
st.latex(r"\sigma = \sqrt{\frac{2 n_1 n_2 (2 n_1 n_2 - n_1 - n_2)}{(n_1+n_2)^2(n_1+n_2-1)}}")
st.write(
"Using these, a Z statistic and corresponding p-value are calculated. "
"A p-value below 0.05 flags the window as non-random."
)
st.markdown("##### Rolling ADF Test Analysis")
st.write(
"We also test for stationarity using the Augmented Dickey-Fuller test over a 60-day rolling window. "
"The estimated model is:"
)
st.latex(r"\Delta y_t = \alpha + \beta t + \gamma y_{t-1} + \sum_{i=1}^{p}\delta_i \Delta y_{t-i} + \epsilon_t")
st.write(
"A p-value below 0.05 indicates that the series is stationary, and those intervals are highlighted on the chart."
)
st.plotly_chart(st.session_state["market_efficiency_fig"], use_container_width=True)
else: # "Mean vs Momentum"
if st.session_state.get("mean_momentum_figs") is None:
st.warning("Press 'Run Analysis' to generate results.")
st.stop()
fig1, fig2, fig3, fig4, fig5 = st.session_state["mean_momentum_figs"]
st.subheader("Mean-reversion vs Momentum.")
st.write(
"This tool identifies momentum and mean reversion zones by calculating variance ratios and rolling autocorrelation. Variance ratios compare the variability of aggregated returns with daily returns, while rolling autocorrelation measures short-term return dependencies."
"Shading marks intervals where variance ratio p-values fall below 0.05 to pinpoint statistically significant zones of momentum or mean reversion."
"For further details on the methodology, please see [this article](https://entreprenerdly.com/momentum-or-reversion-detecting-predictability-zones/)."
)
with st.expander("Theory and Methodology", expanded=False):
st.markdown("##### 1. Variance Ratio Test Over Time")
st.write(
"The variance ratio test estimates whether asset returns follow a random walk or display momentum/mean reversion. "
"This implementation uses a 252-day rolling window to track changes in market efficiency over time. "
"For each date and holding period $q$, it calculates:"
)
st.latex(r"VR(q) = \frac{\operatorname{Var}\left(\sum_{i=1}^{q} r_{t+i}\right)}{q \cdot \operatorname{Var}(r_t)}")
st.write(
"Here, $r_t$ is the daily return and $\\sum_{i=1}^{q} r_{t+i}$ is the cumulative return over a $q$-day period. "
"Under the random walk hypothesis, $VR(q) \\approx 1$. A VR above 1 suggests momentum, while below 1 indicates mean reversion. "
"A Z-score is computed as:"
)
st.latex(r"Z = \frac{VR(q) - 1}{\sqrt{\frac{2(2q - 1)(q - 1)}{3qT}}}")
st.write(
"where $T$ is the window length. The corresponding p-values help filter out random noise. "
"The holding period $q$ represents the number of days over which returns are aggregated."
)
st.markdown("##### 2. Autocorrelation Over Time")
st.write(
"This analysis tracks short-term memory in asset returns using rolling autocorrelation at daily, weekly, and monthly frequencies. "
"Autocorrelation at lag $k$, denoted $\\rho_k$, measures the correlation between $r_t$ and $r_{t-k}$. "
"A significantly positive $\\rho_k$ implies trend persistence (momentum), while a significantly negative $\\rho_k$ suggests reversal (mean reversion). "
"To compute this over time, the returns are processed using a rolling window:"
)
st.latex(r"\hat{\rho}_k = \operatorname{Corr}(r_t, r_{t-k})")
st.write(
"Two views are computed: "
"1. Fixed-lag autocorrelation ($k=1$), which measures the correlation between returns and their immediate lag, and "
"2. Average autocorrelation across lags 1 to $k$, defined as:"
)
st.latex(r"\bar{\rho} = \frac{1}{k} \sum_{i=1}^{k} \hat{\rho}_i")
st.write(
"Statistical thresholds of $\\pm \\frac{2}{\\sqrt{T}}$ are drawn to identify significant deviations from zero, "
"where $T$ is the window size. This helps reveal when return dynamics deviate from randomness."
)
st.plotly_chart(fig1, use_container_width=True)
st.plotly_chart(fig2, use_container_width=True)
st.plotly_chart(fig3, use_container_width=True)
st.plotly_chart(fig4, use_container_width=True)
st.plotly_chart(fig5, use_container_width=True)
# Hide default Streamlit style
st.markdown(
"""
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>
""",
unsafe_allow_html=True
)