mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-26 06:18:05 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user