Spaces:
Sleeping
Sleeping
File size: 15,120 Bytes
c77c6ed b1d7baf c77c6ed b1d7baf c77c6ed 5ed8a97 c77c6ed c56468c c77c6ed 5b069b3 c77c6ed c56468c c77c6ed c56468c c77c6ed c56468c c77c6ed b1d7baf | 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 | 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
@st.cache_data
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')
) |