Saltar a contenido

P20 · SMC / BOS con pivotes confirmados

Campo Valor
Familia Sesión-estructura · estructura de mercado: ruptura de estructura (break of structure, BOS) sobre niveles confirmados que se consumen al romperse. Dirección continua solo después de la primera ruptura
Features requeridas confirmed_pivots (salidas pivot_high_level, pivot_low_level: eventos puntuales, NaN salvo en la barra de confirmación). El último swing no consumido es estado de política
Parámetros y defaults pivot_half_window = 3 (en adelante p)
Rejilla del catálogo pivot_half_window ∈ {2, 3, 5, 8, 13} → 5 candidatos válidos, 5 curvas de pivotes (factor 1×)
variant_keys pivot_half_window
Restricciones entero p ≥ 1 (si no, PolicyParamError); no hay parámetros de política
first_decision_index 2p = 6 con defaults (rejilla: 4, 6, 10, 16, 26)
Activos y timeframes NVDA y KO en los 7 TF (1min1d); AMZN, INTC, MSFT, TSLA en 1d; BME y FX tras el calendario de WP-01b. Usa high, low (feature) y close (política)
Paridad en la matriz PASS en NT_FEATURES, NT_INTENT_REPLAY y NT_ONLINE(vectorta) (12/12 cada una). UNSUPPORTED en NT_ONLINE(native) (12/12): "nautilus_trader 1.231.0 no ofrece un indicador nativo para 'confirmed_pivots'"

Fuentes: docs/estrategias/P20.md (especificación), catalog/strategy_catalog.json, docs/DATOS_POR_ESTRATEGIA.md, runs/parity/summary.json.

Qué es y qué no es

El catálogo es explícito: "La receta se inspira en ruptura de estructura: no reproduce el toolkit LuxAlgo, ni infiere actividad institucional, order blocks o liquidez oculta. Es útil para detectar repainting y fallos de timestamp." P20 está en el catálogo sobre todo como test de causalidad. Este capítulo no atribuye ningún significado institucional a los pivotes.

Qué vas a aprender

  • Qué es un pivote confirmado y por qué su evento pertenece a la barra j + p, nunca a j.
  • Qué es el repainting y cómo un solo índice mal puesto convierte una estrategia honesta en una trampa.
  • Por qué el arrastre y el consumo de niveles viven en la política y no en la feature.
  • A distinguir "hubo ruptura" de "hubo orden" (una ruptura del mismo lado no genera fill).
  • Por qué el checkpoint de P20 es el mejor ejemplo del catálogo de estado de política irreductible.

La idea en una frase

Guarda el último máximo local confirmado y el último mínimo local confirmado; si un cierre supera el máximo, ponte largo; si perfora el mínimo, ponte corto; el nivel roto se gasta.

Intuición de mercado

En una tendencia alcista, el precio va dejando máximos y mínimos locales cada vez más altos. Cuando un cierre supera el último máximo relevante, la "estructura" se confirma al alza; cuando perfora el último mínimo relevante, se ha roto a la baja. Es la lectura de gráfico más antigua del análisis técnico, formalizada con dos reglas objetivas: qué cuenta como máximo local (estrictamente mayor que sus p vecinos a cada lado) y cuándo se sabe (solo cuando existen esos p vecinos a la derecha).

Analogía. Un árbitro de salto de altura: no puede decir que una marca es "el récord del día" hasta que los siguientes saltadores han intentado superarla. El récord se anota cuando se sabe, no cuando se saltó. Y cuando alguien lo supera, esa marca ya no es el listón: se "consume".

Por qué podría funcionar. Rupturas de niveles visibles atraen órdenes (stops de quienes estaban en contra y entradas de quienes esperaban la confirmación), y eso puede prolongar el movimiento.

Cuándo falla.

  • Falsas rupturas: el cierre supera el nivel por unos céntimos y vuelve; con p pequeño hay muchos niveles cercanos y muchas trampas.
  • Rango: en lateral, los pivotes alternan cerca unos de otros y cada BOS contrario es una reversión de delta 2.
  • p grande: con p = 13 hacen falta 27 barras para formar un pivote; en series cortas puede no haber ninguna señal.

