Sethmo Large Delta Bubbles

I am hoping someone might be able to help or provide some insight. I will preface this by stating that I have zero experience or knowledge on how to write code.

As the title suggests, this in regards to an indicator (Sethmo Large Delta Bubbles) that is available on community indicators on the NT web platform. I will attach a screenshot of what they look like at the bottom of the post. Essentially, I am trying to get that indicator onto my NT8 desktop app, so I pasted the code in to Claude asking if it could convert the code so it would be usable within the NT8 desktop app. Claude attempted to convert the code but every time I tried to create the indicator, I kept getting the same error stating “Unhandled exception: At least one object must implement IComparable.” I would inform Claude, it would change the code, I’d put the new code in and would still get the same error. After about 5 attempts I gave up for the time being.

So, I was wondering if anybody could take a look at the code and maybe inform me on how I can ask Claude to adjust it properly. I will attach the screenshot of what I am trying to replicate and then I will post the code as a reply to this topic. Also, on a side not, I am aware that NT8 has the “Orderflow Large Trade Detector” in it’s orderflow suite (which I have used) but I am not a fan of how it plots this particular information.

Hopefully someone will have some insight and I’d appreciate any sort of input you guys may have.

#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using NinjaTrader.Cbi;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.Core.FloatingPoint;
using SharpDX;
using SharpDX.Direct2D1;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
public class SethmoLargeDeltaBubbles : Indicator
{
private double maxDelta = 0;
private Dictionary<int, List> barDeltaData = new Dictionary<int, List>();

    protected override void OnStateChange()
    {
        if (State == State.SetDefaults)
        {
            Description = @"Sethmo Large Delta Bubbles - Displays delta imbalance bubbles at price levels";
            Name = "SethmoLargeDeltaBubbles";
            Calculate = Calculate.OnBarClose; // Changed from OnEachTick for stability
            IsOverlay = true;
            DisplayInDataBox = true;
            DrawOnPricePanel = true;
            ScaleJustification = NinjaTrader.Gui.Chart.ScaleJustification.Right;
            IsSuspendedWhileInactive = true;
            
            // Parameters
            PositiveDeltaColor = Brushes.DodgerBlue;
            NegativeDeltaColor = Brushes.DeepPink;
            EnableTextLabels = true;
            RthVolThreshold = 250;
            PriceLevelsPerBar = 6;
            BubbleOpacity = 35;
            MaxBubbleSize = 35;
        }
        else if (State == State.Configure)
        {
            // This indicator requires volumetric data
        }
        else if (State == State.DataLoaded)
        {
            maxDelta = 0;
            barDeltaData.Clear();
        }
    }

    protected override void OnBarUpdate()
    {
        if (CurrentBar < 1)
            return;

        // Check if we have volumetric data
        if (Bars.BarsSeries.BarsType.IsRemoveLastBarSupported == false)
        {
            Draw.TextFixed(this, "VolumetricWarning", "This indicator requires volumetric bars (Volumetric or Order Flow+)", TextPosition.TopLeft);
            return;
        }

        ProcessBarDelta(CurrentBar);
    }

