using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Buffers; namespace QuanTAlib; /// /// MACD: Moving Average Convergence Divergence /// /// /// MACD is a trend-following momentum indicator that shows the relationship between /// two moving averages of a security's price. /// /// Calculation: /// MACD Line = Fast EMA - Slow EMA /// Signal Line = EMA(MACD Line) /// Histogram = MACD Line - Signal Line /// /// Standard parameters: 12, 26, 9 /// [SkipLocalsInit] public sealed class Macd : ITValuePublisher, IDisposable { private readonly Ema _fastEma; private readonly Ema _slowEma; private readonly Ema _signalEma; private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler _handler; private bool _disposed; public string Name { get; } public bool IsHot => _fastEma.IsHot && _slowEma.IsHot && _signalEma.IsHot; public int WarmupPeriod { get; } public TValue Last { get; private set; } public TValue Signal { get; private set; } public TValue Histogram { get; private set; } public event TValuePublishedHandler? Pub; public Macd(int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9) { _fastEma = new Ema(fastPeriod); _slowEma = new Ema(slowPeriod); _signalEma = new Ema(signalPeriod); _handler = Handle; Name = $"Macd({fastPeriod},{slowPeriod},{signalPeriod})"; WarmupPeriod = Math.Max(fastPeriod, slowPeriod) + signalPeriod - 2; } public Macd(ITValuePublisher source, int fastPeriod = 12, int slowPeriod = 26, int signalPeriod = 9) : this(fastPeriod, slowPeriod, signalPeriod) { _source = source; _source.Pub += _handler; } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } private void Dispose(bool disposing) { if (!_disposed) { if (disposing && _source != null) { _source.Pub -= _handler; } _disposed = true; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _fastEma.Reset(); _slowEma.Reset(); _signalEma.Reset(); Last = default; Signal = default; Histogram = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { var fast = _fastEma.Update(input, isNew); var slow = _slowEma.Update(input, isNew); double macdValue = fast.Value - slow.Value; var macdTValue = new TValue(input.Time, macdValue); var signal = _signalEma.Update(macdTValue, isNew); double histValue = macdValue - signal.Value; Last = macdTValue; Signal = signal; Histogram = new TValue(input.Time, histValue); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } public TSeries Update(TSeries source) { if (source.Count == 0) return []; var 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); Reset(); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); tSpan[i] = source[i].Time; vSpan[i] = Last.Value; } return new TSeries(t, v); } private void Handle(object? sender, in TValueEventArgs args) { Update(args.Value, args.IsNew); } /// /// Calculates the MACD Line (Fast EMA - Slow EMA). /// Does not calculate Signal or Histogram. /// public static void Calculate(ReadOnlySpan source, Span destination, int fastPeriod = 12, int slowPeriod = 26) { if (source.Length != destination.Length) throw new ArgumentException("Source and destination must be same length", nameof(destination)); int len = source.Length; double[] fastBuffer = ArrayPool.Shared.Rent(len); double[] slowBuffer = ArrayPool.Shared.Rent(len); try { Span fastSpan = fastBuffer.AsSpan(0, len); Span slowSpan = slowBuffer.AsSpan(0, len); Ema.Batch(source, fastSpan, fastPeriod); Ema.Batch(source, slowSpan, slowPeriod); SimdExtensions.Subtract(fastSpan, slowSpan, destination); } finally { ArrayPool.Shared.Return(fastBuffer); ArrayPool.Shared.Return(slowBuffer); } } }