diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 92750a50..69cde8a4 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -122,10 +122,10 @@ public class EventingTests return new TBar( DateTime.Now, baseValue, - baseValue + Math.abs(GetRandomDouble(rng) * 10), - baseValue - Math.abs(GetRandomDouble(rng) * 10), + baseValue + Math.Abs(GetRandomDouble(rng) * 10), + baseValue - Math.Abs(GetRandomDouble(rng) * 10), baseValue + (GetRandomDouble(rng) * 5), - Math.abs(GetRandomDouble(rng) * 1000), + Math.Abs(GetRandomDouble(rng) * 1000), true ); } @@ -151,7 +151,7 @@ public class EventingTests } bool areEqual = (double.IsNaN(directIndicator.Value) && double.IsNaN(eventIndicator.Value)) || - Math.abs(directIndicator.Value - eventIndicator.Value) < Tolerance; + Math.Abs(directIndicator.Value - eventIndicator.Value) < Tolerance; Assert.True(areEqual, $"Value indicator {indicatorName} failed: Expected {directIndicator.Value}, Actual {eventIndicator.Value}"); } @@ -177,7 +177,7 @@ public class EventingTests } bool areEqual = (double.IsNaN(directIndicator.Value) && double.IsNaN(eventIndicator.Value)) || - Math.abs(directIndicator.Value - eventIndicator.Value) < Tolerance; + Math.Abs(directIndicator.Value - eventIndicator.Value) < Tolerance; Assert.True(areEqual, $"Bar indicator {indicatorName} failed: Expected {directIndicator.Value}, Actual {eventIndicator.Value}"); } diff --git a/Tests/test_quantower.cs b/Tests/test_quantower.cs index 5d5eb057..2f48f245 100644 --- a/Tests/test_quantower.cs +++ b/Tests/test_quantower.cs @@ -162,7 +162,7 @@ namespace QuanTAlib [Fact] public void Vortex() => TestIndicatorMultipleFields(new[] { "PlusLine", "MinusLine" }); // Oscillators Indicators - [Fact] public void Cti() => TestIndicator("Series"); + [Fact] public void Cti() => TestIndicator("Series"); } } diff --git a/lib/oscillators/Cti.cs b/lib/oscillators/Cti.cs index 379e4b21..cebf634d 100644 --- a/lib/oscillators/Cti.cs +++ b/lib/oscillators/Cti.cs @@ -3,32 +3,37 @@ namespace QuanTAlib; /// /// CTI: Ehler's Correlation Trend Indicator -/// A momentum oscillator that measures the correlation between the price and a lagged version of the price. +/// Measures the correlation between price and an ideal trend line. /// /// /// The CTI calculation process: -/// 1. Calculate the correlation between the price and a lagged version of the price over a specified period. -/// 2. Normalize the correlation values to oscillate between -1 and 1. -/// 3. Use the normalized correlation values to calculate the CTI. +/// 1. Correlates price curve with an ideal trend line (negative count due to backwards data storage) +/// 2. Uses Spearman's correlation algorithm +/// 3. Returns values between -1 and 1 /// /// Key characteristics: /// - Oscillates between -1 and 1 -/// - Positive values indicate bullish momentum -/// - Negative values indicate bearish momentum +/// - Positive values indicate price follows uptrend +/// - Negative values indicate price follows downtrend /// /// Formula: -/// CTI = 2 * (Correlation - 0.5) +/// CTI = (n∑xy - ∑x∑y) / sqrt((n∑x² - (∑x)²)(n∑y² - (∑y)²)) +/// where: +/// x = price curve +/// y = -count (ideal trend line) +/// n = period length /// /// Sources: /// John Ehlers - "Cybernetic Analysis for Stocks and Futures" (2004) -/// https://www.investopedia.com/terms/c/correlation-trend-indicator.asp +/// John Ehlers, Correlation Trend Indicator, Stocks & Commodities May-2020 /// [SkipLocalsInit] public sealed class Cti : AbstractBase { private readonly int _period; private readonly CircularBuffer _priceBuffer; - private readonly Corr _correlation; + private readonly double[] _trendLine; + private const int MinimumPoints = 2; // Minimum points needed for correlation /// The data source object that publishes updates. /// The calculation period (default: 20) @@ -44,7 +49,14 @@ public sealed class Cti : AbstractBase { _period = period; _priceBuffer = new CircularBuffer(period); - _correlation = new Corr(period); + + // Pre-calculate trend line values since they're static + _trendLine = new double[period]; + for (int i = 0; i < period; i++) + { + _trendLine[i] = -i; // negative count for backwards data + } + WarmupPeriod = period; Name = "CTI"; } @@ -62,15 +74,36 @@ public sealed class Cti : AbstractBase protected override double Calculation() { ManageState(Input.IsNew); - _priceBuffer.Add(Input.Value, Input.IsNew); - var laggedPrice = _index >= _period ? _priceBuffer[_index - _period] : double.NaN; - _correlation.Calc(new TValue(Input.Time, Input.Value, Input.IsNew), new TValue(Input.Time, laggedPrice, Input.IsNew)); + // Use available points for early calculations + int points = Math.Min(_index + 1, _period); + if (points < MinimumPoints) return 0; // Need at least 2 points for correlation - if (_index < _period - 1) return double.NaN; + double sx = 0, sy = 0, sxx = 0, sxy = 0, syy = 0; - var correlation = _correlation.Value; - return 2 * (correlation - 0.5); + // Calculate correlation components using available points + for (int i = 0; i < points; i++) + { + double x = _priceBuffer[i]; // price curve + double y = _trendLine[i]; // pre-calculated trend line + + sx += x; + sy += y; + sxx += x * x; + sxy += x * y; + syy += y * y; + } + + // Check for numerical stability + double denomX = points * sxx - sx * sx; + double denomY = points * syy - sy * sy; + + if (denomX > 0 && denomY > 0) + { + return (points * sxy - sx * sy) / Math.Sqrt(denomX * denomY); + } + + return 0; } } diff --git a/quantower/Averages/WmaIndicator.cs b/quantower/Averages/WmaIndicator.cs index bee41d44..0c76128d 100644 --- a/quantower/Averages/WmaIndicator.cs +++ b/quantower/Averages/WmaIndicator.cs @@ -59,6 +59,7 @@ public class WmaIndicator : Indicator, IWatchlistIndicator Series!.SetValue(result.Value); Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here } +#pragma warning disable CA1416 // Validate platform compatibility public override void OnPaintChart(PaintChartEventArgs args) { diff --git a/quantower/Oscillators/CtiIndicator.cs b/quantower/Oscillators/CtiIndicator.cs index f85e2384..51cdb57b 100644 --- a/quantower/Oscillators/CtiIndicator.cs +++ b/quantower/Oscillators/CtiIndicator.cs @@ -3,10 +3,10 @@ using System.Drawing; namespace QuanTAlib { - public class CtiIndicator : Indicator + public class CtiIndicator : Indicator, IWatchlistIndicator { [InputParameter("Period", 0, 1, 100, 1, 0)] - public int Period = 20; + public int Period { get; set; } = 20; [InputParameter("Source Type", 1, variants: new object[] { @@ -21,46 +21,51 @@ namespace QuanTAlib "OHLC4", SourceType.OHLC4, "HLCC4", SourceType.HLCC4 })] - public SourceType SourceType = SourceType.Close; + public SourceType Source { get; set; } = SourceType.Close; [InputParameter("Show Cold Values", 2)] - public bool ShowColdValues = false; + public bool ShowColdValues { get; set; } = true; - private Cti cti; - protected LineSeries? CtiSeries; + private Cti? cti; + protected LineSeries? Series; + protected string? SourceName; public int MinHistoryDepths => Period + 1; int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; public CtiIndicator() { + OnBackGround = false; + SeparateWindow = true; this.Name = "CTI - Ehler's Correlation Trend Indicator"; + SourceName = Source.ToString(); this.Description = "A momentum oscillator that measures the correlation between the price and a lagged version of the price."; - CtiSeries = new($"CTI {Period}", Color: IndicatorExtensions.Oscillators, 2, LineStyle.Solid); - AddLineSeries(CtiSeries); + Series = new($"CTI {Period}", color: IndicatorExtensions.Oscillators, width: 2, LineStyle.Solid); + AddLineSeries(Series); } protected override void OnInit() { cti = new Cti(this.Period); + SourceName = Source.ToString(); base.OnInit(); } protected override void OnUpdate(UpdateArgs args) { TValue input = this.GetInputValue(args, Source); - cti.Calc(value); + TValue result = cti!.Calc(input); - CtiSeries!.SetValue(cti.Value); - CtiSeries!.SetMarker(0, Color.Transparent); + Series!.SetValue(result); + Series!.SetMarker(0, Color.Transparent); } - public override string ShortName => $"CTI ({Period}:{SourceName})"; + public override string ShortName => $"CTI ({Period}:{SourceName})"; #pragma warning disable CA1416 // Validate platform compatibility - public override void OnPaintChart(PaintChartEventArgs args) - { - base.OnPaintChart(args); - this.PaintSmoothCurve(args, CtiSeries!, cti!.WarmupPeriod, showColdValues: ShowColdValues, tension: 0.2); - } + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + this.PaintSmoothCurve(args, Series!, cti!.WarmupPeriod, showColdValues: ShowColdValues, tension: 0.0); + } } }