Files
QuanTAlib/lib/volatility/atrp/Atrp.cs
T
86fe32a682 SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
2026-01-18 19:02:03 -08:00

252 lines
7.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// ATRP: Average True Range Percent
/// </summary>
/// <remarks>
/// ATRP normalizes ATR as a percentage of the closing price, enabling volatility
/// comparison across different assets regardless of their price levels.
///
/// Calculation:
/// 1. True Range (TR) = Max(High - Low, |High - PrevClose|, |Low - PrevClose|)
/// - For the first bar, TR = High - Low
/// 2. ATR = RMA(TR, Period) with warmup compensation
/// 3. ATRP = (ATR / Close) × 100
///
/// Key characteristics:
/// - Normalized volatility allows cross-asset comparison
/// - Higher ATRP indicates higher relative volatility
/// - Typical values range from 0 to 10+ depending on asset class
///
/// Sources:
/// Derived from ATR by J. Welles Wilder, expressed as percentage.
/// </remarks>
[SkipLocalsInit]
public sealed class Atrp : AbstractBase
{
private readonly double _alpha;
private readonly double _decay;
private const double ConvergenceThreshold = 1e-10;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double RawRma,
double E,
double PrevClose,
double LastValidHigh,
double LastValidLow,
double LastValidClose,
bool IsInitialized);
private State _state;
private State _p_state;
/// <summary>
/// Creates ATRP with specified period.
/// </summary>
/// <param name="period">Period for ATR calculation (must be > 0)</param>
public Atrp(int period)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
_alpha = 1.0 / period;
_decay = 1.0 - _alpha;
Name = $"Atrp({period})";
// Warmup based on RMA convergence: ln(0.05) / ln(1 - alpha)
WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(_decay));
_state = new State(0, 1.0, double.NaN, double.NaN, double.NaN, double.NaN, false);
_p_state = _state;
}
/// <summary>
/// Creates ATRP with specified source and period.
/// </summary>
/// <param name="source">Source to subscribe to</param>
/// <param name="period">Period for ATRP calculation</param>
public Atrp(ITValuePublisher source, int period) : this(period)
{
source.Pub += Handle;
}
/// <summary>
/// Creates ATRP from a TBarSeries.
/// </summary>
/// <param name="source">Bar series source</param>
/// <param name="period">Period for ATRP calculation</param>
public Atrp(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);
/// <summary>
/// True if the ATRP has warmed up and is providing valid results.
/// </summary>
public override bool IsHot => _state.E <= 0.05;
/// <summary>
/// Initializes the indicator state using the provided history.
/// Note: ATRP needs OHLCV data. This Prime method expects pre-calculated TR values.
/// </summary>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
for (int i = 0; i < source.Length; i++)
{
double tr = source[i];
_state.RawRma = Math.FusedMultiplyAdd(_state.RawRma, _decay, _alpha * tr);
_state.E *= _decay;
}
if (source.Length > 0)
{
double atr = _state.E > ConvergenceThreshold ? _state.RawRma / (1.0 - _state.E) : _state.RawRma;
// Without close price, we can't calculate ATRP percentage
Last = new TValue(DateTime.UtcNow, atr);
}
_p_state = _state;
}
/// <summary>
/// Resets the ATRP state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Reset()
{
_state = new State(0, 1.0, double.NaN, double.NaN, double.NaN, double.NaN, false);
_p_state = _state;
Last = default;
}
/// <summary>
/// Updates ATRP with a new bar.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
_p_state = _state;
else
_state = _p_state;
// Get valid values with last-value substitution
double high = input.High;
double low = input.Low;
double close = input.Close;
if (double.IsFinite(high)) _state.LastValidHigh = high; else high = _state.LastValidHigh;
if (double.IsFinite(low)) _state.LastValidLow = low; else low = _state.LastValidLow;
if (double.IsFinite(close)) _state.LastValidClose = close; else close = _state.LastValidClose;
// Handle case where no valid values yet
if (double.IsNaN(close))
{
Last = new TValue(input.Time, double.NaN);
PubEvent(Last, isNew);
return Last;
}
// Calculate True Range
double tr;
if (!_state.IsInitialized || double.IsNaN(_state.PrevClose))
{
// First bar: TR = High - Low
tr = high - low;
}
else
{
double hl = high - low;
double hpc = Math.Abs(high - _state.PrevClose);
double lpc = Math.Abs(low - _state.PrevClose);
tr = Math.Max(hl, Math.Max(hpc, lpc));
}
// Calculate ATR using RMA with warmup compensation
_state.RawRma = Math.FusedMultiplyAdd(_state.RawRma, _decay, _alpha * tr);
_state.E *= _decay;
double atr = _state.E > ConvergenceThreshold ? _state.RawRma / (1.0 - _state.E) : _state.RawRma;
// Calculate ATRP: (ATR / Close) * 100
double atrp = close != 0.0 ? (atr / close) * 100.0 : double.NaN;
// Update state
if (isNew)
{
_state.PrevClose = close;
_state.IsInitialized = true;
}
TValue result = new(input.Time, atrp);
Last = result;
PubEvent(Last, isNew);
return result;
}
/// <summary>
/// Updates ATRP with a TValue input.
/// </summary>
/// <exception cref="NotSupportedException">
/// ATRP requires OHLC bar data to calculate the percentage (ATR/Close * 100).
/// Use Update(TBar) instead.
/// </exception>
public override TValue Update(TValue input, bool isNew = true)
{
throw new NotSupportedException(
"ATRP requires OHLC bar data to calculate the percentage (ATR/Close * 100). " +
"Use Update(TBar) instead.");
}
/// <summary>
/// Updates ATRP from a TBarSeries.
/// </summary>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return [];
var t = new List<long>(source.Count);
var v = new List<double>(source.Count);
for (int i = 0; i < source.Count; i++)
{
TValue result = Update(source[i], true);
t.Add(result.Time);
v.Add(result.Value);
}
return new TSeries(t, v);
}
/// <summary>
/// Updates ATRP from a TSeries.
/// </summary>
/// <exception cref="NotSupportedException">
/// ATRP requires OHLC bar data to calculate the percentage (ATR/Close * 100).
/// Use Update(TBarSeries) instead.
/// </exception>
public override TSeries Update(TSeries source)
{
throw new NotSupportedException(
"ATRP requires OHLC bar data to calculate the percentage (ATR/Close * 100). " +
"Use Update(TBarSeries) instead.");
}
/// <summary>
/// Calculates ATRP for the entire series using a new instance.
/// </summary>
public static TSeries Batch(TBarSeries source, int period)
{
var atrp = new Atrp(period);
return atrp.Update(source);
}
}