    private void ProcessBarDelta(int barIndex)
    {
        // Get volumetric data for the bar
        var volumetricBar = Bars.GetVolumetric(barIndex);
        
        if (volumetricBar == null)
            return;

        // Check if Prices collection exists and has data
        if (volumetricBar.Prices == null || volumetricBar.Prices.Count == 0)
            return;

        Dictionary<double, PriceLevelData> combinedProfiles = new Dictionary<double, PriceLevelData>();
        
        double modifiedSize = TickSize * PriceLevelsPerBar;
        double barInit = 0;
        double last = 0;
        bool firstLevel = true;

        // Get all price levels and sort them - filter out any invalid values
        List<double> priceLevels;
        try
        {
            priceLevels = volumetricBar.Prices
                .Where(p => !double.IsNaN(p) && !double.IsInfinity(p))
                .OrderBy(p => p)
                .ToList();
        }
        catch
        {
            return; // Exit if we can't get price levels
        }
        
        if (priceLevels.Count == 0)
            return;
        
        foreach (double price in priceLevels)
        {
            long bidVol = volumetricBar.GetBidVolumeForPrice(price);
            long askVol = volumetricBar.GetAskVolumeForPrice(price);
            long totalVol = bidVol + askVol;

            if (firstLevel)
            {
                barInit = price;
                firstLevel = false;
            }

            if (last != 0 && last >= barInit + modifiedSize)
            {
                barInit = price;
            }

            if (last == 0 || last <= barInit + modifiedSize)
            {
                string key = Math.Round(barInit, 7).ToString();
                
                if (!combinedProfiles.ContainsKey(barInit))
                {
                    combinedProfiles[barInit] = new PriceLevelData
                    {
                        Price = barInit,
                        BidVolume = 0,
                        AskVolume = 0,
                        TotalVolume = 0
                    };
                }

                combinedProfiles[barInit].BidVolume += bidVol;
                combinedProfiles[barInit].AskVolume += askVol;
                combinedProfiles[barInit].TotalVolume += totalVol;
            }
            
            last = price;
        }

        // Calculate max delta for scaling
        if (combinedProfiles.Count > 0)
        {
            try
            {
                double biggestDelta = combinedProfiles.Values
                    .Select(p => Math.Abs(p.Delta))
                    .Where(d => !double.IsNaN(d) && !double.IsInfinity(d))
                    .DefaultIfEmpty(0)
                    .Max();
                
                maxDelta = Math.Max(maxDelta, biggestDelta);
            }
            catch
            {
                // If max calculation fails, continue without updating maxDelta
            }
        }

        // Store the data for rendering
        barDeltaData[barIndex] = combinedProfiles.Values.ToList();
    }

    protected override void OnRender(ChartControl chartControl, ChartScale chartScale)
    {
        base.OnRender(chartControl, chartScale);

        if (Bars == null || chartControl == null)
            return;

        // Render delta bubbles for visible bars
        int firstBarIndex = Math.Max(ChartBars.FromIndex, 0);
        int lastBarIndex = Math.Min(ChartBars.ToIndex, CurrentBar);

        for (int barIndex = firstBarIndex; barIndex <= lastBarIndex; barIndex++)
        {
            if (!barDeltaData.ContainsKey(barIndex))
                continue;

            var priceLevels = barDeltaData[barIndex];
            double modifiedSize = TickSize * PriceLevelsPerBar;

            foreach (var level in priceLevels)
            {
                double delta = level.Delta;
                
                if (Math.Abs(delta) <= RthVolThreshold)
                    continue;

                // Calculate bubble radius with scaling
                double minR = 8;
                double maxR = MaxBubbleSize;
                double minI = RthVolThreshold;
                double maxI = maxDelta + (maxDelta / 2.15);

                double radiusScale = Math.Min(
                    ((Math.Abs(delta) - minI) / (maxI - minI)) * (maxR - minR) + minR,
                    maxR
                );

                if (radiusScale < minR)
                    radiusScale = minR;

                // Calculate screen coordinates
                double priceY = level.Price + (modifiedSize / 2);
                int x = chartControl.GetXByBarIndex(ChartBars, barIndex);
                int y = chartScale.GetYByValue(priceY);

                // Draw bubble
                SharpDX.Direct2D1.Brush brush = delta > 0 
                    ? PositiveDeltaColor.ToDxBrush(RenderTarget) 
                    : NegativeDeltaColor.ToDxBrush(RenderTarget);
                
                brush.Opacity = BubbleOpacity / 100f;

                RenderTarget.FillEllipse(
                    new SharpDX.Direct2D1.Ellipse(new SharpDX.Vector2(x, y), (float)radiusScale, (float)radiusScale),
                    brush
                );

                // Draw text label if enabled
                if (EnableTextLabels)
                {
                    string text = delta.ToString("F0");
                    
                    var textFormat = new SharpDX.DirectWrite.TextFormat(
                        Core.Globals.DirectWriteFactory,
                        "Arial",
                        SharpDX.DirectWrite.FontWeight.Normal,
                        SharpDX.DirectWrite.FontStyle.Normal,
                        9f
                    )
                    {
                        TextAlignment = SharpDX.DirectWrite.TextAlignment.Center,
                        ParagraphAlignment = SharpDX.DirectWrite.ParagraphAlignment.Center
                    };

                    var textBrush = Brushes.White.ToDxBrush(RenderTarget);
                    
                    RenderTarget.DrawText(
                        text,
                        textFormat,
                        new SharpDX.RectangleF(x - 30, y - 10, 60, 20),
                        textBrush
                    );

                    textFormat.Dispose();
                    textBrush.Dispose();
                }

                brush.Dispose();
            }
        }
    }

