Saltar a contenido

Q06 · Risk parity ERC con covarianza regularizada

Campo Valor
Familia Asignación por riesgo: cartera long-only en la que cada activo aporta la misma parte del riesgo total; no hay señal direccional
Universo / panel Panel de retornos multiactivo con precios ejecutables
Datos Retornos de las window barras anteriores a cada rebalanceo
Features shrinkage_covariance (Ledoit–Wolf), erc_weights
Parámetros y defaults window = 252, rebalance_bars = 21, solver_tolerance = 1e-8, max_iter = 500
Rejilla window ∈ {63, 126, 252}, rebalance_bars ∈ {5, 21, 63} → 9 candidatos
Coste esperado O(B·W·S²) en covarianzas; solve iterativo
Estado Especificada, no implementada como estrategia (specified_not_implemented, hito H3)
Motores implicados Ya corren en la plataforma: finaz_risk.covarianza_ledoit_wolf (AG-022), finaz_allocation.resolver_erc (AG-024, ERC OPTIMAL en G5), finaz_discrete.proyectar_cpsat (AG-025, CP-SAT OPTIMAL en G6), finaz_constraints.aprobar (AG-023)

Estado

Q06 no está implementada como estrategia del banco de backtesting: el catálogo la marca specified_not_implemented y la API de backtesting la declara UNSUPPORTED con NOT_IMPLEMENTED_YET. Sí existen y corren los motores de sus piezas: Ledoit-Wolf, ERC, mínima varianza y CP-SAT pasaron los gates G5/G6 en la imagen opt (job g5-opt-001, PASS). Lo que falta es la política que los encadene en cada rebalanceo y la paridad contra el ledger. Las cifras de "ejemplo" de esta ficha son del fixture de la especificación o del job G5 real, y así se indica en cada caso.

Fuentes: catalog/strategy_catalog.json, docs/estrategias/Q06.md (especificación y fixture), packages/finaz_risk, packages/finaz_allocation, packages/finaz_discrete, packages/finaz_constraints, qa_reports/v2/runs/2026-09-20/g5-opt-001.result.json, docs/v2/MOTORES.md (AG-022…AG-025).

Qué vas a aprender

  • Qué es la contribución al riesgo de un activo y qué significa igualarlas (ERC, equal risk contribution).
  • Por qué se regulariza la covarianza con Ledoit–Wolf antes de optimizar.
  • La solución cerrada para dos activos y por qué no depende de la correlación.
  • Cómo la plataforma resuelve ERC, mínima varianza y la conversión a lotes enteros con CP-SAT, con las cifras reales del job G5.
  • La separación de responsabilidades: el optimizador propone, el validador aprueba.

La idea en una frase

Repartir el capital de forma que cada activo contribuya lo mismo al riesgo de la cartera, usando una covarianza estimada con encogimiento para que la optimización no amplifique el ruido.

Intuición y evidencia académica

Intuición. Una cartera 60/40 de acciones y bonos parece diversificada en capital, pero casi todo su riesgo viene de las acciones. La paridad de riesgo invierte la pregunta: en lugar de cuánto dinero en cada activo, cuánto riesgo. Los activos volátiles pesan menos; los tranquilos, más. La literatura de risk parity y de carteras de contribución al riesgo igual la estudia como alternativa a la optimización media-varianza, que depende de estimar retornos esperados (muy ruidosos).

Por qué Ledoit–Wolf. La covarianza muestral con pocas observaciones por activo es ruidosa y a menudo está mal condicionada. El encogimiento la mezcla con un objetivo simple (mu · I), con una intensidad calculada a partir de los datos.

Cuándo falla.

  • Correlaciones que saltan en crisis: la covarianza de 252 barras describe el pasado, no el choque.
  • Activos de muy baja volatilidad reciben pesos enormes; sin apalancamiento la cartera queda dominada por ellos.
  • No hay señal: ERC no predice nada; solo reparte riesgo. Su resultado depende de los activos elegidos.

El modelo matemático

Covarianza regularizada con los W retornos anteriores al rebalanceo:

S      = covarianza muestral
Sigma* = (1 − delta) · S + delta · mu · I       delta = intensidad de Ledoit–Wolf, mu = traza(S)/n

Contribución al riesgo del activo i para pesos w:

