mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +00:00
Add documentation links for various volatility indicators and channels
- Updated BBWN, BBWP, CCV, CV, CVI, EWMA, GKV, HLV, HV, Jvolty, JVOLTYN, MASSI, NATR, RSV, RV, RVI, TR, UI, VOV, VR, YZV indicators with documentation links. - Added documentation links for Aberration, Acceleration Bands, Andrews' Pitchfork, Adaptive Price Zone, ATR Bands, Bollinger Bands, Center of Gravity, Donchian Channels, Decay Min-Max Channel, Detrended Synthetic Price, EACP, EBSW, HOMOD, Jurik Volatility Bands, Keltner Channel, MA Envelope, Min-Max Channel, Price Channel, Regression Channels, Standard Deviation Channel, Stoller Average Range Channel, Super Trend Bands, Ultimate Bands, Ultimate Channel, VWAP Bands, and VWAP with Standard Deviation Bands.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EdcfIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void EdcfIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new EdcfIndicator();
|
||||
|
||||
Assert.Equal(15, indicator.Length);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("EDCF - Ehlers Distance Coefficient Filter", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdcfIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
Assert.Equal(2, EdcfIndicator.MinHistoryDepths);
|
||||
var indicator = new EdcfIndicator();
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdcfIndicator_ShortName_IncludesLengthAndSource()
|
||||
{
|
||||
var indicator = new EdcfIndicator { Length = 10 };
|
||||
|
||||
Assert.Contains("EDCF", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdcfIndicator_Initialize_CreatesInternalEdcf()
|
||||
{
|
||||
var indicator = new EdcfIndicator { Length = 15 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdcfIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EdcfIndicator { Length = 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 EdcfIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new EdcfIndicator { Length = 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 EdcfIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new EdcfIndicator { Length = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdcfIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new EdcfIndicator { Length = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = [100, 102, 104, 103, 105, 107, 106, 108, 110, 109];
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdcfIndicator_DifferentSources_Work()
|
||||
{
|
||||
var sourceTypes = new[] { SourceType.Close, SourceType.Open, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var sourceType in sourceTypes)
|
||||
{
|
||||
var indicator = new EdcfIndicator { Length = 5, Source = sourceType };
|
||||
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 {sourceType} produced non-finite value");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class EdcfIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Length", sortIndex: 1, 2, 100, 1, 0)]
|
||||
public int Length { get; set; } = 15;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Edcf _edcf = null!;
|
||||
private readonly LineSeries _series;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 2;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"EDCF({Length}):{Source}";
|
||||
|
||||
public EdcfIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "EDCF - Ehlers Distance Coefficient Filter";
|
||||
Description = "Nonlinear adaptive filter with distance-based coefficients";
|
||||
_series = new LineSeries(name: $"EDCF {Length}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_edcf = new Edcf(Length);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _edcf.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value;
|
||||
_series.SetValue(value, _edcf.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class EdcfTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
private readonly GBM _gbm;
|
||||
|
||||
public EdcfTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
}
|
||||
|
||||
private static TSeries CreateSeries(params double[] values)
|
||||
{
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
series.Add(new TValue(DateTime.UtcNow.AddMinutes(i), values[i]));
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// A) Constructor Validation
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsName()
|
||||
{
|
||||
var ind = new Edcf(15);
|
||||
Assert.Contains("Edcf", ind.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsWarmupPeriod()
|
||||
{
|
||||
var ind = new Edcf(15);
|
||||
Assert.Equal(15, ind.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultLength()
|
||||
{
|
||||
var ind = new Edcf();
|
||||
Assert.Contains("15", ind.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesLength_TooSmall()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Edcf(1));
|
||||
Assert.Equal("length", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesLength_Zero()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Edcf(0));
|
||||
Assert.Equal("length", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesLength_Negative()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() => new Edcf(-5));
|
||||
Assert.Equal("length", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_MinimumLength()
|
||||
{
|
||||
var ex = Record.Exception(() => new Edcf(2));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// B) Basic Calculation
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void FirstBar_IsPassthrough()
|
||||
{
|
||||
var ind = new Edcf(5);
|
||||
var result = ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(100.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsTValue()
|
||||
{
|
||||
var ind = new Edcf(5);
|
||||
var result = ind.Update(new TValue(DateTime.UtcNow, 50.0));
|
||||
Assert.IsType<TValue>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Last_IsAccessible()
|
||||
{
|
||||
var ind = new Edcf(5);
|
||||
ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(100.0, ind.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ContainsLength()
|
||||
{
|
||||
var ind = new Edcf(10);
|
||||
Assert.Contains("10", ind.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ConvergesToSMA()
|
||||
{
|
||||
// When all prices are equal, EDCF = SMA = constant value
|
||||
var ind = new Edcf(5);
|
||||
double constVal = 42.0;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), constVal));
|
||||
}
|
||||
Assert.Equal(constVal, ind.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RisingInput_ProducesFiniteOutput()
|
||||
{
|
||||
// With a linear trend, EDCF produces a finite weighted average within window range
|
||||
var ind = new Edcf(5);
|
||||
double[] vals = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
|
||||
TValue last = default;
|
||||
for (int i = 0; i < vals.Length; i++)
|
||||
{
|
||||
last = ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), vals[i]));
|
||||
}
|
||||
// EDCF output should be finite and within the window range [6, 10]
|
||||
Assert.True(double.IsFinite(last.Value), $"EDCF should be finite, got {last.Value}");
|
||||
Assert.True(last.Value >= 6.0 && last.Value <= 10.0,
|
||||
$"EDCF {last.Value} should be within window range [6, 10]");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// C) State + Bar Correction (critical)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void IsNew_True_AdvancesState()
|
||||
{
|
||||
var ind = new Edcf(3);
|
||||
ind.Update(new TValue(DateTime.UtcNow, 10.0), isNew: true);
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 20.0), isNew: true);
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 30.0), isNew: true);
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_Rewrites()
|
||||
{
|
||||
var ind = new Edcf(3);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 10.0 + i));
|
||||
}
|
||||
double before = ind.Last.Value;
|
||||
|
||||
// Update same bar with different value
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(4), 50.0), isNew: false);
|
||||
double after = ind.Last.Value;
|
||||
|
||||
Assert.NotEqual(before, after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_Restore()
|
||||
{
|
||||
var ind = new Edcf(3);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var data = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
ind.Update(series[i]);
|
||||
}
|
||||
double originalValue = ind.Last.Value;
|
||||
|
||||
// Feed corrections with isNew=false
|
||||
ind.Update(new TValue(DateTime.UtcNow, 200), isNew: false);
|
||||
ind.Update(new TValue(DateTime.UtcNow, 300), isNew: false);
|
||||
|
||||
// Restore with original last value
|
||||
ind.Update(series[^1], isNew: false);
|
||||
double restoredValue = ind.Last.Value;
|
||||
|
||||
Assert.Equal(originalValue, restoredValue, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var ind = new Edcf(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
Assert.True(ind.IsHot);
|
||||
|
||||
ind.Reset();
|
||||
Assert.False(ind.IsHot);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// D) Warmup / Convergence
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FlipsAtLength()
|
||||
{
|
||||
var ind = new Edcf(5);
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 10.0 * (i + 1)));
|
||||
Assert.False(ind.IsHot);
|
||||
}
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(4), 50.0));
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesLength()
|
||||
{
|
||||
var ind = new Edcf(10);
|
||||
Assert.Equal(10, ind.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// E) Robustness (critical)
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void NaN_UsesLastValid()
|
||||
{
|
||||
var ind = new Edcf(3);
|
||||
ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 200.0));
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 300.0));
|
||||
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(3), double.NaN));
|
||||
Assert.True(double.IsFinite(ind.Last.Value), "NaN should be replaced with last-valid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_UsesLastValid()
|
||||
{
|
||||
var ind = new Edcf(3);
|
||||
ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 200.0));
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 300.0));
|
||||
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(3), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(ind.Last.Value), "Infinity should be replaced with last-valid");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchNaN_Safe()
|
||||
{
|
||||
double[] data = [1.0, 2.0, double.NaN, 4.0, 5.0, double.NaN, 7.0, 8.0];
|
||||
var series = CreateSeries(data);
|
||||
var result = Edcf.Batch(series, 3);
|
||||
foreach (var tv in result)
|
||||
{
|
||||
Assert.True(double.IsFinite(tv.Value), $"Value at {tv.Time} was not finite: {tv.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// F) Consistency (critical) — All 4 modes match
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void AllModes_Match()
|
||||
{
|
||||
int length = 5;
|
||||
int count = 30;
|
||||
var data = _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
|
||||
// Mode 1: Streaming (Update one at a time)
|
||||
var streaming = new Edcf(length);
|
||||
var streamResults = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
// Mode 2: Batch (TSeries)
|
||||
var batchResults = Edcf.Batch(series, length);
|
||||
|
||||
// Mode 3: Span
|
||||
double[] srcValues = series.Values.ToArray();
|
||||
double[] spanResults = new double[series.Count];
|
||||
Edcf.Batch(srcValues.AsSpan(), spanResults.AsSpan(), length);
|
||||
|
||||
// Mode 4: Event-based
|
||||
var pubSource = new TSeries();
|
||||
var eventInd = new Edcf(pubSource, length);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
|
||||
// Compare modes 1, 2, 3
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults[i].Value, Tolerance);
|
||||
Assert.Equal(streamResults[i], spanResults[i], Tolerance);
|
||||
}
|
||||
Assert.Equal(streamResults[^1], eventInd.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// G) Span API Tests
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Span_ValidatesDestinationLength()
|
||||
{
|
||||
double[] src = [1, 2, 3, 4, 5];
|
||||
double[] dst = new double[3]; // too short
|
||||
var ex = Assert.Throws<ArgumentException>(() =>
|
||||
Edcf.Batch(src.AsSpan(), dst.AsSpan(), 3));
|
||||
Assert.Equal("destination", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Span_ValidatesLength()
|
||||
{
|
||||
double[] src = [1, 2, 3, 4, 5];
|
||||
double[] dst = new double[5];
|
||||
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Edcf.Batch(src.AsSpan(), dst.AsSpan(), 1));
|
||||
Assert.Equal("length", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Span_MatchesTSeries()
|
||||
{
|
||||
int length = 5;
|
||||
var data = _gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
|
||||
var batchResult = Edcf.Batch(series, length);
|
||||
|
||||
double[] srcVals = series.Values.ToArray();
|
||||
double[] spanResult = new double[series.Count];
|
||||
Edcf.Batch(srcVals.AsSpan(), spanResult.AsSpan(), length);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, spanResult[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Span_HandlesNaN()
|
||||
{
|
||||
double[] src = [1.0, 2.0, double.NaN, 4.0, 5.0, 6.0, 7.0, 8.0];
|
||||
double[] dst = new double[src.Length];
|
||||
Edcf.Batch(src.AsSpan(), dst.AsSpan(), 3);
|
||||
foreach (double v in dst)
|
||||
{
|
||||
Assert.True(double.IsFinite(v), $"Span result was not finite: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Span_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
int size = 10_000;
|
||||
var data = _gbm.Fetch(size, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
double[] src = data.Close.Values.ToArray();
|
||||
double[] dst = new double[size];
|
||||
|
||||
var ex = Record.Exception(() => Edcf.Batch(src.AsSpan(), dst.AsSpan(), 15));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// H) Chainability
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Pub_FiresOnUpdate()
|
||||
{
|
||||
var ind = new Edcf(3);
|
||||
int fireCount = 0;
|
||||
ind.Pub += (object? _, in TValueEventArgs _) => fireCount++;
|
||||
|
||||
ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(1, fireCount);
|
||||
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 200.0));
|
||||
Assert.Equal(2, fireCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventChaining_Works()
|
||||
{
|
||||
var source = new Edcf(3);
|
||||
var chained = new Edcf(source, 3);
|
||||
|
||||
source.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
source.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 200.0));
|
||||
source.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 300.0));
|
||||
|
||||
Assert.True(double.IsFinite(chained.Last.Value));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
// I) EDCF-specific: SMA Degeneracy
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void FlatPrices_DegeneratesToSMA()
|
||||
{
|
||||
// Per Ehlers: when all prices are the same,
|
||||
// all distance coefficients are equal → SMA
|
||||
var ind = new Edcf(5);
|
||||
double[] flat = [50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0];
|
||||
for (int i = 0; i < flat.Length; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), flat[i]));
|
||||
}
|
||||
// All coefficients are 0, fallback to current price
|
||||
Assert.Equal(50.0, ind.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StepFunction_RespondsQuickly()
|
||||
{
|
||||
// Step from 100 to 200 — EDCF should respond faster than SMA
|
||||
var edcf = new Edcf(5);
|
||||
double[] data = [100, 100, 100, 100, 100, 200, 200, 200, 200, 200];
|
||||
TValue lastEdcf = default;
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
lastEdcf = edcf.Update(new TValue(DateTime.UtcNow.AddMinutes(i), data[i]));
|
||||
}
|
||||
// After full window of 200s, should converge to 200
|
||||
Assert.Equal(200.0, lastEdcf.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultAndIndicator()
|
||||
{
|
||||
var data = _gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
var (results, indicator) = Edcf.Calculate(series, 5);
|
||||
Assert.Equal(30, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_InitializesState()
|
||||
{
|
||||
var ind = new Edcf(5);
|
||||
double[] data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
ind.Prime(data.AsSpan());
|
||||
Assert.True(ind.IsHot);
|
||||
Assert.True(double.IsFinite(ind.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_DoesNotThrow()
|
||||
{
|
||||
var ind = new Edcf(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), i * 10.0));
|
||||
}
|
||||
var ex = Record.Exception(() => ind.Dispose());
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Self-consistency validation tests for the EDCF indicator.
|
||||
/// No external library implements EDCF, so validation uses:
|
||||
/// - All-modes consistency (streaming == batch == span)
|
||||
/// - SMA degeneracy (flat prices → SMA behavior)
|
||||
/// - Constant convergence (constant input → constant output)
|
||||
/// - Smoothing behavior (longer length = smoother output)
|
||||
/// - Determinism (identical inputs → identical outputs)
|
||||
/// - Mathematical properties (weighted average bounds)
|
||||
/// </summary>
|
||||
public class EdcfValidationTests
|
||||
{
|
||||
private const double Tolerance = 1e-9;
|
||||
|
||||
[Fact]
|
||||
public void AllModes_AreConsistent()
|
||||
{
|
||||
int length = 7;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var data = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
|
||||
// Streaming
|
||||
var streaming = new Edcf(length);
|
||||
var streamResults = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = Edcf.Batch(series, length);
|
||||
|
||||
// Span
|
||||
double[] srcVals = series.Values.ToArray();
|
||||
double[] spanResults = new double[series.Count];
|
||||
Edcf.Batch(srcVals.AsSpan(), spanResults.AsSpan(), length);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults[i].Value, Tolerance);
|
||||
Assert.Equal(streamResults[i], spanResults[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchAndStreaming_Match()
|
||||
{
|
||||
int length = 10;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.3, seed: 77);
|
||||
var data = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
|
||||
// Streaming
|
||||
var streaming = new Edcf(length);
|
||||
var streamResults = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamResults[i] = streaming.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = Edcf.Batch(series, length);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResults[i], batchResults[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_ProducesConstantOutput()
|
||||
{
|
||||
double constVal = 77.5;
|
||||
var ind = new Edcf(10);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
var result = ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), constVal));
|
||||
// After first bar, output should always be the constant
|
||||
Assert.Equal(constVal, result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LongerLength_SmoothsMore()
|
||||
{
|
||||
// Longer length should produce smoother output (lower bar-to-bar change variance)
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.1, sigma: 0.3, seed: 99);
|
||||
var data = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
|
||||
var shortEdcf = new Edcf(3);
|
||||
var longEdcf = new Edcf(15);
|
||||
|
||||
var shortResults = new double[series.Count];
|
||||
var longResults = new double[series.Count];
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
shortResults[i] = shortEdcf.Update(series[i]).Value;
|
||||
longResults[i] = longEdcf.Update(series[i]).Value;
|
||||
}
|
||||
|
||||
// Compute first-difference variance (smoothness measure)
|
||||
// Smoother signal = lower first-difference variance
|
||||
int start = 20; // skip warmup
|
||||
double shortDiffVar = 0, longDiffVar = 0;
|
||||
int n = series.Count - start - 1;
|
||||
for (int i = start; i < series.Count - 1; i++)
|
||||
{
|
||||
double sd = shortResults[i + 1] - shortResults[i];
|
||||
shortDiffVar += sd * sd;
|
||||
double ld = longResults[i + 1] - longResults[i];
|
||||
longDiffVar += ld * ld;
|
||||
}
|
||||
shortDiffVar /= n;
|
||||
longDiffVar /= n;
|
||||
|
||||
// Longer filter should have lower first-difference variance (smoother)
|
||||
Assert.True(longDiffVar < shortDiffVar,
|
||||
$"Long diff-var {longDiffVar} should be less than short diff-var {shortDiffVar}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Determinism_IdenticalInputs_IdenticalOutputs()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 55);
|
||||
var data = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
|
||||
var ind1 = new Edcf(8);
|
||||
var ind2 = new Edcf(8);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var r1 = ind1.Update(series[i]);
|
||||
var r2 = ind2.Update(series[i]);
|
||||
Assert.Equal(r1.Value, r2.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Output_BoundedByInputRange()
|
||||
{
|
||||
// EDCF is a weighted average → output must be within input range (once warm)
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.15, seed: 33);
|
||||
var data = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
int length = 5;
|
||||
|
||||
var ind = new Edcf(length);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var result = ind.Update(series[i]);
|
||||
if (i >= length)
|
||||
{
|
||||
// Find min/max of the last 'length' source values
|
||||
double min = double.MaxValue, max = double.MinValue;
|
||||
for (int j = Math.Max(0, i - length + 1); j <= i; j++)
|
||||
{
|
||||
if (series[j].Value < min) { min = series[j].Value; }
|
||||
if (series[j].Value > max) { max = series[j].Value; }
|
||||
}
|
||||
// Allow small tolerance for floating-point
|
||||
Assert.True(result.Value >= min - 1e-6 && result.Value <= max + 1e-6,
|
||||
$"EDCF {result.Value} outside [{min}, {max}] at bar {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_NeverPropagates()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 11);
|
||||
var data = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
var ind = new Edcf(5);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
double val = (i % 7 == 3) ? double.NaN : series[i].Value;
|
||||
var result = ind.Update(new TValue(series[i].Time, val));
|
||||
Assert.True(double.IsFinite(result.Value), $"NaN propagated at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeDataset_Stable()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.1, seed: 22);
|
||||
var data = gbm.Fetch(5000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
var ind = new Edcf(15);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var result = ind.Update(series[i]);
|
||||
Assert.True(double.IsFinite(result.Value), $"Non-finite at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentLengths_AllValid()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 44);
|
||||
var data = gbm.Fetch(40, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = data.Close;
|
||||
|
||||
int[] lengths = [2, 3, 5, 10, 15, 20];
|
||||
foreach (int len in lengths)
|
||||
{
|
||||
var ind = new Edcf(len);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var result = ind.Update(series[i]);
|
||||
Assert.True(double.IsFinite(result.Value),
|
||||
$"Non-finite at bar {i} with length {len}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// EDCF: Ehlers Distance Coefficient Filter
|
||||
/// A nonlinear adaptive FIR filter where coefficients are computed as the sum of
|
||||
/// squared price differences across the observation window. When prices are flat,
|
||||
/// all coefficients are equal (degenerates to SMA). When prices shift rapidly,
|
||||
/// higher weights are assigned to samples with greater price movement.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Reference: John F. Ehlers, "Ehlers Filters" (MESA Software)
|
||||
/// John F. Ehlers, "Nonlinear Ehlers Filters" (S&C V.19:4, pp.25-34)
|
||||
///
|
||||
/// Algorithm:
|
||||
/// For each sample position i in [0, Length-1]:
|
||||
/// Distance2[i] = Σ (Price[i] - Price[i + k])² for k = 1 to Length-1
|
||||
/// Coef[i] = Distance2[i]
|
||||
/// Filter = Σ(Coef[i] × Price[i]) / Σ(Coef[i])
|
||||
///
|
||||
/// Complexity: O(n²) per update where n = Length (nested loop over window)
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Edcf : AbstractBase
|
||||
{
|
||||
private readonly int _length;
|
||||
private readonly RingBuffer _buffer;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double LastValid, int Count);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <param name="length">Filter window length (≥ 2). Default: 15.</param>
|
||||
public Edcf(int length = 15)
|
||||
{
|
||||
if (length < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "Length must be greater than or equal to 2.");
|
||||
}
|
||||
|
||||
_length = length;
|
||||
// Need length samples for the window + (length-1) lookback = 2*length - 1 total
|
||||
// But the inner loop looks back within the same window, so we only need 'length' samples
|
||||
// However, the EasyLanguage code accesses Price[count + LookBack] where count goes to Length-1
|
||||
// and LookBack goes to Length-1, so max index = 2*(Length-1). We need 2*Length - 1 in the buffer.
|
||||
_buffer = new RingBuffer(2 * length - 1);
|
||||
WarmupPeriod = length;
|
||||
Name = $"Edcf({_length})";
|
||||
}
|
||||
|
||||
/// <param name="source">Input data source for event-based chaining.</param>
|
||||
/// <param name="length">Filter window length (≥ 2). Default: 15.</param>
|
||||
public Edcf(ITValuePublisher source, int length = 15) : this(length)
|
||||
{
|
||||
source.Pub += Handle;
|
||||
}
|
||||
|
||||
public override bool IsHot => _s.Count >= _length;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Handle(object? sender, in TValueEventArgs args)
|
||||
{
|
||||
Update(args.Value, args.IsNew);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// State management: save/restore for bar correction
|
||||
// skipcq: CS-R1140 - EDCF reads individual buffer positions so cannot use
|
||||
// Snapshot/Restore (which only saves one slot); UpdateNewest replaces in-place
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
// NaN/Infinity guard: substitute last-valid
|
||||
double value = input.Value;
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
value = double.IsFinite(s.LastValid) ? s.LastValid : 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastValid = value;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_buffer.Add(value);
|
||||
s.Count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
_buffer.UpdateNewest(value);
|
||||
}
|
||||
|
||||
double result;
|
||||
int available = Math.Min(s.Count, _length);
|
||||
|
||||
if (available < 2)
|
||||
{
|
||||
result = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = CalcDistanceFilter(available);
|
||||
}
|
||||
|
||||
_s = s;
|
||||
|
||||
var ret = new TValue(input.Time, result);
|
||||
Last = ret;
|
||||
PubEvent(ret, isNew);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
TSeries result = [];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
result.Add(Update(source[i]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalcDistanceFilter(int windowLen)
|
||||
{
|
||||
// Ehlers Distance Coefficient Filter
|
||||
// For each sample position i in [0, windowLen-1]:
|
||||
// Distance2[i] = Σ(Price[i] - Price[i + k])² for k = 1 to windowLen-1
|
||||
// Coef[i] = Distance2[i]
|
||||
// Filter = Σ(Coef[i] * Price[i]) / Σ(Coef[i])
|
||||
//
|
||||
// Buffer indexing: _buffer[^1] = newest (Price[0] in EasyLanguage)
|
||||
// _buffer[^2] = Price[1], etc.
|
||||
|
||||
double sumCoef = 0.0;
|
||||
double num = 0.0;
|
||||
|
||||
int bufCount = _buffer.Count;
|
||||
|
||||
for (int i = 0; i < windowLen; i++)
|
||||
{
|
||||
double dist2 = 0.0;
|
||||
double priceI = _buffer[bufCount - 1 - i];
|
||||
|
||||
for (int k = 1; k < windowLen; k++)
|
||||
{
|
||||
int idx = i + k;
|
||||
if (idx >= bufCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
double priceK = _buffer[bufCount - 1 - idx];
|
||||
double diff = priceI - priceK;
|
||||
dist2 += diff * diff;
|
||||
}
|
||||
|
||||
sumCoef += dist2;
|
||||
num += dist2 * priceI;
|
||||
}
|
||||
|
||||
return sumCoef > 1e-10 ? num / sumCoef : _buffer[bufCount - 1];
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int length = 15)
|
||||
{
|
||||
var indicator = new Edcf(length);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> destination, int length = 15)
|
||||
{
|
||||
if (destination.Length < source.Length)
|
||||
{
|
||||
throw new ArgumentException("Destination span is shorter than source span.", nameof(destination));
|
||||
}
|
||||
if (length < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "Length must be greater than or equal to 2.");
|
||||
}
|
||||
|
||||
var filter = new Edcf(length);
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
destination[i] = filter.Update(new TValue(0, source[i])).Value;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_s = default;
|
||||
_ps = default;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
long initialTicks = DateTime.UtcNow.Ticks - source.Length * (step?.Ticks ?? TimeSpan.FromSeconds(1).Ticks);
|
||||
TimeSpan increment = step ?? TimeSpan.FromSeconds(1);
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(initialTicks + i * increment.Ticks, source[i]));
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Edcf Indicator) Calculate(TSeries source, int length = 15)
|
||||
{
|
||||
var indicator = new Edcf(length);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_buffer.Clear();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
# EDCF — Ehlers Distance Coefficient Filter
|
||||
|
||||
## Overview
|
||||
|
||||
The **Ehlers Distance Coefficient Filter (EDCF)** is a nonlinear adaptive FIR filter created by John F. Ehlers. Unlike traditional moving averages with fixed or linearly-varying weights, EDCF computes its coefficients dynamically based on the sum of squared price differences across the observation window. This makes the filter highly responsive to price changes while degenerating to a Simple Moving Average when prices are flat.
|
||||
|
||||
## Origin
|
||||
|
||||
- **Author:** John F. Ehlers
|
||||
- **Source:** "Ehlers Filters" (MESA Software); "Nonlinear Ehlers Filters" (Stocks & Commodities V.19:4, pp.25-34)
|
||||
- **Category:** Filters (nonlinear FIR)
|
||||
|
||||
## Algorithm
|
||||
|
||||
For a window of `Length` samples, the filter computes:
|
||||
|
||||
1. **Distance-squared coefficient** for each sample position `i`:
|
||||
|
||||
```
|
||||
Distance2[i] = Σ (Price[i] - Price[i + k])² for k = 1 to Length-1
|
||||
```
|
||||
|
||||
2. **Normalized weighted average**:
|
||||
|
||||
```
|
||||
EDCF = Σ(Distance2[i] × Price[i]) / Σ(Distance2[i])
|
||||
```
|
||||
|
||||
### Key Properties
|
||||
|
||||
| Property | Behavior |
|
||||
|----------|----------|
|
||||
| **Flat prices** | All coefficients are zero → fallback to current price (SMA-like) |
|
||||
| **Trending prices** | Recent samples with large price changes get higher weights → faster response |
|
||||
| **Step function** | Responds much faster than SMA to abrupt price changes |
|
||||
| **Sum of squares** | Uses `Σ(diff²)` instead of `√(Σ(diff²))` to heighten filter response (per Ehlers) |
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Range | Description |
|
||||
|-----------|------|---------|-------|-------------|
|
||||
| `length` | int | 15 | ≥ 2 | Filter window length. Larger = smoother but more lag. |
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
// Streaming
|
||||
var edcf = new Edcf(15);
|
||||
foreach (var bar in data)
|
||||
{
|
||||
TValue result = edcf.Update(bar);
|
||||
}
|
||||
|
||||
// Batch
|
||||
TSeries results = Edcf.Batch(series, 15);
|
||||
|
||||
// Span
|
||||
Edcf.Batch(sourceSpan, destinationSpan, 15);
|
||||
|
||||
// Calculate (returns both results and indicator)
|
||||
var (results, indicator) = Edcf.Calculate(series, 15);
|
||||
|
||||
// Event-based chaining
|
||||
var source = new Ema(period: 10);
|
||||
var edcf = new Edcf(source, 15);
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
AbstractBase (ITValuePublisher, IDisposable)
|
||||
└── Edcf (sealed)
|
||||
├── RingBuffer[2*length-1] — price history window
|
||||
├── State record struct — minimal state (LastValid, Count)
|
||||
├── CalcDistanceFilter() — O(n²) nested loop computation
|
||||
└── Snapshot/Restore — bar correction support
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
- **Record struct** with `LastValid` and `Count` fields
|
||||
- **Snapshot/Restore** via `_ps`/`_s` swap + `RingBuffer.Snapshot()`/`Restore()` for bar correction
|
||||
- **No SIMD** possible due to data-dependent coefficient computation
|
||||
|
||||
### Complexity
|
||||
|
||||
| Operation | Complexity |
|
||||
|-----------|-----------|
|
||||
| Per-bar update | O(n²) where n = Length |
|
||||
| Memory | O(n) — single RingBuffer |
|
||||
| Warmup | `Length` bars |
|
||||
|
||||
## Quality Metrics
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| WarmupPeriod | `Length` |
|
||||
| NaN/Infinity handling | Substitutes last-valid value |
|
||||
| Bar correction | Full save/restore via Snapshot |
|
||||
| Mode consistency | Streaming = Batch = Span |
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **O(n²) complexity**: For large `Length` values (> 50), the nested loop becomes expensive. Consider keeping Length ≤ 30 for real-time use.
|
||||
2. **All-zero coefficients**: When all prices in the window are identical, all distance-squared coefficients are zero. The implementation falls back to the current price.
|
||||
3. **Not an IIR filter**: Despite being classified under Filters, EDCF is a nonlinear FIR filter — it uses a finite observation window with no feedback.
|
||||
4. **Asymmetric response**: In a strong trend, the filter weights recent bars heavily, creating trailing-stop-like behavior. In ranges, it approximates SMA.
|
||||
|
||||
## See Also
|
||||
|
||||
- [Wiener Filter](../wiener/Wiener.md) — adaptive noise-reduction filter
|
||||
- [Laguerre Filter](../laguerre/Laguerre.md) — Ehlers IIR filter with gamma damping
|
||||
- [LMS Filter](../lms/Lms.md) — Least Mean Squares adaptive filter
|
||||
|
||||
## References
|
||||
|
||||
1. Ehlers, J. F. "Ehlers Filters." MESA Software. [PDF](https://www.mesasoftware.com/papers/EhlersFilters.pdf)
|
||||
2. Ehlers, J. F. "Nonlinear Ehlers Filters." *Stocks & Commodities*, V.19:4, pp.25-34.
|
||||
@@ -0,0 +1,65 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("EDCF: Ehlers Distance Coefficient Filter", "EDCF", overlay = true)
|
||||
|
||||
//@function Calculates Ehlers Distance Coefficient Filter using distance-weighted FIR
|
||||
//@param source Series to calculate EDCF from
|
||||
//@param length Filter window length (>= 2)
|
||||
//@returns Distance-weighted filter value
|
||||
//@optimized Uses distance-squared coefficients with O(n²) complexity per bar
|
||||
|
||||
// ——— Inputs ———
|
||||
int p_length = input.int(15, "Length", minval = 2, maxval = 50,
|
||||
tooltip = "Filter window length. Larger = smoother but more lag.")
|
||||
string p_source = input.string("HL2", "Source",
|
||||
options = ["Close", "HL2", "HLC3", "OHLC4", "Open", "High", "Low"])
|
||||
|
||||
// ——— Source selector ———
|
||||
float src = switch p_source
|
||||
"Close" => close
|
||||
"HL2" => hl2
|
||||
"HLC3" => hlc3
|
||||
"OHLC4" => ohlc4
|
||||
"Open" => open
|
||||
"High" => high
|
||||
"Low" => low
|
||||
|
||||
// ——— Distance Coefficient Filter ———
|
||||
// Reference: John F. Ehlers, "Ehlers Filters" (MESA Software)
|
||||
// John F. Ehlers, "Nonlinear Ehlers Filters" (S&C V.19:4, pp.25-34)
|
||||
//
|
||||
// Algorithm:
|
||||
// For each sample position i in [0, Length-1]:
|
||||
// Distance2[i] = Σ (Price[i] - Price[i + k])^2 for k = 1 to Length-1
|
||||
// Coef[i] = Distance2[i]
|
||||
// Filter = Σ(Coef[i] * Price[i]) / Σ(Coef[i])
|
||||
//
|
||||
// When prices are flat, all coefficients are equal → degenerates to SMA.
|
||||
// When prices shift rapidly, distant points get higher weights → faster response.
|
||||
// Uses sum of squares (not sqrt of sum of squares) to heighten filter response.
|
||||
|
||||
edcf(float source, int length) =>
|
||||
float sumCoef = 0.0
|
||||
float num = 0.0
|
||||
|
||||
for count = 0 to length - 1
|
||||
// Compute distance-squared coefficient for this sample position
|
||||
float dist2 = 0.0
|
||||
for lookBack = 1 to length - 1
|
||||
float diff = source[count] - source[count + lookBack]
|
||||
dist2 += diff * diff
|
||||
// The distance-squared value IS the coefficient
|
||||
float coef = dist2
|
||||
sumCoef += coef
|
||||
num += coef * source[count]
|
||||
|
||||
// Normalized weighted average; fallback to current price if all coefficients zero
|
||||
float result = sumCoef != 0.0 ? num / sumCoef : source
|
||||
result
|
||||
|
||||
// ——— Compute ———
|
||||
float filt = edcf(src, p_length)
|
||||
|
||||
// ——— Plot ———
|
||||
plot(filt, "EDCF", color = color.yellow, linewidth = 2)
|
||||
Reference in New Issue
Block a user