// TSI: True Strength Index by William Blau // Momentum oscillator measuring overbought/oversold conditions. // Uses double-smoothed EMA of price momentum vs absolute momentum. using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// TSI: True Strength Index /// /// /// Momentum oscillator that uses double-smoothed exponential moving averages /// of price momentum to reduce noise and identify trend strength. /// Ranges from -100 to +100, with higher values indicating bullish momentum. /// /// Calculation: /// /// Momentum = Price - Price[1] /// TSI = 100 × EMA(EMA(Momentum, longPeriod), shortPeriod) / EMA(EMA(|Momentum|, longPeriod), shortPeriod) /// Signal = EMA(TSI, signalPeriod) /// /// /// Key characteristics: /// - Double smoothing reduces noise and false signals /// - Bounded oscillator: -100 to +100 /// - Signal line crossovers generate trade signals /// - Zero line crossovers indicate trend changes /// /// Detailed documentation [SkipLocalsInit] public sealed class Tsi : AbstractBase { private const int DefaultLongPeriod = 25; private const int DefaultShortPeriod = 13; private const int DefaultSignalPeriod = 13; /// /// Gets the long period for first EMA smoothing. /// public int LongPeriod { get; } /// /// Gets the short period for second EMA smoothing. /// public int ShortPeriod { get; } /// /// Gets the signal line period. /// public int SignalPeriod { get; } private readonly TValuePublishedHandler _handler; // Four EMAs for double smoothing private readonly Ema _emaMomLong; // First smoothing of momentum private readonly Ema _emaMomShort; // Second smoothing of momentum private readonly Ema _emaAbsMomLong; // First smoothing of |momentum| private readonly Ema _emaAbsMomShort; // Second smoothing of |momentum| private readonly Ema _emaSignal; // Signal line EMA private double _prevValue; private double _p_prevValue; private double _lastSignal; private double _p_lastSignal; /// /// Gets the signal line value. /// public double Signal => _lastSignal; public override bool IsHot => _emaMomShort.IsHot && _emaAbsMomShort.IsHot && _emaSignal.IsHot; /// /// Initializes a new instance of the TSI indicator. /// /// The long period for first EMA smoothing (default: 25). /// The short period for second EMA smoothing (default: 13). /// The period for signal line EMA (default: 13). /// Thrown when any period is less than 1. public Tsi(int longPeriod = DefaultLongPeriod, int shortPeriod = DefaultShortPeriod, int signalPeriod = DefaultSignalPeriod) { if (longPeriod < 1) { throw new ArgumentException("Long period must be at least 1", nameof(longPeriod)); } if (shortPeriod < 1) { throw new ArgumentException("Short period must be at least 1", nameof(shortPeriod)); } if (signalPeriod < 1) { throw new ArgumentException("Signal period must be at least 1", nameof(signalPeriod)); } LongPeriod = longPeriod; ShortPeriod = shortPeriod; SignalPeriod = signalPeriod; _handler = Handle; // Initialize EMAs - use period directly for warmup _emaMomLong = new Ema(longPeriod); _emaMomShort = new Ema(shortPeriod); _emaAbsMomLong = new Ema(longPeriod); _emaAbsMomShort = new Ema(shortPeriod); _emaSignal = new Ema(signalPeriod); _prevValue = double.NaN; _p_prevValue = double.NaN; _lastSignal = 0; _p_lastSignal = 0; Name = $"Tsi({longPeriod},{shortPeriod},{signalPeriod})"; WarmupPeriod = longPeriod + shortPeriod + signalPeriod; } public Tsi(ITValuePublisher source, int longPeriod = DefaultLongPeriod, int shortPeriod = DefaultShortPeriod, int signalPeriod = DefaultSignalPeriod) : this(longPeriod, shortPeriod, signalPeriod) { source.Pub += _handler; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_prevValue = _prevValue; _p_lastSignal = _lastSignal; } else { _prevValue = _p_prevValue; _lastSignal = _p_lastSignal; } double val = input.Value; double mom = 0; double absMom = 0; if (!double.IsNaN(_prevValue)) { mom = val - _prevValue; absMom = Math.Abs(mom); } if (isNew) { _prevValue = val; } // Double smooth the momentum: EMA(EMA(mom, longPeriod), shortPeriod) double smoothedMomLong = _emaMomLong.Update(new TValue(input.Time, mom), isNew).Value; double doubleSmoothedMom = _emaMomShort.Update(new TValue(input.Time, smoothedMomLong), isNew).Value; // Double smooth the absolute momentum: EMA(EMA(|mom|, longPeriod), shortPeriod) double smoothedAbsMomLong = _emaAbsMomLong.Update(new TValue(input.Time, absMom), isNew).Value; double doubleSmoothedAbsMom = _emaAbsMomShort.Update(new TValue(input.Time, smoothedAbsMomLong), isNew).Value; // Calculate TSI: 100 × doubleSmoothedMom / doubleSmoothedAbsMom double tsi; const double epsilon = 1e-10; if (Math.Abs(doubleSmoothedAbsMom) < epsilon) { tsi = 0; // Avoid division by zero } else { tsi = 100.0 * doubleSmoothedMom / doubleSmoothedAbsMom; } // Calculate signal line: EMA(TSI, signalPeriod) _lastSignal = _emaSignal.Update(new TValue(input.Time, tsi), isNew).Value; Last = new TValue(input.Time, tsi); PubEvent(Last, isNew); return Last; } 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); // Batch calculate Batch(source.Values, vSpan, LongPeriod, ShortPeriod); source.Times.CopyTo(tSpan); // Restore state for streaming by replaying Reset(); for (int i = 0; i < len; i++) { Update(new TValue(source.Times[i], source.Values[i])); } Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } private void Handle(object? sender, in TValueEventArgs args) { Update(args.Value, args.IsNew); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { foreach (var value in source) { Update(new TValue(DateTime.MinValue, value)); } } public static TSeries Batch(TSeries source, int longPeriod = DefaultLongPeriod, int shortPeriod = DefaultShortPeriod, int signalPeriod = DefaultSignalPeriod) { var tsi = new Tsi(longPeriod, shortPeriod, signalPeriod); return tsi.Update(source); } /// /// Batch calculates TSI values (without signal line). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan source, Span output, int longPeriod = DefaultLongPeriod, int shortPeriod = DefaultShortPeriod) { if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } if (longPeriod < 1) { throw new ArgumentException("Long period must be at least 1", nameof(longPeriod)); } if (shortPeriod < 1) { throw new ArgumentException("Short period must be at least 1", nameof(shortPeriod)); } int len = source.Length; if (len == 0) { return; } // Calculate momentum: source[i] - source[i-1] double[] mom = System.Buffers.ArrayPool.Shared.Rent(len); double[] absMom = System.Buffers.ArrayPool.Shared.Rent(len); double[] smoothedMom = System.Buffers.ArrayPool.Shared.Rent(len); double[] smoothedAbsMom = System.Buffers.ArrayPool.Shared.Rent(len); Span momSpan = mom.AsSpan(0, len); Span absMomSpan = absMom.AsSpan(0, len); Span smoothedMomSpan = smoothedMom.AsSpan(0, len); Span smoothedAbsMomSpan = smoothedAbsMom.AsSpan(0, len); momSpan[0] = 0; absMomSpan[0] = 0; for (int i = 1; i < len; i++) { momSpan[i] = source[i] - source[i - 1]; absMomSpan[i] = Math.Abs(momSpan[i]); } // Double smooth momentum: EMA(EMA(mom, longPeriod), shortPeriod) Ema.Batch(momSpan, smoothedMomSpan, longPeriod); Ema.Batch(smoothedMomSpan, smoothedMomSpan, shortPeriod); // In-place // Double smooth absolute momentum: EMA(EMA(|mom|, longPeriod), shortPeriod) Ema.Batch(absMomSpan, smoothedAbsMomSpan, longPeriod); Ema.Batch(smoothedAbsMomSpan, smoothedAbsMomSpan, shortPeriod); // In-place // Calculate TSI: 100 × smoothedMom / smoothedAbsMom const double epsilon = 1e-10; for (int i = 0; i < len; i++) { if (Math.Abs(smoothedAbsMomSpan[i]) < epsilon) { output[i] = 0; } else { output[i] = 100.0 * smoothedMomSpan[i] / smoothedAbsMomSpan[i]; } } System.Buffers.ArrayPool.Shared.Return(mom); System.Buffers.ArrayPool.Shared.Return(absMom); System.Buffers.ArrayPool.Shared.Return(smoothedMom); System.Buffers.ArrayPool.Shared.Return(smoothedAbsMom); } public static (TSeries Results, Tsi Indicator) Calculate(TSeries source, int longPeriod = DefaultLongPeriod, int shortPeriod = DefaultShortPeriod, int signalPeriod = DefaultSignalPeriod) { var indicator = new Tsi(longPeriod, shortPeriod, signalPeriod); TSeries results = indicator.Update(source); return (results, indicator); } public override void Reset() { _emaMomLong.Reset(); _emaMomShort.Reset(); _emaAbsMomLong.Reset(); _emaAbsMomShort.Reset(); _emaSignal.Reset(); _prevValue = double.NaN; _p_prevValue = double.NaN; _lastSignal = 0; _p_lastSignal = 0; Last = default; } }