From 18759beb5ef2f70f503d30b27fc99735cc9f666a Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Sun, 14 Dec 2025 17:14:51 -0800 Subject: [PATCH] feat: implement McGinley Dynamic Indicator (MGDI) with tests and documentation --- .clinerules/good-indicator.md | 3 +- AGENTS.md | 3 +- lib/trends/_index.md | 2 +- lib/trends/htit/Htit.cs | 16 +- lib/trends/mgdi/Mgdi.Quantower.Tests.cs | 41 +++++ lib/trends/mgdi/Mgdi.Quantower.cs | 69 +++++++++ lib/trends/mgdi/Mgdi.Tests.cs | 99 +++++++++++++ lib/trends/mgdi/Mgdi.Validation.Tests.cs | 126 ++++++++++++++++ lib/trends/mgdi/Mgdi.cs | 181 +++++++++++++++++++++++ lib/trends/mgdi/Mgdi.md | 89 +++++++++++ 10 files changed, 618 insertions(+), 11 deletions(-) create mode 100644 lib/trends/mgdi/Mgdi.Quantower.Tests.cs create mode 100644 lib/trends/mgdi/Mgdi.Quantower.cs create mode 100644 lib/trends/mgdi/Mgdi.Tests.cs create mode 100644 lib/trends/mgdi/Mgdi.Validation.Tests.cs create mode 100644 lib/trends/mgdi/Mgdi.cs create mode 100644 lib/trends/mgdi/Mgdi.md diff --git a/.clinerules/good-indicator.md b/.clinerules/good-indicator.md index 0abfffa8..0e0e400e 100644 --- a/.clinerules/good-indicator.md +++ b/.clinerules/good-indicator.md @@ -93,7 +93,7 @@ Each indicator resides in its own directory such as `lib/trends/`, `lib/indicato * **Optimization:** * Check for SIMD support (`Avx2.IsSupported`). -* Use `stackalloc` for small buffers (threshold ~256). +* Use `stackalloc` for small buffers (threshold ~256) and for internal state buffers in recursive algorithms where SIMD is not applicable. * Implement a scalar fallback path that handles `NaN` safely. * Implement a SIMD path for large, clean datasets (optional but recommended for simple averages). @@ -132,6 +132,7 @@ Follow the standard template and ensure strict adherence to Markdownlint rules, * **MD030:** Ensure exactly one space after list markers (e.g., `* Item`, not `*Item` or `* Item`). * **MD032:** Ensure lists are surrounded by blank lines (one blank line before the first item and one after the last item). +* **No Issues:** Ensure that markdownlint shows no issues for the file. Template structure: diff --git a/AGENTS.md b/AGENTS.md index 013d4f80..8c5fde12 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ We do not store objects in lists. We store primitive arrays. 1. **Zero Allocation**: The `Update` method MUST NOT allocate memory on the heap. Use `stackalloc` or pre-allocated buffers. 2. **O(1) Complexity**: Streaming updates must be constant time. Use circular buffers (`RingBuffer`) or running sums. -3. **SIMD**: Batch operations (`Calculate`) should use `System.Runtime.Intrinsics` (AVX2) where possible. +3. **SIMD**: Batch operations (`Calculate`) should use `System.Runtime.Intrinsics` (AVX2) where possible. If SIMD is not possible due to recursive dependencies, use `stackalloc` for internal buffers to avoid heap allocations. 4. **Inlining**: Use `[MethodImpl(MethodImplOptions.AggressiveInlining)]` on hot methods. 5. **Locals**: Use `[SkipLocalsInit]` to avoid zero-init costs in tight loops. @@ -94,6 +94,7 @@ public TValue Update(TValue input, bool isNew = true) * **Format**: Markdown. * **Content**: Title, Description, Parameters, Formula (LaTeX), C# Usage Examples. * **Index**: Add the new indicator to the category index (e.g., `lib/trends/_index.md`). +* **Linting**: Ensure that markdownlint shows no issues for the file. ## 6. Development Checklist diff --git a/lib/trends/_index.md b/lib/trends/_index.md index d1717034..fb31d3f8 100644 --- a/lib/trends/_index.md +++ b/lib/trends/_index.md @@ -43,7 +43,7 @@ Trend indicators help identify the direction and strength of a market trend. Mov | [LSMA](lsma/Lsma.md) | Least Squares Moving Average | Calculates the linear regression line for a specified period. | | LTMA | Linear Trend MA | | | [MAMA](mama/Mama.md) | Ehlers MESA Adaptive MA | Adapts to market cycles using Hilbert Transform phase measurement. | -| MGDI | McGinley Dynamic Indicator | | +| [MGDI](mgdi/Mgdi.md) | McGinley Dynamic Indicator | A moving average that adjusts for shifts in market speed to minimize lag and whipsaws. | | MMA | Modified MA | | | NOTCH | Notch Filter | | | [PWMA](pwma/Pwma.md) | Parabolic Weighted MA | Uses parabolic weighting ($i^2$) to give more weight to recent data. | diff --git a/lib/trends/htit/Htit.cs b/lib/trends/htit/Htit.cs index 1c86071c..9b70addc 100644 --- a/lib/trends/htit/Htit.cs +++ b/lib/trends/htit/Htit.cs @@ -269,14 +269,14 @@ public sealed class Htit : ITValuePublisher if (len == 0) return; // Buffers - double[] priceBuffer = new double[50]; - double[] smoothBuffer = new double[7]; - double[] detrenderBuffer = new double[7]; - double[] i1Buffer = new double[7]; - double[] q1Buffer = new double[7]; - double[] periodBuffer = new double[2]; - double[] smoothPeriodBuffer = new double[2]; - double[] itBuffer = new double[4]; + Span priceBuffer = stackalloc double[50]; + Span smoothBuffer = stackalloc double[7]; + Span detrenderBuffer = stackalloc double[7]; + Span i1Buffer = stackalloc double[7]; + Span q1Buffer = stackalloc double[7]; + Span periodBuffer = stackalloc double[2]; + Span smoothPeriodBuffer = stackalloc double[2]; + Span itBuffer = stackalloc double[4]; int pIdx = 0, sIdx = 0, dIdx = 0, i1Idx = 0, q1Idx = 0, pdIdx = 0, sdIdx = 0, itIdx = 0; int pCount = 0; diff --git a/lib/trends/mgdi/Mgdi.Quantower.Tests.cs b/lib/trends/mgdi/Mgdi.Quantower.Tests.cs new file mode 100644 index 00000000..186e16e1 --- /dev/null +++ b/lib/trends/mgdi/Mgdi.Quantower.Tests.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using TradingPlatform.BusinessLayer; +using Xunit; + +namespace QuanTAlib.Quantower.Tests; + +public class MgdiIndicatorTests +{ + [Fact] + public void Indicator_Initializes_Correctly() + { + var indicator = new MgdiIndicator(); + Assert.Equal("MGDI - McGinley Dynamic Indicator", indicator.Name); + Assert.Equal("MGDI(14,0.6):Close", indicator.ShortName); + Assert.Equal(14, indicator.MinHistoryDepths); + Assert.Single(indicator.LinesSeries); + } + + [Fact] + public void Indicator_Updates_Correctly() + { + var indicator = new MgdiIndicator(); + indicator.Initialize(); + + // Warmup + for (int i = 0; i < 100; i++) + { + var time = DateTime.UtcNow.AddMinutes(i); + indicator.HistoricalData.AddBar(time, 100 + i, 100 + i, 100 + i, 100 + i); + + var args = new UpdateArgs(UpdateReason.NewBar); + indicator.ProcessUpdate(args); + } + + // Check if value is set (should be non-zero after warmup) + var result = indicator.LinesSeries[0].GetValue(); + Assert.NotEqual(0, result); + Assert.False(double.IsNaN(result)); + } +} diff --git a/lib/trends/mgdi/Mgdi.Quantower.cs b/lib/trends/mgdi/Mgdi.Quantower.cs new file mode 100644 index 00000000..1ee7b7d8 --- /dev/null +++ b/lib/trends/mgdi/Mgdi.Quantower.cs @@ -0,0 +1,69 @@ +using System; +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +public class MgdiIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)] + public int Period { get; set; } = 14; + + [InputParameter("K Factor", sortIndex: 2, 0.1, 10, 0.1, 1)] + public double K { get; set; } = 0.6; + + [IndicatorExtensions.DataSourceInput] + public SourceType Source { get; set; } = SourceType.Close; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + private Mgdi? _mgdi; + protected LineSeries? Series; + protected string? SourceName; + private int _warmupBarIndex = -1; + + public int MinHistoryDepths => Period; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public override string ShortName => $"MGDI({Period},{K}):{SourceName}"; + + public MgdiIndicator() + { + OnBackGround = true; + SeparateWindow = false; + SourceName = Source.ToString(); + Name = "MGDI - McGinley Dynamic Indicator"; + Description = "McGinley Dynamic Indicator"; + Series = new(name: "MGDI", color: Color.Orange, width: 2, style: LineStyle.Solid); + AddLineSeries(Series); + } + + protected override void OnInit() + { + _mgdi = new Mgdi(Period, K); + SourceName = Source.ToString(); + _warmupBarIndex = -1; + base.OnInit(); + } + + protected override void OnUpdate(UpdateArgs args) + { + TValue input = this.GetInputValue(args, Source); + bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar; + + TValue result = _mgdi!.Update(input, isNew); + Series!.SetValue(result.Value); + Series!.SetMarker(0, Color.Transparent); + + if (_warmupBarIndex < 0 && _mgdi.IsHot) + _warmupBarIndex = Count; + } + + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count; + this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2); + } +} diff --git a/lib/trends/mgdi/Mgdi.Tests.cs b/lib/trends/mgdi/Mgdi.Tests.cs new file mode 100644 index 00000000..4e53d29a --- /dev/null +++ b/lib/trends/mgdi/Mgdi.Tests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using QuanTAlib; + +namespace QuanTAlib.Tests; + +public class MgdiTests +{ + private readonly GBM _gbm; + + public MgdiTests() + { + _gbm = new GBM(); + } + + [Fact] + public void IsHot_BecomesTrue_AfterPeriod() + { + var mgdi = new Mgdi(14); + for (int i = 0; i < 14; i++) + { + Assert.False(mgdi.IsHot); + mgdi.Update(new TValue(DateTime.UtcNow.Ticks, 100.0)); + } + Assert.True(mgdi.IsHot); + } + + [Fact] + public void Update_Matches_Calculate() + { + var mgdi = new Mgdi(14); + var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close; + var series = data; + + var resultSeries = mgdi.Update(series); + + // Reset and calculate streaming + mgdi.Reset(); + var streamingResults = new List(); + foreach (var item in data) + { + streamingResults.Add(mgdi.Update(item).Value); + } + + for (int i = 0; i < resultSeries.Count; i++) + { + Assert.Equal(resultSeries.Values[i], streamingResults[i], 1e-9); + } + } + + [Fact] + public void Calculate_Span_Matches_Update() + { + var mgdi = new Mgdi(14); + var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close; + var series = data; + + var resultSeries = mgdi.Update(series); + + var spanInput = data.Values.ToArray(); + var spanOutput = new double[spanInput.Length]; + + Mgdi.Calculate(spanInput, spanOutput, 14); + + for (int i = 0; i < resultSeries.Count; i++) + { + Assert.Equal(resultSeries.Values[i], spanOutput[i], 1e-9); + } + } + + [Fact] + public void Handles_NaN() + { + var mgdi = new Mgdi(14); + mgdi.Update(new TValue(DateTime.UtcNow.Ticks, 100.0)); + mgdi.Update(new TValue(DateTime.UtcNow.Ticks, double.NaN)); + + // Should use last valid value (100.0) for calculation + // MGDI = 100 + (100 - 100) / ... = 100 + Assert.Equal(100.0, mgdi.Last.Value); + } + + [Fact] + public void Constructor_Throws_On_Invalid_Period() + { + Assert.Throws(() => new Mgdi(0)); + } + + [Fact] + public void Constructor_Throws_On_Invalid_K() + { + Assert.Throws(() => new Mgdi(14, 0)); + Assert.Throws(() => new Mgdi(14, -1)); + Assert.Throws(() => new Mgdi(14, double.NaN)); + Assert.Throws(() => new Mgdi(14, double.PositiveInfinity)); + } +} diff --git a/lib/trends/mgdi/Mgdi.Validation.Tests.cs b/lib/trends/mgdi/Mgdi.Validation.Tests.cs new file mode 100644 index 00000000..4c8b39b2 --- /dev/null +++ b/lib/trends/mgdi/Mgdi.Validation.Tests.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Skender.Stock.Indicators; +using OoplesFinance.StockIndicators; +using OoplesFinance.StockIndicators.Models; +using Xunit; +using QuanTAlib; + +namespace QuanTAlib.Tests; + +public class MgdiValidationTests : IDisposable +{ + private readonly ValidationTestData _data; + + public MgdiValidationTests() + { + _data = new ValidationTestData(5000); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _data.Dispose(); + } + } + + [Fact] + public void Validate_Skender_Batch() + { + // Calculate Skender MGDI + // Skender uses Dynamic(14, 0.6) by default if not specified, but let's be explicit + var skenderResults = _data.SkenderQuotes.GetDynamic(14, 0.6).ToList(); + + // Calculate QuanTAlib MGDI + var mgdi = new Mgdi(14, 0.6); + var series = _data.Data; + var quantalibResults = mgdi.Update(series); + + // Compare results + // Skip warmup period + for (int i = quantalibResults.Count - 100; i < quantalibResults.Count; i++) + { + double skenderValue = skenderResults[i].Dynamic ?? double.NaN; + double quantalibValue = quantalibResults.Values[i]; + + if (!double.IsNaN(skenderValue)) + { + Assert.Equal(skenderValue, quantalibValue, 1e-6); + } + } + } + + [Fact] + public void Validate_Skender_Streaming() + { + // Calculate Skender MGDI + var skenderResults = _data.SkenderQuotes.GetDynamic(14, 0.6).ToList(); + + // Calculate QuanTAlib MGDI Streaming + var mgdi = new Mgdi(14, 0.6); + var streamingResults = new List(); + + foreach (var item in _data.Data) + { + streamingResults.Add(mgdi.Update(item).Value); + } + + // Compare results + for (int i = streamingResults.Count - 100; i < streamingResults.Count; i++) + { + double skenderValue = skenderResults[i].Dynamic ?? double.NaN; + double quantalibValue = streamingResults[i]; + + if (!double.IsNaN(skenderValue)) + { + Assert.Equal(skenderValue, quantalibValue, 1e-6); + } + } + } + + [Fact] + public void Validate_Ooples() + { + // Prepare data for Ooples + var ooplesData = _data.SkenderQuotes.Select(q => new TickerData + { + Date = q.Date, + Open = (double)q.Open, + High = (double)q.High, + Low = (double)q.Low, + Close = (double)q.Close, + Volume = (double)q.Volume + }).ToList(); + + // Calculate Ooples MGDI + var stockData = new StockData(ooplesData); + var oResult = stockData.CalculateMcGinleyDynamicIndicator(length: 14); + var oValues = oResult.OutputValues["Mdi"]; + + // Calculate QuanTAlib MGDI + var mgdi = new Mgdi(14, 0.6); + var series = _data.Data; + var quantalibResults = mgdi.Update(series); + + // Compare results + for (int i = quantalibResults.Count - 100; i < quantalibResults.Count; i++) + { + double ooplesValue = oValues[i]; + double quantalibValue = quantalibResults.Values[i]; + + // Ooples might use a slightly different formula or precision + // We'll check for close correlation using relative error + double diff = Math.Abs(ooplesValue - quantalibValue); + double relError = diff / ooplesValue; + Assert.True(relError < 1e-9, $"Relative error {relError} too high at index {i}"); + } + } +} diff --git a/lib/trends/mgdi/Mgdi.cs b/lib/trends/mgdi/Mgdi.cs new file mode 100644 index 00000000..e89bfe51 --- /dev/null +++ b/lib/trends/mgdi/Mgdi.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using QuanTAlib; + +namespace QuanTAlib; + +/// +/// MGDI: McGinley Dynamic Indicator +/// A moving average that adjusts for shifts in market speed, designed to track the market better than existing indicators. +/// It looks like a moving average line, yet it is a smoothing mechanism for prices that turns out to track far better than any moving average. +/// It minimizes price separation and price hugs to avoid whipsaws. +/// +/// +/// Sources: +/// https://www.investopedia.com/terms/m/mcginley-dynamic.asp +/// https://dotnet.stockindicators.dev/indicators/Dynamic/ +/// Formula: MGDI = MGDI[1] + (Price - MGDI[1]) / (k * N * (Price/MGDI[1])^4) +/// Default k = 0.6 +/// +[SkipLocalsInit] +public sealed class Mgdi : ITValuePublisher +{ + public string Name { get; } + public bool IsHot { get; private set; } + public event Action? Pub; + public TValue Last { get; private set; } + + private readonly int _period; + private readonly double _k; + + private record struct State(double LastMgdi, double LastValidValue, int Count); + private State _state; + private State _p_state; + + public Mgdi(int period = 14, double k = 0.6) + { + if (period < 1) throw new ArgumentOutOfRangeException(nameof(period)); + if (double.IsNaN(k) || double.IsInfinity(k) || k <= 0) throw new ArgumentOutOfRangeException(nameof(k), "k must be a finite value greater than 0"); + _period = period; + _k = k; + Name = $"Mgdi({period},{k})"; + Init(); + } + + public Mgdi(ITValuePublisher source, int period = 14, double k = 0.6) : this(period, k) + { + source.Pub += (item) => Update(item); + } + + public void Init() + { + _state = default; + _p_state = default; + IsHot = false; + Last = default; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TValue input, bool isNew = true) + { + if (isNew) _p_state = _state; + else _state = _p_state; + + if (isNew) _state.Count++; + + double price = input.Value; + if (!double.IsFinite(price)) + { + price = _state.LastValidValue; + } + else + { + _state.LastValidValue = price; + } + + if (_state.Count == 1) + { + _state.LastMgdi = price; + } + else + { + double prev = _state.LastMgdi; + if (Math.Abs(prev) > double.Epsilon) + { + double ratio = price / prev; + double ratio4 = ratio * ratio; + ratio4 *= ratio4; + + double denominator = _k * _period * ratio4; + _state.LastMgdi = prev + (price - prev) / denominator; + } + else + { + _state.LastMgdi = price; + } + } + + IsHot = _state.Count >= _period; + Last = new TValue(input.Time, _state.LastMgdi); + Pub?.Invoke(Last); + return Last; + } + + public TSeries Update(TSeries source) + { + if (source.Count == 0) return []; + + int len = source.Count; + var t = new List(len); + var v = new List(len); + CollectionsMarshal.SetCount(t, len); + CollectionsMarshal.SetCount(v, len); + + var tSpan = CollectionsMarshal.AsSpan(t); + var vSpan = CollectionsMarshal.AsSpan(v); + + Calculate(source.Values, vSpan, _period, _k); + source.Times.CopyTo(tSpan); + + // Restore state + Init(); + // Replay last portion to restore state + int startIndex = Math.Max(0, len - Math.Max(_period * 2, 100)); + for (int i = startIndex; i < len; i++) + { + Update(new TValue(source.Times[i], source.Values[i])); + } + + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); + return new TSeries(t, v); + } + + public static TSeries Calculate(TSeries source, int period = 14, double k = 0.6) + { + var mgdi = new Mgdi(period, k); + return mgdi.Update(source); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Calculate(ReadOnlySpan source, Span output, int period = 14, double k = 0.6) + { + if (source.Length != output.Length) + throw new ArgumentException("Source and output must have the same length"); + + if (source.Length == 0) return; + + double lastMgdi = source[0]; + double lastValid = source[0]; + output[0] = lastMgdi; + + for (int i = 1; i < source.Length; i++) + { + double price = source[i]; + if (!double.IsFinite(price)) price = lastValid; + else lastValid = price; + + if (Math.Abs(lastMgdi) > double.Epsilon) + { + double ratio = price / lastMgdi; + double ratio4 = ratio * ratio; + ratio4 *= ratio4; + + double denominator = k * period * ratio4; + lastMgdi += (price - lastMgdi) / denominator; + } + else + { + lastMgdi = price; + } + + output[i] = lastMgdi; + } + } + + public void Reset() + { + Init(); + } +} diff --git a/lib/trends/mgdi/Mgdi.md b/lib/trends/mgdi/Mgdi.md new file mode 100644 index 00000000..8fe0095e --- /dev/null +++ b/lib/trends/mgdi/Mgdi.md @@ -0,0 +1,89 @@ +# MGDI - McGinley Dynamic Indicator + +The McGinley Dynamic Indicator (MGDI) is a type of moving average that was designed to track the market better than existing moving average indicators. It is a technical indicator that improves upon moving average lines by adjusting for shifts in market speed. + +## Core Concepts + +The McGinley Dynamic Indicator solves the problem of varying market speeds by incorporating an automatic adjustment factor into its formula. This factor speeds up or slows down the indicator in trending or ranging markets. + +* **Adaptive:** Automatically adjusts to the speed of the market. +* **Smoothing:** Minimizes price separation and "price hugs" to avoid whipsaws. +* **Lag Reduction:** Reduces lag compared to traditional moving averages like SMA or EMA. + +## Parameters + +| Parameter | Type | Default | Description | +| :--- | :--- | :--- | :--- | +| `period` | `int` | 14 | The number of periods used for the calculation (N). | +| `k` | `double` | 0.6 | A constant factor, typically 60% (0.6). | + +## Formula + +The formula for the McGinley Dynamic Indicator is: + +$$ +MGDI_i = MGDI_{i-1} + \frac{Price_i - MGDI_{i-1}}{k \times N \times (\frac{Price_i}{MGDI_{i-1}})^4} +$$ + +Where: + +* $MGDI_i$ is the current McGinley Dynamic value. +* $MGDI_{i-1}$ is the previous McGinley Dynamic value. +* $Price_i$ is the current price. +* $N$ is the period (number of periods). +* $k$ is the constant factor (usually 0.6). + +## C# Implementation + +### Standard Usage + +```csharp +using QuanTAlib; + +// Create the indicator with default parameters (Period=14, k=0.6) +var mgdi = new Mgdi(period: 14, k: 0.6); + +// Update with a new value +var result = mgdi.Update(new TValue(DateTime.UtcNow, 100.0)); + +Console.WriteLine($"MGDI: {result.Value}"); +``` + +### Span API (High Performance) + +```csharp +using QuanTAlib; + +double[] input = { ... }; // Your price data +double[] output = new double[input.Length]; + +// Calculate MGDI over the entire span +Mgdi.Calculate(input, output, period: 14, k: 0.6); +``` + +### Event-Driven Usage + +```csharp +using QuanTAlib; + +var source = new ObservableSource(); +var mgdi = new Mgdi(source, period: 14, k: 0.6); + +mgdi.Pub += (result) => { + Console.WriteLine($"New MGDI Value: {result.Value}"); +}; + +// When source updates, mgdi will automatically calculate and publish +``` + +## Interpretation + +* **Trend Identification:** Like other moving averages, the MGDI helps identify the trend direction. If the price is above the MGDI line, it suggests an uptrend. If below, a downtrend. +* **Support/Resistance:** The MGDI line can act as dynamic support or resistance levels. +* **Crossovers:** Price crossovers with the MGDI line can signal potential entry or exit points, though it is designed to be a better trend follower than a signal generator. +* **Market Speed:** Because it adjusts to market speed, it hugs prices more closely in fast markets and moves further away in slow markets, reducing false signals. + +## References + +* [Investopedia: McGinley Dynamic Indicator](https://www.investopedia.com/terms/m/mcginley-dynamic.asp) +* [Stock Indicators for .NET: McGinley Dynamic](https://dotnet.stockindicators.dev/indicators/Dynamic/)