diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index b54a3b67..92750a50 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -87,7 +87,8 @@ public class EventingTests ("Rse", new object[] { DefaultPeriod }, new object[] { new TSeries(), DefaultPeriod }), ("Smape", new object[] { DefaultPeriod }, new object[] { new TSeries(), DefaultPeriod }), ("Rsquared", new object[] { DefaultPeriod }, new object[] { new TSeries(), DefaultPeriod }), - ("Huber", new object[] { DefaultPeriod, 1.0 }, new object[] { new TSeries(), DefaultPeriod, 1.0 }) + ("Huber", new object[] { DefaultPeriod, 1.0 }, new object[] { new TSeries(), DefaultPeriod, 1.0 }), + ("Cti", new object[] { DefaultPeriod }, new object[] { new TSeries(), DefaultPeriod }) }; private static readonly (string Name, object[] DirectParams, object[] EventParams)[] BarIndicators = new[] @@ -121,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 ); } @@ -150,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}"); } @@ -176,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 f7221176..5d5eb057 100644 --- a/Tests/test_quantower.cs +++ b/Tests/test_quantower.cs @@ -160,5 +160,9 @@ namespace QuanTAlib [Fact] public void Trix() => TestIndicator("Series"); [Fact] public void Vel() => TestIndicator("Series"); [Fact] public void Vortex() => TestIndicatorMultipleFields(new[] { "PlusLine", "MinusLine" }); + + // Oscillators Indicators + [Fact] public void Cti() => TestIndicator("Series"); + } } diff --git a/Tests/test_updates_oscillators.cs b/Tests/test_updates_oscillators.cs index cb8e38a4..636ede3a 100644 --- a/Tests/test_updates_oscillators.cs +++ b/Tests/test_updates_oscillators.cs @@ -172,4 +172,11 @@ public class OscillatorsUpdateTests : UpdateTestBase Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Cti_Update() + { + var indicator = new Cti(period: 20); + TestTValueUpdate(indicator, indicator.Calc); + } } diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index 4fdc43aa..5ea890c9 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -5,13 +5,13 @@ | Basic Transforms | 6 of 6 | 100% | | Averages & Trends | 33 of 33 | 100% | | Momentum | 16 of 16 | 100% | -| Oscillators | 22 of 29 | 76% | +| Oscillators | 24 of 29 | 83% | | Volatility | 29 of 35 | 83% | | Volume | 19 of 19 | 100% | | Numerical Analysis | 15 of 19 | 79% | | Errors | 16 of 16 | 100% | | Patterns | 0 of 8 | 0% | -| **Total** | **156 of 181** | **86%** | +| **Total** | **158 of 181** | **87%** | |Technical Indicator Name| Class Name| |-----------|:----------:| diff --git a/lib/oscillators/Cti.cs b/lib/oscillators/Cti.cs new file mode 100644 index 00000000..379e4b21 --- /dev/null +++ b/lib/oscillators/Cti.cs @@ -0,0 +1,76 @@ +using System.Runtime.CompilerServices; +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. +/// +/// +/// 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. +/// +/// Key characteristics: +/// - Oscillates between -1 and 1 +/// - Positive values indicate bullish momentum +/// - Negative values indicate bearish momentum +/// +/// Formula: +/// CTI = 2 * (Correlation - 0.5) +/// +/// Sources: +/// John Ehlers - "Cybernetic Analysis for Stocks and Futures" (2004) +/// https://www.investopedia.com/terms/c/correlation-trend-indicator.asp +/// +[SkipLocalsInit] +public sealed class Cti : AbstractBase +{ + private readonly int _period; + private readonly CircularBuffer _priceBuffer; + private readonly Corr _correlation; + + /// The data source object that publishes updates. + /// The calculation period (default: 20) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cti(object source, int period = 20) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cti(int period = 20) + { + _period = period; + _priceBuffer = new CircularBuffer(period); + _correlation = new Corr(period); + WarmupPeriod = period; + Name = "CTI"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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)); + + if (_index < _period - 1) return double.NaN; + + var correlation = _correlation.Value; + return 2 * (correlation - 0.5); + } +} diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index fb675c97..dc310c42 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -1,5 +1,5 @@ # Oscillators indicators -Done: 22, Todo: 7 +Done: 24, Todo: 5 ✔️ AC - Acceleration Oscillator ✔️ AO - Awesome Oscillator @@ -12,7 +12,7 @@ Done: 22, Todo: 7 ✔️ COG - Ehler's Center of Gravity ✔️ COPPOCK - Coppock Curve ✔️ CRSI - Connor RSI -CTI - Ehler's Correlation Trend Indicator +✔️ CTI - Ehler's Correlation Trend Indicator ✔️ DOSC - Derivative Oscillator ✔️ FISHER - Fisher Transform ✔️ EFI - Elder Ray's Force Index diff --git a/quantower/Oscillators/CtiIndicator.cs b/quantower/Oscillators/CtiIndicator.cs new file mode 100644 index 00000000..f85e2384 --- /dev/null +++ b/quantower/Oscillators/CtiIndicator.cs @@ -0,0 +1,66 @@ +using TradingPlatform.BusinessLayer; +using System.Drawing; + +namespace QuanTAlib +{ + public class CtiIndicator : Indicator + { + [InputParameter("Period", 0, 1, 100, 1, 0)] + public int Period = 20; + + [InputParameter("Source Type", 1, variants: new object[] + { + "Open", SourceType.Open, + "High", SourceType.High, + "Low", SourceType.Low, + "Close", SourceType.Close, + "HL2", SourceType.HL2, + "OC2", SourceType.OC2, + "OHL3", SourceType.OHL3, + "HLC3", SourceType.HLC3, + "OHLC4", SourceType.OHLC4, + "HLCC4", SourceType.HLCC4 + })] + public SourceType SourceType = SourceType.Close; + + [InputParameter("Show Cold Values", 2)] + public bool ShowColdValues = false; + + private Cti cti; + protected LineSeries? CtiSeries; + public int MinHistoryDepths => Period + 1; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public CtiIndicator() + { + this.Name = "CTI - Ehler's Correlation Trend Indicator"; + 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); + } + + protected override void OnInit() + { + cti = new Cti(this.Period); + base.OnInit(); + } + + protected override void OnUpdate(UpdateArgs args) + { + TValue input = this.GetInputValue(args, Source); + cti.Calc(value); + + CtiSeries!.SetValue(cti.Value); + CtiSeries!.SetMarker(0, Color.Transparent); + } + + 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); + } + } +}