
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.