Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
braverock
GitHub Repository: braverock/portfolioanalytics
Path: blob/master/R/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
#' constructor for class 'objective'
14
#'
15
#' Typically called as a sub-function by the user function \code{\link{add.objective}}.
16
#' See main documentation there.
17
#'
18
#' @param name name of the objective which will be used to call a function, like 'ES', 'VaR', 'mean'
19
#' @param target univariate target for the objective, default NULL
20
#' @param arguments default arguments to be passed to an objective function when executed
21
#' @param enabled TRUE/FALSE
22
#' @param \dots any other passthrough parameters
23
#' @param multiplier multiplier to apply to the objective, usually 1 or -1
24
#' @param objclass string class to apply, default 'objective'
25
#' @seealso \code{\link{add.objective}}, \code{\link{portfolio.spec}}
26
#' @author Brian G. Peterson
27
#' @export
28
objective<-function(name , target=NULL , arguments, enabled=TRUE , ..., multiplier=1, objclass='objective'){
29
if(!hasArg(name)) stop("you must specify an objective name")
30
if (hasArg(name)) if(is.null(name)) stop("you must specify an objective name")
31
if (!hasArg(arguments) | is.null(arguments)) arguments<-list()
32
if (!is.list(arguments)) stop("arguments must be passed as a named list")
33
34
## now structure and return
35
return(structure( c(list(name = name,
36
target = target,
37
arguments=arguments,
38
enabled = enabled,
39
multiplier = multiplier
40
#call = match.call()
41
),
42
list(...)),
43
class=objclass
44
) # end structure
45
)
46
}
47
48
49
#' check class of an objective object
50
#' @param x an object potentially of type 'objective' to test
51
#' @author Brian G. Peterson
52
#' @export
53
is.objective <- function( x ) {
54
inherits( x, "objective" )
55
}
56
57
#' @rdname add.objective
58
#' @name add.objective
59
#' @export
60
add.objective_v1 <- function(constraints, type, name, arguments=NULL, enabled=TRUE, ..., indexnum=NULL)
61
{
62
if (!is.constraint(constraints)) {stop("constraints passed in are not of class constraint")}
63
64
if (!hasArg(name)) stop("you must supply a name for the objective")
65
if (!hasArg(type)) stop("you must supply a type of objective to create")
66
if (!hasArg(enabled)) enabled=TRUE
67
if (!hasArg(arguments) | is.null(arguments)) arguments<-list()
68
if (!is.list(arguments)) stop("arguments must be passed as a named list")
69
70
assets=constraints$assets
71
72
tmp_objective=NULL
73
74
switch(type,
75
return=, return_objective=
76
{tmp_objective = return_objective(name=name,
77
enabled=enabled,
78
arguments=arguments,
79
... = ...
80
)
81
},
82
83
risk=, portfolio_risk=, portfolio_risk_objective =
84
{tmp_objective = portfolio_risk_objective(name=name,
85
enabled=enabled,
86
arguments=arguments,
87
...=...
88
)
89
},
90
91
risk_budget=, risk_budget_objective=
92
{tmp_objective = risk_budget_objective(assets=constraints$assets,
93
name=name,
94
enabled=enabled,
95
arguments=arguments,
96
...=...
97
)
98
},
99
turnover = {tmp_objective = turnover_objective(name=name,
100
enabled=enabled,
101
arguments=arguments,
102
...=...)
103
},
104
tmp_minmax = {tmp_objective = minmax_objective(name=name,
105
enabled=enabled,
106
arguments=arguments,
107
...=...)
108
},
109
weight_conc=, weight_concentration =
110
{tmp_objective = weight_concentration_objective(name=name,
111
enabled=enabled,
112
arguments=arguments,
113
...=...)
114
},
115
116
null =
117
{return(constraints)} # got nothing, default to simply returning
118
) # end objective type switch
119
if(is.objective(tmp_objective)) {
120
if(!hasArg(indexnum) | (hasArg(indexnum) & is.null(indexnum))) indexnum = length(constraints$objectives)+1
121
tmp_objective$call<-match.call()
122
constraints$objectives[[indexnum]]<-tmp_objective
123
}
124
return(constraints)
125
}
126
127
#' General interface for adding optimization objectives, including risk, return, and risk budget
128
#'
129
#' This function is the main function for adding and updating business objectives in an object of type \code{\link{portfolio.spec}}.
130
#'
131
#' In general, you will define your objective as one of the following types: 'return', 'risk', 'risk_budget', 'quadratic utility', or 'weight_concentration'.
132
#' These have special handling and intelligent defaults for dealing with the function most likely to be
133
#' used as objectives, including mean, median, VaR, ES, etc.
134
#'
135
#' Objectives of type 'turnover' and 'minmax' are also supported.
136
#'
137
#' @param portfolio an object of type 'portfolio' to add the objective to, specifying the portfolio for the optimization, see \code{\link{portfolio}}
138
#' @param constraints a 'v1_constraint' object for backwards compatibility, see \code{\link{constraint}}
139
#' @param type character type of the objective to add or update, currently 'return','risk', 'risk_budget', 'quadratic_utility', or 'weight_concentration'
140
#' @param name name of the objective, should correspond to a function, though we will try to make allowances
141
#' @param arguments default arguments to be passed to an objective function when executed
142
#' @param enabled TRUE/FALSE
143
#' @param \dots any other passthru parameters
144
#' @param indexnum if you are updating a specific objective, the index number in the $objectives list to update
145
#' @author Brian G. Peterson and Ross Bennett
146
#' @aliases add.objective_v2 add.objective_v1
147
#' @seealso \code{\link{objective}}, \code{\link{portfolio.spec}}
148
#' @rdname add.objective
149
#' @name add.objective
150
#' @examples
151
#' data(edhec)
152
#' returns <- edhec[,1:4]
153
#' fund.names <- colnames(returns)
154
#' portf <- portfolio.spec(assets=fund.names)
155
#' # Add some basic constraints
156
#' portf <- add.constraint(portf, type="full_investment")
157
#' portf <- add.constraint(portf, type="long_only")
158
#'
159
#' # Creates a new portfolio object using portf and adds a quadratic utility
160
#' # objective. This will add two objectives to the portfolio object; 1) mean and
161
#' # 2) var. The risk aversion parameter is commonly referred to as lambda in the
162
#' # quadratic utility formulation that controls how much the portfolio variance
163
#' # is penalized.
164
#' portf.maxQU <- add.objective(portf, type="quadratic_utility",
165
#' risk_aversion=0.25)
166
#'
167
#' # Creates a new portfolio object using portf and adds mean as an objective
168
#' portf.maxMean <- add.objective(portf, type="return", name="mean")
169
#'
170
#' # Creates a new portfolio object using portf and adds StdDev as an objective
171
#' portf.minStdDev <- add.objective(portf, type="risk", name="StdDev")
172
#'
173
#' # Creates a new portfolio object using portf and adds ES as an objective.
174
#' # Note that arguments to ES are passed in as a named list.
175
#' portf.minES <- add.objective(portf, type="risk", name="ES",
176
#' arguments=list(p=0.925, clean="boudt"))
177
#'
178
#' # Creates a new portfolio object using portf.minES and adds a risk budget
179
#' # objective with limits on component risk contribution.
180
#' # Note that arguments to ES are passed in as a named list.
181
#' portf.RiskBudgetES <- add.objective(portf.minES, type="risk_budget", name="ES",
182
#' arguments=list(p=0.925, clean="boudt"),
183
#' min_prisk=0, max_prisk=0.6)
184
#'
185
#' # Creates a new portfolio object using portf.minES and adds a risk budget
186
#' # objective with equal component risk contribution.
187
#' # Note that arguments to ES are passed in as a named list.
188
#' portf.EqRiskES <- add.objective(portf.minES, type="risk_budget", name="ES",
189
#' arguments=list(p=0.925, clean="boudt"),
190
#' min_concentration=TRUE)
191
#'
192
#' # Creates a new portfolio object using portf and adds a weight_concentration
193
#' # objective. The conc_aversion parameter controls how much concentration is
194
#' # penalized. The portfolio concentration is defined as the Herfindahl Hirschman
195
#' # Index of the weights.
196
#' portf.conc <- add.objective(portf, type="weight_concentration",
197
#' name="HHI", conc_aversion=0.01)
198
#' @export add.objective
199
#' @export add.objective_v2
200
add.objective <- add.objective_v2 <- function(portfolio, constraints=NULL, type, name, arguments=NULL, enabled=TRUE, ..., indexnum=NULL){
201
if(!is.null(constraints) & inherits(constraints, "v1_constraint")){
202
return(add.objective_v1(constraints=constraints, type=type, name=name, arguments=arguments, enabled=enabled, ...=..., indexnum=indexnum))
203
}
204
205
# This function is based on the original add.objective function, but modified
206
# to add objectives to a portfolio object instead of a constraint object.
207
if (!is.portfolio(portfolio)) {stop("portfolio passed in is not of class portfolio")}
208
209
if (type != "quadratic_utility" & !hasArg(name)) stop("you must supply a name for the objective")
210
if (!hasArg(type)) stop("you must supply a type of objective to create")
211
if (!hasArg(enabled)) enabled=TRUE
212
if (!hasArg(arguments) | is.null(arguments)) arguments<-list()
213
if (!is.list(arguments)) stop("arguments must be passed as a named list")
214
215
assets=portfolio$assets
216
217
tmp_objective=NULL
218
219
switch(type,
220
return=, return_objective=
221
{tmp_objective = return_objective(name=name,
222
enabled=enabled,
223
arguments=arguments,
224
... = ...
225
)
226
},
227
228
risk=, portfolio_risk=, portfolio_risk_objective =
229
{tmp_objective = portfolio_risk_objective(name=name,
230
enabled=enabled,
231
arguments=arguments,
232
...=...
233
)
234
},
235
236
risk_budget=, risk_budget_objective=
237
{tmp_objective = risk_budget_objective(assets=portfolio$assets,
238
name=name,
239
enabled=enabled,
240
arguments=arguments,
241
...=...
242
)
243
},
244
245
turnover = {tmp_objective = turnover_objective(name=name,
246
enabled=enabled,
247
arguments=arguments,
248
...=...)
249
},
250
tmp_minmax = {tmp_objective = minmax_objective(name=name,
251
enabled=enabled,
252
arguments=arguments,
253
...=...)
254
},
255
qu=, quadratic_utility = {tmp_objective = quadratic_utility_objective(enabled=enabled, ...=...)
256
# quadratic_utility_objective returns a list of a return_objective and a portfolio_risk_objective
257
# we just need to combine it to the portfolio$objectives slot and return the portfolio
258
portfolio$objectives <- c(portfolio$objectives, tmp_objective)
259
return(portfolio)
260
},
261
weight_conc=, weight_concentration =
262
{tmp_objective = weight_concentration_objective(name=name,
263
enabled=enabled,
264
arguments=arguments,
265
...=...)
266
},
267
null =
268
{return(portfolio)} # got nothing, default to simply returning
269
) # end objective type switch
270
271
if(is.objective(tmp_objective)) {
272
if(!hasArg(indexnum) | (hasArg(indexnum) & is.null(indexnum))) indexnum = length(portfolio$objectives)+1
273
tmp_objective$call <- match.call()
274
portfolio$objectives[[indexnum]] <- tmp_objective
275
}
276
return(portfolio)
277
}
278
279
280
# update.objective <- function(object, ...) {
281
# # here we do a bunch of magic to update the correct index'd objective
282
#
283
# constraints <- object
284
#
285
# if (is.null(constraints) | !is.constraint(constraints)){
286
# stop("you must pass in an object of class constraints to modify")
287
# }
288
#
289
#
290
# }
291
292
293
#' constructor for class return_objective
294
#'
295
#' if target is null, we'll try to maximize the return metric
296
#'
297
#' if target is set, we'll try to meet or exceed the metric, penalizing a shortfall
298
#'
299
#' @param name name of the objective, should correspond to a function, though we will try to make allowances
300
#' @param target univariate target for the objective
301
#' @param arguments default arguments to be passed to an objective function when executed
302
#' @param multiplier multiplier to apply to the objective, usually 1 or -1
303
#' @param enabled TRUE/FALSE
304
#' @param \dots any other passthru parameters
305
#' @return object of class 'return_objective'
306
#' @author Brian G. Peterson
307
#' @export
308
return_objective <- function(name, target=NULL, arguments=NULL, multiplier=-1, enabled=TRUE, ... )
309
{
310
if(!hasArg(target)) target = NULL
311
## if target is null, we'll try to maximize the return metric
312
if(!hasArg(multiplier)) multiplier=-1
313
return(objective(name=name, target=target, arguments=arguments, enabled=enabled, multiplier=multiplier,objclass=c("return_objective","objective"), ... ))
314
} # end return_objective constructor
315
316
#' constructor for class portfolio_risk_objective
317
#'
318
#' if target is null, we'll try to minimize the risk metric
319
#' @param name name of the objective, should correspond to a function, though we will try to make allowances
320
#' @param target univariate target for the objective
321
#' @param arguments default arguments to be passed to an objective function when executed
322
#' @param multiplier multiplier to apply to the objective, usually 1 or -1
323
#' @param enabled TRUE/FALSE
324
#' @param \dots any other passthru parameters
325
#' @return object of class 'portfolio_risk_objective'
326
#' @author Brian G. Peterson
327
#' @export
328
portfolio_risk_objective <- function(name, target=NULL, arguments=NULL, multiplier=1, enabled=TRUE, ... )
329
{
330
if(is.null(arguments$portfolio_method)) arguments$portfolio_method="single" #use multivariate risk calcs
331
return(objective(name=name,target=target, arguments=arguments, multiplier=multiplier,enabled=enabled, objclass=c("portfolio_risk_objective","objective"), ... ))
332
} # end portfolio_risk_objective constructor
333
334
#' constructor for class risk_budget_objective
335
#'
336
#' @param assets vector of assets to use, should come from constraints object
337
#' @param name name of the objective, should correspond to a function, though we will try to make allowances
338
#' @param target univariate target for the objective
339
#' @param arguments default arguments to be passed to an objective function when executed
340
#' @param multiplier multiplier to apply to the objective, usually 1 or -1
341
#' @param enabled TRUE/FALSE
342
#' @param \dots any other passthru parameters
343
#' @param min_prisk minimum percentage contribution to risk
344
#' @param max_prisk maximum percentage contribution to risk
345
#' @param min_concentration TRUE/FALSE whether to minimize concentration, default FALSE, always TRUE if min_prisk and max_prisk are NULL
346
#' @param min_difference TRUE/FALSE whether to minimize difference between concentration, default FALSE
347
#' @return object of class 'risk_budget_objective'
348
#' @author Brian G. Peterson
349
#' @export
350
risk_budget_objective <- function(assets, name, target=NULL, arguments=NULL, multiplier=1, enabled=TRUE, ..., min_prisk, max_prisk, min_concentration=FALSE, min_difference=FALSE )
351
{
352
if(is.null(arguments$portfolio_method)) arguments$portfolio_method="component"
353
354
#if( is.null(RBlower) ){ RBlower = rep(-Inf,N) } ; if( is.null(RBupper) ){ RBupper = rep(Inf,N) }
355
nassets=length(assets)
356
if(hasArg(min_prisk) & hasArg(max_prisk)) {
357
if (length(min_prisk)>1 & length(max_prisk)>1){
358
if (length(min_prisk)!=length(max_prisk)) { stop("length of min_prisk and max_prisk must be the same") }
359
}
360
}
361
if(hasArg(min_prisk)){
362
if (length(min_prisk)==1) {
363
min_prisk <- rep(min_prisk,nassets)
364
names(min_prisk)<-names(assets)
365
}
366
if (length(min_prisk)!=nassets) stop(paste("length of min_prisk must be equal to 1 or the number of assets",nassets))
367
}
368
if(hasArg(max_prisk)){
369
if (length(max_prisk)==1) {
370
max_prisk <- rep(max_prisk,nassets)
371
names(max_prisk)<-names(assets)
372
}
373
if (length(max_prisk)!=nassets) stop(paste("length of max_prisk must be equal to 1 or the number of assets",nassets))
374
}
375
376
if(!hasArg(max_prisk)) max_prisk = NULL
377
if(!hasArg(min_prisk)) min_prisk = NULL
378
379
if (is.null(min_prisk) & is.null(max_prisk))
380
min_concentration<-TRUE
381
382
Objective<-objective(name=name,target=target, arguments=arguments, multiplier=multiplier,enabled=enabled, objclass=c("risk_budget_objective","objective"), ... )
383
Objective$min_prisk = min_prisk
384
Objective$max_prisk = max_prisk
385
Objective$min_concentration<-min_concentration
386
Objective$min_difference<-min_difference
387
388
return(Objective)
389
} # end risk_budget_objective constructor
390
391
#' constructor for class turnover_objective
392
#'
393
#' if target is null, we'll try to minimize the turnover metric
394
#'
395
#' if target is set, we'll try to meet the metric
396
#'
397
#' @param name name of the objective, should correspond to a function, though we will try to make allowances
398
#' @param target univariate target for the objective
399
#' @param arguments default arguments to be passed to an objective function when executed
400
#' @param multiplier multiplier to apply to the objective, usually 1 or -1
401
#' @param enabled TRUE/FALSE
402
#' @param \dots any other passthru parameters
403
#' @return an objective of class 'turnover_objective'
404
#' @author Ross Bennett
405
#' @export
406
turnover_objective <- function(name, target=NULL, arguments=NULL, multiplier=1, enabled=TRUE, ... )
407
{
408
if(!hasArg(target)) target = NULL
409
## if target is null, we'll try to minimize the turnover metric
410
if(!hasArg(multiplier)) multiplier=1
411
return(objective(name=name, target=target, arguments=arguments, enabled=enabled, multiplier=multiplier,objclass=c("turnover_objective","objective"), ... ))
412
} # end turnover_objective constructor
413
414
#' constructor for class tmp_minmax_objective
415
#'
416
#' This objective allows for min and max targets to be specified.
417
#'
418
#' If target is set, we'll try to meet the metric
419
#'
420
#' If target is NULL and min and max are specified, then do the following:
421
#'
422
#' If max is violated to the upside, penalize the metric. If min is violated to
423
#' the downside, penalize the metric. The purpose of this objective is to try
424
#' to meet the range between min and max
425
#'
426
#' @param name name of the objective, should correspond to a function, though we will try to make allowances
427
#' @param target univariate target for the objective
428
#' @param min minimum value
429
#' @param max maximum value
430
#' @param arguments default arguments to be passed to an objective function when executed
431
#' @param multiplier multiplier to apply to the objective, usually 1 or -1
432
#' @param enabled TRUE/FALSE
433
#' @param \dots any other passthru parameters
434
#' @return object of class 'minmax_objective'
435
#' @author Ross Bennett
436
#' @export
437
minmax_objective <- function(name, target=NULL, arguments=NULL, multiplier=1, enabled=TRUE, ..., min, max )
438
{
439
if(!hasArg(target)) target = NULL
440
## if target is null, we'll try to minimize the metric
441
if(!hasArg(multiplier)) multiplier=1
442
Objective <- objective(name=name, target=target, arguments=arguments, enabled=enabled, multiplier=multiplier,objclass=c("minmax_objective","objective"), ... )
443
Objective$min <- min
444
Objective$max <- max
445
return(Objective)
446
} # end minmax_objective constructor
447
448
#' constructor for quadratic utility objective
449
#'
450
#' This function calls \code{\link{return_objective}} and \code{\link{portfolio_risk_objective}}
451
#' to create a list of the objectives to be added to the portfolio.
452
#'
453
#' @param risk_aversion risk_aversion (i.e. lambda) parameter to penalize variance
454
#' @param target target mean return value
455
#' @param enabled TRUE/FALSE, default enabled=TRUE
456
#' @return a list of two elements
457
#' \itemize{
458
#' \item \code{return_objective}
459
#' \item \code{portfolio_risk_objective}
460
#' }
461
#' @author Ross Bennett
462
#' @export
463
quadratic_utility_objective <- function(risk_aversion=1, target=NULL, enabled=TRUE){
464
qu <- list()
465
qu[[1]] <- return_objective(name="mean", target=target, enabled=enabled)
466
qu[[2]] <- portfolio_risk_objective(name="var", risk_aversion=risk_aversion, enabled=enabled)
467
return(qu)
468
} # end quadratic utility objective constructor
469
470
#' Constructor for weight concentration objective
471
#'
472
#' This function penalizes weight concentration using the Herfindahl-Hirschman Index
473
#' as a measure of concentration.
474
#'
475
#' The \code{conc_aversion} argument can be a scalar or vector of concentration
476
#' aversion values. If \code{conc_aversion} is a scalar and \code{conc_groups} is
477
#' \code{NULL}, then the concentration aversion value will be applied to the overall
478
#' weights.
479
#'
480
#' If \code{conc_groups} is specified as an argument, then the concentration
481
#' aversion value(s) will be applied to each group.
482
#'
483
#' @param name name of concentration measure, currently only "HHI" is supported.
484
#' @param conc_aversion concentration aversion value(s)
485
#' @param conc_groups list of vectors specifying the groups of the assets. Similar
486
#' to \code{groups} in \code{\link{group_constraint}}
487
#' @param arguments default arguments to be passed to an objective function when executed
488
#' @param enabled TRUE/FALSE
489
#' @param \dots any other passthru parameters
490
#' @return an object of class 'weight_concentration_objective'
491
#' @author Ross Bennett
492
#' @export
493
weight_concentration_objective <- function(name, conc_aversion, conc_groups=NULL, arguments=NULL, enabled=TRUE, ...){
494
# TODO: write HHI function to be used by global solvers in constrained_objective
495
496
# check if conc_groups is specified as an argument
497
if(!is.null(conc_groups)){
498
arguments$groups <- conc_groups
499
if(!is.list(conc_groups)) stop("conc_groups must be passed in as a list")
500
501
if(length(conc_aversion) == 1){
502
# if conc_aversion is a scalar, replicate to the number of groups
503
conc_aversion <- rep(conc_aversion, length(conc_groups))
504
}
505
# length of conc_aversion must be equal to the length of conc_groups
506
if(length(conc_aversion) != length(conc_groups)) stop("length of conc_aversion must be equal to length of groups")
507
} else if(is.null(conc_groups)){
508
if(length(conc_aversion) != 1) stop("conc_aversion must be a scalar value when conc_groups are not specified")
509
}
510
Objective <- objective(name=name, enabled=enabled, arguments=arguments, objclass=c("weight_concentration_objective","objective"), ... )
511
Objective$conc_aversion <- conc_aversion
512
Objective$conc_groups <- conc_groups
513
return(Objective)
514
}
515
516
#' Insert a list of objectives into the objectives slot of a portfolio object
517
#'
518
#' This is a helper function primarily for backwards compatibility to insert
519
#' objectives from a 'v1_constraint' object into the v2 'portfolio' object.
520
#'
521
#' @param portfolio object of class 'portfolio'
522
#' @param objectives list of objective objects
523
#' @author Ross Bennett
524
#' @export
525
insert_objectives <- function(portfolio, objectives){
526
# Check portfolio object
527
if (is.null(portfolio) | !is.portfolio(portfolio)){
528
stop("you must pass in an object of class portfolio")
529
}
530
531
# Check that objectives is a list
532
if(!is.list(objectives)) stop("objectives must be passed in as a list")
533
534
# Check that all objects in the list are of class objective
535
for(i in 1:length(objectives)){
536
if(!is.objective(objectives[[i]]))
537
stop("objectives must be passed in as a list and all objects in objectives must be of class 'objective'")
538
}
539
540
portfolio$objectives <- objectives
541
return(portfolio)
542
}
543
544