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 (1min … 1d); 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 aj. - 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
ppequeñ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.
pgrande: conp = 13hacen 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
jporque "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
2pel 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):
- 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"). - Evaluar rupturas con
close[t], estrictas:close > swing_highoclose < swing_low. - Aplicar: la ruptura fija el objetivo y consume el nivel roto (pasa a
Nonehasta 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 j ⟺ H[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 esofirst_valides2py no "el primer pivote real". - Ventana finita y simétrica, sin recursión: un
highmalo contamina exactamente2p + 1ventanas y desaparece. Las últimaspbarras 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:
- 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". - Desfase
p = 2en 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 faltabanH[3]yH[4]. - t = 9:
11,00 > 10,95→ +1 y el nivel se consume. En t=10 no haysh: ninguna ruptura alcista es posible hasta que t=11 lo repuebla. - t = 14:
9,15 < 9,40→ reversión, delta 2. - 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".
- t = 21:
9,00 < 9,05rompe 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.verdictconNT_FEATURES: PASS esperado (12/12 en la matriz).NT_ONLINEusa unaStreamingFeaturepropia con búfer circular de2p + 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
ptiene 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/lowentre vecinos en datos reales (frecuentes: no hay resta de por medio). DATA_GAP_SESSIONSen 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
nbarras, 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
jexigeH[j]estrictamente mayor que suspvecinos a cada lado y solo se conoce enj + 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¶
- Con el fixture, comprueba a mano que
j = 9es pivote alto y quej = 10no lo es. ¿En qué barra se publica el dej = 9y por qué no puede romperse en esa barra? - 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). - 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épdeja más barras en 0 al principio?