Module 5 — Algorithmic trading · Lesson 21 of 23 · 12 min
Coding swings and fair value gaps in Python
Two clean pandas functions to detect swings and FVGs, with attention to look-ahead.
Data format
We assume a pandas DataFrame with a time index and open, high, low, close columns, sorted from oldest to newest. The code is educational and must be adapted to your data.
Swing highs and swing lows
Careful: a swing with n candles on the right is only known n candles later. If you use it as a signal at candle i, you are looking into the future. That is why the function also returns the confirmation index.
import pandas as pd
def find_swings(df: pd.DataFrame, n: int = 3) -> pd.DataFrame:
"""Swing high/low with n candles per side.
'confirmed_at' is the position at which the swing becomes known (i + n)."""
swings = []
highs = df["high"].values
lows = df["low"].values
for i in range(n, len(df) - n):
left_h, right_h = highs[i - n:i], highs[i + 1:i + n + 1]
left_l, right_l = lows[i - n:i], lows[i + 1:i + n + 1]
if highs[i] > left_h.max() and highs[i] > right_h.max():
swings.append({"pos": i, "type": "high", "price": highs[i], "confirmed_at": i + n})
if lows[i] < left_l.min() and lows[i] < right_l.min():
swings.append({"pos": i, "type": "low", "price": lows[i], "confirmed_at": i + n})
return pd.DataFrame(swings)Fair value gap
The FVG is confirmed at the close of the third candle: there is no need to look further, so there is no look-ahead.
def find_fvg(df: pd.DataFrame, min_size: float = 0.0) -> pd.DataFrame:
"""Detects three-candle FVGs. min_size is in price units."""
gaps = []
for i in range(2, len(df)):
c1_high, c1_low = df["high"].iloc[i - 2], df["low"].iloc[i - 2]
c3_high, c3_low = df["high"].iloc[i], df["low"].iloc[i]
if c3_low - c1_high > min_size: # bullish FVG
gaps.append({"pos": i, "type": "bullish", "low": c1_high, "high": c3_low})
elif c1_low - c3_high > min_size: # bearish FVG
gaps.append({"pos": i, "type": "bearish", "low": c3_high, "high": c1_low})
return pd.DataFrame(gaps)Minimum size
A gap of a tenth of a pip is noise. Use a threshold tied to volatility, for example a fraction of the 14-period ATR computed up to the previous candle.
def atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
prev_close = df["close"].shift(1)
tr = pd.concat([
df["high"] - df["low"],
(df["high"] - prev_close).abs(),
(df["low"] - prev_close).abs(),
], axis=1).max(axis=1)
return tr.rolling(period).mean()How to continue
- Build a function that, at each candle, uses only the swings with confirmed_at less than or equal to the current candle
- Generate the signal (for example a return into the FVG after the sweep) and store it in a Series of +1, -1 and 0
- Pass the signal to the backtest in the next lesson
Have a question or want to share your exercise?
Post in the community, or join the free signals room on Telegram.
Educational content, not financial advice. Trading involves risk. ICT is a term referring to the materials of Michael Huddleston: this course is independent and not affiliated.