using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// YZVAMA: Yang-Zhang Volatility Adjusted Moving Average /// /// /// Adaptive MA using Yang-Zhang volatility percentile rank to adjust SMA length. /// Higher volatility → shorter period; uses OHLC log returns for variance. /// /// Calculation: length = max - percentile×(max-min). /// /// Detailed documentation [SkipLocalsInit] public sealed class Yzvama : AbstractBase { [StructLayout(LayoutKind.Auto)] private record struct RmaState(double Ema, double E, bool IsCompensated); [StructLayout(LayoutKind.Auto)] private record struct YzvamaState( RmaState ShortVar, RmaState LongVar, double PrevClose, int SourceHead, double SourceSum, int SourceValidCount, int YzvHead, int YzvCount, bool IsInitialized) { public static YzvamaState New() => new() { ShortVar = new RmaState(Ema: 0, E: 1.0, IsCompensated: false), LongVar = new RmaState(Ema: 0, E: 1.0, IsCompensated: false), PrevClose = double.NaN, SourceHead = 0, SourceSum = 0, SourceValidCount = 0, YzvHead = 0, YzvCount = 0, IsInitialized = false }; } private readonly int _percentileLookback; private readonly int _minLength; private readonly int _maxLength; private readonly double _shortAlpha; private readonly double _longAlpha; private readonly double _shortDecay; private readonly double _longDecay; private readonly double _kShort; private readonly double _kLong; private YzvamaState _state; private YzvamaState _p_state; // Dual-buffer approach for O(1) state transitions instead of O(n) Array.Copy private double[] _activeSourceBuffer; private double[] _backupSourceBuffer; private double[] _activeYzvBuffer; private double[] _backupYzvBuffer; // Sorted YZV buffer for O(n) insert/remove instead of O(n log n) sort private double[] _activeSortedYzv; private double[] _backupSortedYzv; private double _lastValidSource; private double _p_lastValidSource; private const double EPSILON = 1e-10; /// /// Creates YZVAMA with specified parameters. /// /// Short-term YZV period for current volatility (default: 3) /// Long-term YZV period for baseline volatility (default: 50) /// Lookback window for percentile calculation (default: 100) /// Minimum allowed adjusted length (default: 5) /// Maximum allowed adjusted length (default: 100) public Yzvama(int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100) { if (yzvShortPeriod <= 0) { throw new ArgumentException("Short YZV period must be greater than 0", nameof(yzvShortPeriod)); } if (yzvLongPeriod <= 0) { throw new ArgumentException("Long YZV period must be greater than 0", nameof(yzvLongPeriod)); } if (percentileLookback <= 0) { throw new ArgumentException("Percentile lookback must be greater than 0", nameof(percentileLookback)); } if (minLength <= 0) { throw new ArgumentException("Min length must be greater than 0", nameof(minLength)); } if (maxLength <= 0) { throw new ArgumentException("Max length must be greater than 0", nameof(maxLength)); } if (minLength > maxLength) { throw new ArgumentException("Min length must be less than or equal to max length", nameof(minLength)); } _percentileLookback = percentileLookback; _minLength = minLength; _maxLength = maxLength; _shortAlpha = 1.0 / yzvShortPeriod; _longAlpha = 1.0 / yzvLongPeriod; _shortDecay = 1.0 - _shortAlpha; _longDecay = 1.0 - _longAlpha; _kShort = ComputeYangZhangK(yzvShortPeriod); _kLong = ComputeYangZhangK(yzvLongPeriod); // Dual buffers for pointer-swap on state transitions _activeSourceBuffer = new double[maxLength]; _backupSourceBuffer = new double[maxLength]; Array.Fill(_activeSourceBuffer, double.NaN); Array.Fill(_backupSourceBuffer, double.NaN); _activeYzvBuffer = new double[percentileLookback]; _backupYzvBuffer = new double[percentileLookback]; Array.Fill(_activeYzvBuffer, double.NaN); Array.Fill(_backupYzvBuffer, double.NaN); // Sorted buffer maintained incrementally _activeSortedYzv = new double[percentileLookback]; _backupSortedYzv = new double[percentileLookback]; Array.Fill(_activeSortedYzv, double.NaN); Array.Fill(_backupSortedYzv, double.NaN); _state = YzvamaState.New(); _p_state = _state; // Initialize last valid source to NaN to preserve invalid state until a valid input arrives _lastValidSource = double.NaN; _p_lastValidSource = double.NaN; Name = $"Yzvama({yzvShortPeriod},{yzvLongPeriod},{percentileLookback},{minLength},{maxLength})"; WarmupPeriod = Math.Max(Math.Max(yzvLongPeriod, maxLength), percentileLookback); } /// /// Creates YZVAMA with specified source and parameters. /// Subscribes to source.Pub event. /// public Yzvama(ITValuePublisher source, int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100) : this(yzvShortPeriod, yzvLongPeriod, percentileLookback, minLength, maxLength) { source.Pub += Handle; } /// /// True if the YZVAMA has warmed up and is providing valid results. /// public override bool IsHot => _state.SourceValidCount >= _minLength && _state.IsInitialized; [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double ComputeYangZhangK(int period) { if (period <= 1) { return 0.34 / (1.34 + 1.0); } double ratioN = (period + 1.0) / (period - 1.0); return 0.34 / (1.34 + ratioN); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int LowerBound(ReadOnlySpan sorted, int length, double value) { int lo = 0; int hi = length; while (lo < hi) { int mid = lo + ((hi - lo) >> 1); if (sorted[mid] < value) { lo = mid + 1; } else { hi = mid; } } return lo; } /// /// Inserts a value into the sorted buffer at the correct position. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void InsertSorted(double value, int currentCount) { int insertPos = LowerBound(_activeSortedYzv, currentCount, value); if (insertPos < currentCount) { Array.Copy(_activeSortedYzv, insertPos, _activeSortedYzv, insertPos + 1, currentCount - insertPos); } _activeSortedYzv[insertPos] = value; } /// /// Removes a value from the sorted buffer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RemoveSorted(double value, int currentCount) { int removePos = LowerBound(_activeSortedYzv, currentCount, value); if (removePos < currentCount && Math.Abs(_activeSortedYzv[removePos] - value) < EPSILON) { // Shift elements left if not removing the last element if (removePos < currentCount - 1) { Array.Copy(_activeSortedYzv, removePos + 1, _activeSortedYzv, removePos, currentCount - 1 - removePos); } // Clear the now-unused tail slot to avoid stale data _activeSortedYzv[currentCount - 1] = double.NaN; } } /// /// Updates YZVAMA with a TBar input (uses OHLC for YZV, Close as source). /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public TValue Update(TBar input, bool isNew = true) => Update(input, input.Close, isNew); /// /// Updates YZVAMA with a TBar input (uses OHLC for YZV and provided source for SMA). /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public TValue Update(TBar input, double sourceValue, bool isNew = true) { if (isNew) { // Save state - copy current to backup for potential rollback _p_state = _state; _p_lastValidSource = _lastValidSource; // Copy active to backup for rollback capability (only copy, no swap) Array.Copy(_activeSourceBuffer, _backupSourceBuffer, _maxLength); Array.Copy(_activeYzvBuffer, _backupYzvBuffer, _percentileLookback); Array.Copy(_activeSortedYzv, _backupSortedYzv, _percentileLookback); } else { // Restore state from backup - O(1) pointer swap for rollback _state = _p_state; _lastValidSource = _p_lastValidSource; // Swap pointers so backup becomes active (true O(1) rollback) (_activeSourceBuffer, _backupSourceBuffer) = (_backupSourceBuffer, _activeSourceBuffer); (_activeYzvBuffer, _backupYzvBuffer) = (_backupYzvBuffer, _activeYzvBuffer); (_activeSortedYzv, _backupSortedYzv) = (_backupSortedYzv, _activeSortedYzv); } // Sanitize source if (!double.IsFinite(sourceValue)) { sourceValue = double.IsFinite(_lastValidSource) ? _lastValidSource : 0.0; } else { _lastValidSource = sourceValue; } // Compute Yang-Zhang variance components (log returns) double yzvShort = double.NaN; bool canComputeVol = double.IsFinite(input.Open) && double.IsFinite(input.High) && double.IsFinite(input.Low) && double.IsFinite(input.Close) && input.Open > 0 && input.High > 0 && input.Low > 0 && input.Close > 0; var shortVar = _state.ShortVar; var longVar = _state.LongVar; double prevClose = _state.PrevClose; if (canComputeVol) { double pc = (!double.IsFinite(prevClose) || prevClose <= 0) ? input.Open : prevClose; if (pc > 0) { double ro = Math.Log(input.Open / pc); double rc = Math.Log(input.Close / input.Open); double rh = Math.Log(input.High / input.Open); double rl = Math.Log(input.Low / input.Open); double sOSq = ro * ro; double sCSq = rc * rc; double sRsSq = rh * (rh - rc) + rl * (rl - rc); // Use FMA for sSqDailyShort and sSqDailyLong computations // Original: sOSq + _kShort * sCSq + (1.0 - _kShort) * sRsSq double sSqDailyShort = Math.FusedMultiplyAdd(_kShort, sCSq, Math.FusedMultiplyAdd(1.0 - _kShort, sRsSq, sOSq)); double sSqDailyLong = Math.FusedMultiplyAdd(_kLong, sCSq, Math.FusedMultiplyAdd(1.0 - _kLong, sRsSq, sOSq)); // Update short RMA variance shortVar.Ema = Math.FusedMultiplyAdd(shortVar.Ema, _shortDecay, _shortAlpha * sSqDailyShort); shortVar.E *= _shortDecay; if (shortVar.E <= EPSILON) { shortVar.IsCompensated = true; } double shortVarValue = shortVar.IsCompensated ? shortVar.Ema : shortVar.Ema / (1.0 - shortVar.E); yzvShort = shortVarValue >= 0 ? Math.Sqrt(shortVarValue) : double.NaN; // Update long RMA variance (kept for parity with Pine implementation) longVar.Ema = Math.FusedMultiplyAdd(longVar.Ema, _longDecay, _longAlpha * sSqDailyLong); longVar.E *= _longDecay; if (longVar.E <= EPSILON) { longVar.IsCompensated = true; } } } // Update YZV percentile buffer with incremental sorted maintenance int yzvHead = _state.YzvHead; int yzvCount = _state.YzvCount; if (double.IsFinite(yzvShort)) { // If buffer is full, remove the oldest value from sorted if (yzvCount == _percentileLookback && double.IsFinite(_activeYzvBuffer[yzvHead])) { RemoveSorted(_activeYzvBuffer[yzvHead], yzvCount); yzvCount--; } // Insert new value into sorted buffer InsertSorted(yzvShort, yzvCount); yzvCount++; _activeYzvBuffer[yzvHead] = yzvShort; yzvHead = (yzvHead + 1) % _percentileLookback; } // Percentile rank of current yzvShort within lookback window using sorted buffer double percentileValue = 50.0; if (yzvCount > 1 && double.IsFinite(yzvShort)) { int rankPos = LowerBound(_activeSortedYzv, yzvCount, yzvShort); percentileValue = (rankPos / (double)(yzvCount - 1)) * 100.0; } double lengthRange = _maxLength - _minLength; // Use FMA for adjustedLengthF: _maxLength - (percentileValue/100.0)*lengthRange double adjustedLengthF = Math.FusedMultiplyAdd(percentileValue / 100.0, -lengthRange, _maxLength); int adjustedLength = (int)Math.Max(_minLength, Math.Min(_maxLength, adjustedLengthF)); // Update source circular buffer and rolling sum double oldest = _activeSourceBuffer[_state.SourceHead]; int validCount = _state.SourceValidCount; double sourceSum = _state.SourceSum; if (double.IsFinite(oldest)) { sourceSum -= oldest; validCount--; } if (double.IsFinite(sourceValue)) { sourceSum += sourceValue; validCount++; } _activeSourceBuffer[_state.SourceHead] = sourceValue; int newHead = (_state.SourceHead + 1) % _maxLength; // Calculate SMA over adjustedLength most recent values double result = 0; int actualCount = Math.Min(validCount, adjustedLength); if (actualCount > 0) { double partialSum = 0.0; int partialCount = 0; for (int i = 0; i < actualCount; i++) { int idx = (newHead - 1 - i + _maxLength) % _maxLength; double val = _activeSourceBuffer[idx]; if (double.IsFinite(val)) { partialSum += val; partialCount++; } } result = partialCount > 0 ? partialSum / partialCount : sourceValue; } else { result = sourceValue; } _state = new YzvamaState( shortVar, longVar, input.Close, newHead, sourceSum, validCount, yzvHead, yzvCount, IsInitialized: true); Last = new TValue(input.Time, result); PubEvent(Last, isNew); return Last; } /// /// Updates YZVAMA with a TValue input. /// Note: YZVAMA ideally needs OHLC data to compute Yang-Zhang volatility. /// When only a single value is provided, a synthetic bar is created (O=H=L=C), /// resulting in zero volatility and a tendency toward longer adjusted lengths. /// public override TValue Update(TValue input, bool isNew = true) { var syntheticBar = new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0); return Update(syntheticBar, input.Value, isNew); } /// /// Updates YZVAMA with a TBarSeries (Close as source). /// public TSeries Update(TBarSeries source) { if (source.Count == 0) { return []; } int len = source.Count; var t = new List(len); var v = new List(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 bar = source[i]; var result = Update(bar, isNew: true); tSpan[i] = bar.Time; vSpan[i] = result.Value; } return new TSeries(t, v); } /// /// Updates YZVAMA with a TSeries (single values). /// public override TSeries Update(TSeries source) { if (source.Count == 0) { return []; } int len = source.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); var sourceTimes = source.Times; var sourceValues = source.Values; for (int i = 0; i < len; i++) { var result = Update(new TValue(sourceTimes[i], sourceValues[i]), isNew: true); tSpan[i] = sourceTimes[i]; vSpan[i] = result.Value; } return new TSeries(t, v); } /// /// Initializes the indicator state using the provided history (lossy - timestamps discarded). /// For timestamp-preserving priming, use Prime(ReadOnlySpan<TValue>) overload. /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { Reset(); foreach (double val in source) { Update(new TValue(DateTime.MinValue, val), isNew: true); } } /// /// Initializes the indicator state using the provided timestamped history. /// Preserves original timestamps. /// public void Prime(ReadOnlySpan source) { Reset(); foreach (TValue tv in source) { Update(tv, isNew: true); } } /// /// Calculates YZVAMA for the entire bar series using a new instance. /// public static TSeries Batch(TBarSeries source, int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100) { var yzvama = new Yzvama(yzvShortPeriod, yzvLongPeriod, percentileLookback, minLength, maxLength); return yzvama.Update(source); } /// /// Calculates YZVAMA for the entire series using a new instance. /// public static TSeries Batch(TSeries source, int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100) { var yzvama = new Yzvama(yzvShortPeriod, yzvLongPeriod, percentileLookback, minLength, maxLength); return yzvama.Update(source); } public static (TSeries Results, Yzvama Indicator) Calculate(TBarSeries source, int yzvShortPeriod = 3, int yzvLongPeriod = 50, int percentileLookback = 100, int minLength = 5, int maxLength = 100) { var indicator = new Yzvama(yzvShortPeriod, yzvLongPeriod, percentileLookback, minLength, maxLength); TSeries results = indicator.Update(source); return (results, indicator); } /// /// Resets the YZVAMA state. /// public override void Reset() { _state = YzvamaState.New(); _p_state = _state; Array.Fill(_activeSourceBuffer, double.NaN); Array.Fill(_backupSourceBuffer, double.NaN); Array.Fill(_activeYzvBuffer, double.NaN); Array.Fill(_backupYzvBuffer, double.NaN); Array.Fill(_activeSortedYzv, double.NaN); Array.Fill(_backupSortedYzv, double.NaN); // Initialize to NaN to preserve invalid state until valid input arrives _lastValidSource = double.NaN; _p_lastValidSource = double.NaN; Last = default; } }