William Fractal technical indicator implementation I'm really new to both Python, data analysis and Panda and for gathering a bit of familiarity with this components and trying to replicate a trading strategy I'm creating a function that give me the Wiliam Fractal technical indicator, which by nature is a lagging indicator to apply at the currently analysed row of the dataframe, rather than where it actually happen, but without look ahead bias to not have unrealistic backtesting results ( but also avoiding to go back and always looking for the signal 2 rows before when it actually occurs )
the indicator is defined with this constrains
source
\begin{aligned} \text{Bearish Fractal} =\ &amp;\text{High} ( N ) &gt; \text{High} ( N - 2 ) \text{ and} \\ &amp;\text{High} ( N ) &gt; \text{High} ( N - 1 ) \text{ and} \\ &amp;\text{High} ( N ) &gt; \text{High} ( N + 1 ) \text{ and} \\ &amp;\text{High} ( N ) &gt; \text{High} ( N + 2 ) \\ \end{aligned}
\begin{aligned} \text{Bullish Fractal} =\ &amp;\text{Low} ( N ) &lt; \text{Low} ( N - 2 ) \text{ and} \\ &amp;\text{Low} ( N ) &lt; \text{Low} ( N - 1 ) \text{ and} \\ &amp;\text{Low} ( N ) &lt; \text{Low} ( N + 1 ) \text{ and} \\ &amp;\text{Low} ( N ) &lt; \text{Low} ( N + 2 ) \\ \end{aligned}
​
which I implemented with the following code:
def wilFractal(dataframe):
        df = dataframe.copy()
        df['bear_fractal'] = (
                dataframe['high'].shift(4).lt(dataframe['high'].shift(2)) &amp;
                dataframe['high'].shift(3).lt(dataframe['high'].shift(2)) &amp;
                dataframe['high'].shift(1).lt(dataframe['high'].shift(2)) &amp;
                dataframe['high'].lt(dataframe['high'].shift(2))
        )
    
        df['bull_fractal'] = (
                dataframe['low'].shift(4).gt(dataframe['low'].shift(2)) &amp;
                dataframe['low'].shift(3).gt(dataframe['low'].shift(2)) &amp;
                dataframe['low'].shift(1).gt(dataframe['low'].shift(2)) &amp;
                dataframe['low'].gt(dataframe['high'].shift(2))
        )
    
        return df['bear_fractal'], df['bull_fractal']

any suggestions?
usage edit:
The code in this case is used to signal buy signal for a crypto trading bot in this manner:
def populate_buy_trend(self, dataframe, metadata) 
    dataframe.loc[
        dataframe['bull_fractal'],
        'buy'] = 1

    return dataframe
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)!