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