After using RMB to initiate label move with my latency indicator I liked the RMB control and figured it could work well with indicators that are interactive, like anchored VWAP
#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.NinjaScript;
#endregion
// This namespace holds Indicators in this folder and is required. Do not change it.
namespace NinjaTrader.NinjaScript.Indicators
{
public class AnchoredVwap : Indicator
{
// =====================================================================
// Inner types
// =====================================================================
private sealed class VwapAnchor
{
/// <summary>Absolute bar index at which this VWAP is anchored.</summary>
public int BarIndex;
/// <summary>
/// VWAP value per bar index.
/// β’ Indices below BarIndex β NaN (before anchor)
/// β’ Indices BarIndex..lastClosed β computed VWAP
/// β’ Indices beyond lastClosed β NaN (data thread fills on close)
///
/// Threading contract:
/// Writer β OnBarUpdate (data thread): writes index = CurrentBar.
/// Writer β AddAnchorInternal (UI thread): writes all indices while the
/// object is still private (not yet in the shared list).
/// Reader β RebuildGeometries (render thread): reads indices β€ _lastClosedBar.
///
/// CurrentBar (data write) and _lastClosedBar (render read ceiling) advance
/// together: the volatile write to _lastClosedBar in OnBarUpdate acts as a
/// release fence that makes anchor.Values[CurrentBar] visible to any thread
/// that subsequently reads _lastClosedBar with an acquire fence. Because
/// the render thread caps its reads at _lastClosedBar, data and render
/// accesses never touch the same index concurrently β no per-element lock
/// is required.
///
/// The _anchorLock guards only list-structure mutations (Add/Remove) and
/// the _arrayCapacity field.
/// </summary>
public double[] Values;
/// <summary>Index into Palette[] (0βMaxAnchors-1), determines line color.</summary>
public int ColorIndex;
}
// Render-thread-only β not shared with data or UI threads.
private sealed class RenderItem
{
public SharpDX.Direct2D1.PathGeometry Geometry;
public int ColorIndex;
}
// =====================================================================
// Constants
// =====================================================================
private const int MaxAnchors = 8;
// One distinct color per anchor slot. Cycles if anchors are added and removed.
private static readonly SharpDX.Color4[] Palette =
{
new SharpDX.Color4(0.0f, 1.0f, 1.0f, 1.0f), // 0 β cyan
new SharpDX.Color4(1.0f, 1.0f, 0.0f, 1.0f), // 1 β yellow
new SharpDX.Color4(0.2f, 1.0f, 0.2f, 1.0f), // 2 β lime
new SharpDX.Color4(1.0f, 0.5f, 0.0f, 1.0f), // 3 β orange
new SharpDX.Color4(0.6f, 0.4f, 1.0f, 1.0f), // 4 β violet
new SharpDX.Color4(1.0f, 0.4f, 0.7f, 1.0f), // 5 β pink
new SharpDX.Color4(0.4f, 0.8f, 1.0f, 1.0f), // 6 β sky blue
new SharpDX.Color4(1.0f, 1.0f, 1.0f, 1.0f), // 7 β white
};
// =====================================================================
// Member variables β data thread (OnBarUpdate)
// =====================================================================
// Prefix-sum arrays indexed by absolute bar index:
// _cumPV[i] = Ξ£(TP Γ Vol) for bars 0..i (TP = (H+L+C)/3)
// _cumVol[i] = Ξ£(Vol) for bars 0..i
//
// VWAP for anchor A over range [A, i]:
// (_cumPV[i] - _cumPV[A-1]) / (_cumVol[i] - _cumVol[A-1])
//
// Written exclusively by OnBarUpdate.
// Read by the UI thread only at indices β€ _lastClosedBar (see volatile note below).
private double[] _cumPV;
private double[] _cumVol;
// Guarded by _anchorLock when written from EnsureCapacity so that
// AddAnchorInternal always reads a value consistent with the actual array sizes.
private int _arrayCapacity;
// Volatile: written as the LAST step in OnBarUpdate (release fence) so all
// preceding array writes are visible to any thread that reads this value
// (acquire fence). The render thread uses it as the draw ceiling;
// the UI thread uses it to bound the historical fill in AddAnchorInternal.
private volatile int _lastClosedBar = -1;
// =====================================================================
// Member variables β shared (lock _anchorLock)
// =====================================================================
private readonly List<VwapAnchor> _anchors = new List<VwapAnchor>(MaxAnchors);
private readonly object _anchorLock = new object();
// Monotonically increasing; mod Palette.Length when assigning a color.
// Ensures each new anchor gets the next color in the cycle regardless of
// which slots were previously freed.
private int _nextColorIdx = 0;
// =====================================================================
// Member variables β render thread
// =====================================================================
private SharpDX.Direct2D1.SolidColorBrush[] _paletteBrushes;
private RenderItem[] _renderItems = new RenderItem[0];
private volatile bool _geometryDirty = true;
private int _lastFirstBar = -1;
private int _lastLastBar = -1;
private double _lastScaleMin = double.NaN;
private double _lastScaleMax = double.NaN;
// =====================================================================
// Member variables β UI thread (mouse state)
// =====================================================================
private bool _rmbDown;
private Point _rmbDownPos;
private bool _rmbTriggered; // true once the timer fires and places/removes anchor
private DispatcherTimer _rmbTimer; // fires after RmbHoldSeconds while RMB is still held
// =====================================================================
public override string DisplayName => string.Empty;
// =====================================================================
// OnStateChange
// =====================================================================
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
Description = "Anchored VWAP β hold right mouse button on any candle for the " +
"configured delay to place a VWAP anchor. Hold again on the same " +
"candle to remove it. Supports up to 8 simultaneous anchors, " +
"each drawn in a distinct color.";
Name = "AnchoredVwap";
Calculate = Calculate.OnBarClose; // we only draw through last closed bar
IsOverlay = true;
DisplayInDataBox = false;
DrawOnPricePanel = true;
IsAutoScale = false;
IsSuspendedWhileInactive = true;
BarsRequiredToPlot = 0; // allow anchoring at bar 0
LineWidth = 1.5f;
RmbHoldSeconds = 1.0;
}
else if (State == State.DataLoaded)
{
int initCap = Math.Max(Bars.Count + 512, 2048);
_cumPV = new double[initCap];
_cumVol = new double[initCap];
_arrayCapacity = initCap;
SubscribeMouseEvents();
}
else if (State == State.Terminated)
{
UnsubscribeMouseEvents();
DisposeRenderResources();
}
}
// =====================================================================
// DirectX resource management (render thread)
// =====================================================================
public override void OnRenderTargetChanged()
{
DisposeRenderResources();
_geometryDirty = true;
if (RenderTarget == null) return;
_paletteBrushes = new SharpDX.Direct2D1.SolidColorBrush[Palette.Length];
for (int i = 0; i < Palette.Length; i++)
_paletteBrushes[i] = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, Palette[i]);
}
private void DisposeRenderResources()
{
if (_paletteBrushes != null)
{
foreach (var b in _paletteBrushes)
b?.Dispose();
_paletteBrushes = null;
}
DisposeGeometries();
}
private void DisposeGeometries()
{
foreach (var item in _renderItems)
item?.Geometry?.Dispose();
_renderItems = new RenderItem[0];
}
// =====================================================================
// Data processing β OnBarUpdate (data thread, Calculate.OnBarClose)
// =====================================================================
protected override void OnBarUpdate()
{
EnsureCapacity(CurrentBar + 1);
// Extend prefix sums to include this newly-closed bar.
double tp = (High[0] + Low[0] + Close[0]) / 3.0;
double prevPV = CurrentBar > 0 ? _cumPV[CurrentBar - 1] : 0.0;
double prevVol = CurrentBar > 0 ? _cumVol[CurrentBar - 1] : 0.0;
_cumPV[CurrentBar] = prevPV + tp * Volume[0];
_cumVol[CurrentBar] = prevVol + Volume[0];
// Append this bar's VWAP to every active anchor.
// ALL writes here must precede the volatile write to _lastClosedBar below
// so the render thread sees them after its acquire read.
lock (_anchorLock)
{
foreach (var anchor in _anchors)
{
if (CurrentBar < anchor.BarIndex) continue;
// Safety: anchor may have been added between our last EnsureCapacity
// call and now, giving it a Values array that is one bar too short.
if (CurrentBar >= anchor.Values.Length)
ExpandAnchorValues(anchor, _arrayCapacity);
double oPV = anchor.BarIndex > 0 ? _cumPV[anchor.BarIndex - 1] : 0.0;
double oVol = anchor.BarIndex > 0 ? _cumVol[anchor.BarIndex - 1] : 0.0;
double denom = _cumVol[CurrentBar] - oVol;
anchor.Values[CurrentBar] = denom > 0
? (_cumPV[CurrentBar] - oPV) / denom
: double.NaN;
}
}
// Volatile write (release fence) β must be the final operation so the render
// thread's acquire read of _lastClosedBar guarantees it sees all array writes above.
_lastClosedBar = CurrentBar;
// Only flag geometry dirty in realtime; historical OnRender is suppressed anyway.
if (State == State.Realtime)
_geometryDirty = true;
}
private void EnsureCapacity(int required)
{
if (required <= _arrayCapacity) return;
int newCap = Math.Max(_arrayCapacity * 2, required + 256);
Array.Resize(ref _cumPV, newCap);
Array.Resize(ref _cumVol, newCap);
lock (_anchorLock)
{
foreach (var anchor in _anchors)
ExpandAnchorValues(anchor, newCap);
// Commit inside the lock: AddAnchorInternal reads _arrayCapacity under the
// same lock, so it always sees the value that matches the actual array sizes.
_arrayCapacity = newCap;
}
}
/// <summary>
/// Grows <paramref name="anchor"/>.Values to <paramref name="newCap"/> elements,
/// filling every new slot with NaN.
/// </summary>
/// <remarks>
/// Must be called with _anchorLock held when invoked from EnsureCapacity.
/// No lock is required when called from AddAnchorInternal because the anchor
/// object is not yet in the shared list at that point.
/// </remarks>
private static void ExpandAnchorValues(VwapAnchor anchor, int newCap)
{
int old = anchor.Values.Length;
if (newCap <= old) return;
Array.Resize(ref anchor.Values, newCap);
for (int i = old; i < newCap; i++)
anchor.Values[i] = double.NaN;
}
// =====================================================================
// Rendering (render thread)
// =====================================================================
protected override void OnRender(ChartControl chartControl, ChartScale chartScale)
{
if (State == State.Historical || RenderTarget == null) return;
if (_paletteBrushes == null) OnRenderTargetChanged();
int lastClosed = _lastClosedBar; // volatile read β acquire fence
if (lastClosed < 0) return;
int firstBar = ChartBars.FromIndex;
int lastBar = Math.Min(ChartBars.ToIndex, lastClosed);
if (lastBar < firstBar) return;
// Viewport change (scroll / zoom) or price-scale change requires a geometry rebuild.
if (firstBar != _lastFirstBar || lastBar != _lastLastBar
|| chartScale.MinValue != _lastScaleMin || chartScale.MaxValue != _lastScaleMax)
{
_lastFirstBar = firstBar;
_lastLastBar = lastBar;
_lastScaleMin = chartScale.MinValue;
_lastScaleMax = chartScale.MaxValue;
_geometryDirty = true;
}
if (_geometryDirty)
{
RebuildGeometries(chartControl, chartScale, firstBar, lastBar);
_geometryDirty = false;
}
// Draw β one call per anchor, no math.
foreach (var item in _renderItems)
{
if (item?.Geometry == null || item.Geometry.IsDisposed) continue;
RenderTarget.DrawGeometry(item.Geometry, _paletteBrushes[item.ColorIndex], LineWidth);
}
}
/// <summary>
/// Rebuilds the Direct2D path geometry for each active anchor, covering
/// only the currently visible bar range. Called from OnRender when dirty.
/// </summary>
private void RebuildGeometries(ChartControl chartControl, ChartScale chartScale,
int firstBar, int lastBar)
{
DisposeGeometries();
if (ChartBars == null) return;
// Take a snapshot of the anchor list under the lock. The (potentially slow)
// geometry-building loop runs outside the lock so we don't stall OnBarUpdate
// or the UI mouse handlers.
VwapAnchor[] snapshot;
lock (_anchorLock)
{
snapshot = _anchors.Count > 0 ? _anchors.ToArray() : new VwapAnchor[0];
}
_renderItems = new RenderItem[snapshot.Length];
for (int i = 0; i < snapshot.Length; i++)
{
VwapAnchor anchor = snapshot[i];
double[] values = anchor.Values; // capture ref β safe; see VwapAnchor threading contract
int start = Math.Max(firstBar, anchor.BarIndex);
// Always create the item so the draw loop has a valid (possibly
// geometry-less) entry for every anchor.
_renderItems[i] = new RenderItem { ColorIndex = anchor.ColorIndex };
if (start > lastBar) continue; // anchor scrolled off the left edge, or no data yet
SharpDX.Direct2D1.PathGeometry geo = null;
bool figureOpen = false;
try
{
geo = new SharpDX.Direct2D1.PathGeometry(RenderTarget.Factory);
// GeometrySink must be Close()d before the geometry can be drawn,
// and Dispose()d to release the underlying COM reference.
var sink = geo.Open();
try
{
for (int b = start; b <= lastBar; b++)
{
if (b >= values.Length) break; // guard: in-flight resize
double val = values[b];
if (double.IsNaN(val)) continue; // gap (zero-volume bar, pre-anchor)
float x = (float)chartControl.GetXByBarIndex(ChartBars, b);
float y = (float)chartScale.GetYByValue(val);
if (!figureOpen)
{
sink.BeginFigure(
new SharpDX.Vector2(x, y),
SharpDX.Direct2D1.FigureBegin.Hollow);
figureOpen = true;
}
else
{
sink.AddLine(new SharpDX.Vector2(x, y));
}
}
if (figureOpen)
sink.EndFigure(SharpDX.Direct2D1.FigureEnd.Open);
sink.Close();
}
finally
{
sink.Dispose();
}
if (figureOpen)
_renderItems[i].Geometry = geo;
else
{
geo.Dispose();
geo = null;
}
}
catch (Exception ex)
{
Print("AnchoredVwap.RebuildGeometries: " + ex.Message);
geo?.Dispose();
}
}
}
// =====================================================================
// Mouse interaction (UI / WPF dispatcher thread)
// =====================================================================
private void SubscribeMouseEvents()
{
if (ChartControl == null) return;
ChartControl.MouseRightButtonDown += OnRmbDown;
ChartControl.MouseRightButtonUp += OnRmbUp;
ChartControl.MouseMove += OnRmbMouseMove;
}
private void UnsubscribeMouseEvents()
{
if (ChartControl == null) return;
ChartControl.MouseRightButtonDown -= OnRmbDown;
ChartControl.MouseRightButtonUp -= OnRmbUp;
ChartControl.MouseMove -= OnRmbMouseMove;
StopRmbTimer();
}
private void OnRmbDown(object sender, MouseButtonEventArgs e)
{
_rmbDown = true;
_rmbTriggered = false;
_rmbDownPos = e.GetPosition(ChartControl);
// Start a one-shot timer. When it fires (on the UI thread) the anchor
// is placed immediately β no need to wait for button release.
StopRmbTimer();
_rmbTimer = new DispatcherTimer(DispatcherPriority.Input);
_rmbTimer.Interval = TimeSpan.FromSeconds(RmbHoldSeconds);
_rmbTimer.Tick += OnRmbTimerTick;
_rmbTimer.Start();
}
private void OnRmbTimerTick(object sender, EventArgs e)
{
StopRmbTimer();
if (!_rmbDown) return; // button released before timer fired
_rmbTriggered = true;
ToggleAnchorAt(_rmbDownPos.X);
}
private void OnRmbUp(object sender, MouseButtonEventArgs e)
{
if (!_rmbDown) return;
_rmbDown = false;
StopRmbTimer();
// If the timer already placed/removed an anchor, just suppress the
// context menu. If the timer hadn't fired yet (short press), do nothing
// and let NinjaTrader's normal context menu appear.
if (_rmbTriggered)
{
_rmbTriggered = false;
e.Handled = true;
}
}
// Cancels the pan gesture: if the cursor travels more than ~5 px while
// RMB is held, the user is panning β kill the timer before it fires.
private void OnRmbMouseMove(object sender, MouseEventArgs e)
{
if (_rmbTimer == null) return;
Point pos = e.GetPosition(ChartControl);
double dx = pos.X - _rmbDownPos.X;
double dy = pos.Y - _rmbDownPos.Y;
if (dx * dx + dy * dy > 25.0) // 5 px radius
{
_rmbDown = false;
StopRmbTimer();
}
}
private void StopRmbTimer()
{
if (_rmbTimer == null) return;
_rmbTimer.Stop();
_rmbTimer.Tick -= OnRmbTimerTick;
_rmbTimer = null;
}
// Shared toggle logic β called by the timer tick (immediate) and nothing else.
private void ToggleAnchorAt(double chartX)
{
int lcb = _lastClosedBar; // volatile read
if (lcb < 0) return;
int barIdx = BarIdxFromX(chartX);
barIdx = Math.Max(0, Math.Min(barIdx, lcb));
bool removed = false;
lock (_anchorLock)
{
for (int i = 0; i < _anchors.Count; i++)
{
if (_anchors[i].BarIndex != barIdx) continue;
_anchors.RemoveAt(i);
removed = true;
break;
}
}
if (!removed)
AddAnchorInternal(barIdx);
_geometryDirty = true;
ChartControl.InvalidateVisual();
}
/// <summary>
/// Converts a chart-space X pixel coordinate to the nearest bar index via
/// linear interpolation across the currently visible bar range.
/// </summary>
private int BarIdxFromX(double x)
{
if (ChartBars == null) return 0;
int first = ChartBars.FromIndex;
int last = ChartBars.ToIndex;
if (first >= last) return first;
double x0 = ChartControl.GetXByBarIndex(ChartBars, first);
double x1 = ChartControl.GetXByBarIndex(ChartBars, last);
if (Math.Abs(x1 - x0) < 1.0) return first;
double t = (x - x0) / (x1 - x0);
int b = (int)Math.Round(first + t * (last - first));
return Math.Max(first, Math.Min(last, b));
}
/// <summary>
/// Fills the full VWAP series for a new anchor at <paramref name="barIdx"/>
/// using the pre-computed prefix sums, then adds the anchor to the shared list.
/// </summary>
/// <remarks>
/// Called on the UI thread. Safe to read _cumPV / _cumVol at indices β€ upTo
/// because:
/// (a) upTo is obtained via a volatile read of _lastClosedBar (acquire fence),
/// which guarantees all OnBarUpdate writes up to that bar are visible.
/// (b) The data thread only writes index = CurrentBar; we only read β€ upTo
/// which equals the previous CurrentBar, so no index overlaps.
/// The anchor object is fully populated before being inserted into _anchors, so
/// the render and data threads never observe a partially-initialised Values array.
/// </remarks>
private void AddAnchorInternal(int barIdx)
{
int upTo = _lastClosedBar; // volatile read β acquire fence
if (upTo < 0 || barIdx > upTo) return;
// Read _arrayCapacity under the lock so we always see the value last committed
// by EnsureCapacity, even if it ran concurrently on the data thread.
int cap;
lock (_anchorLock)
{
if (_anchors.Count >= MaxAnchors)
_anchors.RemoveAt(0); // FIFO eviction β drop the oldest anchor
cap = _arrayCapacity;
}
// Capture array references (stable for indices we will read: 0..upTo).
double[] pv = _cumPV;
double[] vol = _cumVol;
double offsetPV = barIdx > 0 ? pv[barIdx - 1] : 0.0;
double offsetVol = barIdx > 0 ? vol[barIdx - 1] : 0.0;
double[] values = new double[cap];
// Bars before anchor β NaN.
for (int i = 0; i < barIdx && i < cap; i++)
values[i] = double.NaN;
// Bars from anchor to last closed bar β VWAP via prefix subtraction.
for (int i = barIdx; i <= upTo && i < cap; i++)
{
double d = vol[i] - offsetVol;
values[i] = d > 0 ? (pv[i] - offsetPV) / d : double.NaN;
}
// Bars beyond upTo β NaN; OnBarUpdate fills these as new bars close.
for (int i = upTo + 1; i < cap; i++)
values[i] = double.NaN;
// Insert under lock, with a final capacity sync in case EnsureCapacity
// ran on the data thread between the two lock acquisitions above.
lock (_anchorLock)
{
if (_anchors.Count >= MaxAnchors)
_anchors.RemoveAt(0); // FIFO eviction β re-check in case another was added concurrently
int currentCap = _arrayCapacity;
if (cap < currentCap)
{
// Capacity grew while we were filling; extend to match.
Array.Resize(ref values, currentCap);
for (int i = cap; i < currentCap; i++)
values[i] = double.NaN;
}
_anchors.Add(new VwapAnchor
{
BarIndex = barIdx,
Values = values,
ColorIndex = _nextColorIdx % Palette.Length,
});
_nextColorIdx++;
}
}
// =====================================================================
// Properties
// =====================================================================
[Range(0.5f, 5f)]
[Display(Name = "Line Width",
Description = "VWAP line stroke width in pixels",
GroupName = "Parameters", Order = 1)]
public float LineWidth { get; set; }
[Range(0.5, 5.0)]
[Display(Name = "RMB Hold (s)",
Description = "Seconds the right mouse button must be held to toggle an anchor",
GroupName = "Parameters", Order = 2)]
public double RmbHoldSeconds { get; set; }
}
}

Point at the anchor candle and hold RMB for 1s
