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')
    )