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); } }