Short Term Reversal

In my first post, I’ve talked about momentum in South Korea. Following the methodology of Jegadeesh and Titman (1993), I’ve shown that decile portfolios based on 9 month past return and 1 month gap exihibts momentum trend with the exception of the highest decile. I’ve mentioned that despite the robustness of momentum phenomenon, it is not practical to trade on this due to high transactions cost and shorting constraint. One potential way to circumvent this issue is to trade in the futures market, which allows shorting positions and has very low transactions costs. Yet, Korean futures market has a limitation in that stock futures are not very liquid, especially for contracts that have later expiration dates. Given that stock futures contract maturities are available at monthly frequency, implementing long term momentum strategy (monthly rebalancing) with futures will either necessarily require rollover or trading in less liquid contract market, which can be risky. To ameliate this concern, I will look at a closely related strategy - short term reversal.

Among 2,000+ stocks listed in KOSPI and KOSDAQ, there are about 140 or so stock or ETF related futures and even fewer of them have enough liquidity. The first step in analyzing short-term reversal strategy is to isolate out candidate futures. Here is transaction volume, closing price, and opening price for all stock futures with 2020 March maturity in 30 minute brackets, obtained from Daishin Securities’ Home Trading System (HTS). The data was extracted on March 9th and has starting time of January 2nd, 2020. To isolate out potential stocks, I will first look at average daily volume. Since each contract matures on the second Thursday of its maturity month, I will look at February 15th, 2020 (first day March 2020 contract becomes most liquid contract) onward.

#KOSPI Trend prediction

dt = fread("intra_fut.csv")
head(dt,2) #shows 2 rows of dt
##    futcode stockcode     date time open_price close_price volume
## 1:   111Q3   A005930 20200309 1545        541         538  49919
## 2:   111Q3   A005930 20200309 1530        543         541 142702

futcode is future contract id and stockcode is stockcode of underlying stock. time refers to the closing time of the 30 minute bracket. Korean stock market hours are 9:00 AM - 3:30PM where as Korean futures market hours are 9:00AM - 3:45PM. As a result, time bracket ending on 1545 has exception that it is 15 minutes. Now let’s look at which stocks have highest average volume.

daily_volume = dt[,.(volume = sum(volume)),by = .(date, stockcode)]
avg_volume = daily_volume[,.(volume=mean(volume)), .(stockcode)][order(-volume)]
##    stockcode    volume
## 1:   A005930 616087.28
## 2:   A000660  94400.38

Most traded future is related to A005930: Samsung Eletronics and the second most traded future is related to A000660: SK Hynix.

hist(avg_volume[! stockcode %in%  c('A005930','A000660')]$volume/1000, main = "Avg volume exc. Samsung Electronics/SK Hynix", xlab = "avg volume (000)")

Looking at the histogram of average daily volume for stock futures (Samsung electronics and SK Hynix is excluded to make the plot prettier), majority of stock futures have on average less than 5000 transactions in a day. In this post, I will be looking at 35 stocks with average volume over 10,000 contracts. The data set used in the remainder of the post is here

The trading strategy is rather simple. I will be looking at past 2 day’s cumulative return and go long on stock with lowest return and go short on stock with the highest return. Futures contract is a contract that will have value in direct proportion to the underlying asset. In Korean stock futures’ case, each contract at maturity is worth 10 times the stock price. As a result, futures price moves in unison with the underlying stock price. Thus, to test the robustness of short-term reverasal, I will look at profitability of short term reversal strategy assuming stock prices is futures prices (this is due to the difficulty of obtaining past returns of expired futures contracts).

stock_dt = fread("daily_stocks.csv")

hold_window = 1 #Number of trading days to hold security
past_window = 2 #Number of trading days to compute past return
numports = 7 #Stocks will be divided into 7 bins

#Compute Returns from holding
stock_dt[, ret:=shift(adj_cls, -hold_window)/adj_cls, by = stockid]
#Compute Past Return
stock_dt[, past_ret:=adj_cls/shift(adj_cls,past_window), by = stockid]
#Keep only observations with past returns and future return
stock_dt = stock_dt[! & !]
#Based on past return place into bins
#Average daily returns for each decile portfolio
bin_daily_return = stock_dt[,.(port_ret=mean(ret), past_ret=mean(past_ret)),by=.(momentum_bin,tradedate)]
bin_return = bin_daily_return[,.(bin_ret=mean(port_ret)),by=momentum_bin][order(momentum_bin)]
##    momentum_bin   bin_ret
## 1:            1 1.0003701
## 2:            2 0.9991952
## 3:            3 0.9995643
## 4:            4 0.9985996
## 5:            5 0.9985105
## 6:            6 0.9979376
## 7:            7 0.9987271

It appears that lowest momentum bin (stocks with lowest return in the past 2 days) have the highest subsequant return compared to the other 4 buckets. While the ideal long-short portfolio would long bin 1 and short bin 5, as with momentum strategies, the highest bin appears erratic. If one were to go long on bin 1 and short on bin 6, the resulting long-short portfolio would result in approximately 0.22% daily return after trnascation costs and 2.26 annualized sharpe ratio, which is quite good.

#Long Short Portfolio Performance
longshort = merge(bin_daily_return[momentum_bin==1,.(tradedate, long = port_ret)], 
                    bin_daily_return[momentum_bin==numports-1,.(tradedate, short = port_ret)], by = "tradedate")
print(paste("daily mean return is ", round((mean(longshort$diff)-0.00024)*100,2),"%"))
## [1] "daily mean return is  0.22 %"
print(paste("annualized sharpe ratio is ", round(sqrt(252)*(mean(longshort$diff)-0.00024)/sd(longshort$diff),2)))
## [1] "annualized sharpe ratio is  2.26"

Looking at distribution of returns

hist(longshort$diff*100, main = "Histogram of Reversal Strategy", xlab = "Daily Return(%)")

While definitely executeable, there are few additional things to consider in this strategy. First is market impact.

#Compute opening price and total traded volume daily
daily_dt = dt[order(stockcode, date,time)][,.(volume = sum(volume), open_prc = head(open_price,1)),by = .(date, stockcode)]
#compute daily average cash volume (opening price * volume)
cash_volume = daily_dt[date>='2020-02-14',.(cash_volume=mean(volume*open_prc)), .(stockcode)]
#Isolate only stock futures with over 10,000 trades
cash_volume = cash_volume[stockcode %in% avg_volume[volume>=10000]$stockcode][order(cash_volume)]
summary(cash_volume$cash_volume) #Amount in 000 won
##      Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
##    413891   1723416   5462307  22866004  17563064 352508532

As can be seen in the summary stat above, among the 35 stocks with average daily volume over 10,000 contracts, the smallest stock futures (A008560: MERITZ SECURITIES) trades only about 413,891,000 KRW daily (approximately 413,891$). This is problematic since the largest stock future (A051900: LG HOUSEHOLD & HEALTH CARE) is 14,200,000 KRW per contract and if one were to implement equal-weight long-short, long 1 contract on LG Household & Health Care and equal value short on Meritz Securities would necessarily create market impact.

Second issue is bid-ask spread. This analysis assumes that futures contract can be bought and sold at closing price of the underlying stock. Given that futures market closes 15 minutes later than stock market, knowing the closing price is not an issue. However, for some of these futures with large bid ask spread, the bid-ask spread may eat into the return. Nevertheless, further refining of this strategy into more liquid futures may prove useful.

Tags: R  trading_strategy  futures 

Discussion and feedback