Skip to content
All library documents

Using Return Z-Scores and Other Methods to Flag Unusual Daily Moves

Article Quant Q&A · Author: FFFAR

Summary

The document surveys ways to identify unusually large daily stock returns. One method standardizes a return by subtracting the mean and dividing by the standard deviation estimated over a chosen past window. A non-demeaned variant divides the return by volatility so that the score retains the day’s direction. It also describes weighting past observations more heavily than older ones to make the volatility estimate respond to more recent conditions. Relative returns are not affected by a stock’s nominal price level.

Other suggestions include flagging returns beyond a chosen multiple of volatility, selecting the tails of the observed return distribution, or identifying a sharp spike relative to nearby returns. The replies caution that “unusual” depends on the intended definition, and that constant-volatility normal assumptions can be poor descriptions of financial returns, which may have fat tails and changing volatility. The post includes an attempted time-decayed z-score implementation, but the accepted answer does not verify that code; the described choices also depend on the lookback window, weighting scheme, and data frequency.

Key ideas

  • A rolling z-score compares a daily return with the mean and standard deviation of a past return window.
  • Dividing by volatility without subtracting the mean preserves the sign of the return.
  • Time-decayed weights can give greater influence to recent returns when estimating volatility.
  • Thresholds based on volatility, distribution tails, or local return spikes provide alternative outlier rules.
  • The choice of what counts as unusual is subjective, and financial returns may violate normality and constant-volatility assumptions.

Tags

Full text
# How to identify daily returns as an unusual daily return given a dataset


# How to identify daily returns as an unusual daily return given a dataset












I am currently calculating daily returns of a stock with the following formula:

$$R_t = \frac{P_t - P_{t-1}}{P_{t-1}}$$

However, once I have the data, I am unable to establish a range to classify the daily returns as a usual move or as an unusual move

Is there a statistical method to differentiate the days with unusual returns? I do not know, if this method would be susceptible of the period selected or the price level (if the stock is trading at a price of 200, whereas another stock is trading at 10)

EDIT Code that I have tried to simulate the answer proposed by @AKdemy in python

```
import numpy as np

def z_score(data, j):

    data = np.log(1 + returns)  
    wj = np.array([2**(i/j) for i in range(len(data))])
    wj = wj/np.sum(wj)
    return = np.sum(wj*data)
    vol = np.sqrt(np.sum(wj*(data-return)**2))     
    z_scores = data/vol
    return z_scores
```

Would this code be correct to calculate the time decayed z-score?

## Answer by AKdemy (score 4, accepted)

https://quant.stackexchange.com/a/74229

Given you have your returns $$r_t = \frac{P_t - P_{t-1}}{P_{t-1}}$$

you can compute the absolute z-score as $$Z_t = \frac{r_t-\mu(r)}{\sigma(r)}$$ where $\mu(r)$ and $\sigma(r)$ are the mean and standard deviation of all returns for some past period (e.g. the past three months (90 days)). To get the data more closely to being normal, you could also use the logarithmic returns; see for example here.

Alternatively, you can use a non-demeaned version, to ensure that positive values of today's returns lead to positive values of the z-score (and vice versa). $$Z_t = \frac{r_t}{\sigma(r)}$$

Also, to mitigate the effects of large outlier events in the more distant past, you can modify this further, to a time decayed z-score: $$ \tilde{Z_i} = \frac{r_t}{\widetilde{\sigma(r)}} $$ where $\widetilde{\sigma(r)}$ is the time decayed volatility, for example computed as $$\widetilde{\sigma(r)} = sqrt{\biggl(\frac{\sum_{j=0}^{6M} w_j ( r_{t-1-j}- \bar{r})^2}{\sum w_j}}\biggr) $$ where the mean return $\bar{r}$ and weights $w_j$ are calculated as: $$w_j = 2*^{\frac{j}{1m}}, \bar{r} = \frac{\sum_{j=0}^{6M} w_j * r_{t-1-j}}{\sum w_j}$$ Therefore, in this setup, you get exactly half weight at one month before today (1M and 6M are expressed in days).

If you have a list of stocks, you can subsequently rank them according to the z-score, in any way you wish. This is essentially what Bloomberg's `CMM` function displays for example, where it ranks the securities based on these z-scores with the following outlier bands.

I did not look at your code specifically but tried to replicate myself. Seems Bloomberg uses 90 calendar days, not observations). In any case, the below code will replicate the values you see in the screenshot above. It is not clean code, because I just quickly tried to match the output.

```
def z_score(df,t):
    df1 = df[len(df.Date)-(t+1)::][::-1].reset_index()
    ret = [(df1.Values[i]/df1.Values[i+1]-1) for i in range(len(df1.Values)-1)]
    ret.append(0)
    df1["ret"] = ret
    days = [(df1.Date[0]- df1.Date[i]).days for i in range(len(df1.Values)-1)]
    days.append(0)
    df1["days"] = days
    weight = [2**((t-(df1.Date[0]- df1.Date[i]).days)/30) for i in range(len(df1.Values)-1)]
    weight.append(0)
    df1["weight"] = weight
    df1["ret_weigth"] = df1.ret*weight
    df1 = df1[df1.days<t]
    ret_avg = df1["ret_weigth"].sum()/df1["weight"].sum()
    df1["sigma_weight"] = df1.weight*(df1.ret-ret_avg)**2
    risk = np.sqrt(df1["sigma_weight"].sum()/df1.weight.sum())
    return f'Z-score = {round(df1.ret[0]/risk,2)}' #, df1, ret_avg, risk
```

Given a Dataframe with two columns, named Date and Values, you can compute the z-score according to the `CMM` logic, where the second input is the number of days used for the computation (90 in CMM):

Side remark: According to the documentation, BBG uses absolute (not percentage) changes in yields and spreads for bonds and CDS asset classes.

## Answer by mbison (score 2)

https://quant.stackexchange.com/a/74227

this won't be a very statistcally correct answer. But for a quick and dirty solution you could estimate the volatility using your return time series.

Then you check if there are any returns exceeding N*vol, where N is some integer (like 2).

In real life returns are not normally distributed and the habe fat tails and there is vol clustering etc. So the above won't be a correct stats answer. But provides a good starting point to identity outliers.

## Answer by utopia (score 1)

https://quant.stackexchange.com/a/74228

This is not really a question one can answer precisely. "Abnormal" is quite subjective. Thus, there are a lot of things you can do to identify such returns.

But first: the price level will of course not influence that. You are looking at relative returns, not at prices. Hence price levels do not matter.

As @mbison noted, you can try to roughly estimate probability distributions at each time by assuming a constant global variance and a normal distribution and then define something as abnormal if it is outside of a prediction interval of your choice. This is very imprecise and probably awfully incorrect in a statistical sense. For example, there is a large literature body on changing volatility in financial time series (see e.g. GARCH models).

Another thing you can do is to simply use traditional outlier detection methods. For example, you can just treat the top/bottom X% as outliers and hence "abnormal".

A third quick and dirty suggestion I can give you, is to look at return spikes. For example, if a return goes from 1% to 10% and then to 1% again, you might treat the 10% return as abnormal.

So again, it comes down to your definition of "abnormal". Once you defined it, you can think of statistical methods that suit that definition.

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.