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);
}
}