At the end of the post you may find a reproducible R code, which I (shamlessly) think is written not so bad.

Basically, the quality factor, that the authors of the paper analyze, is a general characteristic of firms that are well governed, have higher profitability or e.g have sound strategy. The difference between this factor and a popular value is that quality is not related to the valuation of a particular company, so it does not take into account market cap in its metrics. This factor was well described and analyzed by Clifford S. Asness, Andrea Frazzini & Lasse Heje Pedersen in their paper “Quality minus Junk”. It’s a very interesting read.

The authors of the paper which we’d like to reproduce try to measure the same factor but without relying on fundamental data, like gross margin or profit growth. The authors came with pretty interesting and clever way to do so. Basic idea is to:

- Divide stock universe by their sectors and market capitalization.
- For each sector identify the worst performing month of a given year (stress time).
- In the next year, form a portfolio of the most stress-stable (best performance) stocks during stress time within their market cap and sector.

So, with the strategy above, we would like to find the stocks that are doing relatively good in hard times, according to the popular saying of Warren Buffet: *A rising tide lifts all boats. Only when the tide goes out do you discover who has been swimming naked*.

The data I gathered contains information on 171 (approx. 40% of listed) randomly sampled stocks from Warsaw Stock Exchange (WSE hereinafter), equally divided into 9 sectors. For each stock we have its market capitalization, prices and industry in which they operate. Number of sectors, as well stocks included should be high enough, so that there would be no stock-specific moves seen on the sector benchmarks. Performance of equally-weighted portfolios of these stocks looks as below:

Except from the healthcare during coronavirus crisis and trade and services in 2015:2017, most of the sectors have stable long term returns, which seems that there is, fortunately, not that much stock-specific moves that drives particular indexes.

Now with sector-wide returns we will identify stressful times, which are the worst performance months each year for each sector. The heatmap below shows performance of each sector and highlighted stressful period.

As one may see, the stressful times are often correlated during times of crisis (Look for example on October 2008), which is unsurprising given that most assets are more correlated during periods of higher volatility. But otherwise there is a significant amount of heterogeneity among sectors. That is why it is important to differentiate between those sectors, when looking at the quality stocks.

Another important variable that we should control is market capitalization. As common sense would suggest, there is a negative relation between market capitalization and volatility of underlying stock. We may try to confirm this relation on our sample of stocks.

Unsurprisingly, our data also exhibits this (significant) relationship. In our sample, an increase of market cap by 1% should lower standard deviation by 0.13%.

That’s why we will divide stocks also by their market cap. For each sector we have 19 stocks, so we can only afford to make two market cap brackets. We will split them by median of the sector market cap. If we would not divide by market capitalization, as previous chart shows, smaller market cap stocks would be less often identified as stress-stable, even though, in theory quality should be characterized by smaller company as well.

Now, with prepared data, we can form portfolios. At this stage there are various rules of doing it and backtest strategy over and over, which may effectively lead to selection bias under multiple strategies. This is a well described phenomena by Marcos Lopez de Prado (e.g in his AiML book). Thus, the portfolios will be choosen based on the straightforward and popular heuristic. We will weight positions according to their rank of performance during stressful time. For stressful vulnerable stocks the same rule apply inversely (the worse performance, the higher weight).

Our strategy will give us 4 portfolios. Big and small market cap of stressfull-stable (SS) and stressfull-vulnerable (SV, worst performing during stressful time). These portfolos as well as some benchmarks are plotted below.

There are 3 additional benchamrks plotted. WIG is a main polish stock index aggregating every stock listed on WSE (comparable to the Wilshire 5000 but popular like S&P500). sWIG80, is an index of 80 small companies on WSE (comparable to the S&P SmallCap 600 Index). Portfolio called “Sample” is equally-weighted portfolio of every stock that we had in our available sample. As we may see by the sample return, compared with WIG and sWIG80, our sample was kind of biased, in a sense that stocks we sampled were most often better than random. But in general, it does not matter that much, since we will compare our strategy with “Sample” benchmark.

First expression about quality factor is very good. Not only stress-stable stocks outperformed general sample and stress-vulnerable stocks but also the order of performance is in line with our priors. Small SS stocks outperformed big ones, which is consistent with well described size anomaly. On the other hand, portfolios that were meant to underperform, i.e. stress-vulnerable, had worse performance than overall sample of our stock universe. Everything seems to be working how it’s supposed to. The only issue is a significant difference between WSE indexes and strategies, which, at least partly, may be explained by survivorship bias of our sample.

We may now take a look at some performance metrics of the strategies.

name | Sharpe ratio | Cumulative return | Annualized return | Annualized volatility | max drawdown |
---|---|---|---|---|---|

