From 6bab00ae8d3fd41985bef29a499571d151b26127 Mon Sep 17 00:00:00 2001 From: Bernardo Reckziegel Date: Wed, 16 Aug 2023 19:22:46 -0300 Subject: [PATCH] typos --- .Rbuildignore | 1 + CRAN-SUBMISSION | 3 ++ R/epo.R | 74 +++++++++++++++++++------------ README.Rmd | 14 +++--- README.md | 15 ++++--- man/epo.Rd | 93 ++++++++++++++++++++++++++++++--------- tests/testthat/test-epo.R | 22 ++++----- 7 files changed, 150 insertions(+), 72 deletions(-) create mode 100644 CRAN-SUBMISSION diff --git a/.Rbuildignore b/.Rbuildignore index b096fe4..490668e 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -8,3 +8,4 @@ ^\.github$ ^codecov\.yml$ ^cran-comments\.md$ +^CRAN-SUBMISSION$ diff --git a/CRAN-SUBMISSION b/CRAN-SUBMISSION new file mode 100644 index 0000000..cc2617c --- /dev/null +++ b/CRAN-SUBMISSION @@ -0,0 +1,3 @@ +Version: 0.1.0 +Date: 2023-07-27 21:45:01 UTC +SHA: bad549aabedebd983d9fc00884262e14bb5d091e diff --git a/R/epo.R b/R/epo.R index aa6255c..67d225c 100644 --- a/R/epo.R +++ b/R/epo.R @@ -1,16 +1,21 @@ #' Enhanced Portfolio Optimization (EPO) #' -#' Computes the optimal portfolio allocation using the EPO method with full-investment -#' constraint. +#' Computes the optimal portfolio allocation using the EPO method. #' #' @param x A data-set with asset returns. It should be a \code{tibble}, a \code{xts} #' or a \code{matrix}. -#' @param signal A \code{double} with the investor's belief's (signals, forecasts). -#' @param method A \code{character}. One of: `simple` or `anchored`. +#' @param signal A \code{double} vector with the investor's belief's (signals, forecasts). +#' @param lambda A \code{double} with the investor's risk-aversion preference. +#' @param method A \code{character}. One of: `"simple"` or `"anchored"`. #' @param w A \code{double} between \code{0} and \code{1}. The shrinkage level #' increases from 0 to 1. -#' @param anchor A \code{double} with the investor anchor (benchmark), in which +#' @param anchor A \code{double} vector with the anchor (benchmark) in which #' the allocation should not deviate too much from. Only used when `method = "anchored"`. +#' @param normalize A \code{boolean} indicating whether the allocation should be +#' normalized to sum \code{1} (full-investment constraint). The default is `normalize = TRUE`. +#' @param endogenous A \code{boolean} indicating whether the risk-aversion parameter +#' should be considered endogenous (only used when `method = "anchored"`). +#' The default is `endogenous = TRUE`. #' #' @return The optimal allocation vector. #' @export @@ -24,13 +29,13 @@ #' ################## #' #' # Traditional Mean-Variance Analysis -#' epo(x = x, signal = s, method = "simple", w = 0) +#' epo(x = x, signal = s, lambda = 10, method = "simple", w = 0) #' #' # 100% Shrinkage -#' epo(x = x, signal = s, method = "simple", w = 1) +#' epo(x = x, signal = s, lambda = 10, method = "simple", w = 1) #' #' # 50% Classical MVO and 50% Shrinkage -#' epo(x = x, signal = s, method = "simple", w = 0.5) +#' epo(x = x, signal = s, lambda = 10, method = "simple", w = 0.5) #' #' #################### #' ### Anchored EPO ### @@ -39,14 +44,14 @@ #' benchmark <- rep(0.25, 4) # 1/N Portfolio #' #' # Traditional Mean-Variance Analysis -#' epo(x = x, signal = s, method = "anchored", w = 0.0, anchor = benchmark) +#' epo(x = x, signal = s, lambda = 10, method = "anchored", w = 0.0, anchor = benchmark) #' #' # 100% on the Anchor portfolio -#' epo(x = x, signal = s, method = "anchored", w = 1.0, anchor = benchmark) +#' epo(x = x, signal = s, lambda = 10, method = "anchored", w = 1.0, anchor = benchmark) #' #' # Somewhere between the two worlds -#' epo(x = x, signal = s, method = "anchored", w = 0.5, anchor = benchmark) -epo <- function(x, signal, method = c("simple", "anchored"), w, anchor = NULL) { +#' epo(x = x, signal = s, lambda = 10, method = "anchored", w = 0.5, anchor = benchmark) +epo <- function(x, signal, lambda, method = c("simple", "anchored"), w, anchor = NULL, normalize = TRUE, endogenous = TRUE) { UseMethod("epo", x) @@ -54,7 +59,7 @@ epo <- function(x, signal, method = c("simple", "anchored"), w, anchor = NULL) { #' @rdname epo #' @export -epo.default <- function(x, signal, method = c("simple", "anchored"), w, anchor = NULL) { +epo.default <- function(x, signal, lambda, method = c("simple", "anchored"), w, anchor = NULL, normalize = TRUE, endogenous = TRUE) { rlang::abort("`x` must be a tibble, xts or a matrix.") @@ -62,34 +67,36 @@ epo.default <- function(x, signal, method = c("simple", "anchored"), w, anchor = #' @rdname epo #' @export -epo.tbl <- function(x, signal, method = c("simple", "anchored"), w, anchor = NULL) { +epo.tbl <- function(x, signal, lambda, method = c("simple", "anchored"), w, anchor = NULL, normalize = TRUE, endogenous = TRUE) { - epo_(x = tbl_to_mtx(x), signal = signal, method = method, w = w, anchor = anchor) + epo_(x = tbl_to_mtx(x), signal = signal, lambda = lambda, method = method, w = w, anchor = anchor, normalize = normalize, endogenous = endogenous) } #' @rdname epo #' @export -epo.xts <- function(x, signal, method = c("simple", "anchored"), w, anchor = NULL) { +epo.xts <- function(x, signal, lambda, method = c("simple", "anchored"), w, anchor = NULL, normalize = TRUE, endogenous = TRUE) { - epo_(x = as.matrix(x), signal = signal, method = method, w = w, anchor = anchor) + epo_(x = as.matrix(x), signal = signal, lambda = lambda, method = method, w = w, anchor = anchor, normalize = normalize, endogenous = endogenous) } #' @rdname epo #' @export -epo.matrix <- function(x, signal, method = c("simple", "anchored"), w, anchor = NULL) { +epo.matrix <- function(x, signal, lambda, method = c("simple", "anchored"), w, anchor = NULL, normalize = TRUE, endogenous = TRUE) { - epo_(x = x, signal = signal, method = method, w = w, anchor = anchor) + epo_(x = x, signal = signal, lambda = lambda, method = method, w = w, anchor = anchor, normalize = normalize, endogenous = endogenous) } #' @keywords internal -epo_ <- function(x, signal, method = c("simple", "anchored"), w, anchor = NULL) { +epo_ <- function(x, signal, lambda, method = c("simple", "anchored"), w, anchor = NULL, normalize = TRUE, endogenous = TRUE) { # Error Handling assertthat::assert_that(assertthat::is.string(method)) + assertthat::assert_that(assertthat::is.number(lambda)) assertthat::assert_that(assertthat::is.number(w)) + assertthat::assert_that(assertthat::is.flag(normalize)) if (NCOL(signal) > 1) { signal <- matrix(signal) @@ -127,15 +134,23 @@ epo_ <- function(x, signal, method = c("simple", "anchored"), w, anchor = NULL) # The simple EPO if (method == "simple") { - .epo <- inv_shrunk_cov %*% signal # equation 16 + .epo <- (1 / lambda) * inv_shrunk_cov %*% signal # equation 16 # The anchored EPO } else if (method == "anchored") { - # footnote 13 - .gamma <- c((sqrt(t(a) %*% cov_tilde %*% a) / sqrt(t(s) %*% inv_shrunk_cov %*% cov_tilde %*% inv_shrunk_cov %*% s))) - # equation 17 - .epo <- inv_shrunk_cov %*% (((1 - w) * .gamma * s) + ((w * I %*% V %*% a))) + if (endogenous) { + + # footnote 13 + .gamma <- c((sqrt(t(a) %*% cov_tilde %*% a) / sqrt(t(s) %*% inv_shrunk_cov %*% cov_tilde %*% inv_shrunk_cov %*% s))) + # equation 17 + .epo <- inv_shrunk_cov %*% (((1 - w) * .gamma * s) + ((w * I %*% V %*% a))) + + } else { + + .epo <- inv_shrunk_cov %*% (((1 - w) * (1 / lambda) * s) + ((w * I %*% V %*% a))) + + } # Error } else { @@ -144,8 +159,13 @@ epo_ <- function(x, signal, method = c("simple", "anchored"), w, anchor = NULL) } - # Full-Investment constraint - .epo <- as.double(.epo / sum(.epo)) + if (normalize) { + + # Force full-investment constraint + .epo <- as.double(.epo / sum(.epo)) + + } + .epo } diff --git a/README.Rmd b/README.Rmd index 71e30ae..d3b3c58 100644 --- a/README.Rmd +++ b/README.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( [![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental) [![R-CMD-check](https://github.com/Reckziegel/epo/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/Reckziegel/epo/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/Reckziegel/epo/branch/main/graph/badge.svg)](https://app.codecov.io/gh/Reckziegel/epo?branch=main) - +[![CRAN status](https://www.r-pkg.org/badges/version/epo)](https://CRAN.R-project.org/package=epo) The Enhanced Portfolio Optimization (EPO) method, described in Pedersen, Babu and Levine (2021), proposes a unifying theory on portfolio optimization. Employing Principal Component Analysis (PCA), the EPO method ranks portfolios based on their variance, from the most to the least important principal components. Notably, the least important principal components emerge as "problem portfolios", primarily due to their low *estimated* risk, leading to the underestimation of their *true* risks. These portfolios offer high expected returns (*ex-ante*) and low realized Sharpe Ratios (*ex-post*), underscoring the challenges faced when using them through standard approaches. @@ -59,13 +59,13 @@ s <- colMeans(x) # it could be any signal ################## # Traditional Mean-Variance Analysis -epo(x = x, signal = s, method = "simple", w = 0) +epo(x = x, signal = s, lambda = 10, method = "simple", w = 0) # 100% Shrinkage -epo(x = x, signal = s, method = "simple", w = 1) +epo(x = x, signal = s, lambda = 10, method = "simple", w = 1) # 50% Classical MVO and 50% Shrinkage -epo(x = x, signal = s, method = "simple", w = 0.5) +epo(x = x, signal = s, lambda = 10, method = "simple", w = 0.5) #################### ### The Anchored EPO @@ -74,13 +74,13 @@ epo(x = x, signal = s, method = "simple", w = 0.5) benchmark <- rep(0.25, 4) # 1/N Portfolio # Traditional Mean-Variance Analysis -epo(x = x, signal = s, method = "anchored", w = 0.0, anchor = benchmark) +epo(x = x, signal = s, lambda = 10, method = "anchored", w = 0.0, anchor = benchmark) # 100% on the Anchor portfolio -epo(x = x, signal = s, method = "anchored", w = 1.0, anchor = benchmark) +epo(x = x, signal = s, lambda = 10, method = "anchored", w = 1.0, anchor = benchmark) # 50% on Mean-Variance Analysis and 50% on the Anchor Portfolio -epo(x = x, signal = s, method = "anchored", w = 0.5, anchor = benchmark) +epo(x = x, signal = s, lambda = 10, method = "anchored", w = 0.5, anchor = benchmark) ``` ## References diff --git a/README.md b/README.md index 3f36c0c..65ea446 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](h [![R-CMD-check](https://github.com/Reckziegel/epo/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/Reckziegel/epo/actions/workflows/R-CMD-check.yaml) [![Codecov test coverage](https://codecov.io/gh/Reckziegel/epo/branch/main/graph/badge.svg)](https://app.codecov.io/gh/Reckziegel/epo?branch=main) - +[![CRAN +status](https://www.r-pkg.org/badges/version/epo)](https://CRAN.R-project.org/package=epo) The Enhanced Portfolio Optimization (EPO) method, described in Pedersen, @@ -87,15 +88,15 @@ s <- colMeans(x) # it could be any signal ################## # Traditional Mean-Variance Analysis -epo(x = x, signal = s, method = "simple", w = 0) +epo(x = x, signal = s, lambda = 10, method = "simple", w = 0) #> [1] 0.1914569 0.9894828 -0.3681779 0.1872382 # 100% Shrinkage -epo(x = x, signal = s, method = "simple", w = 1) +epo(x = x, signal = s, lambda = 10, method = "simple", w = 1) #> [1] 0.2352863 0.3659986 0.1375249 0.2611902 # 50% Classical MVO and 50% Shrinkage -epo(x = x, signal = s, method = "simple", w = 0.5) +epo(x = x, signal = s, lambda = 10, method = "simple", w = 0.5) #> [1] 0.223281853 0.564005906 -0.009868083 0.222580324 #################### @@ -105,15 +106,15 @@ epo(x = x, signal = s, method = "simple", w = 0.5) benchmark <- rep(0.25, 4) # 1/N Portfolio # Traditional Mean-Variance Analysis -epo(x = x, signal = s, method = "anchored", w = 0.0, anchor = benchmark) +epo(x = x, signal = s, lambda = 10, method = "anchored", w = 0.0, anchor = benchmark) #> [1] 0.1914569 0.9894828 -0.3681779 0.1872382 # 100% on the Anchor portfolio -epo(x = x, signal = s, method = "anchored", w = 1.0, anchor = benchmark) +epo(x = x, signal = s, lambda = 10, method = "anchored", w = 1.0, anchor = benchmark) #> [1] 0.25 0.25 0.25 0.25 # 50% on Mean-Variance Analysis and 50% on the Anchor Portfolio -epo(x = x, signal = s, method = "anchored", w = 0.5, anchor = benchmark) +epo(x = x, signal = s, lambda = 10, method = "anchored", w = 0.5, anchor = benchmark) #> [1] 0.2374674 0.4557503 0.1004711 0.2063111 ``` diff --git a/man/epo.Rd b/man/epo.Rd index e929d2a..214ee57 100644 --- a/man/epo.Rd +++ b/man/epo.Rd @@ -8,36 +8,89 @@ \alias{epo.matrix} \title{Enhanced Portfolio Optimization (EPO)} \usage{ -epo(x, signal, method = c("simple", "anchored"), w, anchor = NULL) - -\method{epo}{default}(x, signal, method = c("simple", "anchored"), w, anchor = NULL) - -\method{epo}{tbl}(x, signal, method = c("simple", "anchored"), w, anchor = NULL) - -\method{epo}{xts}(x, signal, method = c("simple", "anchored"), w, anchor = NULL) - -\method{epo}{matrix}(x, signal, method = c("simple", "anchored"), w, anchor = NULL) +epo( + x, + signal, + lambda, + method = c("simple", "anchored"), + w, + anchor = NULL, + normalize = TRUE, + endogenous = TRUE +) + +\method{epo}{default}( + x, + signal, + lambda, + method = c("simple", "anchored"), + w, + anchor = NULL, + normalize = TRUE, + endogenous = TRUE +) + +\method{epo}{tbl}( + x, + signal, + lambda, + method = c("simple", "anchored"), + w, + anchor = NULL, + normalize = TRUE, + endogenous = TRUE +) + +\method{epo}{xts}( + x, + signal, + lambda, + method = c("simple", "anchored"), + w, + anchor = NULL, + normalize = TRUE, + endogenous = TRUE +) + +\method{epo}{matrix}( + x, + signal, + lambda, + method = c("simple", "anchored"), + w, + anchor = NULL, + normalize = TRUE, + endogenous = TRUE +) } \arguments{ \item{x}{A data-set with asset returns. It should be a \code{tibble}, a \code{xts} or a \code{matrix}.} -\item{signal}{A \code{double} with the investor's belief's (signals, forecasts).} +\item{signal}{A \code{double} vector with the investor's belief's (signals, forecasts).} -\item{method}{A \code{character}. One of: \code{simple} or \code{anchored}.} +\item{lambda}{A \code{double} with the investor's risk-aversion preference.} + +\item{method}{A \code{character}. One of: \code{"simple"} or \code{"anchored"}.} \item{w}{A \code{double} between \code{0} and \code{1}. The shrinkage level increases from 0 to 1.} -\item{anchor}{A \code{double} with the investor anchor (benchmark), in which +\item{anchor}{A \code{double} vector with the anchor (benchmark) in which the allocation should not deviate too much from. Only used when \code{method = "anchored"}.} + +\item{normalize}{A \code{boolean} indicating whether the allocation should be +normalized to sum \code{1} (full-investment constraint). The default is \code{normalize = TRUE}.} + +\item{endogenous}{A \code{boolean} indicating whether the risk-aversion parameter +should be considered endogenous (only used when \code{method = "anchored"}). +The default is \code{endogenous = TRUE}.} } \value{ The optimal allocation vector. } \description{ -Computes the optimal portfolio allocation using the EPO method with full-investment -constraint. +Computes the optimal portfolio allocation using the EPO method. } \examples{ x <- diff(log(EuStockMarkets)) # stock returns @@ -48,13 +101,13 @@ s <- colMeans(x) # it could be any signal ################## # Traditional Mean-Variance Analysis -epo(x = x, signal = s, method = "simple", w = 0) +epo(x = x, signal = s, lambda = 10, method = "simple", w = 0) # 100\% Shrinkage -epo(x = x, signal = s, method = "simple", w = 1) +epo(x = x, signal = s, lambda = 10, method = "simple", w = 1) # 50\% Classical MVO and 50\% Shrinkage -epo(x = x, signal = s, method = "simple", w = 0.5) +epo(x = x, signal = s, lambda = 10, method = "simple", w = 0.5) #################### ### Anchored EPO ### @@ -63,11 +116,11 @@ epo(x = x, signal = s, method = "simple", w = 0.5) benchmark <- rep(0.25, 4) # 1/N Portfolio # Traditional Mean-Variance Analysis -epo(x = x, signal = s, method = "anchored", w = 0.0, anchor = benchmark) +epo(x = x, signal = s, lambda = 10, method = "anchored", w = 0.0, anchor = benchmark) # 100\% on the Anchor portfolio -epo(x = x, signal = s, method = "anchored", w = 1.0, anchor = benchmark) +epo(x = x, signal = s, lambda = 10, method = "anchored", w = 1.0, anchor = benchmark) # Somewhere between the two worlds -epo(x = x, signal = s, method = "anchored", w = 0.5, anchor = benchmark) +epo(x = x, signal = s, lambda = 10, method = "anchored", w = 0.5, anchor = benchmark) } diff --git a/tests/testthat/test-epo.R b/tests/testthat/test-epo.R index 2c98226..2578c39 100644 --- a/tests/testthat/test-epo.R +++ b/tests/testthat/test-epo.R @@ -4,11 +4,11 @@ colnames(x) <- colnames(EuStockMarkets) s <- colMeans(x) # Traditional Mean-Variance Analysis -simple_zero_shrinkage <- epo(x = x, signal = s, method = "simple", w = 0) +simple_zero_shrinkage <- epo(x = x, signal = s, lambda = 1, method = "simple", w = 0) # 100% Shrinkage -simple_full_shrinkage <- epo(x = x, signal = s, method = "simple", w = 1) +simple_full_shrinkage <- epo(x = x, signal = s, lambda = 1, method = "simple", w = 1) # 50% Classical MVO and 50% Shrinkage -simple_half_way <- epo(x = x, signal = s, method = "simple", w = 0.5) +simple_half_way <- epo(x = x, signal = s, lambda = 1, method = "simple", w = 0.5) test_that("Simple EPO works", { @@ -30,11 +30,11 @@ test_that("Simple EPO works", { benchmark <- rep(0.25, 4) # 1/N Portfolio # Traditional Mean-Variance Analysis -anchored_zero_shrinkage <- epo(x = x, signal = s, method = "anchored", w = 0.0, anchor = benchmark) +anchored_zero_shrinkage <- epo(x = x, signal = s, lambda = 1, method = "anchored", w = 0.0, anchor = benchmark) # 100% on the Anchor portfolio -anchored_full_shrinkage <- epo(x = x, signal = s, method = "anchored", w = 1.0, anchor = benchmark) +anchored_full_shrinkage <- epo(x = x, signal = s, lambda = 1, method = "anchored", w = 1.0, anchor = benchmark) # Somewhere between the two worlds -anchored_half_way <- epo(x = x, signal = s, method = "anchored", w = 0.5, anchor = benchmark) +anchored_half_way <- epo(x = x, signal = s, lambda = 1, method = "anchored", w = 0.5, anchor = benchmark) test_that("Anchored EPO works", { @@ -67,20 +67,20 @@ test_that("Simple and Anchored are equal on the extremes", { test_that("`epo` can handle with signals in which ncol() > 1", { expect_equal(anchored_zero_shrinkage, - epo(x = x, signal = t(s), method = "anchored", w = 0, anchor = benchmark)) + epo(x = x, signal = t(s), lambda = 1, method = "anchored", w = 0, anchor = benchmark)) }) test_that("`method only accepts `simple` or `anchored`", { - expect_error(epo(x = x, signal = s, method = "some_new_type", w = 0)) + expect_error(epo(x = x, signal = s, lambda = 1, method = "some_new_type", w = 0)) }) test_that("`anchored` requires and 'anchor'", { - expect_error(epo(x = x, signal = s, method = "anchored", w = 0)) + expect_error(epo(x = x, signal = s, lambda = 1, method = "anchored", w = 0)) }) @@ -89,7 +89,7 @@ test_that("`anchored` requires and 'anchor'", { # tibble x_tbl <- dplyr::as_tibble(x) -epo_tbl <- epo(x = x_tbl, signal = s, method = "anchored", w = 0.5, anchor = benchmark) +epo_tbl <- epo(x = x_tbl, signal = s, lambda = 1, method = "anchored", w = 0.5, anchor = benchmark) test_that("Anchored EPO works", { @@ -105,7 +105,7 @@ index <- seq(Sys.Date(), Sys.Date() + 24, "day") # data xts x_xts <- xts::xts(matrix(data, ncol = 4), order.by = index) -epo_xts <- epo(x = x_xts, signal = s, method = "anchored", w = 0.5, anchor = benchmark) +epo_xts <- epo(x = x_xts, signal = s, lambda = 1, method = "anchored", w = 0.5, anchor = benchmark) test_that("Anchored EPO works", {