A volume profile groups traded volume by price instead of by time. To build one in C#, normalize prices onto the instrument’s tick grid, add each input’s volume to its price bucket, then derive the Point of Control (POC) and Value Area High/Low (VAH/VAL) from those buckets. The result is only as meaningful as its inputs: decide whether you have individual trades or bars, what “volume” means in the feed, how a profile period is defined, and which Value Area convention to reproduce.
Decide what the profile represents
A time bar summarizes activity over an interval. A volume profile instead answers how much of the input volume was associated with each price level during a chosen period. The calculation does not make coarse input equivalent to trade-by-trade data: an OHLCV bar provides total volume but does not reveal how that volume was distributed among its prices.
As an Amazon Associate I earn from qualifying purchases.
Before writing the accumulator, specify these parts of the data contract:
Free tools Windows power users keep installed
One-click scans. No signup required.
- Input resolution: executions, quote/trade events, or bars. If using bars, explicitly choose how their volume is assigned; assigning it to the close is an approximation, not a reconstruction of executions.
- Volume semantics: whether the feed reports traded quantity or a proxy such as tick count. The field name alone does not establish its meaning for every asset class or vendor.
- Price grid: the instrument’s tick increment, and whether multiple ticks are combined into one displayed level.
- Profile period: session, fixed bar range, composite range, or rolling window. Session boundaries and calendars are market- and instrument-specific.
- Compatibility target: if matching another platform, reproduce its data resolution, grouping, Value Area percentage, expansion rules, and tie behavior rather than assuming a universal definition.
For example, NinjaTrader documents volume profiles based on last-traded volume at a selected source resolution, with configurable price-level grouping and session, bar, or composite periods. It distinguishes these from price/time and tick profiles, and says tick resolution is required for buy/sell and delta display modes. See its Order Flow Volume Profile documentation; verify feature availability against the platform version you target.
#1 Best Overall
Represent prices on a canonical grid
Do not use raw floating-point prices as dictionary keys. Small representation differences can cause prices that should be identical to land in separate buckets. A practical choice is an integer tick index, calculated from price and the instrument’s tick size. Use the platform’s canonical price-rounding helper where available, and decide how off-grid values are handled.
static long ToTickIndex(decimal price, decimal tickSize)
{
if (tickSize <= 0m)
throw new ArgumentOutOfRangeException(nameof(tickSize));
decimal ticks = price / tickSize;
decimal rounded = decimal.Round(ticks, 0, MidpointRounding.AwayFromZero);
return checked((long)rounded);
}
static decimal ToPrice(long tickIndex, decimal tickSize)
{
return checked(tickIndex * tickSize);
}
This example rounds to the nearest tick and throws on an invalid tick size or overflow. That rounding policy is a choice, not a universal feed rule: if the feed guarantees prices are already aligned, validate that guarantee instead of silently moving prices. For instruments with nonuniform tick schedules, a single tick-size formula may not represent the market’s price grid; use the instrument or platform’s canonical mapping.
Accumulate input without coupling it to the chart
Keep ingestion and accumulation separate from drawing. A minimal execution record might carry a timestamp, instrument identifier, price, and reported quantity, plus an optional aggressor-side value if the feed supplies one. The accumulator should only accept records belonging to its configured instrument and profile period.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #2
For an expanding profile, a dictionary keyed by tick index is straightforward. The following core class uses decimal quantities, rejects negative volume, and chooses the lower-priced bucket when POC volumes tie. The period lifecycle and instrument filtering belong in the caller because their rules depend on the feed and market.
using System;
using System.Collections.Generic;
using System.Linq;
public sealed class VolumeProfile
{
private readonly decimal _tickSize;
private readonly SortedDictionary<long, decimal> _volumeByTick = new();
public VolumeProfile(decimal tickSize)
{
if (tickSize <= 0m)
throw new ArgumentOutOfRangeException(nameof(tickSize));
_tickSize = tickSize;
}
public void Add(decimal price, decimal volume)
{
if (volume < 0m)
throw new ArgumentOutOfRangeException(nameof(volume));
long tick = ToTickIndex(price, _tickSize);
_volumeByTick.TryGetValue(tick, out decimal existing);
_volumeByTick[tick] = checked(existing + volume);
}
public ProfileSnapshot Calculate(decimal valueAreaFraction = 0.70m)
{
if (valueAreaFraction <= 0m || valueAreaFraction > 1m)
throw new ArgumentOutOfRangeException(nameof(valueAreaFraction));
if (_volumeByTick.Count == 0)
throw new InvalidOperationException("Cannot calculate an empty profile.");
long poc = _volumeByTick
.OrderByDescending(pair => pair.Value)
.ThenBy(pair => pair.Key)
.First().Key;
decimal total = _volumeByTick.Values.Sum();
decimal target = total * valueAreaFraction;
long low = poc;
long high = poc;
decimal included = _volumeByTick[poc];
while (included < target)
{
bool hasBelow = low != long.MinValue &&
_volumeByTick.TryGetValue(low - 1, out decimal below);
bool hasAbove = high != long.MaxValue &&
_volumeByTick.TryGetValue(high + 1, out decimal above);
if (!hasBelow && !hasAbove)
break;
// Equal adjacent volumes expand downward for deterministic results.
if (hasBelow && (!hasAbove || below >= above))
{
low--;
included += below;
}
else
{
high++;
included += above;
}
}
return new ProfileSnapshot(
poc, low, high,
ToPrice(poc, _tickSize),
ToPrice(high, _tickSize),
ToPrice(low, _tickSize),
total);
}
private static long ToTickIndex(decimal price, decimal tickSize)
{
decimal ticks = decimal.Round(
price / tickSize, 0, MidpointRounding.AwayFromZero);
return checked((long)ticks);
}
private static decimal ToPrice(long tickIndex, decimal tickSize) =>
checked(tickIndex * tickSize);
}
public sealed record ProfileSnapshot(
long PocTick,
long ValTick,
long VahTick,
decimal Poc,
decimal Vah,
decimal Val,
decimal TotalVolume);
This sample deliberately uses SortedDictionary for a simple ordered representation, but it does not fill gaps between traded levels. Its Value Area expansion stops when neither immediately adjacent tick has a bucket. If the intended convention treats untraded price levels as zero-volume steps and continues across gaps, change that rule and test it against the target platform. The checked arithmetic also surfaces overflow rather than silently corrupting a bucket.
Calculate POC and Value Area consistently
Point of Control
The POC is the price level with the greatest accumulated volume. A full scan is easy to verify and is often the right starting point. Ties need a documented rule: this example chooses the lower price. QuantConnect LEAN’s C# MarketProfile implementation is another reference; it rounds prices to configured granularity and breaks tied maxima by choosing the level closest to the midpoint of the sorted price levels. Those results can differ even with identical input.
Value Area High and Low
Value Area is a contiguous range around the POC intended to include a configured fraction of profile volume. There is no single percentage or expansion convention shared by all implementations. LEAN’s MarketProfile source has a configurable default of 0.70, while NinjaTrader documents a configurable 68% Value Area. These are implementation conventions, not universal market statistics.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →The sample uses a common adjacent-level expansion: begin at POC, compare the next bucket immediately above and below, add the larger one, and repeat until included volume reaches the target. When adjacent volumes tie, it expands lower first. Empty levels are not traversed. A different product may group several levels per comparison, use another tie-break, or traverse empty prices. When compatibility matters, test those details against known output from that product rather than relying on the label “Value Area.”
LEAN’s implementation is useful for studying a C# profile with configurable period, granularity, rolling history, POC, and Value Area outputs: MarketProfile.cs. Its rules are a reference, not a universal standard. QuantConnect’s algorithm documentation also discusses Market Profile VOL and manual indicator updates.
Rank #4
Keep aggressor-side classification optional
Total volume per price does not require buyer/seller aggressor labels. If the feed provides a reliable aggressor flag, store side-specific subtotals alongside total volume. If it does not, a common heuristic calls an execution at or above the ask buyer-aggressed and one at or below the bid seller-aggressed. Trades inside the spread, trades without a contemporaneous quote, and ambiguous updates need an explicit fallback such as “unknown.”
Keep side classification separate from volume accumulation so uncertain classification cannot change total volume. Quote timing, feed conventions, and platform rules affect the result; do not present a heuristic as ground truth. If only bars are available, a bar’s OHLCV alone does not identify the aggressor side of its trades.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Define live updates and profile lifecycle
For live input, add volume as each execution arrives when the required accuracy calls for event-level updates. Derived values can be recalculated immediately or marked dirty and recomputed on a controlled cadence. That is a latency-versus-computation choice: the accumulation can remain current while the displayed POC or Value Area refreshes less often.
Best Value
A completed session or profile range needs a clear finalization boundary. For a rolling window, retain enough information to reverse expired observations: when an old observation leaves, subtract its quantity from its tick bucket and remove the bucket if its quantity becomes zero. A rolling profile that only adds new events will drift from the window it claims to represent.
Publish snapshots to the renderer
The calculation layer should expose a stable snapshot containing the profile levels and derived POC/VAH/VAL. The rendering layer can map visible tick levels and their volumes to pixels, respond to viewport changes, and handle chart scaling without owning the accumulator’s mutable state.
If updates and rendering can run concurrently, publish immutable snapshots or synchronize access; do not let a renderer enumerate a collection while another thread changes it. Measure processing time, worst-case update latency, allocations, recomputation frequency, synchronization waits, and render time before replacing a dictionary with arrays or chunked storage. A data structure that is faster for one price range or update pattern may be worse for another.
Validate the output before optimizing
- Use a small hand-checkable set of executions with repeated prices; confirm each bucket total and the overall total.
- Include prices near tick boundaries and negative or extreme values if the instrument permits them; confirm normalization and overflow behavior.
- Test tied POC and tied adjacent Value Area volumes so deterministic behavior is visible.
- Test sparse prices, zero-volume observations, a single-bucket profile, and an empty profile.
- For bars, document the approximation used to distribute or assign bar volume; do not compare it as if it were tick-level data.
- For rolling profiles, verify that adding and expiring the same observations returns buckets to their expected values.
- If matching a platform, compare outputs using the same input resolution, price grouping, period boundaries, percentage, and expansion rules.
The foundational implementation choices—tick-index keys, accumulation, derived values, and separating rendering—are also outlined in Building a Volume Profile Indicator From Raw Market Data in C#. The correct feed semantics, session calendar, API integration, and precise parity rules still depend on the instrument, vendor, and platform you choose.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




