Skip to content

Replacing magic numbers in Ehlers MAMA and HTIT #1761

@mihakralj

Description

@mihakralj

In the original Ehlers code for MAMA and HTIT, the magic numbers 0.0962 and 0.5769 are used to apply a Finite Impulse Response (FIR) filter to theoretical underlying IIR.

I hate poor rounded/truncated magic numbers, so I spent way too much time with Wolfram Alpha to find what actual ratios were used by Ehlers.

Passband Optimized Weights

  • From: 0.0962 and 0.5769
  • To: 5/52 and 15/26
  • These coefficients define the Hilbert Transformer's frequency response. Using the exact fractions ensures the filter's gain is normalized and uses high precision. If you look at the relationship, 15/26 is exactly 6*(5/52). Using fractions prevents "drift" in the phase calculation caused by cumulative rounding errors in long time-series datasets. I know you cherish precision and hate drift as much as I do. :-)

Gain Regression Constants

The variables adj (adjustment) used 0.075 and 0.54 to correct the amplitude based on the detected period.

  • From: 0.075 (Slope) and 0.54 (Intercept)
  • To: 3/40 and 27/50
  • These numbers come from a linear regression (y = mx + b) designed to compensate for the HT's inherent gain. 3/40 (0.075) is the specific rate at which the signal amplitude must be scaled relative to the period length. 27/50 (0.54) is the base offset.

Using ratios makes the origin of magic numbers in MAMA/HTIT clear: they are derived from geometric properties of the sine waves being analyzed, rather than being "tuned by hand" values.

Suggested change of your code in HTIT (similar would apply for MAMA):

namespace Skender.Stock.Indicators;

