Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
braverock
GitHub Repository: braverock/portfolioanalytics
Path: blob/master/R/charts.risk.R
1433 views
1
2
#' Generic method to chart risk contribution
3
#'
4
#' This function is the generic method to chart risk budget objectives for
5
#' \code{optimize.portfolio}, \code{optimize.portfolio.rebalancing}, and
6
#' \code{opt.list} objects. This function charts the contribution or percent
7
#' contribution of the resulting objective measures of a
8
#' \code{risk_budget_objective}. The risk contributions for \code{optimize.portfolio.rebalancing}
9
#' objects are plotted through time with \code{\link[PerformanceAnalytics]{chart.StackedBar}}.
10
#'
11
#' @details
12
#' \code{neighbors} may be specified in three ways.
13
#' The first is as a single number of neighbors. This will extract the
14
#' \code{neighbors} closest to the portfolios in terms of the \code{out}
15
#' numerical statistic.
16
#' The second method consists of a numeric vector for \code{neighbors}.
17
#' This will extract the \code{neighbors} with portfolio index numbers that
18
#' correspond to the vector contents.
19
#' The third method for specifying \code{neighbors} is to pass in a matrix.
20
#' This matrix should look like the output of \code{\link{extractStats}}, and
21
#' should contain properly named contribution and pct_contrib columns.
22
#'
23
#' @param object optimal portfolio object created by \code{\link{optimize.portfolio}}
24
#' or \code{\link{optimize.portfolio.rebalancing}}
25
#' @param \dots any other passthru parameters to \code{\link{plot}}
26
#' @param neighbors risk contribution or pct_contrib of neighbor portfolios to be plotted, see Details.
27
#' @param match.col string of risk column to match. The \code{opt.list} object
28
#' may contain risk budgets for ES or StdDev and this will match the proper
29
#' column names of the objectives list outp (e.g. ES.contribution).
30
#' @param risk.type "absolute" or "percentage" to plot risk contribution in absolute terms or percentage contribution.
31
#' @param regime integer of the regime number. For use with
32
#' \code{\link{optimize.portfolio.rebalancing}} run with regime switching portfolios.
33
#' @param main main title for the chart.
34
#' @param plot.type "line" or "barplot".
35
#' @param ylab label for the y-axis.
36
#' @param xlab label for the x-axis.
37
#' @param cex.axis the magnification to be used for axis annotation relative to the current setting of \code{cex}.
38
#' @param cex.lab the magnification to be used for axis annotation relative to the current setting of \code{cex}.
39
#' @param element.color provides the color for drawing less-important chart elements, such as the box lines, axis lines, etc.
40
#' @param las numeric in \{0,1,2,3\}; the style of axis labels
41
#' \describe{
42
#' \item{0:}{always parallel to the axis [\emph{default}],}
43
#' \item{1:}{always horizontal,}
44
#' \item{2:}{always perpendicular to the axis,}
45
#' \item{3:}{always vertical.}
46
#' }
47
#' @param ylim set the y-axis limit, same as in \code{\link{plot}}
48
#' @param colorset color palette or vector of colors to use
49
#' @param legend.loc legend.loc NULL, "topright", "right", or "bottomright". If legend.loc is NULL, the legend will not be plotted
50
#' @param cex.legend The magnification to be used for the legend relative to the current setting of \code{cex}
51
#' @seealso \code{\link{optimize.portfolio}} \code{\link{optimize.portfolio.rebalancing}} \code{\link[PerformanceAnalytics]{chart.StackedBar}}
52
#' @export
53
chart.RiskBudget <- function(object, ...){
54
UseMethod("chart.RiskBudget")
55
}
56
57
#' @rdname chart.RiskBudget
58
#' @method chart.RiskBudget optimize.portfolio
59
60
#' @export
61
chart.RiskBudget.optimize.portfolio <- function(object, ..., neighbors=NULL, risk.type="absolute", main="Risk Contribution", ylab="", xlab=NULL, cex.axis=0.8, cex.lab=0.8, element.color="darkgray", las=3, ylim=NULL){
62
if(!inherits(object, "optimize.portfolio")) stop("object must be of class optimize.portfolio")
63
portfolio <- object$portfolio
64
# class of each objective
65
obj_class <- sapply(portfolio$objectives, function(x) class(x)[1])
66
67
if(!("risk_budget_objective" %in% obj_class)) print("no risk_budget_objective")
68
69
# Get the index number of the risk_budget_objectives
70
rb_idx <- which(obj_class == "risk_budget_objective")
71
72
if(length(rb_idx) > 1) message(paste(length(rb_idx), "risk_budget_objectives, generating multiple plots."))
73
74
# list to store $contribution values
75
contrib <- list()
76
77
# list to store $pct_contrib values
78
pct_contrib <- list()
79
80
idx <- NULL
81
for(i in 1:length(object$objective_measures)){
82
if(length(object$objective_measures[[i]]) > 1){
83
# we have an objective measure with contribution and pct_contrib
84
contrib[[i]] <- object$objective_measures[[i]][2]
85
pct_contrib[[i]] <- object$objective_measures[[i]][3]
86
idx <- c(idx, i)
87
}
88
}
89
90
columnnames <- names(object$weights)
91
numassets <- length(columnnames)
92
93
if(is.null(xlab))
94
minmargin = 3
95
else
96
minmargin = 5
97
if(main=="") topmargin=1 else topmargin=4
98
if(las > 1) {# set the bottom border to accommodate labels
99
bottommargin = max(c(minmargin, (strwidth(columnnames,units="in"))/par("cin")[1])) * cex.lab
100
if(bottommargin > 10 ) {
101
bottommargin<-10
102
columnnames<-substr(columnnames,1,19)
103
# par(srt=45) #TODO figure out how to use text() and srt to rotate long labels
104
}
105
}
106
else {
107
bottommargin = minmargin
108
}
109
par(mar = c(bottommargin, 4, topmargin, 2) +.1)
110
111
if(risk.type == "absolute"){
112
for(ii in 1:length(idx)){
113
if(is.null(ylim)){
114
ylim <- range(contrib[[idx[ii]]][[1]])
115
ylim[1] <- min(0, ylim[1])
116
ylim[2] <- ylim[2] * 1.15
117
}
118
objname <- portfolio$objectives[[rb_idx[i]]]$name
119
# Plot values of contribution
120
plot(contrib[[idx[ii]]][[1]], type="n", axes=FALSE, xlab="", ylim=ylim, ylab=paste(objname, "Contribution", sep=" "), main=main, cex.lab=cex.lab, ...)
121
122
# neighbors needs to be in the loop if there is more than one risk_budget_objective
123
if(!is.null(neighbors)){
124
if(is.vector(neighbors)){
125
xtract <- extractStats(object)
126
riskcols <- grep(paste(objname, "contribution", sep="."), colnames(xtract))
127
if(length(riskcols) == 0) stop("Could not extract risk column")
128
if(length(neighbors) == 1){
129
# overplot nearby portfolios defined by 'out'
130
orderx <- order(xtract[,"out"])
131
subsetx <- head(xtract[orderx,], n=neighbors)
132
for(i in 1:neighbors) points(subsetx[i, riskcols], type="b", col="lightblue")
133
} else {
134
# assume we have a vector of portfolio numbers
135
subsetx <- xtract[neighbors, riskcols]
136
for(i in 1:length(neighbors)) points(subsetx[i,], type="b", col="lightblue")
137
}
138
} # end if neighbors is a vector
139
if(is.matrix(neighbors) | is.data.frame(neighbors)){
140
# the user has likely passed in a matrix containing calculated values for contrib or pct_contrib
141
nbriskcol <- grep(paste(objname, "contribution", sep="."), colnames(neighbors))
142
if(length(nbriskcol) == 0) stop(paste("must have '", objname,".contribution' as column name in neighbors",sep=""))
143
if(length(nbriskcol) != numassets) stop("number of 'contribution' columns must equal number of assets")
144
for(i in 1:nrow(neighbors)) points(as.numeric(neighbors[i, nbriskcol]), type="b", col="lightblue")
145
# note that here we need to get risk cols separately from the matrix, not from xtract
146
# also note the need for as.numeric. points() doesn't like matrix inputs
147
} # end neighbors plot for matrix or data.frame
148
} # end if neighbors is not null
149
points(contrib[[idx[ii]]][[1]], type="b", ...)
150
axis(2, cex.axis = cex.axis, col = element.color)
151
axis(1, labels=columnnames, at=1:numassets, las=las, cex.axis = cex.axis, col = element.color)
152
box(col = element.color)
153
} # end for loop of risk_budget_objective
154
} # end plot for absolute risk.type
155
156
if(risk.type %in% c("percent", "percentage", "pct_contrib")){
157
for(ii in 1:length(rb_idx)){
158
min_prisk <- portfolio$objectives[[rb_idx[ii]]]$min_prisk
159
max_prisk <- portfolio$objectives[[rb_idx[ii]]]$max_prisk
160
if(is.null(ylim)){
161
#ylim <- range(c(max_prisk, pct_contrib[[i]][[1]]))
162
#ylim[1] <- min(0, ylim[1])
163
#ylim[2] <- ylim[2] * 1.15
164
ylim <- c(0, 1)
165
}
166
objname <- portfolio$objectives[[rb_idx[i]]]$name
167
# plot percentage contribution
168
plot(pct_contrib[[idx[ii]]][[1]], type="n", axes=FALSE, xlab='', ylim=ylim, ylab=paste(objname, " % Contribution", sep=" "), main=main, cex.lab=cex.lab, ...)
169
# Check for minimum percentage risk (min_prisk) argument
170
if(!is.null(min_prisk)){
171
points(min_prisk, type="b", col="darkgray", lty="solid", lwd=2, pch=24)
172
}
173
if(!is.null(max_prisk)){
174
points(max_prisk, type="b", col="darkgray", lty="solid", lwd=2, pch=25)
175
}
176
177
# neighbors needs to be in the loop if there is more than one risk_budget_objective
178
if(!is.null(neighbors)){
179
if(is.vector(neighbors)){
180
xtract <- extractStats(object)
181
if(risk.type == "absolute"){
182
riskcols <- grep(paste(objname, "contribution", sep="."), colnames(xtract))
183
} else if(risk.type %in% c("percent", "percentage", "pct_contrib")){
184
riskcols <- grep(paste(objname, "pct_contrib", sep="."), colnames(xtract))
185
}
186
if(length(riskcols) == 0) stop("Could not extract risk column")
187
if(length(neighbors) == 1){
188
# overplot nearby portfolios defined by 'out'
189
orderx <- order(xtract[,"out"])
190
subsetx <- head(xtract[orderx,], n=neighbors)
191
for(i in 1:neighbors) points(subsetx[i, riskcols], type="b", col="lightblue")
192
} else {
193
# assume we have a vector of portfolio numbers
194
subsetx <- xtract[neighbors, riskcols]
195
for(i in 1:length(neighbors)) points(subsetx[i,], type="b", col="lightblue")
196
}
197
} # end if neighbors is a vector
198
if(is.matrix(neighbors) | is.data.frame(neighbors)){
199
# the user has likely passed in a matrix containing calculated values for contrib or pct_contrib
200
nbriskcol <- grep(paste(objname, "pct_contrib", sep="."), colnames(neighbors))
201
if(length(nbriskcol) == 0) stop(paste("must have '", objname,".pct_contrib' as column name in neighbors",sep=""))
202
if(length(nbriskcol) != numassets) stop("number of 'pct_contrib' columns must equal number of assets")
203
for(i in 1:nrow(neighbors)) points(as.numeric(neighbors[i, nbriskcol]), type="b", col="lightblue")
204
# note that here we need to get risk cols separately from the matrix, not from xtract
205
# also note the need for as.numeric. points() doesn't like matrix inputs
206
} # end neighbors plot for matrix or data.frame
207
} # end if neighbors is not null
208
points(pct_contrib[[idx[ii]]][[1]], type="b", ...)
209
axis(2, cex.axis = cex.axis, col = element.color)
210
axis(1, labels=columnnames, at=1:numassets, las=las, cex.axis = cex.axis, col = element.color)
211
box(col = element.color)
212
} # end for loop of risk_budget_objective
213
} # end plot for pct_contrib risk.type
214
}
215
216
#' @rdname chart.RiskBudget
217
#' @method chart.RiskBudget optimize.portfolio.rebalancing
218
219
#' @export
220
chart.RiskBudget.optimize.portfolio.rebalancing <- function(object, ..., match.col="ES", risk.type="absolute", regime=NULL, main="Risk Contribution"){
221
222
# Get the objective measures at each rebalance period
223
rebal.obj <- extractObjectiveMeasures(object)
224
225
if(inherits(object$portfolio, "regime.portfolios")){
226
# If the optimize.portfolio.rebalancing object is run with regime switching,
227
# the output of extractObjectiveMeasures is a list of length N where each
228
# element is the objective measures of the corresponding regime. (i.e.
229
# rebal.obj[[1]] is the objective measures for portfolio 1 with regime 1)
230
if(is.null(regime)) regime=1
231
rebal.obj <- rebal.obj[[regime]]
232
}
233
234
if(risk.type == "absolute"){
235
rbcols <- grep(paste(match.col, "contribution", sep="."), colnames(rebal.obj))
236
if(length(rbcols) < 1) stop(paste("No ", match.col, ".contribution columns.", sep=""))
237
rbdata <- rebal.obj[, rbcols]
238
colnames(rbdata) <- gsub("^.*\\.", "", colnames(rbdata))
239
chart.StackedBar(w=rbdata, ylab=paste(match.col, "Contribution", sep=" "), main=main, ...)
240
}
241
242
if(risk.type %in% c("percent", "percentage", "pct_contrib")){
243
rbcols <- grep(paste(match.col, "pct_contrib", sep="."), colnames(rebal.obj))
244
if(length(rbcols) < 1) stop(paste("No ", match.col, ".pct_contrib columns.", sep=""))
245
rbdata <- rebal.obj[, rbcols]
246
colnames(rbdata) <- gsub("^.*\\.", "", colnames(rbdata))
247
chart.StackedBar(w=rbdata, ylab=paste(match.col, "% Contribution", sep=" "), main=main, ...)
248
}
249
}
250
251
252
#' @rdname chart.RiskBudget
253
#' @method chart.RiskBudget opt.list
254
255
#' @export
256
chart.RiskBudget.opt.list <- function(object, ..., match.col="ES", risk.type="absolute", main="Risk Budget", plot.type="line", cex.axis=0.8, cex.lab=0.8, element.color="darkgray", las=3, ylim=NULL, colorset=NULL, legend.loc=NULL, cex.legend=0.8){
257
if(!inherits(object, "opt.list")) stop("object must be of class 'opt.list'")
258
259
if(plot.type %in% c("bar", "barplot")){
260
barplotRiskBudget(object=object, ...=..., match.col=match.col, risk.type=risk.type, main=main, ylim=ylim, cex.axis=cex.axis, cex.lab=cex.lab, element.color=element.color, las=las, colorset=colorset, legend.loc=legend.loc, cex.legend=cex.legend)
261
} else if(plot.type == "line"){
262
263
xtract <- extractObjectiveMeasures(object)
264
265
if(risk.type == "absolute"){
266
# get the index of columns with risk budget
267
rbcols <- grep(paste(match.col, "contribution", sep="."), colnames(xtract))
268
dat <- na.omit(xtract[, rbcols])
269
if(ncol(dat) < 1) stop("No data to plot after na.omit")
270
opt_names <- rownames(dat)
271
# remove everything up to the last dot (.) to extract the names
272
colnames(dat) <- gsub("(.*)\\.", "", colnames(dat))
273
274
# set the colors
275
if(is.null(colorset)) colorset <- 1:nrow(dat)
276
columnnames <- colnames(dat)
277
numassets <- length(columnnames)
278
279
xlab <- NULL
280
if(is.null(xlab))
281
minmargin <- 3
282
else
283
minmargin <- 5
284
if(main=="") topmargin=1 else topmargin=4
285
if(las > 1) {# set the bottom border to accommodate labels
286
bottommargin = max(c(minmargin, (strwidth(columnnames,units="in"))/par("cin")[1])) * cex.lab
287
if(bottommargin > 10 ) {
288
bottommargin <- 10
289
columnnames<-substr(columnnames,1,19)
290
# par(srt=45) #TODO figure out how to use text() and srt to rotate long labels
291
}
292
}
293
else {
294
bottommargin = minmargin
295
}
296
par(mar = c(bottommargin, 4, topmargin, 2) +.1)
297
298
if(is.null(ylim)) ylim <- range(dat)
299
300
plot(dat[1,], type="n", ylim=ylim, xlab='', ylab=paste(match.col, "Contribution", sep=" "), main=main, cex.lab=cex.lab, axes=FALSE)
301
for(i in 1:nrow(dat)){
302
points(dat[i, ], type="b", col=colorset[i], ...) # add dots here
303
}
304
305
# set the axis
306
axis(2, cex.axis=cex.axis, col=element.color)
307
axis(1, labels=columnnames, at=1:numassets, las=las, cex.axis=cex.axis, col=element.color)
308
box(col=element.color)
309
310
# Add a legend
311
if(!is.null(legend.loc)) legend(legend.loc, legend=opt_names, col=colorset, lty=1, bty="n", cex=cex.legend)
312
}
313
314
if(risk.type %in% c("percent", "percentage", "pct_contrib")){
315
# get the index of columns with risk budget
316
rbcols <- grep(paste(match.col, "pct_contrib", sep="."), colnames(xtract))
317
dat <- na.omit(xtract[, rbcols])
318
if(ncol(dat) < 1) stop("No data to plot after na.omit")
319
opt_names <- rownames(dat)
320
# remove everything up to the last dot (.) to extract the names
321
colnames(dat) <- gsub("(.*)\\.", "", colnames(dat))
322
323
# set the colors
324
if(is.null(colorset)) colorset <- 1:nrow(dat)
325
326
columnnames <- colnames(dat)
327
numassets <- length(columnnames)
328
329
xlab <- NULL
330
if(is.null(xlab))
331
minmargin <- 3
332
else
333
minmargin <- 5
334
if(main=="") topmargin=1 else topmargin=4
335
if(las > 1) {# set the bottom border to accommodate labels
336
bottommargin = max(c(minmargin, (strwidth(columnnames,units="in"))/par("cin")[1])) * cex.lab
337
if(bottommargin > 10 ) {
338
bottommargin <- 10
339
columnnames<-substr(columnnames,1,19)
340
# par(srt=45) #TODO figure out how to use text() and srt to rotate long labels
341
}
342
}
343
else {
344
bottommargin = minmargin
345
}
346
par(mar = c(bottommargin, 4, topmargin, 2) +.1)
347
348
if(is.null(ylim)) ylim <- range(dat)
349
350
plot(dat[1,], type="n", ylim=ylim, xlab='', ylab=paste(match.col, "% Contribution", sep=" "), main=main, cex.lab=cex.lab, axes=FALSE)
351
for(i in 1:nrow(dat)){
352
points(dat[i, ], type="b", col=colorset[i], ...) # add dots here
353
}
354
355
axis(2, cex.axis=cex.axis, col=element.color)
356
axis(1, labels=columnnames, at=1:numassets, las=las, cex.axis=cex.axis, col=element.color)
357
box(col=element.color)
358
359
# Add a legend
360
if(!is.null(legend.loc)) legend(legend.loc, legend=opt_names, col=colorset, lty=1, bty="n", cex=cex.legend)
361
}
362
}
363
}
364
365
# This function is called inside chart.RiskBudget.opt.list when plot.type == "bar" or "barplot"
366
barplotRiskBudget <- function(object, ..., match.col="ES", risk.type="absolute", main="Risk Budget", cex.axis=0.8, cex.lab=0.8, element.color="darkgray", las=3, colorset=NULL, legend.loc=NULL, cex.legend=0.8){
367
if(!inherits(object, "opt.list")) stop("object must be of class 'opt.list'")
368
369
xtract <- extractObjectiveMeasures(object)
370
371
if(risk.type == "absolute"){
372
# get the index of columns with risk budget
373
rbcols <- grep(paste(match.col, "contribution", sep="."), colnames(xtract))
374
dat <- na.omit(xtract[, rbcols])
375
if(ncol(dat) < 1) stop("No data to plot after na.omit")
376
opt_names <- rownames(dat)
377
# remove everything up to the last dot (.) to extract the names
378
colnames(dat) <- gsub("(.*)\\.", "", colnames(dat))
379
380
columnnames <- colnames(dat)
381
numassets <- length(columnnames)
382
383
xlab <- NULL
384
if(is.null(xlab))
385
minmargin <- 3
386
else
387
minmargin <- 5
388
if(main=="") topmargin=1 else topmargin=4
389
if(las > 1) {# set the bottom border to accommodate labels
390
bottommargin = max(c(minmargin, (strwidth(columnnames,units="in"))/par("cin")[1])) * cex.lab
391
if(bottommargin > 10 ) {
392
bottommargin <- 10
393
columnnames<-substr(columnnames,1,19)
394
# par(srt=45) #TODO figure out how to use text() and srt to rotate long labels
395
}
396
}
397
else {
398
bottommargin = minmargin
399
}
400
par(mar = c(bottommargin, 4, topmargin, 2) +.1)
401
402
# set the colors
403
if(is.null(colorset)) colorset <- 1:nrow(dat)
404
405
# plot the data
406
barplot(dat, names.arg=columnnames, las=las, cex.names=cex.axis, xlab='', col=colorset, main=main, ylab=paste(match.col, "Contribution", sep=" "), cex.lab=cex.lab, cex.axis=cex.axis, beside=TRUE, ...)
407
408
# set the axis
409
#axis(2, cex.axis=cex.axis, col=element.color)
410
#axis(1, labels=columnnames, at=1:numassets, las=las, cex.axis=cex.axis, col=element.color)
411
box(col=element.color)
412
413
# Add a legend
414
if(!is.null(legend.loc)) legend(legend.loc, legend=opt_names, fill=colorset, bty="n", cex=cex.legend)
415
}
416
417
if(risk.type %in% c("percent", "percentage", "pct_contrib")){
418
# get the index of columns with risk budget
419
rbcols <- grep(paste(match.col, "pct_contrib", sep="."), colnames(xtract))
420
dat <- na.omit(xtract[, rbcols])
421
if(ncol(dat) < 1) stop("No data to plot after na.omit")
422
opt_names <- rownames(dat)
423
# remove everything up to the last dot (.) to extract the names
424
colnames(dat) <- gsub("(.*)\\.", "", colnames(dat))
425
426
columnnames <- colnames(dat)
427
numassets <- length(columnnames)
428
429
xlab <- NULL
430
if(is.null(xlab))
431
minmargin <- 3
432
else
433
minmargin <- 5
434
if(main=="") topmargin=1 else topmargin=4
435
if(las > 1) {# set the bottom border to accommodate labels
436
bottommargin = max(c(minmargin, (strwidth(columnnames,units="in"))/par("cin")[1])) * cex.lab
437
if(bottommargin > 10 ) {
438
bottommargin <- 10
439
columnnames<-substr(columnnames,1,19)
440
# par(srt=45) #TODO figure out how to use text() and srt to rotate long labels
441
}
442
}
443
else {
444
bottommargin = minmargin
445
}
446
par(mar = c(bottommargin, 4, topmargin, 2) +.1)
447
448
# set the colors
449
if(is.null(colorset)) colorset <- 1:nrow(dat)
450
451
# plot the data
452
barplot(dat, names.arg=columnnames, las=las, cex.names=cex.axis, col=colorset, main=main, ylab=paste(match.col, "% Contribution", sep=" "), cex.lab=cex.lab, cex.axis=cex.axis, beside=TRUE, ...)
453
454
#axis(2, cex.axis=cex.axis, col=element.color)
455
#axis(1, labels=columnnames, at=1:numassets, las=las, cex.axis=cex.axis, col=element.color)
456
box(col=element.color)
457
458
# Add a legend
459
if(!is.null(legend.loc)) legend(legend.loc, legend=opt_names, fill=colorset, bty="n", cex=cex.legend)
460
}
461
}
462
463
464
###############################################################################
465
# R (https://r-project.org/) Numeric Methods for Optimization of Portfolios
466
#
467
# Copyright (c) 2004-2021 Brian G. Peterson, Peter Carl, Ross Bennett, Kris Boudt
468
#
469
# This library is distributed under the terms of the GNU Public License (GPL)
470
# for full details see the file COPYING
471
#
472
# $Id$
473
#
474
###############################################################################
475
476