<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ICF Market LTD</title>
    <description>The latest articles on DEV Community by ICF Market LTD (@icf_market).</description>
    <link>https://dev.to/icf_market</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4152444%2Fb32aad45-0500-4a91-854a-25a1b30e6a3c.jpg</url>
      <title>DEV Community: ICF Market LTD</title>
      <link>https://dev.to/icf_market</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/icf_market"/>
    <language>en</language>
    <item>
      <title>NinjaTrader Tick Replay: What Historical Order Flow Really Contains</title>
      <dc:creator>ICF Market LTD</dc:creator>
      <pubDate>Sat, 31 Oct 2026 11:55:43 +0000</pubDate>
      <link>https://dev.to/icf_market/ninjatrader-tick-replay-what-historical-order-flow-really-contains-3472</link>
      <guid>https://dev.to/icf_market/ninjatrader-tick-replay-what-historical-order-flow-really-contains-3472</guid>
      <description>&lt;p&gt;An order-flow indicator can behave correctly on a live NinjaTrader chart yet produce a different-looking historical delta. Sometimes the script never processed historical trade events; sometimes the events were present but the &lt;strong&gt;historical quote context was incomplete&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Tick Replay can help with the first problem. It does &lt;strong&gt;not&lt;/strong&gt; automatically solve the second.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In brief:&lt;/strong&gt; Tick Replay rebuilds historical bars using stored trade ticks and replays &lt;code&gt;Last&lt;/code&gt; events. It does not provide a complete history of changing bid/ask quotes or depth, and it does not make historical strategy fills equivalent to live execution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Tick Replay changes
&lt;/h2&gt;

&lt;p&gt;In a normal historical calculation, an indicator commonly works from completed bars. Setting &lt;code&gt;Calculate = Calculate.OnEachTick&lt;/code&gt; by itself does not create intrabar events from data that is not available.&lt;/p&gt;

&lt;p&gt;With supported historical tick data and Tick Replay enabled, NinjaTrader reconstructs historical bars from stored trade events. A script designed for Tick Replay can receive historical &lt;code&gt;OnBarUpdate()&lt;/code&gt; and &lt;code&gt;OnMarketData()&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;Two event paths matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OnBarUpdate()&lt;/code&gt;&lt;/strong&gt; follows the indicator's &lt;code&gt;Calculate&lt;/code&gt; mode, such as &lt;code&gt;OnEachTick&lt;/code&gt;, &lt;code&gt;OnPriceChange&lt;/code&gt;, or &lt;code&gt;OnBarClose&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OnMarketData()&lt;/code&gt;&lt;/strong&gt; receives historical &lt;code&gt;MarketDataType.Last&lt;/code&gt; events under Tick Replay, including the event's price, volume, and associated best bid/ask prices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NinjaTrader documents that, for the replayed sequence, the relevant &lt;code&gt;OnBarUpdate()&lt;/code&gt; runs &lt;strong&gt;before&lt;/strong&gt; its corresponding &lt;code&gt;OnMarketData()&lt;/code&gt; event. This matters when you group per-trade volume into bars: the bar boundary and the first Last event of the new bar are not interchangeable callbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key limitation: Last events are not full quote history
&lt;/h2&gt;

&lt;p&gt;Historical Tick Replay preserves the best bid and ask &lt;strong&gt;prices associated with a stored Last event&lt;/strong&gt;. It does &lt;strong&gt;not&lt;/strong&gt; replay the independent stream of historical bid/ask changes or bid/ask volume events. Nor does it reconstruct historical Level II depth.&lt;/p&gt;

&lt;p&gt;Even the trade-time bid/ask context deserves scrutiny: NinjaTrader documents that when the source data lacks those values, the platform may &lt;strong&gt;substitute&lt;/strong&gt; bid/ask prices. A classifier can still return a side, but the result is not proof of genuine historical aggressor-side data.&lt;/p&gt;

&lt;p&gt;This distinction is easy to miss when historical charts look convincingly complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal NinjaScript Last-event example
&lt;/h2&gt;

&lt;p&gt;This is an &lt;strong&gt;excerpt for an indicator class&lt;/strong&gt;, not a complete copy-and-compile NinjaScript indicator. It demonstrates where the replayed trade's size and quote context come from; it deliberately leaves session boundaries and data-provenance validation to the surrounding implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fields declared inside an indicator class&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;buyVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;sellVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;unknownVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnMarketData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MarketDataEventArgs&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarketDataType&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;MarketDataType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Last&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// A sane spread is not proof that quotes are historical originals.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bid&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ask&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bid&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;unknownVolume&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;buyVolume&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sellVolume&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="n"&gt;unknownVolume&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Inside the spread&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the use of &lt;strong&gt;&lt;code&gt;e.Volume&lt;/code&gt;&lt;/strong&gt;, the size of this Last event. Using &lt;code&gt;Volume[0]&lt;/code&gt; as though it were the size of an individual print can double-count volume: it represents bar-series volume, not necessarily the current trade's volume.&lt;/p&gt;

