Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import yfinance as yf | |
| import numpy as np | |
| import pandas as pd | |
| import plotly.graph_objs as go | |
| from itertools import product | |
| from datetime import datetime, timedelta | |
| # Set the app layout to wide | |
| st.set_page_config(layout="wide", page_title="Volatility Squeeze Detection") | |
| # Main page title and description | |
| st.title("Volatility Squeeze Detection") | |
| st.markdown(""" | |
| This tool automatically identifies volatility squeezes in real-time using a combination of Bollinger Bands, Keltner Channels, and various technical indicators. | |
| The methodology involves detecting periods of low volatility (squeeze) where the Bollinger Bands move inside the Keltner Channels. These squeezes often precede significant price movements. | |
| You can analyze both stock tickers and cryptocurrency pairs by entering the appropriate asset symbol, setting the parameters, and running the analysis. | |
| """) | |
| # Cache the stock/crypto data using the new st.cache_data with yfinance adjustments | |
| def get_stock_data(ticker, start_date, end_date): | |
| data = yf.download(ticker, start=start_date, end=end_date, 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_date} to {end_date}.") | |
| return data | |
| # Function Definitions | |
| def bollinger_bands(data, window=20, no_of_std=2): | |
| rolling_mean = data['Close'].rolling(window).mean() | |
| rolling_std = data['Close'].rolling(window).std() | |
| data['Bollinger_High'] = rolling_mean + (rolling_std * no_of_std) | |
| data['Bollinger_Low'] = rolling_mean - (rolling_std * no_of_std) | |
| return data | |
| def keltner_channel(data, window=20, atr_mult=1.5): | |
| data['Previous_Close'] = data['Close'].shift(1) | |
| data['TR'] = data[['High', 'Low', 'Previous_Close']].apply(lambda x: max(x[0] - x[1], abs(x[0] - x[2]), abs(x[1] - x[2])), axis=1) | |
| data['ATR'] = data['TR'].rolling(window).mean() | |
| rolling_mean = data['Close'].rolling(window).mean() | |
| data['Keltner_High'] = rolling_mean + (data['ATR'] * atr_mult) | |
| data['Keltner_Low'] = rolling_mean - (data['ATR'] * atr_mult) | |
| return data | |
| def moving_average_filter(data, window=50): | |
| data['MA'] = data['Close'].rolling(window=window).mean() | |
| return data | |
| def add_confirmation_indicators(data, rsi_window=14, macd_short=12, macd_long=26, macd_signal=9): | |
| data['RSI'] = 100 - (100 / (1 + data['Close'].diff(1).clip(lower=0).rolling(window=rsi_window).mean() / | |
| -data['Close'].diff(1).clip(upper=0).rolling(window=rsi_window).mean())) | |
| data['MACD'] = data['Close'].ewm(span=macd_short, adjust=False).mean() - data['Close'].ewm(span=macd_long, adjust=False).mean() | |
| data['MACD_Signal'] = data['MACD'].ewm(span=macd_signal, adjust=False).mean() | |
| return data | |
| def identify_volatility_squeeze(data, base_threshold=0.045, rsi_min=30, rsi_max=70, volatility_window=20): | |
| recent_volatility = data['ATR'].rolling(volatility_window).mean() | |
| dynamic_squeeze_threshold = base_threshold * recent_volatility / recent_volatility.mean() | |
| squeeze_on = (data['Bollinger_High'] < data['Keltner_High'] * (1 + dynamic_squeeze_threshold)) & \ | |
| (data['Bollinger_Low'] > data['Keltner_Low'] * (1 - dynamic_squeeze_threshold)) & \ | |
| (data['RSI'] > rsi_min) & (data['RSI'] < rsi_max) & \ | |
| (data['MACD'] > data['MACD_Signal']) & \ | |
| (abs(data['Close'] - data['MA']) / data['MA'] < 0.015) | |
| data['Squeeze'] = np.where(squeeze_on, 1, 0) | |
| return data | |
| def add_buy_sell_signals(data, buy_proximity_range, sell_proximity_range, n_days=5): | |
| best_buy_accuracy = -np.inf | |
| best_sell_accuracy = -np.inf | |
| best_buy_proximity = None | |
| best_sell_proximity = None | |
| for buy_proximity in buy_proximity_range: | |
| for sell_proximity in sell_proximity_range: | |
| data['Buy_Signal'] = np.where( | |
| (data['Squeeze'] == 1) & (data['Close'] >= data['Bollinger_High'].shift(1) * buy_proximity), 1, 0) | |
| data['Sell_Signal'] = np.where( | |
| (data['Squeeze'] == 1) & (data['Close'] <= data['Bollinger_Low'].shift(1) * sell_proximity), 1, 0) | |
| correct_buy_signals = sum(data['Buy_Signal'].iloc[i] == 1 and data['Close'].iloc[i + n_days] > data['Close'].iloc[i] for i in range(len(data) - n_days)) | |
| correct_sell_signals = sum(data['Sell_Signal'].iloc[i] == 1 and data['Close'].iloc[i + n_days] < data['Close'].iloc[i] for i in range(len(data) - n_days)) | |
| total_buy_signals = data['Buy_Signal'].sum() | |
| total_sell_signals = data['Sell_Signal'].sum() | |
| buy_accuracy = correct_buy_signals / total_buy_signals if total_buy_signals > 0 else 0 | |
| sell_accuracy = correct_sell_signals / total_sell_signals if total_sell_signals > 0 else 0 | |
| if buy_accuracy > best_buy_accuracy: | |
| best_buy_accuracy = buy_accuracy | |
| best_buy_proximity = buy_proximity | |
| if sell_accuracy > best_sell_accuracy: | |
| best_sell_accuracy = sell_accuracy | |
| best_sell_proximity = sell_proximity | |
| data['Buy_Signal'] = np.where( | |
| (data['Squeeze'] == 1) & (data['Close'] >= data['Bollinger_High'].shift(1) * best_buy_proximity), 1, 0) | |
| data['Sell_Signal'] = np.where( | |
| (data['Squeeze'] == 1) & (data['Close'] <= data['Bollinger_Low'].shift(1) * best_sell_proximity), 1, 0) | |
| return data, best_buy_accuracy, best_sell_accuracy, best_buy_proximity, best_sell_proximity | |
| def plot_volatility_squeeze_plotly(data, ticker, show_signals=False, best_buy_proximity=None, best_buy_accuracy=None, best_sell_proximity=None, best_sell_accuracy=None): | |
| fig = go.Figure() | |
| # Add volume to the chart on the secondary y-axis | |
| fig.add_trace(go.Bar(x=data.index, y=data['Volume'], name='Volume', marker_color='lightgray', opacity=0.3, yaxis='y2')) | |
| fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='Close Price')) | |
| fig.add_trace(go.Scatter(x=data.index, y=data['Bollinger_High'], mode='lines', name='Bollinger High', line=dict(dash='dash'))) | |
| fig.add_trace(go.Scatter(x=data.index, y=data['Bollinger_Low'], mode='lines', name='Bollinger Low', line=dict(dash='dash'))) | |
| fig.add_trace(go.Scatter(x=data.index, y=data['Keltner_High'], mode='lines', name='Keltner High', line=dict(dash='dash'))) | |
| fig.add_trace(go.Scatter(x=data.index, y=data['Keltner_Low'], mode='lines', name='Keltner Low', line=dict(dash='dash'))) | |
| fig.add_trace(go.Scatter(x=data.index, y=data['MA'], mode='lines', name='Moving Average', line=dict(dash='dot'))) | |
| squeeze_zones = data[data['Squeeze'] == 1] | |
| # Shade the squeeze areas by filling the area between the top and bottom Bollinger Bands during the squeeze | |
| for start, end in zip(squeeze_zones.index[:-1], squeeze_zones.index[1:]): | |
| if end - start == pd.Timedelta(days=1): # Ensure contiguous regions | |
| fig.add_vrect(x0=start, x1=end, fillcolor="LightSalmon", opacity=0.5, layer="below", line_width=0) | |
| # Add squeeze to the legend | |
| fig.add_trace(go.Scatter(x=[None], y=[None], mode='markers', | |
| marker=dict(size=10, color='LightSalmon'), | |
| legendgroup="Squeeze", | |
| showlegend=True, | |
| name="Squeeze")) | |
| if show_signals: | |
| buy_signals = data[data['Buy_Signal'] == 1] | |
| sell_signals = data[data['Sell_Signal'] == 1] | |
| fig.add_trace(go.Scatter(x=buy_signals.index, y=buy_signals['Close'], mode='markers', name=f'Buy Signal (Proximity: {best_buy_proximity}, Accuracy: {best_buy_accuracy:.2%})', marker=dict(symbol='triangle-up', color='green', size=10))) | |
| fig.add_trace(go.Scatter(x=sell_signals.index, y=sell_signals['Close'], mode='markers', name=f'Sell Signal (Proximity: {best_sell_proximity}, Accuracy: {best_sell_accuracy:.2%})', marker=dict(symbol='triangle-down', color='red', size=10))) | |
| fig.update_layout( | |
| title=f"Volatility Squeeze for {ticker} with Buy/Sell Signals", | |
| xaxis_title="Date", | |
| yaxis_title="Price", | |
| yaxis2=dict( | |
| title="Volume", | |
| overlaying='y', | |
| side='right', | |
| showgrid=False, | |
| ), | |
| width=1600, | |
| height=600, | |
| ) | |
| # Display the plot within a container | |
| with st.container(): | |
| st.plotly_chart(fig, use_container_width=True) | |
| # Sidebar layout | |
| st.sidebar.title("Input Parameters") | |
| with st.sidebar.expander("How to use", expanded=False): | |
| st.write("### How to Use") | |
| st.write(""" | |
| 1. Select the symbol (stock ticker or cryptocurrency pair), date range, and parameters. | |
| 2. Click 'Run Analysis' to generate the results. | |
| 3. Use the toggle inside the 'Trading Signals' section to display buy/sell signals. | |
| """) | |
| # Input widgets | |
| with st.sidebar.expander("Symbol Selection", expanded=True): # Open by default | |
| ticker = st.text_input("Asset Symbol", value="BTC-USD", help="Enter the stock or cryptocurrency symbol, e.g., 'AAPL' for Apple, 'BTC-USD' for Bitcoin.") | |
| start_date = st.date_input("Start Date", value=pd.to_datetime("2020-01-01"), help="Select the start date for the analysis.") | |
| end_date = st.date_input("End Date", value=datetime.now() + timedelta(days=1), help="Select the end date for the analysis.") | |
| with st.sidebar.expander("Analysis Parameters", expanded=True): | |
| window = st.slider("Rolling Window Size", min_value=10, max_value=100, value=20, step=1, help="Window size for the rolling calculations, e.g., Bollinger Bands.") | |
| atr_mult = st.slider("ATR Multiplier", min_value=1.0, max_value=3.0, value=1.5, step=0.1, help="Multiplier for the Average True Range in the Keltner Channel calculation.") | |
| base_threshold = st.slider("Squeeze Threshold", min_value=0.005, max_value=0.05, value=0.045, step=0.005, help="Threshold for determining a squeeze condition.") | |
| with st.sidebar.expander("Confirmation Indicators", expanded=False): | |
| rsi_window = st.slider("RSI Window", min_value=5, max_value=30, value=14, step=1, help="Window size for the Relative Strength Index (RSI) calculation.") | |
| macd_short = st.slider("MACD Short Period", min_value=5, max_value=20, value=12, step=1, help="Short period for the MACD calculation.") | |
| macd_long = st.slider("MACD Long Period", min_value=20, max_value=50, value=26, step=1, help="Long period for the MACD calculation.") | |
| macd_signal = st.slider("MACD Signal Period", min_value=5, max_value=20, value=9, step=1, help="Signal period for the MACD calculation.") | |
| # Trading signals expander with toggle and proximity settings | |
| with st.sidebar.expander("Trading Signals", expanded=False): | |
| show_trading_signals = st.checkbox("Show Trading Signals", value=False, help="Toggle to display indicative buy and sell signals based on a trend-following strategy using volatility squeezes and proximity to Bollinger Bands. Buy signals occur when a squeeze is detected, and the price breaks above the Bollinger High band, suggesting an upward trend. Sell signals occur when the price drops below the Bollinger Low band, indicating a downward trend. Both signals allow adjustable proximity to fine-tune sensitivity.") | |
| buy_proximity_range = st.multiselect( | |
| "Buy Proximity Range", | |
| options=[0.91, 0.93, 0.95, 0.97, 0.98, 0.99], | |
| default=[0.93, 0.95, 0.97], | |
| help="Select proximity values for buy signals relative to the Bollinger High band." | |
| ) | |
| sell_proximity_range = st.multiselect( | |
| "Sell Proximity Range", | |
| options=[1.01, 1.02, 1.03, 1.05, 1.07, 1.09], | |
| default=[1.03, 1.05, 1.07], | |
| help="Select proximity values for sell signals relative to the Bollinger Low band." | |
| ) | |
| n_days = st.slider("Signal Validation Days", min_value=1, max_value=20, value=5, help="Number of days to validate the buy/sell signals after they're generated.") | |
| # Run Button | |
| if st.sidebar.button('Run Analysis'): | |
| # Only run analysis if the input has changed | |
| if ( | |
| ticker != st.session_state.get('ticker_symbol') or | |
| start_date != st.session_state.get('start_date') or | |
| end_date != st.session_state.get('end_date') or | |
| window != st.session_state.get('window') or | |
| atr_mult != st.session_state.get('atr_mult') or | |
| base_threshold != st.session_state.get('base_threshold') or | |
| rsi_window != st.session_state.get('rsi_window') or | |
| macd_short != st.session_state.get('macd_short') or | |
| macd_long != st.session_state.get('macd_long') or | |
| macd_signal != st.session_state.get('macd_signal') or | |
| buy_proximity_range != st.session_state.get('buy_proximity_range') or | |
| sell_proximity_range != st.session_state.get('sell_proximity_range') or | |
| n_days != st.session_state.get('n_days') | |
| ): | |
| data = get_stock_data(ticker, start_date, end_date) | |
| data = bollinger_bands(data, window=window) | |
| data = keltner_channel(data, window=window, atr_mult=atr_mult) | |
| data = moving_average_filter(data, window=window) | |
| data = add_confirmation_indicators(data, rsi_window=rsi_window, macd_short=macd_short, macd_long=macd_long, macd_signal=macd_signal) | |
| data = identify_volatility_squeeze(data, base_threshold=base_threshold) | |
| data, best_buy_accuracy, best_sell_accuracy, best_buy_proximity, best_sell_proximity = add_buy_sell_signals( | |
| data, | |
| buy_proximity_range, | |
| sell_proximity_range, | |
| n_days | |
| ) | |
| # Save results to session state | |
| st.session_state['data'] = data | |
| st.session_state['ticker_symbol'] = ticker | |
| st.session_state['start_date'] = start_date | |
| st.session_state['end_date'] = end_date | |
| st.session_state['window'] = window | |
| st.session_state['atr_mult'] = atr_mult | |
| st.session_state['base_threshold'] = base_threshold | |
| st.session_state['rsi_window'] = rsi_window | |
| st.session_state['macd_short'] = macd_short | |
| st.session_state['macd_long'] = macd_long | |
| st.session_state['macd_signal'] = macd_signal | |
| st.session_state['buy_proximity_range'] = buy_proximity_range | |
| st.session_state['sell_proximity_range'] = sell_proximity_range | |
| st.session_state['n_days'] = n_days | |
| st.session_state['best_buy_proximity'] = best_buy_proximity | |
| st.session_state['best_buy_accuracy'] = best_buy_accuracy | |
| st.session_state['best_sell_proximity'] = best_sell_proximity | |
| st.session_state['best_sell_accuracy'] = best_sell_accuracy | |
| # Plot the results | |
| if 'data' in st.session_state: | |
| data = st.session_state['data'] | |
| plot_volatility_squeeze_plotly( | |
| data, | |
| ticker, | |
| show_signals=show_trading_signals, | |
| best_buy_proximity=st.session_state.get('best_buy_proximity'), | |
| best_buy_accuracy=st.session_state.get('best_buy_accuracy'), | |
| best_sell_proximity=st.session_state.get('best_sell_proximity'), | |
| best_sell_accuracy=st.session_state.get('best_sell_accuracy') | |
| ) |