Re: Python to MT5

11
Adaptive SuperSmoother

Links: |TradingView|TradeStation|NinjaTrader|MT4|MT5a| MT5b | Python|

Python: September 2026
In his article in this issue, “Adaptive SuperSmoother,” John Ehlers introduces his adaptive SuperSmoother indicator based on his original SuperSmoother indicator. Presented here is Python code to construct and display the indicator.

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

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

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

"""

Code: Select all

# import required python modules
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
import math

print(yf.__version__)


# retrive OHLCV using YFinance
symbol = 'QQQ'
ohlcv = yf.download(
    symbol, 
    start="2000-01-01", 
    end="2026-07-11", 
    group_by="Ticker",
    auto_adjust=False,
)
ohlcv = ohlcv[symbol]
ohlcv

# functions to implement indicators calculations 

def rms(series, length):
    series = np.asarray(series, dtype=float)
    out = np.full(len(series), np.nan)

    for i in range(length - 1, len(series)):
        window = series[i - length + 1:i + 1]
        out[i] = np.sqrt(np.mean(window ** 2))

    return out

def supersmoother(price, period):
    price = np.asarray(price, dtype=float)
    out = np.full(len(price), np.nan)

    if len(price) < 3:
        return price.copy()

    Q = np.exp(-1.414 * np.pi / period)
    c1 = 2 * Q * np.cos(1.414 * np.pi / period)
    c2 = Q * Q
    a0 = (1 - c1 + c2) / 2

    # initialization
    out[0] = price[0]
    out[1] = price[1]

    for i in range(2, len(price)):
        out[i] = (
            a0 * (price[i] + price[i - 1])
            + c1 * out[i - 1]
            - c2 * out[i - 2]
        )

    return out

def adaptive_supersmoother(close, period0=20, rms_length=81):
    close = np.asarray(close, dtype=float)
    n = len(close)

    # outputs
    ss_adaptive = np.full(n, np.nan)
    ss_fixed = np.full(n, np.nan)
    period_series = np.full(n, np.nan)

    # initial pass (we update recursively)
    ss_temp = supersmoother(close, period0)

    roc1 = np.full(n, np.nan)
    roc = np.full(n, np.nan)

    for i in range(1, n):
        roc1[i] = ss_temp[i] - ss_temp[i - 1]

        # RMS normalization
        start = max(0, i - rms_length + 1)
        window = roc1[start:i + 1]
        rocrms = np.sqrt(np.mean(window ** 2)) if len(window) > 0 else 0

        if rocrms != 0:
            roc[i] = abs(roc1[i] / rocrms)
        else:
            roc[i] = 0

        roc[i] = min(roc[i], 2)

        period = period0 * (1 - 0.5 * roc[i]) ** 2
        period = max(period, 2)
        period_series[i] = period

    # Recompute adaptive SS properly (true recursive adaptive filter)
    ss_adaptive[0] = close[0]
    ss_adaptive[1] = close[1]

    for i in range(2, n):
        p = period_series[i - 1] if not np.isnan(period_series[i - 1]) else period0

        Q = np.exp(-1.414 * np.pi / p)
        c1 = 2 * Q * np.cos(1.414 * np.pi / p)
        c2 = Q * Q
        a0 = (1 - c1 + c2) / 2

        ss_adaptive[i] = (
            a0 * (close[i] + close[i - 1])
            + c1 * ss_adaptive[i - 1]
            - c2 * ss_adaptive[i - 2]
        )

    return {
        "adaptive_ss": ss_adaptive,
        "fixed_ss": supersmoother(close, period0),
        "roc": roc,
        "period": period_series
    }

def adaptive_supersmoother_series(close, period0=20, rms_length=81):
    return pd.Series(
        adaptive_supersmoother(close, period0, rms_length)["adaptive_ss"],
        index=close.index
    )


# function which encapsultes indicator call and signal generationr 
def run_calcs(ohlcv):
    
    df = ohlcv[['Close']].copy()

    df['SS'] = supersmoother(df['Close'], period=20)
    df['ASS'] = adaptive_supersmoother_series(df['Close'])

    
    df['ASS OSC'] = df['ASS'] - df['SS']
    df['ASS OSC NORM'] = (df['ASS'] - df['SS']) / df['Close'].rolling(20).std()
    
    #df['Signal'] = np.where(df['ASS OSC'] > 0, 1,0)
    df['Signal'] = np.where(df['ASS OSC'] > 0.1, 1,
                    np.where(df['ASS OSC'] < -0.1, 0, np.nan))
    df['Signal'] = df['Signal'].ffill()

    return df

df = run_calcs(ohlcv)
df
def simple_plot(df):
    import matplotlib.pyplot as plt

    fig, (ax1, ax2) = plt.subplots(
        2, 1,
        figsize=(9, 6),
        sharex=True,
        gridspec_kw={"height_ratios": [3, 1]}
    )

    # -------------------
    # TOP: Price + filters + signal
    # -------------------
    ax1.plot(df.index, df['Close'], label='Close', color='black', linewidth=1.5)
    ax1.plot(df.index, df['SS'], label='SuperSmoother (SS)', linewidth=1.2)
    ax1.plot(df.index, df['ASS'], label='Adaptive SS (ASS)', linewidth=1.2)

    ax1.set_ylabel('Price')
    ax1.grid(True)

    # Signal on secondary axis (TOP)
    ax1b = ax1.twinx()
    ax1b.step(df.index, df['Signal'], where='post',
              label='Signal (ASS OSC > 0)', color='green', alpha=0.5)

    ax1b.set_ylabel('Signal (0/1)')
    ax1b.set_ylim(-0.1, 1.1)

    # combine legends (top)
    l1, lab1 = ax1.get_legend_handles_labels()
    l2, lab2 = ax1b.get_legend_handles_labels()
    ax1.legend(l1 + l2, lab1 + lab2, loc='upper left')

    ax1.set_title('Price + SuperSmoother + Adaptive SS with Signal')

    # -------------------
    # BOTTOM: Oscillator
    # -------------------
    ax2.plot(df.index, df['ASS OSC'], label='ASS - SS Oscillator', color='purple', alpha=0.8)
    ax2.plot(df.index, df['ASS OSC NORM'], label='ASS NORM- SS Oscillator', color='orange', alpha=0.8)
    
    ax2.axhline(0, color='black', linewidth=1)

    ax2.set_ylabel('Osc')
    ax2.set_xlabel('Date')
    ax2.grid(True)
    ax2.legend()

    plt.tight_layout()
    plt.show()
simple_plot(df['2026':])
A sample chart plotting the original SuperSmoother indicator, the new adaptive version of the SuperSmoother indicator, and the SuperSmoother oscillator is shown in Figure 8.

Sample Chart FIGURE 8: PYTHON. The original SuperSmoother indicator (cyan plotline) and the adaptive version of the SuperSmoother indicator (orange) are plotted in the price pane; the SuperSmoother oscillator is shown in the lower pane. The perpendicular green lines show example signals.


Re: Python to MT5

12
The UniEMA: A Universal Exponential Moving Average

Links: | Tradingview | TradeStation | NinjaTrader | MT4 | MT5 | Python |

Python: November 2026
In his article in this issue, “The UniEMA: A Universal Exponential Moving Average,” Igor Durkin presents the UniEMA indicator.

Following is a Python code implementation based on the EasyLanguage code provided in Durkin’s article.

Code: Select all

"""
Python code implementing concepts from Igor Durkin's article,
"The UniEMA: A Universal Exponential Moving Average," published in
Technical Analysis of Stocks & Commodities. This code is provided
for the magazine's Traders' Tips section.

