mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
- Added detailed comments explaining the validation limitations for MMA and ZLEMA due to differences in algorithm implementations. - Implemented validation tests for True Range against TALib and Tulip, ensuring directional agreement. - Updated Ulcer Index validation to clarify differences in algorithmic approaches between QuanTAlib and Skender. - Enhanced Ease of Movement tests to verify directional agreement with Tulip's EMV, noting differences in volume scaling. - Expanded Klinger Volume Oscillator tests to validate against Skender and Tulip, focusing on directional agreement across multiple period configurations. - Improved Negative Volume Index tests to compare percentage changes with Tulip, addressing differences in starting values. - Updated Positive Volume Index tests to validate against Tulip, emphasizing percentage change comparisons. - Enhanced Williams Accumulation/Distribution tests to verify directional agreement with Tulip, highlighting formula differences.
238 lines
6.6 KiB
C#
238 lines
6.6 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace QuanTAlib;
|
|
|
|
/// <summary>
|
|
/// CCI: Commodity Channel Index
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Measures the deviation of price from its statistical mean, normalized by mean
|
|
/// absolute deviation. Developed by Donald Lambert to identify cyclical turns.
|
|
///
|
|
/// Calculation:
|
|
/// <code>
|
|
/// 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)
|
|
/// </code>
|
|
///
|
|
/// 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.
|
|
/// </remarks>
|
|
/// <seealso href="Cci.md">Detailed documentation</seealso>
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// Event fired when a new CCI value is calculated.
|
|
/// </summary>
|
|
public event TValuePublishedHandler? Pub;
|
|
|
|
/// <summary>
|
|
/// Most recently calculated CCI value.
|
|
/// </summary>
|
|
public TValue Last => _last;
|
|
|
|
/// <summary>
|
|
/// True when the indicator has enough data for valid calculations.
|
|
/// </summary>
|
|
public bool IsHot => _sampleCount >= _period;
|
|
|
|
/// <summary>
|
|
/// The lookback period.
|
|
/// </summary>
|
|
public int Period => _period;
|
|
|
|
/// <summary>
|
|
/// Number of bars required for warmup.
|
|
/// </summary>
|
|
public int WarmupPeriod => _period;
|
|
|
|
/// <summary>
|
|
/// Returns the default warmup period (<see cref="DefaultPeriod"/>).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This static accessor is provided for backward compatibility. Prefer the instance
|
|
/// <see cref="WarmupPeriod"/> property which returns the actual configured period.
|
|
/// </remarks>
|
|
[Obsolete("Use the instance WarmupPeriod property instead. This static accessor returns the default period (20) and will be removed in a future major version.")]
|
|
public static int DefaultWarmupPeriod => DefaultPeriod;
|
|
|
|
/// <summary>
|
|
/// Creates a CCI indicator with specified period.
|
|
/// </summary>
|
|
/// <param name="period">Lookback period (must be >= 2, default 20)</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the indicator to its initial state.
|
|
/// </summary>
|
|
public void Reset()
|
|
{
|
|
_tpBuffer.Clear();
|
|
_sampleCount = 0;
|
|
_lastValid = 0;
|
|
_last = default;
|
|
_state = default;
|
|
_p_state = default;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the CCI with a new bar.
|
|
/// </summary>
|
|
/// <param name="bar">The input bar with OHLC data</param>
|
|
/// <param name="isNew">True for a new bar, false for updating current bar</param>
|
|
/// <returns>The updated CCI value</returns>
|
|
[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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates CCI from a TBarSeries.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Primes the indicator with historical bars.
|
|
/// </summary>
|
|
public void Prime(TBarSeries source)
|
|
{
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
Update(source[i], true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convenience method for batch processing.
|
|
/// </summary>
|
|
public static TSeries Batch(TBarSeries source, int period = DefaultPeriod)
|
|
{
|
|
var indicator = new Cci(period);
|
|
return indicator.Update(source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates CCI and returns both the result and the indicator instance.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|