&lt;p&gt;These counters are &lt;strong&gt;running totals&lt;/strong&gt;. A bar-level footprint needs additional keys for the instrument, bar/session identity, and integer price level, plus explicit commit/reset logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three traps that distort historical delta
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Using real-time bid/ask helpers for historical trades
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;GetCurrentBid()&lt;/code&gt; and &lt;code&gt;GetCurrentAsk()&lt;/code&gt; are real-time-oriented. NinjaTrader documents that historical calls can return the evaluated bar's close instead. For replayed historical Last events, use the &lt;strong&gt;&lt;code&gt;MarketDataEventArgs.Bid&lt;/code&gt; and &lt;code&gt;.Ask&lt;/code&gt;&lt;/strong&gt; associated with &lt;code&gt;OnMarketData()&lt;/code&gt;—while still checking their provenance.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Confusing calculation frequency with data availability
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Calculate.OnEachTick&lt;/code&gt; controls a calculation path; it does not manufacture missing historical ticks. Conversely, an implementation centered on &lt;code&gt;OnMarketData()&lt;/code&gt; does not always need an expensive &lt;code&gt;OnEachTick&lt;/code&gt; calculation in &lt;code&gt;OnBarUpdate()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Design for the events actually used, then measure performance with your instrument and provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Treating reconstructed data as backtest-quality execution data
&lt;/h3&gt;

&lt;p&gt;Tick Replay is designed for historical data-event processing, &lt;strong&gt;not precise historical order-fill simulation&lt;/strong&gt;. Missing quote information, event ordering assumptions, and unsupported bar types can all affect the result.&lt;/p&gt;

&lt;p&gt;NinjaTrader also notes compatibility limits for some multi-time-frame/multi-instrument scripts and certain bar types, including built-in Renko and Line Break variants. Test those combinations rather than assuming support.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical checklist before you trust a chart
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the provider supplies the required historical &lt;strong&gt;Last tick data&lt;/strong&gt; for the requested dates.&lt;/li&gt;
&lt;li&gt;Inspect how the provider and platform handle missing trade-time bid/ask context.&lt;/li&gt;
&lt;li&gt;Log several &lt;code&gt;OnBarUpdate()&lt;/code&gt; / &lt;code&gt;OnMarketData()&lt;/code&gt; sequences near a bar boundary.&lt;/li&gt;
&lt;li&gt;Confirm that the sum of event sizes matches the volume accounting in your aggregation.&lt;/li&gt;
&lt;li&gt;Test the historical-to-realtime transition and compare with recorded live data where possible.&lt;/li&gt;
&lt;li&gt;Benchmark the actual dataset; avoid universal claims about Tick Replay loading time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At &lt;strong&gt;ICF Market&lt;/strong&gt;, our &lt;a href="https://icf-market.net/education/volume-and-order-flow-analysis" rel="noopener noreferrer"&gt;free volume and order-flow education series&lt;/a&gt; explains the trading concepts behind delta and volume-at-price. This article focuses on the &lt;strong&gt;NinjaScript data mechanics&lt;/strong&gt; required to represent those concepts honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tick Replay replays historical &lt;strong&gt;Last&lt;/strong&gt; events, not an independent historical quote-change stream.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OnMarketData()&lt;/code&gt; is the appropriate source for trade-event price, size, and associated best bid/ask values.&lt;/li&gt;
&lt;li&gt;Quote availability, bar boundaries, and execution-fidelity limits still need explicit tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; When you build historical delta in NinjaScript, how do you indicate that bid/ask context was missing or substituted by the provider?&lt;/p&gt;

