DeltaMomentumConviction (DMC) (Indicator)

DeltaMomentumConviction (DMC)

A multi-lookback momentum framework designed to filter noise by requiring confluence between delta movement and raw price action.

Core Logic

  • Confluence Requirement: Each lookback row evaluates both delta direction and price direction. Confluence (agreement) is required for inclusion; divergence results in a neutral state.

  • Conviction Scoring: Consensus breadth cascades into a continuous conviction score. Low confluence reduces score intensity, effectively dimming the visual and analytical output.

  • Kinetic Filter: Entry signals require a “price-move” confirmation. If delta shifts without a corresponding price move (e.g., lack of dip/rise during a potential reversal), the signal is discarded as noise.

Visual Hierarchy

Layer Description
Background Color represents trend direction; opacity scales with conviction magnitude.
MasterTrend Block plot representing unanimous breadth; opacity fades as conviction wanes.
Row_1..N Block plots: Green/Red (Confluent); Gray (Divergent).
ConvictionLine Continuous line; brightness denotes confluence ratio.
Entry Dots Plotted on conviction zero-crossings + kinetic confirmation.

Entry Signal Logic

  • Long: Conviction crosses from negative to positive AND MasterTrend is UP AND price dipped below the drawdown start point.

  • Short: Conviction crosses from positive to negative AND MasterTrend is DOWN AND price rose above the drawdown start point.

Data Output

  • TrendValue / ConvictionScore

  • BreadthScore / DepthScore

  • DeltaMomo / EntrySignal

Self-calibrating: The system utilizes relative movement thresholds rather than fixed knobs.

#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Cbi;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.SuperDom;
using NinjaTrader.Gui.Tools;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.Core.FloatingPoint;
using NinjaTrader.NinjaScript.DrawingTools;
#endregion

//  DeltaMomentumConviction
//
//  Multi-lookback delta momentum consensus with magnitude weighting.
//
//  Combines delta momentum runs with multi-lookback consensus detection,
//  adds continuous conviction scoring and pullback-recovery entry signals.
//
//  Visual layers (bottom to top):
//    ConvictionLine — continuous line, brightness = confluence ratio, color = direction
//    Zero line      — dashed reference at conviction = 0
//    Entry dots     — appear on conviction zero-cross with kinetic price confirmation
//    Row_1..Row_N   — Block plots: green/red when delta+price confluent, gray when divergent
//    MasterTrend    — Block plot, flips on unanimous confluent breadth, opacity fades with |conviction|
//    Background     — color from trend direction, opacity from conviction magnitude
//
//  Delta-price confluence model:
//    Each row checks both delta momentum direction AND price direction for that lookback.
//    Only rows where both agree (confluent) contribute to directional consensus.
//    Divergent rows (delta and price disagree) render neutral gray and count as nothing.
//    This cascades: breadth drops → conviction drops → master dims → background fades.
//
//  Entry signal logic (self-calibrating, no threshold knobs):
//    Long:  conviction crosses from negative to positive while master trend = UP
//    Short: conviction crosses from positive to negative while master trend = DOWN
//    Kinetic filter: price must have moved counter-trend during the drawdown phase
//    (on renko, close must have dipped below drawdown start price for longs, or
//     risen above it for shorts — if price didn’t move, it’s delta noise, not a pullback)
//
//  Transparent output series for Data Box / Market Analyzer:
//    TrendValue, ConvictionScore, BreadthScore, DepthScore, DeltaMomo, EntrySignal

namespace NinjaTrader.NinjaScript.Indicators
{
[Gui.CategoryOrder(“Parameters”, 1)]
[Gui.CategoryOrder(“Color”, 2)]
[Gui.CategoryOrder(“Opacity”, 3)]

public class DeltaMomentumConviction : Indicator
{
	private OrderFlowCumulativeDelta		cumulativeDelta;
	private bool							trendUp = true;

	// Entry signal drawdown tracking with kinetic price filter
	private bool							_inDrawdown = false;
	private double							_drawdownRefPrice = 0;
	private double							_drawdownExtreme  = 0;

	// Pre-built brush arrays at discrete opacity steps (avoids per-bar allocation)
	private const int						OpacitySteps = 20;
	private Brush[]							bgUpBrushes;
	private Brush[]							bgDownBrushes;

	// Intensity-scaled brushes for heat map rows and master trend
	private const int						IntensitySteps = 10;
	private Brush[]							upIntensityBrushes;
	private Brush[]							downIntensityBrushes;
	private Brush[]							masterUpBrushes;
	private Brush[]							masterDownBrushes;
	private Brush							divergentBrush;

	// Fixed plot indices
	private const int						IdxMasterTrend	= 0;
	private const int						IdxTrendValue	= 1;
	private const int						IdxConviction	= 2;
	private const int						IdxBreadth		= 3;
	private const int						IdxDepth		= 4;
	private const int						IdxDeltaMomo	= 5;
	private const int						IdxConvLine		= 6;
	private const int						IdxEntrySignal	= 7;
	private const int						IdxRowStart		= 8;

	// Conviction line Y mapping: conviction [-1, +1] → Y [ConvCenter ± ConvScale]
	private const double					ConvCenter = 1.5;
	private const double					ConvScale  = 1.2;

	// Row Y offset — rows start above the conviction zone with a gap
	private const double					RowOffset  = 5.0;

