Indicator

Fathom Imbalances

Stacked buyer and seller imbalances, turned into zones.

What the indicator shows

  • An imbalance is a price level where one side overwhelms the other in the bar’s footprint, that is its bid and ask volume price by price. In diagonal mode, the default, aggressive buying (ask) at a level is compared with aggressive selling (bid) one level below: with minimumPercent: 300, the ask must be at least 3 times that bid for a buy imbalance; conversely, the bid must be at least 3 times the ask one level above for a sell imbalance.
  • When at least consecutive consecutive levels (3 by default) are imbalanced on the same side, they are called stacked imbalances: the indicator turns them into a zone. The footprint is not published; only the resulting zone is, when the bar closes.
  • A buy zone marks where buyers dominated across those levels; a sell zone, where sellers dominated. On the Fathom chart, buy zones are blue and sell zones coral.
  • The zone extends to the right while it is active. With triggeredZones, a retest zone (triggered: true) takes over after triggering.

How to use it

  • Price often comes back to these zones: a buy zone then acts as support, a sell zone as resistance.
  • A close on the other side triggers the zone: the level gave way. The retest zone follows that broken level, which can then play the opposite role.
  • Raise consecutive to keep only the clearest imbalances.

How it is computed

  • The bar’s levels are first grouped by groupTicks ticks. mode sets the comparison: diagonal compares a level’s ask with the bid of the level just below (buy imbalance), and a level’s bid with the ask of the level just above (sell imbalance); horizontal compares the ask and bid of the same level; delta-percentage relates the level’s delta to its volume.
  • With diagonal and horizontal, a level is imbalanced when the dominant side is not empty, reaches minimumPercent% of the other (300 = 3 to 1) and exceeds it by at least minimumVolume contracts; an empty opposite side (0 contracts) only counts with includeZero. With delta-percentage, the difference must strictly exceed minimumVolume and be at least minimumPercent% of both sides’ volume; minimumPercent must then be at most 100 (the default, 300, is refused there).
  • At least consecutive consecutive levels imbalanced on the same side form a zone, bounded by the bar and widened by extraTicks ticks on each side.
  • A buy zone is triggered when a bar closes below it (a sell zone, above it); with triggerOnlyTouch, a wick going past it is enough. With triggeredZones, a retest zone (triggered: true) then takes over, until a bar moves back to the other side the same way.
  • A zone and its retest zone live at most extendedBars bars after the bar that created the original zone.

When it updates

  • A zone is sent in progress when the bar that creates it closes, sent again on every change, then sent final (active: false, with its end) when it ends.
  • Zones stay active from bar to bar and from session to session. With resetSession: true, zones in progress are ended when the first bar of the new session closes (6:05 pm New York time with timeframe: 300).
  • <weekSunday> is the YYYY-MM-DD date of the week’s Sunday reopen; <ordinal> numbers the week’s zones in creation order.
  • Weekly reset: on the first trade of the Sunday reopen (6:00 pm New York time), the week’s last bar is processed, then zones still in progress are sent final (active: false, with their end); none carries over to the new week.
Identifier
imbalances
Objects received
zone
Historical cost
1 compute unit per trade

Subscribe

The same params work in real time, on historical data and in exports. Parameters you omit take their default values.

Subscribe message
{
  "t": "subscribe",
  "sub": "imbalances",
  "instrument": "NQ.front",
  "indicator": "imbalances",
  "params": {
    "consecutive": 3,
    "extendedBars": 10,
    "extraTicks": 0,
    "groupTicks": 1,
    "includeZero": false,
    "minimumPercent": 300,
    "minimumVolume": 0,
    "mode": "diagonal",
    "resetSession": false,
    "timeframe": 300,
    "triggerOnlyTouch": false,
    "triggeredZones": true
  },
  "mode": "live",
  "from": "live"
}

Parameters

As soon as a parameter differs from its default, the subscription counts as a custom configuration. To change a parameter, open a new subscription.