Sesgos típicos al evaluarla.

  • Repainting: dibujar el pivote en j porque "en el gráfico se ve ahí". Es exactamente lo que la receta existe para detectar.
  • Ajuste por dividendos: la regla compara niveles absolutos guardados muchas barras antes. Un ajuste entre el pivote y su ruptura mezcla dos escalas; por eso raw_split_adjusted.
  • Contar el 0 inicial como warmup: tras 2p el objetivo puede seguir en 0 durante mucho tiempo; es la decisión legítima "no hay estructura rota todavía" (HOLD), no calentamiento.

Las reglas exactas

La regla canónica del catálogo: "Un pivote alto en j requiere H[j] estrictamente mayor que los p vecinos a izquierda y derecha; solo está disponible al cierre j+p. Pivote bajo simétrico. Guardar el último swing confirmado no consumido. Objetivo +1 si un cierre rompe el último swing alto; −1 si rompe el bajo. Consumir nivel roto; mantener hasta BOS contrario."

Y sus engineering_notes: "El evento pertenece a j+p, nunca a j. Empates no producen pivote."

Orden de operaciones dentro de la barra t (fijado por la especificación):

  1. Registrar los pivotes publicados en t: un nivel nuevo sustituye al anterior no consumido, aunque sea más bajo ("el último", no "el máximo").
  2. Evaluar rupturas con close[t], estrictas: close > swing_high o close < swing_low.
  3. Aplicar: la ruptura fija el objetivo y consume el nivel roto (pasa a None hasta que un pivote nuevo lo repueble).
Situación en el cierre de t Objetivo target[t] Motivo (reason)
t < 2p 0 WARMUP
sin ruptura (incluye close == nivel) el anterior (0 antes de la primera ruptura) HOLD
close[t] > swing_high +1; consume swing_high ENTRY / REVERSAL / HOLD
close[t] < swing_low −1; consume swing_low ENTRY / REVERSAL / HOLD
ambas a la vez (exige swing_high < swing_low) +1 (precedencia alcista); consume los dos ENTRY / REVERSAL / HOLD
close[t] NaN el anterior (los pivotes sí se registran) HOLD
barra no operable (is_tradable = False) se congela el publicado; el estado interno avanza NOT_TRADABLE
  • Entrada: la primera ruptura, en cualquier dirección.
  • Salida: no hay salida a plano; solo un BOS contrario cambia la posición.
  • Reversión: directa, delta 2.
  • Ruptura del mismo lado: consume el nivel pero no genera orden.
# decisión en el cierre de t, fill en el open de t+1
target = 0; swing_high = None; swing_low = None
para cada barra t >= 2p:
    si pivot_high_level[t] no es NaN: swing_high = pivot_high_level[t]   # = H[t-p]
    si pivot_low_level[t]  no es NaN: swing_low  = pivot_low_level[t]    # = L[t-p]
    up   = swing_high no es None y close[t] > swing_high
    down = swing_low  no es None y close[t] < swing_low
    si up y down:  target = +1; swing_high = None; swing_low = None
    si no, si up:  target = +1; swing_high = None
    si no, si down: target = -1; swing_low = None
    si target cambió: orden(delta) -> fill a open[t+1]

Un pivote no puede romperse en su propia barra de confirmación

Si j es pivote alto, H[j] > H[j+p] (es uno de sus vecinos) y siempre C[j+p] ≤ H[j+p]. Luego C[j+p] < H[j]: la ruptura es imposible en la barra de confirmación. Por eso el orden registrar→romper no puede crear señales espurias del mismo signo; sí importa para un pivote del signo contrario confirmado en la misma barra.

Los indicadores que usa

Feature Fórmula first_valid
confirmed_pivots{p} pivote alto en jH[j] > H[j±i] para todo i ∈ 1..p (estricto); se publica pivot_high_level[j+p] = H[j]. Bajo simétrico con L y <. NaN en el resto de barras 2p

first_decision_index = 2p: el primer j pivotable es p (necesita p vecinos a la izquierda) y su confirmación llega en j + p = 2p. La política usa pivot_*_level[t] y close[t] del mismo índice, sin desplazamiento propio. Con defaults, 6.

Dos particularidades:

  • NaN es el valor normal: dentro de valid_mask (t ≥ 2p), un NaN significa "no hubo pivote aquí", no "aún no sé". Por eso first_valid es 2p y no "el primer pivote real".
  • Ventana finita y simétrica, sin recursión: un high malo contamina exactamente 2p + 1 ventanas y desaparece. Las últimas p barras de una serie nunca reciben confirmación; no es un fallo.

