TRADERS’ TIPS

April 2026

Tips Article Thumbnail

For this month’s Traders’ Tips, the focus is John F. Ehlers’ article in this issue, “A Synthetic Oscillator.” Here, we present the April 2026 Traders’ Tips code with possible implementations in various software.

You can right-click on any chart to open it in a new tab or window and view it at it’s originally supplied size, often much larger than the version printed in the magazine.

The Traders’ Tips section is provided to help the reader implement a selected technique from an article in this issue or another recent issue. The entries here are contributed by software developers or programmers for software that is capable of customization.


logo

TradeStation: April 2026

In “A Synthetic Oscillator” in this issue, John Ehlers introduces a nonlinear oscillator designed to reduce lag while maintaining smooth, responsive trading signals. The indicator adapts to changing market conditions by measuring the instantaneous dominant cycle and generating signals through a phase-based sine waveform.

EasyLanguage code for the indicator is shown here and a sample chart plotting the indicator is shown in Figure 1.

Function: $HighPass

{
	$HighPass Function
 	(C) 2004-2024 John F. Ehlers
}

inputs:
	Price(numericseries),
	Period(numericsimple);
	
variables:
	a1( 0 ),
	b1( 0 ),
	c1( 0 ),
	c2( 0 ),
	c3( 0 );

a1 = ExpValue(-1.414 * 3.14159 / Period);
b1 = 2 * a1 * Cosine(1.414 * 180 / Period);
c2 = b1;
c3 = -a1 * a1;
c1 = (1 + c2 - c3) / 4;

if CurrentBar >= 4 then 
 	$HighPass = c1*(Price - 2 * Price[1] + Price[2]) +
	 c2 * $HighPass[1] + c3 * $HighPass[2];
if Currentbar < 4 then 
	$HighPass = 0;


Function: $RMS

{
 $RMS Function
 (C) 2025 John F. Ehlers
}

inputs:
	Price( numericseries ),
	Length( numericsimple );

variable:
	SumSq( 0 ),
	Count( 0 );

SumSq = 0;

for Count = 0 to Length - 1
begin
	SumSq = SumSq + Price[count]*Price[count];
end;

if SumSq <> 0 then 
	$RMS = SquareRoot(SumSq / Length);


Function: $SuperSmoother

{
	$SuperSmoother Function
	(C) 2025 John F. Ehlers
}
inputs:
	Price( numericseries ),
	Period( numericsimple );

variables:
	A0( 0 ),
	Q( 0 ),
	C1( 0 ),
	C2( 0 );
	
Q = ExpValue( -1.414*3.14159 / Period );
C1 = 2 * Q * Cosine(1.414*180 / Period);
C2 = Q*Q;
A0 = (1 - c1 + c2) / 2;

if CurrentBar >= 4 Then $SuperSmoother = A0*( Price +
 Price[1]) + C1 * $SuperSmoother[1] - C2 * $SuperSmoother[2];

if Currentbar < 4 then 
	$SuperSmoother = Price;


Function: $Hann

{
	$Hann Windowed Lowpass FIR Filter Function
	(c) 2025 John F. Ehlers
}
inputs:
	Price( numericseries ),
	Length( numericsimple );

variables:
	count(0),
	coef(0),
	Filt(0);
	Filt = 0;
	coef = 0;

for count = 1 to Length 
begin
	Filt = Filt + (1 - Cosine(360*count / (Length +
	 1)))*Price[count - 1];
	coef = coef + (1 - Cosine(360*count / (Length + 1)));
end;

if coef <> 0 then 
	$Hann = Filt / coef;


Function $UltimateSmoother

{
	UltimateSmoother Function
	(C) 2004-2024 John F. Ehlers
}

inputs:
	Price( numericseries ),
	Period( numericsimple );
	
variables:
	a1( 0 ),
	b1( 0 ),
	c1( 0 ),
	c2( 0 ),
	c3( 0 ),
	US( 0 );
	
a1 = ExpValue(-1.414*3.14159 / Period);
b1 = 2 * a1 * Cosine(1.414*180 / Period);
c2 = b1;
c3 = -a1 * a1;
c1 = (1 + c2 - c3) / 4;