    #region Properties

    [NinjaScriptProperty]
    [XmlIgnore]
    [Display(Name = "Positive Delta Color", Order = 1, GroupName = "Colors")]
    public System.Windows.Media.Brush PositiveDeltaColor { get; set; }

    [Browsable(false)]
    public string PositiveDeltaColorSerializable
    {
        get { return Serialize.BrushToString(PositiveDeltaColor); }
        set { PositiveDeltaColor = Serialize.StringToBrush(value); }
    }

    [NinjaScriptProperty]
    [XmlIgnore]
    [Display(Name = "Negative Delta Color", Order = 2, GroupName = "Colors")]
    public System.Windows.Media.Brush NegativeDeltaColor { get; set; }

    [Browsable(false)]
    public string NegativeDeltaColorSerializable
    {
        get { return Serialize.BrushToString(NegativeDeltaColor); }
        set { NegativeDeltaColor = Serialize.StringToBrush(value); }
    }

    [NinjaScriptProperty]
    [Display(Name = "Enable Text Labels", Order = 1, GroupName = "Display")]
    public bool EnableTextLabels { get; set; }

    [NinjaScriptProperty]
    [Range(1, int.MaxValue)]
    [Display(Name = "Volume Threshold", Order = 2, GroupName = "Display")]
    public int RthVolThreshold { get; set; }

    [NinjaScriptProperty]
    [Range(1, 100)]
    [Display(Name = "Price Levels Per Bar", Order = 3, GroupName = "Display")]
    public int PriceLevelsPerBar { get; set; }

    [NinjaScriptProperty]
    [Range(0, 100)]
    [Display(Name = "Bubble Opacity (%)", Order = 4, GroupName = "Display")]
    public int BubbleOpacity { get; set; }

    [NinjaScriptProperty]
    [Range(5, 100)]
    [Display(Name = "Max Bubble Size", Order = 5, GroupName = "Display")]
    public int MaxBubbleSize { get; set; }

    #endregion
}

// Helper class to store price level data
public class PriceLevelData
{
    public double Price { get; set; }
    public long BidVolume { get; set; }
    public long AskVolume { get; set; }
    public long TotalVolume { get; set; }
    public double Delta => -(BidVolume - AskVolume); // Negative of (bid - ask) to match Tradovate logic
}

}

#region NinjaScript generated code. Neither change nor remove.

namespace NinjaTrader.NinjaScript.Indicators
{
public partial class Indicator : NinjaTrader.Gui.NinjaScript.IndicatorRenderBase
{
private SethmoLargeDeltaBubbles cacheSethmoLargeDeltaBubbles;
public SethmoLargeDeltaBubbles SethmoLargeDeltaBubbles(System.Windows.Media.Brush positiveDeltaColor, System.Windows.Media.Brush negativeDeltaColor, bool enableTextLabels, int rthVolThreshold, int priceLevelsPerBar, int bubbleOpacity, int maxBubbleSize)
{
return SethmoLargeDeltaBubbles(Input, positiveDeltaColor, negativeDeltaColor, enableTextLabels, rthVolThreshold, priceLevelsPerBar, bubbleOpacity, maxBubbleSize);
}

