Disclaimer
This is not financial advice. This is a technical
exercise in
applied portfolio optimization for cryptocurrency, in
Rstudio.
Introduction
Portfolio Optimization is often applied to stocks.
Today, we will apply this tool to cryptocurrencies, to determine the
best percentage allocation of each selected token. This analysis stems
from Modern Portfolio Theory (MPT), which denotes that the
ideal portfolio exists to provide one
max returns coupled with optimal risk.
A key component of this analysis involves calculating the
Sharpe Ratio.1
The higher the Sharpe Ratio, the higher the
portfolio’s risk-adjusted performance.
Process
We will calculate an optimized portfolio methodically by:
- Picking
10 crypto coins/tokens,downloading, andcleaningthe relevant dataset - Testing a
single portfolioby calculating the requirements for theportfolio returns,risk, andSharpe Ratio - Optimizing a spread of
5000 randomly generated portfolioswith the same requirements - Visualizing the
minimum varianceandtangencyportfolios, along with theefficient frontier - Drawing relevant conclusions
Phase 1: Setting Up the Data
Download & load the relevant packages
library(tidyverse) #to download the data
library(tidyquant) #to download the data
library(plotly) #to create interactive charts
library(timetk) #to manipulate the data series
library(forcats) #to manipulate categorical variables
Choose crypto coins & tokens
I will choose 10 personal preferences:
BitcoinEthereumUSD CoinSolanaPolkadotAvalanchePolygonChainlinkAlgorandMonero
Download the price data
We will capture the time range
from 03 JANUARY 2009 to 01 JULY 2022, for
Bitcoin, Ethereum, USD Coin,
Solana, Polkadot, Avalanche,
Polygon, Chainlink, Algorand, and
Monero. The tq_get() function draws the crypto
coins & token USD valuations from Yahoo Finance.
tick <- c('BTC-USD', 'ETH-USD', 'USDC-USD', 'SOL-USD', 'DOT-USD',
'AVAX-USD', 'MATIC-USD', 'LINK-USD', 'ALGO-USD', 'XMR-USD')
price_data <- tq_get(tick,
from = '2009-01-03',
to = '2022-07-01',
get = 'stock.prices')
knitr::kable(head(price_data))
| symbol | date | open | high | low | close | volume | adjusted |
|---|---|---|---|---|---|---|---|
| BTC-USD | 2014-09-17 | 465.864 | 468.174 | 452.422 | 457.334 | 21056800 | 457.334 |
| BTC-USD | 2014-09-18 | 456.860 | 456.860 | 413.104 | 424.440 | 34483200 | 424.440 |
| BTC-USD | 2014-09-19 | 424.103 | 427.835 | 384.532 | 394.796 | 37919700 | 394.796 |
| BTC-USD | 2014-09-20 | 394.673 | 423.296 | 389.883 | 408.904 | 36863600 | 408.904 |
| BTC-USD | 2014-09-21 | 408.085 | 412.426 | 393.181 | 398.821 | 26580100 | 398.821 |
| BTC-USD | 2014-09-22 | 399.100 | 406.916 | 397.130 | 402.152 | 24127600 | 402.152 |
Calculate the daily returns for these stocks, using the
logarithmic (continuous) returns
log_ret_tidy <- price_data %>%
group_by(symbol) %>%
tq_transmute(select = adjusted,
mutate_fun = periodReturn,
period = 'daily',
col_rename = 'ret',
type = 'log')
Survey the first few rows
knitr::kable(head(log_ret_tidy))
| symbol | date | ret |
|---|---|---|
| BTC-USD | 2014-09-17 | 0.0000000 |
| BTC-USD | 2014-09-18 | -0.0746434 |
| BTC-USD | 2014-09-19 | -0.0724015 |
| BTC-USD | 2014-09-20 | 0.0351112 |
| BTC-USD | 2014-09-21 | -0.0249677 |
| BTC-USD | 2014-09-22 | 0.0083174 |
Use the spread() function to convert the tidy format to
a wide format, and convert it into a time series object using the
xts() function
log_ret_xts <- log_ret_tidy %>%
spread(symbol, value = ret) %>%
tk_xts()
knitr::kable(head(log_ret_xts))
| ALGO-USD | AVAX-USD | BTC-USD | DOT-USD | ETH-USD | LINK-USD | MATIC-USD | SOL-USD | USDC-USD | XMR-USD |
|---|---|---|---|---|---|---|---|---|---|
| NA | NA | 0.000000000 | NA | NA | NA | NA | NA | NA | NA |
| NA | NA | -0.074643352 | NA | NA | NA | NA | NA | NA | NA |
| NA | NA | -0.072401507 | NA | NA | NA | NA | NA | NA | NA |
| NA | NA | 0.035111240 | NA | NA | NA | NA | NA | NA | NA |
| NA | NA | -0.024967660 | NA | NA | NA | NA | NA | NA | NA |
| NA | NA | 0.008317417 | NA | NA | NA | NA | NA | NA | NA |
Replace “NA” values with “0”
There are NA (or alternatively null) values
present due to the lack of value captured at those time-date groups
(i.e. the coin or token was not in existence at the time)
log_ret_xts[is.na(log_ret_xts)] <- 0
knitr::kable(head(log_ret_xts))
| ALGO-USD | AVAX-USD | BTC-USD | DOT-USD | ETH-USD | LINK-USD | MATIC-USD | SOL-USD | USDC-USD | XMR-USD |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0.000000000 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | -0.074643352 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | -0.072401507 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 0.035111240 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | -0.024967660 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 0.008317417 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Phase 2: Testing a Single Portfolio
To calculate portfolio returns, risk (standard deviation), and the Sharpe Ratio, the required factors are:
Mean asset (daily) returnsCovariance matrix of all assetsRandom weights
Calculate the mean daily returns for each asset
mean_ret <- colMeans(log_ret_xts)
knitr::kable(round(mean_ret, 5))
| x | |
|---|---|
| ALGO-USD | -0.00083 |
| AVAX-USD | 0.00042 |
| BTC-USD | 0.00131 |
| DOT-USD | 0.00030 |
| ETH-USD | 0.00042 |
| LINK-USD | 0.00116 |
| MATIC-USD | 0.00163 |
| SOL-USD | 0.00124 |
| USDC-USD | 0.00000 |
| XMR-USD | -0.00002 |
Calculate the covariance matrix for all crypto coins & tokens,
and annualize by multiplying by 252
cov_mat <- cov(log_ret_xts) * 252
knitr::kable(round(cov_mat,4))
| ALGO-USD | AVAX-USD | BTC-USD | DOT-USD | ETH-USD | LINK-USD | MATIC-USD | SOL-USD | USDC-USD | XMR-USD | |
|---|---|---|---|---|---|---|---|---|---|---|
| ALGO-USD | 0.5081 | 0.1857 | 0.1657 | 0.1651 | 0.2351 | 0.3091 | 0.3169 | 0.1910 | -0.0021 | 0.2170 |
| AVAX-USD | 0.1857 | 0.3839 | 0.1002 | 0.1604 | 0.1433 | 0.1905 | 0.2290 | 0.1998 | 0.0002 | 0.1319 |
| BTC-USD | 0.1657 | 0.1002 | 0.3868 | 0.0929 | 0.2531 | 0.2560 | 0.1917 | 0.0939 | -0.0018 | 0.2545 |
| DOT-USD | 0.1651 | 0.1604 | 0.0929 | 0.2988 | 0.1377 | 0.1852 | 0.1758 | 0.1515 | -0.0002 | 0.1243 |
| ETH-USD | 0.2351 | 0.1433 | 0.2531 | 0.1377 | 0.4065 | 0.3743 | 0.2716 | 0.1618 | -0.0026 | 0.3238 |
| LINK-USD | 0.3091 | 0.1905 | 0.2560 | 0.1852 | 0.3743 | 0.8008 | 0.3356 | 0.2075 | -0.0023 | 0.3418 |
| MATIC-USD | 0.3169 | 0.2290 | 0.1917 | 0.1758 | 0.2716 | 0.3356 | 0.7912 | 0.2202 | -0.0028 | 0.2493 |
| SOL-USD | 0.1910 | 0.1998 | 0.0939 | 0.1515 | 0.1618 | 0.2075 | 0.2202 | 0.4826 | -0.0001 | 0.1399 |
| USDC-USD | -0.0021 | 0.0002 | -0.0018 | -0.0002 | -0.0026 | -0.0023 | -0.0028 | -0.0001 | 0.0019 | -0.0020 |
| XMR-USD | 0.2170 | 0.1319 | 0.2545 | 0.1243 | 0.3238 | 0.3418 | 0.2493 | 0.1399 | -0.0020 | 0.4851 |
Calculate and check the random weights2
wts <- runif(n = length(tick))
wts <- wts/sum(wts)
knitr::kable(wts)
| x |
|---|
| 0.0738931 |
| 0.0176051 |
| 0.1158264 |
| 0.1405402 |
| 0.0245540 |
| 0.0110357 |
| 0.0966279 |
| 0.1312985 |
| 0.1534317 |
| 0.2351875 |
Calculate and check the portfolio returns
port_returns <- (sum(wts * mean_ret) + 1)^252 - 1
knitr::kable(port_returns)
| x |
|---|
| 0.1286352 |
Calculate and check the portfolio risk
port_risk <- sqrt(t(wts) %*% (cov_mat %*% wts))
knitr::kable(port_risk)
| 0.4071548 |
Calculate and check the Sharpe Ratio3
sharpe_ratio <- port_returns/port_risk
knitr::kable(sharpe_ratio)
| 0.3159367 |
Phase 3: Optimizing with 5000 Portfolios
Optimize by utilizing
5000 random portfolios and requiring a for
loop
num_port <- 5000
Create a matrix to store the weights
all_wts <- matrix(nrow = num_port,
ncol = length(tick))
Create an empty vector to store the
Portfolio returns
port_returns <- vector('numeric', length = num_port)
Create an empty vector to store the
Portfolio Standard deviation
port_risk <- vector('numeric', length = num_port)
Create an empty vector to store the
Portfolio Sharpe Ratio
sharpe_ratio <- vector('numeric', length = num_port)
Run the for loop 5000 times and be sure to use the
set.seed() function for reproducibility
set.seed(1)
for (i in seq_along(port_returns)) {
wts <- runif(length(tick))
wts <- wts/sum(wts)
#store the weights in the matrix
all_wts[i,] <- wts
#portfolio returns
port_ret <- sum(wts * mean_ret)
port_ret <- ((port_ret + 1)^252) - 1
#store the Portfolio Returns values
port_returns[i] <- port_ret
#create and store portfolio risk
port_sd <- sqrt(t(wts) %*% (cov_mat %*% wts))
port_risk[i] <- port_sd
#create and store Portfolio Sharpe Ratios
#assume a 0% Risk free rate
sr <- port_ret/port_sd
sharpe_ratio[i] <- sr
}
Phase 4: Visualizing the Data
Create a data table to store all of the values together
portfolio_values <- tibble(Return = port_returns,
Risk = port_risk,
SharpeRatio = sharpe_ratio)
Coerce the time series objects to tibble objects, then change the column names
all_wts <- tk_tbl(all_wts)
colnames(all_wts) <- colnames(log_ret_xts)
Combine all of the values together
portfolio_values <- tk_tbl(cbind(all_wts, portfolio_values))
#inspect the first few values
knitr::kable(head(portfolio_values))
| ALGO-USD | AVAX-USD | BTC-USD | DOT-USD | ETH-USD | LINK-USD | MATIC-USD | SOL-USD | USDC-USD | XMR-USD | Return | Risk | SharpeRatio |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.0481418 | 0.0674732 | 0.1038693 | 0.1646754 | 0.0365688 | 0.1628952 | 0.1712877 | 0.1198153 | 0.1140704 | 0.0112030 | 0.2252936 | 0.4514293 | 0.4990673 |
| 0.0368588 | 0.0315945 | 0.1229416 | 0.0687347 | 0.1377619 | 0.0890625 | 0.1284167 | 0.1775000 | 0.0680067 | 0.1391226 | 0.2092005 | 0.4691999 | 0.4458665 |
| 0.2234411 | 0.0507126 | 0.1557825 | 0.0300139 | 0.0638790 | 0.0923005 | 0.0032010 | 0.0914097 | 0.2078994 | 0.0813603 | 0.0788695 | 0.3990047 | 0.1976657 |
| 0.0910545 | 0.1132450 | 0.0932193 | 0.0351725 | 0.1562729 | 0.1262589 | 0.1500147 | 0.0203882 | 0.1366933 | 0.0776808 | 0.1590890 | 0.4569036 | 0.3481893 |
| 0.1357237 | 0.1069758 | 0.1294391 | 0.0914312 | 0.0875764 | 0.1305011 | 0.0038573 | 0.0788985 | 0.1210705 | 0.1145265 | 0.1116775 | 0.4304285 | 0.2594565 |
| 0.1166175 | 0.2102762 | 0.1069675 | 0.0597707 | 0.0172573 | 0.0242860 | 0.0772221 | 0.1266318 | 0.1616377 | 0.0993332 | 0.1248760 | 0.3951751 | 0.3160018 |
Look at the portfolios that matter the most
Minimum variance portfolio: maximized returns with minimized riskTangency portfolio: the allocation with highest Sharpe Ratio
min_var <- portfolio_values[which.min(portfolio_values$Risk),]
max_sr <- portfolio_values[which.max(portfolio_values$SharpeRatio),]
Plot the weights of each portfolio
Minimum variance portfolio
p <- min_var %>%
gather(`ALGO-USD`:`XMR-USD`, key = Asset,
value = Weights) %>%
mutate(Asset = as.factor(Asset)) %>%
ggplot(aes(x = fct_reorder(Asset,Weights), y = Weights, fill = Asset)) +
geom_bar(stat = 'identity') +
theme_minimal() +
labs(x = 'Assets', y = 'Weights', title = "Minimum Variance Portfolio Weights") +
scale_y_continuous(labels = scales::percent)
ggplotly(p, height = 550, width = 1000)
Tangency portfolio
p <- max_sr %>%
gather(`ALGO-USD`:`XMR-USD`, key = Asset,
value = Weights) %>%
mutate(Asset = as.factor(Asset)) %>%
ggplot(aes(x = fct_reorder(Asset,Weights), y = Weights, fill = Asset)) +
geom_bar(stat = 'identity') +
theme_minimal() +
labs(x = 'Assets', y = 'Weights', title = "Tangency Portfolio Weights") +
scale_y_continuous(labels = scales::percent)
ggplotly(p, height = 550, width = 1000)
Plot all the random portfolios and visualize the
efficient frontier
p <- portfolio_values %>%
ggplot(aes(x = Risk, y = Return, color = SharpeRatio)) +
geom_point() +
theme_classic() +
scale_y_continuous(labels = scales::percent) +
scale_x_continuous(labels = scales::percent) +
labs(x = 'Annualized Risk',
y = 'Annualized Returns',
title = "Portfolio Optimization & Efficient Frontier") +
geom_point(aes(x = Risk,
y = Return), data = min_var, color = 'red') +
geom_point(aes(x = Risk,
y = Return), data = max_sr, color = 'red') +
annotate('text', x = 0.32, y = 0.26, label = "Tangency Portfolio") +
annotate('text', x = 0.21, y = 0.13, label = "Minimum variance portfolio") +
annotate(geom = 'segment', x = 0.00, xend = 0.00, y = 0.00,
yend = 0.00, color = 'red', arrow = arrow(type = "open")) +
annotate(geom = 'segment', x = 0.00, xend = 0.00, y = 0.00,
yend = 0.00, color = 'red', arrow = arrow(type = "open"))
ggplotly(p, height = 500, width = 800)
Conclusion
From a profile of 5000 randomly generated profiles…
The minimum variance portfolio serves the needs of a slightly more conservative investor with allocations at the listed percentage allocations, with a lower annualized risk.
The tangency portfolio serves a slightly more risk-on investor with allocations at the listed percentage allocations, with a higher estimate of annualized returns.
Within the context of the cryptocurrency market, the annualized risk is high for both portfolio allocations. One month in the cryptocurrency market can be dynamic due to its volatility. As it is a nascent market, it may be prudent to reevaluate a crypto portfolio quarterly.
The
Sharpe Ratiosof the minimum variance and tangency portfolios range between0.37 to 0.64. According to certain sources, the typicalSharpe Ratioof an “actively managed mutual fund” is0.25, and theSharpe Ratiofor a “diversified portfolio of stock and bond ETFs” is anywhere between0.5-0.75.4 As of today, cryptocurrency classifies as anintangible asset.5 Thus, one may determine that the aforementionedSharpe Ratiorange, based on a full allocation to a “diversified” cryptocurrency portfolio, may compare to a classical mix of securities including stocks and bonds.6
For any questions or comments on this analysis please contact kris.kilgroe@sigmacode.io
Session Info
sessionInfo()
#> R version 4.2.1 (2022-06-23)
#> Platform: x86_64-pc-linux-gnu (64-bit)
#> Running under: Linux Mint 20.3
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/blas/libblas.so.3.9.0
#> LAPACK: /usr/lib/x86_64-linux-gnu/lapack/liblapack.so.3.9.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] timetk_2.8.1 plotly_4.10.0
#> [3] tidyquant_1.0.4 quantmod_0.4.20
#> [5] TTR_0.24.3 PerformanceAnalytics_2.0.4
#> [7] xts_0.12.1 zoo_1.8-10
#> [9] lubridate_1.8.0 forcats_0.5.1
#> [11] stringr_1.4.0 dplyr_1.0.9
#> [13] purrr_0.3.4 readr_2.1.2
#> [15] tidyr_1.2.0 tibble_3.1.7
#> [17] ggplot2_3.3.6 tidyverse_1.3.1
#>
#> loaded via a namespace (and not attached):
#> [1] fs_1.5.2 httr_1.4.3 tools_4.2.1 backports_1.4.1
#> [5] bslib_0.3.1 utf8_1.2.2 R6_2.5.1 rpart_4.1.16
#> [9] DBI_1.1.3 lazyeval_0.2.2 colorspace_2.0-3 nnet_7.3-17
#> [13] withr_2.5.0 tidyselect_1.1.2 curl_4.3.2 compiler_4.2.1
#> [17] cli_3.3.0 rvest_1.0.2 xml2_1.3.3 labeling_0.4.2
#> [21] bookdown_0.27 sass_0.4.1 scales_1.2.0 quadprog_1.5-8
#> [25] digest_0.6.29 rmarkdown_2.14 pkgconfig_2.0.3 htmltools_0.5.2
#> [29] parallelly_1.32.0 highr_0.9 dbplyr_2.2.0 fastmap_1.1.0
#> [33] htmlwidgets_1.5.4 rlang_1.0.2 readxl_1.4.0 rstudioapi_0.13
#> [37] farver_2.1.0 jquerylib_0.1.4 generics_0.1.2 jsonlite_1.8.0
#> [41] crosstalk_1.2.0 magrittr_2.0.3 Matrix_1.4-1 Rcpp_1.0.8.3
#> [45] Quandl_2.11.0 munsell_0.5.0 fansi_1.0.3 furrr_0.3.0
#> [49] lifecycle_1.0.1 stringi_1.7.6 yaml_2.3.5 MASS_7.3-57
#> [53] recipes_0.2.0 grid_4.2.1 parallel_4.2.1 listenv_0.8.0
#> [57] crayon_1.5.1 lattice_0.20-45 splines_4.2.1 haven_2.5.0
#> [61] hms_1.1.1 knitr_1.39 pillar_1.7.0 codetools_0.2-18
#> [65] future.apply_1.9.0 reprex_2.0.1 glue_1.6.2 evaluate_0.15
#> [69] rsample_0.1.1 data.table_1.14.2 modelr_0.1.8 vctrs_0.4.1
#> [73] rmdformats_1.0.4 tzdb_0.3.0 cellranger_1.1.0 gtable_0.3.0
#> [77] future_1.26.1 assertthat_0.2.1 xfun_0.31 gower_1.0.0
#> [81] prodlim_2019.11.13 broom_0.8.0 class_7.3-20 survival_3.2-13
#> [85] viridisLite_0.4.0 timeDate_3043.102 hardhat_1.1.0 lava_1.6.10
#> [89] globals_0.15.0 ellipsis_0.3.2 ipred_0.9-13
Footnotes
The
Sharpe RatioDefined: https://news.morningstar.com/classroom2/course.asp?docId=2932&page=4↩︎Dividing the weights by the sum of weights ensures the correct ratio of percentages (all adding to 1.00 or 100%).↩︎
Assume a 0% risk free rate to calculate the Sharpe Ratio. The generic formula is:
Sharpe Ratio= (Rx – Rf) / StdDev Rx, whereRx= expected portfolio return,Rf= Risk-free rate of return, &StdDev Rx= the portfolio standard deviation (or volatility/risk); Link: https://corporatefinanceinstitute.com/resources/knowledge/finance/sharpe-ratio-definition-formula/↩︎Your Sharpe Ratio Is Low For The Same Reasons You’re Bad At Golf: https://seekingalpha.com/article/4239442-your-sharpe-ratio-is-low-for-reasons-bad-golf↩︎
Accounting for cryptocurrencies: https://www.accaglobal.com/in/en/student/exam-support-resources/professional-exams-study-resources/strategic-business-reporting/technical-articles/cryptocurrencies.html↩︎
Again, this is not financial advice.↩︎