Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
braverock
GitHub Repository: braverock/portfolioanalytics
Path: blob/master/R/constrained_objective.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
# TODO add examples
14
15
# TODO add more details about the nuances of the optimization engines
16
17
18
#' @rdname constrained_objective
19
#' @name constrained_objective
20
#' @export
21
constrained_objective_v1 <- function(w, R, constraints, ..., trace=FALSE, normalize=TRUE, storage=FALSE)
22
{
23
if (ncol(R)>length(w)) {
24
R=R[,1:length(w)]
25
}
26
if(!hasArg(penalty)) penalty = 1e4
27
N = length(w)
28
T = nrow(R)
29
if(hasArg(optimize_method))
30
optimize_method=match.call(expand.dots=TRUE)$optimize_method else optimize_method=''
31
if(hasArg(verbose))
32
verbose=match.call(expand.dots=TRUE)$verbose
33
else verbose=FALSE
34
35
# check for valid constraints
36
if (!is.constraint(constraints)) {
37
stop("constraints passed in are not of class constraint")
38
}
39
40
# check that the constraints and the weighting vector have the same length
41
if (N != length(constraints$assets)){
42
warning("length of constraints asset list and weights vector do not match, results may be bogus")
43
}
44
45
out=0
46
47
# do the get here
48
store_output <- try(get('.objectivestorage',envir=.storage),silent=TRUE)
49
if(inherits(store_output,"try-error")) storage=FALSE else storage=TRUE
50
51
if(isTRUE(normalize)){
52
if(!is.null(constraints$min_sum) | !is.null(constraints$max_sum)){
53
# the user has passed in either min_sum or max_sum constraints for the portfolio, or both.
54
# we'll normalize the weights passed in to whichever boundary condition has been violated
55
# NOTE: this means that the weights produced by a numeric optimization algorithm like DEoptim
56
# might violate your constraints, so you'd need to renormalize them after optimizing
57
# we'll create functions for that so the user is less likely to mess it up.
58
59
# NOTE: need to normalize in the optimization wrapper too before we return, since we've normalized in here
60
# In Kris' original function, this was manifested as a full investment constraint
61
# the normalization process produces much faster convergence,
62
# and then we penalize parameters outside the constraints in the next block
63
if(!is.null(constraints$max_sum) & constraints$max_sum != Inf ) {
64
max_sum=constraints$max_sum
65
if(sum(w)>max_sum) { w<-(max_sum/sum(w))*w } # normalize to max_sum
66
}
67
68
if(!is.null(constraints$min_sum) & constraints$min_sum != -Inf ) {
69
min_sum=constraints$min_sum
70
if(sum(w)<min_sum) { w<-(min_sum/sum(w))*w } # normalize to min_sum
71
}
72
73
} # end min_sum and max_sum normalization
74
} else {
75
# the user wants the optimization algorithm to figure it out
76
if(!is.null(constraints$max_sum) & constraints$max_sum != Inf ) {
77
max_sum=constraints$max_sum
78
if(sum(w)>max_sum) { out = out + penalty*(sum(w) - max_sum) } # penalize difference to max_sum
79
}
80
if(!is.null(constraints$min_sum) & constraints$min_sum != -Inf ) {
81
min_sum=constraints$min_sum
82
if(sum(w)<min_sum) { out = out + penalty*(min_sum - sum(w)) } # penalize difference to min_sum
83
}
84
}
85
86
# penalize weights outside my constraints (can be caused by normalization)
87
if (!is.null(constraints$max)){
88
max = constraints$max
89
out = out + sum(w[which(w>max[1:N])]- constraints$max[which(w>max[1:N])])*penalty
90
}
91
if (!is.null(constraints$min)){
92
min = constraints$min
93
out = out + sum(constraints$min[which(w<min[1:N])] - w[which(w<min[1:N])])*penalty
94
}
95
96
nargs <-list(...)
97
if(length(nargs)==0) nargs=NULL
98
if (length('...')==0 | is.null('...')) {
99
# rm('...')
100
nargs=NULL
101
}
102
103
nargs<-set.portfolio.moments(R, constraints, momentargs=nargs)
104
105
if(is.null(constraints$objectives)) {
106
warning("no objectives specified in constraints")
107
} else{
108
if(isTRUE(trace) | isTRUE(storage)) tmp_return<-list()
109
for (objective in constraints$objectives){
110
#check for clean bits to pass in
111
if(objective$enabled){
112
tmp_measure = NULL
113
multiplier = objective$multiplier
114
#if(is.null(objective$arguments) | !is.list(objective$arguments)) objective$arguments<-list()
115
switch(objective$name,
116
mean =,
117
median = {
118
fun = match.fun(objective$name)
119
nargs$x <- ( R %*% w ) #do the multivariate mean/median with Kroneker product
120
},
121
sd =,
122
StdDev = {
123
fun= match.fun(StdDev)
124
},
125
mVaR =,
126
VaR = {
127
fun= match.fun(VaR)
128
if(!inherits(objective,"risk_budget_objective") & is.null(objective$arguments$portfolio_method) & is.null(nargs$portfolio_method)) nargs$portfolio_method='single'
129
if(is.null(objective$arguments$invert)) objective$arguments$invert = FALSE
130
},
131
es =,
132
mES =,
133
CVaR =,
134
cVaR =,
135
ES = {
136
fun = match.fun(ES)
137
if(!inherits(objective,"risk_budget_objective") & is.null(objective$arguments$portfolio_method)& is.null(nargs$portfolio_method)) nargs$portfolio_method='single'
138
if(is.null(objective$arguments$invert)) objective$arguments$invert = FALSE
139
},
140
turnover = {
141
fun = match.fun(turnover) # turnover function included in objectiveFUN.R
142
},
143
{ # see 'S Programming p. 67 for this matching
144
fun<-try(match.fun(objective$name))
145
}
146
)
147
if(is.function(fun)){
148
.formals <- formals(fun)
149
onames <- names(.formals)
150
if(is.list(objective$arguments)){
151
#TODO FIXME only do this if R and weights are in the argument list of the fn
152
if(is.null(nargs$R) | !length(nargs$R)==length(R)) nargs$R <- R
153
154
if(is.null(nargs$weights)) nargs$weights <- w
155
156
pm <- pmatch(names(objective$arguments), onames, nomatch = 0L)
157
if (any(pm == 0L))
158
warning(paste("some arguments stored for",objective$name,"do not match"))
159
# this line overwrites the names of things stored in $arguments with names from formals.
160
# I'm not sure it's a good idea, so commenting for now, until we prove we need it
161
#names(objective$arguments[pm > 0L]) <- onames[pm]
162
.formals[pm] <- objective$arguments[pm > 0L]
163
#now add dots
164
if (length(nargs)) {
165
dargs<-nargs
166
pm <- pmatch(names(dargs), onames, nomatch = 0L)
167
names(dargs[pm > 0L]) <- onames[pm]
168
.formals[pm] <- dargs[pm > 0L]
169
}
170
.formals$... <- NULL
171
}
172
} # TODO do some funky return magic here on try-error
173
174
tmp_measure = try((do.call(fun,.formals)) ,silent=TRUE)
175
176
if(isTRUE(trace) | isTRUE(storage)) {
177
if(is.null(names(tmp_measure))) names(tmp_measure)<-objective$name
178
tmp_return[[objective$name]]<-tmp_measure
179
}
180
181
if(inherits(tmp_measure,"try-error")) {
182
message(paste("objective name",objective$name,"generated an error or warning:",tmp_measure))
183
}
184
185
# now set the new value of the objective output
186
if(inherits(objective,"return_objective")){
187
if (!is.null(objective$target) & is.numeric(objective$target)){ # we have a target
188
out = out + penalty*abs(objective$multiplier)*abs(tmp_measure-objective$target)
189
}
190
# target is null or doesn't exist, just maximize, or minimize violation of constraint
191
out = out + objective$multiplier*tmp_measure
192
} # end handling for return objectives
193
194
if(inherits(objective,"portfolio_risk_objective")){
195
if (!is.null(objective$target) & is.numeric(objective$target)){ # we have a target
196
out = out + penalty*abs(objective$multiplier)*abs(tmp_measure-objective$target)
197
#should we also penalize risk too low for risk targets? or is a range another objective?
198
# # half penalty for risk lower than target
199
# if( prw < (.9*Riskupper) ){ out = out + .5*(penalty*( prw - Riskupper)) }
200
}
201
# target is null or doesn't exist, just maximize, or minimize violation of constraint
202
out = out + abs(objective$multiplier)*tmp_measure
203
} # univariate risk objectives
204
205
if(inherits(objective,"turnover_objective")){
206
if (!is.null(objective$target) & is.numeric(objective$target)){ # we have a target
207
out = out + penalty*abs(objective$multiplier)*abs(tmp_measure-objective$target)
208
}
209
# target is null or doesn't exist, just maximize, or minimize violation of constraint
210
out = out + abs(objective$multiplier)*tmp_measure
211
} # univariate turnover objectives
212
213
if(inherits(objective,"minmax_objective")){
214
if (!is.null(objective$min) & !is.null(objective$max)){ # we have a min and max
215
if(tmp_measure > objective$max){
216
out = out + penalty * objective$multiplier * (tmp_measure - objective$max)
217
}
218
if(tmp_measure < objective$min){
219
out = out + penalty * objective$multiplier * (objective$min - tmp_measure)
220
}
221
}
222
} # temporary minmax objective
223
224
if(inherits(objective,"risk_budget_objective")){
225
# setup
226
227
# out = out + penalty*sum( (percrisk-RBupper)*( percrisk > RBupper ),na.rm=TRUE ) + penalty*sum( (RBlower-percrisk)*( percrisk < RBlower ),na.rm=TRUE )
228
# add risk budget constraint
229
if(!is.null(objective$target) & is.numeric(objective$target)){
230
#in addition to a risk budget constraint, we have a univariate target
231
# the first element of the returned list is the univariate measure
232
# we'll use the univariate measure exactly like we would as a separate objective
233
out = out + penalty*abs(objective$multiplier)*abs(tmp_measure[[1]]-objective$target)
234
#should we also penalize risk too low for risk targets? or is a range another objective?
235
# # half penalty for risk lower than target
236
# if( prw < (.9*Riskupper) ){ out = out + .5*(penalty*( prw - Riskupper)) }
237
}
238
percrisk = tmp_measure[[3]] # third element is percent component contribution
239
RBupper = objective$max_prisk
240
RBlower = objective$min_prisk
241
if(!is.null(RBupper) | !is.null(RBlower)){
242
out = out + penalty * objective$multiplier * sum( (percrisk-RBupper)*( percrisk > RBupper ),na.rm=TRUE ) + penalty*sum( (RBlower-percrisk)*( percrisk < RBlower ),na.rm=TRUE )
243
}
244
# if(!is.null(objective$min_concentration)){
245
# if(isTRUE(objective$min_concentration)){
246
# max_conc<-max(tmp_measure[[2]]) #second element is the contribution in absolute terms
247
# # out=out + penalty * objective$multiplier * max_conc
248
# out = out + objective$multiplier * max_conc
249
# }
250
# }
251
# Combined min_con and min_dif to take advantage of a better concentration obj measure
252
if(!is.null(objective$min_difference) || !is.null(objective$min_concentration)){
253
if(isTRUE(objective$min_difference)){
254
# max_diff<-max(tmp_measure[[2]]-(sum(tmp_measure[[2]])/length(tmp_measure[[2]]))) #second element is the contribution in absolute terms
255
# Uses Herfindahl index to calculate concentration; added scaling perc diffs back to univariate numbers
256
max_diff <- sqrt(sum(tmp_measure[[3]]^2))/100 #third element is the contribution in percentage terms
257
# out = out + penalty * objective$multiplier * max_diff
258
out = out + penalty*objective$multiplier * max_diff
259
}
260
}
261
} # end handling of risk_budget objective
262
263
} # end enabled check
264
} # end loop over objectives
265
} # end objectives processing
266
267
if(isTRUE(verbose)) {
268
print('weights: ')
269
print(paste(w,' '))
270
print(paste("output of objective function",out))
271
print(unlist(tmp_return))
272
}
273
274
if(is.na(out) | is.nan(out) | is.null(out)){
275
#this should never happen
276
warning('NA or NaN produced in objective function for weights ',w)
277
out<-penalty
278
}
279
280
#return
281
if (isTRUE(storage)){
282
#add the new objective results
283
store_output[[length(store_output)+1]]<-list(out=as.numeric(out),weights=w,objective_measures=tmp_return)
284
# do the assign here
285
assign('.objectivestorage', store_output, envir=.storage)
286
}
287
if(!isTRUE(trace)){
288
return(out)
289
} else {
290
return(list(out=as.numeric(out),weights=w,objective_measures=tmp_return))
291
}
292
}
293
294
#' calculate a numeric return value for a portfolio based on a set of constraints and objectives
295
#'
296
#' Function to calculate a numeric return value for a portfolio based on a set of constraints and objectives.
297
#' We'll try to make as few assumptions as possible and only run objectives that are enabled by the user.
298
#'
299
#' If the user has passed in either min_sum or max_sum constraints for the portfolio, or both,
300
#' and are using a numerical optimization method like DEoptim, and normalize=TRUE,
301
#' we'll normalize the weights passed in to whichever boundary condition has been violated.
302
#' If using random portfolios, all the portfolios generated will meet the constraints by construction.
303
#' NOTE: this means that the weights produced by a numeric optimization algorithm like DEoptim, pso, or GenSA
304
#' might violate constraints, and will need to be renormalized after optimizing.
305
#' We apply the same normalization in \code{\link{optimize.portfolio}} so that the weights you see have been
306
#' normalized to min_sum if the generated portfolio is smaller than min_sum or max_sum if the
307
#' generated portfolio is larger than max_sum.
308
#' This normalization increases the speed of optimization and convergence by several orders of magnitude in many cases.
309
#'
310
#' You may find that for some portfolios, normalization is not desirable, if the algorithm
311
#' cannot find a direction in which to move to head towards an optimal portfolio. In these cases,
312
#' it may be best to set normalize=FALSE, and penalize the portfolios if the sum of the weighting
313
#' vector lies outside the min_sum and/or max_sum.
314
#'
315
#' Whether or not we normalize the weights using min_sum and max_sum, and are using a numerical optimization
316
#' engine like DEoptim, we will penalize portfolios that violate weight constraints in much the same way
317
#' we penalize other constraints. If a min_sum/max_sum normalization has not occurred, convergence
318
#' can take a very long time. We currently do not allow for a non-normalized full investment constraint.
319
#' Future version of this function could include this additional constraint penalty.
320
#'
321
#' When you are optimizing a return objective, you must specify a negative multiplier
322
#' for the return objective so that the function will maximize return. If you specify a target return,
323
#' any return that deviates from your target will be penalized. If you do not specify a target return,
324
#' you may need to specify a negative VTR (value to reach) , or the function will not converge.
325
#' Try the maximum expected return times the multiplier (e.g. -1 or -10).
326
#' Adding a return objective defaults the multiplier to -1.
327
#'
328
#' Additional parameters for other solvers
329
#' (e.g. random portfolios or
330
#' \code{\link[DEoptim]{DEoptim.control}} or pso or GenSA
331
#' may be passed in via \dots
332
#'
333
#'
334
#' @param R an xts, vector, matrix, data frame, timeSeries or zoo object of asset returns.
335
#' @param w a vector of weights to test.
336
#' @param portfolio an object of class \code{portfolio} specifying the constraints and objectives for the optimization, see \code{\link{portfolio}}.
337
#' @param \dots any other passthru parameters.
338
#' @param trace TRUE/FALSE whether to include debugging and additional detail in the output list. The default is FALSE. Several charting functions require that \code{trace=TRUE}.
339
#' @param normalize TRUE/FALSE whether to normalize results to min/max sum (TRUE), or let the optimizer penalize portfolios that do not conform (FALSE)
340
#' @param storage TRUE/FALSE default TRUE for DEoptim with trace, otherwise FALSE. not typically user-called.
341
#' @param constraints a v1_constraint object for backwards compatibility with \code{constrained_objective_v1}.
342
#' @param env environment of moments calculated in \code{optimize.portfolio}
343
#' @seealso \code{\link{constraint}}, \code{\link{objective}}, \code{\link[DEoptim]{DEoptim.control}}
344
#' @author Kris Boudt, Peter Carl, Brian G. Peterson, Ross Bennett
345
#' @aliases constrained_objective constrained_objective_v1 constrained_objective_v2
346
#' @rdname constrained_objective
347
#' @export constrained_objective
348
#' @export constrained_objective_v2
349
constrained_objective <- constrained_objective_v2 <- function(w, R, portfolio, ..., trace=FALSE, normalize=TRUE, storage=FALSE, env=NULL)
350
{
351
if (ncol(R) > length(w)) {
352
R <- R[ ,1:length(w)]
353
}
354
if(!hasArg(penalty)) penalty <- 1e4
355
N <- length(w)
356
T <- nrow(R)
357
if(hasArg(optimize_method))
358
optimize_method <- match.call(expand.dots=TRUE)$optimize_method else optimize_method <- ''
359
if(hasArg(verbose))
360
verbose <- match.call(expand.dots=TRUE)$verbose
361
else verbose <- FALSE
362
363
# initial weights
364
init_weights <- w
365
366
# get the constraints from the portfolio object
367
constraints <- get_constraints(portfolio)
368
369
# check for valid portfolio
370
if (!is.portfolio(portfolio)) {
371
stop("portfolio object passed in is not of class portfolio")
372
}
373
374
# check that the assets and the weighting vector have the same length
375
if (N != length(portfolio$assets)){
376
warning("length of portfolio asset list and weights vector do not match, results may be bogus")
377
}
378
379
out <- 0
380
381
# do the get here
382
store_output <- try(get('.objectivestorage',envir=.storage), silent=TRUE)
383
if(inherits(store_output,"try-error")) {
384
storage <- FALSE
385
# warning("could not get .objectivestorage")
386
} else {
387
storage <- TRUE
388
}
389
390
# use fn_map to normalize the weights
391
if(isTRUE(normalize)){
392
w <- fn_map(weights=w, portfolio=portfolio)$weights
393
# end fn_map transformation
394
} else {
395
# the user wants the optimization algorithm to figure it out
396
if(!is.null(constraints$max_sum) & constraints$max_sum != Inf ) {
397
max_sum <- constraints$max_sum
398
if(sum(w) > max_sum) { out <- out + penalty * (sum(w) - max_sum) } # penalize difference to max_sum
399
}
400
if(!is.null(constraints$min_sum) & constraints$min_sum != -Inf ) {
401
min_sum <- constraints$min_sum
402
if(sum(w) < min_sum) { out <- out + penalty * (min_sum - sum(w)) } # penalize difference to min_sum
403
}
404
}
405
406
# penalize weights outside min and max box constraints (can be caused by normalization)
407
if (!is.null(constraints$max)){
408
max <- constraints$max
409
# Only go to penalty term if any of the weights violate max
410
if(any(w > max)){
411
out <- out + sum(w[which(w > max[1:N])] - constraints$max[which(w > max[1:N])]) * penalty
412
}
413
}
414
if (!is.null(constraints$min)){
415
min <- constraints$min
416
# Only go to penalty term if any of the weights violate min
417
if(any(w < min)){
418
out <- out + sum(constraints$min[which(w < min[1:N])] - w[which(w < min[1:N])]) * penalty
419
}
420
}
421
422
# penalize weights that violate group constraints
423
if(!is.null(constraints$groups) & !is.null(constraints$cLO) & !is.null(constraints$cUP)){
424
groups <- constraints$groups
425
cLO <- constraints$cLO
426
cUP <- constraints$cUP
427
# Only go to penalty term if group constraint is violated
428
if(any(group_fail(w, groups, cLO, cUP))){
429
ngroups <- length(groups)
430
for(i in 1:ngroups){
431
tmp_w <- w[groups[[i]]]
432
# penalize for weights that are below cLO
433
if(sum(tmp_w) < cLO[i]){
434
out <- out + penalty * (cLO[i] - sum(tmp_w))
435
}
436
if(sum(tmp_w) > cUP[i]){
437
out <- out + penalty * (sum(tmp_w) - cUP[i])
438
}
439
}
440
}
441
} # End group constraint penalty
442
443
# penalize weights that violate max_pos constraints
444
if(!is.null(constraints$max_pos)){
445
max_pos <- constraints$max_pos
446
tolerance <- .Machine$double.eps^0.5
447
mult <- 1
448
# sum(abs(w) > tolerance) is the number of non-zero assets
449
nzassets <- sum(abs(w) > tolerance)
450
if(nzassets > max_pos){
451
# Do we need a small multiplier term here since (nzassets - max_pos)
452
# will be an integer and much larger than the weight penalty terms
453
out <- out + penalty * mult * (nzassets - max_pos)
454
}
455
} # End position_limit constraint penalty
456
457
# penalize weights that violate diversification constraint
458
if(!is.null(constraints$div_target)){
459
div_target <- constraints$div_target
460
div <- diversification(w)
461
mult <- 1
462
# only penalize if not within +/- 5% of target
463
if((div < div_target * 0.95) | (div > div_target * 1.05)){
464
out <- out + penalty * mult * abs(div - div_target)
465
}
466
} # End diversification constraint penalty
467
468
# penalize weights that violate turnover constraint
469
if(!is.null(constraints$turnover_target)){
470
turnover_target <- constraints$turnover_target
471
to <- turnover(w)
472
mult <- 1
473
# only penalize if not within +/- 5% of target
474
if((to < turnover_target * 0.95) | (to > turnover_target * 1.05)){
475
# print("transform or penalize to meet turnover target")
476
out = out + penalty * mult * abs(to - turnover_target)
477
}
478
} # End turnover constraint penalty
479
480
# penalize weights that violate return target constraint
481
if(!is.null(constraints$return_target)){
482
return_target <- constraints$return_target
483
mean_return <- port.mean(weights=w, mu=env$mu)
484
mult <- 1
485
out = out + penalty * mult * abs(mean_return - return_target)
486
} # End return constraint penalty
487
488
# penalize weights that violate factor exposure constraints
489
if(!is.null(constraints$B)){
490
t.B <- t(constraints$B)
491
lower <- constraints$lower
492
upper <- constraints$upper
493
mult <- 1
494
for(i in 1:nrow(t.B)){
495
tmpexp <- as.numeric(t(w) %*% t.B[i, ])
496
if(tmpexp < lower[i]){
497
out <- out + penalty * mult * (lower[i] - tmpexp)
498
}
499
if(tmpexp > upper[i]){
500
out <- out + penalty * mult * (tmpexp - upper[i])
501
}
502
}
503
} # End factor exposure constraint penalty
504
505
# Add penalty for transaction costs
506
if(!is.null(constraints$ptc)){
507
# calculate total transaction cost using portfolio$assets as initial set of weights
508
tc <- sum(abs(w - portfolio$assets) * constraints$ptc)
509
# for now use a multiplier of 1, may need to adjust this later
510
mult <- 1
511
out <- out + mult * tc
512
} # End transaction cost penalty
513
514
# Add penalty for leverage exposure
515
# This could potentially be added to random portfolios
516
if(!is.null(constraints$leverage)){
517
if((sum(abs(w)) > constraints$leverage)){
518
# only penalize if leverage is exceeded
519
mult <- 1/100
520
out <- out + penalty * mult * abs(sum(abs(w)) - constraints$leverage)
521
}
522
} # End leverage exposure penalty
523
524
# The "..." are passed in from optimize.portfolio and contain the output of
525
# momentFUN. The default is momentFUN=set.portfolio.moments and returns
526
# moments$mu, moments$sigma, moments$m3, moments$m4, etc. depending on the
527
# the functions corresponding to portfolio$objective$name. Would it be better
528
# to make this a formal argument for constrained_objective? This means that
529
# we completely avoid evaluating the set.portfolio.moments function. Can we
530
# trust that all the moments are correctly set in optimize.portfolio through
531
# momentFUN?
532
533
# Add R and w to the environment with the moments
534
# env$R <- R
535
# env$weights <- w
536
537
if(!is.null(env)){
538
nargs <- env
539
} else {
540
# print("calculating moments")
541
# calculating the moments
542
# nargs are used as the arguments for functions corresponding to
543
# objective$name called in the objective loop later
544
momentargs <- eval(substitute(alist(...)))
545
.formals <- formals(set.portfolio.moments)
546
.formals <- modify.args(formals=.formals, arglist=alist(momentargs=momentargs), dots=TRUE)
547
.formals <- modify.args(formals=.formals, arglist=NULL, R=R, dots=TRUE)
548
.formals <- modify.args(formals=.formals, arglist=NULL, portfolio=portfolio, dots=TRUE)
549
.formals$... <- NULL
550
# print(.formals)
551
nargs <- do.call(set.portfolio.moments, .formals)
552
}
553
554
# We should avoid modifying nargs in the loop below.
555
# If we modify nargs with something like nargs$x, nargs is copied and this
556
# should be avoided because nargs could be large because it contains the moments.
557
tmp_args <- list()
558
559
# JMU: Add all the variables in 'env' to tmp_args as names/symbols
560
# tmp_args[ls(env)] <- lapply(ls(env), as.name)
561
562
if(is.null(portfolio$objectives)) {
563
warning("no objectives specified in portfolio")
564
} else{
565
if(isTRUE(trace) | isTRUE(storage)) tmp_return <- list()
566
for (objective in portfolio$objectives){
567
#check for clean bits to pass in
568
if(objective$enabled){
569
tmp_measure <- NULL
570
multiplier <- objective$multiplier
571
#if(is.null(objective$arguments) | !is.list(objective$arguments)) objective$arguments<-list()
572
switch(objective$name,
573
mean =,
574
median = {
575
fun = match.fun(port.mean)
576
# would it be better to do crossprod(w, moments$mu)?
577
# tmp_args$x <- ( R %*% w ) #do the multivariate mean/median with Kroneker product
578
},
579
median = {
580
fun = match.fun(objective$name)
581
tmp_args$x <- ( R %*% w ) #do the multivariate mean/median with Kroneker product
582
},
583
sd =,
584
var =,
585
StdDev = {
586
fun = match.fun(StdDev)
587
},
588
mVaR =,
589
VaR = {
590
fun = match.fun(VaR)
591
if(!inherits(objective,"risk_budget_objective") & is.null(objective$arguments$portfolio_method) & is.null(nargs$portfolio_method)) tmp_args$portfolio_method='single'
592
if(is.null(objective$arguments$invert)) tmp_args$invert = FALSE
593
},
594
es =,
595
mES =,
596
CVaR =,
597
cVaR =,
598
ETL=,
599
mETL=,
600
ES = {
601
fun = match.fun(ES)
602
if(!inherits(objective,"risk_budget_objective") & is.null(objective$arguments$portfolio_method) & is.null(nargs$portfolio_method)) tmp_args$portfolio_method='single'
603
if(is.null(objective$arguments$invert)) tmp_args$invert = FALSE
604
},
605
CSM = {}, ## xinran
606
turnover = {
607
fun = match.fun(turnover) # turnover function included in objectiveFUN.R
608
},
609
{ # see 'S Programming p. 67 for this matching
610
fun <- try(match.fun(objective$name))
611
}
612
)
613
614
if(is.function(fun)){
615
.formals <- formals(fun)
616
# Add the moments from the nargs object
617
# nargs contains the moments, these are being evaluated
618
.formals <- modify.args(formals=.formals, arglist=nargs, dots=TRUE)
619
# Add anything from tmp_args
620
.formals <- modify.args(formals=.formals, arglist=tmp_args, dots=TRUE)
621
# Now add the objective$arguments
622
.formals <- modify.args(formals=.formals, arglist=objective$arguments, dots=TRUE)
623
# Add R and weights if necessary
624
if("R" %in% names(.formals)) .formals <- modify.args(formals=.formals, arglist=NULL, R=R, dots=TRUE)
625
if("weights" %in% names(.formals)) .formals <- modify.args(formals=.formals, arglist=NULL, weights=w, dots=TRUE)
626
# .formals <- modify.args(formals=.formals, arglist=tmp_args, dots=TRUE)
627
.formals$... <- NULL
628
}
629
630
# tmp_measure <- try(do.call(fun, .formals, envir=env), silent=TRUE)
631
tmp_measure <- try(do.call(fun, .formals), silent=TRUE)
632
633
if(isTRUE(trace) | isTRUE(storage)) {
634
# Subsitute 'StdDev' if the objective name is 'var'
635
# if the user passes in var as an objective name, we are actually
636
# calculating StdDev, so we need to change the name here.
637
tmp_objname <- objective$name
638
if(tmp_objname == "var") tmp_objname <- "StdDev"
639
if(is.null(names(tmp_measure))) names(tmp_measure) <- tmp_objname
640
tmp_return[[tmp_objname]] <- tmp_measure
641
}
642
643
if(inherits(tmp_measure, "try-error")) {
644
message(paste("objective name", objective$name, "generated an error or warning:", tmp_measure))
645
}
646
647
# now set the new value of the objective output
648
if(inherits(objective, "return_objective")){
649
if (!is.null(objective$target) & is.numeric(objective$target)){ # we have a target
650
out <- out + penalty*abs(objective$multiplier)*abs(tmp_measure - objective$target)
651
}
652
# target is null or doesn't exist, just maximize, or minimize violation of constraint
653
out <- out + objective$multiplier*tmp_measure
654
} # end handling for return objectives
655
656
if(inherits(objective, "portfolio_risk_objective")){
657
if (!is.null(objective$target) & is.numeric(objective$target)){ # we have a target
658
out <- out + penalty*abs(objective$multiplier)*abs(tmp_measure - objective$target)
659
#should we also penalize risk too low for risk targets? or is a range another objective?
660
# # half penalty for risk lower than target
661
# if( prw < (.9*Riskupper) ){ out = out + .5*(penalty*( prw - Riskupper)) }
662
}
663
# target is null or doesn't exist, just maximize, or minimize violation of constraint
664
out <- out + abs(objective$multiplier)*tmp_measure
665
} # univariate risk objectives
666
667
if(inherits(objective, "turnover_objective")){
668
if (!is.null(objective$target) & is.numeric(objective$target)){ # we have a target
669
out <- out + penalty*abs(objective$multiplier)*abs(tmp_measure - objective$target)
670
}
671
# target is null or doesn't exist, just maximize, or minimize violation of constraint
672
out <- out + abs(objective$multiplier)*tmp_measure
673
} # univariate turnover objectives
674
675
if(inherits(objective, "minmax_objective")){
676
if (!is.null(objective$min) & !is.null(objective$max)){ # we have a min and max
677
if(tmp_measure > objective$max){
678
out <- out + penalty * objective$multiplier * (tmp_measure - objective$max)
679
}
680
if(tmp_measure < objective$min){
681
out <- out + penalty * objective$multiplier * (objective$min - tmp_measure)
682
}
683
}
684
} # temporary minmax objective
685
686
if(inherits(objective, "risk_budget_objective")){
687
# setup
688
689
# out = out + penalty*sum( (percrisk-RBupper)*( percrisk > RBupper ),na.rm=TRUE ) + penalty*sum( (RBlower-percrisk)*( percrisk < RBlower ),na.rm=TRUE )
690
# add risk budget constraint
691
if(!is.null(objective$target) & is.numeric(objective$target)){
692
#in addition to a risk budget constraint, we have a univariate target
693
# the first element of the returned list is the univariate measure
694
# we'll use the univariate measure exactly like we would as a separate objective
695
out = out + penalty*abs(objective$multiplier)*abs(tmp_measure[[1]]-objective$target)
696
#should we also penalize risk too low for risk targets? or is a range another objective?
697
# # half penalty for risk lower than target
698
# if( prw < (.9*Riskupper) ){ out = out + .5*(penalty*( prw - Riskupper)) }
699
}
700
percrisk = tmp_measure[[3]] # third element is percent component contribution
701
RBupper = objective$max_prisk
702
RBlower = objective$min_prisk
703
if(!is.null(RBupper) | !is.null(RBlower)){
704
out = out + penalty * objective$multiplier * sum( (percrisk-RBupper)*( percrisk > RBupper ),na.rm=TRUE ) + penalty*sum( (RBlower-percrisk)*( percrisk < RBlower ),na.rm=TRUE )
705
}
706
# if(!is.null(objective$min_concentration)){
707
# if(isTRUE(objective$min_concentration)){
708
# max_conc<-max(tmp_measure[[2]]) #second element is the contribution in absolute terms
709
# # out=out + penalty * objective$multiplier * max_conc
710
# out = out + objective$multiplier * max_conc
711
# }
712
# }
713
# Combined min_con and min_dif to take advantage of a better concentration obj measure
714
if(!is.null(objective$min_difference) || !is.null(objective$min_concentration)){
715
if(isTRUE(objective$min_difference)){
716
# max_diff<-max(tmp_measure[[2]]-(sum(tmp_measure[[2]])/length(tmp_measure[[2]]))) #second element is the contribution in absolute terms
717
# Uses Herfindahl index to calculate concentration; added scaling perc diffs back to univariate numbers
718
max_diff <- sqrt(sum(tmp_measure[[3]]^2))/100 #third element is the contribution in percentage terms
719
# out = out + penalty * objective$multiplier * max_diff
720
out = out + penalty*objective$multiplier * max_diff
721
}
722
if(isTRUE(objective$min_concentration)){
723
# use HHI to calculate concentration
724
# actual HHI
725
act_hhi <- sum(tmp_measure[[3]]^2)/100
726
# minimum possible HHI
727
min_hhi <- sum(rep(1/length(tmp_measure[[3]]), length(tmp_measure[[3]]))^2)/100
728
out <- out + penalty * objective$multiplier * abs(act_hhi - min_hhi)
729
}
730
}
731
} # end handling of risk_budget objective
732
733
if(inherits(objective, "weight_concentration_objective")){
734
# If the user does not pass in conc_groups, the output of HHI will be a scalar
735
if((length(objective$conc_aversion) == 1) & is.null(objective$conc_groups)){
736
# treat conc_aversion as a multiplier
737
out <- out + penalty * objective$conc_aversion * tmp_measure
738
}
739
# If the user passes in conc_groups, the output of HHI will be a list
740
# The second element of the list will be the group HHI
741
if(length(objective$conc_aversion > 1) & !is.null(objective$conc_groups)){
742
if(length(objective$conc_aversion) == length(tmp_measure[[2]])){
743
# treat the conc_aversion vector as a multiplier per group hhi
744
out <- out + penalty * sum(objective$conc_aversion * tmp_measure[[2]])
745
}
746
}
747
} # weight concentration objective
748
749
} # end enabled check
750
} # end loop over objectives
751
} # end objectives processing
752
753
if(isTRUE(verbose)) {
754
print('weights: ')
755
print(paste(w,' '))
756
print(paste("output of objective function", out))
757
print(unlist(tmp_return))
758
}
759
760
if(is.na(out) | is.nan(out) | is.null(out)){
761
#this should never happen
762
warning('NA or NaN produced in objective function for weights ',w)
763
out <- penalty
764
}
765
766
#return
767
if (isTRUE(storage)){
768
#add the new objective results
769
store_output[[length(store_output)+1]] <- list(out=as.numeric(out), weights=w, init_weights=init_weights, objective_measures=tmp_return)
770
# do the assign here
771
assign('.objectivestorage', store_output, envir=.storage)
772
}
773
if(!isTRUE(trace)){
774
return(out)
775
} else {
776
return(list(out=as.numeric(out), weights=w, objective_measures=tmp_return))
777
}
778
}
779
780