3
\$\begingroup\$

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'],
        'buy'] = 1

    return dataframe
\$\endgroup\$
0

1 Answer 1

11
\$\begingroup\$

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):

timing rolling() vs shift()

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.