&lt;h2&gt;
  
  
  Official NinjaTrader references
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ninjatrader.com/support/helpguides/nt8/developing_for__tick_replay.htm" rel="noopener noreferrer"&gt;Developing for Tick Replay&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ninjatrader.com/support/helpguides/nt8/onmarketdata.htm" rel="noopener noreferrer"&gt;OnMarketData()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ninjatrader.com/support/helpguides/nt8/marketdataeventargs.htm" rel="noopener noreferrer"&gt;MarketDataEventArgs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ninjatrader.com/support/helpguides/nt8/getcurrentask.htm" rel="noopener noreferrer"&gt;GetCurrentAsk()&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This article was prepared with AI assistance and reviewed against NinjaTrader's published documentation. The code excerpt was not compile-tested in NinjaTrader 8/8.1 during this editorial review. Educational content only; not trading advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ninjatrader</category>
      <category>csharp</category>
      <category>algotrading</category>
      <category>trading</category>
    </item>
    <item>
      <title>Classifying Aggressor Side in Order Flow: Quote Rule vs. Tick Rule</title>
      <dc:creator>ICF Market LTD</dc:creator>
      <pubDate>Sat, 31 Oct 2026 11:21:18 +0000</pubDate>
      <link>https://dev.to/icf_market/aggressor-side-classification-in-order-flow-indicators-quote-rule-tick-rule-and-unknown-trades-53m4</link>
      <guid>https://dev.to/icf_market/aggressor-side-classification-in-order-flow-indicators-quote-rule-tick-rule-and-unknown-trades-53m4</guid>
      <description>&lt;p&gt;A footprint chart may show buy volume, sell volume, and delta down to the price level. But a trade print alone does not necessarily tell you &lt;strong&gt;which side initiated the trade&lt;/strong&gt;. Every classification method relies on some combination of exchange flags, quote context, or a price-change heuristic.&lt;/p&gt;

&lt;p&gt;The engineering challenge is not simply to label every print. It is to keep the classification &lt;strong&gt;auditable when the data is incomplete&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In brief:&lt;/strong&gt; Prefer a documented, time-aligned aggressor flag or quote context when available. Treat tick-rule results as estimates, and track trades you cannot classify instead of quietly forcing them into buy or sell volume.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What an aggressor-side label actually means
&lt;/h2&gt;

&lt;p&gt;Every executed trade has a buyer and a seller. A &lt;em&gt;buyer-initiated&lt;/em&gt; trade generally means an incoming order executed against resting sell liquidity; a &lt;em&gt;seller-initiated&lt;/em&gt; trade executed against resting buy liquidity.&lt;/p&gt;

&lt;p&gt;Neither label predicts what price will do next. It describes the likely &lt;strong&gt;mechanism of execution&lt;/strong&gt;, not a trading signal.&lt;/p&gt;

&lt;p&gt;Some feeds supply aggressor-side flags directly. Others expose prints and inside quotes from which a label must be inferred. Before combining data sources, check what each feed promises about quote timestamps, ordering, and venue coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two common classification methods
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Quote rule: compare the trade with the spread
&lt;/h3&gt;

&lt;p&gt;Given an appropriate bid/ask snapshot from the time of the trade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trade &lt;strong&gt;at or above the ask&lt;/strong&gt; → buyer-initiated candidate.&lt;/li&gt;
&lt;li&gt;Trade &lt;strong&gt;at or below the bid&lt;/strong&gt; → seller-initiated candidate.&lt;/li&gt;
&lt;li&gt;Trade &lt;strong&gt;inside the spread&lt;/strong&gt; → uncertain under this rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The word &lt;em&gt;appropriate&lt;/em&gt; matters. A quote received just before a trade is not necessarily the quote that applied when it executed. Trade and quote messages can arrive on different channels or out of order.&lt;/p&gt;

&lt;p&gt;Reject invalid, locked, crossed, or stale quotes according to a documented feed policy. In particular, if both comparisons could be true, do not let the order of your &lt;code&gt;if&lt;/code&gt; statements decide the trade's side.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Tick rule: infer from the previous trade price
&lt;/h3&gt;

&lt;p&gt;If trustworthy quote context is missing, the tick rule provides a cheaper fallback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Price above the previous print → buy candidate.&lt;/li&gt;
&lt;li&gt;Price below the previous print → sell candidate.&lt;/li&gt;
&lt;li&gt;Unchanged price → use an explicit &lt;em&gt;zero-tick policy&lt;/em&gt; or mark unknown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One common zero-tick policy carries forward the last &lt;strong&gt;nonzero&lt;/strong&gt; price-change direction. That is still a heuristic. A wider spread, sparse trading, or incorrectly ordered prints can make it diverge from a quote-based label.&lt;/p&gt;

&lt;h2&gt;
  
  
  C# example: preserve an Unknown state
&lt;/h2&gt;

