Wednesday 15 July 2020

Forex Intraday Seasonality

Over the last week or so I have been reading about/investigating this post's title matter. Some quotes from various papers' abstracts on the matter are:
  • "We provide empirical evidence that the unique signature of the FX market seasonality is indeed due to the different time zones market participants operate from. However, once normalised using our custom-designed procedure, we observe a pattern akin to equity markets. Thus, we have revealed an important FX market property that has not been reported before." - Phd. paper - April 2013
  • "Using 10 years of high‐frequency foreign exchange data, we present evidence of time‐of‐day effects in foreign exchange returns through a significant tendency for currencies to depreciate during local trading hours. We confirm this pattern across a range of currencies and find that, in the case of EUR/USD, it can form a simple, profitable trading strategy" - Paper date - November 2010 - emphasis is mine
  • "This paper examines the intraday seasonality of transacted limit and market orders in the DEM/USD foreign exchange market. Empirical analysis of completed transactions data based on the Dealing 2000-2 electronic inter-dealer broking system indicates significant evidence of intraday seasonality in returns and return volatilities under usual market conditions. Moreover, analysis of realised tail outcomes supports seasonality for extraordinary market conditions across the trading day." - Paper date - May 2007
  • "In this article, we search for the evidence of intraweek and intraday anomalies on the spot foreign exchange (FOREX) market. Having in mind the international scope of this market ... We find that intraday and interaction between day and hour anomalies are present in trading EUR/USD on the spot FOREX market over the period of 10 years" - Paper date - 2014
  • "We find that the underpinnings for the time-varying pattern of the probability of informed trading are rooted in the strategic arrival of informed traders on a particular hour-of-day, day-of-week, and geographic location (market)." - Paper date - April 2008
In addition to this there seem to be numerous blogs, articles online etc. which also suggest that forex seasonality is a real phenomenon, so I thought I'd have a quick look into it myself.

Rather than do a full, statistical analysis I have used the following Octave function
clear all ;
data = dlmread( '/home/path/to/hourly_currency_index_g_mults' ) ;
## aud_x = x( 1)  ; cad_x = x( 2 ) ; chf_x = x( 3 ) ; eur_x = x( 4 ) ; gbp_x = x( 5 ) ; hkd_x = x( 6 ) ;
## jpy_x = x( 7 ) ; nzd_x = x( 8 ) ; sgd_x = x( 9 ) ; usd_x = x( 10 ) ; ## plus 6 for ix to account for date cols
## first 6 cols are YYYY MM DD HH-GMT HH-BST HH-EST
logged_data = data ; logged_data( : , 7 : end ) = log( logged_data( : , 7 : end ) ) ;

## get the days. The days of the week are numbered 1–7 with the first day being Sunday.
days_num = weekday( datenum( [ data(:,1) , data(:,2) , data(:,3) , data(:,5) ] ) ) ; ## BST time

start = input( 'Do you want to enter start date? Y or N ' , 's' ) ;
if ( strcmp( tolower( start ) , 'y' ) )
 year_start = input( 'Enter year YYYY:  ' ) ;
 month_start = input( 'Enter month MM:  ' ) ;
 day_start = input( 'Enter day date:  ' ) ;
 delete_ix = find( (logged_data(:,1)==year_start) .* (logged_data(:,2)==month_start) .* (logged_data(:,3)==day_start) ) ;
 
 if ( !isempty( delete_ix ) )
 logged_data( 1 : delete_ix , : ) = [] ; days_num( 1 : delete_ix , : ) = [] ;
 else
 disp( 'Invalid start date, so charts will show all data.' ) ;
 endif

endif

