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):
- 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.
Regla (catálogo)¶
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
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 | sí | +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 | sí | +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 | sí | +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 | sí | +0,25 | −0,232499 | 0,482499 | 0,829017 | +0,207254 | −0,192746 | +19 | −43 | (sin open[14]) |
10.267,70 |
Qué observar:
- 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,02el peso preliminar llega al topecap_individual = 0,25. - t = 5,
sign(0) = 0. BBB lleva tres sesiones planas a 49,50: momento exactamente cero → peso 0 → se cierra el corto. - Escalado por
gross_cap. Cuando la suma de pesos absolutos (0,50) supera 0,40, todos se multiplican por 0,80. - Ú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]
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_idque incluye los argumentos decointy 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.
WeightIntentArraystransporta 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¶
- En el fixture de Q01, ¿qué pasaría en
t = 3sivol_floorfuese 0? Calcula el peso preliminar de AAA. - Con S = 34 y
quantile = 0,2, ¿cuántos activos compra y vende Q02? ¿Qué peso tiene cada uno? - 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? - Abre
runs/parity/summary_weights.jsony busca el campoonline_equals_batch. ¿En cuántos casos es cierto? - Lee el walk-forward de Q03 en el capítulo siguiente y relaciona
blocks_admittedcon la regla de Engle–Granger.