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/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 89739c87..1928a3d8 100644
--- a/lib/momentum/_list.md
+++ b/lib/momentum/_list.md
@@ -3,16 +3,16 @@
✔️ 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