diff --git a/Tests/test_core.cs b/Tests/test_core.cs new file mode 100644 index 00000000..49d67d4b --- /dev/null +++ b/Tests/test_core.cs @@ -0,0 +1,251 @@ +using Xunit; + +namespace QuanTAlib.Tests; + +public class CoreTests +{ + #region CircularBuffer Tests + + [Fact] + public void CircularBuffer_BasicOperations() + { + var buffer = new CircularBuffer(5); + + // Test initial state + Assert.Equal(5, buffer.Capacity); + Assert.Equal(0, buffer.Count); + + // Test adding items + buffer.Add(1.0); + buffer.Add(2.0); + Assert.Equal(2, buffer.Count); + Assert.Equal(1.0, buffer[0]); + Assert.Equal(2.0, buffer[^1]); + + // Test overflow behavior + buffer.Add(3.0); + buffer.Add(4.0); + buffer.Add(5.0); + buffer.Add(6.0); // Should remove oldest item (1.0) + Assert.Equal(5, buffer.Count); + Assert.Equal(2.0, buffer[0]); + Assert.Equal(6.0, buffer[^1]); + } + + [Fact] + public void CircularBuffer_UpdateBehavior() + { + var buffer = new CircularBuffer(3); + + // Add new values + buffer.Add(1.0, isNew: true); + buffer.Add(2.0, isNew: true); + Assert.Equal(2, buffer.Count); + + // Update last value + buffer.Add(2.5, isNew: false); + Assert.Equal(2, buffer.Count); + Assert.Equal(2.5, buffer[^1]); + } + + [Fact] + public void CircularBuffer_MinMaxSumAverage() + { + var buffer = new CircularBuffer(5); + + buffer.Add(1.0); + buffer.Add(2.0); + buffer.Add(3.0); + buffer.Add(4.0); + buffer.Add(5.0); + + Assert.Equal(1.0, buffer.Min()); + Assert.Equal(5.0, buffer.Max()); + Assert.Equal(15.0, buffer.Sum()); + Assert.Equal(3.0, buffer.Average()); + } + + [Fact] + public void CircularBuffer_Enumeration() + { + var buffer = new CircularBuffer(3); + + buffer.Add(1.0); + buffer.Add(2.0); + buffer.Add(3.0); + + var list = buffer.ToList(); + Assert.Equal(3, list.Count); + Assert.Equal(1.0, list[0]); + Assert.Equal(3.0, list[2]); + } + + #endregion + + #region TBar Tests + + [Fact] + public void TBar_Construction() + { + // Default constructor + var bar1 = new TBar(); + Assert.Equal(0, bar1.Open); + Assert.True(bar1.IsNew); + + // Value constructor + var bar2 = new TBar(10.0); + Assert.Equal(10.0, bar2.Open); + Assert.Equal(10.0, bar2.High); + Assert.Equal(10.0, bar2.Low); + Assert.Equal(10.0, bar2.Close); + + // Full constructor + var time = DateTime.Now; + var bar3 = new TBar(time, 10.0, 12.0, 9.0, 11.0, 1000.0, false); + Assert.Equal(time, bar3.Time); + Assert.Equal(10.0, bar3.Open); + Assert.Equal(12.0, bar3.High); + Assert.Equal(9.0, bar3.Low); + Assert.Equal(11.0, bar3.Close); + Assert.Equal(1000.0, bar3.Volume); + Assert.False(bar3.IsNew); + } + + [Fact] + public void TBar_DerivedValues() + { + var bar = new TBar(DateTime.Now, 10.0, 20.0, 5.0, 15.0, 1000.0); + + Assert.Equal(12.5, bar.HL2); // (20 + 5) / 2 + Assert.Equal(12.5, bar.OC2); // (10 + 15) / 2 + Assert.Equal(11.67, bar.OHL3, 2); // (10 + 20 + 5) / 3 + Assert.Equal(13.33, bar.HLC3, 2); // (20 + 5 + 15) / 3 + Assert.Equal(12.5, bar.OHLC4); // (10 + 20 + 5 + 15) / 4 + Assert.Equal(13.75, bar.HLCC4); // (20 + 5 + 15 + 15) / 4 + } + + [Fact] + public void TBarSeries_Operations() + { + var series = new TBarSeries(); + var time = DateTime.Now; + var bar1 = new TBar(time, 10.0, 12.0, 9.0, 11.0, 1000.0); + var bar2 = new TBar(time.AddMinutes(1), 11.0, 13.0, 10.0, 12.0, 1100.0); + + // Test adding bars + series.Add(bar1); + series.Add(bar2); + Assert.Equal(2, series.Count); + + // Test updating last bar + var bar2Update = new TBar(bar2.Time, 11.0, 13.5, 9.5, 12.5, 1200.0, false); + series.Add(bar2Update); + Assert.Equal(2, series.Count); + Assert.Equal(12.5, series.Last.Close); + + // Test derived series + Assert.Equal(11.0, series.Open.Last.Value); + Assert.Equal(13.5, series.High.Last.Value); + Assert.Equal(9.5, series.Low.Last.Value); + Assert.Equal(12.5, series.Close.Last.Value); + Assert.Equal(1200.0, series.Volume.Last.Value); + } + + #endregion + + #region TValue Tests + + [Fact] + public void TValue_Construction() + { + // Default constructor + var value1 = new TValue(); + Assert.Equal(0, value1.Value); + Assert.True(value1.IsNew); + Assert.True(value1.IsHot); + + // Value constructor + var value2 = new TValue(10.0); + Assert.Equal(10.0, value2.Value); + + // Full constructor + var time = DateTime.Now; + var value3 = new TValue(time, 10.0, false, false); + Assert.Equal(time, value3.Time); + Assert.Equal(10.0, value3.Value); + Assert.False(value3.IsNew); + Assert.False(value3.IsHot); + } + + [Fact] + public void TValue_Conversions() + { + var value = new TValue(10.0); + + // Test implicit conversions + double d = value; + Assert.Equal(10.0, d); + + DateTime time = value; + Assert.Equal(value.Time, time); + + // Test implicit conversion from double + TValue newValue = 20.0; + Assert.Equal(20.0, newValue.Value); + } + + [Fact] + public void TSeries_Operations() + { + var series = new TSeries(); + var time = DateTime.Now; + + // Test adding values + series.Add(time, 10.0); + series.Add(time.AddMinutes(1), 20.0); + Assert.Equal(2, series.Count); + + // Test updating last value + series.Add(new TValue(time.AddMinutes(1), 25.0, false)); + Assert.Equal(2, series.Count); + Assert.Equal(25.0, series.Last.Value); + + // Test adding range of values + var values = new[] { 30.0, 40.0, 50.0 }; + foreach (var value in values) + { + series.Add(time.AddMinutes(series.Count + 1), value); + } + Assert.Equal(5, series.Count); + + // Test conversions + var doubleList = (List)series; + Assert.Equal(5, doubleList.Count); + Assert.Equal(50.0, doubleList[^1]); + + var doubleArray = (double[])series; + Assert.Equal(5, doubleArray.Length); + Assert.Equal(50.0, doubleArray[^1]); + } + + [Fact] + public void TSeries_EventHandling() + { + var series = new TSeries(); + var receivedValues = new List(); + var time = DateTime.Now; + + series.Pub += (object sender, in ValueEventArgs args) => receivedValues.Add(args.Tick.Value); + + series.Add(time, 10.0); + series.Add(time.AddMinutes(1), 20.0); + series.Add(time.AddMinutes(2), 30.0); + + Assert.Equal(3, receivedValues.Count); + Assert.Equal(10.0, receivedValues[0]); + Assert.Equal(20.0, receivedValues[1]); + Assert.Equal(30.0, receivedValues[2]); + } + + #endregion +} diff --git a/Tests/test_eventing.cs b/Tests/test_eventing.cs index 5b0d6db4..3564894e 100644 --- a/Tests/test_eventing.cs +++ b/Tests/test_eventing.cs @@ -13,12 +13,13 @@ public class EventingTests // Create a cryptographically secure random number generator using var rng = RandomNumberGenerator.Create(); - // Create an input series to hold our random values + // Create input series to hold our random values var input = new TSeries(); + var barInput = new TBarSeries(); int p = 10; - // Create a list of indicator pairs (direct calculation and event-based) with names - var indicators = new List<(string Name, AbstractBase Direct, AbstractBase EventBased)> + // Create a list of value-based indicator pairs + var valueIndicators = new List<(string Name, AbstractBase Direct, AbstractBase EventBased)> { ("Afirma", new Afirma(p,p,Afirma.WindowType.BlackmanHarris), new Afirma(input, p,p,Afirma.WindowType.BlackmanHarris)), ("Alma", new Alma(p), new Alma(input, p)), @@ -51,19 +52,15 @@ 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)), @@ -77,12 +74,12 @@ public class EventingTests ("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 + // Volatility indicators (value-based) ("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 + // Error classes ("Mae", new Mae(p), new Mae(input, p)), ("Mapd", new Mapd(p), new Mapd(input, p)), ("Mape", new Mape(p), new Mape(input, p)), @@ -101,26 +98,66 @@ public class EventingTests ("Huber", new Huber(p), new Huber(input, p)) }; - // Generate 200 random values and feed them to both direct and event-based indicators + // Create a list of bar-based indicator pairs + var barIndicators = new List<(string Name, AbstractBase Direct, AbstractBase EventBased)> + { + // Volume indicators + ("Adl", new Adl(), new Adl(barInput)), + ("Adosc", new Adosc(3, 10), new Adosc(barInput, 3, 10)), + ("Aobv", new Aobv(), new Aobv(barInput)), + ("Cmf", new Cmf(20), new Cmf(barInput, 20)), + ("Eom", new Eom(14), new Eom(barInput, 14)), + ("Kvo", new Kvo(34, 55), new Kvo(barInput, 34, 55)), + // Volatility indicators (bar-based) + ("Atr", new Atr(14), new Atr(barInput, 14)) + }; + + // Generate 200 random values and feed them to indicators for (int i = 0; i < 200; i++) { + // Generate random value for value-based indicators double randomValue = GetRandomDouble(rng) * 100; input.Add(randomValue); - // Calculate direct indicators - foreach (var (_, direct, _) in indicators) + // Calculate value-based indicators + foreach (var (_, direct, _) in valueIndicators) { direct.Calc(randomValue); } + + // Generate random bar for bar-based indicators + var bar = new TBar( + DateTime.Now, + randomValue, + randomValue + Math.Abs(GetRandomDouble(rng) * 10), + randomValue - Math.Abs(GetRandomDouble(rng) * 10), + randomValue + GetRandomDouble(rng) * 5, + Math.Abs(GetRandomDouble(rng) * 1000), + true + ); + barInput.Add(bar); + + // Calculate bar-based indicators + foreach (var (_, direct, _) in barIndicators) + { + direct.Calc(bar); + } } - // Compare the results of direct and event-based calculations - for (int i = 0; i < indicators.Count; i++) + // Compare the results for value-based indicators + foreach (var (name, direct, eventBased) in valueIndicators) { - var (name, direct, eventBased) = indicators[i]; bool areEqual = (double.IsNaN(direct.Value) && double.IsNaN(eventBased.Value)) || Math.Abs(direct.Value - eventBased.Value) < 1e-9; - Assert.True(areEqual, $"Indicator {name} failed: Expected {direct.Value}, Actual {eventBased.Value}"); + Assert.True(areEqual, $"Value indicator {name} failed: Expected {direct.Value}, Actual {eventBased.Value}"); + } + + // Compare the results for bar-based indicators + foreach (var (name, direct, eventBased) in barIndicators) + { + bool areEqual = (double.IsNaN(direct.Value) && double.IsNaN(eventBased.Value)) || + Math.Abs(direct.Value - eventBased.Value) < 1e-9; + Assert.True(areEqual, $"Bar indicator {name} failed: Expected {direct.Value}, Actual {eventBased.Value}"); } } diff --git a/Tests/test_updates_momentum.cs b/Tests/test_updates_momentum.cs index a94d2386..3ca93dcc 100644 --- a/Tests/test_updates_momentum.cs +++ b/Tests/test_updates_momentum.cs @@ -88,4 +88,129 @@ public class MomentumUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Dmx_Update() + { + var indicator = new Dmx(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 Pmo_Update() + { + var indicator = new Pmo(period1: 35, period2: 20); + 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 Po_Update() + { + var indicator = new Po(fastPeriod: 10, slowPeriod: 21); + 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 Ppo_Update() + { + var indicator = new Ppo(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 Prs_Update() + { + var indicator = new Prs(); + indicator.SetBenchmark(ReferenceValue); + double initialValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: true)); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.SetBenchmark(GetRandomDouble() + 100); // Ensure positive benchmark + indicator.Calc(new TValue(DateTime.Now, GetRandomDouble(), IsNew: false)); + } + + indicator.SetBenchmark(ReferenceValue); + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Roc_Update() + { + var indicator = new Roc(period: 12); + 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() + 100, IsNew: false)); // Ensure positive prices + } + double finalValue = indicator.Calc(new TValue(DateTime.Now, ReferenceValue, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Mom_Update() + { + var indicator = new Mom(period: 10); + 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 Vel_Update() + { + var indicator = new Vel(period: 10); + 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_oscillators.cs b/Tests/test_updates_oscillators.cs index 297f0ebd..ae2574a5 100644 --- a/Tests/test_updates_oscillators.cs +++ b/Tests/test_updates_oscillators.cs @@ -61,4 +61,36 @@ public class OscillatorsUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Ao_Update() + { + var indicator = new Ao(); + TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, 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 Ac_Update() + { + var indicator = new Ac(); + TBar r = new(DateTime.Now, ReferenceValue, ReferenceValue, ReferenceValue, ReferenceValue, 1000, IsNew: true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(DateTime.Now, GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), GetRandomDouble(), 1000, 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_volume.cs b/Tests/test_updates_volume.cs new file mode 100644 index 00000000..472dd740 --- /dev/null +++ b/Tests/test_updates_volume.cs @@ -0,0 +1,159 @@ +using Xunit; +using System.Security.Cryptography; + +namespace QuanTAlib.Tests; + +public class VolumeUpdateTests +{ + private readonly RandomNumberGenerator rng = RandomNumberGenerator.Create(); + private const int RandomUpdates = 100; + 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(); + double volume = Math.Abs(GetRandomDouble()) * 1000; // Random positive volume + return new TBar(DateTime.Now, open, high, low, close, volume, IsNew); + } + + [Fact] + public void Adl_Update() + { + var indicator = new Adl(); + TBar r = GetRandomBar(true); + + // First calculation with IsNew: true + double value1 = indicator.Calc(r); + + // Multiple recalculations with IsNew: false should not change the value + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + } + + // Final calculation with IsNew: false should match initial value + double value2 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + Assert.Equal(value1, value2, precision); + + // New calculation with IsNew: true should update the value + double value3 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: true)); + Assert.NotEqual(value1, value3, precision); + } + + [Fact] + public void Adosc_Update() + { + var indicator = new Adosc(shortPeriod: 3, longPeriod: 10); + 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 Aobv_Update() + { + var indicator = new Aobv(); + TBar r = GetRandomBar(true); + + // First calculation with IsNew: true + double value1 = indicator.Calc(r); + + // Multiple recalculations with IsNew: false should not change the value + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + } + + // Final calculation with IsNew: false should match initial value + double value2 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + Assert.Equal(value1, value2, precision); + + // New calculation with IsNew: true should update the value + double value3 = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: true)); + Assert.NotEqual(value1, value3, precision); + } + + [Fact] + public void Cmf_Update() + { + var indicator = new Cmf(period: 20); + TBar r = GetRandomBar(true); + + // Generate a sequence of bars for warmup + var warmupBars = new List(); + for (int i = 0; i < indicator.WarmupPeriod; i++) + { + var bar = GetRandomBar(IsNew: true); + warmupBars.Add(bar); + indicator.Calc(bar); + } + + // Calculate initial value after warmup + double initialValue = indicator.Calc(r); + + // Apply random updates + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + + // Reset and replay the same sequence + indicator.Init(); + foreach (var bar in warmupBars) + { + indicator.Calc(bar); + } + 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 Eom_Update() + { + var indicator = new Eom(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 Kvo_Update() + { + var indicator = new Kvo(shortPeriod: 34, longPeriod: 55); + 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/lib/momentum/Dmx.cs b/lib/momentum/Dmx.cs new file mode 100644 index 00000000..3f70113f --- /dev/null +++ b/lib/momentum/Dmx.cs @@ -0,0 +1,174 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// DMX: Enhanced Directional Movement Index using JMA smoothing +/// An improvement over the traditional DMI indicator that uses Jurik Moving Average (JMA) +/// for smoothing instead of Wilder's moving average. This enhancement provides better +/// noise reduction while maintaining responsiveness to significant price movements. +/// +/// +/// The DMX 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 JMA instead of Wilder's smoothing +/// 5. Calculate +DI and -DI as percentages +/// +/// Key improvements over DMI: +/// - Uses JMA's adaptive volatility-based smoothing +/// - Better noise reduction in the directional movement signals +/// - Maintains responsiveness to significant price movements +/// - Reduced lag through JMA's phase-shifting +/// +/// 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 * JMA(+DM) / JMA(TR) +/// -DI = 100 * JMA(-DM) / JMA(TR) +/// +/// Sources: +/// Original DMI by J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978) +/// Enhanced with JMA smoothing by Mark Jurik +/// + +[SkipLocalsInit] +public sealed class Dmx : AbstractBarBase +{ + private readonly Jma _smoothedTr; + private readonly Jma _smoothedPlusDm; + private readonly Jma _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 = 10; + private const int DefaultPhase = 100; + private const double DefaultFactor = 0.25; + + /// + /// 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 DMX calculation (default 14). + /// The phase for the JMA smoothing (default 0). + /// The factor for the JMA smoothing (default 0.45). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dmx(int period = DefaultPeriod, int phase = DefaultPhase, double factor = DefaultFactor) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + _smoothedTr = new(period, phase, factor); + _smoothedPlusDm = new(period, phase, factor); + _smoothedMinusDm = new(period, phase, factor); + _index = 0; + WarmupPeriod = period * 2; // JMA needs more warmup periods than RMA + Name = $"DMX({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods used in the DMX calculation. + /// The phase for the JMA smoothing. + /// The factor for the JMA smoothing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Dmx(object source, int period, int phase = DefaultPhase, double factor = DefaultFactor) : this(period, phase, factor) + { + 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 JMA + _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/Mom.cs b/lib/momentum/Mom.cs new file mode 100644 index 00000000..56e86f33 --- /dev/null +++ b/lib/momentum/Mom.cs @@ -0,0 +1,75 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// Mom: Momentum +/// A basic momentum indicator that measures the change in price over a specified +/// period, helping identify the strength and speed of price movements. +/// +/// +/// The Momentum calculation process: +/// 1. Store historical prices in a circular buffer +/// 2. Calculate absolute difference between current and historical price +/// 3. No scaling factor applied to maintain raw price difference +/// +/// Key characteristics: +/// - Basic momentum measurement +/// - Shows absolute price changes +/// - Zero line crossovers signal trend changes +/// - Foundation for other momentum indicators +/// +/// Formula: +/// Mom = Price - PriceN +/// where PriceN is the price N periods ago +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// Technical Analysis Using Multiple Timeframes by Brian Shannon +/// + +[SkipLocalsInit] +public sealed class Mom : AbstractBase +{ + private readonly CircularBuffer _priceBuffer; + private const int DefaultPeriod = 10; + + /// The lookback period for momentum calculation (default 10). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Mom(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _priceBuffer = new(period + 1); + WarmupPeriod = period; + Name = $"MOM({period})"; + } + + /// The data source object that publishes updates. + /// The lookback period for momentum calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Mom(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _priceBuffer.Add(Input.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_priceBuffer.Count < _priceBuffer.Capacity) + return 0.0; + + return Input.Value - _priceBuffer[0]; + } +} diff --git a/lib/momentum/Pmo.cs b/lib/momentum/Pmo.cs new file mode 100644 index 00000000..cd22df83 --- /dev/null +++ b/lib/momentum/Pmo.cs @@ -0,0 +1,102 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PMO: Price Momentum Oscillator +/// A momentum indicator that uses exponential moving averages of ROC (Rate of Change) +/// to identify overbought and oversold conditions in price movements. +/// +/// +/// The PMO calculation process: +/// 1. Calculate ROC (Rate of Change) of closing prices +/// 2. Apply a first smoothing EMA to the ROC values +/// 3. Apply a second smoothing EMA to the result +/// 4. Multiply by a scaling factor for better visualization +/// +/// Key characteristics: +/// - Double-smoothed momentum indicator +/// - Helps identify overbought/oversold conditions +/// - Useful for trend confirmation and divergence analysis +/// - More responsive than traditional momentum oscillators +/// +/// Formula: +/// ROC = (Close - PrevClose) / PrevClose +/// Signal1 = EMA(ROC, Period1) +/// PMO = EMA(Signal1, Period2) * ScalingFactor +/// +/// Sources: +/// Developed by Carl Swenlin +/// Technical Analysis of Stocks and Commodities magazine +/// + +[SkipLocalsInit] +public sealed class Pmo : AbstractBase +{ + private readonly Ema _smoothing1; + private readonly Ema _smoothing2; + private double _prevClose; + private double _p_prevClose; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod1 = 35; + private const int DefaultPeriod2 = 20; + + /// The first smoothing period (default 35). + /// The second smoothing period (default 20). + /// Thrown when either period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pmo(int period1 = DefaultPeriod1, int period2 = DefaultPeriod2) + { + if (period1 < 1 || period2 < 1) + throw new ArgumentOutOfRangeException(nameof(period1)); + + _smoothing1 = new(period1); + _smoothing2 = new(period2); + _index = 0; + WarmupPeriod = period1 + period2; + Name = $"PMO({period1},{period2})"; + } + + /// The data source object that publishes updates. + /// The first smoothing period. + /// The second smoothing period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Pmo(object source, int period1, int period2) : this(period1, period2) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + _p_prevClose = _prevClose; + } + else + { + _prevClose = _p_prevClose; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_index == 1) + { + _prevClose = Input.Value; + return 0.0; + } + + // Calculate Rate of Change + double roc = (Input.Value - _prevClose) / _prevClose; + _prevClose = Input.Value; + + // Apply double smoothing + double signal1 = _smoothing1.Calc(roc, Input.IsNew); + return _smoothing2.Calc(signal1, Input.IsNew) * ScalingFactor; + } +} diff --git a/lib/momentum/Po.cs b/lib/momentum/Po.cs new file mode 100644 index 00000000..170096bd --- /dev/null +++ b/lib/momentum/Po.cs @@ -0,0 +1,80 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PO: Price Oscillator +/// A momentum indicator that measures the difference between two moving averages +/// of different periods to identify price momentum and potential trend changes. +/// +/// +/// The PO calculation process: +/// 1. Calculate fast EMA of closing prices +/// 2. Calculate slow EMA of closing prices +/// 3. Calculate the difference between fast and slow EMAs +/// 4. Multiply by a scaling factor for better visualization +/// +/// Key characteristics: +/// - Measures momentum through moving average differences +/// - Helps identify trend direction and potential reversals +/// - Zero line crossovers signal trend changes +/// - Similar to MACD but more customizable periods +/// +/// Formula: +/// FastMA = EMA(Close, FastPeriod) +/// SlowMA = EMA(Close, SlowPeriod) +/// PO = (FastMA - SlowMA) * ScalingFactor +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// + +[SkipLocalsInit] +public sealed class Po : AbstractBase +{ + private readonly Ema _fastEma; + private readonly Ema _slowEma; + private const double ScalingFactor = 1.0; + private const int DefaultFastPeriod = 10; + private const int DefaultSlowPeriod = 21; + + /// The fast EMA period (default 10). + /// The slow EMA period (default 21). + /// Thrown when either period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Po(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod) + { + if (fastPeriod < 1 || slowPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(fastPeriod)); + if (fastPeriod >= slowPeriod) + throw new ArgumentException("Fast period must be less than slow period"); + + _fastEma = new(fastPeriod); + _slowEma = new(slowPeriod); + WarmupPeriod = slowPeriod; + Name = $"PO({fastPeriod},{slowPeriod})"; + } + + /// The data source object that publishes updates. + /// The fast EMA period. + /// The slow EMA period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Po(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) + { + // No state management needed for this indicator + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + double fastEma = _fastEma.Calc(Input.Value, Input.IsNew); + double slowEma = _slowEma.Calc(Input.Value, Input.IsNew); + return (fastEma - slowEma) * ScalingFactor; + } +} diff --git a/lib/momentum/Ppo.cs b/lib/momentum/Ppo.cs new file mode 100644 index 00000000..2c46c9b8 --- /dev/null +++ b/lib/momentum/Ppo.cs @@ -0,0 +1,85 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PPO: Percentage Price Oscillator +/// A momentum indicator that shows the percentage difference between two moving averages +/// of different periods, helping identify price momentum and potential trend changes. +/// +/// +/// The PPO calculation process: +/// 1. Calculate fast EMA of closing prices +/// 2. Calculate slow EMA of closing prices +/// 3. Calculate the percentage difference between fast and slow EMAs +/// 4. Multiply by a scaling factor for better visualization +/// +/// Key characteristics: +/// - Measures momentum through percentage differences +/// - Normalized for comparison across different price levels +/// - Zero line crossovers signal trend changes +/// - Similar to MACD but expressed as a percentage +/// +/// Formula: +/// FastMA = EMA(Close, FastPeriod) +/// SlowMA = EMA(Close, SlowPeriod) +/// PPO = ((FastMA - SlowMA) / SlowMA) * 100 +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// StockCharts.com Technical Indicators +/// + +[SkipLocalsInit] +public sealed class Ppo : AbstractBase +{ + private readonly Ema _fastEma; + private readonly Ema _slowEma; + private const double ScalingFactor = 100.0; + private const int DefaultFastPeriod = 12; + private const int DefaultSlowPeriod = 26; + + /// The fast EMA period (default 12). + /// The slow EMA period (default 26). + /// Thrown when either period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ppo(int fastPeriod = DefaultFastPeriod, int slowPeriod = DefaultSlowPeriod) + { + if (fastPeriod < 1 || slowPeriod < 1) + throw new ArgumentOutOfRangeException(nameof(fastPeriod)); + if (fastPeriod >= slowPeriod) + throw new ArgumentException("Fast period must be less than slow period"); + + _fastEma = new(fastPeriod); + _slowEma = new(slowPeriod); + WarmupPeriod = slowPeriod; + Name = $"PPO({fastPeriod},{slowPeriod})"; + } + + /// The data source object that publishes updates. + /// The fast EMA period. + /// The slow EMA period. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ppo(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) + { + // No state management needed for this indicator + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + double fastEma = _fastEma.Calc(Input.Value, Input.IsNew); + double slowEma = _slowEma.Calc(Input.Value, Input.IsNew); + + if (Math.Abs(slowEma) <= double.Epsilon) + return 0.0; + + return ((fastEma - slowEma) / slowEma) * ScalingFactor; + } +} diff --git a/lib/momentum/Prs.cs b/lib/momentum/Prs.cs new file mode 100644 index 00000000..f2bbb682 --- /dev/null +++ b/lib/momentum/Prs.cs @@ -0,0 +1,84 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// PRS: Price Relative Strength +/// A momentum indicator that compares the performance of a security against a benchmark, +/// helping identify which is showing stronger relative momentum. +/// +/// +/// The PRS calculation process: +/// 1. Take the current price of the security +/// 2. Take the current price of the benchmark +/// 3. Calculate the ratio between them +/// 4. Multiply by a scaling factor for better visualization +/// +/// Key characteristics: +/// - Measures relative performance against a benchmark +/// - Helps identify market leaders and laggards +/// - Rising PRS indicates outperformance +/// - Falling PRS indicates underperformance +/// +/// Formula: +/// PRS = (Price / Benchmark) * 100 +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// StockCharts.com Technical Indicators +/// + +[SkipLocalsInit] +public sealed class Prs : AbstractBase +{ + private const double ScalingFactor = 100.0; + private double _benchmark; + private double _p_benchmark; + + /// + /// Initializes a new instance of the PRS indicator + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Prs() + { + WarmupPeriod = 1; + Name = "PRS"; + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Prs(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + /// + /// Sets the current benchmark value + /// + /// The benchmark value to compare against + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetBenchmark(double benchmark) + { + _benchmark = benchmark; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _p_benchmark = _benchmark; + else + _benchmark = _p_benchmark; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_benchmark <= double.Epsilon) + return 0.0; + + return (Input.Value / _benchmark) * ScalingFactor; + } +} diff --git a/lib/momentum/Roc.cs b/lib/momentum/Roc.cs new file mode 100644 index 00000000..f61070ec --- /dev/null +++ b/lib/momentum/Roc.cs @@ -0,0 +1,80 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ROC: Rate of Change +/// A momentum indicator that measures the percentage change in price over a specified +/// period, helping identify the speed and strength of price movements. +/// +/// +/// The ROC calculation process: +/// 1. Store historical prices in a circular buffer +/// 2. Calculate percentage change between current and historical price +/// 3. Multiply by scaling factor for better visualization +/// +/// Key characteristics: +/// - Pure momentum indicator +/// - Oscillates around zero line +/// - Helps identify overbought/oversold conditions +/// - Useful for divergence analysis +/// +/// Formula: +/// ROC = ((Price - PriceN) / PriceN) * 100 +/// where PriceN is the price N periods ago +/// +/// Sources: +/// Technical Analysis of Financial Markets by John J. Murphy +/// Technical Analysis of Stock Trends by Robert D. Edwards and John Magee +/// + +[SkipLocalsInit] +public sealed class Roc : AbstractBase +{ + private readonly CircularBuffer _priceBuffer; + private const double ScalingFactor = 100.0; + private const int DefaultPeriod = 12; + + /// The lookback period for ROC calculation (default 12). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Roc(int period = DefaultPeriod) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _priceBuffer = new(period + 1); + WarmupPeriod = period; + Name = $"ROC({period})"; + } + + /// The data source object that publishes updates. + /// The lookback period for ROC calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Roc(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _priceBuffer.Add(Input.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_priceBuffer.Count < _priceBuffer.Capacity) + return 0.0; + + double oldPrice = _priceBuffer[0]; + if (oldPrice <= double.Epsilon) + return 0.0; + + return ((Input.Value - oldPrice) / oldPrice) * ScalingFactor; + } +} diff --git a/lib/momentum/Vel.cs b/lib/momentum/Vel.cs new file mode 100644 index 00000000..c0a8472e --- /dev/null +++ b/lib/momentum/Vel.cs @@ -0,0 +1,89 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// Vel: Velocity +/// An enhanced momentum indicator that applies Jurik Moving Average (JMA) smoothing +/// to the basic momentum calculation, providing better noise reduction while +/// maintaining responsiveness to significant price movements. +/// +/// +/// The Velocity calculation process: +/// 1. Calculate basic momentum (price difference) +/// 2. Apply JMA smoothing to the momentum values +/// 3. No scaling factor applied to maintain price-based units +/// +/// Key characteristics: +/// - Enhanced momentum measurement with JMA smoothing +/// - Better noise reduction than basic momentum +/// - Maintains responsiveness to significant moves +/// - Reduced lag through JMA's phase-shifting +/// +/// Formula: +/// Mom = Price - PriceN +/// Vel = JMA(Mom, period) +/// +/// Sources: +/// Enhanced with JMA smoothing by Mark Jurik +/// Technical Analysis of Financial Markets by John J. Murphy +/// + +[SkipLocalsInit] +public sealed class Vel : AbstractBase +{ + private readonly CircularBuffer _priceBuffer; + private readonly Jma _smoothing; + private const int DefaultPeriod = 10; + private const int DefaultPhase = 100; + private const double DefaultFactor = 0.25; + + /// The lookback period for velocity calculation (default 10). + /// The phase for the JMA smoothing (default 0). + /// The power factor for the JMA smoothing (default 2.0). + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vel(int period = DefaultPeriod, int phase = DefaultPhase, double factor = DefaultFactor) + { + if (period < 1) + throw new ArgumentOutOfRangeException(nameof(period)); + + _priceBuffer = new(period + 1); + _smoothing = new(period, phase, factor); + WarmupPeriod = period * 2; // JMA needs more warmup periods + Name = $"VEL({period})"; + } + + /// The data source object that publishes updates. + /// The lookback period for velocity calculation. + /// The phase for the JMA smoothing. + /// The power factor for the JMA smoothing. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vel(object source, int period, int phase = DefaultPhase, double power = DefaultFactor) + : this(period, phase, power) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new ValueSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + _priceBuffer.Add(Input.Value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(Input.IsNew); + + if (_priceBuffer.Count < _priceBuffer.Capacity) + return 0.0; + + // Calculate basic momentum + double momentum = Input.Value - _priceBuffer[0]; + + // Apply JMA smoothing + return _smoothing.Calc(momentum, Input.IsNew); + } +} diff --git a/lib/momentum/_list.md b/lib/momentum/_list.md index 398da442..1928a3d8 100644 --- a/lib/momentum/_list.md +++ b/lib/momentum/_list.md @@ -1,16 +1,18 @@ +# Momentum indicators + ✔️ ADX - Average Directional Movement Index ✔️ ADXR - Average Directional Movement Index Rating ✔️ APO - Absolute Price Oscillator -DMI - Directional Movement Index -DMX - Jurik Directional Movement Index +✔️ DMI - Directional Movement Index +✔️ DMX - Jurik Directional Movement Index DPO - Detrended Price Oscillator MACD - Moving Average Convergence/Divergence -MOM - Momentum -PMO - Price Momentum Oscillator -PO - Price Oscillator -PPO - Percentage Price Oscillator -PRS - Price Relative Strength -ROC - Rate of Change +✔️ MOM - Momentum +✔️ PMO - Price Momentum Oscillator +✔️ PO - Price Oscillator +✔️ PPO - Percentage Price Oscillator +✔️ PRS - Price Relative Strength +✔️ ROC - Rate of Change TRIX - 1-day ROC of TEMA -VEL - Jurik Signal Velocity +✔️ VEL - Jurik Signal Velocity VORTEX - Vortex Indicator diff --git a/lib/oscillators/Ac.cs b/lib/oscillators/Ac.cs new file mode 100644 index 00000000..f298d560 --- /dev/null +++ b/lib/oscillators/Ac.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// AC: Acceleration/Deceleration Oscillator +/// A momentum indicator that measures the acceleration and deceleration of the current driving force. +/// It is derived from the Awesome Oscillator (AO) and helps identify potential trend reversals. +/// +/// +/// The AC calculation process: +/// 1. Calculate the Awesome Oscillator (AO) +/// 2. Calculate a 5-period simple moving average of the AO +/// 3. Subtract the 5-period SMA from the current AO value +/// +/// Key characteristics: +/// - Oscillates above and below zero +/// - Measures the acceleration/deceleration of market driving force +/// - Positive values indicate increasing momentum +/// - Negative values indicate decreasing momentum +/// - Can be used to identify potential trend reversals +/// +/// Formula: +/// AC = AO - SMA(AO, 5) +/// +/// Sources: +/// Bill Williams - "Trading Chaos" (1995) +/// https://www.investopedia.com/terms/a/ac.asp +/// + +[SkipLocalsInit] +public sealed class Ac : AbstractBase +{ + private readonly Ao _ao; + private readonly Sma _sma5; + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ac(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ac() + { + _ao = new Ao(); + _sma5 = new Sma(5); + WarmupPeriod = 39; // AO requires 34 periods + 5 for AC's SMA + Name = "AC"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + var ao = _ao.Calc(BarInput, BarInput.IsNew); + _sma5.Calc(ao, BarInput.IsNew); + + return ao - _sma5.Value; + } +} diff --git a/lib/oscillators/Ao.cs b/lib/oscillators/Ao.cs new file mode 100644 index 00000000..97c39ea0 --- /dev/null +++ b/lib/oscillators/Ao.cs @@ -0,0 +1,71 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// AO: Awesome Oscillator +/// A momentum indicator that reflects the precise changes in the market driving force. +/// It is used to affirm trends or to anticipate possible reversals. +/// +/// +/// The AO calculation process: +/// 1. Calculates the 5-period simple moving average of the HL2 (High+Low)/2 values. +/// 2. Calculates the 34-period simple moving average of the HL2 (High+Low)/2 values. +/// 3. Subtracts the 34-period SMA from the 5-period SMA. +/// +/// Key characteristics: +/// - Oscillates above and below zero +/// - Positive values indicate bullish momentum +/// - Negative values indicate bearish momentum +/// - Crosses above zero suggest buying opportunities +/// - Crosses below zero suggest selling opportunities +/// +/// Formula: +/// AO = SMA(HL2, 5) - SMA(HL2, 34) +/// +/// Sources: +/// Bill Williams - "Trading Chaos" (1995) +/// https://www.investopedia.com/terms/a/awesomeoscillator.asp +/// + +[SkipLocalsInit] +public sealed class Ao : AbstractBase +{ + private readonly Sma _sma5; + private readonly Sma _sma34; + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ao(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ao() + { + _sma5 = new Sma(5); + _sma34 = new Sma(34); + WarmupPeriod = 34; + Name = "AO"; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + _sma5.Calc(BarInput.HL2, BarInput.IsNew); + _sma34.Calc(BarInput.HL2, BarInput.IsNew); + + return _sma5.Value - _sma34.Value; + } +} diff --git a/lib/oscillators/_list.md b/lib/oscillators/_list.md index ecda4263..b779856e 100644 --- a/lib/oscillators/_list.md +++ b/lib/oscillators/_list.md @@ -1,5 +1,7 @@ -AC - Acceleration Oscillator -AO - Awesome Oscillator +# Oscillators indicators + +✔️ AC - Acceleration Oscillator +✔️ AO - Awesome Oscillator AROON - Aroon oscillator BOP - Balance of Power CCI - Commodity Channel Index diff --git a/lib/statistics/_list.md b/lib/statistics/_list.md index b7f77ddc..84fa4b37 100644 --- a/lib/statistics/_list.md +++ b/lib/statistics/_list.md @@ -1,3 +1,5 @@ +# Statistics indicators + BETA - Beta coefficient CORR - Correlation Coefficient ✔️ CURVATURE - Rate of Change in Direction or Slope diff --git a/lib/volatility/_list.md b/lib/volatility/_list.md index 5f62b5cf..1ec6089e 100644 --- a/lib/volatility/_list.md +++ b/lib/volatility/_list.md @@ -1,3 +1,5 @@ +# Volatility indicators + ADR - Average Daily Range AP - Andrew's Pitchfork ✔️ ATR - Average True Range @@ -22,7 +24,7 @@ PSAR - Parabolic Stop and Reverse PV - Parkinson Volatility RSV - Rogers-Satchell Volatility ✔️ RV - Realized Volatility -RVI - Relative Volatility Index +✔️ RVI - Relative Volatility Index STARC - Starc Bands SV - Stochastic Volatility TR - True Range diff --git a/lib/volume/Adl.cs b/lib/volume/Adl.cs new file mode 100644 index 00000000..786233ab --- /dev/null +++ b/lib/volume/Adl.cs @@ -0,0 +1,113 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADL: Accumulation Distribution Line (Chaikin) +/// A volume-based indicator that measures the cumulative flow of money into and out +/// of a security. It assesses the relationship between price and volume to determine +/// buying/selling pressure. +/// +/// +/// The ADL calculation process: +/// 1. Calculates Money Flow Multiplier (MFM): +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// 2. Calculates Money Flow Volume (MFV): +/// MFV = MFM × Volume +/// 3. ADL is cumulative sum of MFV values +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Cumulative indicator +/// - No upper/lower bounds +/// - Trend confirmation tool +/// - Divergence indicator +/// +/// Formula: +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// MFV = MFM × Volume +/// ADL = Previous ADL + MFV +/// +/// Market Applications: +/// - Trend confirmation +/// - Volume analysis +/// - Price/volume divergence +/// - Support/resistance levels +/// - Market participation +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/a/accumulationdistribution.asp +/// +/// Note: Focuses on the relationship between price and volume +/// + +[SkipLocalsInit] +public sealed class Adl : AbstractBase +{ + private double _cumulativeAdl; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adl() + { + WarmupPeriod = 1; + Name = "ADL"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adl(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeAdl = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMoneyFlowMultiplier(double close, double high, double low) + { + double range = high - low; + if (range > 0) + { + return ((close - low) - (high - close)) / range; + } + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Money Flow Multiplier + double mfm = CalculateMoneyFlowMultiplier(BarInput.Close, BarInput.High, BarInput.Low); + + // Calculate Money Flow Volume + double mfv = mfm * BarInput.Volume; + + // Update cumulative ADL only for new bars + if (BarInput.IsNew) + { + _cumulativeAdl += mfv; + } + + IsHot = _index >= WarmupPeriod; + return _cumulativeAdl; + } +} diff --git a/lib/volume/Adosc.cs b/lib/volume/Adosc.cs new file mode 100644 index 00000000..0d718fd2 --- /dev/null +++ b/lib/volume/Adosc.cs @@ -0,0 +1,138 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// ADOSC: Chaikin Accumulation/Distribution Oscillator +/// A momentum indicator that measures the strength of accumulation/distribution by combining +/// price and volume with moving averages. It helps identify potential trend reversals and +/// buying/selling pressure. +/// +/// +/// The ADOSC calculation process: +/// 1. Calculate ADL (Accumulation/Distribution Line) +/// a. Money Flow Multiplier = ((Close - Low) - (High - Close)) / (High - Low) +/// b. Money Flow Volume = MFM × Volume +/// c. ADL = Previous ADL + MFV +/// 2. Calculate two EMAs of ADL values +/// 3. Subtract longer EMA from shorter EMA +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Oscillates around zero +/// - Uses two different time periods +/// - Default periods are 3 and 10 days +/// - Shows momentum of money flow +/// +/// Formula: +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// MFV = MFM × Volume +/// ADL = Previous ADL + MFV +/// ADOSC = EMA(ADL, shortPeriod) - EMA(ADL, longPeriod) +/// +/// Market Applications: +/// - Trend confirmation +/// - Divergence analysis +/// - Volume/price relationship +/// - Support/resistance levels +/// - Market reversals +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/c/chaikinoscillator.asp +/// +/// Note: Positive values indicate buying pressure, while negative values indicate selling pressure +/// + +[SkipLocalsInit] +public sealed class Adosc : AbstractBase +{ + private readonly int _longPeriod; + private double _cumulativeAdl; + private double _shortEma; + private double _longEma; + private readonly double _shortAlpha; + private readonly double _longAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adosc(int shortPeriod = 3, int longPeriod = 10) + { + _longPeriod = longPeriod; + WarmupPeriod = longPeriod; // Need longer period for EMA calculation + Name = $"ADOSC({shortPeriod},{_longPeriod})"; + _shortAlpha = 2.0 / (shortPeriod + 1); + _longAlpha = 2.0 / (longPeriod + 1); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Adosc(object source, int shortPeriod = 3, int longPeriod = 10) : this(shortPeriod, longPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeAdl = 0; + _shortEma = 0; + _longEma = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMoneyFlowMultiplier(double close, double high, double low) + { + double range = high - low; + if (range > 0) + { + return ((close - low) - (high - close)) / range; + } + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Money Flow Multiplier + double mfm = CalculateMoneyFlowMultiplier(BarInput.Close, BarInput.High, BarInput.Low); + + // Calculate Money Flow Volume + double mfv = mfm * BarInput.Volume; + + // Update cumulative ADL + _cumulativeAdl += mfv; + + // Calculate EMAs + if (_index <= _longPeriod) + { + // Initialize EMAs + _shortEma = _cumulativeAdl; + _longEma = _cumulativeAdl; + return 0; + } + + // Update EMAs + _shortEma = (_shortAlpha * _cumulativeAdl) + ((1 - _shortAlpha) * _shortEma); + _longEma = (_longAlpha * _cumulativeAdl) + ((1 - _longAlpha) * _longEma); + + // Calculate ADOSC + double adosc = _shortEma - _longEma; + + IsHot = _index >= WarmupPeriod; + return adosc; + } +} diff --git a/lib/volume/Aobv.cs b/lib/volume/Aobv.cs new file mode 100644 index 00000000..540ddd72 --- /dev/null +++ b/lib/volume/Aobv.cs @@ -0,0 +1,133 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// AOBV: Archer On-Balance Volume +/// A modified version of the traditional On-Balance Volume (OBV) indicator that uses a more +/// sophisticated method to determine buying and selling pressure. It considers both the +/// closing price and the price range to provide a more nuanced view of volume flow. +/// +/// +/// The AOBV calculation process: +/// 1. Determine price position within the day's range +/// 2. Apply volume based on price position: +/// - If close is in upper 1/3 of range: Add full volume +/// - If close is in middle 1/3 of range: Add/subtract half volume +/// - If close is in lower 1/3 of range: Subtract full volume +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Cumulative indicator +/// - No upper/lower bounds +/// - More nuanced than traditional OBV +/// - Considers price position in range +/// +/// Formula: +/// Range = High - Low +/// UpperThird = High - (Range / 3) +/// LowerThird = Low + (Range / 3) +/// If Close >= UpperThird: +/// AOBV = Previous AOBV + Volume +/// Else if Close <= LowerThird: +/// AOBV = Previous AOBV - Volume +/// Else: +/// If Close > Previous Close: +/// AOBV = Previous AOBV + (Volume / 2) +/// Else: +/// AOBV = Previous AOBV - (Volume / 2) +/// +/// Market Applications: +/// - Trend confirmation +/// - Volume analysis +/// - Price/volume divergence +/// - Support/resistance levels +/// - Market participation +/// +/// Sources: +/// Steve Archer - Original development +/// Technical Analysis of Stock Trends (Edwards, Magee) +/// +/// Note: Provides a more detailed analysis of volume flow than traditional OBV +/// + +[SkipLocalsInit] +public sealed class Aobv : AbstractBase +{ + private double _cumulativeAobv; + private double _prevClose; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Aobv() + { + WarmupPeriod = 1; + Name = "AOBV"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Aobv(object source) : this() + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _cumulativeAobv = 0; + _prevClose = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + double range = BarInput.High - BarInput.Low; + if (range > 0) + { + double upperThird = BarInput.High - (range / 3); + double lowerThird = BarInput.Low + (range / 3); + + // Determine volume flow based on price position + if (BarInput.Close >= upperThird) + { + _cumulativeAobv += BarInput.Volume; + } + else if (BarInput.Close <= lowerThird) + { + _cumulativeAobv -= BarInput.Volume; + } + else + { + // In middle third, use half volume based on close comparison + _cumulativeAobv += (BarInput.Close > _prevClose) ? + (BarInput.Volume / 2) : -(BarInput.Volume / 2); + } + } + + _prevClose = BarInput.Close; + + IsHot = _index >= WarmupPeriod; + return _cumulativeAobv; + } +} diff --git a/lib/volume/Cmf.cs b/lib/volume/Cmf.cs new file mode 100644 index 00000000..2ac7abf6 --- /dev/null +++ b/lib/volume/Cmf.cs @@ -0,0 +1,129 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CMF: Chaikin Money Flow +/// A volume-weighted technical indicator that measures the amount of Money Flow Volume (MFV) +/// over a specific period. Unlike ADL which is cumulative, CMF averages the Money Flow +/// Volume over a specified period. +/// +/// +/// The CMF calculation process: +/// 1. Calculates Money Flow Multiplier (MFM): +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// 2. Calculates Money Flow Volume (MFV): +/// MFV = MFM × Volume +/// 3. CMF = Sum(MFV) / Sum(Volume) over N periods +/// +/// Key characteristics: +/// - Oscillator between -1 and +1 +/// - Volume-weighted measure +/// - Non-cumulative indicator +/// - Default period is 20 days +/// +/// Formula: +/// MFM = ((Close - Low) - (High - Close)) / (High - Low) +/// MFV = MFM × Volume +/// CMF = Sum(MFV over N periods) / Sum(Volume over N periods) +/// +/// Market Applications: +/// - Trend confirmation +/// - Volume analysis +/// - Price/volume divergence +/// - Support/resistance levels +/// - Market participation +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/c/chaikinmoneyflow.asp +/// +/// Note: Values above zero indicate buying pressure, while values below zero indicate selling pressure +/// + +[SkipLocalsInit] +public sealed class Cmf : AbstractBase +{ + private readonly int _period; + private readonly double[] _mfv; + private readonly double[] _volume; + private int _position; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmf(int period = 20) + { + _period = period; + WarmupPeriod = period; + Name = $"CMF({_period})"; + _mfv = new double[period]; + _volume = new double[period]; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cmf(object source, int period = 20) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _position = 0; + Array.Clear(_mfv, 0, _mfv.Length); + Array.Clear(_volume, 0, _volume.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static double CalculateMoneyFlowMultiplier(double close, double high, double low) + { + double range = high - low; + if (range > 0) + { + return ((close - low) - (high - close)) / range; + } + return 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Money Flow Multiplier + double mfm = CalculateMoneyFlowMultiplier(BarInput.Close, BarInput.High, BarInput.Low); + + // Calculate Money Flow Volume + double currentMfv = mfm * BarInput.Volume; + + // Update circular buffers + _mfv[_position] = currentMfv; + _volume[_position] = BarInput.Volume; + _position = (_position + 1) % _period; + + // Calculate CMF + double sumMfv = 0; + double sumVolume = 0; + for (int i = 0; i < _period; i++) + { + sumMfv += _mfv[i]; + sumVolume += _volume[i]; + } + + double cmf = Math.Abs(sumVolume) > double.Epsilon ? sumMfv / sumVolume : 0; + IsHot = _index >= WarmupPeriod; + return cmf; + } +} diff --git a/lib/volume/Eom.cs b/lib/volume/Eom.cs new file mode 100644 index 00000000..0c2af40b --- /dev/null +++ b/lib/volume/Eom.cs @@ -0,0 +1,132 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// EOM: Ease of Movement +/// A volume-based technical indicator that relates price change to volume, showing the +/// relationship between price change and volume. It emphasizes days where price changes +/// are accomplished with minimal volume and minimizes days where large volume generates +/// small price changes. +/// +/// +/// The EOM calculation process: +/// 1. Calculate the distance moved: +/// Distance = ((High + Low)/2 - (Prior High + Prior Low)/2) +/// 2. Calculate the Box Ratio: +/// BoxRatio = Volume / (High - Low) +/// 3. Calculate single-period EMV: +/// EMV = Distance / BoxRatio +/// 4. Smooth EMV using simple moving average (optional) +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Oscillates around zero +/// - Shows ease of price movement +/// - Default period is 14 days +/// +/// Formula: +/// Distance = ((H + L)/2 - (pH + pL)/2) +/// BoxRatio = Volume / (High - Low) +/// EMV = Distance / BoxRatio +/// EOM = SMA(EMV, period) +/// +/// Market Applications: +/// - Trend strength analysis +/// - Volume/price relationship +/// - Support/resistance breakouts +/// - Market momentum +/// - Divergence identification +/// +/// Sources: +/// Richard W. Arms Jr. - Original development +/// https://www.investopedia.com/terms/e/easeofmovement.asp +/// +/// Note: Positive values suggest prices are rising with light volume (bullish), +/// while negative values suggest prices are falling with light volume (bearish) +/// + +[SkipLocalsInit] +public sealed class Eom : AbstractBase +{ + private readonly int _period; + private readonly double[] _emv; + private int _position; + private double _prevMidpoint; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Eom(int period = 14) + { + _period = period; + WarmupPeriod = period + 1; // Need one extra period for previous midpoint + Name = $"EOM({_period})"; + _emv = new double[period]; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Eom(object source, int period = 14) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _position = 0; + _prevMidpoint = 0; + Array.Clear(_emv, 0, _emv.Length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + double midpoint = (BarInput.High + BarInput.Low) / 2; + double boxRatio = BarInput.Volume / (BarInput.High - BarInput.Low + double.Epsilon); // Avoid division by zero + + // Skip first period to establish previous midpoint + if (_index == 1) + { + _prevMidpoint = midpoint; + return 0; + } + + // Calculate distance moved + double distance = midpoint - _prevMidpoint; + + // Calculate EMV for this period + double emv = distance / boxRatio * 10000; // Multiply by 10000 to make values more readable + + // Store in circular buffer + _emv[_position] = emv; + _position = (_position + 1) % _period; + + // Calculate EOM (simple moving average of EMV) + double sum = 0; + for (int i = 0; i < _period; i++) + { + sum += _emv[i]; + } + double eom = sum / _period; + + // Store current midpoint for next calculation + _prevMidpoint = midpoint; + + IsHot = _index >= WarmupPeriod; + return eom; + } +} diff --git a/lib/volume/Kvo.cs b/lib/volume/Kvo.cs new file mode 100644 index 00000000..4e49c5b4 --- /dev/null +++ b/lib/volume/Kvo.cs @@ -0,0 +1,141 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// KVO: Klinger Volume Oscillator +/// A volume-based technical indicator that compares volume to price movement to identify +/// long-term trends and potential reversals. It helps determine the long-term money flow +/// while remaining sensitive to short-term fluctuations. +/// +/// +/// The KVO calculation process: +/// 1. Calculate Trend: +/// Trend = Current DM > Previous DM ? +1 : -1 +/// 2. Calculate Volume Force (VF): +/// VF = Volume * abs(ROC) * Trend * 100 +/// 3. Calculate two EMAs of VF and their difference: +/// Signal = EMA(VF, shortPeriod) - EMA(VF, longPeriod) +/// +/// Key characteristics: +/// - Volume-weighted measure +/// - Oscillates around zero +/// - Uses two different time periods +/// - Default periods are 34 and 55 days +/// - Shows volume force and price direction +/// +/// Formula: +/// DM = (H + L + C) / 3 +/// Trend = DM > Previous DM ? +1 : -1 +/// VF = Volume * abs(ROC) * Trend * 100 +/// KVO = EMA(VF, shortPeriod) - EMA(VF, longPeriod) +/// +/// Market Applications: +/// - Trend confirmation +/// - Divergence analysis +/// - Volume/price relationship +/// - Support/resistance levels +/// - Market reversals +/// +/// Sources: +/// Stephen Klinger - Original development +/// https://www.investopedia.com/terms/k/klingeroscillator.asp +/// +/// Note: Positive values indicate buying pressure, while negative values indicate selling pressure +/// + +[SkipLocalsInit] +public sealed class Kvo : AbstractBase +{ + private readonly int _longPeriod; + private double _prevDm; + private double _shortEma; + private double _longEma; + private readonly double _shortAlpha; + private readonly double _longAlpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Kvo(int shortPeriod = 34, int longPeriod = 55) + { + _longPeriod = longPeriod; + WarmupPeriod = longPeriod + 1; // Need one extra period for previous DM + Name = $"KVO({shortPeriod},{_longPeriod})"; + _shortAlpha = 2.0 / (shortPeriod + 1); + _longAlpha = 2.0 / (longPeriod + 1); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Kvo(object source, int shortPeriod = 34, int longPeriod = 55) : this(shortPeriod, longPeriod) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevDm = 0; + _shortEma = 0; + _longEma = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate Daily Mean + double dm = (BarInput.High + BarInput.Low + BarInput.Close) / 3; + + // Skip first period to establish previous DM + if (_index == 1) + { + _prevDm = dm; + return 0; + } + + // Calculate Trend + int trend = dm > _prevDm ? 1 : -1; + + // Calculate Rate of Change + double roc = Math.Abs(dm - _prevDm) / _prevDm; + + // Calculate Volume Force + double vf = BarInput.Volume * roc * trend * 100; + + // Calculate EMAs + if (_index <= _longPeriod) + { + // Initialize EMAs + _shortEma = vf; + _longEma = vf; + } + else + { + // Update EMAs + _shortEma = (_shortAlpha * vf) + ((1 - _shortAlpha) * _shortEma); + _longEma = (_longAlpha * vf) + ((1 - _longAlpha) * _longEma); + } + + // Store current DM for next calculation + _prevDm = dm; + + // Calculate KVO + double kvo = _shortEma - _longEma; + + IsHot = _index >= WarmupPeriod; + return kvo; + } +} diff --git a/lib/volume/_list.md b/lib/volume/_list.md index 0b99daa1..86ec2225 100644 --- a/lib/volume/_list.md +++ b/lib/volume/_list.md @@ -1,9 +1,11 @@ -ADL - Chaikin Accumulation Distribution Line -ADOSC - Chaikin Accumulation Distribution Oscillator -AOBV - Archer On-Balance Volume -CMF - Chaikin Money Flow -EOM - Ease of Movement -KVO - Klinger Volume Oscillator +# Volume indicators + +✔️ ADL - Chaikin Accumulation Distribution Line +✔️ ADOSC - Chaikin Accumulation Distribution Oscillator +✔️ AOBV - Archer On-Balance Volume +✔️ CMF - Chaikin Money Flow +✔️ EOM - Ease of Movement +✔️ KVO - Klinger Volume Oscillator MFI - Money Flow Index NVI - Negative Volume Index OBV - On-Balance Volume