mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-07 13:37:44 +00:00
451 lines
13 KiB
C#
451 lines
13 KiB
C#
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace QuanTAlib;
|
|
|
|
/// <summary>
|
|
/// MAVP: Moving Average Variable Period
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// EMA-based moving average where the smoothing period changes per bar.
|
|
/// Each bar receives its own period value, clamped to [minPeriod, maxPeriod],
|
|
/// producing alpha = 2/(period+1). Adaptive warmup compensator tracks the
|
|
/// cumulative product of per-bar (1-alpha) for bias correction.
|
|
///
|
|
/// Calculation: <c>alpha = 2/(clamp(period)+1); EMA += alpha*(P-EMA); result = EMA/(1-E)</c>.
|
|
/// O(1) per bar, zero allocation, no buffer required.
|
|
/// </remarks>
|
|
/// <seealso href="Mavp.md">Detailed documentation</seealso>
|
|
/// <seealso href="mavp.pine">Reference Pine Script implementation</seealso>
|
|
[SkipLocalsInit]
|
|
public sealed class Mavp : AbstractBase
|
|
{
|
|
private readonly int _minPeriod;
|
|
private readonly int _maxPeriod;
|
|
private readonly TValuePublishedHandler _handler;
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
|
private record struct State(double Ema, double E, bool IsHot, bool IsCompensated, double LastValidValue);
|
|
private State _state;
|
|
private State _p_state;
|
|
|
|
/// <summary>
|
|
/// Per-bar effective period. Set this before calling Update(TValue) to control
|
|
/// the smoothing factor for the current bar. Automatically clamped to [minPeriod, maxPeriod].
|
|
/// </summary>
|
|
public double Period { get; set; }
|
|
|
|
/// <summary>
|
|
/// Minimum allowed period for clamping.
|
|
/// </summary>
|
|
public int MinPeriod => _minPeriod;
|
|
|
|
/// <summary>
|
|
/// Maximum allowed period for clamping.
|
|
/// </summary>
|
|
public int MaxPeriod => _maxPeriod;
|
|
|
|
public override bool IsHot => _state.IsHot;
|
|
|
|
private const double COVERAGE_THRESHOLD = 0.05;
|
|
private const double COMPENSATOR_THRESHOLD = 1e-10;
|
|
|
|
/// <summary>
|
|
/// Creates MAVP with specified period bounds.
|
|
/// </summary>
|
|
/// <param name="minPeriod">Minimum allowed period (default 2, must be >= 1).</param>
|
|
/// <param name="maxPeriod">Maximum allowed period (default 30, must be >= minPeriod).</param>
|
|
public Mavp(int minPeriod = 2, int maxPeriod = 30)
|
|
{
|
|
if (minPeriod < 1)
|
|
{
|
|
throw new ArgumentException("Minimum period must be >= 1", nameof(minPeriod));
|
|
}
|
|
|
|
if (maxPeriod < minPeriod)
|
|
{
|
|
throw new ArgumentException("Maximum period must be >= minimum period", nameof(maxPeriod));
|
|
}
|
|
|
|
_minPeriod = minPeriod;
|
|
_maxPeriod = maxPeriod;
|
|
Period = minPeriod;
|
|
_handler = Handle;
|
|
|
|
Name = $"Mavp({minPeriod}, {maxPeriod})";
|
|
WarmupPeriod = maxPeriod;
|
|
|
|
_state = new State(0, 1.0, false, false, double.NaN);
|
|
_p_state = _state;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates MAVP subscribed to a source publisher.
|
|
/// </summary>
|
|
/// <param name="source">Source to subscribe to.</param>
|
|
/// <param name="minPeriod">Minimum allowed period (default 2).</param>
|
|
/// <param name="maxPeriod">Maximum allowed period (default 30).</param>
|
|
public Mavp(ITValuePublisher source, int minPeriod = 2, int maxPeriod = 30)
|
|
: this(minPeriod, maxPeriod)
|
|
{
|
|
source.Pub += _handler;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private double GetValidValue(double input)
|
|
{
|
|
if (double.IsFinite(input))
|
|
{
|
|
_state.LastValidValue = input;
|
|
return input;
|
|
}
|
|
return _state.LastValidValue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates MAVP with a value and explicit per-bar period.
|
|
/// </summary>
|
|
/// <param name="input">Price input.</param>
|
|
/// <param name="period">Per-bar effective period (clamped to [minPeriod, maxPeriod]).</param>
|
|
/// <param name="isNew">True if this is a new bar, false for bar correction.</param>
|
|
/// <returns>Updated MAVP value.</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public TValue Update(TValue input, double period, bool isNew = true)
|
|
{
|
|
Period = period;
|
|
return Update(input, isNew);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
public override TValue Update(TValue input, bool isNew = true)
|
|
{
|
|
if (isNew)
|
|
{
|
|
_p_state = _state;
|
|
}
|
|
else
|
|
{
|
|
_state = _p_state;
|
|
}
|
|
|
|
double val = GetValidValue(input.Value);
|
|
if (double.IsNaN(val))
|
|
{
|
|
Last = new TValue(input.Time, double.NaN);
|
|
PubEvent(Last, isNew);
|
|
return Last;
|
|
}
|
|
|
|
// Clamp period and compute alpha
|
|
double p = Math.Clamp(Period, _minPeriod, _maxPeriod);
|
|
double alpha = 2.0 / (p + 1.0);
|
|
double beta = 1.0 - alpha;
|
|
|
|
// Local copy for JIT struct promotion
|
|
var s = _state;
|
|
|
|
// EMA update: ema = ema * beta + alpha * input
|
|
s.Ema = Math.FusedMultiplyAdd(s.Ema, beta, alpha * val);
|
|
|
|
double result;
|
|
if (!s.IsCompensated)
|
|
{
|
|
s.E *= beta;
|
|
|
|
if (!s.IsHot && s.E <= COVERAGE_THRESHOLD)
|
|
{
|
|
s.IsHot = true;
|
|
}
|
|
|
|
if (s.E <= COMPENSATOR_THRESHOLD)
|
|
{
|
|
s.IsCompensated = true;
|
|
result = s.Ema;
|
|
}
|
|
else
|
|
{
|
|
result = s.Ema / (1.0 - s.E);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result = s.Ema;
|
|
}
|
|
|
|
_state = s;
|
|
|
|
Last = new TValue(input.Time, result);
|
|
PubEvent(Last, isNew);
|
|
return Last;
|
|
}
|
|
|
|
public override TSeries Update(TSeries source)
|
|
{
|
|
if (source.Count == 0)
|
|
{
|
|
return new TSeries([], []);
|
|
}
|
|
|
|
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);
|
|
|
|
source.Times.CopyTo(tSpan);
|
|
|
|
double savedPeriod = Period;
|
|
Reset();
|
|
Period = savedPeriod;
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
vSpan[i] = Update(new TValue(source.Times[i], source.Values[i])).Value;
|
|
}
|
|
|
|
return new TSeries(t, v);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch update with separate period series.
|
|
/// </summary>
|
|
/// <param name="source">Price series.</param>
|
|
/// <param name="periods">Per-bar period series (same length as source).</param>
|
|
/// <returns>Smoothed output series.</returns>
|
|
public TSeries Update(TSeries source, TSeries periods)
|
|
{
|
|
if (source.Count == 0)
|
|
{
|
|
return new TSeries([], []);
|
|
}
|
|
|
|
if (source.Count != periods.Count)
|
|
{
|
|
throw new ArgumentException("Source and periods must have the same length", nameof(periods));
|
|
}
|
|
|
|
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);
|
|
|
|
source.Times.CopyTo(tSpan);
|
|
|
|
Reset();
|
|
for (int i = 0; i < len; i++)
|
|
{
|
|
Period = periods.Values[i];
|
|
vSpan[i] = Update(new TValue(source.Times[i], source.Values[i])).Value;
|
|
}
|
|
|
|
return new TSeries(t, v);
|
|
}
|
|
|
|
private void Handle(object? sender, in TValueEventArgs args)
|
|
{
|
|
Update(args.Value, args.IsNew);
|
|
}
|
|
|
|
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
|
{
|
|
foreach (var value in source)
|
|
{
|
|
Update(new TValue(DateTime.MinValue, value));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch with fixed period for all bars.
|
|
/// </summary>
|
|
public static TSeries Batch(TSeries source, int minPeriod = 2, int maxPeriod = 30, double fixedPeriod = double.NaN)
|
|
{
|
|
var mavp = new Mavp(minPeriod, maxPeriod);
|
|
if (!double.IsNaN(fixedPeriod))
|
|
{
|
|
mavp.Period = fixedPeriod;
|
|
}
|
|
return mavp.Update(source);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Batch with per-bar period series (TSeries).
|
|
/// </summary>
|
|
public static TSeries Batch(TSeries source, TSeries periods, int minPeriod = 2, int maxPeriod = 30)
|
|
{
|
|
var mavp = new Mavp(minPeriod, maxPeriod);
|
|
return mavp.Update(source, periods);
|
|
}
|
|
|
|
/// <summary>
|
|
/// High-performance span-based batch with per-bar periods.
|
|
/// </summary>
|
|
/// <param name="source">Input prices.</param>
|
|
/// <param name="periods">Per-bar periods (same length as source).</param>
|
|
/// <param name="output">Output span (same length as source).</param>
|
|
/// <param name="minPeriod">Minimum allowed period.</param>
|
|
/// <param name="maxPeriod">Maximum allowed period.</param>
|
|
public static void Batch(ReadOnlySpan<double> source, ReadOnlySpan<double> periods, Span<double> output, int minPeriod = 2, int maxPeriod = 30)
|
|
{
|
|
if (minPeriod < 1)
|
|
{
|
|
throw new ArgumentException("Minimum period must be >= 1", nameof(minPeriod));
|
|
}
|
|
|
|
if (maxPeriod < minPeriod)
|
|
{
|
|
throw new ArgumentException("Maximum period must be >= minimum period", nameof(maxPeriod));
|
|
}
|
|
|
|
if (source.Length != output.Length)
|
|
{
|
|
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
|
}
|
|
|
|
if (source.Length != periods.Length)
|
|
{
|
|
throw new ArgumentException("Source and periods must have the same length", nameof(periods));
|
|
}
|
|
|
|
double ema = 0;
|
|
double e = 1.0;
|
|
bool isCompensated = false;
|
|
double lastValid = double.NaN;
|
|
|
|
for (int i = 0; i < source.Length; i++)
|
|
{
|
|
double val = source[i];
|
|
if (double.IsFinite(val))
|
|
{
|
|
lastValid = val;
|
|
}
|
|
else
|
|
{
|
|
val = lastValid;
|
|
}
|
|
|
|
if (double.IsNaN(val))
|
|
{
|
|
output[i] = double.NaN;
|
|
continue;
|
|
}
|
|
|
|
double p = Math.Clamp(periods[i], minPeriod, maxPeriod);
|
|
double alpha = 2.0 / (p + 1.0);
|
|
double beta = 1.0 - alpha;
|
|
|
|
// ema = ema * beta + alpha * val
|
|
ema = Math.FusedMultiplyAdd(ema, beta, alpha * val);
|
|
|
|
if (!isCompensated)
|
|
{
|
|
e *= beta;
|
|
|
|
if (e <= COMPENSATOR_THRESHOLD)
|
|
{
|
|
isCompensated = true;
|
|
output[i] = ema;
|
|
}
|
|
else
|
|
{
|
|
output[i] = ema / (1.0 - e);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
output[i] = ema;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// High-performance span-based batch with fixed period.
|
|
/// </summary>
|
|
public static void Batch(ReadOnlySpan<double> source, Span<double> output, double fixedPeriod, int minPeriod = 2, int maxPeriod = 30)
|
|
{
|
|
if (minPeriod < 1)
|
|
{
|
|
throw new ArgumentException("Minimum period must be >= 1", nameof(minPeriod));
|
|
}
|
|
|
|
if (maxPeriod < minPeriod)
|
|
{
|
|
throw new ArgumentException("Maximum period must be >= minimum period", nameof(maxPeriod));
|
|
}
|
|
|
|
if (source.Length != output.Length)
|
|
{
|
|
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
|
}
|
|
|
|
double p = Math.Clamp(fixedPeriod, minPeriod, maxPeriod);
|
|
double alpha = 2.0 / (p + 1.0);
|
|
double beta = 1.0 - alpha;
|
|
|
|
double ema = 0;
|
|
double e = 1.0;
|
|
bool isCompensated = false;
|
|
double lastValid = double.NaN;
|
|
|
|
for (int i = 0; i < source.Length; i++)
|
|
{
|
|
double val = source[i];
|
|
if (double.IsFinite(val))
|
|
{
|
|
lastValid = val;
|
|
}
|
|
else
|
|
{
|
|
val = lastValid;
|
|
}
|
|
|
|
if (double.IsNaN(val))
|
|
{
|
|
output[i] = double.NaN;
|
|
continue;
|
|
}
|
|
|
|
ema = Math.FusedMultiplyAdd(ema, beta, alpha * val);
|
|
|
|
if (!isCompensated)
|
|
{
|
|
e *= beta;
|
|
|
|
if (e <= COMPENSATOR_THRESHOLD)
|
|
{
|
|
isCompensated = true;
|
|
output[i] = ema;
|
|
}
|
|
else
|
|
{
|
|
output[i] = ema / (1.0 - e);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
output[i] = ema;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static (TSeries Results, Mavp Indicator) Calculate(TSeries source, TSeries periods, int minPeriod = 2, int maxPeriod = 30)
|
|
{
|
|
var indicator = new Mavp(minPeriod, maxPeriod);
|
|
TSeries results = indicator.Update(source, periods);
|
|
return (results, indicator);
|
|
}
|
|
|
|
public override void Reset()
|
|
{
|
|
_state = new State(0, 1.0, false, false, double.NaN);
|
|
_p_state = _state;
|
|
Period = _minPeriod;
|
|
Last = default;
|
|
}
|
|
}
|