P18 · Hull Suite: dirección de HMA¶
| Campo | Valor |
|---|---|
| Familia | Tendencia · subfamilia pendiente de media: dirección continua (siempre largo o corto tras el warmup) |
| Features requeridas | hma (primitiva nueva), que se apoya en dos wma sobre close: wma(floor(period/2)) y wma(period), más una tercera wma interna sobre la serie derivada |
| Parámetros y defaults | period = 55, lag = 2 |
| Rejilla del catálogo | period ∈ {16, 32, 55, 89, 144}, lag ∈ {1, 2, 3} → 15 candidatos válidos, 5 curvas hma y 9 curvas wma distintas |
variant_keys |
period, lag |
| Restricciones | enteros; period ≥ 4 y lag ≥ 1 (si no, PolicyParamError); sin restricción cruzada |
first_decision_index |
(period − 1) + (m − 1) + lag con m = round_half_up(sqrt(period)) = 54 + 6 + 2 = 62 con defaults (peor caso de la rejilla: 157) |
| Activos y timeframes | NVDA y KO en los 7 TF (1min … 1d); AMZN, INTC, MSFT, TSLA en 1d; BME y FX tras el calendario de WP-01b. Solo usa close |
| Paridad en la matriz | PASS en NT_FEATURES, NT_INTENT_REPLAY y NT_ONLINE(vectorta) (12/12 cada una). PARITY_FAIL nativa en NT_ONLINE(native) (12/12), pero en la capa fills, no en features: primera divergencia en la barra 63 de KO 1d, signal_index 62 frente a 56. Las HMA coinciden tras el warmup y las señales comparadas también; lo que cambia es cuándo empieza a operar cada lado (ver Qué mirar) |
Fuentes: docs/estrategias/P18.md (especificación), catalog/strategy_catalog.json, docs/DATOS_POR_ESTRATEGIA.md, runs/parity/summary.json, runs/parity/KO_1d_P18_5bps_NT_ONLINE_native.md.
Qué vas a aprender¶
- Cómo se construye la Hull Moving Average a partir de tres medias ponderadas y por qué adelanta a una media normal.
- Por qué P18 no compara dos medias, sino una media consigo misma desplazada
lagbarras. - A contar un calentamiento encadenado con una parte paralela: máximo para las ramas, suma para la cadena.
- Por qué 15 candidatos cuestan solo 5 curvas
hma(y 9wma, no 10). - Qué significa una divergencia de paridad en fills con señales idénticas.
La idea en una frase¶
Si la HMA de hoy está por encima de la de hace lag barras, estar largo; si está por debajo, estar corto.
Intuición de mercado¶
Una media móvil normal llega tarde: resume el pasado y, cuando el precio gira, sigue apuntando en la dirección vieja durante media ventana. Alan Hull propuso corregir ese retraso con un truco aritmético: 2·WMA(n/2) − WMA(n). La media corta va "por delante" de la larga; restar la larga al doble de la corta extrapola esa ventaja, y una última WMA corta (de longitud ≈ √n) suaviza el resultado. Queda una curva casi tan suave como una media de n barras, pero que gira mucho antes.
P18 solo mira la pendiente de esa curva: ¿sube o baja respecto a hace lag barras?
Analogía. Un coche con retrovisor y con navegador. La media normal es el retrovisor: te dice por dónde has pasado. La HMA es el navegador que estima hacia dónde vas con tu velocidad actual. P18 no pregunta dónde estás, solo si la flecha del navegador apunta hacia arriba o hacia abajo.
Por qué podría funcionar. Como P01, explota el momentum de medio plazo; al reaccionar antes que una SMA o una EMA del mismo periodo, captura una parte mayor de cada tendencia.
Cuándo falla.
- Mercados laterales: una curva rápida también cambia de pendiente rápido. Cada cambio es una reversión con delta 2.
- Sobrerreacción: la extrapolación
2·corta − largaexagera los picos; tras un salto aislado la HMA "se pasa" y luego corrige, generando una reversión espuria. lag = 1conperiodcorto: la señal es casi ruido; cada pequeña ondulación de la curva invierte la posición.
Sesgos típicos al evaluarla.
- Warmup desigual:
period = 144, lag = 3pierde 157 barras;period = 16, lag = 1, solo 19. Sobre Yahoo diario (~2.900 barras) el peor caso descarta el 5,4 % de la muestra. Compara todos los candidatos desde un comienzo común ≥ 157. - Implementaciones "equivalentes" que no lo son:
round(n/2)en vez defloor(n/2), oint(sqrt(n))en vez de redondear, cambian la curva. La receta lo fija expresamente. - Confundirla con EHMA/THMA: el catálogo excluye las variantes exponencial y triple de la suite.
Las reglas exactas¶
La regla canónica del catálogo: "HMA(n)=WMA(2·WMA(C,floor(n/2))−WMA(C,n),round_half_up(sqrt(n))). Objetivo +1 si HMA[t]>HMA[t−lag], −1 si menor; igualdad conserva. Largo/corto en apertura posterior."
Y sus engineering_notes: "Fijar truncamiento de n/2 y redondeo de raíz para evitar diferencias entre lenguajes. Pesos WMA ascendentes de 1 a n, máximo peso al dato reciente."
Situación en el cierre de t |
Objetivo target[t] |
Motivo (reason) |
|---|---|---|
t < first_decision_index (hma[t] o hma[t−lag] no existe) |
0 | WARMUP |
hma[t] > hma[t−lag] (estricto) |
+1 | ENTRY / REVERSAL / HOLD |
hma[t] < hma[t−lag] (estricto) |
−1 | ENTRY / REVERSAL / HOLD |
hma[t] == hma[t−lag] (igualdad exacta en f64) |
el anterior | HOLD |
NaN tardío en hma[t] o hma[t−lag] |
el anterior | HOLD |
barra no operable (is_tradable = False) |
se congela el publicado | NOT_TRADABLE |
- Entrada: la primera barra de decisión ya da ±1; no espera a ningún cruce.
- Salida: no hay salida a plano; la salida de un largo es la reversión a corto.
- Reversión: permitida y directa, delta 2. En P18 todos los fills salvo el primero son de 2 unidades.
- Filtros: ninguno.
lagno es un filtro sino el desplazamiento de la comparación, y vive en la política, no en la feature.
# decisión en el cierre de t, fill en el open de t+1
target_prev = 0
para cada barra t:
si t < first_decision_index: target = 0 # WARMUP
si no, si hma[t] > hma[t - lag]: target = +1
si no, si hma[t] < hma[t - lag]: target = -1
si no: target = target_prev # empate o NaN
si target != target_prev:
delta = target - target_prev # ±1 al abrir, ±2 al invertir
orden(delta) -> fill a open[t+1] # nunca a close[t]
target_prev = target
Comparar con el pasado, nunca con el futuro
El desplazamiento correcto es lagged[lag:] = hma[:-lag] (la barra t ve hma[t−lag]). Escribir lagged[:-lag] = hma[lag:] compara con hma[t+lag] y convierte la estrategia en un oráculo perfecto. Es la fuga número 2 que la especificación enumera en su §8.1.
Los indicadores que usa¶
| Feature | Fórmula | first_valid |
|---|---|---|
wma{n} |
Σ_{i=0}^{n−1} close[t−n+1+i] · (i+1) / (n·(n+1)/2): el cierre más antiguo pesa 1, el más reciente pesa n |
n − 1 |
hma{period} |
half = floor(period/2); m = round_half_up(sqrt(period)); raw = 2·wma(half) − wma(period); hma = wma(raw, m) |
(period − 1) + (m − 1) |
Los dos redondeos, tabulados para la rejilla:
period |
half = floor(p/2) |
sqrt(p) |
m |
first_valid(hma) |
first_decision_index con lag 1 / 2 / 3 |
|---|---|---|---|---|---|
| 16 | 8 | 4,000000 | 4 | 18 | 19 / 20 / 21 |
| 32 | 16 | 5,656854 | 6 | 36 | 37 / 38 / 39 |
| 55 | 27 | 7,416198 | 7 | 60 | 61 / 62 / 63 |
| 89 | 44 | 9,433981 | 9 | 96 | 97 / 98 / 99 |
| 144 | 72 | 12,000000 | 12 | 154 | 155 / 156 / 157 |
Tres cosas que conviene entender del calentamiento:
- Las dos WMA sobre
closeson ramas paralelas:rawvale desde el máximo de susfirst_valid, es decirperiod − 1, no desde la suma. - La WMA sobre
rawencadena: añadem − 1barras encima. lagsuma aparte, porquehma[t−lag]no existe antes defirst_valid(hma) + lag.
Con defaults: max(26, 54) + 6 + 2 = 62. Un cálculo ingenuo (period − 1 = 54) emitiría señales 8 barras antes de tiempo.
period = 32 es el caso discriminante del redondeo: int(sqrt(32)) = 5 pero m = 6. Y period = 55 lo es del truncamiento: round(55/2) = 28 pero half = 27. Curiosidad aritmética de la especificación: sqrt(period) nunca acaba exactamente en ,5 con period entero, así que round_half_up y el round() de Python coinciden siempre aquí; aun así el código debe decir round_half_up.
Detalle de ambas primitivas en Las 23 primitivas de fase B; la idea general de feature frente a política, en Indicadores y features.
Ventanas finitas, no recursiones
A diferencia de la EMA de P01 o P16, las tres WMA son ventanas finitas: un cierre malo en j contamina exactamente las salidas j … j + period + m − 2 y luego desaparece por completo. La especificación lo convierte en test propio (test_p18_finite_window_contamination): con period = 55, m = 7, perturbar close[350] no cambia hma[349] ni hma[411] en adelante.
Ejemplo barra a barra¶
Fixture §6 de docs/estrategias/P18.md: period = 4 (fuera de rejilla a propósito, emite PolicyParamWarning) y lag = 2 (dentro de la rejilla). half = floor(4/2) = 2, m = round_half_up(sqrt(4)) = 2. first_valid(wma2) = 1, first_valid(wma4) = first_valid(raw) = 3, first_valid(hma) = 3 + 1 = 4 → first_decision_index = 4 + 2 = 6. cash_inicial = 1.000,00, tick 0,01, sin costes. El fixture construye open[t] = close[t−1].
Las barras 0–2 (cierres 10,00 / 10,20 / 10,10) solo tienen wma2 (10,1333 en t=1 y t=2); raw y hma son NaN.
| t | O | H | L | C | raw | hma | hma[t−2] | target | orden | fill | pos. | PnL acum. sin costes | equity con 5 bps* |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3 | 10,10 | 10,55 | 10,05 | 10,40 | 10,3700 | NaN | NaN | 0 WARMUP | — | — | 0 | 0,00 | 1.000,00 |
| 4 | 10,40 | 10,85 | 10,35 | 10,70 | 10,7600 | 10,6300 | NaN | 0 WARMUP | — | — | 0 | 0,00 | 1.000,00 |
| 5 | 10,70 | 11,05 | 10,65 | 10,90 | 11,0067 | 10,9244 | NaN | 0 WARMUP | — | — | 0 | 0,00 | 1.000,00 |
| 6 | 10,90 | 10,95 | 10,65 | 10,80 | 10,8967 | 10,9333 | 10,6300 | +1 ENTRY | BUY 1 | — | 0 | 0,00 | 1.000,00 |
| 7 | 10,80 | 10,85 | 10,40 | 10,50 | 10,5100 | 10,6389 | 10,9244 | −1 REVERSAL | SELL 2 | 10,80 | +1 | −0,30 | 999,69 |
| 8 | 10,50 | 10,55 | 10,10 | 10,20 | 10,1200 | 10,2500 | 10,9333 | −1 HOLD | — | 10,50 | −1 | 0,00 | 999,98 |
| 9 | 10,20 | 10,25 | 9,80 | 9,90 | 9,8000 | 9,9067 | 10,6389 | −1 HOLD | — | — | −1 | 0,30 | 1.000,28 |
| 10 | 9,90 | 10,00 | 9,60 | 9,70 | 9,5933 | 9,6622 | 10,2500 | −1 HOLD | — | — | −1 | 0,50 | 1.000,48 |
| 11 | 9,70 | 9,80 | 9,50 | 9,60 | 9,5167 | 9,5422 | 9,9067 | −1 HOLD | — | — | −1 | 0,60 | 1.000,58 |
| 12 | 9,60 | 10,00 | 9,55 | 9,85 | 9,7833 | 9,6944 | 9,6622 | +1 REVERSAL | BUY 2 | — | −1 | 0,35 | 1.000,33 |
| 13 | 9,85 | 10,30 | 9,80 | 10,15 | 10,1950 | 10,0578 | 9,5422 | +1 HOLD | — | 9,85 | +1 | 0,65 | 1.000,62 |
| 14 | 10,15 | 10,60 | 10,10 | 10,45 | 10,5450 | 10,4283 | 9,6944 | +1 HOLD | — | — | +1 | 0,95 | 1.000,92 |
| 15 | 10,45 | 10,85 | 10,35 | 10,70 | 10,8033 | 10,7172 | 10,0578 | +1 HOLD | — | — | +1 | 1,20 | 1.001,17 |
Todo es literal del fixture (§6.1 OHLC, §6.2 features y objetivos, §6.4 fills y serie de equity) salvo la última columna. *"Equity con 5 bps" es cálculo de este manual: escenario hypothetical_5bps (fee_rate = 0,0005, sin spread ni deslizamiento), comisión por fill = |delta| · precio · 0,0005 redondeada al céntimo half-even y restada de la caja en la barra del fill:
- fill 1 (t=7):
1 · 10,80 · 0,0005 = 0,0054 → 0,01; - fill 2 (t=8):
2 · 10,50 · 0,0005 = 0,0105 → 0,01; - fill 3 (t=13):
2 · 9,85 · 0,0005 = 0,00985 → 0,01.
Comisiones acumuladas: 0,01 desde t=7, 0,02 desde t=8, 0,03 desde t=13. El PnL acumulado es equity − 1.000,00 del fixture.
Lo que hay que ver en la tabla:
- Warmup escalonado (t = 0…5):
wma2arranca en t=1,wma4yrawen t=3,hmaen t=4, y la decisión en t=6 por ellag. Seis barras de calentamiento paraperiod = 4;period − 1 = 3lo subestimaría en tres. - Barra 6:
10,9333 > 10,6300(contrahma[4], dos barras atrás) → +1. La orden se llena al open de 7 (10,80). - Barra 7:
10,6389 < 10,9244→ reversión. El largo ha durado una sola barra y el fill del open de 8 vende 2 unidades. - Barra 12:
9,6944 > 9,6622→ reversión a largo por un margen estrecho; compra 2 a 9,85 en el open de 13. - Barra 15: la señal +1 coincide con la posición vigente: no hay delta ni fill (no es el caso "última señal sin fill", sino "sin cambio"). El largo queda abierto; no hay liquidación automática.
Este fixture no distingue open[t+1] de close[t]
Como open[t] = close[t−1] por construcción, llenar al cierre de la señal daría los mismos precios. La especificación lo advierte: el test de next-open de P18 usa otro fixture (cierre 100 → apertura 110). Aquí el orden temporal se ve en la columna fill, que siempre cae una fila por debajo de la orden.
Las cuentas de las barras clave¶
WMA con pesos ascendentes (máximo peso al dato reciente):
wma2[t] = (1·C[t-1] + 2·C[t]) / 3
wma4[t] = (1·C[t-3] + 2·C[t-2] + 3·C[t-1] + 4·C[t]) / 10
Primera HMA (t=4):
wma2[3] = (10,10 + 2·10,40)/3 = 30,90/3 = 10,300000
wma4[3] = (10,00 + 20,40 + 30,30 + 41,60)/10 = 102,30/10 = 10,230000
raw[3] = 2·10,300000 - 10,230000 = 10,370000
wma2[4] = (10,40 + 2·10,70)/3 = 31,80/3 = 10,600000
wma4[4] = (10,20 + 20,20 + 31,20 + 42,80)/10 = 104,40/10 = 10,440000
raw[4] = 2·10,600000 - 10,440000 = 10,760000
hma[4] = (1·10,370000 + 2·10,760000)/3 = 31,890000/3 = 10,630000
Comprobación de pesos: con pesos DESCENDENTES wma4[3] sería
(4·10,00 + 3·10,20 + 2·10,10 + 1·10,40)/10 = 10,120000 (otra curva)
Entrada (t=6):
raw[5] = 2·10,833333 - 10,660000 = 11,006667
raw[6] = 2·10,833333 - 10,770000 = 10,896667
hma[5] = (10,760000 + 2·11,006667)/3 = 10,924444
hma[6] = (11,006667 + 2·10,896667)/3 = 32,800000/3 = 10,933333
10,933333 > hma[4] = 10,630000 -> +1 ENTRY
Reversión (t=7):
raw[7] = 2·10,600000 - 10,690000 = 10,510000
hma[7] = (10,896667 + 2·10,510000)/3 = 10,638889
10,638889 < hma[5] = 10,924444 -> -1 REVERSAL (delta 2)
Caja (sin costes):
1.000,00 - 1·10,80 = 989,20 (fill 1, open[7])
989,20 + 2·10,50 = 1.010,20 (fill 2, open[8])
1.010,20 - 2·9,85 = 990,50 (fill 3, open[13])
Equity final: 990,50 + 1·10,70 = 1.001,20 (+1 abierto)
Trades: largo 10,80 -> 10,50 = -0,30; corto 10,50 -> 9,85 = +0,65;
largo abierto 9,85 -> 10,70 = +0,85 no realizado
Fíjate en t=6: hma[6] > hma[5] por muy poco (10,9333 frente a 10,9244), pero la regla no mira la barra anterior sino hma[4]. Con lag = 3 la comparación sería contra hma[3], que es NaN, y la primera decisión se iría a t=7.
Diagrama¶
flowchart LR
W(["WARMUP: t < first_valid(hma) + lag"]) -->|"hma[t] == hma[t-lag]"| FLAT["Plano 0"]
W -->|"hma[t] > hma[t-lag] · BUY 1"| L["Largo +1"]
W -->|"hma[t] < hma[t-lag] · SELL 1"| S["Corto -1"]
FLAT -->|"sube · BUY 1"| L
FLAT -->|"baja · SELL 1"| S
L -->|"hma[t] < hma[t-lag] · SELL 2"| S
S -->|"hma[t] > hma[t-lag] · BUY 2"| L
L -->|"hma[t] >= hma[t-lag]"| L
S -->|"hma[t] <= hma[t-lag]"| S
El plano solo existe al principio y solo si hay empate exacto, que en la práctica exige una serie de cierres constantes (test test_p18_flat_series_holds_zero: HMA constante, objetivo 0 para siempre, ningún fill). Todos los fills de la órbita largo↔corto son de 2 unidades.
Cómo lanzarla¶
Backtest con ambos motores sobre KO 1d con los defaults (forma "inline" de la estrategia en la API de backtesting; el cuerpo sigue a examples/api/01_backtest_p01_both.request.json). En diario, 62 barras de warmup son unos tres meses:
BASE=http://localhost:8000
SID=$(curl -sS -X POST "$BASE/v1/backtest" -H 'Content-Type: application/json' -d '{
"request_id": "manual-p18-0001",
"correlation_id": null,
"schema_version": "v1",
"payload": {
"strategy": {"template_id": "P18", "params": {"period": 55, "lag": 2},
"name": "Hull 55 lag 2 base", "register": true},
"data": {"asset_id": "KO", "timeframe": "1d",
"window": {"start": "2021-01-01T00:00:00Z", "end": "2025-01-01T00:00:00Z",
"mode": "reset_flat"}},
"execution": {"lane": "SIM-S", "contract_version": "1", "initial_cash": 100000,
"target_lots": [-1, 0, 1], "cost_scenario": "hypothetical_5bps"},
"engine": "both",
"cache": {"cache_mode": "WARM_FEATURES", "write_through": true},
"output": {"output_mode": "metrics", "include_timings": true,
"include_parity": true, "tail_rows": 5},
"engine_options": {"vectorta": {"kernel": "scalar", "use_batch": true},
"nautilus": {"adapter": "NT_FEATURES"}}
}}' | jq -r '.payload.signature.strategy_id')
echo "$SID"
import uuid, requests
BASE = "http://localhost:8000"
env = lambda payload: {"request_id": str(uuid.uuid4()), "correlation_id": None,
"schema_version": "v1", "payload": payload}
payload = {
"strategy": {"template_id": "P18", "params": {"period": 55, "lag": 2},
"name": "Hull 55 lag 2 base", "register": True},
"data": {"asset_id": "KO", "timeframe": "1d",
"window": {"start": "2021-01-01T00:00:00Z",
"end": "2025-01-01T00:00:00Z", "mode": "reset_flat"}},
"execution": {"lane": "SIM-S", "contract_version": "1", "initial_cash": 100000,
"target_lots": [-1, 0, 1], "cost_scenario": "hypothetical_5bps"},
"engine": "both",
"output": {"output_mode": "metrics", "include_parity": True},
"engine_options": {"vectorta": {"kernel": "scalar", "use_batch": True},
"nautilus": {"adapter": "NT_FEATURES"}},
}
r = requests.post(f"{BASE}/v1/backtest", json=env(payload)).json()["payload"]
SID = r["signature"]["strategy_id"]
print(SID, r["parity"]["verdict"])
for res in r["results"]:
print(res["adapter"], res["run_metrics"]["final_equity"], res["run_metrics"]["n_fills"])
Espera en run_metrics.warnings el aviso DATA_GAP_SESSIONS (KO tiene 271 sesiones ausentes; las ventanas WMA cruzan el hueco).
Derivar una variante con lag = 3:
curl -sS -X POST "$BASE/v1/strategies/$SID/derive" -H 'Content-Type: application/json' -d '{
"request_id": "manual-p18-derive", "correlation_id": null, "schema_version": "v1",
"payload": {"param_overrides": {"lag": 3}, "name": "Hull 55 lag 3",
"slug": "hull-55-lag-3", "tags": ["sensibilidad"]}}' | jq '.payload.strategy_id, .payload.cache_forecast'
lag es parámetro de política: según la tabla de invalidación §5.3 de la especificación, la curva hma completa y sus dos wma se reutilizan (acierto de caché) y solo se rehacen política y ledger. El cache_forecast debe listar hma como reutilizable y ningún fallo esperado, igual que el derivado P02-bc0dd2 de P02 con sus umbrales. Si en cambio derivas period: 89, es parámetro de feature: hay que calcular wma(44), wma(89), la wma(9) interna y hma(89); entre dos period distintos no se comparte nada salvo coincidencias de floor(p/2).
Sweep de sus variant_keys (rejilla completa del catálogo, con la forma de 03_sweep_p01_4x4.request.json):
curl -sS -X POST "$BASE/v1/sweep" -H 'Content-Type: application/json' -d '{
"request_id": "manual-p18-sweep", "correlation_id": null, "schema_version": "v1",
"payload": {"base_strategy_id": "'"$SID"'",
"grid": {"period": [16, 32, 55, 89, 144], "lag": [1, 2, 3]},
"candidate_policy": {"drop_invalid": true, "drop_duplicates": true,
"register_strategies": true, "name_template": "{template_name} {period}/{lag}"},
"data": {"asset_id": "KO", "timeframe": "1d",
"window": {"start": "2021-01-01T00:00:00Z", "end": "2025-01-01T00:00:00Z", "mode": "reset_flat"}},
"execution": {"lane": "SIM-S", "contract_version": "1", "initial_cash": 100000,
"cost_scenario": "hypothetical_5bps"},
"engine": "vectorta", "cache": {"cache_mode": "OPT_FULL"},
"output": {"output_mode": "metrics"},
"engine_options": {"vectorta": {"kernel": "scalar", "use_batch": true}}}}' | jq '.payload.job_id, .payload.factorization_plan'
Según §5.4 de la especificación: 15 candidatos cartesianos, 15 válidos, 5 curvas hma (una por period) y 9 curvas wma. El resultado se consulta en GET /v1/jobs/{job_id}.
Cuánto cuesta la rejilla¶
| Concepto | Cuenta |
|---|---|
| Candidatos cartesianos | 5 period × 3 lag = 15 |
| Inválidos | 0 (todos cumplen period ≥ 4 y lag ≥ 1) |
Curvas hma distintas |
5: lag no entra en la FeatureSpec, factor 3× |
Curvas wma sobre close |
{8, 16, 27, 32, 44, 55, 72, 89, 144} = 9, no 10: wma(16) sirve a hma(16) (como wma(period)) y a hma(32) (como wma(floor(32/2))) |
| Cálculos de features | 9 wma + 5 kernels hma = 14 para 15 candidatos |
| Memoria de features en 1min (632.000 barras) | 14 × 5,06 MB ≈ 70,8 MB |
La trampa de memoria de P18
Calcular la WMA como (ventanas * pesos).sum(axis=1) sobre sliding_window_view con period = 144 y 632.000 barras materializa un temporal de 632.000 × 144 × 8 bytes ≈ 728 MB, casi todo el presupuesto de 1 GB por worker. La especificación fija np.convolve como implementación canónica y prohíbe la recurrencia acumulativa (rompe la propiedad de ventana finita).
Qué mirar en los resultados¶
parity.verdictconNT_FEATURES: PASS esperado en features (hmaconrtol 1e-10), señales, fills y equity, como en la matriz (12/12).- Fills de
qty == 2frente al total: deben ser todos menos el primero. Es métrica de comportamiento y de corrección a la vez. - Exposición neta: ±1 en el 100 % de las barras operables posteriores a
first_decision_index. Cualquier 0 es un fallo. - Número de reversiones y duración media del tramo:
lagyperiodgrandes suavizan y reducen reversiones; con 5 bps cada reversión paga dos unidades. - Barras descartadas por calentamiento por candidato: compara la rejilla desde un inicio común ≥ 157.
- Retraso de la HMA frente al precio: la especificación propone medirlo (correlación cruzada) en vez de asumir que "adelanta".
La divergencia nativa, leída con calma¶
En runs/parity/KO_1d_P18_5bps_NT_ONLINE_native.md: features PASS (1297 valores comparados), señales PASS (1295), fills FAIL en el fill nº 0, barra 63: signal_index 62 en el banco (VectorTA) frente a 56 en Nautilus nativo. La tabla de contexto del informe muestra por qué: el HullMovingAverage nativo ya publica valores en las barras 58–59, cuando la HMA canónica aún es NaN (su first_valid con period = 55 es 60), y desde la barra 60 los dos valores son idénticos. El lado nativo, por tanto, empieza a operar antes; las comparaciones de señales tras el warmup coinciden, pero el primer fill cae en otra barra y toda la contabilidad queda desplazada.
Es la lectura que da el propio diagnóstico: "las señales coinciden y los fills no: la divergencia es de ejecución o contabilidad, no de decisión", y el carril que la aísla es NT_INTENT_REPLAY, que reproduce las intenciones del banco y da PASS 12/12. Coincide con lo que la especificación preveía: la HMA no tiene semilla (son ventanas finitas), así que tras el warmup Nautilus coincide bit a bit, no asintóticamente; la diferencia está en el tratamiento del calentamiento.
Causalidad comprobada¶
La especificación define dos tests. El genérico de "futuros alterados": serie de 500 barras, close[350:] multiplicado por 2, y se exige que wma_half, wma_full, raw, hma y los objetivos hasta el índice 300 sean idénticos bit a bit, igual que los fills con fill_index ≤ 300. Y el propio de P18, más fuerte: perturbar solo close[350] y comprobar que la curva vuelve a ser idéntica desde 350 + period + m − 1 (411 con los defaults). Una recursión colada por error haría fallar este segundo test.
Para reanudar en modo continue el checkpoint son 64 números con defaults (period cierres + m − 1 valores de raw + lag valores de hma + el objetivo): el mayor del grupo, pero exacto por construcción, sin aproximación de convergencia.
Errores típicos¶
| Error | Consecuencia | Cómo evitarlo |
|---|---|---|
lag = 0 |
PolicyParamError: hma[t] == hma[t] siempre, el objetivo quedaría en 0 para siempre |
Usa lag ≥ 1; en un sweep se cuenta como inválido |
period < 4 |
PolicyParamError: con floor(p/2) = 1 la WMA corta es el propio cierre y la HMA degenera |
Usa period ≥ 4 |
round(n/2) en vez de floor(n/2) |
Otra curva en todos los period impares (55 → 28 en vez de 27) |
División entera |
int(sqrt(n)) en vez de round_half_up |
Otra curva en period = 32 (5 en vez de 6) y otro first_valid |
floor(x + 0,5) o Decimal con ROUND_HALF_UP |
| Pesos WMA descendentes | Media retrasada; la HMA gira al revés en los giros | wma([10,20,30], 3) = 23,3333, no 16,6667 |
Sumar los first_valid de las dos ramas |
26 barras buenas descartadas con period = 55 |
Máximo para ramas paralelas, suma solo para la cadena |
Olvidar m − 1 en el warmup |
Señales prematuras | first_valid(hma) = (p−1) + (m−1) |
Meter lag en la FeatureSpec |
Se pierde el factor 3× de caché | lag es de política |
| En la versión online, no avanzar el buffer en barras NaN o no operables | El "hace lag barras" pasa a ser "hace lag barras válidas": divergencia permanente |
append incondicional en cada barra cerrada |
Variantes y extensiones¶
- EHMA / THMA: variantes exponencial y triple de la Hull Suite; el catálogo las excluye explícitamente y
vector_ta.ehmaes otro indicador. - Solo largos:
target_lots = [0, 1]con la regla−1 → 0. - Filtro de fuerza: exigir
|hma[t] − hma[t−lag]|mayor que una fracción del ATR antes de invertir; reduce reversiones en lateral, pero es otra receta. - Parientes: P01 (dos EMA por nivel), P16 (EMA con banda), P14 y P19 (otras políticas de dirección continua).
Resumen¶
- P18 calcula la HMA (
WMA(2·WMA(C, floor(n/2)) − WMA(C, n), round_half_up(√n))) y comparahma[t]conhma[t−lag]: +1 si sube, −1 si baja, empate conserva. - Es de dirección continua: tras la entrada, cada fill es una reversión de 2 unidades.
- El warmup combina máximo (ramas paralelas), suma (cadena) y
lag: 62 barras con defaults, 157 en el peor candidato. - 15 candidatos → 5
hmay 9wma, porquelages de política ywma(16)se comparte. - Paridad PASS salvo el carril nativo, que falla en fills por un calentamiento distinto, no por la curva.
Para practicar¶
- Con el fixture, calcula a mano
hma[8]a partir deraw[7] = 10,5100yraw[8] = 10,1200, comprueba 10,2500 y decide el objetivo de t=8 contrahma[6]. - Repite el fixture con
lag = 1: ¿en qué barra es la primera decisión? ¿Cambia alguna señal entre t=6 y t=15? Rehaz la caja y la equity final. - Lanza el sweep 5×3 sobre KO 1d y NVDA 1d. Compara el número de reversiones de
(16, 1)y(144, 3)y el efecto de los 5 bps en cada uno.