if CurrentBar >= 4 then 
 US = (1 - c1)*Price + (2 * c1 - c2) * Price[1] 
 - (c1 + c3) * Price[2] + c2*US[1] + c3 * US[2];
 
if CurrentBar < 4 then 
	US = Price;

$UltimateSmoother = US;


Indicator: Synthetic Oscillator

{
	TASC APR 2026
	Synthetic Oscillator Indicator
	(C) 2025 John F. Ehlers
}

inputs:
	LowerBound( 15 ),
	UpperBound( 25 ),
	Length( 4 );

variables:
	Price( 0 ),
	HP( 0 ),
	LP( 0 ),
	RMS( 0 ),
	Real( 0 ),
	ROC( 0 ),
	QRMS( 0 ),
	Imag( 0 ),
	Denom( 0 ),
	DC( 0 ),
	Count( 0 ),
	Mid( 0 ),
	HP2( 0 ),
	BP( 0 ),
	Phase( 0 ),
	Synth( 0 ),
	Synth2( 0 ),
	ROC2( 0 );

Price = $Hann( Close, 12 );

{ Real component is bandpass filtered and normalized }
HP = $HighPass( Price, UpperBound );
LP = $SuperSmoother( HP, LowerBound );

RMS = $RMS( LP, 100 );
if RMS <> 0 then
	Real = LP / RMS;

{ Imaginary component is rate of change normalized }
ROC = Real - Real[ 1 ];

QRMS = $RMS( ROC, 100 );
if QRMS <> 0 then
	Imag = ROC / QRMS;

{ Solve rate of change of arctangent }
Denom = ( ( Real - Real[ 1 ] ) * Imag ) 
 - ( ( Imag - Imag[ 1 ] ) * Real );
 
if Denom <> 0 then
	DC = 6.28 * ( ( Real * Real ) 
	 + ( Imag * Imag ) ) / Denom;

{ Limit range of measured values }
if DC < LowerBound then
	DC = LowerBound;

if DC > UpperBound then
	DC = UpperBound;

Mid = SquareRoot( LowerBound * UpperBound );

{ Create a bandpass filter at the average dominant cycle period }
HP2 = $HighPass( Close, Mid );
BP  = $UltimateSmoother( HP2, Mid );

{ Cumulate phase and force reset at 0 and 180 degrees }
Phase = Phase + ( 360 / DC );

if BP crosses over 0 then
	Phase = 180 / DC;

if BP crosses under 0 then
	Phase = 180 + ( 180 / DC );

{ Synthetic oscillator is the sine of the cumulative phase angle }
Synth = Sine( Phase );

{ Remove reset glitch if continuity falls in the same quadrant }
if Phase > 0 and Phase < 90 and Synth < Synth[ 1 ] then
	Synth = Synth[ 1 ];

if Phase > 180 and Phase < 270 and Synth > Synth[ 1 ] then
	Synth = Synth[ 1 ];

plot1( Synth );
plot2( 0 );
Sample Chart

FIGURE 1: TRADESTATION. This demonstrates a daily chart of the S&P 500 ETF SPY with the indicator applied, showing a portion of 2025 and 2026.

This article is for informational purposes. No type of trading or investment recommendation, advice, or strategy is being made, given, or in any manner provided by TradeStation Securities or its affiliates.

—John Robinson
TradeStation Securities, Inc.
www.TradeStation.com

BACK TO LIST

logo

Wealth-Lab.com: April 2026

In his article in this issue, John Ehlers introduces a synthetic oscillator he developed for reversion-to-the-mean-type trading.

In Wealth-Lab, users don’t need to code it; they can simply drag and drop the SyntheticOscillator onto a chart from WealthLab’s list of TASC preprogrammed indicators. This can easily be done using Wealth-Lab’s building blocks.

For Wealth-Lab users who also want to see or use the C# code, this time with optimizable parameters, we are also showing that code below.

using WealthLab.Backtest;
using WealthLab.Core;
using WealthLab.Indicators;
using WealthLab.TASC;

namespace WealthScript42
{
    public class SynthOscSample : UserStrategyBase
    {
        Parameter _Ubnd;
        Parameter _Lbnd;
        Parameter _Length;

