Trade Size Distribution Indicator: Formula, Settings and How to Read It

Trade Size Distribution measures how concentrated a bar's volume is on its heaviest price levels. It reports the percentage of the bar's volume that traded on levels above a percentile threshold computed from that same bar, using the 80th percentile by default.

Senzoukria · Indicators · Updated September 2026


Trade Size Distribution ships with the Senzoukria desktop app, in the Tape & flow group of the indicator catalogue. It is drawn in its own panel below the chart.

What Trade Size Distribution measures

For each bar, the volumes of its price levels are sorted and a threshold is taken by nearest rank, without interpolation, so the threshold is always a level volume that was actually observed rather than a number computed between two levels. The indicator then sums the volume of every level strictly above that threshold, divides it by the bar's total level volume and reports the result as a percentage. Levels sharing the same price are merged first and levels with no quantity are excluded, so a source that publishes an unmerged level list cannot inflate the level count. A footprint bar is aggregated and the order of executions is lost, so what is measured is the concentration of flow across prices, not a histogram of individual trade sizes.

The formula, as implemented

This is not a description of how the indicator is usually defined elsewhere — it is what the shipped code computes, documented next to the implementation:

Points d'histogramme : `f` rend `null` quand la barre n'a pas d'information — la barre n'émet alors PAS de point (contrat `HistogramPoint` : pas de trou, pas de valeur inventée). */ function histPoints( bars: readonly FootprintBar[], f: (bar: FootprintBar, i: number) => number | null, ): HistogramPoint[] { const out: HistogramPoint[] = []; for (let i = 0; i < bars.length; i++) { const v = f(bars[i], i); if (v !== null) out.push({ time: bars[i].bucketTsNs, value: v }); } return out; } function histOut( points: HistogramPoint[], positiveColor: string, negativeColor: string, ): SeriesOutput { return { shape: "histogram", points, positiveColor, negativeColor }; } /** Un niveau de la barre indexé par sa CLÉ ENTIÈRE (indice de tick quand le tick est connu, quantification 1e-6 sinon — canon : jamais de clé f64). */ type KeyedLevel = { key: number; price: number; buy: number; sell: number }; /** Niveaux EXPLOITABLES de la barre (canon : qty = 0 et prix non fini exclus), agrégés par clé de prix, rendus en ordre ASCENDANT. L'agrégation est indispensable : deux entrées `levels` au même prix (source qui n'a pas fusionné, ou deux prix qui tombent sur le même tick) ne doivent compter QUE POUR UN niveau — sinon le compte de niveaux, les percentiles et la largeur traversée mentent tous les trois. / function barLevelsByKey( bar: FootprintBar, tickSize: number | null, ): KeyedLevel[] { const byKey = new Map<number, KeyedLevel>(); for (const lvl of usableLevels(bar)) { const key = priceKey(lvl.price, tickSize); const cur = byKey.get(key); if (cur !== undefined) { cur.buy += lvl.buyVolume; cur.sell += lvl.sellVolume; } else { byKey.set(key, { key, price: lvl.price, buy: lvl.buyVolume, sell: lvl.sellVolume, }); } } return [...byKey.values()].sort((a, b) => a.key - b.key); } /** CVD de séance par barre + indice de la PREMIÈRE barre de sa séance. CVD = Σ `totalDelta` depuis l'ouverture CME 17:00 CT, RESET à chaque ouverture (`splitSessions`) — canon, jamais le début du chart. Le second tableau permet aux fenêtres glissantes de refuser de comparer deux barres séparées par un reset : la différence de CVD n'aurait alors aucun sens. / function sessionCvd(bars: readonly FootprintBar[], ctx: SeriesContext): { cvd: number[]; sessionStart: number[]; } { const cvd = new Array<number>(bars.length).fill(0); const sessionStart = new Array<number>(bars.length).fill(0); for (const s of splitSessions(bars, ctx)) { let running = 0; for (let i = s.start; i <= s.end; i++) { running += bars[i].totalDelta; cvd[i] = running; sessionStart[i] = s.start; } } return { cvd, sessionStart }; } /** VWAP de séance par barre : Σ(tp·vol)/Σvol cumulé depuis l'ouverture CME 17:00 CT, reset à CHAQUE ouverture. Prix d'échantillon = prix TYPIQUE (H+L+C)/3, poids = volume de la barre — exactement le mode `typical` de `vwap.ts` (parité ATAS/Sierra/TradingView) : un second VWAP maison qui divergerait du VWAP dessiné serait pire qu'inutile. Barres à volume nul : n'alimentent rien ; avant le premier volume de la séance → `null` (pas de VWAP, pas de distance). / function sessionVwap(bars: readonly FootprintBar[], ctx: SeriesContext): Array<number | null> { const out = new Array<number | null>(bars.length).fill(null); for (const s of splitSessions(bars, ctx)) { let pv = 0; let v = 0; for (let i = s.start; i <= s.end; i++) { const w = bars[i].totalVolume; if (w > 0) { pv += typicalPrice(bars[i]) * w; v += w; } out[i] = v > 0 ? pv / v : null; } } return out; } /** Extrêmes COURANTS (running high/low) de la séance en cours, reset à chaque ouverture 17:00 CT. Définis dès la 1ʳᵉ barre : un extrême courant existe immédiatement, contrairement à une moyenne — pas de warm-up. */ function runningSessionExtremes( bars: readonly FootprintBar[], ctx: SeriesContext, ): Array<{ hi: number; lo: number }> { const out = new Array<{ hi: number; lo: number }>(bars.length); for (const s of splitSessions(bars, ctx)) { let hi = Number.NEGATIVE_INFINITY; let lo = Number.POSITIVE_INFINITY; for (let i = s.start; i <= s.end; i++) { if (bars[i].high > hi) hi = bars[i].high; if (bars[i].low < lo) lo = bars[i].low; out[i] = { hi, lo }; } } return out; } /** Offset de la barre depuis l'ouverture de SA séance, en NANOSECONDES. Les buckets sont des ns entiers sur la grille du timeframe : la clé est exacte (la règle `priceKey` ne concerne que les PRIX). */ function offsetNs(bar: FootprintBar, openSec: number): number { return bar.bucketTsNs - openSec * 1e9; } /** Offset de la barre depuis l'ouverture de SA séance, en MINUTES ENTIÈRES (plancher). Clé plus grossière qu'`offsetNs` — voir `time-of-day-volume` pour la raison d'être de ce choix. */ function offsetMinutes(bar: FootprintBar, openSec: number): number { return Math.floor((bar.bucketTsNs / 1e9 - openSec) / 60); } /** Seuil de percentile par RANG LE PLUS PROCHE (nearest-rank, méthode NIST « inverse empirique ») sur une liste TRIÉE ASCENDANTE et NON VIDE : rang = ⌈pct/100 × m⌉, borné à [1, m] → seuil = trié[rang − 1] Choix documenté : pas d'interpolation linéaire. Sur 4 ou 5 niveaux — le cas courant d'une barre footprint — une interpolation fabriquerait un seuil qui n'existe dans aucun niveau ; le rang le plus proche rend toujours un volume RÉELLEMENT observé. */ function nearestRankThreshold(sortedAsc: readonly number[], pct: number): number { const m = sortedAsc.length; const rank = Math.min(m, Math.max(1, Math.ceil((pct / 100) * m))); return sortedAsc[rank - 1]; } // ── Microstructure (Tape & flow) ──────────────────────────────────────────── /** Distribution des tailles — part du volume venant des niveaux LOURDS : seuil = percentile P des volumes de niveau de la barre (nearest-rank) value = 100 × Σ { vol(niveau) : vol(niveau) > seuil } / Σ vol(niveau) POURQUOI : une barre de 1 000 lots répartie sur 20 niveaux et une barre de 1 000 lots dont 800 sont tombés sur 2 prix ne racontent pas la même histoire. Cet indicateur mesure la CONCENTRATION du flux, c'est-à-dire la trace que laissent les gros participants — le seul angle honnête sur la « taille des trades » que permette une barre footprint AGRÉGÉE : la distribution des tailles trade par trade exigerait le tape brut (l'ordre des exécutions est perdu à l'agrégation). Le doc-comment le dit plutôt que le nom seul. Convention : comparaison STRICTE au seuil (`>`), donc une barre dont TOUS les niveaux pèsent pareil rend 0 — c'est une valeur MESURÉE (aucune concentration), pas un 0/0 déguisé : le dénominateur, lui, est > 0 par construction (`usableLevels` exclut les niveaux à volume nul). Défaut P=80 : le décile supérieur est trop fin pour 3-5 niveaux, la médiane ne mesure plus la concentration mais la simple asymétrie. Barre sans niveau exploitable → PAS de point. Aucun tick size requis (les clés ne servent qu'à dédupliquer).

