File size: 13,228 Bytes
caca8a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5a62998
 
caca8a3
 
 
1b2c776
 
 
 
 
 
 
caca8a3
1b2c776
 
 
caca8a3
 
1b2c776
caca8a3
 
1b2c776
 
 
 
caca8a3
 
 
 
1b2c776
d55457f
caca8a3
 
6820d6d
caca8a3
1b2c776
caca8a3
 
f048ab2
 
 
 
 
 
 
fe1d6a2
caca8a3
1b2c776
caca8a3
83c71ac
caca8a3
 
 
 
1b2c776
caca8a3
 
 
 
 
 
 
 
1b2c776
caca8a3
 
 
 
 
 
 
 
1b2c776
83c71ac
 
 
 
 
 
5a62998
caca8a3
83c71ac
1b2c776
83c71ac
 
 
 
 
caca8a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b2c776
caca8a3
1b2c776
 
bcb29d3
5a62998
1b2c776
caca8a3
 
1b2c776
caca8a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b2c776
caca8a3
 
 
 
 
 
 
 
1b2c776
caca8a3
 
1b2c776
caca8a3
 
 
f048ab2
 
 
 
 
 
 
 
caca8a3
f048ab2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
caca8a3
1b2c776
caca8a3
1b2c776
caca8a3
 
1b2c776
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
caca8a3
 
 
 
 
 
 
 
 
 
1b2c776
caca8a3
 
 
 
1b2c776
caca8a3
 
 
 
1b2c776
 
caca8a3
 
1b2c776
caca8a3
 
 
 
 
11ed418
caca8a3
 
 
 
 
 
 
 
 
 
 
f048ab2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
import streamlit as st
import yfinance as yf
import numpy as np
import pandas as pd
import plotly.graph_objs as go
from plotly.subplots import make_subplots
from itertools import product
import warnings
from datetime import datetime

warnings.filterwarnings("ignore")

# Set Streamlit page configuration
st.set_page_config(page_title="Optimized Mean-Reversion Trading Strategy", layout="wide")

# Title and Description
st.title("Optimized Mean-Reversion Trading Strategy")
st.write('''
This tool backtests and optimizes a mean-reversion trading strategy. The idea behind mean-reversion is that prices tend to revert back to their average over time. The strategy adapts to market conditions by adjusting its moving average windows based on volatility and applying trend filters to identify potential buy or sell points.
''')

with st.expander("How the Strategy Works:", expanded=False):
    st.markdown('''
    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.
    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.
    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.
    4. **Buy/Sell Signals**: 
        - **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.
        - **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.
    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.
    ##### **What You Can Do:**
    - **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.
    - **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.
    - **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.
    ''')

# Sidebar: "How to Use" expander (closed by default)
with st.sidebar.expander("How to Use", expanded=False):
    st.write("""
    1. **Select Ticker**: Choose the asset ticker symbol (e.g., AAPL, TSLA) and date range for historical data.
    2. **Run Strategy**: Click "Run Strategy" to optimize the parameters and run the backtest.
    3. **Adjust Parameters**: Use the sliders to fine-tune the moving average windows, beta, and signal threshold to see how the strategy performs.
    4. **Visualize**: The app displays buy/sell signals, the trend line, and the equity curve.
    """)

st.sidebar.title("Input Parameters")

# Sidebar: Select Ticker and Date Range
with st.sidebar.expander("Asset Settings", expanded=True):
    ticker = st.text_input("Asset Symbol", value="AAPL", help="Ticker symbol or Cryptocurrency Pair (e.g., AAPL, BTC-USD)")
    start_date = st.date_input("Start Date", value=pd.to_datetime("2020-01-01"), help="Select the start date for historical data.")
    end_date = st.date_input("End Date", value=datetime.today() + pd.DateOffset(1), help="Select the end date for historical data.")