ParameterTypeDefaultAllowed valuesUnitDescription
consecutiveinteger31 to 20—Minimum number of consecutive imbalanced levels to form a zone.
extendedBarsinteger101 to 500barsMaximum lifetime of a zone (and of its retest zone), in bars after the bar that created it.
extraTicksinteger00 to 100ticksPadding added on each side of the zone, above the top and below the bottom.
groupTicksinteger11 to 100ticksNumber of ticks grouped into each level before the comparison.
includeZerobooleanfalsetrue · false—Count a level whose opposite side is empty (0 contracts) as an imbalance.
minimumPercentinteger3001 to 10,000%Imbalance threshold, in percent. With diagonal and horizontal, ratio of the dominant side to the other (300 = 3 to 1). With delta-percentage, the delta’s share of the level’s volume: this mode requires an explicit value of at most 100 (the default, 300, is refused there).
minimumVolumeinteger00 to 1,000,000contractsMinimum volume difference between the two sides of a level (strictly greater with delta-percentage).
modechoicediagonaldiagonal · horizontal · delta-percentage—How volumes are compared: diagonal (a level’s ask against the bid of the level just below), horizontal (ask and bid of the same level) or delta-percentage (the level’s delta relative to its volume). Unrelated to the subscription mode (live or confirmed).
resetSessionbooleanfalsetrue · false—End every zone when the first bar of each new session closes.
timeframeinteger3001 · 5 · 10 · 15 · 30 · 60 · 120 · 180 · 300 · 600 · 900 · 1800 · 3600—Bar duration in seconds: from 1 second (1) to 1 hour (3600).
triggerOnlyTouchbooleanfalsetrue · false—Trigger a zone as soon as a wick goes past it, without waiting for a close beyond it.
triggeredZonesbooleantruetrue · false—Create a retest zone when a zone is triggered.

Objects received

Each object keeps the same id from one update to the next. Its data is in the data field. Prices are in ticks: multiply them by the instrument’s tick size (GET /v1/instruments) to get points.

zone sent in progress

Zone; in progress while it is active.

id format: <weekSunday>:<ordinal>. The object is sent in progress (final: false), then sent again on every change; each version fully replaces the previous one. Once it can no longer change, its last version carries final: true, except for objects whose description says they are removed without a final version.

FieldTypeUnitPresentDescription
activeboolean—alwaystrue while the zone is extended; false once it is final.
barstringtimestamp (ns)alwaysStart of the bar that created the zone (for a retest zone, the bar that triggered the original zone).
endnumberbarssometimesEnd of the zone, in bars relative to bar; present once the zone is final. Original zone: index of the bar that ends it (trigger, session open, end of week) minus 1, that is extendedBars on expiry; it is 0, so less than start, for a zone triggered by the very next bar. Retest zone: index of the bar that ends it plus 1 after a retest, minus 1 on expiry, at a session open or at the end of the week.
highintegerticksalwaysTop of the zone.
lowintegerticksalwaysBottom of the zone (equal to the top for a line).
sidestring · buy | sell—alwaysBuy or sell imbalance.
startnumberbarsalwaysStart of the zone, in bars relative to bar (0 = start of bar): 0.5 for an original zone, -0.5 for a retest zone. Matching time, in nanoseconds: BigInt(bar) + BigInt(Math.round(start × timeframe × 1e9)) (bar is in nanoseconds, timeframe in seconds).
triggeredboolean—alwaystrue: retest zone, created when a bar triggered a zone; false: original zone.

Example messages

Messages as the WebSocket sends them to the subscription above, taken from a real NQ session. Historical data returns the same objects, with the same cursor, without the sub field.

Final object
{
  "sub": "imbalances",
  "cursor": "20720.0.0",
  "t": "upsert",
  "id": "2026-09-20:750",
  "final": true,
  "ts": "1790208000058746563",
  "data": {
    "side": "sell",
    "high": 123052,
    "low": 123050,
    "triggered": true,
    "active": false,
    "bar": "1790207400000000000",
    "start": -0.5,
    "end": 2
  }
}
Object in progress
{
  "sub": "imbalances",
  "cursor": "20720.0.1",
  "t": "upsert",
  "id": "2026-09-20:751",
  "final": false,
  "ts": "1790208000058746563",
  "data": {
    "side": "sell",
    "high": 123040,
    "low": 123038,
    "triggered": false,
    "active": true,
    "bar": "1790207700000000000",
    "start": 0.5
  }
}

JSON schemas

To validate or type your data: the JSON schemas of the parameters and of data, also available from GET /v1/indicators. The SDK can generate types from them.