vector_ta.pivot es otro indicador

En VectorTA, pivot / PivotStream son los puntos pivote clásicos (P = (H+L+C)/3 de la sesión anterior, S1, R1…). Comparten nombre y nada más. confirmed_pivots es una primitiva propia (custom_reference) y la especificación exige un test "negativo" que registre la homonimia.

Detalle de la primitiva en Las 23 primitivas de fase B; el reparto feature/política, en Indicadores y features.

Ejemplo barra a barra

Fixture §6 de docs/estrategias/P20.md: 22 barras, p = 2 (dentro de la rejilla, sin aviso), first_decision_index = 4. cash_inicial = 1.000,00, tick 0,01, sin costes. Pivotes detectados en §6.2: alto en j=2 (10,95, publicado en t=4), bajo en j=6 (9,40, t=8), alto en j=9 (11,10, t=11), bajo en j=14 (9,05, t=16), alto en j=17 (10,70, t=19). Las barras 0–2 son warmup como la 3.

sh/sl son los niveles tras la barra (registro y consumo incluidos); "reg." indica el nivel publicado por la feature en esa barra.

t O H L C reg. sh sl target orden fill pos. PnL acum. sin costes equity con 5 bps*
3 10,35 10,60 10,00 10,15 0 WARMUP 0 0,00 1.000,00
4 10,15 10,40 9,75 9,85 alto 10,95 (j=2) 10,95 None 0 HOLD 0 0,00 1.000,00
5 9,85 10,05 9,55 9,60 10,95 None 0 HOLD 0 0,00 1.000,00
6 9,60 9,85 9,40 9,75 10,95 None 0 HOLD 0 0,00 1.000,00
7 9,75 10,15 9,60 10,10 10,95 None 0 HOLD 0 0,00 1.000,00
8 10,10 10,50 9,95 10,45 bajo 9,40 (j=6) 10,95 9,40 0 HOLD 0 0,00 1.000,00
9 10,50 11,10 10,40 11,00 None (consumido) 9,40 +1 ENTRY BUY 1 0 0,00 1.000,00
10 10,70 10,80 10,30 10,40 None 9,40 +1 HOLD 10,70 +1 −0,30 999,69
11 10,40 10,50 10,00 10,10 alto 11,10 (j=9) 11,10 9,40 +1 HOLD +1 −0,60 999,39
12 10,10 10,20 9,70 9,75 11,10 9,40 +1 HOLD +1 −0,95 999,04
13 9,75 9,85 9,35 9,45 11,10 9,40 +1 HOLD +1 −1,25 998,74
14 9,45 9,55 9,05 9,15 11,10 None (consumido) −1 REVERSAL SELL 2 +1 −1,55 998,44
15 9,50 9,90 9,40 9,80 11,10 None −1 HOLD 9,50 −1 −1,50 998,48
16 10,00 10,30 9,80 10,20 bajo 9,05 (j=14) 11,10 9,05 −1 HOLD −1 −1,90 998,08

Continuación (se muestran también las barras 17–21 porque contienen los dos casos que definen la receta: la sustitución a la baja y la ruptura sin orden):

t O H L C reg. sh sl target orden fill pos. PnL acum. sin costes equity con 5 bps*
17 10,40 10,70 10,20 10,60 11,10 9,05 −1 HOLD −1 −2,30 997,68
18 10,30 10,40 9,90 10,00 11,10 9,05 −1 HOLD −1 −1,70 998,28
19 10,00 10,10 9,60 9,70 alto 10,70 (j=17) 10,70 (sustituye a 11,10) 9,05 −1 HOLD −1 −1,40 998,58
20 9,65 9,75 9,25 9,35 10,70 9,05 −1 HOLD −1 −1,05 998,93
21 9,35 9,45 8,95 9,00 10,70 None (consumido) −1 HOLD (BOS sin orden) −1 −0,70 999,28

Todo es literal del fixture (§6.1 OHLC, §6.2 feature, §6.3 estado de política y §6.5 fills y 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=10): 1 · 10,70 · 0,0005 = 0,00535 → 0,01;
  • fill 2 (t=15): 2 · 9,50 · 0,0005 = 0,0095 → 0,01.

