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 > downestricto (el empate no genera DM);DM0 = 0en la semilla; ADX sembrado con los primerosnDX válidos (índicesn−1 .. 2n−2).wavetrend: constante 0,015;wt2 = SMA(wt1, 4)con el 4 fijo; sid == 0con numerador no nulo, NaN y la recursión queda en pausa.ichimoku:senkou_a/bsalen ya desplazadas hacia atrás; Chikou no se calcula (seríaclose[t + 26]visto desdet: 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 los2pvecinos (una meseta no genera pivote) y publicación diferida enj + p. Dentro de la máscara, NaN significa «no hubo pivote», no «calentando».- Sesión:
is_penultimate_regular_barsale 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_streamdecide 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:
SmaStreamsobre serie derivada (sma_of),LinRegStream(linreg_endpoint) yWmaStream(wma). Las 20 restantes soncustom_referenceincremental propio (deques y recursiones barra a barra). MacdStreamse descarta aunque sus valores coinciden: arranca en 33 en vez de 25.
Evidencia: runs/env/feature_streams_ext.json — 23 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¶
- Calcula a mano el
first_validdeichimoku{9,26,52,26}para cada salida y explica por quésenkou_bes 77. - ¿Por qué
rolling_max{p}[t] = donchian_upper{p}[t+1]? Demuéstralo con una serie de 5 barras. - 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). - Lee la entrada de
wavetrendenruns/env/feature_variants_ext_b.jsony explica en una frase la diferencia de semilla con la candidata de VectorTA.