using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// AMAT: Archer Moving Averages Trends /// /// /// Trend system requiring fast/slow EMA alignment in same direction for signals. /// Returns +1 (bullish), -1 (bearish), or 0 (neutral) with strength percentage. /// /// Signal: +1 when FastEMA > SlowEMA and both rising; -1 when FastEMA < SlowEMA and both falling. /// /// Detailed documentation [SkipLocalsInit] public sealed class Amat : ITValuePublisher, IDisposable { [StructLayout(LayoutKind.Auto)] private record struct State( double FastEma, double SlowEma, double FastE, double SlowE, double PrevFastEma, double PrevSlowEma, bool FastIsHot, bool SlowIsHot, bool FastIsCompensated, bool SlowIsCompensated, int TickCount) { public static State New() => new() { FastEma = 0, SlowEma = 0, FastE = 1.0, SlowE = 1.0, PrevFastEma = 0, PrevSlowEma = 0, FastIsHot = false, SlowIsHot = false, FastIsCompensated = false, SlowIsCompensated = false, TickCount = 0, }; } private readonly double _fastAlpha; private readonly double _slowAlpha; private readonly double _fastDecay; private readonly double _slowDecay; private State _state = State.New(); private State _p_state = State.New(); private double _lastValidValue; private double _p_lastValidValue; private ITValuePublisher? _source; private bool _disposed; private const double COVERAGE_THRESHOLD = 0.05; private const double COMPENSATOR_THRESHOLD = 1e-10; /// /// Display name for the indicator. /// public string Name { get; } /// /// Event triggered when a new TValue is available. /// public event TValuePublishedHandler? Pub; /// /// Current trend direction: +1 (bullish), -1 (bearish), 0 (neutral). /// public TValue Last { get; private set; } /// /// Current trend strength as percentage: |Fast - Slow| / Slow * 100. /// public TValue Strength { get; private set; } /// /// Current Fast EMA value. /// public TValue FastEma { get; private set; } /// /// Current Slow EMA value. /// public TValue SlowEma { get; private set; } /// /// True if both EMAs have warmed up and are providing valid results. /// public bool IsHot => _state.FastIsHot && _state.SlowIsHot; /// /// The number of bars required for the indicator to warm up. /// public int WarmupPeriod { get; } /// /// Creates AMAT with specified fast and slow periods. /// /// Fast EMA period (must be > 0) /// Slow EMA period (must be > fast period) public Amat(int fastPeriod = 10, int slowPeriod = 50) { if (fastPeriod <= 0) { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); } if (slowPeriod <= 0) { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); } if (fastPeriod >= slowPeriod) { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); } _fastAlpha = 2.0 / (fastPeriod + 1); _slowAlpha = 2.0 / (slowPeriod + 1); _fastDecay = 1.0 - _fastAlpha; _slowDecay = 1.0 - _slowAlpha; Name = $"Amat({fastPeriod},{slowPeriod})"; WarmupPeriod = slowPeriod; } /// /// Creates AMAT with specified source and periods. /// Subscribes to source.Pub event. /// /// Source to subscribe to /// Fast EMA period /// Slow EMA period public Amat(ITValuePublisher source, int fastPeriod = 10, int slowPeriod = 50) : this(fastPeriod, slowPeriod) { _source = source; source.Pub += Handle; } /// /// Releases resources and unsubscribes from the source publisher. /// public void Dispose() { if (!_disposed) { if (_source != null) { _source.Pub -= Handle; _source = null; } _disposed = true; } } /// /// Resets the AMAT state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _state = State.New(); _p_state = State.New(); _lastValidValue = 0; _p_lastValidValue = 0; Last = default; Strength = default; FastEma = default; SlowEma = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { if (double.IsFinite(input)) { _lastValidValue = input; return input; } return _lastValidValue; } /// /// Updates the indicator with a single value. /// /// Input value /// True if this is a new bar, False if it's an update to the last bar /// Updated trend value (+1, -1, or 0) [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_state = _state; _p_lastValidValue = _lastValidValue; } else { _state = _p_state; _lastValidValue = _p_lastValidValue; } double val = GetValidValue(input.Value); // Store previous EMA values before update double prevFast = _state.FastEma; double prevSlow = _state.SlowEma; // Extract state fields to local variables (record struct properties cannot be passed by ref) double fastEmaState = _state.FastEma; double fastE = _state.FastE; bool fastIsHot = _state.FastIsHot; bool fastIsCompensated = _state.FastIsCompensated; double slowEmaState = _state.SlowEma; double slowE = _state.SlowE; bool slowIsHot = _state.SlowIsHot; bool slowIsCompensated = _state.SlowIsCompensated; int tickCount = _state.TickCount; // Compute Fast EMA with compensation double fastEma = ComputeEma(val, _fastAlpha, _fastDecay, ref fastEmaState, ref fastE, ref fastIsHot, ref fastIsCompensated); // Compute Slow EMA with compensation double slowEma = ComputeEma(val, _slowAlpha, _slowDecay, ref slowEmaState, ref slowE, ref slowIsHot, ref slowIsCompensated); // Update state with new values _state = new State( FastEma: fastEmaState, SlowEma: slowEmaState, FastE: fastE, SlowE: slowE, PrevFastEma: tickCount > 0 ? prevFast : 0, PrevSlowEma: tickCount > 0 ? prevSlow : 0, FastIsHot: fastIsHot, SlowIsHot: slowIsHot, FastIsCompensated: fastIsCompensated, SlowIsCompensated: slowIsCompensated, TickCount: tickCount + 1 ); // Determine trend direction double trend = 0; double strength = 0; if (_state.TickCount >= 2) // Need at least 2 ticks to compare previous values { double prevFastCompensated = GetCompensatedValue(_state.PrevFastEma, _state.FastE * (1.0 / _fastDecay), _state.FastIsCompensated); double prevSlowCompensated = GetCompensatedValue(_state.PrevSlowEma, _state.SlowE * (1.0 / _slowDecay), _state.SlowIsCompensated); bool fastAboveSlow = fastEma > slowEma; bool fastBelowSlow = fastEma < slowEma; bool fastRising = fastEma > prevFastCompensated; bool slowRising = slowEma > prevSlowCompensated; bool fastFalling = fastEma < prevFastCompensated; bool slowFalling = slowEma < prevSlowCompensated; // Bullish: Fast > Slow AND both rising if (fastAboveSlow && fastRising && slowRising) { trend = 1.0; } // Bearish: Fast < Slow AND both falling else if (fastBelowSlow && fastFalling && slowFalling) { trend = -1.0; } // Neutral: mixed conditions else { trend = 0; } // Calculate strength if (slowEma > 0) { strength = Math.Abs(fastEma - slowEma) / slowEma * 100.0; } } Last = new TValue(input.Time, trend); Strength = new TValue(input.Time, strength); FastEma = new TValue(input.Time, fastEma); SlowEma = new TValue(input.Time, slowEma); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the indicator with a bar value. /// /// Input bar /// True if this is a new bar, False if it's an update to the last bar /// Updated trend value (+1, -1, or 0) [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true) { return Update(new TValue(bar.Time, bar.Close), isNew); } /// /// Updates the indicator with a series of values. /// /// Input series /// Series of trend values public TSeries Update(TSeries source) { if (source.Count == 0) { return []; } int len = source.Count; var t = new List(len); var v = new List(len); // Pre-size lists to avoid reallocations 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); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double GetCompensatedValue(double ema, double e, bool isCompensated) { if (isCompensated || e <= COMPENSATOR_THRESHOLD) { return ema; } return ema / (1.0 - e); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double ComputeEma(double input, double alpha, double decay, ref double ema, ref double e, ref bool isHot, ref bool isCompensated) { ema = Math.FusedMultiplyAdd(ema, decay, alpha * input); double result; if (!isCompensated) { e *= decay; if (!isHot && e <= COVERAGE_THRESHOLD) { isHot = true; } if (e <= COMPENSATOR_THRESHOLD) { isCompensated = true; result = ema; } else { result = ema / (1.0 - e); } } else { result = ema; } return result; } /// /// Initializes the indicator state using the provided bar series history. /// /// Historical bar data. public void Prime(TBarSeries source) { Reset(); if (source.Count == 0) { return; } for (int i = 0; i < source.Count; i++) { Update(source[i], isNew: true); } } /// /// Calculates AMAT trend values for a span of input values. /// /// Input values /// Output trend values (+1, -1, 0) /// Output strength values (percentage) /// Fast EMA period /// Slow EMA period [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static void Batch(ReadOnlySpan source, Span trend, Span strength, int fastPeriod = 10, int slowPeriod = 50) { if (source.Length != trend.Length) { throw new ArgumentException("Source and trend must have the same length", nameof(trend)); } if (source.Length != strength.Length) { throw new ArgumentException("Source and strength must have the same length", nameof(strength)); } if (fastPeriod <= 0) { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); } if (slowPeriod <= 0) { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); } if (fastPeriod >= slowPeriod) { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); } int len = source.Length; if (len == 0) { return; } double fastAlpha = 2.0 / (fastPeriod + 1); double slowAlpha = 2.0 / (slowPeriod + 1); // Use ArrayPool for EMA buffers 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); // Calculate Fast and Slow EMAs Ema.Batch(source, fastSpan, fastAlpha); Ema.Batch(source, slowSpan, slowAlpha); // Calculate trend and strength trend[0] = 0; strength[0] = 0; for (int i = 1; i < len; i++) { double fastEma = fastSpan[i]; double slowEma = slowSpan[i]; double prevFastEma = fastSpan[i - 1]; double prevSlowEma = slowSpan[i - 1]; bool fastAboveSlow = fastEma > slowEma; bool fastBelowSlow = fastEma < slowEma; bool fastRising = fastEma > prevFastEma; bool slowRising = slowEma > prevSlowEma; bool fastFalling = fastEma < prevFastEma; bool slowFalling = slowEma < prevSlowEma; // Bullish: Fast > Slow AND both rising if (fastAboveSlow && fastRising && slowRising) { trend[i] = 1.0; } // Bearish: Fast < Slow AND both falling else if (fastBelowSlow && fastFalling && slowFalling) { trend[i] = -1.0; } // Neutral else { trend[i] = 0; } // Strength if (slowEma > 0) { strength[i] = Math.Abs(fastEma - slowEma) / slowEma * 100.0; } else { strength[i] = 0; } } } finally { ArrayPool.Shared.Return(fastBuffer); ArrayPool.Shared.Return(slowBuffer); } } /// /// Calculates AMAT trend values for a span (trend only, no strength). /// /// Input values /// Output trend values (+1, -1, 0) /// Fast EMA period /// Slow EMA period [MethodImpl(MethodImplOptions.AggressiveOptimization)] public static void Batch(ReadOnlySpan source, Span trend, int fastPeriod = 10, int slowPeriod = 50) { if (source.Length != trend.Length) { throw new ArgumentException("Source and trend must have the same length", nameof(trend)); } if (fastPeriod <= 0) { throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod)); } if (slowPeriod <= 0) { throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod)); } if (fastPeriod >= slowPeriod) { throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod)); } int len = source.Length; if (len == 0) { return; } double fastAlpha = 2.0 / (fastPeriod + 1); double slowAlpha = 2.0 / (slowPeriod + 1); // Use single ArrayPool rent with slicing for both EMA buffers double[]? rented = ArrayPool.Shared.Rent(len * 2); try { Span buffer = rented.AsSpan(0, len * 2); Span fastSpan = buffer.Slice(0, len); Span slowSpan = buffer.Slice(len, len); // Calculate Fast and Slow EMAs Ema.Batch(source, fastSpan, fastAlpha); Ema.Batch(source, slowSpan, slowAlpha); // Calculate trend only (no strength computation needed) trend[0] = 0; for (int i = 1; i < len; i++) { double fastEma = fastSpan[i]; double slowEma = slowSpan[i]; double prevFastEma = fastSpan[i - 1]; double prevSlowEma = slowSpan[i - 1]; bool fastAboveSlow = fastEma > slowEma; bool fastBelowSlow = fastEma < slowEma; bool fastRising = fastEma > prevFastEma; bool slowRising = slowEma > prevSlowEma; bool fastFalling = fastEma < prevFastEma; bool slowFalling = slowEma < prevSlowEma; // Bullish: Fast > Slow AND both rising if (fastAboveSlow && fastRising && slowRising) { trend[i] = 1.0; } // Bearish: Fast < Slow AND both falling else if (fastBelowSlow && fastFalling && slowFalling) { trend[i] = -1.0; } // Neutral else { trend[i] = 0; } } } finally { ArrayPool.Shared.Return(rented); } } /// /// Calculates AMAT for the entire series using a new instance. /// /// Input series /// Fast EMA period /// Slow EMA period /// AMAT trend series public static TSeries Batch(TSeries source, int fastPeriod = 10, int slowPeriod = 50) { var amat = new Amat(fastPeriod, slowPeriod); return amat.Update(source); } /// /// Runs a high-performance batch calculation on history and returns /// a "Hot" Amat instance ready to process the next tick immediately. /// /// Historical time series /// Fast EMA period /// Slow EMA period /// A tuple containing the full calculation results and the hot indicator instance public static (TSeries Results, Amat Indicator) Calculate(TSeries source, int fastPeriod = 10, int slowPeriod = 50) { var amat = new Amat(fastPeriod, slowPeriod); TSeries results = amat.Update(source); return (results, amat); } }