Saltar a contenido

Estrategias de cartera (Q01–Q03)

Qué vas a aprender

  • Qué cambia cuando una estrategia ya no dice "compra o vende este activo" sino "reparte el capital entre varios".
  • Las tres recetas cuantitativas implementadas: Q01 (momentum de serie temporal con objetivo de volatilidad), Q02 (momentum transversal 12−1 largo/corto) y Q03 (pairs trading con OLS y cointegración).
  • Las piezas nuevas de infraestructura: panel T×S con watermark, WeightIntentArrays, sizing por pesos y ledger de cartera.
  • Qué dice la paridad Q01–Q03 (9 PASS, 3 UNSUPPORTED por diseño) y por qué el carril nativo de Nautilus no puede existir aquí (D-14).

1. De una señal a un reparto

En las recetas P la política produce un número por barra: target ∈ {−1, 0, +1}. Con una cartera eso no basta. Piensa en la diferencia entre:

  • un semáforo (P01: verde, ámbar, rojo para un coche), y
  • un controlador de tráfico (Q01: cuántos coches dejar pasar por cada una de 34 calles a la vez, sin superar la capacidad total).

El controlador necesita ver todas las calles a la vez antes de decidir. Si una calle todavía no ha informado, espera. Eso es el watermark.

Concepto Recetas P Recetas Q
Entrada una serie de barras un panel T×S (T sesiones × S activos)
Salida de la política IntentArrays (target entero) WeightIntentArrays (pesos por activo + cantidades)
Tamaño de la posición lotes {−1, 0, +1} pesos convertidos a cantidades con la equity del momento
Ledger SIM-S NumPy (sin bucle) ledger SIM-S Numba multi-activo, una sola cuenta
Timeframe los 7 1d (alcance actual: diaria)
Historia mínima 9–271 barras 252 sesiones (Q03 peor caso 504)

2. Infraestructura común

2.1 El panel y su watermark

flowchart LR
    subgraph Panel["Panel T×S (sesión t)"]
      A1[AAPL t ✓]
      A2[AMZN t ✓]
      A3[… ✓]
      A4[XOM t ✗ declarada ausente]
    end
    Panel --> W{¿todas las filas<br/>disponibles o<br/>declaradas ausentes?}
    W -- no --> Espera[esperar]
    W -- sí --> Pol[política de cartera<br/>rebalanceo de t]
    Pol --> WI[WeightIntentArrays]
    WI --> Led[ledger de cartera]

La regla (especificación de Q01, §4.b):

watermark_reached[t] = all_i ( available[t, i] or absent_declared[t, i] )
  • Una barra declarada ausente por el calendario no bloquea el watermark, pero ese activo no es elegible ese día: su peso es 0 y los pesos de los demás se recalculan. Así es como el hueco de KO (271 sesiones) movería una cartera entera.
  • El orden de llegada no importa: si los datos llegan desordenados, al alcanzar el watermark los pesos son los mismos (caso de prueba §6.6 de Q01).

2.2 WeightIntentArrays (D-21)

Es el contrato del "carril de pesos". Tiene un núcleo común y extensiones:

Parte Campos Quién la usa
Núcleo pesos objetivo, target_qty, frozen_equity, decision_price = close[t] Q01, Q02, Q03
Extensión transversal rank, n_eligible Q02
Extensión con modelo model_id, zscore, residual Q03

target_qty se calcula en la política, con la equity "congelada" al cierre de la decisión y el precio de cierre. Después el ledger ejecuta el delta en la apertura siguiente, como siempre.

2.3 El panel real: us_equities_36

Las corridas usan un panel de 34 valores US de Yahoo (XNYS, 2016–2026, 2.690 sesiones densas): AAPL, ADBE, AMD, AMZN, BAC, CAT, COST, CSCO, CVX, DIS, GOOGL, GS, HD, HON, IBM, INTC, JNJ, JPM, MA, MCD, META, MRK, MSFT, NFLX, ORCL, PEP, PFE, PG, QCOM, TSLA, UNH, V, WMT, XOM. NVDA y KO de twelvedata quedan fuera del panel 1d (deuda declarada).

