# 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} =\ &\text{High} ( N ) > \text{High} ( N - 2 ) \text{ and} \\ &\text{High} ( N ) > \text{High} ( N - 1 ) \text{ and} \\ &\text{High} ( N ) > \text{High} ( N + 1 ) \text{ and} \\ &\text{High} ( N ) > \text{High} ( N + 2 ) \\ \end{aligned}

\begin{aligned} \text{Bullish Fractal} =\ &\text{Low} ( N ) < \text{Low} ( N - 2 ) \text{ and} \\ &\text{Low} ( N ) < \text{Low} ( N - 1 ) \text{ and} \\ &\text{Low} ( N ) < \text{Low} ( N + 1 ) \text{ and} \\ &\text{Low} ( N ) < \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)) &
dataframe['high'].shift(3).lt(dataframe['high'].shift(2)) &
dataframe['high'].shift(1).lt(dataframe['high'].shift(2)) &
dataframe['high'].lt(dataframe['high'].shift(2))
)

df['bull_fractal'] = (
dataframe['low'].shift(4).gt(dataframe['low'].shift(2)) &
dataframe['low'].shift(3).gt(dataframe['low'].shift(2)) &
dataframe['low'].shift(1).gt(dataframe['low'].shift(2)) &
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'],

return dataframe


Update: There was a post (now deleted) about parameterizing the number of shift periods, so I've added a period param to both the shift() and rolling() versions below.

## shift() version

1. Instead of making a copy() solely to assign result columns, save some overhead by just using standalone Series.

2. Instead of testing shift(2) vs 4,3,1,0, it's more natural to test unshifted vs -2,-1,1,2.

3. Instead of chaining & & &, it's faster to use a comprehension with np.logical_and.reduce().

4. Instead of hardcoding the number of shifts, parameterize it. Although Investopedia defines this indicator with a period of 2, we can add a period param (with a default of 2) and generate all periods based on that value.

from typing import Tuple

def will_frac(df: pd.DataFrame, period: int = 2) -> Tuple[pd.Series, pd.Series]:
"""Indicate bearish and bullish fractal patterns using shifted Series.

:param df: OHLC data
:param period: number of lower (or higher) points on each side of a high (or low)
:return: tuple of boolean Series (bearish, bullish) where True marks a fractal pattern
"""

periods = [p for p in range(-period, period + 1) if p != 0] # default [-2, -1, 1, 2]

highs = [df['high'] > df['high'].shift(p) for p in periods]
bears = pd.Series(np.logical_and.reduce(highs), index=df.index)

lows = [df['low'] < df['low'].shift(p) for p in periods]
bulls = pd.Series(np.logical_and.reduce(lows), index=df.index)

return bears, bulls


## rolling() version

Alternatively, use rolling() to check if the center value in a sliding window is the max (bear) or min (bull):

from typing import Tuple

def will_frac_roll(df: pd.DataFrame, period: int = 2) -> Tuple[pd.Series, pd.Series]:
"""Indicate bearish and bullish fractal patterns using rolling windows.

:param df: OHLC data
:param period: number of lower (or higher) points on each side of a high (or low)
:return: tuple of boolean Series (bearish, bullish) where True marks a fractal pattern
"""

window = 2 * period + 1 # default 5

bears = df['high'].rolling(window, center=True).apply(lambda x: x[period] == max(x), raw=True)
bulls = df['low'].rolling(window, center=True).apply(lambda x: x[period] == min(x), raw=True)

return bears, bulls


## Timings

The rolling() version is more concise and understandable, but the shift() versions scale much better (memory permitting):