using System; using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// GHLA: Gann High-Low Activator /// SMA-based trailing stop with three-state hysteresis trend detection. /// Output follows SMA(Low) during uptrends and SMA(High) during downtrends. /// /// /// Calculation steps: /// /// SMA_high = running sum of last N highs / N /// SMA_low = running sum of last N lows / N /// Close > SMA_high → trend = +1 (bullish), output = SMA_low /// Close < SMA_low → trend = -1 (bearish), output = SMA_high /// Between both SMAs → retain previous trend (hysteresis) /// /// /// Sources: /// Robert Krausz (1998). "The New Gann Swing Chartist" — Stocks & Commodities V.16:1 /// /// Detailed documentation [SkipLocalsInit] public sealed class Ghla : AbstractBase { private readonly RingBuffer _highBuffer; private readonly RingBuffer _lowBuffer; [StructLayout(LayoutKind.Auto)] private record struct State( double HighSum, double LowSum, double HighSumComp, double LowSumComp, int Trend, double LastValidHigh, double LastValidLow, double LastValidClose ); private State _s; private State _ps; /// /// Creates GHLA with specified SMA period. /// /// SMA lookback period (must be > 0, default 13) public Ghla(int period = 13) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } _highBuffer = new RingBuffer(period); _lowBuffer = new RingBuffer(period); Name = $"Ghla({period})"; WarmupPeriod = period; _s = default; _ps = _s; } /// /// Creates GHLA with specified source and period. /// public Ghla(ITValuePublisher source, int period = 13) : this(period) { source.Pub += Handle; } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// True when both SMA buffers are full. /// public override bool IsHot => _highBuffer.IsFull; /// /// The current trend direction: +1 bullish, -1 bearish, 0 undetermined. /// public int Trend => _s.Trend; /// /// Updates the indicator with a TBar input (preferred method). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true) { return UpdateCore(bar.Time, bar.High, bar.Low, bar.Close, isNew); } /// /// Updates the indicator with a TValue input. /// Treats the value as H=L=C (degenerate case, always neutral zone). /// Prefer Update(TBar) for standard OHLC data. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { return UpdateCore(input.Time, input.Value, input.Value, input.Value, isNew); } /// /// Updates the indicator with a bar series. /// public TSeries Update(TBarSeries 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); for (int i = 0; i < len; i++) { tSpan[i] = source[i].Time; } for (int i = 0; i < len; i++) { var result = Update(source[i], isNew: true); vSpan[i] = result.Value; } return new TSeries(t, v); } /// public override TSeries Update(TSeries source) { // TSeries has no OHLC — treat values as H=L=C (degenerate case) 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 values = source.Values; var times = source.Times; for (int i = 0; i < len; i++) { tSpan[i] = times[i]; var result = Update(new TValue(times[i], values[i]), isNew: true); vSpan[i] = result.Value; } return new TSeries(t, v); } /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { for (int i = 0; i < source.Length; i++) { Update(new TValue(DateTime.UtcNow, source[i]), isNew: true); } } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Reset() { _highBuffer.Clear(); _lowBuffer.Clear(); _s = default; _ps = _s; Last = default; } /// /// Calculates GHLA for the entire bar series using a new instance. /// public static TSeries Batch(TBarSeries source, int period = 13) { var ghla = new Ghla(period); return ghla.Update(source); } /// /// Span-based batch calculation for high, low, and close arrays. /// /// High prices. /// Low prices. /// Close prices. /// Output activator values. /// SMA lookback period. public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span output, int period = 13) { int len = high.Length; if (low.Length != len) { throw new ArgumentException("High and low spans must have the same length", nameof(low)); } if (close.Length != len) { throw new ArgumentException("High and close spans must have the same length", nameof(close)); } if (output.Length < len) { throw new ArgumentException("Output span must be at least as long as input spans", nameof(output)); } if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } if (len == 0) { return; } CalculateScalarCore(high, low, close, output, period); } /// /// Calculates GHLA and returns both results and the indicator instance. /// public static (TSeries Results, Ghla Indicator) Calculate(TBarSeries source, int period = 13) { var indicator = new Ghla(period); TSeries results = indicator.Update(source); return (results, indicator); } // ---- Private implementation ---- [MethodImpl(MethodImplOptions.AggressiveInlining)] private TValue UpdateCore(long timeTicks, double high, double low, double close, bool isNew) { // Snapshot/restore for bar correction if (isNew) { _ps = _s; } else { _s = _ps; } var s = _s; // Handle non-finite values — use last valid per component if (!double.IsFinite(high)) { high = s.LastValidHigh; } else { s.LastValidHigh = high; } if (!double.IsFinite(low)) { low = s.LastValidLow; } else { s.LastValidLow = low; } if (!double.IsFinite(close)) { close = s.LastValidClose; } else { s.LastValidClose = close; } // Update running SMA sums via ring buffers if (isNew) { // High buffer — Kahan compensated double highRemoved = _highBuffer.Count == _highBuffer.Capacity ? _highBuffer.Oldest : 0.0; double hDelta = high - highRemoved - s.HighSumComp; double hNewSum = s.HighSum + hDelta; s.HighSumComp = (hNewSum - s.HighSum) - hDelta; s.HighSum = hNewSum; _highBuffer.Add(high); // Low buffer — Kahan compensated double lowRemoved = _lowBuffer.Count == _lowBuffer.Capacity ? _lowBuffer.Oldest : 0.0; double lDelta = low - lowRemoved - s.LowSumComp; double lNewSum = s.LowSum + lDelta; s.LowSumComp = (lNewSum - s.LowSum) - lDelta; s.LowSum = lNewSum; _lowBuffer.Add(low); } else { // Bar correction: update newest value in both buffers _highBuffer.UpdateNewest(high); s.HighSum = _highBuffer.Sum; s.HighSumComp = 0; _lowBuffer.UpdateNewest(low); s.LowSum = _lowBuffer.Sum; s.LowSumComp = 0; } // Compute SMAs int count = _highBuffer.Count; double smaHigh = count > 0 ? s.HighSum / count : 0.0; double smaLow = count > 0 ? s.LowSum / count : 0.0; // Three-state hysteresis trend detection if (s.Trend == 0) { // Seed: classify first bar if (close >= smaHigh) { s.Trend = 1; } else if (close <= smaLow) { s.Trend = -1; } else { s.Trend = 1; // default bullish per Pine reference } } if (close > smaHigh) { s.Trend = 1; } else if (close < smaLow) { s.Trend = -1; } // else: retain previous trend (hysteresis zone) // Select activator: bullish → SMA(Low), bearish → SMA(High) double activator = s.Trend == 1 ? smaLow : smaHigh; _s = s; Last = new TValue(timeTicks, activator); PubEvent(Last, isNew); return Last; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span output, int period) { int len = high.Length; const int StackAllocThreshold = 256; // High circular buffer double[]? rentedHigh = period > StackAllocThreshold ? ArrayPool.Shared.Rent(period) : null; Span highBuf = rentedHigh != null ? rentedHigh.AsSpan(0, period) : stackalloc double[period]; // Low circular buffer double[]? rentedLow = period > StackAllocThreshold ? ArrayPool.Shared.Rent(period) : null; Span lowBuf = rentedLow != null ? rentedLow.AsSpan(0, period) : stackalloc double[period]; try { double highSum = 0; double highSumComp = 0; double lowSum = 0; double lowSumComp = 0; double lastValidHigh = 0; double lastValidLow = 0; double lastValidClose = 0; int highIdx = 0; int lowIdx = 0; int filled = 0; int trend = 0; // Seed lastValid values for (int k = 0; k < len; k++) { if (double.IsFinite(high[k])) { lastValidHigh = high[k]; break; } } for (int k = 0; k < len; k++) { if (double.IsFinite(low[k])) { lastValidLow = low[k]; break; } } for (int k = 0; k < len; k++) { if (double.IsFinite(close[k])) { lastValidClose = close[k]; break; } } for (int i = 0; i < len; i++) { double h = high[i]; double l = low[i]; double c = close[i]; if (double.IsFinite(h)) { lastValidHigh = h; } else { h = lastValidHigh; } if (double.IsFinite(l)) { lastValidLow = l; } else { l = lastValidLow; } if (double.IsFinite(c)) { lastValidClose = c; } else { c = lastValidClose; } // Kahan-compensated update for high buffer { double deltaH = h - (filled >= period ? highBuf[highIdx] : 0); double yH = deltaH - highSumComp; double tH = highSum + yH; highSumComp = (tH - highSum) - yH; highSum = tH; } highBuf[highIdx] = h; highIdx++; if (highIdx >= period) { highIdx = 0; } // Kahan-compensated update for low buffer { double deltaL = l - (filled >= period ? lowBuf[lowIdx] : 0); double yL = deltaL - lowSumComp; double tL = lowSum + yL; lowSumComp = (tL - lowSum) - yL; lowSum = tL; } lowBuf[lowIdx] = l; lowIdx++; if (lowIdx >= period) { lowIdx = 0; } if (filled < period) { filled++; } double smaH = highSum / filled; double smaL = lowSum / filled; // Hysteresis if (trend == 0) { if (c >= smaH) { trend = 1; } else if (c <= smaL) { trend = -1; } else { trend = 1; // default bullish per Pine reference } } if (c > smaH) { trend = 1; } else if (c < smaL) { trend = -1; } output[i] = trend == 1 ? smaL : smaH; } } finally { if (rentedHigh != null) { ArrayPool.Shared.Return(rentedHigh); } if (rentedLow != null) { ArrayPool.Shared.Return(rentedLow); } } } }