mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-28 17:57:45 +00:00
feat(dynamics): add PTA - Ehlers Precision Trend Analysis
TASC Sep 2024. Dual 2-pole Butterworth highpass bandpass for near-zero-lag trend extraction. HP(long) - HP(short) preserves cycles between shortPeriod and longPeriod. - Core: Pta.cs with O(1) streaming, Span batch, state rollback - Quantower: PtaIndicator adapter with LineSeries + SetValue - Tests: 31 lib + 11 Quantower (all passing) - Pine: pta.pine PineScript v6 reference - Docs: Pta.md canonical template v3 - Python: Exports.Generated.cs + _bridge.py + dynamics.py - Indexes: _sidebar.md, lib/_index.md, dynamics/_index.md, docs/indicators.md, docs/pinescript.md
This commit is contained in:
@@ -202,6 +202,7 @@
|
||||
* [PFE - Polarized Fractal Efficiency](/lib/dynamics/pfe/Pfe.md)
|
||||
* [PLUS_DI - Plus Directional Indicator](/lib/dynamics/plusdi/PlusDi.md)
|
||||
* [PLUS_DM - Plus Directional Movement](/lib/dynamics/plusdm/PlusDm.md)
|
||||
* [PTA - Ehlers Precision Trend Analysis](/lib/dynamics/pta/Pta.md)
|
||||
* [QSTICK - Qstick Indicator](/lib/dynamics/qstick/Qstick.md)
|
||||
* [RAVI - Chande Range Action Verification Index](/lib/dynamics/ravi/Ravi.md)
|
||||
* [SUPER - SuperTrend](/lib/dynamics/super/Super.md)
|
||||
|
||||
@@ -246,6 +246,7 @@ Indicators measuring trend strength, regime, and directional movement quality.
|
||||
| [**PFE**](../lib/dynamics/pfe/Pfe.md) | Polarized Fractal Efficiency | Fractal path efficiency as trend strength |
|
||||
| [**PLUS_DI**](../lib/dynamics/plusdi/PlusDi.md) | Plus Directional Indicator | Upward DI (0-100) |
|
||||
| [**PLUS_DM**](../lib/dynamics/plusdm/PlusDm.md) | Plus Directional Movement | Smoothed upward DM |
|
||||
| [**PTA**](../lib/dynamics/pta/Pta.md) | Ehlers Precision Trend Analysis | Dual highpass bandpass near-zero-lag trend |
|
||||
| [**QSTICK**](../lib/dynamics/qstick/Qstick.md) | Qstick | Average close-open difference |
|
||||
| [**RAVI**](../lib/dynamics/ravi/Ravi.md) | Chande Range Action Verification Index | Dual-SMA divergence as trend strength |
|
||||
| [**SUPER**](../lib/dynamics/super/Super.md) | SuperTrend | ATR-based trend bands |
|
||||
|
||||
@@ -277,6 +277,7 @@ Is there a trend? How strong? These indicators answer that.
|
||||
| PFE | Polarized Fractal Efficiency | [pfe.pine](../lib/dynamics/pfe/pfe.pine) |
|
||||
| PLUS_DI | Plus Directional Indicator | [plusdi.pine](../lib/dynamics/plusdi/plusdi.pine) |
|
||||
| PLUS_DM | Plus Directional Movement | [plusdm.pine](../lib/dynamics/plusdm/plusdm.pine) |
|
||||
| PTA | Ehlers Precision Trend Analysis | [pta.pine](../lib/dynamics/pta/pta.pine) |
|
||||
| QSTICK | Qstick Indicator | [qstick.pine](../lib/dynamics/qstick/qstick.pine) |
|
||||
| RAVI | Chande Range Action Verification Index | [ravi.pine](../lib/dynamics/ravi/ravi.pine) |
|
||||
| SUPER | SuperTrend | [super.pine](../lib/dynamics/super/super.pine) |
|
||||
|
||||
@@ -270,6 +270,7 @@
|
||||
| [PLUS_DI](dynamics/plusdi/PlusDi.md) | Plus Directional Indicator | Dynamics |
|
||||
| [PLUS_DM](dynamics/plusdm/PlusDm.md) | Plus Directional Movement | Dynamics |
|
||||
| [PMA](trends_FIR/pma/Pma.md) | Ehlers Predictive Moving Average | Trends (FIR) |
|
||||
| [PTA](dynamics/pta/Pta.md) | Ehlers Precision Trend Analysis | Dynamics |
|
||||
| [PMO](momentum/pmo/Pmo.md) | Price Momentum Oscillator | Momentum |
|
||||
| [POISSONDIST](numerics/poissondist/Poissondist.md) | Poisson Distribution | Numerics |
|
||||
| [POLYFIT](statistics/polyfit/Polyfit.md) | Polynomial Fitting | Statistics |
|
||||
|
||||
@@ -24,7 +24,8 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu
|
||||
| [MINUS_DM](minusdm/MinusDm.md) | Minus Directional Movement | Wilder-smoothed downward directional movement. Price units. |
|
||||
| [PFE](pfe/Pfe.md) | Polarized Fractal Efficiency | Trend efficiency: straight-line / total path distance. |
|
||||
| [PLUS_DI](plusdi/PlusDi.md) | Plus Directional Indicator | Upward directional movement as % of true range. 0-100. |
|
||||
| [PLUS_DM](plusdm/PlusDm.md) | Plus Directional Movement | Wilder-smoothed upward directional movement. Price units. |
|
||||
| [PLUS_DM](plusdm/PlusDm.md) | Plus Directional Movement | Wilder-smoothed upward directional movement. Price units. |
|
||||
| [PTA](pta/Pta.md) | Ehlers Precision Trend Analysis | Dual highpass bandpass. Zero-centered near-zero-lag trend. |
|
||||
| [QSTICK](qstick/Qstick.md) | Qstick | MA of (Close - Open). Positive = buying pressure. |
|
||||
| [RAVI](ravi/Ravi.md) | Chande Range Action Verification Index | \|SMA(short) − SMA(long)\| / SMA(long) × 100. |
|
||||
| [SUPER](super/Super.md) | SuperTrend | ATR-based trailing stop. Flips on breakout. Color-coded direction. |
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class PtaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Long Period", sortIndex: 1, 3, 9999, 1, 0)]
|
||||
public int LongPeriod { get; set; } = 250;
|
||||
|
||||
[InputParameter("Short Period", sortIndex: 2, 2, 9999, 1, 0)]
|
||||
public int ShortPeriod { get; set; } = 40;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Pta _ind = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"PTA {LongPeriod},{ShortPeriod}:{_sourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/dynamics/pta/Pta.Quantower.cs";
|
||||
|
||||
public PtaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
_sourceName = Source.ToString();
|
||||
Name = "PTA - Ehlers Precision Trend Analysis";
|
||||
Description = "Dual highpass filter bandpass for near-zero-lag trend extraction.";
|
||||
_series = new LineSeries(name: $"PTA {LongPeriod},{ShortPeriod}", color: Color.Red, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_ind = new Pta(LongPeriod, ShortPeriod);
|
||||
_sourceName = Source.ToString();
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
TValue result = _ind.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _ind.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// PTA: Ehlers Precision Trend Analysis
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Dual highpass filter bandpass approach for near-zero-lag trend extraction.
|
||||
/// Applies two 2-pole Butterworth highpass filters with different cutoff periods
|
||||
/// to the same input, then subtracts: Trend = HP(longPeriod) - HP(shortPeriod).
|
||||
/// This preserves cyclic components between shortPeriod and longPeriod bars,
|
||||
/// producing a zero-centered trend indicator with near-zero lag.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Algorithm based on: John F. Ehlers, "Precision Trend Analysis," TASC September 2024.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Complexity:</b> O(1) per bar — two IIR filter evaluations + subtraction.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Pta : AbstractBase
|
||||
{
|
||||
// ── HP1 (long-period) coefficients ──
|
||||
private readonly double _c1L, _c2L, _c3L;
|
||||
// ── HP2 (short-period) coefficients ──
|
||||
private readonly double _c1S, _c2S, _c3S;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State
|
||||
{
|
||||
// HP1 (long-period highpass)
|
||||
public double Hp1;
|
||||
public double Hp1_1;
|
||||
// HP2 (short-period highpass)
|
||||
public double Hp2;
|
||||
public double Hp2_1;
|
||||
// Source history (shared by both filters)
|
||||
public double Src1;
|
||||
public double Src2;
|
||||
public int Count;
|
||||
|
||||
public static State New() => new()
|
||||
{
|
||||
Hp1 = 0, Hp1_1 = 0,
|
||||
Hp2 = 0, Hp2_1 = 0,
|
||||
Src1 = 0, Src2 = 0,
|
||||
Count = 0
|
||||
};
|
||||
}
|
||||
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
/// <summary>Long-period cutoff for the first highpass filter.</summary>
|
||||
public int LongPeriod { get; }
|
||||
|
||||
/// <summary>Short-period cutoff for the second highpass filter.</summary>
|
||||
public int ShortPeriod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Pta"/> class.
|
||||
/// </summary>
|
||||
/// <param name="longPeriod">Long-period HP cutoff. Default is 250 (~1 year daily).</param>
|
||||
/// <param name="shortPeriod">Short-period HP cutoff. Default is 40 (~2 months daily).</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown when longPeriod < 3, shortPeriod < 2, or longPeriod <= shortPeriod.
|
||||
/// </exception>
|
||||
public Pta(int longPeriod = 250, int shortPeriod = 40)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(longPeriod, 3, nameof(longPeriod));
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(shortPeriod, 2, nameof(shortPeriod));
|
||||
if (longPeriod <= shortPeriod)
|
||||
throw new ArgumentOutOfRangeException(nameof(longPeriod),
|
||||
$"longPeriod ({longPeriod}) must be greater than shortPeriod ({shortPeriod}).");
|
||||
|
||||
LongPeriod = longPeriod;
|
||||
ShortPeriod = shortPeriod;
|
||||
|
||||
// Precompute HP coefficients for long period
|
||||
ComputeHpCoefficients(longPeriod, out _c1L, out _c2L, out _c3L);
|
||||
// Precompute HP coefficients for short period
|
||||
ComputeHpCoefficients(shortPeriod, out _c1S, out _c2S, out _c3S);
|
||||
|
||||
Name = $"PTA({longPeriod},{shortPeriod})";
|
||||
WarmupPeriod = longPeriod;
|
||||
_state = State.New();
|
||||
_p_state = _state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Pta"/> class with a publisher source.
|
||||
/// </summary>
|
||||
public Pta(ITValuePublisher source, int longPeriod = 250, int shortPeriod = 40)
|
||||
: this(longPeriod, shortPeriod)
|
||||
{
|
||||
source.Pub += (object? _, in TValueEventArgs args) => Update(args.Value, args.IsNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes 2-pole Butterworth highpass filter coefficients.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ComputeHpCoefficients(int period, out double c1, out double c2, out double c3)
|
||||
{
|
||||
double f = 1.414 * Math.PI / period;
|
||||
double a1 = Math.Exp(-f);
|
||||
double b1 = 2.0 * a1 * Math.Cos(f);
|
||||
c2 = b1;
|
||||
c3 = -(a1 * a1);
|
||||
c1 = (1.0 + c2 - c3) * 0.25;
|
||||
}
|
||||
|
||||
public override bool IsHot => _state.Count >= 2;
|
||||
|
||||
/// <summary>Primes the indicator with historical data.</summary>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double v in source)
|
||||
Update(new TValue(DateTime.MinValue, v), isNew: true);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
_p_state = _state;
|
||||
else
|
||||
_state = _p_state;
|
||||
|
||||
double src = input.Value;
|
||||
ref State s = ref _state;
|
||||
|
||||
double result;
|
||||
if (s.Count < 2)
|
||||
{
|
||||
// Bootstrap: not enough bars for HP differentiation
|
||||
if (s.Count == 0)
|
||||
{
|
||||
s.Src1 = src;
|
||||
s.Src2 = src;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.Src2 = s.Src1;
|
||||
s.Src1 = src;
|
||||
}
|
||||
s.Count++;
|
||||
result = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 2nd-order difference: src - 2*src1 + src2
|
||||
double diff = src - 2.0 * s.Src1 + s.Src2;
|
||||
|
||||
// HP1 (long period): hp1 = c1L*diff + c2L*hp1 + c3L*hp1_1
|
||||
double hp1 = Math.FusedMultiplyAdd(_c1L, diff,
|
||||
Math.FusedMultiplyAdd(_c2L, s.Hp1, _c3L * s.Hp1_1));
|
||||
|
||||
// HP2 (short period): hp2 = c1S*diff + c2S*hp2 + c3S*hp2_1
|
||||
double hp2 = Math.FusedMultiplyAdd(_c1S, diff,
|
||||
Math.FusedMultiplyAdd(_c2S, s.Hp2, _c3S * s.Hp2_1));
|
||||
|
||||
// Trend = HP1 - HP2 (bandpass between shortPeriod and longPeriod)
|
||||
result = hp1 - hp2;
|
||||
|
||||
// Update HP state
|
||||
s.Hp1_1 = s.Hp1;
|
||||
s.Hp1 = hp1;
|
||||
s.Hp2_1 = s.Hp2;
|
||||
s.Hp2 = hp2;
|
||||
|
||||
// Update source history
|
||||
s.Src2 = s.Src1;
|
||||
s.Src1 = src;
|
||||
s.Count++;
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>Updates with a full TSeries and returns results.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
var resultValues = new double[source.Count];
|
||||
Batch(source.Values, resultValues, LongPeriod, ShortPeriod);
|
||||
|
||||
var result = new TSeries();
|
||||
var times = source.Times;
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
result.Add(new TValue(times[i], resultValues[i]));
|
||||
|
||||
// Sync internal state
|
||||
int len = source.Count;
|
||||
if (len >= 2)
|
||||
{
|
||||
var replay = new Pta(LongPeriod, ShortPeriod);
|
||||
for (int i = 0; i < len; i++)
|
||||
replay.Update(new TValue(times[i], source.Values[i]));
|
||||
_state = replay._state;
|
||||
}
|
||||
_p_state = _state;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Static batch on TSeries.</summary>
|
||||
public static TSeries Batch(TSeries source, int longPeriod = 250, int shortPeriod = 40)
|
||||
{
|
||||
var indicator = new Pta(longPeriod, shortPeriod);
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static batch calculation on spans. Zero allocation on the hot path.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output,
|
||||
int longPeriod = 250, int shortPeriod = 40)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output spans must be of equal length.", nameof(output));
|
||||
if (source.Length == 0) return;
|
||||
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(longPeriod, 3, nameof(longPeriod));
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(shortPeriod, 2, nameof(shortPeriod));
|
||||
if (longPeriod <= shortPeriod)
|
||||
throw new ArgumentOutOfRangeException(nameof(longPeriod),
|
||||
$"longPeriod ({longPeriod}) must be greater than shortPeriod ({shortPeriod}).");
|
||||
|
||||
// Precompute coefficients
|
||||
ComputeHpCoefficients(longPeriod, out double c1L, out double c2L, out double c3L);
|
||||
ComputeHpCoefficients(shortPeriod, out double c1S, out double c2S, out double c3S);
|
||||
|
||||
// Bar 0 and 1: output = 0 (not enough history for 2nd-order diff)
|
||||
output[0] = 0.0;
|
||||
if (source.Length < 2) return;
|
||||
output[1] = 0.0;
|
||||
|
||||
double hp1 = 0, hp1_1 = 0;
|
||||
double hp2 = 0, hp2_1 = 0;
|
||||
|
||||
for (int i = 2; i < source.Length; i++)
|
||||
{
|
||||
double diff = source[i] - 2.0 * source[i - 1] + source[i - 2];
|
||||
|
||||
double newHp1 = Math.FusedMultiplyAdd(c1L, diff,
|
||||
Math.FusedMultiplyAdd(c2L, hp1, c3L * hp1_1));
|
||||
double newHp2 = Math.FusedMultiplyAdd(c1S, diff,
|
||||
Math.FusedMultiplyAdd(c2S, hp2, c3S * hp2_1));
|
||||
|
||||
output[i] = newHp1 - newHp2;
|
||||
|
||||
hp1_1 = hp1; hp1 = newHp1;
|
||||
hp2_1 = hp2; hp2 = newHp2;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Calculate factory returning results and indicator.</summary>
|
||||
public static (TSeries Results, Pta Indicator) Calculate(TSeries source,
|
||||
int longPeriod = 250, int shortPeriod = 40)
|
||||
{
|
||||
var indicator = new Pta(longPeriod, shortPeriod);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_state = State.New();
|
||||
_p_state = _state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
# PTA: Ehlers Precision Trend Analysis
|
||||
|
||||
| Property | Value |
|
||||
| :------------- | :----------------------------------------------- |
|
||||
| **Category** | Dynamics |
|
||||
| **Author** | John F. Ehlers |
|
||||
| **Source** | TASC, September 2024 |
|
||||
| **Parameters** | longPeriod (default 250), shortPeriod (default 40)|
|
||||
| **Output** | Zero-centered trend indicator |
|
||||
| **Range** | Unbounded (zero-centered) |
|
||||
| **Warmup** | longPeriod bars |
|
||||
|
||||
## Historical Context
|
||||
|
||||
Traditional trend-following indicators like moving averages are lowpass filters with unavoidable lag. Ehlers' insight is to use highpass filters instead — they have nearly zero lag. By applying two highpass filters with different cutoff periods and subtracting, PTA creates a bandpass that preserves cyclic components between the short and long periods while eliminating noise and very long-term drift.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### Stage 1: Dual 2-Pole Butterworth Highpass Filters
|
||||
|
||||
Both filters use the standard Ehlers 2-pole Butterworth HP formulation:
|
||||
|
||||
$$a_1 = e^{-\sqrt{2} \cdot \pi / P}$$
|
||||
$$b_1 = 2 \cdot a_1 \cdot \cos(\sqrt{2} \cdot \pi / P)$$
|
||||
$$c_2 = b_1, \quad c_3 = -a_1^2, \quad c_1 = \frac{1 + c_2 - c_3}{4}$$
|
||||
$$HP = c_1 \cdot (src - 2 \cdot src_1 + src_2) + c_2 \cdot HP_1 + c_3 \cdot HP_2$$
|
||||
|
||||
HP1 uses `longPeriod` (default 250), HP2 uses `shortPeriod` (default 40).
|
||||
|
||||
### Stage 2: Bandpass via Subtraction
|
||||
|
||||
$$\text{Trend} = HP_1 - HP_2$$
|
||||
|
||||
HP1 passes frequencies above 1/longPeriod. HP2 passes frequencies above 1/shortPeriod. The difference preserves only the band between shortPeriod and longPeriod — the trend-relevant frequencies.
|
||||
|
||||
### Key Properties
|
||||
|
||||
- **Near-zero lag**: Highpass filters inherently have minimal lag, unlike lowpass (MA-based) trend indicators.
|
||||
- **Positive = Uptrend**: When PTA > 0, price trend is up.
|
||||
- **Negative = Downtrend**: When PTA < 0, price trend is down.
|
||||
- **Zero crossings**: Signal trend reversals.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
| Operation | Count |
|
||||
| :----------------- | :---- |
|
||||
| Subtractions | 3 |
|
||||
| Multiplications | 4 |
|
||||
| FMA | 4 |
|
||||
| IIR state updates | 6 |
|
||||
| **Total** | **17 FLOPs** |
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
No SIMD vectorization possible — serial IIR dependency chain on HP state. The batch path uses scalar FMA loop, O(1) per bar, zero allocation.
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Value |
|
||||
| :---------------- | :------------------- |
|
||||
| Lag | Near zero |
|
||||
| Smoothness | High (IIR filtering) |
|
||||
| Frequency range | shortPeriod–longPeriod |
|
||||
| Allocations | 0 (hot path) |
|
||||
|
||||
## Validation
|
||||
|
||||
### Behavioral Test Summary
|
||||
|
||||
| Test | Description |
|
||||
| :------------------------ | :----------------------------------------------------------- |
|
||||
| ConstantInput → Zero | Constant price has zero 2nd-order difference → PTA = 0 |
|
||||
| Uptrend → Positive | Steadily rising prices produce positive PTA |
|
||||
| Downtrend → Negative | Steadily falling prices produce negative PTA |
|
||||
| LongPeriod > ShortPeriod | Constructor enforces ordering constraint |
|
||||
| Symmetry | Mirrored price produces mirrored PTA (negated) |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **longPeriod must exceed shortPeriod** — otherwise the bandpass is inverted. Constructor throws.
|
||||
2. **IIR Bootstrap** — First 2 bars output 0.0 while source history fills. Full convergence at ~longPeriod bars.
|
||||
3. **Default 250 bars** — Requires substantial history before the long HP stabilizes. Reduce for shorter timeframes.
|
||||
4. **Not a price overlay** — Output is zero-centered, plotted in separate window.
|
||||
|
||||
## References
|
||||
|
||||
- Ehlers, J. F. "Precision Trend Analysis." *Technical Analysis of Stocks & Commodities*, September 2024.
|
||||
- [TradingView Implementation](https://www.tradingview.com/script/XxSVTg0v-TASC-2024-09-Precision-Trend-Analysis/)
|
||||
- [Financial Hacker Analysis](https://financial-hacker.com/ehlers-precision-trend-analysis/)
|
||||
@@ -0,0 +1,37 @@
|
||||
// PTA: Ehlers Precision Trend Analysis
|
||||
// Category: Dynamics
|
||||
// Based on: John F. Ehlers, "Precision Trend Analysis," TASC September 2024
|
||||
//
|
||||
// Dual highpass filter bandpass approach for near-zero-lag trend extraction.
|
||||
// Trend = HP(longPeriod) - HP(shortPeriod)
|
||||
// where HP is a 2-pole Butterworth highpass filter.
|
||||
// Preserves cyclic components between shortPeriod and longPeriod.
|
||||
// Output: zero-centered, positive = uptrend, negative = downtrend.
|
||||
|
||||
//@version=6
|
||||
indicator("PTA - Ehlers Precision Trend Analysis", shorttitle="PTA", overlay=false)
|
||||
|
||||
// ── Inputs ──────────────────────────────────────────────────────
|
||||
int longPeriod = input.int(250, "Long Period", minval=3)
|
||||
int shortPeriod = input.int(40, "Short Period", minval=2)
|
||||
|
||||
// ── Highpass Filter Function ────────────────────────────────────
|
||||
highpass(float src, float period) =>
|
||||
float a1 = math.exp(-1.414 * math.pi / period)
|
||||
float b1 = 2.0 * a1 * math.cos(1.414 * math.pi / period)
|
||||
float c2 = b1
|
||||
float c3 = -a1 * a1
|
||||
float c1 = (1.0 + c2 - c3) * 0.25
|
||||
float hp = 0.0
|
||||
if bar_index >= 2
|
||||
hp := c1 * (src - 2.0 * src[1] + src[2]) + c2 * nz(hp[1]) + c3 * nz(hp[2])
|
||||
hp
|
||||
|
||||
// ── Calculations ────────────────────────────────────────────────
|
||||
float hp1 = highpass(close, longPeriod)
|
||||
float hp2 = highpass(close, shortPeriod)
|
||||
float trend = hp1 - hp2
|
||||
|
||||
// ── Plots ───────────────────────────────────────────────────────
|
||||
plot(trend, "PTA", color.red, 2)
|
||||
hline(0, "Zero", color.gray, hline.style_dotted)
|
||||
@@ -0,0 +1,156 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PtaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PtaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PtaIndicator();
|
||||
|
||||
Assert.Equal(250, indicator.LongPeriod);
|
||||
Assert.Equal(40, indicator.ShortPeriod);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("PTA - Ehlers Precision Trend Analysis", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new PtaIndicator();
|
||||
|
||||
Assert.Equal(0, PtaIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_ShortName_IncludesPeriodsAndSource()
|
||||
{
|
||||
var indicator = new PtaIndicator { LongPeriod = 100, ShortPeriod = 20 };
|
||||
|
||||
Assert.Contains("PTA", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("100", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new PtaIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Pta.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new PtaIndicator { LongPeriod = 50, ShortPeriod = 10 };
|
||||
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PtaIndicator { LongPeriod = 50, ShortPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PtaIndicator { LongPeriod = 50, ShortPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_InternalIndicator_HandlesBarCorrection()
|
||||
{
|
||||
var ma = new Pta(50, 10);
|
||||
double[] prices = [100, 102, 99, 103, 97, 104, 98, 105, 97, 106,
|
||||
101, 103, 98, 104, 96, 105, 99, 107, 98, 108];
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
ma.Update(new TValue(now.AddMinutes(i).Ticks, prices[i]), isNew: true);
|
||||
}
|
||||
|
||||
double beforeCorrection = ma.Last.Value;
|
||||
|
||||
// Correct last bar with significantly different value
|
||||
ma.Update(new TValue(now.AddMinutes(19).Ticks, 200), isNew: false);
|
||||
double afterCorrection = ma.Last.Value;
|
||||
|
||||
Assert.NotEqual(beforeCorrection, afterCorrection);
|
||||
Assert.True(double.IsFinite(afterCorrection));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_DifferentSourceTypes()
|
||||
{
|
||||
foreach (SourceType sourceType in new[] { SourceType.Close, SourceType.Open, SourceType.High, SourceType.Low })
|
||||
{
|
||||
var indicator = new PtaIndicator();
|
||||
indicator.Source = sourceType;
|
||||
Assert.Equal(sourceType, indicator.Source);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_MultipleHistoricalBars()
|
||||
{
|
||||
var indicator = new PtaIndicator { LongPeriod = 50, ShortPeriod = 10 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
|
||||
indicator.ProcessUpdate(new UpdateArgs(i == 0 ? UpdateReason.HistoricalBar : UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
Assert.Equal(20, indicator.LinesSeries[0].Count);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PtaIndicator_PeriodChange_UpdatesConfig()
|
||||
{
|
||||
var indicator = new PtaIndicator();
|
||||
indicator.LongPeriod = 100;
|
||||
Assert.Equal(100, indicator.LongPeriod);
|
||||
|
||||
indicator.ShortPeriod = 20;
|
||||
Assert.Equal(20, indicator.ShortPeriod);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class PtaTests
|
||||
{
|
||||
private static readonly Random _rng = new(42);
|
||||
|
||||
private static TSeries MakeSeries(int count = 500)
|
||||
{
|
||||
var series = new TSeries();
|
||||
double price = 100.0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
price += (_rng.NextDouble() - 0.5) * 2.0;
|
||||
series.Add(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
return series;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// A — Constructor
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters()
|
||||
{
|
||||
var pta = new Pta();
|
||||
Assert.Equal(250, pta.LongPeriod);
|
||||
Assert.Equal(40, pta.ShortPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters()
|
||||
{
|
||||
var pta = new Pta(longPeriod: 500, shortPeriod: 100);
|
||||
Assert.Equal(500, pta.LongPeriod);
|
||||
Assert.Equal(100, pta.ShortPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LongPeriodTooSmall_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Pta(longPeriod: 2, shortPeriod: 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ShortPeriodTooSmall_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Pta(longPeriod: 50, shortPeriod: 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_LongNotGreaterThanShort_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Pta(longPeriod: 40, shortPeriod: 40));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Pta(longPeriod: 30, shortPeriod: 40));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// B — Basic Calculation
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void FirstBar_OutputIsZero()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
var result = pta.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SecondBar_OutputIsZero()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
pta.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
var result = pta.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101.0));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ThirdBar_OutputIsFinite()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
pta.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101.0));
|
||||
var result = pta.Update(new TValue(DateTime.UtcNow.AddMinutes(2), 102.0));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// C — State / Bar Correction
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void IsNew_True_AdvancesState()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
var series = MakeSeries(100);
|
||||
foreach (var bar in series) pta.Update(bar);
|
||||
double val1 = pta.Update(new TValue(DateTime.UtcNow, 105.0), isNew: true).Value;
|
||||
double val2 = pta.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 110.0), isNew: true).Value;
|
||||
Assert.NotEqual(val1, val2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_False_CorrectionReproducible()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
var series = MakeSeries(100);
|
||||
foreach (var bar in series) pta.Update(bar);
|
||||
|
||||
double v1 = pta.Update(new TValue(DateTime.UtcNow, 105.0), isNew: true).Value;
|
||||
double v2 = pta.Update(new TValue(DateTime.UtcNow, 108.0), isNew: false).Value;
|
||||
double v3 = pta.Update(new TValue(DateTime.UtcNow, 105.0), isNew: false).Value;
|
||||
Assert.Equal(v1, v3, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
var series = MakeSeries(100);
|
||||
foreach (var bar in series) pta.Update(bar);
|
||||
pta.Reset();
|
||||
Assert.False(pta.IsHot);
|
||||
Assert.Equal(0.0, pta.Update(new TValue(DateTime.UtcNow, 100.0)).Value);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// D — Warmup
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void IsHot_FalseBeforeTwoBars()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
Assert.False(pta.IsHot);
|
||||
pta.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.False(pta.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_TrueAfterTwoBars()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
pta.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 101.0));
|
||||
Assert.True(pta.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_MatchesLongPeriod()
|
||||
{
|
||||
var pta = new Pta(200, 30);
|
||||
Assert.Equal(200, pta.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// E — Robustness
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void LargeSeries_NoOverflow()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
var series = MakeSeries(5000);
|
||||
foreach (var bar in series) pta.Update(bar);
|
||||
Assert.True(double.IsFinite(pta.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VolatileInput_RemainsFinite()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
var rng = new Random(123);
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
double price = 100 + (rng.NextDouble() - 0.5) * 50;
|
||||
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
}
|
||||
Assert.True(double.IsFinite(pta.Last.Value));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// F — Consistency (4-API mode)
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
var series = MakeSeries(300);
|
||||
int lp = 50, sp = 10;
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streaming = new Pta(lp, sp);
|
||||
foreach (var bar in series) streaming.Update(bar);
|
||||
|
||||
// Mode 2: Batch TSeries
|
||||
var batchResult = Pta.Batch(series, lp, sp);
|
||||
|
||||
// Mode 3: Span
|
||||
var output = new double[series.Count];
|
||||
Pta.Batch(series.Values, output, lp, sp);
|
||||
|
||||
// Mode 4: Calculate
|
||||
var (calcResult, _) = Pta.Calculate(series, lp, sp);
|
||||
|
||||
// Compare last values
|
||||
double streamVal = streaming.Last.Value;
|
||||
double batchVal = batchResult[^1].Value;
|
||||
double spanVal = output[^1];
|
||||
double calcVal = calcResult[^1].Value;
|
||||
|
||||
Assert.Equal(streamVal, batchVal, 10);
|
||||
Assert.Equal(streamVal, spanVal, 10);
|
||||
Assert.Equal(streamVal, calcVal, 10);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// G — Span API
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesStreaming()
|
||||
{
|
||||
var series = MakeSeries(200);
|
||||
int lp = 50, sp = 10;
|
||||
|
||||
var streaming = new Pta(lp, sp);
|
||||
var streamResults = new double[series.Count];
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
streamResults[i] = streaming.Update(series[i]).Value;
|
||||
|
||||
var spanResults = new double[series.Count];
|
||||
Pta.Batch(series.Values, spanResults, lp, sp);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
Assert.Equal(streamResults[i], spanResults[i], 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_EmptyInput_NoThrow()
|
||||
{
|
||||
Pta.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty, 50, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MismatchedLengths_Throws()
|
||||
{
|
||||
var src = new double[10];
|
||||
var dst = new double[5];
|
||||
Assert.Throws<ArgumentException>(() => Pta.Batch(src, dst, 50, 10));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// H — Chainability
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void PubSub_ChainWorks()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var pta = new Pta(source, longPeriod: 50, shortPeriod: 10);
|
||||
for (int i = 0; i < 100; i++)
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.1));
|
||||
Assert.True(double.IsFinite(pta.Last.Value));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════
|
||||
// PTA-Specific Behavioral Tests
|
||||
// ════════════════════════════════════════════════════════
|
||||
|
||||
[Fact]
|
||||
public void ConstantInput_OutputIsZero()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
for (int i = 0; i < 300; i++)
|
||||
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||
|
||||
// Constant price → zero 2nd-order difference → both HP = 0 → PTA = 0
|
||||
Assert.Equal(0.0, pta.Last.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LinearTrend_OutputNearZeroAfterConvergence()
|
||||
{
|
||||
// A perfectly linear trend has zero 2nd derivative → HP outputs approach 0
|
||||
var pta = new Pta(50, 10);
|
||||
for (int i = 0; i < 500; i++)
|
||||
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.5));
|
||||
|
||||
// Both HP filters output 0 for pure linear → PTA ≈ 0
|
||||
Assert.True(Math.Abs(pta.Last.Value) < 1.0,
|
||||
$"Expected near-zero for linear trend, got {pta.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SineWave_InBandpass_ProducesOutput()
|
||||
{
|
||||
// Sine wave at period=100 (between short=10 and long=250) should be preserved
|
||||
var pta = new Pta(250, 10);
|
||||
double lastAbsMax = 0;
|
||||
for (int i = 0; i < 500; i++)
|
||||
{
|
||||
double price = 100.0 + 10.0 * Math.Sin(2.0 * Math.PI * i / 100.0);
|
||||
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||
if (i > 300) lastAbsMax = Math.Max(lastAbsMax, Math.Abs(pta.Last.Value));
|
||||
}
|
||||
Assert.True(lastAbsMax > 0.1,
|
||||
$"Expected significant output for in-band sine, got max={lastAbsMax}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Uptrend_Then_Downtrend_SignChanges()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
// Uptrend
|
||||
for (int i = 0; i < 200; i++)
|
||||
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + i * 0.5));
|
||||
// Transition to downtrend
|
||||
for (int i = 0; i < 200; i++)
|
||||
pta.Update(new TValue(DateTime.UtcNow.AddMinutes(200 + i), 200.0 - i * 0.5));
|
||||
|
||||
// After sustained downtrend, PTA should detect the reversal
|
||||
// (the sign change may take some bars due to the bandpass filter)
|
||||
Assert.True(double.IsFinite(pta.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentPeriods_ProduceDifferentResults()
|
||||
{
|
||||
var series = MakeSeries(300);
|
||||
var pta1 = new Pta(100, 20);
|
||||
var pta2 = new Pta(200, 50);
|
||||
foreach (var bar in series)
|
||||
{
|
||||
pta1.Update(bar);
|
||||
pta2.Update(bar);
|
||||
}
|
||||
Assert.NotEqual(pta1.Last.Value, pta2.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_IncludesBothPeriods()
|
||||
{
|
||||
var pta = new Pta(300, 60);
|
||||
Assert.Contains("300", pta.Name);
|
||||
Assert.Contains("60", pta.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsIndicatorAndResults()
|
||||
{
|
||||
var series = MakeSeries(200);
|
||||
var (results, indicator) = Pta.Calculate(series, 50, 10);
|
||||
Assert.Equal(series.Count, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsState()
|
||||
{
|
||||
var pta = new Pta(50, 10);
|
||||
var values = new double[100];
|
||||
for (int i = 0; i < 100; i++) values[i] = 100.0 + i * 0.1;
|
||||
pta.Prime(values);
|
||||
Assert.True(pta.IsHot);
|
||||
}
|
||||
}
|
||||
@@ -368,6 +368,7 @@ HAS_HTTRENDMODE = _bind("qtl_httrendmode", [_dp, _dp, _ci])
|
||||
HAS_ICHIMOKU = _bind("qtl_ichimoku", [_dp, _dp, _dp, _dp, _dp, _ci, _ci, _ci, _ci, _ci, _dp, _dp, _dp, _dp, _dp])
|
||||
HAS_IMPULSE = _bind("qtl_impulse", [_dp, _ci, _ci, _ci, _ci, _ci, _dp])
|
||||
HAS_PFE = _bind("qtl_pfe", [_dp, _dp, _ci, _ci, _ci])
|
||||
HAS_PTA = _bind("qtl_pta", [_dp, _dp, _ci, _ci, _ci])
|
||||
HAS_QSTICK = _bind("qtl_qstick", [_dp, _dp, _dp, _dp, _dp, _ci, _ci, _ci, _dp])
|
||||
HAS_RAVI = _bind("qtl_ravi", [_dp, _dp, _ci, _ci, _ci])
|
||||
HAS_SUPER = _bind("qtl_super", [_dp, _dp, _dp, _dp, _dp, _ci, _cd, _ci, _dp])
|
||||
|
||||
@@ -266,6 +266,18 @@ def plus_dm(high: object, low: object, close: object, period: int = 14, offset:
|
||||
return _wrap(destination, idx, f"PLUS_DM_{period}", "dynamics", offset)
|
||||
|
||||
|
||||
def pta(close: object, longPeriod: int = 250, shortPeriod: int = 40, offset: int = 0, **kwargs) -> object:
|
||||
"""Ehlers Precision Trend Analysis."""
|
||||
longPeriod = int(kwargs.get("long_period", longPeriod))
|
||||
shortPeriod = int(kwargs.get("short_period", shortPeriod))
|
||||
offset = int(offset)
|
||||
src, idx = _arr(close)
|
||||
n = len(src)
|
||||
output = _out(n)
|
||||
_check(_lib.qtl_pta(_ptr(src), _ptr(output), n, longPeriod, shortPeriod))
|
||||
return _wrap(output, idx, f"PTA_{longPeriod}_{shortPeriod}", "dynamics", offset)
|
||||
|
||||
|
||||
def qstick(open: object, high: object, low: object, close: object, volume: object, period: int = 14, useEma: int = 0, offset: int = 0, **kwargs) -> object:
|
||||
"""QStick."""
|
||||
period = int(kwargs.get("length", period))
|
||||
|
||||
@@ -2316,6 +2316,19 @@ public static unsafe partial class Exports
|
||||
catch { return StatusCodes.QTL_ERR_INTERNAL; }
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "qtl_pta")]
|
||||
public static int QtlPta(double* source, double* output, int n, int longPeriod, int shortPeriod)
|
||||
{
|
||||
if (source == null || output == null) return StatusCodes.QTL_ERR_NULL_PTR;
|
||||
if (n <= 0) return StatusCodes.QTL_ERR_INVALID_LENGTH;
|
||||
try
|
||||
{
|
||||
Pta.Batch(Src(source, n), Dst(output, n), longPeriod, shortPeriod);
|
||||
return StatusCodes.QTL_OK;
|
||||
}
|
||||
catch { return StatusCodes.QTL_ERR_INTERNAL; }
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "qtl_rs")]
|
||||
public static int QtlRs(double* baseSeries, double* compSeries, double* output, int n, int smoothPeriod)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user