Saltar a contenido

Las 23 primitivas de fase B

La fase A del proyecto necesitaba once primitivas (SMA, EMA, RSI, ATR, Bollinger, Donchian, Supertrend…) para cinco estrategias. Las quince estrategias populares siguientes (P03–P20) piden mucho más: MACD, Squeeze Momentum, WaveTrend, Ichimoku, Parabolic SAR, DMI/ADX, Stochastic RSI, Hull, Chandelier, pivotes confirmados y primitivas de sesión (rango de apertura, VWAP). Este capítulo presenta las 23 primitivas de fase B, las reglas de registro que exigieron (R1–R5), el carril incremental con online == batch y las deudas que quedan abiertas.

Qué vas a aprender

  • Qué primitiva usa cada estrategia P03–P20 y cómo se relacionan los roles del catálogo con los nombres registrados.
  • El registro extendido (ExtendedPrimitiveDef) y las reglas R1–R5.
  • Cómo se decidió, primitiva a primitiva, si la candidata de VectorTA servía (MATCH, VARIANT, DESCARTADA).
  • Qué garantiza el carril incremental y cómo se prueba online == batch.
  • Las deudas declaradas: DEV-09A3-01, DEV-09A3-02 y DEV-09D-04.

1. Mapa: estrategia → roles → primitivas

El catálogo de estrategias (catalog/strategy_catalog.json) describe cada estrategia con roles (nombres de lo que necesita). El registro de features tiene primitivas (nombres de lo que se calcula). No siempre coinciden, y la regla es: ni un nombre para dos fórmulas.

Estrategia Roles en el catálogo Primitivas registradas que los sirven
P03 MACD ema_fast, ema_slow, macd_signal macd (+ ema de fase A)
P05 Bollinger Bandwidth bbands, bandwidth, rolling_quantile bandwidth, rolling_quantile
P06 Supertrend atr, supertrend_bands, direction supertrend (fase A)
P07 UT Bot atr, recursive_atr_stop atr_trailing_stop
P08 Squeeze Momentum bbands, keltner, rolling_linear_regression keltner_sma, squeeze_momentum, linreg_endpoint, sma_of, rolling_max, rolling_min
P09 WaveTrend hlc3, ema, wavetrend, sma_signal hlc3, wavetrend
P10 Ichimoku rolling_high_low, tenkan, kijun, senkou_a, senkou_b ichimoku (+ rolling_max/rolling_min)
P12 Opening Range Breakout opening_range, session_id, cutoff opening_range, session_position
P13 VWAP de sesión session_vwap, session_weighted_variance session_vwap
P14 Parabolic SAR psar psar
P15 DMI/ADX plus_di, minus_di, adx dmi
P16 Keltner ema, atr (fase A; las bandas se calculan en la política)
P17 Stochastic RSI rsi, rolling_rsi_range, stoch_k, stoch_d stochrsi
P18 Hull Suite wma, hma wma, hma
P19 Chandelier Exit atr, rolling_high_low, recursive_chandelier chandelier
P20 SMC / BOS confirmed_pivots, last_swing_high, last_swing_low confirmed_pivots

Por qué keltner no existe a secas

P08 usa Keltner con SMA del TR (keltner_sma) y P16 con EMA ± ATR (nombre reservado keltner_ema, calculado en la política). Son dos fórmulas; nunca existirá una primitiva llamada keltner sin apellido.

Las 23, en dos grupos de trabajo:

Grupo Paquete Primitivas
A (WP-09a1): ventanas, precios derivados, osciladores y bandas ext_windows.py, ext_oscillators_a.py hlc3, rolling_max, rolling_min, sma_of, rolling_quantile, linreg_endpoint, wma, hma, macd, bandwidth, keltner_sma, squeeze_momentum, atr_trailing_stop (13)
B (WP-09a2): osciladores recursivos y sesión ext_oscillators_b.py, ext_session.py stochrsi, dmi, wavetrend, ichimoku, psar, chandelier, confirmed_pivots, session_position, opening_range, session_vwap (10)