&lt;p&gt;The following excerpt illustrates a conservative classification policy. It is &lt;strong&gt;application-level C#&lt;/strong&gt;, not a complete NinjaScript indicator. It assumes trade and quote timestamps have already been normalized to a comparable clock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Buy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Sell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TradePrint&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;TimeUtc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuoteSnapshot&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;Bid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;TimeUtc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt; &lt;span class="nf"&gt;Classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TradePrint&lt;/span&gt; &lt;span class="n"&gt;trade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;QuoteSnapshot&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;previousPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AggressorSide&lt;/span&gt; &lt;span class="n"&gt;lastNonzeroTickSide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;maxQuoteAge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;usableQuote&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
        &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bid&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ask&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bid&lt;/span&gt;
        &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;trade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeUtc&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeUtc&lt;/span&gt;
        &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;trade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeUtc&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TimeUtc&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;maxQuoteAge&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usableQuote&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sell&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Inside the spread&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Fallback only when quote context is unavailable.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;previousPrice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;previousPrice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trade&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;previousPrice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sell&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lastNonzeroTickSide&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Explicit zero-tick policy&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;maxQuoteAge&lt;/code&gt; threshold is an &lt;strong&gt;application policy&lt;/strong&gt;, not a universal constant. This example also does not prove that two timestamps from different feeds refer to the same clock or that the quote was valid at execution time.&lt;/p&gt;

&lt;p&gt;For production code, normalize prices to integer tick indices where appropriate, record the &lt;strong&gt;method used&lt;/strong&gt; for each label, and test how the zero-tick state survives repeated prices and session boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should happen to unknown volume?
&lt;/h2&gt;

&lt;p&gt;Maintain three buckets rather than quietly assigning every print to a side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;classified_delta  = buy_volume - sell_volume
classified_volume = buy_volume + sell_volume
total_volume      = classified_volume + unknown_volume
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A delta calculated from classified trades may still be useful. It should simply not be presented as though &lt;em&gt;all&lt;/em&gt; traded volume was classified with equal certainty.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;unclassified-volume percentage&lt;/strong&gt; is a useful quality metric: if it spikes on one instrument, session, or provider, that can reveal a quote-data problem before the histogram misleads someone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the data pipeline, not just the chart
&lt;/h2&gt;

&lt;p&gt;A compact regression fixture should include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Trades at bid and ask with valid, synchronized quotes.&lt;/li&gt;
&lt;li&gt;A print inside the spread and a locked or crossed quote.&lt;/li&gt;
&lt;li&gt;A stale quote, out-of-order timestamps, and the first print of a session.&lt;/li&gt;
&lt;li&gt;Multiple consecutive trades at the same price.&lt;/li&gt;
&lt;li&gt;A conservation check: &lt;code&gt;buy + sell + unknown == total&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Keep &lt;strong&gt;ingestion&lt;/strong&gt;, &lt;strong&gt;classification&lt;/strong&gt;, &lt;strong&gt;aggregation&lt;/strong&gt;, and &lt;strong&gt;rendering&lt;/strong&gt; separate. That makes it possible to compare two classification policies against the same recorded data without changing the footprint renderer.&lt;/p&gt;

&lt;p&gt;For background on tape reading and delta, I work with &lt;strong&gt;ICF Market&lt;/strong&gt;, whose &lt;a href="https://icf-market.net/education/volume-and-order-flow-analysis" rel="noopener noreferrer"&gt;free volume and order-flow education series&lt;/a&gt; covers the underlying concepts. The C# policy above is an educational illustration, &lt;strong&gt;not a claim about ICF Market's internal implementation&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An aggressor-side label is a statement about execution, not market direction.&lt;/li&gt;
&lt;li&gt;Quote-rule and tick-rule labels are only as reliable as their inputs and documented assumptions.&lt;/li&gt;
&lt;li&gt;Recording &lt;strong&gt;Unknown&lt;/strong&gt; protects volume accounting and makes feed-quality problems visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Discussion:&lt;/strong&gt; If your feed supplies an aggressor flag &lt;em&gt;and&lt;/em&gt; synchronized bid/ask quotes, do you trust one as authoritative, or do you log disagreements for later analysis?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: This article was prepared with AI assistance. The C# excerpt is illustrative and was not tested against a live market-data feed. Educational content only; not trading advice.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>algotrading</category>
    </item>
    <item>
      <title>Building a Volume Profile Indicator From Raw Market Data in C#</title>
      <dc:creator>ICF Market LTD</dc:creator>
      <pubDate>Wed, 30 Sep 2026 16:46:35 +0000</pubDate>
      <link>https://dev.to/icf_market/building-a-volume-profile-indicator-from-raw-market-data-in-c-1ffn</link>
      <guid>https://dev.to/icf_market/building-a-volume-profile-indicator-from-raw-market-data-in-c-1ffn</guid>
      <description>&lt;h1&gt;
  
  
  Building a High-Performance Volume Profile in C