sigma(w) = sqrt(w' Sigma w)
RC_i     = w_i · (Sigma w)_i / sigma(w)          sum_i RC_i = sigma(w)
relativa = w_i · (Sigma w)_i / (w' Sigma w)      ERC ⇔ relativa = 1/S para todo i

Problema ERC (long-only), en la forma convexa del catálogo:

min_x   0.5 · x' Sigma x − sum_i b_i · ln(x_i)      con x > 0,  b_i = 1/S
w       = x / sum(x)
gradiente: Sigma x − b / x      (condición KKT: x_i (Sigma x)_i = b_i)

Solución cerrada para S = 2, sea cual sea la correlación:

w_1 = sigma_2 / (sigma_1 + sigma_2)        w_2 = sigma_1 / (sigma_1 + sigma_2)

Con dos activos solo hay un término de covarianza y se reparte igual entre las dos contribuciones; con S >= 3 la correlación sí importa.

Pseudocódigo causal

para cada rebalanceo t (cada rebalance_bars, tras el watermark del panel):
    R_W  = retornos [t − W + 1, t]                   solo pasado
    Sigma = Ledoit–Wolf(R_W)                         cachear solo en fechas de rebalanceo
    x, estado = resolver ERC (tolerancia, max_iter)
    si estado != OPTIMAL: NO etiquetar inverse-vol como ERC; registrar y mantener
    w = x / sum(x); comprobar max|RC_rel − 1/S| y residual KKT
    lotes = proyección entera (largest-remainder o CP-SAT) con frozen_equity y close[t]
    validador independiente aprueba o veta; fill en open[t+1]

Cómo lo hace la plataforma

Aquí la plataforma tiene casi todas las piezas como motores reales:

Pieza Motor Función real Evidencia
Limpieza de la ventana finaz_risk aplicar_politica_faltantes, informar_calidad_covarianza Tests G5
Covarianza Ledoit–Wolf finaz_risk (AG-022) covarianza_ledoit_wolf(retornos)EstimacionCovarianza (matriz, intensidad_shrinkage, observaciones_efectivas) Paridad con la intensidad de sklearn
Contribuciones finaz_risk contribuciones_riesgo(pesos, sigma) ERC 50/50 analítico
ERC finaz_allocation (AG-024) resolver_erc(sigma, presupuestos=…, tolerancia=1e-10, max_iter=5000)ResultadoSolver (estado, pesos, solver_id) G5: OPTIMAL, 50/50
Mínima varianza finaz_allocation resolver_minima_varianza(sigma, metodo="auto") (CVXPY + Clarabel) G5: OPTIMAL
Candidato finaz_allocation emitir_candidato → solo PortfolioCandidate, nunca un objetivo aprobado Diseño
Lotes enteros finaz_discrete (AG-025) proyectar_largest_remainder, proyectar_greedy_fill, proyectar_cpsat(pesos, precios, tamanos_lote, nav, tick, limite_ms, semilla, hilos) G6: CP-SAT OPTIMAL en subproceso
Veto finaz_constraints (AG-023) validar(candidato, restricciones) / aprobar(...)ApprovedTarget + ConstraintReport Anti-bypass probado

La regla de diseño que el código hace cumplir: allocation y discreto nunca construyen objetivos aprobados; el validador nunca optimiza. Solo finaz_constraints produce un ApprovedTarget.

Detalle de solver

El catálogo propone un optimizador con gradiente Sigma·x − b/x. El motor de la plataforma resuelve ERC por punto fijo (solver_id = finaz_allocation.erc_punto_fijo.v1), y usa CVXPY con Clarabel para mínima varianza (min_var_cvxpy_clarabel.v1). Ambos caminos deben dar el mismo w dentro de tolerancia; una implementación de Q06 tendría que declarar cuál usa y comprobar el residual KKT.

flowchart LR
    D["Panel de retornos<br/>ventana W = 252"] --> R["finaz_risk<br/>covarianza_ledoit_wolf"]
    R --> A["finaz_allocation<br/>resolver_erc<br/>(punto fijo)"]
    A --> C["PortfolioCandidate<br/>pesos + diagnósticos"]
    C --> X["finaz_discrete<br/>proyectar_cpsat<br/>lotes enteros"]
    X --> V["finaz_constraints<br/>aprobar → ApprovedTarget"]
    V --> L["Ledger de cartera<br/>fill open[t+1]<br/>(política Q06 no implementada)"]

Ejemplo numérico

1. Fixture de la especificación (calculado a mano)

docs/estrategias/Q06.md §6: S = 2, W = 6, un solo rebalanceo. Cierres de A1: 100, 102, 101, 104, 103, 106, 105; de A2: 50, 50,5, 51, 50,75, 51,5, 51,25, 52. Las series están construidas con correlación negativa marcada (−0,76).

Tras Ledoit–Wolf:

Sigma* = [[ 0.000315990637, −0.000120062779],
          [−0.000120062779,  0.000079092135]]
sigma_1 = 0.017776125484     sigma_2 = 0.008893375886
w = [sigma_2, sigma_1] / (sigma_1 + sigma_2) = [0.333466147809, 0.666533852191]

Verificación de contribuciones:

Sigma w = [2.534700e−5, 1.268181e−5]
w' Sigma w = 1.690517e−5
RC_rel = [0.500000000000, 0.500000000000]      max|RC − 0.5| = 2.22e−16

Con la misma diagonal y correlación cero, w sale idéntico: la correlación de −0,76 no cambia ni un dígito con dos activos.

2. Job G5 real de la plataforma

qa_reports/v2/runs/2026-09-20/g5-opt-001.result.json, imagen opt, estado PASS ("ERC+minvar+CP-SAT reales OPTIMAL"). Los casos son fijos y sintéticos:

ERC        Sigma = [[0.04, 0.01], [0.01, 0.04]]   (misma volatilidad)
           estado OPTIMAL · pesos [0.5, 0.5] · solver erc_punto_fijo.v1

Min-var    Sigma = [[0.10, 0.02, 0.01], [0.02, 0.12, 0.03], [0.01, 0.03, 0.08]]
           estado OPTIMAL · pesos [0.3617, 0.2021, 0.4362] · solver min_var_cvxpy_clarabel.v1

CP-SAT     pesos objetivo [0.5, 0.3, 0.2] · precios [100, 200, 50] · lote 1 · NAV 1000
           estado OPTIMAL · lotes [5, 1, 4] · prueba de optimalidad sí · 1773 ms

Cómo leer el CP-SAT. Los nocionales objetivo son 500, 300 y 200. Con lotes [5, 1, 4] los nocionales son 500, 200 y 200: desvío total 0 + 100 + 0 = 100 USD. El objetivo = 10000 que reporta el solver es ese desvío en céntimos (el dinero se escala por 10^decimales(tick) para trabajar en enteros exactos). ¿Por qué no [5, 2, 4]? Porque cuesta 1100 y supera el NAV. ¿Y [5, 1, 6]? Cuesta exactamente 1000, pero desvía 100 en la pata 3 además de los 100 de la pata 2: peor.

Cómo lanzarla

Como estrategia, hoy no se puede. GET /v1/capabilities devuelve Q06 UNSUPPORTED con NOT_IMPLEMENTED_YET; POST /v1/backtest responde 501 STRATEGY_UNSUPPORTED_BY_ADAPTER.

Los motores sí. Job S7 del rol opt (reproduce el G5):

echo '{"job_id": "demo-opt-erc", "rol": "opt", "operacion": "erc_cpsat"}' \
    > /spool/jobs/demo-opt-erc.json
python -m finaz_runtime.jobs.runner --rol opt --once
cat /spool/jobs/demo-opt-erc.result.json

La operación erc_only ejecuta solo el ERC. Ambas operaciones usan casos fijos: el contenido de la petición no cambia la sigma. Para aplicar los motores a tus datos, úsalos como biblioteca dentro de la imagen opt (ejemplo ilustrativo de un rebalanceo, no un backtest):

import numpy as np
from finaz_risk import covarianza_ledoit_wolf, contribuciones_riesgo
from finaz_allocation import resolver_erc
from finaz_discrete import proyectar_cpsat

R = np.loadtxt("retornos_252.csv", delimiter=",")          # 252 x S, sin NaN
est = covarianza_ledoit_wolf(R)
res = resolver_erc(est.matriz)
print(res.estado, res.solver_id, res.pesos)
print(contribuciones_riesgo(np.asarray(res.pesos), est.matriz))
lotes = proyectar_cpsat(pesos=list(res.pesos), precios=[105.0, 52.0],
                        tamanos_lote=[1, 1], nav=100000, tick="0.01",
                        limite_ms=5000, semilla=7, hilos=1)
print(lotes.estado_normalizado, lotes.lotes)

El paso que falta para que esto sea Q06 es la política que lo repita en cada rebalanceo con frozen_equity, close[t] y fill en open[t+1], y la paridad contra el ledger.

Riesgos, límites y qué no hace

  • No implementada como estrategia; los motores sí existen.
  • Un rebalanceo no es un backtest (lo dice el catálogo): la evidencia G5 prueba el solver, no la estrategia.
  • Sin cap individual en la primera versión: añadirlo cambiaría las condiciones ERC.
  • Fallo de convergencia: si el solver no llega a OPTIMAL, no se puede etiquetar una cartera inverse-vol como ERC; FEASIBLE != OPTIMAL se reporta honestamente.
  • CP-SAT en subproceso: por un conflicto con HiGHS se aísla; tiene coste de arranque (1,8 s en el job G5).
  • Long-only y sin apalancamiento: la paridad de riesgo "clásica" apalanca los activos tranquilos; esta receta no.

Resumen

  • ERC iguala las contribuciones al riesgo; con dos activos, w_i ∝ 1/sigma_i independientemente de la correlación.
  • Covarianza Ledoit–Wolf, solver con registro de convergencia y KKT, lotes enteros, validador independiente.
  • Estrategia especificada, no implementada; sus motores (finaz_risk, finaz_allocation, finaz_discrete, finaz_constraints) corren y pasaron G5/G6.

Para practicar

  1. Con Sigma = [[0.04, 0.01], [0.01, 0.09]], calcula a mano los pesos ERC y compruébalos con resolver_erc.
  2. En el caso CP-SAT del job G5, ¿qué lotes saldrían con NAV 1100? Razónalo antes de ejecutar.
  3. ¿Por qué finaz_allocation no puede devolver un ApprovedTarget? ¿Qué riesgo evita esa separación?
  4. Con tres activos, construye una sigma en la que la correlación cambie los pesos ERC respecto a 1/sigma_i.