2. El registro extendido y las reglas R1–R5

Las once primitivas de fase A no se tocan (sus claves de caché siguen bit a bit). Las de fase B viven en un segundo registro cuya definición lleva el kernel dentro:

@dataclass(frozen=True, slots=True)
class ExtendedPrimitiveDef:
    name: str
    formula_version: str
    params: tuple[str, ...]           # orden canónico = orden en la clave
    inputs: tuple[str, ...]           # columnas de BarArrays o salidas de otra primitiva (R4)
    outputs: tuple[str, ...]
    first_valid_rule: FirstValidRule  # + chained_sum, chained_shifted_window, fixed_index, session_local (R1)
    dependencies: Callable[[Mapping], list[FeatureSpec]]
    canonical: Callable[..., dict[str, np.ndarray]]
    warmup_increment: Callable[[Mapping], int] | None = None   # R2
    parallel_deps: bool = False                                # R3
    stream_factory: Callable[[Mapping], StreamingFeature] | None = None
    session_aware: bool = False                                # R5
    first_valid_by_output: Callable[[Mapping, int], dict[str, int]] | None = None
    ...
Regla Qué añade Ejemplo verificado
R1 Nuevas reglas de first_valid chained_sum (base + period − 1), chained_shifted_window (base + period), fixed_index, session_local rolling_quantile{252} sobre bandwidth{20} → 19 + 252 = 271
R2 warmup_increment incremento propio cuando no hay un único period o difiere por salida macd 12/26/9 → 33; hma{55}60; squeeze_momentum{20}38
R3 Dependencias paralelas el calentamiento es el máximo, no la suma keltner_sma{20} → 19 (sumar daría 38 y tiraría 19 barras buenas)
R4 Primitivas sobre una serie derivada inputs nombra la salida de otra primitiva sma_of sobre tr; linreg_endpoint sobre val
R5 Primitivas de sesión calendar_version entra en la clave de caché cambiar un festivo cambia la segmentación sin cambiar un precio
flowchart LR
    BB["bbands{20}<br/>first_valid 19"] --> BW["bandwidth{20}<br/>19"]
    BW --> RQ["rolling_quantile{252}<br/>chained_shifted_window<br/>19 + 252 = 271"]
    E12["ema{12}"] --> M["macd 12/26/9<br/>macd 25 · signal 33"]
    E26["ema{26}"] --> M

3. Las primitivas de un vistazo

3.1 Grupo A

Primitiva Salidas first_valid (defaults) Candidata VectorTA Veredicto
hlc3 hlc3 0 sin candidata
rolling_max / rolling_min un valor n − 1 = 19 minmax DESCARTADA (es un detector de pivotes)
sma_of sma base + n − 1 sma MATCH (2,5e-11)
rolling_quantile quantile base + W = 271 percentile_nearest_rank DESCARTADA por definición (rango más cercano ≠ interpolación lineal)
linreg_endpoint endpoint OLS base + n − 1 linreg MATCH (1,4e-10); tsf extrapola a x = n
wma wma 19 wma MATCH (3,9e-11)
hma hma (n−1)+(m−1) = 60 hma DESCARTADA: usa int(sqrt(n)), no redondeo; se ve con period=32
macd macd, signal, hist 25 / 33 / 33 macd MATCH (1,1e-13)
bandwidth bandwidth 19 bollinger_bands_width MATCH (4,5e-12)
keltner_sma middle, upper, lower 19 keltner DESCARTADA (es EMA±ATR)
squeeze_momentum squeeze, val, m 19 / 19 / 38 squeeze_momentum VARIANT parcial (solo m coincide)
atr_trailing_stop stop n − 1 = 9 sin candidata

3.2 Grupo B

