QuantumLearner commited on
Commit
7ed431a
·
verified ·
1 Parent(s): 669e2e9

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +377 -0
app.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import yfinance as yf
3
+ import numpy as np
4
+ import pandas as pd
5
+ from math import ceil
6
+ from datetime import datetime, timedelta
7
+ import plotly.graph_objects as go
8
+ from plotly.subplots import make_subplots
9
+ from pandas.tseries.offsets import BDay
10
+
11
+ st.set_page_config(page_title="Autocorrelation Periodogram", layout="wide")
12
+
13
+ @st.cache_data(show_spinner=False)
14
+ def run_analysis(ticker, start_date, end_date, length, max_lag,
15
+ lags_per_plot, plot_start_lag, plot_end_lag, data_type):
16
+ df = yf.download(ticker, start=start_date, end=end_date,
17
+ interval="1d", auto_adjust=True)
18
+ if df.empty:
19
+ return None, "No data available for the given inputs."
20
+
21
+ if isinstance(df.columns, pd.MultiIndex):
22
+ df.columns = df.columns.get_level_values(0)
23
+ else:
24
+ df.columns = [c.split("_")[0] for c in df.columns]
25
+
26
+ def ultimate_smoother(src, period):
27
+ a1 = np.exp(-1.414 * np.pi / period)
28
+ c2 = 2.0 * a1 * np.cos(1.414 * np.pi / period)
29
+ c3 = -a1 * a1
30
+ c1 = (1.0 + c2 - c3) / 4.0
31
+ n = len(src)
32
+ out = np.copy(src).astype(float)
33
+ for i in range(3, n):
34
+ out[i] = ((1.0 - c1) * src[i]
35
+ + (2.0 * c1 - c2) * src[i-1]
36
+ - (c1 + c3) * src[i-2]
37
+ + c2 * out[i-1]
38
+ + c3 * out[i-2])
39
+ return out
40
+
41
+ if data_type == "prices":
42
+ raw_series = df["Close"].values
43
+ data_series = ultimate_smoother(raw_series, length)
44
+ elif data_type == "returns":
45
+ prices = df["Close"].values
46
+ log_prices = np.log(prices)
47
+ data_series = np.diff(log_prices, prepend=np.nan)
48
+ data_series[0] = 0.0
49
+ elif data_type == "volatility":
50
+ prices = df["Close"].values
51
+ log_prices = np.log(prices)
52
+ returns = np.diff(log_prices, prepend=np.nan)
53
+ returns[0] = 0.0
54
+ vol_series = pd.Series(returns).rolling(window=length).std().to_numpy()
55
+ vol_series[:length-1] = 0.0
56
+ data_series = vol_series
57
+ else:
58
+ return None, "Invalid data type."
59
+
60
+ def compute_autocorrelation(series, window_length, max_lag):
61
+ n = len(series)
62
+ corrs = np.full((n, max_lag+1), np.nan, dtype=float)
63
+ for i in range(window_length - 1, n):
64
+ window = series[i - window_length + 1 : i + 1]
65
+ sum_x = np.sum(window)
66
+ sum_xx = np.sum(window * window)
67
+ for L in range(max_lag + 1):
68
+ start_lag = i - window_length - L + 1
69
+ end_lag = i - L + 1
70
+ if start_lag < 0:
71
+ continue
72
+ window_lag = series[start_lag : end_lag]
73
+ if len(window_lag) != window_length:
74
+ continue
75
+ sum_y = np.sum(window_lag)
76
+ sum_yy = np.sum(window_lag * window_lag)
77
+ sum_xy = np.sum(window * window_lag)
78
+ denom_x = window_length * sum_xx - sum_x * sum_x
79
+ denom_y = window_length * sum_yy - sum_y * sum_y
80
+ if denom_x > 0 and denom_y > 0:
81
+ numer = window_length * sum_xy - sum_x * sum_y
82
+ corrs[i, L] = numer / np.sqrt(denom_x * denom_y)
83
+ return corrs
84
+
85
+ corrs = compute_autocorrelation(data_series, length, max_lag)
86
+ dates = df.index.to_pydatetime()
87
+
88
+ def slice_corr(corr_matrix, lag_start, lag_end):
89
+ subset = corr_matrix[:, lag_start : lag_end + 1]
90
+ return subset.T
91
+
92
+ plot_range = plot_end_lag - plot_start_lag + 1
93
+ n_plots = ceil(plot_range / lags_per_plot)
94
+ bucket_slices = []
95
+ for i in range(n_plots):
96
+ ls = plot_start_lag + i * lags_per_plot
97
+ le = min(plot_start_lag + (i+1) * lags_per_plot - 1, plot_end_lag)
98
+ subset = slice_corr(corrs, ls, le)
99
+ bucket_slices.append((ls, le, subset))
100
+
101
+ colorscale = [[0.0, 'red'], [0.5, 'yellow'], [1.0, 'green']]
102
+ total_rows = 1 + len(bucket_slices)
103
+ subplot_titles = [""]
104
+ for (ls, le, _) in bucket_slices:
105
+ subplot_titles.append(f"ACI {ls}–{le}")
106
+
107
+ fig = make_subplots(
108
+ rows=total_rows, cols=1,
109
+ shared_xaxes=True,
110
+ row_heights=[2] + [1]*len(bucket_slices),
111
+ vertical_spacing=0.03,
112
+ subplot_titles=subplot_titles
113
+ )
114
+
115
+ if data_type == "prices":
116
+ fig.add_trace(
117
+ go.Scatter(
118
+ x=dates,
119
+ y=df["Close"],
120
+ mode='lines',
121
+ line=dict(width=1.2),
122
+ name="Close Price"
123
+ ),
124
+ row=1, col=1
125
+ )
126
+ fig.add_trace(
127
+ go.Scatter(
128
+ x=dates,
129
+ y=data_series,
130
+ mode='lines',
131
+ line=dict(width=1.2),
132
+ name="Smoothed Price"
133
+ ),
134
+ row=1, col=1
135
+ )
136
+ elif data_type == "returns":
137
+ fig.add_trace(
138
+ go.Scatter(
139
+ x=dates,
140
+ y=data_series,
141
+ mode='lines',
142
+ line=dict(width=1.2),
143
+ name="Log Returns"
144
+ ),
145
+ row=1, col=1
146
+ )
147
+ elif data_type == "volatility":
148
+ fig.add_trace(
149
+ go.Scatter(
150
+ x=dates,
151
+ y=data_series,
152
+ mode='lines',
153
+ line=dict(width=1.2),
154
+ name="Rolling Volatility"
155
+ ),
156
+ row=1, col=1
157
+ )
158
+
159
+ for idx, (ls, le, subset) in enumerate(bucket_slices):
160
+ row_index = idx + 2
161
+ show_colorbar = (idx == len(bucket_slices) - 1)
162
+ heatmap = go.Heatmap(
163
+ x=dates,
164
+ y=list(range(ls, le + 1)),
165
+ z=subset,
166
+ colorscale=colorscale,
167
+ zmin=-1,
168
+ zmax=1,
169
+ showscale=show_colorbar,
170
+ colorbar=dict(title="Correlation") if show_colorbar else None
171
+ )
172
+ fig.add_trace(heatmap, row=row_index, col=1)
173
+
174
+ latest_date = pd.Timestamp(df.index[-1])
175
+ for idx, (ls, le, _) in enumerate(bucket_slices):
176
+ row_number = idx + 2
177
+ tickvals = list(range(ls, le + 1))
178
+ ticktext = [f"{lag} ({(latest_date - BDay(lag)).strftime('%Y-%m-%d')})"
179
+ for lag in tickvals]
180
+ fig.update_yaxes(
181
+ tickmode='array',
182
+ tickvals=tickvals,
183
+ ticktext=ticktext,
184
+ row=row_number,
185
+ tickfont=dict(size=8), #color="white",
186
+ col=1
187
+ )
188
+
189
+ fig.update_layout(
190
+ template="plotly_dark",
191
+ title=dict(text=f"Autocorrelation Indicator - {ticker} - {data_type.capitalize()}"),
192
+ height=800 + 200 * len(bucket_slices),
193
+ width=1600,
194
+ legend=dict(
195
+ orientation="h",
196
+ yanchor="bottom",
197
+ y=1.05,
198
+ xanchor="center",
199
+ x=0.5
200
+ )
201
+ )
202
+ fig.update_xaxes(
203
+ type="date",
204
+ tickangle=45,
205
+ tickformat="%Y-%m-%d"
206
+ )
207
+
208
+ return {"df": df,
209
+ "data_series": data_series,
210
+ "corrs": corrs,
211
+ "dates": dates,
212
+ "bucket_slices": bucket_slices,
213
+ "fig": fig}, None
214
+
215
+ # Initialize session state for results.
216
+ if "results" not in st.session_state:
217
+ st.session_state.results = {}
218
+
219
+ # Top radio for page selection.
220
+ current_page = st.sidebar.radio("Select Page",
221
+ options=["Prices", "Returns", "Volatility"],
222
+ help="Choose analysis type.")
223
+
224
+ st.sidebar.header("User Inputs")
225
+
226
+ with st.sidebar.expander("Data Inputs", expanded=True):
227
+ ticker = st.text_input("Ticker", value="SPY", help="Enter the ticker symbol.")
228
+ start_date = st.date_input("Start Date", value=datetime(2020, 1, 1),
229
+ help="Set the start date for daily data.")
230
+ default_end_date = datetime.today() + timedelta(days=1)
231
+ end_date = st.date_input("End Date", value=default_end_date,
232
+ help="Set the end date for daily data.")
233
+
234
+ with st.sidebar.expander("Methodology Parameters", expanded=True):
235
+ length = st.number_input(
236
+ "Window Size", value=20, min_value=1,
237
+ help="Controls how many days are used when comparing current vs past segments. Also used for smoothing (Prices) and rolling window in volatility."
238
+ )
239
+
240
+ lags_per_plot = st.number_input(
241
+ "Lags per Plot", value=32, min_value=1,
242
+ help="How many lag rows to include in each heatmap panel."
243
+ )
244
+ plot_start_lag = st.number_input(
245
+ "Plot Start Lag", value=30, min_value=0,
246
+ help="Lower bound of lag range to visualize. Set this to skip very short lags."
247
+ )
248
+ plot_end_lag = st.number_input(
249
+ "Plot End Lag", value=120, min_value=0,
250
+ help="Upper bound of lag range to visualize. The tool will measure similarity with up to this many days in the past."
251
+ )
252
+
253
+ max_lag = plot_end_lag
254
+
255
+
256
+ # Run Analysis button.
257
+ if st.sidebar.button("Run Analysis"):
258
+ st.session_state.ticker = ticker
259
+ st.session_state.start_date = start_date
260
+ st.session_state.end_date = end_date
261
+ st.session_state.length = length
262
+ st.session_state.max_lag = max_lag
263
+ st.session_state.lags_per_plot = lags_per_plot
264
+ st.session_state.plot_start_lag = plot_start_lag
265
+ st.session_state.plot_end_lag = plot_end_lag
266
+ st.session_state.page = current_page
267
+
268
+ with st.spinner("Running analysis..."):
269
+ results, error = run_analysis(
270
+ ticker,
271
+ start_date,
272
+ end_date,
273
+ length,
274
+ max_lag,
275
+ lags_per_plot,
276
+ plot_start_lag,
277
+ plot_end_lag,
278
+ current_page.lower()
279
+ )
280
+ st.session_state.results[current_page] = (results, error)
281
+
282
+ # Always show the main title and description
283
+ # Always show the main title and intro
284
+ st.title("Autocorrelation Periodogram")
285
+ st.markdown(
286
+ "This tool visualizes how market structure repeats across time by computing rolling autocorrelations over many lags.\n\n"
287
+ "You can analyze **Prices**, **Returns**, or **Volatility**. The heatmaps show how much today’s behavior resembles the past at different time horizons."
288
+ )
289
+
290
+ # Methodology expander with math
291
+ with st.expander("Methodology", expanded=False):
292
+ st.markdown("""
293
+ **Purpose**
294
+
295
+ Measure how similar the current behavior is to past behavior over multiple lags to detect persistence or reversion in structure.
296
+
297
+ **Autocorrelation formula**:
298
+ """)
299
+ st.latex(r"""
300
+ \rho_{t, L} = \frac{\sum_{i=0}^{N-1}(x_{t-i} - \bar{x})(x_{t-L-i} - \bar{y})}
301
+ {\sqrt{\sum_{i=0}^{N-1}(x_{t-i} - \bar{x})^2} \cdot
302
+ \sqrt{\sum_{i=0}^{N-1}(x_{t-L-i} - \bar{y})^2}}
303
+ """)
304
+ st.markdown("""
305
+ - \( x \): current window
306
+ - \( y \): lagged window shifted by \( L \) days
307
+ - \( N \): window size (set via **Window Size**)
308
+ - \( L \): lag (from 0 to **Max Lag**)
309
+
310
+ **Inputs** (configured in sidebar):
311
+ - **Window Size**: used for autocorrelation and volatility. Also used for smoothing in *Prices* mode.
312
+ - **Max Lag**: upper bound on lag values to compute.
313
+ - **Lags per Plot**: number of lag rows per heatmap.
314
+ - **Plot Start / End Lag**: limits for lags to visualize.
315
+
316
+ **Output**
317
+
318
+ The app displays:
319
+ - A top panel with the selected series.
320
+ - One or more heatmaps below showing autocorrelation across lag ranges.
321
+ - Color scale: green = positive correlation (momentum), red = negative correlation (mean reversion), yellow = no structure.
322
+ """)
323
+
324
+ # Show analysis results (if any)
325
+ if current_page in st.session_state.results:
326
+ results, error = st.session_state.results[current_page]
327
+ st.markdown(f"### {current_page} Analysis")
328
+
329
+ if error:
330
+ st.error(error)
331
+ else:
332
+ lag_start = st.session_state.plot_start_lag
333
+ lag_end = st.session_state.plot_end_lag
334
+ lags_per_plot = st.session_state.lags_per_plot
335
+ n_panels = ceil((lag_end - lag_start + 1) / lags_per_plot)
336
+
337
+ if current_page.lower() == "prices":
338
+ st.markdown(f"""
339
+ **Input type**: Closing prices (smoothed with Ehlers' filter)
340
+ **Top panel**: Raw close vs smoothed price
341
+ **Lower panels**: Autocorrelation of smoothed prices across {n_panels} lag bands
342
+ **Lag range**: {lag_start} to {lag_end}
343
+ **Window size**: {st.session_state.length}
344
+ """)
345
+ elif current_page.lower() == "returns":
346
+ st.markdown(f"""
347
+ **Input type**: Log returns
348
+ **Top panel**: Daily log returns
349
+ **Lower panels**: Autocorrelation of returns across {n_panels} lag bands
350
+ **Lag range**: {lag_start} to {lag_end}
351
+ **Window size**: {st.session_state.length}
352
+ """)
353
+ elif current_page.lower() == "volatility":
354
+ st.markdown(f"""
355
+ **Input type**: Rolling standard deviation of log returns
356
+ **Top panel**: Rolling volatility
357
+ **Lower panels**: Autocorrelation of volatility across {n_panels} lag bands
358
+ **Lag range**: {lag_start} to {lag_end}
359
+ **Window size**: {st.session_state.length}
360
+ """)
361
+
362
+ st.plotly_chart(results["fig"], use_container_width=True)
363
+
364
+ else:
365
+ #st.markdown("#### No analysis run yet")
366
+ st.info("Use the sidebar to set parameters and click **Run Analysis** to display results here.")
367
+
368
+ # Hide default Streamlit style
369
+ st.markdown(
370
+ """
371
+ <style>
372
+ #MainMenu {visibility: hidden;}
373
+ footer {visibility: hidden;}
374
+ </style>
375
+ """,
376
+ unsafe_allow_html=True
377
+ )