mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-15 17:18:05 +00:00
feat(filters): add NET - Ehlers Noise Elimination Technology (TASC Dec 2020)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 > 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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user