Primitiva Salidas first_valid Candidata Veredicto
stochrsi{14,14,3,3} k, d 29 / 31 srsi MATCH (ya en 0..100)
dmi{14} plus_di, minus_di, adx 13 / 13 / 26 adx, di VARIANT (arranca en 27 y difiere)
wavetrend{10,21} wt1, wt2 38 / 41 wavetrend VARIANT (siembra con índices, no con válidos)
ichimoku{9,26,52,26} tenkan, kijun, senkou_a, senkou_b 8, 25, 51, 77 ichimoku_oscillator DESCARTADA (una sola serie)
psar{0.02,0.2} sar, direction, ep, af 1 (fixed_index) sar VARIANT (límite por un solo extremo)
chandelier{22,3.0} long_stop, short_stop, direction 21 chandelier_exit VARIANT (solo el stop activo; SMA(TR))
confirmed_pivots{3} pivot_high_level, pivot_low_level 2p = 6 pivot DESCARTADA (puntos pivote clásicos)
session_position índice y minutos en sesión, penúltima/última barra session_local sin candidata
opening_range range_high, range_low, range_complete, in_range session_local sin candidata
session_vwap vwap, var, cum_volume session_local vwap / NT VolumeWeightedAveragePrice DESCARTADA / UNSUPPORTED (reinicia por día UTC)

Trampa de validación: el MATCH falso

minmax coincide exactamente con rolling_min en las 71 barras donde tiene valor. Una comparación ingenua «ignorando NaN» habría dado diferencia 0 y un MATCH falso. Lo que la descalifica es la cobertura, no el valor. Del mismo modo, hma con period=55 parece MATCH porque int(sqrt(55)) y el redondeo coinciden; por eso el contrato manda verificar con period=32.

session_vwap nace en la versión de fórmula 2

La varianza va por Welford ponderado (D-30), que da números distintos de la fórmula del catálogo. Cambiar el algoritmo sin cambiar la versión sería esconder una diferencia bajo una tolerancia.


4. Convenciones que discriminan implementaciones

Algunas reglas finas deciden si una implementación es la canónica:

  • stochrsi: ventana inclusiva; mx == mn → 50,0; los dos suavizados son SMA, no EMA.
  • dmi: up > down estricto (el empate no genera DM); DM0 = 0 en la semilla; ADX sembrado con los primeros n DX válidos (índices n−1 .. 2n−2).
  • wavetrend: constante 0,015; wt2 = SMA(wt1, 4) con el 4 fijo; si d == 0 con numerador no nulo, NaN y la recursión queda en pausa.
  • ichimoku: senkou_a/b salen ya desplazadas hacia atrás; Chikou no se calcula (sería close[t + 26] visto desde t: información del futuro).
  • psar: primer SAR en el índice 1; límite por los dos extremos previos.
  • chandelier: trinquete con cierre previo y desigualdades estrictas; precedencia alcista si se cumplen ambas condiciones.
  • confirmed_pivots: estricto en los 2p vecinos (una meseta no genera pivote) y publicación diferida en j + p. Dentro de la máscara, NaN significa «no hubo pivote», no «calentando».
  • Sesión: is_penultimate_regular_bar sale de la rejilla del calendario, nunca de mirar la barra siguiente del array.

Trampa float64 en dmi

El fixture de P15 exige comprobar primero que 10.05 − 10.00 == 9.40 − 9.35 bit a bit (empate real de DM) y que 9.60 − 9.55 no lo es. Con la pareja equivocada el test pasaría por el motivo contrario.


5. El carril incremental: online == batch

Un carril incremental (streaming) consume una observación cada vez y no puede mirar el array entero: es causal por construcción, no por disciplina. WP-09a3 dio stream_factory a las 23.

