UK index and UK ETF
I discovered another market neutral opportunity this month.
And this is based on ratio between FTSE 100 index and MSCI based UK ETF (EWU)
Based on backtest, sharpe ratio is close to 0.9.
The composition between these 2 indexes are largely similar and any significant deviation shouldn’t persist for long.
The optimal lookback period for the MA component in bollinger band is approximately 40 days.
As this is backed by stellar performance and economic reason, I’ve decided to deploy some capital to this strategy.
Disclosure
- I executed a long (FTSE 100) CFD short (EWU) strategy on 18-September with approximately $25000 on both positions. Equity to loan (with no cash) outlay is only 3750 dollars on long side. I entered at a ratio of 232 with expected profits of 2.4% excluding further 2% p.a. of interest on short sale cash. I expect a 3% long finance rate of approximately 2 dollars per day. Short interest gain rate is around 1.5% translating to 1 dollars per day. Net finance cost is around 1 dollar a day.
- Max holding period is 24 work days (ending on 22nd October 2019) based on half life formula here (http://en.wikipedia.org/wiki/Ornstein-Uhlenbeck_process) as advised by Ernest Chan. See his example for further explanation
- I’m using pushoverr notification api linked to my phone app within my cron task scheduler. It will inform me to close my positions when the ratio dips below moving average.
suppressMessages(sapply(c("zoo", "tidyr", "plyr", "dplyr",
"gtools","googlesheets", "quantmod",
"urca", "PerformanceAnalytics", "parallel", "TTR", "pushoverr"), require, character.only = T))
## zoo tidyr plyr
## TRUE TRUE TRUE
## dplyr gtools googlesheets
## TRUE TRUE TRUE
## quantmod urca PerformanceAnalytics
## TRUE TRUE TRUE
## parallel TTR pushoverr
## TRUE TRUE TRUE
source('util/calculateReturns.R')
source('util/calculateMaxDD.R')
source('util/backshift.R')
source('util/extract_stock_prices.R')
source('util/cointegration_pair.R')
stock1 = "^FTSE"
stock2 = "EWU"
n_days = 40
prop_train = 0.7
start_date = "2000-07-01"
end_date = "2019-12-30"
#Start of function
data1 = df_crawl_time_series(stock1, start_date, end_date)
## 'getSymbols' currently uses auto.assign=TRUE by default, but will
## use auto.assign=FALSE in 0.5-0. You will still be able to use
## 'loadSymbols' to automatically load data. getOption("getSymbols.env")
## and getOption("getSymbols.auto.assign") will still be checked for
## alternate defaults.
##
## This message is shown once per session and may be disabled by setting
## options("getSymbols.warning4.0"=FALSE). See ?getSymbols for details.
##
## WARNING: There have been significant changes to Yahoo Finance data.
## Please see the Warning section of '?getSymbols.yahoo' for details.
##
## This message is shown once per session and may be disabled by setting
## options("getSymbols.yahoo.warning"=FALSE).
## Warning: ^FTSE contains missing values. Some functions will not work if
## objects contain missing values in the middle of the series. Consider using
## na.omit(), na.approx(), na.fill(), etc to remove or replace them.
data1 = subset(data1, select = c("Date", "Open", "Adj.Close"))
names(data1) = c("Date", "Open", "Close")
data1$Date = as.Date(data1$Date)
data2 = df_crawl_time_series(stock2, start_date, end_date)
data2 = subset(data2, select = c("Date", "Open", "Adj.Close"))
names(data2) = c("Date", "Open", "Close")
data2$Date = as.Date(data2$Date)
#Training and testing index
data1 = xts(data1[, -1], order.by = data1[, 1])
data2 = xts(data2[, -1], order.by = data2[, 1])
data = merge(data1, data2)
data = as.data.frame(data)
data = subset(data, !is.na(data$Close) & !is.na(data$Close.1))
data$ratio = data$Close/ data$Close.1
# plot(data$ratio)
bb_ratio = data.frame(BBands(data$ratio,n = n_days))
data = cbind(data, bb_ratio)
data_sub = tail(data, 1000)
plot(data_sub$ratio)
lines(data_sub$mavg, col = "red")
lines(data_sub$up, col = "blue")
lines(data_sub$dn, col = "green")
#If lower than
data_sub$longs <- data_sub$ratio <= data_sub$dn # buy spread when its value drops below 2 standard deviations.
data_sub$shorts <- data_sub$ratio >= data_sub$up # short spread when its value rises above 2 standard deviations.
# exit any spread position when its value is at moving average
data_sub$longExits <- data_sub$ratio >= data_sub$mavg
data_sub$shortExits <- data_sub$ratio <= data_sub$mavg
# # define indices for training and test sets
# trainset <- 1:as.integer(nrow(data) * prop_train)
# testset <- (length(trainset)+1):nrow(data)
#Signal
data_sub$posL1 = NA
data_sub$posL2 = NA
data_sub$posS1 = NA
data_sub$posS2 = NA
# initialize to 0
data_sub$posL1[1] <- 0; data_sub$posL2[1] <- 0
data_sub$posS1[1] <- 0; data_sub$posS2[1] <- 0
data_sub$posL1[data_sub$longs] <- 1
data_sub$posL2[data_sub$longs] <- -1
data_sub$posS1[data_sub$shorts] <- -1
data_sub$posS2[data_sub$shorts] <- 1
data_sub$posL1[data_sub$longExits] <- 0
data_sub$posL2[data_sub$longExits] <- 0
data_sub$posS1[data_sub$shortExits] <- 0
data_sub$posS2[data_sub$shortExits] <- 0
#positions
data_sub$posL1 <- zoo::na.locf(data_sub$posL1); data_sub$posL2 <- zoo::na.locf(data_sub$posL2)
data_sub$posS1 <- zoo::na.locf(data_sub$posS1); data_sub$posS2 <- zoo::na.locf(data_sub$posS2)
data_sub$position1 <- data_sub$posL1 + data_sub$posS1
data_sub$position2 <- data_sub$posL2 + data_sub$posS2
#Returns
data_sub$dailyret1 <- ROC(data_sub$Close) # last row is [385,] -0.0122636689 -0.0140365802
data_sub$dailyret2 <- ROC(data_sub$Close.1) # last row is [385,] -0.0122636689 -0.0140365802
#Backshifting here. But signal is for following day returns!. So can still use latest Z-score
data_sub$date = as.Date(row.names(data_sub))
data_sub = xts(data_sub[,-which(names(data_sub) == "date")], order.by = data_sub[, which(names(data_sub) == "date")])
#Doesn't account for number of shares!!!!!
data_sub$pnl = lag(data_sub$position1, 1) * data_sub$dailyret1 + lag(data_sub$position2, 1) * data_sub$dailyret2
#Performance analytics
tryCatch({
charts.PerformanceSummary(data_sub$pnl)
}, error = function(e){})
table.Drawdowns(data_sub$pnl)
## From Trough To Depth Length To Trough Recovery
## 1 2017-12-13 2018-02-01 2018-03-27 -0.0703 70 33 37
## 2 2015-12-18 2016-01-27 2016-03-03 -0.0626 50 25 25
## 3 2016-10-06 2016-10-28 2016-12-15 -0.0509 50 17 33
## 4 2016-06-29 2016-07-07 2016-10-05 -0.0471 68 6 62
## 5 2019-07-15 2019-08-01 <NA> -0.0442 50 14 NA
table.DownsideRisk(data_sub$pnl)
## pnl
## Semi Deviation 0.0035
## Gain Deviation 0.0057
## Loss Deviation 0.0045
## Downside Deviation (MAR=210%) 0.0095
## Downside Deviation (Rf=0%) 0.0034
## Downside Deviation (0%) 0.0034
## Maximum Drawdown 0.0703
## Historical VaR (95%) -0.0083
## Historical ES (95%) -0.0125
## Modified VaR (95%) -0.0065
## Modified ES (95%) -0.0065
table.AnnualizedReturns(data_sub$pnl)
## pnl
## Annualized Return 0.0748
## Annualized Std Dev 0.0867
## Annualized Sharpe (Rf=0%) 0.8630
#Extract the moving average, ratio
update = paste(stock1, stock2,
"Current ratio is:", round(as.numeric(data_sub$ratio[nrow(data_sub)]), 3),
"Moving average is", round(as.numeric(data_sub$mavg[nrow(data_sub)]), 3),
"Expected profits left in %", abs(round(100 * (as.numeric(data_sub$ratio[nrow(data_sub)]) - as.numeric(data_sub$mavg[nrow(data_sub)]))/as.numeric(data_sub$ratio[nrow(data_sub)]), 1)),
"UB is", round(as.numeric(data_sub$up[nrow(data_sub)]), 3),
"LB is", round(as.numeric(data_sub$dn[nrow(data_sub)]), 3)
)
if(as.numeric(data_sub$ratio[nrow(data_sub)]) < as.numeric(data_sub$dn[nrow(data_sub)])) {
update = paste(update, "Long ratio!")
} else{
update = paste(update, "\nDon't enter\n")
}
if(as.numeric(data_sub$ratio[nrow(data_sub)]) > as.numeric(data_sub$up[nrow(data_sub)])) {
update = paste(update, "Short ratio!")
} else{
update = paste(update, "Don't enter")
}
# if(as.numeric(data_sub$ratio[nrow(data_sub)]) < as.numeric(data_sub$mavg[nrow(data_sub)])) {
# update = paste(update, "Liquidate position!")
# } else{
# update = paste(update, "Hold")
# }
#pushover(message = update, user = Sys.getenv("pushover_user"), app = Sys.getenv("pushover_app"))