WT Market Depth DOM Panel

Hi guys I am not asking for anything in return, just sharing, I am testing its speed to make sure is accurate, Enjoy 06013-ezgif.com-video-to-gif-converter

This indicator is a left-side NinjaTrader market depth dashboard that combines a DOM ladder, order flow tools, TPO profile, tape reading, liquidity analysis, and market pressure gauges into a single chart-attached panel.

Main Features

DOM Ladder

  • Real-time bid and ask depth.

  • Displays resting buy and sell orders.

  • Highlights current market price.

  • Shows bid volume, ask volume, and delta at each price level.

  • Scrollable ladder with adjustable size.

Major Walls & Stacking

  • Detects largest bid walls.

  • Detects largest ask walls.

  • Shows where liquidity is concentrated.

  • Tracks bid stacking (increasing buy orders).

  • Tracks ask stacking (increasing sell orders).

  • Displays cumulative delta and last trade volume.

Time & Sales

    • Time

    • Price

    • Side

    • Trade size

TPO Market Profile

  • Tracks price acceptance over time.

  • Builds a Time Price Opportunity (TPO) profile.

  • Shows where the market spends the most time.

Most TPO Area

  • Identifies the highest accepted prices.

  • Highlights:

    • POC (Point of Control)

    • High-volume acceptance zones

    • Most traded price levels

Bid / Ask Pressure Gauge

  • Displays buy vs sell pressure.

Adjustable Layout

Window Controls

Users can

  • Show only DOM.

  • Show only TPO.

  • Show only Tape.

  • Show only Walls.

  • Show all windows.

  • Hide any section individually.

Collapsible Panel

X button collapses dashboard.

Clicking WT restores the dashboard.

  
4 Likes

I’m trying to add the code, but it is telling me it’s too long

#region Using declarations
using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.Gui.Chart;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Indicators;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Threading;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
public class WT_MarketDepthDOMPanel : Indicator
{
private Grid chartGrid;
private Grid root;
private Border panel;
private Grid mainGrid;
private Border collapsedTab;
private DispatcherTimer uiTimer;
private bool isHidden;
private double savedRootWidth;
private double savedRootHeight;
private Thickness savedRootMargin = new Thickness(6, 6, 0, 0);

    private readonly object sync = new object();
    private readonly SortedDictionary<double, long> bids = new SortedDictionary<double, long>();
    private readonly SortedDictionary<double, long> asks = new SortedDictionary<double, long>();
    private readonly Dictionary<double, long> prevBids = new Dictionary<double, long>();
    private readonly Dictionary<double, long> prevAsks = new Dictionary<double, long>();
    private readonly List<TapeRow> tape = new List<TapeRow>();
    private readonly Dictionary<double, int> tpo = new Dictionary<double, int>();

    private StackPanel domRows, tapeRows, wallRows, mostTpoRows;
    private TextBlock gaugeText;
    private Border gaugeBidFill, gaugeAskFill;
    private Grid gaugeBar;
    private TextBlock lastTxt, deltaTxt, bidTxt, askTxt, tpoTxt, statusTxt;
    private double lastPrice;
    private double cumDelta;
    private long lastTradeVol;
    private int maxDepthRows = 60;
    private double tickSizeLocal = 0.25;
    private readonly List<DomVisualRow> domVisualRows = new List<DomVisualRow>();
    private bool domRowsBuilt = false;
    private int domRowsBuiltCount = 0;

    public int DomRowsVisible { get; set; }
    public int TpoMaxRows { get; set; }
    public int TpoMaxLetters { get; set; }
    public double TpoFontSize { get; set; }
    public double PanelWidth { get; set; }
    public double DomColumnWidth { get; set; }
    public double TpoColumnWidth { get; set; }
    public double RightColumnWidth { get; set; }
    public bool ShowDomWindow { get; set; }
    public bool ShowTpoWindow { get; set; }
    public bool ShowTimeSalesWindow { get; set; }
    public bool ShowMajorWallsWindow { get; set; }
    public bool ShowBidAskGaugeWindow { get; set; }
    public bool ShowMostTpoAreaWindow { get; set; }
    public string CollapsedTabText { get; set; }
    public double CollapsedWidth { get; set; }

    protected override void OnStateChange()
    {
        if (State == State.SetDefaults)
        {
            Name = "WT Market Depth DOM Panel";
            Description = "Stable no-flash DOM. Bids stay below market price, asks stay above market price. Keeps V16 layout and lower-left WT reopen tab.";
            Calculate = Calculate.OnEachTick;
            IsOverlay = true;
            DisplayInDataBox = false;
            PaintPriceMarkers = false;
            IsSuspendedWhileInactive = false;
            DomRowsVisible = 120;
            TpoMaxRows = 1000;
            TpoMaxLetters = 200;
            TpoFontSize = 9;
            PanelWidth = 1120;
            DomColumnWidth = 255;
            TpoColumnWidth = 380;
            RightColumnWidth = 390;
            ShowDomWindow = true;
            ShowTpoWindow = true;
            ShowTimeSalesWindow = true;
            ShowMajorWallsWindow = true;
            ShowBidAskGaugeWindow = true;
            ShowMostTpoAreaWindow = true;
            CollapsedTabText = "WT";
            CollapsedWidth = 44;
        }
        else if (State == State.DataLoaded)
        {
            tickSizeLocal = TickSize > 0 ? TickSize : 0.25;
        }
        else if (State == State.Historical)
        {
            if (ChartControl != null)
                ChartControl.Dispatcher.InvokeAsync(BuildUi);
        }
        else if (State == State.Terminated)
        {
            if (ChartControl != null)
                ChartControl.Dispatcher.InvokeAsync(RemoveUi);
        }
    }

    protected override void OnBarUpdate()
    {
        if (CurrentBar < 0) return;
        lastPrice = Close[0];
        double bucket = RoundToTick(lastPrice);
        lock (sync)
        {
            if (!tpo.ContainsKey(bucket)) tpo[bucket] = 0;
            tpo[bucket] = Math.Min(5000, tpo[bucket] + 1);
        }
    }

    protected override void OnMarketData(MarketDataEventArgs e)
    {
        if (e.MarketDataType != MarketDataType.Last) return;
        lastPrice = e.Price;
        lastTradeVol = e.Volume;
        double bid = GetBestBid();
        double ask = GetBestAsk();
        string side = "LAST";
        double d = 0;
        if (ask > 0 && e.Price >= ask) { side = "BUY"; d = e.Volume; }
        else if (bid > 0 && e.Price <= bid) { side = "SELL"; d = -e.Volume; }
        cumDelta += d;
        lock (sync)
        {
            tape.Insert(0, new TapeRow { Time = DateTime.Now, Price = e.Price, Volume = e.Volume, Side = side, Delta = d });
            if (tape.Count > 30) tape.RemoveAt(tape.Count - 1);
        }
    }

    protected override void OnMarketDepth(MarketDepthEventArgs e)
    {
        if (e == null) return;
        lock (sync)
        {
            SortedDictionary<double, long> book = e.MarketDataType == MarketDataType.Bid ? bids : asks;
            if (e.Operation == Operation.Remove || e.Volume <= 0)
                book.Remove(e.Price);
            else
                book[e.Price] = e.Volume;
        }
    }

