import streamlit as st import pandas as pd import yfinance as yf import numpy as np import plotly.graph_objs as go from plotly.subplots import make_subplots import warnings warnings.filterwarnings("ignore") from datetime import datetime, timedelta # Set Streamlit page configuration st.set_page_config(page_title="Bollinger Bands and RSI Trading Strategy", layout="wide") # Title and Description st.title("Bollinger Bands and RSI Trading Strategy") st.write(""" This tool backtests and optimizes a trading strategy based on Bollinger Bands and RSI. The strategy generates buy signals when the price is below the lower Bollinger Band and RSI is oversold, and sell signals when the price is above the upper Bollinger Band and RSI is overbought. The process is optimized to find the best combination of parameters (RSI period, Bollinger Bands window, and Bollinger Bands standard deviation) You can adjust the parameters post-run, and visualize the results, including buy/sell signals and the equity curve. """) st.sidebar.title("Input Parameters") # Sidebar: How to use the app (Expander, closed by default) with st.sidebar.expander("How to Use", expanded=False): st.write(""" 1. **Select Ticker and Date Range**: Choose the asset ticker (e.g., AAPL, ASML.AS, BTC-USD, EURUSD=X) and date range for historical data. 2. **Run Strategy**: Click "Run Strategy" to optimize and backtest the Bollinger Bands and RSI strategy. 3. **Adjust Parameters**: After running, adjust RSI period, Bollinger Bands window, standard deviation, and trend threshold. Results update in real time. 4. **View Results**: See strategy performance, buy/sell signals, equity curve, and indicators like Bollinger Bands and RSI. """) # Sidebar: Select Ticker and Date Range with st.sidebar.expander("Asset Settings", expanded=True): ticker = st.text_input("Asset Symbol", value="ASML.AS", help="Indicate Asset Symbol (e.g., ASML.AS, 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=pd.to_datetime("today") + pd.DateOffset(1), help="Select the end date for historical data.") # Run Button in the Sidebar run_button = st.sidebar.button("Run Strategy") # Function to download historical data with yfinance adjustments @st.cache_data def download_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 fetched for {ticker} from {start} to {end}.") return data # RSI calculation def rsi(series, period=14): delta = series.diff(1) gain = delta.where(delta > 0, 0) loss = -delta.where(delta < 0, 0) avg_gain = gain.rolling(window=int(period)).mean() avg_loss = loss.rolling(window=int(period)).mean() rs = avg_gain / avg_loss return 100 - (100 / (1 + rs)) # Bollinger Bands calculation def bollinger_bands(series, window=20, n_std=2): sma = series.rolling(window=int(window)).mean() std = series.rolling(window=int(window)).std() upper_band = sma + (n_std * std) lower_band = sma - (n_std * std) return upper_band, sma, lower_band # SMA calculation def sma(series, window): return series.rolling(window=int(window)).mean() # Strategy calculation function def strategy(data, rsi_period, bb_window, bb_std_dev, sma_window=100, trend_threshold=0.01): data['RSI'] = rsi(data['Close'], period=rsi_period) data['BB_HIGH'], data['BB_MID'], data['BB_LOW'] = bollinger_bands(data['Close'], window=bb_window, n_std=bb_std_dev) data['SMA_200'] = sma(data['Close'], window=sma_window) data['SMA_Diff'] = (data['Close'] - data['SMA_200']) / data['SMA_200'] if trend_threshold == 0: data['Trend_Condition'] = True elif trend_threshold > 0: data['Trend_Condition'] = data['SMA_Diff'].abs() <= trend_threshold else: data['Trend_Condition'] = data['SMA_Diff'].abs() >= abs(trend_threshold) data['Buy_Signal'] = (data['RSI'] < 30) & (data['Close'] < data['BB_LOW']) & data['Trend_Condition'] data['Sell_Signal'] = (data['RSI'] > 70) & (data['Close'] > data['BB_HIGH']) & data['Trend_Condition'] data['Position'] = 0 data.loc[data['Buy_Signal'], 'Position'] = 1 data.loc[data['Sell_Signal'], 'Position'] = -1 data['Position'] = data['Position'].replace(to_replace=0, method='ffill') data['Strategy_Returns'] = data['Position'].shift(1) * data['Close'].pct_change() data['Equity_Curve'] = (1 + data['Strategy_Returns']).cumprod() return data # Optimization function def optimize_strategy(data, rsi_period_range, bb_window_range, bb_std_dev_range, trend_threshold=0.01): best_rsi_period = None best_bb_window = None best_bb_std_dev = None best_equity = -np.inf total_iterations = len(rsi_period_range) * len(bb_window_range) * len(bb_std_dev_range) iteration = 0 progress_bar = st.progress(0) for rsi_period in rsi_period_range: for bb_window in bb_window_range: for bb_std_dev in bb_std_dev_range: iteration += 1 progress_bar.progress(iteration / total_iterations) temp_data = strategy(data.copy(), rsi_period, bb_window, bb_std_dev, trend_threshold=trend_threshold) final_equity = temp_data['Equity_Curve'].iloc[-1] if final_equity > best_equity: best_rsi_period = rsi_period best_bb_window = bb_window best_bb_std_dev = bb_std_dev best_equity = final_equity progress_bar.empty() return best_rsi_period, best_bb_window, best_bb_std_dev, best_equity # Running the strategy if run_button or "data" in st.session_state: if run_button: # Download data data = download_data(ticker, start_date, end_date) # Parameter ranges for optimization rsi_period_range = range(10, 31, 2) bb_window_range = range(11, 32, 2) bb_std_dev_range = [1.5, 1.75, 2, 2.25, 2.5] # Optimize strategy best_rsi_period, best_bb_window, best_bb_std_dev, best_equity = optimize_strategy( data, rsi_period_range, bb_window_range, bb_std_dev_range, trend_threshold=0.00 ) # Cache the best parameters and data after running optimization st.session_state['best_params'] = { "RSI Period": best_rsi_period, "BB Window": best_bb_window, "BB Std Dev": float(best_bb_std_dev), } st.session_state['best_equity'] = best_equity st.session_state['data'] = data st.session_state['adjusted_params'] = None # Reset adjusted parameters # Display best parameters in JSON format after optimization (always displayed) st.subheader("Best Parameters from Optimization") st.json(st.session_state['best_params']) # Allow user to adjust parameters post-run st.sidebar.markdown("### Adjust Parameters Post-Run") if run_button or st.session_state['adjusted_params'] is None: # Reset to optimized parameters when re-running adjusted_rsi_period = st.sidebar.slider( "RSI Period", 10, 30, st.session_state['best_params']['RSI Period'], step=2, help="RSI Period defines the window length for calculating RSI. Increasing it smooths the RSI curve, making it less sensitive to price changes. Decreasing it increases sensitivity, generating more signals." ) adjusted_bb_window = st.sidebar.slider( "BB Window", 11, 32, st.session_state['best_params']['BB Window'], step=2, help="BB Window defines the period over which Bollinger Bands are calculated. Increasing it broadens the bands, capturing long-term trends. Decreasing it narrows the bands, making them more sensitive to short-term price changes." ) adjusted_bb_std_dev = st.sidebar.slider( "BB Std Dev", 1.5, 2.5, float(st.session_state['best_params']['BB Std Dev']), step=0.25, help="BB Std Dev controls how wide the Bollinger Bands are. Increasing this widens the bands, leading to fewer but more significant signals. Decreasing it tightens the bands, generating more frequent signals." ) trend_threshold = st.sidebar.slider( "Trend Threshold", -0.05, 0.05, 0.00, 0.01, help="Trend Threshold adjusts how strict the trend-following condition is. Positive values tighten the condition, filtering out minor fluctuations, while negative values allow more flexibility in trend detection." ) else: # Keep the adjusted parameters if they exist adjusted_rsi_period = st.sidebar.slider( "RSI Period", 10, 30, st.session_state['adjusted_params']['RSI Period'], step=2, help="RSI Period defines the window length for calculating RSI. Increasing it smooths the RSI curve, making it less sensitive to price changes. Decreasing it increases sensitivity, generating more signals." ) adjusted_bb_window = st.sidebar.slider( "BB Window", 11, 32, st.session_state['adjusted_params']['BB Window'], step=2, help="BB Window defines the period over which Bollinger Bands are calculated. Increasing it broadens the bands, capturing long-term trends. Decreasing it narrows the bands, making them more sensitive to short-term price changes." ) adjusted_bb_std_dev = st.sidebar.slider( "BB Std Dev", 1.5, 2.5, float(st.session_state['adjusted_params']['BB Std Dev']), step=0.25, help="BB Std Dev controls how wide the Bollinger Bands are. Increasing this widens the bands, leading to fewer but more significant signals. Decreasing it tightens the bands, generating more frequent signals." ) trend_threshold = st.sidebar.slider( "Trend Threshold", -0.05, 0.05, st.session_state['adjusted_params']['Trend Threshold'], 0.01, help="Trend Threshold adjusts how strict the trend-following condition is. Positive values tighten the condition, filtering out minor fluctuations, while negative values allow more flexibility in trend detection." ) # Recalculate the strategy with adjusted parameters updated_data = strategy(st.session_state['data'].copy(), adjusted_rsi_period, adjusted_bb_window, adjusted_bb_std_dev, trend_threshold=trend_threshold) # Cache the updated parameters and results st.session_state['adjusted_params'] = { "RSI Period": adjusted_rsi_period, "BB Window": adjusted_bb_window, "BB Std Dev": adjusted_bb_std_dev, "Trend Threshold": trend_threshold } st.session_state['updated_data'] = updated_data # Recalculate buy and sell signals buy_signals = updated_data[updated_data['Buy_Signal']] sell_signals = updated_data[updated_data['Sell_Signal']] # Create subplots with 3 rows: Price/Bollinger, RSI, Equity Curve fig = make_subplots(rows=3, cols=1, shared_xaxes=True, subplot_titles=("Price and Bollinger Bands", "RSI", "Equity Curve"), vertical_spacing=0.1) # Price and Bollinger Bands fig.add_trace(go.Scatter(x=updated_data.index, y=updated_data['Close'], mode='lines', name='Close Price'), row=1, col=1) fig.add_trace(go.Scatter(x=updated_data.index, y=updated_data['BB_MID'], mode='lines', name='Bollinger Bands Mid', line=dict(color='red', dash='dash')), row=1, col=1) fig.add_trace(go.Scatter(x=updated_data.index, y=updated_data['BB_HIGH'], mode='lines', name='Bollinger Bands High', line=dict(color='green', dash='dash')), row=1, col=1) fig.add_trace(go.Scatter(x=updated_data.index, y=updated_data['BB_LOW'], mode='lines', name='Bollinger Bands Low', line=dict(color='cyan', dash='dash')), row=1, col=1) # Buy/Sell Signals fig.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['Close'], mode='markers', name='Buy Signal', marker=dict(color='green', symbol='triangle-up', size=10)), row=1, col=1) fig.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['Close'], mode='markers', name='Sell Signal', marker=dict(color='red', symbol='triangle-down', size=10)), row=1, col=1) # RSI fig.add_trace(go.Scatter(x=updated_data.index, y=updated_data['RSI'], mode='lines', name='RSI', line=dict(color='purple')), row=2, col=1) fig.add_hline(y=70, line_dash="dash", row=2, col=1, line_color="red") fig.add_hline(y=30, line_dash="dash", row=2, col=1, line_color="green") # Equity Curve fig.add_trace(go.Scatter(x=updated_data.index, y=updated_data['Equity_Curve'], mode='lines', name='Equity Curve', line=dict(color='white')), row=3, col=1) fig.update_layout( title=f'{ticker} Bollinger Bands and RSI Strategy', xaxis_title='Date', height=800, legend=dict( orientation="h", yanchor="bottom", y=1.15, xanchor="center", x=0.5 ) ) st.plotly_chart(fig, use_container_width=True) hide_streamlit_style = """ """ st.markdown(hide_streamlit_style, unsafe_allow_html=True)