	public SethmoLargeDeltaBubbles SethmoLargeDeltaBubbles(ISeries<double> input, System.Windows.Media.Brush positiveDeltaColor, System.Windows.Media.Brush negativeDeltaColor, bool enableTextLabels, int rthVolThreshold, int priceLevelsPerBar, int bubbleOpacity, int maxBubbleSize)
	{
		if (cacheSethmoLargeDeltaBubbles != null)
			for (int idx = 0; idx < cacheSethmoLargeDeltaBubbles.Length; idx++)
				if (cacheSethmoLargeDeltaBubbles[idx] != null && cacheSethmoLargeDeltaBubbles[idx].EnableTextLabels == enableTextLabels && cacheSethmoLargeDeltaBubbles[idx].RthVolThreshold == rthVolThreshold && cacheSethmoLargeDeltaBubbles[idx].PriceLevelsPerBar == priceLevelsPerBar && cacheSethmoLargeDeltaBubbles[idx].BubbleOpacity == bubbleOpacity && cacheSethmoLargeDeltaBubbles[idx].MaxBubbleSize == maxBubbleSize && cacheSethmoLargeDeltaBubbles[idx].EqualsInput(input))
					return cacheSethmoLargeDeltaBubbles[idx];
		return CacheIndicator<SethmoLargeDeltaBubbles>(new SethmoLargeDeltaBubbles(){ EnableTextLabels = enableTextLabels, RthVolThreshold = rthVolThreshold, PriceLevelsPerBar = priceLevelsPerBar, BubbleOpacity = bubbleOpacity, MaxBubbleSize = maxBubbleSize }, input, ref cacheSethmoLargeDeltaBubbles);
	}
}

}

namespace NinjaTrader.NinjaScript.MarketAnalyzerColumns
{
public partial class MarketAnalyzerColumn : MarketAnalyzerColumnBase
{
public Indicators.SethmoLargeDeltaBubbles SethmoLargeDeltaBubbles(System.Windows.Media.Brush positiveDeltaColor, System.Windows.Media.Brush negativeDeltaColor, bool enableTextLabels, int rthVolThreshold, int priceLevelsPerBar, int bubbleOpacity, int maxBubbleSize)
{
return indicator.SethmoLargeDeltaBubbles(Input, positiveDeltaColor, negativeDeltaColor, enableTextLabels, rthVolThreshold, priceLevelsPerBar, bubbleOpacity, maxBubbleSize);
}

	public Indicators.SethmoLargeDeltaBubbles SethmoLargeDeltaBubbles(ISeries<double> input , System.Windows.Media.Brush positiveDeltaColor, System.Windows.Media.Brush negativeDeltaColor, bool enableTextLabels, int rthVolThreshold, int priceLevelsPerBar, int bubbleOpacity, int maxBubbleSize)
	{
		return indicator.SethmoLargeDeltaBubbles(input, positiveDeltaColor, negativeDeltaColor, enableTextLabels, rthVolThreshold, priceLevelsPerBar, bubbleOpacity, maxBubbleSize);
	}
}

}

namespace NinjaTrader.NinjaScript.Strategies
{
public partial class Strategy : NinjaTrader.Gui.NinjaScript.StrategyRenderBase
{
public Indicators.SethmoLargeDeltaBubbles SethmoLargeDeltaBubbles(System.Windows.Media.Brush positiveDeltaColor, System.Windows.Media.Brush negativeDeltaColor, bool enableTextLabels, int rthVolThreshold, int priceLevelsPerBar, int bubbleOpacity, int maxBubbleSize)
{
return indicator.SethmoLargeDeltaBubbles(Input, positiveDeltaColor, negativeDeltaColor, enableTextLabels, rthVolThreshold, priceLevelsPerBar, bubbleOpacity, maxBubbleSize);
}

