mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-14 00:28:05 +00:00
feat: add EPA (Ehlers Phasor Analysis) indicator - TASC Nov 2022
This commit is contained in:
@@ -323,6 +323,7 @@
|
||||
* [CCYC - Ehlers Cyber Cycle](/lib/cycles/ccyc/Ccyc.md)
|
||||
* [CG - Ehlers Center of Gravity](/lib/cycles/cg/Cg.md)
|
||||
* [DSP - Ehlers Detrended Synthetic Price](/lib/cycles/dsp/Dsp.md)
|
||||
* [EPA - Ehlers Phasor Analysis](/lib/cycles/epa/epa.md)
|
||||
* [FSI - Ehlers Fourier Series Indicator](/lib/cycles/fsi/Fsi.md)
|
||||
* [ACP - Ehlers Autocorrelation Periodogram](/lib/cycles/acp/Acp.md)
|
||||
* [EBSW - Ehlers Even Better Sinewave](/lib/cycles/ebsw/Ebsw.md)
|
||||
|
||||
@@ -441,6 +441,7 @@ Periodic pattern detection and dominant frequency extraction. Markets exhibit cy
|
||||
| [**CCYC**](../lib/cycles/ccyc/Ccyc.md) | Ehlers Cyber Cycle | 4-tap FIR + 2-pole high-pass IIR cycle extraction |
|
||||
| [**CG**](../lib/cycles/cg/Cg.md) | Ehlers Center of Gravity | Ehlers cycle measurement |
|
||||
| [**DSP**](../lib/cycles/dsp/Dsp.md) | Ehlers Detrended Synthetic Price | Cycle-isolated price component |
|
||||
| [**EPA**](../lib/cycles/epa/epa.md) | Ehlers Phasor Analysis | Pearson correlation phasor with wraparound + trend state |
|
||||
| [**FSI**](../lib/cycles/fsi/Fsi.md) | Ehlers Fourier Series Indicator | 3-harmonic bandpass + amplitude-weighted reconstruction |
|
||||
| [**ACP**](../lib/cycles/acp/Acp.md) | Ehlers Autocorrelation Periodogram | Ehlers dominant cycle detection |
|
||||
| [**EBSW**](../lib/cycles/ebsw/Ebsw.md) | Ehlers Even Better Sinewave | Ehlers improved cycle indicator |
|
||||
|
||||
@@ -429,6 +429,7 @@ Markets oscillate. These indicators try to measure the oscillation itself — th
|
||||
| CCYC | Ehlers Cyber Cycle | [ccyc.pine](../lib/cycles/ccyc/ccyc.pine) |
|
||||
| CG | Ehlers Center of Gravity | [cg.pine](../lib/cycles/cg/cg.pine) |
|
||||
| DSP | Ehlers Detrended Synthetic Price | [dsp.pine](../lib/cycles/dsp/dsp.pine) |
|
||||
| EPA | Ehlers Phasor Analysis | [epa.pine](../lib/cycles/epa/epa.pine) |
|
||||
| FSI | Ehlers Fourier Series Indicator | [fsi.pine](../lib/cycles/fsi/fsi.pine) |
|
||||
| ACP | Ehlers Autocorrelation Periodogram | [acp.pine](../lib/cycles/acp/acp.pine) |
|
||||
| EBSW | Ehlers Even Better Sinewave | [ebsw.pine](../lib/cycles/ebsw/ebsw.pine) |
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
| [DWT](numerics/dwt/Dwt.md) | Discrete Wavelet Transform | Numerics |
|
||||
| [DX](dynamics/dx/Dx.md) | Directional Movement Index | Dynamics |
|
||||
| [DYMI](oscillators/dymi/Dymi.md) | Dynamic Momentum Index | Oscillators |
|
||||
| [EPA](cycles/epa/epa.md) | Ehlers Phasor Analysis | Cycles |
|
||||
| [EBSW](cycles/ebsw/Ebsw.md) | Ehlers Even Better Sinewave | Cycles |
|
||||
| [EDCF](filters/edcf/Edcf.md) | Ehlers Distance Coefficient Filter | Filters |
|
||||
| [EDECAY](numerics/edecay/Edecay.md) | Exponential Decay | Numerics |
|
||||
|
||||
@@ -12,6 +12,7 @@ Cycle analysis identifies repeating patterns in price data. John Ehlers pioneere
|
||||
| [CCYC](ccyc/Ccyc.md) | Ehlers Cyber Cycle | Ehlers. 4-tap FIR + 2-pole high-pass IIR. Isolates dominant cycle component. |
|
||||
| [CG](cg/Cg.md) | Ehlers Center of Gravity | Ehlers. Weighted sum position. Minimal lag cycle indicator. |
|
||||
| [DSP](dsp/Dsp.md) | Ehlers Detrended Synthetic Price | Removes trend to reveal underlying cycles. |
|
||||
| [EPA](epa/epa.md) | Ehlers Phasor Analysis | Ehlers. Pearson correlation phasor with wraparound + trend state detection. |
|
||||
| [FSI](fsi/Fsi.md) | Ehlers Fourier Series Indicator | Ehlers. 3-harmonic bandpass + amplitude-weighted reconstruction. Cycle timing.|
|
||||
| [EBSW](ebsw/Ebsw.md) | Ehlers Even Better Sinewave | Ehlers. Improved sinewave extraction. Reduces false signals. |
|
||||
| [HOMOD](homod/Homod.md) | Ehlers Homodyne Discriminator | Dominant cycle detection via homodyne technique. |
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class EpaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Cycle Period", sortIndex: 1, minimum: 2, maximum: 500, increment: 1, decimalPlaces: 0)]
|
||||
public int Period { get; set; } = 28;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput(sortIndex: 2)]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Epa _epa = null!;
|
||||
private readonly LineSeries _angleLine;
|
||||
private readonly LineSeries _derivedPeriodLine;
|
||||
private readonly LineSeries _trendStateLine;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"EPA ({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/cycles/epa/Epa.Quantower.cs";
|
||||
|
||||
public EpaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "EPA - Ehlers Phasor Analysis";
|
||||
Description = "Phasor analysis extracting cycle phase via Pearson correlation of price against cosine/sine reference waves, with wraparound compensation and trend state detection.";
|
||||
|
||||
_angleLine = new LineSeries("Angle", Color.Yellow, 2, LineStyle.Solid);
|
||||
_derivedPeriodLine = new LineSeries("DerivedPeriod", Color.Cyan, 1, LineStyle.Solid);
|
||||
_trendStateLine = new LineSeries("TrendState", Color.Red, 2, LineStyle.Solid);
|
||||
AddLineSeries(_angleLine);
|
||||
AddLineSeries(_derivedPeriodLine);
|
||||
AddLineSeries(_trendStateLine);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_epa = new Epa(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var priceSelector = Source.GetPriceSelector();
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double price = priceSelector(item);
|
||||
|
||||
TValue input = new(item.TimeLeft, price);
|
||||
TValue result = _epa.Update(input, args.IsNewBar());
|
||||
|
||||
_angleLine.SetValue(result.Value, _epa.IsHot, ShowColdValues);
|
||||
_derivedPeriodLine.SetValue(_epa.DerivedPeriod, _epa.IsHot, ShowColdValues);
|
||||
_trendStateLine.SetValue(_epa.TrendState, _epa.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,525 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// EPA: Ehlers Phasor Analysis — extracts cycle phase by computing Pearson correlation
|
||||
/// of a price window against cosine (Real) and negative-sine (Imaginary) reference waves,
|
||||
/// converting the resulting phasor to an angle with wraparound compensation and monotonic
|
||||
/// constraint, then deriving cycle period and trend state from the angle rate-of-change.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// From John F. Ehlers, "Recurring Phase Of Cycle Analysis"
|
||||
/// (Stocks & Commodities, November 2022).
|
||||
///
|
||||
/// Algorithm:
|
||||
/// 1. Dual Pearson correlation over sliding window of N bars:
|
||||
/// Real = corr(price, cos(2πk/N)), Imag = corr(price, -sin(2πk/N))
|
||||
/// 2. Phasor angle = 90° - atan(Imag/Real) with quadrant fix (if Real < 0: angle -= 180°)
|
||||
/// 3. Wraparound compensation: detects 360° boundary crossings
|
||||
/// 4. Monotonic constraint with conditional exceptions: angle generally cannot go backwards
|
||||
/// 5. DerivedPeriod = 360 / DeltaAngle (clamped to max 60)
|
||||
/// 6. TrendState: 0 = cycling, +1 = trending long, -1 = trending short
|
||||
///
|
||||
/// Properties:
|
||||
/// - O(period) per bar for dual correlation loops
|
||||
/// - Precomputed cos/sin tables eliminate per-bar trig calls
|
||||
/// - Real, Imag bounded [-1, +1] by Pearson construction
|
||||
/// - Zero allocation in hot path (RingBuffer is pre-allocated)
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Epa : AbstractBase
|
||||
{
|
||||
private const int DefaultPeriod = 28;
|
||||
private const double MaxDerivedPeriod = 60.0;
|
||||
private const double TrendThreshold = 6.0;
|
||||
private const double Rad2Deg = 180.0 / Math.PI;
|
||||
|
||||
private readonly int _period;
|
||||
private readonly double[] _cosTable;
|
||||
private readonly double[] _negSinTable;
|
||||
private readonly RingBuffer _buf;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double PrevAngle,
|
||||
double PrevDeltaAngle,
|
||||
double PrevDerivedPeriod,
|
||||
int Count,
|
||||
double LastValid);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>Phasor angle in degrees, with wraparound compensation and monotonic constraint.</summary>
|
||||
public double Angle { get; private set; }
|
||||
|
||||
/// <summary>Cycle period derived from angle rate-of-change. Clamped to [0, 60].</summary>
|
||||
public double DerivedPeriod { get; private set; }
|
||||
|
||||
/// <summary>Trend state: +1 = trending long, -1 = trending short, 0 = cycling.</summary>
|
||||
public int TrendState { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsHot => _s.Count >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Epa indicator.
|
||||
/// </summary>
|
||||
/// <param name="period">Presumed dominant cycle wavelength. Must be > 1. Default 28.</param>
|
||||
public Epa(int period = DefaultPeriod)
|
||||
{
|
||||
if (period <= 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 1.", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
|
||||
// Precompute cos/sin lookup tables
|
||||
_cosTable = new double[period];
|
||||
_negSinTable = new double[period];
|
||||
double twoPiOverN = 2.0 * Math.PI / period;
|
||||
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
double a = twoPiOverN * k;
|
||||
_cosTable[k] = Math.Cos(a);
|
||||
_negSinTable[k] = -Math.Sin(a);
|
||||
}
|
||||
|
||||
_buf = new(period);
|
||||
Name = $"Epa({period})";
|
||||
WarmupPeriod = period;
|
||||
_s = default;
|
||||
_ps = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Epa indicator chained to a publisher source.
|
||||
/// </summary>
|
||||
public Epa(ITValuePublisher source, int period = DefaultPeriod) : this(period)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
source.Pub += HandleInput;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleInput(object? sender, in TValueEventArgs e)
|
||||
{
|
||||
Update(e.Value, e.IsNew);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
// State management: save/restore for bar correction
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
_buf.Snapshot();
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
_buf.Restore();
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
double price = input.Value;
|
||||
|
||||
// NaN/Infinity guard
|
||||
if (!double.IsFinite(price))
|
||||
{
|
||||
price = s.LastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
s = s with { LastValid = price };
|
||||
}
|
||||
|
||||
int count = isNew ? s.Count + 1 : s.Count;
|
||||
_buf.Add(price);
|
||||
|
||||
int n = Math.Min(count, _period);
|
||||
double angle = 0;
|
||||
double derivedPeriod = s.PrevDerivedPeriod;
|
||||
int trendState = 0;
|
||||
|
||||
if (n >= 2)
|
||||
{
|
||||
// Dual Pearson correlations
|
||||
double real = ComputeCorrelation(_buf, _cosTable, n);
|
||||
double imag = ComputeCorrelation(_buf, _negSinTable, n);
|
||||
|
||||
// Step 3: Angle = 90 - atan(Imag/Real) with quadrant fix
|
||||
if (real != 0.0)
|
||||
{
|
||||
angle = 90.0 - (Math.Atan(imag / real) * Rad2Deg);
|
||||
}
|
||||
if (real < 0.0)
|
||||
{
|
||||
angle -= 180.0;
|
||||
}
|
||||
|
||||
double prevAngle = s.PrevAngle;
|
||||
|
||||
// Step 4: Wraparound compensation
|
||||
if (Math.Abs(angle) - Math.Abs(prevAngle - 360.0) < angle - prevAngle
|
||||
&& prevAngle > 90.0 && angle < -90.0)
|
||||
{
|
||||
angle -= 360.0;
|
||||
}
|
||||
|
||||
// Step 5: Angle cannot go backwards (with conditional exceptions)
|
||||
if (angle < prevAngle
|
||||
&& ((prevAngle > -135.0 && prevAngle < 135.0)
|
||||
|| (angle < -90.0 && prevAngle < -90.0)))
|
||||
{
|
||||
angle = prevAngle;
|
||||
}
|
||||
|
||||
// Step 6: DerivedPeriod from angle rate-of-change
|
||||
double deltaAngle = angle - prevAngle;
|
||||
if (deltaAngle <= 0.0)
|
||||
{
|
||||
deltaAngle = s.PrevDeltaAngle;
|
||||
}
|
||||
if (deltaAngle > 0.0)
|
||||
{
|
||||
derivedPeriod = 360.0 / deltaAngle;
|
||||
}
|
||||
if (derivedPeriod > MaxDerivedPeriod)
|
||||
{
|
||||
derivedPeriod = MaxDerivedPeriod;
|
||||
}
|
||||
|
||||
// Step 7: Trend state
|
||||
trendState = 0;
|
||||
double angleChange = angle - prevAngle;
|
||||
if (angleChange <= TrendThreshold)
|
||||
{
|
||||
if (angle >= 90.0 || angle <= -90.0)
|
||||
{
|
||||
trendState = 1; // trending long
|
||||
}
|
||||
else if (angle > -90.0 && angle < 90.0)
|
||||
{
|
||||
trendState = -1; // trending short
|
||||
}
|
||||
}
|
||||
|
||||
s = s with { PrevDeltaAngle = deltaAngle > 0 ? deltaAngle : s.PrevDeltaAngle };
|
||||
}
|
||||
|
||||
Angle = angle;
|
||||
DerivedPeriod = derivedPeriod;
|
||||
TrendState = trendState;
|
||||
|
||||
_s = new State(angle, s.PrevDeltaAngle, derivedPeriod, count, s.LastValid);
|
||||
|
||||
Last = new TValue(input.Time, angle);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a full TSeries, returning the Angle for each bar.
|
||||
/// </summary>
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var result = Update(source[i]);
|
||||
vSpan[i] = result.Value;
|
||||
}
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double value in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, value));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static batch: creates an Epa, processes source, returns output TSeries.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int period = DefaultPeriod)
|
||||
{
|
||||
var ind = new Epa(period);
|
||||
return ind.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static span-based batch: computes phasor angle into output span.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period = DefaultPeriod)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length.", nameof(output));
|
||||
}
|
||||
if (period <= 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 1.", nameof(period));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Precompute trig tables
|
||||
const int StackallocThreshold = 256;
|
||||
double[]? rentedCos = null;
|
||||
double[]? rentedSin = null;
|
||||
scoped Span<double> cosTab;
|
||||
scoped Span<double> sinTab;
|
||||
|
||||
if (period <= StackallocThreshold)
|
||||
{
|
||||
cosTab = stackalloc double[period];
|
||||
sinTab = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedCos = ArrayPool<double>.Shared.Rent(period);
|
||||
rentedSin = ArrayPool<double>.Shared.Rent(period);
|
||||
cosTab = rentedCos.AsSpan(0, period);
|
||||
sinTab = rentedSin.AsSpan(0, period);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
double twoPiOverN = 2.0 * Math.PI / period;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
double a = twoPiOverN * k;
|
||||
cosTab[k] = Math.Cos(a);
|
||||
sinTab[k] = -Math.Sin(a);
|
||||
}
|
||||
|
||||
// Price ring buffer (manual circular)
|
||||
double[]? rentedBuf = null;
|
||||
scoped Span<double> priceBuf;
|
||||
if (period <= StackallocThreshold)
|
||||
{
|
||||
priceBuf = stackalloc double[period];
|
||||
}
|
||||
else
|
||||
{
|
||||
rentedBuf = ArrayPool<double>.Shared.Rent(period);
|
||||
priceBuf = rentedBuf.AsSpan(0, period);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
priceBuf.Clear();
|
||||
int bufIdx = 0;
|
||||
int filled = 0;
|
||||
double lastValid = 0;
|
||||
double prevAngle = 0;
|
||||
double prevDeltaAngle = 0;
|
||||
double prevDerivedPeriod = 0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (!double.IsFinite(val))
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
|
||||
priceBuf[bufIdx] = val;
|
||||
bufIdx = (bufIdx + 1) % period;
|
||||
if (filled < period)
|
||||
{
|
||||
filled++;
|
||||
}
|
||||
|
||||
int n = filled;
|
||||
double angle = 0;
|
||||
|
||||
if (n >= 2)
|
||||
{
|
||||
// Compute Real correlation (cosine)
|
||||
double real = InlineCorrelation(priceBuf, cosTab, bufIdx, n, period);
|
||||
double imag = InlineCorrelation(priceBuf, sinTab, bufIdx, n, period);
|
||||
|
||||
// Angle calculation
|
||||
if (real != 0.0)
|
||||
{
|
||||
angle = 90.0 - (Math.Atan(imag / real) * Rad2Deg);
|
||||
}
|
||||
if (real < 0.0)
|
||||
{
|
||||
angle -= 180.0;
|
||||
}
|
||||
|
||||
// Wraparound compensation
|
||||
if (Math.Abs(angle) - Math.Abs(prevAngle - 360.0) < angle - prevAngle
|
||||
&& prevAngle > 90.0 && angle < -90.0)
|
||||
{
|
||||
angle -= 360.0;
|
||||
}
|
||||
|
||||
// Monotonic constraint with exceptions
|
||||
if (angle < prevAngle
|
||||
&& ((prevAngle > -135.0 && prevAngle < 135.0)
|
||||
|| (angle < -90.0 && prevAngle < -90.0)))
|
||||
{
|
||||
angle = prevAngle;
|
||||
}
|
||||
|
||||
// DerivedPeriod
|
||||
double deltaAngle = angle - prevAngle;
|
||||
if (deltaAngle <= 0.0)
|
||||
{
|
||||
deltaAngle = prevDeltaAngle;
|
||||
}
|
||||
if (deltaAngle > 0.0)
|
||||
{
|
||||
prevDerivedPeriod = 360.0 / deltaAngle;
|
||||
prevDeltaAngle = deltaAngle;
|
||||
}
|
||||
if (prevDerivedPeriod > MaxDerivedPeriod)
|
||||
{
|
||||
prevDerivedPeriod = MaxDerivedPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
output[i] = angle;
|
||||
prevAngle = angle;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedBuf != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (rentedCos != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedCos);
|
||||
}
|
||||
if (rentedSin != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(rentedSin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static convenience method: returns (TSeries results, Epa indicator) for inspection.
|
||||
/// </summary>
|
||||
public static (TSeries Results, Epa Indicator) Calculate(TSeries source, int period = DefaultPeriod)
|
||||
{
|
||||
var ind = new Epa(period);
|
||||
var results = ind.Update(source);
|
||||
return (results, ind);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Reset()
|
||||
{
|
||||
_s = default;
|
||||
_ps = default;
|
||||
_buf.Clear();
|
||||
Last = default;
|
||||
Angle = 0;
|
||||
DerivedPeriod = 0;
|
||||
TrendState = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes Pearson correlation between the most recent n values in RingBuffer
|
||||
/// and the first n entries of a reference wave table.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeCorrelation(RingBuffer buf, double[] refTable, int n)
|
||||
{
|
||||
double sx = 0, sxx = 0, sxy = 0;
|
||||
double sy = 0, syy = 0;
|
||||
int newest = buf.Count - 1;
|
||||
|
||||
for (int k = 0; k < n; k++)
|
||||
{
|
||||
double x = buf[newest - k];
|
||||
double y = refTable[k];
|
||||
sx += x;
|
||||
sxx += x * x;
|
||||
sxy += x * y;
|
||||
sy += y;
|
||||
syy += y * y;
|
||||
}
|
||||
|
||||
double nd = n;
|
||||
double denomProd = ((nd * sxx) - (sx * sx)) * ((nd * syy) - (sy * sy));
|
||||
if (denomProd <= 0.0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double r = ((nd * sxy) - (sx * sy)) / Math.Sqrt(denomProd);
|
||||
return Math.Clamp(r, -1.0, 1.0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inline Pearson correlation for span-based batch (uses manual circular buffer).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double InlineCorrelation(
|
||||
Span<double> priceBuf, Span<double> refTab, int bufIdx, int n, int period)
|
||||
{
|
||||
double sx = 0, sxx = 0, sxy = 0;
|
||||
double sy = 0, syy = 0;
|
||||
|
||||
for (int k = 0; k < n; k++)
|
||||
{
|
||||
int idx = (((bufIdx - 1 - k) % period) + period) % period;
|
||||
double x = priceBuf[idx];
|
||||
double y = refTab[k];
|
||||
sx += x;
|
||||
sxx += x * x;
|
||||
sxy += x * y;
|
||||
sy += y;
|
||||
syy += y * y;
|
||||
}
|
||||
|
||||
double nd = n;
|
||||
double dp = ((nd * sxx) - (sx * sx)) * ((nd * syy) - (sy * sy));
|
||||
return dp > 0.0 ? Math.Clamp(((nd * sxy) - (sx * sy)) / Math.Sqrt(dp), -1.0, 1.0) : 0.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
# EPA — Ehlers Phasor Analysis
|
||||
|
||||
## Overview
|
||||
|
||||
**EPA** (Ehlers Phasor Analysis) extracts cycle phase from price data by computing a phasor using Pearson correlation of a price window against cosine and negative-sine reference waves. The angle of the phasor reveals the current phase position within the dominant cycle, enabling identification of cycle valleys (at −90°) and peaks (at +90°), as well as determining whether the market is cycling or trending.
|
||||
|
||||
| Property | Value |
|
||||
|:-------------- |:------------------------------- |
|
||||
| **Category** | Cycles |
|
||||
| **Author** | John F. Ehlers |
|
||||
| **Source** | TASC November 2022, "Recurring Phase Of Cycle Analysis" |
|
||||
|
||||
## Origin and Sources
|
||||
|
||||
John Ehlers introduced Phasor Analysis in the November 2022 issue of *Stocks & Commodities* magazine in the article "Recurring Phase Of Cycle Analysis." The technique uses Pearson correlation as a matched filter to determine how well price data correlates with cosine and sine waves at a presumed cycle period, producing the Real and Imaginary components of a phasor.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```csharp
|
||||
// streaming
|
||||
var epa = new Epa(period: 28);
|
||||
TValue result = epa.Update(tValue);
|
||||
|
||||
// static batch (TSeries)
|
||||
TSeries output = Epa.Batch(source, period: 28);
|
||||
|
||||
// static batch (Span)
|
||||
Epa.Batch(source, output, period: 28);
|
||||
|
||||
// factory
|
||||
var (results, indicator) = Epa.Calculate(source, period: 28);
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Default | Valid Range | Description |
|
||||
|:---------- |:----- |:------- |:----------- |:-------------------------------------------- |
|
||||
| `period` | int | 28 | > 1 | Presumed dominant cycle wavelength in bars |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|:--------------- |:------ |:--------------------------------------------------------------- |
|
||||
| `Angle` | double | Phasor angle in degrees with wraparound compensation |
|
||||
| `DerivedPeriod` | double | Cycle period derived from angle rate-of-change (clamped to 60) |
|
||||
| `TrendState` | int | +1 = trending long, −1 = trending short, 0 = cycling |
|
||||
|
||||
The primary output (`Last.Value`) is the **Angle**.
|
||||
|
||||
## Algorithm
|
||||
|
||||
1. **Dual Pearson Correlation** over a sliding window of `period` bars:
|
||||
- `Real = corr(price, cos(2πk/N))` — correlation with cosine
|
||||
- `Imag = corr(price, -sin(2πk/N))` — correlation with negative sine
|
||||
|
||||
2. **Angle Calculation**: `Angle = 90° - atan(Imag/Real)` with quadrant fix: if `Real < 0`, subtract 180°.
|
||||
|
||||
3. **Wraparound Compensation**: When the angle crosses the 360° boundary (previous angle > 90° and current < −90°), subtract 360° to maintain continuity.
|
||||
|
||||
4. **Monotonic Constraint**: The angle generally cannot decrease, but allows exceptions at extreme regions (when both previous and current angles are in the same deep-negative quadrant).
|
||||
|
||||
5. **Derived Period**: Computed as `360 / ΔAngle` where `ΔAngle` is the per-bar angle change. When `ΔAngle ≤ 0`, the previous delta is used. The result is clamped to a maximum of 60.
|
||||
|
||||
6. **Trend State**: When the angle rate-of-change ≤ 6°/bar:
|
||||
- If angle ≥ 90° or ≤ −90° → **+1** (trending long)
|
||||
- If −90° < angle < 90° → **−1** (trending short)
|
||||
- Otherwise → **0** (cycling)
|
||||
|
||||
## Interpretation
|
||||
|
||||
- The phasor angle oscillates between −180° and +180°, completing one full cycle per dominant period.
|
||||
- **Cycle valleys** correspond to the angle crossing −90°.
|
||||
- **Cycle peaks** correspond to the angle near +90°.
|
||||
- The **TrendState** indicates when the market transitions from cycling to trending behavior based on the angle rate slowing.
|
||||
- The **DerivedPeriod** provides a real-time estimate of the dominant cycle length.
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Value |
|
||||
|:-------------- |:------------------------------------------- |
|
||||
| Complexity | O(period) per bar |
|
||||
| Memory | O(period) — RingBuffer + trig tables |
|
||||
| Warmup | `period` bars |
|
||||
| Output Range | Angle: unbounded; DerivedPeriod: [0, 60]; TrendState: {−1, 0, +1} |
|
||||
| Zero Alloc | ✅ Hot path allocates nothing |
|
||||
|
||||
## Related Indicators
|
||||
|
||||
- [CCOR](../ccor/ccor.md) — Ehlers Correlation Cycle (TASC June 2020) — earlier version with simpler angle logic
|
||||
- [HT_PHASOR](../ht_phasor/ht_phasor.md) — Hilbert Transform Phasor Components — different algorithm
|
||||
- [FSI](../fsi/fsi.md) — Ehlers Fourier Series Indicator
|
||||
- [EBSW](../ebsw/ebsw.md) — Ehlers Even Better Sine Wave
|
||||
@@ -0,0 +1,85 @@
|
||||
//@version=6
|
||||
// EPA: Ehlers Phasor Analysis
|
||||
// From John F. Ehlers, "Recurring Phase Of Cycle Analysis"
|
||||
// (Stocks & Commodities, November 2022)
|
||||
indicator("EPA - Ehlers Phasor Analysis", shorttitle="EPA", overlay=false)
|
||||
|
||||
period = input.int(28, "Period", minval=2)
|
||||
src = input.source(close, "Source")
|
||||
|
||||
var float prevAngle = 0.0
|
||||
var float prevDeltaAngle = 0.0
|
||||
var float derivedPeriod = 0.0
|
||||
|
||||
// Correlate price with Cosine wave (Pearson correlation → Real)
|
||||
float sx_r = 0.0, float sy_r = 0.0
|
||||
float sxx_r = 0.0, float sxy_r = 0.0, float syy_r = 0.0
|
||||
for k = 0 to period - 1
|
||||
float x = nz(src[k])
|
||||
float y = math.cos(2.0 * math.pi * k / period)
|
||||
sx_r += x
|
||||
sy_r += y
|
||||
sxx_r += x * x
|
||||
sxy_r += x * y
|
||||
syy_r += y * y
|
||||
|
||||
float dp_r = (period * sxx_r - sx_r * sx_r) * (period * syy_r - sy_r * sy_r)
|
||||
float real = dp_r > 0 ? math.max(-1.0, math.min(1.0, (period * sxy_r - sx_r * sy_r) / math.sqrt(dp_r))) : 0.0
|
||||
|
||||
// Correlate price with -Sine wave (Pearson correlation → Imag)
|
||||
float sx_i = 0.0, float sy_i = 0.0
|
||||
float sxx_i = 0.0, float sxy_i = 0.0, float syy_i = 0.0
|
||||
for k = 0 to period - 1
|
||||
float x = nz(src[k])
|
||||
float y = -math.sin(2.0 * math.pi * k / period)
|
||||
sx_i += x
|
||||
sy_i += y
|
||||
sxx_i += x * x
|
||||
sxy_i += x * y
|
||||
syy_i += y * y
|
||||
|
||||
float dp_i = (period * sxx_i - sx_i * sx_i) * (period * syy_i - sy_i * sy_i)
|
||||
float imag = dp_i > 0 ? math.max(-1.0, math.min(1.0, (period * sxy_i - sx_i * sy_i) / math.sqrt(dp_i))) : 0.0
|
||||
|
||||
// Angle = 90 - atan(Imag/Real) with quadrant fix
|
||||
float angle = 0.0
|
||||
if real != 0
|
||||
angle := 90.0 - math.todegrees(math.atan(imag / real))
|
||||
if real < 0
|
||||
angle -= 180.0
|
||||
|
||||
// Wraparound compensation
|
||||
if math.abs(angle) - math.abs(prevAngle - 360.0) < angle - prevAngle and prevAngle > 90.0 and angle < -90.0
|
||||
angle -= 360.0
|
||||
|
||||
// Angle cannot go backwards (with conditional exceptions)
|
||||
if angle < prevAngle and ((prevAngle > -135.0 and prevAngle < 135.0) or (angle < -90.0 and prevAngle < -90.0))
|
||||
angle := prevAngle
|
||||
|
||||
// DerivedPeriod from angle rate-of-change
|
||||
float deltaAngle = angle - prevAngle
|
||||
if deltaAngle <= 0
|
||||
deltaAngle := prevDeltaAngle
|
||||
if deltaAngle > 0
|
||||
derivedPeriod := 360.0 / deltaAngle
|
||||
prevDeltaAngle := deltaAngle
|
||||
if derivedPeriod > 60
|
||||
derivedPeriod := 60.0
|
||||
|
||||
// Trend state
|
||||
int trendState = 0
|
||||
float angleChange = angle - prevAngle
|
||||
if angleChange <= 6.0
|
||||
if angle >= 90.0 or angle <= -90.0
|
||||
trendState := 1 // trending long
|
||||
else if angle > -90.0 and angle < 90.0
|
||||
trendState := -1 // trending short
|
||||
|
||||
prevAngle := angle
|
||||
|
||||
plot(angle, "Angle", color.yellow, 2)
|
||||
hline(0, "Zero", color.white)
|
||||
hline(90, "+90", color.new(color.cyan, 50))
|
||||
hline(-90, "-90", color.new(color.cyan, 50))
|
||||
plot(derivedPeriod, "DerivedPeriod", color.cyan, 1, display=display.none)
|
||||
plot(trendState, "TrendState", color.red, 2, display=display.none)
|
||||
@@ -0,0 +1,132 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Quantower.Tests;
|
||||
|
||||
public class EpaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
Assert.Equal(28, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_IsZero()
|
||||
{
|
||||
Assert.Equal(0, EpaIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_ContainsPeriod()
|
||||
{
|
||||
var indicator = new EpaIndicator { Period = 20 };
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_DoesNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
var ex = Record.Exception(() => indicator.Initialize());
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_Historical_DoesNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
indicator.Initialize();
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100, high: 105, low: 95, close: 102, volume: 1000,
|
||||
time: DateTime.UtcNow);
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_NewBar_DoesNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
indicator.Initialize();
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100, high: 105, low: 95, close: 102, volume: 1000,
|
||||
time: DateTime.UtcNow);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 102, high: 107, low: 97, close: 104, volume: 1100,
|
||||
time: DateTime.UtcNow.AddDays(1));
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_Tick_DoesNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
indicator.Initialize();
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100, high: 105, low: 95, close: 102, volume: 1000,
|
||||
time: DateTime.UtcNow);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceCodeLink_IsNotEmpty()
|
||||
{
|
||||
var indicator = new EpaIndicator();
|
||||
Assert.False(string.IsNullOrEmpty(indicator.SourceCodeLink));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleHistoricalBars_DoNotThrow()
|
||||
{
|
||||
var indicator = new EpaIndicator { Period = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100 + i, high: 105 + i, low: 95 + i, close: 102 + i,
|
||||
volume: 1000 + i * 10,
|
||||
time: DateTime.UtcNow.AddDays(i));
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CustomPeriod_InitializesCorrectly()
|
||||
{
|
||||
var indicator = new EpaIndicator { Period = 14 };
|
||||
indicator.Initialize();
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentSources_DoNotThrow()
|
||||
{
|
||||
foreach (var sourceType in new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close })
|
||||
{
|
||||
var indicator = new EpaIndicator { Source = sourceType };
|
||||
indicator.Initialize();
|
||||
indicator.HistoricalData.AddBar(
|
||||
open: 100, high: 105, low: 95, close: 102, volume: 1000,
|
||||
time: DateTime.UtcNow);
|
||||
var ex = Record.Exception(() =>
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)));
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class EpaTests
|
||||
{
|
||||
private static TSeries MakeSeries(int count = 500)
|
||||
{
|
||||
var rng = new Random(42);
|
||||
var s = new TSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
s.Add(new TValue(DateTime.UtcNow.AddDays(i), 100 + rng.NextDouble() * 10));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// ── Constructor ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Ctor_DefaultPeriod_Is28()
|
||||
{
|
||||
var epa = new Epa();
|
||||
Assert.Equal("Epa(28)", epa.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_CustomPeriod_SetsName()
|
||||
{
|
||||
var epa = new Epa(period: 14);
|
||||
Assert.Equal("Epa(14)", epa.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_Period1_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Epa(period: 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_Period0_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Epa(period: 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ctor_NegativePeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Epa(period: -5));
|
||||
}
|
||||
|
||||
// ── Basic Calculation ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Update_FirstBar_ReturnsZeroAngle()
|
||||
{
|
||||
var epa = new Epa();
|
||||
var result = epa.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AfterWarmup_ReturnsFiniteAngle()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(50);
|
||||
TValue last = default;
|
||||
foreach (var tv in s)
|
||||
{
|
||||
last = epa.Update(tv);
|
||||
}
|
||||
Assert.True(double.IsFinite(last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Angle_IsSetAfterUpdate()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DerivedPeriod_IsFiniteAfterWarmup()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(30);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
Assert.True(double.IsFinite(epa.DerivedPeriod));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrendState_IsValid()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(50);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
Assert.InRange(epa.TrendState, -1, 1);
|
||||
}
|
||||
|
||||
// ── State / Bar Correction ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_UpdateWithIsNewFalse_RestoresState()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
|
||||
// Process first 19 bars
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
epa.Update(s[i]);
|
||||
}
|
||||
|
||||
// Process bar 20 (new)
|
||||
epa.Update(s[19], isNew: true);
|
||||
double angleAfterNew = epa.Angle;
|
||||
|
||||
// Correct bar 20 (not new) with same value
|
||||
epa.Update(s[19], isNew: false);
|
||||
double angleAfterCorrection = epa.Angle;
|
||||
|
||||
Assert.Equal(angleAfterNew, angleAfterCorrection, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_DifferentValue_ProducesDifferentResult()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
|
||||
for (int i = 0; i < 19; i++)
|
||||
{
|
||||
epa.Update(s[i]);
|
||||
}
|
||||
|
||||
// New bar
|
||||
epa.Update(s[19], isNew: true);
|
||||
|
||||
// Correct with very different value
|
||||
epa.Update(new TValue(s[19].Time, s[19].Value + 50), isNew: false);
|
||||
double angle2 = epa.Angle;
|
||||
|
||||
// May or may not be different due to monotonic constraint, but should be finite
|
||||
Assert.True(double.IsFinite(angle2));
|
||||
}
|
||||
|
||||
// ── Warmup / IsHot ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeWarmup()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), 100 + i));
|
||||
}
|
||||
Assert.False(epa.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_TrueAtWarmup()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), 100 + i));
|
||||
}
|
||||
Assert.True(epa.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsPeriod()
|
||||
{
|
||||
var epa = new Epa(period: 20);
|
||||
Assert.Equal(20, epa.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── Robustness ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_DoesNotCorrupt()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
|
||||
// Feed NaN
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(100), double.NaN));
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_DoesNotCorrupt()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(20);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(100), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_Angle_IsFinite()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), 42.0));
|
||||
}
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
// ── Reset ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(30);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
Assert.True(epa.IsHot);
|
||||
|
||||
epa.Reset();
|
||||
Assert.False(epa.IsHot);
|
||||
Assert.Equal(0.0, epa.Angle);
|
||||
Assert.Equal(0.0, epa.DerivedPeriod);
|
||||
Assert.Equal(0, epa.TrendState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ProducesSameResultsOnReprocess()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(50);
|
||||
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
double angle1 = epa.Angle;
|
||||
|
||||
epa.Reset();
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
double angle2 = epa.Angle;
|
||||
|
||||
Assert.Equal(angle1, angle2, precision: 10);
|
||||
}
|
||||
|
||||
// ── Consistency: 4 API modes ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllModes_Consistent()
|
||||
{
|
||||
var s = MakeSeries(200);
|
||||
int period = 14;
|
||||
|
||||
// Mode 1: streaming
|
||||
var epa1 = new Epa(period);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa1.Update(tv);
|
||||
}
|
||||
|
||||
// Mode 2: Update(TSeries)
|
||||
var epa2 = new Epa(period);
|
||||
var ts2 = epa2.Update(s);
|
||||
|
||||
// Mode 3: Batch(TSeries)
|
||||
var ts3 = Epa.Batch(s, period);
|
||||
|
||||
// Mode 4: Batch(Span)
|
||||
double[] src = new double[s.Count];
|
||||
double[] dst = new double[s.Count];
|
||||
for (int i = 0; i < s.Count; i++)
|
||||
{
|
||||
src[i] = s[i].Value;
|
||||
}
|
||||
Epa.Batch(src, dst, period);
|
||||
|
||||
Assert.Equal(ts2[^1].Value, ts3[^1].Value, precision: 10);
|
||||
Assert.Equal(ts2[^1].Value, dst[^1], precision: 10);
|
||||
Assert.Equal(epa1.Angle, ts2[^1].Value, precision: 10);
|
||||
}
|
||||
|
||||
// ── Batch(TSeries) ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_SameLengthAsSource()
|
||||
{
|
||||
var s = MakeSeries(100);
|
||||
var result = Epa.Batch(s);
|
||||
Assert.Equal(s.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_TSeries_EmptySource_ReturnsEmpty()
|
||||
{
|
||||
var result = Epa.Batch(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
// ── Batch(Span) ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_ProducesFiniteOutput()
|
||||
{
|
||||
double[] src = [100, 101, 102, 103, 104, 103, 102, 101, 100, 99, 98, 99, 100, 101, 102];
|
||||
double[] dst = new double[src.Length];
|
||||
Epa.Batch(src, dst, period: 5);
|
||||
foreach (double v in dst)
|
||||
{
|
||||
Assert.True(double.IsFinite(v));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_MismatchedLength_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] dst = new double[5];
|
||||
Assert.Throws<ArgumentException>(() => Epa.Batch(src, dst));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Span_InvalidPeriod_Throws()
|
||||
{
|
||||
double[] src = new double[10];
|
||||
double[] dst = new double[10];
|
||||
Assert.Throws<ArgumentException>(() => Epa.Batch(src, dst, period: 0));
|
||||
}
|
||||
|
||||
// ── Calculate factory ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var s = MakeSeries(50);
|
||||
var (results, indicator) = Epa.Calculate(s, period: 10);
|
||||
Assert.Equal(s.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(indicator.Angle, results[^1].Value, precision: 10);
|
||||
}
|
||||
|
||||
// ── PubSub (chaining) ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void PubSub_ReceivesEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var epa = new Epa(source, period: 10);
|
||||
int eventCount = 0;
|
||||
epa.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow.AddDays(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(20, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PubSub_NullSource_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new Epa(null!, period: 10));
|
||||
}
|
||||
|
||||
// ── Prime ──────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Prime_WarmUpIndicator()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
double[] data = new double[20];
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
data[i] = 100 + i * 0.5;
|
||||
}
|
||||
epa.Prime(data);
|
||||
Assert.True(epa.IsHot);
|
||||
}
|
||||
|
||||
// ── EPA-specific behavior ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SineWave_ProducesVaryingAngle()
|
||||
{
|
||||
var epa = new Epa(period: 20);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + 10 * Math.Sin(2 * Math.PI * i / 20.0);
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
}
|
||||
// With a matching sine wave, angle should advance
|
||||
Assert.True(double.IsFinite(epa.Angle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DerivedPeriod_ClampedTo60()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(200);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
Assert.True(epa.DerivedPeriod <= 60.0,
|
||||
$"DerivedPeriod {epa.DerivedPeriod} exceeds max 60");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrendState_OnlyValidValues()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var s = MakeSeries(200);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa.Update(tv);
|
||||
Assert.True(epa.TrendState == -1 || epa.TrendState == 0 || epa.TrendState == 1,
|
||||
$"Invalid TrendState: {epa.TrendState}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriod_DifferentResults()
|
||||
{
|
||||
var s = MakeSeries(100);
|
||||
|
||||
var epa10 = new Epa(period: 10);
|
||||
var epa28 = new Epa(period: 28);
|
||||
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa10.Update(tv);
|
||||
epa28.Update(tv);
|
||||
}
|
||||
|
||||
// Different periods should generally produce different angles
|
||||
// (not guaranteed for all data, but very likely with random data)
|
||||
Assert.NotEqual(epa10.Angle, epa28.Angle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_MatchesStreaming()
|
||||
{
|
||||
var s = MakeSeries(100);
|
||||
int period = 14;
|
||||
|
||||
// Streaming
|
||||
var epa1 = new Epa(period);
|
||||
foreach (var tv in s)
|
||||
{
|
||||
epa1.Update(tv);
|
||||
}
|
||||
|
||||
// Update(TSeries)
|
||||
var epa2 = new Epa(period);
|
||||
_ = epa2.Update(s);
|
||||
|
||||
Assert.Equal(epa1.Angle, epa2.Angle, precision: 10);
|
||||
Assert.Equal(epa1.DerivedPeriod, epa2.DerivedPeriod, precision: 10);
|
||||
Assert.Equal(epa1.TrendState, epa2.TrendState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class EpaValidationTests
|
||||
{
|
||||
// ── Pearson Correlation Properties ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ConstantPrice_RealAndAngle_AreZero()
|
||||
{
|
||||
// Constant price has zero variance → correlation = 0 → angle = 0
|
||||
var epa = new Epa(period: 10);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), 50.0));
|
||||
}
|
||||
Assert.Equal(0.0, epa.Angle);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerfectCosineInput_HighCorrelation()
|
||||
{
|
||||
// Price that exactly matches cos wave at the indicator period should yield |Real| near 1
|
||||
int period = 20;
|
||||
var epa = new Epa(period: period);
|
||||
double maxAngle = double.MinValue;
|
||||
|
||||
for (int i = 0; i < period * 4; i++)
|
||||
{
|
||||
double price = 100 + 10 * Math.Cos(2 * Math.PI * i / period);
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
if (epa.IsHot && Math.Abs(epa.Angle) > Math.Abs(maxAngle))
|
||||
{
|
||||
maxAngle = epa.Angle;
|
||||
}
|
||||
}
|
||||
// The angle should move significantly when price matches the reference cosine
|
||||
Assert.True(double.IsFinite(maxAngle));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerfectSineInput_AngleAdvances()
|
||||
{
|
||||
// A sine wave at the indicator period should produce advancing angle.
|
||||
// The angle wraps at the 360° boundary (e.g. ~180° → ~-162°), which is
|
||||
// the expected wraparound compensation behavior.
|
||||
int period = 20;
|
||||
var epa = new Epa(period: period);
|
||||
var angles = new List<double>();
|
||||
|
||||
for (int i = 0; i < period * 3; i++)
|
||||
{
|
||||
double price = 100 + 10 * Math.Sin(2 * Math.PI * i / period);
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
if (epa.IsHot)
|
||||
{
|
||||
angles.Add(epa.Angle);
|
||||
}
|
||||
}
|
||||
|
||||
// Angle should advance or wrap around (decrease > 300° is a valid wraparound)
|
||||
Assert.True(angles.Count > 0);
|
||||
int advances = 0;
|
||||
for (int i = 1; i < angles.Count; i++)
|
||||
{
|
||||
double delta = angles[i] - angles[i - 1];
|
||||
if (delta >= -0.001)
|
||||
{
|
||||
advances++; // Normal advancement or hold
|
||||
}
|
||||
else if (delta < -300.0)
|
||||
{
|
||||
advances++; // Valid 360° wraparound
|
||||
}
|
||||
// else: backward movement in non-wrap region — allowed by Ehlers' exceptions
|
||||
}
|
||||
// Most transitions should be advancing or wrapping
|
||||
Assert.True(advances > angles.Count / 2,
|
||||
$"Expected majority of angle transitions to advance, got {advances}/{angles.Count}");
|
||||
}
|
||||
|
||||
// ── DerivedPeriod Properties ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DerivedPeriod_AlwaysClampedTo60()
|
||||
{
|
||||
var epa = new Epa(period: 10);
|
||||
var rng = new Random(123);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 20;
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
Assert.True(epa.DerivedPeriod <= 60.0,
|
||||
$"DerivedPeriod {epa.DerivedPeriod} > 60 at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DerivedPeriod_NonNegative()
|
||||
{
|
||||
var epa = new Epa(period: 14);
|
||||
var rng = new Random(456);
|
||||
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 10;
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
Assert.True(epa.DerivedPeriod >= 0.0,
|
||||
$"DerivedPeriod {epa.DerivedPeriod} < 0 at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── TrendState Properties ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TrendState_OnlyValidValues_AllBars()
|
||||
{
|
||||
var epa = new Epa(period: 14);
|
||||
var rng = new Random(789);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 10;
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
Assert.True(epa.TrendState >= -1 && epa.TrendState <= 1,
|
||||
$"Invalid TrendState {epa.TrendState} at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TrendState_HasVariation()
|
||||
{
|
||||
// Over a long enough series with varying data, trend state should not be constant
|
||||
var epa = new Epa(period: 10);
|
||||
var states = new HashSet<int>();
|
||||
var rng = new Random(42);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 20 + 5 * Math.Sin(2 * Math.PI * i / 20.0);
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
if (epa.IsHot)
|
||||
{
|
||||
states.Add(epa.TrendState);
|
||||
}
|
||||
}
|
||||
// Should have at least 2 different states
|
||||
Assert.True(states.Count >= 2,
|
||||
$"Expected at least 2 distinct states, got {states.Count}: [{string.Join(",", states)}]");
|
||||
}
|
||||
|
||||
// ── Deterministic Reproducibility ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Deterministic_SameInput_SameOutput()
|
||||
{
|
||||
var rng1 = new Random(42);
|
||||
var rng2 = new Random(42);
|
||||
var epa1 = new Epa(period: 14);
|
||||
var epa2 = new Epa(period: 14);
|
||||
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
double p1 = 100 + rng1.NextDouble() * 10;
|
||||
double p2 = 100 + rng2.NextDouble() * 10;
|
||||
epa1.Update(new TValue(DateTime.UtcNow.AddDays(i), p1));
|
||||
epa2.Update(new TValue(DateTime.UtcNow.AddDays(i), p2));
|
||||
}
|
||||
|
||||
Assert.Equal(epa1.Angle, epa2.Angle, precision: 14);
|
||||
Assert.Equal(epa1.DerivedPeriod, epa2.DerivedPeriod, precision: 14);
|
||||
Assert.Equal(epa1.TrendState, epa2.TrendState);
|
||||
}
|
||||
|
||||
// ── Consistency: Batch/Streaming/Span ──────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void StreamingVsBatch_Match()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
int n = 200, period = 14;
|
||||
double[] prices = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
prices[i] = 100 + rng.NextDouble() * 10;
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var epa = new Epa(period);
|
||||
double[] streamAngles = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var r = epa.Update(new TValue(DateTime.UtcNow.AddDays(i), prices[i]));
|
||||
streamAngles[i] = r.Value;
|
||||
}
|
||||
|
||||
// Span batch
|
||||
double[] spanAngles = new double[n];
|
||||
Epa.Batch(prices, spanAngles, period);
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
Assert.Equal(streamAngles[i], spanAngles[i], precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchTSeries_MatchesStreaming()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
int n = 200, period = 14;
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
ts.Add(new TValue(DateTime.UtcNow.AddDays(i), 100 + rng.NextDouble() * 10));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var epa = new Epa(period);
|
||||
foreach (var tv in ts)
|
||||
{
|
||||
epa.Update(tv);
|
||||
}
|
||||
|
||||
// Batch(TSeries)
|
||||
var batchResult = Epa.Batch(ts, period);
|
||||
|
||||
Assert.Equal(epa.Angle, batchResult[^1].Value, precision: 10);
|
||||
}
|
||||
|
||||
// ── Reset/Reprocess ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ResetReprocess_MatchesOriginal()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
int n = 100, period = 14;
|
||||
var epa = new Epa(period);
|
||||
double[] prices = new double[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
prices[i] = 100 + rng.NextDouble() * 10;
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), prices[i]));
|
||||
}
|
||||
double angle1 = epa.Angle;
|
||||
double dp1 = epa.DerivedPeriod;
|
||||
int ts1 = epa.TrendState;
|
||||
|
||||
epa.Reset();
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), prices[i]));
|
||||
}
|
||||
|
||||
Assert.Equal(angle1, epa.Angle, precision: 14);
|
||||
Assert.Equal(dp1, epa.DerivedPeriod, precision: 14);
|
||||
Assert.Equal(ts1, epa.TrendState);
|
||||
}
|
||||
|
||||
// ── Period Sensitivity ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_DifferentAngle()
|
||||
{
|
||||
var rng = new Random(42);
|
||||
var epa10 = new Epa(period: 10);
|
||||
var epa28 = new Epa(period: 28);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 10;
|
||||
var tv = new TValue(DateTime.UtcNow.AddDays(i), price);
|
||||
epa10.Update(tv);
|
||||
epa28.Update(tv);
|
||||
}
|
||||
|
||||
Assert.NotEqual(epa10.Angle, epa28.Angle);
|
||||
}
|
||||
|
||||
// ── Finite Output for All Bars ─────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void AllOutputs_AlwaysFinite()
|
||||
{
|
||||
var epa = new Epa(period: 14);
|
||||
var rng = new Random(42);
|
||||
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100 + rng.NextDouble() * 10;
|
||||
epa.Update(new TValue(DateTime.UtcNow.AddDays(i), price));
|
||||
Assert.True(double.IsFinite(epa.Angle), $"Non-finite Angle at bar {i}");
|
||||
Assert.True(double.IsFinite(epa.DerivedPeriod), $"Non-finite DerivedPeriod at bar {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -577,6 +577,7 @@ HAS_ACP = _bind("qtl_acp", [_dp, _ci, _dp, _ci, _ci, _ci, _ci])
|
||||
HAS_LPF = _bind("qtl_lpf", [_dp, _ci, _dp, _ci, _ci, _ci])
|
||||
HAS_AMFM = _bind("qtl_amfm", [_dp, _dp, _ci, _dp, _dp, _ci])
|
||||
HAS_FSI = _bind("qtl_fsi", [_dp, _ci, _dp, _ci, _cd])
|
||||
HAS_EPA = _bind("qtl_epa", [_dp, _ci, _dp, _ci])
|
||||
|
||||
# ── Numerics (Exports.cs — manual) ──
|
||||
HAS_CHANGE = _bind("qtl_change", [_dp, _ci, _dp, _ci])
|
||||
|
||||
@@ -24,6 +24,7 @@ __all__ = [
|
||||
"acp",
|
||||
"amfm",
|
||||
"fsi",
|
||||
"epa",
|
||||
]
|
||||
|
||||
|
||||
@@ -182,3 +183,12 @@ def fsi(close: object, period: int = 20, bandwidth: float = 0.1,
|
||||
src, idx = _arr(close); n = len(src); dst = _out(n)
|
||||
_check(_lib.qtl_fsi(_ptr(src), n, _ptr(dst), period, float(bandwidth)))
|
||||
return _wrap(dst, idx, f"FSI_{period}", "cycles", offset)
|
||||
|
||||
|
||||
def epa(close: object, period: int = 28,
|
||||
offset: int = 0, **kwargs) -> object:
|
||||
"""Ehlers Phasor Analysis."""
|
||||
period = int(kwargs.get("length", period)); offset = int(offset)
|
||||
src, idx = _arr(close); n = len(src); dst = _out(n)
|
||||
_check(_lib.qtl_epa(_ptr(src), n, _ptr(dst), period))
|
||||
return _wrap(dst, idx, f"EPA_{period}", "cycles", offset)
|
||||
|
||||
@@ -1566,6 +1566,16 @@ public static unsafe partial class Exports
|
||||
catch { return StatusCodes.QTL_ERR_INTERNAL; }
|
||||
}
|
||||
|
||||
// Epa: Pattern A (src → dst, int period)
|
||||
[UnmanagedCallersOnly(EntryPoint = "qtl_epa")]
|
||||
public static int QtlEpa(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 { Epa.Batch(Src(src, n), Dst(dst, n), period); return StatusCodes.QTL_OK; }
|
||||
catch { return StatusCodes.QTL_ERR_INTERNAL; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// §8.14 Numerics / transforms
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user