	protected override void OnStateChange()
	{
		if (State == State.SetDefaults)
		{
			Description						= @"Multi-lookback delta momentum consensus with magnitude weighting and entry signals.";
			Name							= "DeltaMomentumConviction";
			Calculate						= Calculate.OnEachTick;
			IsOverlay						= false;
			DisplayInDataBox				= true;
			DrawOnPricePanel				= false;
			DrawHorizontalGridLines			= false;
			DrawVerticalGridLines			= false;
			PaintPriceMarkers				= false;
			ScaleJustification				= NinjaTrader.Gui.Chart.ScaleJustification.Right;
			IsSuspendedWhileInactive		= true;
			MaximumBarsLookBack				= MaximumBarsLookBack.Infinite;
			ShowTransparentPlotsInDataBox	= true;

			// Parameters
			Lookback						= 10;
			NormPeriod						= 200;
			DeltaType						= CumulativeDeltaType.BidAsk;
			ShowTrendMap					= true;

			// Color
			UpColor							= Brushes.Lime;
			DownColor						= Brushes.Crimson;
			BgUpColor						= Brushes.DarkGreen;
			BgDownColor						= Brushes.DarkRed;
			EnableBG						= true;
			EnableBGAll						= true;

			// Opacity
			BgMinOpacity					= 5;
			BgMaxOpacity					= 40;

			// Fixed plots (indices 0-7)
			AddPlot(new Stroke(Brushes.White, 2), PlotStyle.Block, "MasterTrend");
			AddPlot(Brushes.Transparent, "TrendValue");
			AddPlot(Brushes.Transparent, "ConvictionScore");
			AddPlot(Brushes.Transparent, "BreadthScore");
			AddPlot(Brushes.Transparent, "DepthScore");
			AddPlot(Brushes.Transparent, "DeltaMomo");
			AddPlot(new Stroke(Brushes.DodgerBlue, 2), PlotStyle.Line, "ConvictionLine");
			AddPlot(new Stroke(Brushes.Yellow, 4), PlotStyle.Dot, "EntrySignal");

			// Zero reference line for conviction
			AddLine(new Stroke(Brushes.DimGray, DashStyleHelper.Dash, 0.5f), ConvCenter, "Zero");
		}
		else if (State == State.Configure)
		{
			// Tick series required for OrderFlowCumulativeDelta
			AddDataSeries(BarsPeriodType.Tick, 1);

			// Dynamic row plots (indices 8..7+Lookback)
			for (int i = 0; i < Lookback; i++)
			{
				AddPlot(Brushes.White, "Row_" + (i + 1));
				Plots[IdxRowStart + i].PlotStyle = PlotStyle.Block;
				Plots[IdxRowStart + i].Width = 2;
			}

			// Freeze plot brushes for workspace restore safety
			if (UpColor != null)   { Brush b = UpColor.Clone();   b.Freeze(); UpColor = b; }
			if (DownColor != null)  { Brush b = DownColor.Clone(); b.Freeze(); DownColor = b; }

			// Pre-build intensity brushes for heat map rows (opacity 0.15-1.0)
			upIntensityBrushes   = new Brush[IntensitySteps + 1];
			downIntensityBrushes = new Brush[IntensitySteps + 1];
			masterUpBrushes      = new Brush[IntensitySteps + 1];
			masterDownBrushes    = new Brush[IntensitySteps + 1];

			for (int s = 0; s <= IntensitySteps; s++)
			{
				double frac = (double)s / IntensitySteps;

				double plotOp = 0.15 + frac * 0.85;
				if (UpColor != null)
				{ Brush b = UpColor.Clone(); b.Opacity = plotOp; b.Freeze(); upIntensityBrushes[s] = b; }
				if (DownColor != null)
				{ Brush b = DownColor.Clone(); b.Opacity = plotOp; b.Freeze(); downIntensityBrushes[s] = b; }

				double masterOp = 0.20 + frac * 0.80;
				if (UpColor != null)
				{ Brush b = UpColor.Clone(); b.Opacity = masterOp; b.Freeze(); masterUpBrushes[s] = b; }
				if (DownColor != null)
				{ Brush b = DownColor.Clone(); b.Opacity = masterOp; b.Freeze(); masterDownBrushes[s] = b; }
			}

			// Divergent cell brush — neutral gray for rows without delta-price confluence
			divergentBrush = Brushes.DimGray.Clone();
			divergentBrush.Opacity = 0.3;
			divergentBrush.Freeze();

			// Pre-build background brushes at discrete opacity steps
			bgUpBrushes   = new Brush[OpacitySteps + 1];
			bgDownBrushes = new Brush[OpacitySteps + 1];

			double minOp = BgMinOpacity / 100.0;
			double maxOp = BgMaxOpacity / 100.0;

			for (int s = 0; s <= OpacitySteps; s++)
			{
				double frac    = (double)s / OpacitySteps;
				double opacity = minOp + frac * (maxOp - minOp);

				if (BgUpColor != null)
				{
					Brush b = BgUpColor.Clone();
					b.Opacity = opacity;
					b.Freeze();
					bgUpBrushes[s] = b;
				}

				if (BgDownColor != null)
				{
					Brush b = BgDownColor.Clone();
					b.Opacity = opacity;
					b.Freeze();
					bgDownBrushes[s] = b;
				}
			}
		}
		else if (State == State.DataLoaded)
		{
			cumulativeDelta = OrderFlowCumulativeDelta(DeltaType, CumulativeDeltaPeriod.Bar, 0);
		}
	}

