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

1 Like

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

ManyVII

Out of curiosity why a 2yr POC ?

In my view, that will be too much noise.

Perhaps segmenting it to cumulative delta flip as pivot points as a reference point for the POC/SupportRessistance.

Or even adding an Engulfing Value Area, Positive or Negative POC, Negative or Positive Value Area, those kinds of stuffs may decrease the noise.

it was to make sure long range its being acceptance, there for if anyone would shorten it, they wouldn’t have any issues doing so,

Key levels & PDH/PDL - #19 by BillMilton do you mean something like this ?

would it be something like this

1 Like

I see a lot of code of indicators, I am interested on it. but i can’t have the correct code

1 Like