        public SynthOscSample()
        {
            _Lbnd = AddParameter("Lowerbound", ParameterType.Int32, 12, 5, 15);
            _Ubnd = AddParameter("Upperbound", ParameterType.Int32, 25, 15, 30);
            _Length = AddParameter("Length", ParameterType.Int32, 12, 5, 15);
        }

        SyntheticOscillator _synth;
        TimeSeries _roc2;

        public override void Initialize(BarHistory bars)
        {
            _synth = new SyntheticOscillator(bars.Close, _Lbnd.AsInt, _Ubnd.AsInt);
            PlotIndicator(_synth);

            _roc2 = Momentum.Series(Hann.Series(_synth, _Length.AsInt), 1);
            StartIndex = _Ubnd.AsInt;
        }

        public override void Execute(BarHistory bars, int idx)
        {
            if (!HasOpenPosition(bars, PositionType.Long))
            {
                if (_roc2.CrossesOver(0, idx))
                    PlaceTrade(bars, TransactionType.Buy, OrderType.Market);
            }
            else
            {
                if (_roc2.CrossesUnder(0, idx))
                    ClosePosition(LastPosition, OrderType.Market);
            }
        }
    }
}

Figure 2 shows some recent entry/exit trades using the demonstration trading strategy given in Ehlers’ article using the synthetic oscillator. The trading strategy is based on the concept of when the momentum of a Hann-windowed SyntheticOscillator crosses above/below zero.

Sample Chart

FIGURE 2: WEALTH-LAB. Entry/exit signals are displayed on a daily chart of emini S&P 500 futures (ES) for the example trading strategy based on the synthetic oscillator given in John Ehlers’ article in this issue.

—Robert Sucher
Wealth-Lab
www.wealth-lab.com

BACK TO LIST

logo

NinjaTrader: April 2026

In “A Synthetic Oscillator” in this issue, John Ehlers introduces an indicator he developed. The synthetic oscillator discussed in the article is available for download at the following link for NinjaTrader 8:

Once the file is downloaded, you can import the indicator into NinjaTrader 8 from within the control center by selecting Tools → Import → NinjaScript Add-On and then selecting the downloaded file for NinjaTrader 8.

You can review the indicator source code in NinjaTrader 8 by selecting the menu New → NinjaScript Editor → Indicators folder from within the control center window and selecting the file.

A sample chart is shown in Figure 3.

Sample Chart

FIGURE 3: NINJATRADER. John Ehlers’ synthetic oscillator is demonstrated on a daily chart of emini S&P 500 futures (ES).

NinjaScript uses compiled DLLs that run native, not interpreted, to provide you with the highest performance possible.

—Jesse N.
NinjaTrader, LLC
www.ninjatrader.com

BACK TO LIST

logo

RealTest: April 2026

Provided here is coding for use in the RealTest platform to implement the indicator introduced in John Ehlers’ article in this issue, “A Synthetic Oscillator.”

Notes:
	John Ehlers "Synthetic Oscillator", TASC April 2026.
	Implements and plots the indicator as in the article.
	Adds a simple strategy to demonstrate trading signals.

Import:
	DataSource:	Norgate
	IncludeList:	&ES_CCB // back-adjusted continuous contract
	StartDate:	2008-01-01
	EndDate:	Latest
	SaveAs:	es.rtd

Settings:
	DataFile:	es.rtd
	BarSize:	Daily
	StartDate:	2009-01-01
	EndDate:	Latest

Parameters:
	LowerBound:	15
	UpperBound:	25
	Len:	4

