###############################################################################1# R (https://r-project.org/) Numeric Methods for Optimization of Portfolios2#3# Copyright (c) 2004-2021 Brian G. Peterson, Peter Carl, Ross Bennett, Kris Boudt4#5# This library is distributed under the terms of the GNU Public License (GPL)6# for full details see the file COPYING7#8# $Id$9#10###############################################################################111213chart.Weight.DE <- function(object, ..., neighbors = NULL, main="Weights", las = 3, xlab=NULL, cex.lab = 1, element.color = "darkgray", cex.axis=0.8, colorset=NULL, legend.loc="topright", cex.legend=0.8, plot.type="line"){14# Specific to the output of optimize.portfolio with optimize_method="DEoptim"15if(!inherits(object, "optimize.portfolio.DEoptim")) stop("object must be of class 'optimize.portfolio.DEoptim'")1617if(plot.type %in% c("bar", "barplot")){18barplotWeights(object=object, ..., main=main, las=las, xlab=xlab, cex.lab=cex.lab, element.color=element.color, cex.axis=cex.axis, legend.loc=legend.loc, cex.legend=cex.legend, colorset=colorset)19} else if(plot.type == "line"){2021columnnames = names(object$weights)22numassets = length(columnnames)2324constraints <- get_constraints(object$portfolio)2526if(is.null(xlab))27minmargin = 328else29minmargin = 530if(main=="") topmargin=1 else topmargin=431if(las > 1) {# set the bottom border to accommodate labels32bottommargin = max(c(minmargin, (strwidth(columnnames,units="in"))/par("cin")[1])) * cex.lab33if(bottommargin > 10 ) {34bottommargin<-1035columnnames<-substr(columnnames,1,19)36# par(srt=45) #TODO figure out how to use text() and srt to rotate long labels37}38}39else {40bottommargin = minmargin41}42par(mar = c(bottommargin, 4, topmargin, 2) +.1)43if(any(is.infinite(constraints$max)) | any(is.infinite(constraints$min))){44# set ylim based on weights if box constraints contain Inf or -Inf45ylim <- range(object$weights)46} else {47# set ylim based on the range of box constraints min and max48ylim <- range(c(constraints$min, constraints$max))49}50plot(object$weights, type="b", col="blue", axes=FALSE, xlab='', ylim=ylim, ylab="Weights", main=main, pch=16, ...)51if(!any(is.infinite(constraints$min))){52points(constraints$min, type="b", col="darkgray", lty="solid", lwd=2, pch=24)53}54if(!any(is.infinite(constraints$max))){55points(constraints$max, type="b", col="darkgray", lty="solid", lwd=2, pch=25)56}57# if(!is.null(neighbors)){58# if(is.vector(neighbors)){59# xtract=extractStats(object)60# weightcols<-grep('w\\.',colnames(xtract)) #need \\. to get the dot61# if(length(neighbors)==1){62# # overplot nearby portfolios defined by 'out'63# orderx = order(xtract[,"out"])64# subsetx = head(xtract[orderx,], n=neighbors)65# for(i in 1:neighbors) points(subsetx[i,weightcols], type="b", col="lightblue")66# } else{67# # assume we have a vector of portfolio numbers68# subsetx = xtract[neighbors,weightcols]69# for(i in 1:length(neighbors)) points(subsetx[i,], type="b", col="lightblue")70# }71# }72# if(is.matrix(neighbors) | is.data.frame(neighbors)){73# # the user has likely passed in a matrix containing calculated values for risk.col and return.col74# nbweights<-grep('w\\.',colnames(neighbors)) #need \\. to get the dot75# for(i in 1:nrow(neighbors)) points(as.numeric(neighbors[i,nbweights]), type="b", col="lightblue")76# # note that here we need to get weight cols separately from the matrix, not from xtract77# # also note the need for as.numeric. points() doesn't like matrix inputs78# }79# }8081# points(object$weights, type="b", col="blue", pch=16)82axis(2, cex.axis = cex.axis, col = element.color)83axis(1, labels=columnnames, at=1:numassets, las=las, cex.axis = cex.axis, col = element.color)84box(col = element.color)85}86}8788#' @rdname chart.Weights89#' @method chart.Weights optimize.portfolio.DEoptim90#' @export91chart.Weights.optimize.portfolio.DEoptim <- chart.Weight.DE929394chart.Scatter.DE <- function(object, ..., neighbors = NULL, return.col='mean', risk.col='ES', chart.assets=FALSE, element.color = "darkgray", cex.axis=0.8, xlim=NULL, ylim=NULL){95# more or less specific to the output of the DEoptim portfolio code with constraints96# will work to a point with other functions, such as optimize.porfolio.parallel97# there's still a lot to do to improve this.9899if(!inherits(object, "optimize.portfolio.DEoptim")) stop("object must be of class 'optimize.portfolio.DEoptim'")100101R <- object$R102if(is.null(R)) stop("Returns object not detected, must run optimize.portfolio with trace=TRUE")103portfolio <- object$portfolio104xtract = extractStats(object)105columnnames = colnames(xtract)106#return.column = grep(paste("objective_measures",return.col,sep='.'),columnnames)107return.column = pmatch(return.col,columnnames)108if(is.na(return.column)) {109return.col = paste(return.col,return.col,sep='.')110return.column = pmatch(return.col,columnnames)111}112#risk.column = grep(paste("objective_measures",risk.col,sep='.'),columnnames)113risk.column = pmatch(risk.col,columnnames)114if(is.na(risk.column)) {115risk.col = paste(risk.col,risk.col,sep='.')116risk.column = pmatch(risk.col,columnnames)117}118119# if(is.na(return.column) | is.na(risk.column)) stop(return.col,' or ',risk.col, ' do not match extractStats output')120121# If the user has passed in return.col or risk.col that does not match extractStats output122# This will give the flexibility of passing in return or risk metrics that are not123# objective measures in the optimization. This may cause issues with the "neighbors"124# functionality since that is based on the "out" column125if(is.na(return.column) | is.na(risk.column)){126return.col <- gsub("\\..*", "", return.col)127risk.col <- gsub("\\..*", "", risk.col)128warning(return.col,' or ', risk.col, ' do not match extractStats output of $objective_measures slot')129# Get the matrix of weights for applyFUN130wts_index <- grep("w.", columnnames)131wts <- xtract[, wts_index]132if(is.na(return.column)){133tmpret <- applyFUN(R=R, weights=wts, FUN=return.col)134xtract <- cbind(tmpret, xtract)135colnames(xtract)[which(colnames(xtract) == "tmpret")] <- return.col136}137if(is.na(risk.column)){138tmprisk <- applyFUN(R=R, weights=wts, FUN=risk.col)139xtract <- cbind(tmprisk, xtract)140colnames(xtract)[which(colnames(xtract) == "tmprisk")] <- risk.col141}142columnnames = colnames(xtract)143return.column = pmatch(return.col,columnnames)144if(is.na(return.column)) {145return.col = paste(return.col,return.col,sep='.')146return.column = pmatch(return.col,columnnames)147}148risk.column = pmatch(risk.col,columnnames)149if(is.na(risk.column)) {150risk.col = paste(risk.col,risk.col,sep='.')151risk.column = pmatch(risk.col,columnnames)152}153}154# print(colnames(head(xtract)))155156if(chart.assets){157# Get the arguments from the optimize.portfolio$portfolio object158# to calculate the risk and return metrics for the scatter plot.159# (e.g. arguments=list(p=0.925, clean="boudt")160arguments <- NULL # maybe an option to let the user pass in an arguments list?161if(is.null(arguments)){162tmp.args <- unlist(lapply(object$portfolio$objectives, function(x) x$arguments), recursive=FALSE)163tmp.args <- tmp.args[!duplicated(names(tmp.args))]164if(!is.null(tmp.args$portfolio_method)) tmp.args$portfolio_method <- "single"165arguments <- tmp.args166}167# Include risk reward scatter of asset returns168asset_ret <- scatterFUN(R=R, FUN=return.col, arguments)169asset_risk <- scatterFUN(R=R, FUN=risk.col, arguments)170xlim <- range(c(xtract[,risk.column], asset_risk))171ylim <- range(c(xtract[,return.column], asset_ret))172} else {173asset_ret <- NULL174asset_risk <- NULL175}176177# plot the portfolios from DEoptim_objective_results178plot(xtract[,risk.column],xtract[,return.column], xlab=risk.col, ylab=return.col, col="darkgray", axes=FALSE, xlim=xlim, ylim=ylim, ...)179180# plot the risk-reward scatter of the assets181if(chart.assets){182points(x=asset_risk, y=asset_ret)183text(x=asset_risk, y=asset_ret, labels=colnames(R), pos=4, cex=0.8)184}185186if(!is.null(neighbors)){187if(is.vector(neighbors)){188if(length(neighbors)==1){189# overplot nearby portfolios defined by 'out'190orderx = order(xtract[,"out"]) #TODO this won't work if the objective is anything other than mean191subsetx = head(xtract[orderx,], n=neighbors)192} else{193# assume we have a vector of portfolio numbers194subsetx = xtract[neighbors,]195}196points(subsetx[,risk.column], subsetx[,return.column], col="lightblue", pch=1)197}198if(is.matrix(neighbors) | is.data.frame(neighbors)){199# the user has likely passed in a matrix containing calculated values for risk.col and return.col200rtc = pmatch(return.col,columnnames)201if(is.na(rtc)) {202rtc = pmatch(paste(return.col,return.col,sep='.'),columnnames)203}204rsc = pmatch(risk.col,columnnames)205if(is.na(rsc)) {206risk.column = pmatch(paste(risk.col,risk.col,sep='.'),columnnames)207}208for(i in 1:nrow(neighbors)) points(neighbors[i,rsc], neighbors[i,rtc], col="lightblue", pch=1)209}210}211212# points(xtract[1,risk.column],xtract[1,return.column], col="orange", pch=16) # overplot the equal weighted (or seed)213#check to see if portfolio 1 is EW object$random_portoflios[1,] all weights should be the same214# if(!isTRUE(all.equal(object$random_portfolios[1,][1],1/length(object$random_portfolios[1,]),check.attributes=FALSE))){215#show both the seed and EW if they are different216#NOTE the all.equal comparison could fail above if the first element of the first portfolio is the same as the EW weight,217#but the rest is not, shouldn't happen often with real portfolios, only toy examples218# points(xtract[2,risk.column],xtract[2,return.column], col="green", pch=16) # overplot the equal weighted (or seed)219# }220221## Draw solution trajectory222if(!is.null(R) & !is.null(portfolio)){223w.traj = unique(object$DEoutput$member$bestmemit)224rows = nrow(w.traj)225# Only attempt to draw trajectory if rows is greater than or equal to 1226# There may be some corner cases where nrow(w.traj) is equal to 0,227# resulting in a 'subscript out of bounds' error.228if(rows >= 2){229rr = matrix(nrow=rows, ncol=2)230## maybe rewrite as an apply statement by row on w.traj231rtc = NULL232rsc = NULL233trajnames = NULL234for(i in 1:rows){235236w = w.traj[i,]237x = unlist(constrained_objective(w=w, R=R, portfolio=portfolio, trace=TRUE))238names(x)<-name.replace(names(x))239if(is.null(trajnames)) trajnames<-names(x)240if(is.null(rsc)){241rtc = pmatch(return.col,trajnames)242if(is.na(rtc)) {243rtc = pmatch(paste(return.col,return.col,sep='.'),trajnames)244}245rsc = pmatch(risk.col,trajnames)246if(is.na(rsc)) {247rsc = pmatch(paste(risk.col,risk.col,sep='.'),trajnames)248}249}250rr[i,1] = x[rsc] #'FIXME251rr[i,2] = x[rtc] #'FIXME252}253colors2 = colorRamp(c("blue","lightblue"))254colortrail = rgb(colors2((0:rows)/rows),maxColorValue=255)255for(i in 1:rows){256points(rr[i,1], rr[i,2], pch=1, col = colortrail[rows-i+1])257}258259for(i in 2:rows){260segments(rr[i,1], rr[i,2], rr[i-1,1], rr[i-1,2],col = colortrail[rows-i+1], lty = 1, lwd = 2)261}262}263} else{264message("Trajectory cannot be drawn because return object or constraints were not passed.")265}266267268## @TODO: Generalize this to find column containing the "risk" metric269if(length(names(object)[which(names(object)=='constrained_objective')])) {270result.slot<-'constrained_objective'271} else {272result.slot<-'objective_measures'273}274objcols<-unlist(object[[result.slot]])275names(objcols)<-name.replace(names(objcols))276return.column = pmatch(return.col,names(objcols))277if(is.na(return.column)) {278return.col = paste(return.col,return.col,sep='.')279return.column = pmatch(return.col,names(objcols))280}281risk.column = pmatch(risk.col,names(objcols))282if(is.na(risk.column)) {283risk.col = paste(risk.col,risk.col,sep='.')284risk.column = pmatch(risk.col,names(objcols))285}286# risk and return metrics for the optimal weights if the RP object does not287# contain the metrics specified by return.col or risk.col288if(is.na(return.column) | is.na(risk.column)){289return.col <- gsub("\\..*", "", return.col)290risk.col <- gsub("\\..*", "", risk.col)291# warning(return.col,' or ', risk.col, ' do not match extractStats output of $objective_measures slot')292opt_weights <- object$weights293ret <- as.numeric(applyFUN(R=R, weights=opt_weights, FUN=return.col))294risk <- as.numeric(applyFUN(R=R, weights=opt_weights, FUN=risk.col))295points(risk, ret, col="blue", pch=16) #optimal296text(x=risk, y=ret, labels="Optimal",col="blue", pos=4, cex=0.8)297} else {298points(objcols[risk.column], objcols[return.column], col="blue", pch=16) # optimal299text(x=objcols[risk.column], y=objcols[return.column], labels="Optimal",col="blue", pos=4, cex=0.8)300}301axis(1, cex.axis = cex.axis, col = element.color)302axis(2, cex.axis = cex.axis, col = element.color)303box(col = element.color)304}305306#' @rdname chart.RiskReward307#' @method chart.RiskReward optimize.portfolio.DEoptim308#' @export309chart.RiskReward.optimize.portfolio.DEoptim <- chart.Scatter.DE310311312charts.DE <- function(DE, risk.col, return.col, chart.assets, neighbors=NULL, main="DEoptim.Portfolios", xlim=NULL, ylim=NULL, ...){313# Specific to the output of the random portfolio code with constraints314# @TODO: check that DE is of the correct class315op <- par(no.readonly=TRUE)316layout(matrix(c(1,2)),heights=c(2,1.5),widths=1)317par(mar=c(4,4,4,2))318chart.Scatter.DE(object=DE, risk.col=risk.col, return.col=return.col, chart.assets=chart.assets, neighbors=neighbors, main=main, xlim=xlim, ylim=ylim, ...)319par(mar=c(2,4,0,2))320chart.Weight.DE(object=DE, main="", neighbors=neighbors, ...)321par(op)322}323324325#' plot method for objects of class \code{optimize.portfolio}326#'327#' Scatter and weights chart for portfolio optimizations run with trace=TRUE328#'329#' @details330#' \code{return.col} must be the name of a function used to compute the return metric on the random portfolio weights331#' \code{risk.col} must be the name of a function used to compute the risk metric on the random portfolio weights332#'333#' \code{neighbors} may be specified in three ways.334#' The first is as a single number of neighbors. This will extract the \code{neighbors} closest335#' portfolios in terms of the \code{out} numerical statistic.336#' The second method consists of a numeric vector for \code{neighbors}.337#' This will extract the \code{neighbors} with portfolio index numbers that correspond to the vector contents.338#' The third method for specifying \code{neighbors} is to pass in a matrix.339#' This matrix should look like the output of \code{\link{extractStats}}, and should contain340#' \code{risk.col},\code{return.col}, and weights columns all properly named.341#'342#' The ROI and GenSA solvers do not store the portfolio weights like DEoptim or random343#' portfolios, random portfolios can be generated for the scatter plot with the344#' \code{rp} argument.345#'346#' @param x set of portfolios created by \code{\link{optimize.portfolio}}347#' @param \dots any other passthru parameters348#' @param rp TRUE/FALSE to plot feasible portfolios generated by \code{\link{random_portfolios}}349#' @param return.col string name of column to use for returns (vertical axis)350#' @param risk.col string name of column to use for risk (horizontal axis)351#' @param chart.assets TRUE/FALSE to include risk-return scatter of assets352#' @param neighbors set of 'neighbor portfolios to overplot353#' @param main an overall title for the plot: see \code{\link{title}}354#' @param xlim set the limit on coordinates for the x-axis355#' @param ylim set the limit on coordinates for the y-axis356#' @param element.color provides the color for drawing less-important chart elements, such as the box lines, axis lines, etc.357#' @param cex.axis the magnification to be used for axis annotation relative to the current setting of \code{cex}.358#' @rdname plot359#' @method plot optimize.portfolio.DEoptim360#' @export361plot.optimize.portfolio.DEoptim <- function(x, ..., return.col='mean', risk.col='ES', chart.assets=FALSE, neighbors=NULL, main='optimized portfolio plot', xlim=NULL, ylim=NULL) {362charts.DE(DE=x, risk.col=risk.col, return.col=return.col, chart.assets=chart.assets, neighbors=neighbors, main=main, xlim=xlim, ylim=ylim, ...)363}364365366