	public Indicators.SethmoLargeDeltaBubbles SethmoLargeDeltaBubbles(ISeries<double> input , System.Windows.Media.Brush positiveDeltaColor, System.Windows.Media.Brush negativeDeltaColor, bool enableTextLabels, int rthVolThreshold, int priceLevelsPerBar, int bubbleOpacity, int maxBubbleSize)
	{
		return indicator.SethmoLargeDeltaBubbles(input, positiveDeltaColor, negativeDeltaColor, enableTextLabels, rthVolThreshold, priceLevelsPerBar, bubbleOpacity, maxBubbleSize);
	}
}

}

#endregion

I quickly scanned through it. I found it odd that the screenshot you posted is not the volumetric bars, but the code itself seems to assume your chart is the volumetric bars. I recently opened source an indicator that has delta bubbles. Well.. imbalances, so not technically just delta.

3 Likes

There were a lot more problems than just the IComparable issue in that code. It was a fun little challenge to fix it up. The main problems were the lack of volumetric data (as noted by Walee), misuse of Dictionary and List types, and a bar misalignment in the OnRender() code. Here’s the modified code that should work (note: the code below is using 5 minute volumetric bars, if you want something else just change those values, or turn them into properties if you want to choose at runtime):

public class SethmoLargeDeltaBubbles : Indicator
{
    private double maxDelta = 0;
    private Dictionary<int, List<PriceLevelData>> barDeltaData = new Dictionary<int, List<PriceLevelData>>();

    protected override void OnStateChange()
    {
        if (State == State.SetDefaults)
        {
            Description = @"Sethmo Large Delta Bubbles - Displays delta imbalance bubbles at price levels";
            Name = "SethmoLargeDeltaBubbles";
            Calculate = Calculate.OnBarClose; // Changed from OnEachTick for stability
            IsOverlay = true;
            DisplayInDataBox = true;
            DrawOnPricePanel = true;
            ScaleJustification = NinjaTrader.Gui.Chart.ScaleJustification.Right;
            IsSuspendedWhileInactive = true;

            // Parameters
            PositiveDeltaColor = Brushes.DodgerBlue;
            NegativeDeltaColor = Brushes.DeepPink;
            EnableTextLabels = true;
            RthVolThreshold = 250;
            PriceLevelsPerBar = 6;
            BubbleOpacity = 35;
            MaxBubbleSize = 35;
        }
        else if (State == State.Configure)
        {
            // This indicator requires volumetric data
            AddVolumetric(null, BarsPeriodType.Minute, 5, VolumetricDeltaType.BidAsk, 1);
        }
        else if (State == State.DataLoaded)
        {
            maxDelta = 0;
            barDeltaData.Clear();
        }
    }

    protected override void OnBarUpdate()
    {
        if (CurrentBar < 1 || BarsInProgress != 1)
            return;

        // Check if we have volumetric data
        if (!(Bars.BarsSeries.BarsType is VolumetricBarsType))
        {
            Draw.TextFixed(this, "VolumetricWarning", "This indicator requires volumetric bars (Volumetric or Order Flow+)", TextPosition.TopLeft);
            return;
        }

        ProcessBarDelta(CurrentBar);
    }