	protected override void OnBarUpdate()
	{
		// All computation on tick series — primary series ignored
		if (BarsInProgress != 1)
			return;

		// Sync hosted cumulative delta
		cumulativeDelta.Update(cumulativeDelta.BarsArray[1].Count - 1, 1);

		// Need at least 1 prior bar for DeltaMomo[1] access
		if (CurrentBars[0] < 1)
			return;

		// ----------------------------------------------------------------
		// Delta momentum accumulation
		// ----------------------------------------------------------------
		double barDelta = cumulativeDelta.DeltaClose[0];
		double prevMomo = Values[IdxDeltaMomo][1];

		double momo;
		if (barDelta > 0)
			momo = (prevMomo > 0) ? prevMomo + barDelta : barDelta;
		else if (barDelta < 0)
			momo = (prevMomo < 0) ? prevMomo + barDelta : barDelta;
		else
			momo = prevMomo;  // zero delta — no new info, carry forward

		Values[IdxDeltaMomo][0] = momo;

		// Need Lookback bars of DeltaMomo history for trend map comparison
		if (CurrentBars[0] < Lookback + 1)
			return;

		// ----------------------------------------------------------------
		// Rolling max for self-calibrating normalization
		// (computed first — row intensity and depth both need it)
		// ----------------------------------------------------------------
		double absMomo      = Math.Abs(momo);
		int    lookbackBars = Math.Min(NormPeriod, CurrentBars[0]);
		double rollingMax   = 0;

		for (int j = 0; j < lookbackBars; j++)
		{
			double val = Math.Abs(Values[IdxDeltaMomo][j]);
			if (val > rollingMax)
				rollingMax = val;
		}

		// ----------------------------------------------------------------
		// Multi-lookback comparison with delta-price confluence
		// Only rows where BOTH delta momentum and price agree count
		// toward the directional consensus. Divergent rows render gray.
		// ----------------------------------------------------------------
		int upCount   = 0;
		int downCount = 0;

		for (int k = 0; k < Lookback; k++)
		{
			int barsBack = k + 1;

			double deltaDiff = momo - Values[IdxDeltaMomo][barsBack];
			double priceDiff = Closes[0][0] - Closes[0][barsBack];

			// Confluence: both delta and price move in the same direction
			bool confluent = (deltaDiff > 0 && priceDiff > 0) || (deltaDiff < 0 && priceDiff < 0);

			if (confluent)
			{
				if (deltaDiff > 0) upCount++;
				else               downCount++;
			}

			// Row rendering
			int plotIdx = IdxRowStart + k;
			if (ShowTrendMap)
			{
				Values[plotIdx][0] = RowOffset + Lookback - k;

				if (confluent)
				{
					double intensity = (rollingMax > 0) ? Math.Min(Math.Abs(deltaDiff) / (2.0 * rollingMax), 1.0) : 0.5;
					int    iStep     = (int)Math.Round(intensity * IntensitySteps);
					iStep = Math.Max(0, Math.Min(iStep, IntensitySteps));

					PlotBrushes[plotIdx][0] = deltaDiff > 0
						? upIntensityBrushes[iStep]
						: downIntensityBrushes[iStep];
				}
				else
				{
					PlotBrushes[plotIdx][0] = divergentBrush;
				}
			}
			else
			{
				Values[plotIdx][0] = double.NaN;
			}
		}

		// ----------------------------------------------------------------
		// Breadth Score — consensus fraction [-1, +1]
		// ----------------------------------------------------------------
		double breadth = (double)(upCount - downCount) / Lookback;
		Values[IdxBreadth][0] = breadth;

		// ----------------------------------------------------------------
		// Depth Score [0, 1] — uses rollingMax already computed above
		// ----------------------------------------------------------------
		double depth = (rollingMax > 0) ? Math.Min(absMomo / rollingMax, 1.0) : 0;
		Values[IdxDepth][0] = depth;

		// ----------------------------------------------------------------
		// Conviction Score + Master Trend
		// ----------------------------------------------------------------
		double conviction = breadth * depth;
		Values[IdxConviction][0] = conviction;

		// Master trend latch — flip only on unanimity
		bool prevTrendUp = trendUp;
		if (upCount == Lookback && !trendUp)
			trendUp = true;
		else if (downCount == Lookback && trendUp)
			trendUp = false;

		// Reset drawdown tracking on trend flip
		if (trendUp != prevTrendUp)
			_inDrawdown = false;

		// Master trend visual — opacity from |conviction|
		double absConv = Math.Abs(conviction);
		if (ShowTrendMap)
		{
			Values[IdxMasterTrend][0] = RowOffset + Lookback + 2;
			int masterStep = (int)Math.Round(absConv * IntensitySteps);
			masterStep = Math.Max(0, Math.Min(masterStep, IntensitySteps));
			PlotBrushes[IdxMasterTrend][0] = trendUp ? masterUpBrushes[masterStep] : masterDownBrushes[masterStep];
		}
		else
		{
			Values[IdxMasterTrend][0] = double.NaN;
		}

		Values[IdxTrendValue][0] = trendUp ? 1 : -1;

		// ----------------------------------------------------------------
		// Conviction line — sqrt expansion spreads small values away from center,
		// brightness from |conviction| gives equal gradient on both sides
		// ----------------------------------------------------------------
		double expandedConv = Math.Sign(conviction) * Math.Sqrt(Math.Abs(conviction));
		Values[IdxConvLine][0] = ConvCenter + expandedConv * ConvScale;
		int    convLineStep    = (int)Math.Round(Math.Abs(conviction) * IntensitySteps);
		convLineStep = Math.Max(0, Math.Min(convLineStep, IntensitySteps));
		PlotBrushes[IdxConvLine][0] = conviction >= 0
			? upIntensityBrushes[convLineStep]
			: downIntensityBrushes[convLineStep];

		// ----------------------------------------------------------------
		// Entry signal — conviction zero-cross with kinetic price filter
		// Requires price to have actually moved counter-trend during drawdown
		// ----------------------------------------------------------------
		double prevConv = Values[IdxConviction][1];
		bool   entrySignal = false;

		if (trendUp)
		{
			// Enter drawdown phase when conviction goes negative
			if (prevConv < 0 && !_inDrawdown)
			{
				_inDrawdown = true;
				_drawdownRefPrice = Closes[0][1];
				_drawdownExtreme  = Closes[0][0];
			}
			else if (_inDrawdown && prevConv < 0)
			{
				_drawdownExtreme = Math.Min(_drawdownExtreme, Closes[0][0]);
			}

			// Recovery: conviction crosses positive after kinetically confirmed pullback
			bool kineticPullback = _inDrawdown && (_drawdownRefPrice - _drawdownExtreme > 0);
			if (kineticPullback && conviction > 0 && prevConv <= 0)
			{
				entrySignal = true;
				_inDrawdown = false;
			}
		}
		else
		{
			// Enter drawdown phase when conviction goes positive (short trend)
			if (prevConv > 0 && !_inDrawdown)
			{
				_inDrawdown = true;
				_drawdownRefPrice = Closes[0][1];
				_drawdownExtreme  = Closes[0][0];
			}
			else if (_inDrawdown && prevConv > 0)
			{
				_drawdownExtreme = Math.Max(_drawdownExtreme, Closes[0][0]);
			}

			// Recovery: conviction crosses negative after kinetically confirmed rally
			bool kineticRally = _inDrawdown && (_drawdownExtreme - _drawdownRefPrice > 0);
			if (kineticRally && conviction < 0 && prevConv >= 0)
			{
				entrySignal = true;
				_inDrawdown = false;
			}
		}

		if (entrySignal)
		{
			Values[IdxEntrySignal][0] = ConvCenter;
			PlotBrushes[IdxEntrySignal][0] = trendUp
				? upIntensityBrushes[IntensitySteps]
				: downIntensityBrushes[IntensitySteps];
		}
		else
		{
			Values[IdxEntrySignal][0] = double.NaN;
		}

		// ----------------------------------------------------------------
		// Dynamic background opacity from conviction
		// ----------------------------------------------------------------
		if (EnableBG)
		{
			int bgStep = (int)Math.Round(absConv * OpacitySteps);
			bgStep = Math.Max(0, Math.Min(bgStep, OpacitySteps));

			Brush bg = trendUp ? bgUpBrushes[bgStep] : bgDownBrushes[bgStep];

			if (EnableBGAll)
				BackBrushAll = bg;
			else
				BackBrush = bg;
		}
	}