How to read it

  • Low readings mean the bar's volume was spread fairly evenly across its price levels; high readings mean a large share of it landed on very few prices.
  • Concentration appearing at the extreme of a range is worth opening on the footprint itself, where you can see which side of the tape produced it.
  • The number of usable levels in a bar decides what the default threshold can leave above it: below five levels, the 80th percentile lands on the heaviest level and the reading is 0 by construction.
  • A value of exactly 0 is a measured result, not missing data: it means no level carried more volume than the threshold.
  • Bars with no usable level produce no point at all, so a gap in the pane means the bar carried no level data rather than a reading of zero.

Parameters and defaults

Percentile defaults to 80 and accepts 1 to 99. Because the threshold is taken by nearest rank, the percentile and the number of levels interact directly: a bar only returns a non-zero value when it holds at least 100/(100 − P) usable levels, which is five levels at the default. On a bar of exactly five levels the threshold falls on the fourth heaviest, so the reading is the share carried by the single heaviest level; on wider bars it covers roughly the top fifth of them. Lowering the percentile toward 50 puts the threshold at the bar's median level, which raises the values and measures asymmetry more than concentration. Raising it toward 99 requires a hundred levels before anything can exceed the threshold, so on ordinary footprint bars every reading collapses to 0.

Trade Size Distribution — parameters exposed in the app, with the values it ships with.
ParameterTypeDefaultRange
Percentilenumber801 – 99

