2025-12-09 21:32:06 -05:00
|
|
|
|
using System;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
using System.Collections.Generic;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
using System.Runtime.CompilerServices;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
using System.Runtime.InteropServices;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
namespace QuanTAlib;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// MESA Adaptive Moving Average (MAMA)
|
|
|
|
|
|
/// A trend-following indicator that adapts to the market's phase rate of change.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[SkipLocalsInit]
|
2025-12-16 21:16:50 -08:00
|
|
|
|
public sealed class Mama : AbstractBase
|
2025-12-09 21:32:06 -05:00
|
|
|
|
{
|
|
|
|
|
|
public TValue Fama { get; private set; }
|
2025-12-24 20:50:58 -08:00
|
|
|
|
public override bool IsHot => _state.Index > 50;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
private readonly double _fastLimit;
|
|
|
|
|
|
private readonly double _slowLimit;
|
2025-12-24 20:50:58 -08:00
|
|
|
|
private readonly double _scaledFastLimit;
|
2025-12-27 15:46:28 -08:00
|
|
|
|
private readonly TValuePublishedHandler _handler;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
2025-12-27 15:46:28 -08:00
|
|
|
|
[StructLayout(LayoutKind.Auto)]
|
2025-12-10 21:58:45 -05:00
|
|
|
|
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;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
private readonly RingBuffer _priceBuffer;
|
|
|
|
|
|
private readonly RingBuffer _smoothBuffer;
|
|
|
|
|
|
private readonly RingBuffer _detrender;
|
|
|
|
|
|
private readonly RingBuffer _I1_buffer;
|
|
|
|
|
|
private readonly RingBuffer _Q1_buffer;
|
|
|
|
|
|
|
2025-12-21 14:37:44 -08:00
|
|
|
|
// High-precision constants
|
2025-12-24 20:50:58 -08:00
|
|
|
|
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;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
public Mama(double fastLimit = 0.5, double slowLimit = 0.05)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (fastLimit <= slowLimit || fastLimit <= 0 || slowLimit <= 0)
|
|
|
|
|
|
{
|
2025-12-27 15:46:28 -08:00
|
|
|
|
throw new ArgumentException("FastLimit must be > SlowLimit and > 0", nameof(fastLimit));
|
2025-12-09 21:32:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
_fastLimit = fastLimit;
|
|
|
|
|
|
_slowLimit = slowLimit;
|
2025-12-24 20:50:58 -08:00
|
|
|
|
_scaledFastLimit = fastLimit * MinDeltaRadians;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
_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})";
|
2025-12-24 20:50:58 -08:00
|
|
|
|
WarmupPeriod = 50;
|
2025-12-27 15:46:28 -08:00
|
|
|
|
_handler = Handle;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
Init();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Mama(ITValuePublisher source, double fastLimit = 0.5, double slowLimit = 0.05) : this(fastLimit, slowLimit)
|
|
|
|
|
|
{
|
2025-12-27 15:46:28 -08:00
|
|
|
|
source.Pub += _handler;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-27 15:46:28 -08:00
|
|
|
|
private void Handle(object? sender, TValueEventArgs e) => Update(e.Value, e.IsNew);
|
|
|
|
|
|
|
2025-12-24 20:50:58 -08:00
|
|
|
|
private void Init()
|
2025-12-14 21:55:40 -08:00
|
|
|
|
{
|
|
|
|
|
|
Reset();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-16 21:16:50 -08:00
|
|
|
|
public override void Reset()
|
2025-12-09 21:32:06 -05:00
|
|
|
|
{
|
2025-12-10 21:58:45 -05:00
|
|
|
|
_state = default;
|
|
|
|
|
|
_state.Mama = double.NaN;
|
|
|
|
|
|
_state.Fama = double.NaN;
|
|
|
|
|
|
_p_state = _state;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
_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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-25 20:18:14 -08:00
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
|
private static double NormalizeAngle(double angle)
|
|
|
|
|
|
{
|
|
|
|
|
|
while (angle <= -Math.PI) angle += TwoPi;
|
|
|
|
|
|
while (angle > Math.PI) angle -= TwoPi;
|
|
|
|
|
|
return angle;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 21:32:06 -05:00
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
2025-12-16 21:16:50 -08:00
|
|
|
|
private double Step(double price, bool isNew)
|
2025-12-09 21:32:06 -05:00
|
|
|
|
{
|
|
|
|
|
|
if (isNew)
|
|
|
|
|
|
{
|
2025-12-10 21:58:45 -05:00
|
|
|
|
_p_state = _state;
|
|
|
|
|
|
_state.Index++;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-12-10 21:58:45 -05:00
|
|
|
|
_state = _p_state;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!double.IsFinite(price))
|
|
|
|
|
|
{
|
2025-12-10 21:58:45 -05:00
|
|
|
|
price = _state.LastValidPrice;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-12-10 21:58:45 -05:00
|
|
|
|
_state.LastValidPrice = price;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_priceBuffer.Add(price, isNew);
|
|
|
|
|
|
|
2025-12-10 21:58:45 -05:00
|
|
|
|
if (_state.Index > 6)
|
2025-12-09 21:32:06 -05:00
|
|
|
|
{
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double adj = (AdjSlope * _state.Period) + AdjIntercept;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Smooth
|
2025-12-12 13:47:57 -08:00
|
|
|
|
double smooth = (4.0 * _priceBuffer[^1] + 3.0 * _priceBuffer[^2] + 2.0 * _priceBuffer[^3] + _priceBuffer[^4]) * 0.1;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
_smoothBuffer.Add(smooth, isNew);
|
|
|
|
|
|
|
|
|
|
|
|
// Detrender
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double dt = (C1 * _smoothBuffer[^1] + C2 * _smoothBuffer[^3] - C2 * _smoothBuffer[^5] - C1 * _smoothBuffer[^7]) * adj;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
_detrender.Add(dt, isNew);
|
|
|
|
|
|
|
|
|
|
|
|
// Q1
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double q1 = (C1 * dt + C2 * _detrender[^3] - C2 * _detrender[^5] - C1 * _detrender[^7]) * adj;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
_Q1_buffer.Add(q1, isNew);
|
|
|
|
|
|
|
|
|
|
|
|
// I1 = dt[3]
|
2025-12-12 13:47:57 -08:00
|
|
|
|
double i1 = _detrender[^4];
|
2025-12-09 21:32:06 -05:00
|
|
|
|
_I1_buffer.Add(i1, isNew);
|
|
|
|
|
|
|
|
|
|
|
|
// Advance phases
|
|
|
|
|
|
// jI = CalculateHilbertTransform(_i1, adj)
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double jI = (C1 * i1 + C2 * _I1_buffer[^3] - C2 * _I1_buffer[^5] - C1 * _I1_buffer[^7]) * adj;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
// jQ = CalculateHilbertTransform(_q1, adj)
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double jQ = (C1 * q1 + C2 * _Q1_buffer[^3] - C2 * _Q1_buffer[^5] - C1 * _Q1_buffer[^7]) * adj;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Phasor addition
|
|
|
|
|
|
double i2_val = i1 - jQ;
|
|
|
|
|
|
double q2_val = q1 + jI;
|
|
|
|
|
|
|
|
|
|
|
|
// Smooth i2, q2
|
2025-12-24 20:50:58 -08:00
|
|
|
|
_state.I2 = SmoothCoef * i2_val + SmoothPrev * _p_state.I2;
|
|
|
|
|
|
_state.Q2 = SmoothCoef * q2_val + SmoothPrev * _p_state.Q2;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Homodyne discriminator
|
2025-12-10 21:58:45 -05:00
|
|
|
|
double re_val = (_state.I2 * _p_state.I2) + (_state.Q2 * _p_state.Q2);
|
|
|
|
|
|
double im_val = (_state.I2 * _p_state.Q2) - (_state.Q2 * _p_state.I2);
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Smooth re, im
|
2025-12-24 20:50:58 -08:00
|
|
|
|
_state.Re = SmoothCoef * re_val + SmoothPrev * _p_state.Re;
|
|
|
|
|
|
_state.Im = SmoothCoef * im_val + SmoothPrev * _p_state.Im;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Calculate Period
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double angle = Math.Atan2(_state.Im, _state.Re);
|
|
|
|
|
|
double period = Math.Abs(angle) > MinDeltaRadians
|
|
|
|
|
|
? TwoPi / Math.Abs(angle)
|
|
|
|
|
|
: _p_state.Period;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Adjust Period
|
2025-12-12 13:47:57 -08:00
|
|
|
|
double periodCap = _p_state.Period * 1.5;
|
|
|
|
|
|
double periodFloor = _p_state.Period * 0.67;
|
|
|
|
|
|
|
|
|
|
|
|
if (period > periodCap) period = periodCap;
|
|
|
|
|
|
if (period < periodFloor) period = periodFloor;
|
|
|
|
|
|
|
2025-12-24 20:50:58 -08:00
|
|
|
|
if (period < MinPeriod) period = MinPeriod;
|
|
|
|
|
|
if (period > MaxPeriod) period = MaxPeriod;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Smooth Period
|
2025-12-24 20:50:58 -08:00
|
|
|
|
_state.Period = SmoothCoef * period + SmoothPrev * _p_state.Period;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Phase calculation
|
2025-12-24 20:50:58 -08:00
|
|
|
|
_state.Phase = Math.Atan2(q1, i1);
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
// Adaptive alpha
|
2025-12-25 20:18:14 -08:00
|
|
|
|
double diff = NormalizeAngle(_p_state.Phase - _state.Phase);
|
|
|
|
|
|
double delta = Math.Max(Math.Abs(diff), MinDeltaRadians);
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double alpha = _scaledFastLimit / delta;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
alpha = Math.Clamp(alpha, _slowLimit, _fastLimit);
|
|
|
|
|
|
|
|
|
|
|
|
// Final indicators
|
2025-12-12 13:47:57 -08:00
|
|
|
|
_state.Mama = alpha * _priceBuffer[^1] + (1.0 - alpha) * _p_state.Mama;
|
2025-12-24 20:50:58 -08:00
|
|
|
|
_state.Fama = FamaAlphaFactor * alpha * _state.Mama + (1.0 - FamaAlphaFactor * alpha) * _p_state.Fama;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// Initialization phase
|
2025-12-10 21:58:45 -05:00
|
|
|
|
_state.SumPr += price;
|
|
|
|
|
|
double avg = _state.Index > 0 ? _state.SumPr / _state.Index : price;
|
|
|
|
|
|
_state.Mama = avg;
|
|
|
|
|
|
_state.Fama = avg;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
2025-12-09 21:32:06 -05:00
|
|
|
|
// Initialize buffers with 0
|
|
|
|
|
|
_smoothBuffer.Add(0, isNew);
|
|
|
|
|
|
_detrender.Add(0, isNew);
|
|
|
|
|
|
_I1_buffer.Add(0, isNew);
|
|
|
|
|
|
_Q1_buffer.Add(0, isNew);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-16 21:16:50 -08:00
|
|
|
|
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);
|
2025-12-10 21:58:45 -05:00
|
|
|
|
Fama = new TValue(input.Time, _state.Fama);
|
2025-12-27 15:46:28 -08:00
|
|
|
|
PubEvent(Last, isNew);
|
2025-12-09 21:32:06 -05:00
|
|
|
|
return Last;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-16 21:16:50 -08:00
|
|
|
|
public override TSeries Update(TSeries source)
|
2025-12-09 21:32:06 -05:00
|
|
|
|
{
|
2025-12-16 21:16:50 -08:00
|
|
|
|
if (source.Count == 0) return new TSeries([], []);
|
2025-12-09 21:32:06 -05:00
|
|
|
|
|
|
|
|
|
|
int len = source.Count;
|
|
|
|
|
|
var v = new List<double>(len);
|
|
|
|
|
|
var t = new List<long>(len);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < len; i++)
|
|
|
|
|
|
{
|
2025-12-16 21:16:50 -08:00
|
|
|
|
var result = Update(new TValue(source.Times[i], source.Values[i]));
|
|
|
|
|
|
t.Add(result.Time);
|
2025-12-09 21:32:06 -05:00
|
|
|
|
v.Add(result.Value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new TSeries(t, v);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-16 21:16:50 -08:00
|
|
|
|
public override void Prime(ReadOnlySpan<double> source)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var value in source)
|
|
|
|
|
|
{
|
|
|
|
|
|
Step(value, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static TSeries Batch(TSeries source, double fastLimit = 0.5, double slowLimit = 0.05)
|
2025-12-14 21:55:40 -08:00
|
|
|
|
{
|
|
|
|
|
|
var mama = new Mama(fastLimit, slowLimit);
|
|
|
|
|
|
return mama.Update(source);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 21:32:06 -05:00
|
|
|
|
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double fastLimit = 0.5, double slowLimit = 0.05)
|
|
|
|
|
|
{
|
2025-12-16 21:16:50 -08:00
|
|
|
|
if (source.Length == 0) return;
|
2025-12-25 20:53:56 -08:00
|
|
|
|
if (output.Length < source.Length)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new ArgumentOutOfRangeException(nameof(output), "Output buffer must be at least as large as the input buffer.");
|
|
|
|
|
|
}
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Stack allocate buffers for high performance (size 8 for power of 2 masking)
|
|
|
|
|
|
// We need 7 elements, but 8 allows & 7 masking
|
|
|
|
|
|
Span<double> priceBuffer = stackalloc double[8];
|
|
|
|
|
|
Span<double> smoothBuffer = stackalloc double[8];
|
|
|
|
|
|
Span<double> detrender = stackalloc double[8];
|
|
|
|
|
|
Span<double> I1_buffer = stackalloc double[8];
|
|
|
|
|
|
Span<double> Q1_buffer = stackalloc double[8];
|
|
|
|
|
|
|
|
|
|
|
|
int bufferIdx = 0; // Current index for circular buffer
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// State variables
|
|
|
|
|
|
double period = 0, mama = 0, sumPr = 0;
|
|
|
|
|
|
double i2 = 0, q2 = 0, re = 0, im = 0, lastValidPrice = 0;
|
|
|
|
|
|
double p_period = 0, p_phase = 0, p_mama = 0;
|
|
|
|
|
|
double p_i2 = 0, p_q2 = 0, p_re = 0, p_im = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Constants
|
|
|
|
|
|
const int Mask = 7;
|
2025-12-24 20:50:58 -08:00
|
|
|
|
// 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;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
2025-12-09 21:32:06 -05:00
|
|
|
|
for (int i = 0; i < source.Length; i++)
|
|
|
|
|
|
{
|
2025-12-16 21:16:50 -08:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double adj = (AdjSlope * period) + AdjIntercept;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
// 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
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double dt = (C1 * smoothBuffer[bufferIdx] +
|
|
|
|
|
|
C2 * smoothBuffer[(bufferIdx - 2) & Mask] -
|
|
|
|
|
|
C2 * smoothBuffer[(bufferIdx - 4) & Mask] -
|
|
|
|
|
|
C1 * smoothBuffer[(bufferIdx - 6) & Mask]) * adj;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
detrender[bufferIdx] = dt;
|
|
|
|
|
|
|
|
|
|
|
|
// Q1
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double q1 = (C1 * dt +
|
|
|
|
|
|
C2 * detrender[(bufferIdx - 2) & Mask] -
|
|
|
|
|
|
C2 * detrender[(bufferIdx - 4) & Mask] -
|
|
|
|
|
|
C1 * detrender[(bufferIdx - 6) & Mask]) * adj;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
Q1_buffer[bufferIdx] = q1;
|
|
|
|
|
|
|
|
|
|
|
|
// I1 = dt[3]
|
|
|
|
|
|
double i1 = detrender[(bufferIdx - 3) & Mask];
|
|
|
|
|
|
I1_buffer[bufferIdx] = i1;
|
|
|
|
|
|
|
|
|
|
|
|
// Advance phases
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double jI = (C1 * i1 +
|
|
|
|
|
|
C2 * I1_buffer[(bufferIdx - 2) & Mask] -
|
|
|
|
|
|
C2 * I1_buffer[(bufferIdx - 4) & Mask] -
|
|
|
|
|
|
C1 * I1_buffer[(bufferIdx - 6) & Mask]) * adj;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double jQ = (C1 * q1 +
|
|
|
|
|
|
C2 * Q1_buffer[(bufferIdx - 2) & Mask] -
|
|
|
|
|
|
C2 * Q1_buffer[(bufferIdx - 4) & Mask] -
|
|
|
|
|
|
C1 * Q1_buffer[(bufferIdx - 6) & Mask]) * adj;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Phasor addition
|
|
|
|
|
|
double i2_val = i1 - jQ;
|
|
|
|
|
|
double q2_val = q1 + jI;
|
|
|
|
|
|
|
|
|
|
|
|
// Smooth i2, q2
|
2025-12-24 20:50:58 -08:00
|
|
|
|
i2 = SmoothCoef * i2_val + SmoothPrev * p_i2;
|
|
|
|
|
|
q2 = SmoothCoef * q2_val + SmoothPrev * p_q2;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Homodyne discriminator
|
|
|
|
|
|
double re_val = (i2 * p_i2) + (q2 * p_q2);
|
|
|
|
|
|
double im_val = (i2 * p_q2) - (q2 * p_i2);
|
|
|
|
|
|
|
|
|
|
|
|
// Smooth re, im
|
2025-12-24 20:50:58 -08:00
|
|
|
|
re = SmoothCoef * re_val + SmoothPrev * p_re;
|
|
|
|
|
|
im = SmoothCoef * im_val + SmoothPrev * p_im;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Calculate Period
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double angle = Math.Atan2(im, re);
|
|
|
|
|
|
double newPeriod = Math.Abs(angle) > MinDeltaRadians
|
|
|
|
|
|
? TwoPi / Math.Abs(angle)
|
|
|
|
|
|
: p_period;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Adjust Period
|
|
|
|
|
|
double periodCap = p_period * 1.5;
|
|
|
|
|
|
double periodFloor = p_period * 0.67;
|
|
|
|
|
|
|
|
|
|
|
|
if (newPeriod > periodCap) newPeriod = periodCap;
|
|
|
|
|
|
if (newPeriod < periodFloor) newPeriod = periodFloor;
|
|
|
|
|
|
|
2025-12-24 20:50:58 -08:00
|
|
|
|
if (newPeriod < MinPeriod) newPeriod = MinPeriod;
|
|
|
|
|
|
if (newPeriod > MaxPeriod) newPeriod = MaxPeriod;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Smooth Period
|
2025-12-24 20:50:58 -08:00
|
|
|
|
period = SmoothCoef * newPeriod + SmoothPrev * p_period;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Phase calculation
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double phase = Math.Atan2(q1, i1);
|
2025-12-16 21:16:50 -08:00
|
|
|
|
|
|
|
|
|
|
// Adaptive alpha
|
2025-12-25 20:18:14 -08:00
|
|
|
|
double diff = NormalizeAngle(p_phase - phase);
|
|
|
|
|
|
double delta = Math.Max(Math.Abs(diff), MinDeltaRadians);
|
2025-12-24 20:50:58 -08:00
|
|
|
|
double alpha = scaledFastLimit / delta;
|
2025-12-16 21:16:50 -08:00
|
|
|
|
alpha = Math.Clamp(alpha, slowLimit, fastLimit);
|
|
|
|
|
|
|
|
|
|
|
|
// Final indicators
|
|
|
|
|
|
mama = alpha * priceBuffer[bufferIdx] + (1.0 - alpha) * p_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;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// Initialization
|
|
|
|
|
|
sumPr += price;
|
|
|
|
|
|
double avg = count > 0 ? sumPr / count : price;
|
|
|
|
|
|
mama = 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_period = 0; // Initial period state
|
|
|
|
|
|
p_phase = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize other state variables if needed for next iteration logic?
|
|
|
|
|
|
// Actually they just stay 0/default until we hit count > 6
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
output[i] = mama;
|
2025-12-09 21:32:06 -05:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|