QuantumLearner commited on
Commit
c1fa6c7
·
verified ·
1 Parent(s): 6727092

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +378 -0
app.py ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import numpy as np
3
+ import pandas as pd
4
+ import plotly.graph_objects as go
5
+ from datetime import datetime, timedelta
6
+ import yfinance as yf
7
+
8
+ # Set wide page layout and page title
9
+ st.set_page_config(layout="wide", page_title="Self-Tuning SuperTrend and K-Means")
10
+
11
+ # App title and purpose explanation
12
+ st.title("Self-Tuning SuperTrend and K-Means")
13
+ st.write("This tool builds a self-tuning SuperTrend indicator using k-means clustering to adapt stop levels and generate buy/sell signals. It uses daily price data to compute volatility, score multiple configurations, and select the best one in real time.")
14
+
15
+ # Methodology expander (closed by default)
16
+ with st.expander("Methodology", expanded=False):
17
+ #st.write("The tool self-tunes the SuperTrend indicator by testing multiple configurations and picking the best one.")
18
+
19
+ #st.write("**Step 1: Data Preparation**")
20
+ #st.write("Daily price data is downloaded. Key columns (High, Low, Close) are retained and prepped.")
21
+
22
+ st.write("**Volatility Measurement**")
23
+ st.write("An Average True Range (ATR) is computed using an Exponential Moving Average (EMA).")
24
+ st.latex(r'\text{ATR} = \text{EMA}\left(\max\left\{High-Low,\ |High-\text{PrevClose}|,\ |Low-\text{PrevClose}|\right\}\right)')
25
+
26
+ st.write("**Generating SuperTrend Variants**")
27
+ st.write("Multiple SuperTrend signals are calculated. They use:")
28
+ st.latex(r'\text{Upper Band} = hl2 + ATR \times \text{factor}')
29
+ st.latex(r'\text{Lower Band} = hl2 - ATR \times \text{factor}')
30
+
31
+ st.write("**Performance Scoring & Clustering**")
32
+ st.write("Each variant is scored based on price movement. K-means (k=3) clusters these scores into Best, Average, and Worst groups.")
33
+
34
+ st.write("**Final Signal Generation**")
35
+ st.write("The indicator is recomputed using the average factor from the selected cluster. This gives a self-calibrated trading signal.")
36
+
37
+ st.write("This process minimizes manual tuning and adapts with recent price action.")
38
+
39
+ st.write("For more details, see the this article [here](https://entreprenerdly.com/trading-signals-with-adaptive-supertrend-and-k-means/).")
40
+
41
+ # Sidebar inputs explanation
42
+ st.write("#### Adjustable Inputs & Implications")
43
+ st.write("""
44
+ - **Ticker:** The stock symbol to analyze. Changing this lets you switch assets.
45
+ - **Start Date & End Date:** Define the analysis window. End Date defaults to today plus one day.
46
+ - **ATR Length:** Sets the period for ATR. Lower values react faster; higher values smooth out noise.
47
+ - **Minimum/Maximum Multipliers & Step:** Define the range for SuperTrend sensitivity. Smaller steps improve resolution but increase compute time.
48
+ - **Performance Alpha:** Determines the smoothness of the performance score. Lower makes the metric more reactive; higher favors stability.
49
+ - **K-Means Options (From Cluster & Max Iterations):** Choose which cluster (Best, Average, Worst) to use and limit iterations for clustering. This controls how variants are grouped.
50
+ """)
51
+
52
+ # Sidebar inputs
53
+ with st.sidebar:
54
+ st.header("Input Parameters")
55
+
56
+ # Data inputs expander
57
+ with st.expander("Data Inputs", expanded=True):
58
+ ticker = st.text_input(
59
+ "Ticker",
60
+ value="ASML",
61
+ help="Enter the stock symbol to analyze. Example: AAPL, MSFT, NVDA. This determines which asset's data will be used."
62
+ )
63
+ start_date = st.date_input(
64
+ "Start Date",
65
+ value=datetime(2022, 1, 1),
66
+ help="Start of the historical data window. Affects the amount of price history used to compute signals."
67
+ )
68
+ default_end_date = datetime.today() + timedelta(days=1)
69
+ end_date = st.date_input(
70
+ "End Date",
71
+ value=default_end_date,
72
+ help="End of the data window. Automatically set to today + 1 to include the most recent bar."
73
+ )
74
+
75
+ # Methodology parameters expander
76
+ with st.expander("Methodology Parameters", expanded=True):
77
+ atr_length = st.number_input(
78
+ "ATR Length",
79
+ min_value=1,
80
+ value=7,
81
+ step=1,
82
+ help="ATR period controls how volatility is measured. Lower values make the stop more sensitive to short-term moves. Higher values smooth noise but may react slower."
83
+ )
84
+ min_mult = st.number_input(
85
+ "Minimum Multiplier",
86
+ value=1.0,
87
+ step=0.1,
88
+ help="Defines the tightest SuperTrend stop. Lower values mean tighter stops, which react quickly but may whipsaw in choppy conditions."
89
+ )
90
+ max_mult = st.number_input(
91
+ "Maximum Multiplier",
92
+ value=5.0,
93
+ step=0.1,
94
+ help="Defines the widest SuperTrend stop. Higher values give more breathing room but may delay trend changes."
95
+ )
96
+ step_mult = st.number_input(
97
+ "Step",
98
+ value=0.5,
99
+ step=0.1,
100
+ help="Step size between multipliers. Smaller values give finer resolution but increase compute time."
101
+ )
102
+ perf_alpha = st.number_input(
103
+ "Performance Alpha",
104
+ min_value=1,
105
+ value=8,
106
+ step=1,
107
+ help="Controls how quickly the performance metric responds to new price behavior. Lower = more reactive, higher = more stable but slower to adapt."
108
+ )
109
+ from_cluster = st.selectbox(
110
+ "From Cluster",
111
+ options=["Best", "Average", "Worst"],
112
+ help="Selects which k-means cluster to use for final signal generation. 'Best' is typical for trend-following. 'Average' or 'Worst' can simulate conservative or contrarian behavior."
113
+ )
114
+ max_iter = st.number_input(
115
+ "Max Iterations",
116
+ min_value=1,
117
+ value=1000,
118
+ step=1,
119
+ help="Upper limit on how long k-means clustering can run. Higher values allow more precise convergence but slow down the run time."
120
+ )
121
+
122
+ # Action button
123
+ run_analysis = st.button("Run Analysis")
124
+
125
+ if run_analysis:
126
+ # Validate date input
127
+ if start_date >= end_date:
128
+ st.error("Start Date must be before End Date.")
129
+ else:
130
+ with st.spinner("Running analysis..."):
131
+ try:
132
+ # Convert dates to string format
133
+ start_date_str = start_date.strftime("%Y-%m-%d")
134
+ end_date_str = end_date.strftime("%Y-%m-%d")
135
+
136
+ # 1) Download data
137
+ df = yf.download(ticker, start=start_date_str, end=end_date_str, interval="1d", auto_adjust=False)
138
+ if df.empty:
139
+ st.error("No data returned from the data provider.")
140
+ st.stop()
141
+ if isinstance(df.columns, pd.MultiIndex):
142
+ df.columns = df.columns.get_level_values(0)
143
+ df.rename(columns={"Open": "Open", "High": "High", "Low": "Low", "Close": "Close", "Volume": "Volume"}, inplace=True)
144
+ df.dropna(subset=["High", "Low", "Close"], inplace=True)
145
+ df["hl2"] = (df["High"] + df["Low"]) / 2.0
146
+
147
+ # 2) Compute ATR
148
+ df["prev_close"] = df["Close"].shift(1)
149
+ df["tr1"] = df["High"] - df["Low"]
150
+ df["tr2"] = (df["High"] - df["prev_close"]).abs()
151
+ df["tr3"] = (df["Low"] - df["prev_close"]).abs()
152
+ df["tr"] = df[["tr1", "tr2", "tr3"]].max(axis=1)
153
+ df["atr"] = df["tr"].ewm(alpha=2/(atr_length+1), adjust=False).mean()
154
+ df.dropna(inplace=True)
155
+ df.reset_index(drop=False, inplace=True)
156
+ n = len(df)
157
+
158
+ # Helper function: sign
159
+ def sign(x):
160
+ return np.where(x > 0, 1, np.where(x < 0, -1, 0))
161
+
162
+ # 3) Compute supertrend for each factor
163
+ def compute_supertrend(df, factor, perf_alpha):
164
+ arr_close = df["Close"].values
165
+ arr_hl2 = df["hl2"].values
166
+ arr_atr = df["atr"].values
167
+
168
+ trend = np.zeros(n, dtype=int)
169
+ upper = np.zeros(n, dtype=float)
170
+ lower = np.zeros(n, dtype=float)
171
+ output = np.zeros(n, dtype=float)
172
+ perf = np.zeros(n, dtype=float)
173
+
174
+ trend[0] = 1 if arr_close[0] > arr_hl2[0] else 0
175
+ upper[0] = arr_hl2[0]
176
+ lower[0] = arr_hl2[0]
177
+ output[0] = arr_hl2[0]
178
+ perf[0] = 0.0
179
+
180
+ for i in range(1, n):
181
+ up = arr_hl2[i] + arr_atr[i] * factor
182
+ dn = arr_hl2[i] - arr_atr[i] * factor
183
+
184
+ if arr_close[i] > upper[i-1]:
185
+ trend[i] = 1
186
+ elif arr_close[i] < lower[i-1]:
187
+ trend[i] = 0
188
+ else:
189
+ trend[i] = trend[i-1]
190
+
191
+ if arr_close[i-1] < upper[i-1]:
192
+ upper[i] = min(up, upper[i-1])
193
+ else:
194
+ upper[i] = up
195
+
196
+ if arr_close[i-1] > lower[i-1]:
197
+ lower[i] = max(dn, lower[i-1])
198
+ else:
199
+ lower[i] = dn
200
+
201
+ diff_sign = sign(arr_close[i-1] - output[i-1])
202
+ perf[i] = perf[i-1] + 2/(perf_alpha+1)*((arr_close[i] - arr_close[i-1]) * diff_sign - perf[i-1])
203
+ output[i] = lower[i] if trend[i] == 1 else upper[i]
204
+
205
+ return {
206
+ "trend": trend,
207
+ "upper": upper,
208
+ "lower": lower,
209
+ "output": output,
210
+ "perf": perf,
211
+ "factor": factor
212
+ }
213
+
214
+ factors = np.arange(min_mult, max_mult + 0.0001, step_mult)
215
+ st_results = []
216
+ for f in factors:
217
+ st_results.append(compute_supertrend(df, f, perf_alpha))
218
+
219
+ perf_vals = np.array([res["perf"][-1] for res in st_results])
220
+ fact_vals = np.array([res["factor"] for res in st_results])
221
+
222
+ # 4) K-means clustering (k=3)
223
+ def k_means(data, factors, k=3, max_iter=max_iter):
224
+ c1, c2, c3 = np.percentile(data, [25, 50, 75])
225
+ centroids = np.array([c1, c2, c3])
226
+ for _ in range(max_iter):
227
+ clusters = {0: [], 1: [], 2: []}
228
+ cluster_factors = {0: [], 1: [], 2: []}
229
+ for d, f in zip(data, factors):
230
+ dist = np.abs(d - centroids)
231
+ idx = dist.argmin()
232
+ clusters[idx].append(d)
233
+ cluster_factors[idx].append(f)
234
+ new_centroids = np.array([np.mean(clusters[i]) if len(clusters[i]) > 0 else centroids[i] for i in range(3)])
235
+ if np.allclose(new_centroids, centroids):
236
+ break
237
+ centroids = new_centroids
238
+ return clusters, cluster_factors, centroids
239
+
240
+ clusters, cluster_factors, centroids = k_means(perf_vals, fact_vals, k=3, max_iter=max_iter)
241
+ order = np.argsort(centroids)
242
+ sorted_clusters = {i: clusters[j] for i, j in enumerate(order)}
243
+ sorted_cluster_factors = {i: cluster_factors[j] for i, j in enumerate(order)}
244
+ sorted_centroids = centroids[order]
245
+
246
+ if from_cluster == "Best":
247
+ chosen_index = 2
248
+ elif from_cluster == "Average":
249
+ chosen_index = 1
250
+ else:
251
+ chosen_index = 0
252
+
253
+ if len(sorted_cluster_factors[chosen_index]) > 0:
254
+ target_factor = np.mean(sorted_cluster_factors[chosen_index])
255
+ else:
256
+ target_factor = factors[-1]
257
+
258
+ if len(sorted_clusters[chosen_index]) > 0:
259
+ target_perf = np.mean(sorted_clusters[chosen_index])
260
+ else:
261
+ target_perf = 0.0
262
+
263
+ # 5) Recompute final supertrend with target_factor
264
+ st_final = compute_supertrend(df, target_factor, perf_alpha)
265
+ ts = st_final["output"]
266
+ os_arr = np.zeros(n, dtype=int)
267
+ os_arr[0] = 1 if df["Close"].iloc[0] > st_final["upper"][0] else 0
268
+
269
+ for i in range(1, n):
270
+ c = df["Close"].iloc[i]
271
+ up = st_final["upper"][i]
272
+ dn = st_final["lower"][i]
273
+ if c > up:
274
+ os_arr[i] = 1
275
+ elif c < dn:
276
+ os_arr[i] = 0
277
+ else:
278
+ os_arr[i] = os_arr[i-1]
279
+
280
+ # Build an adaptive MA for the trailing stop
281
+ den_close_diff = (df["Close"] - df["Close"].shift(1)).abs()
282
+ den = den_close_diff.ewm(alpha=2/(perf_alpha+1), adjust=False).mean()
283
+ den_val = den.iloc[-1] if den.iloc[-1] != 0 else 1e-9
284
+ perf_idx = max(target_perf, 0) / den_val
285
+
286
+ perf_ama = np.zeros(n, dtype=float)
287
+ perf_ama[0] = ts[0]
288
+ for i in range(1, n):
289
+ perf_ama[i] = perf_ama[i-1] + perf_idx * (ts[i] - perf_ama[i-1])
290
+
291
+ # 6) Build Plotly chart
292
+ fig = go.Figure()
293
+ fig.add_trace(go.Scatter(
294
+ x=df["Date"],
295
+ y=df["Close"],
296
+ mode="lines",
297
+ line=dict(color="silver", width=1.2),
298
+ name="Close Price"
299
+ ))
300
+
301
+ ts_bull = np.where(os_arr == 1, ts, np.nan)
302
+ ts_bear = np.where(os_arr == 0, ts, np.nan)
303
+
304
+ fig.add_trace(go.Scatter(
305
+ x=df["Date"],
306
+ y=ts_bull,
307
+ mode="lines",
308
+ line=dict(color="teal", width=1.2),
309
+ name="Bullish Stop"
310
+ ))
311
+ fig.add_trace(go.Scatter(
312
+ x=df["Date"],
313
+ y=ts_bear,
314
+ mode="lines",
315
+ line=dict(color="red", width=1.2),
316
+ name="Bearish Stop"
317
+ ))
318
+ fig.add_trace(go.Scatter(
319
+ x=df["Date"],
320
+ y=perf_ama,
321
+ mode="lines",
322
+ line=dict(color="orange", width=1.0),
323
+ opacity=0.7,
324
+ name="Trailing Stop AMA"
325
+ ))
326
+
327
+ for i in range(1, n):
328
+ if os_arr[i] != os_arr[i-1]:
329
+ if os_arr[i] == 1:
330
+ fig.add_trace(go.Scatter(
331
+ x=[df["Date"].iloc[i]],
332
+ y=[ts[i]],
333
+ mode="markers",
334
+ marker=dict(symbol="triangle-up", size=10, color="teal",
335
+ line=dict(color="white", width=1)),
336
+ name="Bullish Signal",
337
+ showlegend=False
338
+ ))
339
+ else:
340
+ fig.add_trace(go.Scatter(
341
+ x=[df["Date"].iloc[i]],
342
+ y=[ts[i]],
343
+ mode="markers",
344
+ marker=dict(symbol="triangle-down", size=10, color="red",
345
+ line=dict(color="white", width=1)),
346
+ name="Bearish Signal",
347
+ showlegend=False
348
+ ))
349
+
350
+ fig.update_layout(
351
+ title=f"SuperTrend (Clustering) - {ticker} [Factor ~ {target_factor:.2f}]",
352
+ xaxis_title="Date",
353
+ yaxis_title="Price",
354
+ template="plotly_dark",
355
+ width=1600,
356
+ height=800
357
+ )
358
+ fig.update_xaxes(tickformat='%Y-%m-%d')
359
+
360
+ # Display results
361
+ st.markdown("### Analysis Results")
362
+ st.write("The plot below shows the closing price with the SuperTrend indicators and signals.")
363
+ st.plotly_chart(fig, use_container_width=True)
364
+
365
+ except Exception as e:
366
+ st.error("An error occurred during the analysis.")
367
+ st.error(str(e))
368
+
369
+ # Hide default Streamlit style
370
+ st.markdown(
371
+ """
372
+ <style>
373
+ #MainMenu {visibility: hidden;}
374
+ footer {visibility: hidden;}
375
+ </style>
376
+ """,
377
+ unsafe_allow_html=True
378
+ )