Search code examples
pythonpandasnumpyfinancetechnical-indicator

How to implement the Hindenburg omen indicator?


As defined here the Hindenburg omen indicator is:

The daily number of new 52-week highs and 52-week lows in a stock market index are greater than a threshold amount (typically 2.2%).

To me it means, we roll daily and look back 52 weeks or 252 business/trading days, then count the number of highs (or lows) and finally compute the return of that or pct_change, which is the ratio of new highs (or lows) they want to monitor e.g., being above 2.2%

import pandas as pd
import numpy as np
import yfinance as yf

# download the S&P500 
df = yf.download('^GSPC')
# compute the "highs" and "lows"
df['Highs'] = df['Close'].rolling(252).apply(lambda x: x.cummax().diff().
    apply(lambda x: np.where(x > 0, 1, 0)).sum()).pct_change()
df['Lows'] = df['Close'].rolling(252).apply(lambda x: x.cummin().diff().
    apply(lambda x: np.where(x < 0, 1, 0)).sum()).pct_change()

Did we understand it the same way? is there a better way to do it?


Solution

  • Interesting question! Could I suggest the following code - it runs much faster than the apply solution because it is vectorised, and also lays out the steps a bit more clearly so you can inspect the interim results.

    I got a different result to your code - you can compare by also plotting your result on the timeseries at bottom.

    import pandas as pd
    import numpy as np
    import yfinance as yf
    
    # download the S&P500 
    df = yf.download('^GSPC')
    
    # Constants
    n_trading_day_window = 252
    
    # Simplify the input dataframe to only the relevant column
    df_hin_omen = df[['Close']]
    # Calculate rolling highs and lows
    df_hin_omen.insert(1, 'rolling_high', df_hin_omen['Close'].rolling(n_trading_day_window).max())
    df_hin_omen.insert(2, 'rolling_low', df_hin_omen['Close'].rolling(n_trading_day_window).min())
    # High and low are simply when the given row matches the 252 day high or low
    df_hin_omen.insert(3, 'is_high', df_hin_omen.Close == df_hin_omen.rolling_high)
    df_hin_omen.insert(4, 'is_low', df_hin_omen.Close == df_hin_omen.rolling_low)
    # Calculate rolling percentages
    df_hin_omen.insert(5, 'percent_highs', df_hin_omen.is_high.rolling(n_trading_day_window).sum() / n_trading_day_window)
    df_hin_omen.insert(6, 'percent_lows', df_hin_omen.is_low.rolling(n_trading_day_window).sum() / n_trading_day_window)
    

    Once you have run this, you can inspect the results as follows:

    import matplotlib, matplotlib.pyplot as plt
    fig, ax = plt.subplots(figsize=(16, 6))
    df_hin_omen.resample('w').mean().percent_highs.plot(ax=ax)
    df_hin_omen.resample('w').mean().percent_lows.plot(ax=ax)
    

    From the Hindenburg Omen Definition, "The Hindenburg Omen looks for a statistical deviation from the premise that under normal conditions, some stocks are either making new 52-week highs or new 52-week lows. It would be abnormal if both were occurring at the same time."

    So from a look at our graph, my interpretation is that the stock market is currently closing at a lot of 52 week highs, but is not showing many 52 week lows. Please also note the article cited states that "It was reported that it had correctly predicted a significant stock market decline only 25% of the time." so I'm not sure if we can read too much into this...

    enter image description here

    Edit

    I've had a look at your code and I don't think that the use of the pct_change function is correct - that will calculate the change on the rolling differential, so a movement from eg 0.10% to 0.11% would actually equate to a 10% change. Instead you want the rolling sum over the past year and divide that by the number of days in the year, per my code above.