Data:
	// Common constants
	decay_factor:	-1.414 * 3.14159
	phase_angle:	1.414 * 180

	// Hann windowed filter of Close (Len 12)
	filt:	Sum((1 - Cosine(360 * FunBar / 13)) * Close, 12)
	coef:	Sum(1 - Cosine(360 * FunBar / 13), 12)
	hann:	filt / coef

	// Highpass Filter at UpperBound period
	hp_Q:	exp(decay_factor / UpperBound)
	hp_c1:	2 * hp_Q * Cosine(phase_angle / UpperBound)
	hp_c2:	hp_Q * hp_Q
	hp_a0:	(1 + hp_c1 + hp_c2) / 4
	HP:	if(BarNum >= 4, hp_a0 * (hann - 2 * hann[1] + hann[2]) + hp_c1 * HP[1] - hp_c2 * HP[2], 0)

	// SuperSmoother at LowerBound period
	ss_Q:	exp(decay_factor / LowerBound)
	ss_c1:	2 * ss_Q * Cosine(phase_angle / LowerBound)
	ss_c2:	ss_Q * ss_Q
	ss_a0:	(1 - ss_c1 + ss_c2) / 2
	LP:	if(BarNum >= 4, ss_a0 * (HP + HP[1]) + ss_c1 * LP[1] - ss_c2 * LP[2], HP)

	// RMS of LP and normalized Real component
	RMS_LP:	Sqr(SumSQ(LP, 100) / 100)
	Real:	if(RMS_LP, LP / RMS_LP, nonan(Real[1]))

	// Imaginary component (normalized rate of change of Real)
	ROC_R:	Real - Real[1]
	QRMS:	Sqr(SumSQ(ROC_R, 100) / 100)
	Imag:	if(QRMS, ROC_R / QRMS, nonan(Imag[1]))

	// Dominant Cycle period
	Denom:	(Real - Real[1]) * Imag - (Imag - Imag[1]) * Real
	rawDC:	if(Denom, 6.28 * (Real * Real + Imag * Imag) / Denom, nonan(rawDC[1]))
	DC:	Bound(rawDC, LowerBound, UpperBound)

	// Mid frequency for bandpass filter
	MidP:	Sqr(LowerBound * UpperBound)

	// Highpass of Close at Mid period
	hp2_Q:	exp(decay_factor / MidP)
	hp2_c1:	2 * hp2_Q * Cosine(phase_angle / MidP)
	hp2_c2:	hp2_Q * hp2_Q
	hp2_a0:	(1 + hp2_c1 + hp2_c2) / 4
	HP2:	if(BarNum >= 4, hp2_a0 * (Close - 2 * Close[1] + Close[2]) + hp2_c1 * HP2[1] - hp2_c2 * HP2[2], 0)

	// Ultimate Smoother of HP2 at Mid period (same period as above so use those elements again)
	BP:	if(BarNum >= 4, (1 - hp2_a0) * HP2 + (2 * hp2_a0- hp2_c1) * hp2[1] + (hp2_c2 - hp2_a0) * hp2[2] + hp2_c1 * nonan(BP[1]) - hp2_c2 * nonan(BP[2]), HP2)

	// Phase accumulation with reset at zero crossings of BP
	Phase:	Select(Cross(BP, 0), 180 / DC, Cross(0, BP), 180 + 180 / DC, nonan(Phase[1]) + 360 / DC)

	// Synthetic Oscillator with glitch removal
	rawSynth:	Sine(Phase)
	Synth:	if((Phase > 0 and Phase < 90 and rawSynth < nonan(Synth[1])) or (Phase > 180 and Phase < 270 and rawSynth > nonan(Synth[1])), nonan(Synth[1]), rawSynth)

	// Trading signal: Hann smoothed Synth and its rate of change
	filt2:	Sum((1 - Cosine(360 * FunBar / (Len + 1))) * Synth, Len)
	coef2:	Sum(1 - Cosine(360 * FunBar / (Len + 1)), Len)
	Synth2:	filt2 / coef2
	ROC2:	Synth2 - Synth2[1]
	goLong:	Cross(ROC2, 0)
	goShort:	Cross(0, ROC2)

Charts:
	Synth:	Synth {|}
	Zero:	0 {|}

Strategy: synth_long
	Side:	Long
	Quantity:	1
	EntrySetup:	goLong
	ExitRule:	goShort

Strategy: synth_short
	Side:	Short
	Quantity:	1
	EntrySetup:	goShort
	ExitRule:	goLong

Figure 4 shows an example of the oscillator plotted on a chart of ES.

Sample Chart

FIGURE 4: REALTEST. The synthetic oscillator is plotted beneath a daily chart of emini S&P 500 futures (ES).

—Marsten Parker
MHP Trading
mhp@mhptrading.com

BACK TO LIST

logo

TradingView: April 2026

The TradingView Pine Script code presented here implements the synthetic oscillator presented in John Ehlers’ article in this issue, “A Synthetic Oscillator.”

