twopirllc / pandas-ta

Technical Analysis Indicators - Pandas TA is an easy to use Python 3 Pandas Extension with 150+ Indicators
https://twopirllc.github.io/pandas-ta/
MIT License
5.46k stars 1.06k forks source link

Add Trend Quality Indicator #745

Open kbs-code opened 11 months ago

kbs-code commented 11 months ago

Trend Quality was created by David Sepiashvili and was unveiled in an issue of Stocks and Commodities magazine in 2004. The basic idea behind it is that it is not enough to rate a market as "trending" or "not trending". We must elaborate more on the "strength" or "quality" of a trend so that we can make better trading decisions. In the original issue, the author created 2 indicators called the trend quality "q" and "b" indicator. I won't discuss the "b" indicator here because I think it is less useful and adopted less by the trading community.

The "q" indicator is quite useful because it returns values based on the quality of the trend. From the author: Readings in the range from +2 to +5, or from -2 to -5, can indicate moderate trending, and readings above Q=+5 or below Q=-5 indicate strong trending. Strong upward trending often leads to the security’s overvaluing, and strong downward trending often results in the security’s undervaluing.

I myself have noticed tq values go as high as 20 since in FX markets can seemingly trend forever.
Since it's unveiling tq has been rewritten for a few trading platforms, most notably mt4/5, but you can also find versions for it in ThinkOrSwim and TradingView. The version you see in most trading platforms is the "q" indicator and it is directional in nature.

The goat of MT4/5 indicator development mladen did a megathread on trend quality which you can find here: https://www.mql5.com/en/forum/181010.

I tried to port trend quality to python but I haven't produced anything usable yet. Here's what I've done so far: https://github.com/kbs-code/trend_quality_python/tree/master

In that repo, the file https://github.com/kbs-code/trend_quality_python/blob/master/reference/trend_quality_q1_directional_indicator.mq4 is a good reference for how the indicator should behave in mt4.

In the mql4 code, the indicator works on a for loop as it iterates over close prices. I tried to reverse engineer this behavior and apply the calculation in a vectorized manner using pandas data frames but failed. I've considered using lambdas or itertuples and have avoided trying iterrows because it's frowned upon. I've tried masks as well but I'm afraid that might introduce look-ahead bias.

Sources: https://www.prorealcode.com/wp-content/uploads/2019/03/24-Q-Indicator.pdf https://github.com/kbs-code/trend_quality_python/tree/master https://www.mql5.com/en/forum/181010 https://usethinkscript.com/threads/trend-quality-indicator-with-alert-for-thinkorswim.9/ https://www.tradingview.com/script/k3mnaGcQ-Trend-Quality-Indicator/ trend_quality_2 trend_quality_1

kbs-code commented 11 months ago

added "pandas_ta - like" boilerplate code for tq. Still stuck on calculating tq values. Please see https://github.com/kbs-code/trend_quality_python/blob/master/research.ipynb

kbs-code commented 11 months ago

added cells demonstrating chunking by macd sign https://github.com/kbs-code/trend_quality_python/blob/master/research.ipynb

kbs-code commented 10 months ago

Job post for this task added on Upwork: https://www.upwork.com/jobs/~012fd863b8c28f53ab

twopirllc commented 9 months ago

@kbs-code

So you want to pay someone other than the maintainer? 🤷🏼‍♂️ 😕

kbs-code commented 9 months ago

When I posted that, I thought that you could take up the task as well. It was open to anyone. The job remains unfinished, I may tackle it again in the future. But right now I'm not hiring anyone anymore.

chris-official commented 5 months ago

Hi @kbs-code, I came up with the following code. However, I am not sure if my chunking works as intended since I used a different approach that reduces the number of loops to the number of chunks instead of iterating over each row. But feel free to take a look and try it out. Hope it helps!

# Trend Quality Indicator
def trend_quality(series: pd.Series, fast_period: int = 10, slow_period: int = 40, 
trend_period: int = 10, noise_period: int = 10, c: float = 2.0, noise_type: str = "squared") -> pd.DataFrame:

    # calculate moving averages
    ma_fast = series.ewm(span=fast_period).mean()
    ma_slow = series.ewm(span=slow_period).mean()

    # calculate crossovers
    fast_shift = ma_fast.shift(1)
    slow_shift = ma_slow.shift(1)
    up_cross = (ma_fast > ma_slow) & (fast_shift < slow_shift)
    down_cross = (ma_fast < ma_slow) & (fast_shift > slow_shift)
    crosses = series[up_cross | down_cross].index.to_list()
    crosses.append(None)  # add None to include last cycle

    # calculate price change
    pc = series.diff().fillna(0)

    # calculate cumulative price change
    cpc = pd.Series(0., index=series.index)
    last_idx = None
    for idx in crosses:
        cpc.loc[last_idx:idx] = pc.loc[last_idx:idx].cumsum()
        last_idx = idx

    # calculate trend
    trend = cpc.ewm(span=trend_period).mean()

    # calculate noise
    if noise_type == "linear":
        abs_diff = (cpc - trend).abs()
        noise = abs_diff.ewm(span=noise_period).mean()
    elif noise_type == "squared":
        square_diff = (cpc - trend) ** 2
        noise = np.sqrt(square_diff.ewm(span=noise_period).mean())
    else:
        raise ValueError("Noise type invalid.")

    # calculate q indicator
    q_indicator = trend / (noise * c)

    # calculate b indicator
    b_indicator = trend.abs() / (trend.abs() + noise) * 100

    # return indicators
    return pd.DataFrame({
        "Price Change": pc,
        "Cumulative Price Change": cpc,
        "Trend": trend,
        "Noise": noise,
        "Q_Indicator": q_indicator,
        "B_Indicator": b_indicator
    }, index=series.index)
