data(edhec)
R <- edhec["2008::2012", 1:6]
funds <- colnames(R)PortfolioAnalytics provides a flexible framework for portfolio optimization that separates the specification of objectives and constraints from the solver used to find optimal portfolios. This solver-agnostic design allows users to swap optimization backends without rewriting their portfolio specification, making it straightforward to compare results across methods.
The typical workflow is:
portfolio.spec()add.constraint()add.objective()optimize.portfolio(), passing an optimize_methodBecause the first three steps are solver-independent, comparing solvers requires changing only the optimize_method argument (and, in some cases, a ratio flag such as maxSR=TRUE).
This vignette provides:
PortfolioAnalytics supports solvers spanning three categories: convex solvers that exploit problem structure for exact solutions, global metaheuristic solvers that use stochastic search for arbitrary objectives, and a multi-objective solver for Pareto optimization.
CVXR provides the most comprehensive convex solver support in PortfolioAnalytics. It uses Disciplined Convex Programming (DCP) to model and solve LP, QP, and SOCP problems. Supported objectives include variance, ES/CVaR, CSM (Coherent Second Moment), EQS (Expected Quadratic Shortfall), and HHI (weight concentration), as well as ratio objectives (Sharpe, STARR, CSM ratio, EQS ratio) via Charnes-Cooper transformations.
CVXR delegates to a backend solver selected automatically or by the user:
| Backend | Problem classes | Selection |
|---|---|---|
| OSQP | QP (variance, Sharpe) | Default for QP problems |
| CLARABEL | LP, QP, SOCP (ES, CSM, EQS, HHI) | Default for non-QP problems |
| SCS | LP, QP, SOCP | User-specified |
| ECOS | LP, SOCP | User-specified |
| GLPK | LP, MILP | User-specified |
| MOSEK | LP, QP, SOCP | User-specified (commercial) |
| GUROBI | LP, QP, MILP | User-specified (commercial) |
To select a backend explicitly, pass a two-element vector: optimize_method = c("CVXR", "ECOS").
The R Optimization Infrastructure (ROI) dispatches to plugin solvers based on problem structure. When optimize_method = "ROI", the plugin is selected automatically:
| Plugin | Problem class | Used for |
|---|---|---|
quadprog |
QP | Min variance, max quadratic utility, max Sharpe ratio |
glpk |
LP/MILP | Max return, min ES/CVaR, max STARR, position limits |
symphony |
LP/MILP | Alternative to glpk |
Users can also specify the plugin directly: optimize_method = "quadprog" or optimize_method = "glpk".
ROI additionally supports turnover constraints (gmv_opt_toc), proportional transaction costs (gmv_opt_ptc), and leverage exposure constraints (gmv_opt_leverage) for QP problems, as well as position limits via MILP formulations.
A standalone QP solver accessed via optimize_method = "osqp". It supports mean and variance/StdDev objectives only. When both are specified, osqp solves the maximum Sharpe ratio problem natively using a Charnes-Cooper (homogeneous) QP reformulation—no maxSR flag is needed.
A standalone LP/MILP solver accessed via optimize_method = "Rglpk". It supports mean and ES/CVaR objectives. When both are specified, Rglpk solves the maximum STARR ratio problem natively via a Charnes-Cooper LP transformation. Position limits (including group position limits) are supported via MILP formulations with binary variables.
Metaheuristic solvers are general-purpose optimization algorithms that do not require convexity or explicit mathematical formulations. They are also called global optimization solvers or global stochastic solvers or numerical optimization solvers. PortfolioAnalytics includes Differential Evolution (DEoptim), Generalized Simulated Annealing (GenSA), Particle Swarm Optimization (pso), and a built-in random search method.
Metaheuristic solvers minimize constrained_objective(), a penalty-based wrapper that evaluates any R function as an objective and adds penalty terms for constraint violations. This makes them suitable for non-convex or black-box problems at the cost of longer run times and no guarantee of global optimality.
| Solver | Package | Method | Key parameters |
|---|---|---|---|
| DEoptim | DEoptim |
Differential Evolution | itermax, NP, strategy (default: 2 DE/local-to-best/1/bin) |
| GenSA | GenSA |
Generalized Simulated Annealing | maxit, temperature |
| pso | pso |
Particle Swarm Optimization | maxit, s (swarm size) |
| random | built-in | Random portfolio search | search_size, rp_method |
DEoptim uses fnMap (the fn_map() function) to project candidate solutions back to feasible space after each mutation step, ensuring that each generation of candidate portfolios are actually feasible given the constraints, while GenSA and pso rely on internal normalization via fn_map() inside constrained_objective(). The fn_map() function enforces box, leverage, group, position limit, and factor exposure constraints. For factor exposure, fn_map() uses a QP projection step (via quadprog::solve.QP) that finds the nearest feasible weight vector satisfying all linear constraints simultaneously.
The random method generates a matrix of feasible random portfolios up front and evaluates them all, selecting the best. The rp_method argument controls the random portfolio generation method, with options for uniform random sampling (rp_method="sample"), grid search (rp_method="grid"), and Simplex (rp_method="simplex") solutions which try to sample the outer vertex bounds of the combinations. The random portfolio method is also used as an initial population for the DEoptim, GenSA, and pso solvers when initial_pop = TRUE (the default) to supply the global solvers with a starting point that meets all or almost all of the constraints on the portfolio so that the solver engine starts with a full set of feasible portfolios rather than only using box constraints, which is typically the only constraint the global solvers support natively.
Note
Metaheuristic solvers should generally not be used for problems that have a convex formulation (minimum variance, minimum ES, maximum Sharpe, etc.). Convex solvers are faster, deterministic, and guaranteed to find the global optimum. Metaheuristic solvers are included in the convex worked examples below for comparison and completeness, but in practice they are most valuable for non-convex objectives such as risk budgets, custom risk measures, or problems with non-convex constraints.
The worked examples in this vignette use a small number of iterations (
search_size = 5000) and default hyperparameters for all metaheuristic solvers. In practice, increasing the number of generations/iterations and tuning solver-specific hyperparameters (e.g.,NPandstrategyfor DEoptim,temperaturefor GenSA, swarm sizesfor pso) can be expected to improve convergence speed and solution quality. PortfolioAnalytics attempts to set reasonable defaults for all solvers, but users should experiment with these settings for their specific problem and computational budget.
The global solvers are also suitable for many multi-objective problems that do not have a convex formulation, such as maximizing return subject to a risk budget constraint or minimizing a custom risk measure subject to factor exposure limits and other corner limits which break convexity. They may also be the best choice for significantly non-normal distributions where the convex approximations of variance and ES may not be good representations of future risk and reward.
The mco package (NSGA-II) is available via optimize_method = "mco" for Pareto-optimal frontier computation across multiple objectives simultaneously.
| Solver | Type | Problem classes | Ratio objectives | Position limits |
|---|---|---|---|---|
| CVXR | Convex | LP, QP, SOCP | maxSR, maxSTARR, CSMratio, EQSratio | No |
| ROI | Convex | QP, LP/MILP | maxSR, maxSTARR | Yes (MILP) |
| osqp | Convex | QP | maxSR (implicit) | No |
| Rglpk | Convex | LP/MILP | maxSTARR (implicit) | Yes (MILP) |
| DEoptim | Metaheuristic | Any | Via combined objectives | Via penalty |
| GenSA | Metaheuristic | Any | Via combined objectives | Via penalty |
| pso | Metaheuristic | Any | Via combined objectives | Via penalty |
| random | Metaheuristic | Any | Via combined objectives | Via penalty |
| mco | Multi-objective | Any (Pareto) | N/A | Via penalty |
The following table maps each optimization problem to the solvers that support it. Native means the solver handles the problem with a direct mathematical formulation. Penalty means the solver uses constrained_objective() with penalty terms for constraint satisfaction.
| Problem | CVXR | ROI | osqp | Rglpk | DEoptim | GenSA | pso | random |
|---|---|---|---|---|---|---|---|---|
| Max return | Native | Native | Native | Native | Penalty | Penalty | Penalty | Penalty |
| Min variance / StdDev | Native | Native | Native | – | Penalty | Penalty | Penalty | Penalty |
| Min ES / CVaR | Native | Native | – | Native | Penalty | Penalty | Penalty | Penalty |
| Min CSM | Native | – | – | – | Penalty | Penalty | Penalty | Penalty |
| Min EQS | Native | – | – | – | – | – | – | – |
| Max Sharpe ratio | Native | Native | Native | – | Penalty | Penalty | Penalty | Penalty |
| Max STARR (mean/ES) | Native | Native | – | Native | Penalty | Penalty | Penalty | Penalty |
| Max CSM ratio | Native | – | – | – | Penalty | Penalty | Penalty | Penalty |
| Max EQS ratio | Native | – | – | – | – | – | – | – |
| Max quadratic utility | Native | Native | – | – | Penalty | Penalty | Penalty | Penalty |
| Min variance + HHI | Native | Native | – | – | Penalty | Penalty | Penalty | Penalty |
| Risk budget (ES) | – | – | – | – | Penalty | Penalty | Penalty | Penalty |
| Risk budget (StdDev) | – | – | – | – | Penalty | Penalty | Penalty | Penalty |
Notes:
constrained_objective dispatch for CSMmaxSR flag is needed| Constraint | CVXR | ROI | osqp | Rglpk | Metaheuristic5 |
|---|---|---|---|---|---|
| Box (min/max per asset) | Yes | Yes | Yes | Yes | Yes |
| Weight sum / leverage | Yes | Yes | Yes | Yes | Yes (penalty) |
| Full investment | Yes | Yes | Yes | Yes | Yes (penalty) |
| Long only | Yes | Yes | Yes | Yes | Yes (penalty) |
| Group | Yes | Yes | Yes | Yes | Yes (penalty) |
| Target return | Yes | Yes | Yes | Yes | Yes (penalty) |
| Factor exposure | Yes | Yes | Yes | Yes | Yes (QP projection) |
| Factor exposure (MILP) | – | Yes | – | – | Yes (QP projection) |
| Turnover | Yes | Yes1 | – | – | Yes (penalty) |
| Transaction cost | – | Yes2 | – | – | Yes (penalty) |
| Position limit | – | Yes3 | – | Yes3 | Yes (penalty) |
| Leverage exposure | – | Yes4 | – | – | Yes (penalty) |
| Diversification | – | – | – | – | Yes (penalty) |
1 QP problems only, via gmv_opt_toc(). 2 QP problems only, via gmv_opt_ptc(). 3 Via MILP formulation with binary variables. 4 QP problems only, via gmv_opt_leverage(). 5 additional constraints supported via fn_map() projection step.
All examples use a subset of the EDHEC hedge fund index data.
data(edhec)
R <- edhec["2008::2012", 1:6]
funds <- colnames(R)We define a base portfolio specification with full-investment and long-only constraints that will be reused across examples.
base.port <- portfolio.spec(assets = funds)
base.port <- add.constraint(base.port, type = "full_investment")
base.port <- add.constraint(base.port, type = "long_only")Minimize portfolio variance (equivalently, standard deviation). This is a QP problem supported natively by CVXR, ROI, and osqp.
minvar.port <- add.objective(base.port, type = "risk", name = "StdDev")minvar.cvxr <- optimize.portfolio(R, minvar.port, optimize_method = "CVXR")
minvar.cvxr
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = minvar.port, optimize_method = "CVXR")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0000 0.1934 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.8066 0.0000
#>
#> Objective Measures:
#> StdDev
#> 0.01056minvar.roi <- optimize.portfolio(R, minvar.port, optimize_method = "ROI")
minvar.roi
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = minvar.port, optimize_method = "ROI")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0000 0.1934 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.8066 0.0000
#>
#> Objective Measure:
#> StdDev
#> 0.01056minvar.osqp <- optimize.portfolio(R, minvar.port, optimize_method = "osqp")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
minvar.osqp
#> $weights
#> Convertible Arbitrage CTA Global Distressed Securities
#> -4.840016e-18 1.933516e-01 -4.747323e-18
#> Emerging Markets Equity Market Neutral Event Driven
#> -5.093419e-18 8.066484e-01 -4.283774e-18
#>
#> $objective_measures
#> $objective_measures$StdDev
#> [,1]
#> [1,] 0.01056127
#> attr(,"names")
#> [1] "StdDev"
#>
#>
#> $opt_values
#> $opt_values$StdDev
#> [,1]
#> [1,] 0.01056127
#> attr(,"names")
#> [1] "StdDev"
#>
#>
#> $out
#> [1] 5.57702e-05
#>
#> $call
#> optimize.portfolio(R = R, portfolio = minvar.port, optimize_method = "osqp")
#>
#> $portfolio
#> **************************************************
#> PortfolioAnalytics Portfolio Specification
#> **************************************************
#>
#> Call:
#> portfolio.spec(assets = funds)
#>
#> Number of assets: 6
#> Asset Names
#> [1] "Convertible Arbitrage" "CTA Global" "Distressed Securities"
#> [4] "Emerging Markets" "Equity Market Neutral" "Event Driven"
#>
#> Constraints
#> Enabled constraint types
#> - full_investment
#> - long_only
#>
#> Objectives:
#> Enabled objective names
#> - StdDev
#>
#>
#> $data_summary
#> $data_summary$first
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2008-01-31 -9e-04 0.0255 -0.0233
#> Emerging Markets Equity Market Neutral Event Driven
#> 2008-01-31 -0.0503 -0.0112 -0.0271
#>
#> $data_summary$last
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2012-12-31 0.0098 0.0057 0.0259
#> Emerging Markets Equity Market Neutral Event Driven
#> 2012-12-31 0.033 0.0037 0.0193
#>
#>
#> $elapsed_time
#> Time difference of 0.004856825 secs
#>
#> $end_t
#> [1] "2026-05-29 09:11:27.44125"
#>
#> attr(,"class")
#> [1] "optimize.portfolio.osqp" "optimize.portfolio"Global solvers require slightly relaxed leverage constraints because they work with continuous weight vectors that may not sum to exactly 1.
minvar.port.meta <- base.port
minvar.port.meta$constraints[[1]]$min_sum <- 0.99
minvar.port.meta$constraints[[1]]$max_sum <- 1.01
minvar.port.meta <- add.objective(minvar.port.meta, type = "risk",
name = "StdDev")
set.seed(42)
minvar.de <- optimize.portfolio(R, minvar.port.meta,
optimize_method = "DEoptim",
search_size = 5000, trace = TRUE)
#> Iteration: 1 bestvalit: 0.011190 bestmemit: 0.032000 0.048000 0.000000 0.000000 0.914000 0.000000
#> Iteration: 2 bestvalit: 0.011190 bestmemit: 0.032000 0.048000 0.000000 0.000000 0.914000 0.000000
#> Iteration: 3 bestvalit: 0.011190 bestmemit: 0.032000 0.048000 0.000000 0.000000 0.914000 0.000000
#> Iteration: 4 bestvalit: 0.011190 bestmemit: 0.032000 0.048000 0.000000 0.000000 0.914000 0.000000
#> Iteration: 5 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 6 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 7 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 8 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 9 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 10 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 11 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 12 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 13 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 14 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> Iteration: 15 bestvalit: 0.011163 bestmemit: 0.006000 0.184000 0.004000 0.026000 0.740000 0.044000
#> [1] 0.006 0.184 0.004 0.026 0.740 0.044
minvar.de
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = minvar.port.meta, optimize_method = "DEoptim",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.006 0.184 0.004
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.026 0.740 0.044
#>
#> Objective Measures:
#> StdDev
#> 0.01116set.seed(42)
minvar.gensa <- optimize.portfolio(R, minvar.port.meta,
optimize_method = "GenSA",
search_size = 5000, trace = TRUE)
#> Emini is: 0.01069479267
#> xmini are:
#> 0.1947578841 0.2746808776 0.1643902301 0.4881932622 0.08269209905 0.115836455
#> Totally it used 7.564263 secs
#> No. of function call is: 5890
#> Algorithm reached max number of iterations.
minvar.gensa
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = minvar.port.meta, optimize_method = "GenSA",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1490 0.2101 0.1257
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.3734 0.0632 0.0886
#>
#> Objective Measures:
#> StdDev
#> 0.02344set.seed(42)
minvar.pso <- optimize.portfolio(R, minvar.port.meta,
optimize_method = "pso",
search_size = 5000, trace = TRUE)
#> S=14, K=3, p=0.1993, w0=0.7213, w1=0.7213, c.p=1.193, c.g=1.193
#> v.max=NA, d=2.449, vectorize=FALSE, hybrid=off
#> It 10: fitness=0.01118, swarm diam.=0.8723
#> It 20: fitness=0.01118, swarm diam.=0.9671
#> It 30: fitness=0.01118, swarm diam.=0.965
#> It 40: fitness=0.01118, swarm diam.=0.8885
#> It 50: fitness=0.01118, swarm diam.=0.9498
#> It 60: fitness=0.01086, swarm diam.=0.9047
#> It 70: fitness=0.01086, swarm diam.=1.007
#> It 80: fitness=0.01086, swarm diam.=0.9466
#> It 90: fitness=0.01086, swarm diam.=1
#> It 100: fitness=0.01086, swarm diam.=0.9769
#> It 110: fitness=0.01085, swarm diam.=1.283
#> It 120: fitness=0.01085, swarm diam.=1.288
#> It 130: fitness=0.01085, swarm diam.=1.387
#> It 140: fitness=0.01085, swarm diam.=1.29
#> It 150: fitness=0.01085, swarm diam.=1.309
#> It 160: fitness=0.01082, swarm diam.=0.9673
#> It 170: fitness=0.0108, swarm diam.=0.8444
#> It 180: fitness=0.01071, swarm diam.=1.051
#> It 190: fitness=0.01071, swarm diam.=1.017
#> It 200: fitness=0.01071, swarm diam.=1.111
#> It 210: fitness=0.01071, swarm diam.=1.188
#> It 220: fitness=0.01071, swarm diam.=1.271
#> It 230: fitness=0.01071, swarm diam.=1.275
#> It 240: fitness=0.01071, swarm diam.=1.46
#> It 250: fitness=0.01071, swarm diam.=1.151
#> It 260: fitness=0.01068, swarm diam.=0.945
#> It 270: fitness=0.01068, swarm diam.=1.292
#> It 280: fitness=0.01068, swarm diam.=1.156
#> It 290: fitness=0.01068, swarm diam.=1.206
#> It 300: fitness=0.01068, swarm diam.=1.112
#> Maximal number of iterations reached
minvar.pso
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = minvar.port.meta, optimize_method = "pso",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.2986 0.3182 0.0127
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.3243 0.0562
#>
#> Objective Measures:
#> StdDev
#> 0.01425minvar.weights <- round(cbind(
CVXR = minvar.cvxr$weights,
ROI = minvar.roi$weights,
osqp = minvar.osqp$weights,
DEoptim = minvar.de$weights,
GenSA = minvar.gensa$weights,
pso = minvar.pso$weights
), 4)
knitr::kable(minvar.weights, caption = "Minimum Variance Weights")| CVXR | ROI | osqp | DEoptim | GenSA | pso | |
|---|---|---|---|---|---|---|
| Convertible Arbitrage | 0.0000 | 0.0000 | 0.0000 | 0.006 | 0.1490 | 0.2986 |
| CTA Global | 0.1934 | 0.1934 | 0.1934 | 0.184 | 0.2101 | 0.3182 |
| Distressed Securities | 0.0000 | 0.0000 | 0.0000 | 0.004 | 0.1257 | 0.0127 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 | 0.026 | 0.3734 | 0.0000 |
| Equity Market Neutral | 0.8066 | 0.8066 | 0.8066 | 0.740 | 0.0632 | 0.3243 |
| Event Driven | 0.0000 | 0.0000 | 0.0000 | 0.044 | 0.0886 | 0.0562 |
Note
All convex solvers produce identical or near-identical results for minimum variance, while global solvers vary widely, DEoptim coming close, and GenSA and pso taking significantly longer and being farther from the optimal solution.
Minimize Expected Shortfall (also called CVaR or ETL) at the 95% confidence level. This is an LP problem (Rockafellar-Uryasev formulation) supported natively by CVXR, ROI, and Rglpk.
mines.port <- add.objective(base.port, type = "risk", name = "ES",
arguments = list(p = 0.95))mines.cvxr <- optimize.portfolio(R, mines.port, optimize_method = "CVXR")
mines.cvxr
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = mines.port, optimize_method = "CVXR")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0000 0.4984 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.4223 0.0793
#>
#> Objective Measures:
#> ES
#> 0.01917mines.roi <- optimize.portfolio(R, mines.port, optimize_method = "ROI")
mines.roi
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = mines.port, optimize_method = "ROI")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0000 0.4984 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.4223 0.0793
#>
#> Objective Measure:
#> ES
#> 0.01917mines.rglpk <- optimize.portfolio(R, mines.port, optimize_method = "Rglpk")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
mines.rglpk
#> $weights
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.00000000 0.49839381 0.00000000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.00000000 0.42228655 0.07931963
#>
#> $objective_measures
#> $objective_measures$ES
#> [,1]
#> ES 0.02140448
#> attr(,"names")
#> [1] "ES"
#>
#>
#> $opt_values
#> $opt_values$ES
#> [,1]
#> ES 0.02140448
#> attr(,"names")
#> [1] "ES"
#>
#>
#> $out
#> [1] -0.01917222
#>
#> $call
#> optimize.portfolio(R = R, portfolio = mines.port, optimize_method = "Rglpk")
#>
#> $portfolio
#> **************************************************
#> PortfolioAnalytics Portfolio Specification
#> **************************************************
#>
#> Call:
#> portfolio.spec(assets = funds)
#>
#> Number of assets: 6
#> Asset Names
#> [1] "Convertible Arbitrage" "CTA Global" "Distressed Securities"
#> [4] "Emerging Markets" "Equity Market Neutral" "Event Driven"
#>
#> Constraints
#> Enabled constraint types
#> - full_investment
#> - long_only
#>
#> Objectives:
#> Enabled objective names
#> - ES
#>
#>
#> $data_summary
#> $data_summary$first
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2008-01-31 -9e-04 0.0255 -0.0233
#> Emerging Markets Equity Market Neutral Event Driven
#> 2008-01-31 -0.0503 -0.0112 -0.0271
#>
#> $data_summary$last
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2012-12-31 0.0098 0.0057 0.0259
#> Emerging Markets Equity Market Neutral Event Driven
#> 2012-12-31 0.033 0.0037 0.0193
#>
#>
#> $elapsed_time
#> Time difference of 0.003423214 secs
#>
#> $end_t
#> [1] "2026-05-29 09:11:42.951499"
#>
#> attr(,"class")
#> [1] "optimize.portfolio.Rglpk" "optimize.portfolio"mines.port.meta <- base.port
mines.port.meta$constraints[[1]]$min_sum <- 0.99
mines.port.meta$constraints[[1]]$max_sum <- 1.01
mines.port.meta <- add.objective(mines.port.meta, type = "risk", name = "ES",
arguments = list(p = 0.95))
set.seed(42)
mines.de <- optimize.portfolio(R, mines.port.meta,
optimize_method = "DEoptim",
search_size = 5000, trace = TRUE)
#> Iteration: 1 bestvalit: 0.023263 bestmemit: 0.036000 0.326000 0.000000 0.000000 0.472000 0.168000
#> Iteration: 2 bestvalit: 0.023220 bestmemit: 0.008000 0.392000 0.012000 0.014000 0.308000 0.258000
#> Iteration: 3 bestvalit: 0.023220 bestmemit: 0.008000 0.392000 0.012000 0.014000 0.308000 0.258000
#> Iteration: 4 bestvalit: 0.022794 bestmemit: 0.000000 0.474000 0.008000 0.000000 0.260000 0.258000
#> Iteration: 5 bestvalit: 0.022794 bestmemit: 0.000000 0.474000 0.008000 0.000000 0.260000 0.258000
#> Iteration: 6 bestvalit: 0.022602 bestmemit: 0.022000 0.414000 0.000000 0.050000 0.448000 0.076000
#> Iteration: 7 bestvalit: 0.022493 bestmemit: 0.000000 0.336000 0.010000 0.022000 0.526000 0.098000
#> Iteration: 8 bestvalit: 0.022284 bestmemit: 0.039473 0.461331 0.098677 0.010000 0.342000 0.056000
#> Iteration: 9 bestvalit: 0.022284 bestmemit: 0.039473 0.461331 0.098677 0.010000 0.342000 0.056000
#> Iteration: 10 bestvalit: 0.022284 bestmemit: 0.039473 0.461331 0.098677 0.010000 0.342000 0.056000
#> Iteration: 11 bestvalit: 0.022284 bestmemit: 0.039473 0.461331 0.098677 0.010000 0.342000 0.056000
#> Iteration: 12 bestvalit: 0.022284 bestmemit: 0.039473 0.461331 0.098677 0.010000 0.342000 0.056000
#> Iteration: 13 bestvalit: 0.022284 bestmemit: 0.039473 0.461331 0.098677 0.010000 0.342000 0.056000
#> Iteration: 14 bestvalit: 0.021969 bestmemit: 0.014000 0.360000 0.082000 0.000000 0.548000 0.000000
#> Iteration: 15 bestvalit: 0.021202 bestmemit: 0.000000 0.386000 0.008000 0.006000 0.598000 0.004000
#> Iteration: 16 bestvalit: 0.021202 bestmemit: 0.000000 0.386000 0.008000 0.006000 0.598000 0.004000
#> Iteration: 17 bestvalit: 0.021202 bestmemit: 0.000000 0.386000 0.008000 0.006000 0.598000 0.004000
#> Iteration: 18 bestvalit: 0.021056 bestmemit: 0.002000 0.422000 0.008000 0.000000 0.484000 0.090000
#> Iteration: 19 bestvalit: 0.021056 bestmemit: 0.002000 0.422000 0.008000 0.000000 0.484000 0.090000
#> Iteration: 20 bestvalit: 0.021056 bestmemit: 0.002000 0.422000 0.008000 0.000000 0.484000 0.090000
#> Iteration: 21 bestvalit: 0.021056 bestmemit: 0.002000 0.422000 0.008000 0.000000 0.484000 0.090000
#> Iteration: 22 bestvalit: 0.021056 bestmemit: 0.002000 0.422000 0.008000 0.000000 0.484000 0.090000
#> Iteration: 23 bestvalit: 0.021056 bestmemit: 0.002000 0.422000 0.008000 0.000000 0.484000 0.090000
#> Iteration: 24 bestvalit: 0.021056 bestmemit: 0.002000 0.422000 0.008000 0.000000 0.484000 0.090000
#> Iteration: 25 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 26 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 27 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 28 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 29 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 30 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 31 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 32 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 33 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 34 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> Iteration: 35 bestvalit: 0.021055 bestmemit: 0.000000 0.394000 0.084000 0.000000 0.520000 0.000000
#> [1] 0.000 0.394 0.084 0.000 0.520 0.000
mines.de
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = mines.port.meta, optimize_method = "DEoptim",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.000 0.394 0.084
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.000 0.520 0.000
#>
#> Objective Measures:
#> ES
#> 0.02105set.seed(42)
mines.gensa <- optimize.portfolio(R, mines.port.meta,
optimize_method = "GenSA",
search_size = 5000, trace = TRUE)
#> Emini is: 0.0207662555
#> xmini are:
#> 0.7899720724 0.8895356971 0.5320053265 0.9854125038 0.6276693311 0.04337861694
#> Totally it used 9.295722 secs
#> No. of function call is: 6839
#> Algorithm reached max number of iterations.
mines.gensa
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = mines.port.meta, optimize_method = "GenSA",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.2063 0.2323 0.1389
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.2573 0.1639 0.0113
#>
#> Objective Measures:
#> ES
#> 0.05149set.seed(42)
mines.pso <- optimize.portfolio(R, mines.port.meta,
optimize_method = "pso",
search_size = 5000, trace = TRUE)
#> S=14, K=3, p=0.1993, w0=0.7213, w1=0.7213, c.p=1.193, c.g=1.193
#> v.max=NA, d=2.449, vectorize=FALSE, hybrid=off
#> It 10: fitness=0.02213, swarm diam.=0.8311
#> It 20: fitness=0.02213, swarm diam.=0.9327
#> It 30: fitness=0.02213, swarm diam.=1.073
#> It 40: fitness=0.02213, swarm diam.=1.201
#> It 50: fitness=0.02213, swarm diam.=1.202
#> It 60: fitness=0.02213, swarm diam.=1.548
#> It 70: fitness=0.02173, swarm diam.=1.437
#> It 80: fitness=0.02173, swarm diam.=1.36
#> It 90: fitness=0.02122, swarm diam.=1.234
#> It 100: fitness=0.02122, swarm diam.=1.212
#> It 110: fitness=0.02122, swarm diam.=1.365
#> It 120: fitness=0.02122, swarm diam.=1.191
#> It 130: fitness=0.02122, swarm diam.=1.239
#> It 140: fitness=0.02122, swarm diam.=1.353
#> It 150: fitness=0.02104, swarm diam.=1.02
#> It 160: fitness=0.02104, swarm diam.=1.031
#> It 170: fitness=0.02104, swarm diam.=1.111
#> It 180: fitness=0.02104, swarm diam.=1.123
#> It 190: fitness=0.02104, swarm diam.=0.9963
#> It 200: fitness=0.02104, swarm diam.=1.052
#> It 210: fitness=0.02104, swarm diam.=1.104
#> It 220: fitness=0.02104, swarm diam.=0.9389
#> It 230: fitness=0.02082, swarm diam.=1.298
#> It 240: fitness=0.02082, swarm diam.=1.064
#> It 250: fitness=0.02082, swarm diam.=1.155
#> It 260: fitness=0.02082, swarm diam.=1.146
#> It 270: fitness=0.02082, swarm diam.=1.159
#> It 280: fitness=0.02082, swarm diam.=1.193
#> It 290: fitness=0.02082, swarm diam.=1.365
#> It 300: fitness=0.02082, swarm diam.=1.155
#> Maximal number of iterations reached
mines.pso
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = mines.port.meta, optimize_method = "pso",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1041 0.0400 0.1463
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.2253 0.2135 0.2809
#>
#> Objective Measures:
#> ES
#> 0.03069mines.weights <- round(cbind(
CVXR = mines.cvxr$weights,
ROI = mines.roi$weights,
Rglpk = mines.rglpk$weights,
DEoptim = mines.de$weights,
GenSA = mines.gensa$weights,
pso = mines.pso$weights
), 4)
knitr::kable(mines.weights, caption = "Minimum ES Weights")| CVXR | ROI | Rglpk | DEoptim | GenSA | pso | |
|---|---|---|---|---|---|---|
| Convertible Arbitrage | 0.0000 | 0.0000 | 0.0000 | 0.000 | 0.2063 | 0.1041 |
| CTA Global | 0.4984 | 0.4984 | 0.4984 | 0.394 | 0.2323 | 0.0400 |
| Distressed Securities | 0.0000 | 0.0000 | 0.0000 | 0.084 | 0.1389 | 0.1463 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 | 0.000 | 0.2573 | 0.2253 |
| Equity Market Neutral | 0.4223 | 0.4223 | 0.4223 | 0.520 | 0.1639 | 0.2135 |
| Event Driven | 0.0793 | 0.0793 | 0.0793 | 0.000 | 0.0113 | 0.2809 |
Minimize the Coherent Second Moment, a downside risk measure that combines VaR and the second moment of losses beyond VaR. This is a SOCP problem available only through CVXR among the convex solvers.
mincsm.port <- add.objective(base.port, type = "risk", name = "CSM",
arguments = list(p = 0.05))mincsm.cvxr <- optimize.portfolio(R, mincsm.port, optimize_method = "CVXR")
mincsm.cvxr
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = mincsm.port, optimize_method = "CVXR")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0000 0.4234 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.4369 0.1397
#>
#> Objective Measures:
#> CSM
#> 0.02079Maximize expected (mean) return. This is an LP problem supported by all convex solvers.
maxret.port <- add.objective(base.port, type = "return", name = "mean")maxret.cvxr <- optimize.portfolio(R, maxret.port, optimize_method = "CVXR")
maxret.cvxr
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxret.port, optimize_method = "CVXR")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 1 0 0
#> Emerging Markets Equity Market Neutral Event Driven
#> 0 0 0
#>
#> Objective Measures:
#> mean
#> 0.004973maxret.roi <- optimize.portfolio(R, maxret.port, optimize_method = "ROI")
maxret.roi
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxret.port, optimize_method = "ROI")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 1 0 0
#> Emerging Markets Equity Market Neutral Event Driven
#> 0 0 0
#>
#> Objective Measure:
#> mean
#> 0.004973maxret.rglpk <- optimize.portfolio(R, maxret.port, optimize_method = "Rglpk")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
maxret.rglpk
#> $weights
#> Convertible Arbitrage CTA Global Distressed Securities
#> 1 0 0
#> Emerging Markets Equity Market Neutral Event Driven
#> 0 0 0
#>
#> $objective_measures
#> $objective_measures$mean
#> mean
#> 0.004973333
#>
#>
#> $opt_values
#> $opt_values$mean
#> mean
#> 0.004973333
#>
#>
#> $out
#> [1] 0.004973333
#>
#> $call
#> optimize.portfolio(R = R, portfolio = maxret.port, optimize_method = "Rglpk")
#>
#> $portfolio
#> **************************************************
#> PortfolioAnalytics Portfolio Specification
#> **************************************************
#>
#> Call:
#> portfolio.spec(assets = funds)
#>
#> Number of assets: 6
#> Asset Names
#> [1] "Convertible Arbitrage" "CTA Global" "Distressed Securities"
#> [4] "Emerging Markets" "Equity Market Neutral" "Event Driven"
#>
#> Constraints
#> Enabled constraint types
#> - full_investment
#> - long_only
#>
#> Objectives:
#> Enabled objective names
#> - mean
#>
#>
#> $data_summary
#> $data_summary$first
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2008-01-31 -9e-04 0.0255 -0.0233
#> Emerging Markets Equity Market Neutral Event Driven
#> 2008-01-31 -0.0503 -0.0112 -0.0271
#>
#> $data_summary$last
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2012-12-31 0.0098 0.0057 0.0259
#> Emerging Markets Equity Market Neutral Event Driven
#> 2012-12-31 0.033 0.0037 0.0193
#>
#>
#> $elapsed_time
#> Time difference of 0.002377987 secs
#>
#> $end_t
#> [1] "2026-05-29 09:12:01.105494"
#>
#> attr(,"class")
#> [1] "optimize.portfolio.Rglpk" "optimize.portfolio"maxret.weights <- round(cbind(
CVXR = maxret.cvxr$weights,
ROI = maxret.roi$weights,
Rglpk = maxret.rglpk$weights
), 4)
knitr::kable(maxret.weights, caption = "Maximum Return Weights")| CVXR | ROI | Rglpk | |
|---|---|---|---|
| Convertible Arbitrage | 1 | 1 | 1 |
| CTA Global | 0 | 0 | 0 |
| Distressed Securities | 0 | 0 | 0 |
| Emerging Markets | 0 | 0 | 0 |
| Equity Market Neutral | 0 | 0 | 0 |
| Event Driven | 0 | 0 | 0 |
With long-only and full-investment constraints, maximum return concentrates entirely in the single highest-returning asset. All convex solvers produce identical results.
Maximize the ratio of mean return to standard deviation. This is a fractional program reformulated as a QP via the Charnes-Cooper transformation.
The portfolio requires both a return and a risk objective:
maxsr.port <- add.objective(base.port, type = "return", name = "mean")
maxsr.port <- add.objective(maxsr.port, type = "risk", name = "StdDev")maxsr.cvxr <- optimize.portfolio(R, maxsr.port,
optimize_method = "CVXR", maxSR = TRUE)
maxsr.cvxr
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxsr.port, optimize_method = "CVXR",
#> maxSR = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0887 0.4285 0.4828
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.0000 0.0000
#>
#> Objective Measures:
#> mean
#> 0.003947
#>
#>
#> StdDev
#> 0.01642
#>
#>
#> Sharpe Ratio
#> 0.2404maxsr.roi <- optimize.portfolio(R, maxsr.port,
optimize_method = "ROI", maxSR = TRUE)
maxsr.roi
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxsr.port, optimize_method = "ROI",
#> maxSR = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0887 0.4285 0.4828
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.0000 0.0000
#>
#> Objective Measure:
#> StdDev
#> 0.01642
#>
#>
#> mean
#> 0.003947Important
The
maxSR = TRUEflag is required for CVXR and ROI. Without it, both solvers default to maximizing quadratic utility (mean - lambda * variance) rather than the Sharpe ratio.
osqp solves the Sharpe ratio problem implicitly when both mean and variance objectives are present. No maxSR flag is needed.
maxsr.osqp <- optimize.portfolio(R, maxsr.port, optimize_method = "osqp")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
maxsr.osqp
#> $weights
#> Convertible Arbitrage CTA Global Distressed Securities
#> 8.869180e-02 4.285369e-01 4.827713e-01
#> Emerging Markets Equity Market Neutral Event Driven
#> 6.540764e-19 -5.951872e-18 -8.085930e-18
#>
#> $objective_measures
#> $objective_measures$mean
#> mean
#> 0.003946873
#>
#> $objective_measures$StdDev
#> [,1]
#> [1,] 0.01641576
#> attr(,"names")
#> [1] "StdDev"
#>
#>
#> $opt_values
#> $opt_values$mean
#> mean
#> 0.003946873
#>
#> $opt_values$StdDev
#> [,1]
#> [1,] 0.01641576
#> attr(,"names")
#> [1] "StdDev"
#>
#>
#> $out
#> [1] 8.649398
#>
#> $call
#> optimize.portfolio(R = R, portfolio = maxsr.port, optimize_method = "osqp")
#>
#> $portfolio
#> **************************************************
#> PortfolioAnalytics Portfolio Specification
#> **************************************************
#>
#> Call:
#> portfolio.spec(assets = funds)
#>
#> Number of assets: 6
#> Asset Names
#> [1] "Convertible Arbitrage" "CTA Global" "Distressed Securities"
#> [4] "Emerging Markets" "Equity Market Neutral" "Event Driven"
#>
#> Constraints
#> Enabled constraint types
#> - full_investment
#> - long_only
#>
#> Objectives:
#> Enabled objective names
#> - mean
#> - StdDev
#>
#>
#> $data_summary
#> $data_summary$first
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2008-01-31 -9e-04 0.0255 -0.0233
#> Emerging Markets Equity Market Neutral Event Driven
#> 2008-01-31 -0.0503 -0.0112 -0.0271
#>
#> $data_summary$last
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2012-12-31 0.0098 0.0057 0.0259
#> Emerging Markets Equity Market Neutral Event Driven
#> 2012-12-31 0.033 0.0037 0.0193
#>
#>
#> $elapsed_time
#> Time difference of 0.002844095 secs
#>
#> $end_t
#> [1] "2026-05-29 09:12:01.387682"
#>
#> attr(,"class")
#> [1] "optimize.portfolio.osqp" "optimize.portfolio"Metaheuristic solvers approximate the Sharpe ratio by combining both objectives: out = -1 * mean + 1 * StdDev. This drives the optimizer toward high-return, low-risk portfolios but is not mathematically equivalent to maximizing mean/StdDev.
maxsr.port.meta <- base.port
maxsr.port.meta$constraints[[1]]$min_sum <- 0.99
maxsr.port.meta$constraints[[1]]$max_sum <- 1.01
maxsr.port.meta <- add.objective(maxsr.port.meta, type = "return",
name = "mean")
maxsr.port.meta <- add.objective(maxsr.port.meta, type = "risk",
name = "StdDev")
set.seed(42)
maxsr.de <- optimize.portfolio(R, maxsr.port.meta,
optimize_method = "DEoptim",
search_size = 5000, trace = TRUE)
#> Iteration: 1 bestvalit: 0.009817 bestmemit: 0.078000 0.358000 0.098000 0.002000 0.474000 0.000000
#> Iteration: 2 bestvalit: 0.009817 bestmemit: 0.078000 0.358000 0.098000 0.002000 0.474000 0.000000
#> Iteration: 3 bestvalit: 0.009436 bestmemit: 0.126000 0.258000 0.034000 0.000000 0.558000 0.014000
#> Iteration: 4 bestvalit: 0.009436 bestmemit: 0.126000 0.258000 0.034000 0.000000 0.558000 0.014000
#> Iteration: 5 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 6 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 7 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 8 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 9 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 10 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 11 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 12 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 13 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 14 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 15 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 16 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 17 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 18 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 19 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 20 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 21 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 22 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 23 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 24 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> [1] 0.056 0.204 0.040 0.000 0.686 0.004
maxsr.de
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxsr.port.meta, optimize_method = "DEoptim",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.056 0.204 0.040
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.000 0.686 0.004
#>
#> Objective Measures:
#> mean
#> 0.001732
#>
#>
#> StdDev
#> 0.01092set.seed(42)
maxsr.gensa <- optimize.portfolio(R, maxsr.port.meta,
optimize_method = "GenSA",
search_size = 5000, trace = TRUE)
#> Emini is: 0.009172386553
#> xmini are:
#> 0.7815859572 0.1979082211 0.8706658439 0.1762869507 0.8778037676 0.7008468873
#> Totally it used 7.542615 secs
#> No. of function call is: 5435
#> Algorithm reached max number of iterations.
maxsr.gensa
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxsr.port.meta, optimize_method = "GenSA",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.2190 0.0554 0.2439
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0494 0.2459 0.1963
#>
#> Objective Measures:
#> mean
#> 0.003317
#>
#>
#> StdDev
#> 0.01952set.seed(42)
maxsr.pso <- optimize.portfolio(R, maxsr.port.meta,
optimize_method = "pso",
search_size = 5000, trace = TRUE)
#> S=14, K=3, p=0.1993, w0=0.7213, w1=0.7213, c.p=1.193, c.g=1.193
#> v.max=NA, d=2.449, vectorize=FALSE, hybrid=off
#> It 10: fitness=0.009941, swarm diam.=1.01
#> It 20: fitness=0.009708, swarm diam.=1.263
#> It 30: fitness=0.009303, swarm diam.=0.8942
#> It 40: fitness=0.009303, swarm diam.=0.6566
#> It 50: fitness=0.009303, swarm diam.=0.867
#> It 60: fitness=0.009303, swarm diam.=0.7307
#> It 70: fitness=0.009303, swarm diam.=0.8989
#> It 80: fitness=0.009303, swarm diam.=0.8204
#> It 90: fitness=0.009303, swarm diam.=0.9491
#> It 100: fitness=0.009303, swarm diam.=1.042
#> It 110: fitness=0.00916, swarm diam.=0.8446
#> It 120: fitness=0.00916, swarm diam.=1.017
#> It 130: fitness=0.00916, swarm diam.=1.037
#> It 140: fitness=0.00916, swarm diam.=0.7328
#> It 150: fitness=0.00916, swarm diam.=0.868
#> It 160: fitness=0.00916, swarm diam.=0.7855
#> It 170: fitness=0.00916, swarm diam.=0.9233
#> It 180: fitness=0.00916, swarm diam.=0.8976
#> It 190: fitness=0.00916, swarm diam.=0.9045
#> It 200: fitness=0.00916, swarm diam.=0.8788
#> It 210: fitness=0.00916, swarm diam.=1.131
#> It 220: fitness=0.00916, swarm diam.=0.8961
#> It 230: fitness=0.00916, swarm diam.=0.7668
#> It 240: fitness=0.00916, swarm diam.=0.8377
#> It 250: fitness=0.00916, swarm diam.=0.8561
#> It 260: fitness=0.00916, swarm diam.=0.8791
#> It 270: fitness=0.00916, swarm diam.=0.729
#> It 280: fitness=0.00916, swarm diam.=0.9386
#> It 290: fitness=0.00916, swarm diam.=0.949
#> It 300: fitness=0.00916, swarm diam.=0.8336
#> Maximal number of iterations reached
maxsr.pso
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxsr.port.meta, optimize_method = "pso",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1950 0.2595 0.1114
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0383 0.1893 0.2164
#>
#> Objective Measures:
#> mean
#> 0.003183
#>
#>
#> StdDev
#> 0.01637maxsr.weights <- round(cbind(
CVXR = maxsr.cvxr$weights,
ROI = maxsr.roi$weights,
osqp = maxsr.osqp$weights,
DEoptim = maxsr.de$weights,
GenSA = maxsr.gensa$weights,
pso = maxsr.pso$weights
), 4)
knitr::kable(maxsr.weights, caption = "Maximum Sharpe Ratio Weights")| CVXR | ROI | osqp | DEoptim | GenSA | pso | |
|---|---|---|---|---|---|---|
| Convertible Arbitrage | 0.0887 | 0.0887 | 0.0887 | 0.056 | 0.2190 | 0.1950 |
| CTA Global | 0.4285 | 0.4285 | 0.4285 | 0.204 | 0.0554 | 0.2595 |
| Distressed Securities | 0.4828 | 0.4828 | 0.4828 | 0.040 | 0.2439 | 0.1114 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 | 0.000 | 0.0494 | 0.0383 |
| Equity Market Neutral | 0.0000 | 0.0000 | 0.0000 | 0.686 | 0.2459 | 0.1893 |
| Event Driven | 0.0000 | 0.0000 | 0.0000 | 0.004 | 0.1963 | 0.2164 |
Maximize the STARR ratio: mean return divided by Expected Shortfall. This is a fractional LP solved via Charnes-Cooper transformation.
maxstarr.port <- add.objective(base.port, type = "return", name = "mean")
maxstarr.port <- add.objective(maxstarr.port, type = "risk", name = "ES",
arguments = list(p = 0.95))maxstarr.cvxr <- optimize.portfolio(R, maxstarr.port,
optimize_method = "CVXR")
maxstarr.cvxr
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxstarr.port, optimize_method = "CVXR")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0000 0.5909 0.4091
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.0000 0.0000
#>
#> Objective Measures:
#> mean
#> 0.003636
#>
#>
#> ES
#> 0.0242
#>
#>
#> ES ratio
#> 0.1502maxstarr.roi <- optimize.portfolio(R, maxstarr.port,
optimize_method = "ROI")
maxstarr.roi
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxstarr.port, optimize_method = "ROI")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0000 0.5909 0.4091
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.0000 0.0000
#>
#> Objective Measure:
#> mean
#> 0.003636
#>
#>
#> ES
#> 0.0242Note
Unlike
maxSR, themaxSTARRflag defaults toTRUEfor both CVXR and ROI when both mean and ES objectives are present. PassmaxSTARR = FALSEto minimize ES subject to a mean-ES trade-off instead.
Rglpk solves the STARR ratio implicitly when both mean and ES objectives are present, using its own Charnes-Cooper LP formulation.
maxstarr.rglpk <- optimize.portfolio(R, maxstarr.port,
optimize_method = "Rglpk")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
maxstarr.rglpk
#> $weights
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0000000 0.5908558 0.4091442
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000000 0.0000000 0.0000000
#>
#> $objective_measures
#> $objective_measures$mean
#> mean
#> 0.003636305
#>
#> $objective_measures$ES
#> [,1]
#> ES 0.02580996
#> attr(,"names")
#> [1] "ES"
#>
#>
#> $opt_values
#> $opt_values$mean
#> mean
#> 0.003636305
#>
#> $opt_values$ES
#> [,1]
#> ES 0.02580996
#> attr(,"names")
#> [1] "ES"
#>
#>
#> $out
#> [1] -6.655709
#>
#> $call
#> optimize.portfolio(R = R, portfolio = maxstarr.port, optimize_method = "Rglpk")
#>
#> $portfolio
#> **************************************************
#> PortfolioAnalytics Portfolio Specification
#> **************************************************
#>
#> Call:
#> portfolio.spec(assets = funds)
#>
#> Number of assets: 6
#> Asset Names
#> [1] "Convertible Arbitrage" "CTA Global" "Distressed Securities"
#> [4] "Emerging Markets" "Equity Market Neutral" "Event Driven"
#>
#> Constraints
#> Enabled constraint types
#> - full_investment
#> - long_only
#>
#> Objectives:
#> Enabled objective names
#> - mean
#> - ES
#>
#>
#> $data_summary
#> $data_summary$first
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2008-01-31 -9e-04 0.0255 -0.0233
#> Emerging Markets Equity Market Neutral Event Driven
#> 2008-01-31 -0.0503 -0.0112 -0.0271
#>
#> $data_summary$last
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2012-12-31 0.0098 0.0057 0.0259
#> Emerging Markets Equity Market Neutral Event Driven
#> 2012-12-31 0.033 0.0037 0.0193
#>
#>
#> $elapsed_time
#> Time difference of 0.003211021 secs
#>
#> $end_t
#> [1] "2026-05-29 09:12:16.777162"
#>
#> attr(,"class")
#> [1] "optimize.portfolio.Rglpk" "optimize.portfolio"Metaheuristic solvers approximate the STARR ratio by combining both objectives: out = -1 * mean + 1 * ES.
maxstarr.port.meta <- base.port
maxstarr.port.meta$constraints[[1]]$min_sum <- 0.99
maxstarr.port.meta$constraints[[1]]$max_sum <- 1.01
maxstarr.port.meta <- add.objective(maxstarr.port.meta, type = "return",
name = "mean")
maxstarr.port.meta <- add.objective(maxstarr.port.meta, type = "risk",
name = "ES",
arguments = list(p = 0.95))
set.seed(42)
maxstarr.de <- optimize.portfolio(R, maxstarr.port.meta,
optimize_method = "DEoptim",
search_size = 5000, trace = TRUE)
#> Iteration: 1 bestvalit: 0.020139 bestmemit: 0.014000 0.542000 0.160000 0.000000 0.226000 0.052000
#> Iteration: 2 bestvalit: 0.020139 bestmemit: 0.014000 0.542000 0.160000 0.000000 0.226000 0.052000
#> Iteration: 3 bestvalit: 0.019149 bestmemit: 0.070000 0.470000 0.004000 0.004000 0.448000 0.004000
#> Iteration: 4 bestvalit: 0.019149 bestmemit: 0.070000 0.470000 0.004000 0.004000 0.448000 0.004000
#> Iteration: 5 bestvalit: 0.019149 bestmemit: 0.070000 0.470000 0.004000 0.004000 0.448000 0.004000
#> Iteration: 6 bestvalit: 0.018985 bestmemit: 0.036000 0.444000 0.000000 0.000000 0.484000 0.042000
#> Iteration: 7 bestvalit: 0.018985 bestmemit: 0.036000 0.444000 0.000000 0.000000 0.484000 0.042000
#> Iteration: 8 bestvalit: 0.018985 bestmemit: 0.036000 0.444000 0.000000 0.000000 0.484000 0.042000
#> Iteration: 9 bestvalit: 0.018985 bestmemit: 0.036000 0.444000 0.000000 0.000000 0.484000 0.042000
#> Iteration: 10 bestvalit: 0.018985 bestmemit: 0.036000 0.444000 0.000000 0.000000 0.484000 0.042000
#> Iteration: 11 bestvalit: 0.018985 bestmemit: 0.036000 0.444000 0.000000 0.000000 0.484000 0.042000
#> Iteration: 12 bestvalit: 0.018985 bestmemit: 0.036000 0.444000 0.000000 0.000000 0.484000 0.042000
#> Iteration: 13 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 14 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 15 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 16 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 17 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 18 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 19 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 20 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 21 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 22 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> Iteration: 23 bestvalit: 0.018968 bestmemit: 0.044000 0.466000 0.026000 0.000000 0.450000 0.016000
#> [1] 0.044 0.466 0.026 0.000 0.450 0.016
maxstarr.de
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxstarr.port.meta, optimize_method = "DEoptim",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.044 0.466 0.026
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.000 0.450 0.016
#>
#> Objective Measures:
#> mean
#> 0.002187
#>
#>
#> ES
#> 0.02116set.seed(42)
maxstarr.gensa <- optimize.portfolio(R, maxstarr.port.meta,
optimize_method = "GenSA",
search_size = 5000, trace = TRUE)
#> Emini is: 0.01860028079
#> xmini are:
#> 0.2346061771 0.537997332 0.5701743227 0.7071267854 0.4719949641 0.3294747412
#> Totally it used 8.540598 secs
#> No. of function call is: 5929
#> Algorithm reached max number of iterations.
maxstarr.gensa
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxstarr.port.meta, optimize_method = "GenSA",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0831 0.1906 0.2020
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.2505 0.1672 0.1167
#>
#> Objective Measures:
#> mean
#> 0.002611
#>
#>
#> ES
#> 0.04877set.seed(42)
maxstarr.pso <- optimize.portfolio(R, maxstarr.port.meta,
optimize_method = "pso",
search_size = 5000, trace = TRUE)
#> S=14, K=3, p=0.1993, w0=0.7213, w1=0.7213, c.p=1.193, c.g=1.193
#> v.max=NA, d=2.449, vectorize=FALSE, hybrid=off
#> It 10: fitness=0.02144, swarm diam.=1.043
#> It 20: fitness=0.01998, swarm diam.=1.657
#> It 30: fitness=0.01921, swarm diam.=1.267
#> It 40: fitness=0.01921, swarm diam.=1.363
#> It 50: fitness=0.01921, swarm diam.=1.248
#> It 60: fitness=0.01921, swarm diam.=1.332
#> It 70: fitness=0.01908, swarm diam.=1.63
#> It 80: fitness=0.01908, swarm diam.=1.592
#> It 90: fitness=0.01896, swarm diam.=1.43
#> It 100: fitness=0.01896, swarm diam.=1.344
#> It 110: fitness=0.01896, swarm diam.=1.401
#> It 120: fitness=0.01896, swarm diam.=1.162
#> It 130: fitness=0.01896, swarm diam.=1.477
#> It 140: fitness=0.01896, swarm diam.=1.2
#> It 150: fitness=0.01896, swarm diam.=1.163
#> It 160: fitness=0.01896, swarm diam.=1.085
#> It 170: fitness=0.01896, swarm diam.=1.61
#> It 180: fitness=0.01896, swarm diam.=1.302
#> It 190: fitness=0.01896, swarm diam.=1.306
#> It 200: fitness=0.01896, swarm diam.=1.05
#> It 210: fitness=0.01896, swarm diam.=1.229
#> It 220: fitness=0.01896, swarm diam.=1.508
#> It 230: fitness=0.01896, swarm diam.=1.541
#> It 240: fitness=0.01896, swarm diam.=1.251
#> It 250: fitness=0.01896, swarm diam.=1.328
#> It 260: fitness=0.01896, swarm diam.=1.251
#> It 270: fitness=0.01896, swarm diam.=1.405
#> It 280: fitness=0.01896, swarm diam.=1.197
#> It 290: fitness=0.01896, swarm diam.=1.348
#> It 300: fitness=0.01896, swarm diam.=1.186
#> Maximal number of iterations reached
maxstarr.pso
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxstarr.port.meta, optimize_method = "pso",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.3841 0.0978 0.0874
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0032 0.2189 0.2186
#>
#> Objective Measures:
#> mean
#> 0.003554
#>
#>
#> ES
#> 0.05446maxstarr.weights <- round(cbind(
CVXR = maxstarr.cvxr$weights,
ROI = maxstarr.roi$weights,
Rglpk = maxstarr.rglpk$weights,
DEoptim = maxstarr.de$weights,
GenSA = maxstarr.gensa$weights,
pso = maxstarr.pso$weights
), 4)
knitr::kable(maxstarr.weights, caption = "Maximum STARR Weights")| CVXR | ROI | Rglpk | DEoptim | GenSA | pso | |
|---|---|---|---|---|---|---|
| Convertible Arbitrage | 0.0000 | 0.0000 | 0.0000 | 0.044 | 0.0831 | 0.3841 |
| CTA Global | 0.5909 | 0.5909 | 0.5909 | 0.466 | 0.1906 | 0.0978 |
| Distressed Securities | 0.4091 | 0.4091 | 0.4091 | 0.026 | 0.2020 | 0.0874 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 | 0.000 | 0.2505 | 0.0032 |
| Equity Market Neutral | 0.0000 | 0.0000 | 0.0000 | 0.450 | 0.1672 | 0.2189 |
| Event Driven | 0.0000 | 0.0000 | 0.0000 | 0.016 | 0.1167 | 0.2186 |
Maximize mean return minus a risk penalty: $U = \mu^T w - \frac{\lambda}{2} w^T \Sigma w$. The risk_aversion parameter controls the trade-off.
maxqu.port <- add.objective(base.port, type = "return", name = "mean")
maxqu.port <- add.objective(maxqu.port, type = "risk", name = "StdDev",
risk_aversion = 4)When both mean and variance objectives are present and maxSR is not set, CVXR solves the quadratic utility problem.
maxqu.cvxr <- optimize.portfolio(R, maxqu.port, optimize_method = "CVXR")
maxqu.cvxr
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxqu.port, optimize_method = "CVXR")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1436 0.3390 0.5174
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.0000 0.0000
#>
#> Objective Measures:
#> optimal value
#> -0.0007271
#>
#>
#> mean
#> 0.00412
#>
#>
#> StdDev
#> 0.01741ROI also defaults to quadratic utility when maxSR is not set.
maxqu.roi <- optimize.portfolio(R, maxqu.port, optimize_method = "ROI")
maxqu.roi
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxqu.port, optimize_method = "ROI")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1436 0.3390 0.5174
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.0000 0.0000
#>
#> Objective Measure:
#> mean
#> 0.00412
#>
#>
#> StdDev
#> 0.01741maxqu.port.meta <- base.port
maxqu.port.meta$constraints[[1]]$min_sum <- 0.99
maxqu.port.meta$constraints[[1]]$max_sum <- 1.01
maxqu.port.meta <- add.objective(maxqu.port.meta, type = "return",
name = "mean")
maxqu.port.meta <- add.objective(maxqu.port.meta, type = "risk",
name = "StdDev", risk_aversion = 4)
set.seed(42)
maxqu.de <- optimize.portfolio(R, maxqu.port.meta,
optimize_method = "DEoptim",
search_size = 5000, trace = TRUE)
#> Iteration: 1 bestvalit: 0.009817 bestmemit: 0.078000 0.358000 0.098000 0.002000 0.474000 0.000000
#> Iteration: 2 bestvalit: 0.009817 bestmemit: 0.078000 0.358000 0.098000 0.002000 0.474000 0.000000
#> Iteration: 3 bestvalit: 0.009436 bestmemit: 0.126000 0.258000 0.034000 0.000000 0.558000 0.014000
#> Iteration: 4 bestvalit: 0.009436 bestmemit: 0.126000 0.258000 0.034000 0.000000 0.558000 0.014000
#> Iteration: 5 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 6 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 7 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 8 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 9 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 10 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 11 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 12 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 13 bestvalit: 0.009411 bestmemit: 0.000000 0.306000 0.058000 0.002000 0.624000 0.014000
#> Iteration: 14 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 15 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 16 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 17 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 18 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 19 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 20 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 21 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 22 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 23 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> Iteration: 24 bestvalit: 0.009190 bestmemit: 0.056000 0.204000 0.040000 0.000000 0.686000 0.004000
#> [1] 0.056 0.204 0.040 0.000 0.686 0.004
maxqu.de
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxqu.port.meta, optimize_method = "DEoptim",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.056 0.204 0.040
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.000 0.686 0.004
#>
#> Objective Measures:
#> mean
#> 0.001732
#>
#>
#> StdDev
#> 0.01092set.seed(42)
maxqu.gensa <- optimize.portfolio(R, maxqu.port.meta,
optimize_method = "GenSA",
search_size = 5000, trace = TRUE)
#> Emini is: 0.009172386553
#> xmini are:
#> 0.7815859572 0.1979082211 0.8706658439 0.1762869507 0.8778037676 0.7008468873
#> Totally it used 7.210107 secs
#> No. of function call is: 5435
#> Algorithm reached max number of iterations.
maxqu.gensa
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxqu.port.meta, optimize_method = "GenSA",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.2190 0.0554 0.2439
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0494 0.2459 0.1963
#>
#> Objective Measures:
#> mean
#> 0.003317
#>
#>
#> StdDev
#> 0.01952set.seed(42)
maxqu.pso <- optimize.portfolio(R, maxqu.port.meta,
optimize_method = "pso",
search_size = 5000, trace = TRUE)
#> S=14, K=3, p=0.1993, w0=0.7213, w1=0.7213, c.p=1.193, c.g=1.193
#> v.max=NA, d=2.449, vectorize=FALSE, hybrid=off
#> It 10: fitness=0.009941, swarm diam.=1.01
#> It 20: fitness=0.009708, swarm diam.=1.263
#> It 30: fitness=0.009303, swarm diam.=0.8942
#> It 40: fitness=0.009303, swarm diam.=0.6566
#> It 50: fitness=0.009303, swarm diam.=0.867
#> It 60: fitness=0.009303, swarm diam.=0.7307
#> It 70: fitness=0.009303, swarm diam.=0.8989
#> It 80: fitness=0.009303, swarm diam.=0.8204
#> It 90: fitness=0.009303, swarm diam.=0.9491
#> It 100: fitness=0.009303, swarm diam.=1.042
#> It 110: fitness=0.00916, swarm diam.=0.8446
#> It 120: fitness=0.00916, swarm diam.=1.017
#> It 130: fitness=0.00916, swarm diam.=1.037
#> It 140: fitness=0.00916, swarm diam.=0.7328
#> It 150: fitness=0.00916, swarm diam.=0.868
#> It 160: fitness=0.00916, swarm diam.=0.7855
#> It 170: fitness=0.00916, swarm diam.=0.9233
#> It 180: fitness=0.00916, swarm diam.=0.8976
#> It 190: fitness=0.00916, swarm diam.=0.9045
#> It 200: fitness=0.00916, swarm diam.=0.8788
#> It 210: fitness=0.00916, swarm diam.=1.131
#> It 220: fitness=0.00916, swarm diam.=0.8961
#> It 230: fitness=0.00916, swarm diam.=0.7668
#> It 240: fitness=0.00916, swarm diam.=0.8377
#> It 250: fitness=0.00916, swarm diam.=0.8561
#> It 260: fitness=0.00916, swarm diam.=0.8791
#> It 270: fitness=0.00916, swarm diam.=0.729
#> It 280: fitness=0.00916, swarm diam.=0.9386
#> It 290: fitness=0.00916, swarm diam.=0.949
#> It 300: fitness=0.00916, swarm diam.=0.8336
#> Maximal number of iterations reached
maxqu.pso
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = maxqu.port.meta, optimize_method = "pso",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1950 0.2595 0.1114
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0383 0.1893 0.2164
#>
#> Objective Measures:
#> mean
#> 0.003183
#>
#>
#> StdDev
#> 0.01637maxqu.weights <- round(cbind(
CVXR = maxqu.cvxr$weights,
ROI = maxqu.roi$weights,
DEoptim = maxqu.de$weights,
GenSA = maxqu.gensa$weights,
pso = maxqu.pso$weights
), 4)
knitr::kable(maxqu.weights, caption = "Maximum Quadratic Utility Weights")| CVXR | ROI | DEoptim | GenSA | pso | |
|---|---|---|---|---|---|
| Convertible Arbitrage | 0.1436 | 0.1436 | 0.056 | 0.2190 | 0.1950 |
| CTA Global | 0.3390 | 0.3390 | 0.204 | 0.0554 | 0.2595 |
| Distressed Securities | 0.5174 | 0.5174 | 0.040 | 0.2439 | 0.1114 |
| Emerging Markets | 0.0000 | 0.0000 | 0.000 | 0.0494 | 0.0383 |
| Equity Market Neutral | 0.0000 | 0.0000 | 0.686 | 0.2459 | 0.1893 |
| Event Driven | 0.0000 | 0.0000 | 0.004 | 0.1963 | 0.2164 |
Risk budget objectives decompose total portfolio risk into per-asset contributions and constrain or minimize the concentration of those contributions. This requires component risk decomposition, which is only available through metaheuristic solvers.
erc.port <- base.port
erc.port$constraints[[1]]$min_sum <- 0.99
erc.port$constraints[[1]]$max_sum <- 1.01
erc.port <- add.objective(erc.port, type = "risk_budget", name = "ES",
min_concentration = TRUE,
arguments = list(p = 0.95))set.seed(42)
erc.de <- optimize.portfolio(R, erc.port,
optimize_method = "DEoptim",
search_size = 5000, trace = TRUE)
#> Iteration: 1 bestvalit: 7.091496 bestmemit: 0.094000 0.050000 0.092000 0.168000 0.416000 0.188000
#> Iteration: 2 bestvalit: 7.091496 bestmemit: 0.094000 0.050000 0.092000 0.168000 0.416000 0.188000
#> Iteration: 3 bestvalit: 7.091496 bestmemit: 0.094000 0.050000 0.092000 0.168000 0.416000 0.188000
#> Iteration: 4 bestvalit: 7.091496 bestmemit: 0.094000 0.050000 0.092000 0.168000 0.416000 0.188000
#> Iteration: 5 bestvalit: 7.091496 bestmemit: 0.094000 0.050000 0.092000 0.168000 0.416000 0.188000
#> Iteration: 6 bestvalit: 7.091496 bestmemit: 0.094000 0.050000 0.092000 0.168000 0.416000 0.188000
#> Iteration: 7 bestvalit: 7.091496 bestmemit: 0.094000 0.050000 0.092000 0.168000 0.416000 0.188000
#> Iteration: 8 bestvalit: 7.091496 bestmemit: 0.094000 0.050000 0.092000 0.168000 0.416000 0.188000
#> Iteration: 9 bestvalit: 7.091496 bestmemit: 0.094000 0.050000 0.092000 0.168000 0.416000 0.188000
#> Iteration: 10 bestvalit: 6.831183 bestmemit: 0.126000 0.024000 0.038000 0.125782 0.456000 0.226000
#> Iteration: 11 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 12 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 13 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 14 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 15 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 16 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 17 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 18 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 19 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 20 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> Iteration: 21 bestvalit: 3.966845 bestmemit: 0.117581 0.524000 0.148000 0.097948 0.010000 0.098000
#> [1] 0.11758116 0.52400000 0.14800000 0.09794806 0.01000000 0.09800000
erc.de
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = erc.port, optimize_method = "DEoptim",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1176 0.5240 0.1480
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0979 0.0100 0.0980
#>
#> Objective Measures:
#> ES
#> 0.03017
#>
#> contribution :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0066423 0.0070923 0.0055737
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0069117 0.0001378 0.0038119
#>
#> pct_contrib_MES :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.220163 0.235080 0.184746
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.229095 0.004566 0.126349set.seed(42)
erc.gensa <- optimize.portfolio(R, erc.port,
optimize_method = "GenSA",
search_size = 5000, trace = TRUE)
#> Emini is: 2.442988362
#> xmini are:
#> 0.2298262036 0.7557807141 0.4189108486 0.04193776442 0.4525699682 0.9832920137
#> Totally it used 7.150976 secs
#> No. of function call is: 4746
#> Algorithm reached max number of iterations.
erc.gensa
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = erc.port, optimize_method = "GenSA",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0805 0.2648 0.1468
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0147 0.1586 0.3446
#>
#> Objective Measures:
#> ES
#> 0.03384
#>
#> contribution :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.006291 -0.001272 0.007396
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.001306 0.002950 0.017169
#>
#> pct_contrib_MES :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.18590 -0.03758 0.21855
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.03859 0.08718 0.50736set.seed(42)
erc.pso <- optimize.portfolio(R, erc.port,
optimize_method = "pso",
search_size = 5000, trace = TRUE)
#> S=14, K=3, p=0.1993, w0=0.7213, w1=0.7213, c.p=1.193, c.g=1.193
#> v.max=NA, d=2.449, vectorize=FALSE, hybrid=off
#> It 10: fitness=8.161, swarm diam.=1.072
#> It 20: fitness=6.283, swarm diam.=1.477
#> It 30: fitness=6.283, swarm diam.=1.146
#> It 40: fitness=6.153, swarm diam.=1.511
#> It 50: fitness=6.153, swarm diam.=1.53
#> It 60: fitness=4.812, swarm diam.=1.314
#> It 70: fitness=4.812, swarm diam.=1.399
#> It 80: fitness=4.812, swarm diam.=1.601
#> It 90: fitness=4.812, swarm diam.=1.605
#> It 100: fitness=4.812, swarm diam.=1.38
#> It 110: fitness=4.812, swarm diam.=1.369
#> It 120: fitness=4.812, swarm diam.=1.51
#> It 130: fitness=4.812, swarm diam.=1.471
#> It 140: fitness=2.062, swarm diam.=1.099
#> It 150: fitness=2.062, swarm diam.=1.129
#> It 160: fitness=2.062, swarm diam.=1.176
#> It 170: fitness=2.062, swarm diam.=1.132
#> It 180: fitness=2.062, swarm diam.=1.083
#> It 190: fitness=2.062, swarm diam.=0.9367
#> It 200: fitness=2.062, swarm diam.=1.235
#> It 210: fitness=2.062, swarm diam.=1.192
#> It 220: fitness=2.062, swarm diam.=1.039
#> It 230: fitness=2.062, swarm diam.=1.071
#> It 240: fitness=2.062, swarm diam.=1.325
#> It 250: fitness=2.062, swarm diam.=1.04
#> It 260: fitness=2.062, swarm diam.=1.016
#> It 270: fitness=2.062, swarm diam.=1.096
#> It 280: fitness=2.062, swarm diam.=1.254
#> It 290: fitness=2.062, swarm diam.=0.995
#> It 300: fitness=2.062, swarm diam.=1.033
#> Maximal number of iterations reached
erc.pso
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = erc.port, optimize_method = "pso",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0813 0.2895 0.2621
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.1118 0.1283 0.1371
#>
#> Objective Measures:
#> ES
#> 0.03763
#>
#> contribution :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.006752 -0.002592 0.013906
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.010509 0.002108 0.006946
#>
#> pct_contrib_MES :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.17945 -0.06888 0.36956
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.27929 0.05601 0.18458erc.weights <- round(cbind(
DEoptim = erc.de$weights,
GenSA = erc.gensa$weights,
pso = erc.pso$weights
), 4)
knitr::kable(erc.weights, caption = "Equal Risk Contribution Weights")| DEoptim | GenSA | pso | |
|---|---|---|---|
| Convertible Arbitrage | 0.1176 | 0.0805 | 0.0813 |
| CTA Global | 0.5240 | 0.2648 | 0.2895 |
| Distressed Securities | 0.1480 | 0.1468 | 0.2621 |
| Emerging Markets | 0.0979 | 0.0147 | 0.1118 |
| Equity Market Neutral | 0.0100 | 0.1586 | 0.1283 |
| Event Driven | 0.0980 | 0.3446 | 0.1371 |
# Percentage ES contribution for each solver
erc.pct <- round(cbind(
DEoptim = erc.de$objective_measures$ES$pct_contrib_MES,
GenSA = erc.gensa$objective_measures$ES$pct_contrib_MES,
pso = erc.pso$objective_measures$ES$pct_contrib_MES
), 4)
knitr::kable(erc.pct, caption = "ES Percentage Risk Contributions")| DEoptim | GenSA | pso | |
|---|---|---|---|
| Convertible Arbitrage | 0.2202 | 0.1859 | 0.1794 |
| CTA Global | 0.2351 | -0.0376 | -0.0689 |
| Distressed Securities | 0.1847 | 0.2185 | 0.3696 |
| Emerging Markets | 0.2291 | 0.0386 | 0.2793 |
| Equity Market Neutral | 0.0046 | 0.0872 | 0.0560 |
| Event Driven | 0.1263 | 0.5074 | 0.1846 |
Constrain each asset’s percentage risk contribution to be at most 40%:
rb.port <- base.port
rb.port$constraints[[1]]$min_sum <- 0.99
rb.port$constraints[[1]]$max_sum <- 1.01
rb.port <- add.objective(rb.port, type = "return", name = "mean")
rb.port <- add.objective(rb.port, type = "risk_budget", name = "ES",
max_prisk = 0.4,
arguments = list(p = 0.95))set.seed(42)
rb.de <- optimize.portfolio(R, rb.port,
optimize_method = "DEoptim",
search_size = 5000, trace = TRUE)
#> Iteration: 1 bestvalit: -0.003444 bestmemit: 0.120000 0.122000 0.286000 0.034000 0.142000 0.296000
#> Iteration: 2 bestvalit: -0.003444 bestmemit: 0.120000 0.122000 0.286000 0.034000 0.142000 0.296000
#> Iteration: 3 bestvalit: -0.003444 bestmemit: 0.120000 0.122000 0.286000 0.034000 0.142000 0.296000
#> Iteration: 4 bestvalit: -0.003444 bestmemit: 0.120000 0.122000 0.286000 0.034000 0.142000 0.296000
#> Iteration: 5 bestvalit: -0.003812 bestmemit: 0.200000 0.000000 0.278000 0.040000 0.046000 0.430000
#> Iteration: 6 bestvalit: -0.003812 bestmemit: 0.200000 0.000000 0.278000 0.040000 0.046000 0.430000
#> Iteration: 7 bestvalit: -0.003812 bestmemit: 0.200000 0.000000 0.278000 0.040000 0.046000 0.430000
#> Iteration: 8 bestvalit: -0.004018 bestmemit: 0.158000 0.092594 0.376000 0.002000 0.010000 0.354000
#> Iteration: 9 bestvalit: -0.004099 bestmemit: 0.240148 0.020000 0.406000 0.030000 0.036000 0.266000
#> Iteration: 10 bestvalit: -0.004099 bestmemit: 0.240148 0.020000 0.406000 0.030000 0.036000 0.266000
#> Iteration: 11 bestvalit: -0.004099 bestmemit: 0.240148 0.020000 0.406000 0.030000 0.036000 0.266000
#> Iteration: 12 bestvalit: -0.004099 bestmemit: 0.240148 0.020000 0.406000 0.030000 0.036000 0.266000
#> Iteration: 13 bestvalit: -0.004099 bestmemit: 0.240148 0.020000 0.406000 0.030000 0.036000 0.266000
#> Iteration: 14 bestvalit: -0.004099 bestmemit: 0.240148 0.020000 0.406000 0.030000 0.036000 0.266000
#> Iteration: 15 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 16 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 17 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 18 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 19 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 20 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 21 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 22 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 23 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 24 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> Iteration: 25 bestvalit: -0.004146 bestmemit: 0.247646 0.000000 0.412000 0.028000 0.030000 0.280000
#> [1] 0.2476464 0.0000000 0.4120000 0.0280000 0.0300000 0.2800000
rb.de
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = rb.port, optimize_method = "DEoptim",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.2476 0.0000 0.4120
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0280 0.0300 0.2800
#>
#> Objective Measures:
#> mean
#> 0.004146
#>
#>
#> ES
#> 0.06423
#>
#> contribution :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0251905 0.0000000 0.0224463
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0029075 0.0001465 0.0135397
#>
#> pct_contrib_MES :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.392189 0.000000 0.349465
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.045267 0.002281 0.210798set.seed(42)
rb.gensa <- optimize.portfolio(R, rb.port,
optimize_method = "GenSA",
search_size = 5000, trace = TRUE)
#> It: 1, obj value (lsEnd): -0.002950877885 indTrace: 1
#> Emini is: -0.004199396667
#> xmini are:
#> 0.8135796739 0.04670930232 0.2166766241 0.2973080734 0.8581236364 0.5630752591
#> Totally it used 9.630088 secs
#> No. of function call is: 6605
#> Algorithm reached max number of iterations.
rb.gensa
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = rb.port, optimize_method = "GenSA",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.2939 0.0169 0.0783
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.1074 0.3100 0.2034
#>
#> Objective Measures:
#> mean
#> 0.002923
#>
#>
#> ES
#> 0.05684
#>
#> contribution :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0291999 -0.0002882 0.0041707
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0107913 0.0028514 0.0101184
#>
#> pct_contrib_MES :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.51369 -0.00507 0.07337
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.18984 0.05016 0.17800set.seed(42)
rb.pso <- optimize.portfolio(R, rb.port,
optimize_method = "pso",
search_size = 5000, trace = TRUE)
#> S=14, K=3, p=0.1993, w0=0.7213, w1=0.7213, c.p=1.193, c.g=1.193
#> v.max=NA, d=2.449, vectorize=FALSE, hybrid=off
#> It 10: fitness=-0.003547, swarm diam.=0.8817
#> It 20: fitness=-0.00378, swarm diam.=1.091
#> It 30: fitness=-0.004137, swarm diam.=1.126
#> It 40: fitness=-0.004137, swarm diam.=0.9721
#> It 50: fitness=-0.004137, swarm diam.=0.8708
#> It 60: fitness=-0.004137, swarm diam.=0.9325
#> It 70: fitness=-0.004137, swarm diam.=1.019
#> It 80: fitness=-0.004138, swarm diam.=0.7233
#> It 90: fitness=-0.004138, swarm diam.=0.7485
#> It 100: fitness=-0.004138, swarm diam.=0.7245
#> It 110: fitness=-0.004138, swarm diam.=0.8421
#> It 120: fitness=-0.004138, swarm diam.=0.8642
#> It 130: fitness=-0.004138, swarm diam.=0.7953
#> It 140: fitness=-0.004138, swarm diam.=0.6846
#> It 150: fitness=-0.004138, swarm diam.=0.6506
#> It 160: fitness=-0.004138, swarm diam.=0.9272
#> It 170: fitness=-0.004138, swarm diam.=1.037
#> It 180: fitness=-0.004138, swarm diam.=0.6598
#> It 190: fitness=-0.004138, swarm diam.=0.8344
#> It 200: fitness=-0.004138, swarm diam.=0.7623
#> It 210: fitness=-0.004138, swarm diam.=0.8373
#> It 220: fitness=-0.004163, swarm diam.=0.7662
#> It 230: fitness=-0.004163, swarm diam.=0.6454
#> It 240: fitness=-0.004163, swarm diam.=0.6585
#> It 250: fitness=-0.004163, swarm diam.=0.6258
#> It 260: fitness=-0.004186, swarm diam.=0.9085
#> It 270: fitness=-0.004186, swarm diam.=0.951
#> It 280: fitness=-0.004186, swarm diam.=0.7059
#> It 290: fitness=-0.004186, swarm diam.=0.965
#> It 300: fitness=-0.004186, swarm diam.=0.9533
#> Maximal number of iterations reached
rb.pso
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = rb.port, optimize_method = "pso",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1968 0.3137 0.1687
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.1448 0.1238 0.0622
#>
#> Objective Measures:
#> mean
#> 0.003095
#>
#>
#> ES
#> 0.04172
#>
#> contribution :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.017966 -0.004500 0.009113
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.014192 0.001737 0.003212
#>
#> pct_contrib_MES :
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.43063 -0.10787 0.21845
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.34017 0.04162 0.07700rb.weights <- round(cbind(
DEoptim = rb.de$weights,
GenSA = rb.gensa$weights,
pso = rb.pso$weights
), 4)
knitr::kable(rb.weights, caption = "Bounded Risk Budget Weights")| DEoptim | GenSA | pso | |
|---|---|---|---|
| Convertible Arbitrage | 0.2476 | 0.2939 | 0.1968 |
| CTA Global | 0.0000 | 0.0169 | 0.3137 |
| Distressed Securities | 0.4120 | 0.0783 | 0.1687 |
| Emerging Markets | 0.0280 | 0.1074 | 0.1448 |
| Equity Market Neutral | 0.0300 | 0.3100 | 0.1238 |
| Event Driven | 0.2800 | 0.2034 | 0.0622 |
rb.pct <- round(cbind(
DEoptim = rb.de$objective_measures$ES$pct_contrib_MES,
GenSA = rb.gensa$objective_measures$ES$pct_contrib_MES,
pso = rb.pso$objective_measures$ES$pct_contrib_MES
), 4)
knitr::kable(rb.pct, caption = "ES Percentage Risk Contributions (max 40%)")| DEoptim | GenSA | pso | |
|---|---|---|---|
| Convertible Arbitrage | 0.3922 | 0.5137 | 0.4306 |
| CTA Global | 0.0000 | -0.0051 | -0.1079 |
| Distressed Securities | 0.3495 | 0.0734 | 0.2184 |
| Emerging Markets | 0.0453 | 0.1898 | 0.3402 |
| Equity Market Neutral | 0.0023 | 0.0502 | 0.0416 |
| Event Driven | 0.2108 | 0.1780 | 0.0770 |
The Herfindahl-Hirschman Index (HHI) penalty discourages weight concentration by adding λHHI∑wi2 to the objective.
hhi.port <- add.objective(base.port, type = "risk", name = "StdDev")
hhi.port <- add.objective(hhi.port, type = "weight_concentration",
name = "HHI", conc_aversion = 0.1)hhi.cvxr <- optimize.portfolio(R, hhi.port, optimize_method = "CVXR")
hhi.cvxr
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = hhi.port, optimize_method = "CVXR")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1655 0.1696 0.1661
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.1635 0.1689 0.1664
#>
#> Objective Measures:
#> StdDev
#> 0.01943
#>
#>
#> HHI
#> 0.1667hhi.roi <- optimize.portfolio(R, hhi.port, optimize_method = "ROI")
hhi.roi
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = hhi.port, optimize_method = "ROI")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1655 0.1696 0.1661
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.1635 0.1689 0.1664
#>
#> Objective Measure:
#> StdDev
#> 0.01943hhi.weights <- round(cbind(
CVXR = hhi.cvxr$weights,
ROI = hhi.roi$weights
), 4)
knitr::kable(hhi.weights, caption = "Variance + HHI Weights")| CVXR | ROI | |
|---|---|---|
| Convertible Arbitrage | 0.1655 | 0.1655 |
| CTA Global | 0.1696 | 0.1696 |
| Distressed Securities | 0.1661 | 0.1661 |
| Emerging Markets | 0.1635 | 0.1635 |
| Equity Market Neutral | 0.1689 | 0.1689 |
| Event Driven | 0.1664 | 0.1664 |
Factor exposure constraints bound the portfolio’s exposure to specified risk factors. Given a factor loading matrix B (N assets × K factors), the constraint enforces:
lowerk ≤ BkTw ≤ upperk ∀ k = 1, …, K
This is useful for controlling sector tilts, style exposures (value, momentum, size), or any linear factor model. When B is a binary group membership matrix, factor exposure constraints are equivalent to group constraints.
Factor exposure constraints are supported as hard linear constraints by CVXR, ROI (including all ROI sub-problems: QP, LP, MILP, turnover, transaction cost, and leverage variants), osqp (both single-objective and Charnes-Cooper maxSR paths), and Rglpk (all six sub-cases: maxReturn, minES, and maxSTARR, each with and without position limits). Metaheuristic solvers enforce them via fn_map(), which uses a QP projection step (quadprog::solve.QP) to find the nearest weight vector satisfying box, leverage, and factor exposure constraints simultaneously. This provides hard constraint enforcement rather than relying solely on penalty terms.
We construct a simple style-factor loading matrix for our 6-asset universe. Assets are assigned loadings to two factors (e.g., “Equity Beta” and “Credit Spread Sensitivity”), and we constrain portfolio exposure to each.
# Factor loading matrix: 6 assets x 2 factors
B <- matrix(c(
1.2, 0.8, 0.3, 0.5, 0.1, 0.9, # Factor 1: Equity Beta
0.2, 0.5, 0.9, 0.7, 0.3, 0.4 # Factor 2: Credit Spread
), ncol = 2, byrow = FALSE)
rownames(B) <- funds
colnames(B) <- c("Equity.Beta", "Credit.Spread")
B
#> Equity.Beta Credit.Spread
#> Convertible Arbitrage 1.2 0.2
#> CTA Global 0.8 0.5
#> Distressed Securities 0.3 0.9
#> Emerging Markets 0.5 0.7
#> Equity Market Neutral 0.1 0.3
#> Event Driven 0.9 0.4
# Exposure bounds
fe.lower <- c(0.4, 0.3)
fe.upper <- c(0.8, 0.6)
# Portfolio with factor exposure constraint
fe.port <- add.constraint(base.port, type = "factor_exposure",
B = B, lower = fe.lower, upper = fe.upper)
fe.port <- add.objective(fe.port, type = "risk", name = "StdDev")fe.cvxr <- optimize.portfolio(R, fe.port, optimize_method = "CVXR")
fe.cvxr
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = fe.port, optimize_method = "CVXR")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0908 0.2858 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.6233 0.0000
#>
#> Objective Measures:
#> StdDev
#> 0.01114Verify the exposure constraints are satisfied:
exposures.cvxr <- as.numeric(t(B) %*% fe.cvxr$weights)
names(exposures.cvxr) <- colnames(B)
data.frame(Lower = fe.lower, Exposure = round(exposures.cvxr, 4),
Upper = fe.upper)| Lower | Exposure | Upper | |
|---|---|---|---|
| Equity.Beta | 0.4 | 0.4000 | 0.8 |
| Credit.Spread | 0.3 | 0.3481 | 0.6 |
fe.roi <- optimize.portfolio(R, fe.port, optimize_method = "ROI")
fe.roi
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = fe.port, optimize_method = "ROI")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0908 0.2858 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.6233 0.0000
#>
#> Objective Measure:
#> StdDev
#> 0.01114exposures.roi <- as.numeric(t(B) %*% fe.roi$weights)
names(exposures.roi) <- colnames(B)
data.frame(Lower = fe.lower, Exposure = round(exposures.roi, 4),
Upper = fe.upper)| Lower | Exposure | Upper | |
|---|---|---|---|
| Equity.Beta | 0.4 | 0.4000 | 0.8 |
| Credit.Spread | 0.3 | 0.3481 | 0.6 |
fe.osqp <- optimize.portfolio(R, fe.port, optimize_method = "osqp")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
fe.osqp
#> $weights
#> Convertible Arbitrage CTA Global Distressed Securities
#> 9.082433e-02 2.858475e-01 -1.439642e-20
#> Emerging Markets Equity Market Neutral Event Driven
#> -1.887314e-20 6.233282e-01 -8.589298e-21
#>
#> $objective_measures
#> $objective_measures$StdDev
#> [,1]
#> [1,] 0.01113586
#> attr(,"names")
#> [1] "StdDev"
#>
#>
#> $opt_values
#> $opt_values$StdDev
#> [,1]
#> [1,] 0.01113586
#> attr(,"names")
#> [1] "StdDev"
#>
#>
#> $out
#> [1] 6.200367e-05
#>
#> $call
#> optimize.portfolio(R = R, portfolio = fe.port, optimize_method = "osqp")
#>
#> $portfolio
#> **************************************************
#> PortfolioAnalytics Portfolio Specification
#> **************************************************
#>
#> Call:
#> portfolio.spec(assets = funds)
#>
#> Number of assets: 6
#> Asset Names
#> [1] "Convertible Arbitrage" "CTA Global" "Distressed Securities"
#> [4] "Emerging Markets" "Equity Market Neutral" "Event Driven"
#>
#> Constraints
#> Enabled constraint types
#> - full_investment
#> - long_only
#> - factor_exposure
#>
#> Objectives:
#> Enabled objective names
#> - StdDev
#>
#>
#> $data_summary
#> $data_summary$first
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2008-01-31 -9e-04 0.0255 -0.0233
#> Emerging Markets Equity Market Neutral Event Driven
#> 2008-01-31 -0.0503 -0.0112 -0.0271
#>
#> $data_summary$last
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2012-12-31 0.0098 0.0057 0.0259
#> Emerging Markets Equity Market Neutral Event Driven
#> 2012-12-31 0.033 0.0037 0.0193
#>
#>
#> $elapsed_time
#> Time difference of 0.002856731 secs
#>
#> $end_t
#> [1] "2026-05-29 09:13:23.482329"
#>
#> attr(,"class")
#> [1] "optimize.portfolio.osqp" "optimize.portfolio"exposures.osqp <- as.numeric(t(B) %*% fe.osqp$weights)
names(exposures.osqp) <- colnames(B)
data.frame(Lower = fe.lower, Exposure = round(exposures.osqp, 4),
Upper = fe.upper)| Lower | Exposure | Upper | |
|---|---|---|---|
| Equity.Beta | 0.4 | 0.4000 | 0.8 |
| Credit.Spread | 0.3 | 0.3481 | 0.6 |
Rglpk is a linear solver, so we demonstrate it with a return-maximization objective subject to factor exposure bounds:
fe.rglpk.port <- add.constraint(base.port, type = "factor_exposure",
B = B, lower = fe.lower, upper = fe.upper)
fe.rglpk.port <- add.objective(fe.rglpk.port, type = "return", name = "mean")
fe.rglpk <- optimize.portfolio(R, fe.rglpk.port, optimize_method = "Rglpk")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
fe.rglpk
#> $weights
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.5555556 0.0000000 0.4444444
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000000 0.0000000 0.0000000
#>
#> $objective_measures
#> $objective_measures$mean
#> mean
#> 0.004837778
#>
#>
#> $opt_values
#> $opt_values$mean
#> mean
#> 0.004837778
#>
#>
#> $out
#> [1] 0.004837778
#>
#> $call
#> optimize.portfolio(R = R, portfolio = fe.rglpk.port, optimize_method = "Rglpk")
#>
#> $portfolio
#> **************************************************
#> PortfolioAnalytics Portfolio Specification
#> **************************************************
#>
#> Call:
#> portfolio.spec(assets = funds)
#>
#> Number of assets: 6
#> Asset Names
#> [1] "Convertible Arbitrage" "CTA Global" "Distressed Securities"
#> [4] "Emerging Markets" "Equity Market Neutral" "Event Driven"
#>
#> Constraints
#> Enabled constraint types
#> - full_investment
#> - long_only
#> - factor_exposure
#>
#> Objectives:
#> Enabled objective names
#> - mean
#>
#>
#> $data_summary
#> $data_summary$first
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2008-01-31 -9e-04 0.0255 -0.0233
#> Emerging Markets Equity Market Neutral Event Driven
#> 2008-01-31 -0.0503 -0.0112 -0.0271
#>
#> $data_summary$last
#> Convertible Arbitrage CTA Global Distressed Securities
#> 2012-12-31 0.0098 0.0057 0.0259
#> Emerging Markets Equity Market Neutral Event Driven
#> 2012-12-31 0.033 0.0037 0.0193
#>
#>
#> $elapsed_time
#> Time difference of 0.002521515 secs
#>
#> $end_t
#> [1] "2026-05-29 09:13:23.507349"
#>
#> attr(,"class")
#> [1] "optimize.portfolio.Rglpk" "optimize.portfolio"exposures.rglpk <- as.numeric(t(B) %*% fe.rglpk$weights)
names(exposures.rglpk) <- colnames(B)
data.frame(Lower = fe.lower, Exposure = round(exposures.rglpk, 4),
Upper = fe.upper)| Lower | Exposure | Upper | |
|---|---|---|---|
| Equity.Beta | 0.4 | 0.8000 | 0.8 |
| Credit.Spread | 0.3 | 0.5111 | 0.6 |
DEoptim and other metaheuristic solvers enforce factor exposure constraints via fn_map(), which applies a QP projection step (using quadprog::solve.QP) after its standard perturbation loop. This finds the nearest weight vector (in L2 norm) satisfying box, leverage, and factor exposure constraints simultaneously.
fe.port.meta <- base.port
fe.port.meta$constraints[[1]]$min_sum <- 0.99
fe.port.meta$constraints[[1]]$max_sum <- 1.01
fe.port.meta <- add.constraint(fe.port.meta, type = "factor_exposure",
B = B, lower = fe.lower, upper = fe.upper)
fe.port.meta <- add.objective(fe.port.meta, type = "risk", name = "StdDev")
set.seed(42)
fe.de <- optimize.portfolio(R, fe.port.meta,
optimize_method = "DEoptim",
search_size = 5000, trace = TRUE)
#> Iteration: 1 bestvalit: 0.011875 bestmemit: 0.156000 0.266000 0.046000 0.000000 0.522000 0.000000
#> Iteration: 2 bestvalit: 0.011875 bestmemit: 0.156000 0.266000 0.046000 0.000000 0.522000 0.000000
#> Iteration: 3 bestvalit: 0.011751 bestmemit: 0.020980 0.371069 0.066680 0.000000 0.547724 0.003547
#> Iteration: 4 bestvalit: 0.011324 bestmemit: 0.044202 0.304793 0.010532 0.000000 0.606828 0.043645
#> Iteration: 5 bestvalit: 0.011324 bestmemit: 0.044202 0.304793 0.010532 0.000000 0.606828 0.043645
#> Iteration: 6 bestvalit: 0.011324 bestmemit: 0.044202 0.304793 0.010532 0.000000 0.606828 0.043645
#> Iteration: 7 bestvalit: 0.011324 bestmemit: 0.044202 0.304793 0.010532 0.000000 0.606828 0.043645
#> Iteration: 8 bestvalit: 0.011324 bestmemit: 0.044202 0.304793 0.010532 0.000000 0.606828 0.043645
#> Iteration: 9 bestvalit: 0.011324 bestmemit: 0.044202 0.304793 0.010532 0.000000 0.606828 0.043645
#> Iteration: 10 bestvalit: 0.011324 bestmemit: 0.044202 0.304793 0.010532 0.000000 0.606828 0.043645
#> Iteration: 11 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 12 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 13 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 14 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 15 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 16 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 17 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 18 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 19 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 20 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> Iteration: 21 bestvalit: 0.011279 bestmemit: 0.042523 0.291169 0.000000 0.000000 0.615800 0.060508
#> [1] 4.252308e-02 2.911692e-01 1.033989e-19 0.000000e+00 6.158000e-01
#> [6] 6.050769e-02
fe.de
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = fe.port.meta, optimize_method = "DEoptim",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0425 0.2912 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.6158 0.0605
#>
#> Objective Measures:
#> StdDev
#> 0.01128exposures.de <- as.numeric(t(B) %*% fe.de$weights)
names(exposures.de) <- colnames(B)
data.frame(Lower = fe.lower, Exposure = round(exposures.de, 4),
Upper = fe.upper)| Lower | Exposure | Upper | |
|---|---|---|---|
| Equity.Beta | 0.4 | 0.400 | 0.8 |
| Credit.Spread | 0.3 | 0.363 | 0.6 |
set.seed(42)
fe.gensa <- optimize.portfolio(R, fe.port.meta,
optimize_method = "GenSA",
search_size = 5000, trace = TRUE)
#> Emini is: 0.01124541629
#> xmini are:
#> 0.7718594936 0.3857476548 0.8832790596 0.6192575092 0.6062140233 0.9545417546
#> Totally it used 6.334694 secs
#> No. of function call is: 4733
#> Algorithm reached max number of iterations.
fe.gensa
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = fe.port.meta, optimize_method = "GenSA",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.1847 0.0923 0.2114
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.1482 0.1451 0.2284
#>
#> Objective Measures:
#> StdDev
#> 0.02128exposures.gensa <- as.numeric(t(B) %*% fe.gensa$weights)
names(exposures.gensa) <- colnames(B)
data.frame(Lower = fe.lower, Exposure = round(exposures.gensa, 4),
Upper = fe.upper)| Lower | Exposure | Upper | |
|---|---|---|---|
| Equity.Beta | 0.4 | 0.6530 | 0.8 |
| Credit.Spread | 0.3 | 0.5119 | 0.6 |
set.seed(42)
fe.pso <- optimize.portfolio(R, fe.port.meta,
optimize_method = "pso",
search_size = 5000, trace = TRUE)
#> S=14, K=3, p=0.1993, w0=0.7213, w1=0.7213, c.p=1.193, c.g=1.193
#> v.max=NA, d=2.449, vectorize=FALSE, hybrid=off
#> It 10: fitness=0.01187, swarm diam.=1.2
#> It 20: fitness=0.01187, swarm diam.=1.309
#> It 30: fitness=0.01187, swarm diam.=0.9344
#> It 40: fitness=0.01187, swarm diam.=1.143
#> It 50: fitness=0.01136, swarm diam.=1.245
#> It 60: fitness=0.01125, swarm diam.=1.027
#> It 70: fitness=0.01125, swarm diam.=1.093
#> It 80: fitness=0.01125, swarm diam.=1.021
#> It 90: fitness=0.01125, swarm diam.=1.445
#> It 100: fitness=0.01125, swarm diam.=1.116
#> It 110: fitness=0.01125, swarm diam.=1.179
#> It 120: fitness=0.01125, swarm diam.=1.428
#> It 130: fitness=0.01125, swarm diam.=1.275
#> It 140: fitness=0.01125, swarm diam.=1.336
#> It 150: fitness=0.01125, swarm diam.=1.451
#> It 160: fitness=0.01125, swarm diam.=1.466
#> It 170: fitness=0.01125, swarm diam.=1.477
#> It 180: fitness=0.01125, swarm diam.=1.462
#> It 190: fitness=0.01125, swarm diam.=1.555
#> It 200: fitness=0.01125, swarm diam.=1.368
#> It 210: fitness=0.01124, swarm diam.=1.219
#> It 220: fitness=0.01124, swarm diam.=1.333
#> It 230: fitness=0.01124, swarm diam.=1.676
#> It 240: fitness=0.01124, swarm diam.=1.57
#> It 250: fitness=0.01124, swarm diam.=1.685
#> It 260: fitness=0.01124, swarm diam.=1.555
#> It 270: fitness=0.01124, swarm diam.=1.407
#> It 280: fitness=0.01124, swarm diam.=1.7
#> It 290: fitness=0.01124, swarm diam.=1.328
#> It 300: fitness=0.01124, swarm diam.=1.185
#> Maximal number of iterations reached
fe.pso
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = fe.port.meta, optimize_method = "pso",
#> search_size = 5000, trace = TRUE)
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0326 0.5837 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.3737 0.0000
#>
#> Objective Measures:
#> StdDev
#> 0.01407exposures.pso <- as.numeric(t(B) %*% fe.pso$weights)
names(exposures.pso) <- colnames(B)
data.frame(Lower = fe.lower, Exposure = round(exposures.pso, 4),
Upper = fe.upper)| Lower | Exposure | Upper | |
|---|---|---|---|
| Equity.Beta | 0.4 | 0.5435 | 0.8 |
| Credit.Spread | 0.3 | 0.4105 | 0.6 |
fe.weights <- round(cbind(
CVXR = fe.cvxr$weights,
ROI = fe.roi$weights,
osqp = fe.osqp$weights,
DEoptim = fe.de$weights,
GenSA = fe.gensa$weights,
pso = fe.pso$weights
), 4)
knitr::kable(fe.weights, caption = "Factor Exposure Constrained Weights")| CVXR | ROI | osqp | DEoptim | GenSA | pso | |
|---|---|---|---|---|---|---|
| Convertible Arbitrage | 0.0908 | 0.0908 | 0.0908 | 0.0425 | 0.1847 | 0.0326 |
| CTA Global | 0.2858 | 0.2858 | 0.2858 | 0.2912 | 0.0923 | 0.5837 |
| Distressed Securities | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.2114 | 0.0000 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 | 0.0000 | 0.1482 | 0.0000 |
| Equity Market Neutral | 0.6233 | 0.6233 | 0.6233 | 0.6158 | 0.1451 | 0.3737 |
| Event Driven | 0.0000 | 0.0000 | 0.0000 | 0.0605 | 0.2284 | 0.0000 |
Factor exposure constraints compose with any objective supported by the solver. For example, maximum return subject to factor exposure bounds:
fe.maxret.port <- add.constraint(base.port, type = "factor_exposure",
B = B, lower = fe.lower, upper = fe.upper)
fe.maxret.port <- add.objective(fe.maxret.port, type = "return", name = "mean")
fe.maxret.cvxr <- optimize.portfolio(R, fe.maxret.port,
optimize_method = "CVXR")
fe.maxret.roi <- optimize.portfolio(R, fe.maxret.port,
optimize_method = "ROI")
fe.maxret.rglpk <- optimize.portfolio(R, fe.maxret.port,
optimize_method = "Rglpk")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
knitr::kable(round(cbind(CVXR = fe.maxret.cvxr$weights,
ROI = fe.maxret.roi$weights,
Rglpk = fe.maxret.rglpk$weights), 4),
caption = "Max Return with Factor Exposure Constraints")| CVXR | ROI | Rglpk | |
|---|---|---|---|
| Convertible Arbitrage | 0.5556 | 0.5556 | 0.5556 |
| CTA Global | 0.0000 | 0.0000 | 0.0000 |
| Distressed Securities | 0.4444 | 0.4444 | 0.4444 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 |
| Equity Market Neutral | 0.0000 | 0.0000 | 0.0000 |
| Event Driven | 0.0000 | 0.0000 | 0.0000 |
And minimum ES subject to factor exposure bounds:
fe.mines.port <- add.constraint(base.port, type = "factor_exposure",
B = B, lower = fe.lower, upper = fe.upper)
fe.mines.port <- add.objective(fe.mines.port, type = "risk", name = "ES",
arguments = list(p = 0.95))
fe.mines.cvxr <- optimize.portfolio(R, fe.mines.port,
optimize_method = "CVXR")
fe.mines.roi <- optimize.portfolio(R, fe.mines.port,
optimize_method = "ROI")
fe.mines.rglpk <- optimize.portfolio(R, fe.mines.port,
optimize_method = "Rglpk")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
knitr::kable(round(cbind(CVXR = fe.mines.cvxr$weights,
ROI = fe.mines.roi$weights,
Rglpk = fe.mines.rglpk$weights), 4),
caption = "Min ES with Factor Exposure Constraints")| CVXR | ROI | Rglpk | |
|---|---|---|---|
| Convertible Arbitrage | 0.0000 | 0.0000 | 0.0000 |
| CTA Global | 0.4984 | 0.4984 | 0.4984 |
| Distressed Securities | 0.0000 | 0.0000 | 0.0000 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 |
| Equity Market Neutral | 0.4223 | 0.4223 | 0.4223 |
| Event Driven | 0.0793 | 0.0793 | 0.0793 |
And minimum variance subject to factor exposure bounds (osqp vs CVXR):
fe.minvar.port <- add.constraint(base.port, type = "factor_exposure",
B = B, lower = fe.lower, upper = fe.upper)
fe.minvar.port <- add.objective(fe.minvar.port, type = "risk", name = "StdDev")
fe.minvar.osqp <- optimize.portfolio(R, fe.minvar.port,
optimize_method = "osqp")
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
fe.minvar.cvxr <- optimize.portfolio(R, fe.minvar.port,
optimize_method = "CVXR")
knitr::kable(round(cbind(osqp = fe.minvar.osqp$weights,
CVXR = fe.minvar.cvxr$weights), 4),
caption = "Min Variance with Factor Exposure Constraints")| osqp | CVXR | |
|---|---|---|
| Convertible Arbitrage | 0.0908 | 0.0908 |
| CTA Global | 0.2858 | 0.2858 |
| Distressed Securities | 0.0000 | 0.0000 |
| Emerging Markets | 0.0000 | 0.0000 |
| Equity Market Neutral | 0.6233 | 0.6233 |
| Event Driven | 0.0000 | 0.0000 |
PortfolioAnalytics also supports statistical factor models for moment estimation via statistical.factor.model(). This is conceptually distinct from factor exposure constraints: factor models estimate the covariance (and higher co-moment) matrices from a small number of principal components, which can improve estimation stability for large asset universes.
The momentFUN argument to optimize.portfolio() controls how moments are estimated. The built-in set.portfolio.moments() supports a "boudt" method that fits a PCA-based factor model:
# Use factor-model moments for min variance optimization
fm.port <- add.objective(base.port, type = "risk", name = "StdDev")fm.roi <- optimize.portfolio(R, fm.port, optimize_method = "ROI",
momentFUN = "set.portfolio.moments",
method = "boudt", k = 3)
fm.roi
#> ***********************************
#> PortfolioAnalytics Optimization
#> ***********************************
#>
#> Call:
#> optimize.portfolio(R = R, portfolio = fm.port, optimize_method = "ROI",
#> method = "boudt", k = 3, momentFUN = "set.portfolio.moments")
#>
#> Optimal Weights:
#> Convertible Arbitrage CTA Global Distressed Securities
#> 0.0000 0.1987 0.0000
#> Emerging Markets Equity Market Neutral Event Driven
#> 0.0000 0.8013 0.0000
#>
#> Objective Measure:
#> StdDev
#> 0.01065knitr::kable(round(cbind(
Sample = minvar.roi$weights,
FactorModel = fm.roi$weights
), 4), caption = "Sample vs Factor Model Moment Estimation")| Sample | FactorModel | |
|---|---|---|
| Convertible Arbitrage | 0.0000 | 0.0000 |
| CTA Global | 0.1934 | 0.1987 |
| Distressed Securities | 0.0000 | 0.0000 |
| Emerging Markets | 0.0000 | 0.0000 |
| Equity Market Neutral | 0.8066 | 0.8013 |
| Event Driven | 0.0000 | 0.0000 |
The factor model approach can be combined with any solver and any objective type. It is especially valuable for portfolios with many assets (50+) where the sample covariance matrix may be poorly conditioned.
We benchmark each objective/solver combination using system.time(), averaging over multiple runs for the stochastic solvers. Convex solvers typically run in under a second; metaheuristic solvers may take considerably longer depending on search_size and convergence criteria.
Each subsection shows the benchmark code, a timing table, and a results comparison table showing weights and objective values across solvers.
bench.minvar.cvxr <- system.time(
res.minvar.cvxr <- optimize.portfolio(R, minvar.port,
optimize_method = "CVXR"))
bench.minvar.roi <- system.time(
res.minvar.roi <- optimize.portfolio(R, minvar.port,
optimize_method = "ROI"))
bench.minvar.osqp <- system.time(
res.minvar.osqp <- optimize.portfolio(R, minvar.port,
optimize_method = "osqp"))
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
bench.minvar.de.times <- replicate(n_runs, {
t <- system.time(
res <- optimize.portfolio(R, minvar.port.meta,
optimize_method = "DEoptim",
search_size = 5000))
c(elapsed = t["elapsed"], obj = res$opt_values[[1]])
})
bench.minvar.de.elapsed <- median(bench.minvar.de.times["elapsed.elapsed", ])
# use the result from the last DEoptim run
res.minvar.de <- optimize.portfolio(R, minvar.port.meta,
optimize_method = "DEoptim",
search_size = 5000)
bench.minvar.gensa.times <- replicate(n_runs, {
t <- system.time(
res <- optimize.portfolio(R, minvar.port.meta,
optimize_method = "GenSA",
search_size = 5000))
c(elapsed = t["elapsed"])
})
bench.minvar.gensa.elapsed <- median(bench.minvar.gensa.times)
res.minvar.gensa <- optimize.portfolio(R, minvar.port.meta,
optimize_method = "GenSA",
search_size = 5000)
bench.minvar.pso.times <- replicate(n_runs, {
t <- system.time(
res <- optimize.portfolio(R, minvar.port.meta,
optimize_method = "pso",
search_size = 5000))
c(elapsed = t["elapsed"])
})
bench.minvar.pso.elapsed <- median(bench.minvar.pso.times)
res.minvar.pso <- optimize.portfolio(R, minvar.port.meta,
optimize_method = "pso",
search_size = 5000)
bench.minvar <- data.frame(
Solver = c("CVXR", "ROI", "osqp", "DEoptim", "GenSA", "pso"),
Time_sec = c(bench.minvar.cvxr["elapsed"],
bench.minvar.roi["elapsed"],
bench.minvar.osqp["elapsed"],
bench.minvar.de.elapsed,
bench.minvar.gensa.elapsed,
bench.minvar.pso.elapsed)
)| Solver | Time (sec) |
|---|---|
| CVXR | 0.030 |
| ROI | 0.008 |
| osqp | 0.003 |
| DEoptim | 1.361 |
| GenSA | 7.527 |
| pso | 5.441 |
| CVXR | ROI | osqp | DEoptim | GenSA | pso | |
|---|---|---|---|---|---|---|
| Convertible Arbitrage | 0.0000 | 0.0000 | 0.0000 | 0.008 | 0.2048 | 0.0007 |
| CTA Global | 0.1934 | 0.1934 | 0.1934 | 0.182 | 0.1527 | 0.2091 |
| Distressed Securities | 0.0000 | 0.0000 | 0.0000 | 0.032 | 0.2782 | 0.1140 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 | 0.000 | 0.2353 | 0.3635 |
| Equity Market Neutral | 0.8066 | 0.8066 | 0.8066 | 0.772 | 0.0681 | 0.1503 |
| Event Driven | 0.0000 | 0.0000 | 0.0000 | 0.002 | 0.0709 | 0.1723 |
| Solver | Portfolio StdDev |
|---|---|
| CVXR | 0.010561 |
| ROI | 0.010561 |
| osqp | 0.010561 |
| DEoptim | 0.010696 |
| GenSA | 0.022695 |
| pso | 0.021554 |
bench.mines.cvxr <- system.time(
res.mines.cvxr <- optimize.portfolio(R, mines.port,
optimize_method = "CVXR"))
bench.mines.roi <- system.time(
res.mines.roi <- optimize.portfolio(R, mines.port,
optimize_method = "ROI"))
bench.mines.rglpk <- system.time(
res.mines.rglpk <- optimize.portfolio(R, mines.port,
optimize_method = "Rglpk"))
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
bench.mines.de.times <- replicate(n_runs, {
t <- system.time(
res <- optimize.portfolio(R, mines.port.meta,
optimize_method = "DEoptim",
search_size = 5000))
c(elapsed = t["elapsed"])
})
bench.mines.de.elapsed <- median(bench.mines.de.times)
res.mines.de <- optimize.portfolio(R, mines.port.meta,
optimize_method = "DEoptim",
search_size = 5000)
bench.mines.gensa.times <- replicate(n_runs, {
t <- system.time(
res <- optimize.portfolio(R, mines.port.meta,
optimize_method = "GenSA",
search_size = 5000))
c(elapsed = t["elapsed"])
})
bench.mines.gensa.elapsed <- median(bench.mines.gensa.times)
res.mines.gensa <- optimize.portfolio(R, mines.port.meta,
optimize_method = "GenSA",
search_size = 5000)
bench.mines.pso.times <- replicate(n_runs, {
t <- system.time(
res <- optimize.portfolio(R, mines.port.meta,
optimize_method = "pso",
search_size = 5000))
c(elapsed = t["elapsed"])
})
bench.mines.pso.elapsed <- median(bench.mines.pso.times)
res.mines.pso <- optimize.portfolio(R, mines.port.meta,
optimize_method = "pso",
search_size = 5000)
bench.mines <- data.frame(
Solver = c("CVXR", "ROI", "Rglpk", "DEoptim", "GenSA", "pso"),
Time_sec = c(bench.mines.cvxr["elapsed"],
bench.mines.roi["elapsed"],
bench.mines.rglpk["elapsed"],
bench.mines.de.elapsed,
bench.mines.gensa.elapsed,
bench.mines.pso.elapsed)
)| Solver | Time (sec) |
|---|---|
| CVXR | 0.059 |
| ROI | 0.007 |
| Rglpk | 0.004 |
| DEoptim | 1.362 |
| GenSA | 8.736 |
| pso | 5.639 |
#> Warning in Return.portfolio.geometric(R = R, weights = weights, wealth.index =
#> wealth.index, : The weights for one or more periods do not sum up to 1:
#> assuming a return of 0 for the residual weights
#> Warning in Return.portfolio.geometric(R = R, weights = weights, wealth.index =
#> wealth.index, : The weights for one or more periods do not sum up to 1:
#> assuming a return of 0 for the residual weights
#> Warning in Return.portfolio.geometric(R = R, weights = weights, wealth.index =
#> wealth.index, : The weights for one or more periods do not sum up to 1:
#> assuming a return of 0 for the residual weights
| CVXR | ROI | Rglpk | DEoptim | GenSA | pso | |
|---|---|---|---|---|---|---|
| Convertible Arbitrage | 0.0000 | 0.0000 | 0.0000 | 0.000 | 0.0066 | 0.0056 |
| CTA Global | 0.4984 | 0.4984 | 0.4984 | 0.440 | 0.1839 | 0.3480 |
| Distressed Securities | 0.0000 | 0.0000 | 0.0000 | 0.020 | 0.1771 | 0.0000 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 | 0.002 | 0.2715 | 0.0761 |
| Equity Market Neutral | 0.4223 | 0.4223 | 0.4223 | 0.528 | 0.3462 | 0.3407 |
| Event Driven | 0.0793 | 0.0793 | 0.0793 | 0.008 | 0.0248 | 0.2396 |
| Solver | ES (p=0.95) |
|---|---|
| CVXR | -0.020058 |
| ROI | -0.020058 |
| Rglpk | -0.020058 |
| DEoptim | -0.019821 |
| GenSA | -0.039863 |
| pso | -0.024417 |
bench.maxsr.cvxr <- system.time(
res.maxsr.cvxr <- optimize.portfolio(R, maxsr.port,
optimize_method = "CVXR",
maxSR = TRUE))
bench.maxsr.roi <- system.time(
res.maxsr.roi <- optimize.portfolio(R, maxsr.port,
optimize_method = "ROI",
maxSR = TRUE))
bench.maxsr.osqp <- system.time(
res.maxsr.osqp <- optimize.portfolio(R, maxsr.port,
optimize_method = "osqp"))
#> Leverage constraint min_sum and max_sum are restrictive,
#> consider relaxing. e.g. 'full_investment' constraint
#> should be min_sum=0.99 and max_sum=1.01
bench.maxsr.de.times <- replicate(n_runs, {
t <- system.time(
res <- optimize.portfolio(R, maxsr.port.meta,
optimize_method = "DEoptim",
search_size = 5000))
c(elapsed = t["elapsed"])
})
bench.maxsr.de.elapsed <- median(bench.maxsr.de.times)
res.maxsr.de <- optimize.portfolio(R, maxsr.port.meta,
optimize_method = "DEoptim",
search_size = 5000)
bench.maxsr.gensa.times <- replicate(n_runs, {
t <- system.time(
res <- optimize.portfolio(R, maxsr.port.meta,
optimize_method = "GenSA",
search_size = 5000))
c(elapsed = t["elapsed"])
})
bench.maxsr.gensa.elapsed <- median(bench.maxsr.gensa.times)
res.maxsr.gensa <- optimize.portfolio(R, maxsr.port.meta,
optimize_method = "GenSA",
search_size = 5000)
bench.maxsr.pso.times <- replicate(n_runs, {
t <- system.time(
res <- optimize.portfolio(R, maxsr.port.meta,
optimize_method = "pso",
search_size = 5000))
c(elapsed = t["elapsed"])
})
bench.maxsr.pso.elapsed <- median(bench.maxsr.pso.times)
res.maxsr.pso <- optimize.portfolio(R, maxsr.port.meta,
optimize_method = "pso",
search_size = 5000)
bench.maxsr <- data.frame(
Solver = c("CVXR", "ROI", "osqp", "DEoptim", "GenSA", "pso"),
Time_sec = c(bench.maxsr.cvxr["elapsed"],
bench.maxsr.roi["elapsed"],
bench.maxsr.osqp["elapsed"],
bench.maxsr.de.elapsed,
bench.maxsr.gensa.elapsed,
bench.maxsr.pso.elapsed)
)| Solver | Time (sec) |
|---|---|
| CVXR | 0.045 |
| ROI | 0.093 |
| osqp | 0.005 |
| DEoptim | 1.351 |
| GenSA | 7.431 |
| pso | 5.616 |
#> Warning in Return.portfolio.geometric(R = R, weights = weights, wealth.index =
#> wealth.index, : The weights for one or more periods do not sum up to 1:
#> assuming a return of 0 for the residual weights
#> Warning in Return.portfolio.geometric(R = R, weights = weights, wealth.index =
#> wealth.index, : The weights for one or more periods do not sum up to 1:
#> assuming a return of 0 for the residual weights
#> Warning in Return.portfolio.geometric(R = R, weights = weights, wealth.index =
#> wealth.index, : The weights for one or more periods do not sum up to 1:
#> assuming a return of 0 for the residual weights
| CVXR | ROI | osqp | DEoptim | GenSA | pso | |
|---|---|---|---|---|---|---|
| Convertible Arbitrage | 0.0887 | 0.0887 | 0.0887 | 0.006 | 0.2247 | 0.1316 |
| CTA Global | 0.4285 | 0.4285 | 0.4285 | 0.174 | 0.1892 | 0.1695 |
| Distressed Securities | 0.4828 | 0.4828 | 0.4828 | 0.006 | 0.1006 | 0.1031 |
| Emerging Markets | 0.0000 | 0.0000 | 0.0000 | 0.000 | 0.0807 | 0.2172 |
| Equity Market Neutral | 0.0000 | 0.0000 | 0.0000 | 0.806 | 0.2315 | 0.0000 |
| Event Driven | 0.0000 | 0.0000 | 0.0000 | 0.004 | 0.1834 | 0.3886 |
| Solver | Sharpe Ratio |
|---|---|
| CVXR | 0.239778 |
| ROI | 0.239778 |
| osqp | 0.239778 |
| DEoptim | 0.125247 |
| GenSA | 0.175116 |
| pso | 0.139940 |
The choice of solver depends on the problem structure:
| Solver | 10 assets | 50 assets | 200+ assets |
|---|---|---|---|
| CVXR | < 1s | < 2s | seconds |
| ROI | < 0.1s | < 0.5s | < 2s |
| osqp | < 0.1s | < 0.5s | < 1s |
| Rglpk | < 0.1s | < 0.5s | < 2s |
| DEoptim | seconds | minutes | minutes–hours |
| random | seconds | seconds | minutes |
The following inconsistencies and gaps were identified during the preparation of this vignette:
The maxSR flag defaults to FALSE, requiring users to explicitly pass maxSR = TRUE when both mean and variance objectives are present. In contrast, maxSTARR, CSMratio, and EQSratio all default to TRUE when their respective objective pairs are present. This asymmetry is a known behavioral difference that users should be aware of.
The EQSratio flag is not included in the weight_scale computation for CVXR constraint scaling (line 2997 of optimize.portfolio.R), and EQS ratio solutions are not normalized post-solve. This means box, group, and weight sum constraints may not be properly applied for EQS ratio problems.
The constrained_objective() function has an empty code block for the CSM objective (R/constrained_objective.R, line 606), meaning CSM is not directly available as a risk measure in metaheuristic solvers. Users who need CSM with DEoptim/GenSA/pso must supply a custom objective function.
| Constraint | Gap |
|---|---|
| Turnover | CVXR supports turnover constraints but the implementation modifies the objective (penalty on distance from initial weights) rather than adding a strict constraint; ROI supports strict turnover via gmv_opt_toc for QP problems only |
| Transaction cost | Only ROI supports proportional transaction costs (gmv_opt_ptc) |
| Factor exposure | CVXR, ROI, osqp, and Rglpk support factor exposure as hard linear constraints; metaheuristic solvers enforce via QP projection in fn_map() |
| Position limit | Only ROI (MILP) and Rglpk (MILP) support exact position limits |
| Diversification | Only metaheuristic solvers support the diversification constraint |
rp_transform() function used inside fn_map() for iterative perturbation does not directly handle factor exposure constraints. Instead, fn_map() applies a post-perturbation QP projection step that enforces factor exposure bounds. Random portfolio generation (random_portfolios()) does not use this projection, so randomly generated starting portfolios may not satisfy factor exposure bounds.| Type string(s) | Constraint class | Description |
|---|---|---|
"box" |
box_constraint |
Per-asset upper and lower weight bounds |
"long_only" |
box_constraint |
Shortcut: min=0, max=1 for all assets |
"weight", "leverage", "weight_sum" |
weight_sum_constraint |
Bounds on sum of weights (min_sum, max_sum) |
"full_investment" |
weight_sum_constraint |
Shortcut: min_sum=1, max_sum=1 |
"dollar_neutral", "active" |
weight_sum_constraint |
Shortcut: min_sum=0, max_sum=0 |
"group" |
group_constraint |
Group-level weight sum bounds |
"turnover" |
turnover_constraint |
Maximum total turnover from initial weights |
"diversification" |
diversification_constraint |
Diversification target |
"position_limit" |
position_limit_constraint |
Max non-zero positions (max_pos, max_pos_long, max_pos_short) |
"return" |
return_constraint |
Minimum target return |
"factor_exposure" |
factor_exposure_constraint |
Factor exposure bounds via matrix B |
"transaction", "transaction_cost" |
transaction_cost_constraint |
Proportional transaction costs |
"leverage_exposure" |
leverage_exposure_constraint |
Limit on sum of absolute weights |
sessionInfo()
#> R version 4.6.0 (2026-04-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 24.04.4 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] parallel stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] ROI.plugin.glpk_1.0-0 ROI.plugin.quadprog_1.0-1
#> [3] ROI_1.0-2 pso_1.0.4
#> [5] GenSA_1.1.15 DEoptim_2.2-8
#> [7] CVXR_1.8.2 PortfolioAnalytics_2.1.2
#> [9] PerformanceAnalytics_2.1.0 foreach_1.5.2
#> [11] xts_0.14.2 zoo_1.8-15
#>
#> loaded via a namespace (and not attached):
#> [1] Matrix_1.7-5 jsonlite_2.0.0
#> [3] compiler_4.6.0 highs_1.12.0-3
#> [5] Rcpp_1.1.1-1.1 slam_0.1-55
#> [7] ROI.plugin.symphony_1.0-0 mco_1.17
#> [9] yaml_2.3.12 fastmap_1.2.0
#> [11] clarabel_0.11.2 lattice_0.22-9
#> [13] Rsymphony_0.1-33 knitr_1.51
#> [15] iterators_1.0.14 backports_1.5.1
#> [17] checkmate_2.3.4 maketools_1.3.2
#> [19] osqp_1.0.0 rlang_1.2.0
#> [21] xfun_0.57 quadprog_1.5-8
#> [23] sys_3.4.3 S7_0.2.2
#> [25] registry_0.5-1 cli_3.6.6
#> [27] Rglpk_0.6-5.1 digest_0.6.39
#> [29] grid_4.6.0 gmp_0.7-5.1
#> [31] scs_3.2.7 evaluate_1.0.5
#> [33] codetools_0.2-20 buildtools_1.0.0
#> [35] rmarkdown_2.31 tools_4.6.0
#> [37] htmltools_0.5.9