// TD_SEQ: TD Sequential // Tom DeMark's exhaustion counting system — two-phase state machine. // Phase 1 (Setup): counts consecutive closes vs close[comparePeriod]; ±9 completes. // Phase 2 (Countdown): non-consecutive close vs high[2]/low[2]; ±13 completes. // All state is O(1) scalars — no circular buffers required. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// TD_SEQ: TD Sequential /// /// /// Tom DeMark's exhaustion counting system that identifies potential trend reversals /// through two phases: /// /// Phase 1 — Setup (±1 to ±9): consecutive closes vs close[comparePeriod]. /// Positive = sell setup, negative = buy setup. Completes at ±9. /// Phase 2 — Countdown (±1 to ±13): non-consecutive close vs high/low[2]. /// Begins after a completed setup. Completes at ±13. /// /// All state maintained in O(1) scalar variables — no buffers needed beyond /// a small fixed history ring for close[comparePeriod], high[2], and low[2]. /// /// References: /// DeMark, T.R. (1994). The New Science of Technical Analysis. Wiley. /// PineScript reference: td_seq.pine /// /// [SkipLocalsInit] public sealed class TdSeq : ITValuePublisher { private readonly int _comparePeriod; private readonly int _closeSize; // = comparePeriod + 1 // Close history ring: stores last (comparePeriod+1) values so we can read close[comparePeriod] private readonly double[] _closeHist; private readonly double[] _closeSnap; private int _closeIdx; // next write slot private int _closeCount; // how many slots filled (0.._closeSize) private int _closeIdxSnap; private int _closeCountSnap; // High/Low history ring: stores last 3 values for high[2] / low[2] private readonly double[] _highHist; private readonly double[] _lowHist; private readonly double[] _highSnap; private readonly double[] _lowSnap; private int _hlIdx; // next write slot (mod 3) private int _hlCount; // how many slots filled (0..3) private int _hlIdxSnap; private int _hlCountSnap; [StructLayout(LayoutKind.Auto)] private record struct State( int SetupCount, int CountdownCount, int CountdownDir, bool SetupComplete, double LastValidClose, double LastValidHigh, double LastValidLow); private State _s; private State _ps; private readonly TBarPublishedHandler _barHandler; /// Display name of the indicator. public string Name { get; } /// Bars required before Phase 1 produces valid output. public int WarmupPeriod { get; } /// True once enough close history exists to compare close[comparePeriod]. public bool IsHot => _closeCount > _comparePeriod; /// Current setup count (−9..+9). Positive = sell setup, negative = buy setup. public int Setup => _s.SetupCount; /// Current countdown count (−13..+13). Non-zero only after a completed setup. public int Countdown => _s.CountdownCount; /// Last published TValue. Value = countdown when active; setup otherwise. public TValue Last { get; private set; } /// public event TValuePublishedHandler? Pub; /// Creates TD Sequential with the specified compare period. /// Bars back for setup comparison (default 4, must be > 0) public TdSeq(int comparePeriod = 4) { if (comparePeriod <= 0) { throw new ArgumentException("Compare period must be greater than 0", nameof(comparePeriod)); } _comparePeriod = comparePeriod; _closeSize = comparePeriod + 1; _closeHist = new double[_closeSize]; _closeSnap = new double[_closeSize]; _highHist = new double[3]; _lowHist = new double[3]; _highSnap = new double[3]; _lowSnap = new double[3]; Name = $"TdSeq({comparePeriod})"; WarmupPeriod = comparePeriod + 1; _barHandler = HandleBar; } /// Creates TD Sequential subscribed to a bar publisher. public TdSeq(TBarSeries source, int comparePeriod = 4) : this(comparePeriod) { source.Pub += _barHandler; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void HandleBar(object? sender, in TBarEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew) => Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew }); /// /// Processes a bar and returns the current indicator value. /// /// OHLCV bar (Close for setup, High/Low for countdown) /// True to advance state; false to rewrite the current bar [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { // Sanitize inputs — substitute last-valid on non-finite double close = double.IsFinite(input.Close) ? input.Close : _s.LastValidClose; double high = double.IsFinite(input.High) ? input.High : _s.LastValidHigh; double low = double.IsFinite(input.Low) ? input.Low : _s.LastValidLow; if (isNew) { // Snapshot before mutation _ps = _s; Array.Copy(_closeHist, _closeSnap, _closeSize); Array.Copy(_highHist, _highSnap, 3); Array.Copy(_lowHist, _lowSnap, 3); _closeIdxSnap = _closeIdx; _closeCountSnap = _closeCount; _hlIdxSnap = _hlIdx; _hlCountSnap = _hlCount; // Advance close ring _closeHist[_closeIdx] = close; _closeIdx = (_closeIdx + 1) % _closeSize; if (_closeCount < _closeSize) { _closeCount++; } // Advance hi/lo ring _highHist[_hlIdx] = high; _lowHist[_hlIdx] = low; _hlIdx = (_hlIdx + 1) % 3; if (_hlCount < 3) { _hlCount++; } } else { // Rollback rings to snapshot _s = _ps; Array.Copy(_closeSnap, _closeHist, _closeSize); Array.Copy(_highSnap, _highHist, 3); Array.Copy(_lowSnap, _lowHist, 3); _closeIdx = _closeIdxSnap; _closeCount = _closeCountSnap; _hlIdx = _hlIdxSnap; _hlCount = _hlCountSnap; // Re-write newest slots with corrected values int newestClose = ((_closeIdx - 1) + _closeSize) % _closeSize; _closeHist[newestClose] = close; int newestHl = ((_hlIdx - 1) + 3) % 3; _highHist[newestHl] = high; _lowHist[newestHl] = low; } // Track last-valid prices for NaN substitution if (double.IsFinite(input.Close)) { _s.LastValidClose = close; } if (double.IsFinite(input.High)) { _s.LastValidHigh = high; } if (double.IsFinite(input.Low)) { _s.LastValidLow = low; } if (!IsHot) { Last = new TValue(input.Time, 0.0); PubEvent(Last, isNew); return Last; } // close[comparePeriod] = the oldest entry in the close ring: // after writing, _closeIdx points to the NEXT write slot. // That slot holds the oldest value (it is _comparePeriod bars ago). double prevClose = _closeHist[_closeIdx % _closeSize]; // --- Phase 1: Setup counting --- State s = _s; int newSetup; if (close < prevClose) { newSetup = s.SetupCount < 0 ? s.SetupCount - 1 : -1; } else if (close > prevClose) { newSetup = s.SetupCount > 0 ? s.SetupCount + 1 : 1; } else { newSetup = 0; } if (newSetup > 9) { newSetup = 9; } if (newSetup < -9) { newSetup = -9; } // Detect completed setup (first time reaching ±9) if (Math.Abs(newSetup) == 9 && !s.SetupComplete) { s.SetupComplete = true; s.CountdownCount = 0; s.CountdownDir = newSetup > 0 ? 1 : -1; } // Clear setupComplete if streak broke or reversed if (Math.Abs(newSetup) < Math.Abs(s.SetupCount) || (newSetup > 0 && s.SetupCount < 0) || (newSetup < 0 && s.SetupCount > 0)) { s.SetupComplete = false; } s.SetupCount = newSetup; // --- Phase 2: Countdown (non-consecutive) --- if (s.CountdownDir != 0 && _hlCount >= 3) { // high[2] and low[2] = oldest entry in the 3-element hi/lo ring // After writing, _hlIdx points to the next write slot = oldest slot int oldestHl = _hlIdx % 3; double high2 = _highHist[oldestHl]; double low2 = _lowHist[oldestHl]; if (s.CountdownDir == -1 && close < low2) { s.CountdownCount--; } else if (s.CountdownDir == 1 && close > high2) { s.CountdownCount++; } if (Math.Abs(s.CountdownCount) >= 13) { s.CountdownCount = s.CountdownDir == 1 ? 13 : -13; s.CountdownDir = 0; } // Opposite ±9 setup resets countdown if ((s.CountdownDir == 1 && newSetup == -9) || (s.CountdownDir == -1 && newSetup == 9)) { s.CountdownCount = 0; s.CountdownDir = newSetup > 0 ? 1 : -1; } } _s = s; // Output: countdown value when active; setup value otherwise double result = (double)(_s.CountdownDir != 0 ? _s.CountdownCount : _s.SetupCount); Last = new TValue(input.Time, result); PubEvent(Last, isNew); return Last; } /// Resets all state and history to zero. public void Reset() { _s = default; _ps = default; Array.Clear(_closeHist); Array.Clear(_closeSnap); Array.Clear(_highHist); Array.Clear(_lowHist); Array.Clear(_highSnap); Array.Clear(_lowSnap); _closeIdx = 0; _closeCount = 0; _closeIdxSnap = 0; _closeCountSnap = 0; _hlIdx = 0; _hlCount = 0; _hlIdxSnap = 0; _hlCountSnap = 0; Last = default; } /// /// Calculates TD Sequential for an entire bar series. /// /// Source bar series /// Bars back for setup comparison (default 4) /// TSeries containing the combined setup/countdown output per bar public static TSeries Calculate(TBarSeries source, int comparePeriod = 4) { var indicator = new TdSeq(comparePeriod); int len = source.Count; var results = new TSeries(); for (int i = 0; i < len; i++) { results.Add(indicator.Update(source[i], isNew: true)); } return results; } }