Reservas que viajan en cada resultado

Cada corrida de cartera lleva estos avisos en warnings (texto real de runs/parity/summary_weights.json):

  • SURVIVORSHIP_BIAS: el universo está formado por valores que sobrevivieron; el resultado no acredita rentabilidad.
  • PRICE_ADJUSTMENT_NOT_PIT: los precios ajustados se recalculan hacia atrás con cada evento corporativo; no es point-in-time.
  • COSTES: fee 5 pb por fill, escenario de ensayo, no una tarifa de mercado.

3. Q01 — Time-series momentum con objetivo de volatilidad

Idea

Cada activo se mira contra su propio pasado: si ha subido en el último año, se compra; si ha bajado, se vende. El tamaño se ajusta para que cada posición aporte un riesgo parecido: los activos muy volátiles pesan menos.

En cada rebalanceo:

s_i       = sign( log(P_i[t] / P_i[t−L]) )                 # dirección
v_i       = std(retornos diarios, ventana V) · sqrt(252)   # volatilidad anualizada
w_i_prel  = s_i · min( cap_individual, vol_target / (S · max(v_i, vol_floor)) )
si Σ|w| > gross_cap → escalar todos los pesos por gross_cap / Σ|w|
q_i       = w_i · equity / precio_cierre_i   → se ejecuta en la apertura siguiente

Pesos de Q01 según momentum y volatilidad objetivo

Figura 1. Q01: signo del momentum de 252 días escalado por vol_target / vol realizada de 63 días; a más volatilidad, menos peso.

Parámetro Default Rejilla
lookback (L) 252 63, 126, 252
vol_window (V) 63 20, 63, 126
vol_target 0,10
gross_cap 1,5
vol_floor 0,02
cap_individual 0,25
rebalance_bars 21 5, 21

Ejemplo a mano (fixture de Q01.md §6)

Dos activos (AAA sube, BBB baja), lookback = 3, vol_window = 3, rebalance_bars = 2, gross_cap = 0,40 (deliberadamente bajo para que el escalado actúe), cash_inicial = 10.000, sin costes. Rebalanceos en t = 3, 5, 7, 9, 11, 13.

t reb. w_A prel. w_B prel. gross escala w_A w_B q_A obj. q_B obj. fill en open[t] equity cierre
3 +0,25 −0,25 0,50 0,80 +0,20 −0,20 +19 −40 10.000,00
4 no +19 −40 AAA +19 @ 102,50; BBB −40 @ 49,50 9.990,50
5 +0,25 0,00 0,25 1,00 +0,25 0,00 +24 0 10.028,50
6 no +24 0 AAA +5 @ 104,50; BBB +40 @ 49,50 10.050,00
7 +0,25 −0,217133 0,467133 0,856288 +0,214072 −0,185928 +20 −39 10.074,00
8 no +20 −39 AAA −4 @ 105,50; BBB −39 @ 47,50 10.071,50
13 +0,25 −0,232499 0,482499 0,829017 +0,207254 −0,192746 +19 −43 (sin open[14]) 10.267,70

Qué observar:

  1. t = 3, actúa el suelo de volatilidad. La volatilidad anualizada de AAA es 0,0013 (sube casi en línea recta). Sin suelo, el peso sería enorme; con vol_floor = 0,02 el peso preliminar llega al tope cap_individual = 0,25.
  2. t = 5, sign(0) = 0. BBB lleva tres sesiones planas a 49,50: momento exactamente cero → peso 0 → se cierra el corto.
  3. Escalado por gross_cap. Cuando la suma de pesos absolutos (0,50) supera 0,40, todos se multiplican por 0,80.
  4. Última decisión sin fill (t = 13): la cartera queda abierta y se valora al cierre.

4. Q02 — Momentum transversal 12−1, largo/corto

Idea

En lugar de comparar cada activo con su pasado, se comparan los activos entre sí: se compran los que más han subido y se venden los que menos. El "12−1" significa que se mide el retorno de los últimos 12 meses saltándose el último mes (que suele revertir).

Regla (catálogo)

