Optimization Solvers in PortfolioAnalytics

Introduction

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:

  1. Create a portfolio specification with portfolio.spec()
  2. Add constraints with add.constraint()
  3. Add objectives with add.objective()
  4. Solve with optimize.portfolio(), passing an optimize_method

Because 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:

  • An overview of all supported optimization solvers
  • A compatibility matrix mapping objective specifications to solvers
  • Worked examples for each objective/solver combination
  • Performance benchmarks comparing time-to-converge and solution quality
  • Guidance on solver selection

Solver Overview

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.

Convex Solvers

CVXR

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").

ROI

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.

osqp

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.

Rglpk

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

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., NP and strategy for DEoptim, temperature for GenSA, swarm size s for 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.

Multi-Objective

The mco package (NSGA-II) is available via optimize_method = "mco" for Pareto-optimal frontier computation across multiple objectives simultaneously.

Solver Summary

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

Objective & Constraint Support Matrix

Objective Support

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:

  • CSM and EQS objectives are available only through CVXR. Metaheuristic solvers can minimize CSM indirectly by specifying a custom objective function, but there is no built-in constrained_objective dispatch for CSM
  • Risk budget objectives (including equal risk contribution) require component risk decomposition and are only supported by metaheuristic solvers
  • osqp solves max Sharpe implicitly when both mean and variance objectives are present (Charnes-Cooper QP); no maxSR flag is needed
  • Rglpk solves max STARR implicitly when both mean and ES objectives are present (Charnes-Cooper LP)

Constraint Support

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.

Worked Examples

Data Setup

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")

Minimum Variance

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")

CVXR

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.01056

ROI

minvar.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.01056

osqp

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
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"

DEoptim

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.01116

GenSA

set.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.02344

pso

set.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.01425

Comparison

minvar.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")
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.

Minimum ES / CVaR

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))

CVXR

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.01917

ROI

mines.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.01917

Rglpk

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
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"

DEoptim

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.02105

GenSA

set.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.05149

pso

set.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.03069

Comparison

mines.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")
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

Minimum CSM

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))

CVXR

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.02079

Maximum Return

Maximize expected (mean) return. This is an LP problem supported by all convex solvers.

maxret.port <- add.objective(base.port, type = "return", name = "mean")

CVXR

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.004973

ROI

maxret.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.004973

Rglpk

maxret.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"

Comparison

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")
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.

Maximum Sharpe Ratio

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")

CVXR

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.2404

ROI

maxsr.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.003947

Important

The maxSR = TRUE flag is required for CVXR and ROI. Without it, both solvers default to maximizing quadratic utility (mean - lambda * variance) rather than the Sharpe ratio.

osqp

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"

DEoptim

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.01092

GenSA

set.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.01952

pso

set.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.01637

Comparison

maxsr.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")
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

Maximum STARR (Mean / ES)

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))

CVXR

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.1502

ROI

maxstarr.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.0242

Note

Unlike maxSR, the maxSTARR flag defaults to TRUE for both CVXR and ROI when both mean and ES objectives are present. Pass maxSTARR = FALSE to minimize ES subject to a mean-ES trade-off instead.

Rglpk

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"

DEoptim

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.02116

GenSA

set.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.04877

pso

set.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.05446

Comparison

maxstarr.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")
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

Maximum Quadratic Utility

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)

CVXR

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.01741

ROI

ROI 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.01741

DEoptim

maxqu.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.01092

GenSA

set.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.01952

pso

set.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.01637

Comparison

maxqu.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")
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 Budgets

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.

Equal ES Risk Contribution

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.126349
set.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.50736
set.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.18458

ERC Comparison

erc.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")
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")
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

Bounded Risk Contribution

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.210798
set.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.17800
set.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.07700

Bounded Risk Budget Comparison

rb.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")
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%)")
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

Weight Concentration (HHI)

The Herfindahl-Hirschman Index (HHI) penalty discourages weight concentration by adding λHHIwi2 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)

CVXR

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.1667

ROI

hhi.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.01943

Comparison

hhi.weights <- round(cbind(
  CVXR = hhi.cvxr$weights,
  ROI  = hhi.roi$weights
), 4)
knitr::kable(hhi.weights, caption = "Variance + HHI Weights")
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

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.

Setup

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")

CVXR

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.01114

Verify 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

ROI

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.01114
exposures.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

osqp

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 (maxReturn)

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

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.01128
exposures.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

GenSA

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.02128
exposures.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

pso

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.01407
exposures.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

Comparison

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")
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 with Different Objectives

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")
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")
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")
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

Factor Models for Moment Estimation

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.01065
knitr::kable(round(cbind(
  Sample = minvar.roi$weights,
  FactorModel = fm.roi$weights
), 4), caption = "Sample vs Factor Model Moment Estimation")
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.

Performance Benchmarks

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.

Minimum Variance

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
Weights
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
Objective Values
Solver Portfolio StdDev
CVXR 0.010561
ROI 0.010561
osqp 0.010561
DEoptim 0.010696
GenSA 0.022695
pso 0.021554

Minimum ES

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
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
Objective Values
Solver ES (p=0.95)
CVXR -0.020058
ROI -0.020058
Rglpk -0.020058
DEoptim -0.019821
GenSA -0.039863
pso -0.024417

Maximum Sharpe Ratio

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
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
Objective Values
Solver Sharpe Ratio
CVXR 0.239778
ROI 0.239778
osqp 0.239778
DEoptim 0.125247
GenSA 0.175116
pso 0.139940

Solver Selection Guide

Decision Framework

The choice of solver depends on the problem structure:

  1. Is the objective convex? (variance, ES, CSM, return, ratios thereof)
    • Yes: use a convex solver for exact, fast results
    • No: use a metaheuristic solver
  2. Which convex solver?
    • CVXR is the most versatile, supporting LP, QP, and SOCP problems with the widest objective coverage (including CSM and EQS)
    • ROI is well-tested for QP (min variance, quadratic utility) and LP (max return, min ES) problems, and uniquely supports turnover, transaction cost, and leverage exposure constraints
    • osqp is fast for pure QP problems but limited in scope
    • Rglpk is fast for LP problems and supports position limits via MILP
  3. Do you need risk budgets, custom objectives, or non-convex constraints?
    • Use DEoptim (best convergence for most problems), GenSA (good for high-dimensional problems), pso, or random (simplest, good for prototyping)
  4. Do you need position limits (cardinality constraints)?
    • ROI and Rglpk support MILP formulations with binary variables
    • Metaheuristic solvers handle position limits via penalty terms

Scalability

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

Gaps & Normalization Notes

The following inconsistencies and gaps were identified during the preparation of this vignette:

Ratio Flag Defaults

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.

EQS Ratio Constraints

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.

CSM in Metaheuristic Solvers

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 Coverage Gaps

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

Factor Exposure Gaps

  • The 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.

Appendix

Constraint Type Reference

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

Session Info

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