using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// USI: Ehlers Ultimate Strength Index /// /// /// A symmetric RSI replacement that uses the UltimateSmoother filter instead of /// Wilder's exponential smoothing. Output ranges from -1 to +1 with significantly /// reduced lag compared to traditional RSI. /// /// Pipeline: /// /// SU = max(0, Close - Close[1]), SD = max(0, Close[1] - Close) /// avgSU = SMA(SU, 4), avgSD = SMA(SD, 4) /// USU = UltimateSmoother(avgSU, period), USD = UltimateSmoother(avgSD, period) /// USI = (USU - USD) / (USU + USD) when denom > 0 /// /// /// Reference: John F. Ehlers, "Ultimate Strength Index (USI)", /// Technical Analysis of Stocks & Commodities, November 2024. /// /// Detailed documentation /// Reference Pine Script implementation [SkipLocalsInit] public sealed class Usi : AbstractBase { private const int SmaLen = 4; private const double MinSmoothed = 0.01; [StructLayout(LayoutKind.Auto)] private record struct State( double PrevClose, // SMA(4) circular buffers double Su0, double Su1, double Su2, double Su3, double Sd0, double Sd1, double Sd2, double Sd3, int BufIdx, // UltimateSmoother IIR state for SU path double Usu1, double Usu2, double AvgSu1, double AvgSu2, // UltimateSmoother IIR state for SD path double Usd1, double Usd2, double AvgSd1, double AvgSd2, // Output double Usi, int Count, double LastValid) { public static State New() => new() { PrevClose = double.NaN, Su0 = 0, Su1 = 0, Su2 = 0, Su3 = 0, Sd0 = 0, Sd1 = 0, Sd2 = 0, Sd3 = 0, BufIdx = 0, Usu1 = 0, Usu2 = 0, AvgSu1 = 0, AvgSu2 = 0, Usd1 = 0, Usd2 = 0, AvgSd1 = 0, AvgSd2 = 0, Usi = 0, Count = 0, LastValid = 0 }; } // USF precomputed coefficients private readonly double _k0; // (1 - c1) private readonly double _k1; // (2*c1 - c2) private readonly double _k2; // -(c1 + c3) private readonly double _c2; private readonly double _c3; private State _s = State.New(); private State _ps = State.New(); /// /// Creates USI with specified period. /// /// UltimateSmoother filter period (must be > 0, default 28) public Usi(int period = 28) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } // UltimateSmoother coefficients (same as USF) double arg = Math.Sqrt(2) * Math.PI / period; double expArg = Math.Exp(-arg); _c2 = 2.0 * expArg * Math.Cos(arg); _c3 = -(expArg * expArg); double c1 = (1.0 + _c2 - _c3) / 4.0; _k0 = 1.0 - c1; _k1 = 2.0 * c1 - _c2; _k2 = -(c1 + _c3); Name = $"Usi({period})"; WarmupPeriod = period + SmaLen; } /// /// Creates USI with specified source and period. /// public Usi(ITValuePublisher source, int period = 28) : this(period) { source.Pub += Handle; } /// /// Creates USI with a TSeries source, primes from history, then subscribes. /// public Usi(TSeries source, int period = 28) : this(period) { Prime(source.Values); if (source.Count > 0) { Last = new TValue(source.LastTime, Last.Value); } source.Pub += Handle; } public override bool IsHot => _s.Count >= WarmupPeriod; public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { if (source.Length == 0) { return; } _s = State.New(); _ps = State.New(); for (int i = 0; i < source.Length; i++) { double val = source[i]; if (double.IsFinite(val)) { _s.LastValid = val; } else { val = _s.LastValid; } Step(val); } Last = new TValue(DateTime.MinValue, _s.Usi); _ps = _s; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public override TValue Update(TValue input, bool isNew = true) { if (isNew) { _ps = _s; } else { _s = _ps; } double val = input.Value; if (double.IsFinite(val)) { _s.LastValid = val; } else { val = _s.LastValid; } Step(val); Last = new TValue(input.Time, _s.Usi); PubEvent(Last, isNew); return Last; } [MethodImpl(MethodImplOptions.AggressiveOptimization)] public override TSeries Update(TSeries source) { if (source.Count == 0) { return []; } int len = source.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); source.Times.CopyTo(tSpan); Reset(); for (int i = 0; i < len; i++) { double val = source.Values[i]; if (double.IsFinite(val)) { _s.LastValid = val; } else { val = _s.LastValid; } Step(val); vSpan[i] = _s.Usi; } _ps = _s; Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } /// /// Core streaming step: SU/SD → SMA(4) → UltimateSmoother → normalize. /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void Step(double close) { _s.Count++; // First bar: seed PrevClose, no SU/SD yet if (double.IsNaN(_s.PrevClose)) { _s.PrevClose = close; return; } // --- Strength Up / Strength Down --- double diff = close - _s.PrevClose; double su = diff > 0 ? diff : 0.0; double sd = diff < 0 ? -diff : 0.0; _s.PrevClose = close; // --- SMA(4) circular buffer for SU --- int idx = _s.BufIdx & 3; // idx mod 4 switch (idx) { case 0: _s.Su0 = su; _s.Sd0 = sd; break; case 1: _s.Su1 = su; _s.Sd1 = sd; break; case 2: _s.Su2 = su; _s.Sd2 = sd; break; case 3: _s.Su3 = su; _s.Sd3 = sd; break; } _s.BufIdx++; double avgSu = (_s.Su0 + _s.Su1 + _s.Su2 + _s.Su3) * 0.25; double avgSd = (_s.Sd0 + _s.Sd1 + _s.Sd2 + _s.Sd3) * 0.25; // --- UltimateSmoother for USU and USD --- double usu, usd; if (_s.Count < 8) { // Bootstrap: pass-through before IIR has enough history usu = avgSu; usd = avgSd; } else { // USF IIR: k0*avg + k1*avg[1] + k2*avg[2] + c2*USF[1] + c3*USF[2] usu = Math.FusedMultiplyAdd(_c3, _s.Usu2, Math.FusedMultiplyAdd(_c2, _s.Usu1, Math.FusedMultiplyAdd(_k2, _s.AvgSu2, Math.FusedMultiplyAdd(_k1, _s.AvgSu1, _k0 * avgSu)))); usd = Math.FusedMultiplyAdd(_c3, _s.Usd2, Math.FusedMultiplyAdd(_c2, _s.Usd1, Math.FusedMultiplyAdd(_k2, _s.AvgSd2, Math.FusedMultiplyAdd(_k1, _s.AvgSd1, _k0 * avgSd)))); } // Shift IIR state _s.Usu2 = _s.Usu1; _s.Usu1 = usu; _s.AvgSu2 = _s.AvgSu1; _s.AvgSu1 = avgSu; _s.Usd2 = _s.Usd1; _s.Usd1 = usd; _s.AvgSd2 = _s.AvgSd1; _s.AvgSd1 = avgSd; // --- Normalization --- double denom = usu + usd; if (denom > MinSmoothed) { _s.Usi = Math.Clamp((usu - usd) / denom, -1.0, 1.0); } // else: keep previous _s.Usi value (denominator near zero = flat market) } /// /// Batch calculation returning a TSeries. /// public static TSeries Batch(TSeries source, int period = 28) { var indicator = new Usi(period); return indicator.Update(source); } /// /// Batch calculation writing to a pre-allocated output span. Zero-allocation hot path. /// public static void Batch(ReadOnlySpan source, Span output, int period = 28) { if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } if (source.Length == 0) { return; } var indicator = new Usi(period); for (int i = 0; i < source.Length; i++) { double val = source[i]; if (double.IsFinite(val)) { indicator._s.LastValid = val; } else { val = indicator._s.LastValid; } indicator.Step(val); output[i] = indicator._s.Usi; } } /// /// Creates a hot indicator from historical data, ready for streaming. /// public static (TSeries Results, Usi Indicator) Calculate(TSeries source, int period = 28) { var indicator = new Usi(period); TSeries results = indicator.Update(source); return (results, indicator); } public override void Reset() { _s = State.New(); _ps = _s; Last = default; } }