# --- Instance non convexe : mandat concentre 6 actifs, cardinalite K=2 ---
asset_names_c = asset_names + ["Hedge Funds"]
mu_c = np.array([0.05, 0.10, 0.15, 0.20, 0.25, 0.17])
sig_c = np.array([0.10, np.sqrt(0.02), np.sqrt(0.03), 0.20, np.sqrt(0.05), 0.11])
corr_c = np.array([
[1.00, 0.10, 0.12, 0.15, 0.12, 0.05],
[0.10, 1.00, 0.15, 0.16, 0.14, 0.08],
[0.12, 0.15, 1.00, 0.30, 0.28, 0.10],
[0.15, 0.16, 0.30, 1.00, 0.32, 0.12],
[0.12, 0.14, 0.28, 0.32, 1.00, 0.15],
[0.05, 0.08, 0.10, 0.12, 0.15, 1.00],
])
cov_c = np.outer(sig_c, sig_c) * corr_c
K_c = 2 # au plus 2 actifs
def project_topk_c(weights, k=K_c):
"""Projette un vecteur de poids continus sur la contrainte de cardinalite :
conserve les k plus gros poids, renormalise."""
w = np.maximum(np.array(weights, dtype=float), 0.0)
if (w > 0).sum() > k:
keep = np.argsort(w)[-k:]
mask = np.zeros_like(w)
mask[keep] = w[keep]
w = mask
total = w.sum()
return w / total if total > 1e-12 else np.ones(len(w)) / len(w)
def ret_c(w):
return float(w @ mu_c)
def risk_c(w):
return float(np.sqrt(w @ cov_c @ w))
# Reference EXACTE : enumeration des paires x QP min-variance par cible de rendement.
t_start = time.time()
targets_c = np.linspace(mu_c.min(), mu_c.max(), 41)
exact_pts = []
for s in combinations(range(len(mu_c)), 2):
mu_s = mu_c[list(s)]
cov_s = cov_c[np.ix_(list(s), list(s))]
for tgt in targets_c:
if tgt < mu_s.min() - 1e-12 or tgt > mu_s.max() + 1e-12:
continue
wv = cp.Variable(2)
p = cp.Problem(cp.Minimize(cp.quad_form(wv, cp.psd_wrap(cov_s))),
[cp.sum(wv) == 1, wv >= 0, mu_s @ wv >= tgt])
p.solve(solver=cp.CLARABEL)
if wv.value is not None:
w2 = np.zeros(len(mu_c))
w2[list(s)] = wv.value
exact_pts.append((risk_c(w2), ret_c(w2)))
t_exact = time.time() - t_start
# Filtre non domine -> front exact
exact_all = np.array(sorted(exact_pts))
exact_front = []
best_r = -np.inf
for sg, rt in exact_all:
if rt > best_r + 1e-9:
exact_front.append((sg, rt))
best_r = rt
exact_front = np.array(exact_front)
print(f"Reference exacte : {len(exact_pts)} points enumeres -> {len(exact_front)} non domines "
f"({t_exact:.1f}s, {len(list(combinations(range(6), 2)))} paires x QP)")
# Mesure geometrique : quels points exacts sont atteignables par une somme ponderee ?
def scalarization_reachable(sig_f, ret_f, k):
"""Point k atteignable ssi il existe alpha > 0 dominant tous les autres points."""
lo, hi = 0.0, np.inf
num = ret_f[k] - ret_f # R_k - R_i
den = sig_f[k] - sig_f # sigma_k - sigma_i
for i, (n, d) in enumerate(zip(num, den)):
if i == k:
continue
if abs(d) < 1e-14:
if n <= 1e-12:
return False
elif d < 0:
lo = max(lo, n / d)
else:
hi = min(hi, n / d)
return hi > max(lo, 0.0)
reach_mask = np.array([
scalarization_reachable(exact_front[:, 0], exact_front[:, 1], k)
for k in range(len(exact_front))
])
n_unreach = int((~reach_mask).sum())
print(f"Points du front exact atteignables par somme ponderee (alpha > 0) : "
f"{int(reach_mask.sum())}/{len(exact_front)}")
print(f"Points INATTEIGNABLES (sous le hull convexe) : {n_unreach} "
f"-- aucune valeur d'alpha ne peut les rendre optimum")