using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// ATRN: Average True Range Normalized /// /// /// ATR normalized to [0,1] via min-max scaling over 10Ă—period lookback. /// Enables cross-asset volatility comparison regardless of price scale. /// /// Calculation: ATRN = (ATR - minATR) / (maxATR - minATR). /// /// Detailed documentation [SkipLocalsInit] public sealed class Atrn : AbstractBase { private readonly int _lookbackWindow; private readonly Rma _rma; private readonly RingBuffer _atrBuffer; [StructLayout(LayoutKind.Auto)] private record struct State( TBar PrevBar, bool IsInitialized, double LastValidTr, double LastValidAtr); private State _state; private State _p_state; private ITValuePublisher? _publisher; private bool _disposed; /// /// Creates ATRN with specified period. /// /// Period for ATR calculation (must be > 0) public Atrn(int period) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } _lookbackWindow = 10 * period; _rma = new Rma(period); _atrBuffer = new RingBuffer(_lookbackWindow); Name = $"Atrn({period})"; WarmupPeriod = _rma.WarmupPeriod + _lookbackWindow; _state = new State(PrevBar: default, IsInitialized: false, LastValidTr: 0.0, LastValidAtr: 0.0); _p_state = _state; } /// /// Creates ATRN with specified source and period. /// /// Source to subscribe to /// Period for ATR calculation public Atrn(ITValuePublisher source, int period) : this(period) { _publisher = source; source.Pub += Handle; } /// /// Creates ATRN from a TBarSeries. /// /// Bar series source /// Period for ATR calculation public Atrn(TBarSeries source, int period) : this(period) { var result = Update(source); if (result.Count > 0) { Last = result.Last; } } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// True if the ATRN has warmed up and is providing valid results. /// public override bool IsHot => _rma.IsHot && _atrBuffer.Count >= _lookbackWindow; /// /// Initializes the indicator state using the provided history. /// Note: ATRN needs OHLCV data. This Prime method expects pre-calculated TR values. /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { for (int i = 0; i < source.Length; i++) { double tr = source[i]; TValue atr = _rma.Update(new TValue(DateTime.UtcNow.AddMinutes(i), tr), isNew: true); _atrBuffer.Add(atr.Value); } if (_atrBuffer.Count > 0) { double currentAtr = _atrBuffer[^1]; double maxAtr = GetMax(); double minAtr = GetMin(); double normalized = minAtr < maxAtr ? (currentAtr - minAtr) / (maxAtr - minAtr) : 0.5; Last = new TValue(DateTime.UtcNow, normalized); } } /// /// Resets the ATRN state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Reset() { _rma.Reset(); _atrBuffer.Clear(); _state = new State(PrevBar: default, IsInitialized: false, LastValidTr: 0.0, LastValidAtr: 0.0); _p_state = _state; Last = default; } /// /// Updates ATRN with a new bar. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { if (isNew) { _p_state = _state; _atrBuffer.Snapshot(); } else { _state = _p_state; _atrBuffer.Restore(); } // Calculate True Range FIRST (before RMA update for bar correction) double tr; if (!_state.IsInitialized) { // First bar: TR = High - Low tr = input.High - input.Low; } else { double hl = input.High - input.Low; double hpc = Math.Abs(input.High - _state.PrevBar.Close); double lpc = Math.Abs(input.Low - _state.PrevBar.Close); tr = Math.Max(hl, Math.Max(hpc, lpc)); } // Handle non-finite values if (!double.IsFinite(tr)) { tr = _state.LastValidTr; } // Calculate ATR using RMA (now uses freshly computed TR for both new and correction paths) TValue atrResult = _rma.Update(new TValue(input.Time, tr), isNew); double currentAtr = atrResult.Value; // Handle non-finite ATR if (!double.IsFinite(currentAtr)) { currentAtr = _state.LastValidAtr; } // Add to buffer for min-max calculation _atrBuffer.Add(currentAtr); // Calculate normalized value double maxAtr = GetMax(); double minAtr = GetMin(); double normalized = minAtr < maxAtr ? (currentAtr - minAtr) / (maxAtr - minAtr) : 0.5; // Update state _state = isNew ? new State(PrevBar: input, IsInitialized: true, LastValidTr: tr, LastValidAtr: currentAtr) : _state with { LastValidTr = tr, LastValidAtr = currentAtr }; TValue result = new(input.Time, normalized); Last = result; PubEvent(Last, isNew); return result; } /// /// Updates ATRN with a TValue input. /// This treats the input value as the TR itself. /// public override TValue Update(TValue input, bool isNew = true) { if (isNew) { _p_state = _state; _atrBuffer.Snapshot(); } else { _state = _p_state; _atrBuffer.Restore(); } double tr = input.Value; if (!double.IsFinite(tr)) { tr = _state.LastValidTr; } TValue atrResult = _rma.Update(new TValue(input.Time, tr), isNew); double currentAtr = atrResult.Value; if (!double.IsFinite(currentAtr)) { currentAtr = _state.LastValidAtr; } _atrBuffer.Add(currentAtr); double maxAtr = GetMax(); double minAtr = GetMin(); double normalized = minAtr < maxAtr ? (currentAtr - minAtr) / (maxAtr - minAtr) : 0.5; _state = _state with { LastValidTr = tr, LastValidAtr = currentAtr }; TValue result = new(input.Time, normalized); Last = result; PubEvent(Last, isNew); return result; } /// /// Updates ATRN from a TBarSeries. /// public TSeries Update(TBarSeries source) { if (source.Count == 0) { return []; } var t = new List(source.Count); var v = new List(source.Count); for (int i = 0; i < source.Count; i++) { TValue result = Update(source[i], isNew: true); t.Add(result.Time); v.Add(result.Value); } return new TSeries(t, v); } /// /// Updates ATRN from a TSeries (assumes values are already TR). /// public override TSeries Update(TSeries source) { if (source.Count == 0) { return []; } var t = new List(source.Count); var v = new List(source.Count); for (int i = 0; i < source.Count; i++) { TValue result = Update(source[i], isNew: true); t.Add(source[i].Time); v.Add(result.Value); } return new TSeries(t, v); } /// /// Calculates ATRN for the entire series using a new instance. /// public static TSeries Batch(TBarSeries source, int period) { var atrn = new Atrn(period); return atrn.Update(source); } public static (TSeries Results, Atrn Indicator) Calculate(TBarSeries source, int period) { var indicator = new Atrn(period); TSeries results = indicator.Update(source); return (results, indicator); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetMax() { ReadOnlySpan span = _atrBuffer.GetSpan(); if (span.IsEmpty) { return 0; } double max = double.MinValue; for (int i = 0; i < span.Length; i++) { if (span[i] > max) { max = span[i]; } } return max; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetMin() { ReadOnlySpan span = _atrBuffer.GetSpan(); if (span.IsEmpty) { return 0; } double min = double.MaxValue; for (int i = 0; i < span.Length; i++) { if (span[i] < min) { min = span[i]; } } return min; } /// protected override void Dispose(bool disposing) { if (!_disposed) { if (disposing && _publisher != null) { _publisher.Pub -= Handle; _publisher = null; } _disposed = true; } base.Dispose(disposing); } }