small_SS | 1.020 | 9.889 | 0.348 | 0.186 | 0.704 |

big_SS | 0.927 | 5.989 | 0.275 | 0.166 | 0.680 |

sample_ret | 0.852 | 4.377 | 0.234 | 0.157 | 0.695 |

big_SV | 0.607 | 2.659 | 0.176 | 0.181 | 0.716 |

small_SV | 0.331 | 0.846 | 0.080 | 0.186 | 0.749 |

swig | 0.177 | 0.240 | 0.027 | 0.173 | 0.724 |

wig | 0.147 | 0.129 | 0.015 | 0.209 | 0.686 |

Sharpe ratio (performance relative to risk) is also quite good and what is more important is that stress stable portfolios have better sharpe ratio than sample, whereas stress-vulnerable lower. The strategy apparently works, even to the extent that stress-stable portfolio have lower volatility with higher total return, compared to the same market cap stress-vulnerable. We could also try making market neutral portfolio shorting stress-vulnerable but there could be technical issues shorting stocks on WSE.

There are also some quite appealing, technical features of this strategy, in particular, there is only one rebalancing during a year (although, i have not tried doing it more frequently) and one does not need complex instruments to apply it. Perhaps this is why the strategy is not On the other side, we analyzed strategy that held on average 40 stocks, which is quite high amount. Of course, the strategy should also perform good when we lower this number. Still, we could test and validate this strategy for indefinitely long time.

Another conclusion that i got is that, it’s good to read most recent papers from top journals. Alpha’s of strategies come and go, so it’s necessary to be up to date with financial research.

Feel free to contact me in case of any questions or feedback :)

mateuszdadej@gmail.com or twitter

]]>`JuliaCall`

library that enables us to do so. Alternatively, there is also `XRJulia`

library available.
It is necessery to tell R where is Julia.exe stored, so the loaded library can communicate with it accordingly.

Next we will define function that price European options based on Monte Carlo simulation. Namely, simulates random path of prices many times for a given set of parameters and calculates value of option based on expected value of simulated payoffs.

The model of motion of prices will be a following commonly used stochastic differential equation, called geometric Brownian motion:

\(dS_t = \mu S_t dt + \sigma S_t dW_t\) Where $W_t$, $\mu$ and $\sigma$ are respectively a Wiener process, drift and volatility.

Price dynamics model as well as chosen derivative is not very complex. The point of herein document is to compare R and Julia performance on some well-known example. As show below, it is also easy to price such an option with the same price dynamics using a well known workhorse in finance, namely Black-Scholes formula

Now we can check if the function is working properly. To do so, we will define Black-Scholes formula with a following equation for pricing call options:

\(C(S_T, t) = N(d_1)S_t - N(d_2)PV(K)\) \(d_1 = \frac{1}{\sigma \sqrt{T} }\bigg[ln \bigg(\frac{S_t}{K}\bigg) + \bigg(r + \frac{\sigma^2}{2} \bigg)t \bigg]\) \(d_2 = d_1 - \sigma \sqrt{t}\) \(PV(K) = Ke^{-rt}\) Where,

- $N(\cdot)$ is a C.D.F. of a standard normal distribution
- $S_t$ - Current price of underlying asset
- $K$ - Option strike price
- $t$ - time to option expiry
- $r$ - Interest rate of risk-free asset
- $\sigma$ - Returns volatility of underlying asset price

Prices are almost the same. The difference is insignificant and is only related to the number of simulations executed by `MC_option_pricing`

function. Results from this function will converge to the result of analytical function as number of simulations increases.

We will use `julia_command()`

function to define very similar function but in Julia, which can be used in R.

Alternatively, we can import whole script to R.

Next, we can define variables for both of the functions to compute. The most important will be the number of simulations that a particular function have to perform. This will ultimately determine how computationally expensive function will be.

Script for Julia have dependency, so we should also load a `Distributions`

library.

Now we will use both functions to price put option based on 1000000 simulations, each with 100 rows/observations.

Note that Julia is sensitive to object types and sometimes need to have objects specified in explicit way. (use of as.integer() function).

As before, Monte Carlo method have stochastic properities so the price vary a little. That is why, in order to obtain very precise value, it is neccessery to execute many simulations. According to R the price is 11.993 and Julia said it costs 12.011, a difference of 0.018.

The computation took 18.56 seconds for R and 4.97 seconds for Julia, a difference of 13.59 seconds. Julia was 3.734 times faster.

It might be also interesting how long it takes to execute these functions many times but with fewer simulations. Package `microbenchmark`

enables to do it easily.

As we see in the summary, Julia function is on average 2.653 faster than R (by a median it’s x 2.582). Over 100 repeats, none of R executions was faster than the slowest one of Julia.

]]>