using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// MAMA: MESA Adaptive Moving Average
///
///
/// Ehlers' dual-output adaptive filter using Hilbert Transform for cycle measurement.
/// MAMA tracks price closely; FAMA provides smoother confirmation signal.
///
/// Key features: homodyne discriminator, adaptive alpha from phase rate-of-change.
///
/// Detailed documentation
/// Reference Pine Script implementation
[SkipLocalsInit]
public sealed class Mama : AbstractBase
{
public TValue Fama { get; private set; }
public override bool IsHot => _state.Index > 50;
private readonly double _fastLimit;
private readonly double _slowLimit;
private readonly double _scaledFastLimit;
private readonly TValuePublishedHandler _handler;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double Period, double Phase, double Mama, double Fama, double SumPr,
double I2, double Q2, double Re, double Im, double LastValidPrice, int Index
);
private State _state;
private State _p_state;
private readonly RingBuffer _priceBuffer;
private readonly RingBuffer _smoothBuffer;
private readonly RingBuffer _detrender;
private readonly RingBuffer _I1_buffer;
private readonly RingBuffer _Q1_buffer;
// High-precision constants
private const double C1 = 5.0 / 52.0; // ~0.09615385
private const double C2 = 15.0 / 26.0; // ~0.57692308
// Hilbert Transform Correction Factors
// These empirical constants (0.075 and 0.54) are derived by John Ehlers to tune
// the Hilbert Transform for the expected range of market cycles.
// CorrectionFactor = 0.075 * Period + 0.54
private const double AdjSlope = 3.0 / 40.0; // 0.075
private const double AdjIntercept = 27.0 / 50.0; // 0.54
private const double TwoPi = 2.0 * Math.PI;
private const double MinDeltaRadians = Math.PI / 180.0; // 1 degree in radians
private const double SmoothCoef = 0.2;
private const double SmoothPrev = 0.8;
private const double FamaAlphaFactor = 0.5;
private const double MinPeriod = 6.0;
private const double MaxPeriod = 50.0;
public Mama(double fastLimit = 0.5, double slowLimit = 0.05)
{
if (fastLimit <= slowLimit || fastLimit <= 0 || slowLimit <= 0)
{
throw new ArgumentException("FastLimit must be > SlowLimit and > 0", nameof(fastLimit));
}
_fastLimit = fastLimit;
_slowLimit = slowLimit;
_scaledFastLimit = fastLimit * MinDeltaRadians;
_priceBuffer = new RingBuffer(7);
_smoothBuffer = new RingBuffer(7);
_detrender = new RingBuffer(7);
_I1_buffer = new RingBuffer(7);
_Q1_buffer = new RingBuffer(7);
Name = $"Mama({fastLimit:F2},{slowLimit:F2})";
WarmupPeriod = 50;
_handler = Handle;
Init();
}
public Mama(ITValuePublisher source, double fastLimit = 0.5, double slowLimit = 0.05) : this(fastLimit, slowLimit)
{
source.Pub += _handler;
}
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
private void Init()
{
Reset();
}
public override void Reset()
{
_state = default;
_state.Mama = double.NaN;
_state.Fama = double.NaN;
_p_state = _state;
_priceBuffer.Clear();
_smoothBuffer.Clear();
_detrender.Clear();
_I1_buffer.Clear();
_Q1_buffer.Clear();
Last = new TValue(DateTime.MinValue, double.NaN);
Fama = new TValue(DateTime.MinValue, double.NaN);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double NormalizeAngle(double angle)
{
// Guard against non-finite inputs to prevent infinite loop
if (!double.IsFinite(angle))
{
return 0.0; // Return neutral angle for invalid inputs
}
while (angle <= -Math.PI)
{
angle += TwoPi;
}
while (angle > Math.PI)
{
angle -= TwoPi;
}
return angle;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double Step(double price, bool isNew)
{
if (isNew)
{
_p_state = _state;
_state.Index++;
}
else
{
_state = _p_state;
}
if (!double.IsFinite(price))
{
price = _state.LastValidPrice;
}
else
{
_state.LastValidPrice = price;
}
_priceBuffer.Add(price, isNew);
if (_state.Index > 6)
{
double adj = (AdjSlope * _state.Period) + AdjIntercept;
// Smooth
double smooth = Math.FusedMultiplyAdd(4.0, _priceBuffer[^1], Math.FusedMultiplyAdd(3.0, _priceBuffer[^2], Math.FusedMultiplyAdd(2.0, _priceBuffer[^3], _priceBuffer[^4]))) * 0.1;
_smoothBuffer.Add(smooth, isNew);
// Detrender
double dt = Math.FusedMultiplyAdd(C1, _smoothBuffer[^1], Math.FusedMultiplyAdd(C2, _smoothBuffer[^3], Math.FusedMultiplyAdd(-C2, _smoothBuffer[^5], -C1 * _smoothBuffer[^7]))) * adj;
_detrender.Add(dt, isNew);
// Q1
double q1 = Math.FusedMultiplyAdd(C1, dt, Math.FusedMultiplyAdd(C2, _detrender[^3], Math.FusedMultiplyAdd(-C2, _detrender[^5], -C1 * _detrender[^7]))) * adj;
_Q1_buffer.Add(q1, isNew);
// I1 = dt[3]
double i1 = _detrender[^4];
_I1_buffer.Add(i1, isNew);
// Advance phases
// jI = CalculateHilbertTransform(_i1, adj)
double jI = Math.FusedMultiplyAdd(C1, i1, Math.FusedMultiplyAdd(C2, _I1_buffer[^3], Math.FusedMultiplyAdd(-C2, _I1_buffer[^5], -C1 * _I1_buffer[^7]))) * adj;
// jQ = CalculateHilbertTransform(_q1, adj)
double jQ = Math.FusedMultiplyAdd(C1, q1, Math.FusedMultiplyAdd(C2, _Q1_buffer[^3], Math.FusedMultiplyAdd(-C2, _Q1_buffer[^5], -C1 * _Q1_buffer[^7]))) * adj;
// Phasor addition
double i2_val = i1 - jQ;
double q2_val = q1 + jI;
// Smooth i2, q2 (using FMA for precision)
_state.I2 = Math.FusedMultiplyAdd(SmoothCoef, i2_val, SmoothPrev * _p_state.I2);
_state.Q2 = Math.FusedMultiplyAdd(SmoothCoef, q2_val, SmoothPrev * _p_state.Q2);
// Homodyne discriminator
double re_val = Math.FusedMultiplyAdd(_state.I2, _p_state.I2, _state.Q2 * _p_state.Q2);
double im_val = Math.FusedMultiplyAdd(_state.I2, _p_state.Q2, -_state.Q2 * _p_state.I2);
// Smooth re, im (using FMA)
_state.Re = Math.FusedMultiplyAdd(SmoothCoef, re_val, SmoothPrev * _p_state.Re);
_state.Im = Math.FusedMultiplyAdd(SmoothCoef, im_val, SmoothPrev * _p_state.Im);
// Calculate Period
double angle = Math.Atan2(_state.Im, _state.Re);
double period = Math.Abs(angle) > MinDeltaRadians
? TwoPi / Math.Abs(angle)
: _p_state.Period;
// Adjust Period
double periodCap = _p_state.Period * 1.5;
double periodFloor = _p_state.Period * 0.67;
if (period > periodCap)
{
period = periodCap;
}
if (period < periodFloor)
{
period = periodFloor;
}
if (period < MinPeriod)
{
period = MinPeriod;
}
if (period > MaxPeriod)
{
period = MaxPeriod;
}
// Smooth Period (using FMA)
_state.Period = Math.FusedMultiplyAdd(SmoothCoef, period, SmoothPrev * _p_state.Period);
// Phase calculation
_state.Phase = Math.Atan2(q1, i1);
// Adaptive alpha
double diff = NormalizeAngle(_p_state.Phase - _state.Phase);
double delta = Math.Max(Math.Abs(diff), MinDeltaRadians);
double alpha = _scaledFastLimit / delta;
alpha = Math.Clamp(alpha, _slowLimit, _fastLimit);
// Final indicators (using FMA for precision)
double decay = 1.0 - alpha;
_state.Mama = Math.FusedMultiplyAdd(_p_state.Mama, decay, alpha * _priceBuffer[^1]);
double famaAlpha = FamaAlphaFactor * alpha;
double famaDecay = 1.0 - famaAlpha;
_state.Fama = Math.FusedMultiplyAdd(_p_state.Fama, famaDecay, famaAlpha * _state.Mama);
}
else
{
// Initialization phase
_state.SumPr += price;
double avg = _state.Index > 0 ? _state.SumPr / _state.Index : price;
_state.Mama = avg;
_state.Fama = avg;
_state.Period = MinPeriod;
// Initialize buffers with 0
_smoothBuffer.Add(0, isNew);
_detrender.Add(0, isNew);
_I1_buffer.Add(0, isNew);
_Q1_buffer.Add(0, isNew);
}
return _state.Mama;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
double mama = Step(input.Value, isNew);
Last = new TValue(input.Time, mama);
Fama = new TValue(input.Time, _state.Fama);
PubEvent(Last, isNew);
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var v = new List(len);
var t = new List(len);
for (int i = 0; i < len; i++)
{
var result = Update(new TValue(source.Times[i], source.Values[i]));
t.Add(result.Time);
v.Add(result.Value);
}
return new TSeries(t, v);
}
///
/// Primes the indicator with historical data.
///
/// Historical price data
/// Time step parameter (unused for this indicator but required by base signature)
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
_ = step; // Parameter required by base signature but not used by MAMA
foreach (var value in source)
{
Step(value, isNew: true);
}
}
public static TSeries Batch(TSeries source, double fastLimit = 0.5, double slowLimit = 0.05)
{
var mama = new Mama(fastLimit, slowLimit);
return mama.Update(source);
}
public static void Batch(ReadOnlySpan source, Span output, double fastLimit = 0.5, double slowLimit = 0.05, Span famaOutput = default)
{
if (fastLimit <= 0)
{
throw new ArgumentOutOfRangeException(nameof(fastLimit), "FastLimit must be > 0");
}
if (slowLimit <= 0)
{
throw new ArgumentOutOfRangeException(nameof(slowLimit), "SlowLimit must be > 0");
}
if (fastLimit > 1)
{
throw new ArgumentOutOfRangeException(nameof(fastLimit), "FastLimit must be <= 1");
}
if (slowLimit > 1)
{
throw new ArgumentOutOfRangeException(nameof(slowLimit), "SlowLimit must be <= 1");
}
if (fastLimit <= slowLimit)
{
throw new ArgumentOutOfRangeException(nameof(fastLimit), "FastLimit must be > SlowLimit");
}
if (source.Length == 0)
{
return;
}
if (output.Length < source.Length)
{
throw new ArgumentOutOfRangeException(nameof(output), "Output buffer must be at least as large as the input buffer.");
}
if (!famaOutput.IsEmpty && famaOutput.Length < source.Length)
{
throw new ArgumentOutOfRangeException(nameof(famaOutput), "FAMA output buffer must be at least as large as the input buffer.");
}
// Stack allocate buffers for high performance (size 8 for power of 2 masking)
// We need 7 elements, but 8 allows & 7 masking
Span priceBuffer = stackalloc double[8];
Span smoothBuffer = stackalloc double[8];
Span detrender = stackalloc double[8];
Span I1_buffer = stackalloc double[8];
Span Q1_buffer = stackalloc double[8];
int bufferIdx = 0; // Current index for circular buffer
int count = 0;
// State variables (initialized: used before assignment; uninitialized: always assigned before read)
double period = MinPeriod, sumPr = 0, lastValidPrice = 0;
double mama = 0, fama = 0, i2 = 0, q2 = 0, re = 0, im = 0;
double p_period = MinPeriod, p_phase = 0, p_mama = 0, p_fama = 0;
double p_i2 = 0, p_q2 = 0, p_re = 0, p_im = 0;
// Constants
const int Mask = 7;
// Pre-scale fastLimit by MinDeltaRadians so alpha calculation
// produces same numerical results as degree-based formula:
// alpha_rad = (fastLimit × π/180) / delta_rad ≡ alpha_deg = fastLimit / delta_deg
double scaledFastLimit = fastLimit * MinDeltaRadians;
for (int i = 0; i < source.Length; i++)
{
double price = source[i];
if (!double.IsFinite(price))
{
price = count > 0 ? lastValidPrice : 0.0;
}
else
{
lastValidPrice = price;
}
// Circular buffer update
bufferIdx = (bufferIdx + 1) & Mask;
priceBuffer[bufferIdx] = price;
count++;
if (count > 6)
{
double adj = (AdjSlope * period) + AdjIntercept;
// Smooth
double smooth = (4.0 * priceBuffer[bufferIdx] +
3.0 * priceBuffer[(bufferIdx - 1) & Mask] +
2.0 * priceBuffer[(bufferIdx - 2) & Mask] +
priceBuffer[(bufferIdx - 3) & Mask]) * 0.1;
smoothBuffer[bufferIdx] = smooth;
// Detrender
double dt = (C1 * smoothBuffer[bufferIdx] +
C2 * smoothBuffer[(bufferIdx - 2) & Mask] -
C2 * smoothBuffer[(bufferIdx - 4) & Mask] -
C1 * smoothBuffer[(bufferIdx - 6) & Mask]) * adj;
detrender[bufferIdx] = dt;
// Q1
double q1 = (C1 * dt +
C2 * detrender[(bufferIdx - 2) & Mask] -
C2 * detrender[(bufferIdx - 4) & Mask] -
C1 * detrender[(bufferIdx - 6) & Mask]) * adj;
Q1_buffer[bufferIdx] = q1;
// I1 = dt[3]
double i1 = detrender[(bufferIdx - 3) & Mask];
I1_buffer[bufferIdx] = i1;
// Advance phases
double jI = (C1 * i1 +
C2 * I1_buffer[(bufferIdx - 2) & Mask] -
C2 * I1_buffer[(bufferIdx - 4) & Mask] -
C1 * I1_buffer[(bufferIdx - 6) & Mask]) * adj;
double jQ = (C1 * q1 +
C2 * Q1_buffer[(bufferIdx - 2) & Mask] -
C2 * Q1_buffer[(bufferIdx - 4) & Mask] -
C1 * Q1_buffer[(bufferIdx - 6) & Mask]) * adj;
// Phasor addition
double i2_val = i1 - jQ;
double q2_val = q1 + jI;
// Smooth i2, q2 (using FMA for precision)
i2 = Math.FusedMultiplyAdd(SmoothCoef, i2_val, SmoothPrev * p_i2);
q2 = Math.FusedMultiplyAdd(SmoothCoef, q2_val, SmoothPrev * p_q2);
// Homodyne discriminator
double re_val = Math.FusedMultiplyAdd(i2, p_i2, q2 * p_q2);
double im_val = Math.FusedMultiplyAdd(i2, p_q2, -q2 * p_i2);
// Smooth re, im (using FMA)
re = Math.FusedMultiplyAdd(SmoothCoef, re_val, SmoothPrev * p_re);
im = Math.FusedMultiplyAdd(SmoothCoef, im_val, SmoothPrev * p_im);
// Calculate Period
double angle = Math.Atan2(im, re);
double newPeriod = Math.Abs(angle) > MinDeltaRadians
? TwoPi / Math.Abs(angle)
: p_period;
// Adjust Period
double periodCap = p_period * 1.5;
double periodFloor = p_period * 0.67;
if (newPeriod > periodCap)
{
newPeriod = periodCap;
}
if (newPeriod < periodFloor)
{
newPeriod = periodFloor;
}
if (newPeriod < MinPeriod)
{
newPeriod = MinPeriod;
}
if (newPeriod > MaxPeriod)
{
newPeriod = MaxPeriod;
}
// Smooth Period (using FMA)
period = Math.FusedMultiplyAdd(SmoothCoef, newPeriod, SmoothPrev * p_period);
// Phase calculation
double phase = Math.Atan2(q1, i1);
// Adaptive alpha
double diff = NormalizeAngle(p_phase - phase);
double delta = Math.Max(Math.Abs(diff), MinDeltaRadians);
double alpha = scaledFastLimit / delta;
alpha = Math.Clamp(alpha, slowLimit, fastLimit);
// Final indicators (using FMA for precision)
double decay = 1.0 - alpha;
mama = Math.FusedMultiplyAdd(p_mama, decay, alpha * priceBuffer[bufferIdx]);
double famaAlpha = FamaAlphaFactor * alpha;
double famaDecay = 1.0 - famaAlpha;
fama = Math.FusedMultiplyAdd(p_fama, famaDecay, famaAlpha * mama);
// Update previous state
p_i2 = i2;
p_q2 = q2;
p_re = re;
p_im = im;
p_period = period;
p_phase = phase;
p_mama = mama;
p_fama = fama;
}
else
{
// Initialization
sumPr += price;
double avg = count > 0 ? sumPr / count : price;
mama = avg;
fama = avg;
// Init simple state
smoothBuffer[bufferIdx] = 0;
detrender[bufferIdx] = 0;
I1_buffer[bufferIdx] = 0;
Q1_buffer[bufferIdx] = 0;
// Set initial p_state
p_mama = avg;
p_fama = avg;
p_period = MinPeriod;
p_phase = 0;
}
output[i] = mama;
if (!famaOutput.IsEmpty)
{
famaOutput[i] = fama;
}
}
}
public static (TSeries Results, Mama Indicator) Calculate(TSeries source, double fastLimit = 0.5, double slowLimit = 0.05)
{
var indicator = new Mama(fastLimit, slowLimit);
TSeries results = indicator.Update(source);
return (results, indicator);
}
}