library(ROI); library(ROI.plugin.glpk)
## Warning: package 'ROI' was built under R version 4.4.3
## ROI: R Optimization Infrastructure
## Registered solver plugins: nlminb, glpk.
## Default solver: auto.
## Warning: package 'ROI.plugin.glpk' was built under R version 4.4.3
cvar_shortfall <- function(R, W0=1, T=1.0, alpha=0.95, rho=1.0){
S <- nrow(R); D <- ncol(R)
# Vars: x(1..D)>=0, zeta (VaR proxy), u(1..S)>=0
nvar <- D + 1 + S
# Objective: minimize rho * [ zeta + (1/((1-alpha)S)) * sum u_s ]
obj <- c(rep(0, D), rho, rep(rho/((1-alpha)*S), S))
# (1) sum x = 1
A1 <- matrix(0, 1, nvar); A1[1, 1:D] <- 1
dir <- "=="; rhs <- 1
# (2) u_s >= T - W_s - zeta
# => -u_s <= W_s + zeta - T
# W_s = W0*(1 + R_s x)
A2 <- matrix(0, S, nvar)
A2[, 1:D] <- W0 * R # x part (moved to LHS)
A2[, D+1] <- 1 # zeta
A2[, (D+2):(D+1+S)] <- -diag(S) # -u_s
b2 <- rep(T - W0, S)
L <- rbind(A1, A2)
dir <- c(dir, rep(">=", S)) # (A2 * vars) >= b2 is u >= T - W - zeta moved correctly
rhs <- c(rhs, b2)
lb <- c(rep(0, D), -Inf, rep(0, S))
ub <- rep(Inf, nvar)
model <- OP(objective = obj,
constraints = L_constraint(L, dir, rhs),
bounds = V_bound(li=1:nvar, lb=lb, ui=1:nvar, ub=ub),
maximum = FALSE)
sol <- ROI_solve(model, solver="glpk")
list(x = sol$solution[1:D],
zeta = sol$solution[D+1],
u = sol$solution[(D+2):(D+1+S)],
objective = sol$objval)
}
# Example:
set.seed(2)
S <- 5000; D <- 3
R <- cbind(rnorm(S,0.08,0.15), rnorm(S,0.03,0.005), rnorm(S,0.02,0.02))
res <- cvar_shortfall(R, W0=1, T=1.02, alpha=0.975, rho=1.0)
print(res$x); print(res$objective)
## [1] 0.01628024 0.98371976 0.00000000
## [1] 0.008879157
\[ [ R ;=; \begin{bmatrix} r_{E,1} & r_{C,1} & r_{B,1} \ r_{E,2} & r_{C,2} & r_{B,2} \ \vdots & \vdots & \vdots \ r_{E,S} & r_{C,S} & r_{B,S} \end{bmatrix} ] \]
Each row is one scenario; each column is one asset’s return in that scenario. It is not “3×3” unless you literally have S = 3 scenarios.
The “3×3” you sometimes see is the covariance matrix \((\Sigma)\) of the three assets—that’s a different object (assets×assets), used if you generate scenarios parametrically.
Pros: zero modelling, realistic co-moves. Cons: only repeats the past.
Estimate mean vector \((\mu \in \mathbb{R}^3)\) and covariance \((\Sigma \in \mathbb{R}^{3\times3})\) from history (or judgment).
Draw (S) scenarios from a joint distribution:
Stack the draws: that’s your (R).
Pros: easy to stress (change \((\mu,\Sigma)\), add fat tails). Cons: normal can understate crashes unless you use t or add jumps.
This is where the “3×3” shows up: \((\Sigma)\) is 3×3 because you have 3 assets.
Pros: lets you craft named stress events; bonds respond to TERM/CRD directly. Cons: you must pick factors and betas.
Term deposit: near-fixed return over the horizon (quoted TD rate for the term).
Bond fund (market-to-market): use duration/convexity with a yield shock:
\[ [ r_B \approx\underbrace{\text{carry}}_{\text{coupon/price yield}} ;-; D ,\Delta y ;+; \tfrac{1}{2}C,(\Delta y)^2 ;-; \Delta \text{spread} ] \]
where (D) is duration, (C) convexity, (y) the rate move. Stress scenarios: sharp rate up (neg return), spread widening (credit).
If minimum lots make bonds infeasible for small investors, set the bond column to 0 or omit it—your solver will cope.
\[ [ W_s = W_0\left(1 + r_{E,s}x_E + r_{C,s}x_C + r_{B,s}x_B\right),\quad \bar W = \frac{1}{S}\sum_s W_s. ] \]
Pick horizon (monthly is common).
Build (R) (S×3) with one of A/B/C:
Inspect columns (means, stdevs) and correlations—do they look reasonable?
Add stress rows explicitly (e.g., “GFC-like”, “rate +300 bps”), even if they’re extreme outliers.
Run your chosen stress solver \((maximin/minimax/CVaR)\).
python
import numpy as np
S = 10000 # scenarios
# Historical monthly means (illustrative):
mu = np.array([0.008, 0.0003, 0.003]) # shares, cash, bond
sd = np.array([0.045, 0.0008, 0.015])
rho = np.array([[1.00, 0.10, -0.20], # assumed corr
[0.10, 1.00, 0.10],
[-0.20,0.10, 1.00]])
Sigma = np.outer(sd, sd) * rho
# (B) Parametric scenarios
R_param = np.random.multivariate_normal(mu, Sigma, size=S)
# (A) Bootstrap scenarios (if you have real columns E, C, B as arrays)
# idx = np.random.randint(0, len(E), size=S)
# R_boot = np.column_stack([E[idx], C[idx], B[idx]])
# Add explicit stress rows
stress = np.array([
[-0.25, 0.0003, -0.05], # equity crash + bond selloff
[-0.35, 0.0003, +0.02], # worse equity, bond rally
])
R = np.vstack([R_param, stress])
That’s it: R is S×3. Feed it to any of your stress solvers.
Weights \((x)\):
Objective value (z) for minimax shortfall:
### How to read it (Minimax Shortfall vs floor (T))
Recall the model: minimize the worst shortfall \((z)\) subject to \((z \ge T - W_s,\ \forall s)\), with \((W_s = 1 + r_s^\top x)\) (assuming \((W_0=1)\)).
If you tell me your actual (T) (floor) and confirm the three asset return generators/columns, I can translate (z) into the exact worst-case wealth and suggest a small parameter tweak (e.g., CVaR level ()) to get a more balanced allocation if that’s your goal.