using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// IMPULSE: Elder Impulse System /// /// /// Combines a 13-period EMA (inertia) with MACD(12,26,9) histogram (momentum) /// to classify each bar as bullish, bearish, or neutral. /// /// Calculation: /// /// Green (+1): EMA rising AND MACD-Histogram rising /// Red (-1): EMA falling AND MACD-Histogram falling /// Blue ( 0): Mixed signals (neither green nor red) /// /// /// Key characteristics: /// - O(1) update complexity per bar /// - Composes internal EMA and MACD child indicators /// - Output value is the 13-period EMA (suitable for overlay plotting) /// - Signal property provides the discrete impulse state (-1, 0, +1) /// - Default parameters: EMA(13), MACD(12,26,9) per Alexander Elder /// /// Detailed documentation [SkipLocalsInit] public sealed class Impulse : ITValuePublisher, IDisposable { private readonly Ema _ema; private readonly Macd _macd; private readonly ITValuePublisher? _source; private readonly TValuePublishedHandler _handler; private bool _disposed; private double _prevEma; private double _prevHistogram; private int _sampleCount; // Snapshot state for bar correction private double _p_prevEma; private double _p_prevHistogram; private int _p_sampleCount; /// Display name for the indicator. public string Name { get; } /// Current EMA value (suitable for overlay plotting). public TValue Last { get; private set; } /// Current impulse signal: +1 (bullish/green), -1 (bearish/red), 0 (neutral/blue). public int Signal { get; private set; } /// True when both EMA and MACD are warmed up and comparison values exist. public bool IsHot => _sampleCount > 1 && _ema.IsHot && _macd.IsHot; /// Bars required for the indicator to warm up. public int WarmupPeriod { get; } /// The EMA period parameter. public int EmaPeriod { get; } /// The MACD fast period parameter. public int MacdFast { get; } /// The MACD slow period parameter. public int MacdSlow { get; } /// The MACD signal period parameter. public int MacdSignal { get; } public event TValuePublishedHandler? Pub; /// /// Creates an Elder Impulse System indicator. /// /// EMA period for trend inertia (default 13). /// MACD fast EMA period (default 12). /// MACD slow EMA period (default 26). /// MACD signal EMA period (default 9). public Impulse(int emaPeriod = 13, int macdFast = 12, int macdSlow = 26, int macdSignal = 9) { if (emaPeriod < 1) { throw new ArgumentException("EMA period must be at least 1.", nameof(emaPeriod)); } if (macdFast < 1) { throw new ArgumentException("MACD fast period must be at least 1.", nameof(macdFast)); } if (macdSlow < 1) { throw new ArgumentException("MACD slow period must be at least 1.", nameof(macdSlow)); } if (macdSignal < 1) { throw new ArgumentException("MACD signal period must be at least 1.", nameof(macdSignal)); } EmaPeriod = emaPeriod; MacdFast = macdFast; MacdSlow = macdSlow; MacdSignal = macdSignal; _ema = new Ema(emaPeriod); _macd = new Macd(macdFast, macdSlow, macdSignal); _handler = Handle; Name = $"Impulse({emaPeriod},{macdFast},{macdSlow},{macdSignal})"; WarmupPeriod = Math.Max(emaPeriod, macdSlow) + macdSignal - 1; } /// /// Creates an Elder Impulse System chained to a source publisher. /// public Impulse(ITValuePublisher source, int emaPeriod = 13, int macdFast = 12, int macdSlow = 26, int macdSignal = 9) : this(emaPeriod, macdFast, macdSlow, macdSignal) { _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() { _ema.Reset(); _macd.Reset(); _prevEma = 0; _prevHistogram = 0; _sampleCount = 0; _p_prevEma = 0; _p_prevHistogram = 0; _p_sampleCount = 0; Signal = 0; Last = default; } /// /// Updates the Elder Impulse System with a new close price value. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { // State management for bar correction if (isNew) { _p_prevEma = _prevEma; _p_prevHistogram = _prevHistogram; _p_sampleCount = _sampleCount; } else { _prevEma = _p_prevEma; _prevHistogram = _p_prevHistogram; _sampleCount = _p_sampleCount; } // Update child indicators var emaResult = _ema.Update(input, isNew); _ = _macd.Update(input, isNew); double currentEma = emaResult.Value; double currentHistogram = _macd.Histogram.Value; // Classify impulse if (_sampleCount > 0) { bool emaRising = currentEma > _prevEma; bool emaFalling = currentEma < _prevEma; bool histRising = currentHistogram > _prevHistogram; bool histFalling = currentHistogram < _prevHistogram; if (emaRising && histRising) { Signal = 1; } else if (emaFalling && histFalling) { Signal = -1; } else { Signal = 0; } } else { Signal = 0; } // Advance state _prevEma = currentEma; _prevHistogram = currentHistogram; if (isNew) { _sampleCount++; } Last = emaResult; Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates with a price bar (uses Close price). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true) { return Update(new TValue(bar.Time, bar.Close), isNew); } /// /// Updates with a value series. /// public TSeries Update(TSeries source) { if (source.Count == 0) { return new TSeries([], []); } 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); 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); } /// /// Updates with a bar series. /// public TSeries Update(TBarSeries source) { if (source.Count == 0) { return new TSeries([], []); } 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); var times = source.Open.Times; Reset(); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); tSpan[i] = times[i]; vSpan[i] = Last.Value; } return new TSeries(t, v); } /// /// Primes the indicator with historical data. /// public void Prime(TSeries source) { Reset(); for (int i = 0; i < source.Count; i++) { Update(new TValue(new DateTime(source.Times[i], DateTimeKind.Utc), source.Values[i]), isNew: true); } } /// /// Batch calculation returning EMA values. /// public static TSeries Batch(TSeries source, int emaPeriod = 13, int macdFast = 12, int macdSlow = 26, int macdSignal = 9) { var indicator = new Impulse(emaPeriod, macdFast, macdSlow, macdSignal); return indicator.Update(source); } /// /// Returns the indicator and its results. /// public static (TSeries Results, Impulse Indicator) Calculate(TSeries source, int emaPeriod = 13, int macdFast = 12, int macdSlow = 26, int macdSignal = 9) { var indicator = new Impulse(emaPeriod, macdFast, macdSlow, macdSignal); var results = indicator.Update(source); return (results, indicator); } /// /// Returns the indicator and its results for a bar series. /// public static (TSeries Results, Impulse Indicator) Calculate(TBarSeries source, int emaPeriod = 13, int macdFast = 12, int macdSlow = 26, int macdSignal = 9) { var indicator = new Impulse(emaPeriod, macdFast, macdSlow, macdSignal); var results = indicator.Update(source); return (results, indicator); } private void Handle(object? sender, in TValueEventArgs args) { Update(args.Value, args.IsNew); } }