&lt;/h1&gt;

&lt;p&gt;Most charting platforms display volume as a single bar beneath each candle, showing how much trading activity occurred during a given time interval.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Volume Profile&lt;/strong&gt; asks a different question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much volume traded at each price?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of grouping volume by time, a Volume Profile groups it by price. The result is a histogram that highlights the price levels where the market concentrated most of its activity, as well as the areas through which price moved relatively quickly.&lt;/p&gt;

&lt;p&gt;The basic idea is simple.&lt;/p&gt;

&lt;p&gt;Building a Volume Profile correctly from raw trade data — while keeping it efficient enough for real-time updates — is a much more interesting engineering problem.&lt;/p&gt;

&lt;p&gt;This article presents a practical architecture for implementing one in C#, with a focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Price indexing&lt;/li&gt;
&lt;li&gt;Trade classification&lt;/li&gt;
&lt;li&gt;Point of Control (POC)&lt;/li&gt;
&lt;li&gt;Value Area calculation&lt;/li&gt;
&lt;li&gt;Incremental updates&lt;/li&gt;
&lt;li&gt;Rendering performance&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Represent Prices as Integer Ticks
&lt;/h2&gt;

&lt;p&gt;A first implementation might use a dictionary like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;volumeByPrice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is convenient, but using floating-point prices as identity keys is unnecessary and can introduce subtle problems.&lt;/p&gt;

&lt;p&gt;Suppose an instrument has a minimum price increment, or tick size, of &lt;code&gt;0.25&lt;/code&gt;. Prices such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5234.00
5234.25
5234.50
5234.75
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;already belong to a discrete integer grid.&lt;/p&gt;

&lt;p&gt;Instead of storing the price itself, convert each price to a &lt;strong&gt;tick index&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;PriceToTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;tickSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;checked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;tickSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;MidpointRounding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AwayFromZero&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The price is now represented by an integer.&lt;/p&gt;

&lt;p&gt;For example, with a tick size of &lt;code&gt;0.25&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5234.00 -&amp;gt; 20936
5234.25 -&amp;gt; 20937
5234.50 -&amp;gt; 20938
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production code, if the trading platform provides its own canonical price-rounding utilities, use those instead of introducing a separate rounding convention.&lt;/p&gt;

&lt;p&gt;The important principle is that &lt;strong&gt;price identity should be based on ticks rather than floating-point equality&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Start with Correctness Before Optimizing Storage
&lt;/h2&gt;

&lt;p&gt;Once prices are represented as integer ticks, a dictionary becomes a much safer and clearer starting point.&lt;/p&gt;

&lt;p&gt;First, define the possible aggressor sides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Buy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Sell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Unknown&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then define the information stored at each price level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileLevel&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;TotalVolume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;BuyVolume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;SellVolume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;UnknownVolume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;TradeCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The profile can initially be stored as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProfileLevel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Updating the profile for each incoming trade is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnTrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AggressorSide&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;tickSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;PriceToTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tickSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;ProfileLevel&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ProfileLevel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalVolume&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TradeCount&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;

    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BuyVolume&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;AggressorSide&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SellVolume&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnknownVolume&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;Dictionary&amp;lt;long, ProfileLevel&amp;gt;&lt;/code&gt; is a good correctness-first implementation.&lt;/p&gt;

&lt;p&gt;If profiling later shows that dictionary lookup overhead is significant, the storage layer can be replaced with an offset array, chunked array, or another contiguous structure without changing the rest of the profile logic.&lt;/p&gt;

&lt;p&gt;That decision should come from &lt;strong&gt;measurement rather than assumption&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Do &lt;code&gt;BuyVolume&lt;/code&gt; and &lt;code&gt;SellVolume&lt;/code&gt; Come From?
&lt;/h2&gt;

&lt;p&gt;This is an important detail.&lt;/p&gt;

&lt;p&gt;A trade does not universally arrive with a perfect &lt;em&gt;buyer-initiated&lt;/em&gt; or &lt;em&gt;seller-initiated&lt;/em&gt; label. Depending on the data feed and trading platform, the aggressor side may need to be inferred from market context.&lt;/p&gt;

&lt;p&gt;A common approach is to compare the execution price with the current bid and ask.&lt;/p&gt;

&lt;p&gt;A trade executing at or near the ask can be classified as &lt;strong&gt;buyer-aggressed&lt;/strong&gt;, while a trade executing at or near the bid can be classified as &lt;strong&gt;seller-aggressed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Trades occurring between the quotes may require a fallback rule or may simply remain classified as &lt;code&gt;Unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The exact classification method is feed- and platform-dependent.&lt;/p&gt;