What it does not show

This is not a per-trade size distribution. Reconstructing the size of each execution requires the raw tape, and that information is gone once a bar aggregates its levels. The strict comparison has a consequence worth knowing: a bar that traded at a single price returns 0, because no level can exceed a threshold equal to itself, even though such a bar is maximally concentrated; the same arithmetic explains every 0 on narrow bars at the default percentile. The reading therefore depends on how many levels a bar contains, which follows from the timeframe, the bar's range and the price grouping of the source, so readings are only comparable within one instrument and one timeframe. Finally, it requires level data with volume per price; a source that only provides open, high, low, close and volume produces nothing.

Using it in Senzoukria

Add it from the Indicators panel of any footprint chart or candle chart. It runs on futures data from Rithmic or Databento and on crypto pairs from Binance and Bybit, on the same engine — the calculation does not change with the venue, only the data feeding it does. Market data subscriptions are billed by the provider, separately from the app.

See the full indicator library, or start with the order flow guide if you are new to reading aggression, delta and absorption.

Frequently asked questions

Can you measure individual trade sizes on a footprint chart?
Not from an aggregated footprint bar. Aggregation keeps the volume traded at each price but discards the sequence and size of the individual executions that produced it. What remains measurable is how concentrated that volume is across prices, which is what this indicator reports.
What does a reading of 0 on trade size distribution mean?
It means no price level in that bar carried more volume than the percentile threshold, because the comparison is strict. That happens when all levels weigh roughly the same, on a bar that traded at a single price, and on any bar with fewer than five usable levels at the default 80th percentile, where the threshold lands on the heaviest level itself. It is a measured value, not an error.
Which percentile should I use for trade size distribution?
Start from the default of 80, which works on bars carrying five levels or more. Before changing it, check how many levels your bars actually have: a percentile P needs at least 100/(100 − P) levels before any level can exceed the threshold, so 99 is unusable on a typical footprint bar. A percentile near 50 measures how asymmetric the distribution is rather than how concentrated it is.

Keep reading