//  TASC Issue: April 2026
//     Article: Avoiding Whipsaw Trades
//              A Synthetic Oscillator
//  Article By: John F. Ehlers
//    Language: TradingView's Pine Script® v6
// Provided By: PineCoders, for tradingview.com


//@version=6
TITLE = "TASC 2026.04 A Synthetic Oscillator"
indicator(TITLE, "SO", false)

//#region Inputs

float src = input.source(close, "Source Series:")
int lb = input.int(15, "Lower Bound:")
int ub = input.int(25, "Upper Bound:")

//#endregion

//#region Functions

// @function The SuperSmoother is a second-order infinite im-
// pulse response (IIR) filter, meaning that it uses two
// previous calculations of the filter output in the cur-
// rent calculation of the filter response.
// @param   src     Source series
// @param   period  Critical period
// @returns Smoothed series
SuperSmoother (float src, int period) =>
    float q = math.exp(-1.414*math.pi/period)
    float c1 = 2.0*q*math.cos(1.414*math.pi/period)
    float c2 = q*q
    float a0 = (1.0 - c1 + c2)/2
    float ss = src
    if bar_index >= 4
        ss := a0*(src + src[1]) + 
              c1*ss[1] - c2*ss[2]
    ss

// @function      The UltimateSmoother is a filter created
//                by subtracting the response of a high-pass 
//                filter from that of an all-pass filter.
// @param src     Source series.
// @param period  Critical period.
// @returns       Smoothed series
UltimateSmoother (float src, int period) =>
    float q = math.exp(-1.414*math.pi/period)
    float c1 = 2.0*q*math.cos(1.414*math.pi/period)
    float c2 = q*q
    float a0 = (1.0 + c1 + c2)/4.0
    float us = src
    if bar_index >= 4
        us := (1.0 - a0)*src + 
              (2.0*a0 - c1)*src[1] + 
              (c2 - a0)*src[2] + 
              c1*nz(us[1]) - c2*nz(us[2])
    us

// @function Root Mean Square
RMS (float Source, int Length) =>
    float s2 = math.sum(Source*Source, Length)
    if s2 != 0
        math.sqrt(s2/Length)
    else
        0.0

// @function High Pass Filter
HP (float src, int Period) =>
    float Q = math.exp(-1.414*math.pi/Period)
    float c1 = 2.0*Q*math.cos(1.414*math.pi/Period)
    float c2 = Q*Q
    float a0 = (1 + c1 + c2)/4
    float hp = 0.0
    if bar_index >= 4
        hp := a0*(src - 2*src[1] + 
              src[2]) + c1*hp[1] - c2*hp[2]
    hp

//@function Hann Filter
Hann (float src, int length) =>
    float filt = 0.0
    float coef = 0.0
    for c = 1 to length
        float p = math.cos(2*math.pi*c/(length + 1))
        filt += 1.0 - p*src[c-1]
        coef += 1.0 - p
    coef != 0.0 ? filt/coef : 0.0

// @function Synthetic Oscillator.
// @param   src     Source Series.
// @param   LB      Lower Bound.
// @param   UB      Upper Bound.
// @param   length  Length period.
// @returns Synthetic Oscillator.
SO (float src=close, int LB=15, int UB=25) =>
    float price = Hann(src,12)
    // Real component is bandpass filtered and normalized
    float hp = HP(price, UB)
    float lp = SuperSmoother(hp, LB)
    float rms = RMS(lp, 100)
    float re = rms != 0.0 ? lp/rms : 0.0
    // Imaginary component is rate of change normalized.
    float roc = re - re[1]
    float qrms = RMS(roc, 100)
    float im = qrms != 0.0 ? roc/qrms : 0.0
    // Solve rate of change of arctangent.
    float denom = roc*im - (im - im[1])*re
    float dc = denom != 0.0 ? 
      6.28*(re*re + im*im)/denom : 0.0
    // Limit range of measured values.
    dc := math.max(LB, math.min(UB, dc))
    int mid = int(math.sqrt(LB*UB))
    // Create a Bandpass filter at the Average Dominant
    // Cycle Period.
    float hp2 = HP(src, mid)
    float bp = UltimateSmoother(hp2, mid)
    // Cumulate Phase and force reset at 0 and 180 degrees
    var float ph = 0.0
    ph += 2*math.pi/dc
    bool xo = ta.crossover(bp, 0.0)
    bool xu = ta.crossunder(bp, 0.0)
    switch
        xo => ph := math.pi/dc
        xu => ph := math.pi + math.pi/dc
    
    // Synthetic Oscillator is the Sine of the cumulative
    // phase angle.
    float so = math.sin(ph)
    // Remove reset glitch if continuity falls in the 
    // same quadrant.
    switch
        ph > 0.0 and ph < math.pi/2 and so < so[1] =>
            so := so[1]
        ph > math.pi and ph < 3*math.pi/2 and so > so[1] => 
            so := so[1]
        =>
            so

