using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// CHOP: Choppiness Index /// /// /// Non-directional indicator measuring market trendiness (E.W. Dreiss). /// Range [0-100]: Low values indicate trending, high values indicate choppy/sideways markets. /// /// Calculation: CHOP = 100 × LOG10(SUM(TR, n) / (MaxHigh - MinLow)) / LOG10(n). /// /// Key Levels: /// - Above 61.8: Market is consolidating (choppy) /// - Below 38.2: Market is trending /// - 50: Neutral midpoint /// /// Detailed documentation [SkipLocalsInit] public sealed class Chop : ITValuePublisher { private readonly int _period; private readonly RingBuffer _trValues; private readonly RingBuffer _highs; private readonly RingBuffer _lows; // Bar correction state private double _trSum; private double _savedTrSum; private double _prevClose; private double _savedPrevClose; /// /// Display name for the indicator. /// public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Current CHOP value. /// public TValue Last { get; private set; } /// /// True if the indicator has enough data for a full period calculation. /// public bool IsHot => _trValues.IsFull; /// /// The period parameter. /// public int Period => _period; /// /// The number of bars required for the indicator to warm up. /// public int WarmupPeriod { get; } /// /// Creates CHOP indicator with specified period. /// /// Lookback period (must be >= 2) public Chop(int period = 14) { if (period < 2) { throw new ArgumentException("Period must be at least 2", nameof(period)); } _period = period; Name = $"CHOP({period})"; WarmupPeriod = period; _trValues = new RingBuffer(period); _highs = new RingBuffer(period); _lows = new RingBuffer(period); _trSum = 0.0; _savedTrSum = 0.0; _prevClose = double.NaN; _savedPrevClose = double.NaN; } /// /// Resets the indicator state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _trValues.Clear(); _highs.Clear(); _lows.Clear(); _trSum = 0.0; _savedTrSum = 0.0; _prevClose = double.NaN; _savedPrevClose = double.NaN; Last = default; } /// /// Updates the CHOP indicator with a new bar. /// /// The price bar (High, Low, Close required) /// True for new bar, false for update of current bar /// The current CHOP value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { double high = input.High; double low = input.Low; double close = input.Close; // Handle NaN/Infinity inputs if (!double.IsFinite(high) || !double.IsFinite(low) || !double.IsFinite(close)) { Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } if (isNew) { // Save state for potential correction _savedTrSum = _trSum; _savedPrevClose = _prevClose; } else { // Restore state for correction _trSum = _savedTrSum; _prevClose = _savedPrevClose; } // Calculate True Range double pc = double.IsNaN(_prevClose) ? close : _prevClose; double tr = Math.Max(high - low, Math.Max(Math.Abs(high - pc), Math.Abs(low - pc))); // Update rolling sum: subtract old value if buffer is full if (_trValues.IsFull) { _trSum -= _trValues[0]; } // Add new values to buffers _trValues.Add(tr, isNew); _highs.Add(high, isNew); _lows.Add(low, isNew); _trSum += tr; // Update previous close for next bar if (isNew) { _prevClose = close; } // Calculate CHOP if we have enough data double chop = ComputeChop(); Last = new TValue(input.Time, chop); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates with a bar series. /// public TSeries Update(TBarSeries source) { if (source.Count == 0) { return new TSeries([], []); } int len = source.Count; var tList = new List(len); var vList = new List(len); var times = source.Open.Times; for (int i = 0; i < len; i++) { var result = Update(source[i], isNew: true); tList.Add(times[i]); vList.Add(result.Value); } return new TSeries(tList, vList); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double ComputeChop() { int count = _trValues.Count; if (count < 2) { return double.NaN; } // Find max high and min low in the period double maxHigh = double.MinValue; double minLow = double.MaxValue; var highsBuffer = _highs.InternalBuffer; var lowsBuffer = _lows.InternalBuffer; int capacity = _highs.Capacity; int start = _highs.StartIndex; for (int i = 0; i < count; i++) { int idx = (start + i) % capacity; double h = highsBuffer[idx]; double l = lowsBuffer[idx]; if (h > maxHigh) { maxHigh = h; } if (l < minLow) { minLow = l; } } double priceRange = maxHigh - minLow; // Avoid division by zero if (priceRange <= 0.0) { return double.NaN; } // CHOP = 100 * LOG10(SUM_TR / RANGE) / LOG10(n) double logRatio = Math.Log10(_trSum / priceRange); double logN = Math.Log10(count); double chop = 100.0 * logRatio / logN; // Clamp to [0, 100] return Math.Clamp(chop, 0.0, 100.0); } /// /// 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); } } /// /// Batch calculation with default parameters. /// public static TSeries Batch(TBarSeries source) { return Batch(source, period: 14); } /// /// Batch calculation with specified parameters. /// public static TSeries Batch(TBarSeries source, int period) { var indicator = new Chop(period); return indicator.Update(source); } public static (TSeries Results, Chop Indicator) Calculate(TBarSeries source) { var indicator = new Chop(); TSeries results = indicator.Update(source); return (results, indicator); } }