diff --git a/.vscode/settings.json b/.vscode/settings.json index 335e6e7c..e27b36ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,11 +7,17 @@ } ], "sarif-viewer.connectToGithubCodeScanning": "on", - "dotnet.dotnetPath": "/opt/homebrew/bin/", "omnisharp.useModernNet": true, - "omnisharp.sdkPath": "/opt/homebrew/bin/dotnet", "sonarlint.connectedMode.project": { "connectionId": "mihakralj", "projectKey": "mihakralj_QuanTAlib" - } -} \ No newline at end of file + }, + "dotnet.backgroundAnalysis.analyzerDiagnosticsScope": "fullSolution", + "dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true, + "dotnetAcquisitionExtension.enableTelemetry": false, + "dotnet-test-explorer.testProjectPath": "Tests", + "dotnet-test-explorer.autoWatch": true, + "dotnet-test-explorer.showCodeLens": true, + "dotnet-test-explorer.testArguments": "/p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=./TestResults/coverage.cobertura.xml" + +} diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 11c1e048..31f09837 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -2,15 +2,16 @@ QuanTAlib.Tests QuanTAlib.Tests + false - - runtime; build; native; contentfiles; analyzers; buildtransitive - all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 57f519ca..5b0d6db4 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -51,12 +51,37 @@ public class EventingTests ("Tema", new Tema(p), new Tema(input, p)), ("Kama", new Kama(2, 30, 6), new Kama(input, 2, 30, 6)), ("Zlema", new Zlema(p), new Zlema(input, p)), + // Added missing averages + ("Sinema", new Sinema(p), new Sinema(input, p)), + ("Smma", new Smma(p), new Smma(input, p)), + ("T3", new T3(p), new T3(input, p)), + ("Trima", new Trima(p), new Trima(input, p)), + ("Vidya", new Vidya(p), new Vidya(input, p)), + // momentum indicators + ("Apo", new Apo(12, 26), new Apo(input, 12, 26)), // oscillators ("Rsi", new Rsi(p), new Rsi(input, p)), ("Rsx", new Rsx(p), new Rsx(input, p)), ("Cmo", new Cmo(p), new Cmo(input, p)), + // statistics + ("Curvature", new Curvature(p), new Curvature(input, p)), + ("Entropy", new Entropy(p), new Entropy(input, p)), + ("Kurtosis", new Kurtosis(p), new Kurtosis(input, p)), + ("Max", new Max(p), new Max(input, p)), + ("Median", new Median(p), new Median(input, p)), + ("Min", new Min(p), new Min(input, p)), + ("Mode", new Mode(p), new Mode(input, p)), + ("Percentile", new Percentile(p, 0.5), new Percentile(input, p, 0.5)), + ("Skew", new Skew(p), new Skew(input, p)), + ("Slope", new Slope(p), new Slope(input, p)), + ("Stddev", new Stddev(p), new Stddev(input, p)), + ("Variance", new Variance(p), new Variance(input, p)), + ("Zscore", new Zscore(p), new Zscore(input, p)), // volatility + ("Hv", new Hv(p), new Hv(input, p)), + ("Jvolty", new Jvolty(p), new Jvolty(input, p)), ("Rv", new Rv(p), new Rv(input, p)), + ("Rvi", new Rvi(p), new Rvi(input, p)), // error classes ("Mae", new Mae(p), new Mae(input, p)), ("Mapd", new Mapd(p), new Mapd(input, p)), diff --git a/Tests/test_updates_momentum.cs b/Tests/test_updates_momentum.cs new file mode 100644 index 00000000..a94d2386 --- /dev/null +++ b/Tests/test_updates_momentum.cs @@ -0,0 +1,91 @@ +using Xunit; +using System.Security.Cryptography; + +namespace QuanTAlib.Tests; + +public class MomentumUpdateTests +{ + private readonly RandomNumberGenerator rng = RandomNumberGenerator.Create(); + private const int RandomUpdates = 100; + private const double ReferenceValue = 100.0; + private const int precision = 8; + + private double GetRandomDouble() + { + byte[] bytes = new byte[8]; + rng.GetBytes(bytes); + return (double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue * 200 - 100; // Range: -100 to 100 + } + + private TBar GetRandomBar(bool IsNew) + { + double open = GetRandomDouble(); + double high = open + Math.Abs(GetRandomDouble()); + double low = open - Math.Abs(GetRandomDouble()); + double close = low + (high - low) * GetRandomDouble(); + return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); + } + + [Fact] + public void Adx_Update() + { + var indicator = new Adx(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Adxr_Update() + { + var indicator = new Adxr(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Apo_Update() + { + var indicator = new Apo(fastPeriod: 12, slowPeriod: 26); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Dmi_Update() + { + var indicator = new Dmi(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } +} diff --git a/Tests/test_updates_oscillators.cs b/Tests/test_updates_oscillators.cs new file mode 100644 index 00000000..297f0ebd --- /dev/null +++ b/Tests/test_updates_oscillators.cs @@ -0,0 +1,64 @@ +using Xunit; +using System.Security.Cryptography; + +namespace QuanTAlib.Tests; + +public class OscillatorsUpdateTests +{ + private readonly RandomNumberGenerator rng = RandomNumberGenerator.Create(); + private const int RandomUpdates = 100; + private const double ReferenceValue = 100.0; + private const int precision = 8; + + private double GetRandomDouble() + { + byte[] bytes = new byte[8]; + rng.GetBytes(bytes); + return (double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue * 200 - 100; // Range: -100 to 100 + } + + [Fact] + public void Rsi_Update() + { + var indicator = new Rsi(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Rsx_Update() + { + var indicator = new Rsx(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Cmo_Update() + { + var indicator = new Cmo(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } +} diff --git a/Tests/test_updates_volatility.cs b/Tests/test_updates_volatility.cs index 5a041c26..f8877a8e 100644 --- a/Tests/test_updates_volatility.cs +++ b/Tests/test_updates_volatility.cs @@ -46,13 +46,28 @@ public class VolatilityUpdateTests public void Historical_Update() { var indicator = new Hv(period: 14); - double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(GetRandomBar(false)); + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); } - double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Jvolty_Update() + { + var indicator = new Jvolty(period: 14); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); Assert.Equal(initialValue, finalValue, precision); } @@ -61,13 +76,13 @@ public class VolatilityUpdateTests public void Realized_Update() { var indicator = new Rv(period: 14); - double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(GetRandomBar(false)); + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); } - double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); Assert.Equal(initialValue, finalValue, precision); } @@ -76,13 +91,13 @@ public class VolatilityUpdateTests public void Rvi_Update() { var indicator = new Rvi(period: 14); - double initialValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true)); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); for (int i = 0; i < RandomUpdates; i++) { - indicator.Calc(GetRandomBar(false)); + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); } - double finalValue = indicator.Calc(new TBar(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: false)); + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); Assert.Equal(initialValue, finalValue, precision); } diff --git a/benchmark/benchmark.csproj b/benchmark/benchmark.csproj deleted file mode 100644 index 0940953e..00000000 --- a/benchmark/benchmark.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - Exe - net8.0 - false - enable - latest - false - - - - - - - - - - - - true - AnyCPU - pdbonly - true - true - - - \ No newline at end of file diff --git a/benchmark/growthBench.cs b/benchmark/growthBench.cs deleted file mode 100644 index 6dd123e4..00000000 --- a/benchmark/growthBench.cs +++ /dev/null @@ -1,73 +0,0 @@ -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Running; - -namespace QuanTAlib; - -public class Program -{ - public static void Main(string[] args) - { - var config = DefaultConfig.Instance - .WithOption(ConfigOptions.DisableOptimizationsValidator, true); - BenchmarkRunner.Run(config); - } -} - -[MemoryDiagnoser] -[SimpleJob(RuntimeMoniker.Net80, launchCount: 1, warmupCount: 3, iterationCount: 5)] -public class EmaBenchmark -{ - private const int Period = 10; - private const int Length = 100_000; - private GbmFeed gbm = null!; - private TSeries inputs = null!; - - [GlobalSetup] - public void Setup() - { - gbm = new GbmFeed(); - inputs = new(); - - for (int i = 0; i < Length; i++) - { - TBar item = gbm.Generate(DateTime.Now); - inputs.Add(new TValue(item.Time, item.Close, true, true)); - } - } - - [Benchmark] - public void Ema_bench() - { - Ema ma1 = new(Period); - for (int i = 0; i < Length; i++) - { - TValue item = gbm.Generate(DateTime.Now).Close; - ma1.Calc(item); - } - - } - - [Benchmark] - public void Alma_bench() - { - Alma ma1 = new(Period); - for (int i = 0; i < Length; i++) - { - TValue item = gbm.Generate(DateTime.Now).Close; - ma1.Calc(item); - } - } - - [Benchmark] - public void Dema_bench() - { - Dema ma1 = new(Period); - for (int i = 0; i < Length; i++) - { - TValue item = gbm.Generate(DateTime.Now).Close; - ma1.Calc(item); - } - } -} \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index c98425ed..eb060953 100644 --- a/docs/index.html +++ b/docs/index.html @@ -63,11 +63,11 @@ - - - - - + + + + + diff --git a/lib/momentum/Adx.cs b/lib/momentum/Adx.cs new file mode 100644 index 00000000..d5d061d9 --- /dev/null +++ b/lib/momentum/Adx.cs @@ -0,0 +1,179 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADX: Average Directional Movement Index +/// A technical analysis indicator used to measure the strength of a trend, +/// regardless of its direction. ADX combines the Positive and Negative +/// Directional Movement Indicators to determine trend strength. +/// +/// +/// The ADX calculation process: +/// 1. Calculate True Range (TR) +/// 2. Calculate +DM (Positive Directional Movement) +/// 3. Calculate -DM (Negative Directional Movement) +/// 4. Smooth TR, +DM, and -DM using Wilder's smoothing +/// 5. Calculate +DI and -DI +/// 6. Calculate DX (Directional Index) +/// 7. Smooth DX to get ADX +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Values above 25 indicate strong trend +/// - Values below 20 indicate weak or no trend +/// - Can be used with +DI and -DI for trade signals +/// - Does not indicate trend direction, only strength +/// +/// Formula: +/// TR = max(high-low, abs(high-prevClose), abs(low-prevClose)) +/// +DM = if(high-prevHigh > prevLow-low) then max(high-prevHigh, 0) else 0 +/// -DM = if(prevLow-low > high-prevHigh) then max(prevLow-low, 0) else 0 +/// +DI = 100 * smoothed(+DM) / smoothed(TR) +/// -DI = 100 * smoothed(-DM) / smoothed(TR) +/// DX = 100 * abs(+DI - -DI) / (+DI + -DI) +/// ADX = smoothed(DX) +/// +/// Sources: +/// J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978) +/// https://www.investopedia.com/terms/a/adx.asp +/// +/// Note: Default period of 14 was recommended by Wilder +/// + +[SkipLocalsInit] +public sealed class Adx : AbstractBarBase +{ + private readonly Rma _smoothedTr; + private readonly Rma _smoothedPlusDm; + private readonly Rma _smoothedMinusDm; + private readonly Rma _smoothedDx; + private double _prevHigh, _prevLow, _prevClose; + private double _p_prevHigh, _p_prevLow, _p_prevClose; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 14; + + /// The number of periods used in the ADX calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adx(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + _smoothedTr = new(period, useSma: true); + _smoothedPlusDm = new(period, useSma: true); + _smoothedMinusDm = new(period, useSma: true); + _smoothedDx = new(period, useSma: true); + _index = 0; + WarmupPeriod = period * 2; // Need extra period for DX smoothing + Name = $"ADX({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the ADX calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adx(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_prevHigh = _prevHigh; + _p_prevLow = _prevLow; + _p_prevClose = _prevClose; + } + else + { + _prevHigh = _p_prevHigh; + _prevLow = _p_prevLow; + _prevClose = _p_prevClose; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateTrueRange(double high, double low, double prevClose) + { + double hl = high - low; + double hpc = Math.Abs(high - prevClose); + double lpc = Math.Abs(low - prevClose); + return Math.Max(hl, Math.Max(hpc, lpc)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double plusDm, double minusDm) CalculateDirectionalMovement( + double high, double low, double prevHigh, double prevLow) + { + double upMove = high - prevHigh; + double downMove = prevLow - low; + + double plusDm = 0.0; + double minusDm = 0.0; + + if (upMove > downMove && upMove > 0) + plusDm = upMove; + else if (downMove > upMove && downMove > 0) + minusDm = downMove; + + return (plusDm, minusDm); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateDx(double plusDi, double minusDi) + { + double sum = plusDi + minusDi; + if (sum > 0) + return ScalingFactor * Math.Abs(plusDi - minusDi) / sum; + return 0.0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_index == 1) + { + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + return 0.0; + } + + // Calculate True Range and Directional Movement + double tr = CalculateTrueRange(Input.High, Input.Low, _prevClose); + var (plusDm, minusDm) = CalculateDirectionalMovement( + Input.High, Input.Low, _prevHigh, _prevLow); + + // Update previous values + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + + // Smooth the indicators using Wilder's method + _smoothedTr.Calc(tr, Input.IsNew); + _smoothedPlusDm.Calc(plusDm, Input.IsNew); + _smoothedMinusDm.Calc(minusDm, Input.IsNew); + + // Calculate +DI and -DI + double smoothedTr = _smoothedTr.Value; + if (smoothedTr > 0) + { + double plusDi = ScalingFactor * _smoothedPlusDm.Value / smoothedTr; + double minusDi = ScalingFactor * _smoothedMinusDm.Value / smoothedTr; + + // Calculate DX + double dx = CalculateDx(plusDi, minusDi); + + // Smooth DX to get ADX + _smoothedDx.Calc(dx, Input.IsNew); + return _smoothedDx.Value; + } + + return 0.0; + } +} diff --git a/lib/momentum/Adxr.cs b/lib/momentum/Adxr.cs new file mode 100644 index 00000000..e2457f41 --- /dev/null +++ b/lib/momentum/Adxr.cs @@ -0,0 +1,90 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADXR: Average Directional Movement Index Rating +/// A momentum indicator that measures trend strength by comparing the current ADX +/// value with a historical ADX value. ADXR helps identify potential trend +/// reversals earlier than standard ADX. +/// +/// +/// The ADXR calculation process: +/// 1. Calculate current period ADX +/// 2. Calculate historical period ADX (shifted back by period) +/// 3. Average the current and historical ADX values +/// +/// Key characteristics: +/// - Oscillates between 0 and 100 +/// - Values above 25 indicate strong trend +/// - Values below 20 indicate weak or no trend +/// - Faster at identifying trend changes than ADX +/// - Does not indicate trend direction, only strength +/// +/// Formula: +/// ADXR = (Current ADX + Historical ADX) / 2 +/// where: +/// Historical ADX = ADX value from 'period' bars ago +/// +/// Sources: +/// J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978) +/// https://www.investopedia.com/terms/a/adxr.asp +/// +/// Note: Default period of 14 was recommended by Wilder +/// + +[SkipLocalsInit] +public sealed class Adxr : AbstractBarBase +{ + private readonly Adx _currentAdx; + private readonly CircularBuffer _historicalAdx; + private const int DefaultPeriod = 14; + + /// The number of periods used in the ADXR calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adxr(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + _currentAdx = new(period); + _historicalAdx = new(period); + _index = 0; + WarmupPeriod = period * 3; // Need extra periods for historical ADX + Name = $"ADXR({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the ADXR calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adxr(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate current ADX + double currentAdx = _currentAdx.Value; + _currentAdx.Calc(Input); + + // Store ADX value in historical buffer + _historicalAdx.Add(currentAdx, Input.IsNew); + + // Calculate ADXR once we have enough historical data + if (_index > _historicalAdx.Capacity) + return (currentAdx + _historicalAdx.Oldest()) / 2.0; + + return currentAdx; + } +} diff --git a/lib/momentum/Apo.cs b/lib/momentum/Apo.cs new file mode 100644 index 00000000..686d9b5c --- /dev/null +++ b/lib/momentum/Apo.cs @@ -0,0 +1,92 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// APO: Absolute Price Oscillator +/// A momentum indicator that measures the absolute difference between two moving +/// averages of different periods. APO helps identify trend direction and potential +/// reversals by showing the momentum of price movement. +/// +/// +/// The APO calculation process: +/// 1. Calculate fast period moving average +/// 2. Calculate slow period moving average +/// 3. Calculate absolute difference between the two averages +/// +/// Key characteristics: +/// - Oscillates above and below zero +/// - Positive values indicate upward price momentum +/// - Negative values indicate downward price momentum +/// - Zero line crossovers signal potential trend changes +/// - Similar to MACD but uses simple moving averages +/// +/// Formula: +/// APO = Fast MA - Slow MA +/// where: +/// Fast MA = Moving average of shorter period +/// Slow MA = Moving average of longer period +/// +/// Sources: +/// https://www.investopedia.com/terms/p/ppo.asp +/// https://school.stockcharts.com/doku.php?id=technical_indicators:price_oscillators_ppo +/// +/// Note: Default periods are 12 and 26, similar to MACD +/// + +[SkipLocalsInit] +public sealed class Apo : AbstractBase +{ + private readonly Sma _fastMa; + private readonly Sma _slowMa; + private const int DefaultFastPeriod = 12; + private const int DefaultSlowPeriod = 26; + + /// The number of periods for the fast moving average (default 12). + /// The number of periods for the slow moving average (default 26). + /// Thrown when either period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Apo(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod) + { + if (fastPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(fastPeriod)); + if (slowPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(slowPeriod)); + if (fastPeriod >= slowPeriod) + throw new ArgumentException("Fast period must be less than slow period"); + + _fastMa = new(fastPeriod); + _slowMa = new(slowPeriod); + WarmupPeriod = slowPeriod; + Name = $"APO({fastPeriod},{slowPeriod})"; + } + + /// The data source object that publishes updates. + /// The number of periods for the fast moving average. + /// The number of periods for the slow moving average. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Apo(object source, int fastPeriod, int slowPeriod) : this(fastPeriod, slowPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _index++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + // Calculate both moving averages + double fastMa = _fastMa.Calc(Input.Value, Input.IsNew); + double slowMa = _slowMa.Calc(Input.Value, Input.IsNew); + + // Calculate absolute difference + return fastMa - slowMa; + } +} diff --git a/lib/momentum/Dmi.cs b/lib/momentum/Dmi.cs new file mode 100644 index 00000000..f0c7c49f --- /dev/null +++ b/lib/momentum/Dmi.cs @@ -0,0 +1,171 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// DMI: Directional Movement Index +/// A technical indicator that identifies the directional movement of price by +/// comparing successive highs and lows. DMI consists of two lines: +DI and -DI, +/// which help determine trend direction and strength. +/// +/// +/// The DMI calculation process: +/// 1. Calculate True Range (TR) +/// 2. Calculate +DM (Positive Directional Movement) +/// 3. Calculate -DM (Negative Directional Movement) +/// 4. Smooth TR, +DM, and -DM using Wilder's smoothing +/// 5. Calculate +DI and -DI as percentages +/// +/// Key characteristics: +/// - Both +DI and -DI oscillate between 0 and 100 +/// - When +DI > -DI, uptrend is indicated +/// - When -DI > +DI, downtrend is indicated +/// - Crossovers of +DI and -DI signal potential trend changes +/// - Used in conjunction with ADX for trend trading +/// +/// Formula: +/// TR = max(high-low, abs(high-prevClose), abs(low-prevClose)) +/// +DM = if(high-prevHigh > prevLow-low) then max(high-prevHigh, 0) else 0 +/// -DM = if(prevLow-low > high-prevHigh) then max(prevLow-low, 0) else 0 +/// +DI = 100 * smoothed(+DM) / smoothed(TR) +/// -DI = 100 * smoothed(-DM) / smoothed(TR) +/// +/// Sources: +/// J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978) +/// https://www.investopedia.com/terms/d/dmi.asp +/// +/// Note: Default period of 14 was recommended by Wilder +/// + +[SkipLocalsInit] +public sealed class Dmi : AbstractBarBase +{ + private readonly Rma _smoothedTr; + private readonly Rma _smoothedPlusDm; + private readonly Rma _smoothedMinusDm; + private double _prevHigh, _prevLow, _prevClose; + private double _p_prevHigh, _p_prevLow, _p_prevClose; + private double _plusDi, _minusDi; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 14; + + /// + /// Gets the most recent +DI value + /// + public double PlusDI => _plusDi; + + /// + /// Gets the most recent -DI value + /// + public double MinusDI => _minusDi; + + /// The number of periods used in the DMI calculation (default 14). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dmi(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + _smoothedTr = new(period, useSma: true); + _smoothedPlusDm = new(period, useSma: true); + _smoothedMinusDm = new(period, useSma: true); + _index = 0; + WarmupPeriod = period + 1; + Name = $"DMI({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the DMI calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dmi(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_prevHigh = _prevHigh; + _p_prevLow = _prevLow; + _p_prevClose = _prevClose; + } + else + { + _prevHigh = _p_prevHigh; + _prevLow = _p_prevLow; + _prevClose = _p_prevClose; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateTrueRange(double high, double low, double prevClose) + { + double hl = high - low; + double hpc = Math.Abs(high - prevClose); + double lpc = Math.Abs(low - prevClose); + return Math.Max(hl, Math.Max(hpc, lpc)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double plusDm, double minusDm) CalculateDirectionalMovement( + double high, double low, double prevHigh, double prevLow) + { + double upMove = high - prevHigh; + double downMove = prevLow - low; + + double plusDm = 0.0; + double minusDm = 0.0; + + if (upMove > downMove && upMove > 0) + plusDm = upMove; + else if (downMove > upMove && downMove > 0) + minusDm = downMove; + + return (plusDm, minusDm); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_index == 1) + { + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + return 0.0; + } + + // Calculate True Range and Directional Movement + double tr = CalculateTrueRange(Input.High, Input.Low, _prevClose); + var (plusDm, minusDm) = CalculateDirectionalMovement( + Input.High, Input.Low, _prevHigh, _prevLow); + + // Update previous values + _prevHigh = Input.High; + _prevLow = Input.Low; + _prevClose = Input.Close; + + // Smooth the indicators using Wilder's method + _smoothedTr.Calc(tr, Input.IsNew); + _smoothedPlusDm.Calc(plusDm, Input.IsNew); + _smoothedMinusDm.Calc(minusDm, Input.IsNew); + + // Calculate +DI and -DI + double smoothedTr = _smoothedTr.Value; + if (smoothedTr > 0) + { + _plusDi = ScalingFactor * _smoothedPlusDm.Value / smoothedTr; + _minusDi = ScalingFactor * _smoothedMinusDm.Value / smoothedTr; + return _plusDi - _minusDi; // Return the difference as main value + } + + _plusDi = 0.0; + _minusDi = 0.0; + return 0.0; + } +} diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md index 873798b0..398da442 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -1,6 +1,6 @@ -ADX - Average Directional Movement Index -ADXR - Average Directional Movement Index -APO - Absolute Price Oscillator +✔️ ADX - Average Directional Movement Index +✔️ ADXR - Average Directional Movement Index Rating +✔️ APO - Absolute Price Oscillator DMI - Directional Movement Index DMX - Jurik Directional Movement Index DPO - Detrended Price Oscillator diff --git a/lib/volatility/Jvolty.cs b/lib/volatility/Jvolty.cs index 424bc8cb..ffac4d58 100644 --- a/lib/volatility/Jvolty.cs +++ b/lib/volatility/Jvolty.cs @@ -96,7 +96,7 @@ public sealed class Jvolty : AbstractBase public Jvolty(object source, int period, int phase = DefaultPhase) : this(period, phase) { var pubEvent = source.GetType().GetEvent("Pub"); - pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/quantower/Momentum/_Momentum.csproj b/quantower/Momentum/_Momentum.csproj new file mode 100644 index 00000000..00cb1a71 --- /dev/null +++ b/quantower/Momentum/_Momentum.csproj @@ -0,0 +1,33 @@ + + + Momentum + Indicator + 0.0.0.0 + bin\$(Configuration)\ + true + true + true + false + + + + + + + + + + + ..\..\.github\TradingPlatform.BusinessLayer.dll + + + TradingPlatform.BusinessLayer.xml + + + + + + + + diff --git a/quantower/Oscillators/_Oscillators.csproj b/quantower/Oscillators/_Oscillators.csproj new file mode 100644 index 00000000..ed3a39fe --- /dev/null +++ b/quantower/Oscillators/_Oscillators.csproj @@ -0,0 +1,33 @@ + + + Oscillators + Indicator + 0.0.0.0 + bin\$(Configuration)\ + true + true + true + false + + + + + + + + + + + ..\..\.github\TradingPlatform.BusinessLayer.dll + + + TradingPlatform.BusinessLayer.xml + + + + + + + + diff --git a/quantower/Volume/_Volume.csproj b/quantower/Volume/_Volume.csproj new file mode 100644 index 00000000..13266f2a --- /dev/null +++ b/quantower/Volume/_Volume.csproj @@ -0,0 +1,33 @@ + + + Volume + Indicator + 0.0.0.0 + bin\$(Configuration)\ + true + true + true + false + + + + + + + + + + + ..\..\.github\TradingPlatform.BusinessLayer.dll + + + TradingPlatform.BusinessLayer.xml + + + + + + + +