    private void ProcessBarDelta(int barIndex)
    {
        
        // Get volumetric data for the bar
        VolumetricBarsType volumetricBars = Bars.BarsSeries.BarsType as VolumetricBarsType;

        Dictionary<double, PriceLevelData> combinedProfiles = new Dictionary<double, PriceLevelData>();

        double modifiedSize = TickSize * PriceLevelsPerBar;
        double barInit = 0;
        double last = 0;
        bool firstLevel = true;

        VolumetricData volumetricData = volumetricBars.Volumes[CurrentBar];

        List<double> prices = new List<double>();
        for (double price = volumetricData.Low; price <= volumetricData.High; price += Instrument.MasterInstrument.TickSize)
        {
            long bidVol = volumetricData.GetBidVolumeForPrice(price);
            long askVol = volumetricData.GetAskVolumeForPrice(price);
            long totalVol = bidVol + askVol;

            if (firstLevel)
            {
                barInit = price;
                firstLevel = false;
            }

            if (last != 0 && last >= barInit + modifiedSize)
            {
                barInit = price;
            }

            if (last == 0 || last <= barInit + modifiedSize)
            {
                string key = Math.Round(barInit, 7).ToString();

                if (!combinedProfiles.ContainsKey(barInit))
                {
                    combinedProfiles.Add(barInit,new PriceLevelData
                    {
                        Price = barInit,
                        BidVolume = 0,
                        AskVolume = 0,
                        TotalVolume = 0
                    });
                }

                combinedProfiles[barInit].BidVolume += bidVol;
                combinedProfiles[barInit].AskVolume += askVol;
                combinedProfiles[barInit].TotalVolume += totalVol;
            }

            last = price;
        }

        // Calculate max delta for scaling
        if (combinedProfiles.Count > 0)
        {
            try
            {
                double biggestDelta = combinedProfiles.Values
                    .Select(p => Math.Abs(p.Delta))
                    .Where(d => !double.IsNaN(d) && !double.IsInfinity(d))
                    .DefaultIfEmpty(0)
                    .Max();

                maxDelta = Math.Max(maxDelta, biggestDelta);
            }
            catch
            {
                // If max calculation fails, continue without updating maxDelta
            }
        }

        // Store the data for rendering
        barDeltaData.Add(barIndex,combinedProfiles.Values.ToList());
    }

    protected override void OnRender(ChartControl chartControl, ChartScale chartScale)
    {
        base.OnRender(chartControl, chartScale);

        if (Bars == null || chartControl == null)
            return;

        // Render delta bubbles for visible bars
        int firstBarIndex = Math.Max(ChartBars.FromIndex, 0);
        int lastBarIndex = Math.Min(ChartBars.ToIndex, CurrentBar);

        for (int barIndex = firstBarIndex; barIndex <= lastBarIndex; barIndex++)
        {
            DateTime barTime = Bars.GetTime(barIndex);

            int volBarIndex = BarsArray[1].GetBar(barTime);

            if (volBarIndex <0 || !barDeltaData.ContainsKey(volBarIndex))
                continue;

            var priceLevels = barDeltaData[volBarIndex];
            double modifiedSize = TickSize * PriceLevelsPerBar;

            foreach (var level in priceLevels)
            {
                double delta = level.Delta;

                if (Math.Abs(delta) <= RthVolThreshold)
                    continue;

                // Calculate bubble radius with scaling
                double minR = 8;
                double maxR = MaxBubbleSize;
                double minI = RthVolThreshold;
                double maxI = maxDelta + (maxDelta / 2.15);

                double radiusScale = Math.Min(
                    ((Math.Abs(delta) - minI) / (maxI - minI)) * (maxR - minR) + minR,
                    maxR
                );

                if (radiusScale < minR)
                    radiusScale = minR;

                // Calculate screen coordinates
                double priceY = level.Price + (modifiedSize / 2);
                int x = chartControl.GetXByBarIndex(ChartBars, barIndex);
                int y = chartScale.GetYByValue(priceY);

                // Draw bubble
                SharpDX.Direct2D1.Brush brush = delta > 0
                    ? PositiveDeltaColor.ToDxBrush(RenderTarget)
                    : NegativeDeltaColor.ToDxBrush(RenderTarget);

                brush.Opacity = BubbleOpacity / 100f;

                RenderTarget.FillEllipse(
                    new SharpDX.Direct2D1.Ellipse(new SharpDX.Vector2(x, y), (float)radiusScale, (float)radiusScale),
                    brush
                );

                // Draw text label if enabled
                if (EnableTextLabels)
                {
                    string text = delta.ToString("F0");

                    var textFormat = new SharpDX.DirectWrite.TextFormat(
                        Core.Globals.DirectWriteFactory,
                        "Arial",
                        SharpDX.DirectWrite.FontWeight.Normal,
                        SharpDX.DirectWrite.FontStyle.Normal,
                        9f
                    )
                    {
                        TextAlignment = SharpDX.DirectWrite.TextAlignment.Center,
                        ParagraphAlignment = SharpDX.DirectWrite.ParagraphAlignment.Center
                    };

                    var textBrush = Brushes.White.ToDxBrush(RenderTarget);

                    RenderTarget.DrawText(
                        text,
                        textFormat,
                        new SharpDX.RectangleF(x - 30, y - 10, 60, 20),
                        textBrush
                    );

                    textFormat.Dispose();
                    textBrush.Dispose();
                }

                brush.Dispose();
            }
        }
    }

