Skip to content

Commit 57ece34

Browse files
committed
fix(indicators): Improve FVG detection using three-candle pattern
- Changed from simple gap detection to proper three-candle pattern - Increased detection rate from 2 to 304 gaps on 25-day MNQ data - Added min_gap_percent parameter for percentage-based filtering - Added fvg_gap_percent column showing gap size as percentage - Improved mitigation logic to properly track gap fills - Updated documentation to reflect Smart Money Concepts approach The FVG indicator now properly identifies price imbalance zones that are more likely to act as support/resistance levels, making it much more useful for trading strategies. Recommended settings for futures: - Min gap size: 0.5x average bar range - Min gap percent: 0.02-0.05% - Mitigation threshold: 50%
1 parent 6f91384 commit 57ece34

File tree

2 files changed

+77
-36
lines changed

2 files changed

+77
-36
lines changed

src/project_x_py/indicators/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -931,16 +931,18 @@ def FVG(
931931
low_column: str = "low",
932932
close_column: str = "close",
933933
min_gap_size: float = 0.0,
934+
min_gap_percent: float = 0.0,
934935
check_mitigation: bool = False,
935936
mitigation_threshold: float = 0.5,
936937
) -> pl.DataFrame:
937-
"""Fair Value Gap (TA-Lib style)."""
938+
"""Fair Value Gap (TA-Lib style) - uses three-candle pattern."""
938939
return calculate_fvg(
939940
data,
940941
high_column=high_column,
941942
low_column=low_column,
942943
close_column=close_column,
943944
min_gap_size=min_gap_size,
945+
min_gap_percent=min_gap_percent,
944946
check_mitigation=check_mitigation,
945947
mitigation_threshold=mitigation_threshold,
946948
)

src/project_x_py/indicators/fvg.py

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)