Files
2026-02-23 17:27:35 -08:00

585 lines
21 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// YZVAMA: Yang-Zhang Volatility Adjusted Moving Average
/// </summary>
/// <remarks>
/// Adaptive MA using Yang-Zhang volatility percentile rank to adjust SMA length.
/// Higher volatility → shorter period; uses OHLC log returns for variance.
///
/// Calculation: <c>length = max - percentile×(max-min)</c>.
/// </remarks>
/// <seealso href="Yzvama.md">Detailed documentation</seealso>
[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;
/// <summary>
/// Creates YZVAMA with specified parameters.
/// </summary>
/// <param name="yzvShortPeriod">Short-term YZV period for current volatility (default: 3)</param>
/// <param name="yzvLongPeriod">Long-term YZV period for baseline volatility (default: 50)</param>
/// <param name="percentileLookback">Lookback window for percentile calculation (default: 100)</param>
/// <param name="minLength">Minimum allowed adjusted length (default: 5)</param>
/// <param name="maxLength">Maximum allowed adjusted length (default: 100)</param>
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);
}
/// <summary>
/// Creates YZVAMA with specified source and parameters.
/// Subscribes to source.Pub event.
/// </summary>
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;
}
/// <summary>
/// True if the YZVAMA has warmed up and is providing valid results.
/// </summary>
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<double> 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;
}
/// <summary>
/// Inserts a value into the sorted buffer at the correct position.
/// </summary>
[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;
}
/// <summary>
/// Removes a value from the sorted buffer.
/// </summary>
[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;
}
}
/// <summary>
/// Updates YZVAMA with a TBar input (uses OHLC for YZV, Close as source).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public TValue Update(TBar input, bool isNew = true) => Update(input, input.Close, isNew);
/// <summary>
/// Updates YZVAMA with a TBar input (uses OHLC for YZV and provided source for SMA).
/// </summary>
[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;
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Updates YZVAMA with a TBarSeries (Close as source).
/// </summary>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(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);
}
/// <summary>
/// Updates YZVAMA with a TSeries (single values).
/// </summary>
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(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);
}
/// <summary>
/// Initializes the indicator state using the provided history (lossy - timestamps discarded).
/// For timestamp-preserving priming, use Prime(ReadOnlySpan&lt;TValue&gt;) overload.
/// </summary>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
Reset();
foreach (double val in source)
{
Update(new TValue(DateTime.MinValue, val), isNew: true);
}
}
/// <summary>
/// Initializes the indicator state using the provided timestamped history.
/// Preserves original timestamps.
/// </summary>
public void Prime(ReadOnlySpan<TValue> source)
{
Reset();
foreach (TValue tv in source)
{
Update(tv, isNew: true);
}
}
/// <summary>
/// Calculates YZVAMA for the entire bar series using a new instance.
/// </summary>
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);
}
/// <summary>
/// Calculates YZVAMA for the entire series using a new instance.
/// </summary>
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);
}
/// <summary>
/// Resets the YZVAMA state.
/// </summary>
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;
}
}