QuantumLearner commited on
Commit
1b2c776
·
verified ·
1 Parent(s): fe1d6a2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +71 -33
app.py CHANGED
@@ -21,45 +21,51 @@ This tool backtests and optimizes a mean-reversion trading strategy. The idea be
21
 
22
  with st.expander("How the Strategy Works:", expanded=False):
23
  st.markdown('''
24
- 1. **Dynamic Window Sizes**: The strategy changes the window size it uses for calculating moving averages based on market volatility.
25
- 2. **Exponential Moving Average (EMA)**: Uses an EMA that gives more weight to recent prices.
26
- 3. **Trend Filter**: Ensures trades align with the overall trend.
27
- 4. **Buy/Sell Signals**:
28
- - **Buy Signal**: Generated when the price is below the EMA (with an adaptive threshold) and below the trend.
29
- - **Sell Signal**: Generated when the price is above the EMA and above the trend.
30
- 5. **Grid Search Optimization**: Tests different parameter combinations to find the best performance.
31
  ##### **What You Can Do:**
32
- - **Adjust Parameters**: Tweak the base window, alpha, beta, and signal threshold.
33
- - **Visual Feedback**: View buy/sell signals and the equity curve on a chart.
 
34
  ''')
35
 
 
36
  with st.sidebar.expander("How to Use", expanded=False):
37
  st.write("""
38
- 1. **Select Ticker**: Choose an asset symbol (e.g., AAPL, TSLA) and a date range.
39
- 2. **Run Strategy**: Click "Run Strategy" to optimize parameters and run the backtest.
40
- 3. **Adjust Parameters**: Use sliders to fine-tune the moving average window, beta, and signal threshold.
41
- 4. **Visualize**: The app displays buy/sell signals and the equity curve.
42
  """)
43
 
44
  st.sidebar.title("Input Parameters")
45
 
 
46
  with st.sidebar.expander("Asset Settings", expanded=True):
47
  ticker = st.text_input("Asset Symbol", value="AAPL", help="Ticker symbol or Cryptocurrency Pair (e.g., AAPL, BTC-USD)")
48
  start_date = st.date_input("Start Date", value=pd.to_datetime("2020-01-01"), help="Select the start date for historical data.")
49
  end_date = st.date_input("End Date", value=datetime.today() + pd.DateOffset(1), help="Select the end date for historical data.")
50
 
51
- # Modified get_data: squeeze the 'Close' column to ensure a 1D Series
52
  @st.cache_data
53
  def get_data(ticker, start, end):
54
  data = yf.download(ticker, start=start, end=end)
 
55
  return data['Close'].squeeze()
56
 
 
57
  def OU_parameters_ema(data, window):
58
  window = int(window) # ensure window is scalar
59
  mu = data.ewm(span=window).mean()
60
  sigma = data.ewm(span=window).std()
61
  return mu, sigma
62
 
 
63
  def dynamic_window(data, base_window=60, volatility_window=20):
64
  volatility = data.rolling(window=volatility_window).std()
65
  adjusted_window = base_window / (volatility / volatility.mean())
@@ -68,6 +74,7 @@ def dynamic_window(data, base_window=60, volatility_window=20):
68
  adjusted_window = adjusted_window.round().astype(int).clip(lower=20, upper=120)
69
  return adjusted_window
70
 
 
71
  def trading_strategy(data, base_window=60, base_alpha=1.0, beta=0.1, trend_window=200, signal_threshold=0):
72
  windows = dynamic_window(data, base_window=base_window)
73
  buy_signals = []