# Function to download data
@st.cache_data
def get_data(ticker, start, end):
    data = yf.download(ticker, start=start, end=end, auto_adjust=False)
    if isinstance(data.columns, pd.MultiIndex):
        data.columns = data.columns.get_level_values(0)
    if data.empty:
        raise ValueError(f"No data retrieved for {ticker}")
    if len(data) < 300:  # Ensure enough data for largest trend window (300)
        raise ValueError(f"Insufficient data points for {ticker}. Need at least 300 days.")
    return data['Close'].squeeze()

# Exponential Moving Average based OU parameters
def OU_parameters_ema(data, window):
    window = int(window)  # ensure window is scalar
    mu = data.ewm(span=window).mean()
    sigma = data.ewm(span=window).std()
    return mu, sigma

# Dynamic window size based on volatility
def dynamic_window(data, base_window=60, volatility_window=20):
    volatility = data.rolling(window=volatility_window).std()
    adjusted_window = base_window / (volatility / volatility.mean())
    adjusted_window = adjusted_window.replace([np.inf, -np.inf], np.nan)
    adjusted_window = adjusted_window.fillna(base_window)
    adjusted_window = adjusted_window.round().astype(int).clip(lower=20, upper=120)
    return adjusted_window

# Trading strategy with trend filter and adjustable parameters
def trading_strategy(data, base_window=60, base_alpha=1.0, beta=0.1, trend_window=200, signal_threshold=0):
    windows = dynamic_window(data, base_window=base_window)
    buy_signals = []
    sell_signals = []
    positions = []
    trend = data.rolling(window=trend_window).mean()

    for i in range(len(data)):
        # Use trading only if trend is defined; otherwise, append NaN and no position
        if i < trend_window - 1:
            buy_signals.append(np.nan)
            sell_signals.append(np.nan)
            positions.append(0)
            continue

        window = int(windows.iloc[i])
        mu, sigma = OU_parameters_ema(data[:i+1], window=window)
        alpha = base_alpha + beta * float(sigma.iloc[-1])
        # Convert values to floats to ensure scalar comparisons
        price = float(data.iloc[i])
        mu_value = float(mu.iloc[-1])
        sigma_value = float(sigma.iloc[-1])
        trend_value = float(trend.iloc[i])
        
        if price < mu_value - (alpha + signal_threshold) * sigma_value and price < trend_value:
            buy_signals.append(price)
            sell_signals.append(np.nan)
            positions.append(1)
        elif price > mu_value + (alpha + signal_threshold) * sigma_value and price > trend_value:
            buy_signals.append(np.nan)
            sell_signals.append(price)
            positions.append(-1)
        else:
            buy_signals.append(np.nan)
            sell_signals.append(np.nan)
            positions.append(0)

    return buy_signals, sell_signals, positions, trend

# Function to calculate performance metric and equity curve
def calculate_performance(data, positions):
    # Convert data to a 1D NumPy array to avoid shape issues
    data_np = data.to_numpy().flatten()
    returns = np.diff(data_np) / data_np[:-1]
    strategy_returns = np.array(positions[:-1]) * returns
    equity_curve = np.cumprod(1 + strategy_returns) * 100  # Start with an initial value of 100
    return equity_curve

# Grid search for best parameters with progress
def grid_search(data, param_grid):
    best_params = None
    best_performance = -np.inf
    best_positions = None
    best_trend = None

    total_iterations = len(param_grid['base_window']) * len(param_grid['base_alpha']) * len(param_grid['beta']) * len(param_grid['trend_window'])
    iteration = 0

    progress_bar = st.progress(0)

    for base_window, base_alpha, beta, trend_window in product(*param_grid.values()):
        iteration += 1
        progress_bar.progress(iteration / total_iterations)

        buy_signals, sell_signals, positions, trend = trading_strategy(data, base_window=base_window, base_alpha=base_alpha, beta=beta, trend_window=trend_window)
        equity_curve = calculate_performance(data, np.array(positions))
        performance = equity_curve[-1]
        if performance > best_performance:
            best_performance = performance
            best_params = (base_window, base_alpha, beta, trend_window)
            best_positions = positions
            best_trend = trend

    progress_bar.empty()  # Remove the progress bar when done
    return best_params, best_performance, best_positions, best_trend

