Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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)**:
|
| 26 |
-
3. **Trend Filter**:
|
| 27 |
-
4. **Buy/Sell Signals**:
|
| 28 |
-
- **Buy Signal**:
|
| 29 |
-
- **Sell Signal**:
|
| 30 |
-
5. **Grid Search Optimization**:
|
| 31 |
##### **What You Can Do:**
|
| 32 |
-
- **Adjust Parameters**:
|
| 33 |
-
- **
|
|
|
|
| 34 |
''')
|
| 35 |
|
|
|
|
| 36 |
with st.sidebar.expander("How to Use", expanded=False):
|
| 37 |
st.write("""
|
| 38 |
-
1. **Select Ticker**: Choose
|
| 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
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
#
|
| 111 |
def calculate_performance(data, positions):
|
| 112 |
-
|
|
|
|
| 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 #
|
| 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',
|