1#' mapping function to transform or penalize weights that violate constraints2#'3#' The purpose of the mapping function is to transform a weights vector4#' that does not meet all the constraints into a weights vector that5#' does meet the constraints, if one exists, hopefully with a minimum6#' of transformation.7#'8#' The first step is to test for violation of the constraint. If the constraint9#' is violated, we will apply a transformation such that the weights vector10#' satisfies the constraints. The following constraint types are tested in11#' the mapping function: leverage, box, group, and position limit. The12#' transformation logic is based on code from the random portfolio sample method.13#'14#' If relax=TRUE, we will attempt to relax the constraints if a feasible15#' portfolio could not be formed with an initial call to \code{rp_transform}.16#' We will attempt to relax the constraints up to 5 times. If we do not have a17#' feasible portfolio after attempting to relax the constraints, then we will18#' default to returning the weights vector that violates the constraints.19#'20#' @param weights vector of weights21#' @param portfolio object of class \code{portfolio}22#' @param relax TRUE/FALSE, default FALSE. Enable constraints to be relaxed.23#' @param verbose print error messages for debuggin purposes24#' @param \dots any other passthru parameters25#' @return26#' \describe{27#' \item{weights:}{vector of transformed weights meeting constraints.}28#' \item{min: }{vector of min box constraints that may have been modified if relax=TRUE.}29#' \item{max: }{vector of max box constraints that may have been modified if relax=TRUE.}30#' \item{cLO: }{vector of lower bound group constraints that may have been modified if relax=TRUE.}31#' \item{cUP: }{vector of upper bound group constraints that may have been modified if relax=TRUE.}32#' }33#' @author Ross Bennett34#' @export35fn_map <- function(weights, portfolio, relax=FALSE, verbose=FALSE, ...){36if(!is.portfolio(portfolio)) stop("portfolio passed in is not of class 'portfolio'")3738nassets <- length(portfolio$assets)3940# step 1: Get the constraints out of the portfolio object41constraints <- get_constraints(portfolio)42min_sum <- constraints$min_sum43max_sum <- constraints$max_sum44# rp_transform will rarely find a feasible portfolio if there is not some45# 'wiggle room' between min_sum and max_sum46if((max_sum - min_sum) < 0.02){47min_sum <- min_sum - 0.0148max_sum <- max_sum + 0.0149}5051weight_seq <- portfolio$weight_seq52if(is.null(weight_seq)){53weight_seq <- generatesequence(min=min(constraints$min), max=max(constraints$max), by=0.002)54}55weight_seq <- as.vector(weight_seq)5657min <- constraints$min58max <- constraints$max59groups <- constraints$groups60cLO <- constraints$cLO61cUP <- constraints$cUP62group_pos <- constraints$group_pos63div_target <- constraints$div_target64turnover_target <- constraints$turnover_target65max_pos <- constraints$max_pos66max_pos_long <- constraints$max_pos_long67max_pos_short <- constraints$max_pos_short68leverage <- constraints$leverage69tolerance <- .Machine$double.eps^0.57071# We will modify the weights vector so create a temporary copy72# modified for transformation or to relax constraints73tmp_weights <- weights74tmp_min <- min75tmp_max <- max76tmp_cLO <- cLO77tmp_cUP <- cUP78tmp_max_pos <- max_pos79tmp_max_pos_long <- max_pos_long80tmp_max_pos_short <- max_pos_short81tmp_leverage <- leverage8283# Do we need to step through each constraint type sequentially or can we just84# call rp_transform once now that it has been modified to handle constraint85# types seperately?8687# step 2: check that the vector of weights satisfies the constraints,88# transform weights if constraint is violated89# TRUE if the weights vector is in compliance with the constraints90# FALSE if the weights vector violates the constraint9192# check leverage constraints93if(!is.null(min_sum) & !is.null(max_sum)){94if(!(sum(tmp_weights) >= min_sum & sum(tmp_weights) <= max_sum)){95# Try to transform only considering leverage and box constraints96tmp_weights <- try(rp_transform(w=tmp_weights,97min_sum=min_sum,98max_sum=max_sum,99min_box=tmp_min,100max_box=tmp_max,101groups=NULL,102cLO=NULL,103cUP=NULL,104max_pos=NULL,105group_pos=NULL,106max_pos_long=NULL,107max_pos_short=NULL,108leverage=tmp_leverage,109weight_seq=weight_seq,110max_permutations=500),111silent=TRUE) # FALSE for testing112if(inherits(tmp_weights, "try-error")){113# Default to initial weights114tmp_weights <- weights115} # end try-error recovery116} # end check for leverage constraint violation117} # end check for NULL arguments118119# check box constraints120if(!is.null(tmp_min) & !is.null(tmp_max)){121if(!(all(tmp_weights >= tmp_min) & all(tmp_weights <= tmp_max))){122# Try to transform only considering leverage and box constraints123tmp_weights <- try(rp_transform(w=tmp_weights,124min_sum=min_sum,125max_sum=max_sum,126min_box=tmp_min,127max_box=tmp_max,128groups=NULL,129cLO=NULL,130cUP=NULL,131max_pos=NULL,132group_pos=NULL,133max_pos_long=NULL,134max_pos_short=NULL,135leverage=tmp_leverage,136weight_seq=weight_seq,137max_permutations=500),138silent=TRUE) # FALSE for testing139if(inherits(tmp_weights, "try-error")){140if(verbose) message(tmp_weights)141# Default to initial weights142tmp_weights <- weights143# Try to relax constraints if relax=TRUE144if(relax){145i <- 1146# loop while constraints are violated and relax constraints147# try to relax constraints up to 5 times148while((sum(tmp_weights) < min_sum | sum(tmp_weights) > max_sum | any(tmp_weights < tmp_min) | any(tmp_weights > tmp_max)) & i <= 5){149# check if min is violated150if(any(tmp_weights < tmp_min)){151# Find which elements of min are violated and decrease by a random amount152tmp_min[which(tmp_weights < tmp_min)] <- tmp_min[which(tmp_weights < tmp_min)] - runif(1, 0.01, 0.05)153}154# check if max is violated155if(any(tmp_weights > tmp_max)){156# Find which elements of min are violated and increase by a random amount157tmp_max[which(tmp_weights < tmp_max)] <- tmp_max[which(tmp_weights < tmp_max)] + runif(1, 0.01, 0.05)158}159160# Now try the transformation again161tmp_weights <- try(rp_transform(w=tmp_weights,162min_sum=min_sum,163max_sum=max_sum,164min_box=tmp_min,165max_box=tmp_max,166groups=NULL,167cLO=NULL,168cUP=NULL,169max_pos=NULL,170group_pos=NULL,171max_pos_long=NULL,172max_pos_short=NULL,173leverage=tmp_leverage,174weight_seq=weight_seq,175max_permutations=500),176silent=TRUE) # FALSE for testing177# Default to original weights if this fails again178if(inherits(tmp_weights, "try-error")) tmp_weights <- weights179i <- i + 1180}181# We have a feasible portfolio in terms of min_sum and max_sum,182# but were unable to produce a portfolio that satisfies box constraints183if(isTRUE(all.equal(tmp_weights, weights))){184# reset min and max to their original values and penalize later185tmp_min <- min186tmp_max <- max187}188} # end if(relax) statement189} # end try-error recovery190} # end check for box constraint violation191} # end check for NULL arguments192193# check group constraints194if(!is.null(groups) & !is.null(tmp_cLO) & !is.null(tmp_cUP)){195if(any(group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP, group_pos))){196# Try to transform only considering leverage, box, and group constraints197tmp_weights <- try(rp_transform(w=tmp_weights,198min_sum=min_sum,199max_sum=max_sum,200min_box=tmp_min,201max_box=tmp_max,202groups=groups,203cLO=tmp_cLO,204cUP=tmp_cUP,205max_pos=NULL,206group_pos=group_pos,207max_pos_long=NULL,208max_pos_short=NULL,209leverage=tmp_leverage,210weight_seq=weight_seq,211max_permutations=500),212silent=TRUE) # FALSE for testing213if(inherits(tmp_weights, "try-error")){214if(verbose) message(tmp_weights)215# Default to initial weights216tmp_weights <- weights217# Try to relax constraints if relax=TRUE218if(relax){219i <- 1220# loop while constraints are violated and relax constraints221# Try to relax constraints up to 5 times222while(((sum(tmp_weights) < min_sum | sum(tmp_weights) > max_sum) | (any(tmp_weights < tmp_min) | any(tmp_weights > tmp_max)) | any(group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP, group_pos))) & i <= 5){223if(any(group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP, group_pos))){224# I know which group failed, but not if it was cUP or cLO that was violated225# Maybe I can modify group_fail to report back what was violated and only relax cLO or cUP, not both226# This relaxes both cLO and cUP227tmp_cLO[group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP)] <- tmp_cLO[group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP)] - runif(1, 0.01, 0.05)228tmp_cUP[group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP)] <- tmp_cUP[group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP)] + runif(1, 0.01, 0.05)229}230# Now try the transformation again231tmp_weights <- try(rp_transform(w=tmp_weights,232min_sum=min_sum,233max_sum=max_sum,234min_box=tmp_min,235max_box=tmp_max,236groups=groups,237cLO=tmp_cLO,238cUP=tmp_cUP,239max_pos=NULL,240group_pos=group_pos,241max_pos_long=NULL,242max_pos_short=NULL,243leverage=tmp_leverage,244weight_seq=weight_seq,245max_permutations=500),246silent=TRUE) # FALSE for testing247if(inherits(tmp_weights, "try-error")) tmp_weights <- weights248i <- i + 1249}250# We have a feasible portfolio in terms of min_sum and max_sum,251# but were unable to produce a portfolio that satisfies group constraints252if(isTRUE(all.equal(tmp_weights, weights))){253# reset min and max to their original values and penalize later254tmp_cLO <- cLO255tmp_cUP <- cUP256}257} # end if(relax) statement258} # end try-error recovery259} # end check for group constraint violation260} # end check for NULL arguments261262# check position_limit constraints263if(!is.null(tmp_max_pos) | !is.null(tmp_max_pos_long) | !is.null(tmp_max_pos_short)){264if(pos_limit_fail(tmp_weights, tmp_max_pos, tmp_max_pos_long, tmp_max_pos_short)){265# Try to transform only considering leverage, box, group, and position_limit constraints266tmp_weights <- try(rp_transform(w=tmp_weights,267min_sum=min_sum,268max_sum=max_sum,269min_box=tmp_min,270max_box=tmp_max,271groups=groups,272cLO=tmp_cLO,273cUP=tmp_cUP,274max_pos=tmp_max_pos,275group_pos=group_pos,276max_pos_long=tmp_max_pos_long,277max_pos_short=tmp_max_pos_short,278leverage=tmp_leverage,279weight_seq=weight_seq,280max_permutations=500),281silent=TRUE) # FALSE for testing282if(inherits(tmp_weights, "try-error")){283if(verbose) message(tmp_weights)284# Default to initial weights285tmp_weights <- weights286if(relax){287i <- 1288while(pos_limit_fail(tmp_weights, tmp_max_pos, tmp_max_pos_long, tmp_max_pos_short) & (i <= 5)){289# increment tmp_max_pos by 1290if(!is.null(tmp_max_pos)) tmp_max_pos <- min(nassets, tmp_max_pos + 1)291if(!is.null(tmp_max_pos_long)) tmp_max_pos_long <- min(nassets, tmp_max_pos_long + 1)292if(!is.null(tmp_max_pos_short)) tmp_max_pos_short <- min(nassets, tmp_max_pos_short + 1)293# Now try the transformation again294tmp_weights <- try(rp_transform(w=tmp_weights,295min_sum=min_sum,296max_sum=max_sum,297min_box=tmp_min,298max_box=tmp_max,299groups=groups,300cLO=tmp_cLO,301cUP=tmp_cUP,302max_pos=tmp_max_pos,303group_pos=group_pos,304max_pos_long=tmp_max_pos_long,305max_pos_short=tmp_max_pos_short,306leverage=tmp_leverage,307weight_seq=weight_seq,308max_permutations=500),309silent=TRUE) # FALSE for testing310if(inherits(tmp_weights, "try-error")) tmp_weights <- weights311i <- i + 1312}313} # end if(relax) statement314} # end try-error recovery315} # end check for position limit constraint violation316} # end check for NULL arguments317318# check leverage constraints319if(!is.null(tmp_leverage)){320if(sum(abs(tmp_weights)) > tmp_leverage){321# Try to transform only considering weight_sum, box, group, position_limit, and leverage exposure constraints322tmp_weights <- try(rp_transform(w=tmp_weights,323min_sum=min_sum,324max_sum=max_sum,325min_box=tmp_min,326max_box=tmp_max,327groups=groups,328cLO=tmp_cLO,329cUP=tmp_cUP,330max_pos=tmp_max_pos,331group_pos=group_pos,332max_pos_long=tmp_max_pos_long,333max_pos_short=tmp_max_pos_short,334leverage=tmp_leverage,335weight_seq=weight_seq,336max_permutations=500),337silent=TRUE) # FALSE for testing338if(inherits(tmp_weights, "try-error")){339if(verbose) message(tmp_weights)340# Default to initial weights341tmp_weights <- weights342if(relax){343i <- 1344while(sum(abs(tmp_weights)) > tmp_leverage & (i <= 5)){345# increment tmp_leverage by 1%346tmp_leverage <- tmp_leverage * 1.01347# Now try the transformation again348tmp_weights <- try(rp_transform(w=tmp_weights,349min_sum=min_sum,350max_sum=max_sum,351min_box=tmp_min,352max_box=tmp_max,353groups=groups,354cLO=tmp_cLO,355cUP=tmp_cUP,356max_pos=tmp_max_pos,357group_pos=group_pos,358max_pos_long=tmp_max_pos_long,359max_pos_short=tmp_max_pos_short,360leverage=tmp_leverage,361weight_seq=weight_seq,362max_permutations=500),363silent=TRUE) # FALSE for testing364if(inherits(tmp_weights, "try-error")) tmp_weights <- weights365i <- i + 1366}367} # end if(relax) statement368} # end try-error recovery369} # end check for leverage exposure violation370} # end check for NULL arguments371372names(tmp_weights) <- names(weights)373return(list(weights=tmp_weights,374min=tmp_min,375max=tmp_max,376cLO=tmp_cLO,377cUP=tmp_cUP,378max_pos=tmp_max_pos,379max_pos_long=tmp_max_pos_long,380max_pos_short=tmp_max_pos_short,381leverage=tmp_leverage))382}383384385386#' Transform a weights vector to satisfy constraints387#'388#' This function uses a block of code from \code{\link{randomize_portfolio}}389#' to transform the weight vector if either the weight_sum (leverage)390#' constraints, box constraints, group constraints, position_limit constraints,391#' or leverage exposure constraints are violated. The logic from392#' \code{randomize_portfolio} is heavily utilized here with extensions to393#' handle more complex constraints.394#' The resulting weights vector might be quite different from the original weights vector.395#'396#' @param w weights vector to be transformed397#' @param min_sum minimum sum of all asset weights, default 0.99398#' @param max_sum maximum sum of all asset weights, default 1.01399#' @param min_box numeric or named vector specifying minimum weight box constraints400#' @param max_box numeric or named vector specifying maximum weight box constraints401#' @param groups vector specifying the groups of the assets402#' @param cLO numeric or vector specifying minimum weight group constraints403#' @param cUP numeric or vector specifying minimum weight group constraints404#' @param max_pos maximum assets with non-zero weights405#' @param group_pos vector specifying maximum number assets with non-zero weights per group406#' @param max_pos_long maximum number of assets with long (i.e. buy) positions407#' @param max_pos_short maximum number of assets with short (i.e. sell) positions408#' @param leverage maximum leverage exposure where leverage is defined as \code{sum(abs(weights))}409#' @param max_permutations integer: maximum number of iterations to try for a valid portfolio, default 200410#' @param weight_seq vector of seed sequence of weights411#' @return named weighting vector412#' @author Peter Carl, Brian G. Peterson, Ross Bennett (based on an idea by Pat Burns)413#' @export414rp_transform <- function(w,415min_sum,416max_sum,417min_box,418max_box,419groups=NULL,420cLO=NULL,421cUP=NULL,422max_pos=NULL,423group_pos=NULL,424max_pos_long=NULL,425max_pos_short=NULL,426leverage=NULL,427weight_seq=NULL,428max_permutations=200){429tmp_w <- w430431# Set some reasonable default values432# Maybe I should leave these as NULL values and incorporate that into the433# checks434#if(is.null(min_sum)) min_sum <- 0.99435#if(is.null(max_sum)) max_sum <- 1.01436#if(is.null(min_box)) min_box <- rep(-Inf, length(tmp_w))437#if(is.null(max_box)) max_box <- rep(Inf, length(tmp_w))438if(is.null(max_pos)) max_pos <- length(tmp_w)439#if(is.null(max_poslong)) max_pos_long <- length(tmp_w)440#if(is.null(max_pos_short)) max_pos_short <- length(tmp_w)441#if(is.null(leverage)) leverage <- Inf442443# Generate a weight sequence, we should check for portfolio$weight_seq444if(is.null(weight_seq))445weight_seq <- generatesequence(min=min(min_box), max=max(max_box), by=0.002)446447# make sure there is a 0 in weight_seq if we have a position limit constraint448if((!is.null(max_pos) | !is.null(group_pos) | !is.null(max_pos_long) | !is.null(max_pos_short)) & !is.element(0, weight_seq)) weight_seq <- c(0, weight_seq)449450# Tolerance for "non-zero" definition for position limit constraints451tolerance <- .Machine$double.eps^0.5452453# initialize the outer while loop454permutations <- 1455456# while we have not reached max_permutations and the following constraints457# are violated:458# - min_sum459# - max_sum460# - leverage461# - max_pos, max_pos_long, max_pos_short462# - group463464# Do we want to check all constraints in here?465# Box constraints should be satisfied by construction so we should not need466# to check those here467while (( min_sum_fail(tmp_w, min_sum) |468max_sum_fail(tmp_w, max_sum) |469leverage_fail(tmp_w, leverage) |470pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) |471any(group_fail(tmp_w, groups, cLO, cUP)) ) &472(permutations < max_permutations)) {473474# cat("permutation #:", permutations, "\n")475permutations <- permutations+1476477# Reset tmp_w to original weights vector478# I'm not sure we want to do this here because it puts us back to where we479# started, but it seems to help with the position limit constraint480# tmp_w <- weights481482# Reset the random index based on the maximum position constraint483# This basically allows us to generate a portfolio of max_pos assets484# with the given constraints and then add assets with zero weight485random_index <- sample(1:length(tmp_w), max_pos)486487# Get the index values that are not in random_index and set them equal to 0488full_index <- 1:length(tmp_w)489not_index <- setdiff(full_index, random_index)490tmp_w[not_index] <- 0491492# min_sum violation493if(min_sum_fail(tmp_w, min_sum)){494tmp_w <- rp_increase(weights=tmp_w,495min_sum=min_sum,496max_box=max_box,497weight_seq=weight_seq)498}499500# max_sum violation501if(max_sum_fail(tmp_w, max_sum)){502tmp_w <- rp_decrease(weights=tmp_w,503max_sum=max_sum,504min_box=min_box,505weight_seq=weight_seq)506}507508# leverage violation509if(leverage_fail(tmp_w, leverage)){510tmp_w <- rp_decrease_leverage(weights=tmp_w,511max_box=max_box,512min_box=min_box,513leverage=leverage,514weight_seq=weight_seq)515}516517# position limit violation518if(pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short)){519tmp_w <- rp_position_limit(weights=tmp_w,520min_box=min_box,521max_box=max_box,522max_pos=max_pos,523max_pos_long=max_pos_long,524max_pos_short=max_pos_short,525weight_seq=weight_seq)526}527528# group violation529if(any(group_fail(tmp_w, groups, cLO, cUP, group_pos))){530n_groups <- length(groups)531for(j in 1:n_groups){532# index of the weights vector belonging to the jth group533j_idx <- groups[[j]]534# weights of the jth group535tmp_group_w <- tmp_w[j_idx]536537# May be easier to just make a recursive call and treat each group538# as a portfolio of weight vectors539tmp_w[j_idx] <- rp_transform(w=tmp_group_w,540min_sum=cLO[j],541max_sum=cUP[j],542min_box=min_box[j_idx],543max_box=max_box[j_idx],544group_pos=group_pos[j])545546# treat this as if min_sum were violated547# if(sum(tmp_group_w) < cLO[j]){548# tmp_w[j_idx] <- rp_increase(weights=tmp_group_w,549# min_sum=cLO[j],550# max_box=max_box[j_idx],551# weight_seq=weight_seq)552# }553554# treat this as if max_sum were violated555# if(sum(tmp_group_w) > cUP[j]){556# tmp_w[j_idx] <- rp_decrease(weights=tmp_group_w,557# max_sum=cUP[j],558# min_box=min_box[j_idx],559# weight_seq=weight_seq)560# }561}562} # end group violation loop563} # end final walk towards the edges564portfolio <- tmp_w565566colnames(portfolio) <- colnames(w)567568# checks for infeasible portfolio569# Stop execution and return an error if an infeasible portfolio is created570# This will be useful in fn_map so that we can catch the error and take571# action (try again with more permutations, relax constraints, different572# method to normalize, etc.)573if (sum(portfolio) < min_sum | sum(portfolio) > max_sum){574portfolio <- w575stop("Infeasible portfolio created, perhaps increase max_permutations and/or adjust your parameters.")576}577return(portfolio)578}579580# rp_transform <- function(w,581# min_sum=0.99,582# max_sum=1.01,583# min,584# max,585# groups,586# cLO,587# cUP,588# max_pos=NULL,589# group_pos=NULL,590# max_pos_long=NULL,591# max_pos_short=NULL,592# leverage=NULL,593# max_permutations=200){594# # Uses logic from randomize_portfolio to "normalize" a weights vector to595# # satisfy min_sum and max_sum while accounting for box and group constraints596# # Modified from randomize_portfolio to trigger the while loops if any weights597# # violate min or max box constraints. A weights vector would not be transformed598# # in randomize_portfolio if min_sum and max_sum were satisfied, but the599# # min/max constraints were violated.600#601# # Set the tolerance to determine non-zero weights602# tolerance=.Machine$double.eps^0.5603#604# # Set value for max_pos if it is not specified605# if(is.null(max_pos)) max_pos <- length(w)606#607# # Set value for leverage if it is not specified608# if(is.null(leverage)) leverage <- Inf609#610# # Determine maximum number of non-zero weights611# if(!is.null(group_pos)) {612# max_group_pos <- sum(group_pos)613# } else {614# max_group_pos <- length(w)615# }616#617# # Set maximum number of assets based on max_pos and group_pos618# max_assets <- min(max_pos, max_group_pos)619#620# # Create a temporary min vector that will be modified, because a feasible621# # portfolio is rarely created if all(min > 0). This is due to the while622# # loop that checks any(tmp_w < min).623# tmp_min <- min624#625# # If weight_i = 0 and min_i > 0, then this will violate box constraints626# # even though weight_i = 0 to satisfy position_limit constraints. Modify627# # the tmp_min vector and set tmp_min_i equal to zero where weights_i = 0.628# # If w is less than or equal to tolerance then it is essentially 0629# if(any(abs(w) <= tolerance)){630# if(any(tmp_min[which(abs(w) <= tolerance)] > 0)){631# tmp_min[which(abs(w) <= tolerance)] <- -tolerance632# }633# }634#635# # return w if all constraints are satisfied636# if((sum(w) >= min_sum & sum(w) <= max_sum) &637# (all(w >= tmp_min) & all(w <= max)) &638# (all(!group_fail(w, groups, cLO, cUP, group_pos))) &639# !pos_limit_fail(w, max_pos, max_pos_long, max_pos_short) &640# (sum(abs(w)) <= leverage)){641# return(w)642# }643#644# # generate a sequence of weights based on min/max box constraints645# weight_seq <- generatesequence(min=min(min), max=max(max), by=0.002)646# # make sure there is a 0 in weight_seq647# if((!is.null(max_pos) | !is.null(group_pos) | !is.null(max_pos_long) | !is.null(max_pos_short)) & !is.element(0, weight_seq)) weight_seq <- c(0, weight_seq)648#649# # start the permutations counter650# permutations <- 1651#652# # create a temporary weights vector that will be modified in the while loops653# tmp_w <- w654#655# # while any constraint is violated and we have not reached max_permutations656# while ((sum(tmp_w) < min_sum |657# sum(tmp_w) > max_sum |658# any(tmp_w < tmp_min) |659# any(tmp_w > max) |660# any(group_fail(tmp_w, groups, cLO, cUP, group_pos)) |661# pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) |662# sum(abs(w)) > leverage) &663# permutations <= max_permutations) {664# permutations = permutations + 1665# # check our box constraints on total portfolio weight666# # reduce(increase) total portfolio size till you get a match667# # 1> check to see which bound you've failed on, probably set this as a pair of while loops668# # 2> randomly select a column and move only in the direction *towards the bound*, maybe call a function inside a function669# # 3> check and repeat670#671# # reset tmp_w and tmp_min to their original values672# tmp_w <- w673# tmp_min <- min674#675# random_index <- sample(1:length(tmp_w), max_assets)676#677# # Get the index values that are not in random_index and set them equal to 0678# full_index <- 1:length(tmp_w)679# not_index <- setdiff(full_index, random_index)680# tmp_w[not_index] <- 0681#682# # set some tmp_min values equal to zero so the while loops do not see a683# # violation of any(tmp_w < tmp_min). This tends to force weights to 0 and684# # works well for long only, but we may want to allow negative weights.685# # tmp_min[not_index] <- 0686# # Only set values of tmp_min that are greater than 0 to 0687# tmp_min[not_index[which(tmp_min[not_index] > 0)]] <- 0688#689# # Transform weights to satisfy max_pos_long and max_pos_short before being690# # passed into the main loops691# # Both max_pos_long and max_pos_short should be specified692# if(!is.null(max_pos_long)){693# pos_idx <- which(tmp_w > 0)694# neg_idx <- which(tmp_w < 0)695#696# # Check if number of positive weights exceeds max_pos_long697# if(length(pos_idx) > max_pos_long){698# # Randomly sample positive weights that cause violation of max_pos_long699# # and replace with randomly sampled negative weights from weight_seq700# make_neg_idx <- sample(pos_idx, length(pos_idx) - max_pos_long)701# for(i in make_neg_idx){702# tmp_idx <- weight_seq[weight_seq < 0 & weight_seq >= min[i]]703# if(length(tmp_idx) > 0){704# tmp_w[i] <- sample(tmp_idx, 1)705# } else {706# # This should never happen if the correct weight_seq and min is specified707# tmp_w[i] <- -tmp_w[i]708# }709# }710# }711# }712# if(!is.null(max_pos_short)){713# # Check if number of negative weights exceeds max_pos_short714# if(length(neg_idx) > max_pos_short){715# # Randomly sample negative weights that cause violation of max_pos_short716# # and replace with randomly sampled positive weights from weight_seq717# make_pos_idx <- sample(neg_idx, length(neg_idx) - max_pos_short)718# for(i in make_pos_idx){719# tmp_seq <- weight_seq[weight_seq > 0 & weight_seq <= max[i]]720# if(length(tmp_seq) > 0){721# tmp_w[i] <- sample(tmp_seq, 1)722# } else {723# # This should never happen if the correct weight_seq and max is specified724# tmp_w[i] <- -tmp_w[i]725# }726# }727# }728# }729#730# i = 1731# # We increase elements here if the sum of the weights exceeds max_sum or732# # any of the other constraints are violated733# while ((sum(tmp_w) < min_sum |734# any(tmp_w < tmp_min) |735# any(tmp_w > max) |736# any(group_fail(tmp_w, groups, cLO, cUP, group_pos)) |737# pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) |738# sum(abs(tmp_w)) > leverage) &739# i <= length(tmp_w)) {740# # randomly permute and increase a random portfolio element741# cur_index <- random_index[i]742# cur_val <- tmp_w[cur_index]743# tmp_seq <- weight_seq[(weight_seq >= cur_val) & (weight_seq <= max[cur_index])]744# n_tmp_seq <- length(tmp_seq)745# if (n_tmp_seq > 1) {746# # randomly sample an element from weight_seq that is greater than cur_val and less than max747# # tmp_w[cur_index] <- sample(weight_seq[(weight_seq >= cur_val) & (weight_seq <= max[cur_index])], 1)748# tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]749# # print(paste("new val:",tmp_w[cur_index]))750# } else {751# if (n_tmp_seq == 1) {752# # tmp_w[cur_index] <- weight_seq[(weight_seq >= cur_val) & (weight_seq <= max[cur_index])]753# tmp_w[cur_index] <- tmp_seq754# }755# }756# i=i+1 # increment our counter757# } # end increase loop758# # need to reset i here otherwise the decreasing loop will be ignored759# # group_fail does not test for direction of violation, just that group constraints were violated760# i = 1761# # We decrease elements here if the sum of the weights exceeds max_sum or762# # any of the other constraints are violated763# while ((sum(tmp_w) > max_sum |764# any(tmp_w < tmp_min) |765# any(tmp_w > max) |766# any(group_fail(tmp_w, groups, cLO, cUP, group_pos)) |767# pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) |768# sum(abs(tmp_w)) > leverage) &769# i <= length(tmp_w)) {770# # randomly permute and decrease a random portfolio element771# cur_index <- random_index[i]772# cur_val <- tmp_w[cur_index]773# tmp_seq <- weight_seq[(weight_seq <= cur_val) & (weight_seq >= tmp_min[cur_index])]774# n_tmp_seq <- length(tmp_seq)775# if (n_tmp_seq > 1) {776# # randomly sample an element from weight_seq that is less than cur_val and greater than tmp_min777# # tmp_w[cur_index] <- sample(weight_seq[(weight_seq <= cur_val) & (weight_seq >= tmp_min[cur_index])] , 1)778# tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]779# } else {780# if (n_tmp_seq == 1) {781# # tmp_w[cur_index] <- weight_seq[(weight_seq <= cur_val) & (weight_seq >= tmp_min[cur_index])]782# tmp_w[cur_index] <- tmp_seq783# }784# }785# i=i+1 # increment our counter786# } # end decrease loop787# #cat("permutations:", permutations, "\n")788# #cat("weights:", tmp_w, "\n")789# #cat("sum(weights):", sum(tmp_w), "\n")790# #cat("sum(abs(weights)):", sum(abs(tmp_w)), "\n")791# } # end final walk towards the edges792#793# portfolio <- tmp_w794#795# colnames(portfolio)<-colnames(w)796#797# # checks for infeasible portfolio798# # Stop execution and return an error if an infeasible portfolio is created799# # This will be useful in fn_map so that we can catch the error and take800# # action (try again with more permutations, relax constraints, different801# # method to normalize, etc.)802# if (sum(portfolio) < min_sum | sum(portfolio) > max_sum){803# portfolio <- w804# stop("Infeasible portfolio created, perhaps increase max_permutations and/or adjust your parameters.")805# }806# # if(isTRUE(all.equal(w,portfolio))) {807# # if (sum(w)>=min_sum & sum(w)<=max_sum) {808# # warning("Unable to generate a feasible portfolio different from w, perhaps adjust your parameters.")809# # return(w)810# # } else {811# # warning("Unable to generate a feasible portfolio, perhaps adjust your parameters.")812# # return(NULL)813# # }814# # }815# return(portfolio)816# }817818#' Test if group constraints have been violated819#'820#' The function loops through each group and tests if cLO or cUP have been violated821#' for the given group. This is a helper function for \code{\link{rp_transform}}.822#'823#' @param weights weights vector to test824#' @param groups list of vectors specifying the groups of the assets825#' @param cLO numeric or vector specifying minimum weight group constraints826#' @param cUP numeric or vector specifying minimum weight group constraints827#' @param group_pos vector specifying the number of non-zero weights per group828#' @return logical vector: TRUE if group constraints are violated for a given group829#' @author Ross Bennett830group_fail <- function(weights, groups, cLO, cUP, group_pos=NULL){831# return FALSE if groups, cLO, or cUP is NULL832if(is.null(groups) | is.null(cLO) | is.null(cUP)) return(FALSE)833group_count <- sapply(groups, length)834# group_pos sets a limit on the number of non-zero weights by group835# Set equal to groups if NULL836if(is.null(group_pos)) group_pos <- group_count837tolerance <- .Machine$double.eps^0.5838839n.groups <- length(groups)840group_fail <- vector(mode="logical", length=n.groups)841842for(i in 1:n.groups){843# sum of the weights for a given group844tmp.w <- weights[groups[[i]]]845group_fail[i] <- ( (sum(tmp.w) < cLO[i]) | (sum(tmp.w) > cUP[i]) | (sum(abs(tmp.w) > tolerance) > group_pos[i]) )846}847# returns logical vector of groups. TRUE if either cLO or cUP is violated848return(group_fail)849}850851#' function to check for violation of position limits constraints852#'853#' This is used as a helper function for \code{\link{rp_transform}} to check854#' for violation of position limit constraints. The position limit constraints855#' checked are max_pos, max_pos_long, and max_pos_short.856#'857#' @param weights vector of weights to test858#' @param max_pos maximum number of assets with non-zero weights859#' @param max_pos_long maximum number of assets with long (i.e. buy) positions860#' @param max_pos_short maximum number of assets with short (i.e. sell) positions861#' @return TRUE if any position_limit is violated. FALSE if all position limits are satisfied862#' @export863pos_limit_fail <- function(weights, max_pos, max_pos_long, max_pos_short){864# tolerance for "non-zero" definition865tolerance <- .Machine$double.eps^0.5866867# Check if max_pos is violated868if(!is.null(max_pos)){869if(sum(abs(weights) > tolerance) > max_pos){870return(TRUE)871}872}873874# Check if max_pos_long is violated875if(!is.null(max_pos_long)){876if(sum(weights > tolerance) > max_pos_long){877return(TRUE)878}879}880881# Check if max_pos_short is violated882if(!is.null(max_pos_short)){883if(sum(weights < -tolerance) > max_pos_short){884return(TRUE)885}886}887# Return FALSE if nothing is violated888return(FALSE)889}890891min_sum_fail <- function(weights, min_sum){892# return FALSE if min_sum is null893if(is.null(min_sum)) return(FALSE)894895# sum of weights violate min_sum constraint896return(sum(weights) < min_sum)897}898899max_sum_fail <- function(weights, max_sum){900# return FALSE if max_sum is null901if(is.null(max_sum)) return(FALSE)902903# sum of weights violate max_sum constraint904return(sum(weights) > max_sum)905}906907leverage_fail <- function(weights, leverage){908# return FALSE if leverage is null909if(is.null(leverage)) return(FALSE)910911# sum of absolute value of weight violates leverage constraint912return(sum(abs(weights)) > leverage)913}914915rp_increase <- function(weights, min_sum, max_box, weight_seq){916# randomly permute and increase a random portfolio element if the sum of917# the weights is less than min_sum while respecting box constraints918919if(sum(weights) >= min_sum) return(weights)920921tmp_w <- weights922n_weights <- length(weights)923# random_index <- sample(1:length(weights), max_pos)924random_index <- sample(1:n_weights, n_weights)925i <- 1926while (sum(tmp_w) < min_sum & i <= n_weights) {927# print("min_sum violation loop")928929cur_index <- random_index[i]930cur_val <- tmp_w[cur_index]931tmp_seq <- weight_seq[(weight_seq > cur_val) & (weight_seq <= max_box[cur_index])]932n_tmp_seq <- length(tmp_seq)933if(n_tmp_seq > 1){934# randomly sample one of the larger weights935tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]936} else if(n_tmp_seq == 1){937tmp_w[cur_index] <- tmp_seq938}939i <- i + 1 # increment our counter940} # end increase loop941return(tmp_w)942}943944rp_decrease <- function(weights, max_sum, min_box, weight_seq){945# randomly permute and decrease a random portfolio element if the sum of946# the weights is greater than max_sum while respecting box constraints947948if(sum(weights) <= max_sum) return(weights)949950tmp_w <- weights951n_weights <- length(weights)952# random_index <- sample(1:length(weights), max_pos)953random_index <- sample(1:n_weights, n_weights)954955i <- 1956while (sum(tmp_w) > max_sum & i <= n_weights) {957# print("max_sum violation loop")958959cur_index <- random_index[i]960cur_val <- tmp_w[cur_index]961tmp_seq <- weight_seq[(weight_seq < cur_val) & (weight_seq >= min_box[cur_index])]962n_tmp_seq <- length(tmp_seq)963if(n_tmp_seq > 1){964tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]965} else if(n_tmp_seq == 1){966tmp_w[cur_index] <- tmp_seq967}968i <- i + 1 # increment our counter969} # end decrease loop970return(tmp_w)971}972973rp_decrease_leverage <- function(weights, max_box, min_box, leverage, weight_seq){974# randomly permute and increae decrease a random portfolio element975# according to leverage exposure while respecting box constraints976977tmp_w <- weights978n_weights <- length(weights)979# random_index <- sample(1:length(weights), max_pos)980random_index <- sample(1:n_weights, n_weights)981982# set counter to 1 for leverage violation loop983i <- 1984while (sum(abs(tmp_w)) > leverage & i <= length(tmp_w)) {985#print("leverage violation loop")986987cur_index <- random_index[i]988cur_val <- tmp_w[cur_index]989990tmp_seq <- NULL991# check the sign of the current value992if(cur_val < 0){993# if the current value is negative, we want to increase to lower994# sum(abs(weights)) while respecting uppper bound box constraint995tmp_seq <- weight_seq[(weight_seq > cur_val) & (weight_seq <= max_box[cur_index])]996} else if(cur_val > 0){997# if the current value is positive, we want to decrease to lower998# sum(abs(weights)) while respecting lower bound box constraint999tmp_seq <- weight_seq[(weight_seq < cur_val) & (weight_seq >= min_box[cur_index])]1000}1001# tmp_seq can be NULL if cur_val is zero1002if(!is.null(tmp_seq)){1003n_tmp_seq <- length(tmp_seq)10041005if(n_tmp_seq > 1) {1006# randomly sample one of the weights1007tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]1008} else if(n_tmp_seq == 1){1009tmp_w[cur_index] <- tmp_seq1010}1011}1012i <- i + 1 # increment our counter1013} # end leverage violation loop1014return(tmp_w)1015}10161017rp_position_limit <- function(weights, max_pos=NULL, max_pos_long=NULL, max_pos_short=NULL, min_box, max_box, weight_seq){1018tmp_w <- weights1019n_weights <- length(weights)1020# random_index <- sample(1:length(weights), max_pos)1021random_index <- sample(1:n_weights, n_weights)10221023tolerance <- .Machine$double.eps^0.510241025# set counter to 1 for position limit violation loop1026i <- 11027while ( pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) & i <= length(tmp_w)) {1028#print("position limit violation loop")10291030cur_index <- random_index[i]1031cur_val <- tmp_w[cur_index]10321033if(!is.null(max_pos_long)){1034# Check if max_pos_long is violated1035# If max_pos_long is violated, we we grab a positive weight and set it1036# to be between min_box and 01037if(sum(tmp_w > tolerance) > max_pos_long){1038if(cur_val > tolerance){1039# subset such that min_box_i <= weight_i <= 01040tmp_seq <- weight_seq[(weight_seq <= 0) & (weight_seq >= min_box[cur_index])]1041n_tmp_seq <- length(tmp_seq)1042if(n_tmp_seq > 1){1043tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]1044} else if(n_tmp_seq == 1){1045tmp_w[cur_index] <- tmp_seq1046}1047}1048} # end max_pos_long violation loop1049}10501051if(!is.null(max_pos_short)){1052# Check if max_pos_short is violated1053# If max_pos_short is violated, we grab a negative weight and set it1054# to be between 0 and max_box1055if(sum(tmp_w < tolerance) > max_pos_short){1056if(cur_val < tolerance){1057# subset such that 0 <= weight_i <= max_box_i1058tmp_seq <- weight_seq[(weight_seq >= 0) & (weight_seq <= max_box[cur_index])]1059n_tmp_seq <- length(tmp_seq)1060if(n_tmp_seq > 1){1061tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]1062} else if(n_tmp_seq == 1){1063tmp_w[cur_index] <- tmp_seq1064}1065}1066} # end max_pos_short violation loop1067}1068i <- i + 1 # increment our counter1069} # end position limit violation loop1070return(tmp_w)1071}107210731074# test1075# w <- c(0.1, 0.25, 0.3, 0.15, 0.05, 0.15)1076# min <- rep(0.1, length(w))1077# max <- rep(0.45, length(w))1078# w1 <- rp_normalize(w=w, min_sum=0.99, max_sum=1.01, min=min, max=max)1079# w11080# sum(w1)1081# any(w1 < min)1082# any(w1 > max)10831084# library(PortfolioAnalytics)1085# data(edhec)1086# ret <- edhec[, 1:4]1087# funds <- colnames(ret)1088#1089# pspec <- portfolio.spec(assets=funds)1090# pspec <- add.constraint(portfolio=pspec, type="weight_sum", min_sum=0.99, max_sum=1.01, enabled=TRUE)1091# pspec <- add.constraint(portfolio=pspec, type="box", enabled=TRUE)1092# pspec <- add.constraint(portfolio=pspec, type="group", groups=c(2,2), group_min=c(0.1, 0.2), group_max=c(0.3, 0.8), enabled=TRUE)1093#1094# weights <- c(0.15, 0.2, 0.15, 0.5)1095# sum(weights)1096#1097# (w <- constraint_fn_map(weights, pspec))1098# sum(w)109911001101###############################################################################1102# R (https://r-project.org/) Numeric Methods for Optimization of Portfolios1103#1104# Copyright (c) 2004-2021 Brian G. Peterson, Peter Carl, Ross Bennett, Kris Boudt1105#1106# This library is distributed under the terms of the GNU Public License (GPL)1107# for full details see the file COPYING1108#1109# $Id$1110#1111###############################################################################111211131114