    private void BuildUi()
    {
        RemoveUi();
        if (ChartControl == null) return;
        chartGrid = FindParentGrid(ChartControl.Parent);
        if (chartGrid == null) return;

        root = new Grid
        {
            HorizontalAlignment = HorizontalAlignment.Left,
            VerticalAlignment = VerticalAlignment.Top,
            Margin = new Thickness(6, 22, 0, 0),
            Width = PanelWidth,
            Height = GetPanelHeight()
        };
        System.Windows.Controls.Panel.SetZIndex(root, 9999);

        panel = new Border
        {
            Background = new SolidColorBrush(Color.FromArgb(235, 4, 10, 16)),
            BorderBrush = new SolidColorBrush(Color.FromRgb(38, 82, 110)),
            BorderThickness = new Thickness(1),
            CornerRadius = new CornerRadius(5),
            Padding = new Thickness(8)
        };
        root.Children.Add(panel);

        collapsedTab = BuildCollapsedTab();
        collapsedTab.Visibility = Visibility.Collapsed;
        root.Children.Add(collapsedTab);

        mainGrid = new Grid();
        mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(46) });
        mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
        panel.Child = mainGrid;

        BuildHeader();
        BuildBody();

        chartGrid.Children.Add(root);
        uiTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) };
        uiTimer.Tick += (s, e) => SafeRefreshUi();
        uiTimer.Start();
    }

    private Border BuildCollapsedTab()
    {
        Border tab = new Border
        {
            Width = Math.Max(28, CollapsedWidth),
            Height = 54,
            HorizontalAlignment = HorizontalAlignment.Left,
            VerticalAlignment = VerticalAlignment.Bottom,
            Background = new SolidColorBrush(Color.FromArgb(235, 4, 10, 16)),
            BorderBrush = new SolidColorBrush(Color.FromRgb(0, 190, 255)),
            BorderThickness = new Thickness(1),
            CornerRadius = new CornerRadius(4),
            Cursor = System.Windows.Input.Cursors.Hand,
            ToolTip = "Open WT Market Depth DOM Panel"
        };

        TextBlock txt = new TextBlock
        {
            Text = string.IsNullOrWhiteSpace(CollapsedTabText) ? "WT" : CollapsedTabText,
            Foreground = Brushes.Cyan,
            FontWeight = FontWeights.Bold,
            FontSize = 14,
            TextAlignment = TextAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center,
            HorizontalAlignment = HorizontalAlignment.Center
        };
        tab.Child = txt;
        tab.MouseLeftButtonDown += (s, e) => RestoreFromTab();
        return tab;
    }

    private void CollapseToTab()
    {
        try
        {
            if (root == null || panel == null || collapsedTab == null) return;
            if (!isHidden)
            {
                savedRootWidth = root.Width;
                savedRootHeight = root.Height;
                savedRootMargin = root.Margin;
            }
            isHidden = true;
            panel.Visibility = Visibility.Collapsed;
            collapsedTab.Visibility = Visibility.Visible;
            root.Width = Math.Max(28, CollapsedWidth);
            root.Height = 58;
            root.HorizontalAlignment = HorizontalAlignment.Left;
            root.VerticalAlignment = VerticalAlignment.Bottom;
            root.Margin = new Thickness(6, 0, 0, 40);
        }
        catch { }
    }

    private void RestoreFromTab()
    {
        try
        {
            if (root == null || panel == null || collapsedTab == null) return;
            isHidden = false;
            root.HorizontalAlignment = HorizontalAlignment.Left;
            root.VerticalAlignment = VerticalAlignment.Top;
            root.Margin = savedRootMargin;
            root.Width = savedRootWidth > 100 ? savedRootWidth : PanelWidth;
            root.Height = savedRootHeight > 100 ? savedRootHeight : GetPanelHeight();
            panel.Visibility = Visibility.Visible;
            collapsedTab.Visibility = Visibility.Collapsed;
        }
        catch { }
    }

    private void BuildHeader()
    {
        Grid h = new Grid();
        for (int i = 0; i < 8; i++) h.ColumnDefinitions.Add(new ColumnDefinition { Width = i == 0 ? new GridLength(180) : new GridLength(1, GridUnitType.Star) });
        h.Children.Add(MakeText("Wizard Trends Inc", 16, Brushes.White, FontWeights.Bold, 0, 0));
        statusTxt = MakeText("LIVE", 12, Brushes.Lime, FontWeights.Bold, 0, 1);
        lastTxt = MakeText("LAST\n--", 12, Brushes.White, FontWeights.Bold, 0, 2);
        deltaTxt = MakeText("DELTA\n0", 12, Brushes.Red, FontWeights.Bold, 0, 3);
        bidTxt = MakeText("BID\n0", 12, Brushes.Lime, FontWeights.Bold, 0, 4);
        askTxt = MakeText("ASK\n0", 12, Brushes.Red, FontWeights.Bold, 0, 5);
        Button hide = new Button { Content = "X", FontWeight = FontWeights.Bold, Foreground = Brushes.White, Background = Brushes.Transparent, BorderBrush = new SolidColorBrush(Color.FromRgb(35, 60, 80)) };
        hide.Click += (s, e) => CollapseToTab();
        Grid.SetColumn(hide, 7); h.Children.Add(hide);
        Grid.SetRow(h, 0); mainGrid.Children.Add(h);
    }

    private void BuildBody()
    {
        Grid b = new Grid();
        bool showDom = ShowDomWindow;
        bool showTpo = ShowTpoWindow;
        bool showRight = ShowTimeSalesWindow || ShowMajorWallsWindow || ShowBidAskGaugeWindow || ShowMostTpoAreaWindow;

        b.ColumnDefinitions.Add(new ColumnDefinition { Width = showDom ? new GridLength(Math.Max(180, DomColumnWidth)) : new GridLength(0) });
        b.ColumnDefinitions.Add(new ColumnDefinition { Width = (showDom && (showTpo || showRight)) ? new GridLength(6) : new GridLength(0) });
        b.ColumnDefinitions.Add(new ColumnDefinition { Width = showTpo ? new GridLength(Math.Max(190, TpoColumnWidth)) : new GridLength(0) });
        b.ColumnDefinitions.Add(new ColumnDefinition { Width = (showTpo && showRight) ? new GridLength(6) : new GridLength(0) });
        b.ColumnDefinitions.Add(new ColumnDefinition { Width = showRight ? new GridLength(Math.Max(260, RightColumnWidth)) : new GridLength(0) });
        b.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
        Grid.SetRow(b, 1); mainGrid.Children.Add(b);

        domRows = null; tpoTxt = null; tapeRows = null; wallRows = null; mostTpoRows = null;
        gaugeText = null; gaugeBidFill = null; gaugeAskFill = null; gaugeBar = null;
        domVisualRows.Clear(); domRowsBuilt = false; domRowsBuiltCount = 0;

        if (showDom)
            AddPanelBox(b, "DOM LADDER", out domRows, 0, 0, 1);

        if (showDom && (showTpo || showRight))
            AddVerticalResizeThumb(b, 1, b.ColumnDefinitions[0], showTpo ? b.ColumnDefinitions[2] : b.ColumnDefinitions[4]);

        if (showTpo)
        {
            StackPanel tpoHost;
            AddPanelBox(b, "TPO / MARKET PROFILE", out tpoHost, 0, 2, 1);
            tpoTxt = new TextBlock { Foreground = Brushes.Cyan, FontSize = TpoFontSize, FontFamily = new FontFamily("Consolas"), TextWrapping = TextWrapping.NoWrap };
            tpoHost.Children.Add(tpoTxt);
        }

        if (showTpo && showRight)
            AddVerticalResizeThumb(b, 3, b.ColumnDefinitions[2], b.ColumnDefinitions[4]);

        if (showRight)
        {
            Grid right = new Grid();
            int row = 0;
            if (ShowTimeSalesWindow)
            {
                right.RowDefinitions.Add(new RowDefinition { Height = new GridLength(0.28, GridUnitType.Star), MinHeight = 120 });
                AddPanelBox(right, "TIME & SALES", out tapeRows, row, 0, 1);
                row++;
                if (ShowMajorWallsWindow || ShowBidAskGaugeWindow || ShowMostTpoAreaWindow)
                {
                    right.RowDefinitions.Add(new RowDefinition { Height = new GridLength(6) });
                    AddHorizontalResizeThumb(right, row, right.RowDefinitions[row - 1], null);
                    row++;
                }
            }
            if (ShowMajorWallsWindow)
            {
                right.RowDefinitions.Add(new RowDefinition { Height = new GridLength(0.30, GridUnitType.Star), MinHeight = 120 });
                AddPanelBox(right, "MAJOR WALLS + STACKING", out wallRows, row, 0, 1);
                row++;
                if (ShowBidAskGaugeWindow || ShowMostTpoAreaWindow)
                {
                    right.RowDefinitions.Add(new RowDefinition { Height = new GridLength(6) });
                    AddHorizontalResizeThumb(right, row, right.RowDefinitions[row - 1], null);
                    row++;
                }
            }
            if (ShowBidAskGaugeWindow)
            {
                right.RowDefinitions.Add(new RowDefinition { Height = new GridLength(100), MinHeight = 80 });
                BuildBidAskGauge(right, row);
                row++;
                if (ShowMostTpoAreaWindow)
                {
                    right.RowDefinitions.Add(new RowDefinition { Height = new GridLength(6) });
                    AddHorizontalResizeThumb(right, row, right.RowDefinitions[row - 1], null);
                    row++;
                }
            }
            if (ShowMostTpoAreaWindow)
            {
                right.RowDefinitions.Add(new RowDefinition { Height = new GridLength(0.38, GridUnitType.Star), MinHeight = 120 });
                AddPanelBox(right, "MOST TPO AREA", out mostTpoRows, row, 0, 1);
            }
            Grid.SetColumn(right, 4); b.Children.Add(right);
        }

        AddCornerResizeThumb(root);
    }

    private void AddVerticalResizeThumb(Grid parent, int col, ColumnDefinition leftCol, ColumnDefinition rightCol)
    {
        Thumb thumb = new Thumb
        {
            Width = 6,
            HorizontalAlignment = HorizontalAlignment.Stretch,
            VerticalAlignment = VerticalAlignment.Stretch,
            Background = new SolidColorBrush(Color.FromArgb(80, 65, 110, 135)),
            Cursor = System.Windows.Input.Cursors.SizeWE
        };
        thumb.DragDelta += (s, e) =>
        {
            try
            {
                if (leftCol == null || rightCol == null) return;
                double left = leftCol.ActualWidth > 0 ? leftCol.ActualWidth : (leftCol.Width.IsAbsolute ? leftCol.Width.Value : 240);
                double right = rightCol.ActualWidth > 0 ? rightCol.ActualWidth : (rightCol.Width.IsAbsolute ? rightCol.Width.Value : 260);
                double dx = e.HorizontalChange;
                double leftMin = 170;
                double rightMin = 170;
                if (left + dx < leftMin) dx = leftMin - left;
                if (right - dx < rightMin) dx = right - rightMin;
                double newLeft = Math.Max(leftMin, left + dx);
                double newRight = Math.Max(rightMin, right - dx);
                leftCol.Width = new GridLength(newLeft, GridUnitType.Pixel);
                rightCol.Width = new GridLength(newRight, GridUnitType.Pixel);
                if (root != null && mainGrid != null)
                {
                    double needed = newLeft + newRight + 80;
                    if (root.Width < needed) root.Width = needed;
                }
            }
            catch { }
        };
        Grid.SetColumn(thumb, col);
        Grid.SetRow(thumb, 0);
        parent.Children.Add(thumb);
    }

    private void AddHorizontalResizeThumb(Grid parent, int row, RowDefinition upperRow, RowDefinition lowerRow)
    {
        Thumb thumb = new Thumb
        {
            Height = 6,
            HorizontalAlignment = HorizontalAlignment.Stretch,
            VerticalAlignment = VerticalAlignment.Stretch,
            Background = new SolidColorBrush(Color.FromArgb(80, 65, 110, 135)),
            Cursor = System.Windows.Input.Cursors.SizeNS
        };
        thumb.DragDelta += (s, e) =>
        {
            try
            {
                if (upperRow == null) return;
                Grid grid = parent as Grid;
                RowDefinition lower = lowerRow;
                if (lower == null && grid != null)
                {
                    int idx = grid.RowDefinitions.IndexOf(upperRow);
                    for (int i = idx + 2; i < grid.RowDefinitions.Count; i++)
                    {
                        if (grid.RowDefinitions[i].Height.Value > 0) { lower = grid.RowDefinitions[i]; break; }
                    }
                }
                if (lower == null) return;
                double upper = upperRow.ActualHeight > 0 ? upperRow.ActualHeight : (upperRow.Height.IsAbsolute ? upperRow.Height.Value : 130);
                double lowerH = lower.ActualHeight > 0 ? lower.ActualHeight : (lower.Height.IsAbsolute ? lower.Height.Value : 130);
                double dy = e.VerticalChange;
                double min = 90;
                if (upper + dy < min) dy = min - upper;
                if (lowerH - dy < min) dy = lowerH - min;
                double newUpper = Math.Max(min, upper + dy);
                double newLower = Math.Max(min, lowerH - dy);
                upperRow.Height = new GridLength(newUpper, GridUnitType.Pixel);
                lower.Height = new GridLength(newLower, GridUnitType.Pixel);
                if (root != null)
                {
                    double needed = newUpper + newLower + 210;
                    if (root.Height < needed) root.Height = needed;
                }
            }
            catch { }
        };
        Grid.SetRow(thumb, row);
        Grid.SetColumn(thumb, 0);
        parent.Children.Add(thumb);
    }

    private void AddCornerResizeThumb(Grid parent)
    {
        if (parent == null) return;
        Thumb grip = new Thumb
        {
            Width = 16,
            Height = 16,
            HorizontalAlignment = HorizontalAlignment.Right,
            VerticalAlignment = VerticalAlignment.Bottom,
            Background = new SolidColorBrush(Color.FromArgb(115, 0, 190, 255)),
            Cursor = System.Windows.Input.Cursors.SizeNWSE,
            Margin = new Thickness(0, 0, 2, 2)
        };
        grip.DragDelta += (s, e) =>
        {
            try
            {
                if (root == null) return;
                root.Width = Math.Max(620, root.Width + e.HorizontalChange);
                root.Height = Math.Max(430, root.Height + e.VerticalChange);
            }
            catch { }
        };
        System.Windows.Controls.Panel.SetZIndex(grip, 10000);
        parent.Children.Add(grip);
    }

    private void BuildBidAskGauge(Grid parent, int targetRow)
    {
        Border box = new Border
        {
            Margin = new Thickness(0, 0, 8, 8),
            Padding = new Thickness(8),
            Background = new SolidColorBrush(Color.FromArgb(160, 5, 14, 24)),
            BorderBrush = new SolidColorBrush(Color.FromRgb(35, 70, 95)),
            BorderThickness = new Thickness(1),
            CornerRadius = new CornerRadius(4)
        };
        StackPanel sp = new StackPanel();
        sp.Children.Add(new TextBlock { Text = "BID / ASK PRESSURE GAUGE", Foreground = Brushes.White, FontWeight = FontWeights.Bold, FontSize = 12, Margin = new Thickness(0, 0, 0, 6) });
        gaugeBar = new Grid { Height = 28, MinWidth = 140 };
        Grid bar = gaugeBar;
        bar.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
        bar.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
        Border bg = new Border { Background = new SolidColorBrush(Color.FromArgb(70, 20, 30, 40)), CornerRadius = new CornerRadius(3) };
        Grid.SetColumnSpan(bg, 2); bar.Children.Add(bg);
        gaugeBidFill = new Border { Background = new SolidColorBrush(Color.FromArgb(180, 0, 210, 70)), HorizontalAlignment = HorizontalAlignment.Right, CornerRadius = new CornerRadius(3, 0, 0, 3) };
        gaugeAskFill = new Border { Background = new SolidColorBrush(Color.FromArgb(180, 230, 0, 20)), HorizontalAlignment = HorizontalAlignment.Left, CornerRadius = new CornerRadius(0, 3, 3, 0) };
        Grid.SetColumn(gaugeBidFill, 0); Grid.SetColumn(gaugeAskFill, 1);
        bar.Children.Add(gaugeBidFill); bar.Children.Add(gaugeAskFill);
        gaugeText = new TextBlock { Text = "BID 0%  |  ASK 0%", Foreground = Brushes.White, FontSize = 12, FontWeight = FontWeights.Bold, TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
        Grid.SetColumnSpan(gaugeText, 2); bar.Children.Add(gaugeText);
        sp.Children.Add(bar);
        box.Child = sp;
        Grid.SetRow(box, targetRow); Grid.SetColumn(box, 0); parent.Children.Add(box);
    }

    private void AddPanelBox(Grid parent, string title, out StackPanel content, int row, int col, int rowSpan)
    {
        Border box = new Border
        {
            Margin = new Thickness(0, 0, 8, 8),
            Padding = new Thickness(8),
            Background = new SolidColorBrush(Color.FromArgb(160, 5, 14, 24)),
            BorderBrush = new SolidColorBrush(Color.FromRgb(35, 70, 95)),
            BorderThickness = new Thickness(1),
            CornerRadius = new CornerRadius(4)
        };

        Grid host = new Grid();
        host.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
        host.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

        TextBlock titleBlock = new TextBlock
        {
            Text = title,
            Foreground = Brushes.White,
            FontWeight = FontWeights.Bold,
            FontSize = 12,
            Margin = new Thickness(0, 0, 0, 6)
        };
        Grid.SetRow(titleBlock, 0);
        host.Children.Add(titleBlock);

        content = new StackPanel();
        ScrollViewer scroll = new ScrollViewer
        {
            VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
            HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
            CanContentScroll = false,
            Content = content
        };
        Grid.SetRow(scroll, 1);
        host.Children.Add(scroll);
1 Like
        box.Child = host;
        Grid.SetRow(box, row); Grid.SetColumn(box, col); Grid.SetRowSpan(box, rowSpan);
        parent.Children.Add(box);
    }

    private void SafeRefreshUi()
    {
        try
        {
            if (State == State.Terminated) return;
            if (ChartControl == null || root == null || panel == null || mainGrid == null) return;

            RefreshUi();
        }
        catch (Exception ex)
        {
            try
            {
                if (statusTxt != null)
                {
                    statusTxt.Text = "UI ERROR";
                    statusTxt.Foreground = Brushes.Red;
                }
                if (wallRows != null)
                {
                    wallRows.Children.Clear();
                    AddLine(wallRows, "Runtime UI error caught safely.", Brushes.Red, 12, true);
                    AddLine(wallRows, ex.Message, Brushes.White, 10, false);
                }
            }
            catch { }
        }
    }

    private void RefreshUi()
    {
        if (domRows == null) return;
        // keep user-stretched height; do not force reset during timer refresh
        List<KeyValuePair<double, long>> bidList, askList;
        List<TapeRow> tapeCopy;
        Dictionary<double, int> tpoCopy;
        lock (sync)
        {
            double centerRef = lastPrice > 0 ? RoundToTick(lastPrice) : 0;
            if (centerRef > 0)
            {
                // Critical DOM safety: bids must stay below market, asks must stay above market.
                bidList = bids.Where(x => x.Key < centerRef).OrderByDescending(x => x.Key).Take(Math.Max(maxDepthRows, DomRowsVisible)).ToList();
                askList = asks.Where(x => x.Key > centerRef).OrderBy(x => x.Key).Take(Math.Max(maxDepthRows, DomRowsVisible)).ToList();
            }
            else
            {
                bidList = bids.Reverse().Take(Math.Max(maxDepthRows, DomRowsVisible)).ToList();
                askList = asks.Take(Math.Max(maxDepthRows, DomRowsVisible)).ToList();
            }
            tapeCopy = tape.Take(16).ToList();
            tpoCopy = new Dictionary<double, int>(tpo);
        }

        long bidTotal = bidList.Sum(x => x.Value);
        long askTotal = askList.Sum(x => x.Value);
        if (bidTxt != null) bidTxt.Text = "BID\n" + bidTotal.ToString("N0");
        if (askTxt != null) askTxt.Text = "ASK\n" + askTotal.ToString("N0");
        if (lastTxt != null) lastTxt.Text = "LAST\n" + (lastPrice > 0 ? lastPrice.ToString("0.00") : "--");
        if (deltaTxt != null)
        {
            deltaTxt.Text = "DELTA\n" + cumDelta.ToString("N0");
            deltaTxt.Foreground = cumDelta >= 0 ? Brushes.Lime : Brushes.Red;
        }
        if (statusTxt != null)
        {
            statusTxt.Text = "LIVE";
            statusTxt.Foreground = Brushes.Lime;
        }

        if (domRows != null) RenderDom(bidList, askList);
        if (wallRows != null) RenderWallsAndStacking(bidList, askList);
        if (tapeRows != null) RenderTape(tapeCopy);
        if (tpoTxt != null) RenderTpo(tpoCopy);
        if (mostTpoRows != null) RenderMostTpoArea(tpoCopy);
        if (gaugeText != null) RenderBidAskGauge(bidTotal, askTotal);
    }

    private void RenderDom(List<KeyValuePair<double, long>> bidList, List<KeyValuePair<double, long>> askList)
    {
        if (domRows == null) return;

        double center = lastPrice > 0 ? RoundToTick(lastPrice) : 0;
        if (center <= 0)
        {
            if (!domRowsBuilt)
            {
                domRows.Children.Clear();
                domRows.Children.Add(MakeText("Waiting for live Level II depth...", 12, Brushes.Yellow, FontWeights.Bold, 0, 0));
            }
            return;
        }

        int halfRows = Math.Max(10, DomRowsVisible / 2);
        int neededRows = halfRows * 2 + 1;

        // Build visual controls once. Do not clear/recreate every timer tick.
        if (!domRowsBuilt || domRowsBuiltCount != neededRows)
            BuildDomVisualRows(neededRows);

        long max = Math.Max(1, Math.Max(bidList.Count > 0 ? bidList.Max(x => x.Value) : 1, askList.Count > 0 ? askList.Max(x => x.Value) : 1));
        Dictionary<double, long> bidMap = bidList.GroupBy(x => RoundToTick(x.Key)).ToDictionary(g => g.Key, g => g.Last().Value);
        Dictionary<double, long> askMap = askList.GroupBy(x => RoundToTick(x.Key)).ToDictionary(g => g.Key, g => g.Last().Value);

        int rowIndex = 0;
        for (int i = halfRows; i >= -halfRows && rowIndex < domVisualRows.Count; i--, rowIndex++)
        {
            double price = RoundToTick(center + i * tickSizeLocal);

            long bv = 0;
            long av = 0;

            // Hard location lock: never show bid size above/at market and never show ask size below/at market.
            if (price < center)
                bidMap.TryGetValue(price, out bv);
            if (price > center)
                askMap.TryGetValue(price, out av);

            DomVisualRow row = domVisualRows[rowIndex];
            row.PriceText.Text = price.ToString("0.00");
            row.BidText.Text = bv > 0 ? bv.ToString("N0") : "";
            row.AskText.Text = av > 0 ? av.ToString("N0") : "";
            long d = bv - av;
            row.DeltaText.Text = d == 0 ? "" : d.ToString("+0;-0");
            row.DeltaText.Foreground = d >= 0 ? Brushes.Lime : Brushes.Red;
            row.RowGrid.Background = Math.Abs(price - center) < tickSizeLocal / 2.0
                ? new SolidColorBrush(Color.FromArgb(90, 230, 170, 0))
                : Brushes.Transparent;

            UpdateDepthBar(row.BidBar, bv, max, true);
            UpdateDepthBar(row.AskBar, av, max, false);
        }
    }

    private void BuildDomVisualRows(int visualRowCount)
    {
        domRows.Children.Clear();
        domVisualRows.Clear();

        Grid header = new Grid();
        for (int i = 0; i < 4; i++) header.ColumnDefinitions.Add(new ColumnDefinition());
        header.Children.Add(MakeText("BID", 11, Brushes.Lime, FontWeights.Bold, 0, 0));
        header.Children.Add(MakeText("PRICE", 11, Brushes.White, FontWeights.Bold, 0, 1));
        header.Children.Add(MakeText("ASK", 11, Brushes.Red, FontWeights.Bold, 0, 2));
        header.Children.Add(MakeText("Δ", 11, Brushes.White, FontWeights.Bold, 0, 3));
        domRows.Children.Add(header);

        for (int i = 0; i < visualRowCount; i++)
        {
            Grid r = new Grid { Height = 15, Background = Brushes.Transparent };
            for (int c = 0; c < 4; c++) r.ColumnDefinitions.Add(new ColumnDefinition());

            DomVisualRow row = new DomVisualRow();
            row.RowGrid = r;
            row.BidText = new TextBlock { Foreground = Brushes.Lime, FontSize = 11, FontWeight = FontWeights.Bold, TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
            row.PriceText = new TextBlock { Foreground = Brushes.White, FontSize = 11, FontWeight = FontWeights.Bold, TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
            row.AskText = new TextBlock { Foreground = Brushes.Red, FontSize = 11, FontWeight = FontWeights.Bold, TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
            row.DeltaText = new TextBlock { Foreground = Brushes.White, FontSize = 11, FontWeight = FontWeights.Bold, TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
            row.BidBar = new Border { HorizontalAlignment = HorizontalAlignment.Right, Background = new SolidColorBrush(Color.FromArgb(90, 0, 220, 70)), Width = 0 };
            row.AskBar = new Border { HorizontalAlignment = HorizontalAlignment.Left, Background = new SolidColorBrush(Color.FromArgb(100, 230, 0, 20)), Width = 0 };

            AddDomCell(r, row.BidBar, row.BidText, 0);
            AddDomCell(r, null, row.PriceText, 1);
            AddDomCell(r, row.AskBar, row.AskText, 2);
            AddDomCell(r, null, row.DeltaText, 3);

            domRows.Children.Add(r);
            domVisualRows.Add(row);
        }

        domRowsBuilt = true;
        domRowsBuiltCount = visualRowCount;
    }

    private void AddDomCell(Grid rowGrid, Border bar, TextBlock text, int col)
    {
        Grid cell = new Grid();
        if (bar != null) cell.Children.Add(bar);
        cell.Children.Add(text);
        Grid.SetColumn(cell, col);
        rowGrid.Children.Add(cell);
    }

    private void UpdateDepthBar(Border bar, long val, long max, bool bidSide)
    {
        if (bar == null) return;
        if (val <= 0)
        {
            bar.Width = 0;
            return;
        }
        bar.Width = Math.Max(4, 48.0 * val / Math.Max(1, max));
        bar.HorizontalAlignment = bidSide ? HorizontalAlignment.Right : HorizontalAlignment.Left;
    }

    private void AddCell(Grid r, string txt, Brush fg, int col, long val, long max, bool bidSide)
    {
        Grid cell = new Grid();
        if (val > 0)
        {
            Border bar = new Border
            {
                Width = Math.Max(4, 48.0 * val / max),
                HorizontalAlignment = bidSide ? HorizontalAlignment.Right : HorizontalAlignment.Left,
                Background = bidSide ? new SolidColorBrush(Color.FromArgb(90, 0, 220, 70)) : new SolidColorBrush(Color.FromArgb(100, 230, 0, 20))
            };
            cell.Children.Add(bar);
        }
        TextBlock t = new TextBlock { Text = txt, Foreground = fg, FontSize = 11, FontWeight = FontWeights.Bold, TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
        cell.Children.Add(t);
        Grid.SetColumn(cell, col); r.Children.Add(cell);
    }

    private void RenderWallsAndStacking(List<KeyValuePair<double, long>> bidList, List<KeyValuePair<double, long>> askList)
    {
        if (wallRows == null) return;
        wallRows.Children.Clear();

        if (bidList.Count == 0 && askList.Count == 0)
        {
            AddLine(wallRows, "Waiting for live Level II depth...", Brushes.Yellow, 12, true);
            AddLine(wallRows, "Use live data or Playback with market depth recorded.", Brushes.White, 11, false);
            return;
        }

        var topBids = bidList.OrderByDescending(x => x.Value).Take(4).ToList();
        var topAsks = askList.OrderByDescending(x => x.Value).Take(4).ToList();

        AddLine(wallRows, "BID WALLS", Brushes.Lime, 12, true);
        foreach (var b in topBids)
            AddLine(wallRows, b.Key.ToString("0.00") + "   " + b.Value.ToString("N0") + " contracts", Brushes.White, 11, true);

        AddLine(wallRows, "", Brushes.White, 4, false);
        AddLine(wallRows, "ASK WALLS", Brushes.Red, 12, true);
        foreach (var a in topAsks)
            AddLine(wallRows, a.Key.ToString("0.00") + "   " + a.Value.ToString("N0") + " contracts", Brushes.White, 11, true);

        int bidStack = 0, askStack = 0;
        List<string> bidStackPrices = new List<string>();
        List<string> askStackPrices = new List<string>();
        foreach (var b in bidList.Take(25))
        {
            long old;
            prevBids.TryGetValue(b.Key, out old);
            if (old > 0 && b.Value > old) { bidStack++; if (bidStackPrices.Count < 5) bidStackPrices.Add(b.Key.ToString("0.00")); }
            prevBids[b.Key] = b.Value;
        }
        foreach (var a in askList.Take(25))
        {
            long old;
            prevAsks.TryGetValue(a.Key, out old);
            if (old > 0 && a.Value > old) { askStack++; if (askStackPrices.Count < 5) askStackPrices.Add(a.Key.ToString("0.00")); }
            prevAsks[a.Key] = a.Value;
        }

        AddLine(wallRows, "", Brushes.White, 4, false);
        AddLine(wallRows, "BID STACKING: " + bidStack + " levels", Brushes.Lime, 12, true);
        if (bidStackPrices.Count > 0) AddLine(wallRows, string.Join("  ", bidStackPrices), Brushes.Lime, 11, false);
        AddLine(wallRows, "ASK STACKING: " + askStack + " levels", Brushes.Red, 12, true);
        if (askStackPrices.Count > 0) AddLine(wallRows, string.Join("  ", askStackPrices), Brushes.Red, 11, false);

        AddLine(wallRows, "", Brushes.White, 4, false);
        AddLine(wallRows, "CUM DELTA: " + cumDelta.ToString("N0"), cumDelta >= 0 ? Brushes.Lime : Brushes.Red, 12, true);
        AddLine(wallRows, "LAST TRADE VOL: " + lastTradeVol.ToString("N0"), Brushes.White, 11, false);
    }

    private void RenderBidAskGauge(long bidTotal, long askTotal)
    {
        if (gaugeText == null || gaugeBidFill == null || gaugeAskFill == null) return;
        double total = Math.Max(1.0, bidTotal + askTotal);
        double bidPct = bidTotal / total;
        double askPct = askTotal / total;
        double half = 150;
        try
        {
            if (gaugeBar != null && !double.IsNaN(gaugeBar.ActualWidth) && gaugeBar.ActualWidth > 80)
                half = Math.Max(40, gaugeBar.ActualWidth / 2.0);
        }
        catch { }
        gaugeBidFill.Width = Math.Max(2, half * bidPct);
        gaugeAskFill.Width = Math.Max(2, half * askPct);
        gaugeText.Text = "BID " + (bidPct * 100.0).ToString("0") + "%  |  ASK " + (askPct * 100.0).ToString("0") + "%";
        gaugeText.Foreground = bidPct >= askPct ? Brushes.White : Brushes.White;
    }

    private void RenderMostTpoArea(Dictionary<double, int> map)
    {
        if (mostTpoRows == null) return;
        mostTpoRows.Children.Clear();
        if (map == null || map.Count == 0)
        {
            AddLine(mostTpoRows, "Waiting for TPO data...", Brushes.Yellow, 12, true);
            return;
        }

        var top = map.OrderByDescending(x => x.Value).Take(12).ToList();
        int max = Math.Max(1, top.Max(x => x.Value));
        double poc = top[0].Key;
        AddLine(mostTpoRows, "POC / MOST ACCEPTED: " + poc.ToString("0.00") + "  " + top[0].Value + " TPO", Brushes.Yellow, 12, true);
        AddLine(mostTpoRows, "", Brushes.White, 3, false);
        foreach (var x in top)
        {
            Grid r = new Grid { Height = 18 };
            r.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(76) });
            r.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
            AddGridText(r, x.Key.ToString("0.00"), x.Key == poc ? Brushes.Yellow : Brushes.Cyan, 0, 11, true);
            Grid barHost = new Grid();
            Border bar = new Border
            {
                Width = Math.Max(8, 190.0 * x.Value / max),
                HorizontalAlignment = HorizontalAlignment.Left,
                Background = x.Key == poc ? new SolidColorBrush(Color.FromArgb(170, 240, 190, 0)) : new SolidColorBrush(Color.FromArgb(150, 0, 170, 220)),
                CornerRadius = new CornerRadius(2)
            };
            barHost.Children.Add(bar);
            TextBlock t = new TextBlock { Text = x.Value.ToString(), Foreground = Brushes.White, FontSize = 10, FontWeight = FontWeights.Bold, Margin = new Thickness(4, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center };
            barHost.Children.Add(t);
            Grid.SetColumn(barHost, 1); r.Children.Add(barHost);
            mostTpoRows.Children.Add(r);
        }
    }

    private void RenderTape(List<TapeRow> rows)
    {
        if (tapeRows == null) return;
        tapeRows.Children.Clear();
        if (rows.Count == 0)
        {
            AddLine(tapeRows, "Waiting for live tape...", Brushes.Yellow, 12, true);
            return;
        }

        Grid header = new Grid { Height = 18 };
        header.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(70) });
        header.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
        header.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(55) });
        header.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(55) });
        AddGridText(header, "TIME", Brushes.White, 0, 11, true);
        AddGridText(header, "PRICE", Brushes.White, 1, 11, true);
        AddGridText(header, "SIDE", Brushes.White, 2, 11, true);
        AddGridText(header, "SIZE", Brushes.White, 3, 11, true);
        tapeRows.Children.Add(header);

        foreach (var x in rows.Take(22))
        {
            Brush sideBrush = x.Side == "BUY" ? Brushes.Lime : (x.Side == "SELL" ? Brushes.Red : Brushes.White);
            Grid r = new Grid { Height = 18 };
            r.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(70) });
            r.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) });
            r.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(55) });
            r.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(55) });
            if (x.Side == "BUY") r.Background = new SolidColorBrush(Color.FromArgb(32, 0, 180, 50));
            else if (x.Side == "SELL") r.Background = new SolidColorBrush(Color.FromArgb(36, 200, 0, 0));
            AddGridText(r, x.Time.ToString("HH:mm:ss"), sideBrush, 0, 11, false);
            AddGridText(r, x.Price.ToString("0.00"), sideBrush, 1, 11, true);
            AddGridText(r, x.Side, sideBrush, 2, 11, true);
            AddGridText(r, x.Volume.ToString("N0"), sideBrush, 3, 11, true);
            tapeRows.Children.Add(r);
        }
    }

    private void RenderTpo(Dictionary<double, int> map)
    {
        if (tpoTxt == null) return;
        if (map == null || map.Count == 0) { tpoTxt.Text = "Waiting for bars..."; return; }
        tpoTxt.FontSize = TpoFontSize;
        int rows = TpoMaxRows <= 0 ? map.Count : Math.Max(10, TpoMaxRows);
        int letters = TpoMaxLetters <= 0 ? 5000 : Math.Max(1, TpoMaxLetters);
        var ordered = map.OrderByDescending(x => x.Key).Take(rows);
        var lines = ordered.Select(x => x.Key.ToString("0.00") + "  " + new string('A', Math.Max(1, Math.Min(letters, x.Value))));
        tpoTxt.Text = string.Join("\n", lines);
        if (tpoTxt.Parent is StackPanel sp && sp.Parent is ScrollViewer sv)
        {
            sv.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
            sv.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
        }
    }

    private void AddLine(StackPanel parent, string text, Brush color, double fontSize, bool bold)
    {
        parent.Children.Add(new TextBlock
        {
            Text = text,
            Foreground = color,
            FontSize = fontSize,
            FontWeight = bold ? FontWeights.Bold : FontWeights.Normal,
            FontFamily = new FontFamily("Consolas"),
            TextWrapping = TextWrapping.Wrap
        });
    }

    private void AddGridText(Grid g, string text, Brush color, int col, double fontSize, bool bold)
    {
        TextBlock t = new TextBlock
        {
            Text = text,
            Foreground = color,
            FontSize = fontSize,
            FontWeight = bold ? FontWeights.Bold : FontWeights.Normal,
            FontFamily = new FontFamily("Consolas"),
            VerticalAlignment = VerticalAlignment.Center,
            TextAlignment = TextAlignment.Center
        };
        Grid.SetColumn(t, col);
        g.Children.Add(t);
    }

    private double GetPanelHeight()
    {
        try
        {
            double h = ChartControl != null ? ChartControl.ActualHeight - 34 : 720;
            if (double.IsNaN(h) || h < 620) h = 720;
            return h;
        }
        catch { return 720; }
    }

    private void AddBox(StackPanel parent, string title, out StackPanel content, double height)
    {
        Border box = MakeBox(height);
        StackPanel sp = new StackPanel();
        sp.Children.Add(new TextBlock { Text = title, Foreground = Brushes.White, FontWeight = FontWeights.Bold, FontSize = 12, Margin = new Thickness(0, 0, 0, 6) });
        content = new StackPanel();
        sp.Children.Add(content);
        box.Child = sp;
        parent.Children.Add(box);
    }

    private void AddTextBox(StackPanel parent, string title, out TextBlock text, double height)
    {
        Border box = MakeBox(height);
        StackPanel sp = new StackPanel();
        sp.Children.Add(new TextBlock { Text = title, Foreground = Brushes.White, FontWeight = FontWeights.Bold, FontSize = 12, Margin = new Thickness(0, 0, 0, 6) });
        text = new TextBlock { Foreground = Brushes.Cyan, FontSize = 11, FontFamily = new FontFamily("Consolas"), TextWrapping = TextWrapping.Wrap };
        sp.Children.Add(text);
        box.Child = sp;
        parent.Children.Add(box);
    }

    private Border MakeBox(double height)
    {
        return new Border
        {
            Height = height,
            Margin = new Thickness(0, 0, 8, 8),
            Padding = new Thickness(8),
            Background = new SolidColorBrush(Color.FromArgb(160, 5, 14, 24)),
            BorderBrush = new SolidColorBrush(Color.FromRgb(35, 70, 95)),
            BorderThickness = new Thickness(1),
            CornerRadius = new CornerRadius(4)
        };
    }

    private TextBlock MakeText(string text, double size, Brush fg, FontWeight fw, int row, int col)
    {
        TextBlock t = new TextBlock { Text = text, Foreground = fg, FontSize = size, FontWeight = fw, TextAlignment = TextAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
        Grid.SetRow(t, row); Grid.SetColumn(t, col); return t;
    }

    private double GetBestBid() { lock (sync) return bids.Count > 0 ? bids.Keys.Max() : 0; }
    private double GetBestAsk() { lock (sync) return asks.Count > 0 ? asks.Keys.Min() : 0; }
    private double RoundToTick(double p) { return tickSizeLocal <= 0 ? p : Math.Round(p / tickSizeLocal) * tickSizeLocal; }

    private void RemoveUi()
    {
        if (uiTimer != null) { uiTimer.Stop(); uiTimer = null; }
        if (root != null && chartGrid != null && chartGrid.Children.Contains(root)) chartGrid.Children.Remove(root);
        root = null; panel = null; collapsedTab = null; mainGrid = null; chartGrid = null;
    }

    private Grid FindParentGrid(DependencyObject start)
    {
        DependencyObject current = start ?? ChartControl;
        while (current != null)
        {
            Grid g = current as Grid;
            if (g != null) return g;
            try { current = VisualTreeHelper.GetParent(current); }
            catch { break; }
        }
        try
        {
            return ChartControl != null ? VisualTreeHelper.GetParent(ChartControl) as Grid : null;
        }
        catch { return null; }
    }

    private class DomVisualRow
    {
        public Grid RowGrid;
        public TextBlock BidText;
        public TextBlock PriceText;
        public TextBlock AskText;
        public TextBlock DeltaText;
        public Border BidBar;
        public Border AskBar;
    }

    private class TapeRow
    {
        public DateTime Time;
        public double Price;
        public long Volume;
        public string Side;
        public double Delta;
    }
}

}

