Something I’ve been working on. Especially useful with v8.1.7.x since it’s choking at much lower tick rates.
#region Using declarations
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Data;
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 SystemVsTickTimeMovable : Indicator
{
#region Member Variables
// Latency tracking — written on the market-data thread, read on the render thread.
// Thread safety is provided by the volatile displayNeedsUpdate flag:
// • Writing: update these fields first, then set displayNeedsUpdate = true
// (volatile write = release fence, guarantees prior writes are visible).
// • Reading: only inside RebuildDisplayCache, reached after a volatile read
// of displayNeedsUpdate (acquire fence, guarantees we see the latest values).
// No lock needed in OnRender as long as the write ordering above is maintained.
private double currentLatencyMs;
private double latencyEma;
private bool emaInitialized;
private int sampleCounter;
// Tick rate tracking — same threading contract as the latency fields above.
private int tickCount;
private DateTime lastRateCalcTime;
private double currentTickRate;
private bool rateInitialized;
// DirectX resources — owned and used exclusively on the render thread.
private SharpDX.Direct2D1.SolidColorBrush textBrush;
private SharpDX.Direct2D1.SolidColorBrush backgroundBrush;
private SharpDX.Direct2D1.SolidColorBrush warningBrush;
private SharpDX.Direct2D1.SolidColorBrush criticalBrush;
private SharpDX.DirectWrite.TextFormat textFormat;
// Render cache — rebuilt only when displayNeedsUpdate is true.
private string cachedLatencyText = string.Empty;
private SharpDX.RectangleF cachedBackgroundRect;
private SharpDX.RectangleF cachedTextRect;
private SharpDX.Direct2D1.SolidColorBrush cachedDisplayBrush;
private SharpDX.DirectWrite.TextLayout cachedTextLayout;
private volatile bool displayNeedsUpdate = true;
// Hit-test bounds: four separate volatile floats so render-thread writes and
// UI-thread reads are always coherent without a lock. A SharpDX.RectangleF
// struct cannot be marked volatile, and writing its four float fields in one
// non-atomic operation risks a torn read on the UI thread.
private volatile float hitLeft;
private volatile float hitTop;
private volatile float hitRight;
private volatile float hitBottom;
// Draggable position — volatile float so writes from the UI thread (mouse drag)
// are immediately visible to the render thread, and the render thread's clamping
// writes are visible back to the UI thread. float is 32-bit, so volatile is valid.
private volatile float _savedPosX;
private volatile float _savedPosY;
private bool positionInitialized;
// Drag state — accessed exclusively from the WPF UI thread.
private bool isDragging;
private bool isRightButtonDown;
private DateTime rightButtonDownTime;
private float dragOffsetX;
private float dragOffsetY;
#endregion
public override string DisplayName => string.Empty;
#region OnStateChange
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
Description = "Displays latency between system time and market data tick timestamps. " +
"Hold the right mouse button over the box for the configured delay to drag it.";
Name = "SystemVsTickTimeMovable";
Calculate = Calculate.OnEachTick;
IsOverlay = true;
DisplayInDataBox = false;
DrawOnPricePanel = true;
IsAutoScale = false;
IsSuspendedWhileInactive = true;
// Measurement parameters
_fontSize = 14;
_warningThresholdMs = 250;
_criticalThresholdMs = 500;
_useEma = true;
_alpha = 0.1;
_sampleInterval = 100;
_dragDelaySeconds = 3.0;
// Negative sentinel — triggers default placement on the first render.
_savedPosX = -1f;
_savedPosY = -1f;
// Colors
TextColor = Brushes.White;
BackgroundColor = Brushes.Black;
WarningColor = Brushes.Orange;
CriticalColor = Brushes.Red;
}
else if (State == State.DataLoaded)
{
lastRateCalcTime = Core.Globals.Now;
SubscribeMouseEvents();
}
else if (State == State.Terminated)
{
UnsubscribeMouseEvents();
DisposeBrushes();
DisposeTextFormat();
DisposeTextLayout();
}
}
#endregion
#region DirectX Resource Management
public override void OnRenderTargetChanged()
{
DisposeBrushes();
DisposeTextFormat();
DisposeTextLayout();
displayNeedsUpdate = true;
if (RenderTarget == null) return;
textBrush = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, ToSharpDXColor(TextColor));
backgroundBrush = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, ToSharpDXColor(BackgroundColor));
warningBrush = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, ToSharpDXColor(WarningColor));
criticalBrush = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, ToSharpDXColor(CriticalColor));
textFormat = new SharpDX.DirectWrite.TextFormat(
Core.Globals.DirectWriteFactory,
"Consolas",
(float)FontSize)
{
TextAlignment = SharpDX.DirectWrite.TextAlignment.Leading,
ParagraphAlignment = SharpDX.DirectWrite.ParagraphAlignment.Near
};
}
private void DisposeBrushes()
{
if (textBrush != null) { textBrush.Dispose(); textBrush = null; }
if (backgroundBrush != null) { backgroundBrush.Dispose(); backgroundBrush = null; }
if (warningBrush != null) { warningBrush.Dispose(); warningBrush = null; }
if (criticalBrush != null) { criticalBrush.Dispose(); criticalBrush = null; }
}
private void DisposeTextFormat()
{
if (textFormat != null) { textFormat.Dispose(); textFormat = null; }
}
private void DisposeTextLayout()
{
if (cachedTextLayout != null) { cachedTextLayout.Dispose(); cachedTextLayout = null; }
}
private SharpDX.Color ToSharpDXColor(SolidColorBrush brush) =>
new SharpDX.Color(brush.Color.R, brush.Color.G, brush.Color.B, brush.Color.A);
#endregion
#region Data Processing
protected override void OnMarketData(MarketDataEventArgs marketDataUpdate)
{
if (marketDataUpdate.MarketDataType != MarketDataType.Last)
return;
// --- Tick rate tracking (unthrottled for accuracy) ---
tickCount++;
var now = Core.Globals.Now;
double elapsedSec = (now - lastRateCalcTime).TotalSeconds;
if (elapsedSec >= 0.5)
{
double instantaneousRate = tickCount / elapsedSec;
currentTickRate = !rateInitialized
? instantaneousRate
: (0.3 * instantaneousRate) + (0.7 * currentTickRate);
rateInitialized = true;
tickCount = 0;
lastRateCalcTime = now;
// Volatile write — releases the tick-rate update to the render thread.
displayNeedsUpdate = true;
}
// --- Latency tracking (throttled to reduce CPU overhead) ---
if (++sampleCounter < SampleInterval) return;
sampleCounter = 0;
currentLatencyMs = (now - marketDataUpdate.Time).TotalMilliseconds;
if (UseEma)
{
if (!emaInitialized)
{
latencyEma = currentLatencyMs;
emaInitialized = true;
}
else
{
latencyEma = (Alpha * currentLatencyMs) + ((1.0 - Alpha) * latencyEma);
}
}
// Volatile write — release fence. All writes to currentLatencyMs and
// latencyEma above are guaranteed to be visible to any thread that
// subsequently reads displayNeedsUpdate (an acquire fence).
displayNeedsUpdate = true;
}
#endregion
#region Rendering
protected override void OnRender(ChartControl chartControl, ChartScale chartScale)
{
if (State == State.Historical || RenderTarget == null) return;
// Lazily recreate resources lost after a device reset.
if (textBrush == null || backgroundBrush == null ||
warningBrush == null || criticalBrush == null || textFormat == null)
OnRenderTargetChanged();
// Default placement on the first render, or when loading a saved workspace.
if (!positionInitialized)
{
if (_savedPosX < 0f)
{
_savedPosX = (float)chartControl.ActualWidth - 200f;
_savedPosY = 12f;
}
positionInitialized = true;
displayNeedsUpdate = true;
}
try
{
// Volatile read — acquire fence. Guarantees this thread sees all double
// writes that preceded the matching displayNeedsUpdate = true assignment.
if (displayNeedsUpdate)
{
RebuildDisplayCache(chartControl);
displayNeedsUpdate = false;
}
RenderTarget.FillRectangle(cachedBackgroundRect, backgroundBrush);
// DrawTextLayout reuses the cached DirectWrite layout object;
// DrawText(string) would recreate it on every frame.
RenderTarget.DrawTextLayout(
new SharpDX.Vector2(cachedTextRect.Left, cachedTextRect.Top),
cachedTextLayout,
cachedDisplayBrush,
SharpDX.Direct2D1.DrawTextOptions.None);
}
catch (Exception ex)
{
Print("SystemVsTickTime.OnRender error: " + ex.Message);
}
}
/// <summary>
/// Rebuilds layout measurements and cached draw state.
/// Called from the render thread only when <see cref="displayNeedsUpdate"/> is set.
/// </summary>
private void RebuildDisplayCache(ChartControl chartControl)
{
// Compose display text.
string newText = string.Format("Latency: {0:0.0} ms", currentLatencyMs);
if (UseEma && emaInitialized)
newText += string.Format("\nEMA: {0:0.0} ms", latencyEma);
newText += string.Format("\nTick Rate: {0:0} t/s", currentTickRate);
// Select text color based on threshold levels.
if (currentLatencyMs >= CriticalThresholdMs) cachedDisplayBrush = criticalBrush;
else if (currentLatencyMs >= WarningThresholdMs) cachedDisplayBrush = warningBrush;
else cachedDisplayBrush = textBrush;
// Only recreate the expensive TextLayout when the text actually changes.
if (cachedTextLayout == null || cachedLatencyText != newText)
{
cachedLatencyText = newText;
DisposeTextLayout();
cachedTextLayout = new SharpDX.DirectWrite.TextLayout(
Core.Globals.DirectWriteFactory,
cachedLatencyText,
textFormat,
400f, 200f);
}
const float padding = 6f;
// Clamp position so the box never escapes the chart canvas.
float maxX = (float)chartControl.ActualWidth - cachedTextLayout.Metrics.Width - padding * 2f;
float maxY = (float)chartControl.ActualHeight - cachedTextLayout.Metrics.Height - padding * 2f;
_savedPosX = Math.Max(0f, Math.Min(_savedPosX, maxX));
_savedPosY = Math.Max(0f, Math.Min(_savedPosY, maxY));
cachedBackgroundRect = new SharpDX.RectangleF(
_savedPosX,
_savedPosY,
cachedTextLayout.Metrics.Width + padding * 2f,
cachedTextLayout.Metrics.Height + padding * 2f);
cachedTextRect = new SharpDX.RectangleF(
_savedPosX + padding,
_savedPosY + padding,
cachedTextLayout.Metrics.Width,
cachedTextLayout.Metrics.Height);
// Publish hit-test bounds to the UI thread via four volatile floats.
// A SharpDX.RectangleF struct cannot itself be volatile, and assigning its
// four fields in a non-atomic way risks a torn read on the UI thread.
hitLeft = cachedBackgroundRect.Left;
hitTop = cachedBackgroundRect.Top;
hitRight = cachedBackgroundRect.Right;
hitBottom = cachedBackgroundRect.Bottom;
}
#endregion
#region Mouse Drag Interaction
private void SubscribeMouseEvents()
{
if (ChartControl == null) return;
ChartControl.MouseRightButtonDown += OnChartMouseRightButtonDown;
ChartControl.MouseRightButtonUp += OnChartMouseRightButtonUp;
ChartControl.MouseMove += OnChartMouseMove;
}
private void UnsubscribeMouseEvents()
{
if (ChartControl == null) return;
ChartControl.MouseRightButtonDown -= OnChartMouseRightButtonDown;
ChartControl.MouseRightButtonUp -= OnChartMouseRightButtonUp;
ChartControl.MouseMove -= OnChartMouseMove;
ChartControl.Cursor = Cursors.Arrow;
}
private void OnChartMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
isRightButtonDown = true;
rightButtonDownTime = Core.Globals.Now;
}
private void OnChartMouseRightButtonUp(object sender, MouseButtonEventArgs e)
{
if (!isRightButtonDown) return;
isRightButtonDown = false;
if (isDragging)
{
isDragging = false;
ChartControl.Cursor = Cursors.Arrow;
e.Handled = true;
}
}
private void OnChartMouseMove(object sender, MouseEventArgs e)
{
// Early exit: ignore all mouse movement unless RMB is held.
// This skips coordinate translation and hit testing on every idle frame.
if (!isRightButtonDown) return;
double elapsedSeconds = (Core.Globals.Now - rightButtonDownTime).TotalSeconds;
if (elapsedSeconds <= DragDelaySeconds) return;
Point pos = e.GetPosition(ChartControl);
if (!isDragging )
{
// Only engage if the cursor is actually over the stats box.
// This prevents an accidental drag when the user holds RMB to pan the chart.
if (pos.X < hitLeft || pos.X > hitRight ||
pos.Y < hitTop || pos.Y > hitBottom)
return;
isDragging = true;
ChartControl.Cursor = Cursors.SizeAll;
// Capture the cursor-to-box offset at the moment drag engages.
// Without this the box snaps its top-left corner to the cursor position.
dragOffsetX = (float)pos.X - hitLeft;
dragOffsetY = (float)pos.Y - hitTop;
}
if (isDragging)
{
_savedPosX = (float)pos.X - dragOffsetX;
_savedPosY = (float)pos.Y - dragOffsetY;
displayNeedsUpdate = true;
ChartControl.InvalidateVisual();
}
}
#endregion
#region Properties — Parameters
[Range(8, 36)]
[Display(Name = "Font Size", Description = "Font size for the latency display",
GroupName = "Parameters", Order = 1)]
public int FontSize
{
get => _fontSize;
set
{
_fontSize = value;
DisposeTextFormat();
DisposeTextLayout();
displayNeedsUpdate = true;
if (RenderTarget != null)
OnRenderTargetChanged();
}
}
private int _fontSize;
[Display(Name = "Use EMA", Description = "Smooth the raw latency reading with an Exponential Moving Average",
GroupName = "Parameters", Order = 2)]
public bool UseEma
{
get => _useEma;
set { _useEma = value; displayNeedsUpdate = true; }
}
private bool _useEma;
[Range(0.001, 1.0)]
[Display(Name = "EMA Alpha", Description = "Smoothing factor — higher = more responsive to new samples",
GroupName = "Parameters", Order = 3)]
public double Alpha
{
get => _alpha;
set => _alpha = value;
}
private double _alpha;
[Range(0, 10000)]
[Display(Name = "Warning Threshold (ms)", Description = "Latency at which the display switches to the warning color",
GroupName = "Parameters", Order = 4)]
public int WarningThresholdMs
{
get => _warningThresholdMs;
set { _warningThresholdMs = value; displayNeedsUpdate = true; }
}
private int _warningThresholdMs;
[Range(0, 10000)]
[Display(Name = "Critical Threshold (ms)", Description = "Latency at which the display switches to the critical color",
GroupName = "Parameters", Order = 5)]
public int CriticalThresholdMs
{
get => _criticalThresholdMs;
set { _criticalThresholdMs = value; displayNeedsUpdate = true; }
}
private int _criticalThresholdMs;
[Range(1, 1000)]
[Display(Name = "Sample Interval (ticks)", Description = "Process one latency reading every N ticks — reduces CPU overhead on fast instruments",
GroupName = "Parameters", Order = 6)]
public int SampleInterval
{
get => _sampleInterval;
set => _sampleInterval = Math.Max(1, value);
}
private int _sampleInterval;
[Range(0.5, 10.0)]
[Display(Name = "Drag Delay (seconds)", Description = "How long the right mouse button must be held over the box before dragging activates",
GroupName = "Parameters", Order = 7)]
public double DragDelaySeconds
{
get => _dragDelaySeconds;
set => _dragDelaySeconds = Math.Max(0.5, value);
}
private double _dragDelaySeconds;
#endregion
#region Properties — Saved Position (hidden, serialized to persist drag location)
[Browsable(false)]
public float SavedPosX
{
get => _savedPosX;
set => _savedPosX = value;
}
[Browsable(false)]
public float SavedPosY
{
get => _savedPosY;
set => _savedPosY = value;
}
#endregion
#region Properties — Colors
[XmlIgnore]
[Display(Name = "Text Color", Description = "Normal latency text color",
GroupName = "Colors", Order = 1)]
public SolidColorBrush TextColor
{
get => _textColor;
set { _textColor = value; displayNeedsUpdate = true; }
}
private SolidColorBrush _textColor;
[Browsable(false)]
public string TextColorSerialize
{
get => Serialize.BrushToString(TextColor);
set => TextColor = (SolidColorBrush)Serialize.StringToBrush(value);
}
[XmlIgnore]
[Display(Name = "Background Color", Description = "Stats box background color",
GroupName = "Colors", Order = 2)]
public SolidColorBrush BackgroundColor
{
get => _backgroundColor;
set { _backgroundColor = value; displayNeedsUpdate = true; }
}
private SolidColorBrush _backgroundColor;
[Browsable(false)]
public string BackgroundColorSerialize
{
get => Serialize.BrushToString(BackgroundColor);
set => BackgroundColor = (SolidColorBrush)Serialize.StringToBrush(value);
}
[XmlIgnore]
[Display(Name = "Warning Color", Description = "Text color when latency exceeds the warning threshold",
GroupName = "Colors", Order = 3)]
public SolidColorBrush WarningColor
{
get => _warningColor;
set { _warningColor = value; displayNeedsUpdate = true; }
}
private SolidColorBrush _warningColor;
[Browsable(false)]
public string WarningColorSerialize
{
get => Serialize.BrushToString(WarningColor);
set => WarningColor = (SolidColorBrush)Serialize.StringToBrush(value);
}
[XmlIgnore]
[Display(Name = "Critical Color", Description = "Text color when latency exceeds the critical threshold",
GroupName = "Colors", Order = 4)]
public SolidColorBrush CriticalColor
{
get => _criticalColor;
set { _criticalColor = value; displayNeedsUpdate = true; }
}
private SolidColorBrush _criticalColor;
[Browsable(false)]
public string CriticalColorSerialize
{
get => Serialize.BrushToString(CriticalColor);
set => CriticalColor = (SolidColorBrush)Serialize.StringToBrush(value);
}
#endregion
}
}
