@@ -60,13 +60,15 @@ def calculate(
6060 ** kwargs : Any ,
6161 ) -> pl .DataFrame :
6262 """
63- Calculate Fair Value Gaps (FVG).
63+ Calculate Fair Value Gaps (FVG) using three-candle pattern .
6464
6565 A bullish FVG occurs when:
66- - Current candle's low > Previous candle's high (gap up)
66+ - Candle 3's low > Candle 1's high (creating an unfilled gap)
67+ - This gap represents an area of imbalance
6768
6869 A bearish FVG occurs when:
69- - Current candle's high < Previous candle's low (gap down)
70+ - Candle 3's high < Candle 1's low (creating an unfilled gap)
71+ - This gap represents an area of imbalance
7072
7173 Args:
7274 data: DataFrame with OHLC data
@@ -75,18 +77,20 @@ def calculate(
7577 low_column: Low price column (default: "low")
7678 close_column: Close price column (default: "close")
7779 min_gap_size: Minimum gap size (in price units) to consider valid (default: 0.0)
80+ min_gap_percent: Minimum gap size as percentage (default: 0.0)
7881 check_mitigation: Whether to check if gaps have been mitigated (default: False)
7982 mitigation_threshold: Percentage of gap that needs to be filled to consider it mitigated (default: 0.5)
8083
8184 Returns:
8285 DataFrame with FVG columns added:
8386 - fvg_bullish: Boolean indicating bullish FVG
8487 - fvg_bearish: Boolean indicating bearish FVG
85- - fvg_bullish_start: Start of bullish gap (previous high)
86- - fvg_bullish_end: End of bullish gap (current low)
87- - fvg_bearish_start: Start of bearish gap (previous low)
88- - fvg_bearish_end: End of bearish gap (current high)
88+ - fvg_bullish_start: Start of bullish gap (candle 1 high)
89+ - fvg_bullish_end: End of bullish gap (candle 3 low)
90+ - fvg_bearish_start: Start of bearish gap (candle 1 low)
91+ - fvg_bearish_end: End of bearish gap (candle 3 high)
8992 - fvg_gap_size: Size of the gap
93+ - fvg_gap_percent: Gap size as percentage
9094 - fvg_mitigated: Boolean indicating if gap has been mitigated (if check_mitigation=True)
9195
9296 Example:
@@ -99,47 +103,52 @@ def calculate(
99103 low_column = kwargs .get ("low_column" , "low" )
100104 close_column = kwargs .get ("close_column" , "close" )
101105 min_gap_size = kwargs .get ("min_gap_size" , 0.0 )
106+ min_gap_percent = kwargs .get ("min_gap_percent" , 0.0 )
102107 check_mitigation = kwargs .get ("check_mitigation" , False )
103108 mitigation_threshold = kwargs .get ("mitigation_threshold" , 0.5 )
104109
105110 required_cols : list [str ] = [high_column , low_column , close_column ]
106111 self .validate_data (data , required_cols )
107- self .validate_data_length (data , 2 ) # Need at least 2 candles
112+ self .validate_data_length (data , 3 ) # Need at least 3 candles for pattern
108113
109- # Get shifted values for comparison
114+ # Get values for three-candle pattern
110115 result = data .with_columns (
111116 [
112- # Previous candle values
113- pl .col (high_column ).shift (1 ).alias ("prev_high" ),
114- pl .col (low_column ).shift (1 ).alias ("prev_low" ),
117+ # Candle 1 (two bars ago)
118+ pl .col (high_column ).shift (2 ).alias ("candle1_high" ),
119+ pl .col (low_column ).shift (2 ).alias ("candle1_low" ),
120+ # Candle 2 (previous bar) - not used in gap detection but kept for potential future use
121+ pl .col (high_column ).shift (1 ).alias ("candle2_high" ),
122+ pl .col (low_column ).shift (1 ).alias ("candle2_low" ),
123+ # Candle 3 is current bar
115124 ]
116125 )
117126
118- # Identify FVGs (gaps between consecutive bars)
127+ # Identify FVGs using three-candle pattern
119128 result = result .with_columns (
120129 [
121- # Bullish FVG: current low > prev high (gap up)
122- (pl .col (low_column ) > pl .col ("prev_high " )).alias ("fvg_bullish_raw" ),
123- # Bearish FVG: current high < prev low (gap down)
124- (pl .col (high_column ) < pl .col ("prev_low " )).alias ("fvg_bearish_raw" ),
130+ # Bullish FVG: current low > candle 1 high
131+ (pl .col (low_column ) > pl .col ("candle1_high " )).alias ("fvg_bullish_raw" ),
132+ # Bearish FVG: current high < candle 1 low
133+ (pl .col (high_column ) < pl .col ("candle1_low " )).alias ("fvg_bearish_raw" ),
125134 ]
126135 )
127136
128137 # Calculate gap boundaries and size
129138 result = result .with_columns (
130139 [
131- # Bullish gap: from prev high to current low
140+ # Bullish gap: from candle 1 high to current low
132141 pl .when (pl .col ("fvg_bullish_raw" ))
133- .then (pl .col ("prev_high " ))
142+ .then (pl .col ("candle1_high " ))
134143 .otherwise (None )
135144 .alias ("fvg_bullish_start" ),
136145 pl .when (pl .col ("fvg_bullish_raw" ))
137146 .then (pl .col (low_column ))
138147 .otherwise (None )
139148 .alias ("fvg_bullish_end" ),
140- # Bearish gap: from prev low to current high
149+ # Bearish gap: from candle 1 low to current high
141150 pl .when (pl .col ("fvg_bearish_raw" ))
142- .then (pl .col ("prev_low " ))
151+ .then (pl .col ("candle1_low " ))
143152 .otherwise (None )
144153 .alias ("fvg_bearish_start" ),
145154 pl .when (pl .col ("fvg_bearish_raw" ))
@@ -149,26 +158,49 @@ def calculate(
149158 ]
150159 )
151160
152- # Calculate gap size
161+ # Calculate gap size and percentage
153162 result = result .with_columns (
154163 [
155164 pl .when (pl .col ("fvg_bullish_raw" ))
156165 .then ((pl .col ("fvg_bullish_end" ) - pl .col ("fvg_bullish_start" )).abs ())
157166 .when (pl .col ("fvg_bearish_raw" ))
158167 .then ((pl .col ("fvg_bearish_start" ) - pl .col ("fvg_bearish_end" )).abs ())
159168 .otherwise (None )
160- .alias ("fvg_gap_size" )
169+ .alias ("fvg_gap_size" ),
170+ # Calculate gap as percentage of price
171+ pl .when (pl .col ("fvg_bullish_raw" ))
172+ .then (
173+ (
174+ (pl .col ("fvg_bullish_end" ) - pl .col ("fvg_bullish_start" )).abs ()
175+ / pl .col ("fvg_bullish_start" )
176+ * 100
177+ )
178+ )
179+ .when (pl .col ("fvg_bearish_raw" ))
180+ .then (
181+ (
182+ (pl .col ("fvg_bearish_start" ) - pl .col ("fvg_bearish_end" )).abs ()
183+ / pl .col ("fvg_bearish_start" )
184+ * 100
185+ )
186+ )
187+ .otherwise (None )
188+ .alias ("fvg_gap_percent" ),
161189 ]
162190 )
163191
164- # Apply minimum gap size filter
192+ # Apply minimum gap size and percentage filters
165193 result = result .with_columns (
166194 [
167195 (
168- pl .col ("fvg_bullish_raw" ) & (pl .col ("fvg_gap_size" ) >= min_gap_size )
196+ pl .col ("fvg_bullish_raw" )
197+ & (pl .col ("fvg_gap_size" ) >= min_gap_size )
198+ & (pl .col ("fvg_gap_percent" ) >= min_gap_percent )
169199 ).alias ("fvg_bullish" ),
170200 (
171- pl .col ("fvg_bearish_raw" ) & (pl .col ("fvg_gap_size" ) >= min_gap_size )
201+ pl .col ("fvg_bearish_raw" )
202+ & (pl .col ("fvg_gap_size" ) >= min_gap_size )
203+ & (pl .col ("fvg_gap_percent" ) >= min_gap_percent )
172204 ).alias ("fvg_bearish" ),
173205 ]
174206 )
@@ -201,26 +233,28 @@ def calculate(
201233 # Look at subsequent candles for mitigation
202234 future_data = result .filter (pl .col ("_row_idx" ) > gap_idx )
203235
204- if is_bullish :
236+ if is_bullish and row [ "fvg_bullish_start" ] is not None :
205237 gap_start = row ["fvg_bullish_start" ]
206238 gap_end = row ["fvg_bullish_end" ]
207239 gap_size = gap_end - gap_start
208240 mitigation_amount = gap_size * mitigation_threshold
209- # Bullish gap is mitigated when price goes back below gap_end - mitigation_amount
210- mitigation_level = gap_end - mitigation_amount
241+ # Bullish gap is mitigated when price retraces into the gap
242+ mitigation_level = gap_start + mitigation_amount
211243 mitigated_rows = future_data .filter (
212244 pl .col (low_column ) <= mitigation_level
213245 )
214- else :
246+ elif not is_bullish and row [ "fvg_bearish_start" ] is not None :
215247 gap_start = row ["fvg_bearish_start" ]
216248 gap_end = row ["fvg_bearish_end" ]
217249 gap_size = gap_start - gap_end
218250 mitigation_amount = gap_size * mitigation_threshold
219- # Bearish gap is mitigated when price goes back above gap_end + mitigation_amount
220- mitigation_level = gap_end + mitigation_amount
251+ # Bearish gap is mitigated when price retraces into the gap
252+ mitigation_level = gap_start - mitigation_amount
221253 mitigated_rows = future_data .filter (
222254 pl .col (high_column ) >= mitigation_level
223255 )
256+ else :
257+ continue
224258
225259 if len (mitigated_rows ) > 0 :
226260 mitigated [gap_idx ] = True
@@ -242,8 +276,10 @@ def calculate(
242276
243277 # Clean up intermediate columns
244278 columns_to_drop : list [str ] = [
245- "prev_high" ,
246- "prev_low" ,
279+ "candle1_high" ,
280+ "candle1_low" ,
281+ "candle2_high" ,
282+ "candle2_low" ,
247283 "fvg_bullish_raw" ,
248284 "fvg_bearish_raw" ,
249285 ]
@@ -259,11 +295,12 @@ def calculate_fvg(
259295 low_column : str = "low" ,
260296 close_column : str = "close" ,
261297 min_gap_size : float = 0.0 ,
298+ min_gap_percent : float = 0.0 ,
262299 check_mitigation : bool = False ,
263300 mitigation_threshold : float = 0.5 ,
264301) -> pl .DataFrame :
265302 """
266- Calculate Fair Value Gaps (convenience function).
303+ Calculate Fair Value Gaps using three-candle pattern (convenience function).
267304
268305 See FVG.calculate() for detailed documentation.
269306
@@ -273,6 +310,7 @@ def calculate_fvg(
273310 low_column: Low price column
274311 close_column: Close price column
275312 min_gap_size: Minimum gap size to consider valid
313+ min_gap_percent: Minimum gap size as percentage
276314 check_mitigation: Whether to check if gaps have been mitigated
277315 mitigation_threshold: Percentage of gap that needs to be filled to consider it mitigated
278316
@@ -286,6 +324,7 @@ def calculate_fvg(
286324 low_column = low_column ,
287325 close_column = close_column ,
288326 min_gap_size = min_gap_size ,
327+ min_gap_percent = min_gap_percent ,
289328 check_mitigation = check_mitigation ,
290329 mitigation_threshold = mitigation_threshold ,
291330 )
0 commit comments