Find the best SMA for the instrument

sma2

5 Likes

That looks great, great work

are you not publishing this source code for this?

Here it is. It’s not meant to be an indicator to keep on your chart so it’s not optimized, may have bugs. Just run it when you want to discover interesting SMA periods. On time based charts you’ll find usual suspects. Results are more interesting on non-time based charts.

#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 CurveFittingSMA : Indicator
    {
        // =====================================================================
        // Inner types
        // =====================================================================

        private sealed class SmaAnchor
        {
            public int BarIndex;
            public int Period;
            public double AnchorPrice;
            public double[] Values;
            public int ColorIndex;
            public double RunningSum;
        }

        // Render-thread-only.
        private sealed class RenderItem
        {
            public SharpDX.Direct2D1.PathGeometry Geometry;
            public int                            ColorIndex;
            public int                            BarIndex;
            public double                         AnchorPrice;
            public int                            Period;
        }

        // =====================================================================
        // Constants
        // =====================================================================

        private const int MaxAnchors = 8;

        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 β€” primary-bar metadata (data thread β†’ UI/render)
        // =====================================================================

        private double[] _barClose;
        private int      _barArrayCapacity;

        // Updated as the LAST step in OnBarUpdate β€” release fence.
        private volatile int _lastClosedBar = -1;

        // =====================================================================
        // Member variables β€” shared (lock _anchorLock)
        // =====================================================================

        private readonly List<SmaAnchor> _anchors    = new List<SmaAnchor>(MaxAnchors);
        private readonly object          _anchorLock = new object();
        private          int             _nextColorIdx = 0;

        // =====================================================================
        // Member variables β€” UI ↔ render thread (volatile reference)
        // =====================================================================

        private volatile SmaAnchor _previewAnchor;

        // =====================================================================
        // Member variables β€” render thread only
        // =====================================================================

        private SharpDX.Direct2D1.SolidColorBrush[]  _paletteBrushes;
        private SharpDX.DirectWrite.Factory           _dwFactory;
        private SharpDX.DirectWrite.TextFormat        _labelTextFormat;
        private SharpDX.Direct2D1.SolidColorBrush    _labelBrush;
        private RenderItem[]                          _renderItems        = new RenderItem[0];
        private RenderItem                            _previewRenderItem;
        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 only (mouse state)
        // =====================================================================

        private bool            _rmbDown;
        private Point           _rmbDownPos;
        private bool            _isPreviewing;
        private DispatcherTimer _rmbTimer;
        private int             _previewPeriod;

        // =====================================================================

        public override string DisplayName => string.Empty;

        // =====================================================================
        // OnStateChange
        // =====================================================================

        protected override void OnStateChange()
        {
            if (State == State.SetDefaults)
            {
                Description =
                    "Curve Fitting SMA β€” hold right mouse button on any candle for the " +
                    "configured delay to begin previewing. Move the mouse up and down to " +
                    "adjust the SMA period. Release to commit; release within Β±1 bar of " +
                    "an existing anchor to erase it. Supports up to 8 anchors (FIFO).";
                Name         = "CurveFittingSMA";
                Calculate    = Calculate.OnBarClose;
                IsOverlay    = true;
                DisplayInDataBox         = false;
                DrawOnPricePanel         = true;
                IsAutoScale              = false;
                IsSuspendedWhileInactive = true;
                BarsRequiredToPlot       = 0;

                LineWidth      = 1.5f;
                RmbHoldSeconds = 1.0;
                DefaultPeriod  = 20;
            }
            else if (State == State.DataLoaded)
            {
                int barCap = Math.Max(Bars.Count + 512, 2048);

                _barClose         = new double[barCap];
                _barArrayCapacity = barCap;

                SubscribeMouseEvents();
            }
            else if (State == State.Terminated)
            {
                UnsubscribeMouseEvents();
                DisposeRenderResources();
                _dwFactory?.Dispose();
                _dwFactory = null;
            }
        }

        // =====================================================================
        // 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]);

            if (_dwFactory == null)
                _dwFactory = new SharpDX.DirectWrite.Factory(
                    SharpDX.DirectWrite.FactoryType.Shared);

            _labelTextFormat = new SharpDX.DirectWrite.TextFormat(
                _dwFactory,
                "Consolas",
                SharpDX.DirectWrite.FontWeight.Bold,
                SharpDX.DirectWrite.FontStyle.Normal,
                SharpDX.DirectWrite.FontStretch.Normal,
                14f)
            {
                TextAlignment      = SharpDX.DirectWrite.TextAlignment.Leading,
                ParagraphAlignment = SharpDX.DirectWrite.ParagraphAlignment.Center,
            };

            _labelBrush = new SharpDX.Direct2D1.SolidColorBrush(
                RenderTarget, new SharpDX.Color4(1f, 1f, 1f, 0.95f));
        }

        private void DisposeRenderResources()
        {
            if (_paletteBrushes != null)
            {
                foreach (var b in _paletteBrushes) b?.Dispose();
                _paletteBrushes = null;
            }
            _labelBrush?.Dispose();
            _labelBrush = null;
            _labelTextFormat?.Dispose();
            _labelTextFormat = null;
            DisposeGeometries();
        }

        private void DisposeGeometries()
        {
            foreach (var item in _renderItems)
                item?.Geometry?.Dispose();
            _renderItems = new RenderItem[0];

            _previewRenderItem?.Geometry?.Dispose();
            _previewRenderItem = null;
        }

        // =====================================================================
        // Data processing β€” OnBarUpdate (data thread)
        // =====================================================================

        protected override void OnBarUpdate()
        {
            EnsureBarCapacity(CurrentBar + 1);
            _barClose[CurrentBar] = Close[0];

            lock (_anchorLock)
            {
                foreach (var anchor in _anchors)
                {
                    if (CurrentBar < anchor.BarIndex) continue;
                    if (CurrentBar >= anchor.Values.Length)
                        ExpandAnchorValues(anchor, _barArrayCapacity);

                    int period = anchor.Period;
                    int count  = CurrentBar - anchor.BarIndex + 1;
                    
                    anchor.RunningSum += _barClose[CurrentBar];
                    if (count > period)
                    {
                        anchor.RunningSum -= _barClose[CurrentBar - period];
                        anchor.Values[CurrentBar] = anchor.RunningSum / period;
                    }
                    else
                    {
                        anchor.Values[CurrentBar] = anchor.RunningSum / count;
                    }
                }
            }

            _lastClosedBar = CurrentBar;

            if (State == State.Realtime)
                _geometryDirty = true;
        }

        private void EnsureBarCapacity(int required)
        {
            if (required <= _barArrayCapacity) return;
            int newCap = Math.Max(_barArrayCapacity * 2, required + 256);

            Array.Resize(ref _barClose, newCap);

            lock (_anchorLock)
            {
                foreach (var anchor in _anchors)
                    ExpandAnchorValues(anchor, newCap);
                _barArrayCapacity = newCap;
            }
        }

        private static void ExpandAnchorValues(SmaAnchor 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;
            if (lastClosed < 0) return;

            int firstBar = ChartBars.FromIndex;
            int lastBar  = Math.Min(ChartBars.ToIndex, lastClosed);
            if (lastBar < firstBar) return;

            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 committed anchors β€” full opacity with permanent labels.
            foreach (var item in _renderItems)
            {
                if (item?.Geometry == null || item.Geometry.IsDisposed) continue;
                RenderTarget.DrawGeometry(item.Geometry, _paletteBrushes[item.ColorIndex], LineWidth);
                DrawAnchorLabel(chartControl, chartScale, item.BarIndex, item.AnchorPrice, 
                                item.Period, item.ColorIndex, 1.0f);
            }

            // Draw preview anchor β€” reduced opacity with label.
            if (_previewRenderItem?.Geometry != null && !_previewRenderItem.Geometry.IsDisposed)
            {
                var   brush        = _paletteBrushes[_previewRenderItem.ColorIndex];
                float savedOpacity = brush.Opacity;
                brush.Opacity      = 0.45f;
                RenderTarget.DrawGeometry(_previewRenderItem.Geometry, brush, LineWidth);
                brush.Opacity      = savedOpacity;
                
                DrawAnchorLabel(chartControl, chartScale, _previewRenderItem.BarIndex, 
                                _previewRenderItem.AnchorPrice, _previewRenderItem.Period, 
                                _previewRenderItem.ColorIndex, 0.6f);
            }
        }

        /// <summary>
        /// Draws a filled circle and period label at the anchor point.
        /// Used for both committed anchors (full opacity) and preview (reduced opacity).
        /// </summary>
        private void DrawAnchorLabel(ChartControl chartControl, ChartScale chartScale,
                                     int barIdx, double price, int period, int colorIdx, float opacity)
        {
            if (_labelTextFormat == null || _labelBrush == null) return;
            if (ChartBars == null) return;

            int visBarIdx = Math.Max(ChartBars.FromIndex, Math.Min(barIdx, ChartBars.ToIndex));
            float x = (float)chartControl.GetXByBarIndex(ChartBars, visBarIdx);
            float y = (float)chartScale.GetYByValue(price);

            const float R = 4.5f;
            var circle = new SharpDX.Direct2D1.Ellipse(new SharpDX.Vector2(x, y), R, R);

            var pBrush = _paletteBrushes[colorIdx];
            float saved = pBrush.Opacity;
            
            // Draw filled circle
            pBrush.Opacity = opacity * 0.75f;
            RenderTarget.FillEllipse(circle, pBrush);
            
            // Draw period label
            string label = $"P:{period}";
            var rect = new SharpDX.RectangleF(x + R + 4f, y - 11f, 65f, 22f);
            pBrush.Opacity = opacity;
            RenderTarget.DrawText(label, _labelTextFormat, rect, _labelBrush);
            
            pBrush.Opacity = saved;
        }

        private void RebuildGeometries(ChartControl chartControl, ChartScale chartScale,
                                       int firstBar, int lastBar)
        {
            DisposeGeometries();
            if (ChartBars == null) return;

            SmaAnchor[] snapshot;
            lock (_anchorLock)
            {
                snapshot = _anchors.Count > 0 ? _anchors.ToArray() : new SmaAnchor[0];
            }

            _renderItems = new RenderItem[snapshot.Length];
            for (int i = 0; i < snapshot.Length; i++)
                _renderItems[i] = BuildSingleGeometry(
                    snapshot[i], chartControl, chartScale, firstBar, lastBar);

            SmaAnchor preview = _previewAnchor;
            if (preview != null)
                _previewRenderItem = BuildSingleGeometry(
                    preview, chartControl, chartScale, firstBar, lastBar);
        }

        private RenderItem BuildSingleGeometry(SmaAnchor anchor,
                                               ChartControl chartControl,
                                               ChartScale   chartScale,
                                               int firstBar, int lastBar)
        {
            var item  = new RenderItem 
            { 
                ColorIndex  = anchor.ColorIndex,
                BarIndex    = anchor.BarIndex,
                AnchorPrice = anchor.AnchorPrice,
                Period      = anchor.Period
            };
            
            int start = Math.Max(firstBar, anchor.BarIndex);
            if (start > lastBar) return item;

            double[] values  = anchor.Values;

            SharpDX.Direct2D1.PathGeometry geo = null;
            bool figureOpen = false;

            try
            {
                geo  = new SharpDX.Direct2D1.PathGeometry(RenderTarget.Factory);
                var sink = geo.Open();
                try
                {
                    for (int b = start; b <= lastBar; b++)
                    {
                        if (b >= values.Length) break;

                        double val = values[b];
                        if (double.IsNaN(val)) continue;

                        float px = (float)chartControl.GetXByBarIndex(ChartBars, b);
                        float py = (float)chartScale.GetYByValue(val);

                        if (!figureOpen)
                        {
                            sink.BeginFigure(new SharpDX.Vector2(px, py),
                                             SharpDX.Direct2D1.FigureBegin.Hollow);
                            figureOpen = true;
                        }
                        else
                        {
                            sink.AddLine(new SharpDX.Vector2(px, py));
                        }
                    }

                    if (figureOpen)
                        sink.EndFigure(SharpDX.Direct2D1.FigureEnd.Open);

                    sink.Close();
                }
                finally
                {
                    sink.Dispose();
                }

                if (figureOpen)
                    item.Geometry = geo;
                else
                {
                    geo.Dispose();
                    geo = null;
                }
            }
            catch (Exception ex)
            {
                Print("CurveFittingSMA.BuildSingleGeometry: " + ex.Message);
                geo?.Dispose();
            }

            return item;
        }

        // =====================================================================
        // 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;
            _isPreviewing = false;
            _rmbDownPos   = e.GetPosition(ChartControl);
            _previewPeriod = DefaultPeriod;

            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;

            int lcb = _lastClosedBar;
            if (lcb < 0) return;

            int barIdx = BarIdxFromX(_rmbDownPos.X);
            barIdx = Math.Max(0, Math.Min(barIdx, lcb));

            BuildPreviewAnchor(barIdx, _previewPeriod);
            _isPreviewing  = true;
            _geometryDirty = true;
            ChartControl.InvalidateVisual();
        }

        private void OnRmbUp(object sender, MouseButtonEventArgs e)
        {
            if (!_rmbDown) return;
            _rmbDown = false;
            StopRmbTimer();

            if (!_isPreviewing) return;

            _isPreviewing = false;
            e.Handled     = true;

            int lcb = _lastClosedBar;
            if (lcb >= 0)
            {
                Point pos           = e.GetPosition(ChartControl);
                int   releaseBarIdx = BarIdxFromX(pos.X);
                releaseBarIdx       = Math.Max(0, Math.Min(releaseBarIdx, lcb));

                bool erased = false;
                lock (_anchorLock)
                {
                    for (int i = 0; i < _anchors.Count; i++)
                    {
                        if (Math.Abs(_anchors[i].BarIndex - releaseBarIdx) > 1) continue;
                        _anchors.RemoveAt(i);
                        erased = true;
                        break;
                    }
                }

                if (!erased)
                    CommitPreviewAnchor();
            }

            _previewAnchor = null;
            _geometryDirty = true;
            ChartControl.InvalidateVisual();
        }

        private void OnRmbMouseMove(object sender, MouseEventArgs e)
        {
            if (!_rmbDown) return;

            Point pos = e.GetPosition(ChartControl);

            if (_isPreviewing)
            {
                int lcb = _lastClosedBar;
                if (lcb < 0) return;

                int newBarIdx = BarIdxFromX(pos.X);
                newBarIdx     = Math.Max(0, Math.Min(newBarIdx, lcb));

                double dy = pos.Y - _rmbDownPos.Y;
                int period = DefaultPeriod - (int)(dy / 5.0);
                period = Math.Max(2, Math.Min(200, period));

                SmaAnchor current = _previewAnchor;
                if (current != null
                    && current.BarIndex == newBarIdx
                    && current.Period   == period)
                    return;

                BuildPreviewAnchor(newBarIdx, period);
                _geometryDirty = true;
                ChartControl.InvalidateVisual();
            }
            else
            {
                double dx = pos.X - _rmbDownPos.X;
                double dy = pos.Y - _rmbDownPos.Y;
                if (dx * dx + dy * dy > 25.0)
                {
                    _rmbDown = false;
                    StopRmbTimer();
                }
            }
        }

        private void StopRmbTimer()
        {
            if (_rmbTimer == null) return;
            _rmbTimer.Stop();
            _rmbTimer.Tick -= OnRmbTimerTick;
            _rmbTimer       = null;
        }

        // =====================================================================
        // Anchor building helpers (UI thread)
        // =====================================================================

        private void BuildPreviewAnchor(int barIdx, int period)
        {
            SmaAnchor anchor = BuildSmaAnchor(barIdx, period);
            _previewAnchor    = anchor;
        }

        private void CommitPreviewAnchor()
        {
            SmaAnchor preview = _previewAnchor;
            if (preview == null) return;

            lock (_anchorLock)
            {
                if (_anchors.Count >= MaxAnchors)
                    _anchors.RemoveAt(0);

                int      cap    = _barArrayCapacity;
                double[] values = preview.Values;
                int      oldLen = values.Length;
                if (oldLen < cap)
                {
                    Array.Resize(ref values, cap);
                    for (int i = oldLen; i < cap; i++)
                        values[i] = double.NaN;
                    preview.Values = values;
                }

                preview.ColorIndex = _nextColorIdx % Palette.Length;
                _nextColorIdx++;
                _anchors.Add(preview);
            }
        }

        private SmaAnchor BuildSmaAnchor(int barIdx, int period)
        {
            int upTo = _lastClosedBar;
            if (upTo < 0 || barIdx > upTo) return null;

            int barCap;
            lock (_anchorLock) { barCap = _barArrayCapacity; }

            double[] values = new double[barCap];

            for (int i = 0; i < barIdx && i < barCap; i++)
                values[i] = double.NaN;

            double sum = 0;
            for (int i = barIdx; i <= upTo && i < barCap; i++)
            {
                sum += _barClose[i];
                int count = i - barIdx + 1;
                if (count >= period)
                {
                    if (count > period) sum -= _barClose[i - period];
                    values[i] = sum / period;
                }
                else
                {
                    values[i] = sum / count;
                }
            }

            for (int i = upTo + 1; i < barCap; i++)
                values[i] = double.NaN;

            int colorIdx;
            lock (_anchorLock) { colorIdx = _nextColorIdx % Palette.Length; }

            return new SmaAnchor
            {
                BarIndex    = barIdx,
                Period      = period,
                AnchorPrice = _barClose[barIdx],
                Values      = values,
                ColorIndex  = colorIdx,
                RunningSum  = sum
            };
        }

        // =====================================================================
        // Coordinate helpers
        // =====================================================================

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

        // =====================================================================
        // Properties
        // =====================================================================

        [Range(0.5f, 5f)]
        [Display(Name        = "Line Width",
                 Description = "SMA 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 begin previewing",
                 GroupName   = "Parameters", Order = 2)]
        public double RmbHoldSeconds { get; set; }

        [Range(2, 200)]
        [Display(Name        = "Default Period",
                 Description = "Initial SMA period when starting preview",
                 GroupName   = "Parameters", Order = 3)]
        public int DefaultPeriod { get; set; }
    }
}


Right click and hold to start a SMA. Move up and down to adjust period.

2 Likes