using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// CCI: Commodity Channel Index
///
///
/// Measures the deviation of price from its statistical mean, normalized by mean
/// absolute deviation. Developed by Donald Lambert to identify cyclical turns.
///
/// Calculation:
///
/// TP = (High + Low + Close) / 3
/// SMA = Simple Moving Average of TP over period
/// Mean Deviation = SUM(|TP - SMA|) / period
/// CCI = (TP - SMA) / (0.015 * Mean Deviation)
///
///
/// Key levels:
/// - Above +100: Strong uptrend, potentially overbought
/// - Below -100: Strong downtrend, potentially oversold
/// - Zero line crossover: Trend change signal
///
/// The 0.015 constant ensures approximately 70-80% of values fall between +100 and -100.
///
/// Detailed documentation
[SkipLocalsInit]
public sealed class Cci : ITValuePublisher
{
private const int DefaultPeriod = 20;
private const double LambertConstant = 0.015;
private readonly int _period;
private readonly RingBuffer _tpBuffer;
private int _sampleCount;
private double _lastValid;
private TValue _last;
// State for bar correction
[StructLayout(LayoutKind.Auto)]
private record struct State(int SampleCount, double LastValid, double Sum);
private State _state, _p_state;
///
/// Event fired when a new CCI value is calculated.
///
public event TValuePublishedHandler? Pub;
///
/// Most recently calculated CCI value.
///
public TValue Last => _last;
///
/// True when the indicator has enough data for valid calculations.
///
public bool IsHot => _sampleCount >= _period;
///
/// The lookback period.
///
public int Period => _period;
///
/// Number of bars required for warmup.
///
public int WarmupPeriod => _period;
///
/// Creates a CCI indicator with specified period.
///
/// Lookback period (must be >= 2, default 20)
public Cci(int period = DefaultPeriod)
{
if (period < 2)
{
throw new ArgumentException("Period must be >= 2", nameof(period));
}
_period = period;
_tpBuffer = new RingBuffer(period);
_sampleCount = 0;
_lastValid = 0;
_last = new TValue(DateTime.MinValue, 0);
}
///
/// Resets the indicator to its initial state.
///
public void Reset()
{
_tpBuffer.Clear();
_sampleCount = 0;
_lastValid = 0;
_last = default;
_state = default;
_p_state = default;
}
///
/// Updates the CCI with a new bar.
///
/// The input bar with OHLC data
/// True for a new bar, false for updating current bar
/// The updated CCI value
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
// State management for bar correction
if (isNew)
{
_p_state = _state;
_sampleCount++;
}
else
{
_state = _p_state;
_sampleCount = _state.SampleCount + 1;
}
// Calculate typical price
double tp = (bar.High + bar.Low + bar.Close) / 3.0;
// Handle invalid values
if (!double.IsFinite(tp))
{
tp = _lastValid;
}
else
{
_lastValid = tp;
}
// Add to buffer
_tpBuffer.Add(tp, isNew);
// Calculate CCI
double result = CalculateCci(tp);
// Save state
_state = new State(_sampleCount, _lastValid, 0);
_last = new TValue(bar.Time, result);
Pub?.Invoke(this, new TValueEventArgs { Value = _last, IsNew = isNew });
return _last;
}
///
/// Updates CCI from a TBarSeries.
///
public TSeries Update(TBarSeries source)
{
var result = new TSeries(source.Count);
for (int i = 0; i < source.Count; i++)
{
var tv = Update(source[i], true);
result.Add(tv, true);
}
return result;
}
///
/// Primes the indicator with historical bars.
///
public void Prime(TBarSeries source)
{
for (int i = 0; i < source.Count; i++)
{
Update(source[i], true);
}
}
///
/// Convenience method for batch processing.
///
public static TSeries Batch(TBarSeries source, int period = DefaultPeriod)
{
var indicator = new Cci(period);
return indicator.Update(source);
}
///
/// Calculates CCI and returns both the result and the indicator instance.
///
public static (TSeries Results, Cci Indicator) Calculate(TBarSeries source, int period = DefaultPeriod)
{
var indicator = new Cci(period);
var results = indicator.Update(source);
return (results, indicator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateCci(double currentTp)
{
int count = _tpBuffer.Count;
if (count == 0)
{
return 0.0;
}
// Calculate SMA of typical prices
double sum = 0.0;
for (int i = 0; i < count; i++)
{
sum += _tpBuffer[i];
}
double sma = sum / count;
// Calculate mean deviation
double devSum = 0.0;
for (int i = 0; i < count; i++)
{
devSum += Math.Abs(_tpBuffer[i] - sma);
}
double meanDev = devSum / count;
// Calculate CCI
if (meanDev <= double.Epsilon)
{
return 0.0;
}
return (currentTp - sma) / (LambertConstant * meanDev);
}
}