Volatility-scaled fair value gap analysis for detecting bull/bear swings and dynamic stop-losses in Python.
Fair value gaps mark points where price moves quickly and leave a gap between buyers and sellers.
These gaps can signal support / resistance zones and bullish / bearish swings. This is especially so when the gaps are scaled by volatilility.
With the method presented here, we get two key lines on the chart: one for potential support, one for potential resistance.
When price crosses above the resistance line, it signals bullish control. If price falls below the support line, bearish momentum takes over.
The complete Python notebook for the analysis is provided below.

1. How FVG Trailing Stop Works
A fair value gap is a section of the chart where price moves so fast, it skips over certain levels. No trades happen in that range.
The market leaves behind ‘unfinished business’, i.e. zones where aggressive buyers or sellers overwhelmed the other side.
In practical terms:
A fair value gap forms when the low of a bar is higher than the high of the previous bar (up-gap), or the high of a bar is lower than the low of the previous bar (down-gap):
In liquid markets, this type of gap shows strong and sudden imbalance, usually after important news, a breakout, or heavy volume.
These gaps are magnets for price. The market often returns to “fill” them.
Figure 1. Fair Value Gap Analysis: Price gaps up, leaves an unfilled zone, then drifts back to fill it.
But not all gaps matter equally.
Gaps in quiet markets are common noise. The signal comes when a gap forms and volatility is high.
That’s why we scale gap detection by volatility, using ATR.
Volatility-Adjusted Gap Memory
To avoid treating every gap the same, the method adapts how long a gap “counts” as significant.
The gap memory length changes as volatility changes:
- When ATR is above normal, the method remembers gaps longer.
- When ATR drops, it forgets gaps faster.
This focuses attention on gaps formed during meaningful price moves.
Tracking Swings and Marking Levels
The algorithm looks for two types of fair value gaps:
- Bullish gap (signals a burst of buying strength).
- Bearish gap (signals a flush of selling pressure).
Each time a new gap appears, it’s added to the list of active levels. If price returns and “fills” a gap, that level drops off the list.
Constructing the Support and Resistance Lines
For each bar, the method takes the average of all active bullish gaps (potential support) and the average of all active bearish gaps (potential resistance).
Both lines are smoothed with an exponential moving average:
where

Regime Detection: When Price Crosses a Line
- If price closes above the resistance line, it signals bullish control.
- If price closes below the support line, bearish momentum takes over.
Say NVDA gaps up after earnings, jumping $12 above the previous day’s high while ATR is high.
The method marks this as a new resistance. If price holds above the resistance line, it stays in a bullish regime.
If price falls and closes below the support line (maybe on a high-volume reversal), the signal flips to bearish.
2. FVG Trailing Stops in Python
2.1 Setting the Parameters
First, import the necessary libraries.
Then, we set key parameters to control how sensitive the method is, how wide stops are, and how volatility scaling works.
- Increase ATR_LEN: fewer, smoother gaps.
- Increase BASE_FVG_LEN: gaps last longer, regime flips are slower.
- Increase SMOOTH_LEN: fewer whipsaws.
- Increase GAP_Z: only large gaps count.
- Increase ENV_MULT: wider stops, looser risk.
- Increase VOL_SENSITIVITY: memory expands more when volatility rises.
# Imports
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import yfinance as yf
# Parameters — tweak here
TICKER = "NVDA"
PERIOD = "2y" # e.g. "60d", "1y"
INTERVAL = "1d" # e.g. "15m", "1d"
ATR_LEN = 20 # ↑ smoother ATR, fewer gaps; ↓ faster ATR, more gaps
BASE_FVG_LEN = 5 # ↑ gaps persist, slower flips; ↓ gaps decay, quicker flips
SMOOTH_LEN = 9 # ↑ slower displacement, fewer whipsaws; ↓ faster, more whipsaws
GAP_Z = 0.8 # ↑ only big gaps count; ↓ small gaps count too
ENV_MULT = 0.6 # ↑ wider stop, looser risk; ↓ tighter stop, quicker exits
VOL_SENSITIVITY = 0.5 # ↑ memory expands with vol; ↓ memory stays near base
SHOW_INACTIVE_SIDE = False # True shows both lines; False hides inactive line
2.2 Getting the Data Function
The download function pulls clean OHLCV data with yfinance.
Can be daily, hourly, etc.
def download_ohlc(ticker: str, period: str, interval: str) -> pd.DataFrame:
df = yf.download(
ticker, period=period, interval=interval,
auto_adjust=True, progress=False
)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.columns = df.columns.map(str.title)
return df.dropna(subset=["Open", "High", "Low", "Close"])
2.3 Technical Indicator Functions
ATR and EMA are the core indicators. ATR measures recent volatility. EMA smooths noisy levels.
Newsletter