@@ -76,7 +83,7 @@ def trading_strategy(data, base_window=60, base_alpha=1.0, beta=0.1, trend_windo
76
  trend = data.rolling(window=trend_window).mean()
77
 
78
  for i in range(len(data)):
79
- # For early indices where the trend is not fully defined, skip trading logic.
80
  if i < trend_window - 1:
81
  buy_signals.append(np.nan)
82
  sell_signals.append(np.nan)
@@ -86,7 +93,7 @@ def trading_strategy(data, base_window=60, base_alpha=1.0, beta=0.1, trend_windo
86
  window = int(windows.iloc[i])
87
  mu, sigma = OU_parameters_ema(data[:i+1], window=window)
88
  alpha = base_alpha + beta * float(sigma.iloc[-1])
89
- # Force each value to be a float to ensure scalar comparisons:
90
  price = float(data.iloc[i])
91
  mu_value = float(mu.iloc[-1])
92
  sigma_value = float(sigma.iloc[-1])
@@ -107,14 +114,16 @@ def trading_strategy(data, base_window=60, base_alpha=1.0, beta=0.1, trend_windo
107
 
108
  return buy_signals, sell_signals, positions, trend
109
 
110
- # Modified calculate_performance: explicitly convert the price Series to a 1D NumPy array.
111
  def calculate_performance(data, positions):
112
- data_np = data.to_numpy() # ensure 1D array
 
113
  returns = np.diff(data_np) / data_np[:-1]
114
  strategy_returns = np.array(positions[:-1]) * returns
115
- equity_curve = np.cumprod(1 + strategy_returns) * 100 # starting equity of 100
116
  return equity_curve
117
 
 
118
  def grid_search(data, param_grid):
119
  best_params = None
120
  best_performance = -np.inf
@@ -130,9 +139,7 @@ def grid_search(data, param_grid):
130
  iteration += 1
131
  progress_bar.progress(iteration / total_iterations)
132
 
133
- buy_signals, sell_signals, positions, trend = trading_strategy(
134
- data, base_window=base_window, base_alpha=base_alpha, beta=beta, trend_window=trend_window
135
- )
136
  equity_curve = calculate_performance(data, np.array(positions))
137
  performance = equity_curve[-1]
138
  if performance > best_performance:
@@ -141,12 +148,14 @@ def grid_search(data, param_grid):
141
  best_positions = positions
142
  best_trend = trend
143
 
144
- progress_bar.empty()
145
  return best_params, best_performance, best_positions, best_trend
146
 
 
147
  run_button = st.sidebar.button("Run Strategy")
148
 
149
  if run_button:
 
150
  data = get_data(ticker, start_date, end_date)
151
  param_grid = {
152
  'base_window': [30, 50, 70, 90],
@@ -160,6 +169,7 @@ if run_button:
160
  st.session_state['best_positions'] = best_positions
161
  st.session_state['best_trend'] = best_trend
162
 
 
163
  st.json({
164
  "Best Parameters": {
165
  "Base Window": best_params[0],
@@ -169,19 +179,42 @@ if run_button:
169
  }
170
  })
171
 
 
172
  if 'best_params' in st.session_state:
 
173
  st.sidebar.subheader("Adjust Parameters")
174
- base_window = st.sidebar.slider("Base Window", 20, 120, st.session_state['best_params'][0],
175
- help="Adjust the base window size.")
176
- base_alpha = st.sidebar.slider("Base Alpha", 0.1, 2.0, st.session_state['best_params'][1], 0.1,
177
- help="Adjust the base alpha value.")
178
- beta = st.sidebar.slider("Beta", 0.01, 0.3, st.session_state['best_params'][2], 0.01,
179
- help="Adjust the beta value.")
180
- trend_window = st.sidebar.slider("Trend Window", 50, 400, st.session_state['best_params'][3],
181
- help="Adjust the trend window size.")
182
- signal_threshold = st.sidebar.slider("Signal Threshold", -0.2, 0.2, 0.0, 0.01,
183
- help="Adjust the signal threshold.")
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  buy_signals, sell_signals, positions, trend = trading_strategy(
186
  st.session_state['data'],
187
  base_window=base_window,
@@ -192,16 +225,21 @@ if 'best_params' in st.session_state:
192
  )
193
  equity_curve = calculate_performance(st.session_state['data'], positions)
194
 
 
195
  fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
196
  subplot_titles=("Price and Signals", "Equity Curve"),
197
  vertical_spacing=0.1)
198
 
 
199
  fig.add_trace(go.Scatter(x=st.session_state['data'].index, y=st.session_state['data'], mode='lines', name='Price'), row=1, col=1)
200
  fig.add_trace(go.Scatter(x=st.session_state['data'].index, y=trend, mode='lines', name=f"Trend Filter (SMA {trend_window})", line=dict(dash='dash')), row=1, col=1)
201
  fig.add_trace(go.Scatter(x=st.session_state['data'].index, y=buy_signals, mode='markers', name='Buy Signal', marker=dict(color='green', symbol='triangle-up', size=10)), row=1, col=1)
202
  fig.add_trace(go.Scatter(x=st.session_state['data'].index, y=sell_signals, mode='markers', name='Sell Signal', marker=dict(color='red', symbol='triangle-down', size=10)), row=1, col=1)
 
 
203
  fig.add_trace(go.Scatter(x=st.session_state['data'].index[1:], y=equity_curve, mode='lines', name='Equity Curve'), row=2, col=1)
204
 
 
205
  fig.update_layout(
206
  height=800,
207
  title=f'{ticker} Optimized Mean-Reversion Trading Strategy',
 
21
 
22
  with st.expander("How the Strategy Works:", expanded=False):
23
  st.markdown('''
24
+ 1. **Dynamic Window Sizes**: The strategy changes the window size it uses for calculating moving averages based on how volatile the market is. When volatility is high, it uses shorter windows, making it more responsive to rapid price movements. When volatility is low, it uses longer windows, which smooths out the data and reduces noise.
25
+ 2. **Exponential Moving Average (EMA)**: The strategy uses an EMA to track the average price. The EMA gives more weight to recent prices, so it reacts more quickly to changes compared to a simple moving average. This helps capture shifts in the market earlier.
26
+ 3. **Trend Filter**: A trend filter is added to make sure the strategy only takes trades in the direction of the overall market trend. This helps avoid taking trades that go against the bigger picture, which can lead to bad signals.
27
+ 4. **Buy/Sell Signals**:
28
+ - **Buy Signal**: A buy signal is generated when the price drops below the EMA and the trend shows an uptrend. This suggests that the price is likely to bounce back.
29
+ - **Sell Signal**: A sell signal occurs when the price goes above the EMA and the trend is showing a downtrend, indicating the price might fall soon.
30
+ 5. **Grid Search Optimization**: The app runs a grid search to test different combinations of parameters (like window sizes and thresholds) to find the ones that work best for the selected data. This helps maximize the strategy's performance.
31
  ##### **What You Can Do:**
32
+ - **Adjust Parameters**: After running the initial optimization, you can tweak the base window size, alpha, beta, and signal threshold to see how the strategy’s performance changes.
33
+ - **Signal Threshold**: This controls how strict the buy/sell signals are. A lower threshold will give you more signals, while a higher threshold will be more selective.
34
+ - **Visual Feedback**: The app shows you the strategy’s performance visually, plotting buy/sell signals on a price chart and showing an equity curve so you can see how well the strategy performs over time.
35
  ''')
36
 
37
+ # Sidebar: "How to Use" expander (closed by default)
38
  with st.sidebar.expander("How to Use", expanded=False):
39
  st.write("""
40
+ 1. **Select Ticker**: Choose the asset ticker symbol (e.g., AAPL, TSLA) and date range for historical data.
41
+ 2. **Run Strategy**: Click "Run Strategy" to optimize the parameters and run the backtest.
42
+ 3. **Adjust Parameters**: Use the sliders to fine-tune the moving average windows, beta, and signal threshold to see how the strategy performs.
43
+ 4. **Visualize**: The app displays buy/sell signals, the trend line, and the equity curve.
44
  """)
45
 
46
  st.sidebar.title("Input Parameters")
47
 
48
+ # Sidebar: Select Ticker and Date Range
49
  with st.sidebar.expander("Asset Settings", expanded=True):
50
  ticker = st.text_input("Asset Symbol", value="AAPL", help="Ticker symbol or Cryptocurrency Pair (e.g., AAPL, BTC-USD)")
51
  start_date = st.date_input("Start Date", value=pd.to_datetime("2020-01-01"), help="Select the start date for historical data.")
52
  end_date = st.date_input("End Date", value=datetime.today() + pd.DateOffset(1), help="Select the end date for historical data.")
53
 
54
+ # Function to download data
55
  @st.cache_data
56
  def get_data(ticker, start, end):
57
  data = yf.download(ticker, start=start, end=end)
58
+ # Squeeze the 'Close' column to ensure a 1D Series (fixes new yfinance behavior)
59
  return data['Close'].squeeze()
60
 
61
+ # Exponential Moving Average based OU parameters
62
  def OU_parameters_ema(data, window):
63
  window = int(window) # ensure window is scalar
64
  mu = data.ewm(span=window).mean()
65
  sigma = data.ewm(span=window).std()
66
  return mu, sigma
67
 
68
+ # Dynamic window size based on volatility
69
  def dynamic_window(data, base_window=60, volatility_window=20):
70
  volatility = data.rolling(window=volatility_window).std()
71
  adjusted_window = base_window / (volatility / volatility.mean())
 
74
  adjusted_window = adjusted_window.round().astype(int).clip(lower=20, upper=120)
75
  return adjusted_window
76
 
77
+ # Trading strategy with trend filter and adjustable parameters
78
  def trading_strategy(data, base_window=60, base_alpha=1.0, beta=0.1, trend_window=200, signal_threshold=0):
79
  windows = dynamic_window(data, base_window=base_window)
80
  buy_signals = []
 
83
  trend = data.rolling(window=trend_window).mean()
84
 
85
  for i in range(len(data)):
86
+ # Use trading only if trend is defined; otherwise, append NaN and no position
87
  if i < trend_window - 1:
88
  buy_signals.append(np.nan)
89
  sell_signals.append(np.nan)
 
93
  window = int(windows.iloc[i])
94
  mu, sigma = OU_parameters_ema(data[:i+1], window=window)
95
  alpha = base_alpha + beta * float(sigma.iloc[-1])
96
+ # Convert values to floats to ensure scalar comparisons
97
  price = float(data.iloc[i])
98
  mu_value = float(mu.iloc[-1])
99
  sigma_value = float(sigma.iloc[-1])
 
114
 
115
  return buy_signals, sell_signals, positions, trend
116
 
117
+ # Function to calculate performance metric and equity curve
118
  def calculate_performance(data, positions):
119
+ # Convert data to a 1D NumPy array to avoid shape issues
120
+ data_np = data.to_numpy().flatten()
121
  returns = np.diff(data_np) / data_np[:-1]
122
  strategy_returns = np.array(positions[:-1]) * returns
123
+ equity_curve = np.cumprod(1 + strategy_returns) * 100 # Start with an initial value of 100
124
  return equity_curve
125
 
126
+ # Grid search for best parameters with progress
127
  def grid_search(data, param_grid):
128
  best_params = None
129
  best_performance = -np.inf
 
139
  iteration += 1
140
  progress_bar.progress(iteration / total_iterations)
141
 
142
+ buy_signals, sell_signals, positions, trend = trading_strategy(data, base_window=base_window, base_alpha=base_alpha, beta=beta, trend_window=trend_window)
 
 
143
  equity_curve = calculate_performance(data, np.array(positions))
144
  performance = equity_curve[-1]
145
  if performance > best_performance:
 
148
  best_positions = positions
149
  best_trend = trend
150
 
151
+ progress_bar.empty() # Remove the progress bar when done
152
  return best_params, best_performance, best_positions, best_trend
153
 
154
+ # Run Button in the Sidebar
155
  run_button = st.sidebar.button("Run Strategy")
156
 
157
  if run_button:
158
+ # Get historical data
159
  data = get_data(ticker, start_date, end_date)
160
  param_grid = {
161
  'base_window': [30, 50, 70, 90],
 
169
  st.session_state['best_positions'] = best_positions
170
  st.session_state['best_trend'] = best_trend
171
 
172
+ # Display best parameters in JSON format
173
  st.json({
174
  "Best Parameters": {
175
  "Base Window": best_params[0],
 
179
  }
180
  })
181
 
182
+ # If the session state has the optimized data, allow updating the signal threshold and other parameters without re-running the optimization
183
  if 'best_params' in st.session_state:
184
+ # Sliders to adjust parameters and signal threshold
185
  st.sidebar.subheader("Adjust Parameters")
 
 
 
 
 
 
 
 
 
 
186
 
187
+ base_window = st.sidebar.slider(
188
+ "Base Window",
189
+ 20, 120, st.session_state['best_params'][0],
190
+ help="Adjust the base window size. A larger window smooths the data more but reacts slower to price changes."
191
+ )
192
+
193
+ base_alpha = st.sidebar.slider(
194
+ "Base Alpha",
195
+ 0.1, 2.0, st.session_state['best_params'][1], 0.1,
196
+ help="Adjust the base alpha value. A higher alpha increases the sensitivity to deviations from the mean."
197
+ )
198
+
199
+ beta = st.sidebar.slider(
200
+ "Beta",
201
+ 0.01, 0.3, st.session_state['best_params'][2], 0.01,
202
+ help="Adjust the beta value, which controls how much volatility affects the adaptive threshold."
203
+ )
204
+
205
+ trend_window = st.sidebar.slider(
206
+ "Trend Window",
207
+ 50, 400, st.session_state['best_params'][3],
208
+ help="Adjust the trend window size. A larger trend window smooths long-term trends but reacts slower to trend changes."
209
+ )
210
+
211
+ signal_threshold = st.sidebar.slider(
212
+ "Signal Threshold",
213
+ -0.2, 0.2, 0.0, 0.01,
214
+ help="Adjust the signal threshold: Lower values are more lenient (generate more signals), while higher values are more restrictive."
215
+ )
216
+
217
+ # Apply the trading strategy with the adjusted parameters
218
  buy_signals, sell_signals, positions, trend = trading_strategy(
219
  st.session_state['data'],
220
  base_window=base_window,
 
225
  )
226
  equity_curve = calculate_performance(st.session_state['data'], positions)
227
 
228
+ # Plotting with adjustments for easier comparison of x-axis
229
  fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
230
  subplot_titles=("Price and Signals", "Equity Curve"),
231
  vertical_spacing=0.1)
232
 
233
+ # Price and signal plot
234
  fig.add_trace(go.Scatter(x=st.session_state['data'].index, y=st.session_state['data'], mode='lines', name='Price'), row=1, col=1)
235
  fig.add_trace(go.Scatter(x=st.session_state['data'].index, y=trend, mode='lines', name=f"Trend Filter (SMA {trend_window})", line=dict(dash='dash')), row=1, col=1)
236
  fig.add_trace(go.Scatter(x=st.session_state['data'].index, y=buy_signals, mode='markers', name='Buy Signal', marker=dict(color='green', symbol='triangle-up', size=10)), row=1, col=1)
237
  fig.add_trace(go.Scatter(x=st.session_state['data'].index, y=sell_signals, mode='markers', name='Sell Signal', marker=dict(color='red', symbol='triangle-down', size=10)), row=1, col=1)
238
+
239
+ # Equity Curve Plot
240
  fig.add_trace(go.Scatter(x=st.session_state['data'].index[1:], y=equity_curve, mode='lines', name='Equity Curve'), row=2, col=1)
241
 
242
+ # Adjust layout for better clarity
243
  fig.update_layout(
244
  height=800,
245
  title=f'{ticker} Optimized Mean-Reversion Trading Strategy',