Skip to content

Python

Price Consolidation Boxes: Ranges, Breakouts, and Retests Using Python

3 February 20268 min readPython
FabTrader author portrait

FabTrader

Article overview

Markets don’t trend most of the time — they pause, compress, and consolidate. Before every meaningful move up or down, price typically spends weeks locked inside a tight range where volatility contracts and both buyers and sellers reach a temporary balance. This article introduces a rule-based price consolidation box framework that objectively identifies these ranges, tracks their evolution, and detects breakouts, breakdowns, and post-breakout retests using pure price and volatility. Unlike subjective chart patterns or classical Darvas Boxes, this approach relies on clearly defined rules that make it suitable for screeners, backtests, and automated analysis. Using a practical Python implementation, we walk through how consolidation ranges are formed, validated, extended, and finally resolved — helping you spot high-quality expansion opportunities before the crowd reacts.

Markets spend far more time moving sideways than trending. Before every strong trend, there is usually a phase where price:

  • compresses into a tight range
  • absorbs supply and demand
  • prepares for a directional expansion

This article explains a rule-based method to identify such price consolidation boxes, track their evolution, and detect:

  • Breakouts / Breakdowns
  • False moves
  • Support–Resistance retests after breakout

We’ll also walk through a fully working Python implementation that you can use for:

  • screeners
  • backtests
  • dashboards
  • alerts

1️⃣ What is a Price Consolidation Box?

A price consolidation box is a well-defined price range where:

  • Price oscillates between a clear high and low
  • Volatility contracts
  • Neither buyers nor sellers dominate decisively

Visually, this looks like price being “boxed in” between two horizontal levels.

Key characteristics of a valid consolidation:

  1. Multiple candles respect the same high and low
  2. The range does not expand aggressively
  3. Volatility remains contained
  4. Breakout happens on a closing basis

This is not the classical Darvas Box — instead, it is a practical, rule-driven consolidation range suitable for systematic trading and screening.

How to access the corresponding indicator on TradingView?

Search for 'Darvas Lines/Box' indicator on TradingView and you will find it.

2️⃣ Core Idea Behind the Algorithm

The logic is built as a state machine that processes price candle-by-candle.

At any point, the system is in one of four phases:

PhaseMeaningPhase 0Searching for a valid consolidation rangePhase 1Range defined, waiting for confirmationPhase 2Actively monitoring for breakout or range expansionPhase 3Breakout occurred, cooling off and retest monitoring

This ensures:

  • One box at a time
  • No repainting
  • Clean lifecycle for each consolidation

3️⃣ Step-by-Step: How the Consolidation Box is Identified

🔹 Step 1: Define the Initial Range

For every new candle, the algorithm looks back at the last N candles (default: 10) and computes:

  • Highest high
  • Lowest low

You can choose whether these levels come from:

  • Wicks (High / Low)
  • Candle body (Open / Close)
Range High = max(highs over last N bars)
Range Low  = min(lows over last N bars)

🔹 Step 2: Validate the Range Using Volatility (ATR Filter)

Not all ranges are meaningful.

To filter out wide, noisy ranges, we compare the box height with volatility:

Box Height ≤ ATR × Max Multiplier

This ensures:

  • The range is tight
  • Price is genuinely consolidating
  • Breakouts are meaningful

🔹 Step 3: Allow Range Extension (Before Breakout)

Markets are rarely perfect.

Before a breakout happens, price may:

  • slightly push the high
  • slightly dip the low

Instead of discarding the box immediately, the algorithm:

  • extends the range if a new extreme appears
  • keeps monitoring as long as no breakout occurs

This makes the box adaptive but disciplined.

4️⃣ Breakout & Breakdown Detection

A breakout is confirmed only when price closes outside the range.

  • Bullish Breakout: Close > Range High
  • Bearish Breakdown: Close < Range Low

Wick-based moves are ignored — this avoids many false signals.

Once a breakout occurs:

  • The box is finalized
  • The breakout direction is recorded
  • The system enters a cooldown phase

5️⃣ Retest Logic (Post-Breakout Validation)

Strong breakouts often retest the broken level before continuing.

This system checks:

  • A fixed number of candles after breakout (default: 20)
  • Whether price comes close to the breakout level
  • Without violating it decisively

Retest rules:

  • For bullish breakouts:
    • Price dips near former resistance (now support)
  • For bearish breakdowns:
    • Price rallies near former support (now resistance)

A configurable percentage gap defines how close price must come to count as a retest.

This helps distinguish:

  • strong breakouts
  • weak or failed breakouts

6️⃣ Cooldown Phase: Avoiding Overlapping Boxes

After a breakout:

  • The system waits for a fixed number of candles (default: 13)
  • No new box is allowed during this period

This prevents:

  • overlapping consolidations
  • cluttered signals
  • overtrading

Once the cooldown ends, the system resets and looks for the next consolidation.

7️⃣ What the Python Code Produces

Instead of drawing chart objects, the Python implementation outputs structured data:

Each detected consolidation produces:

Start Date
End Date
Range High
Range Low
Breakout Direction (UP / DOWN)
Retest Confirmed (True / False)
This design makes the system:

  • backtest-friendly
  • scanner-ready
  • easy to integrate with alerts or dashboards

8️⃣ Why This Works Well in Real Markets

This approach works because it aligns with market structure:

  • Consolidation = balance
  • Breakout = imbalance
  • Retest = acceptance or rejection

It avoids:

  • indicator noise
  • subjective pattern drawing
  • hindsight bias

And focuses purely on:

  • price
  • volatility
  • structure

