Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -21,51 +21,44 @@ 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
|
| 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 |
-
- **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
|
| 41 |
-
2. **Run Strategy**: Click "Run Strategy" to optimize
|
| 42 |
-
3. **Adjust Parameters**: Use
|
| 43 |
-
4. **Visualize**: The app displays buy/sell signals
|
| 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 |
return data['Close']
|
| 59 |
|
| 60 |
-
# Exponential Moving Average based OU parameters with adjustment
|
| 61 |
def OU_parameters_ema(data, window):
|
| 62 |
-
#
|
| 63 |
-
window = int(window)
|
| 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,7 +67,6 @@ def dynamic_window(data, base_window=60, volatility_window=20):
|
|
| 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 = []
|
|
@@ -82,16 +74,23 @@ def trading_strategy(data, base_window=60, base_alpha=1.0, beta=0.1, trend_windo
|
|
| 82 |
positions = []
|
| 83 |
trend = data.rolling(window=trend_window).mean()
|
| 84 |
|
|
|
|
| 85 |
for i in range(len(data)):
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
window = int(windows.iloc[i])
|
| 88 |
mu, sigma = OU_parameters_ema(data[:i+1], window=window)
|
| 89 |
-
alpha = base_alpha + beta * sigma.iloc[-1]
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
| 95 |
if price < mu_value - (alpha + signal_threshold) * sigma_value and price < trend_value:
|
| 96 |
buy_signals.append(price)
|
| 97 |
sell_signals.append(np.nan)
|
|
@@ -107,14 +106,12 @@ 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 |
-
# Function to calculate performance metric and equity curve
|
| 111 |
def calculate_performance(data, positions):
|
| 112 |
returns = np.diff(data) / data[:-1]
|
| 113 |
strategy_returns = np.array(positions[:-1]) * returns
|
| 114 |
-
equity_curve = np.cumprod(1 + strategy_returns) * 100 #
|
| 115 |
return equity_curve
|
| 116 |
|
| 117 |
-
# Grid search for best parameters with progress
|
| 118 |
def grid_search(data, param_grid):
|
| 119 |
best_params = None
|
| 120 |
best_performance = -np.inf
|
|
@@ -141,34 +138,25 @@ 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 in the Sidebar
|
| 148 |
run_button = st.sidebar.button("Run Strategy")
|
| 149 |
|
| 150 |
if run_button:
|
| 151 |
-
# Get historical data
|
| 152 |
data = get_data(ticker, start_date, end_date)
|
| 153 |
-
|
| 154 |
-
# Define the parameter grid
|
| 155 |
param_grid = {
|
| 156 |
'base_window': [30, 50, 70, 90],
|
| 157 |
'base_alpha': [0.5, 1.0, 1.5],
|
| 158 |
'beta': [0.05, 0.1, 0.15],
|
| 159 |
'trend_window': [100, 200, 300]
|
| 160 |
}
|
| 161 |
-
|
| 162 |
-
# Perform grid search to find the best parameters
|
| 163 |
best_params, best_performance, best_positions, best_trend = grid_search(data, param_grid)
|
| 164 |
-
|
| 165 |
-
# Cache the results for faster updates when adjusting parameters
|
| 166 |
st.session_state['data'] = data
|
| 167 |
st.session_state['best_params'] = best_params
|
| 168 |
st.session_state['best_positions'] = best_positions
|
| 169 |
st.session_state['best_trend'] = best_trend
|
| 170 |
|
| 171 |
-
# Display best parameters in JSON format
|
| 172 |
st.json({
|
| 173 |
"Best Parameters": {
|
| 174 |
"Base Window": best_params[0],
|
|
@@ -178,41 +166,19 @@ if run_button:
|
|
| 178 |
}
|
| 179 |
})
|
| 180 |
|
| 181 |
-
# If the session state has the optimized data, allow updating the signal threshold and other parameters without re-running the optimization
|
| 182 |
if 'best_params' in st.session_state:
|
| 183 |
st.sidebar.subheader("Adjust Parameters")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
-
base_window = st.sidebar.slider(
|
| 186 |
-
"Base Window",
|
| 187 |
-
20, 120, st.session_state['best_params'][0],
|
| 188 |
-
help="Adjust the base window size."
|
| 189 |
-
)
|
| 190 |
-
|
| 191 |
-
base_alpha = st.sidebar.slider(
|
| 192 |
-
"Base Alpha",
|
| 193 |
-
0.1, 2.0, st.session_state['best_params'][1], 0.1,
|
| 194 |
-
help="Adjust the base alpha value."
|
| 195 |
-
)
|
| 196 |
-
|
| 197 |
-
beta = st.sidebar.slider(
|
| 198 |
-
"Beta",
|
| 199 |
-
0.01, 0.3, st.session_state['best_params'][2], 0.01,
|
| 200 |
-
help="Adjust the beta value."
|
| 201 |
-
)
|
| 202 |
-
|
| 203 |
-
trend_window = st.sidebar.slider(
|
| 204 |
-
"Trend Window",
|
| 205 |
-
50, 400, st.session_state['best_params'][3],
|
| 206 |
-
help="Adjust the trend window size."
|
| 207 |
-
)
|
| 208 |
-
|
| 209 |
-
signal_threshold = st.sidebar.slider(
|
| 210 |
-
"Signal Threshold",
|
| 211 |
-
-0.2, 0.2, 0.0, 0.01,
|
| 212 |
-
help="Adjust the signal threshold."
|
| 213 |
-
)
|
| 214 |
-
|
| 215 |
-
# Apply the trading strategy with adjusted parameters
|
| 216 |
buy_signals, sell_signals, positions, trend = trading_strategy(
|
| 217 |
st.session_state['data'],
|
| 218 |
base_window=base_window,
|
|
@@ -221,11 +187,8 @@ if 'best_params' in st.session_state:
|
|
| 221 |
trend_window=trend_window,
|
| 222 |
signal_threshold=signal_threshold
|
| 223 |
)
|
| 224 |
-
|
| 225 |
-
# Calculate the updated equity curve
|
| 226 |
equity_curve = calculate_performance(st.session_state['data'], positions)
|
| 227 |
|
| 228 |
-
# Plotting
|
| 229 |
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
| 230 |
subplot_titles=("Price and Signals", "Equity Curve"),
|
| 231 |
vertical_spacing=0.1)
|
|
|
|
| 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 |
@st.cache_data
|
| 52 |
def get_data(ticker, start, end):
|
| 53 |
data = yf.download(ticker, start=start, end=end)
|
| 54 |
return data['Close']
|
| 55 |
|
|
|
|
| 56 |
def OU_parameters_ema(data, window):
|
| 57 |
+
window = int(window) # ensure window is scalar
|
|
|
|
| 58 |
mu = data.ewm(span=window).mean()
|
| 59 |
sigma = data.ewm(span=window).std()
|
| 60 |
return mu, sigma
|
| 61 |
|
|
|
|
| 62 |
def dynamic_window(data, base_window=60, volatility_window=20):
|
| 63 |
volatility = data.rolling(window=volatility_window).std()
|
| 64 |
adjusted_window = base_window / (volatility / volatility.mean())
|
|
|
|
| 67 |
adjusted_window = adjusted_window.round().astype(int).clip(lower=20, upper=120)
|
| 68 |
return adjusted_window
|
| 69 |
|
|
|
|
| 70 |
def trading_strategy(data, base_window=60, base_alpha=1.0, beta=0.1, trend_window=200, signal_threshold=0):
|
| 71 |
windows = dynamic_window(data, base_window=base_window)
|
| 72 |
buy_signals = []
|
|
|
|
| 74 |
positions = []
|
| 75 |
trend = data.rolling(window=trend_window).mean()
|
| 76 |
|
| 77 |
+
# For indices where the trend is not fully defined, append NaNs and no position.
|
| 78 |
for i in range(len(data)):
|
| 79 |
+
if i < trend_window - 1:
|
| 80 |
+
buy_signals.append(np.nan)
|
| 81 |
+
sell_signals.append(np.nan)
|
| 82 |
+
positions.append(0)
|
| 83 |
+
continue
|
| 84 |
+
|
| 85 |
window = int(windows.iloc[i])
|
| 86 |
mu, sigma = OU_parameters_ema(data[:i+1], window=window)
|
| 87 |
+
alpha = base_alpha + beta * float(sigma.iloc[-1])
|
| 88 |
+
# Convert each to float to ensure scalar comparison
|
| 89 |
+
price = float(data.iloc[i])
|
| 90 |
+
mu_value = float(mu.iloc[-1])
|
| 91 |
+
sigma_value = float(sigma.iloc[-1])
|
| 92 |
+
trend_value = float(trend.iloc[i])
|
| 93 |
+
|
| 94 |
if price < mu_value - (alpha + signal_threshold) * sigma_value and price < trend_value:
|
| 95 |
buy_signals.append(price)
|
| 96 |
sell_signals.append(np.nan)
|
|
|
|
| 106 |
|
| 107 |
return buy_signals, sell_signals, positions, trend
|
| 108 |
|
|
|
|
| 109 |
def calculate_performance(data, positions):
|
| 110 |
returns = np.diff(data) / data[:-1]
|
| 111 |
strategy_returns = np.array(positions[:-1]) * returns
|
| 112 |
+
equity_curve = np.cumprod(1 + strategy_returns) * 100 # starting equity of 100
|
| 113 |
return equity_curve
|
| 114 |
|
|
|
|
| 115 |
def grid_search(data, param_grid):
|
| 116 |
best_params = None
|
| 117 |
best_performance = -np.inf
|
|
|
|
| 138 |
best_positions = positions
|
| 139 |
best_trend = trend
|
| 140 |
|
| 141 |
+
progress_bar.empty()
|
| 142 |
return best_params, best_performance, best_positions, best_trend
|
| 143 |
|
|
|
|
| 144 |
run_button = st.sidebar.button("Run Strategy")
|
| 145 |
|
| 146 |
if run_button:
|
|
|
|
| 147 |
data = get_data(ticker, start_date, end_date)
|
|
|
|
|
|
|
| 148 |
param_grid = {
|
| 149 |
'base_window': [30, 50, 70, 90],
|
| 150 |
'base_alpha': [0.5, 1.0, 1.5],
|
| 151 |
'beta': [0.05, 0.1, 0.15],
|
| 152 |
'trend_window': [100, 200, 300]
|
| 153 |
}
|
|
|
|
|
|
|
| 154 |
best_params, best_performance, best_positions, best_trend = grid_search(data, param_grid)
|
|
|
|
|
|
|
| 155 |
st.session_state['data'] = data
|
| 156 |
st.session_state['best_params'] = best_params
|
| 157 |
st.session_state['best_positions'] = best_positions
|
| 158 |
st.session_state['best_trend'] = best_trend
|
| 159 |
|
|
|
|
| 160 |
st.json({
|
| 161 |
"Best Parameters": {
|
| 162 |
"Base Window": best_params[0],
|
|
|
|
| 166 |
}
|
| 167 |
})
|
| 168 |
|
|
|
|
| 169 |
if 'best_params' in st.session_state:
|
| 170 |
st.sidebar.subheader("Adjust Parameters")
|
| 171 |
+
base_window = st.sidebar.slider("Base Window", 20, 120, st.session_state['best_params'][0],
|
| 172 |
+
help="Adjust the base window size.")
|
| 173 |
+
base_alpha = st.sidebar.slider("Base Alpha", 0.1, 2.0, st.session_state['best_params'][1], 0.1,
|
| 174 |
+
help="Adjust the base alpha value.")
|
| 175 |
+
beta = st.sidebar.slider("Beta", 0.01, 0.3, st.session_state['best_params'][2], 0.01,
|
| 176 |
+
help="Adjust the beta value.")
|
| 177 |
+
trend_window = st.sidebar.slider("Trend Window", 50, 400, st.session_state['best_params'][3],
|
| 178 |
+
help="Adjust the trend window size.")
|
| 179 |
+
signal_threshold = st.sidebar.slider("Signal Threshold", -0.2, 0.2, 0.0, 0.01,
|
| 180 |
+
help="Adjust the signal threshold.")
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
buy_signals, sell_signals, positions, trend = trading_strategy(
|
| 183 |
st.session_state['data'],
|
| 184 |
base_window=base_window,
|
|
|
|
| 187 |
trend_window=trend_window,
|
| 188 |
signal_threshold=signal_threshold
|
| 189 |
)
|
|
|
|
|
|
|
| 190 |
equity_curve = calculate_performance(st.session_state['data'], positions)
|
| 191 |
|
|
|
|
| 192 |
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
| 193 |
subplot_titles=("Price and Signals", "Equity Curve"),
|
| 194 |
vertical_spacing=0.1)
|