Market Profile: Time at Price, and the One Number Our Engine Ships
A market profile is a chart of how long price stayed somewhere. Split the session into brackets, give each bracket a letter, and stamp that letter against every price the bracket traded through. Stack the letters sideways and the fat part of the shape marks where the auction spent its time.
Our engine does not build that grid. It ships batch_volume_profile_poc, which buckets volume rather than time and reads only each bar's close — two substitutions, both deliberate, neither of them cosmetic. Across 367,516 real UTC sessions those two constructions picked the same price bucket 15.9% of the time. This page is what changes, measured one substitution at a time.
What a market profile is
Steidlmayer's construction is a histogram whose unit is a half-hour of trading rather than a share or a contract.
Each bracket contributes one mark to every price level it touched, so a level that price visited during nine brackets carries nine marks whether a fortune changed hands there or nothing did. Wide rows mark acceptance; the thin tails at either end mark prices the auction rejected quickly.
Two derived readings come out of the shape. The point of control is the row with the most marks, and the value area is the band around it holding some agreed fraction of the total — conventionally 70%. Both are statements about duration, and that is the distinction the rest of this page keeps returning to, because the number our engine gives you is a statement about size instead.
TRX, below, is the happy case: volume and time landed on the same bucket, and that bucket held a quarter of the day.
What our engine ships instead
batch_volume_profile_poc does per bar. Nothing about a session enters the calculation, and the bucket array is discarded as soon as its fullest entry has been found.One number per bar, from a rolling window, weighted by volume.
How Vike computes it. For every bar i,batch_volume_profile_poctakes the trailing 50 bars ending at i inclusive, sets the range from the lowest low to the highest high, splits it into 24 equal buckets, and adds each bar's whole volume to the bucket containing that bar's close. It returns the centre price of the fullest bucket,price_min + (index + 0.5) * bin_width. Ties go to the lowest bucket. Bars before the window fills return NaN.
When I ported this kernel I ran it beside the Python function it came from, over hourly bars across 40 symbols. That is 1,177,763 compared values, largest absolute difference exactly 0.000e+00, and every warm-up mask agreeing. The measurements below are measurements of that code.
Notice what the window does to the idea of a session. A market profile is bounded by the trading day; this kernel is bounded by a bar count that slides forward every bar and pays no attention to where a day begins. On 69,876 sessions where both could be computed, the rolling reading landed within half a bucket of the session reading only 26.0% of the time.
Time at price is not volume at price
Here is the substitution isolated, with everything else held fixed.
Take the same 24-bar session and the same 24 bucket edges, then change only what accumulates: volume in one profile, a count of bars in the other. Across 367,516 sessions those two named the same bucket on 37.1%, with a median gap of 2 buckets. So swapping duration for size moves the point of control roughly two rows of a 24-row grid, most of the time.
Add the second substitution — the crate reads the close alone, while a real TPO grid marks every level a bar traded through — and agreement collapses to 15.9%, with 31.7% of sessions disagreeing by six buckets or more. A quarter of a session's price range, from the same bars.
The shapes differ too, and in the direction you would expect. Seventy percent of a session's volume fits in a median of 6 buckets, while the same 70% of TPO marks needs 11. Time spreads; money concentrates. The GNS-style case below is the close-only effect on its own: counting closes instead of volume moved the point of control eight buckets on identical bars.
The session a 24/7 market does not have
Market profile was designed around a bell, and crypto does not have one.
This construction assumes a bounded auction: an open, a close, an initial balance built in the first hour, and a value area that means something because everyone traded inside the same window. A market that never shuts has none of those landmarks, so any session boundary you pick is a convention rather than a fact.
We use the UTC day here, which is the least arbitrary choice available and still arbitrary. Shift the boundary by six hours and the range changes, the bucket edges change, and therefore the point of control changes — none of which reflects anything the market did. Our engine sidesteps the argument by not having sessions at all, which trades one problem for another: a rolling window never disagrees with itself about where the day starts, and it also never tells you where value sat today.
The value area is deliberately missing
The crate says so itself, in the module header above the kernel.
structure.rs states that the unregistered volume_profile histogram helper "is a chart-render concern and is not ported here". So there is no value area in the indicator surface, no value area high, no value area low, and no bin array crossing the API. The bucket array exists inside the kernel for one statement — find the fullest bucket — and is dropped on the next line.
When I first read that line I expected to disagree with it, and I have come round: the boundary is right, and worth defending rather than apologising for. A value area is a display decision wearing the clothes of a measurement: choose 70% or 68%, expand by higher-neighbour or by nearest-neighbour, and identical data yields different levels. Freezing one of those choices into a per-bar series would hide it. Keeping it in the chart layer, where it is visible and adjustable, is the honest place for it.
Every histogram drawn on this page is that discarded intermediate, rebuilt with the kernel's own bucketing so you can see what it saw before it threw the array away.
Where market profile fails
Three failures, and the first is the one this page has been building toward.
Which construction you used decides the level. Look at the ENA session below: the volume point of control and the TPO point of control sat seventeen buckets apart on the same day, so two analysts with the same bars and different tooling would have drawn "value" at opposite ends of the range. Neither was wrong. They measured different things and gave the answer the same name.
Then there are sessions with no point of control worth the phrase. RENDER, below, had a busiest bucket holding 13.5% of the day's volume across 24 buckets — barely more than three times what a flat distribution would put there. Quoting a level off that shape implies a concentration the session never had, and nothing in the output warns you.
And reported volume is only as honest as the venue publishing it. That is a sharper problem in crypto than in any regulated market, because a profile built on inflated numbers produces a precise, confident and entirely fictional level, with nothing in the output to flag it. The TPO version at least degrades differently: a wash trade adds no time.
Market profile vs volume profile
These two answer neighbouring questions, they get confused for one another constantly, and the difference is worth stating bluntly rather than diplomatically.
Market profile measures time at price and is bounded by a session. Volume profile measures size at price and can be bounded by anything — a session, a visible range, or the rolling 50-bar window our engine actually uses. The overlap is that both call their busiest row a point of control, which is exactly why the 15.9% agreement figure above is worth carrying around.
If you want the arithmetic of what we ship, the volume profile breakdown covers the kernel in full. For how a chart type can delete information before you ever see it, renko makes the same argument in a different register, and the trading indicators overview sets out the lag every window-based tool shares.
Frequently asked questions
What is the point of control in market profile? The price row carrying the most time — in the classic construction, the most letters. Our engine's equivalent is the fullest volume bucket in a trailing 50-bar window, and on real sessions those two agreed 15.9% of the time.
What is the difference between TPO and volume profile? TPO counts brackets; volume profile counts traded size. Both build a histogram over price, and on our tape the volume version was far more concentrated — 70% of it fitted in 6 buckets against 11 for TPO.
Does market profile work in crypto? The histogram works anywhere. The session does not, because a 24-hour market has no open or close, so the boundary is a convention you choose and every level moves when you change it.
Why does our engine not return a value area? Because the crate treats the histogram, the value area and its bounds as chart rendering rather than as an indicator, and says so in the module header. One number per bar leaves the function.
Can I read a market profile off candlesticks? Only loosely. A candlestick chart records four prices per bar, while a profile needs how long price spent at each level inside the bar — which is why our kernel substitutes the close and this page measures what that costs.
This is educational material, not financial advice. A profile describes bars that have already printed, every figure above was measured on past sessions, and trading carries substantial risk of loss.