9️⃣ Full Python Implementation

Below is the complete Python implementation used to detect these consolidation boxes:

# -------------------------------------------------------------------------
#              FabTrader Algorithmic Trading - Tutorials
# -------------------------------------------------------------------------
# CONTACT:
# - Website: https://fabtrader.in
# - Email: [email protected]
#
# Usage: Educational Purposes & training use only. Not for commercial redistribution.
# ------------------------------------------------------------------------

import pandas as pd
from datetime import date, timedelta


def get_historical_data(symbol, start_date, end_date, interval):
    """
    Placeholder function. Replace this with your function to supply
    historical candle stick data
    Must return a Pandas DataFrame with:
    index  : Date (datetime)
    columns: Open, High, Low, Close, Volume
    """
    raise NotImplementedError


def calculate_atr(df, length=14):
    high = df['High']
    low = df['Low']
    close = df['Close']

    prev_close = close.shift(1)

    tr = pd.concat([
        high - low,
        (high - prev_close).abs(),
        (low - prev_close).abs()
    ], axis=1).max(axis=1)

    atr = tr.rolling(length).mean()
    return atr


def darvas_box(
    symbol,
    start_date,
    end_date,
    interval="1W",
    range_len=10,
    cooldown_bars=13,
    atr_len=14,
    max_atr_mult=3.0,
    retest_bars=20,
    retest_max_gap_pct=1.0,
    line_source="WICKS"  # or "BODY"
):
    df = get_historical_data(symbol, start_date, end_date, interval)
    df = df.copy()

    # Source high / low
    if line_source == "BODY":
        df['SourceHigh'] = df[['Open', 'Close']].max(axis=1)
        df['SourceLow'] = df[['Open', 'Close']].min(axis=1)
    else:
        df['SourceHigh'] = df['High']
        df['SourceLow'] = df['Low']

    df['ATR'] = calculate_atr(df, atr_len)

    results = []

    phase = 0
    armed_start_idx = None
    cooldown_start_idx = None

    range_high = None
    range_low = None
    final_high = None
    final_low = None

    breakout_up = False
    breakout_down = False
    is_retested = False

    for i in range(len(df)):
        row = df.iloc[i]

        # =========================
        # PHASE 0 — Define Range
        # =========================
        if phase == 0 and i >= range_len:
            window = df.iloc[i - range_len:i]

            hh = window['SourceHigh'].max()
            ll = window['SourceLow'].min()

            box_height = hh - ll
            atr_val = row['ATR']

            if pd.notna(atr_val) and box_height <= atr_val * max_atr_mult:
                range_high = hh
                range_low = ll
                final_high = hh
                final_low = ll
                armed_start_idx = i - range_len
                phase = 1
                phase0_idx = i

        # =========================
        # PHASE 1 — Wait 1 bar
        # =========================
        elif phase == 1 and i > phase0_idx:
            phase = 2

        # =========================
        # PHASE 2 — Breakout or Extend
        # =========================
        elif phase == 2:
            close = row['Close']

            breakout_up = close > range_high
            breakout_down = close < range_low

            if breakout_up or breakout_down:
                # Breakout confirmed
                cooldown_start_idx = i
                phase = 3
                is_retested = False

                results.append({
                    "symbol": symbol,
                    "start_date": df.index[armed_start_idx],
                    "end_date": df.index[i],
                    "high": final_high,
                    "low": final_low,
                    "breakout": "UP" if breakout_up else "DOWN",
                    "retested": False
                })

            else:
                # Extend range if needed
                if row['SourceHigh'] > range_high:
                    range_high = row['SourceHigh']
                    final_high = range_high

                if row['SourceLow'] < range_low:
                    range_low = row['SourceLow']
                    final_low = range_low

        # =========================
        # PHASE 3 — Cooldown + Retest
        # =========================
        elif phase == 3:
            lookback = i - cooldown_start_idx

            if not is_retested and lookback > 0:
                safe_lb = min(retest_bars, lookback)
                recent = df.iloc[i - safe_lb:i]

                breakout_price = range_high if breakout_up else range_low
                gap = breakout_price * retest_max_gap_pct / 100

                if breakout_up:
                    lowest = recent['Low'].min()
                    if breakout_price - gap <= lowest < breakout_price:
                        is_retested = True
                        results[-1]["retested"] = True

                if breakout_down:
                    highest = recent['High'].max()
                    if breakout_price < highest <= breakout_price + gap:
                        is_retested = True
                        results[-1]["retested"] = True

            if lookback >= cooldown_bars:
                phase = 0

    return pd.DataFrame(results)

if __name__ == '__main__':

    pd.set_option("display.max_rows", None, "display.max_columns", None)

    end_date = date.today()
    start_date =  end_date - timedelta(days=7)

    boxes = darvas_box(
        symbol="NIFTY 50",
        start_date=start_date,
        end_date=end_date,
        interval="5minute"
    )

    print(boxes)
    # Save results to a csv file
    boxes.to_csv("ConsolidationBox.csv")

Practical Applications

There are a number of ways this indicator could be used in a strategy (both intraday or swing). As continuation to this article, I plan to publish another one in future containing one of my personal strategies that is based on this indicator and its backtest results.

Final Thoughts

This consolidation box framework can be used as:

  • a standalone breakout strategy
  • a market structure filter
  • a pre-condition for trend-following systems

The real edge comes not from predicting breakouts, but from:

  • waiting patiently
  • defining structure objectively
  • reacting only when price proves itself

If you use this logic in your own tools or experiments, feel free to adapt the parameters to your market, timeframe, and trading style.

More from Python