	/// <summary>
	/// When ShowTrendMap = true: full range for rows + master + conviction zone.
	/// When ShowTrendMap = false: tight range around conviction zone — fills the panel.
	/// </summary>
	public override void OnCalculateMinMax()
	{
		MinValue = -0.5;
		MaxValue = ShowTrendMap ? RowOffset + Lookback + 4 : 3.5;
	}

	#region Properties

	// --- Programmatic access ---
	[Browsable(false)] [XmlIgnore] public Series<double> MasterTrend     { get { return Values[IdxMasterTrend]; } }
	[Browsable(false)] [XmlIgnore] public Series<double> TrendValue      { get { return Values[IdxTrendValue]; } }
	[Browsable(false)] [XmlIgnore] public Series<double> ConvictionScore { get { return Values[IdxConviction]; } }
	[Browsable(false)] [XmlIgnore] public Series<double> BreadthScore    { get { return Values[IdxBreadth]; } }
	[Browsable(false)] [XmlIgnore] public Series<double> DepthScore      { get { return Values[IdxDepth]; } }
	[Browsable(false)] [XmlIgnore] public Series<double> DeltaMomo       { get { return Values[IdxDeltaMomo]; } }
	[Browsable(false)] [XmlIgnore] public Series<double> ConvictionLine  { get { return Values[IdxConvLine]; } }
	[Browsable(false)] [XmlIgnore] public Series<double> EntrySignal     { get { return Values[IdxEntrySignal]; } }

	// --- Parameters ---

	[NinjaScriptProperty]
	[Range(1, 20)]
	[Display(Name = "Lookback [1-20]", Description = "Number of bars back to compare delta momentum", Order = 1, GroupName = "Parameters")]
	public int Lookback
	{ get; set; }

	[NinjaScriptProperty]
	[Range(50, 500)]
	[Display(Name = "Norm Period [50-500]", Description = "Rolling window for self-calibrating depth normalization", Order = 2, GroupName = "Parameters")]
	public int NormPeriod
	{ get; set; }

	[Display(Name = "Delta Type", Description = "BidAsk or UpDownTick", Order = 3, GroupName = "Parameters")]
	public CumulativeDeltaType DeltaType
	{ get; set; }

	[Display(Name = "Show Trend Map", Description = "When off, isolates conviction line with entry dots", Order = 4, GroupName = "Parameters")]
	public bool ShowTrendMap
	{ get; set; }

	// --- Color ---

	[XmlIgnore]
	[Display(Name = "Up Color", Order = 1, GroupName = "Color")]
	public Brush UpColor
	{ get; set; }
	[Browsable(false)]
	public string UpColorSerializable
	{
		get { return Serialize.BrushToString(UpColor); }
		set { UpColor = Serialize.StringToBrush(value); }
	}

	[XmlIgnore]
	[Display(Name = "Down Color", Order = 2, GroupName = "Color")]
	public Brush DownColor
	{ get; set; }
	[Browsable(false)]
	public string DownColorSerializable
	{
		get { return Serialize.BrushToString(DownColor); }
		set { DownColor = Serialize.StringToBrush(value); }
	}

	[Display(Name = "Enable Background", Order = 3, GroupName = "Color")]
	public bool EnableBG
	{ get; set; }

	[Display(Name = "Color Full Chart", Order = 4, GroupName = "Color")]
	public bool EnableBGAll
	{ get; set; }

	[XmlIgnore]
	[Display(Name = "Background Up", Order = 5, GroupName = "Color")]
	public Brush BgUpColor
	{ get; set; }
	[Browsable(false)]
	public string BgUpColorSerializable
	{
		get { return Serialize.BrushToString(BgUpColor); }
		set { BgUpColor = Serialize.StringToBrush(value); }
	}

	[XmlIgnore]
	[Display(Name = "Background Down", Order = 6, GroupName = "Color")]
	public Brush BgDownColor
	{ get; set; }
	[Browsable(false)]
	public string BgDownColorSerializable
	{
		get { return Serialize.BrushToString(BgDownColor); }
		set { BgDownColor = Serialize.StringToBrush(value); }
	}

	// --- Opacity ---

	[Range(0, 100)]
	[Display(Name = "Background Min Opacity", Description = "Opacity at zero conviction", Order = 1, GroupName = "Opacity")]
	public int BgMinOpacity
	{ get; set; }

	[Range(0, 100)]
	[Display(Name = "Background Max Opacity", Description = "Opacity at full conviction", Order = 2, GroupName = "Opacity")]
	public int BgMaxOpacity
	{ get; set; }

	#endregion
}

}

GIving back with some tools I’ve built.

You can answer any question you have about this and how it works by using AI, just pour the code into an AI took and interrogate the code.

1 Like

Thanks! I had to remove above to compile. Btw, this indicator requires order flow subscription.

1 Like

Yes, ninatrader offers order flow subscription free now, and serious traders already bought it. It’d be hard to trade futures without orderflow.