Written by:
Rajeev Jain, September 2026
jainraje@yahoo.com

Complete source code:
https://github.com/jainraje/TraderTipArticles/

"""

# import required python modules
import pandas as pd
import numpy as np
import yfinance as yf

import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.collections import LineCollection
from matplotlib.lines import Line2D

print(yf.__version__)

# get SPY price data from Yahoo Finance

ticker = 'SPY'
start_date = "1995-01-01"
end_date = "2026-09-22"

ohlcv = yf.download(
    ticker, 
    start=start_date, 
    end=end_date, 
    #group_by="Ticker",
    auto_adjust=False,
    progress=False,        # suppress download progress output
)
ohlcv = ohlcv.stack('Ticker', future_stack=True ).reset_index().set_index('Date')

# uniema and rsi indicator functions

def uniema(price, length=14, poles=2, order=3,
           weight_factor=2.0, damping_factor=0.5):
    
    """Universal EMA with configurable poles, order, weight, and damping."""
    price = np.asarray(price, dtype=float)

    if length < 1 or poles < 1 or order < 1:
        raise ValueError("length, poles, and order must be >= 1")
    if price.size == 0:
        return np.array([], dtype=float)

    alpha = order * weight_factor / (
        length + order * weight_factor - 1
    )
    beta = 1 - alpha

    state = np.full((order, poles), price[0], dtype=float)
    result = np.empty_like(price)
    result[0] = price[0]

    for t in range(1, len(price)):
        value = price[t]

        for k in range(order):
            for j in range(poles):
                state[k, j] = beta * state[k, j] + alpha * value

                value = (
                    state[k, j]
                    if j == 0
                    else value + damping_factor *
                    (state[k, 0] - state[k, j])
                )

        result[t] = value

    return result


def rsi(close, length=14, method="WILDERS",
        poles=2, order=3, weight_factor=1.0,
        damping_factor=0.5):
    """RSI using Wilder or UniEMA smoothing."""
    is_series = isinstance(close, pd.Series)
    index = close.index if is_series else None
    price = np.asarray(close, dtype=float)
    method = method.upper()

    if price.ndim != 1:
        raise ValueError("close must be one-dimensional")
    if length < 1:
        raise ValueError("length must be >= 1")
    if method not in {"WILDERS", "UNIEMA"}:
        raise ValueError("method must be 'WILDERS' or 'UNIEMA'")
    if price.size == 0:
        result = np.array([], dtype=float)
        return pd.Series(result, index=index, name="RSI") if is_series else result
    if np.isnan(price).any():
        raise ValueError("close contains NaN values")

    change = np.diff(price, prepend=price[0])
    gain = np.maximum(change, 0)
    loss = np.maximum(-change, 0)

    if method == "WILDERS":
        poles, order, weight_factor, damping_factor = 1, 1, 1.0, 0.0

    args = (
        length, poles, order,
        weight_factor, damping_factor,
    )

    average_gain = uniema(gain, *args)
    average_loss = uniema(loss, *args)

    strength = np.divide(
        average_gain,
        average_loss,
        out=np.full_like(average_gain, np.inf),
        where=average_loss != 0,
    )

    result = 100 - 100 / (1 + strength)
    result[(average_gain == 0) & (average_loss == 0)] = 50

    if is_series:
        return pd.Series(
            result,
            index=index,
            name=f"RSI_{length}_{method}",
        )

    return result


def run_calculations(ohlcv):
    
    # Remove rows with missing closing prices before calculating indicators
    df = ohlcv.dropna(subset=["Close"]).copy()
    
    # Calculate UniEMA, standard Wilder RSI, and UniEMA-smoothed RSI
    df['UniEMA'] = uniema(df['Close'],15,2,3,2,0.7)
    df['RSI'] = rsi(df['Close'], 14)
    df['UniRSI'] = rsi(df['Close'], 14, "UNIEMA",2,3,1,0.5)
    
    return df

df = run_calculations(ohlcv)



# here is one example of how to generate trading signals from the 
# uniema, unirsi indicators. there are many other ways.
def trading_logic(df):
    """
    Generate a long/cash trading signal using UniEMA trend direction
    and a UniRSI momentum regime.

    Signal 1:
        1 when UniEMA is rising; otherwise 0.

    Signal 2:
        1 when UniRSI rises above 50.
        0 when UniRSI falls below 45.
        Retains its previous state between 45 and 50.

    Combined signal:
        Enter long when both signals equal 1.
        Exit when both signals equal 0.
        Retain the current position when the signals disagree.
    """

    # Identify the current UniEMA trend direction
    df["Signal 1"] = np.where(
        df["UniEMA"] > df["UniEMA"].shift(1), 1, 0
    )

    # Create a UniRSI regime with 50/45 hysteresis
    df["Signal 2"] = np.where(df["UniRSI"] > 50, 1, np.nan)
    df["Signal 2"] = np.where(
        df["UniRSI"] < 45, 0, df["Signal 2"]
    )

    # Initialize in cash and retain the last confirmed regime
    df.at[df.index[0], "Signal 2"] = 0
    df["Signal 2"] = df["Signal 2"].ffill()

    # Enter when both trend and momentum are bullish
    cond_buy = (
        (df["Signal 1"] == 1) &
        (df["Signal 2"] == 1)
    )
    df["Signal"] = np.where(cond_buy, 1, np.nan)

    # Exit when both trend and momentum are bearish
    cond_sell = (
        (df["Signal 1"] == 0) &
        (df["Signal 2"] == 0)
    )
    df["Signal"] = np.where(
        cond_sell, 0, df["Signal"]
    )

    # Initialize in cash and hold the last confirmed position
    df.at[df.index[0], "Signal"] = 0
    df["Signal"] = df["Signal"].ffill()

    return df

df = trading_logic(df)




# Plot price with trend-colored UniEMA and trading signals, 
# together with RSI and regime-colored UniRSI

def plot_signals(df, title="UniEMA and UniRSI"):
    dates = pd.to_datetime(
        df["Date"]
        if "Date" in df.columns
        else df.index.get_level_values(0)
    )

    x = mdates.date2num(dates)

    close = df["Close"].to_numpy()
    uniema = df["UniEMA"].to_numpy()
    rsi = df["RSI"].to_numpy()
    unirsi = df["UniRSI"].to_numpy()
    signal2 = df["Signal 2"].to_numpy()
    signal = df["Signal"].ffill().fillna(0).to_numpy()

    def colored_line(ax, x, y, colors, width=1.8):
        points = np.column_stack([x, y])
        segments = np.stack(
            [points[:-1], points[1:]],
            axis=1,
        )

        collection = LineCollection(
            segments,
            colors=np.asarray(colors)[1:],
            linewidth=width,
        )

        ax.add_collection(collection)
        ax.autoscale_view()

    fig, (ax1, ax2) = plt.subplots(
        2,
        1,
        figsize=(9, 6),
        sharex=True,
        gridspec_kw={"height_ratios": [2, 1]},
    )

    # --------------------------------------------------
    # Price panel
    # --------------------------------------------------

    ax1.plot(
        x,
        close,
        color="black",
        linewidth=0.9,
    )

    # Combined trading regime
    ax1.fill_between(
        x,
        0,
        1,
        where=signal == 1,
        transform=ax1.get_xaxis_transform(),
        color="green",
        alpha=0.08,
        step="post",
    )

    ax1.fill_between(
        x,
        0,
        1,
        where=signal == 0,
        transform=ax1.get_xaxis_transform(),
        color="red",
        alpha=0.04,
        step="post",
    )

    # UniEMA slope colors
    ema_colors = np.where(
        uniema >= np.r_[uniema[0], uniema[:-1]],
        "green",
        "red",
    )

    colored_line(
        ax1,
        x,
        uniema,
        ema_colors,
        width=2,
    )

    # Buy and sell markers
    signal_change = np.diff(
        signal,
        prepend=signal[0],
    )

    buy = signal_change == 1
    sell = signal_change == -1

    ax1.scatter(
        x[buy],
        close[buy],
        marker="^",
        color="green",
        edgecolor="black",
        linewidth=0.4,
        s=55,
        zorder=5,
    )

    ax1.scatter(
        x[sell],
        close[sell],
        marker="v",
        color="red",
        edgecolor="black",
        linewidth=0.4,
        s=55,
        zorder=5,
    )

    ax1.set_title(title)
    ax1.set_ylabel("Price")
    ax1.grid(alpha=0.25)

    ax1.legend(handles=[
        Line2D([], [], color="black", label="Close"),
        Line2D([], [], color="green", lw=2, label="UniEMA rising"),
        Line2D([], [], color="red", lw=2, label="UniEMA falling"),
        Line2D(
            [], [],
            marker="^",
            color="green",
            markeredgecolor="black",
            linestyle="",
            label="Buy",
        ),
        Line2D(
            [], [],
            marker="v",
            color="red",
            markeredgecolor="black",
            linestyle="",
            label="Sell",
        ),
    ])

    # --------------------------------------------------
    # RSI panel
    # --------------------------------------------------

    ax2.plot(
        x,
        rsi,
        color="gray",
        linewidth=1,
    )

    # UniRSI colors from Signal 2
    unirsi_colors = np.where(
        signal2 == 1,
        "green",
        np.where(signal2 == 0, "red", "gray"),
    )

    colored_line(
        ax2,
        x,
        unirsi,
        unirsi_colors,
        width=1.8,
    )

    ax2.axhline(70, color="red", linestyle="--", alpha=0.5)
    ax2.axhline(50, color="gray", linestyle=":", alpha=0.6)
    ax2.axhline(45, color="orange", linestyle=":", alpha=0.6)
    ax2.axhline(30, color="green", linestyle="--", alpha=0.5)

    ax2.set_ylim(0, 100)
    ax2.set_ylabel("RSI")
    #ax2.set_xlabel("Date")
    ax2.grid(alpha=0.25)

    ax2.legend(handles=[
        Line2D([], [], color="gray", label="RSI"),
        Line2D([], [], color="green", lw=2, label="UniRSI buy"),
        Line2D([], [], color="red", lw=2, label="UniRSI sell"),
    ])

    ax2.xaxis_date()
    ax2.xaxis.set_major_formatter(
        mdates.DateFormatter("%Y-%m")
    )

    fig.autofmt_xdate()
    fig.tight_layout()
    plt.show()

    return
    
plot_signals(df['2026-04':'2026-06'], title=f"{ticker} — UniEMA and UniRSI")

An example chart of the UniEMA and UniRSI plotted on a daily chart of SPY is shown in Figure 6.

Sample Chart
FIGURE 6: PYTHON. The UniEMA and UniRSI are plotted on a daily chart of SPY.
These users thanked the author Banzai for the post:
Pelle

Re: Python to MT5

13
Links: | TradingView | TradeStation | NinjaTrader | MT4 | MT5 | Python |

Jack & Jill

Python: December 2026

The Python code shown here is an implementation of John Ehlers’ indicators described in his article in this issue, “The Jack & Jill Indicator.”

Code: Select all

"""
Python implementation of the Jack and Jill indicator using adaptive
and contra-adaptive SuperSmoother filters, with an illustrative
long or cash trading rule and a chart of the resulting signals.

