Trade Size Distribution 指标:公式、参数与读法
成交规模分布(Trade Size Distribution)衡量一根K线的成交量在其最重价位上集中到什么程度。它报告成交在某个百分位阈值以上价位的成交量占K线成交量的百分比,阈值从同一根K线计算,默认为第 80 百分位。
Senzoukria · 指标 · Updated September 2026
Trade Size Distribution — 逐笔成交与资金流, 显示在图表下方的独立面板中.
Trade Size Distribution 测量的是什么
对每根K线,把各价位的成交量排序,按最近秩(nearest rank)取阈值,不做插值,因此阈值始终是一个实际观察到的价位成交量,而不是两个价位之间算出来的数字。然后指标把严格高于该阈值的每个价位的成交量加总,除以K线各价位成交量之和,以百分比报告结果。同一价格的价位会先合并,没有数量的价位被排除,所以发布未合并价位列表的数据源无法虚增价位数。足迹K线是聚合的,成交的先后顺序已经丢失,所以测量的是流量在各价格上的集中度,而不是单笔成交规模的直方图。
公式:代码中的实现
这不是别处常见的定义,而是所发布代码实际计算的内容,记录在实现旁边:
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).
如何解读
- 读数低说明K线的成交量相当均匀地分布在各价位上;读数高说明其中很大一部分落在极少数几个价格上。
- 出现在区间极端的集中值得打开足迹图本身查看,在那里能看到是成交的哪一方造成的。
- K线中可用价位的数量决定了默认阈值之上还能留下什么:少于五个价位时,第 80 百分位落在最重的价位上,读数按构造为 0。
- 正好为 0 的数值是测量结果,不是缺失数据:它表示没有任何价位的成交量超过阈值。
- 没有可用价位的K线完全不画点,所以面板中的空缺表示该K线没有价位数据,而不是读数为零。
参数与默认值
百分位(Percentile)默认为 80,可取 1 到 99。由于阈值按最近秩取值,百分位与价位数直接相互影响:K线至少有 100/(100 − P) 个可用价位时才会返回非零值,默认设置下是五个价位。在正好五个价位的K线上,阈值落在第四重的价位,所以读数就是最重那一个价位所占的份额;在更宽的K线上,它大致覆盖最重的五分之一价位。把百分位降向 50,阈值就落在K线的中位价位上,数值会升高,衡量的更多是不对称而不是集中。把它升向 99,需要一百个价位才可能有价位超过阈值,因此在普通足迹K线上所有读数都会塌缩为 0。
| 参数 | 类型 | 默认值 | 取值范围 |
|---|---|---|---|
| Percentile | number | 80 | 1 – 99 |
它无法显示的内容
这不是逐笔成交的规模分布。还原每笔成交的规模需要原始逐笔数据,而K线一旦把价位聚合,这些信息就没有了。严格比较有一个值得了解的后果:只在一个价格成交的K线返回 0,因为没有价位能超过等于自身的阈值,尽管这样的K线集中度最高;同样的算术也解释了默认百分位下窄K线上的每一个 0。因此读数取决于K线包含多少价位,而这又取决于周期、K线波幅和数据源的价格分组,所以读数只在同一品种、同一周期内可比。最后,它需要带有逐价格成交量的价位数据;只提供开高低收和成交量的数据源不会产生任何结果。
相关指标
- Unfinished Auctions — 逐笔成交与资金流
- Effort vs Result — 逐笔成交与资金流
- POC Drift (ticks) — 逐笔成交与资金流
- Cumulative buy volume — 逐笔成交与资金流
- Cumulative sell volume — 逐笔成交与资金流
- Buy/sell ratio — 逐笔成交与资金流
其他语言版本
Frequently asked questions
- 能在足迹图上测量单笔成交的规模吗?
- 不能从聚合后的足迹K线测量。聚合保留了每个价格上的成交量,但丢弃了产生这些成交量的每笔成交的顺序和规模。仍能测量的是这些成交量在各价格上有多集中,这正是本指标报告的内容。
- 成交规模分布读数为 0 是什么意思?
- 表示这根K线中没有任何价位的成交量超过百分位阈值,因为比较是严格的。以下情况都会出现:各价位分量大致相同时;K线只在一个价格成交时;以及在默认第 80 百分位下,可用价位少于五个的任何K线,此时阈值就落在最重的价位本身。这是一个测量值,不是错误。
- 成交规模分布应该用哪个百分位?
- 从默认的 80 开始,它适用于有五个或更多价位的K线。改动之前,先看看你的K线实际有多少价位:百分位 P 至少需要 100/(100 − P) 个价位,才可能有价位超过阈值,所以 99 在典型的足迹K线上无法使用。接近 50 的百分位衡量的是分布有多不对称,而不是有多集中。