#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