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

• Make sure you use clean price data or you will detect data errors rather than true price changes. Jan 2, 2023 at 11:41

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.

• I have tried to look for the term decayed volatility and write that formula in python, but I have not seen documentation on $\widetilde{\sigma(r)}$ is it possible that it goes by another more used term? Jan 8, 2023 at 19:41
• I am not sure there is another name for this particular implementation that Bloomberg uses. It's really just some weights used in the standard volatility calculation. A common way to use weights is called EWMA (exponentially weighted moving average), for which there should be plenty of python libraries. Jan 9, 2023 at 1:18
• could you please check the EDIT I have done in the question. I think this is calculating the time decayed z-score as specified in your answer. Thank you Jan 16, 2023 at 12:11
• Can we re-open questions like this that are meaningful (even if slightly off topic) and have a good answer? Jan 16, 2023 at 13:29
• @nbbo2, feel free to vote to reopen if you can. Jan 19, 2023 at 15:06

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.

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.