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