Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
braverock
GitHub Repository: braverock/portfolioanalytics
Path: blob/master/R/constraint_fn_map.R
1433 views
1
2
#' mapping function to transform or penalize weights that violate constraints
3
#'
4
#' The purpose of the mapping function is to transform a weights vector
5
#' that does not meet all the constraints into a weights vector that
6
#' does meet the constraints, if one exists, hopefully with a minimum
7
#' of transformation.
8
#'
9
#' The first step is to test for violation of the constraint. If the constraint
10
#' is violated, we will apply a transformation such that the weights vector
11
#' satisfies the constraints. The following constraint types are tested in
12
#' the mapping function: leverage, box, group, and position limit. The
13
#' transformation logic is based on code from the random portfolio sample method.
14
#'
15
#' If relax=TRUE, we will attempt to relax the constraints if a feasible
16
#' portfolio could not be formed with an initial call to \code{rp_transform}.
17
#' We will attempt to relax the constraints up to 5 times. If we do not have a
18
#' feasible portfolio after attempting to relax the constraints, then we will
19
#' default to returning the weights vector that violates the constraints.
20
#'
21
#' @param weights vector of weights
22
#' @param portfolio object of class \code{portfolio}
23
#' @param relax TRUE/FALSE, default FALSE. Enable constraints to be relaxed.
24
#' @param verbose print error messages for debuggin purposes
25
#' @param \dots any other passthru parameters
26
#' @return
27
#' \describe{
28
#' \item{weights:}{vector of transformed weights meeting constraints.}
29
#' \item{min: }{vector of min box constraints that may have been modified if relax=TRUE.}
30
#' \item{max: }{vector of max box constraints that may have been modified if relax=TRUE.}
31
#' \item{cLO: }{vector of lower bound group constraints that may have been modified if relax=TRUE.}
32
#' \item{cUP: }{vector of upper bound group constraints that may have been modified if relax=TRUE.}
33
#' }
34
#' @author Ross Bennett
35
#' @export
36
fn_map <- function(weights, portfolio, relax=FALSE, verbose=FALSE, ...){
37
if(!is.portfolio(portfolio)) stop("portfolio passed in is not of class 'portfolio'")
38
39
nassets <- length(portfolio$assets)
40
41
# step 1: Get the constraints out of the portfolio object
42
constraints <- get_constraints(portfolio)
43
min_sum <- constraints$min_sum
44
max_sum <- constraints$max_sum
45
# rp_transform will rarely find a feasible portfolio if there is not some
46
# 'wiggle room' between min_sum and max_sum
47
if((max_sum - min_sum) < 0.02){
48
min_sum <- min_sum - 0.01
49
max_sum <- max_sum + 0.01
50
}
51
52
weight_seq <- portfolio$weight_seq
53
if(is.null(weight_seq)){
54
weight_seq <- generatesequence(min=min(constraints$min), max=max(constraints$max), by=0.002)
55
}
56
weight_seq <- as.vector(weight_seq)
57
58
min <- constraints$min
59
max <- constraints$max
60
groups <- constraints$groups
61
cLO <- constraints$cLO
62
cUP <- constraints$cUP
63
group_pos <- constraints$group_pos
64
div_target <- constraints$div_target
65
turnover_target <- constraints$turnover_target
66
max_pos <- constraints$max_pos
67
max_pos_long <- constraints$max_pos_long
68
max_pos_short <- constraints$max_pos_short
69
leverage <- constraints$leverage
70
tolerance <- .Machine$double.eps^0.5
71
72
# We will modify the weights vector so create a temporary copy
73
# modified for transformation or to relax constraints
74
tmp_weights <- weights
75
tmp_min <- min
76
tmp_max <- max
77
tmp_cLO <- cLO
78
tmp_cUP <- cUP
79
tmp_max_pos <- max_pos
80
tmp_max_pos_long <- max_pos_long
81
tmp_max_pos_short <- max_pos_short
82
tmp_leverage <- leverage
83
84
# Do we need to step through each constraint type sequentially or can we just
85
# call rp_transform once now that it has been modified to handle constraint
86
# types seperately?
87
88
# step 2: check that the vector of weights satisfies the constraints,
89
# transform weights if constraint is violated
90
# TRUE if the weights vector is in compliance with the constraints
91
# FALSE if the weights vector violates the constraint
92
93
# check leverage constraints
94
if(!is.null(min_sum) & !is.null(max_sum)){
95
if(!(sum(tmp_weights) >= min_sum & sum(tmp_weights) <= max_sum)){
96
# Try to transform only considering leverage and box constraints
97
tmp_weights <- try(rp_transform(w=tmp_weights,
98
min_sum=min_sum,
99
max_sum=max_sum,
100
min_box=tmp_min,
101
max_box=tmp_max,
102
groups=NULL,
103
cLO=NULL,
104
cUP=NULL,
105
max_pos=NULL,
106
group_pos=NULL,
107
max_pos_long=NULL,
108
max_pos_short=NULL,
109
leverage=tmp_leverage,
110
weight_seq=weight_seq,
111
max_permutations=500),
112
silent=TRUE) # FALSE for testing
113
if(inherits(tmp_weights, "try-error")){
114
# Default to initial weights
115
tmp_weights <- weights
116
} # end try-error recovery
117
} # end check for leverage constraint violation
118
} # end check for NULL arguments
119
120
# check box constraints
121
if(!is.null(tmp_min) & !is.null(tmp_max)){
122
if(!(all(tmp_weights >= tmp_min) & all(tmp_weights <= tmp_max))){
123
# Try to transform only considering leverage and box constraints
124
tmp_weights <- try(rp_transform(w=tmp_weights,
125
min_sum=min_sum,
126
max_sum=max_sum,
127
min_box=tmp_min,
128
max_box=tmp_max,
129
groups=NULL,
130
cLO=NULL,
131
cUP=NULL,
132
max_pos=NULL,
133
group_pos=NULL,
134
max_pos_long=NULL,
135
max_pos_short=NULL,
136
leverage=tmp_leverage,
137
weight_seq=weight_seq,
138
max_permutations=500),
139
silent=TRUE) # FALSE for testing
140
if(inherits(tmp_weights, "try-error")){
141
if(verbose) message(tmp_weights)
142
# Default to initial weights
143
tmp_weights <- weights
144
# Try to relax constraints if relax=TRUE
145
if(relax){
146
i <- 1
147
# loop while constraints are violated and relax constraints
148
# try to relax constraints up to 5 times
149
while((sum(tmp_weights) < min_sum | sum(tmp_weights) > max_sum | any(tmp_weights < tmp_min) | any(tmp_weights > tmp_max)) & i <= 5){
150
# check if min is violated
151
if(any(tmp_weights < tmp_min)){
152
# Find which elements of min are violated and decrease by a random amount
153
tmp_min[which(tmp_weights < tmp_min)] <- tmp_min[which(tmp_weights < tmp_min)] - runif(1, 0.01, 0.05)
154
}
155
# check if max is violated
156
if(any(tmp_weights > tmp_max)){
157
# Find which elements of min are violated and increase by a random amount
158
tmp_max[which(tmp_weights < tmp_max)] <- tmp_max[which(tmp_weights < tmp_max)] + runif(1, 0.01, 0.05)
159
}
160
161
# Now try the transformation again
162
tmp_weights <- try(rp_transform(w=tmp_weights,
163
min_sum=min_sum,
164
max_sum=max_sum,
165
min_box=tmp_min,
166
max_box=tmp_max,
167
groups=NULL,
168
cLO=NULL,
169
cUP=NULL,
170
max_pos=NULL,
171
group_pos=NULL,
172
max_pos_long=NULL,
173
max_pos_short=NULL,
174
leverage=tmp_leverage,
175
weight_seq=weight_seq,
176
max_permutations=500),
177
silent=TRUE) # FALSE for testing
178
# Default to original weights if this fails again
179
if(inherits(tmp_weights, "try-error")) tmp_weights <- weights
180
i <- i + 1
181
}
182
# We have a feasible portfolio in terms of min_sum and max_sum,
183
# but were unable to produce a portfolio that satisfies box constraints
184
if(isTRUE(all.equal(tmp_weights, weights))){
185
# reset min and max to their original values and penalize later
186
tmp_min <- min
187
tmp_max <- max
188
}
189
} # end if(relax) statement
190
} # end try-error recovery
191
} # end check for box constraint violation
192
} # end check for NULL arguments
193
194
# check group constraints
195
if(!is.null(groups) & !is.null(tmp_cLO) & !is.null(tmp_cUP)){
196
if(any(group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP, group_pos))){
197
# Try to transform only considering leverage, box, and group constraints
198
tmp_weights <- try(rp_transform(w=tmp_weights,
199
min_sum=min_sum,
200
max_sum=max_sum,
201
min_box=tmp_min,
202
max_box=tmp_max,
203
groups=groups,
204
cLO=tmp_cLO,
205
cUP=tmp_cUP,
206
max_pos=NULL,
207
group_pos=group_pos,
208
max_pos_long=NULL,
209
max_pos_short=NULL,
210
leverage=tmp_leverage,
211
weight_seq=weight_seq,
212
max_permutations=500),
213
silent=TRUE) # FALSE for testing
214
if(inherits(tmp_weights, "try-error")){
215
if(verbose) message(tmp_weights)
216
# Default to initial weights
217
tmp_weights <- weights
218
# Try to relax constraints if relax=TRUE
219
if(relax){
220
i <- 1
221
# loop while constraints are violated and relax constraints
222
# Try to relax constraints up to 5 times
223
while(((sum(tmp_weights) < min_sum | sum(tmp_weights) > max_sum) | (any(tmp_weights < tmp_min) | any(tmp_weights > tmp_max)) | any(group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP, group_pos))) & i <= 5){
224
if(any(group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP, group_pos))){
225
# I know which group failed, but not if it was cUP or cLO that was violated
226
# Maybe I can modify group_fail to report back what was violated and only relax cLO or cUP, not both
227
# This relaxes both cLO and cUP
228
tmp_cLO[group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP)] <- tmp_cLO[group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP)] - runif(1, 0.01, 0.05)
229
tmp_cUP[group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP)] <- tmp_cUP[group_fail(tmp_weights, groups, tmp_cLO, tmp_cUP)] + runif(1, 0.01, 0.05)
230
}
231
# Now try the transformation again
232
tmp_weights <- try(rp_transform(w=tmp_weights,
233
min_sum=min_sum,
234
max_sum=max_sum,
235
min_box=tmp_min,
236
max_box=tmp_max,
237
groups=groups,
238
cLO=tmp_cLO,
239
cUP=tmp_cUP,
240
max_pos=NULL,
241
group_pos=group_pos,
242
max_pos_long=NULL,
243
max_pos_short=NULL,
244
leverage=tmp_leverage,
245
weight_seq=weight_seq,
246
max_permutations=500),
247
silent=TRUE) # FALSE for testing
248
if(inherits(tmp_weights, "try-error")) tmp_weights <- weights
249
i <- i + 1
250
}
251
# We have a feasible portfolio in terms of min_sum and max_sum,
252
# but were unable to produce a portfolio that satisfies group constraints
253
if(isTRUE(all.equal(tmp_weights, weights))){
254
# reset min and max to their original values and penalize later
255
tmp_cLO <- cLO
256
tmp_cUP <- cUP
257
}
258
} # end if(relax) statement
259
} # end try-error recovery
260
} # end check for group constraint violation
261
} # end check for NULL arguments
262
263
# check position_limit constraints
264
if(!is.null(tmp_max_pos) | !is.null(tmp_max_pos_long) | !is.null(tmp_max_pos_short)){
265
if(pos_limit_fail(tmp_weights, tmp_max_pos, tmp_max_pos_long, tmp_max_pos_short)){
266
# Try to transform only considering leverage, box, group, and position_limit constraints
267
tmp_weights <- try(rp_transform(w=tmp_weights,
268
min_sum=min_sum,
269
max_sum=max_sum,
270
min_box=tmp_min,
271
max_box=tmp_max,
272
groups=groups,
273
cLO=tmp_cLO,
274
cUP=tmp_cUP,
275
max_pos=tmp_max_pos,
276
group_pos=group_pos,
277
max_pos_long=tmp_max_pos_long,
278
max_pos_short=tmp_max_pos_short,
279
leverage=tmp_leverage,
280
weight_seq=weight_seq,
281
max_permutations=500),
282
silent=TRUE) # FALSE for testing
283
if(inherits(tmp_weights, "try-error")){
284
if(verbose) message(tmp_weights)
285
# Default to initial weights
286
tmp_weights <- weights
287
if(relax){
288
i <- 1
289
while(pos_limit_fail(tmp_weights, tmp_max_pos, tmp_max_pos_long, tmp_max_pos_short) & (i <= 5)){
290
# increment tmp_max_pos by 1
291
if(!is.null(tmp_max_pos)) tmp_max_pos <- min(nassets, tmp_max_pos + 1)
292
if(!is.null(tmp_max_pos_long)) tmp_max_pos_long <- min(nassets, tmp_max_pos_long + 1)
293
if(!is.null(tmp_max_pos_short)) tmp_max_pos_short <- min(nassets, tmp_max_pos_short + 1)
294
# Now try the transformation again
295
tmp_weights <- try(rp_transform(w=tmp_weights,
296
min_sum=min_sum,
297
max_sum=max_sum,
298
min_box=tmp_min,
299
max_box=tmp_max,
300
groups=groups,
301
cLO=tmp_cLO,
302
cUP=tmp_cUP,
303
max_pos=tmp_max_pos,
304
group_pos=group_pos,
305
max_pos_long=tmp_max_pos_long,
306
max_pos_short=tmp_max_pos_short,
307
leverage=tmp_leverage,
308
weight_seq=weight_seq,
309
max_permutations=500),
310
silent=TRUE) # FALSE for testing
311
if(inherits(tmp_weights, "try-error")) tmp_weights <- weights
312
i <- i + 1
313
}
314
} # end if(relax) statement
315
} # end try-error recovery
316
} # end check for position limit constraint violation
317
} # end check for NULL arguments
318
319
# check leverage constraints
320
if(!is.null(tmp_leverage)){
321
if(sum(abs(tmp_weights)) > tmp_leverage){
322
# Try to transform only considering weight_sum, box, group, position_limit, and leverage exposure constraints
323
tmp_weights <- try(rp_transform(w=tmp_weights,
324
min_sum=min_sum,
325
max_sum=max_sum,
326
min_box=tmp_min,
327
max_box=tmp_max,
328
groups=groups,
329
cLO=tmp_cLO,
330
cUP=tmp_cUP,
331
max_pos=tmp_max_pos,
332
group_pos=group_pos,
333
max_pos_long=tmp_max_pos_long,
334
max_pos_short=tmp_max_pos_short,
335
leverage=tmp_leverage,
336
weight_seq=weight_seq,
337
max_permutations=500),
338
silent=TRUE) # FALSE for testing
339
if(inherits(tmp_weights, "try-error")){
340
if(verbose) message(tmp_weights)
341
# Default to initial weights
342
tmp_weights <- weights
343
if(relax){
344
i <- 1
345
while(sum(abs(tmp_weights)) > tmp_leverage & (i <= 5)){
346
# increment tmp_leverage by 1%
347
tmp_leverage <- tmp_leverage * 1.01
348
# Now try the transformation again
349
tmp_weights <- try(rp_transform(w=tmp_weights,
350
min_sum=min_sum,
351
max_sum=max_sum,
352
min_box=tmp_min,
353
max_box=tmp_max,
354
groups=groups,
355
cLO=tmp_cLO,
356
cUP=tmp_cUP,
357
max_pos=tmp_max_pos,
358
group_pos=group_pos,
359
max_pos_long=tmp_max_pos_long,
360
max_pos_short=tmp_max_pos_short,
361
leverage=tmp_leverage,
362
weight_seq=weight_seq,
363
max_permutations=500),
364
silent=TRUE) # FALSE for testing
365
if(inherits(tmp_weights, "try-error")) tmp_weights <- weights
366
i <- i + 1
367
}
368
} # end if(relax) statement
369
} # end try-error recovery
370
} # end check for leverage exposure violation
371
} # end check for NULL arguments
372
373
names(tmp_weights) <- names(weights)
374
return(list(weights=tmp_weights,
375
min=tmp_min,
376
max=tmp_max,
377
cLO=tmp_cLO,
378
cUP=tmp_cUP,
379
max_pos=tmp_max_pos,
380
max_pos_long=tmp_max_pos_long,
381
max_pos_short=tmp_max_pos_short,
382
leverage=tmp_leverage))
383
}
384
385
386
387
#' Transform a weights vector to satisfy constraints
388
#'
389
#' This function uses a block of code from \code{\link{randomize_portfolio}}
390
#' to transform the weight vector if either the weight_sum (leverage)
391
#' constraints, box constraints, group constraints, position_limit constraints,
392
#' or leverage exposure constraints are violated. The logic from
393
#' \code{randomize_portfolio} is heavily utilized here with extensions to
394
#' handle more complex constraints.
395
#' The resulting weights vector might be quite different from the original weights vector.
396
#'
397
#' @param w weights vector to be transformed
398
#' @param min_sum minimum sum of all asset weights, default 0.99
399
#' @param max_sum maximum sum of all asset weights, default 1.01
400
#' @param min_box numeric or named vector specifying minimum weight box constraints
401
#' @param max_box numeric or named vector specifying maximum weight box constraints
402
#' @param groups vector specifying the groups of the assets
403
#' @param cLO numeric or vector specifying minimum weight group constraints
404
#' @param cUP numeric or vector specifying minimum weight group constraints
405
#' @param max_pos maximum assets with non-zero weights
406
#' @param group_pos vector specifying maximum number assets with non-zero weights per group
407
#' @param max_pos_long maximum number of assets with long (i.e. buy) positions
408
#' @param max_pos_short maximum number of assets with short (i.e. sell) positions
409
#' @param leverage maximum leverage exposure where leverage is defined as \code{sum(abs(weights))}
410
#' @param max_permutations integer: maximum number of iterations to try for a valid portfolio, default 200
411
#' @param weight_seq vector of seed sequence of weights
412
#' @return named weighting vector
413
#' @author Peter Carl, Brian G. Peterson, Ross Bennett (based on an idea by Pat Burns)
414
#' @export
415
rp_transform <- function(w,
416
min_sum,
417
max_sum,
418
min_box,
419
max_box,
420
groups=NULL,
421
cLO=NULL,
422
cUP=NULL,
423
max_pos=NULL,
424
group_pos=NULL,
425
max_pos_long=NULL,
426
max_pos_short=NULL,
427
leverage=NULL,
428
weight_seq=NULL,
429
max_permutations=200){
430
tmp_w <- w
431
432
# Set some reasonable default values
433
# Maybe I should leave these as NULL values and incorporate that into the
434
# checks
435
#if(is.null(min_sum)) min_sum <- 0.99
436
#if(is.null(max_sum)) max_sum <- 1.01
437
#if(is.null(min_box)) min_box <- rep(-Inf, length(tmp_w))
438
#if(is.null(max_box)) max_box <- rep(Inf, length(tmp_w))
439
if(is.null(max_pos)) max_pos <- length(tmp_w)
440
#if(is.null(max_poslong)) max_pos_long <- length(tmp_w)
441
#if(is.null(max_pos_short)) max_pos_short <- length(tmp_w)
442
#if(is.null(leverage)) leverage <- Inf
443
444
# Generate a weight sequence, we should check for portfolio$weight_seq
445
if(is.null(weight_seq))
446
weight_seq <- generatesequence(min=min(min_box), max=max(max_box), by=0.002)
447
448
# make sure there is a 0 in weight_seq if we have a position limit constraint
449
if((!is.null(max_pos) | !is.null(group_pos) | !is.null(max_pos_long) | !is.null(max_pos_short)) & !is.element(0, weight_seq)) weight_seq <- c(0, weight_seq)
450
451
# Tolerance for "non-zero" definition for position limit constraints
452
tolerance <- .Machine$double.eps^0.5
453
454
# initialize the outer while loop
455
permutations <- 1
456
457
# while we have not reached max_permutations and the following constraints
458
# are violated:
459
# - min_sum
460
# - max_sum
461
# - leverage
462
# - max_pos, max_pos_long, max_pos_short
463
# - group
464
465
# Do we want to check all constraints in here?
466
# Box constraints should be satisfied by construction so we should not need
467
# to check those here
468
while (( min_sum_fail(tmp_w, min_sum) |
469
max_sum_fail(tmp_w, max_sum) |
470
leverage_fail(tmp_w, leverage) |
471
pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) |
472
any(group_fail(tmp_w, groups, cLO, cUP)) ) &
473
(permutations < max_permutations)) {
474
475
# cat("permutation #:", permutations, "\n")
476
permutations <- permutations+1
477
478
# Reset tmp_w to original weights vector
479
# I'm not sure we want to do this here because it puts us back to where we
480
# started, but it seems to help with the position limit constraint
481
# tmp_w <- weights
482
483
# Reset the random index based on the maximum position constraint
484
# This basically allows us to generate a portfolio of max_pos assets
485
# with the given constraints and then add assets with zero weight
486
random_index <- sample(1:length(tmp_w), max_pos)
487
488
# Get the index values that are not in random_index and set them equal to 0
489
full_index <- 1:length(tmp_w)
490
not_index <- setdiff(full_index, random_index)
491
tmp_w[not_index] <- 0
492
493
# min_sum violation
494
if(min_sum_fail(tmp_w, min_sum)){
495
tmp_w <- rp_increase(weights=tmp_w,
496
min_sum=min_sum,
497
max_box=max_box,
498
weight_seq=weight_seq)
499
}
500
501
# max_sum violation
502
if(max_sum_fail(tmp_w, max_sum)){
503
tmp_w <- rp_decrease(weights=tmp_w,
504
max_sum=max_sum,
505
min_box=min_box,
506
weight_seq=weight_seq)
507
}
508
509
# leverage violation
510
if(leverage_fail(tmp_w, leverage)){
511
tmp_w <- rp_decrease_leverage(weights=tmp_w,
512
max_box=max_box,
513
min_box=min_box,
514
leverage=leverage,
515
weight_seq=weight_seq)
516
}
517
518
# position limit violation
519
if(pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short)){
520
tmp_w <- rp_position_limit(weights=tmp_w,
521
min_box=min_box,
522
max_box=max_box,
523
max_pos=max_pos,
524
max_pos_long=max_pos_long,
525
max_pos_short=max_pos_short,
526
weight_seq=weight_seq)
527
}
528
529
# group violation
530
if(any(group_fail(tmp_w, groups, cLO, cUP, group_pos))){
531
n_groups <- length(groups)
532
for(j in 1:n_groups){
533
# index of the weights vector belonging to the jth group
534
j_idx <- groups[[j]]
535
# weights of the jth group
536
tmp_group_w <- tmp_w[j_idx]
537
538
# May be easier to just make a recursive call and treat each group
539
# as a portfolio of weight vectors
540
tmp_w[j_idx] <- rp_transform(w=tmp_group_w,
541
min_sum=cLO[j],
542
max_sum=cUP[j],
543
min_box=min_box[j_idx],
544
max_box=max_box[j_idx],
545
group_pos=group_pos[j])
546
547
# treat this as if min_sum were violated
548
# if(sum(tmp_group_w) < cLO[j]){
549
# tmp_w[j_idx] <- rp_increase(weights=tmp_group_w,
550
# min_sum=cLO[j],
551
# max_box=max_box[j_idx],
552
# weight_seq=weight_seq)
553
# }
554
555
# treat this as if max_sum were violated
556
# if(sum(tmp_group_w) > cUP[j]){
557
# tmp_w[j_idx] <- rp_decrease(weights=tmp_group_w,
558
# max_sum=cUP[j],
559
# min_box=min_box[j_idx],
560
# weight_seq=weight_seq)
561
# }
562
}
563
} # end group violation loop
564
} # end final walk towards the edges
565
portfolio <- tmp_w
566
567
colnames(portfolio) <- colnames(w)
568
569
# checks for infeasible portfolio
570
# Stop execution and return an error if an infeasible portfolio is created
571
# This will be useful in fn_map so that we can catch the error and take
572
# action (try again with more permutations, relax constraints, different
573
# method to normalize, etc.)
574
if (sum(portfolio) < min_sum | sum(portfolio) > max_sum){
575
portfolio <- w
576
stop("Infeasible portfolio created, perhaps increase max_permutations and/or adjust your parameters.")
577
}
578
return(portfolio)
579
}
580
581
# rp_transform <- function(w,
582
# min_sum=0.99,
583
# max_sum=1.01,
584
# min,
585
# max,
586
# groups,
587
# cLO,
588
# cUP,
589
# max_pos=NULL,
590
# group_pos=NULL,
591
# max_pos_long=NULL,
592
# max_pos_short=NULL,
593
# leverage=NULL,
594
# max_permutations=200){
595
# # Uses logic from randomize_portfolio to "normalize" a weights vector to
596
# # satisfy min_sum and max_sum while accounting for box and group constraints
597
# # Modified from randomize_portfolio to trigger the while loops if any weights
598
# # violate min or max box constraints. A weights vector would not be transformed
599
# # in randomize_portfolio if min_sum and max_sum were satisfied, but the
600
# # min/max constraints were violated.
601
#
602
# # Set the tolerance to determine non-zero weights
603
# tolerance=.Machine$double.eps^0.5
604
#
605
# # Set value for max_pos if it is not specified
606
# if(is.null(max_pos)) max_pos <- length(w)
607
#
608
# # Set value for leverage if it is not specified
609
# if(is.null(leverage)) leverage <- Inf
610
#
611
# # Determine maximum number of non-zero weights
612
# if(!is.null(group_pos)) {
613
# max_group_pos <- sum(group_pos)
614
# } else {
615
# max_group_pos <- length(w)
616
# }
617
#
618
# # Set maximum number of assets based on max_pos and group_pos
619
# max_assets <- min(max_pos, max_group_pos)
620
#
621
# # Create a temporary min vector that will be modified, because a feasible
622
# # portfolio is rarely created if all(min > 0). This is due to the while
623
# # loop that checks any(tmp_w < min).
624
# tmp_min <- min
625
#
626
# # If weight_i = 0 and min_i > 0, then this will violate box constraints
627
# # even though weight_i = 0 to satisfy position_limit constraints. Modify
628
# # the tmp_min vector and set tmp_min_i equal to zero where weights_i = 0.
629
# # If w is less than or equal to tolerance then it is essentially 0
630
# if(any(abs(w) <= tolerance)){
631
# if(any(tmp_min[which(abs(w) <= tolerance)] > 0)){
632
# tmp_min[which(abs(w) <= tolerance)] <- -tolerance
633
# }
634
# }
635
#
636
# # return w if all constraints are satisfied
637
# if((sum(w) >= min_sum & sum(w) <= max_sum) &
638
# (all(w >= tmp_min) & all(w <= max)) &
639
# (all(!group_fail(w, groups, cLO, cUP, group_pos))) &
640
# !pos_limit_fail(w, max_pos, max_pos_long, max_pos_short) &
641
# (sum(abs(w)) <= leverage)){
642
# return(w)
643
# }
644
#
645
# # generate a sequence of weights based on min/max box constraints
646
# weight_seq <- generatesequence(min=min(min), max=max(max), by=0.002)
647
# # make sure there is a 0 in weight_seq
648
# if((!is.null(max_pos) | !is.null(group_pos) | !is.null(max_pos_long) | !is.null(max_pos_short)) & !is.element(0, weight_seq)) weight_seq <- c(0, weight_seq)
649
#
650
# # start the permutations counter
651
# permutations <- 1
652
#
653
# # create a temporary weights vector that will be modified in the while loops
654
# tmp_w <- w
655
#
656
# # while any constraint is violated and we have not reached max_permutations
657
# while ((sum(tmp_w) < min_sum |
658
# sum(tmp_w) > max_sum |
659
# any(tmp_w < tmp_min) |
660
# any(tmp_w > max) |
661
# any(group_fail(tmp_w, groups, cLO, cUP, group_pos)) |
662
# pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) |
663
# sum(abs(w)) > leverage) &
664
# permutations <= max_permutations) {
665
# permutations = permutations + 1
666
# # check our box constraints on total portfolio weight
667
# # reduce(increase) total portfolio size till you get a match
668
# # 1> check to see which bound you've failed on, probably set this as a pair of while loops
669
# # 2> randomly select a column and move only in the direction *towards the bound*, maybe call a function inside a function
670
# # 3> check and repeat
671
#
672
# # reset tmp_w and tmp_min to their original values
673
# tmp_w <- w
674
# tmp_min <- min
675
#
676
# random_index <- sample(1:length(tmp_w), max_assets)
677
#
678
# # Get the index values that are not in random_index and set them equal to 0
679
# full_index <- 1:length(tmp_w)
680
# not_index <- setdiff(full_index, random_index)
681
# tmp_w[not_index] <- 0
682
#
683
# # set some tmp_min values equal to zero so the while loops do not see a
684
# # violation of any(tmp_w < tmp_min). This tends to force weights to 0 and
685
# # works well for long only, but we may want to allow negative weights.
686
# # tmp_min[not_index] <- 0
687
# # Only set values of tmp_min that are greater than 0 to 0
688
# tmp_min[not_index[which(tmp_min[not_index] > 0)]] <- 0
689
#
690
# # Transform weights to satisfy max_pos_long and max_pos_short before being
691
# # passed into the main loops
692
# # Both max_pos_long and max_pos_short should be specified
693
# if(!is.null(max_pos_long)){
694
# pos_idx <- which(tmp_w > 0)
695
# neg_idx <- which(tmp_w < 0)
696
#
697
# # Check if number of positive weights exceeds max_pos_long
698
# if(length(pos_idx) > max_pos_long){
699
# # Randomly sample positive weights that cause violation of max_pos_long
700
# # and replace with randomly sampled negative weights from weight_seq
701
# make_neg_idx <- sample(pos_idx, length(pos_idx) - max_pos_long)
702
# for(i in make_neg_idx){
703
# tmp_idx <- weight_seq[weight_seq < 0 & weight_seq >= min[i]]
704
# if(length(tmp_idx) > 0){
705
# tmp_w[i] <- sample(tmp_idx, 1)
706
# } else {
707
# # This should never happen if the correct weight_seq and min is specified
708
# tmp_w[i] <- -tmp_w[i]
709
# }
710
# }
711
# }
712
# }
713
# if(!is.null(max_pos_short)){
714
# # Check if number of negative weights exceeds max_pos_short
715
# if(length(neg_idx) > max_pos_short){
716
# # Randomly sample negative weights that cause violation of max_pos_short
717
# # and replace with randomly sampled positive weights from weight_seq
718
# make_pos_idx <- sample(neg_idx, length(neg_idx) - max_pos_short)
719
# for(i in make_pos_idx){
720
# tmp_seq <- weight_seq[weight_seq > 0 & weight_seq <= max[i]]
721
# if(length(tmp_seq) > 0){
722
# tmp_w[i] <- sample(tmp_seq, 1)
723
# } else {
724
# # This should never happen if the correct weight_seq and max is specified
725
# tmp_w[i] <- -tmp_w[i]
726
# }
727
# }
728
# }
729
# }
730
#
731
# i = 1
732
# # We increase elements here if the sum of the weights exceeds max_sum or
733
# # any of the other constraints are violated
734
# while ((sum(tmp_w) < min_sum |
735
# any(tmp_w < tmp_min) |
736
# any(tmp_w > max) |
737
# any(group_fail(tmp_w, groups, cLO, cUP, group_pos)) |
738
# pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) |
739
# sum(abs(tmp_w)) > leverage) &
740
# i <= length(tmp_w)) {
741
# # randomly permute and increase a random portfolio element
742
# cur_index <- random_index[i]
743
# cur_val <- tmp_w[cur_index]
744
# tmp_seq <- weight_seq[(weight_seq >= cur_val) & (weight_seq <= max[cur_index])]
745
# n_tmp_seq <- length(tmp_seq)
746
# if (n_tmp_seq > 1) {
747
# # randomly sample an element from weight_seq that is greater than cur_val and less than max
748
# # tmp_w[cur_index] <- sample(weight_seq[(weight_seq >= cur_val) & (weight_seq <= max[cur_index])], 1)
749
# tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]
750
# # print(paste("new val:",tmp_w[cur_index]))
751
# } else {
752
# if (n_tmp_seq == 1) {
753
# # tmp_w[cur_index] <- weight_seq[(weight_seq >= cur_val) & (weight_seq <= max[cur_index])]
754
# tmp_w[cur_index] <- tmp_seq
755
# }
756
# }
757
# i=i+1 # increment our counter
758
# } # end increase loop
759
# # need to reset i here otherwise the decreasing loop will be ignored
760
# # group_fail does not test for direction of violation, just that group constraints were violated
761
# i = 1
762
# # We decrease elements here if the sum of the weights exceeds max_sum or
763
# # any of the other constraints are violated
764
# while ((sum(tmp_w) > max_sum |
765
# any(tmp_w < tmp_min) |
766
# any(tmp_w > max) |
767
# any(group_fail(tmp_w, groups, cLO, cUP, group_pos)) |
768
# pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) |
769
# sum(abs(tmp_w)) > leverage) &
770
# i <= length(tmp_w)) {
771
# # randomly permute and decrease a random portfolio element
772
# cur_index <- random_index[i]
773
# cur_val <- tmp_w[cur_index]
774
# tmp_seq <- weight_seq[(weight_seq <= cur_val) & (weight_seq >= tmp_min[cur_index])]
775
# n_tmp_seq <- length(tmp_seq)
776
# if (n_tmp_seq > 1) {
777
# # randomly sample an element from weight_seq that is less than cur_val and greater than tmp_min
778
# # tmp_w[cur_index] <- sample(weight_seq[(weight_seq <= cur_val) & (weight_seq >= tmp_min[cur_index])] , 1)
779
# tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]
780
# } else {
781
# if (n_tmp_seq == 1) {
782
# # tmp_w[cur_index] <- weight_seq[(weight_seq <= cur_val) & (weight_seq >= tmp_min[cur_index])]
783
# tmp_w[cur_index] <- tmp_seq
784
# }
785
# }
786
# i=i+1 # increment our counter
787
# } # end decrease loop
788
# #cat("permutations:", permutations, "\n")
789
# #cat("weights:", tmp_w, "\n")
790
# #cat("sum(weights):", sum(tmp_w), "\n")
791
# #cat("sum(abs(weights)):", sum(abs(tmp_w)), "\n")
792
# } # end final walk towards the edges
793
#
794
# portfolio <- tmp_w
795
#
796
# colnames(portfolio)<-colnames(w)
797
#
798
# # checks for infeasible portfolio
799
# # Stop execution and return an error if an infeasible portfolio is created
800
# # This will be useful in fn_map so that we can catch the error and take
801
# # action (try again with more permutations, relax constraints, different
802
# # method to normalize, etc.)
803
# if (sum(portfolio) < min_sum | sum(portfolio) > max_sum){
804
# portfolio <- w
805
# stop("Infeasible portfolio created, perhaps increase max_permutations and/or adjust your parameters.")
806
# }
807
# # if(isTRUE(all.equal(w,portfolio))) {
808
# # if (sum(w)>=min_sum & sum(w)<=max_sum) {
809
# # warning("Unable to generate a feasible portfolio different from w, perhaps adjust your parameters.")
810
# # return(w)
811
# # } else {
812
# # warning("Unable to generate a feasible portfolio, perhaps adjust your parameters.")
813
# # return(NULL)
814
# # }
815
# # }
816
# return(portfolio)
817
# }
818
819
#' Test if group constraints have been violated
820
#'
821
#' The function loops through each group and tests if cLO or cUP have been violated
822
#' for the given group. This is a helper function for \code{\link{rp_transform}}.
823
#'
824
#' @param weights weights vector to test
825
#' @param groups list of vectors specifying the groups of the assets
826
#' @param cLO numeric or vector specifying minimum weight group constraints
827
#' @param cUP numeric or vector specifying minimum weight group constraints
828
#' @param group_pos vector specifying the number of non-zero weights per group
829
#' @return logical vector: TRUE if group constraints are violated for a given group
830
#' @author Ross Bennett
831
group_fail <- function(weights, groups, cLO, cUP, group_pos=NULL){
832
# return FALSE if groups, cLO, or cUP is NULL
833
if(is.null(groups) | is.null(cLO) | is.null(cUP)) return(FALSE)
834
group_count <- sapply(groups, length)
835
# group_pos sets a limit on the number of non-zero weights by group
836
# Set equal to groups if NULL
837
if(is.null(group_pos)) group_pos <- group_count
838
tolerance <- .Machine$double.eps^0.5
839
840
n.groups <- length(groups)
841
group_fail <- vector(mode="logical", length=n.groups)
842
843
for(i in 1:n.groups){
844
# sum of the weights for a given group
845
tmp.w <- weights[groups[[i]]]
846
group_fail[i] <- ( (sum(tmp.w) < cLO[i]) | (sum(tmp.w) > cUP[i]) | (sum(abs(tmp.w) > tolerance) > group_pos[i]) )
847
}
848
# returns logical vector of groups. TRUE if either cLO or cUP is violated
849
return(group_fail)
850
}
851
852
#' function to check for violation of position limits constraints
853
#'
854
#' This is used as a helper function for \code{\link{rp_transform}} to check
855
#' for violation of position limit constraints. The position limit constraints
856
#' checked are max_pos, max_pos_long, and max_pos_short.
857
#'
858
#' @param weights vector of weights to test
859
#' @param max_pos maximum number of assets with non-zero weights
860
#' @param max_pos_long maximum number of assets with long (i.e. buy) positions
861
#' @param max_pos_short maximum number of assets with short (i.e. sell) positions
862
#' @return TRUE if any position_limit is violated. FALSE if all position limits are satisfied
863
#' @export
864
pos_limit_fail <- function(weights, max_pos, max_pos_long, max_pos_short){
865
# tolerance for "non-zero" definition
866
tolerance <- .Machine$double.eps^0.5
867
868
# Check if max_pos is violated
869
if(!is.null(max_pos)){
870
if(sum(abs(weights) > tolerance) > max_pos){
871
return(TRUE)
872
}
873
}
874
875
# Check if max_pos_long is violated
876
if(!is.null(max_pos_long)){
877
if(sum(weights > tolerance) > max_pos_long){
878
return(TRUE)
879
}
880
}
881
882
# Check if max_pos_short is violated
883
if(!is.null(max_pos_short)){
884
if(sum(weights < -tolerance) > max_pos_short){
885
return(TRUE)
886
}
887
}
888
# Return FALSE if nothing is violated
889
return(FALSE)
890
}
891
892
min_sum_fail <- function(weights, min_sum){
893
# return FALSE if min_sum is null
894
if(is.null(min_sum)) return(FALSE)
895
896
# sum of weights violate min_sum constraint
897
return(sum(weights) < min_sum)
898
}
899
900
max_sum_fail <- function(weights, max_sum){
901
# return FALSE if max_sum is null
902
if(is.null(max_sum)) return(FALSE)
903
904
# sum of weights violate max_sum constraint
905
return(sum(weights) > max_sum)
906
}
907
908
leverage_fail <- function(weights, leverage){
909
# return FALSE if leverage is null
910
if(is.null(leverage)) return(FALSE)
911
912
# sum of absolute value of weight violates leverage constraint
913
return(sum(abs(weights)) > leverage)
914
}
915
916
rp_increase <- function(weights, min_sum, max_box, weight_seq){
917
# randomly permute and increase a random portfolio element if the sum of
918
# the weights is less than min_sum while respecting box constraints
919
920
if(sum(weights) >= min_sum) return(weights)
921
922
tmp_w <- weights
923
n_weights <- length(weights)
924
# random_index <- sample(1:length(weights), max_pos)
925
random_index <- sample(1:n_weights, n_weights)
926
i <- 1
927
while (sum(tmp_w) < min_sum & i <= n_weights) {
928
# print("min_sum violation loop")
929
930
cur_index <- random_index[i]
931
cur_val <- tmp_w[cur_index]
932
tmp_seq <- weight_seq[(weight_seq > cur_val) & (weight_seq <= max_box[cur_index])]
933
n_tmp_seq <- length(tmp_seq)
934
if(n_tmp_seq > 1){
935
# randomly sample one of the larger weights
936
tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]
937
} else if(n_tmp_seq == 1){
938
tmp_w[cur_index] <- tmp_seq
939
}
940
i <- i + 1 # increment our counter
941
} # end increase loop
942
return(tmp_w)
943
}
944
945
rp_decrease <- function(weights, max_sum, min_box, weight_seq){
946
# randomly permute and decrease a random portfolio element if the sum of
947
# the weights is greater than max_sum while respecting box constraints
948
949
if(sum(weights) <= max_sum) return(weights)
950
951
tmp_w <- weights
952
n_weights <- length(weights)
953
# random_index <- sample(1:length(weights), max_pos)
954
random_index <- sample(1:n_weights, n_weights)
955
956
i <- 1
957
while (sum(tmp_w) > max_sum & i <= n_weights) {
958
# print("max_sum violation loop")
959
960
cur_index <- random_index[i]
961
cur_val <- tmp_w[cur_index]
962
tmp_seq <- weight_seq[(weight_seq < cur_val) & (weight_seq >= min_box[cur_index])]
963
n_tmp_seq <- length(tmp_seq)
964
if(n_tmp_seq > 1){
965
tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]
966
} else if(n_tmp_seq == 1){
967
tmp_w[cur_index] <- tmp_seq
968
}
969
i <- i + 1 # increment our counter
970
} # end decrease loop
971
return(tmp_w)
972
}
973
974
rp_decrease_leverage <- function(weights, max_box, min_box, leverage, weight_seq){
975
# randomly permute and increae decrease a random portfolio element
976
# according to leverage exposure while respecting box constraints
977
978
tmp_w <- weights
979
n_weights <- length(weights)
980
# random_index <- sample(1:length(weights), max_pos)
981
random_index <- sample(1:n_weights, n_weights)
982
983
# set counter to 1 for leverage violation loop
984
i <- 1
985
while (sum(abs(tmp_w)) > leverage & i <= length(tmp_w)) {
986
#print("leverage violation loop")
987
988
cur_index <- random_index[i]
989
cur_val <- tmp_w[cur_index]
990
991
tmp_seq <- NULL
992
# check the sign of the current value
993
if(cur_val < 0){
994
# if the current value is negative, we want to increase to lower
995
# sum(abs(weights)) while respecting uppper bound box constraint
996
tmp_seq <- weight_seq[(weight_seq > cur_val) & (weight_seq <= max_box[cur_index])]
997
} else if(cur_val > 0){
998
# if the current value is positive, we want to decrease to lower
999
# sum(abs(weights)) while respecting lower bound box constraint
1000
tmp_seq <- weight_seq[(weight_seq < cur_val) & (weight_seq >= min_box[cur_index])]
1001
}
1002
# tmp_seq can be NULL if cur_val is zero
1003
if(!is.null(tmp_seq)){
1004
n_tmp_seq <- length(tmp_seq)
1005
1006
if(n_tmp_seq > 1) {
1007
# randomly sample one of the weights
1008
tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]
1009
} else if(n_tmp_seq == 1){
1010
tmp_w[cur_index] <- tmp_seq
1011
}
1012
}
1013
i <- i + 1 # increment our counter
1014
} # end leverage violation loop
1015
return(tmp_w)
1016
}
1017
1018
rp_position_limit <- function(weights, max_pos=NULL, max_pos_long=NULL, max_pos_short=NULL, min_box, max_box, weight_seq){
1019
tmp_w <- weights
1020
n_weights <- length(weights)
1021
# random_index <- sample(1:length(weights), max_pos)
1022
random_index <- sample(1:n_weights, n_weights)
1023
1024
tolerance <- .Machine$double.eps^0.5
1025
1026
# set counter to 1 for position limit violation loop
1027
i <- 1
1028
while ( pos_limit_fail(tmp_w, max_pos, max_pos_long, max_pos_short) & i <= length(tmp_w)) {
1029
#print("position limit violation loop")
1030
1031
cur_index <- random_index[i]
1032
cur_val <- tmp_w[cur_index]
1033
1034
if(!is.null(max_pos_long)){
1035
# Check if max_pos_long is violated
1036
# If max_pos_long is violated, we we grab a positive weight and set it
1037
# to be between min_box and 0
1038
if(sum(tmp_w > tolerance) > max_pos_long){
1039
if(cur_val > tolerance){
1040
# subset such that min_box_i <= weight_i <= 0
1041
tmp_seq <- weight_seq[(weight_seq <= 0) & (weight_seq >= min_box[cur_index])]
1042
n_tmp_seq <- length(tmp_seq)
1043
if(n_tmp_seq > 1){
1044
tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]
1045
} else if(n_tmp_seq == 1){
1046
tmp_w[cur_index] <- tmp_seq
1047
}
1048
}
1049
} # end max_pos_long violation loop
1050
}
1051
1052
if(!is.null(max_pos_short)){
1053
# Check if max_pos_short is violated
1054
# If max_pos_short is violated, we grab a negative weight and set it
1055
# to be between 0 and max_box
1056
if(sum(tmp_w < tolerance) > max_pos_short){
1057
if(cur_val < tolerance){
1058
# subset such that 0 <= weight_i <= max_box_i
1059
tmp_seq <- weight_seq[(weight_seq >= 0) & (weight_seq <= max_box[cur_index])]
1060
n_tmp_seq <- length(tmp_seq)
1061
if(n_tmp_seq > 1){
1062
tmp_w[cur_index] <- tmp_seq[sample.int(n=n_tmp_seq, size=1L, replace=FALSE, prob=NULL)]
1063
} else if(n_tmp_seq == 1){
1064
tmp_w[cur_index] <- tmp_seq
1065
}
1066
}
1067
} # end max_pos_short violation loop
1068
}
1069
i <- i + 1 # increment our counter
1070
} # end position limit violation loop
1071
return(tmp_w)
1072
}
1073
1074
1075
# test
1076
# w <- c(0.1, 0.25, 0.3, 0.15, 0.05, 0.15)
1077
# min <- rep(0.1, length(w))
1078
# max <- rep(0.45, length(w))
1079
# w1 <- rp_normalize(w=w, min_sum=0.99, max_sum=1.01, min=min, max=max)
1080
# w1
1081
# sum(w1)
1082
# any(w1 < min)
1083
# any(w1 > max)
1084
1085
# library(PortfolioAnalytics)
1086
# data(edhec)
1087
# ret <- edhec[, 1:4]
1088
# funds <- colnames(ret)
1089
#
1090
# pspec <- portfolio.spec(assets=funds)
1091
# pspec <- add.constraint(portfolio=pspec, type="weight_sum", min_sum=0.99, max_sum=1.01, enabled=TRUE)
1092
# pspec <- add.constraint(portfolio=pspec, type="box", enabled=TRUE)
1093
# pspec <- add.constraint(portfolio=pspec, type="group", groups=c(2,2), group_min=c(0.1, 0.2), group_max=c(0.3, 0.8), enabled=TRUE)
1094
#
1095
# weights <- c(0.15, 0.2, 0.15, 0.5)
1096
# sum(weights)
1097
#
1098
# (w <- constraint_fn_map(weights, pspec))
1099
# sum(w)
1100
1101
1102
###############################################################################
1103
# R (https://r-project.org/) Numeric Methods for Optimization of Portfolios
1104
#
1105
# Copyright (c) 2004-2021 Brian G. Peterson, Peter Carl, Ross Bennett, Kris Boudt
1106
#
1107
# This library is distributed under the terms of the GNU Public License (GPL)
1108
# for full details see the file COPYING
1109
#
1110
# $Id$
1111
#
1112
###############################################################################
1113
1114