System vs Tick latency {code}

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
    }
}

MovableLatencyStats2

Press RMB for >3seconds before dragging

1 Like

That is an untradeable latency - mine is like < 10ms

You’re selling when I’m about to buy. lol

wow that’s laggy, that’s crazy

1 Like

is your latency due to heavy indicators, 300 ms does not seem to bother you, but what is the use case you are trying to solve here, are you using this as a tool to not trade when latency is too high how do these extra parameters help you make that decision, is currentLatencyMs not sufficient information, just curious how are you benefitting from rest of the information on the screen.

It’s a System time vs Ticks time, it is not a network latency (like ping). In above example my system clock was off so latency showed higher than it was. You can go to time.is to see if your system clock is in sync.

From my observations, tick rate above 225t/s starts increasing latency in version 7.x. And, no I’m not ok with higher latency, I started using this indicator because unexpected latency caused really bad exits. I also use NQ and MNQ charts so I can tell right away when there’s a problem (each instrument has it’s own processing pipeline so MNQ lags while NQ does not).

i think it is a standard practise in trading machines to sync system clock every 5 minutes using the scheduler.

I would have thought NQ will have more ticks than MNQ and hence the lag should be on NQ…

have you tried testing the lag with no indicators on the chart, or has NT genuinely broken something in the platform.

7.x has lower throughput, others posted the same, just search it up in this forum. For the same value there’s 10MNQ to 1 NQ, think about it.

Use Dimension 4 to synchronize your clock every ‘period’ you set it for.

Thanks for the tip. I am using Meinberg ntp now.