using System.Runtime.CompilerServices;
namespace QuanTAlib;
///
/// VORTEX: Vortex Indicator
///
///
/// Trend indicator using vortex movements and true range (Botes & Siepman 2010).
/// VI+ measures positive vortex movement, VI- measures negative vortex movement.
/// Crossovers signal trend changes: VI+ crossing above VI- indicates bullish trend.
///
/// Calculation: VI+ = Sum(VM+, N) / Sum(TR, N); VI- = Sum(VM-, N) / Sum(TR, N)
/// where VM+ = |High - Low[1]|, VM- = |Low - High[1]|, TR = True Range.
///
/// Detailed documentation
[SkipLocalsInit]
public sealed class Vortex : ITValuePublisher
{
private readonly int _period;
private readonly RingBuffer _vmPlusBuffer;
private readonly RingBuffer _vmMinusBuffer;
private readonly RingBuffer _trBuffer;
private TBar _prevBar;
private TBar _p_prevBar;
private bool _isInitialized;
// Running sums for O(1) updates
private double _sumVmPlus, _sumVmMinus, _sumTr;
///
/// Display name for the indicator.
///
public string Name { get; }
public event TValuePublishedHandler? Pub;
///
/// Current VI+ value (Positive Vortex Indicator).
/// This is also the Last value for convenience.
///
public TValue Last { get; private set; }
///
/// Current VI+ value (Positive Vortex Indicator).
///
public TValue ViPlus { get; private set; }
///
/// Current VI- value (Negative Vortex Indicator).
///
public TValue ViMinus { get; private set; }
///
/// True if the indicator has enough data for a full period calculation.
///
public bool IsHot => _vmPlusBuffer.IsFull;
///
/// The period parameter.
///
public int Period => _period;
///
/// The number of bars required for the indicator to warm up.
///
public int WarmupPeriod { get; }
///
/// Creates Vortex indicator with specified period.
///
/// Lookback period for summing (must be > 1, default 14)
public Vortex(int period = 14)
{
if (period <= 1)
{
throw new ArgumentException("Period must be greater than 1", nameof(period));
}
_period = period;
Name = $"Vortex({period})";
WarmupPeriod = period;
_vmPlusBuffer = new RingBuffer(period);
_vmMinusBuffer = new RingBuffer(period);
_trBuffer = new RingBuffer(period);
_isInitialized = false;
}
///
/// Resets the indicator state.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_prevBar = default;
_p_prevBar = default;
_isInitialized = false;
_vmPlusBuffer.Clear();
_vmMinusBuffer.Clear();
_trBuffer.Clear();
_sumVmPlus = _sumVmMinus = _sumTr = 0;
Last = default;
ViPlus = default;
ViMinus = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (!_isInitialized)
{
_prevBar = input;
_p_prevBar = input;
_isInitialized = true;
Last = new TValue(input.Time, 0);
ViPlus = Last;
ViMinus = new TValue(input.Time, 0);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
// Bar correction: restore previous state and recalculate sums from buffer
if (!isNew)
{
_prevBar = _p_prevBar;
// Recalculate sums from buffer contents (excluding the newest that will be replaced)
_sumVmPlus = _vmPlusBuffer.Sum - _vmPlusBuffer.Newest;
_sumVmMinus = _vmMinusBuffer.Sum - _vmMinusBuffer.Newest;
_sumTr = _trBuffer.Sum - _trBuffer.Newest;
}
else
{
// Save state for potential correction
_p_prevBar = _prevBar;
}
// Calculate values 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 prevHigh = double.IsFinite(_prevBar.High) ? _prevBar.High : high;
double prevLow = double.IsFinite(_prevBar.Low) ? _prevBar.Low : low;
double prevClose = double.IsFinite(_prevBar.Close) ? _prevBar.Close : high;
// VM+ = |High - Low[1]|
double vmPlus = Math.Abs(high - prevLow);
// VM- = |Low - High[1]|
double vmMinus = Math.Abs(low - prevHigh);
// True Range = max(High - Low, |High - Close[1]|, |Low - Close[1]|)
double tr = Math.Max(high - low, Math.Max(Math.Abs(high - prevClose), Math.Abs(low - prevClose)));
// For isNew=true with full buffer, subtract oldest before adding
if (isNew && _vmPlusBuffer.IsFull)
{
_sumVmPlus -= _vmPlusBuffer.Oldest;
_sumVmMinus -= _vmMinusBuffer.Oldest;
_sumTr -= _trBuffer.Oldest;
}
// Add new values to buffers
_vmPlusBuffer.Add(vmPlus, isNew);
_vmMinusBuffer.Add(vmMinus, isNew);
_trBuffer.Add(tr, isNew);
// Update sums
_sumVmPlus += vmPlus;
_sumVmMinus += vmMinus;
_sumTr += tr;
// Calculate VI+ and VI- only when buffer is full
double viPlus = 0;
double viMinus = 0;
if (_vmPlusBuffer.IsFull && _sumTr > 0)
{
viPlus = _sumVmPlus / _sumTr;
viMinus = _sumVmMinus / _sumTr;
}
if (isNew)
{
_prevBar = input;
}
ViPlus = new TValue(input.Time, viPlus);
ViMinus = new TValue(input.Time, viMinus);
Last = ViPlus; // VI+ is the primary output
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([], []);
}
int len = source.Count;
var viPlusValues = new double[len];
var viMinusValues = new double[len];
Batch(source.High.Values, source.Low.Values, source.Close.Values, _period, viPlusValues, viMinusValues);
var tList = new List(len);
var vList = new List(viPlusValues);
var times = source.Open.Times;
for (int i = 0; i < len; i++)
{
tList.Add(times[i]);
}
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i], isNew: true);
}
return new TSeries(tList, vList);
}
///
/// Initializes the indicator state using the provided bar series history.
///
/// Historical bar data.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Prime(TBarSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
///
/// Calculates Vortex indicator values using O(n) sliding window algorithm.
///
/// High prices
/// Low prices
/// Close prices
/// Lookback period
/// Output VI+ values
/// Output VI- values
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close,
int period, Span viPlus, Span viMinus)
{
int len = high.Length;
if (len == 0 || len != low.Length || len != close.Length || len != viPlus.Length || len != viMinus.Length || period <= 1)
{
return;
}
// First bar - no previous bar available
viPlus[0] = 0;
viMinus[0] = 0;
if (len < 2)
{
return;
}
// Calculate individual VM+, VM-, TR values
Span vmPlusValues = stackalloc double[len];
Span vmMinusValues = stackalloc double[len];
Span trValues = stackalloc double[len];
vmPlusValues[0] = 0;
vmMinusValues[0] = 0;
trValues[0] = high[0] - low[0];
for (int i = 1; i < len; i++)
{
vmPlusValues[i] = Math.Abs(high[i] - low[i - 1]);
vmMinusValues[i] = Math.Abs(low[i] - high[i - 1]);
trValues[i] = Math.Max(high[i] - low[i], Math.Max(Math.Abs(high[i] - close[i - 1]), Math.Abs(low[i] - close[i - 1])));
}
// Calculate running sums
double sumVmPlus = 0, sumVmMinus = 0, sumTr = 0;
for (int i = 1; i < len; i++)
{
// Add current values
sumVmPlus += vmPlusValues[i];
sumVmMinus += vmMinusValues[i];
sumTr += trValues[i];
// Remove oldest if past period
if (i > period)
{
sumVmPlus -= vmPlusValues[i - period];
sumVmMinus -= vmMinusValues[i - period];
sumTr -= trValues[i - period];
}
// Calculate ratios
if (i >= period && sumTr > 0)
{
viPlus[i] = sumVmPlus / sumTr;
viMinus[i] = sumVmMinus / sumTr;
}
else
{
viPlus[i] = 0;
viMinus[i] = 0;
}
}
}
public static TSeries Batch(TBarSeries source)
{
return Batch(source, 14);
}
public static TSeries Batch(TBarSeries source, int period)
{
var vortex = new Vortex(period);
return vortex.Update(source);
}
public static (TSeries Results, Vortex Indicator) Calculate(TBarSeries source)
{
var indicator = new Vortex();
TSeries results = indicator.Update(source);
return (results, indicator);
}
}