score_i = log( P_i[t − skip] / P_i[t − lookback] )
en cada rebalanceo: ordenar elegibles por score (desempate por asset_id)
comprar el top q, vender el bottom q; cada lado suma 0,5 de exposición absoluta
mínimo 10 elegibles; si no, cartera plana; top y bottom no se solapan
Parámetro Default Rejilla
lookback 252 126, 252
skip 21 5, 21
quantile 0,2 0,1, 0,2, 0,3
rebalance_bars 21 (fijo, fuera de rejilla, D-20)

Por qué Q02 pasó de 'solo software' a ejecutable

Con 6 activos la regla "mínimo 10 elegibles" dejaba la cartera siempre plana (D-23). En WP-10 se descargó una sola vez el panel de 34 valores US: S = 34 ≥ 10 → EJECUTABLE, con las reservas declaradas (supervivencia, adjclose no point-in-time, borrow_cost_modeled: false). Los números no acreditan rentabilidad.

5. Q03 — Pairs trading: OLS y cointegración

Idea

Dos acciones que "se mueven juntas" (por ejemplo NVDA y AMZN) mantienen un diferencial que tiende a volver a su media. Cuando se separa demasiado, se compra la barata y se vende la cara; cuando se junta, se cierra.

Regla (catálogo)

flowchart TD
    F[Formación: últimas 252 sesiones] --> OLS["OLS: log Y = α + β·log X"]
    OLS --> EG{"¿Engle–Granger p menor que 0,05<br/>y 0,2 ≤ β ≤ 5?"}
    EG -- no --> NA[bloque NO admitido<br/>sin operaciones]
    EG -- sí --> CAL[calibrar media y std del residuo<br/>con las últimas z_window sesiones]
    CAL --> BL[Bloque de 21 sesiones<br/>parámetros CONGELADOS]
    BL --> Z["z = (residuo − media)/std"]
    Z --> R{reglas}
    R -- "z menor que −entry" --> L[largo residuo:<br/>+Y, −β·X]
    R -- "z > entry" --> S[corto residuo]
    R -- "abs z menor que exit,<br/>abs z mayor que stop,<br/>holding ≥ max_hold" --> C[cerrar]

Z-score del spread con umbrales de entrada, salida y stop

Figura 2. Q03: se abre contra el desvío con |z| ≥ 2, se cierra con |z| ≤ 0,5 y se corta con |z| ≥ 4 (z_window 60).

Parámetro Default Rejilla
formation 252 126, 252, 504
z_window 60
entry 2,0 1,5, 2,0, 2,5
exit 0,5 0,0, 0,5, 1,0
stop 4,0
max_hold 20
refit_bars 21

Puntos clave:

  • Parámetros congelados por bloque. Durante las 21 sesiones del bloque, α, β, media y desviación no se tocan: nada de reajustar con datos que aún no existían.
  • Modelos cacheados. Cada ajuste tiene un model_id que incluye los argumentos de coint y la versión de statsmodels (D-20); Q03 es la única receta del grupo con modelo ajustado, así que prueba la caché models/ que necesitarán recetas futuras.
  • Parejas preseleccionadas. NVDA–AMZN y MSFT–INTC vienen del plan; la minería de parejas está fuera de alcance.

Un hallazgo honesto del fixture

En el fixture a mano de Q03, el p-valor real de Engle–Granger (statsmodels 0.15.0) es 0,9859: la pareja se rechazaría. La admisión del fixture es contrafáctica y declarada para poder mostrar la mecánica de entrada y salida.

6. El ledger de cartera

El ledger Numba (finazbench/ledger/sim_s_numba.py) lleva una sola cuenta para todos los activos y ejecuta cada pata en su primera apertura operable (D-25). Tiene dos modos de sizing:

Modo Qué hace Cuándo
rebalance_each_bar = False cantidad congelada al abrir estrategias direccionales
rebalance_each_bar = True la cantidad se recalcula cada barra contra la equity pesos objetivo constantes

Medido sobre P01 long-only en NVDA 1d (docs/POLITICAS_Y_LEDGER.md §4):

fills coste total equity final
Tamaño congelado 242 667,25 45.889,27
Rebalanceo continuo 476 656,24 44.880,58

