Generic crossover/intersection detection

Chapters 102 and 105 each hand-wrote a crossover check for one specific pair of series (MACD/signal, %K/%D). This chapter generalizes that into one function that detects an intersection between *any* two series — two indicators, an indicator and a fixed level, or price and an indicator.

import pandas as pd

def detect_crossover(series_a: pd.Series, series_b: pd.Series) -> pd.Series:
    """Returns 1 where A crosses above B, -1 where A crosses below B, 0 otherwise."""
    diff = series_a - series_b
    prev_diff = diff.shift(1)

    crossed_up = (diff > 0) & (prev_diff <= 0)
    crossed_down = (diff < 0) & (prev_diff >= 0)

    signal = pd.Series(0, index=series_a.index)
    signal[crossed_up] = 1
    signal[crossed_down] = -1
    return signal

This single function replaces every hand-written crossover in chapters 102, 105, 106 — detect_crossover(macd_df["macd"], macd_df["signal"]) reproduces chapter 102's MACD crossover exactly; detect_crossover(df["close"], pd.Series(70, index=df.index)) detects price crossing a fixed level; detect_crossover(stoch_df["k"], stoch_df["d"]) reproduces chapter 105's stochastic crossover.

Crossing a fixed threshold (a common special case)

def detect_threshold_cross(series: pd.Series, threshold: float) -> pd.Series:
    level = pd.Series(threshold, index=series.index)
    return detect_crossover(series, level)

rsi_cross_30 = detect_threshold_cross(rsi_series, 30)

Exact intersection *value* — where two lines would cross between bars

Crossover detection tells you a crossing happened *by* a given bar, but the actual intersection point in continuous time falls between the previous and current bar's closes. For precise level computation (rare need, but relevant for e.g. drawing an exact trendline break price):

def interpolated_crossover_price(a_prev, a_curr, b_prev, b_curr) -> float | None:
    """Linear interpolation: where would A and B have been exactly equal,
    between the previous and current bar?"""
    denom = (a_curr - a_prev) - (b_curr - b_prev)
    if denom == 0:
        return None   # parallel, no intersection in this interval
    t = (b_prev - a_prev) / denom
    if not (0 <= t <= 1):
        return None   # crossing didn't actually occur within this bar interval
    return a_prev + t * (a_curr - a_prev)

Confirmed vs. immediate crossover — filtering noise

A crossover that reverses on the very next bar (a "whipsaw") is common in choppy conditions. Require confirmation — the crossed state must hold for N subsequent bars — before treating it as a real signal:

def confirmed_crossover(series_a: pd.Series, series_b: pd.Series, confirm_bars: int = 2) -> pd.Series:
    raw = detect_crossover(series_a, series_b)
    diff_sign = (series_a - series_b).apply(lambda x: 1 if x > 0 else -1)

    confirmed = pd.Series(0, index=series_a.index)
    for i in range(confirm_bars, len(series_a)):
        if raw.iloc[i - confirm_bars] != 0:
            direction = raw.iloc[i - confirm_bars]
            window = diff_sign.iloc[i - confirm_bars : i + 1]
            if (window == direction).all():
                confirmed.iloc[i] = direction
    return confirmed

This one generic module (detect_crossover, detect_threshold_cross, confirmed_crossover) is what every "X crosses Y" condition in chapters 114-117 builds on — never re-derive crossover logic per indicator pair again.

Next: 114 — Weighted confluence scoring across indicators