Hello. I tried to convert this ehlers indicator via claude 4.6. I’m not sure if the conversion is correct. If anyone with expertise could kindly help/confirm this.
The formula was derived in 10 steps, which are explained here.
The Hann FIR low-pass filter is a non-recursive FIR filter: each tap depends only on the previous taps, not on its own past output. For this reason, it can be implemented within an AFL function using Ref() — there is no need for an indexed loop. Reduces high-frequency noise prior to IIR filtering.
The RMS (Root Mean Square) FUNCTION calculates the root mean square over Len bars. Used to normalise Real and Imag so that they have unit amplitude regardless of volatility. This is also non-recursive and has been implemented using Ref().
First step, pre-smoothing with a Hann filter (Length=12) attenuates noise prior to high-pass filtering. The ‘+ 0’ forces AFL to treat the result as a local array rather than as a reference to the function.
Second step, a 2-pole high-pass filter on HannPrice (Period=UpperBound) removes the trend components (low frequencies). It only passes cycles with a period <= UpperBound.
AFL NOTE: IIR (recursive) filters CANNOT be implemented within an AFL function because their output depends on previous values of the same output (HP[i-1], // HP[i-2]).
AFL functions do not maintain state between bars. For this reason, all IIR filters are implemented using for loops in the main body, with arrays initialised to Close*0.
NOTE on coefficients: the original Ehlers formula uses c1 = (1 + c2 - c3) / 4, but in AFL, with c3 negative, this results in hc1 > 1, making the filter unstable (it diverges). The correct formula for stability is: hc1 = (1 - hc2/2 + hc3) / 4
STEP 3 — 2-pole SuperSmoother on HP (Period=LowerBound) IIR low-pass filter that eliminates cycles with period < LowerBound. Combined with HighPass, it creates a bandpass filter: it only passes cycles between LowerBound and UpperBound.
STEP 4 — Real component normalised by RMS LP contains the bandpass signal. Dividing it by its RMS normalises it to an amplitude of ~1, which is necessary to correctly calculate the phase angle in the complex plane.
STEP 5 — IMAGINARY component normalised by RMS In signal analysis, the imaginary component of a sinusoidal signal is its derivative (rate of change). This is also normalised by its own RMS.
STEP 6 — Calculation of the Dominant Cycle (DC) Ehlers uses the derivative of the arctangent of (Imag/Real) to measure the instantaneous frequency of the signal. The formula avoids the discontinuity of the arctangent by working directly with the derivatives of Real and Imag. DC is then restricted to the interval [LowerBound, UpperBound].
STEP 7 — HighPass on Close (Period=Mid) Second high-pass filter applied directly to Close (not to HannPrice) to construct the BP signal.
STEP 8 — UltimateSmoother on HP2 (Period=Mid) Ehlers’ IIR filter with virtually zero latency. Combined with HP2, it creates a bandpass filter centred on Mid. BP is used to detect zero crossings and reset the phase of the synthetic oscillator. NOTE: for UltimateSmoother, hc1 uses the original formula (1 + uc2 - uc3) / 4 — this is correct here because the filter structure differs from HighPass. STEP 9 — Phase Accumulation The phase is incremented by 360/DC degrees per bar, simulating the rotation of a phasor at the DC period. At BP zero crossings, the phase is reset: - upward crossing: Phase = 180/DC (start of positive half-cycle) - downward crossing: Phase = 180 + 180/DC (start of negative half-cycle) warmup=100 ignores the first bars where the filters are not yet stabilised. Phase is kept within [0,360] using the modulus to prevent unlimited accumulation, which would cause an incorrect sin() in AFL. AFL NOTE: in EasyLanguage, ‘Phase = Phase + 360/DC’ is automatically recursive (it uses the value from the previous bar). In AFL, the explicit loop Phase[i-1] is required.
STEP 10 — Synthetic Oscillator Synth is the sine of the accumulated phase, so it oscillates between -1 and +1 at the dominant cycle’s frequency. Glitch correction eliminates visual discontinuities that occur when the phase is reset but the signal is still in the same quadrant.
SetBarsRequired( 500, 0 );
LowerBound = Param( "LowerBound", 15, 5, 50, 1 );
UpperBound = Param( "UpperBound", 25, 5, 100, 1 );
function Hann_func( Price, Len )
{
Filt = 0;
coef = 0;
for( count = 1; count <= Len; count++ )
{
w = 1 - cos( 360 * count / ( Len + 1 ) * 3.14159265 / 180 );
Filt = Filt + w * Ref( Price, -( count - 1 ) );
coef = coef + w;
}
return IIf( coef != 0, Filt / coef, Price );
}
function RMS_func( Price, Len )
{
SumSq = 0;
for( count = 0; count < Len; count++ )
SumSq = SumSq + Ref( Price, -count ) * Ref( Price, -count );
return IIf( SumSq != 0, sqrt( SumSq / Len ), 0.0001 );
}
HannPrice = Hann_func( Close, 12 ) + 0;
_a1 = exp( -1.414 * 3.14159265 / UpperBound );
hc2 = 2 * _a1 * cos( 1.414 * 180 / UpperBound * 3.14159265 / 180 );
hc3 = -_a1 * _a1;
hc1 = ( 1 - hc2/2 + hc3 ) / 4;
HP = Close * 0;
for( i = 2; i < BarCount; i++ )
HP[i] = hc1 * ( HannPrice[i] - 2*HannPrice[i-1] + HannPrice[i-2] )
+ hc2 * HP[i-1] + hc3 * HP[i-2];
_Q = exp( -1.414 * 3.14159265 / LowerBound );
ss_C1 = 2 * _Q * cos( 1.414 * 180 / LowerBound * 3.14159265 / 180 );
ss_C2 = _Q * _Q;
ss_A0 = ( 1 - ss_C1 + ss_C2 ) / 2;
LP = Close * 0;
for( i = 2; i < BarCount; i++ )
LP[i] = ss_A0 * ( HP[i] + HP[i-1] )
+ ss_C1 * LP[i-1] - ss_C2 * LP[i-2];
RMSval = RMS_func( LP, 100 );
Real = IIf( RMSval != 0, LP / RMSval, 0 );
RealROC = Real - Ref( Real, -1 );
QRMSval = RMS_func( RealROC, 100 );
Imag = IIf( QRMSval != 0, RealROC / QRMSval, 0 );
Denom = ( Real - Ref(Real,-1) ) * Imag
- ( Imag - Ref(Imag,-1) ) * Real;
DC = IIf( Denom != 0,
6.28 * ( Real*Real + Imag*Imag ) / Denom,
( LowerBound + UpperBound ) / 2 );
DC = Max( LowerBound, Min( UpperBound, DC ) );
Mid = sqrt( LowerBound * UpperBound );
_a1m = exp( -1.414 * 3.14159265 / Mid );
hc2m = 2 * _a1m * cos( 1.414 * 180 / Mid * 3.14159265 / 180 );
hc3m = -_a1m * _a1m;
hc1m = ( 1 - hc2m/2 + hc3m ) / 4;
HP2 = Close * 0;
for( i = 2; i < BarCount; i++ )
HP2[i] = hc1m * ( Close[i] - 2*Close[i-1] + Close[i-2] )
+ hc2m * HP2[i-1] + hc3m * HP2[i-2];
_a1u = exp( -1.414 * 3.14159265 / Mid );
uc2 = 2 * _a1u * cos( 1.414 * 180 / Mid * 3.14159265 / 180 );
uc3 = -_a1u * _a1u;
uc1 = ( 1 + uc2 - uc3 ) / 4;
BP = Close * 0;
for( i = 2; i < BarCount; i++ )
BP[i] = ( 1 - uc1 ) * HP2[i]
+ ( 2*uc1 - uc2 ) * HP2[i-1]
- ( uc1 + uc3 ) * HP2[i-2]
+ uc2 * BP[i-1]
+ uc3 * BP[i-2];
// -- Fase: proteggi DC e ignora barre iniziali instabili --
warmup = 100;
Phase = Close * 0;
for( i = warmup; i < BarCount; i++ )
{
safeDC = Max( LowerBound, Min( UpperBound, DC[i] ) );
Phase[i] = Phase[i-1] + 360 / safeDC;
if( BP[i] > 0 AND BP[i-1] <= 0 )
Phase[i] = 180 / safeDC;
if( BP[i] < 0 AND BP[i-1] >= 0 )
Phase[i] = 180 + ( 180 / safeDC );
// Evita accumulo illimitato — tieni Phase in [0, 360]
Phase[i] = Phase[i] - 360 * int( Phase[i] / 360 );
}
Synth = sin( Phase * 3.14159265 / 180 );
for( i = warmup+1; i < BarCount; i++ )
{
if( Phase[i] > 0 AND Phase[i] < 90 AND Synth[i] < Synth[i-1] )
Synth[i] = Synth[i-1];
if( Phase[i] > 180 AND Phase[i] < 270 AND Synth[i] > Synth[i-1] )
Synth[i] = Synth[i-1];
}
Plot( Synth, "Synth Osc", colorBlue, styleLine | styleThick );
Plot( 0, "Zero", colorGrey40, styleLine );
PlotOHLC( 0, IIf( Synth > 0, Synth, 0 ), 0, IIf( Synth > 0, Synth, 0 ),
"", colorGreen, styleCloud | styleNoLabel, Null, Null, Null, -1 );
PlotOHLC( 0, IIf( Synth < 0, Synth, 0 ), 0, IIf( Synth < 0, Synth, 0 ),
"", colorRed, styleCloud | styleNoLabel, Null, Null, Null, -1 );
SynthColor = IIf( LastValue(Synth) > 0, colorGreen, colorRed );
Title = EncodeColor( colorWhite ) + "Synthetic Oscillator (Ehlers)"
+ " | LB=" + LowerBound + " UB=" + UpperBound
+ " | DC=" + WriteVal( LastValue(DC), 1.2 )
+ " Phase=" + WriteVal( LastValue(Phase), 1.2 )
+ " Synth=" + EncodeColor( SynthColor )
+ WriteVal( LastValue(Synth), 4.3 );

