Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
braverock
GitHub Repository: braverock/portfolioanalytics
Path: blob/master/R/charts.DE.R
1433 views
1
###############################################################################
2
# R (https://r-project.org/) Numeric Methods for Optimization of Portfolios
3
#
4
# Copyright (c) 2004-2021 Brian G. Peterson, Peter Carl, Ross Bennett, Kris Boudt
5
#
6
# This library is distributed under the terms of the GNU Public License (GPL)
7
# for full details see the file COPYING
8
#
9
# $Id$
10
#
11
###############################################################################
12
13
14
chart.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"){
15
# Specific to the output of optimize.portfolio with optimize_method="DEoptim"
16
if(!inherits(object, "optimize.portfolio.DEoptim")) stop("object must be of class 'optimize.portfolio.DEoptim'")
17
18
if(plot.type %in% c("bar", "barplot")){
19
barplotWeights(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)
20
} else if(plot.type == "line"){
21
22
columnnames = names(object$weights)
23
numassets = length(columnnames)
24
25
constraints <- get_constraints(object$portfolio)
26
27
if(is.null(xlab))
28
minmargin = 3
29
else
30
minmargin = 5
31
if(main=="") topmargin=1 else topmargin=4
32
if(las > 1) {# set the bottom border to accommodate labels
33
bottommargin = max(c(minmargin, (strwidth(columnnames,units="in"))/par("cin")[1])) * cex.lab
34
if(bottommargin > 10 ) {
35
bottommargin<-10
36
columnnames<-substr(columnnames,1,19)
37
# par(srt=45) #TODO figure out how to use text() and srt to rotate long labels
38
}
39
}
40
else {
41
bottommargin = minmargin
42
}
43
par(mar = c(bottommargin, 4, topmargin, 2) +.1)
44
if(any(is.infinite(constraints$max)) | any(is.infinite(constraints$min))){
45
# set ylim based on weights if box constraints contain Inf or -Inf
46
ylim <- range(object$weights)
47
} else {
48
# set ylim based on the range of box constraints min and max
49
ylim <- range(c(constraints$min, constraints$max))
50
}
51
plot(object$weights, type="b", col="blue", axes=FALSE, xlab='', ylim=ylim, ylab="Weights", main=main, pch=16, ...)
52
if(!any(is.infinite(constraints$min))){
53
points(constraints$min, type="b", col="darkgray", lty="solid", lwd=2, pch=24)
54
}
55
if(!any(is.infinite(constraints$max))){
56
points(constraints$max, type="b", col="darkgray", lty="solid", lwd=2, pch=25)
57
}
58
# if(!is.null(neighbors)){
59
# if(is.vector(neighbors)){
60
# xtract=extractStats(object)
61
# weightcols<-grep('w\\.',colnames(xtract)) #need \\. to get the dot
62
# if(length(neighbors)==1){
63
# # overplot nearby portfolios defined by 'out'
64
# orderx = order(xtract[,"out"])
65
# subsetx = head(xtract[orderx,], n=neighbors)
66
# for(i in 1:neighbors) points(subsetx[i,weightcols], type="b", col="lightblue")
67
# } else{
68
# # assume we have a vector of portfolio numbers
69
# subsetx = xtract[neighbors,weightcols]
70
# for(i in 1:length(neighbors)) points(subsetx[i,], type="b", col="lightblue")
71
# }
72
# }
73
# if(is.matrix(neighbors) | is.data.frame(neighbors)){
74
# # the user has likely passed in a matrix containing calculated values for risk.col and return.col
75
# nbweights<-grep('w\\.',colnames(neighbors)) #need \\. to get the dot
76
# for(i in 1:nrow(neighbors)) points(as.numeric(neighbors[i,nbweights]), type="b", col="lightblue")
77
# # note that here we need to get weight cols separately from the matrix, not from xtract
78
# # also note the need for as.numeric. points() doesn't like matrix inputs
79
# }
80
# }
81
82
# points(object$weights, type="b", col="blue", pch=16)
83
axis(2, cex.axis = cex.axis, col = element.color)
84
axis(1, labels=columnnames, at=1:numassets, las=las, cex.axis = cex.axis, col = element.color)
85
box(col = element.color)
86
}
87
}
88
89
#' @rdname chart.Weights
90
#' @method chart.Weights optimize.portfolio.DEoptim
91
#' @export
92
chart.Weights.optimize.portfolio.DEoptim <- chart.Weight.DE
93
94
95
chart.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){
96
# more or less specific to the output of the DEoptim portfolio code with constraints
97
# will work to a point with other functions, such as optimize.porfolio.parallel
98
# there's still a lot to do to improve this.
99
100
if(!inherits(object, "optimize.portfolio.DEoptim")) stop("object must be of class 'optimize.portfolio.DEoptim'")
101
102
R <- object$R
103
if(is.null(R)) stop("Returns object not detected, must run optimize.portfolio with trace=TRUE")
104
portfolio <- object$portfolio
105
xtract = extractStats(object)
106
columnnames = colnames(xtract)
107
#return.column = grep(paste("objective_measures",return.col,sep='.'),columnnames)
108
return.column = pmatch(return.col,columnnames)
109
if(is.na(return.column)) {
110
return.col = paste(return.col,return.col,sep='.')
111
return.column = pmatch(return.col,columnnames)
112
}
113
#risk.column = grep(paste("objective_measures",risk.col,sep='.'),columnnames)
114
risk.column = pmatch(risk.col,columnnames)
115
if(is.na(risk.column)) {
116
risk.col = paste(risk.col,risk.col,sep='.')
117
risk.column = pmatch(risk.col,columnnames)
118
}
119
120
# if(is.na(return.column) | is.na(risk.column)) stop(return.col,' or ',risk.col, ' do not match extractStats output')
121
122
# If the user has passed in return.col or risk.col that does not match extractStats output
123
# This will give the flexibility of passing in return or risk metrics that are not
124
# objective measures in the optimization. This may cause issues with the "neighbors"
125
# functionality since that is based on the "out" column
126
if(is.na(return.column) | is.na(risk.column)){
127
return.col <- gsub("\\..*", "", return.col)
128
risk.col <- gsub("\\..*", "", risk.col)
129
warning(return.col,' or ', risk.col, ' do not match extractStats output of $objective_measures slot')
130
# Get the matrix of weights for applyFUN
131
wts_index <- grep("w.", columnnames)
132
wts <- xtract[, wts_index]
133
if(is.na(return.column)){
134
tmpret <- applyFUN(R=R, weights=wts, FUN=return.col)
135
xtract <- cbind(tmpret, xtract)
136
colnames(xtract)[which(colnames(xtract) == "tmpret")] <- return.col
137
}
138
if(is.na(risk.column)){
139
tmprisk <- applyFUN(R=R, weights=wts, FUN=risk.col)
140
xtract <- cbind(tmprisk, xtract)
141
colnames(xtract)[which(colnames(xtract) == "tmprisk")] <- risk.col
142
}
143
columnnames = colnames(xtract)
144
return.column = pmatch(return.col,columnnames)
145
if(is.na(return.column)) {
146
return.col = paste(return.col,return.col,sep='.')
147
return.column = pmatch(return.col,columnnames)
148
}
149
risk.column = pmatch(risk.col,columnnames)
150
if(is.na(risk.column)) {
151
risk.col = paste(risk.col,risk.col,sep='.')
152
risk.column = pmatch(risk.col,columnnames)
153
}
154
}
155
# print(colnames(head(xtract)))
156
157
if(chart.assets){
158
# Get the arguments from the optimize.portfolio$portfolio object
159
# to calculate the risk and return metrics for the scatter plot.
160
# (e.g. arguments=list(p=0.925, clean="boudt")
161
arguments <- NULL # maybe an option to let the user pass in an arguments list?
162
if(is.null(arguments)){
163
tmp.args <- unlist(lapply(object$portfolio$objectives, function(x) x$arguments), recursive=FALSE)
164
tmp.args <- tmp.args[!duplicated(names(tmp.args))]
165
if(!is.null(tmp.args$portfolio_method)) tmp.args$portfolio_method <- "single"
166
arguments <- tmp.args
167
}
168
# Include risk reward scatter of asset returns
169
asset_ret <- scatterFUN(R=R, FUN=return.col, arguments)
170
asset_risk <- scatterFUN(R=R, FUN=risk.col, arguments)
171
xlim <- range(c(xtract[,risk.column], asset_risk))
172
ylim <- range(c(xtract[,return.column], asset_ret))
173
} else {
174
asset_ret <- NULL
175
asset_risk <- NULL
176
}
177
178
# plot the portfolios from DEoptim_objective_results
179
plot(xtract[,risk.column],xtract[,return.column], xlab=risk.col, ylab=return.col, col="darkgray", axes=FALSE, xlim=xlim, ylim=ylim, ...)
180
181
# plot the risk-reward scatter of the assets
182
if(chart.assets){
183
points(x=asset_risk, y=asset_ret)
184
text(x=asset_risk, y=asset_ret, labels=colnames(R), pos=4, cex=0.8)
185
}
186
187
if(!is.null(neighbors)){
188
if(is.vector(neighbors)){
189
if(length(neighbors)==1){
190
# overplot nearby portfolios defined by 'out'
191
orderx = order(xtract[,"out"]) #TODO this won't work if the objective is anything other than mean
192
subsetx = head(xtract[orderx,], n=neighbors)
193
} else{
194
# assume we have a vector of portfolio numbers
195
subsetx = xtract[neighbors,]
196
}
197
points(subsetx[,risk.column], subsetx[,return.column], col="lightblue", pch=1)
198
}
199
if(is.matrix(neighbors) | is.data.frame(neighbors)){
200
# the user has likely passed in a matrix containing calculated values for risk.col and return.col
201
rtc = pmatch(return.col,columnnames)
202
if(is.na(rtc)) {
203
rtc = pmatch(paste(return.col,return.col,sep='.'),columnnames)
204
}
205
rsc = pmatch(risk.col,columnnames)
206
if(is.na(rsc)) {
207
risk.column = pmatch(paste(risk.col,risk.col,sep='.'),columnnames)
208
}
209
for(i in 1:nrow(neighbors)) points(neighbors[i,rsc], neighbors[i,rtc], col="lightblue", pch=1)
210
}
211
}
212
213
# points(xtract[1,risk.column],xtract[1,return.column], col="orange", pch=16) # overplot the equal weighted (or seed)
214
#check to see if portfolio 1 is EW object$random_portoflios[1,] all weights should be the same
215
# if(!isTRUE(all.equal(object$random_portfolios[1,][1],1/length(object$random_portfolios[1,]),check.attributes=FALSE))){
216
#show both the seed and EW if they are different
217
#NOTE the all.equal comparison could fail above if the first element of the first portfolio is the same as the EW weight,
218
#but the rest is not, shouldn't happen often with real portfolios, only toy examples
219
# points(xtract[2,risk.column],xtract[2,return.column], col="green", pch=16) # overplot the equal weighted (or seed)
220
# }
221
222
## Draw solution trajectory
223
if(!is.null(R) & !is.null(portfolio)){
224
w.traj = unique(object$DEoutput$member$bestmemit)
225
rows = nrow(w.traj)
226
# Only attempt to draw trajectory if rows is greater than or equal to 1
227
# There may be some corner cases where nrow(w.traj) is equal to 0,
228
# resulting in a 'subscript out of bounds' error.
229
if(rows >= 2){
230
rr = matrix(nrow=rows, ncol=2)
231
## maybe rewrite as an apply statement by row on w.traj
232
rtc = NULL
233
rsc = NULL
234
trajnames = NULL
235
for(i in 1:rows){
236
237
w = w.traj[i,]
238
x = unlist(constrained_objective(w=w, R=R, portfolio=portfolio, trace=TRUE))
239
names(x)<-name.replace(names(x))
240
if(is.null(trajnames)) trajnames<-names(x)
241
if(is.null(rsc)){
242
rtc = pmatch(return.col,trajnames)
243
if(is.na(rtc)) {
244
rtc = pmatch(paste(return.col,return.col,sep='.'),trajnames)
245
}
246
rsc = pmatch(risk.col,trajnames)
247
if(is.na(rsc)) {
248
rsc = pmatch(paste(risk.col,risk.col,sep='.'),trajnames)
249
}
250
}
251
rr[i,1] = x[rsc] #'FIXME
252
rr[i,2] = x[rtc] #'FIXME
253
}
254
colors2 = colorRamp(c("blue","lightblue"))
255
colortrail = rgb(colors2((0:rows)/rows),maxColorValue=255)
256
for(i in 1:rows){
257
points(rr[i,1], rr[i,2], pch=1, col = colortrail[rows-i+1])
258
}
259
260
for(i in 2:rows){
261
segments(rr[i,1], rr[i,2], rr[i-1,1], rr[i-1,2],col = colortrail[rows-i+1], lty = 1, lwd = 2)
262
}
263
}
264
} else{
265
message("Trajectory cannot be drawn because return object or constraints were not passed.")
266
}
267
268
269
## @TODO: Generalize this to find column containing the "risk" metric
270
if(length(names(object)[which(names(object)=='constrained_objective')])) {
271
result.slot<-'constrained_objective'
272
} else {
273
result.slot<-'objective_measures'
274
}
275
objcols<-unlist(object[[result.slot]])
276
names(objcols)<-name.replace(names(objcols))
277
return.column = pmatch(return.col,names(objcols))
278
if(is.na(return.column)) {
279
return.col = paste(return.col,return.col,sep='.')
280
return.column = pmatch(return.col,names(objcols))
281
}
282
risk.column = pmatch(risk.col,names(objcols))
283
if(is.na(risk.column)) {
284
risk.col = paste(risk.col,risk.col,sep='.')
285
risk.column = pmatch(risk.col,names(objcols))
286
}
287
# risk and return metrics for the optimal weights if the RP object does not
288
# contain the metrics specified by return.col or risk.col
289
if(is.na(return.column) | is.na(risk.column)){
290
return.col <- gsub("\\..*", "", return.col)
291
risk.col <- gsub("\\..*", "", risk.col)
292
# warning(return.col,' or ', risk.col, ' do not match extractStats output of $objective_measures slot')
293
opt_weights <- object$weights
294
ret <- as.numeric(applyFUN(R=R, weights=opt_weights, FUN=return.col))
295
risk <- as.numeric(applyFUN(R=R, weights=opt_weights, FUN=risk.col))
296
points(risk, ret, col="blue", pch=16) #optimal
297
text(x=risk, y=ret, labels="Optimal",col="blue", pos=4, cex=0.8)
298
} else {
299
points(objcols[risk.column], objcols[return.column], col="blue", pch=16) # optimal
300
text(x=objcols[risk.column], y=objcols[return.column], labels="Optimal",col="blue", pos=4, cex=0.8)
301
}
302
axis(1, cex.axis = cex.axis, col = element.color)
303
axis(2, cex.axis = cex.axis, col = element.color)
304
box(col = element.color)
305
}
306
307
#' @rdname chart.RiskReward
308
#' @method chart.RiskReward optimize.portfolio.DEoptim
309
#' @export
310
chart.RiskReward.optimize.portfolio.DEoptim <- chart.Scatter.DE
311
312
313
charts.DE <- function(DE, risk.col, return.col, chart.assets, neighbors=NULL, main="DEoptim.Portfolios", xlim=NULL, ylim=NULL, ...){
314
# Specific to the output of the random portfolio code with constraints
315
# @TODO: check that DE is of the correct class
316
op <- par(no.readonly=TRUE)
317
layout(matrix(c(1,2)),heights=c(2,1.5),widths=1)
318
par(mar=c(4,4,4,2))
319
chart.Scatter.DE(object=DE, risk.col=risk.col, return.col=return.col, chart.assets=chart.assets, neighbors=neighbors, main=main, xlim=xlim, ylim=ylim, ...)
320
par(mar=c(2,4,0,2))
321
chart.Weight.DE(object=DE, main="", neighbors=neighbors, ...)
322
par(op)
323
}
324
325
326
#' plot method for objects of class \code{optimize.portfolio}
327
#'
328
#' Scatter and weights chart for portfolio optimizations run with trace=TRUE
329
#'
330
#' @details
331
#' \code{return.col} must be the name of a function used to compute the return metric on the random portfolio weights
332
#' \code{risk.col} must be the name of a function used to compute the risk metric on the random portfolio weights
333
#'
334
#' \code{neighbors} may be specified in three ways.
335
#' The first is as a single number of neighbors. This will extract the \code{neighbors} closest
336
#' portfolios in terms of the \code{out} numerical statistic.
337
#' The second method consists of a numeric vector for \code{neighbors}.
338
#' This will extract the \code{neighbors} with portfolio index numbers that correspond to the vector contents.
339
#' The third method for specifying \code{neighbors} is to pass in a matrix.
340
#' This matrix should look like the output of \code{\link{extractStats}}, and should contain
341
#' \code{risk.col},\code{return.col}, and weights columns all properly named.
342
#'
343
#' The ROI and GenSA solvers do not store the portfolio weights like DEoptim or random
344
#' portfolios, random portfolios can be generated for the scatter plot with the
345
#' \code{rp} argument.
346
#'
347
#' @param x set of portfolios created by \code{\link{optimize.portfolio}}
348
#' @param \dots any other passthru parameters
349
#' @param rp TRUE/FALSE to plot feasible portfolios generated by \code{\link{random_portfolios}}
350
#' @param return.col string name of column to use for returns (vertical axis)
351
#' @param risk.col string name of column to use for risk (horizontal axis)
352
#' @param chart.assets TRUE/FALSE to include risk-return scatter of assets
353
#' @param neighbors set of 'neighbor portfolios to overplot
354
#' @param main an overall title for the plot: see \code{\link{title}}
355
#' @param xlim set the limit on coordinates for the x-axis
356
#' @param ylim set the limit on coordinates for the y-axis
357
#' @param element.color provides the color for drawing less-important chart elements, such as the box lines, axis lines, etc.
358
#' @param cex.axis the magnification to be used for axis annotation relative to the current setting of \code{cex}.
359
#' @rdname plot
360
#' @method plot optimize.portfolio.DEoptim
361
#' @export
362
plot.optimize.portfolio.DEoptim <- function(x, ..., return.col='mean', risk.col='ES', chart.assets=FALSE, neighbors=NULL, main='optimized portfolio plot', xlim=NULL, ylim=NULL) {
363
charts.DE(DE=x, risk.col=risk.col, return.col=return.col, chart.assets=chart.assets, neighbors=neighbors, main=main, xlim=xlim, ylim=ylim, ...)
364
}
365
366