#region NinjaScript generated code. Neither change nor remove.

namespace NinjaTrader.NinjaScript.Indicators
{
public partial class Indicator : NinjaTrader.Gui.NinjaScript.IndicatorRenderBase
{
private WT_MarketDepthDOMPanel cacheWT_MarketDepthDOMPanel;
public WT_MarketDepthDOMPanel WT_MarketDepthDOMPanel()
{
return WT_MarketDepthDOMPanel(Input);
}

    public WT_MarketDepthDOMPanel WT_MarketDepthDOMPanel(ISeries<double> input)
    {
        if (cacheWT_MarketDepthDOMPanel != null)
            for (int idx = 0; idx < cacheWT_MarketDepthDOMPanel.Length; idx++)
                if (cacheWT_MarketDepthDOMPanel[idx] != null && cacheWT_MarketDepthDOMPanel[idx].EqualsInput(input))
                    return cacheWT_MarketDepthDOMPanel[idx];
        return CacheIndicator<WT_MarketDepthDOMPanel>(new WT_MarketDepthDOMPanel(), input, ref cacheWT_MarketDepthDOMPanel);
    }
}

}

namespace NinjaTrader.NinjaScript.MarketAnalyzerColumns
{
public partial class MarketAnalyzerColumn : MarketAnalyzerColumnBase
{
public Indicators.WT_MarketDepthDOMPanel WT_MarketDepthDOMPanel()
{
return indicator.WT_MarketDepthDOMPanel(Input);
}

