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; } }