mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 13:08:04 +00:00
filters update
This commit is contained in:
@@ -9,13 +9,13 @@ Volatility measures the magnitude of price changes, independent of direction. Lo
|
||||
| [ADR](adr/Adr.md) | Average Daily Range | Simple High-Low range without gap adjustment. |
|
||||
| [ATR](atr/Atr.md) | Average True Range | Standard volatility measure accounting for gaps via True Range. |
|
||||
| [ATRN](atrn/Atrn.md) | ATR Normalized | ATR normalized to [0,1] based on historical min/max. |
|
||||
| [ATRP](atrp/Atrp.md) | ATR Percent | ATR as percentage of close price. |
|
||||
| [BBW](bbw/Bbw.md) | Bollinger Band Width | Distance between upper and lower Bollinger Bands. |
|
||||
| [BBWN](bbwn/Bbwn.md) | BB Width Normalized | BBW normalized to [0,1] range. |
|
||||
| [BBWP](bbwp/Bbwp.md) | BB Width Percentile | BBW percentile rank over lookback. |
|
||||
| [CCV](ccv/Ccv.md) | Close-to-Close Volatility | Annualized volatility from log returns. |
|
||||
| [CV](cv/Cv.md) | Conditional Volatility | GARCH(1,1) model for time-varying volatility. |
|
||||
| [CVI](cvi/Cvi.md) | Chaikin Volatility | Rate of change in smoothed High-Low range. |
|
||||
| ETHERM | Elder's Thermometer | Absolute bar range in ATR units. Identifies abnormal activity. |
|
||||
| [EWMA](ewma/Ewma.md) | EWMA Volatility | Exponentially weighted squared returns with bias correction. |
|
||||
| [GKV](gkv/Gkv.md) | Garman-Klass Volatility | Efficient OHLC-based estimator with RMA smoothing. |
|
||||
| [HLV](hlv/Hlv.md) | High-Low Volatility (Parkinson) | Range-based volatility using only high-low prices. |
|
||||
@@ -23,7 +23,7 @@ Volatility measures the magnitude of price changes, independent of direction. Lo
|
||||
| [JVOLTY](jvolty/Jvolty.md) | Jurik Volatility | Adaptive volatility from JMA with 128-bar trimmed mean distribution. |
|
||||
| [JVOLTYN](jvoltyn/Jvoltyn.md) | Jurik Volatility Normalized | JVOLTY normalized to [0,100] scale. |
|
||||
| [MASSI](massi/Massi.md) | Mass Index | Range expansion/contraction for reversal detection. |
|
||||
| [NATR](natr/Natr.md) | Normalized ATR | ATR as percentage (equivalent to ATRP). |
|
||||
| [NATR](natr/Natr.md) | Normalized ATR | ATR as percentage of close price. Also known as ATRP. |
|
||||
| [RSV](rsv/Rsv.md) | Rogers-Satchell Volatility | OHLC estimator with drift adjustment. |
|
||||
| [RV](rv/Rv.md) | Realized Volatility | High-frequency intraday volatility. |
|
||||
| [RVI](rvi/Rvi.md) | Relative Volatility Index | Directional volatility measure. |
|
||||
|
||||
@@ -155,7 +155,7 @@ Validated against external libraries in `Atr.Validation.Tests.cs`. Tests run aga
|
||||
|
||||
1. **Directionality Assumption**: ATR is non-directional. A crashing market has high ATR. A rallying market has high ATR. Do not use ATR to predict direction. Use it to measure potential magnitude of moves.
|
||||
|
||||
2. **Scale Dependence**: ATR is absolute, not percentage-based. An ATR of 5.0 on a $100 stock (5% daily range) differs from ATR of 5.0 on a $10 stock (50% daily range). Use ATRP (ATR Percent) or NATR for cross-asset comparisons.
|
||||
2. **Scale Dependence**: ATR is absolute, not percentage-based. An ATR of 5.0 on a $100 stock (5% daily range) differs from ATR of 5.0 on a $10 stock (50% daily range). Use NATR (Normalized ATR, also known as ATRP) for cross-asset comparisons.
|
||||
|
||||
3. **Lag Characteristics**: Because RMA decays slowly, ATR lags actual volatility changes. It tells what *has* happened, not what *will* happen. A volatility spike appears immediately; the subsequent decay takes many bars.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ While ATR tells you *how much* an asset moves, ATRN tells you *how unusual* that
|
||||
|
||||
## Historical Context
|
||||
|
||||
ATRN is a practical extension of Wilder's ATR, developed to solve the **context problem** in volatility analysis. Raw ATR values are meaningless in isolation—you need to compare them to something. Some traders compare ATR to price (ATRP/NATR), which gives a percentage. ATRN takes a different approach: it compares ATR to its own recent range.
|
||||
ATRN is a practical extension of Wilder's ATR, developed to solve the **context problem** in volatility analysis. Raw ATR values are meaningless in isolation—you need to compare them to something. Some traders compare ATR to price (NATR), which gives a percentage. ATRN takes a different approach: it compares ATR to its own recent range.
|
||||
|
||||
This normalization approach is common in machine learning and signal processing, where inputs are scaled to [0,1] for better model performance. ATRN applies the same principle to volatility measurement.
|
||||
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AtrpIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void AtrpIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new AtrpIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("ATRP - Average True Range Percent", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new AtrpIndicator { Period = 20 };
|
||||
Assert.Equal("ATRP 20", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new AtrpIndicator();
|
||||
|
||||
Assert.Equal(0, AtrpIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_Initialize_CreatesInternalAtrp()
|
||||
{
|
||||
var indicator = new AtrpIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AtrpIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with volatility
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val > 0); // ATRP should be positive with volatility
|
||||
Assert.True(val < 100); // ATRP as percentage should be reasonable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new AtrpIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 128, 115, 125, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 5, 10, 14, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new AtrpIndicator { Period = period };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Period {period} should produce finite value");
|
||||
Assert.True(val > 0, $"Period {period} should produce positive ATRP");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new AtrpIndicator();
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
|
||||
indicator.Period = 5;
|
||||
Assert.Equal(5, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new AtrpIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new AtrpIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Atrp.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AtrpIndicator_Description_IsSet()
|
||||
{
|
||||
var indicator = new AtrpIndicator();
|
||||
Assert.Contains("percentage", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class AtrpIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Atrp _atrp = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"ATRP {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volatility/atrp/Atrp.Quantower.cs";
|
||||
|
||||
public AtrpIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "ATRP - Average True Range Percent";
|
||||
Description = "Measures volatility as a percentage of the closing price";
|
||||
|
||||
_series = new LineSeries(name: "ATRP", color: Color.Blue, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_atrp = new Atrp(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _atrp.Update(bar, args.IsNewBar());
|
||||
|
||||
_series.SetValue(result.Value, _atrp.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class AtrpTests
|
||||
{
|
||||
// ============== Constructor & Parameter Validation ==============
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Atrp(0));
|
||||
Assert.Throws<ArgumentException>(() => new Atrp(-1));
|
||||
|
||||
var atrp = new Atrp(14);
|
||||
Assert.NotNull(atrp);
|
||||
}
|
||||
|
||||
// ============== Basic Functionality ==============
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
atrp.Update(bar);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(atrp.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
|
||||
Assert.Equal(0, atrp.Last.Value);
|
||||
|
||||
TValue result = atrp.Update(bar);
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, atrp.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FirstValue_ReturnsPercentage()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000);
|
||||
// First bar TR = High - Low = 110 - 90 = 20
|
||||
// ATRP = (20 / 100) * 100 = 20%
|
||||
|
||||
TValue result = atrp.Update(bar);
|
||||
|
||||
Assert.Equal(20.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
|
||||
Assert.Equal(0, atrp.Last.Value);
|
||||
Assert.False(atrp.IsHot);
|
||||
Assert.Contains("Atrp", atrp.Name, StringComparison.Ordinal);
|
||||
Assert.True(atrp.WarmupPeriod > 0);
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
atrp.Update(bar);
|
||||
|
||||
Assert.NotEqual(0, atrp.Last.Value);
|
||||
}
|
||||
|
||||
// ============== State Management & Bar Correction ==============
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
atrp.Update(bar1, isNew: true);
|
||||
double value1 = atrp.Last.Value;
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 100, 108, 1000);
|
||||
atrp.Update(bar2, isNew: true);
|
||||
double value2 = atrp.Last.Value;
|
||||
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
atrp.Update(bar1, isNew: true);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 100, 108, 1000);
|
||||
atrp.Update(bar2, isNew: true);
|
||||
double beforeUpdate = atrp.Last.Value;
|
||||
|
||||
var bar2Modified = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 120, 90, 108, 1000);
|
||||
atrp.Update(bar2Modified, isNew: false);
|
||||
double afterUpdate = atrp.Last.Value;
|
||||
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed first 99
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
atrp.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Update with 100th point (isNew=true)
|
||||
atrp.Update(bars[99], true);
|
||||
|
||||
// Update with modified 100th point (isNew=false)
|
||||
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 10.0, bars[99].Low - 10.0, bars[99].Close, bars[99].Volume);
|
||||
double val2 = atrp.Update(modifiedBar, false).Value;
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var atrp2 = new Atrp(14);
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
atrp2.Update(bars[i]);
|
||||
}
|
||||
double val3 = atrp2.Update(modifiedBar, true).Value;
|
||||
|
||||
Assert.Equal(val3, val2, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var atrp = new Atrp(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Feed 10 new values
|
||||
TBar tenthBar = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
tenthBar = bars[i];
|
||||
atrp.Update(tenthBar, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = atrp.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 10; i < 19; i++)
|
||||
{
|
||||
atrp.Update(bars[i], isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th bar again with isNew=false
|
||||
TValue finalResult = atrp.Update(tenthBar, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_Works()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
atrp.Update(bar);
|
||||
}
|
||||
|
||||
double lastVal = atrp.Last.Value;
|
||||
Assert.NotEqual(0, lastVal);
|
||||
|
||||
atrp.Reset();
|
||||
Assert.Equal(0, atrp.Last.Value);
|
||||
Assert.False(atrp.IsHot);
|
||||
|
||||
// After reset, should accept new values
|
||||
atrp.Update(bars[0]);
|
||||
Assert.NotEqual(0, atrp.Last.Value);
|
||||
}
|
||||
|
||||
// ============== Warmup & Convergence ==============
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var atrp = new Atrp(5);
|
||||
|
||||
Assert.False(atrp.IsHot);
|
||||
|
||||
int steps = 0;
|
||||
var baseTime = DateTime.UtcNow;
|
||||
while (!atrp.IsHot && steps < 100)
|
||||
{
|
||||
var bar = new TBar(baseTime.AddMinutes(steps), 100, 110, 90, 100, 1000);
|
||||
atrp.Update(bar);
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(atrp.IsHot);
|
||||
Assert.True(steps > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsPositive()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
Assert.True(atrp.WarmupPeriod > 0);
|
||||
|
||||
var atrp2 = new Atrp(20);
|
||||
Assert.True(atrp2.WarmupPeriod > 0);
|
||||
|
||||
// WarmupPeriod should increase with the period parameter
|
||||
Assert.True(atrp2.WarmupPeriod >= atrp.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ============== NaN/Infinity Handling ==============
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var atrp = new Atrp(5);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
atrp.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 98, 108, 1000);
|
||||
atrp.Update(bar2);
|
||||
|
||||
// Feed bar with NaN values
|
||||
var barWithNaN = new TBar(DateTime.UtcNow.AddMinutes(2), double.NaN, 115, 100, 112, 1000);
|
||||
var resultAfterNaN = atrp.Update(barWithNaN);
|
||||
|
||||
// Result should be finite
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var atrp = new Atrp(5);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
|
||||
atrp.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 98, 108, 1000);
|
||||
atrp.Update(bar2);
|
||||
|
||||
// Feed bar with Infinity
|
||||
var barWithInf = new TBar(DateTime.UtcNow.AddMinutes(2), 108, double.PositiveInfinity, 100, 112, 1000);
|
||||
var resultAfterInf = atrp.Update(barWithInf);
|
||||
|
||||
Assert.True(double.IsFinite(resultAfterInf.Value) || double.IsPositiveInfinity(resultAfterInf.Value));
|
||||
}
|
||||
|
||||
// ============== Consistency Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var atrpIterative = new Atrp(14);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
iterativeResults.Add(atrpIterative.Update(bar));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = Atrp.Batch(bars, 14);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TBarSeries_Update_MatchesStreaming()
|
||||
{
|
||||
var atrp1 = new Atrp(14);
|
||||
var atrp2 = new Atrp(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
atrp1.Update(bar);
|
||||
}
|
||||
|
||||
// Batch
|
||||
atrp2.Update(bars);
|
||||
|
||||
Assert.Equal(atrp1.Last.Value, atrp2.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chainability_Works()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var result = atrp.Update(bars);
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.Equal(atrp.Last.Value, result.Last.Value);
|
||||
}
|
||||
|
||||
// ============== ATRP-Specific Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void ATRP_IsPercentageOfPrice()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000);
|
||||
// TR = 20, Close = 100
|
||||
// ATRP = (20 / 100) * 100 = 20%
|
||||
|
||||
var result = atrp.Update(bar);
|
||||
Assert.Equal(20.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ATRP_HigherPriceAsset_LowerPercentage()
|
||||
{
|
||||
// Same volatility (TR=20) but different price levels
|
||||
var atrp1 = new Atrp(14);
|
||||
var atrp2 = new Atrp(14);
|
||||
|
||||
// Low price asset: Close = 100, TR = 20 -> ATRP = 20%
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000);
|
||||
var result1 = atrp1.Update(bar1);
|
||||
|
||||
// High price asset: Close = 1000, TR = 20 -> ATRP = 2%
|
||||
var bar2 = new TBar(DateTime.UtcNow, 1000, 1010, 990, 1000, 1000);
|
||||
var result2 = atrp2.Update(bar2);
|
||||
|
||||
Assert.True(result1.Value > result2.Value);
|
||||
Assert.Equal(20.0, result1.Value, 1e-10);
|
||||
Assert.Equal(2.0, result2.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ATRP_ProportionalVolatility_SamePercentage()
|
||||
{
|
||||
var atrp1 = new Atrp(14);
|
||||
var atrp2 = new Atrp(14);
|
||||
|
||||
// Asset 1: Close = 100, TR = 10 (10% volatility)
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000);
|
||||
var result1 = atrp1.Update(bar1);
|
||||
|
||||
// Asset 2: Close = 1000, TR = 100 (10% volatility)
|
||||
var bar2 = new TBar(DateTime.UtcNow, 1000, 1050, 950, 1000, 1000);
|
||||
var result2 = atrp2.Update(bar2);
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value, 1e-10);
|
||||
Assert.Equal(10.0, result1.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ============== Static Batch Method ==============
|
||||
|
||||
[Fact]
|
||||
public void StaticBatch_Works()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var results = Atrp.Batch(bars, 14);
|
||||
|
||||
Assert.Equal(50, results.Count);
|
||||
Assert.True(double.IsFinite(results.Last.Value));
|
||||
}
|
||||
|
||||
// ============== Edge Cases ==============
|
||||
|
||||
[Fact]
|
||||
public void SingleBar_ReturnsValidResult()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 100, 1000);
|
||||
|
||||
var result = atrp.Update(bar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(20.0, result.Value, 1e-10); // (H-L)/Close * 100 = 20/100 * 100 = 20%
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Period1_Works()
|
||||
{
|
||||
var atrp = new Atrp(1);
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
var result = atrp.Update(bar);
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
Assert.True(atrp.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlatBars_ZeroVolatility()
|
||||
{
|
||||
var atrp = new Atrp(5);
|
||||
|
||||
// All bars have same OHLC values
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
atrp.Update(bar);
|
||||
}
|
||||
|
||||
// ATRP should be 0 for flat bars
|
||||
Assert.Equal(0.0, atrp.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroClose_ReturnsNaN()
|
||||
{
|
||||
var atrp = new Atrp(14);
|
||||
var bar = new TBar(DateTime.UtcNow, 0, 10, -10, 0, 1000);
|
||||
|
||||
var result = atrp.Update(bar);
|
||||
|
||||
Assert.True(double.IsNaN(result.Value));
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Enums;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// ATRP validation tests.
|
||||
/// ATRP = (ATR / Close) × 100
|
||||
/// Since external libraries don't have direct ATRP, we validate by computing ATR
|
||||
/// from external libraries and converting to ATRP using the same formula.
|
||||
/// </summary>
|
||||
public sealed class AtrpValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public AtrpValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Batch()
|
||||
{
|
||||
int[] periods = { 14 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ATRP (batch TSeries)
|
||||
var atrp = new Atrp(period);
|
||||
var qResult = atrp.Update(_testData.Bars);
|
||||
|
||||
// Calculate Skender ATR and convert to ATRP
|
||||
var sAtr = _testData.SkenderQuotes.GetAtr(period).ToList();
|
||||
var closeValues = _testData.SkenderQuotes.ToList();
|
||||
|
||||
// Build expected ATRP values: (ATR / Close) * 100
|
||||
var expectedAtrp = new List<double>();
|
||||
for (int i = 0; i < sAtr.Count; i++)
|
||||
{
|
||||
double? atr = sAtr[i].Atr;
|
||||
double close = (double)closeValues[i].Close;
|
||||
if (atr.HasValue && close > 0)
|
||||
{
|
||||
expectedAtrp.Add((atr.Value / close) * 100.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
expectedAtrp.Add(double.NaN);
|
||||
}
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, expectedAtrp, (s) => s, 100, ValidationHelper.SkenderTolerance);
|
||||
}
|
||||
_output.WriteLine("ATRP Batch(TSeries) validated successfully against Skender ATR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender_Streaming()
|
||||
{
|
||||
int[] periods = { 14 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ATRP (streaming)
|
||||
var atrp = new Atrp(period);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Bars)
|
||||
{
|
||||
qResults.Add(atrp.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate Skender ATR and convert to ATRP
|
||||
var sAtr = _testData.SkenderQuotes.GetAtr(period).ToList();
|
||||
var closeValues = _testData.SkenderQuotes.ToList();
|
||||
|
||||
// Build expected ATRP values
|
||||
var expectedAtrp = new List<double>();
|
||||
for (int i = 0; i < sAtr.Count; i++)
|
||||
{
|
||||
double? atr = sAtr[i].Atr;
|
||||
double close = (double)closeValues[i].Close;
|
||||
if (atr.HasValue && close > 0)
|
||||
{
|
||||
expectedAtrp.Add((atr.Value / close) * 100.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
expectedAtrp.Add(double.NaN);
|
||||
}
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResults, expectedAtrp, (s) => s, 100, ValidationHelper.SkenderTolerance);
|
||||
}
|
||||
_output.WriteLine("ATRP Streaming validated successfully against Skender ATR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Batch()
|
||||
{
|
||||
int[] periods = { 14 };
|
||||
|
||||
// Note: QuanTAlib ATRP uses warmup-compensated RMA which gives slightly different
|
||||
// results than TA-Lib's classic Wilder's approach. The difference (~4-7%) accumulates
|
||||
// over 5000 bars but both implementations are mathematically valid.
|
||||
// Using absolute tolerance of 0.10 to account for accumulated drift divergence
|
||||
// QuanTAlib warmup-compensated RMA diverges from TA-Lib classic Wilder over time
|
||||
const double AtrpTolerance = 0.10;
|
||||
|
||||
// Prepare data for TA-Lib (double[])
|
||||
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
|
||||
double[] atrOutput = new double[hData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ATRP (batch TSeries)
|
||||
var atrp = new Atrp(period);
|
||||
var qResult = atrp.Update(_testData.Bars);
|
||||
|
||||
// Calculate TA-Lib ATR
|
||||
var retCode = TALib.Functions.Atr(hData, lData, cData, 0..^0, atrOutput, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.AtrLookback(period);
|
||||
|
||||
// Convert ATR to ATRP: (ATR / Close) * 100
|
||||
var expectedAtrp = new double[atrOutput.Length];
|
||||
for (int i = outRange.Start.Value; i < outRange.End.Value; i++)
|
||||
{
|
||||
double atr = atrOutput[i];
|
||||
double close = cData[i];
|
||||
expectedAtrp[i] = close > 0 ? (atr / close) * 100.0 : double.NaN;
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, expectedAtrp, outRange, lookback, tolerance: AtrpTolerance);
|
||||
}
|
||||
_output.WriteLine("ATRP Batch(TSeries) validated successfully against TA-Lib ATR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib_Streaming()
|
||||
{
|
||||
int[] periods = { 14 };
|
||||
|
||||
// Note: QuanTAlib ATRP uses warmup-compensated RMA which gives slightly different
|
||||
// results than TA-Lib's classic Wilder's approach. The difference (~4-7%) accumulates
|
||||
// over 5000 bars but both implementations are mathematically valid.
|
||||
// Using absolute tolerance of 0.10 to account for accumulated drift divergence
|
||||
// QuanTAlib warmup-compensated RMA diverges from TA-Lib classic Wilder over time
|
||||
const double AtrpTolerance = 0.10;
|
||||
|
||||
// Prepare data for TA-Lib (double[])
|
||||
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
|
||||
double[] atrOutput = new double[hData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ATRP (streaming)
|
||||
var atrp = new Atrp(period);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Bars)
|
||||
{
|
||||
qResults.Add(atrp.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate TA-Lib ATR
|
||||
var retCode = TALib.Functions.Atr(hData, lData, cData, 0..^0, atrOutput, out var outRange, period);
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
int lookback = TALib.Functions.AtrLookback(period);
|
||||
|
||||
// Convert ATR to ATRP
|
||||
var expectedAtrp = new double[atrOutput.Length];
|
||||
for (int i = outRange.Start.Value; i < outRange.End.Value; i++)
|
||||
{
|
||||
double atr = atrOutput[i];
|
||||
double close = cData[i];
|
||||
expectedAtrp[i] = close > 0 ? (atr / close) * 100.0 : double.NaN;
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResults, expectedAtrp, outRange, lookback, tolerance: AtrpTolerance);
|
||||
}
|
||||
_output.WriteLine("ATRP Streaming validated successfully against TA-Lib ATR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_Batch()
|
||||
{
|
||||
int[] periods = { 14 };
|
||||
|
||||
// Prepare data for Tulip (double[])
|
||||
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ATRP (batch TSeries)
|
||||
var atrp = new Atrp(period);
|
||||
var qResult = atrp.Update(_testData.Bars);
|
||||
|
||||
// Calculate Tulip ATR
|
||||
var atrIndicator = Tulip.Indicators.atr;
|
||||
double[][] inputs = { hData, lData, cData };
|
||||
double[] options = { period };
|
||||
|
||||
// Tulip ATR lookback
|
||||
int lookback = atrIndicator.Start(options);
|
||||
double[][] outputs = { new double[hData.Length - lookback] };
|
||||
|
||||
atrIndicator.Run(inputs, options, outputs);
|
||||
var tAtr = outputs[0];
|
||||
|
||||
// Convert ATR to ATRP: (ATR / Close) * 100
|
||||
var expectedAtrp = new double[tAtr.Length];
|
||||
for (int i = 0; i < tAtr.Length; i++)
|
||||
{
|
||||
int dataIndex = lookback + i;
|
||||
double close = cData[dataIndex];
|
||||
expectedAtrp[i] = close > 0 ? (tAtr[i] / close) * 100.0 : double.NaN;
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, expectedAtrp, lookback, tolerance: ValidationHelper.TulipTolerance);
|
||||
}
|
||||
_output.WriteLine("ATRP Batch(TSeries) validated successfully against Tulip ATR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip_Streaming()
|
||||
{
|
||||
int[] periods = { 14 };
|
||||
|
||||
// Prepare data for Tulip (double[])
|
||||
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
|
||||
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
|
||||
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ATRP (streaming)
|
||||
var atrp = new Atrp(period);
|
||||
var qResults = new List<double>();
|
||||
foreach (var item in _testData.Bars)
|
||||
{
|
||||
qResults.Add(atrp.Update(item).Value);
|
||||
}
|
||||
|
||||
// Calculate Tulip ATR
|
||||
var atrIndicator = Tulip.Indicators.atr;
|
||||
double[][] inputs = { hData, lData, cData };
|
||||
double[] options = { period };
|
||||
|
||||
// Tulip ATR lookback
|
||||
int lookback = atrIndicator.Start(options);
|
||||
double[][] outputs = { new double[hData.Length - lookback] };
|
||||
|
||||
atrIndicator.Run(inputs, options, outputs);
|
||||
var tAtr = outputs[0];
|
||||
|
||||
// Convert ATR to ATRP
|
||||
var expectedAtrp = new double[tAtr.Length];
|
||||
for (int i = 0; i < tAtr.Length; i++)
|
||||
{
|
||||
int dataIndex = lookback + i;
|
||||
double close = cData[dataIndex];
|
||||
expectedAtrp[i] = close > 0 ? (tAtr[i] / close) * 100.0 : double.NaN;
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResults, expectedAtrp, lookback, tolerance: ValidationHelper.TulipTolerance);
|
||||
}
|
||||
_output.WriteLine("ATRP Streaming validated successfully against Tulip ATR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Ooples_Batch()
|
||||
{
|
||||
int[] periods = { 14 };
|
||||
|
||||
// Prepare data for Ooples (List<TickerData>)
|
||||
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Close = (double)q.Close,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Open = (double)q.Open,
|
||||
Volume = (double)q.Volume
|
||||
}).ToList();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib ATRP (batch TSeries)
|
||||
var atrp = new Atrp(period);
|
||||
var qResult = atrp.Update(_testData.Bars);
|
||||
|
||||
// Calculate Ooples ATR
|
||||
var stockData = new StockData(ooplesData);
|
||||
var oAtr = stockData.CalculateAverageTrueRange(MovingAvgType.WildersSmoothingMethod, period).OutputValues.Values.First();
|
||||
|
||||
// Convert ATR to ATRP
|
||||
var expectedAtrp = new List<double>();
|
||||
for (int i = 0; i < oAtr.Count; i++)
|
||||
{
|
||||
double atr = oAtr[i];
|
||||
double close = ooplesData[i].Close;
|
||||
expectedAtrp.Add(close > 0 ? (atr / close) * 100.0 : double.NaN);
|
||||
}
|
||||
|
||||
// Compare last 100 records
|
||||
ValidationHelper.VerifyData(qResult, expectedAtrp, (s) => s, 100, ValidationHelper.OoplesTolerance);
|
||||
}
|
||||
_output.WriteLine("ATRP Batch(TSeries) validated successfully against Ooples ATR");
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// ATRP: Average True Range Percent
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ATR as percentage of closing price for cross-asset volatility comparison.
|
||||
/// Higher values indicate greater relative volatility; typical range 0-10%.
|
||||
///
|
||||
/// Calculation: <c>ATRP = (ATR / Close) × 100</c>.
|
||||
/// </remarks>
|
||||
/// <seealso href="Atrp.md">Detailed documentation</seealso>
|
||||
[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 = Math.Abs(close) > 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);
|
||||
}
|
||||
|
||||
public static (TSeries Results, Atrp Indicator) Calculate(TBarSeries source, int period)
|
||||
{
|
||||
var indicator = new Atrp(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
# ATRP: Average True Range Percent
|
||||
|
||||
> "Volatility without context is noise. ATRP gives you context."
|
||||
|
||||
ATRP normalizes the Average True Range (ATR) as a percentage of the closing price. This transforms an absolute volatility measure into a relative one, enabling meaningful comparisons across different price levels and different assets.
|
||||
|
||||
A $5 stock and a $500 stock might both have an ATR of 2.0, but their volatility profiles are completely different. ATRP reveals the truth: the $5 stock is moving 40% while the $500 stock is moving 0.4%.
|
||||
|
||||
## Historical Context
|
||||
|
||||
ATRP is a derivative of J. Welles Wilder Jr.'s ATR, introduced in his 1978 work *New Concepts in Technical Trading Systems*. While Wilder focused on absolute range, traders quickly realized that percentage-based normalization was necessary for portfolio-level analysis and cross-asset comparison.
|
||||
|
||||
The indicator gained prominence with the rise of systematic trading strategies that needed to compare volatility across diverse asset classes—equities, commodities, forex—without the distortion of absolute price differences.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
ATRP builds on ATR's foundation and adds a single normalization step:
|
||||
|
||||
1. **True Range (TR)**: Captures the "real" distance price traveled, including gaps.
|
||||
2. **RMA Smoothing**: Wilder's smoothing method ($\alpha = 1/N$) provides the characteristic slow decay.
|
||||
3. **Percentage Normalization**: Divides by current close price and multiplies by 100.
|
||||
|
||||
### Why Percentage Matters
|
||||
|
||||
Consider two scenarios:
|
||||
|
||||
* **Stock A**: Price = \$100, ATR = 5.0 → ATRP = 5%
|
||||
* **Stock B**: Price = \$10, ATR = 2.0 → ATRP = 20%
|
||||
|
||||
ATR alone suggests Stock A is more volatile. ATRP reveals Stock B moves four times more in percentage terms—critical information for position sizing and risk management.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. True Range (TR)
|
||||
|
||||
$$
|
||||
TR_t = \max(H_t - L_t, |H_t - C_{t-1}|, |L_t - C_{t-1}|)
|
||||
$$
|
||||
|
||||
Where:
|
||||
|
||||
* $H_t$: Current High
|
||||
* $L_t$: Current Low
|
||||
* $C_{t-1}$: Previous Close
|
||||
|
||||
### 2. Average True Range (ATR)
|
||||
|
||||
$$
|
||||
ATR_t = RMA(TR, N)
|
||||
$$
|
||||
|
||||
Expanding the RMA:
|
||||
|
||||
$$
|
||||
ATR_t = \frac{ATR_{t-1} \times (N-1) + TR_t}{N}
|
||||
$$
|
||||
|
||||
### 3. ATRP (Percentage)
|
||||
|
||||
$$
|
||||
ATRP_t = \frac{ATR_t}{C_t} \times 100
|
||||
$$
|
||||
|
||||
Where $C_t$ is the current closing price.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | 10 | High; O(1) calculation via RMA + single division. |
|
||||
| **Allocations** | 0 | Zero-allocation in hot paths. |
|
||||
| **Complexity** | O(1) | Constant time regardless of period. |
|
||||
| **Accuracy** | 10 | Matches ATR-based calculation exactly. |
|
||||
| **Timeliness** | 4 | Inherits ATR's lag due to RMA smoothing. |
|
||||
| **Overshoot** | 0 | Bounded by mathematical definition. |
|
||||
| **Smoothness** | 8 | Smooth decay from RMA; slight additional noise from close price variation. |
|
||||
|
||||
## Validation
|
||||
|
||||
ATRP is validated by computing ATR from external libraries and applying the same percentage formula.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | ✅ | Validated. |
|
||||
| **TA-Lib** | ✅ | Validated via `(TA_ATR / Close) × 100`. |
|
||||
| **Skender** | ✅ | Validated via `(GetAtr / Close) × 100`. |
|
||||
| **Tulip** | ✅ | Validated via `(atr / Close) × 100`. |
|
||||
| **Ooples** | ✅ | Validated via `(CalculateAverageTrueRange / Close) × 100`. |
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Position Sizing
|
||||
|
||||
ATRP enables volatility-adjusted position sizing:
|
||||
|
||||
```
|
||||
Position Size = Risk Capital / (ATRP × Entry Price)
|
||||
```
|
||||
|
||||
This ensures each position carries equivalent percentage risk regardless of the asset's absolute price.
|
||||
|
||||
### Cross-Asset Comparison
|
||||
|
||||
Compare volatility across:
|
||||
|
||||
* Different price levels (penny stocks vs. blue chips)
|
||||
* Different asset classes (equities vs. commodities)
|
||||
* Different time periods (adjusting for price drift)
|
||||
|
||||
### Regime Detection
|
||||
|
||||
* **ATRP < 1%**: Low volatility regime—expect consolidation, mean reversion strategies favored.
|
||||
* **ATRP 2-4%**: Normal volatility—standard trend-following conditions.
|
||||
* **ATRP > 5%**: High volatility regime—crisis conditions, wider stops required.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
* **Lag**: ATRP inherits ATR's lag. It tells you what volatility *was*, not what it *will be*.
|
||||
* **Close Price Sensitivity**: A sharp close price move affects both the numerator (via TR) and denominator (close), creating transient spikes. Use multiple periods for confirmation.
|
||||
* **Zero/Near-Zero Prices**: Assets approaching zero will show extreme ATRP values. Ensure minimum price thresholds in screeners.
|
||||
* **Dividend Adjustments**: Unadjusted price data can create artificial gaps around ex-dividend dates, inflating TR.
|
||||
|
||||
## Related Indicators
|
||||
|
||||
* **ATR**: The absolute volatility measure ATRP normalizes.
|
||||
* **NATR**: Similar concept; some implementations differ in smoothing or warmup handling.
|
||||
* **ATRN**: ATR normalized to [0,1] range based on historical min/max.
|
||||
* **Volatility Ratio**: Compares current TR to average TR for breakout detection.
|
||||
@@ -1,40 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Average True Range Percent (ATRP)", "ATRP", overlay=false, format=format.percent, precision=2)
|
||||
|
||||
//@function Calculates the Average True Range Percent (ATRP)
|
||||
//@param length The period length for the ATR calculation.
|
||||
//@returns The ATRP value.
|
||||
//@optimized Beta precomputation for RMA warmup compensation
|
||||
atrp(simple int length) =>
|
||||
if length <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
var float prevClose = close
|
||||
float tr1 = high - low
|
||||
float tr2 = math.abs(high - prevClose)
|
||||
float tr3 = math.abs(low - prevClose)
|
||||
float trueRange = math.max(tr1, tr2, tr3)
|
||||
prevClose := close
|
||||
float alpha = 1.0 / float(length)
|
||||
float beta = 1.0 - alpha
|
||||
var float EPSILON = 1e-10
|
||||
var float raw_rma = 0.0
|
||||
var float e = 1.0
|
||||
float atr = na
|
||||
if not na(trueRange)
|
||||
raw_rma := (raw_rma * (length - 1) + trueRange) / length
|
||||
e *= beta
|
||||
atr := e > EPSILON ? raw_rma / (1.0 - e) : raw_rma
|
||||
close != 0.0 ? atr / close * 100 : na
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_length = input.int(14, "Length", minval=1, tooltip="Number of bars used for the ATR calculation")
|
||||
|
||||
// Calculation
|
||||
atrpValue = atrp(i_length)
|
||||
|
||||
// Plot
|
||||
plot(atrpValue, "ATRP", color=color.yellow, linewidth=2)
|
||||
@@ -186,8 +186,7 @@ Ensures equal percentage risk per position regardless of asset price.
|
||||
## Related Indicators
|
||||
|
||||
- **ATR**: Absolute volatility measure NATR normalizes
|
||||
- **ATRP**: Mathematically identical; different naming convention
|
||||
- **ATRN**: ATR normalized to [0,1] based on historical min/max
|
||||
- **ATRN**: ATR normalized to [0,1] based on historical min/max (different algorithm)
|
||||
- **CV**: Coefficient of Variation—alternative percentage volatility measure
|
||||
- **HV**: Historical Volatility—annualized standard deviation approach
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ TR is one of the most consistently implemented indicators across all libraries.
|
||||
|
||||
2. **Confusing TR with ATR**: TR is the raw, unsmoothed value per bar. ATR is TR smoothed over a period. TR can be very volatile; ATR provides a more stable volatility estimate.
|
||||
|
||||
3. **Unit dependency**: TR is in the same units as price. A $500 stock might have TR=10 while a $50 stock has TR=1, even if percentage volatility is identical. Use NATR (Normalized ATR) or ATRP (ATR Percent) for percentage-based comparisons.
|
||||
3. **Unit dependency**: TR is in the same units as price. A $500 stock might have TR=10 while a $50 stock has TR=1, even if percentage volatility is identical. Use NATR (Normalized ATR) for percentage-based comparisons.
|
||||
|
||||
4. **Gap sensitivity**: TR captures gaps, which may or may not be desirable. For intraday-only volatility, use High-Low range instead.
|
||||
|
||||
@@ -263,8 +263,7 @@ If gap contribution > 50% of TR: Significant gap move
|
||||
| Indicator | Relationship to TR |
|
||||
| :--- | :--- |
|
||||
| **ATR** | Smoothed TR (RMA/Wilder's MA) |
|
||||
| **NATR** | ATR / Close × 100 |
|
||||
| **ATRP** | ATR / Close × 100 (same as NATR) |
|
||||
| **NATR** | ATR / Close × 100 (also known as ATRP) |
|
||||
| **Keltner Channel** | Uses ATR for band width |
|
||||
| **Chandelier Exit** | Uses ATR for trailing stop |
|
||||
| **SuperTrend** | Uses ATR for trend bands |
|
||||
|
||||
@@ -240,7 +240,7 @@ Trend following: Best when VR 1.0-1.5 (movement with stability)
|
||||
| **Bollinger Width** | Both measure volatility; VR uses TR, BB uses std dev |
|
||||
| **Keltner Width** | KC uses ATR; VR provides ratio view of same data |
|
||||
| **ADX** | ADX measures trend strength; VR measures volatility expansion |
|
||||
| **ATRP** | ATRP = ATR/Close×100; VR = TR/ATR |
|
||||
| **NATR** | NATR = ATR/Close×100; VR = TR/ATR |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user