@@ -46,6 +46,111 @@ def bollinger_bands(
4646 )
4747
4848
49+ def bollinger_overshoot (
50+ data : Union [PdDataFrame , PlDataFrame ],
51+ source_column = 'Close' ,
52+ period = 20 ,
53+ std_dev = 2 ,
54+ result_column = 'bollinger_overshoot'
55+ ) -> Union [PdDataFrame , PlDataFrame ]:
56+ """
57+ Calculate Bollinger Band overshoot percentage for a price series.
58+
59+ Measures how far the price has exceeded the upper or lower band,
60+ expressed as a percentage of the half-band width (distance from
61+ middle to upper/lower band).
62+
63+ Calculation:
64+ - When price > upper band (bullish overshoot):
65+ Overshoot % = ((Price - Upper Band) / (Upper Band - Middle Band)) × 100
66+ - When price < lower band (bearish overshoot):
67+ Overshoot % = ((Price - Lower Band) / (Middle Band - Lower Band)) × 100
68+ - When price is within bands: 0%
69+
70+ Example interpretation:
71+ - A 40% overshoot means the price is 40% of the band width beyond the band
72+ - Positive values indicate overbought conditions (above upper band)
73+ - Negative values indicate oversold conditions (below lower band)
74+ - High overshoots (e.g., 40% for silver) indicate risk of mean reversion
75+
76+ Args:
77+ data: pandas or polars DataFrame with price data
78+ source_column: Column name containing the price data (default: 'Close')
79+ period: Rolling window period for calculation (default: 20)
80+ std_dev: Number of standard deviations for bands (default: 2)
81+ result_column: Name for the result column
82+ (default: 'bollinger_overshoot')
83+
84+ Returns:
85+ DataFrame with added overshoot percentage column
86+ """
87+ # First calculate the bands
88+ data = bollinger_bands (
89+ data ,
90+ source_column = source_column ,
91+ period = period ,
92+ std_dev = std_dev ,
93+ middle_band_column_result_column = 'BB_middle_temp' ,
94+ upper_band_column_result_column = 'BB_upper_temp' ,
95+ lower_band_column_result_column = 'BB_lower_temp'
96+ )
97+
98+ if isinstance (data , PdDataFrame ):
99+ import numpy as np
100+
101+ price = data [source_column ]
102+ upper = data ['BB_upper_temp' ]
103+ middle = data ['BB_middle_temp' ]
104+ lower = data ['BB_lower_temp' ]
105+
106+ # Calculate half-band width (same for upper and
107+ # lower with symmetric bands)
108+ half_band_width = upper - middle
109+
110+ # Calculate overshoot
111+ # Above upper band: positive overshoot
112+ # Below lower band: negative overshoot
113+ # Within bands: 0
114+ overshoot = np .where (
115+ price > upper ,
116+ ((price - upper ) / half_band_width ) * 100 ,
117+ np .where (
118+ price < lower ,
119+ ((price - lower ) / half_band_width ) * 100 ,
120+ 0.0
121+ )
122+ )
123+
124+ data [result_column ] = overshoot
125+
126+ # Drop temporary columns
127+ data = data .drop (
128+ columns = ['BB_middle_temp' , 'BB_upper_temp' , 'BB_lower_temp' ]
129+ )
130+ return data
131+
132+ elif isinstance (data , PlDataFrame ):
133+ half_band_width = pl .col ('BB_upper_temp' ) - pl .col ('BB_middle_temp' )
134+
135+ overshoot = pl .when (pl .col (source_column ) >
136+ pl .col ('BB_upper_temp' )).then (
137+ ((pl .col (source_column ) -
138+ pl .col ('BB_upper_temp' )) / half_band_width ) * 100
139+ ).when (pl .col (source_column ) < pl .col ('BB_lower_temp' )).then (
140+ ((pl .col (source_column ) -
141+ pl .col ('BB_lower_temp' )) / half_band_width ) * 100
142+ ).otherwise (0.0 )
143+
144+ return data .with_columns (
145+ overshoot .alias (result_column )
146+ ).drop (['BB_middle_temp' , 'BB_upper_temp' , 'BB_lower_temp' ])
147+
148+ else :
149+ raise PyIndicatorException (
150+ "Input data must be a pandas or polars DataFrame."
151+ )
152+
153+
49154def bollinger_width (
50155 data : Union [PdDataFrame , PlDataFrame ],
51156 source_column = 'Close' ,
0 commit comments