    public Indicators.WT_MarketDepthDOMPanel WT_MarketDepthDOMPanel(ISeries<double> input)
    {
        return indicator.WT_MarketDepthDOMPanel(input);
    }
}

}

namespace NinjaTrader.NinjaScript.Strategies
{
public partial class Strategy : NinjaTrader.Gui.NinjaScript.StrategyRenderBase
{
public Indicators.WT_MarketDepthDOMPanel WT_MarketDepthDOMPanel()
{
return indicator.WT_MarketDepthDOMPanel(Input);
}

    public Indicators.WT_MarketDepthDOMPanel WT_MarketDepthDOMPanel(ISeries<double> input)
    {
        return indicator.WT_MarketDepthDOMPanel(input);
    }
}

}

#endregion

4 Likes

Thanks for sharing. You don’t have to paste “#region NinjaScript generated code. Neither change nor remove.” region.

1 Like

For posting C# code try:

```csharp
Your code.
```

Example long class:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
using GammaVannaCharm.Core.Exposure;
using GammaVannaCharm.Core.Options;

namespace GammaVannaCharm.Core.MarketData.Cboe;

public static class CboeOptionsParser
{
    private const string DataFieldName = "data";
    private const string OptionsFieldName = "options";
    private static readonly string[] TimestampFieldNames = { "timestamp", "time", "last_update", "lastUpdate" };
    private static readonly string[] CurrentPriceFieldNames = { "current_price", "currentPrice", "last", "price" };
    private static readonly string[] OptionFieldNames = { "option", "option_symbol", "optionSymbol", "symbol" };
    private static readonly string[] OpenInterestFieldNames = { "open_interest", "openInterest", "oi" };
    private static readonly string[] VolumeFieldNames = { "volume", "vol" };
    private static readonly string[] ImpliedVolatilityFieldNames = { "iv", "implied_volatility", "impliedVolatility" };
    private static readonly string[] GammaFieldNames = { "gamma" };
    private static readonly string[] BidFieldNames = { "bid" };
    private static readonly string[] AskFieldNames = { "ask" };
    private static readonly string[] LastTradePriceFieldNames = { "last_trade_price", "lastTradePrice", "last_price", "lastPrice", "last" };

