---
title: "4.1.5 Optimización con CVaR, drawdown, rotación y costes"
description: "CVaR de Rockafellar-Uryasev, CDaR y caída máxima como programas lineales en cvxpy, y costes de horquilla e impacto con límites de rotación."
url: "https://pythonparatrading.com/curso-python-trading/nivel-4/optimizacion-cvar-drawdown-turnover-y-costes/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-4/optimizacion-cvar-drawdown-turnover-y-costes.png"
---

# 4.1.5 · Optimización con CVaR, drawdown, rotación y costes

> La varianza castiga igual las subidas que las bajadas y no ve la trayectoria. Esta lección optimiza carteras con medidas de cola (CVaR) y de caída (CDaR y caída máxima) formuladas como programas lineales en cvxpy, las contrasta con skfolio y Riskfolio-Lib, mide qué aguantan fuera de muestra y añade lo que decide el resultado neto en real: los costes y la rotación.

- Curso: [Nivel 4: Carteras, riesgo, ejecución y trading en real](https://pythonparatrading.com/curso-python-trading/nivel-4/)
- Módulo: 4.1 · Teoría y optimización de carteras
- Duración: 75 min
- Requisitos: [4.1.1 · Markowitz: frontera eficiente y restricciones reales](https://pythonparatrading.com/curso-python-trading/nivel-4/markowitz-frontera-eficiente-y-restricciones/), [4.1.4 · Risk parity, mínima varianza, HRP y HERC](https://pythonparatrading.com/curso-python-trading/nivel-4/risk-parity-minima-varianza-hrp-y-herc/), [2.4.7 · Modelado de costes: comisiones, horquilla, impacto, financiación y préstamo de títulos](https://pythonparatrading.com/curso-python-trading/nivel-2/modelado-de-costes/), [2.6.1 · CAGR, Sharpe, Sortino, Calmar, Omega, drawdown y Ulcer Index](https://pythonparatrading.com/curso-python-trading/nivel-2/metricas-de-rentabilidad-y-riesgo/)

## Al terminar esta lección

- Definir VaR y CVaR históricos y comprobar con datos que la función de Rockafellar-Uryasev tiene su mínimo en el VaR y vale el CVaR.
- Formular el mínimo CVaR, el mínimo CDaR y la mínima caída máxima como programas lineales en cvxpy y obtener los mismos pesos que skfolio y Riskfolio-Lib.
- Medir dentro y fuera de muestra qué aporta cada medida de riesgo frente a la varianza, y por qué optimizar la caída máxima sobreajusta.
- Modelar el coste de rebalancear con media horquilla e impacto de raíz cuadrada, y controlarlo con una penalización en el objetivo o con un tope de rotación.

## Lo que la varianza no ve

La varianza mide la dispersión alrededor de la media: una subida del 3 % la aumenta tanto como una caída del 3 %, y
le da igual que las pérdidas lleguen repartidas o seguidas. Para quien gestiona dinero las dos cosas importan: lo que
duele es la cola de pérdidas y la caída acumulada desde el último máximo. Para una distribución normal da igual, porque
la varianza lo resume todo; para una estrategia que vende seguros, no.

Los datos de esta lección son los rendimientos diarios en exceso del tipo libre de nueve estrategias sintéticas
(`estrategias4_diarias.parquet`), parecidas a las de los niveles 2 y 3, con crisis, agrupamiento de volatilidad y
**eventos fechados** que imitan episodios reales: el crash de los quants de agosto de 2007, el desplome del carry en
octubre de 2008, el crash de momentum de 2009, el Volmageddon de febrero de 2018 y marzo de 2020. Son sintéticos: los
números sirven para comparar métodos, no como evidencia de que una estrategia funcione. La décima columna,
`degradada`, se queda fuera (es para la lección 4.2.5).

```python
import numpy as np
import pandas as pd

est = pd.read_parquet("estrategias4_diarias.parquet")
est = est.set_index("fecha").drop(columns="degradada")
R = est.values                       # rendimientos simples en exceso
T, n = R.shape

def cvar_hist(x, beta):
    """Pérdida media en la peor fracción 1 − beta de los días."""
    perdida = -np.asarray(x)
    var = np.quantile(perdida, beta)
    return perdida[perdida >= var].mean()

tabla = pd.DataFrame({
    "vol_%": 100 * np.sqrt(252) * est.std(),
    "asimetria": est.skew(),
    "peor_dia_%": 100 * est.min(),
    "cvar95_%": [100 * cvar_hist(est[c], 0.95) for c in est],
    "cvar99_%": [100 * cvar_hist(est[c], 0.99) for c in est],
})
print(T, "sesiones,", n, "estrategias")
tabla.round(2)
```
```output
5031 sesiones, 9 estrategias
                   vol_%  asimetria  peor_dia_%  cvar95_%  cvar99_%
tendencia_futuros  10.73       0.08       -4.94      1.52      2.53
carry_futuros       7.54      -0.18       -3.19      1.09      1.77
carry_divisas       9.09      -0.20       -4.59      1.31      2.18
venta_puts         11.90      -0.35       -6.56      1.73      2.93
pares               5.96      -0.46       -3.90      0.87      1.47
momentum_acciones  12.19      -0.56       -8.04      1.77      3.10
valor_acciones      9.04       0.08       -3.18      1.28      1.95
venta_volatilidad  16.37      -2.76      -19.29      2.45      4.71
reversion_corto     7.07      -0.03       -4.01      1.02      1.75
```

La venta de volatilidad tiene la mayor volatilidad (16,4 %), una asimetría de −2,76 y un peor día del −19,3 %: es la
estrategia de cola. Su CVaR al 99 % (4,71 %) es un 50 % mayor que el
siguiente, el del momentum de acciones (3,10 %). La venta de puts y el momentum de
acciones tienen colas más moderadas pero también largas.

## VaR y CVaR

Dado un nivel de confianza β (0,95 o 0,99) y la pérdida diaria $L = -r$:

- El **VaR** al nivel β es el cuantil β de las pérdidas: la pérdida que solo se supera en un $(1-\beta)$ de los días.
- El **CVaR** (*conditional value at risk*, también *expected shortfall*) es la pérdida media cuando se supera el
  VaR: la media del peor $(1-\beta)$ de los días.

El VaR dice dónde empieza la cola y nada de lo que hay dentro; el CVaR la promedia. Además el CVaR es una **medida
coherente** en el sentido de Artzner, Delbaen, Eber y Heath (1999): entre otras propiedades, es subaditiva (el riesgo
de una cartera no supera la suma de los riesgos) y convexa en los pesos. El VaR no es subaditivo en general, y
minimizarlo es un problema no convexo con muchos mínimos locales. Cómo estimar el VaR y el CVaR, y cómo comprobar si
un modelo de VaR acierta, son la lección 4.2.3; aquí interesan como objetivos de optimización.

El problema práctico del CVaR es que depende de un cuantil, y los cuantiles no son funciones cómodas para un
optimizador. Rockafellar y Uryasev (2000) resolvieron el problema con una función auxiliar:

$$
F_\beta(w, \alpha) = \alpha + \frac{1}{(1-\beta)\,T} \sum_{t=1}^{T} \max\!\left(-r_t'w - \alpha,\; 0\right).
$$

Para unos pesos fijos, $F_\beta$ es convexa en α, su mínimo se alcanza en el VaR y su valor mínimo es el CVaR. Lo
mejor: minimizar $F_\beta$ **a la vez** en $w$ y en α da la cartera de mínimo CVaR, sin calcular ningún cuantil.
Antes de optimizar, la comprobación con la cartera 1/N:

```python
from scipy.optimize import minimize_scalar

w_1n = np.full(n, 1 / n)
p = R @ w_1n                          # rendimiento de la cartera 1/N

def F(alfa, beta=0.95):
    """Función de Rockafellar-Uryasev: su mínimo en alfa es el CVaR."""
    return alfa + np.maximum(-p - alfa, 0).mean() / (1 - beta)

res = minimize_scalar(F, bounds=(0, 0.05), method="bounded",
                      options={"xatol": 1e-10})
print(f"VaR 95 % (cuantil):     {np.quantile(-p, 0.95):.5f}")
print(f"alfa óptimo:            {res.x:.5f}")
print(f"CVaR 95 % directo:      {cvar_hist(p, 0.95):.5f}")
print(f"mínimo de F:            {res.fun:.5f}")
```
```output
VaR 95 % (cuantil):     0.00429
alfa óptimo:            0.00430
CVaR 95 % directo:      0.00622
mínimo de F:            0.00622
```

*Fig. — Función de Rockafellar-Uryasev de la cartera 1/N de nueve estrategias: su mínimo está en el VaR y vale el CVaR.* Curva convexa de F(α) frente al umbral α, de 0 a 1,2 %. Empieza en el 2,0 % con α = 0, baja hasta un mínimo (destacado) en α = 0,43 %, el VaR al 95 %, donde vale 0,62 %, el CVaR al 95 %, y después sube en línea casi recta hasta el 1,2 %.

El α que minimiza $F$ (0,00430) coincide con el cuantil del 95 % de las pérdidas (0,00429; la diferencia es la
discretización del cuantil entre dos observaciones) y el mínimo de $F$, 0,00622, con el CVaR calculado a mano. La
figura enseña por qué: a la izquierda del VaR, subir α resta más de lo que suma; a la derecha, la función crece con
pendiente 1.

## El mínimo CVaR como programa lineal

El término $\max(\cdot, 0)$ se linealiza con una variable auxiliar por escenario, $u_t \ge 0$ y
$u_t \ge -r_t'w - \alpha$. Queda un programa lineal con $T + n + 1$ variables:

$$
\min_{w,\,\alpha,\,u}\; \alpha + \frac{1}{(1-\beta)T}\sum_t u_t
\quad \text{s.a.} \quad u_t \ge -r_t'w - \alpha,\;\; u_t \ge 0,\;\; \textstyle\sum_i w_i = 1,\;\; w \ge 0.
$$

```mermaid
flowchart TB
  E[Escenarios r_t] --> U[Exceso u_t]
  A[Umbral alfa] --> U
  W[Pesos w] --> U
  U --> O[alfa + media u_t]
  O --> S[Solver LP]
```

Cada día de la muestra es un escenario. Con 5.031 días son 5.041 variables y unas 10.000 restricciones: para un
solver lineal como HiGHS, un problema pequeño. La función `cvar_cp` devuelve la expresión y sus restricciones para
poder usarla como objetivo o como restricción:

```python
import cvxpy as cp
import riskfolio as rp
from skfolio import RiskMeasure
from skfolio.optimization import MeanRisk, ObjectiveFunction

def cvar_cp(w, R, beta):
    """CVaR como LP: devuelve la expresión y sus restricciones."""
    alfa = cp.Variable()
    u = cp.Variable(R.shape[0])
    expr = alfa + cp.sum(u) / ((1 - beta) * R.shape[0])
    return expr, [u >= -R @ w - alfa, u >= 0]

def min_cvar(R, beta=0.95):
    w = cp.Variable(R.shape[1])
    expr, extra = cvar_cp(w, R, beta)
    cp.Problem(cp.Minimize(expr),
               [cp.sum(w) == 1, w >= 0] + extra).solve(solver="HIGHS")
    return np.clip(w.value, 0, None)

propia = min_cvar(R)

skf = MeanRisk(objective_function=ObjectiveFunction.MINIMIZE_RISK,
               risk_measure=RiskMeasure.CVAR, cvar_beta=0.95).fit(est)
port = rp.Portfolio(returns=est, alpha=0.05)
port.assets_stats(method_mu="hist", method_cov="hist")
rkf = port.optimization(model="Classic", rm="CVaR", obj="MinRisk",
                        hist=True)

print(f"CVaR 95 %: {100 * cvar_hist(R @ propia, 0.95):.4f} %")
print(f"skfolio:   {np.abs(skf.weights_ - propia).max():.1e}")
print(f"riskfolio: {np.abs(rkf['weights'] - propia).max():.1e}")
pd.Series(propia, est.columns).round(3)
```
```output
CVaR 95 %: 0.4627 %
skfolio:   7.0e-10
riskfolio: 3.7e-10
tendencia_futuros    0.116
carry_futuros        0.151
carry_divisas        0.082
venta_puts           0.022
pares                0.256
momentum_acciones    0.071
valor_acciones       0.145
venta_volatilidad    0.000
reversion_corto      0.157
dtype: float64
```

Los tres coinciden hasta $10^{-9}$: skfolio (`cvar_beta=0.95`) y Riskfolio-Lib (`alpha=0.05`, `hist=True`) resuelven
la misma formulación. La cartera reparte el peso entre pares, reversión a corto, valor, carry de futuros y tendencia,
y deja la venta de volatilidad a cero. Su CVaR al 95 % es del 0,46 % diario.

¿Cambia algo frente a la mínima varianza?

```python
def min_var(R):
    w = cp.Variable(R.shape[1])
    L = np.linalg.cholesky(np.cov(R.T))
    cp.Problem(cp.Minimize(cp.sum_squares(L.T @ w)),
               [cp.sum(w) == 1, w >= 0]).solve(solver="CLARABEL")
    return np.clip(w.value, 0, None)

def metricas(w, R):
    p = R @ w
    curva = np.cumprod(1 + p)
    return {
        "vol_%": 100 * np.sqrt(252) * p.std(),
        "cvar95_%": 100 * cvar_hist(p, 0.95),
        "cvar99_%": 100 * cvar_hist(p, 0.99),
        "max_caida_%": 100 * (curva / np.maximum.accumulate(curva)
                              - 1).min(),
        "sharpe": np.sqrt(252) * p.mean() / p.std(),
    }

carteras = {"1/N": w_1n, "min_var": min_var(R),
            "min_cvar95": min_cvar(R, 0.95),
            "min_cvar99": min_cvar(R, 0.99)}
pesos = pd.DataFrame(carteras, index=est.columns)
print(pd.DataFrame({k: metricas(w, R) for k, w in carteras.items()})
      .T.round(2))
pesos.round(3)
```
```output
            vol_%  cvar95_%  cvar99_%  max_caida_%  sharpe
1/N          4.49      0.62      0.96       -14.59    0.84
min_var      3.45      0.47      0.67        -8.94    0.98
min_cvar95   3.46      0.46      0.67        -9.38    0.98
min_cvar99   3.51      0.47      0.65       -10.25    1.01
                     1/N  min_var  min_cvar95  min_cvar99
tendencia_futuros  0.111    0.116       0.116       0.123
carry_futuros      0.111    0.137       0.151       0.115
carry_divisas      0.111    0.068       0.082       0.118
venta_puts         0.111    0.024       0.022       0.039
pares              0.111    0.253       0.256       0.195
momentum_acciones  0.111    0.090       0.071       0.086
valor_acciones     0.111    0.150       0.145       0.142
venta_volatilidad  0.111    0.001       0.000       0.000
reversion_corto    0.111    0.162       0.157       0.183
```

Poco. La mínima varianza y el mínimo CVaR al 95 % tienen casi los mismos pesos y las mismas métricas; el mínimo CVaR
al 99 % se separa algo (más reversión, menos pares) y baja el CVaR al 99 % de 0,67 % a 0,65 %. La razón es que la
mínima varianza ya descarta la estrategia de cola gruesa: la venta de volatilidad tiene tanta varianza que pesa cero, o casi,
con cualquier medida, y la cartera que queda, de ocho estrategias poco correladas, tiene colas poco asimétricas.
Cuando la asimetría está en activos que la varianza no penaliza lo suficiente, las dos medidas se separan más; con
estos datos, la diferencia práctica está en otra parte.

## Lo que el CVaR histórico no ve

La diferencia aparece cuando se busca rendimiento. El experimento: con los datos de junio de 2009 a enero de 2018 (un
periodo sin crisis grandes en los datos), se elige la cartera de máximo rendimiento medio con un 5 % de volatilidad
(media-varianza) y la de máximo rendimiento con el **mismo CVaR al 99 %** que esa cartera (media-CVaR). Las dos tienen
el mismo riesgo de cola en la muestra. Después se miden de febrero de 2018 a 2025, con el Volmageddon y marzo de 2020
dentro.

```python
dentro = est.loc["2009-06":"2018-01"].values
fuera = est.loc["2018-02":].values
mu, Sig = dentro.mean(0), np.cov(dentro.T)

# media-varianza: máximo rendimiento con un 5 % de volatilidad
w = cp.Variable(n)
cp.Problem(cp.Maximize(mu @ w),
           [cp.quad_form(w, Sig) * 252 <= 0.05**2,
            cp.sum(w) == 1, w >= 0]).solve(solver="CLARABEL")
w_mv = np.clip(w.value, 0, None)

# media-CVaR: máximo rendimiento con el mismo CVaR 99 % en muestra
tope = cvar_hist(dentro @ w_mv, 0.99)
w = cp.Variable(n)
expr, extra = cvar_cp(w, dentro, 0.99)
cp.Problem(cp.Maximize(mu @ w),
           [expr <= tope, cp.sum(w) == 1, w >= 0] + extra
           ).solve(solver="HIGHS")
w_cv = np.clip(w.value, 0, None)

print(pd.DataFrame({"media_var": w_mv, "media_cvar": w_cv},
                   index=est.columns).round(2))
filas = {}
for nombre, wt in [("media_var", w_mv), ("media_cvar", w_cv)]:
    for tramo, X in [("dentro", dentro), ("fuera", fuera)]:
        m = metricas(wt, X)
        m["peor_dia_%"] = 100 * (X @ wt).min()
        filas[(nombre, tramo)] = m
pd.DataFrame(filas).T.round(2)
```
```output
                   media_var  media_cvar
tendencia_futuros       0.05        0.00
carry_futuros           0.00        0.00
carry_divisas           0.00        0.00
venta_puts              0.22        0.32
pares                   0.07        0.00
momentum_acciones       0.24        0.16
valor_acciones          0.14        0.20
venta_volatilidad       0.10        0.07
reversion_corto         0.19        0.26
                   vol_%  cvar95_%  cvar99_%  max_caida_%  sharpe  peor_dia_%
media_var  dentro   5.00      0.68      1.05        -7.04    1.31       -1.68
           fuera    5.15      0.72      1.26       -11.22    0.90       -2.37
media_cvar dentro   5.56      0.74      1.04        -8.47    1.23       -1.57
           fuera    5.54      0.78      1.30       -13.51    0.79       -2.61
```

Dentro de la muestra, la media-CVaR cumple lo que promete: el mismo CVaR al 99 % (1,04 % frente a 1,05 %), un peor
día menos malo (−1,57 % frente a −1,68 %) y algo más de volatilidad, que no le importa. Para conseguirlo sube la venta
de puts del 22 % al 32 %. Fuera de muestra, la media-CVaR pierde en todo: CVaR al 99 % del 1,30 %, caída máxima del
13,5 % frente al 11,2 % y un peor día del −2,61 %. Los peores días cuentan la historia:

```python
fechas_f = est.loc["2018-02":].index
pf = pd.DataFrame({"media_var": fuera @ w_mv,
                   "media_cvar": fuera @ w_cv,
                   "venta_puts": est.loc["2018-02":, "venta_puts"]},
                  index=fechas_f)
sesgo = est.loc["2009-06":"2018-01", "venta_puts"].skew()
print(f"asimetría de venta_puts en 2009-2018: {sesgo:.2f}")
(100 * pf.nsmallest(4, "media_cvar")).round(2)
```
```output
asimetría de venta_puts en 2009-2018: 0.32
            media_var  media_cvar  venta_puts
fecha
2020-03-16      -2.37       -2.61       -6.55
2020-03-12      -1.15       -1.95       -5.81
2018-10-03      -1.42       -1.89       -4.90
2018-02-05      -2.15       -1.81       -2.66
```

Marzo de 2020: la venta de puts pierde un 6,55 % el día 16 y un 5,81 % el 12. La media-CVaR, con más venta de puts,
lo sufre más. En la muestra de 2009-2018 la venta de puts había tenido una cola moderada (asimetría ligeramente
positiva en ese tramo), y el CVaR histórico solo puede ver las pérdidas que ocurrieron. El optimizador hizo lo que se
le pidió: buscó el activo cuyo riesgo de cola **medido** era bajo para su rendimiento, que es justo el que tenía el
riesgo escondido. Es el mismo fenómeno de «maximizar el error» de la lección 4.1.1, ahora en las colas.

<aside class="callout" data-kind="riesgo">

Una cartera optimizada con CVaR histórico no está protegida contra una crisis que no esté en la muestra. Si el riesgo
de una estrategia es un evento raro (vender seguros, carry, liquidez), la ventana tiene que contenerlo o hay que
añadirlo con escenarios de estrés (lección 4.2.4). Optimizar contra la cola medida en un periodo tranquilo empuja la
cartera hacia el riesgo que no se ve.

</aside>

## Drawdown: CDaR y caída máxima

La **caída** (*drawdown*) en $t$ es la distancia entre el máximo acumulado hasta $t$ y el valor en $t$. La caída
máxima es la mayor de todas; el **CDaR** (*conditional drawdown at risk*, Chekhlov, Uryasev y Zabarankin, 2005) es la
media del peor $(1-\beta)$ de las caídas, el equivalente del CVaR sobre la trayectoria.

Para que sea lineal se usa la suma acumulada **sin capitalizar** de los rendimientos, $c_t = \sum_{s \le t} r_s'w$,
lineal en $w$. El máximo acumulado se modela con un techo $h_t$: $h_t \ge c_t$, $h_t \ge h_{t-1}$, $h_0 \ge 0$. La
caída es $h_t - c_t$ (el techo mínimo compatible con las restricciones es exactamente el máximo acumulado, y el
optimizador no tiene incentivo para subirlo). Con eso, el CDaR se linealiza con el mismo truco de Rockafellar-Uryasev
y la caída máxima es el máximo de $h_t - c_t$. Con rendimientos diarios pequeños, la suma sin capitalizar y el
logaritmo del valor capitalizado se parecen; las métricas de la tabla se calculan sobre el valor capitalizado.

El experimento: optimizar con 2006-2015 y medir en 2016-2025.

```python
def caidas_cp(w, R):
    """Caídas de la suma acumulada (sin capitalizar) como restricciones
    lineales: techo ≥ suma y techo no decreciente."""
    c = cp.cumsum(R @ w)
    techo = cp.Variable(len(R))
    restr = [techo >= c, techo[1:] >= techo[:-1], techo[0] >= 0]
    return techo - c, restr

def min_cdar(R, beta=0.95):
    w, alfa, z = cp.Variable(R.shape[1]), cp.Variable(), \
        cp.Variable(len(R))
    dd, restr = caidas_cp(w, R)
    obj = alfa + cp.sum(z) / ((1 - beta) * len(R))
    cp.Problem(cp.Minimize(obj),
               restr + [z >= dd - alfa, z >= 0, cp.sum(w) == 1,
                        w >= 0]).solve(solver="HIGHS")
    return np.clip(w.value, 0, None)

def min_maxdd(R):
    w = cp.Variable(R.shape[1])
    dd, restr = caidas_cp(w, R)
    cp.Problem(cp.Minimize(cp.max(dd)),
               restr + [cp.sum(w) == 1, w >= 0]).solve(solver="HIGHS")
    return np.clip(w.value, 0, None)

ant, post = est.loc[:"2015"].values, est.loc["2016":].values
cart = {"1/N": w_1n, "min_var": min_var(ant),
        "min_cvar99": min_cvar(ant, 0.99), "min_cdar95": min_cdar(ant),
        "min_max_caida": min_maxdd(ant)}
filas = {}
for k, w in cart.items():
    a, b = metricas(w, ant), metricas(w, post)
    filas[k] = {"caida_dentro_%": a["max_caida_%"],
                "caida_fuera_%": b["max_caida_%"],
                "sharpe_dentro": a["sharpe"],
                "sharpe_fuera": b["sharpe"],
                "n_ef": 1 / (w ** 2).sum()}
pd.DataFrame(filas).T.round(2)
```
```output
               caida_dentro_%  caida_fuera_%  sharpe_dentro  sharpe_fuera  n_ef
1/N                    -14.59          -9.67           0.50          1.18  9.00
min_var                 -8.52          -5.03           0.68          1.30  6.57
min_cvar99              -9.85          -5.19           0.63          1.34  6.29
min_cdar95              -5.70          -9.43           0.89          0.99  5.49
min_max_caida           -5.33          -8.54           0.88          0.97  5.23
```

La figura de la cabecera pone estas caídas una al lado de otra. Las dos carteras que optimizan la caída consiguen la
menor dentro de la muestra (5,70 % y 5,33 %, frente al 8,52 % de la mínima varianza) y un Sharpe más alto (0,89 y
0,88). Fuera, sus caídas casi se duplican (9,43 % y 8,54 %), muy por encima de la mínima varianza (5,03 %) y del
mínimo CVaR al 99 % (5,19 %), y sus Sharpes (0,99 y 0,97) son los dos más bajos de las cinco. La 1/N cae más dentro que ninguna (14,59 %) y
fuera un 9,67 %, apenas más que el mínimo CDaR.

La explicación es estadística. La caída máxima de diez años la deciden dos o tres episodios. Minimizarla es elegir la
combinación que mejor esquivó **esos** episodios, y con nueve estrategias hay combinaciones que los esquivan en parte
por azar. La varianza y el CVaR usan todos los días o todo el 5 % peor de los días: miles o cientos de observaciones
frente a unas pocas trayectorias de caída. Es el problema de las pruebas múltiples (lección 2.1.8) dentro de un
optimizador. La caída sirve como **restricción de control** (lección 4.2.5) o como métrica de evaluación, con
cuidado; como objetivo de optimización con pocos años de datos, sobreajusta.

## Costes y rotación

Todo lo anterior supone que rebalancear es gratis. No lo es, y en una cartera grande los costes de ejecución pueden
comerse el rendimiento de una estrategia de bajo riesgo. Un modelo razonable de coste, en fracción de la cartera,
para mover $\Delta w_i$ del activo $i$ con una cartera de valor $V$:

$$
\text{coste}(\Delta w) = \sum_i \frac{h_i}{2}\,|\Delta w_i| + \sum_i Y\,\sigma_i \sqrt{\frac{V}{ADV_i}}\;|\Delta w_i|^{3/2}.
$$

El primer término es la media horquilla $h_i/2$. El segundo es el **impacto de la raíz cuadrada**: el coste por
unidad negociada crece con la raíz de la fracción del volumen diario que se negocia, $Y\sigma\sqrt{Q/ADV}$, y
multiplicado por lo negociado da un exponente 3/2 (lecciones 2.4.7 y 4.3.1). Los dos términos son convexos, así que
caben en cvxpy: `cp.abs` y `cp.power(·, 1.5)`. `riesgo4_liquidez.csv` da, para cada activo del universo como ETF
ficticio, el volumen medio diario, la horquilla, la volatilidad diaria y el coeficiente $Y$.

```python
precios = pd.read_parquet("universo4_precios.parquet")
precios = precios.set_index("fecha")
fact = pd.read_parquet("universo4_factores_diarios.parquet")
fact = fact.set_index("fecha")
r = precios.pct_change()
r.iloc[0] = precios.iloc[0] / 100 - 1
cols = [c for c in precios if c not in ("MONET", "RF_T13")]
r = r[cols]
exceso = r.sub(fact["rf"], axis=0)

liq = pd.read_csv("riesgo4_liquidez.csv", index_col="activo").loc[cols]
CARTERA = 5e9                                   # USD
a = liq["horquilla_pb"].values / 2 / 1e4        # media horquilla
b = (liq["impacto_y"] * liq["vol_diaria"]
     * np.sqrt(CARTERA / liq["adv_usd"])).values

def coste(dw):
    """Coste en fracción de la cartera: horquilla + raíz cuadrada."""
    dw = np.abs(dw)
    return a @ dw + b @ dw**1.5

ej = ["RV_EEUU", "RF_T37", "MP_ENE", "INFRA"]
pb = [1e4 * coste(np.where(np.array(cols) == c, 0.01, 0)) / 0.01
      for c in ej]
pd.DataFrame({"adv_musd": liq.loc[ej, "adv_usd"] / 1e6,
              "dias_adv": 0.01 * CARTERA / liq.loc[ej, "adv_usd"],
              "coste_pb_de_lo_movido": pb}, index=ej).round(2)
```
```output
         adv_musd  dias_adv  coste_pb_de_lo_movido
RV_EEUU   7302.01      0.01                   5.94
RF_T37     798.84      0.06                   4.66
MP_ENE      99.76      0.50                  88.17
INFRA       23.48      2.13                  99.61
```

Con 5.000 millones de USD, mover un 1 % de la cartera en el ETF de la bolsa de EE. UU. es una centésima de su volumen
diario y cuesta 5,94 pb de lo movido; en el bono a 3-7 años, 4,66 pb. En energía son medio día de volumen y 88 pb; en
infraestructuras, más de dos días de volumen y casi 100 pb. Un optimizador sin costes trata igual un punto de cada uno.

Hay dos formas de meter los costes en el problema:

- **Penalizar el coste en el objetivo**: minimizar $\text{CVaR}(w) + \kappa \cdot \text{coste}(w - w_0)$, con $w_0$ la
  cartera actual. κ traduce costes a unidades de riesgo: con κ = 1, un punto básico de coste pesa lo mismo que un punto
  básico de CVaR diario.
- **Limitar la rotación**: añadir $\sum_i |w_i - w_{0,i}| \le \tau$ y no tocar el objetivo.

```mermaid
flowchart TB
  P[Cartera actual w0] --> M{"¿Mejora > coste?"}
  M -- sí --> T[Operar]
  M -- no --> N[No operar]
  T --> F[Nueva cartera]
  N --> F
```

Con el coste en el objetivo aparece una **zona sin operaciones**: el término $|\Delta w|$ tiene una pendiente
distinta de cero en $\Delta w = 0$, así que solo compensa mover un activo si la mejora marginal de riesgo supera su
coste marginal, y la zona es más ancha cuanto más caro es el activo. Es lo que hace, de forma explícita, el
rebalanceo por umbrales de la lección 4.1.7.

El experimento: mínimo CVaR al 95 % de los 36 activos con riesgo del universo (sin el monetario ni la letra a 1-3
años, como en la lección 4.1.4), reoptimizado el primer día de cada mes con el año anterior, con 5.000 millones y
cuatro valores de κ, más un tope de rotación del 5 % al mes sin coste en el objetivo. Los costes se cobran el día del
cambio. El problema se construye una vez por variante con parámetros de cvxpy (`cp.Parameter`) para los escenarios y
la cartera actual; cada mes solo cambian sus valores y cvxpy reutiliza la compilación. La columna `no_optimos` cuenta
las veces que el solver no terminó con estado `optimal`.

```python
V = 252
fechas = exceso.index
primeras = pd.Series(fechas, fechas).groupby(
    fechas.to_period("M")).first()
rebal = [d for d in primeras if fechas.get_loc(d) >= V]
m = len(cols)

def problema(kappa, tope=None):
    """Mínimo CVaR 95 % + kappa · coste de pasar de w0 a w."""
    Rp, w0 = cp.Parameter((V, m)), cp.Parameter(m)
    w = cp.Variable(m)
    expr, extra = cvar_cp(w, Rp, 0.95)
    dw = w - w0
    c = a @ cp.abs(dw) + b @ cp.power(cp.abs(dw), 1.5)
    restr = [cp.sum(w) == 1, w >= 0] + extra
    if tope is not None:
        restr.append(cp.norm1(dw) <= tope)
    return cp.Problem(cp.Minimize(expr + kappa * c), restr), Rp, w0, w

def resolver(prob, Rp, w0, w, X, previo):
    Rp.value, w0.value = X, previo
    prob.solve(solver="CLARABEL")
    wn = np.clip(w.value, 0, None)
    return wn / wn.sum(), prob.status != "optimal"

inicial = problema(0)                   # la primera cartera, sin costes

def rebalanceo(kappa, tope=None):
    prob = problema(kappa, tope)
    previo, trozos, rot, gasto, malos = None, [], [], [], 0
    for j, d in enumerate(rebal):
        i = fechas.get_loc(d)
        X = exceso.iloc[i - V : i].values
        if previo is None:
            previo, _ = resolver(*inicial, X, np.full(m, 1 / m))
        wn, malo = resolver(*prob, X, previo)
        malos += malo
        dw = wn - previo
        rot.append(np.abs(dw).sum())
        gasto.append(coste(dw))
        fin = rebal[j + 1] if j + 1 < len(rebal) else None
        bl = r.loc[d:fin]
        bl = bl.iloc[:-1] if fin is not None else bl
        g = (1 + bl).cumprod().values
        valor = g @ wn
        rr = valor / np.r_[1, valor[:-1]] - 1
        rr[0] -= gasto[-1]               # el coste, el día del cambio
        trozos.append(pd.Series(rr, bl.index))
        previo = g[-1] * wn / valor[-1]
    p = pd.concat(trozos)
    p = p - fact["rf"].loc[p.index]
    return {"exceso_%": 25200 * p.mean(),
            "vol_%": 100 * np.sqrt(252) * p.std(),
            "cvar95_%": 100 * cvar_hist(p, 0.95),
            "rotacion_%": 1200 * np.mean(rot),
            "coste_pb": 12e4 * np.mean(gasto), "no_optimos": malos}

resultado = {f"kappa={k}": rebalanceo(k) for k in (0, 0.3, 1, 2)}
resultado["tope 5 %"] = rebalanceo(0, tope=0.05)
print(rebal[0].date(), "a", fechas[-1].date())
pd.DataFrame(resultado).T.round(2)
```
```output
2007-02-01 a 2025-12-31
           exceso_%  vol_%  cvar95_%  rotacion_%  coste_pb  no_optimos
kappa=0        0.49   3.41      0.50      164.07     55.16         0.0
kappa=0.3      0.56   3.35      0.49       56.79     11.85         0.0
kappa=1        0.48   3.28      0.49       29.12      3.63         0.0
kappa=2        0.55   3.26      0.48       16.40      1.41         0.0
tope 5 %       0.47   3.35      0.49       58.40     15.48         0.0
```

*Fig. — Mínimo CVaR mensual en 36 activos con 5.000 millones de USD: rotación y coste anual según la penalización de costes, y con un tope de rotación (2007-2025).* Cinco puntos. Sin penalización (kappa 0), 164 % de rotación anual y 55 puntos básicos de coste al año. Kappa 0,3: 57 % y 11,9 pb. Kappa 1 (destacado): 29 % y 3,6 pb. Kappa 2: 16 % y 1,4 pb. Con un tope del 5 % de rotación al mes y sin coste en el objetivo: 58 % y 15,5 pb, más caro que kappa 0,3 con una rotación parecida. El CVaR fuera de muestra se mantiene entre el 0,48 % y el 0,50 % en los cinco casos.

Sin costes en el objetivo (κ = 0), el mínimo CVaR mensual rota un 164 % al año y paga 55 pb anuales. La solución de
un programa lineal está en un vértice: un cambio pequeño en los escenarios la hace saltar a otro vértice, aunque el
CVaR apenas cambie. Con κ = 0,3 la rotación baja al 57 % y el coste a 11,9 pb; con κ = 1, al 29 % y 3,6 pb; con κ = 2,
al 16 % y 1,4 pb. El CVaR fuera de muestra no empeora: de 0,50 % a 0,48 %. La volatilidad, tampoco (de 3,41 % a
3,26 %). El solver termina en `optimal` en todos los meses de todas las variantes. El exceso neto se mueve entre el
0,47 % y el 0,56 % sin un orden claro: la diferencia de costes, de hasta 54 pb al año, es del mismo tamaño que el
ruido del rendimiento de estas carteras en 19 años, y no se distingue en un backtest. Que no se vea no quiere decir
que no se pague: el coste es seguro y la diferencia de rendimiento, no.

El tope del 5 % al mes da una rotación parecida a κ = 0,3 (58 %) pero cuesta más (15,5 pb): limita cuánto se mueve,
no qué se mueve, y gasta su presupuesto en los activos caros si mejoran el CVaR. La penalización sabe lo que cuesta
cada activo.

¿Qué κ elegir? No hay uno correcto. Lo honesto es fijarlo antes de mirar el resultado fuera de muestra, o elegirlo
con validación cruzada temporal (lección 2.5.2), y no buscar el que maximiza el Sharpe del backtest. Boyd y otros
(2017) desarrollan este enfoque para varios periodos: costes, impacto y restricciones en un mismo problema convexo
que se resuelve en cada rebalanceo.

## Errores típicos

- **Optimizar la medida que se va a enseñar.** La caída máxima y el CVaR de la muestra salen bien porque se
  optimizaron sobre ella; fuera de muestra, el sobreajuste aparece.
- **CVaR con una ventana sin crisis.** Solo ve las colas que hubo. Hay que comprobar qué episodios contiene la
  ventana y completar con estrés.
- **Ignorar el estado del solver.** Un `optimal_inaccurate` de cvxpy con cono de potencia es frecuente cuando la
  solución está en la zona sin operaciones ($\Delta w = 0$ es el vértice del cono). Hay que comprobar `prob.status` en cada
  rebalanceo y no usar pesos de un problema que no convergió.
- **Costes como tope de rotación.** Un tope igual para todos los activos no distingue el ETF líquido del ilíquido.
- **Rendimientos logarítmicos en la cartera.** La suma de pesos por rendimientos solo es exacta con rendimientos
  simples; con logarítmicos, CVaR y Sharpe de la cartera salen sesgados.

## Ejercicios

1. Repite el mínimo CVaR de las nueve estrategias al 99 % con `min_cvar` y con skfolio (`cvar_beta=0.99`), compara
   los pesos y di qué estrategia gana más peso al pasar del 95 % al 99 %.

**Solución.**

```python
w99 = min_cvar(R, 0.99)
skf99 = MeanRisk(objective_function=ObjectiveFunction.MINIMIZE_RISK,
                 risk_measure=RiskMeasure.CVAR,
                 cvar_beta=0.99).fit(est)
dif = np.abs(skf99.weights_ - w99).max()
print(f"diferencia con skfolio: {dif:.1e}")
(pd.Series(w99 - min_cvar(R, 0.95), est.columns)
 .sort_values().round(3))
```
```output
diferencia con skfolio: 4.8e-09
pares               -0.061
carry_futuros       -0.036
valor_acciones      -0.003
venta_volatilidad    0.000
tendencia_futuros    0.006
momentum_acciones    0.015
venta_puts           0.017
reversion_corto      0.025
carry_divisas        0.036
dtype: float64
```

Las dos implementaciones coinciden (diferencia de $5 \cdot 10^{-9}$). Al 99 % el CVaR solo mira el 1 % peor de los
días, unos 50 escenarios, y la cartera se desplaza hacia las estrategias que mejor se portaron en esos días concretos:
gana peso el carry de divisas (+3,6 puntos) y la reversión a corto (+2,5), y lo pierden los pares (−6,1) y el carry de
futuros (−3,6). Con tan pocos escenarios, el mínimo CVaR al
99 % es más sensible a la muestra que al 95 %.

2. Con una cartera de 500 millones en lugar de 5.000, ¿cuánto cuesta mover un 1 % en cada uno de los cuatro activos
   del ejemplo? ¿Qué término del coste cambia y por qué no cambia en proporción?

**Solución.**

```python
b_500 = (liq["impacto_y"] * liq["vol_diaria"]
         * np.sqrt(5e8 / liq["adv_usd"])).values
def coste_500(dw):
    dw = np.abs(dw)
    return a @ dw + b_500 @ dw**1.5

pd.Series([1e4 * coste_500(np.where(np.array(cols) == c, 0.01, 0))
           / 0.01 for c in ej], index=ej).round(2)
```
```output
RV_EEUU     2.18
RF_T37      2.15
MP_ENE     30.63
INFRA      39.20
dtype: float64
```

Solo cambia el impacto: con una cartera diez veces menor, el 1 % es una décima parte de lo que era en volumen diario y
el impacto por unidad se divide por $\sqrt{10} \approx 3,16$. La media horquilla no depende del tamaño. Por eso ningún
coste baja a la décima parte: `RV_EEUU` pasa de 5,94 a 2,18 pb e `INFRA` de 99,61 a 39,20 pb. Los ilíquidos siguen
siendo caros: mover un 1 % en infraestructuras cuesta 18 veces más que en la bolsa de EE. UU. La capacidad de una estrategia (lección 2.2.3) es justo esto:
el tamaño a partir del cual el impacto se come el rendimiento.

3. Sin código: un compañero propone elegir cada mes la cartera de mínima caída máxima de los últimos tres años, «porque
   es lo que más le importa al cliente». Explica con los resultados de la lección qué esperarías y qué propondrías en
   su lugar.

**Solución.**

La caída máxima de tres años depende de uno o dos episodios. La cartera que la minimiza es la que mejor esquivó esos
episodios, en parte por azar, y cambiará mucho de un mes a otro (cada mes entra y sale un episodio de la ventana), así
que rotará y pagará costes. En la lección, la mínima caída máxima de 2006-2015 cayó un 5,3 % en muestra y un 8,5 %
después, más que la mínima varianza (5,0 %). Mejor: optimizar una medida que use muchos datos (varianza o CVaR al
95 %), con costes en el objetivo, y controlar la caída con una regla explícita y evaluada aparte, como reducir la
exposición o detener la cartera al superar un umbral (lección 4.2.5), o con escenarios de estrés (4.2.4). La caída que
le importa al cliente se gestiona, no se optimiza en la muestra.

## Autoevaluación

1. En la función de Rockafellar-Uryasev F(α) = α + E[max(−r − α, 0)] / (1 − β), ¿qué son el α que la minimiza y su valor mínimo?

   - A. El VaR al nivel β y el CVaR al nivel β
   - B. La media y la varianza de las pérdidas
   - C. El CVaR y el VaR, en ese orden
   - D. Dos parámetros que hay que fijar a mano

   *Respuesta A: El VaR al nivel β y el CVaR al nivel β.* Con la cartera 1/N de las nueve estrategias, el α óptimo sale 0,00430 y el cuantil del 95 % de las pérdidas 0,00429; el mínimo de F vale 0,00622, igual que la media de las pérdidas del 5 % peor de los días. Minimizar F en α y en los pesos a la vez minimiza el CVaR sin calcular cuantiles.

2. ¿Por qué el mínimo CVaR de las nueve estrategias se parece tanto a la mínima varianza?

   - A. Porque el CVaR y la varianza son la misma medida
   - B. Porque las dos descartan casi por completo la estrategia de cola gruesa (venta de volatilidad) y, una vez fuera, la cartera diversificada tiene colas poco asimétricas
   - C. Por un error del solver
   - D. Porque el CVaR solo usa el 5 % de los datos

   *Respuesta B: Porque las dos descartan casi por completo la estrategia de cola gruesa (venta de volatilidad) y, una vez fuera, la cartera diversificada tiene colas poco asimétricas.* La venta de volatilidad tiene una asimetría de −2,76 y un 16,4 % de volatilidad: las dos medidas le dan peso cero. Con el resto, la cartera tiene un CVaR al 95 % de 0,46-0,47 % en los dos casos y la diferencia de pesos es de pocos puntos.

3. La cartera de mínima caída máxima de 2006-2015 cae un 5,3 % en esa muestra. ¿Qué pasa en 2016-2025?

   - A. Cae menos, porque el optimizador aprendió a evitar las caídas
   - B. Lo mismo, un 5,3 %
   - C. Cae un 8,5 %, más que la mínima varianza (5,0 %): la caída máxima de la muestra depende de unos pocos episodios y el optimizador se ajusta a ellos
   - D. No se puede calcular fuera de muestra

   *Respuesta C: Cae un 8,5 %, más que la mínima varianza (5,0 %): la caída máxima de la muestra depende de unos pocos episodios y el optimizador se ajusta a ellos.* La caída máxima es una estadística de una sola trayectoria. Minimizarla elige la combinación que mejor esquivó las caídas concretas de 2006-2015, en parte por azar. Fuera de muestra, la mínima varianza, que usa todos los días, cae menos.

4. Con 5.000 millones de USD, mover un 1 % de la cartera en `INFRA` cuesta unos 100 pb de lo movido y en `RV_EEUU` unos 6. ¿Por qué tanta diferencia?

   - A. Por la comisión del broker
   - B. Porque `INFRA` es más volátil
   - C. Por los impuestos
   - D. Porque el 1 % de la cartera son 2,13 días de volumen de `INFRA` y 0,01 de `RV_EEUU`: el impacto crece con la raíz cuadrada de esa fracción, y la horquilla de `INFRA` es además 26 veces mayor

   *Respuesta D: Porque el 1 % de la cartera son 2,13 días de volumen de `INFRA` y 0,01 de `RV_EEUU`: el impacto crece con la raíz cuadrada de esa fracción, y la horquilla de `INFRA` es además 26 veces mayor.* El modelo de coste es media horquilla más Y·σ·√(Q/ADV). `INFRA` tiene un volumen medio de 23,5 millones de USD al día y una horquilla de 22,5 pb; `RV_EEUU`, 7.302 millones y 0,87 pb.

5. En el mínimo CVaR mensual, un tope de rotación del 5 % al mes da un 58 % de rotación anual y 15,5 pb de coste; una penalización de costes con κ = 0,3 da un 57 % y 11,9 pb. ¿Por qué la penalización sale más barata con la misma rotación?

   - A. Por azar
   - B. Porque el tope limita cuánto se mueve pero no qué: gasta el presupuesto en activos caros si mejoran el CVaR; la penalización sabe lo que cuesta cada activo y mueve los baratos
   - C. Porque el tope no es convexo
   - D. Porque la penalización reduce el CVaR

   *Respuesta B: Porque el tope limita cuánto se mueve pero no qué: gasta el presupuesto en activos caros si mejoran el CVaR; la penalización sabe lo que cuesta cada activo y mueve los baratos.* El tope trata igual un punto de `RV_EEUU` que uno de `INFRA`. La penalización lleva el coste de cada activo dentro del objetivo: solo compensa operar si la mejora de riesgo supera el coste marginal, y eso crea una zona sin operaciones distinta para cada activo.

## Ideas clave

- El CVaR al nivel β es la pérdida media en el peor (1 − β) de los días. Es coherente y convexo; el VaR no.
- Rockafellar-Uryasev convierte el mínimo CVaR en un programa lineal con una variable por escenario; cvxpy, skfolio y Riskfolio-Lib dan los mismos pesos (diferencias por debajo de una milmillonésima).
- El CVaR histórico solo ve las colas que hubo en la muestra: la media-CVaR de 2009-2018 cargó más venta de puts que la media-varianza y perdió más en marzo de 2020.
- CDaR y caída máxima también son lineales, pero optimizar una estadística de una sola trayectoria sobreajusta: caída del 5,3 % dentro y del 8,5 % fuera, peor que la mínima varianza.
- Con costes de horquilla e impacto de raíz cuadrada en el objetivo, la rotación del mínimo CVaR mensual baja del 164 % al 29 % al año y el coste de 55 a 3,6 pb, con el mismo CVaR fuera de muestra.

## Recursos

### Research

- [Modelos Cuantitativos y Ejecución Algorítmica](https://pythonparatrading.com/modelos-cuantitativos-ejecucion-algoritmica/) · Research-0009 · 11 min

### Documentación

- [cvxpy: funciones atómicas (power, abs, cumsum, max)](https://www.cvxpy.org/tutorial/functions/index.html) · cvxpy.org
- [skfolio: MeanRisk](https://skfolio.org/generated/skfolio.optimization.MeanRisk.html) · skfolio.org
- [Riskfolio-Lib: Portfolio](https://riskfolio-lib.readthedocs.io/en/latest/riskfoliolib/portfolio.html) · riskfolio-lib.readthedocs.io

### Libros

- [Rockafellar, R. T. y Uryasev, S. (2000): Optimization of Conditional Value-at-Risk. Journal of Risk 2(3), 21-41](https://doi.org/10.21314/JOR.2000.038) · doi.org
- [Rockafellar, R. T. y Uryasev, S. (2002): Conditional value-at-risk for general loss distributions. Journal of Banking & Finance 26(7), 1443-1471](https://doi.org/10.1016/S0378-4266(02)00271-6) · doi.org
- [Chekhlov, A., Uryasev, S. y Zabarankin, M. (2005): Drawdown Measure in Portfolio Optimization. International Journal of Theoretical and Applied Finance 8(1), 13-58](https://doi.org/10.1142/S0219024905002767) · doi.org
- [Artzner, P., Delbaen, F., Eber, J.-M. y Heath, D. (1999): Coherent Measures of Risk. Mathematical Finance 9(3), 203-228](https://doi.org/10.1111/1467-9965.00068) · doi.org
- [Boyd, S. y otros (2017): Multi-Period Trading via Convex Optimization. Foundations and Trends in Optimization 3(1), 1-76](https://doi.org/10.1561/2400000023) · doi.org

### Datos

- [Datos del nivel 4: rendimientos diarios de diez estrategias, 2006-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-4/estrategias4_diarias.parquet) · Descarga · estrategias4_diarias.parquet
- [Datos del nivel 4: liquidez de los 38 activos (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/riesgo4_liquidez.csv) · Descarga · riesgo4_liquidez.csv

Lección anterior: [4.1.4 · Risk parity, mínima varianza, HRP y HERC](https://pythonparatrading.com/curso-python-trading/nivel-4/risk-parity-minima-varianza-hrp-y-herc/)

Lección siguiente: [4.1.6 · Modelos factoriales: CAPM, Fama-French, PCA y atribución](https://pythonparatrading.com/curso-python-trading/nivel-4/modelos-factoriales-capm-fama-french-y-pca/)
