October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk8 min

C# Volume Profile Indicator: Build It From Market Data

A practical guide to building a C# volume profile engine, from tick-grid price keys and volume buckets to POC, Value Area, rolling updates, and rendering.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.