Copy the code sections into Jupyter notebook cells and run in order.
The example uses SPY and a 120-bar base period. Change the ticker,
dates and period to explore other instruments and settings.

Written by:
Rajeev Jain, October 2026
jainraje@yahoo.com

Complete source code:
https://github.com/jainraje/TraderTipArticles/
"""

# import required python modules
import pandas as pd
import numpy as np
import yfinance as yf

import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.collections import LineCollection
from matplotlib.lines import Line2D

print(yf.__version__)

# get SPY price data from Yahoo Finance

# Choose the ticker and history range; Yahoo excludes end_date itself.
ticker = 'SPY'
start_date = "1995-01-01"
end_date = "2026-10-17"

ohlcv = yf.download(
    ticker, 
    start=start_date, 
    end=end_date, 
    #group_by="Ticker",
    auto_adjust=False,
)
ohlcv = ohlcv.stack('Ticker', future_stack=True ).reset_index().set_index('Date')
ohlcv
# indicator functions

import numpy as np
import pandas as pd
from math import exp, cos, sqrt


# Calculate one SuperSmoother value from the current price and prior values.
# Shared by the fixed-period filter and the adaptive Jack and Jill filters.
def _smoother_step(close, previous_close, y1, y2, period):
    """One SuperSmoother calculation, shared by both routines."""
    q = exp(-1.414 * 3.14159 / period)
    c1 = 2 * q * cos(1.414 * np.pi / period)
    c2 = q * q
    a0 = (1 - c1 + c2) / 2

    return a0 * (close + previous_close) + c1 * y1 - c2 * y2


# Smooth a complete price Series; period can be fixed or one value per bar.
def super_smoother(close, period):
    """Period can be fixed or an array containing one period per bar."""
    close = pd.Series(close, dtype=float)
    x = close.to_numpy()
    periods = np.broadcast_to(np.asarray(period, dtype=float), x.shape)
    output = x.copy()

    for i in range(3, len(x)):
        output[i] = _smoother_step(
            x[i], x[i - 1],
            output[i - 1], output[i - 2],
            periods[i],
        )

    return pd.Series(output, index=close.index, name="SuperSmoother")


# Measure recent change magnitude over length bars for normalization.
# Missing initial history is treated as zero; a zero sum retains the last RMS.
def rms(price, length):
    """Rolling RMS with zero-padded initial history."""
    price = pd.Series(price, dtype=float)

    result = np.sqrt(
        price.pow(2).rolling(length, min_periods=1).sum() / length
    )

    # Preserve the article's behavior when SumSq is zero.
    return result.replace(0, np.nan).ffill().fillna(0)


# Build the adaptive Jack and contra-adaptive Jill lines in one forward pass.
# period0 controls their base responsiveness; return Jack, Jill and Jack-Jill.
def jack_jill(close, period0=20):
    close = pd.Series(close, dtype=float)
    x = close.to_numpy()
    n = len(x)

    if not n or not np.isfinite(x).all():
        raise ValueError("close must contain finite prices.")
    if not np.isfinite(period0) or period0 < 2:
        raise ValueError("period0 must be finite and at least 2.")

    jack = np.empty(n)
    jill = np.empty(n)

    jack_changes = np.zeros(n)
    jill_changes = np.zeros(n)

    jack_period = jill_period = 20.0
    jack_rms = jill_rms = 0.0
    jack_roc = jill_roc = 0.0

    for i in range(n):
        if i < 3:
            jack[i] = jill[i] = x[i]
        else:
            jack[i] = _smoother_step(
                x[i], x[i - 1],
                jack[i - 1], jack[i - 2],
                jack_period,
            )
            jill[i] = _smoother_step(
                x[i], x[i - 1],
                jill[i - 1], jill[i - 2],
                jill_period,
            )

        # Zero prehistory, matching the preceding implementation.
        jack_changes[i] = jack[i] - (jack[i - 1] if i else 0.0)
        jill_changes[i] = jill[i] - (jill[i - 1] if i else 0.0)

        # Evaluate only the current 81-bar window.
        start = max(0, i - 80)
        jc = jack_changes[start:i + 1]
        lc = jill_changes[start:i + 1]

        jack_sum_sq = float(np.dot(jc, jc))
        jill_sum_sq = float(np.dot(lc, lc))

        if jack_sum_sq != 0:
            jack_rms = sqrt(jack_sum_sq / 81)

        if jill_sum_sq != 0:
            jill_rms = sqrt(jill_sum_sq / 81)

        if jack_rms != 0:
            jack_roc = min(abs(jack_changes[i] / jack_rms), 2)

        if jill_rms != 0:
            jill_roc = min(abs(jill_changes[i] / jill_rms), 2)

        # These periods apply to the NEXT bar.
        jack_period = max(
            2, period0 * (1 - 0.5 * jack_roc) ** 2
        )
        jill_period = period0 * (1 + 0.5 * jill_roc) ** 2

    return pd.DataFrame(
        {
            "Jack": jack,
            "Jill": jill,
            "Oscillator": jack - jill,
        },
        index=close.index,
    )


# Remove a session gap when the bar time equals session_open + time_bars.
# Times use HHMM arithmetic; this helper is not used in the daily example.
def degap(price, session_open, time_bars):
    """Price must have a DatetimeIndex in the session timezone."""
    price = pd.Series(price, dtype=float)
    x = price.to_numpy()
    output = np.empty(len(x))
    times = price.index.hour * 100 + price.index.minute
    gap = 0.0

    for i in range(len(x)):
        if times[i] == session_open + time_bars:
            previous = output[i - 1] if i else 0.0
            gap = x[i] - previous

        output[i] = x[i] - gap

    return pd.Series(output, index=price.index, name="Degapped")


df = ohlcv.copy()
# Change period0 to tune responsiveness; calculate before slicing history.
df = df.join(jack_jill(df["Close"], period0=120))
df




# Illustrative long/cash rule using (Jack - Jill) / Close.
# Enter when the spread is above 0; exit when it is below -0.04 (-4%).
# Between thresholds, including equality, keep the previous signal.
# Start in cash. Return a copy with Signal: 1 = long, 0 = cash.
# Change the entry/exit thresholds below to suit the intended trading rule.
# Signals use closing data; model execution separately when backtesting.
def trading_logic(df):
    df = df.copy()

    if df.empty:
        return df

    # Divide by Close so thresholds are comparable across price levels.
    oscillator = (df["Jack"] - df["Jill"]) / df["Close"]

    # Adjust 0 and -0.04 for entry and exit; -0.04 means a -4% spread.
    df["Signal"] = np.select(
        [oscillator > 0, oscillator < -0.04],
        [1, 0],
        default=np.nan,
    )

    # Signal: 1 = long, 0 = cash. Start in cash; hold state between thresholds.
    df.iloc[0, df.columns.get_loc("Signal")] = 0
    df["Signal"] = df["Signal"].ffill().astype(int)

    return df
    
df = trading_logic(df)
df

import matplotlib.pyplot as plt

# Plot price, Jack, Jill and the percentage spread for the supplied DataFrame.
# Pass a date slice to limit the chart; markers and shading follow Signal.
# Title is centered and the figure is 9 by 6; edit styling here as needed.
def plot_signals(df, title="Jack & Jill"):
    """Plot indicators, signal-date markers and matching long-state shading."""
    d = df.copy()
    if d.empty:
        raise ValueError("DataFrame is empty.")
    # Mark signal changes at that day’s close, not at an execution price.
    change = d["Signal"].diff().fillna(0)
    invested = d["Signal"].eq(1)
    previous = invested.shift(fill_value=False)
    starts = list(d.index[invested & ~previous])
    ends = list(d.index[~invested & previous])
    if invested.iloc[-1]:
        ends.append(d.index[-1])

    fig, ax = plt.subplots(
        2, 1, figsize=(9, 6), sharex=True, layout="constrained",
        gridspec_kw={"height_ratios": [3, 1]}
    )
    for col, color in [("Close", "#586575"), ("Jack", "#1265c8"), ("Jill", "#e38a19")]:
        ax[0].plot(d.index, d[col], color=color, lw=1.3, label=col)
    for mask, marker, color, label in [
        (change.gt(0), "^", "#129252", "Buy signal"),
        (change.lt(0), "v", "#cd3349", "Sell signal")
    ]:
        ax[0].scatter(
            d.index[mask], d.loc[mask, "Close"], marker=marker, color=color,
            s=65, edgecolors="white", zorder=5, label=label
        )

    # If trading thresholds change, update the chart guide lines below too.
    spread = 100 * (d["Jack"] - d["Jill"]) / d["Close"]
    ax[1].plot(d.index, spread, color="#7954ac", label="Spread (%)")
    ax[1].axhline(0, color="gray", ls="--", lw=.9, label="Entry: 0%")
    ax[1].axhline(-1, color="#cd3349", ls="--", lw=.9, label="Exit: −1%")
    for a, label in zip(ax, ["Price", "Spread (%)"]):
        for start, end in zip(starts, ends):
            a.axvspan(start, end, color="#129252", alpha=.06, lw=0)
        a.set_ylabel(label)
        a.legend(loc="upper left", ncol=3, frameon=False, fontsize=8)
        a.tick_params(labelsize=8)
        a.grid(alpha=.15)
        a.spines[["top", "right"]].set_visible(False)
    ax[0].set_title(title, loc="center")
    return fig, ax

# Select the chart date range here; keep full history for indicator calculation.
fig, ax = plot_signals(signals["2026":], title="SPY - Jack & Jill Indicator & Trading Signals, period=120")
plt.show()
An example of the Jack & Jill indicator and oscillator are plotted on a daily chart of SPY, shown in Figure 8.

Sample Chart FIGURE 8: PYTHON. The Jack & Jill indicator is plotted on a daily chart of SPY.