//#endregion

//#region Calculations

so = SO(src, lb, ub)

//#endregion

//#region Plots

plot(so, "Synthetic Oscillator")
hline(0, "Zero Line")

//#endregion

The indicator is available on TradingView from the PineCodersTASC account at https://www.tradingview.com/u/PineCodersTASC/#published-scripts.

An example chart is shown in Figure 5.

Sample Chart

FIGURE 5: TRADINGVIEW. An example of John Ehlers’ synthetic oscillator is plotted beneath a daily chart of emini S&P 500 futures (ES).

—PineCoders, for TradingView
www.TradingView.com

BACK TO LIST

logo

NeuroShell Trader: April 2026

The synthetic oscillator, introduced in John Ehlers’ article in this issue, “A Synthetic Oscillator,” can be easily implemented in NeuroShell Trader using NeuroShell Trader’s ability to call external dynamic linked libraries. Dynamic linked libraries (DLLs) can be written in C, C++, orPower Basic.

After moving the code given in the article to your preferred compiler and creating a DLL, you can insert the resulting indicator(s) as follows:

  1. Select “New indicator …” from the insert menu
  2. Choose the “External program & library calls” category.
  3. Select the appropriate external DLL call indicator.
  4. Set up the parameters to match your DLL.
  5. Select the finished button.
Sample Chart

FIGURE 6: NEUROSHELL TRADER. This NeuroShell Trader chart demonstrates the synthetic oscillator on a chart of Emini S&P 500 futures (ES).

Users of NeuroShell Trader can go to the Stocks & Commodities section of the NeuroShell Trader free technical support website to download a copy of this or any previous Traders’ Tips.

—Ward Systems Group, Inc.
sales@wardsystems.com
www.neuroshell.com

BACK TO LIST

Python: April 2026

Provided here is Python code to implement concepts described in John Ehlers’ article in this issue, “A Synthetic Oscillator.”

Readers will also find the following code in a Jupyter notebook on GitHub at: https://github.com/jainraje/TraderTipArticles.

"""
Python code to implement concepts in Technical Analysis of Stocks & Commodiities magazine 
April 2026 article "A Synthetic Oscillator" by John F Ehlers. This python code is provided 
for TraderTips section of the magazine.

Written By:
Rajeev Jain, Feb 2026
jainraje@yahoo,com

All code available in GitHub:
https://github.com/jainraje/TraderTipArticles/

HELPER FUNCTIONS 
Not included in Trader Tip article but included in the associated
Jupyter notebook found in the GitHub repo.

Functions called:
- HannFilter
- SuperSmoother
- HighPassFilter
- UltimateSmoother
- RMS

"""

# import required python libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
import math
print(yf.__version__)

# MAIN CODE FOR ARTICLE