## create individual day indices
monday_indices = [ ( 0 : 1 : 23 )' , zeros( 24 , 10 ) ] ;
tuesday_indices = monday_indices ;
wednesday_indices = monday_indices ;
thursday_indices = monday_indices ;
friday_indices = monday_indices ;
alldays_indices = monday_indices ;

running_denom = zeros( 24 , 10 ) ;

for jj = 0 : 23
ix = find( ( days_num == 2 ) .* ( logged_data( : , 5 ) == jj ) ) ;
running_denom( jj + 1 , : ) = running_denom( jj + 1 , : ) + numel( ix ) ;
monday_indices( jj + 1 , 2 : end ) = sum( logged_data( ix , 7 : end ) , 1 ) ./ numel( ix ) ;
alldays_indices( jj + 1 , 2 : end ) = sum( logged_data( ix , 7 : end ) , 1 ) ;
endfor

for jj = 0 : 23
ix = find( ( days_num == 3 ) .* ( logged_data( : , 5 ) == jj ) ) ;
running_denom( jj + 1 , : ) = running_denom( jj + 1 , : ) + numel( ix ) ;
tuesday_indices( jj + 1 , 2 : end ) = sum( logged_data( ix , 7 : end ) , 1 ) ./ numel( ix ) ;
alldays_indices( jj + 1 , 2 : end ) = alldays_indices( jj + 1 , 2 : end ) .+ sum( logged_data( ix , 7 : end ) , 1 ) ;
endfor

for jj = 0 : 23
ix = find( ( days_num == 4 ) .* ( logged_data( : , 5 ) == jj ) ) ;
running_denom( jj + 1 , : ) = running_denom( jj + 1 , : ) + numel( ix ) ;
wednesday_indices( jj + 1 , 2 : end ) = sum( logged_data( ix , 7 : end ) , 1 ) ./ numel( ix ) ;
alldays_indices( jj + 1 , 2 : end ) = alldays_indices( jj + 1 , 2 : end ) .+ sum( logged_data( ix , 7 : end ) , 1 ) ;
endfor

for jj = 0 : 23
ix = find( ( days_num == 5 ) .* ( logged_data( : , 5 ) == jj ) ) ;
running_denom( jj + 1 , : ) = running_denom( jj + 1 , : ) + numel( ix ) ;
thursday_indices( jj + 1 , 2 : end ) = sum( logged_data( ix , 7 : end ) , 1 ) ./ numel( ix ) ;
alldays_indices( jj + 1 , 2 : end ) = alldays_indices( jj + 1 , 2 : end ) .+ sum( logged_data( ix , 7 : end ) , 1 ) ;
endfor

for jj = 0 : 20 ## market closes at 17:00 EST
ix = find( ( days_num == 6 ) .* ( logged_data( : , 5 ) == jj ) ) ;
running_denom( jj + 1 , : ) = running_denom( jj + 1 , : ) + numel( ix ) ;
friday_indices( jj + 1 , 2 : end ) = sum( logged_data( ix , 7 : end ) , 1 ) ./ numel( ix ) ;
alldays_indices( jj + 1 , 2 : end ) = alldays_indices( jj + 1 , 2 : end ) .+ sum( logged_data( ix , 7 : end ) , 1 ) ;
endfor

alldays_indices( : , 2 : end ) = alldays_indices( : , 2 : end ) ./ running_denom ;

monday_indices( : , 2 : end ) = cumsum( monday_indices( : , 2 : end ) ) ;
tuesday_indices( : , 2 : end ) = cumsum( tuesday_indices( : , 2 : end ) ) ;
wednesday_indices( : , 2 : end ) = cumsum( wednesday_indices( : , 2 : end ) ) ;
thursday_indices( : , 2 : end ) = cumsum( thursday_indices( : , 2 : end ) ) ;
friday_indices( : , 2 : end ) = cumsum( friday_indices( : , 2 : end ) ) ;
alldays_indices( : , 2 : end ) = cumsum( alldays_indices( : , 2 : end ) ) ;

if ( ishandle(1) )
 clf(1) ;
endif
figure( 1 ) ;
h1 = axes( 'position' , [ 0.03 , 0.54 , 0.30 , 0.43 ] ) ; plot( monday_indices(:,3) , 'k' , 'linewidth' , 2 , ...
monday_indices(:,4) , 'c' , 'linewidth' , 2 , ...
monday_indices(:,5) , 'b' , 'linewidth' , 2 , ...
monday_indices(:,6) , 'r' , 'linewidth' , 2 , ...
monday_indices(:,11) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'MONDAY' ) ;
legend( 'CAD' , 'CHF' , 'EUR' , 'GBP' , 'USD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h2 = axes( 'position' , [ 0.36 , 0.54 , 0.30 , 0.43 ] ) ; plot( tuesday_indices(:,3) , 'k' , 'linewidth' , 2 , ...
tuesday_indices(:,4) , 'c' , 'linewidth' , 2 , ...
tuesday_indices(:,5) , 'b' , 'linewidth' , 2 , ...
tuesday_indices(:,6) , 'r' , 'linewidth' , 2 , ...
tuesday_indices(:,11) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'TUESDAY' ) ;
legend( 'CAD' , 'CHF' , 'EUR' , 'GBP' , 'USD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h3 = axes( 'position' , [ 0.69 , 0.54 , 0.30 , 0.43 ] ) ; plot( wednesday_indices(:,3) , 'k' , 'linewidth' , 2 , ...
wednesday_indices(:,4) , 'c' , 'linewidth' , 2 , ...
wednesday_indices(:,5) , 'b' , 'linewidth' , 2 , ...
wednesday_indices(:,6) , 'r' , 'linewidth' , 2 , ...
wednesday_indices(:,11) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'WEDNESDAY' ) ;
legend( 'CAD' , 'CHF' , 'EUR' , 'GBP' , 'USD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h4 = axes( 'position' , [ 0.03 , 0.04 , 0.30 , 0.43 ] ) ; plot( thursday_indices(:,3) , 'k' , 'linewidth' , 2 , ...
thursday_indices(:,4) , 'c' , 'linewidth' , 2 , ...
thursday_indices(:,5) , 'b' , 'linewidth' , 2 , ...
thursday_indices(:,6) , 'r' , 'linewidth' , 2 , ...
thursday_indices(:,11) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'THURSDAY' ) ;
legend( 'CAD' , 'CHF' , 'EUR' , 'GBP' , 'USD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h5 = axes( 'position' , [ 0.36 , 0.04 , 0.30 , 0.43 ] ) ; plot( friday_indices(:,3) , 'k' , 'linewidth' , 2 , ...
friday_indices(:,4) , 'c' , 'linewidth' , 2 , ...
friday_indices(:,5) , 'b' , 'linewidth' , 2 , ...
friday_indices(:,6) , 'r' , 'linewidth' , 2 , ...
friday_indices(:,11) , 'g' , 'linewidth' , 2 ) ; xlim([0 21]) ; grid minor on ; title( 'FRIDAY' ) ;
legend( 'CAD' , 'CHF' , 'EUR' , 'GBP' , 'USD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h6 = axes( 'position' , [ 0.69 , 0.04 , 0.30 , 0.43 ] ) ; plot( alldays_indices(:,3) , 'k' , 'linewidth' , 2 , ...
alldays_indices(:,4) , 'c' , 'linewidth' , 2 , ...
alldays_indices(:,5) , 'b' , 'linewidth' , 2 , ...
alldays_indices(:,6) , 'r' , 'linewidth' , 2 , ...
alldays_indices(:,11) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'ALL DAYS COMBINED' ) ;
legend( 'CAD' , 'CHF' , 'EUR' , 'GBP' , 'USD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

if ( ishandle(2) )
 clf(2) ;
endif
figure( 2 ) ;
h1 = axes( 'position' , [ 0.03 , 0.54 , 0.30 , 0.43 ] ) ; plot( monday_indices(:,2) , 'k' , 'linewidth' , 2 , ...
monday_indices(:,7) , 'c' , 'linewidth' , 2 , ...
monday_indices(:,8) , 'b' , 'linewidth' , 2 , ...
monday_indices(:,9) , 'r' , 'linewidth' , 2 , ...
monday_indices(:,10) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'MONDAY' ) ;
legend( 'AUD' , 'HKD' , 'JPY' , 'NZD' , 'SGD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h2 = axes( 'position' , [ 0.36 , 0.54 , 0.30 , 0.43 ] ) ; plot( tuesday_indices(:,2) , 'k' , 'linewidth' , 2 , ...
tuesday_indices(:,7) , 'c' , 'linewidth' , 2 , ...
tuesday_indices(:,8) , 'b' , 'linewidth' , 2 , ...
tuesday_indices(:,9) , 'r' , 'linewidth' , 2 , ...
tuesday_indices(:,10) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'TUESDAY' ) ;
legend( 'AUD' , 'HKD' , 'JPY' , 'NZD' , 'SGD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h3 = axes( 'position' , [ 0.69 , 0.54 , 0.30 , 0.43 ] ) ; plot( wednesday_indices(:,2) , 'k' , 'linewidth' , 2 , ...
wednesday_indices(:,7) , 'c' , 'linewidth' , 2 , ...
wednesday_indices(:,8) , 'b' , 'linewidth' , 2 , ...
wednesday_indices(:,9) , 'r' , 'linewidth' , 2 , ...
wednesday_indices(:,10) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'WEDNESDAY' ) ;
legend( 'AUD' , 'HKD' , 'JPY' , 'NZD' , 'SGD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h4 = axes( 'position' , [ 0.03 , 0.04 , 0.30 , 0.43 ] ) ; plot( thursday_indices(:,2) , 'k' , 'linewidth' , 2 , ...
thursday_indices(:,7) , 'c' , 'linewidth' , 2 , ...
thursday_indices(:,8) , 'b' , 'linewidth' , 2 , ...
thursday_indices(:,9) , 'r' , 'linewidth' , 2 , ...
thursday_indices(:,10) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'THURSDAY' ) ;
legend( 'AUD' , 'HKD' , 'JPY' , 'NZD' , 'SGD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h5 = axes( 'position' , [ 0.36 , 0.04 , 0.30 , 0.43 ] ) ; plot( friday_indices(:,2) , 'k' , 'linewidth' , 2 , ...
friday_indices(:,7) , 'c' , 'linewidth' , 2 , ...
friday_indices(:,8) , 'b' , 'linewidth' , 2 , ...
friday_indices(:,9) , 'r' , 'linewidth' , 2 , ...
friday_indices(:,10) , 'g' , 'linewidth' , 2 ) ; xlim([0 21]) ; grid minor on ; title( 'FRIDAY' ) ;
legend( 'AUD' , 'HKD' , 'JPY' , 'NZD' , 'SGD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;

h6 = axes( 'position' , [ 0.69 , 0.04 , 0.30 , 0.43 ] ) ; plot( alldays_indices(:,2) , 'k' , 'linewidth' , 2 , ...
alldays_indices(:,7) , 'c' , 'linewidth' , 2 , ...
alldays_indices(:,8) , 'b' , 'linewidth' , 2 , ...
alldays_indices(:,9) , 'r' , 'linewidth' , 2 , ...
alldays_indices(:,10) , 'g' , 'linewidth' , 2 ) ; xlim([0 23]) ; grid minor on ; title( 'ALL DAYS COMBINED' ) ;
legend( 'AUD' , 'HKD' , 'JPY' , 'NZD' , 'SGD' , 'location' , 'north' , 'orientation' , 'horizontal' ) ;
vline( 7 , 'r' ) ; vline( 12 , 'g' ) ;
to conduct a quick visual analysis. This builds upon my recent work on fx pairs via oanda api and currency strength, and uses hourly data since June 2012.

This produces 24 hour seasonality charts of CAD, CHF, EUR, GBP and USD, i.e. the European and North American currencies.
The x-axis is in British Summer Time (BST) hours, the vertical red and green lines indicate 7:00am opens in London and New York respectively, all charts end at 17:00 New York (EST) time and the y-axis is hourly log returns. The individual currency seasonality lines are the cummulative cross-sectional means at BST 00, BST 01 ... etc. per weekday and all days combined (see subchart titles). BST Sunday evenings' returns prior to Monday trading are not included.

A similar chart for the Asian time zone currencies of AUD, HKD, JPY, NZD and SGD is also produced.
The function allows charts with a user selected data begin date to be plotted, but the illustrations above use all data available to me, i.e. hourly data since 2012.

It seems to me that, as indicated in my highlighting above, there is definite intraday forex seasonalilty in play. However, readers should be cautioned that the above is only a general tendency based on the last 9 years or so of hourly data. A more recent "data snapshot" of only data since the beginning of 2020 can tell a slightly different story:
look at GBP (red line) on Tuesdays and Wednesdays, for example. As always with stuff one reads online, even the extremely high quality stuff on this blog 😊, Caveat emptor.

No comments: