Spaces:
Sleeping
Sleeping
| 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 | |
| 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 = """ | |
| <style> | |
| #MainMenu {visibility: hidden;} | |
| footer {visibility: hidden;} | |
| </style> | |
| """ | |
| st.markdown(hide_streamlit_style, unsafe_allow_html=True) |