&lt;p&gt;For this reason, the Volume Profile itself and the buy/sell classification logic should be treated as separate concerns.&lt;/p&gt;

&lt;p&gt;Total volume can still be calculated correctly even when aggressor classification is unavailable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Point of Control
&lt;/h2&gt;

&lt;p&gt;Once volume has been accumulated by price, calculating the &lt;strong&gt;Point of Control (POC)&lt;/strong&gt; is conceptually simple.&lt;/p&gt;

&lt;p&gt;The POC is the price level with the highest accumulated volume.&lt;/p&gt;

&lt;p&gt;A full implementation could calculate it by scanning every level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POC = level with maximum TotalVolume
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, a live profile usually does not need to scan the entire histogram after every trade.&lt;/p&gt;

&lt;p&gt;When a trade updates one price level, compare that level's new volume with the currently cached POC volume.&lt;/p&gt;

&lt;p&gt;If the updated level becomes larger, update the cached POC.&lt;/p&gt;

&lt;p&gt;This makes normal POC maintenance effectively &lt;strong&gt;constant-time per trade update&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define Tie-Breaking Explicitly
&lt;/h3&gt;

&lt;p&gt;One small detail matters: ties.&lt;/p&gt;

&lt;p&gt;If two price levels contain exactly the same volume, the implementation should use a deterministic tie-breaking rule.&lt;/p&gt;

&lt;p&gt;Possible approaches include selecting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The lower price&lt;/li&gt;
&lt;li&gt;The higher price&lt;/li&gt;
&lt;li&gt;The level closest to another reference point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The specific choice is less important than defining the behavior explicitly.&lt;/p&gt;

&lt;p&gt;Undefined tie behavior can cause the POC to appear to jump unpredictably between price levels.&lt;/p&gt;




&lt;h2&gt;
  
  
  Value Area Is More Complicated
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Value Area&lt;/strong&gt; is typically defined as a contiguous region around the POC containing a configurable fraction of the profile's total volume — commonly around &lt;strong&gt;70%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, there is an important implementation detail:&lt;/p&gt;

&lt;p&gt;Different platforms may use slightly different Value Area expansion conventions.&lt;/p&gt;

&lt;p&gt;Some implementations compare one price level at a time. Others compare groups or pairs of levels. Tie-handling rules may also differ.&lt;/p&gt;

&lt;p&gt;If the objective is exact parity with another trading platform, its documented Value Area convention should be reproduced exactly.&lt;/p&gt;

&lt;p&gt;For this example, we will use a simple &lt;strong&gt;contiguous one-level expansion rule&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The process is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start at the POC.&lt;/li&gt;
&lt;li&gt;Look at the next available price level above and below the current Value Area.&lt;/li&gt;
&lt;li&gt;Compare their volumes.&lt;/li&gt;
&lt;li&gt;Add whichever level contains more volume.&lt;/li&gt;
&lt;li&gt;Continue until the target percentage of total volume has been reached.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;First, define a helper method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="nf"&gt;VolumeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProfileLevel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;ProfileLevel&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalVolume&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0L&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then calculate the Value Area:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;Low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;ComputeValueArea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IReadOnlyDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ProfileLevel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;pocTick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;minTick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;maxTick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;totalVolume&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;valueAreaFraction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;targetVolume&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;totalVolume&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;valueAreaFraction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pocTick&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pocTick&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;accumulated&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;VolumeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pocTick&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accumulated&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;targetVolume&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;minTick&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxTick&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;volumeAbove&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;maxTick&lt;/span&gt;
                &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;VolumeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;volumeBelow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;minTick&lt;/span&gt;
                &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;VolumeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;volumeAbove&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;volumeBelow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;volumeAbove&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;accumulated&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;volumeAbove&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;--;&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;volumeBelow&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;accumulated&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;volumeBelow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This algorithm is intentionally simple.&lt;/p&gt;

&lt;p&gt;It produces a contiguous Value Area, but it should not be treated as a universal market-profile standard.&lt;/p&gt;

&lt;p&gt;The important engineering decision is to &lt;strong&gt;define the chosen convention explicitly and test it against known profiles&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation Note: Sparse Profiles and Zero-Volume Ties
&lt;/h3&gt;

&lt;p&gt;An important edge case arises in sparse volume profiles, where adjacent price levels may contain no traded volume.&lt;/p&gt;

&lt;p&gt;In the simplified implementation above, the condition &lt;code&gt;volumeAbove &amp;gt;= volumeBelow&lt;/code&gt; favors upward expansion whenever both sides have equal volume, including when both are zero. As a result, the Value Area boundaries can depend on the tie-breaking convention rather than solely on the distribution of traded volume.&lt;/p&gt;