def synthetic_oscillator(close, params=(15, 25, 4)):
    """
    Synthetic Oscillator function (Python version compatible with list-based helper functions)
    close       : list of closing prices
    lower_bound : Lower bound of cycle period
    upper_bound : Upper bound of cycle period
    length      : smoothing length (not directly used here)
    returns: list of oscillator values

    """

    lower_bound = params[0]
    upper_bound = params[1]
    length = params[2]

    n = len(close)
    synth = [0] * n
    phase = [0] * n

    # Step 1: Price preprocessing with Hann window
    price = hann_lowpass(close, length=12)

    # Step 2: Real component (bandpass filtered & normalized)
    hp = high_pass(price, upper_bound)
    lp = super_smoother(hp, lower_bound)
    rms_lp = rms(lp, 100)
    real = [lp[i] / rms_lp[i] if rms_lp[i] != 0 else 0 for i in range(n)]

    # Step 3: Imaginary component (rate of change normalized)
    roc = [0] * n
    for i in range(1, n):
        roc[i] = real[i] - real[i-1]
    qrms = rms(roc, 100)
    imag = [roc[i] / qrms[i] if qrms[i] != 0 else 0 for i in range(n)]

    # Step 4: Dominant cycle period (DC) calculation
    dc = [0] * n
    for i in range(1, n):
        denom = (real[i] - real[i-1]) * imag[i] - (imag[i] - imag[i-1]) * real[i]
        if denom != 0:
            dc_val = 2 * math.pi * (real[i]**2 + imag[i]**2) / denom
            # limit DC to lower and upper bounds
            dc[i] = max(lower_bound, min(dc_val, upper_bound))
        else:
            dc[i] = lower_bound

    # Step 5: Midpoint cycle
    mid = math.sqrt(lower_bound * upper_bound)

    # Step 6: Bandpass filter at average dominant cycle
    hp2 = high_pass(close, mid)
    bp = ultimate_smoother(hp2, mid)

    # Step 7: Phase accumulation
    for i in range(1, n):
        phase[i] = phase[i-1] + 360 / dc[i]

        # Reset phase at BP zero crossings
        if bp[i-1] < 0 <= bp[i]:
            phase[i] = 180 / dc[i]
        elif bp[i-1] > 0 >= bp[i]:
            phase[i] = 180 + 180 / dc[i]

    # Step 8: Synthetic oscillator = sine of cumulative phase
    synth = [math.sin(math.radians(p)) for p in phase]

    # Step 9: Remove reset glitch if continuity falls in same quadrant
    for i in range(1, n):
        if 0 < phase[i] < 90 and synth[i] < synth[i-1]:
            synth[i] = synth[i-1]
        elif 180 < phase[i] < 270 and synth[i] > synth[i-1]:
            synth[i] = synth[i-1]

    return synth

def plot_trading_signals(df, params, plot_so_ena=False, plot_roc2_ena=True, plot_buy_sell_ena=True):
    
    fig, (ax1, ax2) = plt.subplots(
        2, 1,
        figsize=(9, 6),
        sharex=True,
        constrained_layout=True,
        gridspec_kw={'height_ratios': [2, 1]}
    )
    
    # --- Price subplot ---
    ax1.plot(df.index, df['Close'], label='Close', color='black')
    ax1.set_ylabel('Close Price')

    if plot_buy_sell_ena:
        ax1.set_title(f"Ticker='{symbol}', Close & Buy and Sell Signals")
    else:
        ax1.set_title(f"Ticker='{symbol}', Close")
    ax1.grid(True)

    if plot_buy_sell_ena:
        # --- Plot Buy/Sell markers only on transitions ---
        if 'Signal' in df.columns:
            buy_idx = df[(df['Signal'] == 1) & (df['Signal'].shift(1) != 1)]
            sell_idx = df[(df['Signal'] == -1) & (df['Signal'].shift(1) != -1)]
    
            ax1.scatter(
                buy_idx.index,
                buy_idx['Close'],
                marker='^',
                color='green',
                s=100,
                label='Buy',
                zorder=3
            )
    
            ax1.scatter(
                sell_idx.index,
                sell_idx['Close'],
                marker='v',
                color='red',
                s=100,
                label='Sell',
                zorder=3
            )

    ax1.legend(loc='upper left')

    # --- Oscillator subplot ---
    ax2_left = ax2
    ax2_right = ax2.twinx()


    title_text = f"params={params}"

    if plot_so_ena and 'SO' in df.columns:
        ax2_right.plot(df.index, df['SO'], label='Synthetic Oscillator', color='darkblue')
        title_text += f", SO_last={df['SO'].iloc[-1]:.2f}"

    if plot_roc2_ena and 'ROC2' in df.columns:
        ax2_right.plot(df.index, df['ROC2'], label='ROC2', color='darkblue')
        title_text += f", ROC2_last={df['ROC2'].iloc[-1]:.2f}"
        if 'Signal' in df.columns:
            ax2_left.plot(df.index, df['Signal'], label='Signal', color='lightblue')
            ax2_left.set_ylabel('Signal')

    ax2_right.set_ylabel('Oscillators')
    ax2_right.axhline(0, color='black', linewidth=2)
    ax2_right.set_title(title_text)
    ax2_right.grid(True)

    lines_left, labels_left = ax2_left.get_legend_handles_labels()
    lines_right, labels_right = ax2_right.get_legend_handles_labels()
    ax2_right.legend(lines_left + lines_right, labels_left + labels_right, loc='upper left')
    
    plt.xticks(rotation=45)
    plt.show()


