using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// UO: Ultimate Oscillator /// A momentum oscillator that uses three different time periods to reduce volatility /// and false signals. It incorporates a weighted average of three oscillator calculations /// using different periods. /// /// /// The UO calculation process: /// 1. Calculate buying pressure (BP): Close - Min(Low, Prior Close) /// 2. Calculate true range (TR): Max(High, Prior Close) - Min(Low, Prior Close) /// 3. Calculate average of BP/TR for each period /// 4. Apply weights to each period's average /// 5. Scale result to oscillator range /// /// Key characteristics: /// - Oscillates between 0 and 100 /// - Uses multiple timeframes to reduce false signals /// - Weighted sum of three periods /// - Traditional overbought level at 70 /// - Traditional oversold level at 30 /// /// Formula: /// UO = 100 * ((4 * Average7) + (2 * Average14) + Average28) / (4 + 2 + 1) /// where: /// Average7 = 7-period average of BP/TR /// Average14 = 14-period average of BP/TR /// Average28 = 28-period average of BP/TR /// /// Sources: /// Larry Williams - "New Trading Dimensions" (1998) /// https://www.investopedia.com/terms/u/ultimateoscillator.asp /// /// Note: Default periods (7,14,28) and weights (4,2,1) were recommended by Williams /// [SkipLocalsInit] public sealed class Uo : AbstractBase { private readonly CircularBuffer _bp1; private readonly CircularBuffer _tr1; private readonly CircularBuffer _bp2; private readonly CircularBuffer _tr2; private readonly CircularBuffer _bp3; private readonly CircularBuffer _tr3; private readonly double _weight1; private readonly double _weight2; private readonly double _weight3; private double _prevClose; private const int DefaultPeriod1 = 7; private const int DefaultPeriod2 = 14; private const int DefaultPeriod3 = 28; private const double DefaultWeight1 = 4.0; private const double DefaultWeight2 = 2.0; private const double DefaultWeight3 = 1.0; private const double ScalingFactor = 100.0; /// The first period (default 7). /// The second period (default 14). /// The third period (default 28). /// Weight for first period (default 4). /// Weight for second period (default 2). /// Weight for third period (default 1). /// Thrown when any period is less than 1 or any weight is less than or equal to 0. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Uo(int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3, double weight1 = DefaultWeight1, double weight2 = DefaultWeight2, double weight3 = DefaultWeight3) { ArgumentOutOfRangeException.ThrowIfLessThan(period1, 1); ArgumentOutOfRangeException.ThrowIfLessThan(period2, 1); ArgumentOutOfRangeException.ThrowIfLessThan(period3, 1); ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(weight1, 0); ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(weight2, 0); ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(weight3, 0); _weight1 = weight1; _weight2 = weight2; _weight3 = weight3; _bp1 = new(period1); _tr1 = new(period1); _bp2 = new(period2); _tr2 = new(period2); _bp3 = new(period3); _tr3 = new(period3); WarmupPeriod = period3; Name = $"UO({period1},{period2},{period3})"; } /// The data source object that publishes updates. /// The first period. /// The second period. /// The third period. /// Weight for first period. /// Weight for second period. /// Weight for third period. [MethodImpl(MethodImplOptions.AggressiveInlining)] public Uo(object source, int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3, double weight1 = DefaultWeight1, double weight2 = DefaultWeight2, double weight3 = DefaultWeight3) : this(period1, period2, period3, weight1, weight2, weight3) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override void ManageState(bool isNew) { if (isNew) { if (_index == 0) _prevClose = BarInput.Close; _index++; } } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static double CalculateAverage(CircularBuffer bp, CircularBuffer tr) { double trSum = tr.Sum(); return trSum >= double.Epsilon ? bp.Sum() / trSum : 0; } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] protected override double Calculation() { ManageState(BarInput.IsNew); // Calculate buying pressure and true range double minLowPrevClose = Math.Min(BarInput.Low, _prevClose); double maxHighPrevClose = Math.Max(BarInput.High, _prevClose); double bp = BarInput.Close - minLowPrevClose; double tr = maxHighPrevClose - minLowPrevClose; if (BarInput.IsNew) { // Add values to buffers _bp1.Add(bp); _tr1.Add(tr); _bp2.Add(bp); _tr2.Add(tr); _bp3.Add(bp); _tr3.Add(tr); _prevClose = BarInput.Close; } // Not enough data if (_index <= 1) return 0; // Calculate averages for each period double avg1 = CalculateAverage(_bp1, _tr1); double avg2 = CalculateAverage(_bp2, _tr2); double avg3 = CalculateAverage(_bp3, _tr3); // Calculate weighted sum double weightSum = _weight1 + _weight2 + _weight3; return ScalingFactor * (((_weight1 * avg1) + (_weight2 * avg2) + (_weight3 * avg3)) / weightSum); } }