mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-14 16:48:04 +00:00
369 lines
13 KiB
C#
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;
|
|
}
|
|
}
|