Contraintuitivo: más fills, menos coste. Al recortar la posición cuando la equity baja, el rebalanceo mantiene posiciones más pequeñas, y la comisión es proporcional al nocional. Por eso cada corrida debe declarar qué contrato de ejecución usó.

7. Paridad Q01–Q03: 9 PASS, 3 UNSUPPORTED

La paridad compara el carril de referencia (VTA_CPU_LEDGER en modo cartera) con tres adaptadores de Nautilus multi-instrumento (D-40): reproducir pesos (NT_WEIGHT_REPLAY), recibir features precalculadas (NT_WEIGHT_FEATURES) y calcular en línea (NT_WEIGHT_ONLINE). Resultado real de runs/parity/summary_weights.json (generado el 2026-09-16, 12 casos, escenario 5 pb):

Receta Datos Barras Activos fdi Fills (A = B) REPLAY FEATURES ONLINE Nativo
Q01 US34 1d 2.690 34 252 2.912 PASS PASS PASS UNSUPPORTED
Q02 US34 1d 2.690 34 252 1.690 PASS PASS PASS UNSUPPORTED
Q03 NVDA–AMZN 1d 2.941 2 252 254 PASS PASS PASS UNSUPPORTED

En los nueve PASS, además, online_equals_batch = true: la versión barra a barra coincide con la versión vectorizada.

flowchart LR
    REF[VTA_CPU_LEDGER<br/>cartera] --- R[NT_WEIGHT_REPLAY ✔]
    REF --- F[NT_WEIGHT_FEATURES ✔]
    REF --- O[NT_WEIGHT_ONLINE ✔]
    REF -.- N[NT_ONLINE nativo ✘<br/>UNSUPPORTED D-14]

¿Por qué el nativo es UNSUPPORTED? Texto literal del resultado:

NT_ONLINE_native = UNSUPPORTED por diseño (D-14): Nautilus no tiene panel nativo, ni ranking transversal, ni OLS de formación, ni test de cointegración, ni z contra parámetros congelados. No se fabrica un número con un indicador parecido (§8.6).

Tiempos de desarrollo, no de benchmark

El fichero lo dice explícitamente: los tiempos son de una repetición, sin calentamientos ni las veinte medidas del protocolo, en máquina compartida. is_benchmark: false: no alimentan ningún speedup.

Fichas completas

La ficha completa de cada estrategia cuantitativa está en quant/Qxx.md (índice); Q04–Q10 están especificadas pero fuera del alcance implementado:

Q01 · Q02 · Q03 · Q04 · Q05 · Q06 · Q07 · Q08 · Q09 · Q10

Resumen

  • Las recetas Q trabajan sobre un panel T×S y producen pesos, no señales discretas.
  • El watermark garantiza que ninguna decisión de cartera se toma hasta que todos los activos de esa sesión están disponibles o declarados ausentes.
  • WeightIntentArrays transporta pesos, cantidades y, según la receta, ranking (Q02) o modelo (Q03).
  • Q01 = momentum por activo con objetivo de volatilidad; Q02 = ranking transversal 12−1 con mínimo 10 elegibles; Q03 = pares con OLS + Engle–Granger y parámetros congelados por bloque.
  • Paridad: 9 PASS contra tres adaptadores de Nautilus y 3 UNSUPPORTED por diseño (no hay panel nativo).
  • Todo resultado de cartera lleva sus reservas (supervivencia, ajuste no PIT, costes de ensayo).

Para practicar

  1. En el fixture de Q01, ¿qué pasaría en t = 3 si vol_floor fuese 0? Calcula el peso preliminar de AAA.
  2. Con S = 34 y quantile = 0,2, ¿cuántos activos compra y vende Q02? ¿Qué peso tiene cada uno?
  3. Explica por qué Q03 congela α y β durante el bloque. ¿Qué error se cometería si se reajustaran cada día con la ventana que incluye t?
  4. Abre runs/parity/summary_weights.json y busca el campo online_equals_batch. ¿En cuántos casos es cierto?
  5. Lee el walk-forward de Q03 en el capítulo siguiente y relaciona blocks_admitted con la regla de Engle–Granger.