using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// MA Type enumeration for MAENV indicator.
///
public enum MaenvType
{
/// Simple Moving Average (O(1) with ring buffer)
SMA = 0,
/// Exponential Moving Average (O(1) with warmup)
EMA = 1,
/// Weighted Moving Average (O(n))
WMA = 2
}
///
/// MAENV: Moving Average Envelope
/// A percentage-based envelope using a selectable moving average as the middle line.
/// Middle = MA(source, period) - SMA, EMA, or WMA
/// Upper = Middle + (Middle × percentage / 100)
/// Lower = Middle - (Middle × percentage / 100)
///
[SkipLocalsInit]
public sealed class Maenv : ITValuePublisher, IDisposable
{
private readonly int _period;
private readonly double _percentage;
private readonly MaenvType _maType;
private readonly double _emaAlpha;
// Ring buffer for SMA
private readonly double[]? _smaBuffer;
[StructLayout(LayoutKind.Auto)]
private record struct State(
// EMA state
double EmaSum,
double EmaWeight,
// SMA state
double SmaSum,
int SmaHead,
int SmaCount,
// WMA state
int WmaCount,
// General
double LastValid,
int Bars,
bool IsHot);
private State _state;
private State _p_state;
private double[]? _p_smaBuffer;
// WMA lookback buffer
private readonly double[]? _wmaBuffer;
private double[]? _p_wmaBuffer;
private readonly TValuePublishedHandler _valueHandler;
// Subscription tracking for IDisposable
private TSeries? _source;
private bool _disposed;
public string Name { get; }
public int WarmupPeriod { get; }
public TValue Last { get; private set; }
public TValue Upper { get; private set; }
public TValue Lower { get; private set; }
public bool IsHot => _state.IsHot;
public event TValuePublishedHandler? Pub;
public Maenv(int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (percentage <= 0.0)
{
throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be > 0.");
}
_period = period;
_percentage = percentage;
_maType = maType;
_emaAlpha = 2.0 / (period + 1);
WarmupPeriod = period;
Name = $"Maenv({period},{percentage},{maType})";
_valueHandler = HandleValue;
// Allocate buffers based on MA type
if (maType == MaenvType.SMA)
{
_smaBuffer = new double[period];
_p_smaBuffer = new double[period];
}
else if (maType == MaenvType.WMA)
{
_wmaBuffer = new double[period];
_p_wmaBuffer = new double[period];
}
Reset();
}
public Maenv(TSeries source, int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA) : this(period, percentage, maType)
{
_source = source;
Prime(source);
source.Pub += _valueHandler;
}
///
/// Releases the event subscription to the source publisher.
///
public void Dispose()
{
if (_disposed)
{
return;
}
if (_source != null)
{
_source.Pub -= _valueHandler;
_source = null;
}
_disposed = true;
}
private void HandleValue(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PubEvent(TValue value, bool isNew = true) =>
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0, false);
_p_state = _state;
if (_smaBuffer != null)
{
Array.Fill(_smaBuffer, 0.0);
_p_smaBuffer = (double[])_smaBuffer.Clone();
}
if (_wmaBuffer != null)
{
Array.Fill(_wmaBuffer, 0.0);
_p_wmaBuffer = (double[])_wmaBuffer.Clone();
}
Last = default;
Upper = default;
Lower = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValid(double value, bool isNew)
{
if (double.IsFinite(value))
{
if (isNew)
{
_state = _state with { LastValid = value };
}
return value;
}
return _state.LastValid;
}
// ========================
// Update overloads (adjacent per S4136)
// ========================
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
if (_smaBuffer != null && _p_smaBuffer != null)
{
Array.Copy(_smaBuffer, _p_smaBuffer, _period);
}
if (_wmaBuffer != null && _p_wmaBuffer != null)
{
Array.Copy(_wmaBuffer, _p_wmaBuffer, _period);
}
}
else
{
_state = _p_state;
if (_smaBuffer != null && _p_smaBuffer != null)
{
Array.Copy(_p_smaBuffer, _smaBuffer, _period);
}
if (_wmaBuffer != null && _p_wmaBuffer != null)
{
Array.Copy(_p_wmaBuffer, _wmaBuffer, _period);
}
}
double value = GetValid(input.Value, isNew);
if (isNew)
{
_state = _state with { Bars = _state.Bars + 1 };
}
double middle = _maType switch
{
MaenvType.SMA => CalculateSMA(value, isNew),
MaenvType.EMA => CalculateEMA(value, isNew),
MaenvType.WMA => CalculateWMA(value, isNew),
_ => value
};
double dist = middle * _percentage / 100.0;
double upper = middle + dist;
double lower = middle - dist;
if (!_state.IsHot && _state.Bars >= WarmupPeriod)
{
_state = _state with { IsHot = true };
}
Last = new TValue(input.Time, middle);
Upper = new TValue(input.Time, upper);
Lower = new TValue(input.Time, lower);
PubEvent(Last, isNew);
return Last;
}
public (TSeries Middle, TSeries Upper, TSeries Lower) Update(TSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMiddle = new List(len);
var vMiddle = new List(len);
var tUpper = new List(len);
var vUpper = new List(len);
var tLower = new List(len);
var vLower = new List(len);
CollectionsMarshal.SetCount(tMiddle, len);
CollectionsMarshal.SetCount(vMiddle, len);
CollectionsMarshal.SetCount(tUpper, len);
CollectionsMarshal.SetCount(vUpper, len);
CollectionsMarshal.SetCount(tLower, len);
CollectionsMarshal.SetCount(vLower, len);
var tSpan = CollectionsMarshal.AsSpan(tMiddle);
var vMiddleSpan = CollectionsMarshal.AsSpan(vMiddle);
var vUpperSpan = CollectionsMarshal.AsSpan(vUpper);
var vLowerSpan = CollectionsMarshal.AsSpan(vLower);
// Process through streaming path to compute results and prime state in one pass
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i], isNew: true);
vMiddleSpan[i] = Last.Value;
vUpperSpan[i] = Upper.Value;
vLowerSpan[i] = Lower.Value;
}
source.Times.CopyTo(tSpan);
tSpan.CopyTo(CollectionsMarshal.AsSpan(tUpper));
tSpan.CopyTo(CollectionsMarshal.AsSpan(tLower));
var lastTime = new DateTime(source.Times[^1], DateTimeKind.Utc);
Last = new TValue(lastTime, vMiddleSpan[^1]);
Upper = new TValue(lastTime, vUpperSpan[^1]);
Lower = new TValue(lastTime, vLowerSpan[^1]);
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
}
// ========================
// Private MA calculation helpers
// ========================
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateSMA(double value, bool isNew)
{
if (_smaBuffer == null)
{
return value;
}
// Calculate new count (always increment if not full, for both isNew cases)
int currentCount = _state.SmaCount;
int calcCount = currentCount < _period ? currentCount + 1 : currentCount;
// Remove oldest value from sum if buffer is full
double oldest = _smaBuffer[_state.SmaHead];
double newSum = _state.SmaSum;
if (currentCount >= _period)
{
newSum -= oldest;
}
// Add new value
newSum += value;
// Update buffer
_smaBuffer[_state.SmaHead] = value;
int newHead = (_state.SmaHead + 1) % _period;
// Persist state only for isNew=true
if (isNew)
{
_state = _state with
{
SmaSum = newSum,
SmaHead = newHead,
SmaCount = calcCount
};
}
return newSum / calcCount;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateEMA(double value, bool isNew)
{
// Use EmaWeight==0 to detect first value for correct isNew=false behavior
if (Math.Abs(_state.EmaWeight) < double.Epsilon)
{
// First value - persist only for isNew=true
if (isNew)
{
_state = _state with
{
EmaSum = value,
EmaWeight = 1.0
};
}
return value;
}
// EMA with warmup compensation
double newSum = Math.FusedMultiplyAdd(_state.EmaSum, 1.0 - _emaAlpha, value * _emaAlpha);
double newWeight = Math.FusedMultiplyAdd(_state.EmaWeight, 1.0 - _emaAlpha, _emaAlpha);
// Persist state only for isNew=true
if (isNew)
{
_state = _state with
{
EmaSum = newSum,
EmaWeight = newWeight
};
}
return newSum / newWeight;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateWMA(double value, bool isNew)
{
if (_wmaBuffer == null)
{
return value;
}
// Calculate count for this bar (always increment if not full, for both isNew cases)
int currentCount = _state.WmaCount;
int calcCount = currentCount < _period ? currentCount + 1 : currentCount;
// Shift buffer (always shift if count > 1, regardless of isNew)
// This ensures restoration produces same buffer state as original
if (calcCount > 1)
{
for (int i = _period - 1; i > 0; i--)
{
_wmaBuffer[i] = _wmaBuffer[i - 1];
}
}
_wmaBuffer[0] = value;
// Persist state only for isNew=true
if (isNew)
{
_state = _state with { WmaCount = calcCount };
}
// Calculate WMA
double norm = 0.0;
double sum = 0.0;
for (int i = 0; i < calcCount; i++)
{
double w = (_period - i) * _period;
norm += w;
sum += _wmaBuffer[i] * w;
}
return norm > 0 ? sum / norm : value;
}
public void Prime(TSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
// ========================
// Batch overloads (adjacent per S4136)
// ========================
///
/// Batch calculation using spans (zero allocation for SMA and EMA).
///
public static void Batch(
ReadOnlySpan source,
Span middle,
Span upper,
Span lower,
int period,
double percentage = 1.0,
MaenvType maType = MaenvType.EMA)
{
if (period < 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be >= 1.");
}
if (percentage <= 0.0)
{
throw new ArgumentOutOfRangeException(nameof(percentage), "Percentage must be > 0.");
}
if (middle.Length < source.Length || upper.Length < source.Length || lower.Length < source.Length)
{
throw new ArgumentException("Output spans must be at least as long as input", nameof(middle));
}
int len = source.Length;
if (len == 0)
{
return;
}
switch (maType)
{
case MaenvType.SMA:
BatchSMA(source, middle, upper, lower, period, percentage);
break;
case MaenvType.EMA:
BatchEMA(source, middle, upper, lower, period, percentage);
break;
case MaenvType.WMA:
BatchWMA(source, middle, upper, lower, period, percentage);
break;
default:
BatchEMA(source, middle, upper, lower, period, percentage);
break;
}
}
public static (TSeries Middle, TSeries Upper, TSeries Lower) Batch(TSeries source, int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA)
{
int len = source.Count;
var tMiddle = new List(len);
var vMiddle = new List(len);
var tUpper = new List(len);
var vUpper = new List(len);
var tLower = new List(len);
var vLower = new List(len);
CollectionsMarshal.SetCount(tMiddle, len);
CollectionsMarshal.SetCount(vMiddle, len);
CollectionsMarshal.SetCount(tUpper, len);
CollectionsMarshal.SetCount(vUpper, len);
CollectionsMarshal.SetCount(tLower, len);
CollectionsMarshal.SetCount(vLower, len);
Batch(source.Values,
CollectionsMarshal.AsSpan(vMiddle),
CollectionsMarshal.AsSpan(vUpper),
CollectionsMarshal.AsSpan(vLower),
period, percentage, maType);
source.Times.CopyTo(CollectionsMarshal.AsSpan(tMiddle));
CollectionsMarshal.AsSpan(tMiddle).CopyTo(CollectionsMarshal.AsSpan(tUpper));
CollectionsMarshal.AsSpan(tMiddle).CopyTo(CollectionsMarshal.AsSpan(tLower));
return (new TSeries(tMiddle, vMiddle), new TSeries(tUpper, vUpper), new TSeries(tLower, vLower));
}
// ========================
// Private batch helpers
// ========================
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void BatchSMA(
ReadOnlySpan source,
Span middle,
Span upper,
Span lower,
int period,
double percentage)
{
int len = source.Length;
Span buffer = period <= 256 ? stackalloc double[period] : new double[period];
buffer.Clear();
double sum = 0.0;
int head = 0;
int count = 0;
for (int i = 0; i < len; i++)
{
double value = source[i];
// Remove oldest if full
if (count >= period)
{
sum -= buffer[head];
}
else
{
count++;
}
// Add new
sum += value;
buffer[head] = value;
head = (head + 1) % period;
double ma = sum / count;
double dist = ma * percentage / 100.0;
middle[i] = ma;
upper[i] = ma + dist;
lower[i] = ma - dist;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void BatchEMA(
ReadOnlySpan source,
Span middle,
Span upper,
Span lower,
int period,
double percentage)
{
int len = source.Length;
double alpha = 2.0 / (period + 1);
double emaSum = source[0];
double emaWeight = 1.0;
double ma = emaSum;
double dist = ma * percentage / 100.0;
middle[0] = ma;
upper[0] = ma + dist;
lower[0] = ma - dist;
for (int i = 1; i < len; i++)
{
double value = source[i];
emaSum = Math.FusedMultiplyAdd(emaSum, 1.0 - alpha, value * alpha);
emaWeight = Math.FusedMultiplyAdd(emaWeight, 1.0 - alpha, alpha);
ma = emaSum / emaWeight;
dist = ma * percentage / 100.0;
middle[i] = ma;
upper[i] = ma + dist;
lower[i] = ma - dist;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void BatchWMA(
ReadOnlySpan source,
Span middle,
Span upper,
Span lower,
int period,
double percentage)
{
int len = source.Length;
for (int i = 0; i < len; i++)
{
double norm = 0.0;
double sum = 0.0;
int count = Math.Min(i + 1, period);
for (int j = 0; j < count; j++)
{
double w = (period - j) * period;
norm += w;
sum += source[i - j] * w;
}
double ma = norm > 0 ? sum / norm : source[i];
double dist = ma * percentage / 100.0;
middle[i] = ma;
upper[i] = ma + dist;
lower[i] = ma - dist;
}
}
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Maenv Indicator) Calculate(TSeries source, int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA)
{
// Use parameterless constructor to avoid double-priming:
// The Maenv(source, ...) constructor already calls Prime(source),
// so calling Update(source) afterwards would Prime again.
var indicator = new Maenv(period, percentage, maType);
var results = indicator.Update(source);
return (results, indicator);
}
}