fix(docs): correct .md documentation across errors, dynamics, filters, forecasts, momentum, numerics, oscillators, reversals, statistics, trends, volatility, volume

Deep review of all indicator categories verified .md headers against .cs WarmupPeriod, parameters, inputs, and outputs. Fixes include warmup corrections, parameter documentation, output type accuracy, and Pine Script alignment.
This commit is contained in:
Miha Kralj
2026-03-10 18:38:23 -07:00
parent 8906c62dcf
commit 35a6702b06
178 changed files with 2579 additions and 998 deletions
-2
View File
@@ -9,8 +9,6 @@ indicator("ADX Variable Moving Average (ADXVMA)", "ADXVMA", overlay=true)
//@returns ADXVMA value that adapts smoothing based on trend strength measured by ADX
//@optimized O(1) per bar using Wilder's RMA with warmup compensation for all smoothed components
adxvma(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
float alpha = 1.0 / float(period)
float beta = 1.0 - alpha
float EPSILON = 1e-10
-2
View File
@@ -11,8 +11,6 @@ indicator("Ahrens Moving Average (AHRENS)", "AHRENS", overlay=true)
//@reference Richard D. Ahrens, "Build A Better Moving Average" (Stocks & Commodities V.31:11, October 2013)
//@optimized O(1) per bar via circular buffer for lagged MA state; O(period) memory for the ring buffer
ahrens(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
// Circular buffer to store past ahma values for period-bar lookback
var array<float> buffer = array.new_float(period, na)
-2
View File
@@ -20,8 +20,6 @@ indicator("Coral Filter (CORAL)", "CORAL", overlay=true)
// bfr = -cd³*i6 + c3*i5 + c4*i4 + c5*i3
// Note: c3 + c4 + c5 + (-cd³) = 1 (unity DC gain).
coral(series float source, simple int period, simple float cd = 0.4) =>
if period <= 0
runtime.error("Period must be greater than 0")
float di = (period - 1.0) / 2.0 + 1.0
float c1 = 2.0 / (di + 1.0)
float c2 = 1.0 - c1
+1 -2
View File
@@ -18,9 +18,9 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class Decay : AbstractBase
{
private readonly int _period;
private readonly double _scale;
private int _count;
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValid, double LastOutput);
private State _state, _p_state;
private int _p_count;
@@ -40,7 +40,6 @@ public sealed class Decay : AbstractBase
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_period = period;
_scale = 1.0 / period;
Name = $"Decay({period})";
WarmupPeriod = 1;
-2
View File
@@ -8,8 +8,6 @@ indicator("Linear Decay (DECAY)", "DECAY", overlay=true)
//@param length Decay period
//@returns Decayed value that tracks peaks and descends linearly
decay(series float source, simple int length) =>
if length <= 0
runtime.error("Length must be greater than 0")
var float prev = na
float scale = 1.0 / length
float result = na
-2
View File
@@ -10,8 +10,6 @@ indicator("Ehlers Decycler (DECYCLER)", "DECYCLER", overlay=true)
//@returns Decycler value (source minus high-pass filtered component)
//@optimized Uses 2-pole Butterworth HP with O(1) complexity per bar
decycler(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be positive")
float src = na(source) ? 0.0 : source
-2
View File
@@ -10,8 +10,6 @@ indicator("Double Exponential Moving Average (DEMA)", "DEMA", overlay=true)
//@returns DEMA value from first bar with proper compensation
//@optimized Uses exponential warmup compensator on both EMA stages for O(1) complexity
dema(series float source, simple int period=0, simple float alpha=0) =>
if alpha <= 0 and period <= 0
runtime.error("Alpha or period must be provided")
float a = alpha > 0 ? alpha : 2.0 / (period + 1)
float beta = 1.0 - a
var bool warmup = true
@@ -0,0 +1,210 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class EdecayIndicatorTests
{
[Fact]
public void EdecayIndicator_Constructor_SetsDefaults()
{
var indicator = new EdecayIndicator();
Assert.Equal(5, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("EDECAY - Exponential Decay", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void EdecayIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new EdecayIndicator { Period = 20 };
Assert.Equal(0, EdecayIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void EdecayIndicator_ShortName_IncludesPeriodAndSource()
{
var indicator = new EdecayIndicator { Period = 15 };
Assert.Contains("EDECAY", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void EdecayIndicator_Initialize_CreatesLineSeries()
{
var indicator = new EdecayIndicator { Period = 5 };
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void EdecayIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new EdecayIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
Assert.Equal(1, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void EdecayIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new EdecayIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void EdecayIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new EdecayIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void EdecayIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new EdecayIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(
now.AddMinutes(i),
100 + i * 2,
105 + i * 2,
95 + i * 2,
102 + i * 2);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.Equal(20, indicator.LinesSeries[0].Count);
for (int i = 0; i < 20; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
}
}
[Fact]
public void EdecayIndicator_DifferentSourceTypes_Work()
{
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new EdecayIndicator { Period = 5, Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
$"Source {source} should produce finite value");
}
}
[Fact]
public void EdecayIndicator_Period_CanBeChanged()
{
var indicator = new EdecayIndicator { Period = 10 };
Assert.Equal(10, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
Assert.Equal(0, EdecayIndicator.MinHistoryDepths);
}
[Fact]
public void EdecayIndicator_Uptrend_OutputFollowsPrice()
{
var indicator = new EdecayIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
double price = 100 + i * 5;
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// In uptrend, edecay output should equal close price (input > decayed)
double lastValue = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(145, lastValue, 1); // last close = 100 + 9*5 = 145
}
[Fact]
public void EdecayIndicator_FlatPrices_OutputEqualsInput()
{
var indicator = new EdecayIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double lastValue = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(100, lastValue, 1);
}
[Fact]
public void EdecayIndicator_DifferentPeriods_Work()
{
var periods = new[] { 1, 5, 10, 20 };
foreach (var period in periods)
{
var indicator = new EdecayIndicator { Period = period };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 102 + i, 98 + i, 101 + i);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.Equal(10, indicator.LinesSeries[0].Count);
}
}
}
+60
View File
@@ -0,0 +1,60 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
/// <summary>
/// EDECAY (Exponential Decay) Quantower indicator.
/// Tracks peaks and decays exponentially at a rate of (period-1)/period per bar.
/// Formula: output = max(input, prev_output * (period-1)/period)
/// </summary>
[SkipLocalsInit]
public class EdecayIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 5;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Edecay _edecay = null!;
protected LineSeries Series;
protected string SourceName = null!;
private Func<IHistoryItem, double> _priceSelector = null!;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"EDECAY {Period}:{SourceName}";
public EdecayIndicator()
{
OnBackGround = true;
SeparateWindow = false;
SourceName = Source.ToString();
Name = "EDECAY - Exponential Decay";
Description = "Exponential Decay: output = max(input, prev_output * (period-1)/period)";
Series = new LineSeries(name: $"EDECAY {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
_edecay = new Edecay(Period);
SourceName = Source.ToString();
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _edecay.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
Series.SetValue(result.Value, _edecay.IsHot, ShowColdValues);
}
}
+449
View File
@@ -0,0 +1,449 @@
using Xunit;
namespace QuanTAlib.Tests;
public class EdecayTests
{
private readonly TSeries _gbm;
private const int TestPeriod = 5;
private const int DataPoints = 100;
public EdecayTests()
{
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.5, seed: 42);
var bars = gbm.Fetch(DataPoints, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
_gbm = bars.Close;
}
#region Constructor Tests
[Fact]
public void Constructor_WithValidPeriod_SetsProperties()
{
var edecay = new Edecay(TestPeriod);
Assert.Equal($"Edecay({TestPeriod})", edecay.Name);
Assert.Equal(1, edecay.WarmupPeriod);
}
[Fact]
public void Constructor_WithZeroPeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Edecay(0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_WithNegativePeriod_ThrowsArgumentException()
{
var ex = Assert.Throws<ArgumentException>(() => new Edecay(-1));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Constructor_WithSource_SubscribesToEvents()
{
var source = new TSeries(DataPoints);
var edecay = new Edecay(source, TestPeriod);
Assert.NotNull(edecay);
}
#endregion
#region Basic Calculation Tests
[Fact]
public void Update_FirstBar_ReturnsInputValue()
{
var edecay = new Edecay(TestPeriod);
var tv = edecay.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(100.0, tv.Value);
}
[Fact]
public void Update_DecayingValues_OutputDecaysExponentially()
{
var edecay = new Edecay(5); // scale = 4/5 = 0.8
var time = DateTime.UtcNow;
// First bar at 1.0
edecay.Update(new TValue(time, 1.0), true);
// Next bars at 0.0 — output should decay by ×0.8 per bar
var tv1 = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true);
Assert.Equal(0.8, tv1.Value, 10); // 1.0 * 0.8
var tv2 = edecay.Update(new TValue(time.AddSeconds(2), 0.0), true);
Assert.Equal(0.64, tv2.Value, 10); // 0.8 * 0.8
var tv3 = edecay.Update(new TValue(time.AddSeconds(3), 0.0), true);
Assert.Equal(0.512, tv3.Value, 10); // 0.64 * 0.8
}
[Fact]
public void Update_RisingInput_FollowsInput()
{
var edecay = new Edecay(5);
var time = DateTime.UtcNow;
edecay.Update(new TValue(time, 100.0), true);
var tv = edecay.Update(new TValue(time.AddSeconds(1), 105.0), true);
Assert.Equal(105.0, tv.Value, 10); // input > decayed, so follows input
}
[Fact]
public void Last_IsAccessible()
{
var edecay = new Edecay(TestPeriod);
edecay.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(100.0, edecay.Last.Value, 10);
}
[Fact]
public void IsHot_ReturnsFalseBeforeFirstBar()
{
var edecay = new Edecay(TestPeriod);
Assert.False(edecay.IsHot);
}
[Fact]
public void IsHot_ReturnsTrueAfterFirstBar()
{
var edecay = new Edecay(TestPeriod);
edecay.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.True(edecay.IsHot);
}
[Fact]
public void Name_IsAccessible()
{
var edecay = new Edecay(TestPeriod);
Assert.Equal($"Edecay({TestPeriod})", edecay.Name);
}
#endregion
#region State Management Tests
[Fact]
public void Update_WithIsNewTrue_AdvancesState()
{
var edecay = new Edecay(TestPeriod);
var time = DateTime.UtcNow;
edecay.Update(new TValue(time, 100.0), true);
edecay.Update(new TValue(time.AddSeconds(1), 105.0), true);
edecay.Update(new TValue(time.AddSeconds(2), 110.0), true);
Assert.NotEqual(default, edecay.Last);
}
[Fact]
public void Update_WithIsNewFalse_UpdatesCurrentState()
{
var edecay = new Edecay(5); // scale = 0.8
var time = DateTime.UtcNow;
edecay.Update(new TValue(time, 1.0), true);
var first = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true);
// Correct same bar with different value
var corrected = edecay.Update(new TValue(time.AddSeconds(1), 0.5), false);
// first: max(0.0, 1.0*0.8)=0.8
Assert.Equal(0.8, first.Value, 10);
// corrected: max(0.5, 1.0*0.8)=0.8
Assert.Equal(0.8, corrected.Value, 10);
}
[Fact]
public void Update_IterativeCorrections_RestoresPreviousState()
{
var edecay = new Edecay(5);
var time = DateTime.UtcNow;
edecay.Update(new TValue(time, 1.0), true);
var baseline = edecay.Update(new TValue(time.AddSeconds(1), 0.5), true);
// Make several corrections
edecay.Update(new TValue(time.AddSeconds(1), 0.9), false);
edecay.Update(new TValue(time.AddSeconds(1), 0.1), false);
var restored = edecay.Update(new TValue(time.AddSeconds(1), 0.5), false);
Assert.Equal(baseline.Value, restored.Value, 10);
}
[Fact]
public void Reset_ClearsStateAndLastValidTracking()
{
var edecay = new Edecay(TestPeriod);
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
edecay.Update(new TValue(time.AddSeconds(i), 100.0 + i));
}
edecay.Reset();
Assert.Equal(default, edecay.Last);
Assert.False(edecay.IsHot);
}
#endregion
#region Robustness Tests
[Fact]
public void Update_WithNaN_UsesLastValidValue()
{
var edecay = new Edecay(5);
var time = DateTime.UtcNow;
edecay.Update(new TValue(time, 1.0), true);
var afterNaN = edecay.Update(new TValue(time.AddSeconds(1), double.NaN), true);
Assert.True(double.IsFinite(afterNaN.Value));
// NaN uses last valid (1.0), so max(1.0, 1.0*0.8)=1.0
Assert.Equal(1.0, afterNaN.Value, 10);
}
[Fact]
public void Update_WithInfinity_UsesLastValidValue()
{
var edecay = new Edecay(5);
var time = DateTime.UtcNow;
edecay.Update(new TValue(time, 1.0), true);
var afterInf = edecay.Update(new TValue(time.AddSeconds(1), double.PositiveInfinity), true);
Assert.True(double.IsFinite(afterInf.Value));
}
[Fact]
public void Update_BatchNaN_HandlesSafely()
{
var edecay = new Edecay(TestPeriod);
var time = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
var value = i % 3 == 0 ? double.NaN : 100.0 + i;
var tv = edecay.Update(new TValue(time.AddSeconds(i), value), true);
Assert.True(double.IsFinite(tv.Value));
}
}
#endregion
#region Consistency Tests (All 4 modes must match)
[Fact]
public void AllModes_ProduceSameResults()
{
// Mode 1: Batch via TSeries
var batchResult = Edecay.Batch(_gbm, TestPeriod);
// Mode 2: Streaming
var streamingEdecay = new Edecay(TestPeriod);
var streamingResult = new TSeries(DataPoints);
for (int i = 0; i < _gbm.Count; i++)
{
var tv = streamingEdecay.Update(new TValue(_gbm[i].Time, _gbm[i].Value), true);
streamingResult.Add(tv, true);
}
// Mode 3: Span-based
Span<double> spanOutput = stackalloc double[DataPoints];
Edecay.Batch(_gbm.Values, spanOutput, TestPeriod);
// Mode 4: Event-driven
var eventEdecay = new Edecay(TestPeriod);
var eventResult = new TSeries(DataPoints);
eventEdecay.Pub += (object? _, in TValueEventArgs e) => eventResult.Add(e.Value, e.IsNew);
for (int i = 0; i < _gbm.Count; i++)
{
eventEdecay.Update(new TValue(_gbm[i].Time, _gbm[i].Value), true);
}
int compareCount = Math.Min(100, DataPoints);
for (int i = DataPoints - compareCount; i < DataPoints; i++)
{
Assert.Equal(batchResult[i].Value, streamingResult[i].Value, 10);
Assert.Equal(batchResult[i].Value, spanOutput[i], 10);
Assert.Equal(batchResult[i].Value, eventResult[i].Value, 10);
}
}
#endregion
#region Span API Tests
[Fact]
public void Calculate_Span_ValidatesEmptySource()
{
var ex = Assert.Throws<ArgumentException>(() =>
{
ReadOnlySpan<double> empty = [];
Span<double> output = stackalloc double[1];
Edecay.Batch(empty, output, TestPeriod);
});
Assert.Equal("source", ex.ParamName);
}
[Fact]
public void Calculate_Span_ValidatesOutputLength()
{
var ex = Assert.Throws<ArgumentException>(() =>
{
ReadOnlySpan<double> source = stackalloc double[] { 1, 2, 3, 4, 5 };
Span<double> output = stackalloc double[3]; // too short
Edecay.Batch(source, output, TestPeriod);
});
Assert.Equal("output", ex.ParamName);
}
[Fact]
public void Calculate_Span_ValidatesPeriod()
{
var ex = Assert.Throws<ArgumentException>(() =>
{
ReadOnlySpan<double> source = stackalloc double[] { 1, 2, 3, 4, 5 };
Span<double> output = stackalloc double[5];
Edecay.Batch(source, output, 0);
});
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Calculate_Span_MatchesTSeries()
{
var batchResult = Edecay.Batch(_gbm, TestPeriod);
Span<double> spanOutput = stackalloc double[DataPoints];
Edecay.Batch(_gbm.Values, spanOutput, TestPeriod);
for (int i = 0; i < DataPoints; i++)
{
Assert.Equal(batchResult[i].Value, spanOutput[i], 10);
}
}
[Fact]
public void Calculate_Span_LargeData_NoStackOverflow()
{
int largeSize = 10000;
double[] source = new double[largeSize];
double[] output = new double[largeSize];
for (int i = 0; i < largeSize; i++)
{
source[i] = 100.0 + i * 0.1;
}
Edecay.Batch(source, output, TestPeriod);
Assert.Equal(largeSize, output.Length);
}
#endregion
#region Chainability Tests
[Fact]
public void Pub_FiresOnUpdate()
{
var edecay = new Edecay(TestPeriod);
bool eventFired = false;
edecay.Pub += (object? _, in TValueEventArgs e) => eventFired = true;
edecay.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.True(eventFired);
}
[Fact]
public void EventBasedChaining_Works()
{
var source = new TSeries(10);
var edecay = new Edecay(source, 2);
var results = new List<double>();
edecay.Pub += (object? _, in TValueEventArgs e) => results.Add(e.Value.Value);
for (int i = 0; i < 10; i++)
{
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0 + i), true);
}
Assert.Equal(10, results.Count);
}
#endregion
#region Edecay-Specific Tests
[Fact]
public void Edecay_Period1_DecaysToZero()
{
var edecay = new Edecay(1); // scale = 0/1 = 0.0
var time = DateTime.UtcNow;
edecay.Update(new TValue(time, 5.0), true);
var tv = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true);
// max(0.0, 5.0*0.0) = 0.0
Assert.Equal(0.0, tv.Value, 10);
}
[Fact]
public void Edecay_ConstantInput_OutputEqualsInput()
{
var edecay = new Edecay(5);
var time = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
var tv = edecay.Update(new TValue(time.AddSeconds(i), 100.0), true);
Assert.Equal(100.0, tv.Value, 10);
}
}
[Fact]
public void Edecay_OutputNeverBelowInput()
{
var edecay = new Edecay(10);
var time = DateTime.UtcNow;
var rng = new Random(42);
for (int i = 0; i < 100; i++)
{
double input = rng.NextDouble() * 200;
var tv = edecay.Update(new TValue(time.AddSeconds(i), input), true);
Assert.True(tv.Value >= input || Math.Abs(tv.Value - input) < 1e-10,
$"Output {tv.Value} should be >= input {input}");
}
}
[Fact]
public void Edecay_DiffersFromLinearDecay()
{
var edecay = new Edecay(5); // scale = 0.8
var decay = new Decay(5); // scale = 0.2
var time = DateTime.UtcNow;
// Start both at 100
edecay.Update(new TValue(time, 100.0), true);
decay.Update(new TValue(time, 100.0), true);
// Feed 0.0 and compare
var e1 = edecay.Update(new TValue(time.AddSeconds(1), 0.0), true);
var d1 = decay.Update(new TValue(time.AddSeconds(1), 0.0), true);
// Edecay: max(0, 100*0.8) = 80
// Decay: max(0, 100-0.2) = 99.8
Assert.Equal(80.0, e1.Value, 10);
Assert.Equal(99.8, d1.Value, 10);
Assert.NotEqual(e1.Value, d1.Value);
}
#endregion
}
@@ -0,0 +1,260 @@
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for EDECAY (Exponential Decay) against the Tulip Indicators algorithm.
/// The Tulip .NET binding does not expose decay/edecay directly, so validation
/// uses manual computation of the Tulip ti_edecay algorithm:
/// output[0] = input[0]
/// output[i] = max(input[i], output[i-1] * (period-1)/period)
/// </summary>
public sealed class EdecayValidationTests(ITestOutputHelper output) : IDisposable
{
private readonly ValidationTestData _testData = new();
private readonly ITestOutputHelper _output = output;
private bool _disposed;
private const int TestPeriod = 5;
private const double TulipTolerance = 1e-9;
public void Dispose()
{
Dispose(disposing: true);
}
private void Dispose(bool disposing)
{
if (_disposed) { return; }
_disposed = true;
if (disposing) { _testData?.Dispose(); }
}
/// <summary>
/// Reference implementation of Tulip ti_edecay for validation.
/// </summary>
private static double[] TulipEdecay(double[] input, int period)
{
double[] output = new double[input.Length];
double scale = (period - 1.0) / period;
output[0] = input[0];
for (int i = 1; i < input.Length; i++)
{
double d = output[i - 1] * scale;
output[i] = input[i] > d ? input[i] : d;
}
return output;
}
#region Tulip Algorithm Validation
[Fact]
public void Edecay_MatchesTulipEdecay_Batch()
{
double[] input = _testData.RawData.ToArray();
var quantResult = Edecay.Batch(_testData.Data, TestPeriod);
double[] tulipResult = TulipEdecay(input, TestPeriod);
int count = quantResult.Count;
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
Assert.True(
Math.Abs(quantResult[i].Value - tulipResult[i]) <= TulipTolerance,
$"Mismatch at index {i}: QuanTAlib={quantResult[i].Value:G17}, Tulip={tulipResult[i]:G17}");
}
_output.WriteLine("Edecay Batch validated successfully against Tulip edecay algorithm");
}
[Fact]
public void Edecay_MatchesTulipEdecay_Streaming()
{
double[] input = _testData.RawData.ToArray();
var edecay = new Edecay(TestPeriod);
var streamingResults = new List<double>();
foreach (var item in _testData.Data)
{
streamingResults.Add(edecay.Update(item).Value);
}
double[] tulipResult = TulipEdecay(input, TestPeriod);
int count = streamingResults.Count;
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
Assert.True(
Math.Abs(streamingResults[i] - tulipResult[i]) <= TulipTolerance,
$"Mismatch at index {i}: QuanTAlib={streamingResults[i]:G17}, Tulip={tulipResult[i]:G17}");
}
_output.WriteLine("Edecay Streaming validated successfully against Tulip edecay algorithm");
}
[Fact]
public void Edecay_MatchesTulipEdecay_Span()
{
double[] input = _testData.RawData.ToArray();
var quantOutput = new double[input.Length];
Edecay.Batch(new ReadOnlySpan<double>(input), quantOutput, TestPeriod);
double[] tulipResult = TulipEdecay(input, TestPeriod);
int count = quantOutput.Length;
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
Assert.True(
Math.Abs(quantOutput[i] - tulipResult[i]) <= TulipTolerance,
$"Mismatch at index {i}: QuanTAlib={quantOutput[i]:G17}, Tulip={tulipResult[i]:G17}");
}
_output.WriteLine("Edecay Span validated successfully against Tulip edecay algorithm");
}
#endregion
#region Different Periods
[Theory]
[InlineData(1)]
[InlineData(5)]
[InlineData(10)]
[InlineData(20)]
[InlineData(50)]
public void Edecay_MatchesTulipEdecay_DifferentPeriods(int period)
{
double[] input = _testData.RawData.ToArray();
var quantResult = Edecay.Batch(_testData.Data, period);
double[] tulipResult = TulipEdecay(input, period);
int count = quantResult.Count;
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
Assert.True(
Math.Abs(quantResult[i].Value - tulipResult[i]) <= TulipTolerance,
$"Period={period}, Mismatch at index {i}: QuanTAlib={quantResult[i].Value:G17}, Tulip={tulipResult[i]:G17}");
}
}
#endregion
#region Edge Cases
[Fact]
public void Edecay_HandlesConstantValues()
{
var constantData = new TSeries(100);
for (int i = 0; i < 100; i++)
{
constantData.Add(new TValue(DateTime.UtcNow.AddSeconds(i), 100.0), true);
}
var result = Edecay.Batch(constantData, TestPeriod);
// Constant input: output always equals input since input >= decayed
for (int i = 0; i < 100; i++)
{
Assert.Equal(100.0, result[i].Value, TulipTolerance);
}
}
[Fact]
public void Edecay_HandlesExponentiallyDecreasing()
{
double[] input = new double[20];
for (int i = 0; i < 20; i++)
{
input[i] = 100.0 * Math.Pow(0.9, i);
}
var quantOutput = new double[20];
Edecay.Batch(input, quantOutput, TestPeriod);
double[] tulipResult = TulipEdecay(input, TestPeriod);
for (int i = 0; i < 20; i++)
{
Assert.Equal(tulipResult[i], quantOutput[i], TulipTolerance);
}
}
[Fact]
public void Batch_MatchesStreaming_IdenticalResults()
{
var batchResult = Edecay.Batch(_testData.Data, TestPeriod);
var edecay = new Edecay(TestPeriod);
var streamingResults = new List<double>();
foreach (var item in _testData.Data)
{
streamingResults.Add(edecay.Update(item).Value);
}
int count = _testData.Data.Count;
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
Assert.Equal(batchResult[i].Value, streamingResults[i], ValidationHelper.DefaultTolerance);
}
_output.WriteLine("Edecay Batch vs Streaming consistency validated");
}
[Fact]
public void Edecay_OutputAlwaysGreaterOrEqualInput()
{
double[] input = _testData.RawData.ToArray();
var quantOutput = new double[input.Length];
Edecay.Batch(input, quantOutput, TestPeriod);
for (int i = 0; i < input.Length; i++)
{
Assert.True(quantOutput[i] >= input[i] - 1e-15,
$"Output {quantOutput[i]} must be >= input {input[i]} at index {i}");
}
}
[Fact]
public void Edecay_DecayIsMultiplicative()
{
// With period=5, scale = 4/5 = 0.8
// After a spike, each subsequent bar without new highs should multiply by 0.8
double[] input = [100.0, 0.0, 0.0, 0.0, 0.0, 0.0];
double[] tulipResult = TulipEdecay(input, TestPeriod);
// output[0] = 100.0
// output[1] = max(0, 100 * 0.8) = 80.0
// output[2] = max(0, 80 * 0.8) = 64.0
// output[3] = max(0, 64 * 0.8) = 51.2
// output[4] = max(0, 51.2 * 0.8) = 40.96
// output[5] = max(0, 40.96 * 0.8) = 32.768
Assert.Equal(100.0, tulipResult[0], TulipTolerance);
Assert.Equal(80.0, tulipResult[1], TulipTolerance);
Assert.Equal(64.0, tulipResult[2], TulipTolerance);
Assert.Equal(51.2, tulipResult[3], TulipTolerance);
Assert.Equal(40.96, tulipResult[4], TulipTolerance);
Assert.Equal(32.768, tulipResult[5], TulipTolerance);
var quantOutput = new double[6];
Edecay.Batch(input, quantOutput, TestPeriod);
for (int i = 0; i < 6; i++)
{
Assert.Equal(tulipResult[i], quantOutput[i], TulipTolerance);
}
}
#endregion
}
+1 -2
View File
@@ -18,9 +18,9 @@ namespace QuanTAlib;
[SkipLocalsInit]
public sealed class Edecay : AbstractBase
{
private readonly int _period;
private readonly double _scale;
private int _count;
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValid, double LastOutput);
private State _state, _p_state;
private int _p_count;
@@ -40,7 +40,6 @@ public sealed class Edecay : AbstractBase
throw new ArgumentException("Period must be >= 1", nameof(period));
}
_period = period;
_scale = (period - 1.0) / period;
Name = $"Edecay({period})";
WarmupPeriod = 1;
+165
View File
@@ -0,0 +1,165 @@
# EDECAY: Exponential Decay
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Trends (IIR) |
| **Inputs** | Source (close) |
| **Parameters** | `period` (default 5) |
| **Outputs** | Single series (Edecay) |
| **Output range** | Same as input (overlay) |
| **Warmup** | `1` bar |
### TL;DR
- EDECAY (Exponential Decay) tracks the maximum of the current input and the previous output multiplied by a decay factor of `(period-1)/period`.
- Parameterized by `period` (default 5).
- Output range: Same as input — this is an overlay indicator.
- Requires `1` bar of warmup before first valid output (IsHot = true).
- Validated against Tulip Indicators `ti_edecay` reference algorithm.
> "A ratchet that only moves down gradually: price can push it up instantly, but gravity pulls it back at an exponential pace — faster when far from zero, slower as it approaches."
EDECAY implements the exponential decaying function. When price is above the decayed level, output snaps to price. When price falls below, the output decays exponentially by multiplying by `(period-1)/period` per bar, creating a ceiling that gradually descends. Unlike linear DECAY which subtracts a fixed amount, EDECAY's multiplicative factor produces a proportional decay rate.
## Historical Context
The exponential decay indicator originates from the Tulip Indicators library, a high-performance C library of technical indicators. It provides a peak-tracking mechanism where the tracked level decays at a proportional rate. The indicator is useful for:
- **Trailing stops**: The decaying level acts as a trailing stop that descends proportionally.
- **Peak detection**: Identifies when price last reached a new high relative to the decay rate.
- **Signal filtering**: Removes noise by requiring price to exceed the decayed level to register as significant.
## Architecture & Physics
### 1. Pure IIR (No Buffer)
The indicator requires no history buffer — only the previous output value is needed:
$$
\text{state} = \{y_{t-1}\}
$$
This makes it O(1) in both time and space.
### 2. Exponential Decay Calculation
$$
y_t = \max(x_t, \; y_{t-1} \cdot \frac{p-1}{p})
$$
where:
- $x_t$ = current input value
- $y_{t-1}$ = previous output value
- $p$ = period parameter
- $\frac{p-1}{p}$ = multiplicative decay factor per bar
### 3. First Bar Initialization
$$
y_0 = x_0
$$
The first bar simply passes through the input value.
### 4. State Management
The indicator uses state rollback for bar correction:
```
if isNew:
save current state as previous
else:
restore previous state
```
## Mathematical Foundation
### Core Formula
$$
y_t = \max(x_t, \; y_{t-1} \cdot s)
$$
where $s = \frac{p-1}{p}$ is the multiplicative decay factor.
### Decay Behavior
After a peak at value $v$, with no new inputs exceeding the decayed level, the output follows:
$$
y_{t+k} = v \cdot s^k = v \cdot \left(\frac{p-1}{p}\right)^k
$$
The output asymptotically approaches zero but never reaches it ($v > 0$).
### Comparison with Linear Decay
| Property | DECAY (Linear) | EDECAY (Exponential) |
|----------|----------------|---------------------|
| Formula | $y - \frac{1}{p}$ | $y \cdot \frac{p-1}{p}$ |
| Decay rate | Constant absolute | Proportional to current value |
| Reaches zero | Yes, in finite time | No, asymptotic approach |
| Scale-invariant | No | Yes |
### Properties
| Property | Value |
|----------|-------|
| Lookback | 0 |
| Output ≥ Input | Always (by construction) |
| Decay rate | Proportional $\frac{p-1}{p}$ |
| Monotonic when decaying | Yes (strictly decreasing) |
| Scale-invariant | Yes |
## Performance Profile
### Operation Count (Streaming Mode)
| Operation | Count | Notes |
| :--- | :---: | :--- |
| MUL | 1 | prev_output × scale |
| MAX/CMP | 1 | max(input, decayed) |
| State copy | 1 | rollback support |
| **Total** | **~3 ops** | Extremely lightweight |
### Batch Mode (Span-based)
| Operation | Complexity | Notes |
| :--- | :---: | :--- |
| Per-element | O(1) | Mul + compare |
| Total | O(n) | Linear scan |
| Memory | O(1) | No additional allocation |
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 10/10 | Exact arithmetic, no approximation |
| **Timeliness** | 10/10 | Zero lag on upward moves |
| **Smoothness** | 3/10 | Exponential curve smoother than linear staircase |
| **Simplicity** | 10/10 | Single multiplication + compare |
## Validation
| Library | Status | Notes |
| :--- | :---: | :--- |
| **Tulip** | ✅ | Manual ti_edecay algorithm matches exactly |
## Common Pitfalls
1. **Not a moving average**: Edecay is a peak-tracking/envelope indicator, not a smoothing filter. It only descends when price is below the decayed level.
2. **Proportional decay rate**: Unlike linear DECAY, EDECAY decays proportionally. For a stock at $100 with period=5, the first bar decays by $20; for a stock at $10, it decays by $2. This makes EDECAY scale-invariant.
3. **Period interpretation**: Period=5 means `scale = 4/5 = 0.8`, so each bar retains 80% of the previous value. After 5 bars, approximately 32.8% of the peak value remains.
4. **First bar**: The first bar always equals the input — there is no warmup period in the traditional sense.
5. **Asymmetric behavior**: Upward moves are instant (output = input), but downward moves are rate-limited to multiplication by `(period-1)/period` per bar.
6. **Never reaches zero**: Unlike linear DECAY, exponential decay asymptotically approaches zero but never reaches it (assuming positive values).
## References
- Tulip Indicators Library: https://tulipindicators.org/edecay
- Kegel, L. "Tulip Indicators" — Open-source C library of technical indicators.
+33
View File
@@ -0,0 +1,33 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("Exponential Decay (EDECAY)", "EDECAY", overlay=true)
//@function Calculates exponential decay: output = max(input, prev_output * (period-1)/period)
//@param source Source price series
//@param length Decay period
//@returns Decayed value that tracks peaks and descends exponentially
//@optimized Uses multiplicative decay factor for O(1) complexity per bar
edecay(series float source, simple int length) =>
var float prev = na
float scale = (length - 1.0) / length
float result = na
if na(prev)
result := source
else
float d = prev * scale
result := source > d ? source : d
prev := result
result
// ---------- Main loop ----------
// Inputs
i_source = input.source(close, "Source")
i_length = input.int(5, "Length", minval=1)
// Calculate Edecay
float edecay_val = edecay(i_source, i_length)
// Plot
plot(edecay_val, "Edecay", color=color.yellow, linewidth=2)
-2
View File
@@ -10,8 +10,6 @@ indicator("Exponential Moving Average (EMA)", "EMA", overlay=true)
//@returns EMA value from first bar with proper compensation
//@optimized Uses exponential warmup compensator for O(1) complexity and valid output from bar 1
ema(series float source, simple int period=0, simple float alpha=0) =>
if alpha <= 0 and period <= 0
runtime.error("Alpha or period must be provided")
float a = alpha > 0 ? alpha : 2.0 / (period + 1)
float beta = 1.0 - a
var bool warmup = true
-2
View File
@@ -14,8 +14,6 @@ indicator("Generalized Double Exponential Moving Average (GDEMA)", "GDEMA", over
//@reference Patrick G. Mulloy, "Smoothing Data with Faster Moving Averages" (TASC, Feb 1994)
//@optimized O(1) per bar; two cascaded EMA states with shared warmup compensator
gdema(series float source, simple int period, simple float vfactor) =>
if period <= 0
runtime.error("Period must be greater than 0")
float a = 2.0 / (period + 1)
float beta = 1.0 - a
-2
View File
@@ -15,8 +15,6 @@ indicator("Holt Exponential Moving Average (HOLT)", "HOLT", overlay=true)
// When gamma=0, degenerates to standard EMA (no trend correction).
// When gamma=alpha, provides balanced level/trend tracking.
holt(series float source, simple int period, simple float gamma=0) =>
if period <= 0
runtime.error("Period must be greater than 0")
float alpha = 2.0 / (period + 1)
float g = gamma > 0 ? gamma : alpha
var float level = na
-6
View File
@@ -11,12 +11,6 @@ indicator("Kaufman's Adaptive Moving Average (KAMA)", "KAMA", overlay=true)
//@returns KAMA value with efficiency ratio-based adaptive smoothing
//@optimized Uses efficiency ratio calculation for O(n) complexity per bar due to lookback sum
kama(series float source, simple int period, simple float fast_alpha=0.666667, simple float slow_alpha=0.0645) =>
if period <= 0
runtime.error("Period must be greater than 0")
if fast_alpha <= 0 or slow_alpha <= 0
runtime.error("Alpha values must be greater than 0")
if fast_alpha <= slow_alpha
runtime.error("Fast alpha must be greater than slow alpha")
var float kama_state = na
float current_kama = na
if not na(source)
-3
View File
@@ -16,9 +16,6 @@ indicator("Leader EMA (LEMA)", "LEMA", overlay=true)
// Stocks & Commodities, 26(7), 30-37.
//@optimized O(1) per bar — two IIR state variables with warmup compensation
lema(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
float price = nz(source)
float alpha = 2.0 / (period + 1)
float beta = 1.0 - alpha
-2
View File
@@ -12,8 +12,6 @@ indicator("Linear Trend Moving Average (LTMA)", "LTMA", overlay=true)
// EMA1 lags by τ = (1−α)/α bars; EMA1EMA2 ≈ slope·τ; result = EMA1 + (EMA1EMA2).
// Initializing both EMAs to source on bar 1 gives zero warmup bias with no compensator needed.
ltma(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
float alpha = 2.0 / (period + 1)
float beta = 1.0 - alpha
-2
View File
@@ -10,8 +10,6 @@ indicator("Ehlers MESA Adaptive Moving Average (MAMA)", "MAMA", overlay=true)
//@returns [mama, fama] array containing MAMA and FAMA values
//@optimized Uses Hilbert Transform phase detection for O(1) complexity per bar
mama(series float source, float fastLimit=0.5, float slowLimit=0.05) =>
if fastLimit < slowLimit or fastLimit <= 0 or slowLimit < 0
runtime.error("MAMA: fastLimit must be > slowLimit > 0")
var float mama_val = na
var float fama_val = na
var float period = 0.0
-4
View File
@@ -11,10 +11,6 @@ indicator("Moving Average Variable Period (MAVP)", "MAVP", overlay=true)
//@returns EMA value with variable alpha = 2/(period+1), compensated warmup
//@optimized Uses adaptive warmup compensator that tracks cumulative (1-alpha) product for O(1) per bar
mavp(series float source, series float period, simple int min_period, simple int max_period) =>
if min_period < 1
runtime.error("min_period must be >= 1")
if max_period < min_period
runtime.error("max_period must be >= min_period")
var float ema = 0.0
var float e = 1.0
var bool warmup = true
-3
View File
@@ -13,9 +13,6 @@ indicator("MCNMA - McNicholl EMA", "MCNMA", overlay=true)
//@param period Lookback period (must be > 0)
//@returns McNicholl EMA value from bar 1
mcnma(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
float src = nz(source)
float alpha = 2.0 / (period + 1)
float beta = 1.0 - alpha
-4
View File
@@ -10,10 +10,6 @@ indicator("McGinley Dynamic Indicator (MGDI)", "MGDI", overlay=true)
//@returns MGDI value that tracks price movements more closely than EMAs
//@optimized Uses adaptive smoothing with O(1) complexity after initialization
mgdi(series float source, simple int period, simple float factor=0.6) =>
if period <= 0
runtime.error("Period must be greater than 0")
if factor <= 0
runtime.error("Factor must be greater than 0")
var float mgdi_val = na
if not na(source)
if na(mgdi_val)
-2
View File
@@ -9,8 +9,6 @@ indicator("Modified Moving Average (MMA)", "MMA", overlay=true)
//@returns MMA value, combines SMA with weighted component for balanced smoothing
//@optimized Uses circular buffer for O(1) sum updates, O(n) for weighted component
mma(series float source, simple int period) =>
if period < 2
runtime.error("Period must be at least 2")
var array<float> buffer = array.new_float(math.min(math.max(2, period), 4000), na)
var int head = 0
var float sum = 0.0
-3
View File
@@ -21,9 +21,6 @@ indicator("NMA - Natural Moving Average", "NMA", overlay=true)
// @param period Lookback window for volatility analysis (must be > 0)
// @returns NMA value
nma(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
float src = nz(source)
// Step 1: scaled natural log of price
-4
View File
@@ -10,10 +10,6 @@ indicator("Regularized EMA (REMA)", "REMA", overlay=true)
//@returns REMA value, calculates from first bar using available data
//@optimized Uses regularization term to reduce noise for O(1) complexity
rema(series float source, simple int period, simple float lambda=0.5) =>
if period <= 0
runtime.error("Period must be greater than 0")
if lambda < 0.0 or lambda > 1.0
runtime.error("Lambda must be between 0 and 1")
float alpha = 2.0 / (period + 1.0)
var float rema_val = na
var float prev_rema = na
-4
View File
@@ -10,10 +10,6 @@ indicator("Recursive Gaussian Moving Average (RGMA)", "RGMA", overlay=true)
//@returns RGMA value with gaussian-like smoothing properties using recursive calculation
//@optimized Uses cascaded exponential filters for O(1) complexity per bar
rgma(series float source, simple int period, simple int passes=3) =>
if period <= 0
runtime.error("Period must be greater than 0")
if passes <= 0
runtime.error("Passes must be greater than 0")
simple float alpha = 2.0 / (period / math.sqrt(passes) + 1.0)
var array<float> filters = array.new_float(passes, na)
float result = na
-2
View File
@@ -9,8 +9,6 @@ indicator("Wilder's Moving Average (RMA)", "RMA", overlay=true)
//@returns RMA value from first bar with proper compensation for early values
//@optimized Uses exponential warmup compensator with Wilder's alpha (1/period) for O(1) complexity
rma(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be provided")
float a = 1.0 / float(period)
float beta = 1.0 - a
var bool warmup = true
-2
View File
@@ -10,8 +10,6 @@ indicator("Tillson T3 Moving Average (T3)", "T3", overlay=true)
//@returns T3 value with optimized coefficients
//@optimized Uses six cascaded EMAs with precomputed coefficients for O(1) complexity
t3(series float src, simple int period, simple float v) =>
if period <= 0
runtime.error("T3 period must be > 0")
float a = 2.0 / (period + 1)
float v2 = v * v
float v3 = v2 * v
-2
View File
@@ -11,8 +11,6 @@ indicator("Triple Exponential Moving Average (TEMA)", "TEMA", overlay=true)
//@returns TEMA value from first bar with proper compensation
//@optimized Uses exponential warmup compensator on all three EMA stages for O(1) complexity
tema(series float source, simple int period=0, simple float alpha=0.0, simple bool corrected=false) =>
if alpha <= 0 and period <= 0
runtime.error("Alpha or period must be provided")
float a1 = alpha > 0 ? alpha : (period > 0 ? 2.0 / (period + 1) : 0.1)
float r = math.pow(1.0 / a1, 1.0 / 3.0)
float a2 = corrected ? a1 * r : a1
-8
View File
@@ -13,14 +13,6 @@ indicator("Volatility Adjusted Moving Average (VAMA)", "VAMA", overlay=true)
//@returns VAMA value
//@optimized Uses RMA compensator for ATR and circular buffer for O(1) sum updates
vama(series float source, simple int base_length, simple int short_atr_period=10, simple int long_atr_period=50, simple int min_length=5, simple int max_length=100) =>
if base_length <= 0
runtime.error("Base length must be greater than 0")
if short_atr_period <= 0 or long_atr_period <= 0
runtime.error("ATR periods must be greater than 0")
if min_length <= 0 or max_length <= 0
runtime.error("Min and max length must be greater than 0")
if min_length > max_length
runtime.error("Min length must be less than or equal to max length")
var float prevClose = na
float tr1 = high - low
float tr2 = math.abs(high - prevClose)
-2
View File
@@ -10,8 +10,6 @@ indicator("Variable Index Dynamic Average (VIDYA)", "VIDYA", overlay=true)
//@returns VIDYA value that adapts to market volatility
//@optimized Uses volatility index calculation with O(n) complexity per bar due to lookback loops
vidya(series float source, simple int period, simple int std_period=0) =>
if period <= 0
runtime.error("Period must be greater than 0")
float alpha = 2.0 / (period + 1.0)
var float vidya = na
if not na(source)
-8
View File
@@ -13,14 +13,6 @@ indicator("Yang-Zhang Volatility Adjusted Moving Average (YZVAMA)", "YZVAMA", ov
//@returns YZVAMA value
//@optimized Uses RMA compensators for YZV and circular buffers for O(1) sum updates
yzvama(series float source, simple int yzv_short_period=3, simple int yzv_long_period=50, simple int percentile_lookback=100, simple int min_length=5, simple int max_length=100) =>
if yzv_short_period <= 0 or yzv_long_period <= 0
runtime.error("All periods must be greater than 0")
if min_length <= 0 or max_length <= 0
runtime.error("Min and max length must be greater than 0")
if min_length > max_length
runtime.error("Min length must be less than or equal to max length")
if percentile_lookback <= 0
runtime.error("Percentile lookback must be greater than 0")
var float prev_close = na
float o = open
float h = high
-2
View File
@@ -10,8 +10,6 @@ indicator("Zero-Lag Double EMA (ZLDEMA)", "ZLDEMA", overlay=true)
//@returns ZLDEMA value with zero-lag effect applied
//@optimized Uses lag compensation buffer and exponential warmup compensator on both EMA stages for O(1) complexity
zldema(series float source, simple int period=0, simple float alpha=0) =>
if alpha <= 0 and period <= 0
runtime.error("Alpha or period must be provided")
float a = alpha > 0 ? alpha : 2.0 / (period + 1)
float beta = 1.0 - a
simple int lag = math.max(1, math.round((period - 1) / 2))
-2
View File
@@ -10,8 +10,6 @@ indicator("Zero-Lag EMA (ZLEMA)", "ZLEMA", overlay=true)
//@returns ZLEMA value with zero-lag effect applied
//@optimized Uses lag compensation buffer and exponential warmup compensator for O(1) complexity
zlema(series float source, simple int period=0, simple float alpha=0) =>
if alpha <= 0 and period <= 0
runtime.error("Alpha or period must be provided")
float a = alpha > 0 ? alpha : 2.0 / (period + 1)
float beta = 1.0 - a
simple int lag = math.max(1, math.round((period - 1) / 2))
+8 -13
View File
@@ -10,13 +10,8 @@ indicator("Zero-Lag Triple EMA (ZLTEMA)", "ZLTEMA", overlay=true)
//@returns ZLTEMA value with zero-lag effect applied
//@optimized Uses lag compensation buffer and exponential warmup compensator on all three EMA stages for O(1) complexity
zltema(series float source, simple int period=0, simple float alpha=0) =>
if alpha <= 0 and period <= 0
runtime.error("Alpha or period must be provided")
float a1 = alpha > 0 ? alpha : 2.0 / (period + 1)
float beta1 = 1.0 - a1
float r = math.pow(1.0 / a1, 1.0 / 3.0)
float a2 = a1 * r
float a3 = a2 * r
float a = alpha > 0 ? alpha : 2.0 / (period + 1)
float beta = 1.0 - a
simple int lag = math.max(1, math.round((period - 1) / 2))
var bool warmup = true
var float e = 1.0
@@ -37,21 +32,21 @@ zltema(series float source, simple int period=0, simple float alpha=0) =>
array.push(priceBuffer, source)
float laggedPrice = nz(array.get(priceBuffer, 0), source)
float signal = 2 * source - laggedPrice
ema1_raw := a1 * (signal - ema1_raw) + ema1_raw
ema1_raw := a * (signal - ema1_raw) + ema1_raw
if warmup
e *= beta1
e *= beta
float c = 1.0 / (1.0 - e)
ema1 := c * ema1_raw
ema2_raw := a2 * (ema1 - ema2_raw) + ema2_raw
ema2_raw := a * (ema1 - ema2_raw) + ema2_raw
ema2 := c * ema2_raw
ema3_raw := a3 * (ema2 - ema3_raw) + ema3_raw
ema3_raw := a * (ema2 - ema3_raw) + ema3_raw
ema3 := c * ema3_raw
warmup := e > 1e-10
else
ema1 := ema1_raw
ema2_raw := a2 * (ema1 - ema2_raw) + ema2_raw
ema2_raw := a * (ema1 - ema2_raw) + ema2_raw
ema2 := ema2_raw
ema3_raw := a3 * (ema2 - ema3_raw) + ema3_raw
ema3_raw := a * (ema2 - ema3_raw) + ema3_raw
ema3 := ema3_raw
3 * ema1 - 3 * ema2 + ema3
else