&lt;p&gt;For example, consider a profile with 1,000 contracts at tick 100 (POC), 500 at tick 98, 40 at tick 103, and 460 at tick 90. With a 70% target, the current implementation produces a Value Area from ticks 98 to 103, while a downward tie-breaking preference produces ticks 98 to 100.&lt;/p&gt;

&lt;p&gt;Neither convention should be assumed to represent a universal standard. Production implementations should explicitly document their expansion and tie-breaking rules, consider alternatives such as look-ahead to the nearest non-empty levels, and validate the resulting VAH and VAL against sparse-profile test cases and the target platform's calculation convention.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Performance Problem: Value Area Updates
&lt;/h2&gt;

&lt;p&gt;The POC is relatively easy to maintain incrementally.&lt;/p&gt;

&lt;p&gt;Value Area is different.&lt;/p&gt;

&lt;p&gt;Every trade changes the total profile volume, and any change to a price bucket can potentially affect where the Value Area boundaries should be located.&lt;/p&gt;

&lt;p&gt;Therefore, the following optimization is unsafe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Only recompute Value Area when the POC changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The POC can remain at exactly the same price while changes in the surrounding volume distribution cause the &lt;strong&gt;Value Area High (VAH)&lt;/strong&gt; or &lt;strong&gt;Value Area Low (VAL)&lt;/strong&gt; to move.&lt;/p&gt;

&lt;p&gt;A more practical architecture is to mark the Value Area as &lt;strong&gt;dirty&lt;/strong&gt; whenever the profile changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;valueAreaDirty&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnProfileChanged&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;valueAreaDirty&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of recalculating the Value Area after every individual trade, recompute it at a controlled cadence.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trade arrives
    |
    +--&amp;gt; Update price bucket
    |
    +--&amp;gt; Update cached POC if necessary
    |
    +--&amp;gt; Mark Value Area dirty


Every 100–250 ms
    |
    +--&amp;gt; If Value Area is dirty
            |
            +--&amp;gt; Recompute VAH / VAL
            |
            +--&amp;gt; Clear dirty flag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact interval depends on the application.&lt;/p&gt;

&lt;p&gt;A research tool, historical-profile calculator, execution interface, and visual chart indicator may all have very different latency requirements.&lt;/p&gt;

&lt;p&gt;The important distinction is between:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data accuracy&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;and&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual refresh frequency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The underlying volume buckets can update on every incoming trade, while more expensive derived values are refreshed at a controlled cadence.&lt;/p&gt;

&lt;p&gt;A final full recomputation can also be performed when a session or profile is finalized.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dictionary or Array?
&lt;/h2&gt;

&lt;p&gt;Once the implementation is working correctly, the storage layer can be benchmarked.&lt;/p&gt;

&lt;p&gt;A dictionary offers several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It handles expanding price ranges naturally.&lt;/li&gt;
&lt;li&gt;It requires relatively little memory-management code.&lt;/li&gt;
&lt;li&gt;It is easy to reason about.&lt;/li&gt;
&lt;li&gt;It avoids allocating storage for unused price levels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many instruments, the number of active price levels within a single profile is modest enough that a dictionary is perfectly acceptable.&lt;/p&gt;

&lt;p&gt;If profiling shows that dictionary lookup overhead is actually significant, an &lt;strong&gt;offset array&lt;/strong&gt; can eliminate hashing entirely.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arrayIndex = tickIndex - baseTick
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The main challenge is expansion in both directions.&lt;/p&gt;

&lt;p&gt;Repeatedly reallocating and copying a large array whenever the session reaches a new high or low can eliminate much of the theoretical performance benefit.&lt;/p&gt;

&lt;p&gt;Two possible alternatives are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Over-allocating storage in blocks&lt;/li&gt;
&lt;li&gt;Using a chunked or page-based layout, with each block containing a fixed number of price levels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, the correct choice depends on measurement.&lt;/p&gt;

&lt;p&gt;The fastest data structure on paper is not necessarily the fastest overall system once resizing, memory allocation, cache behavior, and implementation complexity are taken into account.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keep Computation Separate from Rendering
&lt;/h2&gt;

&lt;p&gt;The profile engine should understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Price ticks&lt;/li&gt;
&lt;li&gt;Volume&lt;/li&gt;
&lt;li&gt;Trade count&lt;/li&gt;
&lt;li&gt;Aggressor classification&lt;/li&gt;
&lt;li&gt;POC&lt;/li&gt;
&lt;li&gt;Value Area&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It should know &lt;strong&gt;nothing about pixels&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Rendering is a separate problem.&lt;/p&gt;

