VolumeProfile of visible bars using secondary tick data

VolSys has some nice VolumeProfile options. So does TopstepX. This indicator adds VolumeProfile for visible bars. Plus with a key modifier, volume profile between cursor and right side of the screen. I like the data structures so I share it here so others can use it as a reference. It’s completely AI coded.

#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.DrawingTools;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
    /// <summary>
    /// Display Volume Profile — builds a volume profile from all completed bars
    /// currently visible on the chart.  Scrolling or zooming incrementally adds
    /// or subtracts only the bars that entered or left the visible window;
    /// a full rebuild is used when the delta exceeds half the current window span.
    ///
    /// MODIFIER KEY (Ctrl) mode:
    ///   Hold Ctrl and hover over the chart.  The profile is drawn from the
    ///   rightmost completed bar back to the bar under the cursor, letting you
    ///   interactively inspect any sub-range.  Release Ctrl to revert to the
    ///   full visible window.
    ///
    /// DESIGN — bidirectional sliding window over a permanent BarData store:
    ///   allBars        : Dictionary keyed by bar index; written once per bar on
    ///                    the primary series, never modified afterward.
    ///   displayProfile : SortedDictionary always equal to Σ allBars[windowFrom..windowTo].
    ///                    Updated incrementally in OnRender when the visible range
    ///                    changes; rebuilt from scratch when the delta is large.
    ///   Tick path      : AccumulateBar into currentBar only — displayProfile is
    ///                    never touched per tick.  The live bar is intentionally
    ///                    excluded from the display profile.
    /// </summary>
    public class DisplayVolumeProfile : Indicator
    {
        // -----------------------------------------------------------------------
        // Inner types
        // -----------------------------------------------------------------------

        private struct PriceLevel
        {
            public long Buy;
            public long Sell;
            public long Total { get { return Buy + Sell; } }
        }

        /// <summary>
        /// Immutable snapshot of one completed bar's volume by price level.
        /// Written once when the bar closes; read-only afterward.
        /// </summary>
        private class BarData
        {
            public readonly Dictionary<double, PriceLevel> Levels =
                new Dictionary<double, PriceLevel>();
            public long TotalVolume;
        }

        // -----------------------------------------------------------------------
        // State
        // -----------------------------------------------------------------------

        /// <summary>
        /// Permanent store: every completed bar, keyed by primary-series bar index.
        /// Grows monotonically; entries are never mutated or removed.
        /// </summary>
        private Dictionary<int, BarData> allBars;

        /// <summary>Accumulates tick data for the bar currently forming.</summary>
        private BarData currentBar;

        private int lastBarIndex          = -1;
        private int lastCompletedBarIndex = -1;

        // -----------------------------------------------------------------------
        // displayProfile — the single source of truth for rendering and VA.
        //
        // Invariant:  displayProfile == Σ allBars[windowFromIndex..windowToIndex]
        //
        // SortedDictionary: iteration in ascending price order for rendering and
        // value-area traversal without an extra sort allocation.
        // -----------------------------------------------------------------------
        private SortedDictionary<double, PriceLevel> displayProfile;
        private long   displayMaxVol;
        private long   displayTotalVol;
        private double displayPoc;

        private int    windowFromIndex = -1;
        private int    windowToIndex   = -1;

        private double vah, val;

        // -----------------------------------------------------------------------
        // Modifier-key / cursor state
        //
        // cursorBarIndex : bar index under the mouse, updated in the MouseMove
        //                  handler; -1 until the mouse has entered the chart.
        // -----------------------------------------------------------------------
        private int cursorBarIndex = -1;

        // DirectX brushes
        private SharpDX.Direct2D1.Brush volBrushDX;
        private SharpDX.Direct2D1.Brush buyBrushDX;
        private SharpDX.Direct2D1.Brush sellBrushDX;

        // -----------------------------------------------------------------------
        // OnStateChange
        // -----------------------------------------------------------------------

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Name             = "Display Volume Profile";
                Description      = "Volume profile built from all completed bars visible on the chart. "
                                 + "Hold Ctrl to pin the right edge and drag left to inspect any sub-range.";
                IsChartOnly      = true;
                IsOverlay        = true;
                DisplayInDataBox = false;
                DrawOnPricePanel = true;

                EnableModifierKey = true;
                BracketMinutes   = 30;
                ValueArea        = 70;
                Width            = 70;
                OffsetPixels     = 10;
                Opacity          = 40;
                ValueAreaOpacity = 60;
                ShowBuySell      = false;
                ShowPoc          = true;
                ShowValueArea    = true;
                VolumeBrush      = Brushes.CornflowerBlue;
                BuyBrush         = Brushes.DarkCyan;
                SellBrush        = Brushes.MediumVioletRed;
                PocStroke        = new Stroke(Brushes.Goldenrod, 1);
                ValueAreaStroke  = new Stroke(Brushes.CornflowerBlue, DashStyleHelper.Dash, 1);
            }
            else if (State == State.Configure)
            {
                Calculate = Calculate.OnEachTick;
                AddDataSeries(BarsPeriodType.Tick, 1);

                allBars        = new Dictionary<int, BarData>();
                currentBar     = new BarData();
                displayProfile = new SortedDictionary<double, PriceLevel>();
            }
            else if (State == State.Historical)
            {
                SetZOrder(1);

                if (ChartControl != null)
                {
                    ChartControl.PreviewMouseMove += OnChartMouseMove;
                    ChartControl.PreviewKeyUp     += OnChartKeyUp;
                }
            }
            else if (State == State.Terminated)
            {
                if (ChartControl != null)
                {
                    ChartControl.PreviewMouseMove -= OnChartMouseMove;
                    ChartControl.PreviewKeyUp     -= OnChartKeyUp;
                }
            }
        }

        // -----------------------------------------------------------------------
        // Mouse / keyboard event handlers
        // -----------------------------------------------------------------------

        /// <summary>
        /// Tracks the bar under the cursor.  When Ctrl is held, forces a redraw
        /// so the profile updates smoothly as the mouse moves.
        /// </summary>
        private void OnChartMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if (ChartControl == null) return;

            var pos = e.GetPosition(ChartControl);
            cursorBarIndex = ChartBars.GetBarIdxByX(ChartControl, (int)pos.X);

            if (EnableModifierKey &&
                (Keyboard.IsKeyDown(Key.LeftCtrl)  || Keyboard.IsKeyDown(Key.RightCtrl) ||
                 Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)))
                ChartControl.InvalidateVisual();
        }

        /// <summary>
        /// Forces a redraw when Ctrl is released so the profile reverts to the
        /// full visible window immediately.
        /// </summary>
        private void OnChartKeyUp(object sender, System.Windows.Input.KeyEventArgs e)
        {
            if (e.Key == Key.LeftCtrl  || e.Key == Key.RightCtrl  ||
                e.Key == Key.LeftShift || e.Key == Key.RightShift || e.Key == Key.System)
                ChartControl?.InvalidateVisual();
        }

        // -----------------------------------------------------------------------
        // OnBarUpdate
        // -----------------------------------------------------------------------

        protected override void OnBarUpdate()
        {
            // ------------------------------------------------------------------
            // Tick series — accumulate into currentBar only.
            // displayProfile is deliberately not touched here.
            // ------------------------------------------------------------------
            if (BarsInProgress == 1)
            {
                double price  = Closes[1][0];
                long   volume = (long)Volumes[1][0];
                if (volume <= 0) return;

                double ask = BarsArray[1].GetAsk(CurrentBar);
                double bid = BarsArray[1].GetBid(CurrentBar);

                long buyVol, sellVol;
                if (ask > 0 && bid > 0 && bid < ask)
                {
                    buyVol  = price >= ask ? volume : 0;
                    sellVol = price <= bid ? volume : 0;
                    // trades inside the spread: assigned to neither
                }
                else
                {
                    // bid/ask unavailable during historical replay — split equally
                    buyVol  = volume / 2;
                    sellVol = volume - buyVol;
                }

                AccumulateBar(currentBar, price, buyVol, sellVol);
                return;
            }

            // ------------------------------------------------------------------
            // Primary series — bar boundary detected
            // ------------------------------------------------------------------
            if (CurrentBar == lastBarIndex) return;

            // The bar at lastBarIndex just closed.  Snapshot it into allBars.
            if (lastBarIndex >= 0 && currentBar.TotalVolume > 0)
            {
                allBars[lastBarIndex]  = currentBar;
                lastCompletedBarIndex  = lastBarIndex;
            }

            lastBarIndex = CurrentBar;
            currentBar   = new BarData();
        }

        // -----------------------------------------------------------------------
        // Profile mutation helpers
        // -----------------------------------------------------------------------

        private static void AccumulateBar(BarData bar, double price, long buy, long sell)
        {
            PriceLevel lvl;
            bar.Levels.TryGetValue(price, out lvl);
            bar.Levels[price] = new PriceLevel { Buy = lvl.Buy + buy, Sell = lvl.Sell + sell };
            bar.TotalVolume  += buy + sell;
        }

        /// <summary>O(log k) — keeps displayMaxVol / displayPoc current on additions.</summary>
        private void AddToDisplay(double price, long buy, long sell)
        {
            PriceLevel lvl;
            displayProfile.TryGetValue(price, out lvl);
            displayProfile[price] = new PriceLevel { Buy = lvl.Buy + buy, Sell = lvl.Sell + sell };

            displayTotalVol += buy + sell;

            long newTotal = displayProfile[price].Total;
            if (newTotal > displayMaxVol)
            {
                displayMaxVol = newTotal;
                displayPoc    = price;
            }
        }

        /// <summary>
        /// Subtract one bar's contribution from displayProfile.
        /// Does not update displayMaxVol / displayPoc — caller must RescanDisplayStats afterward.
        /// </summary>
        private void SubtractFromDisplay(BarData bar)
        {
            foreach (var kvp in bar.Levels)
            {
                PriceLevel lvl;
                if (!displayProfile.TryGetValue(kvp.Key, out lvl)) continue;

                long newBuy  = Math.Max(lvl.Buy  - kvp.Value.Buy,  0);
                long newSell = Math.Max(lvl.Sell - kvp.Value.Sell, 0);

                if (newBuy == 0 && newSell == 0)
                    displayProfile.Remove(kvp.Key);
                else
                    displayProfile[kvp.Key] = new PriceLevel { Buy = newBuy, Sell = newSell };
            }
        }

        /// <summary>O(k) full rescan.  Only called after subtractions, never per tick.</summary>
        private void RescanDisplayStats()
        {
            displayMaxVol   = 0;
            displayTotalVol = 0;
            displayPoc      = 0;

            foreach (var kvp in displayProfile)
            {
                displayTotalVol += kvp.Value.Total;
                if (kvp.Value.Total > displayMaxVol)
                {
                    displayMaxVol = kvp.Value.Total;
                    displayPoc    = kvp.Key;
                }
            }
        }

        // -----------------------------------------------------------------------
        // Batch helpers used by UpdateWindow
        // -----------------------------------------------------------------------

        private void AddBarRange(int fromIdx, int toIdx)
        {
            for (int i = fromIdx; i <= toIdx; i++)
            {
                BarData bar;
                if (!allBars.TryGetValue(i, out bar)) continue;
                foreach (var kvp in bar.Levels)
                    AddToDisplay(kvp.Key, kvp.Value.Buy, kvp.Value.Sell);
            }
        }

        private void SubtractBarRange(int fromIdx, int toIdx)
        {
            for (int i = fromIdx; i <= toIdx; i++)
            {
                BarData bar;
                if (!allBars.TryGetValue(i, out bar)) continue;
                SubtractFromDisplay(bar);
            }
        }

        private void RebuildDisplay(int fromIdx, int toIdx)
        {
            displayProfile.Clear();
            displayMaxVol   = 0;
            displayTotalVol = 0;
            displayPoc      = 0;
            vah = val       = 0;

            AddBarRange(fromIdx, toIdx);
        }

        // -----------------------------------------------------------------------
        // Window management — called from OnRender when visible range changes
        // -----------------------------------------------------------------------

        /// <summary>
        /// Bring displayProfile in sync with [newFrom, newTo].
        ///
        /// Strategy:
        ///   delta = bars entering + bars leaving the window
        ///   if delta > half the current window span → full rebuild (O(n × m))
        ///   else → incremental: subtract departing bars, rescan, add arriving bars
        ///
        /// Subtractions always precede additions so that RescanDisplayStats gives
        /// a correct baseline before AddToDisplay starts updating the max/poc.
        /// </summary>
        private void UpdateWindow(int newFrom, int newTo)
        {
            // First call — nothing to diff against
            if (windowFromIndex < 0)
            {
                RebuildDisplay(newFrom, newTo);
                windowFromIndex = newFrom;
                windowToIndex   = newTo;
                return;
            }

            int oldSpan = windowToIndex - windowFromIndex + 1;
            int delta   = Math.Abs(newFrom - windowFromIndex)
                        + Math.Abs(newTo   - windowToIndex);

            if (delta > oldSpan / 2)
            {
                RebuildDisplay(newFrom, newTo);
                windowFromIndex = newFrom;
                windowToIndex   = newTo;
                return;
            }

            // --- incremental path ---
            bool needsRescan = false;

            // Left side contracted (bars dropped from the left)
            if (newFrom > windowFromIndex)
            {
                SubtractBarRange(windowFromIndex, newFrom - 1);
                needsRescan = true;
            }
            // Right side contracted (bars dropped from the right)
            if (newTo < windowToIndex)
            {
                SubtractBarRange(newTo + 1, windowToIndex);
                needsRescan = true;
            }

            // Rescan once after all subtractions so the baseline is correct
            // before any additions update displayMaxVol / displayPoc
            if (needsRescan) RescanDisplayStats();

            // Left side expanded (new bars added on the left)
            if (newFrom < windowFromIndex)
                AddBarRange(newFrom, windowFromIndex - 1);

            // Right side expanded (new bars added on the right)
            if (newTo > windowToIndex)
                AddBarRange(windowToIndex + 1, newTo);

            windowFromIndex = newFrom;
            windowToIndex   = newTo;
        }

        // -----------------------------------------------------------------------
        // Value Area
        // -----------------------------------------------------------------------

        /// <summary>
        /// Standard value area: start at POC, greedily expand up or down
        /// (whichever adds more volume) until the target % is enclosed.
        /// Runs on displayProfile.Keys which is already sorted — one ToList()
        /// allocation, acceptable at bar-boundary / scroll frequency.
        /// </summary>
        private void CalculateValueArea()
        {
            if (displayProfile.Count == 0 || displayMaxVol == 0) return;

            var  prices = displayProfile.Keys.ToList();
            int  pocIdx = prices.BinarySearch(displayPoc);
            if  (pocIdx < 0) pocIdx = 0;

            long pocVol      = displayProfile.ContainsKey(displayPoc)
                               ? displayProfile[displayPoc].Total
                               : displayMaxVol;
            long target      = (long)(displayTotalVol * (ValueArea / 100.0));
            long accumulated = pocVol;
            int  upIdx       = pocIdx + 1;
            int  downIdx     = pocIdx - 1;
            vah = displayPoc;
            val = displayPoc;

            while (accumulated < target)
            {
                long upVol   = upIdx   < prices.Count ? displayProfile[prices[upIdx]].Total   : 0;
                long downVol = downIdx >= 0            ? displayProfile[prices[downIdx]].Total : 0;

                if (upVol == 0 && downVol == 0) break;

                if (upVol >= downVol) { accumulated += upVol;   vah = prices[upIdx++];   }
                else                  { accumulated += downVol; val = prices[downIdx--]; }
            }
        }

        // -----------------------------------------------------------------------
        // Rendering
        // -----------------------------------------------------------------------

        /// <summary>
        /// Returns the index of the last completed bar whose open time is ≤ endTime,
        /// starting the search from fromBarIdx.  Used by the Shift time-bracket mode.
        /// Linear scan is intentional: bracket sizes are typically small relative to
        /// bar count, so this is O(bracket bars), not O(all bars).
        /// </summary>
        private int FindToIndexByTime(int fromBarIdx, DateTime endTime)
        {
            for (int i = fromBarIdx + 1; i <= lastCompletedBarIndex; i++)
            {
                if (BarsArray[0].GetTime(i) > endTime)
                    return i - 1;
            }
            return lastCompletedBarIndex;
        }

        protected override void OnRender(ChartControl chartControl, ChartScale chartScale)
        {
            if (lastCompletedBarIndex < 0) return;

            // ------------------------------------------------------------------
            // Determine the target window.
            //
            // Normal mode   : full visible range [FromIndex .. lastCompleted]
            // Modifier mode : right edge pinned at lastCompleted,
            //                 left edge follows the cursor — lets the user sweep
            //                 any contiguous sub-range by hovering left or right.
            // ------------------------------------------------------------------
            bool ctrlDown  = EnableModifierKey
                          && (Keyboard.IsKeyDown(Key.LeftCtrl)  || Keyboard.IsKeyDown(Key.RightCtrl));
            bool shiftDown = EnableModifierKey
                          && (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift));

            int newFrom, newTo;

            newTo = Math.Min(ChartBars.ToIndex, lastCompletedBarIndex);

            if (ctrlDown && cursorBarIndex >= 0)
            {
                // Ctrl mode: right edge pinned at lastCompleted, left edge follows cursor
                int clampedCursor = Math.Max(0, Math.Min(cursorBarIndex, lastCompletedBarIndex));
                newFrom = clampedCursor;
                newTo   = lastCompletedBarIndex;
            }
            else if (shiftDown && cursorBarIndex >= 0)
            {
                // Shift mode: fixed-width time bracket — cursor is the left edge,
                // window extends forward BracketMinutes regardless of bar count.
                newFrom = Math.Max(0, Math.Min(cursorBarIndex, lastCompletedBarIndex));
                DateTime bracketEnd = BarsArray[0].GetTime(newFrom)
                                    + TimeSpan.FromMinutes(BracketMinutes);
                newTo = FindToIndexByTime(newFrom, bracketEnd);
            }
            else
            {
                newFrom = ChartBars.FromIndex;
            }

            if (newFrom > newTo) return;

            // Keep displayProfile in sync with the target window
            if (newFrom != windowFromIndex || newTo != windowToIndex)
            {
                UpdateWindow(newFrom, newTo);
                CalculateValueArea();
            }

            if (displayProfile.Count == 0 || displayMaxVol == 0) return;

            RenderTarget.AntialiasMode = SharpDX.Direct2D1.AntialiasMode.Aliased;

            double tickSize = chartControl.Instrument.MasterInstrument.TickSize;

            // Anchor the profile to the right edge of the visible area
            int barX   = chartControl.GetXByBarIndex(ChartBars, ChartBars.ToIndex);
            int startX = barX + OffsetPixels;
            int avail  = chartControl.CanvasRight - startX;
            int maxW   = Math.Max((int)(avail * (Width / 100f)), 10);

            // SortedDictionary iterates in ascending price order — no sort needed
            foreach (var kvp in displayProfile)
            {
                double price = kvp.Key;
                float  yTop  = (float)chartScale.GetYByValue(price + tickSize);
                float  yBot  = (float)chartScale.GetYByValue(price);
                float  barH  = Math.Max(yBot - yTop, 1f);
                bool   inVA  = price >= val && price <= vah;
                float  opac  = (inVA ? ValueAreaOpacity : Opacity) / 100f;

                if (ShowBuySell)
                {
                    float sellW = maxW * (kvp.Value.Sell / (float)displayMaxVol);
                    float buyW  = maxW * (kvp.Value.Buy  / (float)displayMaxVol);
                    sellBrushDX.Opacity = opac;
                    buyBrushDX.Opacity  = opac;
                    RenderTarget.FillRectangle(
                        new SharpDX.RectangleF(startX,          yTop, sellW, barH), sellBrushDX);
                    RenderTarget.FillRectangle(
                        new SharpDX.RectangleF(startX + sellW,  yTop, buyW,  barH), buyBrushDX);
                }
                else
                {
                    float barW = maxW * (kvp.Value.Total / (float)displayMaxVol);
                    volBrushDX.Opacity = opac;
                    RenderTarget.FillRectangle(
                        new SharpDX.RectangleF(startX, yTop, barW, barH), volBrushDX);
                }
            }

            if (ShowPoc && displayPoc != 0)
            {
                float yPoc = (float)chartScale.GetYByValue(displayPoc + tickSize * 0.5);
                RenderTarget.DrawLine(
                    new SharpDX.Vector2(startX,        yPoc),
                    new SharpDX.Vector2(startX + maxW, yPoc),
                    PocStroke.BrushDX, PocStroke.Width, PocStroke.StrokeStyle);
            }

            if (ShowValueArea && vah != 0 && val != 0)
            {
                float yVAH = (float)chartScale.GetYByValue(vah + tickSize * 0.5);
                float yVAL = (float)chartScale.GetYByValue(val + tickSize * 0.5);
                RenderTarget.DrawLine(
                    new SharpDX.Vector2(startX,        yVAH),
                    new SharpDX.Vector2(startX + maxW, yVAH),
                    ValueAreaStroke.BrushDX, ValueAreaStroke.Width, ValueAreaStroke.StrokeStyle);
                RenderTarget.DrawLine(
                    new SharpDX.Vector2(startX,        yVAL),
                    new SharpDX.Vector2(startX + maxW, yVAL),
                    ValueAreaStroke.BrushDX, ValueAreaStroke.Width, ValueAreaStroke.StrokeStyle);
            }
        }

        public override void OnRenderTargetChanged()
        {
            if (volBrushDX  != null) volBrushDX.Dispose();
            if (buyBrushDX  != null) buyBrushDX.Dispose();
            if (sellBrushDX != null) sellBrushDX.Dispose();

            if (RenderTarget != null)
            {
                volBrushDX  = VolumeBrush.ToDxBrush(RenderTarget);
                buyBrushDX  = BuyBrush.ToDxBrush(RenderTarget);
                sellBrushDX = SellBrush.ToDxBrush(RenderTarget);
                PocStroke.RenderTarget       = RenderTarget;
                ValueAreaStroke.RenderTarget = RenderTarget;
            }
        }

        // -----------------------------------------------------------------------
        // Properties
        // -----------------------------------------------------------------------

        #region Setup

        [Display(Name = "Enable Modifier Key (Ctrl)",
                 Description = "When enabled, hold Ctrl to pin the right edge and sweep the window with the cursor",
                 Order = 1, GroupName = "Setup")]
        public bool EnableModifierKey { get; set; }

        [Range(1, 1440)]
        [Display(Name = "Bracket Minutes (Shift)",
                 Description = "Hold Shift to show a fixed-width time bracket starting at the cursor. This sets the bracket width in minutes.",
                 Order = 2, GroupName = "Setup")]
        public int BracketMinutes { get; set; }

        [NinjaScriptProperty]
        [Range(10, 90)]
        [Display(Name = "Value Area (%)",
                 Description = "Percentage of total volume enclosed by the value area",
                 Order = 1, GroupName = "Setup")]
        public int ValueArea { get; set; }

        #endregion

        #region Visual

        [Range(5, 100)]
        [Display(Name = "Profile Width (%)", Order = 1, GroupName = "Visual")]
        public int Width { get; set; }

        [Range(-200, 200)]
        [Display(Name = "Offset (pixels)",
                 Description = "Horizontal offset from the rightmost visible bar",
                 Order = 2, GroupName = "Visual")]
        public int OffsetPixels { get; set; }

        [Range(1, 100)]
        [Display(Name = "Opacity (%)", Order = 3, GroupName = "Visual")]
        public int Opacity { get; set; }

        [Range(1, 100)]
        [Display(Name = "Value Area Opacity (%)", Order = 4, GroupName = "Visual")]
        public int ValueAreaOpacity { get; set; }

        [Display(Name = "Show Buy / Sell Split", Order = 5, GroupName = "Visual")]
        public bool ShowBuySell { get; set; }

        [Display(Name = "Show POC", Order = 6, GroupName = "Visual")]
        public bool ShowPoc { get; set; }

        [Display(Name = "Show Value Area", Order = 7, GroupName = "Visual")]
        public bool ShowValueArea { get; set; }

        [XmlIgnore]
        [Display(Name = "Volume Color", Order = 8, GroupName = "Visual")]
        public Brush VolumeBrush { get; set; }
        [Browsable(false)]
        public string VolumeBrushSerialize
        {
            get { return Serialize.BrushToString(VolumeBrush); }
            set { VolumeBrush = Serialize.StringToBrush(value); }
        }

        [XmlIgnore]
        [Display(Name = "Buy Color", Order = 9, GroupName = "Visual")]
        public Brush BuyBrush { get; set; }
        [Browsable(false)]
        public string BuyBrushSerialize
        {
            get { return Serialize.BrushToString(BuyBrush); }
            set { BuyBrush = Serialize.StringToBrush(value); }
        }

        [XmlIgnore]
        [Display(Name = "Sell Color", Order = 10, GroupName = "Visual")]
        public Brush SellBrush { get; set; }
        [Browsable(false)]
        public string SellBrushSerialize
        {
            get { return Serialize.BrushToString(SellBrush); }
            set { SellBrush = Serialize.StringToBrush(value); }
        }

        #endregion

        #region Lines

        [Display(Name = "POC", Order = 1, GroupName = "Lines")]
        public Stroke PocStroke { get; set; }

        [Display(Name = "Value Area", Order = 2, GroupName = "Lines")]
        public Stroke ValueAreaStroke { get; set; }

        #endregion
    }
}

DVP