mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 11:38:05 +00:00
- Updated the Prime method signature in multiple indicators (Jma, Kama, Lsma, Mama, Mgdi, Pwma, Rma, Sma, Ssf, Super, T3, Tema, Trima, Usf, Vidya, Wma, Atr) to accept an optional TimeSpan parameter for improved flexibility. - Added unit tests for Lsma to verify Dispose functionality, ensuring proper unsubscription from the source and thread safety. - Enhanced Mama and Wma classes to handle non-finite inputs gracefully and added checks for valid parameters in constructors. - Introduced additional tests for T3 to validate constructor behavior with invalid volume factors. - Ensured all indicators maintain consistent behavior when handling edge cases, such as empty buffers and non-finite values.
465 lines
16 KiB
C#
465 lines
16 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Runtime.CompilerServices;
|
||
using System.Runtime.InteropServices;
|
||
|
||
namespace QuanTAlib;
|
||
|
||
/// <summary>
|
||
/// MESA Adaptive Moving Average (MAMA)
|
||
/// A trend-following indicator that adapts to the market's phase rate of change.
|
||
/// </summary>
|
||
[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, 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 = (4.0 * _priceBuffer[^1] + 3.0 * _priceBuffer[^2] + 2.0 * _priceBuffer[^3] + _priceBuffer[^4]) * 0.1;
|
||
_smoothBuffer.Add(smooth, isNew);
|
||
|
||
// Detrender
|
||
double dt = (C1 * _smoothBuffer[^1] + C2 * _smoothBuffer[^3] - C2 * _smoothBuffer[^5] - C1 * _smoothBuffer[^7]) * adj;
|
||
_detrender.Add(dt, isNew);
|
||
|
||
// Q1
|
||
double q1 = (C1 * dt + C2 * _detrender[^3] - 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 = (C1 * i1 + C2 * _I1_buffer[^3] - C2 * _I1_buffer[^5] - C1 * _I1_buffer[^7]) * adj;
|
||
// jQ = CalculateHilbertTransform(_q1, adj)
|
||
double jQ = (C1 * q1 + C2 * _Q1_buffer[^3] - C2 * _Q1_buffer[^5] - C1 * _Q1_buffer[^7]) * adj;
|
||
|
||
// Phasor addition
|
||
double i2_val = i1 - jQ;
|
||
double q2_val = q1 + jI;
|
||
|
||
// Smooth i2, q2
|
||
_state.I2 = SmoothCoef * i2_val + SmoothPrev * _p_state.I2;
|
||
_state.Q2 = SmoothCoef * q2_val + SmoothPrev * _p_state.Q2;
|
||
|
||
// Homodyne discriminator
|
||
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);
|
||
|
||
// Smooth re, im
|
||
_state.Re = SmoothCoef * re_val + SmoothPrev * _p_state.Re;
|
||
_state.Im = 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
|
||
_state.Period = 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
|
||
_state.Mama = alpha * _priceBuffer[^1] + (1.0 - alpha) * _p_state.Mama;
|
||
_state.Fama = FamaAlphaFactor * alpha * _state.Mama + (1.0 - FamaAlphaFactor * alpha) * _p_state.Fama;
|
||
}
|
||
else
|
||
{
|
||
// Initialization phase
|
||
_state.SumPr += price;
|
||
double avg = _state.Index > 0 ? _state.SumPr / _state.Index : price;
|
||
_state.Mama = avg;
|
||
_state.Fama = avg;
|
||
|
||
// 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<double>(len);
|
||
var t = new List<long>(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);
|
||
}
|
||
|
||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||
{
|
||
foreach (var value in source)
|
||
{
|
||
Step(value, 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 Calculate(ReadOnlySpan<double> source, Span<double> output, double fastLimit = 0.5, double slowLimit = 0.05, Span<double> famaOutput = default)
|
||
{
|
||
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<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, fama = 0, sumPr = 0;
|
||
double i2 = 0, q2 = 0, re = 0, im = 0, lastValidPrice = 0;
|
||
double p_period = 0, 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
|
||
i2 = SmoothCoef * i2_val + SmoothPrev * p_i2;
|
||
q2 = SmoothCoef * q2_val + SmoothPrev * p_q2;
|
||
|
||
// Homodyne discriminator
|
||
double re_val = (i2 * p_i2) + (q2 * p_q2);
|
||
double im_val = (i2 * p_q2) - (q2 * p_i2);
|
||
|
||
// Smooth re, im
|
||
re = SmoothCoef * re_val + SmoothPrev * p_re;
|
||
im = 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
|
||
period = 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
|
||
mama = alpha * priceBuffer[bufferIdx] + (1.0 - alpha) * p_mama;
|
||
fama = FamaAlphaFactor * alpha * mama + (1.0 - FamaAlphaFactor * alpha) * p_fama;
|
||
|
||
// 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 = 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;
|
||
if (!famaOutput.IsEmpty)
|
||
{
|
||
famaOutput[i] = fama;
|
||
}
|
||
}
|
||
}
|
||
}
|