    private static readonly Regex OptionSymbolRegex = new Regex(
        "^(.*?)(\\d{6})([CP])(\\d{8})$",
        RegexOptions.Compiled | RegexOptions.CultureInvariant);

    private static readonly Lazy<TimeZoneInfo> s_usEasternTimeZone = new Lazy<TimeZoneInfo>(() =>
    {
        TimeZoneInfo timeZone;
        if (TryFindTimeZone("Eastern Standard Time", out timeZone)
            || TryFindTimeZone("America/New_York", out timeZone))
        {
            return timeZone;
        }
        return null;
    });

    public static bool TryParseOptionChainSnapshot(string underlyingSymbol, string json, bool zeroDteOnly, out OptionChainSnapshot snapshot)
    {
        snapshot = null;

        if (string.IsNullOrWhiteSpace(underlyingSymbol) || string.IsNullOrWhiteSpace(json))
            return false;

        JsonDocument document;
        if (!TryParseJsonObject(json, out document))
            return false;

        using (document)
        {
            var root = document.RootElement;
            JsonElement data;
            var hasData = TryGetObjectField(root, DataFieldName, out data);

            double underlyingPrice;
            if (!TryReadNumberField(root, CurrentPriceFieldNames, out underlyingPrice)
                && !(hasData && TryReadNumberField(data, CurrentPriceFieldNames, out underlyingPrice)))
            {
                return false;
            }
            if (underlyingPrice <= 0.0)
                return false;

            List<JsonElement> optionRows;
            if (!TryReadOptionRows(root, hasData, data, out optionRows) || optionRows.Count == 0)
                return false;

            var snapshotTimestampUtc = ResolveSnapshotTimestampUtc(root, hasData, data);
            var zeroDteDate = ResolveZeroDteDate(optionRows, snapshotTimestampUtc);
            var contracts = new List<OptionContractData>();
            for (var index = 0; index < optionRows.Count; index++)
            {
                var optionRow = optionRows[index];
                if (optionRow.ValueKind != JsonValueKind.Object)
                    continue;

                string optionSymbol;
                if (!TryReadStringField(optionRow, OptionFieldNames, out optionSymbol))
                    continue;

                OptionRight right;
                double strike;
                long expirationUnixSeconds;
                if (!TryParseOptionSymbol(optionSymbol, out right, out strike, out expirationUnixSeconds))
                    continue;

                if (zeroDteOnly && UnixSecondsToUtc(expirationUnixSeconds).Date != zeroDteDate)
                    continue;

                var openInterest = ReadLongField(optionRow, OpenInterestFieldNames);
                var volume = ReadLongField(optionRow, VolumeFieldNames);
                var impliedVolatility = ReadDoubleField(optionRow, ImpliedVolatilityFieldNames);
                var gamma = ReadDoubleField(optionRow, GammaFieldNames);
                var bid = ReadDoubleField(optionRow, BidFieldNames);
                var ask = ReadDoubleField(optionRow, AskFieldNames);
                var lastPrice = ReadDoubleField(optionRow, LastTradePriceFieldNames);

                if (impliedVolatility <= 0.0)
                    continue;

                var contract = new OptionContractData
                {
                    Symbol = optionSymbol,
                    Right = right,
                    Strike = strike,
                    OpenInterest = openInterest,
                    Volume = volume,
                    ImpliedVolatility = impliedVolatility,
                    Gamma = gamma,
                    Bid = bid,
                    Ask = ask,
                    LastPrice = lastPrice,
                    ExpirationUnixSeconds = expirationUnixSeconds
                };
                if (!TryValidateContract(contract))
                    continue;

                contracts.Add(contract);
            }

            if (contracts.Count == 0)
                return false;

            snapshot = new OptionChainSnapshot(underlyingSymbol, underlyingPrice, snapshotTimestampUtc, contracts);
            return true;
        }
    }

