Space32 / app.py
QuantumLearner's picture
Update app.py
5ed8a97 verified
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')
)