&lt;p&gt;A renderer needs to determine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which price levels are currently visible&lt;/li&gt;
&lt;li&gt;How much horizontal space is available&lt;/li&gt;
&lt;li&gt;How volume maps to bar width&lt;/li&gt;
&lt;li&gt;When the viewport has changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping computation and rendering separate provides several benefits.&lt;/p&gt;

&lt;p&gt;The same profile model can support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A classic horizontal Volume Profile histogram&lt;/li&gt;
&lt;li&gt;A heatmap&lt;/li&gt;
&lt;li&gt;A footprint-style visualization&lt;/li&gt;
&lt;li&gt;A non-visual analytics component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation also prevents expensive visual operations from creeping into the market-data processing path.&lt;/p&gt;

&lt;p&gt;If market-data processing and rendering occur on different execution contexts or threads, expose a stable snapshot of the profile state or use a short, well-defined synchronization strategy.&lt;/p&gt;

&lt;p&gt;The renderer should not iterate over mutable profile state while that state is simultaneously being updated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measure Before Optimizing
&lt;/h2&gt;

&lt;p&gt;For a live indicator, useful performance metrics include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average processing time per trade&lt;/li&gt;
&lt;li&gt;Worst-case processing time&lt;/li&gt;
&lt;li&gt;Allocations per update&lt;/li&gt;
&lt;li&gt;Value Area recomputations per second&lt;/li&gt;
&lt;li&gt;Number of active price levels&lt;/li&gt;
&lt;li&gt;Render time per frame&lt;/li&gt;
&lt;li&gt;Time spent waiting on synchronization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without measurements, it is easy to spend hours replacing a dictionary that was never the actual bottleneck.&lt;/p&gt;

&lt;p&gt;In many real-time charting systems, the larger performance problems come from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rendering overhead&lt;/li&gt;
&lt;li&gt;Unnecessary object allocation&lt;/li&gt;
&lt;li&gt;Excessive synchronization&lt;/li&gt;
&lt;li&gt;Repeated calculation of derived values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These can create more performance problems than the initial volume accumulation itself.&lt;/p&gt;

&lt;p&gt;A useful engineering order is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correctness first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instrumentation second.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimization third.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Architecture
&lt;/h2&gt;

&lt;p&gt;A clean implementation naturally separates into three distinct layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Ingestion Layer
&lt;/h3&gt;

&lt;p&gt;The ingestion layer receives trades and is responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normalizing prices to integer ticks&lt;/li&gt;
&lt;li&gt;Classifying aggressor side where possible&lt;/li&gt;
&lt;li&gt;Updating the appropriate volume buckets&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Profile Layer
&lt;/h3&gt;

&lt;p&gt;The profile layer manages derived state, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Point of Control&lt;/li&gt;
&lt;li&gt;Total profile volume&lt;/li&gt;
&lt;li&gt;Value Area High&lt;/li&gt;
&lt;li&gt;Value Area Low&lt;/li&gt;
&lt;li&gt;Periodic Value Area recomputation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Rendering Layer
&lt;/h3&gt;

&lt;p&gt;The rendering layer consumes a stable representation of the profile state and converts it into pixels.&lt;/p&gt;

&lt;p&gt;It handles visual concerns such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visible price ranges&lt;/li&gt;
&lt;li&gt;Histogram dimensions&lt;/li&gt;
&lt;li&gt;Bar scaling&lt;/li&gt;
&lt;li&gt;Viewport changes&lt;/li&gt;
&lt;li&gt;Frame rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping these three layers separate makes the system easier to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test&lt;/li&gt;
&lt;li&gt;Benchmark&lt;/li&gt;
&lt;li&gt;Maintain&lt;/li&gt;
&lt;li&gt;Optimize&lt;/li&gt;
&lt;li&gt;Extend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also allows the underlying profile engine to remain independent of any particular visualization method.&lt;/p&gt;




&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;If you've built a live Volume Profile or a similar market-microstructure tool, how do you handle Value Area recomputation?&lt;/p&gt;

&lt;p&gt;Do you periodically recompute it from the full profile, maintain a more sophisticated incremental structure, or use another approach entirely?&lt;/p&gt;

&lt;p&gt;I'm particularly interested in implementations that keep the data model simple while still performing well under high message rates.&lt;/p&gt;

</description>
      <category>algotrading</category>
      <category>csharp</category>
      <category>performance</category>
      <category>fintech</category>
    </item>
  </channel>
</rss>