    private static bool TryParseJsonObject(string json, out JsonDocument document)
    {
        document = null;
        try
        {
            document = JsonDocument.Parse(json);
            if (document.RootElement.ValueKind == JsonValueKind.Object)
                return true;

            document.Dispose();
            document = null;
            return false;
        }
        catch (JsonException)
        {
            return false;
        }
        catch (ArgumentException)
        {
            return false;
        }
    }

    private static bool TryValidateContract(OptionContractData contract)
    {
        try
        {
            contract.Validate();
            return true;
        }
        catch (InvalidOperationException)
        {
            return false;
        }
    }

    private static bool TryGetObjectField(JsonElement fields, string fieldName, out JsonElement value)
    {
        value = default(JsonElement);
        return fields.ValueKind == JsonValueKind.Object
            && fields.TryGetProperty(fieldName, out value)
            && value.ValueKind == JsonValueKind.Object;
    }

    private static bool TryReadOptionRows(JsonElement root, bool hasData, JsonElement data, out List<JsonElement> optionRows)
    {
        optionRows = new List<JsonElement>();

        AddOptionRows(root, optionRows);
        if (optionRows.Count == 0 && hasData)
            AddOptionRows(data, optionRows);

        return optionRows.Count > 0;
    }

    private static void AddOptionRows(JsonElement fields, List<JsonElement> optionRows)
    {
        if (fields.ValueKind != JsonValueKind.Object || optionRows == null)
            return;

        JsonElement options;
        if (!fields.TryGetProperty(OptionsFieldName, out options))
            return;

        AddOptionRowsFromElement(options, optionRows);
    }