# The following function contains all indicator calculations and trading logic.
def calc_trading_strategy(ohlcv, params=None):

    if params is None:
        params=(17, 23, 8)

    length = params[2]

    df = ohlcv.copy()
    # synthetic oscillator indicator (aka SO)
    df['SO'] = synthetic_oscillator(df['Close'], params)

    # run SO output through Hann lowpass filter and perform ROC 
    df['SO2'] = hann_lowpass(df['SO'], params[2])
    df['ROC2'] = df['SO2'] - df['SO2'].shift()

    # buy logic (ROC2 cross above 0)
    cond_buy = (df['ROC2'] > 0) & (df['ROC2'].shift() < 0)
    df['Signal'] = np.where(cond_buy, 1, np.nan)

    # sell logic (ROC2 cross below 0)
    cond_sell = (df['ROC2'] < 0) & (df['ROC2'].shift() > 0)
    df['Signal'] = np.where(cond_sell, -1, df['Signal'])
    df['Signal'] = df['Signal'].fillna(method='ffill')

    # add BUY and SELL alerts to dataframe
    df['Alert'] = np.where((df['Signal']==1) & (df['Signal'].shift()== -1), 'BUY', '')
    df['Alert'] = np.where((df['Signal']==-1) & (df['Signal'].shift()== 1), 'SELL', df['Alert'])

    return df


# EXAMPLE USAGE

# download price data from Yahoo Finance
symbol = '^GSPC'
symbol = 'ES=F'
ohlcv = yf.download(
    symbol, 
    start="2000-01-01", 
    end="2026-02-12", 
    group_by="Ticker",
    auto_adjust=True,
    progress=False,
)
ohlcv = ohlcv[symbol]


# Call trading strategy and plotting functions. 
# Use slicing technique to set plot start and end timeframes.
# Set arguments on plotting function to enable or disable 
# desired indicator plots

# example usage below shows closing price for instrument in top subplot
# and Synthetic Oscillator in the bottom subplot

# --- Set Indicator Parameters as desired ---
lower_bound = 17
upper_bound = 23
length = 8
params=(lower_bound, upper_bound, length)    
df = calc_trading_strategy(ohlcv, params)
plot_trading_signals(df['2010':'2010'], params, plot_so_ena=True, plot_roc2_ena=False, plot_buy_sell_ena=False)


# Call trading strategy and plotting functions. 
# Use slicing technique to set plot start and end timeframes.
# Note the Signal line (light blue) generates the buy and sell
# signals as the ROC2 indicator goes above and below zero.
# Inspect plot for buy or sell signal.

# --- Set Indicator Parameters as desired ---
lower_bound = 17
upper_bound = 23
length = 8

params=(lower_bound, upper_bound, length)    
df = calc_trading_strategy(ohlcv, params)
plot_trading_signals(df['2010':'2010'], params, plot_so_ena=False, plot_roc2_ena=True, plot_buy_sell_ena=True)


# Inspect resulting dataframe for a BUY or SELL Alert
# example below shows most recent last 12 days
df.tail(12)


# Filter to inspect all BUY and SELL Alerts
# Use slicing techniques to zero in on specific time frames
cond = df['Alert'] != ''
df[cond]['2025':'2025'].head(10)
Sample Chart

FIGURE 7: PYTHON. An example plot of John Ehlers’ synthetic oscillator is demonstrated on a chart of Emini S&P 500 futures (ES).

Sample Chart

FIGURE 8: PYTHON. Example trading signals display buy and sell points from a demonstration trading system based on the synthetic oscillator.

—Rajeev Jain

BACK TO LIST

Originally published in the April 2026 issue of
Technical Analysis of STOCKS & COMMODITIES magazine.
All rights reserved. © Copyright 2026, Technical Analysis, Inc.