mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
475 lines
14 KiB
C#
475 lines
14 KiB
C#
using System.Runtime.CompilerServices;
|
||
|
||
namespace QuanTAlib;
|
||
|
||
/// <summary>
|
||
/// DX: Directional Movement Index
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Unsmoothed trend strength indicator [0-100] regardless of direction (Wilder).
|
||
/// Unlike ADX, DX is not smoothed - it shows raw directional movement strength.
|
||
/// Values above 25 indicate strong trend. DX is the building block for ADX.
|
||
///
|
||
/// Calculation: <c>DX = |+DI - -DI| / (+DI + -DI) × 100</c> where DI values use RMA-smoothed +DM/-DM/TR.
|
||
/// </remarks>
|
||
/// <seealso href="Dx.md">Detailed documentation</seealso>
|
||
[SkipLocalsInit]
|
||
public sealed class Dx : ITValuePublisher
|
||
{
|
||
private readonly int _period;
|
||
private readonly double _invPeriod; // 1 / period
|
||
private TBar _prevBar;
|
||
private TBar _p_prevBar;
|
||
private bool _isInitialized;
|
||
|
||
// State for TR, +DM, -DM smoothing
|
||
private double _trSum, _dmPlusSum, _dmMinusSum;
|
||
private double _p_trSum, _p_dmPlusSum, _p_dmMinusSum;
|
||
private int _samples;
|
||
private int _p_samples;
|
||
|
||
private double _trSmooth, _dmPlusSmooth, _dmMinusSmooth;
|
||
private double _p_trSmooth, _p_dmPlusSmooth, _p_dmMinusSmooth;
|
||
|
||
/// <summary>
|
||
/// Display name for the indicator.
|
||
/// </summary>
|
||
public string Name { get; }
|
||
|
||
public event TValuePublishedHandler? Pub;
|
||
|
||
/// <summary>
|
||
/// Current DX value.
|
||
/// </summary>
|
||
public TValue Last { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Current +DI value.
|
||
/// </summary>
|
||
public TValue DiPlus { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Current -DI value.
|
||
/// </summary>
|
||
public TValue DiMinus { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Current smoothed +DM value (Wilder-smoothed raw plus directional movement, before TR normalization).
|
||
/// Equivalent to TA-Lib PLUS_DM.
|
||
/// </summary>
|
||
public TValue DmPlus { get; private set; }
|
||
|
||
/// <summary>
|
||
/// Current smoothed -DM value (Wilder-smoothed raw minus directional movement, before TR normalization).
|
||
/// Equivalent to TA-Lib MINUS_DM.
|
||
/// </summary>
|
||
public TValue DmMinus { get; private set; }
|
||
|
||
/// <summary>
|
||
/// True if the DX has warmed up and is providing valid results.
|
||
/// </summary>
|
||
public bool IsHot => _samples >= _period;
|
||
|
||
/// <summary>
|
||
/// The period parameter.
|
||
/// </summary>
|
||
public int Period => _period;
|
||
|
||
/// <summary>
|
||
/// The number of bars required for the indicator to warm up.
|
||
/// </summary>
|
||
public int WarmupPeriod { get; }
|
||
|
||
/// <summary>
|
||
/// Creates DX with specified period.
|
||
/// </summary>
|
||
/// <param name="period">Period for DX calculation (must be > 0)</param>
|
||
public Dx(int period = 14)
|
||
{
|
||
if (period <= 0)
|
||
{
|
||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||
}
|
||
|
||
_period = period;
|
||
_invPeriod = 1.0 / period;
|
||
Name = $"DX({period})";
|
||
WarmupPeriod = period;
|
||
_isInitialized = false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Resets the DX state.
|
||
/// </summary>
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public void Reset()
|
||
{
|
||
_prevBar = default;
|
||
_p_prevBar = default;
|
||
_isInitialized = false;
|
||
|
||
_trSum = _dmPlusSum = _dmMinusSum = 0;
|
||
_p_trSum = _p_dmPlusSum = _p_dmMinusSum = 0;
|
||
_samples = _p_samples = 0;
|
||
|
||
_trSmooth = _dmPlusSmooth = _dmMinusSmooth = 0;
|
||
_p_trSmooth = _p_dmPlusSmooth = _p_dmMinusSmooth = 0;
|
||
|
||
Last = default;
|
||
DiPlus = default;
|
||
DiMinus = default;
|
||
DmPlus = default;
|
||
DmMinus = default;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public TValue Update(TBar input, bool isNew = true)
|
||
{
|
||
if (isNew)
|
||
{
|
||
_p_prevBar = _prevBar;
|
||
_p_trSum = _trSum;
|
||
_p_dmPlusSum = _dmPlusSum;
|
||
_p_dmMinusSum = _dmMinusSum;
|
||
_p_samples = _samples;
|
||
_p_trSmooth = _trSmooth;
|
||
_p_dmPlusSmooth = _dmPlusSmooth;
|
||
_p_dmMinusSmooth = _dmMinusSmooth;
|
||
}
|
||
else
|
||
{
|
||
_prevBar = _p_prevBar;
|
||
_trSum = _p_trSum;
|
||
_dmPlusSum = _p_dmPlusSum;
|
||
_dmMinusSum = _p_dmMinusSum;
|
||
_samples = _p_samples;
|
||
_trSmooth = _p_trSmooth;
|
||
_dmPlusSmooth = _p_dmPlusSmooth;
|
||
_dmMinusSmooth = _p_dmMinusSmooth;
|
||
}
|
||
|
||
if (!_isInitialized)
|
||
{
|
||
if (isNew)
|
||
{
|
||
_prevBar = input;
|
||
_isInitialized = true;
|
||
}
|
||
return new TValue(input.Time, 0);
|
||
}
|
||
|
||
// Calculate TR with NaN/Infinity guards
|
||
double high = double.IsFinite(input.High) ? input.High : _prevBar.High;
|
||
double low = double.IsFinite(input.Low) ? input.Low : _prevBar.Low;
|
||
double prevClose = double.IsFinite(_prevBar.Close) ? _prevBar.Close : high;
|
||
double prevHigh = double.IsFinite(_prevBar.High) ? _prevBar.High : high;
|
||
double prevLow = double.IsFinite(_prevBar.Low) ? _prevBar.Low : low;
|
||
|
||
double hl = high - low;
|
||
double hpc = Math.Abs(high - prevClose);
|
||
double lpc = Math.Abs(low - prevClose);
|
||
double tr = Math.Max(hl, Math.Max(hpc, lpc));
|
||
|
||
// Guard TR against non-finite values
|
||
if (!double.IsFinite(tr))
|
||
{
|
||
tr = 0;
|
||
}
|
||
|
||
// Calculate DM using guarded values
|
||
double dmPlus = 0;
|
||
double dmMinus = 0;
|
||
double upMove = high - prevHigh;
|
||
double downMove = prevLow - low;
|
||
|
||
// Guard moves against non-finite values
|
||
if (!double.IsFinite(upMove))
|
||
{
|
||
upMove = 0;
|
||
}
|
||
|
||
if (!double.IsFinite(downMove))
|
||
{
|
||
downMove = 0;
|
||
}
|
||
|
||
if (upMove > downMove && upMove > 0)
|
||
{
|
||
dmPlus = upMove;
|
||
}
|
||
|
||
if (downMove > upMove && downMove > 0)
|
||
{
|
||
dmMinus = downMove;
|
||
}
|
||
|
||
if (isNew)
|
||
{
|
||
// Store sanitized values to prevent NaN/Infinity propagation to next bar
|
||
double close = double.IsFinite(input.Close) ? input.Close : prevClose;
|
||
_prevBar = new TBar(input.Time, high, high, low, close, input.Volume);
|
||
}
|
||
|
||
// Smooth TR, +DM, -DM
|
||
if (_samples < _period)
|
||
{
|
||
_trSum += tr;
|
||
_dmPlusSum += dmPlus;
|
||
_dmMinusSum += dmMinus;
|
||
_samples++;
|
||
|
||
if (_samples == _period)
|
||
{
|
||
// Wilder's initialization for TR, +DM, and -DM uses the un-averaged sum (scaled sum).
|
||
_trSmooth = _trSum;
|
||
_dmPlusSmooth = _dmPlusSum;
|
||
_dmMinusSmooth = _dmMinusSum;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Wilder's smoothing: Smooth = Smooth - Smooth/N + Input
|
||
// This is different from RMA: Smooth = Smooth * (N-1)/N + Input/N
|
||
_trSmooth = _trSmooth - (_trSmooth * _invPeriod) + tr;
|
||
_dmPlusSmooth = _dmPlusSmooth - (_dmPlusSmooth * _invPeriod) + dmPlus;
|
||
_dmMinusSmooth = _dmMinusSmooth - (_dmMinusSmooth * _invPeriod) + dmMinus;
|
||
}
|
||
|
||
// Calculate DI and DX
|
||
double diPlus = 0;
|
||
double diMinus = 0;
|
||
double dx = 0;
|
||
|
||
if (_samples >= _period)
|
||
{
|
||
if (_trSmooth > 1e-10)
|
||
{
|
||
diPlus = (_dmPlusSmooth / _trSmooth) * 100.0;
|
||
diMinus = (_dmMinusSmooth / _trSmooth) * 100.0;
|
||
}
|
||
|
||
// Guard against NaN/Infinity in DI calculations
|
||
if (!double.IsFinite(diPlus))
|
||
{
|
||
diPlus = 0;
|
||
}
|
||
|
||
if (!double.IsFinite(diMinus))
|
||
{
|
||
diMinus = 0;
|
||
}
|
||
|
||
double diSum = diPlus + diMinus;
|
||
if (diSum > 1e-10)
|
||
{
|
||
dx = (Math.Abs(diPlus - diMinus) / diSum) * 100.0;
|
||
}
|
||
|
||
// Guard against NaN/Infinity in DX calculation
|
||
if (!double.IsFinite(dx))
|
||
{
|
||
dx = 0;
|
||
}
|
||
}
|
||
|
||
// Ensure all outputs are finite; if not, use previous values or 0
|
||
if (!double.IsFinite(diPlus))
|
||
{
|
||
diPlus = double.IsFinite(DiPlus.Value) ? DiPlus.Value : 0;
|
||
}
|
||
|
||
if (!double.IsFinite(diMinus))
|
||
{
|
||
diMinus = double.IsFinite(DiMinus.Value) ? DiMinus.Value : 0;
|
||
}
|
||
|
||
if (!double.IsFinite(dx))
|
||
{
|
||
dx = double.IsFinite(Last.Value) ? Last.Value : 0;
|
||
}
|
||
|
||
DiPlus = new TValue(input.Time, diPlus);
|
||
DiMinus = new TValue(input.Time, diMinus);
|
||
DmPlus = new TValue(input.Time, _samples >= _period ? _dmPlusSmooth : 0);
|
||
DmMinus = new TValue(input.Time, _samples >= _period ? _dmMinusSmooth : 0);
|
||
Last = new TValue(input.Time, dx);
|
||
|
||
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
|
||
return Last;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public TValue Update(TValue input, bool isNew = true)
|
||
{
|
||
return Update(new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0), isNew);
|
||
}
|
||
|
||
public TSeries Update(TBarSeries source)
|
||
{
|
||
if (source.Count == 0)
|
||
{
|
||
return new TSeries([], []);
|
||
}
|
||
|
||
var len = source.Count;
|
||
var v = new double[len];
|
||
|
||
// Use the static Calculate method for performance
|
||
Batch(source.High.Values, source.Low.Values, source.Close.Values, _period, v);
|
||
|
||
// Create lists for TSeries
|
||
var tList = new List<long>(len);
|
||
var times = source.Open.Times;
|
||
for (int i = 0; i < len; i++)
|
||
{
|
||
tList.Add(times[i]);
|
||
}
|
||
|
||
// Restore state by replaying the whole series
|
||
Reset();
|
||
for (int i = 0; i < len; i++)
|
||
{
|
||
Update(source[i], isNew: true);
|
||
}
|
||
|
||
return new TSeries(tList, [.. v]);
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void CalcTrDm(int i, ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, out double tr, out double dmPlus, out double dmMinus)
|
||
{
|
||
double h = high[i];
|
||
double l = low[i];
|
||
double pc = close[i - 1];
|
||
double ph = high[i - 1];
|
||
double pl = low[i - 1];
|
||
|
||
double hl = h - l;
|
||
double hpc = Math.Abs(h - pc);
|
||
double lpc = Math.Abs(l - pc);
|
||
tr = Math.Max(hl, Math.Max(hpc, lpc));
|
||
|
||
double up = h - ph;
|
||
double down = pl - l;
|
||
dmPlus = (up > down && up > 0) ? up : 0;
|
||
dmMinus = (down > up && down > 0) ? down : 0;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static double CalcDx(double trSmooth, double dmPlusSmooth, double dmMinusSmooth)
|
||
{
|
||
double diPlus = (trSmooth > 1e-10) ? (dmPlusSmooth / trSmooth) * 100.0 : 0;
|
||
double diMinus = (trSmooth > 1e-10) ? (dmMinusSmooth / trSmooth) * 100.0 : 0;
|
||
double diSum = diPlus + diMinus;
|
||
return (diSum > 1e-10) ? (Math.Abs(diPlus - diMinus) / diSum) * 100.0 : 0;
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
private static void WilderSmooth(double input, double invPeriod, ref double smoothed)
|
||
{
|
||
// Wilder's smoothing: Smooth = Smooth - Smooth/N + Input
|
||
smoothed = smoothed - (smoothed * invPeriod) + input;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Initializes the indicator state using the provided bar series history.
|
||
/// </summary>
|
||
/// <param name="source">Historical bar data.</param>
|
||
public void Prime(TBarSeries source)
|
||
{
|
||
Reset();
|
||
if (source.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
for (int i = 0; i < source.Count; i++)
|
||
{
|
||
Update(source[i], isNew: true);
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static void Batch(ReadOnlySpan<double> high, ReadOnlySpan<double> low, ReadOnlySpan<double> close, int period, Span<double> destination)
|
||
{
|
||
int len = high.Length;
|
||
if (len < period + 1)
|
||
{
|
||
destination.Clear();
|
||
return;
|
||
}
|
||
|
||
double invPeriod = 1.0 / period;
|
||
|
||
// Initialize with zeros
|
||
for (int i = 0; i <= period; i++)
|
||
{
|
||
destination[i] = 0;
|
||
}
|
||
|
||
// Phase 1: Accumulate TR, +DM, -DM for the first 'period' bars
|
||
double trSum = 0;
|
||
double dmPlusSum = 0;
|
||
double dmMinusSum = 0;
|
||
|
||
for (int i = 1; i <= period; i++)
|
||
{
|
||
CalcTrDm(i, high, low, close, out double tr, out double dmPlus, out double dmMinus);
|
||
trSum += tr;
|
||
dmPlusSum += dmPlus;
|
||
dmMinusSum += dmMinus;
|
||
}
|
||
|
||
// Initialize smoothed values
|
||
double trSmooth = trSum;
|
||
double dmPlusSmooth = dmPlusSum;
|
||
double dmMinusSmooth = dmMinusSum;
|
||
|
||
// Calculate DX at period index
|
||
double dx = CalcDx(trSmooth, dmPlusSmooth, dmMinusSmooth);
|
||
destination[period] = dx;
|
||
|
||
// Phase 2: Calculate DX for the rest of the series
|
||
for (int i = period + 1; i < len; i++)
|
||
{
|
||
CalcTrDm(i, high, low, close, out double tr, out double dmPlus, out double dmMinus);
|
||
|
||
WilderSmooth(tr, invPeriod, ref trSmooth);
|
||
WilderSmooth(dmPlus, invPeriod, ref dmPlusSmooth);
|
||
WilderSmooth(dmMinus, invPeriod, ref dmMinusSmooth);
|
||
|
||
dx = CalcDx(trSmooth, dmPlusSmooth, dmMinusSmooth);
|
||
destination[i] = dx;
|
||
}
|
||
}
|
||
|
||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||
public static TSeries Batch(TBarSeries source, int period = 14)
|
||
{
|
||
if (source.Count == 0)
|
||
{
|
||
return new TSeries([], []);
|
||
}
|
||
|
||
var len = source.Count;
|
||
var v = new double[len];
|
||
Batch(source.High.Values, source.Low.Values, source.Close.Values, period, v);
|
||
|
||
var tList = new List<long>(len);
|
||
var times = source.Open.Times;
|
||
for (int i = 0; i < len; i++)
|
||
{
|
||
tList.Add(times[i]);
|
||
}
|
||
|
||
return new TSeries(tList, [.. v]);
|
||
}
|
||
|
||
public static (TSeries Results, Dx Indicator) Calculate(TBarSeries source, int period = 14)
|
||
{
|
||
var indicator = new Dx(period);
|
||
TSeries results = indicator.Update(source);
|
||
return (results, indicator);
|
||
}
|
||
}
|