// True Range (TR) Indicator // Measures the maximum price movement including gaps from the previous close using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// TR: True Range /// A volatility measure that captures the maximum price movement including gaps. /// True Range accounts for overnight gaps by comparing current High-Low range /// against the previous close. /// /// /// Calculation steps: /// /// Calculate three ranges: (High - Low), |High - prevClose|, |Low - prevClose| /// True Range = max(all three ranges) /// /// /// Key characteristics: /// /// Bar-by-bar calculation (no smoothing) /// Always positive (absolute values used for gap calculations) /// First bar uses High - Low (no previous close available) /// Foundation for ATR (Average True Range) /// /// /// Sources: /// J. Welles Wilder Jr. (1978). "New Concepts in Technical Trading Systems" /// [SkipLocalsInit] public sealed class Tr : AbstractBase { [StructLayout(LayoutKind.Auto)] private record struct State( double PrevClose, double LastValidHigh, double LastValidLow, double LastValidClose, double LastTr, int Count ); private State _s; private State _ps; /// /// Initializes a new instance of the Tr class. /// public Tr() { WarmupPeriod = 1; Name = "Tr"; _s = new State(double.NaN, 0, 0, 0, 0, 0); _ps = _s; } /// /// Initializes a new instance of the Tr class with a source. /// /// The data source for chaining. public Tr(ITValuePublisher source) : this() { source.Pub += Handle; } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// True if the indicator has enough data for valid results. /// public override bool IsHot => _s.Count >= WarmupPeriod; /// /// Computes the True Range for given bar values. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double ComputeTrueRange(double high, double low, double prevClose) { double tr1 = high - low; double tr2 = Math.Abs(high - prevClose); double tr3 = Math.Abs(low - prevClose); return Math.Max(tr1, Math.Max(tr2, tr3)); } /// /// Updates the indicator with a TValue input. /// For TR, this treats the value as a close price (uses value for H, L, and C). /// Prefer Update(TBar) for standard OHLC data. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { // For TValue input, treat it as if H=L=C (no range) return UpdateCore(input.Time, input.Value, input.Value, input.Value, isNew); } /// /// Updates the indicator with a new bar (preferred method). /// /// The input bar. /// Whether this is a new bar or an update. /// The calculated True Range value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true) { return UpdateCore(bar.Time, bar.High, bar.Low, bar.Close, isNew); } /// /// Updates the indicator with a bar series. /// /// The source bar series. /// A TSeries containing the True Range values. public TSeries Update(TBarSeries 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); // Extract OHLC data Span highs = len <= 128 ? stackalloc double[len] : new double[len]; Span lows = len <= 128 ? stackalloc double[len] : new double[len]; Span closes = len <= 128 ? stackalloc double[len] : new double[len]; for (int i = 0; i < len; i++) { highs[i] = source[i].High; lows[i] = source[i].Low; closes[i] = source[i].Close; tSpan[i] = source[i].Time; } Batch(highs, lows, closes, vSpan); // Update internal state for (int i = 0; i < len; i++) { Update(source[i], isNew: true); } return new TSeries(t, v); } public override TSeries Update(TSeries source) { 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); var values = source.Values; // When using TSeries (close prices only), TR = |close[i] - close[i-1]| (gap-based) // This is a degenerate case - prefer TBarSeries for (int i = 0; i < len; i++) { tSpan[i] = source.Times[i]; vSpan[i] = (i == 0) ? 0 : Math.Abs(values[i] - values[i - 1]); } // Update internal state for (int i = 0; i < len; i++) { Update(new TValue(source.Times[i], values[i]), isNew: true); } return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private TValue UpdateCore(long timeTicks, double high, double low, double close, bool isNew) { if (isNew) { _ps = _s; } else { _s = _ps; } var s = _s; // Handle non-finite values - use last valid values if (!double.IsFinite(high)) { high = s.LastValidHigh; } else { s.LastValidHigh = high; } if (!double.IsFinite(low)) { low = s.LastValidLow; } else { s.LastValidLow = low; } if (!double.IsFinite(close)) { close = s.LastValidClose; } else { s.LastValidClose = close; } double tr; if (s.Count == 0 || !double.IsFinite(s.PrevClose)) { // First bar or no previous close: use High - Low tr = high - low; } else { tr = ComputeTrueRange(high, low, s.PrevClose); } if (!double.IsFinite(tr) || tr < 0) { tr = s.LastTr; } else { s.LastTr = tr; } // Update state s.PrevClose = close; if (isNew) { s.Count++; } _s = s; Last = new TValue(timeTicks, tr); PubEvent(Last, isNew); return Last; } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { for (int i = 0; i < source.Length; i++) { Update(new TValue(DateTime.UtcNow, source[i]), isNew: true); } } public override void Reset() { _s = new State(double.NaN, 0, 0, 0, 0, 0); _ps = _s; Last = default; } /// /// Calculates True Range for a bar series (static). /// /// The source bar series. /// A TSeries containing the True Range values. public static TSeries Batch(TBarSeries source) { var tr = new Tr(); return tr.Update(source); } /// /// Batch calculation using spans for OHLC data. /// /// High prices. /// Low prices. /// Close prices. /// Output True Range values. public static void Batch( ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, Span output) { int len = high.Length; if (low.Length != len || close.Length != len) { throw new ArgumentException("All input spans must have the same length", nameof(low)); } if (output.Length < len) { throw new ArgumentException("Output span must be at least as long as input spans", nameof(output)); } if (len == 0) { return; } double lastValidHigh = 0; double lastValidLow = 0; double lastValidClose = 0; double lastTr = 0; for (int i = 0; i < len; i++) { double h = high[i]; double l = low[i]; double c = close[i]; // Handle non-finite values if (!double.IsFinite(h)) { h = lastValidHigh; } else { lastValidHigh = h; } if (!double.IsFinite(l)) { l = lastValidLow; } else { lastValidLow = l; } double tr; if (i == 0) { // First bar: use High - Low tr = h - l; } else { double prevClose = close[i - 1]; if (!double.IsFinite(prevClose)) { // Fall back to last valid close from previous bars prevClose = lastValidClose; } tr = ComputeTrueRange(h, l, prevClose); } // Update lastValidClose AFTER computing TR so fallback uses previous bar's close if (double.IsFinite(c)) { lastValidClose = c; } if (!double.IsFinite(tr) || tr < 0) { tr = lastTr; } else { lastTr = tr; } output[i] = tr; } } /// /// Batch calculation using a TBarSeries (convenience overload). /// /// The source bar series. /// Output True Range values. public static void Batch(TBarSeries source, Span output) { int len = source.Count; if (output.Length < len) { throw new ArgumentException("Output span must be at least as long as source", nameof(output)); } if (len == 0) { return; } Span highs = len <= 128 ? stackalloc double[len] : new double[len]; Span lows = len <= 128 ? stackalloc double[len] : new double[len]; Span closes = len <= 128 ? stackalloc double[len] : new double[len]; for (int i = 0; i < len; i++) { highs[i] = source[i].High; lows[i] = source[i].Low; closes[i] = source[i].Close; } Batch(highs, lows, closes, output); } public static (TSeries Results, Tr Indicator) Calculate(TBarSeries source) { var indicator = new Tr(); TSeries results = indicator.Update(source); return (results, indicator); } }