Parameters schema
JSON
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://fathomcharts.com/schemas/imbalances/params.json",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "consecutive": {
      "description": "Minimum number of consecutive imbalanced levels to form a zone.",
      "type": "integer",
      "default": 3,
      "minimum": 1,
      "maximum": 20
    },
    "extendedBars": {
      "description": "Maximum lifetime of a zone (and of its retest zone), in bars after the bar that created it.",
      "type": "integer",
      "default": 10,
      "minimum": 1,
      "maximum": 500
    },
    "extraTicks": {
      "description": "Padding added on each side of the zone, above the top and below the bottom.",
      "type": "integer",
      "default": 0,
      "minimum": 0,
      "maximum": 100
    },
    "groupTicks": {
      "description": "Number of ticks grouped into each level before the comparison.",
      "type": "integer",
      "default": 1,
      "minimum": 1,
      "maximum": 100
    },
    "includeZero": {
      "description": "Count a level whose opposite side is empty (0 contracts) as an imbalance.",
      "type": "boolean",
      "default": false
    },
    "minimumPercent": {
      "description": "Imbalance threshold, in percent. With `diagonal` and `horizontal`, ratio of the dominant side to the other (300 = 3 to 1). With `delta-percentage`, the delta’s share of the level’s volume: this mode requires an explicit value of at most 100 (the default, 300, is refused there).",
      "type": "integer",
      "default": 300,
      "minimum": 1,
      "maximum": 10000
    },
    "minimumVolume": {
      "description": "Minimum volume difference between the two sides of a level (strictly greater with `delta-percentage`).",
      "type": "integer",
      "default": 0,
      "minimum": 0,
      "maximum": 1000000
    },
    "mode": {
      "description": "How volumes are compared: `diagonal` (a level’s ask against the bid of the level just below), `horizontal` (ask and bid of the same level) or `delta-percentage` (the level’s delta relative to its volume). Unrelated to the subscription `mode` (`live` or `confirmed`).",
      "type": "string",
      "default": "diagonal",
      "enum": [
        "diagonal",
        "horizontal",
        "delta-percentage"
      ]
    },
    "resetSession": {
      "description": "End every zone when the first bar of each new session closes.",
      "type": "boolean",
      "default": false
    },
    "timeframe": {
      "description": "Bar duration in seconds: from 1 second (`1`) to 1 hour (`3600`).",
      "type": "integer",
      "default": 300,
      "enum": [
        1,
        5,
        10,
        15,
        30,
        60,
        120,
        180,
        300,
        600,
        900,
        1800,
        3600
      ]
    },
    "triggerOnlyTouch": {
      "description": "Trigger a zone as soon as a wick goes past it, without waiting for a close beyond it.",
      "type": "boolean",
      "default": false
    },
    "triggeredZones": {
      "description": "Create a retest zone when a zone is triggered.",
      "type": "boolean",
      "default": true
    }
  },
  "allOf": [
    {
      "description": "In `delta-percentage` mode, `minimumPercent` must be given and be at most 100 (the default, 300, is refused there).",
      "if": {
        "properties": {
          "mode": {
            "const": "delta-percentage"
          }
        },
        "required": [
          "mode"
        ]
      },
      "then": {
        "properties": {
          "minimumPercent": {
            "maximum": 100
          }
        },
        "required": [
          "minimumPercent"
        ]
      }
    }
  ]
}
data field schema
JSON
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://fathomcharts.com/schemas/imbalances/data.json",
  "title": "zone",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "active": {
      "description": "`true` while the zone is extended; `false` once it is final.",
      "type": "boolean"
    },
    "bar": {
      "description": "Start of the bar that created the zone (for a retest zone, the bar that triggered the original zone).",
      "type": "string",
      "pattern": "^[0-9]+$"
    },
    "end": {
      "description": "End of the zone, in bars relative to `bar`; present once the zone is final. Original zone: index of the bar that ends it (trigger, session open, end of week) minus 1, that is `extendedBars` on expiry; it is 0, so less than `start`, for a zone triggered by the very next bar. Retest zone: index of the bar that ends it plus 1 after a retest, minus 1 on expiry, at a session open or at the end of the week.",
      "type": "number"
    },
    "high": {
      "description": "Top of the zone.",
      "type": "integer"
    },
    "low": {
      "description": "Bottom of the zone (equal to the top for a line).",
      "type": "integer"
    },
    "side": {
      "description": "Buy or sell imbalance.",
      "type": "string",
      "enum": [
        "buy",
        "sell"
      ]
    },
    "start": {
      "description": "Start of the zone, in bars relative to `bar` (0 = start of `bar`): `0.5` for an original zone, `-0.5` for a retest zone. Matching time, in nanoseconds: `BigInt(bar) + BigInt(Math.round(start × timeframe × 1e9))` (`bar` is in nanoseconds, `timeframe` in seconds).",
      "type": "number"
    },
    "triggered": {
      "description": "`true`: retest zone, created when a bar triggered a zone; `false`: original zone.",
      "type": "boolean"
    }
  },
  "required": [
    "side",
    "high",
    "low",
    "triggered",
    "active",
    "bar",
    "start"
  ]
}