using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// ATR: Average True Range /// /// /// Wilder's volatility measure using RMA-smoothed True Range. /// Accounts for gaps via max of H-L, |H-PrevClose|, |L-PrevClose|. /// /// Calculation: ATR = RMA(TR, period). /// /// Detailed documentation [SkipLocalsInit] public sealed class Atr : AbstractBase { private readonly Rma _rma; private readonly TValuePublishedHandler _handler; private TBar _prevBar; private TBar _p_prevBar; private bool _isInitialized; private bool _p_isInitialized; /// /// Creates ATR with specified period. /// /// Period for ATR calculation (must be > 0) public Atr(int period) { if (period <= 0) { throw new ArgumentException("Period must be greater than 0", nameof(period)); } _rma = new Rma(period); Name = $"Atr({period})"; WarmupPeriod = _rma.WarmupPeriod; _isInitialized = false; _handler = Handle; } /// /// Creates ATR with specified source and period. /// /// Source to subscribe to /// Period for ATR calculation public Atr(ITValuePublisher source, int period) : this(period) { source.Pub += _handler; } /// /// Creates ATR with specified source and period. /// public Atr(TBarSeries source, int period) : this(period) { var tr = CalculateTrueRange(source); _rma.Prime(tr.Values); Last = _rma.Last; // Set internal state for subsequent Update(TBar) calls if (source.Count > 0) { _prevBar = source.Last; _isInitialized = true; } // We can't automatically subscribe to TBarSeries updates via this constructor // because AbstractBase doesn't enforce TBarSeries subscription structure, // but we can rely on manual updates or the user subscribing. } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// True if the ATR has warmed up and is providing valid results. /// public override bool IsHot => _rma.IsHot; /// /// Initializes the indicator state using the provided history. /// Note: ATR needs OHLCV data to calculate TR properly. /// This Prime method expects pre-calculated TR values or handles basic priming /// if the user erroneously passes non-TR data. Ideally, use Batched TBarSeries. /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { _rma.Prime(source); Last = _rma.Last; } /// /// Resets the ATR state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Reset() { _rma.Reset(); _prevBar = default; _p_prevBar = default; _isInitialized = false; _p_isInitialized = false; Last = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { // Snapshot/restore for bar correction if (isNew) { _p_prevBar = _prevBar; _p_isInitialized = _isInitialized; } else { _prevBar = _p_prevBar; _isInitialized = _p_isInitialized; } double tr; if (!_isInitialized) { // For the very first bar, Wilder defines TR as High - Low tr = input.High - input.Low; } else { // Calculate TR double hl = input.High - input.Low; double hpc = Math.Abs(input.High - _prevBar.Close); double lpc = Math.Abs(input.Low - _prevBar.Close); tr = Math.Max(hl, Math.Max(hpc, lpc)); } if (isNew) { _prevBar = input; _isInitialized = true; } // Smooth TR using RMA TValue result = _rma.Update(new TValue(input.Time, tr), isNew); Last = result; PubEvent(Last, isNew); return result; } /// /// Update for TValue input (not recommended for ATR as it needs OHLC). /// This treats the input value as the TR itself. /// public override TValue Update(TValue input, bool isNew = true) { // If user passes a single value, we assume it IS the True Range TValue result = _rma.Update(input, isNew); Last = result; PubEvent(Last, isNew); return result; } public TSeries Update(TBarSeries source) { if (source.Count == 0) { return []; } // 1. Calculate TR series TSeries trSeries = CalculateTrueRange(source); // 2. Run RMA on TR var result = _rma.Update(trSeries); Last = _rma.Last; // 3. Synchronize state for subsequent updates _prevBar = source.Last; _isInitialized = true; return result; } // AbstractBase.Update(TSeries) public override TSeries Update(TSeries source) { // Assumes source is already TR if (source.Count == 0) { return _rma.Update(source); } var result = _rma.Update(source); // Update instance state to match RMA state Last = _rma.Last; _isInitialized = true; // Note: _prevBar cannot be updated from TSeries (no OHLC data) // but _isInitialized signals that subsequent TBar updates should work return result; } private static TSeries CalculateTrueRange(TBarSeries source) { var t = new List(source.Count); var v = new List(source.Count); if (source.Count == 0) { return new TSeries(t, v); } // First bar TR = H - L t.Add(source[0].Time); v.Add(source[0].High - source[0].Low); for (int i = 1; i < source.Count; i++) { var bar = source[i]; var prevBar = source[i - 1]; double hl = bar.High - bar.Low; double hpc = Math.Abs(bar.High - prevBar.Close); double lpc = Math.Abs(bar.Low - prevBar.Close); double tr = Math.Max(hl, Math.Max(hpc, lpc)); t.Add(bar.Time); v.Add(tr); } return new TSeries(t, v); } /// /// Calculates ATR for the entire series using a new instance. /// public static TSeries Batch(TBarSeries source, int period) { var atr = new Atr(period); return atr.Update(source); } public static (TSeries Results, Atr Indicator) Calculate(TBarSeries source, int period) { var indicator = new Atr(period); TSeries results = indicator.Update(source); return (results, indicator); } }