1 Like

I converted it to delta derived from tick data series. Very nice indicator, solid logic behind it.

2 Likes

Care to share the code :grinning_face:

No guarantees that results are as good as the original. Maybe someone with the subscription can compare the two. It’s a hybrid, using delta engine for historical and OnMarketData for realtime.

Summary
// Based on the original DMC by @Maverick
#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Cbi;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.SuperDom;
using NinjaTrader.Gui.Tools;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.Core.FloatingPoint;
using NinjaTrader.NinjaScript.DrawingTools;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
    public class CheapDeltaMomentumConviction : Indicator
    {
        // --- Lock-free Delta Engine State ---
        private int							lastPrimaryBarIndex = -1;
        private double							currentBarDelta = 0;

        // True once we hit State.Realtime — switches delta source from the
        // tick-series/GetAsk-GetBid reconstruction to live OnMarketData prints.
        private bool							useMarketData = false;

        private bool							trendUp = true;

        // Entry signal drawdown tracking with kinetic price filter
        private bool							_inDrawdown = false;
        private double							_drawdownRefPrice = 0;
        private double							_drawdownExtreme  = 0;

        // Pre-built brush arrays at discrete opacity steps (avoids per-bar allocation)
        private const int						OpacitySteps = 20;
        private Brush[]							bgUpBrushes;
        private Brush[]							bgDownBrushes;

        // Intensity-scaled brushes for heat map rows and master trend
        private const int						IntensitySteps = 10;
        private Brush[]							upIntensityBrushes;
        private Brush[]							downIntensityBrushes;
        private Brush[]							masterUpBrushes;
        private Brush[]							masterDownBrushes;
        private Brush							divergentBrush;

        // Fixed plot indices
        private const int						IdxMasterTrend	= 0;
        private const int						IdxTrendValue	= 1;
        private const int						IdxConviction	= 2;
        private const int						IdxBreadth		= 3;
        private const int						IdxDepth		= 4;
        private const int						IdxDeltaMomo	= 5;
        private const int						IdxConvLine		= 6;
        private const int						IdxEntrySignal	= 7;
        private const int						IdxRowStart		= 8;

        // Conviction line Y mapping: conviction [-1, +1] → Y [ConvCenter ± ConvScale]
        private const double					ConvCenter = 1.5;
        private const double					ConvScale  = 1.2;

        // Row Y offset — rows start above the conviction zone with a gap
        private const double					RowOffset  = 5.0;

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Description						= @"Multi-lookback delta momentum consensus with magnitude weighting and entry signals. Uses a lock-free Bid/Ask delta engine (tick-series reconstruction historically, OnMarketData in realtime) instead of premium Order Flow Cumulative Delta.";
                Name							= "CheapDeltaMomentumConviction";
                Calculate						= Calculate.OnEachTick;
                IsOverlay						= false;
                DisplayInDataBox				= true;
                DrawOnPricePanel				= false;
                DrawHorizontalGridLines			= false;
                DrawVerticalGridLines			= false;
                PaintPriceMarkers				= false;
                ScaleJustification				= NinjaTrader.Gui.Chart.ScaleJustification.Right;
                IsSuspendedWhileInactive		= true;
                MaximumBarsLookBack				= MaximumBarsLookBack.Infinite;
                ShowTransparentPlotsInDataBox	= true;

                // Parameters
                Lookback						= 10;
                NormPeriod						= 200;
                ShowTrendMap					= true;

                // Color
                UpColor							= Brushes.Lime;
                DownColor						= Brushes.Crimson;
                BgUpColor						= Brushes.DarkGreen;
                BgDownColor						= Brushes.DarkRed;
                EnableBG						= true;
                EnableBGAll						= true;

                // Opacity
                BgMinOpacity					= 5;
                BgMaxOpacity					= 40;

                // Fixed plots (indices 0-7)
                AddPlot(new Stroke(Brushes.White, 2), PlotStyle.Block, "MasterTrend");
                AddPlot(Brushes.Transparent, "TrendValue");
                AddPlot(Brushes.Transparent, "ConvictionScore");
                AddPlot(Brushes.Transparent, "BreadthScore");
                AddPlot(Brushes.Transparent, "DepthScore");
                AddPlot(Brushes.Transparent, "DeltaMomo");
                AddPlot(new Stroke(Brushes.DodgerBlue, 2), PlotStyle.Line, "ConvictionLine");
                AddPlot(new Stroke(Brushes.Yellow, 4), PlotStyle.Dot, "EntrySignal");

                // Zero reference line for conviction
                AddLine(new Stroke(Brushes.DimGray, DashStyleHelper.Dash, 0.5f), ConvCenter, "Zero");
            }
            else if (State == State.Configure)
            {
                // Tick series still required for historical delta reconstruction
                AddDataSeries(BarsPeriodType.Tick, 1);

                // Dynamic row plots (indices 8..7+Lookback)
                for (int i = 0; i < Lookback; i++)
                {
                    AddPlot(Brushes.White, "Row_" + (i + 1));
                    Plots[IdxRowStart + i].PlotStyle = PlotStyle.Block;
                    Plots[IdxRowStart + i].Width = 2;
                }

                // Freeze plot brushes for workspace restore safety
                if (UpColor != null)   { Brush b = UpColor.Clone();   b.Freeze(); UpColor = b; }
                if (DownColor != null)  { Brush b = DownColor.Clone(); b.Freeze(); DownColor = b; }

                // Pre-build intensity brushes for heat map rows (opacity 0.15-1.0)
                upIntensityBrushes   = new Brush[IntensitySteps + 1];
                downIntensityBrushes = new Brush[IntensitySteps + 1];
                masterUpBrushes      = new Brush[IntensitySteps + 1];
                masterDownBrushes    = new Brush[IntensitySteps + 1];

                for (int s = 0; s <= IntensitySteps; s++)
                {
                    double frac = (double)s / IntensitySteps;

                    double plotOp = 0.15 + frac * 0.85;
                    if (UpColor != null)
                    { Brush b = UpColor.Clone(); b.Opacity = plotOp; b.Freeze(); upIntensityBrushes[s] = b; }
                    if (DownColor != null)
                    { Brush b = DownColor.Clone(); b.Opacity = plotOp; b.Freeze(); downIntensityBrushes[s] = b; }

                    double masterOp = 0.20 + frac * 0.80;
                    if (UpColor != null)
                    { Brush b = UpColor.Clone(); b.Opacity = masterOp; b.Freeze(); masterUpBrushes[s] = b; }
                    if (DownColor != null)
                    { Brush b = DownColor.Clone(); b.Opacity = masterOp; b.Freeze(); masterDownBrushes[s] = b; }
                }

                // Divergent cell brush — neutral gray for rows without delta-price confluence
                divergentBrush = Brushes.DimGray.Clone();
                divergentBrush.Opacity = 0.3;
                divergentBrush.Freeze();

                // Pre-build background brushes at discrete opacity steps
                bgUpBrushes   = new Brush[OpacitySteps + 1];
                bgDownBrushes = new Brush[OpacitySteps + 1];

                double minOp = BgMinOpacity / 100.0;
                double maxOp = BgMaxOpacity / 100.0;

                for (int s = 0; s <= OpacitySteps; s++)
                {
                    double frac    = (double)s / OpacitySteps;
                    double opacity = minOp + frac * (maxOp - minOp);

                    if (BgUpColor != null)
                    {
                        Brush b = BgUpColor.Clone();
                        b.Opacity = opacity;
                        b.Freeze();
                        bgUpBrushes[s] = b;
                    }

                    if (BgDownColor != null)
                    {
                        Brush b = BgDownColor.Clone();
                        b.Opacity = opacity;
                        b.Freeze();
                        bgDownBrushes[s] = b;
                    }
                }
            }
            else if (State == State.Realtime)
            {
                // From here on, delta accrual switches to live OnMarketData prints
                // instead of the tick-series/GetAsk-GetBid reconstruction. Any
                // partially-built currentBarDelta from the last historical bar is
                // preserved — we don't reset here, only on the next new primary bar.
                useMarketData = true;
            }
        }

        /// <summary>
        /// Realtime-only delta accrual. Fires once per actual trade print once the
        /// indicator transitions to State.Realtime (and during tick replay). Uses the
        /// Ask/Bid that were live at the moment of the trade, which is more accurate
        /// than reconstructing bid/ask from the tick-series bar close.
        /// </summary>
        protected override void OnMarketData(NinjaTrader.Data.MarketDataEventArgs e)
        {
            if (!useMarketData)
                return;

            if (e.MarketDataType != MarketDataType.Last)
                return;

            if (CurrentBars[0] < 0)
                return;

            double price  = e.Price;
            long   volume = e.Volume;
            double ask    = e.Ask;
            double bid    = e.Bid;

            if (volume <= 0)
                return;

            long buyVol, sellVol;
            if (ask > 0 && bid > 0 && bid < ask)
            {
                if (price >= ask)
                {
                    buyVol  = volume;
                    sellVol = 0;
                }
                else if (price <= bid)
                {
                    buyVol  = 0;
                    sellVol = volume;
                }
                else
                {
                    // Midpoint print — classify proportionally by proximity to ask/bid
                    // instead of discarding the volume entirely.
                    buyVol  = (long)Math.Round(volume * (price - bid) / (ask - bid));
                    sellVol = volume - buyVol;
                }
            }
            else
            {
                buyVol  = volume / 2;
                sellVol = volume - buyVol;
            }

            currentBarDelta += (buyVol - sellVol);
        }

        protected override void OnBarUpdate()
        {
            // ----------------------------------------------------------------
            // Primary-series bar-boundary reset. Runs regardless of which delta
            // source is active, since both need currentBarDelta zeroed exactly
            // once per new primary bar.
            // ----------------------------------------------------------------
            if (BarsInProgress == 0)
            {
                int primaryBar = CurrentBars[0];
                if (primaryBar != lastPrimaryBarIndex)
                {
                    lastPrimaryBarIndex = primaryBar;
                    currentBarDelta = 0;
                }
                return;
            }

            // All remaining computation runs off the tick series — primary series
            // itself carries no logic beyond the reset above.
            if (BarsInProgress != 1)
                return;

            if (CurrentBars[0] < 0)
                return;

            // ----------------------------------------------------------------
            // Lock-free Delta Engine — historical path only.
            // In realtime, OnMarketData has already accrued currentBarDelta for
            // this bar, so skip the tick-series reconstruction to avoid double
            // counting.
            // ----------------------------------------------------------------
            if (!useMarketData)
            {
                double price = Close[0];
                long volume = (long)Volume[0];

                if (volume > 0)
                {
                    double ask = Bars.GetAsk(CurrentBar);
                    double bid = Bars.GetBid(CurrentBar);

                    long buyVol, sellVol;
                    if (ask > 0 && bid > 0 && bid < ask)
                    {
                        if (price >= ask)
                        {
                            buyVol  = volume;
                            sellVol = 0;
                        }
                        else if (price <= bid)
                        {
                            buyVol  = 0;
                            sellVol = volume;
                        }
                        else
                        {
                            buyVol  = (long)Math.Round(volume * (price - bid) / (ask - bid));
                            sellVol = volume - buyVol;
                        }
                    }
                    else
                    {
                        buyVol  = volume / 2;
                        sellVol = volume - buyVol;
                    }
                    currentBarDelta += (buyVol - sellVol);
                }
            }

            // Need at least 1 prior bar for DeltaMomo[1] access
            if (CurrentBars[0] < 1)
                return;

            // ----------------------------------------------------------------
            // Delta momentum accumulation
            // ----------------------------------------------------------------
            double barDelta = currentBarDelta;
            double prevMomo = Values[IdxDeltaMomo][1];

            double momo;
            if (barDelta > 0)
                momo = (prevMomo > 0) ? prevMomo + barDelta : barDelta;
            else if (barDelta < 0)
                momo = (prevMomo < 0) ? prevMomo + barDelta : barDelta;
            else
                momo = prevMomo;  // zero delta — no new info, carry forward

            Values[IdxDeltaMomo][0] = momo;

            // Need Lookback bars of DeltaMomo history for trend map comparison
            if (CurrentBars[0] < Lookback + 1)
                return;

            // ----------------------------------------------------------------
            // Rolling max for self-calibrating normalization
            // (computed first — row intensity and depth both need it)
            // ----------------------------------------------------------------
            double absMomo      = Math.Abs(momo);
            int    lookbackBars = Math.Min(NormPeriod, CurrentBars[0]);
            double rollingMax   = 0;

            for (int j = 0; j < lookbackBars; j++)
            {
                double val = Math.Abs(Values[IdxDeltaMomo][j]);
                if (val > rollingMax)
                    rollingMax = val;
            }

            // ----------------------------------------------------------------
            // Multi-lookback comparison with delta-price confluence
            // Only rows where BOTH delta momentum and price agree count
            // toward the directional consensus. Divergent rows render gray.
            // ----------------------------------------------------------------
            int upCount   = 0;
            int downCount = 0;

            for (int k = 0; k < Lookback; k++)
            {
                int barsBack = k + 1;

                double deltaDiff = momo - Values[IdxDeltaMomo][barsBack];
                double priceDiff = Closes[0][0] - Closes[0][barsBack];

                // Confluence: both delta and price move in the same direction
                bool confluent = (deltaDiff > 0 && priceDiff > 0) || (deltaDiff < 0 && priceDiff < 0);

                if (confluent)
                {
                    if (deltaDiff > 0) upCount++;
                    else               downCount++;
                }

                // Row rendering
                int plotIdx = IdxRowStart + k;
                if (ShowTrendMap)
                {
                    Values[plotIdx][0] = RowOffset + Lookback - k;

                    if (confluent)
                    {
                        double intensity = (rollingMax > 0) ? Math.Min(Math.Abs(deltaDiff) / (2.0 * rollingMax), 1.0) : 0.5;
                        int    iStep     = (int)Math.Round(intensity * IntensitySteps);
                        iStep = Math.Max(0, Math.Min(iStep, IntensitySteps));

                        PlotBrushes[plotIdx][0] = deltaDiff > 0
                            ? upIntensityBrushes[iStep]
                            : downIntensityBrushes[iStep];
                    }
                    else
                    {
                        PlotBrushes[plotIdx][0] = divergentBrush;
                    }
                }
                else
                {
                    Values[plotIdx][0] = double.NaN;
                }
            }

            // ----------------------------------------------------------------
            // Breadth Score — consensus fraction [-1, +1]
            // ----------------------------------------------------------------
            double breadth = (double)(upCount - downCount) / Lookback;
            Values[IdxBreadth][0] = breadth;

            // ----------------------------------------------------------------
            // Depth Score [0, 1] — uses rollingMax already computed above
            // ----------------------------------------------------------------
            double depth = (rollingMax > 0) ? Math.Min(absMomo / rollingMax, 1.0) : 0;
            Values[IdxDepth][0] = depth;

            // ----------------------------------------------------------------
            // Conviction Score + Master Trend
            // ----------------------------------------------------------------
            double conviction = breadth * depth;
            Values[IdxConviction][0] = conviction;

            // Master trend latch — flip only on unanimity
            bool prevTrendUp = trendUp;
            if (upCount == Lookback && !trendUp)
                trendUp = true;
            else if (downCount == Lookback && trendUp)
                trendUp = false;

            // Reset drawdown tracking on trend flip
            if (trendUp != prevTrendUp)
                _inDrawdown = false;

            // Master trend visual — opacity from |conviction|
            double absConv = Math.Abs(conviction);
            if (ShowTrendMap)
            {
                Values[IdxMasterTrend][0] = RowOffset + Lookback + 2;
                int masterStep = (int)Math.Round(absConv * IntensitySteps);
                masterStep = Math.Max(0, Math.Min(masterStep, IntensitySteps));
                PlotBrushes[IdxMasterTrend][0] = trendUp ? masterUpBrushes[masterStep] : masterDownBrushes[masterStep];
            }
            else
            {
                Values[IdxMasterTrend][0] = double.NaN;
            }

            Values[IdxTrendValue][0] = trendUp ? 1 : -1;

            // ----------------------------------------------------------------
            // Conviction line — sqrt expansion spreads small values away from center,
            // brightness from |conviction| gives equal gradient on both sides
            // ----------------------------------------------------------------
            double expandedConv = Math.Sign(conviction) * Math.Sqrt(Math.Abs(conviction));
            Values[IdxConvLine][0] = ConvCenter + expandedConv * ConvScale;
            int    convLineStep    = (int)Math.Round(Math.Abs(conviction) * IntensitySteps);
            convLineStep = Math.Max(0, Math.Min(convLineStep, IntensitySteps));
            PlotBrushes[IdxConvLine][0] = conviction >= 0
                ? upIntensityBrushes[convLineStep]
                : downIntensityBrushes[convLineStep];

            // ----------------------------------------------------------------
            // Entry signal — conviction zero-cross with kinetic price filter
            // Requires price to have actually moved counter-trend during drawdown
            // ----------------------------------------------------------------
            double prevConv = Values[IdxConviction][1];
            bool   entrySignal = false;

            if (trendUp)
            {
                // Enter drawdown phase when conviction goes negative
                if (prevConv < 0 && !_inDrawdown)
                {
                    _inDrawdown = true;
                    _drawdownRefPrice = Closes[0][1];
                    _drawdownExtreme  = Closes[0][0];
                }
                else if (_inDrawdown && prevConv < 0)
                {
                    _drawdownExtreme = Math.Min(_drawdownExtreme, Closes[0][0]);
                }

                // Recovery: conviction crosses positive after kinetically confirmed pullback
                bool kineticPullback = _inDrawdown && (_drawdownRefPrice - _drawdownExtreme > 0);
                if (kineticPullback && conviction > 0 && prevConv <= 0)
                {
                    entrySignal = true;
                    _inDrawdown = false;
                }
            }
            else
            {
                // Enter drawdown phase when conviction goes positive (short trend)
                if (prevConv > 0 && !_inDrawdown)
                {
                    _inDrawdown = true;
                    _drawdownRefPrice = Closes[0][1];
                    _drawdownExtreme  = Closes[0][0];
                }
                else if (_inDrawdown && prevConv > 0)
                {
                    _drawdownExtreme = Math.Max(_drawdownExtreme, Closes[0][0]);
                }

                // Recovery: conviction crosses negative after kinetically confirmed rally
                bool kineticRally = _inDrawdown && (_drawdownExtreme - _drawdownRefPrice > 0);
                if (kineticRally && conviction < 0 && prevConv >= 0)
                {
                    entrySignal = true;
                    _inDrawdown = false;
                }
            }

            if (entrySignal)
            {
                Values[IdxEntrySignal][0] = ConvCenter;
                PlotBrushes[IdxEntrySignal][0] = trendUp
                    ? upIntensityBrushes[IntensitySteps]
                    : downIntensityBrushes[IntensitySteps];
            }
            else
            {
                Values[IdxEntrySignal][0] = double.NaN;
            }

            // ----------------------------------------------------------------
            // Dynamic background opacity from conviction
            // ----------------------------------------------------------------
            if (EnableBG)
            {
                int bgStep = (int)Math.Round(absConv * OpacitySteps);
                bgStep = Math.Max(0, Math.Min(bgStep, OpacitySteps));

                Brush bg = trendUp ? bgUpBrushes[bgStep] : bgDownBrushes[bgStep];

                if (EnableBGAll)
                    BackBrushAll = bg;
                else
                    BackBrush = bg;
            }
        }

        /// <summary>
        /// When ShowTrendMap = true: full range for rows + master + conviction zone.
        /// When ShowTrendMap = false: tight range around conviction zone — fills the panel.
        /// </summary>
        public override void OnCalculateMinMax()
        {
            MinValue = -0.5;
            MaxValue = ShowTrendMap ? RowOffset + Lookback + 4 : 3.5;
        }

        #region Properties

        // --- Programmatic access ---
        [Browsable(false)] [XmlIgnore] public Series<double> MasterTrend     { get { return Values[IdxMasterTrend]; } }
        [Browsable(false)] [XmlIgnore] public Series<double> TrendValue      { get { return Values[IdxTrendValue]; } }
        [Browsable(false)] [XmlIgnore] public Series<double> ConvictionScore { get { return Values[IdxConviction]; } }
        [Browsable(false)] [XmlIgnore] public Series<double> BreadthScore    { get { return Values[IdxBreadth]; } }
        [Browsable(false)] [XmlIgnore] public Series<double> DepthScore      { get { return Values[IdxDepth]; } }
        [Browsable(false)] [XmlIgnore] public Series<double> DeltaMomo       { get { return Values[IdxDeltaMomo]; } }
        [Browsable(false)] [XmlIgnore] public Series<double> ConvictionLine  { get { return Values[IdxConvLine]; } }
        [Browsable(false)] [XmlIgnore] public Series<double> EntrySignal     { get { return Values[IdxEntrySignal]; } }

        // --- Parameters ---

        [NinjaScriptProperty]
        [Range(1, 20)]
        [Display(Name = "Lookback [1-20]", Description = "Number of bars back to compare delta momentum", Order = 1, GroupName = "Parameters")]
        public int Lookback
        { get; set; }

        [NinjaScriptProperty]
        [Range(50, 500)]
        [Display(Name = "Norm Period [50-500]", Description = "Rolling window for self-calibrating depth normalization", Order = 2, GroupName = "Parameters")]
        public int NormPeriod
        { get; set; }

        [Display(Name = "Show Trend Map", Description = "When off, isolates conviction line with entry dots", Order = 3, GroupName = "Parameters")]
        public bool ShowTrendMap
        { get; set; }

        // --- Color ---

        [XmlIgnore]
        [Display(Name = "Up Color", Order = 1, GroupName = "Color")]
        public Brush UpColor
        { get; set; }
        [Browsable(false)]
        public string UpColorSerializable
        {
            get { return Serialize.BrushToString(UpColor); }
            set { UpColor = Serialize.StringToBrush(value); }
        }

        [XmlIgnore]
        [Display(Name = "Down Color", Order = 2, GroupName = "Color")]
        public Brush DownColor
        { get; set; }
        [Browsable(false)]
        public string DownColorSerializable
        {
            get { return Serialize.BrushToString(DownColor); }
            set { DownColor = Serialize.StringToBrush(value); }
        }

        [Display(Name = "Enable Background", Order = 3, GroupName = "Color")]
        public bool EnableBG
        { get; set; }

        [Display(Name = "Color Full Chart", Order = 4, GroupName = "Color")]
        public bool EnableBGAll
        { get; set; }

        [XmlIgnore]
        [Display(Name = "Background Up", Order = 5, GroupName = "Color")]
        public Brush BgUpColor
        { get; set; }
        [Browsable(false)]
        public string BgUpColorSerializable
        {
            get { return Serialize.BrushToString(BgUpColor); }
            set { BgUpColor = Serialize.StringToBrush(value); }
        }

        [XmlIgnore]
        [Display(Name = "Background Down", Order = 6, GroupName = "Color")]
        public Brush BgDownColor
        { get; set; }
        [Browsable(false)]
        public string BgDownColorSerializable
        {
            get { return Serialize.BrushToString(BgDownColor); }
            set { BgDownColor = Serialize.StringToBrush(value); }
        }

        // --- Opacity ---

        [Range(0, 100)]
        [Display(Name = "Background Min Opacity", Description = "Opacity at zero conviction", Order = 1, GroupName = "Opacity")]
        public int BgMinOpacity
        { get; set; }

        [Range(0, 100)]
        [Display(Name = "Background Max Opacity", Description = "Opacity at full conviction", Order = 2, GroupName = "Opacity")]
        public int BgMaxOpacity
        { get; set; }

        #endregion
    }
}