kbs-code commented 5 months ago

Thank you very much @chris-official for sharing this. I did an initial test of your code which you can find at https://github.com/kbs-code/trend_quality_python/blob/master/user_submission.ipynb.

I only spent a few hours putting that together but so far, the results don't seem production-ready. I'll take a deeper look into your code when I have time as I only changed your parameters to match mladen's default ones.

chris-official commented 5 months ago

Good news @kbs-code, I again looked into my code as well as the test notebook you provided. While my first implementation is based on the original paper, I implemented the indicator a second time but this time focused on recreating the MetaTrader code of your file "trend_quality_q1_directional_indicator.mq4". Now, the results look much more similar to what is shown in your reference image.

Here are some of the most noticeable differences of the new implementation:

Some important considerations:

Feel free to try out the new code and make changes as you like. Maybe you can come up with your own improved indicator version! I hope this helps!

def trend_quality(price: pd.Series, fast_period: int = 7, slow_period: int = 15, 
                  trend_period: int = 4, noise_period: int = 250, c: float = 2.0, noise_type: str = "squared") -> pd.DataFrame:

    # calculate moving averages
    emaf = price.ewm(span=fast_period).mean()
    emas = price.ewm(span=slow_period).mean()

    # calcualte trend sign
    macd = emaf - emas
    sign = pd.Series(np.nan, index=price.index)
    sign[macd > 0] = 1
    sign[macd < 0] = -1

    # calculate price change
    change = price.diff().abs()

    # calculate cumulative price change and trend
    cpc = pd.Series(np.nan, index=price.index)
    trend = pd.Series(np.nan, index=price.index)
    for i in range(1, len(sign)):
        if sign[i] != sign[i-1]:
            cpc[i] = 0
            trend[i] = 0
        else:
            cpc[i] = sign[i] * change[i] + cpc[i-1]
            trend[i] = cpc[i] * (1 / trend_period) + trend[i-1] * (1 - (1 / trend_period))

    # calculate noise
    if noise_type == "linear":
        dt = (cpc - trend).abs()
        avgDt = dt.rolling(noise_period, min_periods=1).mean()
        noise = c * avgDt
    elif noise_type == "squared":
        dt = (cpc - trend) ** 2
        avgDt = dt.rolling(noise_period, min_periods=1).mean()
        noise = c * np.sqrt(avgDt)
    else:
        raise ValueError("Noise type invalid.")

    # calculate q indicator
    trendQ = (trend / noise)

    # return indicators
    return pd.DataFrame({
        "Price Change": change,
        "Cumulative Price Change": cpc,
        "Trend": trend,
        "Noise": noise,
        "Q_Indicator": trendQ,
    }, index=price.index).fillna(0)
chris-official commented 5 months ago

As you can see the peaks of the new indicator are much more pronounced but several bars later than the old indicator.

image

kbs-code commented 5 months ago

@chris-official thanks a lot for this. I wish there were some things explained more clearly in the original paper. In the paper there is a diagram which shows the Q indicator resetting abruptly if the trend reverses, and I think the macd changing signs triggers this. Although macd is not explicitly mentioned, from what I understand the calculation is still the same.

I think a long or short Q indicator is useful and illustrated in the original paper. The only thing he says about it is "Here, the reversal indicator is built in". I assume he coded his own directional indicator but didn't reveal all the code / math. And because he didn't reveal everything, mladen sought to add more code to match the illustration. I could be wrong.

I also don't know why I see a 250 noise period as default but for MetaTrader the default value is often 250. I'm not sure if mladen started that or someone else.

I think the question now is which version of the TQ should make it into pandas_ta. Other traders who use MetaTrader will be more familiar with mladen's versions, but some traders may prefer the original author's indicator.

Thanks for all the pointers and writing the code.

kbs-code commented 5 months ago

@twopirllc If I wanted to contribute the tq indicator to pandas_ta do I have to follow any guidelines? I have been using your macd as a template of sorts. Can I leave out talib mode? Since tq uses macd, do I have to use pandas_ta macd or can I calculate it myself?

twopirllc commented 5 months ago

@kbs-code

do I have to follow any guidelines?

You must checkout the development branch and style your code similarly to other indicators (or as close as you can as possible).

Can I leave out talib mode?

I will most likely put it anyhow if you leave it out and on by default. It's just an option so not a big deal.

some traders may prefer the original author's indicator.

Based on xp. This is also true.

KJ