Comisiones acumuladas: 0,01 desde t=10, 0,02 desde t=15. El PnL acumulado es equity − 1.000,00 del fixture.

Lo que hay que ver en las tablas:

  1. t = 4: primera barra de decisión con un nivel ya registrado y objetivo 0 legítimo (HOLD, decision_index_valid = True). No es warmup: es "no hay estructura rota".
  2. Desfase p = 2 en los cinco pivotes: el máximo de la barra 2 se conoce en la 4; el de la 9, en la 11. En la barra 2 la feature publica NaN porque aún faltaban H[3] y H[4].
  3. t = 9: 11,00 > 10,95 → +1 y el nivel se consume. En t=10 no hay sh: ninguna ruptura alcista es posible hasta que t=11 lo repuebla.
  4. t = 14: 9,15 < 9,40 → reversión, delta 2.
  5. t = 19: el pivote nuevo (10,70) sustituye a 11,10, que nunca se rompió. El listón baja: se guarda "el último", no "el máximo".
  6. t = 21: 9,00 < 9,05 rompe y consume el nivel bajo, pero el objetivo ya era −1: hay BOS y no hay orden.

Este fixture sí distingue open[t+1] de close[t]

A diferencia de los de P18 y P19, aquí los dos fills caen en barras con gap: se compra a open[10] = 10,70, no a close[9] = 11,00; se vende a open[15] = 9,50, no a close[14] = 9,15. Es el único fixture del grupo que comprueba el next-open directamente.

Las cuentas de las barras clave

Pivote alto j=2 (p=2): H[2] = 10,95 frente a H[0]=10,35, H[1]=10,70, H[3]=10,60, H[4]=10,40
                       los cuatro estrictos -> pivote; se publica en j+p = 4
                       L[2] = 10,30 < L[0] = 9,85 ? No -> no es pivote bajo
Barra 4:  registra sh = 10,95;  9,85 > 10,95 ? No  -> target 0, HOLD
          (C[4] <= H[4] = 10,40 < H[2] = 10,95: romper aquí es imposible)
Pivote bajo j=6: L[6] = 9,40 frente a 9,75, 9,55, 9,60, 9,95 -> pivote, publicado en 8
Barra 9:  11,00 > sh = 10,95 (por 0,05) -> +1 ENTRY, sh = None
          fill en open[10] = 10,70 -> caja 1.000,00 - 10,70 = 989,30
Barra 14: 9,15 < sl = 9,40 -> -1 REVERSAL, sl = None
          fill en open[15] = 9,50, SELL 2 -> caja 989,30 + 2·9,50 = 1.008,30
Barra 19: pivote alto j=17: H[17] = 10,70 frente a 9,90, 10,30, 10,40, 10,10 -> publicado en 19
          sh = 10,70 sustituye a 11,10 sin compararlos
Barra 21: 9,00 < sl = 9,05 -> ruptura, sl = None; target ya era -1 -> sin orden
Equity final: 1.008,30 + (-1)·9,00 = 999,30
Trades: largo 10,70 -> 9,50 = -1,20; corto abierto 9,50 -> 9,00 = +0,50 no realizado

Repainting, con números

La especificación (§8.1) mide el efecto de publicar el pivote en j en vez de en j + p sobre este mismo fixture: 12 de los 22 objetivos cambian y la equity final pasa de 999,30 a 1.000,00. Parece una mejora, pero lo es por no operar: el sesgo no tiene que parecer una ganancia "de trading" para ser un bug grave. El test test_p20_pivot_published_at_j_plus_p_not_j fija que pivot_high_level[2] es NaN y pivot_high_level[4] == 10,95, y que el desfase es exactamente p.

Diagrama

flowchart LR
    W(["WARMUP: t < 2p"]) -->|"t >= 2p"| FLAT["Plano 0 (HOLD)"]
    FLAT -->|"C > swing_high · BUY 1 · consume sh"| L["Largo +1"]
    FLAT -->|"C < swing_low · SELL 1 · consume sl"| S["Corto -1"]
    L -->|"C < swing_low y no C > swing_high · SELL 2 · consume sl"| S
    S -->|"C > swing_high · BUY 2 · consume sh (y sl si ambas)"| L
    L -->|"C > swing_high · consume sh, sin orden"| L
    S -->|"C < swing_low · consume sl, sin orden"| S
    FLAT -->|"sin ruptura"| FLAT

