using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// VAMA: Volatility Adjusted Moving Average /// /// /// Adaptive MA that adjusts period based on long/short ATR volatility ratio. /// Higher volatility → shorter period (faster); lower volatility → longer period (smoother). /// /// Calculation: length = baseLength × (LongATR/ShortATR), clamped to [min, max]. /// /// Detailed documentation [SkipLocalsInit] public sealed class Vama : AbstractBase { [StructLayout(LayoutKind.Auto)] private record struct RmaState(double Ema, double E, bool IsCompensated); [StructLayout(LayoutKind.Auto)] private record struct VamaState( RmaState ShortAtr, RmaState LongAtr, double PrevClose, int BufferHead, double BufferSum, int ValidCount, bool IsInitialized) { public static VamaState New() => new() { ShortAtr = new RmaState(Ema: 0, E: 1.0, IsCompensated: false), LongAtr = new RmaState(Ema: 0, E: 1.0, IsCompensated: false), PrevClose = double.NaN, BufferHead = 0, BufferSum = 0, ValidCount = 0, IsInitialized = false }; } private readonly int _baseLength; 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 VamaState _state; private VamaState _p_state; private readonly double[] _buffer; private readonly double[] _p_buffer; private double _lastValidValue; private double _p_lastValidValue; private const double EPSILON = 1e-10; /// /// Creates VAMA with specified parameters. /// /// Base period for the moving average (default: 20) /// Short-term ATR period for current volatility (default: 10) /// Long-term ATR period for reference volatility (default: 50) /// Minimum allowed adjusted length (default: 5) /// Maximum allowed adjusted length (default: 100) public Vama(int baseLength = 20, int shortAtrPeriod = 10, int longAtrPeriod = 50, int minLength = 5, int maxLength = 100) { if (baseLength <= 0) { throw new ArgumentException("Base length must be greater than 0", nameof(baseLength)); } if (shortAtrPeriod <= 0) { throw new ArgumentException("Short ATR period must be greater than 0", nameof(shortAtrPeriod)); } if (longAtrPeriod <= 0) { throw new ArgumentException("Long ATR period must be greater than 0", nameof(longAtrPeriod)); } 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)); } _baseLength = baseLength; _minLength = minLength; _maxLength = maxLength; _shortAlpha = 1.0 / shortAtrPeriod; _longAlpha = 1.0 / longAtrPeriod; _shortDecay = 1.0 - _shortAlpha; _longDecay = 1.0 - _longAlpha; _buffer = new double[maxLength]; _p_buffer = new double[maxLength]; Array.Fill(_buffer, double.NaN); Array.Fill(_p_buffer, double.NaN); _state = VamaState.New(); _p_state = _state; Name = $"Vama({baseLength},{shortAtrPeriod},{longAtrPeriod})"; WarmupPeriod = Math.Max(longAtrPeriod, maxLength); } /// /// Creates VAMA with specified source and parameters. /// Subscribes to source.Pub event. /// public Vama(ITValuePublisher source, int baseLength = 20, int shortAtrPeriod = 10, int longAtrPeriod = 50, int minLength = 5, int maxLength = 100) : this(baseLength, shortAtrPeriod, longAtrPeriod, minLength, maxLength) { source.Pub += Handle; } /// /// True if the VAMA has warmed up and is providing valid results. /// public override bool IsHot => _state.ValidCount >= _minLength && _state.IsInitialized; [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// Updates VAMA with a TBar input (uses Close for smoothing, OHLC for True Range). /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public TValue Update(TBar input, bool isNew = true) { if (isNew) { _p_state = _state; Array.Copy(_buffer, _p_buffer, _maxLength); _p_lastValidValue = _lastValidValue; } else { _state = _p_state; Array.Copy(_p_buffer, _buffer, _maxLength); _lastValidValue = _p_lastValidValue; } // Calculate True Range double trueRange; if (!_state.IsInitialized || double.IsNaN(_state.PrevClose)) { trueRange = input.High - input.Low; } else { double hl = input.High - input.Low; double hpc = Math.Abs(input.High - _state.PrevClose); double lpc = Math.Abs(input.Low - _state.PrevClose); trueRange = Math.Max(hl, Math.Max(hpc, lpc)); } // Update ATRs with bias compensation (RMA style) var shortAtr = _state.ShortAtr; var longAtr = _state.LongAtr; shortAtr.Ema = Math.FusedMultiplyAdd(shortAtr.Ema, _shortDecay, _shortAlpha * trueRange); shortAtr.E *= _shortDecay; if (shortAtr.E <= EPSILON) { shortAtr.IsCompensated = true; } longAtr.Ema = Math.FusedMultiplyAdd(longAtr.Ema, _longDecay, _longAlpha * trueRange); longAtr.E *= _longDecay; if (longAtr.E <= EPSILON) { longAtr.IsCompensated = true; } // Compensated ATR values double shortAtrValue = shortAtr.IsCompensated ? shortAtr.Ema : shortAtr.Ema / (1.0 - shortAtr.E); double longAtrValue = longAtr.IsCompensated ? longAtr.Ema : longAtr.Ema / (1.0 - longAtr.E); // Calculate volatility ratio double volatilityRatio = shortAtrValue > EPSILON ? longAtrValue / shortAtrValue : 1.0; // Calculate adjusted length double calcLength = _baseLength * volatilityRatio; int adjustedLength = (int)Math.Max(_minLength, Math.Min(_maxLength, calcLength)); // Update circular buffer with source value double sourceValue = input.Close; if (!double.IsFinite(sourceValue)) { sourceValue = _lastValidValue; } else { _lastValidValue = sourceValue; } // Remove oldest value from sum if it was valid double oldest = _buffer[_state.BufferHead]; int validCount = _state.ValidCount; double bufferSum = _state.BufferSum; if (double.IsFinite(oldest)) { bufferSum -= oldest; validCount--; } // Add new value if (double.IsFinite(sourceValue)) { bufferSum += sourceValue; validCount++; } _buffer[_state.BufferHead] = sourceValue; int newHead = (_state.BufferHead + 1) % _maxLength; // Calculate SMA over adjusted_length 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 = _buffer[idx]; if (double.IsFinite(val)) { partialSum += val; partialCount++; } } result = partialCount > 0 ? partialSum / partialCount : sourceValue; } else { result = sourceValue; } // Update state _state = new VamaState( shortAtr, longAtr, input.Close, newHead, bufferSum, validCount, IsInitialized: true); Last = new TValue(input.Time, result); PubEvent(Last, isNew); return Last; } /// /// Updates VAMA with a TValue input. /// Note: VAMA ideally needs OHLC data for True Range calculation. /// When only a single value is provided, TR is approximated as 0 (no volatility), /// which means the adjusted length stays at base_length. /// [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public override TValue Update(TValue input, bool isNew = true) { // Create a synthetic bar with O=H=L=C for single-value input // This results in TR = 0, so volatility ratio stays at 1 var syntheticBar = new TBar(input.Time, input.Value, input.Value, input.Value, input.Value, 0); return Update(syntheticBar, isNew); } /// /// Updates VAMA with a TBarSeries. /// 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 VAMA 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. /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { Reset(); foreach (double val in source) { Update(new TValue(DateTime.MinValue, val), isNew: true); } } /// /// Calculates VAMA for the entire bar series using a new instance. /// public static TSeries Batch(TBarSeries source, int baseLength = 20, int shortAtrPeriod = 10, int longAtrPeriod = 50, int minLength = 5, int maxLength = 100) { var vama = new Vama(baseLength, shortAtrPeriod, longAtrPeriod, minLength, maxLength); return vama.Update(source); } /// /// Calculates VAMA for the entire series using a new instance. /// public static TSeries Batch(TSeries source, int baseLength = 20, int shortAtrPeriod = 10, int longAtrPeriod = 50, int minLength = 5, int maxLength = 100) { var vama = new Vama(baseLength, shortAtrPeriod, longAtrPeriod, minLength, maxLength); return vama.Update(source); } public static (TSeries Results, Vama Indicator) Calculate(TBarSeries source, int baseLength = 20, int shortAtrPeriod = 10, int longAtrPeriod = 50, int minLength = 5, int maxLength = 100) { var indicator = new Vama(baseLength, shortAtrPeriod, longAtrPeriod, minLength, maxLength); TSeries results = indicator.Update(source); return (results, indicator); } /// /// Resets the VAMA state. /// public override void Reset() { _state = VamaState.New(); _p_state = _state; Array.Fill(_buffer, double.NaN); Array.Fill(_p_buffer, double.NaN); _lastValidValue = 0; _p_lastValidValue = 0; Last = default; } }