3
\$\begingroup\$

I'm creating a function that gives me the Williams 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 the need to go back and always looking for the signal 2 rows before when it actually occurs).

The indicator is defined with these constraints:

\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?

The code in this case is used as a 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
tdy
2,2661 gold badge10 silver badges21 bronze badges
asked Apr 18, 2021 at 20:37
\$\endgroup\$
0

1 Answer 1

13
\$\begingroup\$
  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.

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

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

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

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

timing rolling() vs shift()

answered Apr 19, 2021 at 10:16
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.