Cada estado lleva además dos "ranuras" (swing_high, swing_low) que se llenan con cada pivote publicado y se vacían al romperse. Las flechas que vuelven al mismo estado son las rupturas sin orden, como t=21 del fixture.

Cómo lanzarla

Backtest con ambos motores sobre NVDA 1d con el default (forma "inline" de la estrategia en la API de backtesting; el cuerpo sigue a examples/api/01_backtest_p01_both.request.json). En diario, p = 3 significa que un pivote se confirma tres sesiones después:

BASE=http://localhost:8000
SID=$(curl -sS -X POST "$BASE/v1/backtest" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p20-0001",
  "correlation_id": null,
  "schema_version": "v1",
  "payload": {
    "strategy": {"template_id": "P20", "params": {"pivot_half_window": 3},
                 "name": "BOS pivotes p3 base", "register": true},
    "data": {"asset_id": "NVDA", "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": "P20", "params": {"pivot_half_window": 3},
                 "name": "BOS pivotes p3 base", "register": True},
    "data": {"asset_id": "NVDA", "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"])

Derivar una variante con p = 5:

curl -sS -X POST "$BASE/v1/strategies/$SID/derive" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p20-derive", "correlation_id": null, "schema_version": "v1",
  "payload": {"param_overrides": {"pivot_half_window": 5}, "name": "BOS pivotes p5",
              "slug": "bos-pivotes-p5", "tags": ["sensibilidad"]}}' | jq '.payload.strategy_id, .payload.cache_forecast'