# Run Button in the Sidebar
run_button = st.sidebar.button("Run Strategy")

if run_button:
    try:
        # Get historical data
        data = get_data(ticker, start_date, end_date)
        param_grid = {
            'base_window': [30, 50, 70, 90],
            'base_alpha': [0.5, 1.0, 1.5],
            'beta': [0.05, 0.1, 0.15],
            'trend_window': [100, 200, 300]
        }
        best_params, best_performance, best_positions, best_trend = grid_search(data, param_grid)
        st.session_state['data'] = data
        st.session_state['best_params'] = best_params
        st.session_state['best_positions'] = best_positions
        st.session_state['best_trend'] = best_trend

        # Display best parameters in JSON format
        st.json({
            "Best Parameters": {
                "Base Window": best_params[0],
                "Base Alpha": best_params[1],
                "Beta": best_params[2],
                "Trend Window": best_params[3]
            }
        })
    except Exception as e:
        st.error(f"An error occurred while running the analysis: {e}")

# If the session state has the optimized data, allow updating the signal threshold and other parameters without re-running the optimization
if 'best_params' in st.session_state:
    # Sliders to adjust parameters and signal threshold
    st.sidebar.subheader("Adjust Parameters")

    base_window = st.sidebar.slider(
        "Base Window",
        20, 120, st.session_state['best_params'][0],
        help="Adjust the base window size. A larger window smooths the data more but reacts slower to price changes."
    )

    base_alpha = st.sidebar.slider(
        "Base Alpha",
        0.1, 2.0, st.session_state['best_params'][1], 0.1,
        help="Adjust the base alpha value. A higher alpha increases the sensitivity to deviations from the mean."
    )

    beta = st.sidebar.slider(
        "Beta",
        0.01, 0.3, st.session_state['best_params'][2], 0.01,
        help="Adjust the beta value, which controls how much volatility affects the adaptive threshold."
    )

    trend_window = st.sidebar.slider(
        "Trend Window",
        50, 400, st.session_state['best_params'][3],
        help="Adjust the trend window size. A larger trend window smooths long-term trends but reacts slower to trend changes."
    )

    signal_threshold = st.sidebar.slider(
        "Signal Threshold",
        -0.2, 0.2, 0.0, 0.01,
        help="Adjust the signal threshold: Lower values are more lenient (generate more signals), while higher values are more restrictive."
    )

    # Apply the trading strategy with the adjusted parameters
    buy_signals, sell_signals, positions, trend = trading_strategy(
        st.session_state['data'],
        base_window=base_window,
        base_alpha=base_alpha,
        beta=beta,
        trend_window=trend_window,
        signal_threshold=signal_threshold
    )
    equity_curve = calculate_performance(st.session_state['data'], positions)

    # Plotting with adjustments for easier comparison of x-axis
    fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
                        subplot_titles=("Price and Signals", "Equity Curve"),
                        vertical_spacing=0.1)

    # Price and signal plot
    fig.add_trace(go.Scatter(x=st.session_state['data'].index, y=st.session_state['data'], mode='lines', name='Price'), row=1, col=1)
    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)
    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)
    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)

    # Equity Curve Plot
    fig.add_trace(go.Scatter(x=st.session_state['data'].index[1:], y=equity_curve, mode='lines', name='Equity Curve'), row=2, col=1)

    # Adjust layout for better clarity
    fig.update_layout(
        height=800,
        title=f'{ticker} Optimized Mean-Reversion Trading Strategy',
        xaxis_title='Date',
        yaxis_title='Price',
        legend=dict(orientation="h", yanchor="bottom", y=1.15, xanchor="center", x=0.5),
        font=dict(size=12)
    )

    st.plotly_chart(fig, use_container_width=True)

hide_streamlit_style = """
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_streamlit_style, unsafe_allow_html=True)