William Fractal technical indicator implementation
Looks good, just a few ideas:

Since the function only returns two series, you can save some overhead by removing the copy() and just working with series.

np.logical_and.reduce() allows you to build the shifted series in a comprehension (and also seems slightly faster than chaining &amp; &amp; &amp;, based on some rough testing).

Instead of testing shift(2) against 4,3,1,0, I find it more intuitive to test the unshifted series against -2,-1,1,2.


def wilFractal(df):
    periods = (-2, -1, 1, 2)
    
    bear_fractal = pd.Series(np.logical_and.reduce([
        df['high'] &gt; df['high'].shift(period) for period in periods
    ]), index=df.index)

    bull_fractal = pd.Series(np.logical_and.reduce([
        df['low'] &lt; df['low'].shift(period) for period in periods
    ]), index=df.index)

    return bear_fractal, bull_fractal

An alternative idea is to use Series.rolling() with a window size of 5 and check if the center value in each window is the max (bear) or min (bull):
def roll(df):
    bear_fractal = df['high'].rolling(5, center=True).apply(lambda x: x[2] == max(x), raw=True)
    bull_fractal = df['low'].rolling(5, center=True).apply(lambda x: x[2] == min(x), raw=True)
    return bear_fractal, bull_fractal

The rolling() code is more concise, but it turns out your shift() version scales better (memory permitting)!