I would like to create a dashboard that publishes the factors FX investors care about: country fundamentals, carry, and price action.
The Economist publishes country fundamental data on their website. Here is the code to collect and clean that data.
library(tidyverse)
library(rvest)
library(RSelenium)
library(stringr)
library(gmailr)
library(tidyquant)
library(cluster)
library(ggdendro)
library(gridExtra)
library(directlabels)
#Get html using a JS phantom browser
url.base <- "http://www.economist.com/indicators"
pJS <- phantom()
Sys.sleep(1) # give the binary a moment
remDr <- remoteDriver(browserName = 'phantomjs')
remDr$open()
remDr$navigate(url.base)
html.raw <- read_html(remDr$getPageSource()[[1]])
#Parse Data into a data frame
vector.text <- html.raw %>%
html_nodes("tr") %>%
html_text() %>%
str_split("\n") %>% unlist() %>% trimws() %>% .[. != ""]
index.start <- str_which(vector.text, "United States")
vector.text <- vector.text[index.start:length(vector.text)]
data.initial.raw <- tibble(country = vector.text,
GDP.latest = lead(vector.text),
GDP.quarter = lead(vector.text,2),
GDP.2017 = lead(vector.text,3),
GDP.2018 = lead(vector.text,4),
IP = lead(vector.text,5),
CPI.latest = lead(vector.text,6),
CPI.year.ago = lead(vector.text,7),
CPI.2017 = lead(vector.text,8),
unemployment.rate = lead(vector.text,9),
trade.balance = lead(vector.text,10),
CAB = lead(vector.text,11),
CAB.ratio = lead(vector.text,12),
currency.today = lead(vector.text,13),
currrency.year.ago = lead(vector.text,14),
budget.balance = lead(vector.text,15),
interest.rate.short = lead(vector.text,16),
interest.rate.long = lead(vector.text,17)) %>%
filter(str_detect(country, "[0-9]") == FALSE) %>%
filter(country != "na") %>%
filter(str_detect(country, "nil") == FALSE) %>%
filter(country != "-")
convert.to.numeric <- function(x,n) {
str_sub(x,0,n) %>% as.numeric()
}
#Clean the data
data.initial.clean <- data.initial.raw %>%
select(country, GDP.latest, IP, CPI.latest, unemployment.rate, CAB.ratio, budget.balance, interest.rate.short, interest.rate.long) %>%
mutate(GDP.latest = convert.to.numeric(GDP.latest,4),
IP = convert.to.numeric(GDP.latest,4),
CPI.latest = convert.to.numeric(CPI.latest,4),
unemployment.rate = convert.to.numeric(unemployment.rate,3),
CAB.ratio = as.numeric(CAB.ratio),
budget.balance = as.numeric(budget.balance),
interest.rate.short = as.numeric(interest.rate.short),
interest.rate.long = convert.to.numeric(interest.rate.long,4))
Now, we rank the countries. To do so, we calculate z-scores for each of the fundamental values, then create a final rank that averages the z-score for each of the consituent indicators.
data.tidy <- data.initial.clean %>%
gather(key = "indicator", value = "value", GDP.latest:interest.rate.long) %>%
arrange(indicator, -value) %>%
group_by(indicator) %>%
mutate(rank = row_number()) %>%
filter(value != "NA")
#Distinguish between indicators where higher numbers are good or bad
indicators.bad <- data.tidy %>%
filter(indicator == "CPI.latest" | indicator == "unemployment.rate") %>%
arrange(indicator, value) %>%
mutate(rank = row_number())
indicators.good <- data.tidy %>%
filter(indicator != "CPI.latest" & indicator != "unemployment.rate" & indicator != "interest.rate.short")
ranks.overall <- bind_rows(indicators.bad, indicators.good) %>%
group_by(country) %>%
summarize(rank.overall = mean(rank)) %>%
arrange(rank.overall) %>%
mutate(rank = row_number()) %>%
select(country, rank)
#Merge the original indicators with the final country ranks
data.dashboard.fundamental <-
bind_rows(indicators.bad, indicators.good) %>%
select(country, indicator, value) %>%
arrange(country, indicator) %>%
filter(country != "+") %>%
spread(indicator, value) %>%
left_join(ranks.overall) %>%
arrange(rank)
Here is the final ranking as a table:
Country | Budget Surplus | CAB Ratio | CPI | GDP | Long Rates | Industrial Production | Unemployment | Rank |
---|---|---|---|---|---|---|---|---|
Singapore | -0.7 | 19.5 | NA | 3.6 | 2.4 | 3.6 | 2.1 | 1 |
Ireland | -0.3 | 5.1 | 0.2 | 10.0 | 1.1 | 10.0 | 6.0 | 2 |
Thailand | -2.3 | 9.7 | 0.4 | 4.0 | 2.5 | 4.0 | 1.3 | 3 |
Slovenia | -0.3 | 6.0 | 1.2 | 6.0 | NA | 6.0 | 9.0 | 4 |
Czech Republic | 0.5 | 0.9 | 1.8 | 5.1 | 1.9 | 5.1 | 2.4 | 5 |
Hong Kong | 0.4 | 4.8 | 1.6 | 3.4 | 2.0 | 3.4 | 2.9 | 6 |
Luxembourg | 0.9 | 4.7 | 1.0 | 3.2 | NA | 3.2 | 6.0 | 7 |
South Korea | 0.7 | 5.1 | 1.4 | 3.0 | 2.8 | 3.0 | 4.6 | 8 |
Malaysia | -2.8 | 2.8 | 2.7 | 5.9 | 4.0 | 5.9 | 3.3 | 9 |
Germany | 0.8 | 7.8 | 1.4 | 2.9 | 0.6 | 2.9 | 3.6 | 10 |
Hungary | -2.6 | 3.7 | 1.9 | 4.4 | 2.7 | 4.4 | 3.8 | 11 |
Taiwan | -0.8 | 13.6 | 2.2 | 3.3 | 1.0 | 3.3 | 3.7 | 12 |
Estonia | -0.4 | 2.0 | 3.1 | 5.0 | NA | 5.0 | 5.3 | 13 |
Netherlands | 0.6 | 9.8 | 1.2 | 2.9 | 0.7 | 2.9 | 5.2 | 14 |
Philippines | -2.0 | 0.4 | 3.9 | 6.6 | 6.1 | 6.6 | 5.3 | 15 |
Vietnam | -6.3 | 0.0 | 3.2 | 6.8 | 4.2 | 6.8 | 2.3 | 16 |
Switzerland | 0.8 | 9.7 | 0.6 | 1.9 | 0.1 | 1.9 | 2.9 | 17 |
China | -4.0 | 1.3 | 2.9 | 6.8 | NA | 6.8 | 3.9 | 18 |
New Zealand | 1.1 | -2.6 | 1.6 | 3.0 | 3.0 | 3.0 | 4.5 | 19 |
Israel | -2.5 | 3.4 | 0.1 | 2.9 | 1.8 | 2.9 | 3.7 | 20 |
Sweden | 0.5 | 4.2 | 1.6 | 3.3 | 0.8 | 3.3 | 7.0 | 21 |
Iceland | 1.3 | 5.6 | 2.2 | 1.5 | NA | 1.5 | 2.4 | 22 |
Indonesia | -2.3 | -1.9 | 3.2 | 5.2 | 6.6 | 5.2 | 5.5 | 23 |
Latvia | -1.2 | -0.2 | 1.9 | 4.2 | NA | 4.2 | 8.1 | 24 |
Poland | -2.7 | 0.0 | 1.9 | 4.3 | 3.4 | 4.3 | 6.9 | 25 |
India | -3.5 | -2.0 | 4.4 | 7.2 | 7.7 | 7.2 | 6.1 | 26 |
Austria | -0.8 | 2.0 | 1.8 | 2.9 | 0.7 | 2.9 | 5.5 | 27 |
Norway | 4.9 | 5.5 | 2.2 | 1.4 | 2.0 | 1.4 | 4.1 | 28 |
Turkey | -2.1 | -5.2 | 10.0 | 11.0 | 12.7 | 11.0 | 10.0 | 29 |
Russia | -1.1 | 2.7 | 2.2 | 1.8 | 8.1 | 1.8 | 5.2 | 30 |
Denmark | -0.9 | 7.8 | 0.6 | 1.2 | 0.7 | 1.2 | 4.1 | 32 |
Lithuania | 0.6 | 0.8 | 3.5 | 3.9 | 1.2 | 3.9 | 9.1 | 33 |
Slovakia | -1.1 | -0.6 | 2.1 | 3.5 | 0.9 | 3.5 | 5.9 | 34 |
Portugal | -1.0 | 0.0 | 0.6 | 2.4 | 1.8 | 2.4 | 8.1 | 35 |
Euro area | -1.0 | 3.1 | 1.2 | 2.7 | 0.6 | 2.7 | 8.6 | 36 |
Canada | -1.8 | -2.6 | 1.7 | 2.9 | 2.2 | 2.9 | 5.8 | 37 |
Spain | -2.6 | 1.6 | 1.1 | 3.1 | 1.3 | 3.1 | 16.0 | 38 |
Finland | -1.4 | 1.2 | 0.6 | 2.6 | 0.7 | 2.6 | 8.8 | 39 |
Greece | -0.5 | -1.6 | 0.1 | 1.8 | 4.1 | 1.8 | 20.0 | 40 |
Japan | -4.9 | 3.7 | 1.3 | 2.0 | 0.0 | 2.0 | 2.4 | 41 |
Australia | -1.2 | -1.8 | 1.9 | 2.4 | 2.8 | 2.4 | 5.5 | 42 |
Chile | -2.2 | -0.2 | 2.0 | 2.2 | 4.5 | 2.2 | 6.5 | 43 |
Italy | -2.1 | 2.6 | 0.6 | 1.6 | 2.0 | 1.6 | 11.0 | 44 |
United States | -4.5 | -2.7 | 2.2 | 2.5 | 2.9 | 2.5 | 4.1 | 45 |
Pakistan | -5.6 | -5.0 | 3.8 | 5.7 | NA | 5.7 | 5.9 | 46 |
Belgium | -1.5 | -0.3 | 1.5 | 1.9 | 0.9 | 1.9 | 6.6 | 47 |
Argentina | -5.6 | -4.8 | 25.0 | 4.2 | 4.2 | 4.2 | 8.3 | 48 |
France | -2.7 | -0.9 | 1.2 | 2.5 | 0.9 | 2.5 | 9.0 | 49 |
Mexico | -2.3 | -2.0 | 5.3 | 1.5 | 7.5 | 1.5 | 3.4 | 50 |
Peru | -3.5 | -1.3 | 1.2 | 2.2 | NA | 2.2 | 8.5 | 51 |
Brazil | -7.0 | -1.3 | 2.8 | 2.1 | 8.1 | 2.1 | 12.0 | 52 |
Venezuela | -15.5 | 0.6 | NA | -8.8 | 8.2 | -8.8 | 7.3 | 53 |
Ukraine | -2.6 | -4.2 | 14.0 | 1.8 | NA | 1.8 | 1.4 | 54 |
Colombia | -2.0 | -3.0 | 3.4 | 1.6 | 6.6 | 1.6 | 11.0 | 55 |
Saudi Arabia | -7.2 | 4.0 | 3.0 | -0.7 | NA | -0.7 | 5.8 | 56 |
Britain | -2.8 | -4.4 | 3.0 | 1.4 | 1.6 | 1.4 | 4.4 | 57 |
South Africa | -3.6 | -2.7 | 4.4 | 1.5 | 8.1 | 1.5 | 26.0 | 58 |
Egypt | -10.3 | -3.8 | 14.0 | NA | NA | NA | 11.0 | 59 |
FX traders also care about carry. That is, how much can the earn (or do they have to pay) by being long one currency and short another. This table extracts the short rate for the countries whos currencies are tradable on the Oanda platform.
Country | Interest Rate |
---|---|
Turkey | 13.9 |
Mexico | 7.8 |
South Africa | 7.1 |
China | 4.7 |
Malaysia | 3.6 |
United States | 2.1 |
Australia | 2.1 |
Saudi Arabia | 2.0 |
New Zealand | 1.9 |
Canada | 1.6 |
Poland | 1.5 |
Hong Kong | 1.1 |
Norway | 1.0 |
Thailand | 1.0 |
Czech Republic | 0.9 |
Britain | 0.6 |
Hungary | 0.0 |
Japan | -0.1 |
Euro area | -0.3 |
Sweden | -0.4 |
Switzerland | -0.7 |
Singapore | NA |
FX traders care about momntume - currencies that have recently gained in price often keep gaining in price. This code shows you how to collect FX data from Oanda.
library(tidyquant)
data.fx <- c("EUR/USD", "USD/JPY","USD/MXN","USD/NOK","USD/TRY","GBP/USD","USD/CAD","USD/AUD","NZD/USD","USD/CHF","USD/CZK",
"USD/DKK","USD/HKD","USD/HUF","USD/PLN","USD/SAR","USD/SEK","USD/SGD","USD/THB","USD/ZAR", "USD/CNH") %>%
tq_get(get = "exchange.rates") %>%
arrange(desc(date))
To created a momentum dashboard, we rebase all currencies into USD, then compute the 15, 50, and 180 day returns for all currencies we can trade on Oanda.
#Base all currencies in USD
flip.sign <- function(x) {
x %>%
str_replace("/USD","") %>% str_c("USD/",.)
}
data.fx.momentum <-
data.fx %>%
group_by(symbol) %>%
#calculate lookback returns with the Delt fucntion
tq_mutate(select = exchange.rate, mutate_fun = Delt, k = 15, col_rename = "return.15.days") %>%
tq_mutate(select = exchange.rate, mutate_fun = Delt, k = 50, col_rename = "return.50.days") %>%
tq_mutate(select = exchange.rate, mutate_fun = Delt, k = 179, col_rename = "return.180.days") %>%
arrange(desc(date)) %>%
slice(1) %>% #only select the most recent day
gather(lookback, return, exchange.rate:return.180.days) %>%
filter(lookback != "exchange.rate") %>%
mutate(return = round(100*return, digits = 2)) %>%
ungroup() %>%
#denominate all currency pairs as base USD
mutate(return = ifelse(str_detect(symbol, "USD$") == TRUE, (-1)*return, return),
symbol = ifelse(str_detect(symbol, "USD$") == TRUE, flip.sign(symbol), symbol)) %>%
spread(lookback, return) %>%
select(symbol,return.15.days, return.50.days, return.180.days) %>%
arrange(desc(return.15.days))
Here is the momentum dashboard, in a table:
Symbol | 15 Day Return | 50 Day Return | 180 Day Return |
---|---|---|---|
USD/TRY | 2.05 | 2.47 | 12.66 |
USD/CAD | 1.84 | 3.95 | 6.16 |
USD/ZAR | 1.16 | -2.51 | -10.44 |
USD/CHF | 0.72 | -1.64 | -1.54 |
USD/HKD | 0.19 | 0.29 | 0.28 |
USD/PLN | 0.15 | -0.03 | -5.11 |
USD/SEK | 0.14 | 2.13 | 2.97 |
USD/CNH | 0.00 | -1.46 | -3.64 |
USD/SAR | 0.00 | 0.00 | 0.00 |
USD/GBP | -0.20 | 0.07 | -2.75 |
USD/CZK | -0.45 | -0.68 | -5.84 |
USD/AUD | -0.64 | 1.36 | 1.56 |
USD/JPY | -0.65 | -3.77 | -3.98 |
USD/DKK | -0.67 | -0.84 | -3.41 |
USD/EUR | -0.72 | -0.92 | -3.64 |
USD/THB | -0.75 | -2.09 | -5.89 |
USD/SGD | -0.76 | -0.66 | -2.62 |
USD/MXN | -0.77 | -0.90 | 5.23 |
USD/NZD | -0.86 | 0.06 | -0.42 |
USD/NOK | -1.35 | -1.54 | -1.49 |
USD/HUF | -1.42 | -0.31 | -2.80 |
Currency traders should be aware of currency correlations as well. To quickly visualize currency correlations, we do hierarchical clustering of the monthly returns for all currencies over the past 6 months, then visuzalize as a dengrogram.
library(tidyquant)
library(ggdendro)
library(cluster)
data.fx <- read_csv("data.fx.csv")
flip.sign <- function(x) {
x %>%
str_replace("/USD","") %>% str_c("USD/",.)
}
data.fx.to.cluster <-
data.fx %>%
group_by(symbol) %>%
#Calculate monthly returns
tq_transmute(select = exchange.rate,
mutate_fun = periodReturn,
period = "monthly",
type = "arithmetic") %>%
#Denominate all returns in base USD
ungroup() %>%
mutate(monthly.returns = ifelse(str_detect(symbol, "USD$") == TRUE, (-1)*monthly.returns, monthly.returns),
symbol = ifelse(str_detect(symbol, "USD$") == TRUE, flip.sign(symbol), symbol)) %>%
spread(date, monthly.returns)
data.fx.to.cluster %>%
#Munge the data
remove_rownames() %>%
column_to_rownames(var = "symbol") %>%
#Calculate the clusters
dist() %>%
hclust(method = "ward") %>%
#Plot the dendrogram
ggdendrogram(., rotate = TRUE) +
labs(title = "Currency Dendrogram",
subtitle = "Clustering monthly returns for past half-year",
caption = "Source: Granite Peak Advisory")
There is a class of currencies that are disprorportiontely effected by commodities prices. Those countries are Canada, Norway, New Zealand, Sweden, and Australia. This table shows the returns for the commodities currencies.
library(tidyquant)
library(directlabels)
data.fx <- read_csv("data.fx.csv")
list.commodity.currencies <- c("USD/CAD","USD/NOK","NZD/USD","USD/SEK","USD/AUD")
data.fx %>%
filter(symbol %in% list.commodity.currencies) %>%
group_by(symbol) %>%
arrange(symbol, date) %>%
tq_mutate(select = exchange.rate, mutate_fun = Delt, k = 1, col_rename = "delta") %>%
ungroup() %>%
mutate(delta = ifelse(str_detect(symbol, "USD$") == TRUE, (-1)*delta, delta),
symbol = ifelse(str_detect(symbol, "USD$") == TRUE, flip.sign(symbol), symbol)) %>%
na.omit() %>%
group_by(symbol) %>%
mutate(index = 100*cumprod(1+delta)) %>%
ggplot(aes(date,index, group = symbol)) +
geom_line(aes(colour = symbol)) +
theme_minimal() +
labs(title = "Commodity Currencies",
subtitle = "Performance over last 6 months, rebased to 100",
x = "Date",
y = "Currency Index") +
geom_dl(aes(label = symbol), method = list(dl.combine("last.points"),cex = 0.8)) +
scale_x_date(expand=c(0, 50)) +
scale_color_discrete(guide = FALSE)
We have collected, cleaned, and presented data that FX traders will need.