    #region Properties

    [NinjaScriptProperty]
    [XmlIgnore]
    [Display(Name = "Positive Delta Color", Order = 1, GroupName = "Colors")]
    public System.Windows.Media.Brush PositiveDeltaColor { get; set; }

    [Browsable(false)]
    public string PositiveDeltaColorSerializable
    {
        get { return Serialize.BrushToString(PositiveDeltaColor); }
        set { PositiveDeltaColor = Serialize.StringToBrush(value); }
    }

    [NinjaScriptProperty]
    [XmlIgnore]
    [Display(Name = "Negative Delta Color", Order = 2, GroupName = "Colors")]
    public System.Windows.Media.Brush NegativeDeltaColor { get; set; }

    [Browsable(false)]
    public string NegativeDeltaColorSerializable
    {
        get { return Serialize.BrushToString(NegativeDeltaColor); }
        set { NegativeDeltaColor = Serialize.StringToBrush(value); }
    }

    [NinjaScriptProperty]
    [Display(Name = "Enable Text Labels", Order = 1, GroupName = "Display")]
    public bool EnableTextLabels { get; set; }

    [NinjaScriptProperty]
    [Range(1, int.MaxValue)]
    [Display(Name = "Volume Threshold", Order = 2, GroupName = "Display")]
    public int RthVolThreshold { get; set; }

    [NinjaScriptProperty]
    [Range(1, 100)]
    [Display(Name = "Price Levels Per Bar", Order = 3, GroupName = "Display")]
    public int PriceLevelsPerBar { get; set; }

    [NinjaScriptProperty]
    [Range(0, 100)]
    [Display(Name = "Bubble Opacity (%)", Order = 4, GroupName = "Display")]
    public int BubbleOpacity { get; set; }

    [NinjaScriptProperty]
    [Range(5, 100)]
    [Display(Name = "Max Bubble Size", Order = 5, GroupName = "Display")]
    public int MaxBubbleSize { get; set; }

    #endregion
}

// Helper class to store price level data
public class PriceLevelData
{
    public double Price { get; set; }
    public long BidVolume { get; set; }
    public long AskVolume { get; set; }
    public long TotalVolume { get; set; }
    public double Delta => -(BidVolume - AskVolume); // Negative of (bid - ask) to match Tradovate logic
}
1 Like

That’s a cool looking indicator Walee! Thanks for sharing and making it available to everyone. I’m going to have to check it out asap.

1 Like

Thanks you guys. much appreciated. I will definitely give both options a try. Thanks for taking the time to look at the code and provide some feedback/insight!

Were you ale to get it working on NT8? i tried with the copied scripts above but was always getting errors

I’m not sure about the Sethmo one. I have used Sethmo indicators on Tradovate but never on NT8. However if it’s just this indicator you are looking for, there is an indicator called “Large Trades Detector” on NT8 that comes with the Orderflow + tools. I use it, alongside my Bookmap and it’s pretty accurate. SO you get the same bubbles appear when a trade triggers your thresholds.

If you’re trading through a prop firm, you get the Orderflow+ tools for free. You just need to login to NT with the pro firm account.

Hope that helps. Sorry I can’t help with the code!

Sorry, just saw this. But no, I wasn’t able to get it to work. I tried to have chatgpt, grok and claude look at it to see and it only created more errors. So I unfortunately gave up. I just ended up using motivewave on a separate screen.

Same i tried many different things with Claude and wasn’t able to get it working properly. Ended up buying the TradeDevil Big Trade indicator for NT8 which i love