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,
double SmoothedPercentile,
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,
SmoothedPercentile = 50.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 readonly double _pctAlpha;
private readonly double _pctDecay;
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);
_pctAlpha = 2.0 / (percentileLookback + 1.0);
_pctDecay = 1.0 - _pctAlpha;
// 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;
}
// EMA-smooth the percentile to prevent wild adjusted_length swings (matches Pine)
double smoothedPct = Math.FusedMultiplyAdd(_state.SmoothedPercentile, _pctDecay, _pctAlpha * percentileValue);
double lengthRange = _maxLength - _minLength;
// Use FMA for adjustedLengthF: _maxLength - (smoothedPct/100.0)*lengthRange
double adjustedLengthF = Math.FusedMultiplyAdd(smoothedPct / 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,
smoothedPct,
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;
}
}