    private static void AddOptionRowsFromElement(JsonElement options, List<JsonElement> optionRows)
    {
        if (optionRows == null)
            return;

        if (options.ValueKind == JsonValueKind.Array)
        {
            foreach (var optionRow in options.EnumerateArray())
            {
                if (optionRow.ValueKind == JsonValueKind.Object)
                    optionRows.Add(optionRow);
            }
            return;
        }

        if (options.ValueKind != JsonValueKind.Object)
            return;

        foreach (var optionGroup in options.EnumerateObject())
            AddOptionRowsFromElement(optionGroup.Value, optionRows);
    }

    private static DateTime ResolveSnapshotTimestampUtc(JsonElement root, bool hasData, JsonElement data)
    {
        string timestampText;
        if (TryReadStringField(root, TimestampFieldNames, out timestampText))
        {
            var resolvedTimestamp = ResolveSnapshotTimestampUtc(timestampText);
            if (resolvedTimestamp != default(DateTime) && resolvedTimestamp != DateTime.MinValue)
                return resolvedTimestamp;
            if (!hasData)
                return resolvedTimestamp;
        }

        if (hasData && TryReadStringField(data, TimestampFieldNames, out timestampText))
        {
            return ResolveSnapshotTimestampUtc(timestampText);
        }

        return DateTime.UtcNow;
    }

    private static DateTime ResolveSnapshotTimestampUtc(string timestampText)
    {
        if (string.IsNullOrWhiteSpace(timestampText))
            return DateTime.UtcNow;

        timestampText = timestampText.Trim();
        DateTimeOffset timestampOffset;
        if (DateTimeOffset.TryParse(
            timestampText,
            CultureInfo.InvariantCulture,
            DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
            out timestampOffset))
        {
            return timestampOffset.UtcDateTime;
        }

        DateTime parsedTimestamp;
        if (DateTime.TryParseExact(
                timestampText,
                "yyyy-MM-dd HH:mm:ss",
                CultureInfo.InvariantCulture,
                DateTimeStyles.None,
                out parsedTimestamp)
            || DateTime.TryParseExact(
                timestampText,
                "yyyy-MM-ddTHH:mm:ss",
                CultureInfo.InvariantCulture,
                DateTimeStyles.None,
                out parsedTimestamp)
            || DateTime.TryParseExact(
                timestampText,
                "yyyy-MM-dd",
                CultureInfo.InvariantCulture,
                DateTimeStyles.None,
                out parsedTimestamp))
        {
            // CBOE delayed_quotes/options payloads used by CboeOptionsClient publish UTC
            // response/update timestamps, not a guaranteed delayed-market as-of time, but omit
            // a timezone designator in current responses and fixtures.
            return DateTime.SpecifyKind(parsedTimestamp, DateTimeKind.Utc);
        }

        return DateTime.UtcNow;
    }

    private static DateTime ResolveZeroDteDate(IList<JsonElement> optionRows, DateTime snapshotTimestampUtc)
    {
        var chainDate = snapshotTimestampUtc.Date;
        var zeroDteDate = DateTime.MaxValue.Date;
        for (var index = 0; index < optionRows.Count; index++)
        {
            var optionRow = optionRows[index];
            if (optionRow.ValueKind != JsonValueKind.Object)
                continue;

            string optionSymbol;
            if (!TryReadStringField(optionRow, OptionFieldNames, out optionSymbol))
                continue;

            OptionRight right;
            double strike;
            long expirationUnixSeconds;
            if (!TryParseOptionSymbol(optionSymbol, out right, out strike, out expirationUnixSeconds))
                continue;

            var expirationDate = UnixSecondsToUtc(expirationUnixSeconds).Date;
            if (expirationDate >= chainDate && expirationDate < zeroDteDate)
                zeroDteDate = expirationDate;
        }

        return zeroDteDate == DateTime.MaxValue.Date ? chainDate : zeroDteDate;
    }

