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.