sequenceDiagram
    participant B as Barras
    participant S as Stream (make_stream)
    participant R as run_stream
    loop t = 0 … N−1
        B->>S: observación t (cierre, barra, H+L, sesión…)
        S->>R: valor(es) en t o NaN(s) de calentamiento
    end
    R->>R: comparar con el batch desde first_valid<br/>por salida, con la tolerancia registrada
  • feed_stream decide qué se alimenta a cada primitiva (EXTENDED_STREAM_FEED): cierre, high, low, barra, high+low, series derivadas (R4) o sesión (session_id + relojes + volumen).
  • Solo tres candidatas del wheel reproducen la canónica y se usan en streaming: SmaStream sobre serie derivada (sma_of), LinRegStream (linreg_endpoint) y WmaStream (wma). Las 20 restantes son custom_reference incremental propio (deques y recursiones barra a barra).
  • MacdStream se descarta aunque sus valores coinciden: arranca en 33 en vez de 25.

Evidencia: runs/env/feature_streams_ext.json23 filas MATCH con first_valid idéntico por salida; la mayoría bit a bit y la peor 4,3e-11 (linreg_endpoint). La matriz de capacidades cubre las 34 primitivas de fases A+B (102 filas) y supports_streaming es 34/34 en vectorta.

A nivel de estrategia, la política online (estado barra a barra) se compara con la batch: por ejemplo, online == batch en las siete estrategias de un lote sobre NVDA 1d, y en Q01/Q02 bit a bit en target_qty y fills (Bitácora, WP-09/WP-10).

Futuros alterados

Además de la paridad, cada primitiva pasa el test de futuros alterados: perturbar el final de la serie no puede cambiar ni un bit del prefijo. Si cambia, el cálculo está mirando el futuro.


6. Deudas declaradas

Deuda Qué falta Por qué importa
DEV-09A3-01 Activar la ruta batch nativa de VectorTA para las candidatas MATCH (macd, sma_of, linreg_endpoint, wma, stochrsi; la sonda acredita 5 de 6; bandwidth no es cableable porque su kernel no recibe close) Hoy las 23 declaran vectorta = None: el tiempo del proveedor vectorta en fase B es cálculo propio, no del wheel
DEV-09A3-02 provider_vectorta._build_result ignora first_valid_by_output squeeze_momentum.squeeze declara 0 en vez de 19 en ese proveedor (la sonda usa el declarado como suelo)
DEV-09D-04 Máscara del helper de confirmed_pivots no cableada El matiz «NaN dentro de la máscara = sin pivote» no llega al helper

Decisión conservadora y reversible

Acreditar la equivalencia sin cablear la llamada nativa es deliberado: el test que prueba el MATCH ya existe, así que activar la ruta nativa de macd o wma es «cambiar un campo» cuando se decida, con la evidencia ya escrita.


Resumen

  • Fase B añade 23 primitivas (13 del grupo A y 10 del grupo B) para P03–P20; los roles del catálogo se sirven con nombres registrados sin ambigüedad.
  • R1–R5 extienden el registro: nuevas reglas de first_valid, incrementos de warmup, dependencias paralelas, series derivadas y claves con calendario.
  • Cada candidata de VectorTA se juzgó con datos: 6 MATCH (sma_of, linreg_endpoint, wma, macd, bandwidth, stochrsi), varias VARIANT y DESCARTADAS con motivo.
  • Las 23 tienen carril incremental con paridad stream/batch acreditada (online == batch).
  • Quedan tres deudas declaradas: DEV-09A3-01, DEV-09A3-02 y DEV-09D-04.

Para practicar

  1. Calcula a mano el first_valid de ichimoku{9,26,52,26} para cada salida y explica por qué senkou_b es 77.
  2. ¿Por qué rolling_max{p}[t] = donchian_upper{p}[t+1]? Demuéstralo con una serie de 5 barras.
  3. Diseña un test de «futuros alterados» para confirmed_pivots{2} y explica qué barras del prefijo pueden cambiar (pista: la publicación es diferida).
  4. Lee la entrada de wavetrend en runs/env/feature_variants_ext_b.json y explica en una frase la diferencia de semilla con la candidata de VectorTA.