    private static bool TryParseOptionSymbol(string optionSymbol, out OptionRight right, out double strike, out long expirationUnixSeconds)
    {
        right = OptionRight.Call;
        strike = 0.0;
        expirationUnixSeconds = 0L;

        var match = OptionSymbolRegex.Match(optionSymbol ?? string.Empty);
        if (!match.Success || match.Groups.Count < 5)
            return false;

        var optionRoot = match.Groups[1].Value;
        right = match.Groups[3].Value == "P" ? OptionRight.Put : OptionRight.Call;
        long rawStrike;
        if (!long.TryParse(match.Groups[4].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out rawStrike))
            return false;
        strike = rawStrike / 1000.0;

        var expirationDateText = match.Groups[2].Value;
        DateTime expirationDate;
        if (!DateTime.TryParseExact(expirationDateText, "yyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out expirationDate))
            return false;

        expirationUnixSeconds = GetCboeOptionExpirationUnixSeconds(optionRoot, expirationDate.Date);
        return strike > 0.0 && expirationUnixSeconds > 0L;
    }

    private static long GetCboeOptionExpirationUnixSeconds(string optionRoot, DateTime expirationDate)
    {
        // CBOE symbols carry only the expiration date. Treating that date as midnight UTC makes
        // live 0DTE contracts look expired and forces the Greeks path into its minimum-time clamp.
        // Use the root-specific listed close where CBOE symbols provide enough information.
        var normalizedRoot = (optionRoot ?? string.Empty).Trim().ToUpperInvariant();
        var closeDate = expirationDate.Date;
        var closeHour = 16.0;

        if (normalizedRoot == "SPY" || normalizedRoot == "QQQ")
        {
            closeHour = 16.25;
        }
        else if (normalizedRoot == "SPX" && IsStandardMonthlyExpiration(expirationDate.Date))
        {
            closeDate = GetPreviousWeekday(expirationDate.Date);
            closeHour = 17.0;
        }

        return ToUsEasternUnixSeconds(closeDate, closeHour);
    }

    private static long ToUsEasternUnixSeconds(DateTime localDate, double hour)
    {
        var easternTime = DateTime.SpecifyKind(localDate.Date.AddHours(hour), DateTimeKind.Unspecified);
        var easternTimeZone = s_usEasternTimeZone.Value;
        if (easternTimeZone != null)
            return ToUnixSeconds(TimeZoneInfo.ConvertTimeToUtc(easternTime, easternTimeZone));

        var utcTime = easternTime - GetFallbackUsEasternUtcOffset(localDate.Date);
        return ToUnixSeconds(DateTime.SpecifyKind(utcTime, DateTimeKind.Utc));
    }

    private static bool IsStandardMonthlyExpiration(DateTime expirationDate)
    {
        return expirationDate.DayOfWeek == DayOfWeek.Friday
            && expirationDate.Day >= 15
            && expirationDate.Day <= 21;
    }

    private static DateTime GetPreviousWeekday(DateTime localDate)
    {
        var previous = localDate.Date.AddDays(-1.0);
        while (previous.DayOfWeek == DayOfWeek.Saturday || previous.DayOfWeek == DayOfWeek.Sunday)
            previous = previous.AddDays(-1.0);

        return previous;
    }


    private static bool TryFindTimeZone(string timeZoneId, out TimeZoneInfo timeZone)
    {
        timeZone = null;
        try
        {
            timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
            return timeZone != null;
        }
        catch (TimeZoneNotFoundException)
        {
            return false;
        }
        catch (InvalidTimeZoneException)
        {
            return false;
        }
    }

    private static TimeSpan GetFallbackUsEasternUtcOffset(DateTime localDate)
    {
        return IsFallbackUsEasternDaylightSavingDate(localDate.Date)
            ? TimeSpan.FromHours(-4.0)
            : TimeSpan.FromHours(-5.0);
    }

    private static bool IsFallbackUsEasternDaylightSavingDate(DateTime localDate)
    {
        var daylightStart = GetNthSunday(localDate.Year, 3, 2);
        var daylightEnd = GetNthSunday(localDate.Year, 11, 1);
        return localDate >= daylightStart && localDate < daylightEnd;
    }

    private static DateTime GetNthSunday(int year, int month, int occurrence)
    {
        var firstDayOfMonth = new DateTime(year, month, 1);
        var daysUntilSunday = ((int)DayOfWeek.Sunday - (int)firstDayOfMonth.DayOfWeek + 7) % 7;
        return firstDayOfMonth.AddDays(daysUntilSunday + ((occurrence - 1) * 7));
    }

    private static double ReadDoubleField(JsonElement fields, string[] fieldNames)
    {
        double value;
        return TryReadNumberField(fields, fieldNames, out value) ? value : 0.0;
    }

    private static long ReadLongField(JsonElement fields, string[] fieldNames)
    {
        var value = ReadDoubleField(fields, fieldNames);
        if (value <= 0.0)
            return 0L;

        return (long)Math.Round(value, MidpointRounding.AwayFromZero);
    }

    private static bool TryReadStringField(JsonElement fields, string fieldName, out string value)
    {
        return TryReadStringField(fields, new[] { fieldName }, out value);
    }

    private static bool TryReadStringField(JsonElement fields, string[] fieldNames, out string value)
    {
        value = null;
        if (fields.ValueKind != JsonValueKind.Object || fieldNames == null)
            return false;

        for (var index = 0; index < fieldNames.Length; index++)
        {
            JsonElement rawValue;
            if (string.IsNullOrWhiteSpace(fieldNames[index])
                || !fields.TryGetProperty(fieldNames[index], out rawValue)
                || rawValue.ValueKind != JsonValueKind.String)
            {
                continue;
            }

            value = rawValue.GetString();
            if (!string.IsNullOrWhiteSpace(value))
                return true;
        }

        return false;
    }

    private static bool TryReadNumberField(JsonElement fields, string fieldName, out double value)
    {
        return TryReadNumberField(fields, new[] { fieldName }, out value);
    }

    private static bool TryReadNumberField(JsonElement fields, string[] fieldNames, out double value)
    {
        value = 0.0;
        if (fields.ValueKind != JsonValueKind.Object || fieldNames == null)
            return false;

        for (var index = 0; index < fieldNames.Length; index++)
        {
            JsonElement rawValue;
            if (!string.IsNullOrWhiteSpace(fieldNames[index])
                && fields.TryGetProperty(fieldNames[index], out rawValue)
                && TryReadNumber(rawValue, out value))
            {
                return true;
            }
        }

        return false;
    }

    private static bool TryReadNumber(JsonElement rawValue, out double value)
    {
        value = 0.0;
        if (rawValue.ValueKind == JsonValueKind.Number)
            return rawValue.TryGetDouble(out value) && IsFinite(value);

        if (rawValue.ValueKind != JsonValueKind.String)
            return false;

        var text = rawValue.GetString();
        if (string.IsNullOrWhiteSpace(text))
            return false;

        if (!double.TryParse(text.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out value))
            return false;

        return IsFinite(value);
    }

    private static bool IsFinite(double value)
    {
        return !double.IsNaN(value) && !double.IsInfinity(value);
    }

    private static DateTime UnixSecondsToUtc(long unixSeconds)
    {
        return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(unixSeconds);
    }

    private static long ToUnixSeconds(DateTime timestampUtc)
    {
        return (long)(timestampUtc - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
    }
}
3 Likes

I used your code to get started with a DOM I always wanted to make. Thanks!
ladder

3 Likes

Glad it helped out have fun and enjoy,

this long vertical dom just gave me an idea, I’m currently working on a two-year point of control, however I’m using trend lines with no separations, with a heat map behind it, I just bumped into a glitch problem so I have to start my code from scratch again lol , however now I would like to see a dom some sort on it , will be so hard to get it to expectations, I will try though lol , I will show u guys the image once I’m done coding what I need before I add data to it

I’m working on this, this the activity in between 2023 and 2026 is so scary lol, I’m planning on adding a dom type of price to calculate the most activity areas, and the smooth runs, if anyone has any better ideas feel free to say , I’m curious sis this will be a total failure or an ok idea to build,

1 Like

This is great work.

I assume it is ok for me to modify for my use?

I don’t need all of the items and would be useful to customize for my charts.

yes feel free to use it and make changes

I am amazed (and pleased) it is so resource friendly.

Thank you

1 Like