pivot_half_window es parámetro de feature (el único que tiene P20): según la tabla de invalidación §5.3 de la especificación, al pasar de 3 a 5 la ventana cambia de tamaño y hay que recalcular confirmed_pivots completa, además de política y ledger; solo se reutilizan datos, manifiesto y calendario. Espera confirmed_pivots como fallo de caché. P20 no tiene ningún parámetro de política con el que ver un acierto de caché de features; el único cambio que lo daría es el de costes (0 → 5 bps), que reutiliza features y decisiones y solo rehace el ledger.

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-p20-sweep", "correlation_id": null, "schema_version": "v1",
  "payload": {"base_strategy_id": "'"$SID"'",
    "grid": {"pivot_half_window": [2, 3, 5, 8, 13]},
    "candidate_policy": {"drop_invalid": true, "drop_duplicates": true,
                         "register_strategies": true, "name_template": "{template_name} p{pivot_half_window}"},
    "data": {"asset_id": "NVDA", "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: 5 candidatos, 5 válidos, 5 curvas de pivotes, factor de reutilización 1× (nada que compartir). Es la rejilla más pequeña del catálogo de populares. El resultado se consulta en GET /v1/jobs/{job_id}.

Cuánto cuesta la rejilla

Concepto Cuenta
Candidatos 5 (un solo parámetro)
Curvas confirmed_pivots 5 × 2 salidas = 10 arrays; en 1min (632.000 barras) ≈ 50,6 MB
Reutilización entre estrategias ninguna
Trampa de memoria construir los vecinos con np.concatenate materializa ≈ 263 MB con p = 13; la implementación canónica usa np.maximum de dos reducciones sobre vistas (pico ≈ 20 MB)
Warmup común de la rejilla 26 barras (p = 13)

Qué mirar en los resultados

Deuda declarada DEV-09D-04

La máscara del helper de confirmed_pivots no está cableada. Tenlo en cuenta al interpretar los pivotes confirmados. Ver docs/ESTADO_Y_REANUDACION.md §4.

  • parity.verdict con NT_FEATURES: PASS esperado (12/12 en la matriz). NT_ONLINE usa una StreamingFeature propia con búfer circular de 2p + 1; no hay stream de VectorTA aplicable ni nativo de Nautilus.
  • Número de pivotes confirmados (altos y bajos) por candidato: el primer diagnóstico de si p tiene sentido en esa serie.
  • BOS frente a cambios de objetivo: la diferencia son rupturas del mismo lado (t=21 del fixture). Sin esta métrica se confunden.
  • Barras en 0 tras 2p, antes de la primera ruptura: puede ser una fracción grande; una equity plana al principio no es un fallo.
  • Antigüedad del nivel al romperse (t − (j + p)): la métrica más informativa sobre qué hace realmente P20.
  • Sustituciones que bajan el listón y empates de high/low entre vecinos en datos reales (frecuentes: no hay resta de por medio).
  • DATA_GAP_SESSIONS en KO: habrá pivotes confirmados a caballo de huecos de varias sesiones; se reporta, no se filtra.

Causalidad comprobada

Además del test genérico (300 barras, high/low/close[250:] × 1,5; salidas de la feature, objetivos y fills idénticos hasta el índice 200), la especificación define tres específicos: el de publicación en j + p, el de perturbación local (subir solo H[4] de 10,40 a 11,00 anula el pivote de j=2 en la barra 4, pero no cambia nada antes de 4: "publicar con retraso" no es "usar el futuro") y el de contaminación finita (perturbar high[150] con p = 3 solo puede cambiar las publicaciones 150–156).

El checkpoint de continue son 15 números con defaults (4p valores de ventana, los dos niveles y el objetivo), pero los niveles swing_high/swing_low no se pueden derivar de las curvas de la feature: dependen de qué niveles ya se consumieron. Ni guardando todas las features basta. Además None (consumido) debe distinguirse de "ausente del checkpoint".

Errores típicos

Error Consecuencia Cómo evitarlo
pivot_half_window = 0 PolicyParamError: sin vecinos, toda barra sería pivote alto y bajo a la vez (repainting instantáneo) p ≥ 1
Publicar el pivote en j Repainting: 12 de 22 objetivos distintos en el fixture El evento pertenece a j + p
np.argmax(ventana) == p Falsos pivotes en empates (argmax devuelve el primer máximo) centro > max(vecinos sin el centro)
>= en la detección Mesetas generan pivotes solapados Empates no producen pivote
No consumir el nivel roto Un BOS por barra; reabre en cuanto cambia la dirección Nivel a None tras romper
Consumir solo uno si rompen ambos BOS fantasma en la barra siguiente Consumir los dos
Guardar el máximo en vez del último Otra estrategia (t=19 del fixture) Sustitución sin comparar
Saltarse barras no operables Pivotes perdidos para siempre Avanzar el estado en todas las barras; congelar solo la publicación
initialize() que solo pone el objetivo a 0 Un nivel de otro tramo se rompe en la primera barra Reiniciar objetivo y los dos niveles
Usar vector_ta.pivot Otro indicador (pivotes clásicos) Primitiva propia confirmed_pivots

Variantes y extensiones

  • Cola de swings no consumidos o "máximo en lugar de último": el catálogo no las pide y cambian la estrategia; necesitarían su propia entrada.
  • Filtro de huecos: descartar pivotes confirmados a caballo de un hueco usando gap_sessions_before; es otra receta, no un ajuste de esta.
  • Solo largos: target_lots = [0, 1] con −1 → 0.
  • Parientes: P11 Donchian (ruptura de canal de n barras, sin confirmación diferida), P12 (ruptura del rango de apertura), P19 Chandelier (extremos rodantes inclusivos, otra semántica: no debe compartir feature con P20).

Resumen

  • Un pivote alto en j exige H[j] estrictamente mayor que sus p vecinos a cada lado y solo se conoce en j + p.
  • La política guarda el último nivel alto y bajo no consumidos; un cierre que los rompe fija +1 o −1 y consume el nivel.
  • Arranca en 0 y puede quedarse ahí mucho tiempo (HOLD, no warmup); después, solo un BOS contrario la invierte con delta 2.
  • 5 candidatos, 5 curvas, ninguna reutilización: la rejilla más pequeña y barata del catálogo.
  • Paridad PASS en los carriles con feature canónica; Nautilus nativo no tiene nada equivalente (UNSUPPORTED).

Para practicar

  1. Con el fixture, comprueba a mano que j = 9 es pivote alto y que j = 10 no lo es. ¿En qué barra se publica el de j = 9 y por qué no puede romperse en esa barra?
  2. Rehaz las tablas publicando el pivote en j (repainting): ¿en qué barra entra ahora la estrategia y a qué precio? Compara con la cifra de la especificación (12 objetivos distintos, equity final 1.000,00).
  3. Lanza el sweep de 5 candidatos sobre NVDA 1d y KO 1d. Para cada p, compara número de pivotes, número de BOS y número de fills. ¿Qué p deja más barras en 0 al principio?