Files
QuanTAlib/lib/trends_IIR/vama/Vama.cs
T
2026-01-18 22:39:58 -08:00

369 lines
13 KiB
C#

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// VAMA: Volatility Adjusted Moving Average
/// </summary>
/// <remarks>
/// VAMA dynamically adjusts its smoothing period based on the ratio of long-term
/// to short-term volatility (measured via ATR). During low volatility periods,
/// the effective period increases for smoother output; during high volatility,
/// it decreases for faster response.
///
/// Calculation:
/// 1. Short ATR = RMA(TR, short_period) with bias compensation
/// 2. Long ATR = RMA(TR, long_period) with bias compensation
/// 3. Volatility Ratio = Long_ATR / Short_ATR (clamped to avoid division by zero)
/// 4. Adjusted Length = base_length * volatility_ratio, clamped to [min_length, max_length]
/// 5. VAMA = SMA(source, adjusted_length)
///
/// O(1) ATR updates via RMA; O(adjusted_length) for SMA over the buffer.
/// </remarks>
[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(0, 1.0, false),
LongAtr = new RmaState(0, 1.0, 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;
/// <summary>
/// Creates VAMA with specified parameters.
/// </summary>
/// <param name="baseLength">Base period for the moving average (default: 20)</param>
/// <param name="shortAtrPeriod">Short-term ATR period for current volatility (default: 10)</param>
/// <param name="longAtrPeriod">Long-term ATR period for reference volatility (default: 50)</param>
/// <param name="minLength">Minimum allowed adjusted length (default: 5)</param>
/// <param name="maxLength">Maximum allowed adjusted length (default: 100)</param>
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);
}
/// <summary>
/// Creates VAMA with specified source and parameters.
/// Subscribes to source.Pub event.
/// </summary>
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;
}
/// <summary>
/// True if the VAMA has warmed up and is providing valid results.
/// </summary>
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);
/// <summary>
/// Updates VAMA with a TBar input (uses Close for smoothing, OHLC for True Range).
/// </summary>
[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,
true);
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
return Last;
}
/// <summary>
/// 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.
/// </summary>
[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);
}
/// <summary>
/// Updates VAMA with a TBarSeries.
/// </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, true);
tSpan[i] = bar.Time;
vSpan[i] = result.Value;
}
return new TSeries(t, v);
}
/// <summary>
/// Updates VAMA 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]), true);
tSpan[i] = sourceTimes[i];
vSpan[i] = result.Value;
}
return new TSeries(t, v);
}
/// <summary>
/// Initializes the indicator state using the provided history.
/// </summary>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
Reset();
foreach (double val in source)
{
Update(new TValue(DateTime.MinValue, val), true);
}
}
/// <summary>
/// Calculates VAMA for the entire bar series using a new instance.
/// </summary>
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);
}
/// <summary>
/// Calculates VAMA for the entire series using a new instance.
/// </summary>
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);
}
/// <summary>
/// Resets the VAMA state.
/// </summary>
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;
}
}