/// <summary>
/// Provides methods for calculating the Hilbert Transform Instantaneous Trendline (HTL) indicator.
/// </summary>
public static partial class HtTrendline
{
    /// <summary>
    /// Converts a list of time-series values to Hilbert Transform Instantaneous Trendline (HTL) results.
    /// </summary>
    /// <param name="source">The list of time-series values to transform.</param>
    /// <returns>A list of HTL results and smoothed price.</returns>
    /// <exception cref="ArgumentNullException">Thrown when the source list is null.</exception>
    public static IReadOnlyList<HtlResult> ToHtTrendline(
        this IReadOnlyList<IReusable> source)
    {
        // prefer HL2 when IQuote
        IReadOnlyList<IReusable> values
            = source.ToPreferredList(CandlePart.HL2);

        // initialize
        int length = values.Count;
        List<HtlResult> results = new(length);

        double pr = new double[length]; // price
        double sp = new double[length]; // smooth price
        double dt = new double[length]; // detrender
        double pd = new double[length]; // period

        double q1 = new double[length]; // quadrature
        double i1 = new double[length]; // in-phase

        double q2 = new double[length]; // adj. quadrature
        double i2 = new double[length]; // adj. in-phase

        double re = new double[length];
        double im = new double[length];

        double sd = new double[length]; // smooth period
        double it = new double[length]; // instantaneous trend (raw)

        // Fractional Ratios for Filter Coefficients and Linear Regressions
        const double CoeffA = 5d / 52d;    // Primary outer weights (0.09615...)
        const double CoeffB = 15d / 26d;   // Secondary inner weights (0.57692...)
        const double GainSlope = 3d / 40d; // Gain correction slope (0.075)
        const double GainInt = 27d / 50d;  // Gain correction intercept (0.54)

        // roll through source values
        for (int i = 0; i < length; i++)
        {
            IReusable s = values[i];
            pr[i] = s.Value;

            if (i > 5)
            {
                // Amplitude correction derived from previous period to maintain unity gain
                double adj = (GainSlope * pd[i - 1]) + GainInt;

                // 4-tap low-pass smoothing FIR filter
                sp[i] = ((4d * pr[i]) + (3d * pr[i - 1]) + (2d * pr[i - 2]) + pr[i - 3]) / 10d;

                // Improved Hilbert Transformer (7-tap FIR)
                dt[i] = ((CoeffA * sp[i]) + (CoeffB * sp[i - 2]) - (CoeffB * sp[i - 4]) - (CoeffA * sp[i - 6])) * adj;

                // Decompose into In-Phase and Quadrature components
                q1[i] = ((CoeffA * dt[i]) + (CoeffB * dt[i - 2]) - (CoeffB * dt[i - 4]) - (CoeffA * dt[i - 6])) * adj;
                i1[i] = dt[i - 3]; // 3-bar center-tap delay to align phases

                // Advance phases by 90 degrees to create analytic complex signal
                double jI = ((CoeffA * i1[i]) + (CoeffB * i1[i - 2]) - (CoeffB * i1[i - 4]) - (CoeffA * i1[i - 6])) * adj;
                double jQ = ((CoeffA * q1[i]) + (CoeffB * q1[i - 2]) - (CoeffB * q1[i - 4]) - (CoeffA * q1[i - 6])) * adj;

                // Phasor addition for temporal averaging
                i2[i] = i1[i] - jQ;
                q2[i] = q1[i] + jI;

                // Smoothing I and Q components (1/5 and 4/5 EMA)
                i2[i] = (0.2d * i2[i]) + (0.8d * i2[i - 1]);
                q2[i] = (0.2d * q2[i]) + (0.8d * q2[i - 1]);

                // Homodyne Discriminator: complex multiplication by previous conjugate
                re[i] = (i2[i] * i2[i - 1]) + (q2[i] * q2[i - 1]);
                im[i] = (i2[i] * q2[i - 1]) - (q2[i] * i2[i - 1]);

                re[i] = (0.2d * re[i]) + (0.8d * re[i - 1]);
                im[i] = (0.2d * im[i]) + (0.8d * im[i - 1]);

                // Calculate cycle period based on phase rotation (2*PI radians = 360 degrees)
                pd[i] = im[i]!= 0 && re[i]!= 0
                   ? 2d * Math.PI / Math.Atan(im[i] / re[i])
                    : 0d;

                // Constraint logic using exact ratio thresholds
                pd[i] = pd[i] > (3d / 2d) * pd[i - 1]? (3d / 2d) * pd[i - 1] : pd[i]; // 1.5x limit
                pd[i] = pd[i] < (67d / 100d) * pd[i - 1]? (67d / 100d) * pd[i - 1] : pd[i]; // 0.67x limit
                pd[i] = pd[i] < 6d? 6d : pd[i];
                pd[i] = pd[i] > 50d? 50d : pd[i];

                // Recursive smoothing of the period measurement
                pd[i] = (0.2d * pd[i]) + (0.8d * pd[i - 1]);
                sd[i] = (33d / 100d * pd[i]) + (67d / 100d * sd[i - 1]);

                // Calculate the Instantaneous Trendline over the Dominant Cycle
                int dcPeriods = (int)(double.IsNaN(sd[i])? 0 : sd[i] + 0.5d);
                double sumPr = 0;
                for (int d = i - dcPeriods + 1; d <= i; d++)
                {
                    if (d >= 0)
                    {
                        sumPr += pr[d];
                    }
                    else
                    {
                        dcPeriods--;
                    }
                }

                it[i] = dcPeriods > 0? sumPr / dcPeriods : pr[i];

                // Final indicator compilation
                results.Add(new(
                    Timestamp: s.Timestamp,
                    DcPeriods: dcPeriods > 0? dcPeriods : null,
                    Trendline: i >= 11 
                     ? (((4d * it[i]) + (3d * it[i - 1]) + (2d * it[i - 2]) + it[i - 3]) / 10d).NaN2Null()
                      : pr[i].NaN2Null(),
                    SmoothPrice: (((4d * pr[i]) + (3d * pr[i - 1]) + (2d * pr[i - 2]) + pr[i - 3]) / 10d).NaN2Null()));
            }
            else
            {
                // Initialization sequence
                results.Add(new(
                    Timestamp: s.Timestamp,
                    DcPeriods: null,
                    Trendline: pr[i].NaN2Null(),
                    SmoothPrice: null));

                pd[i] = 0;
                sp[i] = 0;
                dt[i] = 0;
                i1[i] = 0;
                q1[i] = 0;
                i2[i] = 0;
                q2[i] = 0;
                re[i] = 0;
                im[i] = 0;
                sd[i] = 0;
            }
        }

        return results;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions