feat(filters): add NET - Ehlers Noise Elimination Technology (TASC Dec 2020)

This commit is contained in:
Miha Kralj
2026-03-17 14:38:31 -07:00
parent f7dcc20f7c
commit 8112009b32
14 changed files with 1092 additions and 0 deletions
+1
View File
@@ -106,6 +106,7 @@
* [LMS - Least Mean Squares](/lib/filters/lms/Lms.md)
* [LOESS - LOESS Smoothing](/lib/filters/loess/Loess.md)
* [MODF - Modular Filter](/lib/filters/modf/Modf.md)
* [NET - Ehlers Noise Elimination Technology](/lib/filters/net/Net.md)
* [NOTCH - Notch Filter](/lib/filters/notch/Notch.md)
* [NW - Nadaraya-Watson Estimator](/lib/filters/nw/Nw.md)
* [ONEEURO - One Euro Filter](/lib/filters/oneeuro/OneEuro.md)
+1
View File
@@ -144,6 +144,7 @@ Signal processing filters adapted for financial time series. Designed to separat
| [**LMS**](../lib/filters/lms/Lms.md) | Least Mean Squares | Widrow-Hoff adaptive FIR filter |
| [**LOESS**](../lib/filters/loess/Loess.md) | LOESS Smoothing | Local polynomial regression |
| [**MODF**](../lib/filters/modf/Modf.md) | Modular Filter | Dual-path adaptive filter with state selection |
| [**NET**](../lib/filters/net/Net.md) | Ehlers Noise Elimination Technology | Kendall Tau-a rank correlation for noise elimination |
| [**NOTCH**](../lib/filters/notch/Notch.md) | Notch Filter | Single frequency rejection |
| [**NW**](../lib/filters/nw/Nw.md) | Nadaraya-Watson Estimator | Non-parametric Gaussian kernel regression smoothing |
| [**ONEEURO**](../lib/filters/oneeuro/OneEuro.md) | One Euro Filter | Speed-adaptive low-pass, adaptive cutoff |
+1
View File
@@ -172,6 +172,7 @@ These are the heavy artillery. Kalman filters, Butterworth filters, wavelets. If
| LMS | Least Mean Squares | [lms.pine](../lib/filters/lms/lms.pine) |
| LOESS | LOESS Smoothing | [loess.pine](../lib/filters/loess/loess.pine) |
| MODF | Modular Filter | [modf.pine](../lib/filters/modf/modf.pine) |
| NET | Ehlers Noise Elimination Technology | [net.pine](../lib/filters/net/net.pine) |
| NOTCH | Notch Filter | [notch.pine](../lib/filters/notch/notch.pine) |
| NW | Nadaraya-Watson Estimator | [nw.pine](../lib/filters/nw/nw.pine) |
| ONEEURO | One Euro Filter | [oneeuro.pine](../lib/filters/oneeuro/oneeuro.pine) |
+1
View File
@@ -243,6 +243,7 @@
| [MSLE](errors/msle/Msle.md) | Mean Squared Log Error | Errors |
| [MSTOCH](oscillators/mstoch/Mstoch.md) | Ehlers MESA Stochastic | Oscillators |
| [NATR](volatility/natr/Natr.md) | Normalized ATR | Volatility |
| [NET](filters/net/Net.md) | Ehlers Noise Elimination Technology | Filters |
| [NLMA](trends_FIR/nlma/Nlma.md) | Non-Lag Moving Average | Trends (FIR) |
| [NMA](trends_IIR/nma/Nma.md) | Natural Moving Average | Trends (IIR) |
| [NORMDIST](numerics/normdist/Normdist.md) | Normal Distribution | Numerics |
+1
View File
@@ -28,6 +28,7 @@ Signal processing filters adapted for financial time series. These are not indic
| [LMS](lms/Lms.md) | Least Mean Squares | Widrow-Hoff adaptive FIR. NLMS weight update. O(order) per bar. |
| [LOESS](loess/Loess.md) | LOESS Smoothing | Local polynomial regression. Robust to outliers. |
| [MODF](modf/Modf.md) | Modular Filter | Dual-path adaptive filter with upper/lower EMA bands and state selection. |
| [NET](net/Net.md) | Ehlers Noise Elimination Technology | Kendall Tau-a rank correlation for noise/trend separation. Bounded [-1, +1]. |
| [NOTCH](notch/Notch.md) | Notch Filter | Band-stop. Removes specific frequency (e.g., 60 Hz noise). |
| [NW](nw/Nw.md) | Nadaraya-Watson Kernel Regression | Non-parametric kernel regression smoothing. Bandwidth-adaptive. |
| [ONEEURO](oneeuro/OneEuro.md) | One Euro Filter | Speed-adaptive low-pass. Adaptive cutoff from signal derivative. |
+53
View File
@@ -0,0 +1,53 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class NetIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 2, 100, 1, 0)]
public int Period { get; set; } = 14;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Net _net = null!;
private readonly LineSeries _series;
private Func<IHistoryItem, double> _priceSelector = null!;
public static int MinHistoryDepths => 2;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"NET({Period}):{Source}";
public NetIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "NET - Ehlers Noise Elimination Technology";
Description = "Kendall Tau-a rank correlation for noise elimination";
_series = new LineSeries(name: $"NET {Period}", color: IndicatorExtensions.Oscillators, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
protected override void OnInit()
{
_priceSelector = Source.GetPriceSelector();
_net = new Net(Period);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
bool isNew = args.IsNewBar();
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
double value = _net.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value;
_series.SetValue(value, _net.IsHot, ShowColdValues);
}
}
+228
View File
@@ -0,0 +1,228 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// NET: Ehlers Noise Elimination Technology
/// Applies Kendall Tau-a rank correlation to the input series over a rolling window.
/// Positive output means the series is trending up (concordant pairs dominate);
/// negative means trending down. Output is bounded [-1, +1].
/// </summary>
/// <remarks>
/// Reference: John F. Ehlers, "Noise Elimination Technology" (TASC, December 2020)
///
/// Algorithm:
/// Store last 'period' values in buffer (newest at index Count-1).
/// For each pair (i, k) where i &gt; k (i is older index, k is newer index):
/// Num -= Sign(older - newer)
/// Denom = 0.5 × period × (period - 1)
/// NET = Num / Denom
///
/// Complexity: O(n²) per update where n = period (nested pairwise comparison)
/// No IIR state — purely FIR/windowed from RingBuffer.
/// </remarks>
[SkipLocalsInit]
public sealed class Net : AbstractBase
{
private readonly int _period;
private readonly double _denomRecip;
private readonly RingBuffer _buffer;
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValid, int Count);
private State _s;
private State _ps;
/// <summary>
/// Initializes an Ehlers Noise Elimination Technology indicator.
/// </summary>
/// <param name="period">Lookback window for Kendall tau (≥ 2). Default: 14.</param>
public Net(int period = 14)
{
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2.");
}
_period = period;
_denomRecip = 1.0 / (0.5 * period * (period - 1));
_buffer = new RingBuffer(period);
WarmupPeriod = period;
Name = $"Net({_period})";
}
/// <summary>
/// Initializes a NET indicator and subscribes it to a source publisher.
/// </summary>
/// <param name="source">Input data source for event-based chaining.</param>
/// <param name="period">Lookback window for Kendall tau (≥ 2). Default: 14.</param>
public Net(ITValuePublisher source, int period = 14) : this(period)
{
source.Pub += Handle;
}
public override bool IsHot => _s.Count >= _period;
[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: direct buffer correction (no Snapshot/Restore)
// NET reads individual buffer positions, so Snapshot/Restore is unsafe.
// skipcq: CS-R1140
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, _period);
if (available < 2)
{
result = 0.0;
}
else
{
result = CalcKendallTau(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 CalcKendallTau(int windowLen)
{
// Kendall Tau-a: count concordant/discordant pairs
// Buffer: _buffer[0] = oldest, _buffer[Count-1] = newest
// Map to Ehlers X[]: X[0]=newest=_buffer[Count-1], X[i]=_buffer[Count-1-i]
//
// Ehlers loop: for i=1 to N-1, for k=0 to i-1: Num -= Sign(X[i] - X[k])
// X[i] is older than X[k] (i > k means further back in time)
// So: Num -= Sign(older - newer)
// Rising series: older < newer → Sign < 0 → -Sign > 0 → Num > 0 → positive tau
double num = 0.0;
int bufCount = _buffer.Count;
for (int i = 1; i < windowLen; i++)
{
double xi = _buffer[bufCount - 1 - i]; // older value (X[i])
for (int k = 0; k < i; k++)
{
double xk = _buffer[bufCount - 1 - k]; // newer value (X[k])
num -= Math.Sign(xi - xk);
}
}
double denom = 0.5 * windowLen * (windowLen - 1);
return num * (windowLen == _period ? _denomRecip : 1.0 / denom);
}
public static TSeries Batch(TSeries source, int period = 14)
{
var indicator = new Net(period);
return indicator.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> source, Span<double> destination, int period = 14)
{
if (destination.Length < source.Length)
{
throw new ArgumentException("Destination span is shorter than source span.", nameof(destination));
}
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 2.");
}
var filter = new Net(period);
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, Net Indicator) Calculate(TSeries source, int period = 14)
{
var indicator = new Net(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_buffer.Clear();
}
base.Dispose(disposing);
}
}
+99
View File
@@ -0,0 +1,99 @@
# NET: Ehlers Noise Elimination Technology
**NET** applies Kendall Tau-a rank correlation to a rolling window of the input series. It measures the degree of monotonic trend: +1 means perfectly rising, 1 means perfectly falling, 0 means no trend. Unlike Pearson correlation (used in CTI), Kendall tau is nonparametric and robust to outliers.
| Property | Value |
| :------------- | :--------------------------- |
| **Category** | Filters |
| **Author** | John F. Ehlers |
| **Source** | TASC, December 2020 |
| **Parameters** | period (int, default 14, ≥ 2) |
| **Output** | double, bounded [1, +1] |
| **Inputs** | Single series (Close, HL2, etc.) |
## Historical Context
Published in *Technical Analysis of Stocks & Commodities* (December 2020), "Noise Elimination Technology — Clarify Your Indicators Using Kendall Correlation." Ehlers applies rank-order statistics to filter noise from any indicator output or price series without adding lag (unlike smoothing filters).
## Architecture & Physics
### Kendall Tau-a Concordance
For a window of $N$ values $X_0$ (newest) through $X_{N-1}$ (oldest), compute all $\binom{N}{2}$ pairs:
$$\tau = \frac{\sum_{i>k} -\text{sgn}(X_i - X_k)}{\frac{N(N-1)}{2}}$$
Where $i$ indexes older values and $k$ indexes newer values. When the series is rising (newer > older), $\text{sgn}(X_i - X_k) < 0$, so $-\text{sgn} > 0$, yielding positive $\tau$.
### No IIR State
NET is purely FIR — the output depends only on the current window contents. No recursive state means:
- Zero floating-point drift
- Perfect reset/restart behavior
- Bar correction is trivial (just replace newest buffer value)
### Bounded Output
The denominator $\frac{N(N-1)}{2}$ equals the total number of pairs. The numerator can range from $-\frac{N(N-1)}{2}$ (all discordant) to $+\frac{N(N-1)}{2}$ (all concordant), so $\tau \in [-1, +1]$.
## Performance Profile
### Operation Count (Streaming Mode, Scalar)
| Operation | Count per bar |
| :-------------------- | :------------------- |
| Comparisons (Sign) | $\frac{N(N-1)}{2}$ |
| Subtractions | $\frac{N(N-1)}{2}$ |
| Accumulation | $\frac{N(N-1)}{2}$ |
| Final division | 1 multiply |
For $N = 14$: $\frac{14 \times 13}{2} = 91$ pair evaluations per bar.
### Batch Mode (SIMD Analysis)
Not SIMD-friendly: the inner loop has data-dependent branching (`Math.Sign`). The O(N²) nested loop structure prevents vectorization. For typical $N \leq 20$, the absolute cost is negligible.
### Quality Metrics
| Metric | Rating |
| :---------------------- | :----- |
| Lag (bars) | 0 (no smoothing applied) |
| Overshoot | None (bounded output) |
| Noise sensitivity | Low (rank-based, immune to outlier magnitudes) |
| Computational cost | O(N²) per bar |
| Memory | O(N) — one RingBuffer |
## Validation
Validated against mathematical properties of Kendall Tau-a:
- Perfectly rising sequence → $\tau = +1$
- Perfectly falling sequence → $\tau = -1$
- Constant input → $\tau = 0$
- Random input → $|\tau|$ small
- Bounded: all outputs $\in [-1, +1]$
### Behavioral Test Summary
| Test Category | Tests | Description |
| :--------------------- | :---- | :---------- |
| Constructor | 3 | Period validation, default values |
| Basic Calculation | 4 | Core algorithm correctness |
| State / Bar Correction | 4 | Rollback consistency |
| Warmup / Convergence | 3 | Cold → hot transition |
| Robustness | 3 | NaN, Infinity, edge cases |
| Consistency | 4 | All API modes match |
| Span API | 2 | ReadOnlySpan paths |
| Chainability | 2 | Event pipeline |
| NET-Specific | 8 | Kendall properties, boundary conditions |
## Common Pitfalls
1. **Period too large**: O(N²) cost grows quadratically. Keep $N \leq 50$ for real-time use.
2. **Not a smoother**: NET does not smooth the input — it measures monotonic trend strength. Use it to filter *decisions*, not to filter *price*.
3. **Ties**: Tau-a does not adjust for ties. In continuous financial data, exact ties are rare. If ties are common (e.g., rounded data), consider Tau-b.
4. **Zero during warmup**: Before the buffer fills, NET returns 0 (not NaN). Check `IsHot` for valid readings.
## References
- Ehlers, J.F. "Noise Elimination Technology." *Technical Analysis of Stocks & Commodities*, December 2020.
- Kendall, M.G. "A New Measure of Rank Correlation." *Biometrika*, 1938.
+58
View File
@@ -0,0 +1,58 @@
// Licensed under the Apache License, Version 2.0
// © mihakralj
//@version=6
indicator("NET: Ehlers Noise Elimination Technology", "NET", overlay = false)
//@function Calculates Ehlers Noise Elimination Technology using Kendall Tau-a
//@param source Series to calculate NET from
//@param period Lookback window for Kendall correlation (>= 2)
//@returns Kendall tau coefficient [-1, +1]
//@optimized O(n²) pairwise concordance/discordance per bar
// ——— Inputs ———
int p_period = input.int(14, "Period", minval = 2, maxval = 100,
tooltip = "Lookback window for Kendall tau. Larger = smoother but slower response.")
string p_source = input.string("Close", "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
// ——— Noise Elimination Technology (Kendall Tau-a) ———
// Reference: John F. Ehlers, "Noise Elimination Technology" (TASC, December 2020)
//
// Algorithm:
// Store last 'period' values: X[0]=current, X[1]=1 bar ago, ..., X[N-1]=oldest
// For each pair (i, k) where i > k (i is older, k is newer):
// Num -= Sign(X[i] - X[k])
// Denom = 0.5 * period * (period - 1)
// NET = Num / Denom
//
// When price is rising, newer values > older values → Sign(older-newer) < 0 → -Sign > 0 → NET positive
// When price is falling, newer values < older values → Sign(older-newer) > 0 → -Sign < 0 → NET negative
// Output bounded [-1, +1]: fraction of concordant minus discordant pairs
net(float source, int period) =>
float num = 0.0
for i = 1 to period - 1
for k = 0 to i - 1
num -= math.sign(source[i] - source[k])
float denom = 0.5 * period * (period - 1)
float result = denom > 0 ? num / denom : 0.0
result
// ——— Compute ———
float filt = net(src, p_period)
// ——— Plot ———
hline(0, "Zero", color = color.gray, linestyle = hline.style_dotted)
hline(0.5, "+0.5", color = color.new(color.green, 70), linestyle = hline.style_dotted)
hline(-0.5, "-0.5", color = color.new(color.red, 70), linestyle = hline.style_dotted)
plot(filt, "NET", color = filt >= 0 ? color.green : color.red, linewidth = 2)
@@ -0,0 +1,90 @@
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class NetIndicatorTests
{
[Fact]
public void Constructor_DefaultValues()
{
var indicator = new NetIndicator();
Assert.Equal(14, indicator.Period);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Contains("NET", indicator.Name, StringComparison.Ordinal);
Assert.Contains("Ehlers", indicator.Name, StringComparison.Ordinal);
}
[Fact]
public void MinHistoryDepths_Returns2()
{
Assert.Equal(2, NetIndicator.MinHistoryDepths);
}
[Fact]
public void ShortName_IncludesPeriodAndSource()
{
var indicator = new NetIndicator { Period = 20, Source = SourceType.High };
Assert.Equal("NET(20):High", indicator.ShortName);
}
[Fact]
public void ShortName_DefaultParams()
{
var indicator = new NetIndicator();
Assert.Equal("NET(14):Close", indicator.ShortName);
}
[Fact]
public void Initialize_CreatesInternalIndicator()
{
var indicator = new NetIndicator();
indicator.Period = 10;
Assert.NotNull(indicator);
}
[Fact]
public void ProcessUpdate_Historical()
{
var indicator = new NetIndicator();
Assert.True(indicator.Period >= 2);
}
[Fact]
public void ProcessUpdate_NewBar()
{
var indicator = new NetIndicator();
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void ProcessUpdate_Tick()
{
var indicator = new NetIndicator();
indicator.Period = 5;
Assert.Equal(5, indicator.Period);
}
[Fact]
public void SourceCodeLink_NotEmpty()
{
var indicator = new NetIndicator();
Assert.NotNull(indicator.Name);
Assert.NotEmpty(indicator.Name);
}
[Fact]
public void SeparateWindow_IsTrue()
{
var indicator = new NetIndicator();
Assert.NotNull(indicator);
}
[Fact]
public void CustomPeriod_Accepted()
{
var indicator = new NetIndicator { Period = 30 };
Assert.Equal(30, indicator.Period);
Assert.Equal("NET(30):Close", indicator.ShortName);
}
}
+539
View File
@@ -0,0 +1,539 @@
namespace QuanTAlib;
public class NetTests
{
private static TSeries MakeSeries(int count = 500)
{
var rng = new Random(42);
TSeries series = [];
for (int i = 0; i < count; i++)
{
series.Add(new TValue(DateTime.UtcNow.AddSeconds(i).Ticks, 100.0 + rng.NextDouble() * 10.0));
}
return series;
}
// ═══════════════════════════════════════════════════════════════════
// Constructor Tests
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Constructor_DefaultPeriod_Is14()
{
var net = new Net();
Assert.Equal("Net(14)", net.Name);
}
[Fact]
public void Constructor_CustomPeriod()
{
var net = new Net(period: 20);
Assert.Equal("Net(20)", net.Name);
}
[Fact]
public void Constructor_PeriodBelowMin_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Net(period: 1));
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(int.MinValue)]
public void Constructor_InvalidPeriods_Throw(int bad)
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Net(period: bad));
}
// ═══════════════════════════════════════════════════════════════════
// Basic Calculation Tests
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Calc_RisingSeries_PositiveOutput()
{
var net = new Net(period: 5);
TValue result = default;
for (int i = 1; i <= 10; i++)
{
result = net.Update(new TValue(i, i * 10.0));
}
Assert.True(result.Value > 0.0, $"Expected positive NET for rising series, got {result.Value}");
}
[Fact]
public void Calc_FallingSeries_NegativeOutput()
{
var net = new Net(period: 5);
TValue result = default;
for (int i = 1; i <= 10; i++)
{
result = net.Update(new TValue(i, 100.0 - i * 10.0));
}
Assert.True(result.Value < 0.0, $"Expected negative NET for falling series, got {result.Value}");
}
[Fact]
public void Calc_PerfectlyRising_ReturnsPositiveOne()
{
var net = new Net(period: 5);
TValue result = default;
for (int i = 1; i <= 5; i++)
{
result = net.Update(new TValue(i, (double)i));
}
Assert.Equal(1.0, result.Value, 10);
}
[Fact]
public void Calc_PerfectlyFalling_ReturnsNegativeOne()
{
var net = new Net(period: 5);
TValue result = default;
for (int i = 1; i <= 5; i++)
{
result = net.Update(new TValue(i, 100.0 - i));
}
Assert.Equal(-1.0, result.Value, 10);
}
// ═══════════════════════════════════════════════════════════════════
// State / Bar Correction Tests
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void BarCorrection_SecondUpdateOverwritesFirst()
{
var net = new Net(period: 5);
for (int i = 1; i <= 6; i++)
{
net.Update(new TValue(i, 50.0 + i));
}
// Replace last bar value (isNew=false) with same value
var result1 = net.Update(new TValue(7, 60.0));
var result2 = net.Update(new TValue(7, 60.0), isNew: false);
Assert.Equal(result1.Value, result2.Value, 12);
}
[Fact]
public void BarCorrection_IsNewFalseThenTrue_MatchesSingleUpdate()
{
// Path A: feed 10 bars normally
var netA = new Net(period: 5);
for (int i = 1; i <= 9; i++)
{
netA.Update(new TValue(i, 50.0 + i));
}
var resultA = netA.Update(new TValue(10, 60.0));
// Path B: feed 9 bars, then bar 10 as tick update then new bar
var netB = new Net(period: 5);
for (int i = 1; i <= 9; i++)
{
netB.Update(new TValue(i, 50.0 + i));
}
netB.Update(new TValue(10, 55.0)); // initial tick
netB.Update(new TValue(10, 58.0), false); // correction
netB.Update(new TValue(10, 60.0), false); // final correction
var resultB = netB.Last;
Assert.Equal(resultA.Value, resultB.Value, 12);
}
[Fact]
public void BarCorrection_MultipleCorrections_StableOutput()
{
var net = new Net(period: 10);
for (int i = 1; i <= 20; i++)
{
net.Update(new TValue(i, 50.0 + i * 0.5));
}
double firstVal = net.Update(new TValue(21, 70.0)).Value;
// Multiple corrections should still produce same result
for (int t = 0; t < 5; t++)
{
net.Update(new TValue(21, 70.0), false);
}
double lastVal = net.Last.Value;
Assert.Equal(firstVal, lastVal, 12);
}
[Fact]
public void BarCorrection_DifferentValues_ChangesResult()
{
var net = new Net(period: 5);
for (int i = 1; i <= 5; i++)
{
net.Update(new TValue(i, 50.0 + i));
}
double v1 = net.Update(new TValue(6, 100.0)).Value;
double v2 = net.Update(new TValue(6, 1.0), false).Value;
Assert.NotEqual(v1, v2);
}
// ═══════════════════════════════════════════════════════════════════
// Warmup / Convergence Tests
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Warmup_BeforePeriod_IsNotHot()
{
var net = new Net(period: 10);
for (int i = 1; i < 10; i++)
{
net.Update(new TValue(i, 50.0 + i));
Assert.False(net.IsHot, $"Should not be hot at bar {i}");
}
}
[Fact]
public void Warmup_AtPeriod_BecomesHot()
{
var net = new Net(period: 10);
for (int i = 1; i <= 10; i++)
{
net.Update(new TValue(i, 50.0 + i));
}
Assert.True(net.IsHot);
}
[Fact]
public void Warmup_FirstBar_ReturnsZero()
{
var net = new Net(period: 5);
var result = net.Update(new TValue(1, 100.0));
Assert.Equal(0.0, result.Value);
}
// ═══════════════════════════════════════════════════════════════════
// Robustness Tests
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Robustness_NaN_SubstitutesLastValid()
{
var net = new Net(period: 5);
for (int i = 1; i <= 5; i++)
{
net.Update(new TValue(i, 50.0 + i));
}
var result = net.Update(new TValue(6, double.NaN));
Assert.True(double.IsFinite(result.Value), "Output should be finite after NaN input");
}
[Fact]
public void Robustness_Infinity_SubstitutesLastValid()
{
var net = new Net(period: 5);
for (int i = 1; i <= 5; i++)
{
net.Update(new TValue(i, 50.0 + i));
}
var result = net.Update(new TValue(6, double.PositiveInfinity));
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Robustness_NegativeInfinity_SubstitutesLastValid()
{
var net = new Net(period: 5);
for (int i = 1; i <= 5; i++)
{
net.Update(new TValue(i, 50.0 + i));
}
var result = net.Update(new TValue(6, double.NegativeInfinity));
Assert.True(double.IsFinite(result.Value));
}
// ═══════════════════════════════════════════════════════════════════
// Consistency Tests (all API modes must match)
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Consistency_StreamVsBatch_Match()
{
TSeries input = MakeSeries(200);
int period = 10;
// Streaming mode
var netStream = new Net(period);
TSeries streamResult = [];
foreach (var v in input)
{
streamResult.Add(netStream.Update(v));
}
// Batch mode
var batchResult = Net.Batch(input, period);
for (int i = 0; i < input.Count; i++)
{
Assert.Equal(streamResult[i].Value, batchResult[i].Value, 12);
}
}
[Fact]
public void Consistency_SpanVsStreaming_Match()
{
TSeries input = MakeSeries(200);
int period = 10;
// Streaming
var netStream = new Net(period);
TSeries streamResult = [];
foreach (var v in input)
{
streamResult.Add(netStream.Update(v));
}
// Span
double[] src = new double[input.Count];
for (int i = 0; i < input.Count; i++)
{
src[i] = input[i].Value;
}
double[] dst = new double[src.Length];
Net.Batch(src.AsSpan(), dst.AsSpan(), period);
for (int i = 0; i < input.Count; i++)
{
Assert.Equal(streamResult[i].Value, dst[i], 12);
}
}
[Fact]
public void Consistency_UpdateTSeries_MatchesStreaming()
{
TSeries input = MakeSeries(100);
int period = 8;
// Streaming
var netS = new Net(period);
TSeries streamResult = [];
foreach (var v in input)
{
streamResult.Add(netS.Update(v));
}
// Update(TSeries)
var netU = new Net(period);
TSeries updateResult = netU.Update(input);
for (int i = 0; i < input.Count; i++)
{
Assert.Equal(streamResult[i].Value, updateResult[i].Value, 12);
}
}
[Fact]
public void Consistency_Calculate_MatchesStreaming()
{
TSeries input = MakeSeries(100);
int period = 8;
// Streaming
var netS = new Net(period);
TSeries streamResult = [];
foreach (var v in input)
{
streamResult.Add(netS.Update(v));
}
// Calculate
var (calcResult, _) = Net.Calculate(input, period);
for (int i = 0; i < input.Count; i++)
{
Assert.Equal(streamResult[i].Value, calcResult[i].Value, 12);
}
}
// ═══════════════════════════════════════════════════════════════════
// Span API Tests
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Span_DestinationTooShort_Throws()
{
double[] src = [1, 2, 3, 4, 5];
double[] dst = new double[3];
Assert.Throws<ArgumentException>(() => Net.Batch(src.AsSpan(), dst.AsSpan(), 3));
}
[Fact]
public void Span_InvalidPeriod_Throws()
{
double[] src = [1, 2, 3, 4, 5];
double[] dst = new double[5];
Assert.Throws<ArgumentOutOfRangeException>(() => Net.Batch(src.AsSpan(), dst.AsSpan(), 1));
}
// ═══════════════════════════════════════════════════════════════════
// Chainability / Events Tests
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Chain_EventFires()
{
var net = new Net(period: 5);
int eventCount = 0;
net.Pub += (object? _, in TValueEventArgs _) => eventCount++;
for (int i = 1; i <= 10; i++)
{
net.Update(new TValue(i, 50.0 + i));
}
Assert.Equal(10, eventCount);
}
[Fact]
public void Chain_SourceSubscription()
{
TSeries source = [];
var net = new Net(source, period: 5);
int eventCount = 0;
net.Pub += (object? _, in TValueEventArgs _) => eventCount++;
for (int i = 1; i <= 10; i++)
{
source.Add(new TValue(i, 50.0 + i));
}
Assert.Equal(10, eventCount);
}
// ═══════════════════════════════════════════════════════════════════
// NET-Specific Tests
// ═══════════════════════════════════════════════════════════════════
[Fact]
public void Net_ConstantInput_ReturnsZero()
{
var net = new Net(period: 5);
for (int i = 1; i <= 10; i++)
{
net.Update(new TValue(i, 42.0));
}
Assert.Equal(0.0, net.Last.Value, 12);
}
[Fact]
public void Net_OutputBounded()
{
var net = new Net(period: 10);
var rng = new Random(123);
for (int i = 0; i < 1000; i++)
{
net.Update(new TValue(i, rng.NextDouble() * 200.0 - 100.0));
Assert.InRange(net.Last.Value, -1.0, 1.0);
}
}
[Fact]
public void Net_Symmetry_RisingFalling()
{
// τ for [1,2,3,4,5] should be -τ for [5,4,3,2,1]
var netRise = new Net(period: 5);
for (int i = 1; i <= 5; i++)
{
netRise.Update(new TValue(i, (double)i));
}
var netFall = new Net(period: 5);
for (int i = 1; i <= 5; i++)
{
netFall.Update(new TValue(i, 6.0 - i));
}
Assert.Equal(netRise.Last.Value, -netFall.Last.Value, 12);
}
[Fact]
public void Net_DifferentPeriods_DifferentResults()
{
TSeries input = MakeSeries(50);
var net5 = new Net(period: 5);
var net20 = new Net(period: 20);
for (int i = 0; i < input.Count; i++)
{
net5.Update(input[i]);
net20.Update(input[i]);
}
// Different periods should generally produce different results
Assert.NotEqual(net5.Last.Value, net20.Last.Value);
}
[Fact]
public void Net_Reset_ClearsState()
{
var net = new Net(period: 5);
for (int i = 1; i <= 10; i++)
{
net.Update(new TValue(i, 50.0 + i));
}
Assert.True(net.IsHot);
net.Reset();
Assert.False(net.IsHot);
var result = net.Update(new TValue(1, 100.0));
Assert.Equal(0.0, result.Value);
}
[Fact]
public void Net_Prime_SetsState()
{
var net = new Net(period: 5);
double[] primeData = [10, 20, 30, 40, 50];
net.Prime(primeData);
Assert.True(net.IsHot);
Assert.Equal(1.0, net.Last.Value, 10); // perfectly rising
}
[Fact]
public void Net_KnownSequence_CorrectTau()
{
// For [1, 3, 2, 5, 4] (stored newest-first in X[]):
// X[0]=4 (newest), X[1]=5, X[2]=2, X[3]=3, X[4]=1 (oldest)
// Using Ehlers formula: for i=1 to 4, for k=0 to i-1: Num -= Sign(X[i]-X[k])
//
// But we feed as a time series: bar1=1, bar2=3, bar3=2, bar4=5, bar5=4
// In RingBuffer: [0]=1, [1]=3, [2]=2, [3]=5, [4]=4
// Mapping: X[0]=buf[4]=4, X[1]=buf[3]=5, X[2]=buf[2]=2, X[3]=buf[1]=3, X[4]=buf[0]=1
//
// i=1,k=0: -(Sign(5-4)) = -1
// i=2,k=0: -(Sign(2-4)) = +1; k=1: -(Sign(2-5)) = +1
// i=3,k=0: -(Sign(3-4)) = +1; k=1: -(Sign(3-5)) = +1; k=2: -(Sign(3-2)) = -1
// i=4,k=0: -(Sign(1-4)) = +1; k=1: -(Sign(1-5)) = +1; k=2: -(Sign(1-2)) = +1; k=3: -(Sign(1-3)) = +1
// Num = -1 + 1 + 1 + 1 + 1 - 1 + 1 + 1 + 1 + 1 = 6
// Denom = 0.5 * 5 * 4 = 10
// Tau = 6/10 = 0.6
var net = new Net(period: 5);
double[] seq = [1, 3, 2, 5, 4];
for (int i = 0; i < seq.Length; i++)
{
net.Update(new TValue(i + 1, seq[i]));
}
Assert.Equal(0.6, net.Last.Value, 10);
}
[Fact]
public void Net_LargeDataset_NoCrash()
{
var net = new Net(period: 14);
var rng = new Random(99);
for (int i = 0; i < 100_000; i++)
{
net.Update(new TValue(i, 100.0 + rng.NextDouble() * 50.0));
}
Assert.True(double.IsFinite(net.Last.Value));
Assert.InRange(net.Last.Value, -1.0, 1.0);
}
}
+1
View File
@@ -558,6 +558,7 @@ HAS_CHEBY1 = _bind("qtl_cheby1", [_dp, _ci, _dp, _ci, _cd])
HAS_CHEBY2 = _bind("qtl_cheby2", [_dp, _ci, _dp, _ci, _cd])
HAS_ELLIPTIC = _bind("qtl_elliptic", [_dp, _ci, _dp, _ci])
HAS_EDCF = _bind("qtl_edcf", [_dp, _ci, _dp, _ci])
HAS_NET = _bind("qtl_net", [_dp, _ci, _dp, _ci])
HAS_BPF = _bind("qtl_bpf", [_dp, _ci, _dp, _ci, _ci])
HAS_ALAGUERRE = _bind("qtl_alaguerre", [_dp, _ci, _dp, _ci, _ci])
HAS_BILATERAL = _bind("qtl_bilateral", [_dp, _ci, _dp, _ci, _cd, _cd])
+9
View File
@@ -38,6 +38,7 @@ __all__ = [
"cheby2",
"elliptic",
"edcf",
"net",
"bpf",
"alaguerre",
"bilateral",
@@ -377,6 +378,14 @@ def edcf(close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
return _wrap(dst, idx, f"EDCF_{period}", "filters", offset)
def net(close: object, period: int = 14, offset: int = 0, **kwargs) -> object:
"""Ehlers Noise Elimination Technology."""
period = int(kwargs.get("length", period)); offset = int(offset)
src, idx = _arr(close); n = len(src); dst = _out(n)
_check(_lib.qtl_net(_ptr(src), n, _ptr(dst), period))
return _wrap(dst, idx, f"NET_{period}", "filters", offset)
def bpf(close: object, period: int = 14, bandwidth: int = 5,
offset: int = 0, **kwargs) -> object:
"""Bandpass Filter."""
+10
View File
@@ -1461,6 +1461,16 @@ public static unsafe partial class Exports
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
// Net: Pattern A (src, out, int period)
[UnmanagedCallersOnly(EntryPoint = "qtl_net")]
public static int QtlNet(double* src, int n, double* dst, int period)
{
int v = Chk1(src, dst, n); if (v != 0) return v;
v = ChkPeriod(period); if (v != 0) return v;
try { Net.Batch(Src(src, n), Dst(dst, n), period); return StatusCodes.QTL_OK; }
catch { return StatusCodes.QTL_ERR_INTERNAL; }
}
// ═══════════════════════════════════════════════════════════════════════
// §8.12 Cycles
// ═══════════════════════════════════════════════════════════════════════