Anyone that’s been around the markets knows that the monthly release of the United States Department of Labor’s Non-Farm Payrolls (NFP) data can have a tremendous impact, especially in the short term.
NFP is a snapshot of the state of the employment situation in the US, representing the total number of paid workers, excluding farm employees and public servants.
We know your barn is hiding a giant mining station, Rick
The release of the monthly NFP data typically causes large swings in the currency markets, even when the results are in line with estimates.
Here, we are interested in exploring potential seasonal effects around the release of this data. For example, does price tend to drift prior to the release? If so, which way?
For this analysis, we’ll explore the EUR/USD exchange rate.
To set up this research problem, we need to know that NFP is released on the first Friday of the month at 8:30am ET – usually. If the first Friday is a holiday, NFP is released the following Friday.
These sorts of details can make seasonal analysis tedious, but we have to know about them… Here we will just analyse the months when NFP is released on the first Friday.
We’ll use Zorro for this. Zorro contains pre-defined seasonality functions for this kind of work… but unfortunately “the time around the first Friday of the month at 8:30 am ET” isn’t one of the things they considered!
In the script below, we plot a histogram of the average cumulative returns, and their standard deviation, at five-minute intervals prior to and post-NFP release:
/* NFP DRIFT NFP data is released first Friday of the month at 8:30 ET */ function run() { set(PLOTNOW); StartDate = 2009; EndDate = 2018; BarPeriod = 5; asset("EUR/USD"); int StartSeason = 600; //time of day to commence season int EndSeason = 1100; //time of day to end season vars Closes = series(priceClose()); vars Returns = series((Closes[0]-Closes[1])/Closes[1]); // set up cumulative returns series to track returns over the whole season vars CumReturns = series(); CumReturns[0] = (Returns[0]+1)*(CumReturns[1]+1)-1; // at StartSeason, reset CumReturn to zero if(ldow(ET) == 5 and day() <= 7 and ltod(ET) == StartSeason) { CumReturns[0] = 0; } // plot histogram static int i; if(is(INITRUN)) i = 0; if(ldow(ET) == 5 and day() <= 7 and between(ltod(ET), StartSeason, EndSeason)) { plotBar("SD_CumRet", i, ltod(ET), CumReturns[0]*100, DEV|BARS|LBL2, LIGHTBLUE); plotBar("Avg_CumRet", i, ltod(ET), CumReturns[0]*100, AVG|BARS|LBL2, BLUE); // plotBar("SD (x4)", i, ltod(ET), Returns[0]*100/4, DEV|BARS|LBL2, LIGHTBLUE); // plotBar("Avg", i, ltod(ET), Returns[0]*100, AVG|BARS|LBL2, BLUE); i++; } if(ltod(ET) > EndSeason) i = 0; }
Note we need to ensure that our cumulative returns from the start of the season are zero, hence we set the current value of the cumulative returns series to zero at 6:00 am ET on the first Friday of the month.
That means that we are comparing price evolution on different days from the same starting point. We can also comment lines 35-36 and uncomment lines 37-38 in order to plot bar-by-bar (as opposed to cumulative) returns and standard deviations (the latter scaled by a factor of four).
Here’s the resulting histogram:
That doesn’t really tell us much:
- prior to the release, the price goes nowhere on average
- after the release, the price went down a little on average
- that negative return is likely representative of little more than the overall bias of the EUR/USD market over the simulation period.
So we haven’t really learned anything of value, other than perhaps the obvious increase in volatility after the release, here represented by the standard deviation of bar-by-bar returns.
What if we also consider the direction of the market following the announcement?
For example, we plot separate histograms for price evolution for markets that went up following the NFP release, and for markets that went down.
But wouldn’t that involve looking into the future?
Yes, it would, at least from the perspective of the individual periods being analyzed.
But here we can use this future-peeking to our advantage to uncover some hopefully useful insights about the relationships between pre- and post-NFP price evolution.
Future-peeking is fine in this exercise because we are not (yet) designing a trading system – we are merely seeking to unravel some interesting relationships from past data.
To do that, we need to know whether, on a particular day, the NFP release caused the markets to go up, or to go down.
Our trading system might be based on the insights gained through this exercise, but it will not be based on knowledge of the future. If this is confusing, I hope it becomes clear after seeing an example.
Here’s the code for separating our price evolution histograms on the basis of a bullish or bearish market post-NFP:
/* NFP DRIFT NFP data is released first Friday of the month at 8:30 ET */ function run() { set(PEEK|PLOTNOW); StartDate = 2009; EndDate = 2018; BarPeriod = 5; asset("EUR/USD"); int StartSeason = 600; //time of day to commence season int EndSeason = 1100; //time of day to end season vars Closes = series(priceClose()); vars Returns = series((Closes[0]-Closes[1])/Closes[1]); // set up cumulative returns series to track returns over the whole season vars CumReturns = series(); CumReturns[0] = (Returns[0]+1)*(CumReturns[1]+1)-1; static int i; if(is(INITRUN)) i = 0; // at StartSeason, get first hour's return post NFP, and reset CumReturn to zero static var NFP_Return; if(ldow(ET) == 5 and day() <= 7 and ltod(ET) == StartSeason) { // calculate number of BarPeriods to use for future peeking post-NFP returns int timeDiff = 830 - StartSeason; var minutes = floor(timeDiff/100)*60 + timeDiff % 100; int offset = minutes/BarPeriod; //number of bars to NFP release // calculate post-NFP returns (1 hour) NFP_Return = (priceClose(-(offset+60/BarPeriod))-priceClose(-offset))/priceClose(-offset); CumReturns[0] = 0; } if(ldow(ET) == 5 and day() <= 7 and between(ltod(ET), StartSeason, EndSeason) and NFP_Return > 0) { plotBar("SD_CumRet", i, ltod(ET), CumReturns[0]*100, DEV|BARS|LBL2, LIGHTBLUE); plotBar("Avg_CumRet", i, ltod(ET), CumReturns[0]*100, AVG|BARS|LBL2, BLUE); // plotBar("SD (x4)", i, ltod(ET), Returns[0]*100/4, DEV|BARS|LBL2, LIGHTBLUE); // plotBar("Avg", i, ltod(ET), Returns[0]*100, AVG|BARS|LBL2, BLUE); i++; } if(ltod(ET) > EndSeason) i = 0; }
Here we set the PEEK
flag, which enables the price()
functions to access data from the future.
Now, when we reset our cumulative returns to zero at StartSeason
, we also calculate the number of BarPeriods
into the future, we need in order to calculate the first hour’s return post-NFP (lines 33-35, calculation of first hour’s return post-NFP is in line 38).
We then use that return’s sign to filter certain NFP days from our histogram (line 42). Just change the greater-than or less-than sign to get the direction you want.
Here’s the histogram for price evolution on days when NFP produces a bullish reaction:
And here’s the histogram for the negative NFP reaction:
Well, that’s a little more interesting!
We can see that prior to a bullish NFP release (bullish for the Euro in this case, bearish for the dollar), the EUR/USD market drifts upwards, on average.
Conversely, prior to a bearish NFP release, the EUR/USD market drifts downward, on average.
What’s going on here?
Are there people out there who have prior knowledge of the release and are positioning themselves accordingly?
The former, at least, is entirely possible, and it doesn’t have to be due to any criminal leaking of sensitive data.
No doubt there are innovative and well-resourced organizations out there that gather their own data and build their own predictive models for forecasting the NFP numbers (which are probably easier to forecast than the markets themselves).
Could the drift we identified above simply be these well-informed traders positioning themselves? If so, would we want to trade with them?
Also, note the significant variation in price evolution pre-NFP release as evidenced by the large standard deviation of bar-by-bar returns.
This implies that sometimes (often, probably), you’ll see behaviour that is not at all representative of the average behaviour, which of course will confound any attempt to act on pre-NFP price evolution.
But still, I find this an interesting insight into a very specific seasonal effect, and the intent is that you will be able to adapt the research framework presented here to investigate other events or effects you deem interesting.
Great post as usual!