P11 · Donchian / Turtle simplificado¶
| Campo | Valor |
|---|---|
| Familia | Volatilidad-ruptura · ruptura de canal con dos canales asimétricos (entrada ancha, salida estrecha); alterna plano y posición, nunca invierte |
| Features requeridas | donchian{entry} y donchian{exit} (primitiva donchian, entradas high, low; salidas upper, lower); la política lee además la columna close |
| Parámetros y defaults | entry = 20, exit = 10 |
| Rejilla del catálogo | entry ∈ {20, 40, 55, 80, 100}, exit ∈ {5, 10, 20} → 15 solicitados, 14 válidos, 1 inválido, 7 canales distintos |
variant_keys |
entry, exit |
| Restricciones | enteros ≥ 1 y exit < entry (estricta; si no, PolicyParamError) |
first_decision_index |
entry = 20 con defaults (100 en el peor punto de la rejilla) |
| Activos y timeframes | NVDA y KO en los 7 TF (1min … 1d); AMZN, INTC, MSFT, TSLA en 1d; BME con reserva; FX solo con OHLC reparado |
| 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), capa features: primera divergencia en la barra 32 de KO 1d, donchian_entry.upper 49,73 frente a 49,17; variante vta_inclusive_shifted frente a nt_inclusive_window (el Donchian nativo de Nautilus incluye la barra actual en la ventana). Es una variante declarada, no un bug |
Fuentes: docs/estrategias/P11.md (especificación), catalog/strategy_catalog.json, docs/DATOS_POR_ESTRATEGIA.md, runs/parity/summary.json.
Qué vas a aprender¶
- Por qué el canal de Donchian de P11 usa las barras estrictamente anteriores y por qué, si incluyera la barra actual, la estrategia no operaría nunca.
- Qué significa "supera" y "rompe" en términos de desigualdades estrictas, y qué pasa cuando el cierre toca el canal.
- Por qué romper el canal de entrada contrario implica siempre romper el de salida, y por qué la salida tiene prioridad (sin reversión).
- A seguir en el fixture un largo, una salida, un corto y una señal final que no se ejecuta.
- Por qué la rejilla 5×3 da 14 candidatos y solo 7 canales.
La idea en una frase¶
Comprar cuando el cierre supera el máximo de las últimas entry barras, vender en corto cuando rompe el mínimo, y salir cuando el cierre rompe el canal más corto de exit barras en contra.
Intuición de mercado¶
Un máximo de 20 barras es una frontera que el mercado ha respetado durante un mes (en diario). Cuando el cierre la supera, algo ha cambiado: hay compradores dispuestos a pagar más que en todo el periodo. El sistema de las Tortugas de los años ochenta convirtió esa idea en reglas mecánicas; P11 conserva solo el núcleo: ruptura de canal para entrar, canal más estrecho para salir.
Analogía. Un río que lleva un mes sin salirse de su cauce. El día que el agua supera la marca más alta del mes, es probable que llueva más arriba y la crecida siga. Para decidir que la crecida acabó no esperas a que baje del mínimo del mes: te basta con que baje del mínimo de la última semana y media.
Por qué podría funcionar. Las rupturas capturan el inicio de tendencias largas; la asimetría entrada ancha/salida estrecha corta las pérdidas antes de devolver toda la ganancia.
Cuándo falla.
- Rangos con falsas rupturas: el cierre supera el máximo, se entra al open siguiente y el precio vuelve al canal.
- Gaps en la apertura siguiente: la señal es al cierre, el fill al
open[t+1], que puede estar muy por encima del canal. - Pocos trades: con
entry = 100sobre1dhay pocos trades por década; cualquier métrica es ruido.
Sesgos típicos al evaluarla.
- Confundir P11 con el sistema Turtle completo: no hay unidades múltiples, ni stop por N, ni filtro de "saltarse el trade tras uno ganador".
- Ejecutar al precio del canal: suponer un stop colocado en el máximo que nadie colocó.
- Canales que cruzan un hueco de datos (KO, 271 sesiones ausentes): el canal abarca años y suprime rupturas.
Las reglas exactas¶
La regla canónica del catálogo: "Canal de entrada: máximo H y mínimo L de las n barras estrictamente anteriores. Entrar largo si C supera el máximo y corto si rompe el mínimo. Salir largo al romper el mínimo de e barras anteriores; corto con máximo de e. Sin piramidación ni la regla histórica de saltarse trades ganadores."
Canales (spec §4.b): EU[t] = max(high[t−entry .. t−1]), EL[t] = min(low[t−entry .. t−1]), XU[t], XL[t] igual con exit. La barra t no entra.
Situación en el cierre de t |
Objetivo target[t] |
Motivo (reason) |
|---|---|---|
t < first_decision_index |
0 | WARMUP |
plano y C[t] > EU[t] |
+1 | ENTRY |
plano y C[t] < EL[t] |
−1 | ENTRY |
largo y C[t] < XL[t] |
0 | EXIT |
corto y C[t] > XU[t] |
0 | EXIT |
toque exacto (C == EU, C == XL…), sin condición o NaN |
el anterior | HOLD |
barra no operable (is_tradable = False) |
se congela el publicado | NOT_TRADABLE |
- Entrada: solo desde plano; las cuatro desigualdades son estrictas (tocar no es superar).
- Salida: prioritaria. Como
exit < entry, siempreEL[t] <= XL[t], así queC < EL(entrada corta) implicaC < XL(salida del largo). La barra que justificaría un corto primero cierra el largo. - Reversión: prohibida;
|delta|siempre 1. La entrada contraria llega, si procede, en la barra siguiente desde plano. - Sin piramidación ni memoria del resultado del trade anterior.
# decisión en el cierre de t, fill en el open de t+1
target_prev = 0
para cada barra t:
si t < entry: target = 0 # WARMUP
si no, si target_prev == +1: target = 0 si C[t] < XL[t] si no +1
si no, si target_prev == -1: target = 0 si C[t] > XU[t] si no -1
si no: # plano
si C[t] > EU[t]: target = +1
si no, si C[t] < EL[t]: target = -1
si no: target = 0
si target != target_prev:
orden(target - target_prev) -> fill a open[t+1] # nunca al precio del canal
target_prev = target
El canal excluye la barra que lo rompe
Si el máximo incluyera high[t], como C[t] <= high[t] siempre, C[t] > max sería imposible: cero trades. El desplazamiento vive en la feature (first_valid = period, no period − 1), no en la política. Es la advertencia de engineering_notes del catálogo y el test de causalidad específico de P11.
Los indicadores que usa¶
| Feature | Fórmula | first_valid |
|---|---|---|
donchian{p}.upper |
max(high[t−p .. t−1]) (rebanada abierta por la derecha) |
p |
donchian{p}.lower |
min(low[t−p .. t−1]) |
p |
first_decision_index = max(entry, exit) = entry (porque exit < entry) = 20 con defaults. El canal de salida es válido antes (en exit), pero no adelanta la primera decisión: con el estado en 0 las salidas son irrelevantes.
No confundir con rolling_max/rolling_min, que son inclusivas (first_valid = p − 1) y las usan P08, P10 y P19. Más detalle en Las 23 primitivas de fase B y en Indicadores y features.
Ejemplo barra a barra¶
Fixture §6 de docs/estrategias/P11.md: entry = 4, exit = 2 (fuera de rejilla a propósito; cumplen exit < entry), 16 barras, cash_inicial = 1.000,00, tick 0,01, sin costes. first_valid(donchian{4}) = 4, first_valid(donchian{2}) = 2 → first_decision_index = 4.
Barras 0–2 omitidas (canal de entrada NaN; en la 2 ya existe el de salida: XU 102,00, XL 99,00).
| t | O | H | L | C | EU | EL | XU | XL | target | orden | fill | pos. | PnL acum. sin costes | equity con 5 bps* |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 3 | 100,80 | 102,50 | 100,50 | 101,50 | NaN | NaN | 102,00 | 99,50 | 0 WARMUP | — | — | 0 | 0,00 | 1.000,00 |
| 4 | 101,20 | 103,00 | 101,00 | 102,50 | 102,50 | 99,00 | 102,50 | 99,50 | 0 HOLD (toque) | — | — | 0 | 0,00 | 1.000,00 |
| 5 | 102,50 | 105,00 | 102,00 | 104,00 | 103,00 | 99,50 | 103,00 | 100,50 | +1 ENTRY | BUY 1 | — | 0 | 0,00 | 1.000,00 |
| 6 | 104,50 | 106,00 | 103,00 | 105,00 | 105,00 | 99,50 | 105,00 | 101,00 | +1 HOLD | — | 104,50 | +1 | 0,50 | 1.000,45 |
| 7 | 103,50 | 104,00 | 101,00 | 102,00 | 106,00 | 100,50 | 106,00 | 102,00 | +1 HOLD | — | — | +1 | −2,50 | 997,45 |
| 8 | 101,50 | 102,00 | 99,00 | 100,00 | 106,00 | 101,00 | 106,00 | 101,00 | 0 EXIT | SELL 1 | — | +1 | −4,50 | 995,45 |
| 9 | 99,50 | 100,00 | 97,00 | 98,00 | 106,00 | 99,00 | 104,00 | 99,00 | −1 ENTRY | SELL 1 | 99,50 | 0 | −5,00 | 994,90 |
| 10 | 98,50 | 99,00 | 96,00 | 97,00 | 106,00 | 97,00 | 102,00 | 97,00 | −1 HOLD | — | 98,50 | −1 | −3,50 | 996,35 |
| 11 | 97,50 | 98,00 | 95,00 | 96,00 | 104,00 | 96,00 | 100,00 | 96,00 | −1 HOLD | — | — | −1 | −2,50 | 997,35 |
| 12 | 96,50 | 97,00 | 94,00 | 95,00 | 102,00 | 95,00 | 99,00 | 95,00 | −1 HOLD | — | — | −1 | −1,50 | 998,35 |
| 13 | 96,50 | 99,00 | 96,00 | 98,00 | 100,00 | 94,00 | 98,00 | 94,00 | −1 HOLD (toque) | — | — | −1 | −4,50 | 995,35 |
| 14 | 98,50 | 101,00 | 98,00 | 100,00 | 99,00 | 94,00 | 99,00 | 94,00 | 0 EXIT | BUY 1 | — | −1 | −6,50 | 993,35 |
| 15 | 100,50 | 103,00 | 100,00 | 102,00 | 101,00 | 94,00 | 101,00 | 96,00 | +1 ENTRY | BUY 1 (sin fill) | 100,50 | 0 | −7,00 | 992,80 |
OHLC, canales, objetivos, motivos, los cuatro fills (5→6 a 104,50; 8→9 a 99,50; 9→10 a 98,50; 14→15 a 100,50) y la equity en las barras 6–10, 12 y 15 son literales del fixture; el resto del PnL es caja + pos · close − 1.000,00 con la caja del fixture. *"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 en la barra del fill:
- fill 1 (barra 6):
104,50 · 0,0005 = 0,05225 → 0,05; - fill 2 (barra 9):
99,50 · 0,0005 = 0,04975 → 0,05; - fill 3 (barra 10):
98,50 · 0,0005 = 0,04925 → 0,05; - fill 4 (barra 15):
100,50 · 0,0005 = 0,05025 → 0,05.
Total 0,20; equity final 993,00 − 0,20 = 992,80. La orden BUY decidida en la barra 15 no se llena: no existe open[16].
Lo que hay que ver en la tabla:
- Barra 4:
C = 102,50 == EU = 102,50. Tocar no es superar: no entra. - Barra 5:
C = 104,00 > EU = 103,00 = max(high[1..4]). Con ventana inclusiva el canal sería 105,00 (elhigh[5]) y la entrada no existiría. - Barra 8:
enter_shortyexit_longciertos a la vez (la misma comparación:EL[8] = XL[8] = 101,00). Manda la salida: objetivo 0, no −1. El corto llega en la barra 9, desde plano, con una unidad por pata. - Barras 10–12:
C == ELexactamente (97, 96, 95): no serían entradas nuevas; el corto se mantiene porqueCno superaXU. - Barra 14: el simétrico de la 8 (
EU[14] = XU[14] = 99,00): sale del corto; la entrada larga llega en la 15 y se queda sin fill al final de la muestra. Equity final sin costes: 993,00 con posición 0.
Las cuentas de las barras clave¶
Primera decisión (t=4):
EU[4] = max(high[0..3]) = max(101,00; 102,00; 101,50; 102,50) = 102,50
EL[4] = min(low[0..3]) = min(99,00; 100,00; 99,50; 100,50) = 99,00
102,50 > 102,50 -> False -> 0 (HOLD)
Entrada (t=5):
EU[5] = max(high[1..4]) = max(102,00; 101,50; 102,50; 103,00) = 103,00
104,00 > 103,00 -> True -> +1
con ventana inclusiva: max(high[2..5]) = 105,00 ; 104,00 > 105,00 -> False
Salida con entrada contraria (t=8):
EL[8] = min(low[4..7]) = min(101,00; 102,00; 103,00; 101,00) = 101,00
XL[8] = min(low[6..7]) = min(103,00; 101,00) = 101,00
100,00 < 101,00 en ambos; estado +1 -> manda exit_long -> 0
Caja: 1.000,00 − 104,50 = 895,50 (fill 1) ; 895,50 + 99,50 = 995,00 (fill 2)
995,00 + 98,50 = 1.093,50 (fill 3) ; 1.093,50 − 100,50 = 993,00 (fill 4)
Equity: cierre 6 = 895,50 + 105,00 = 1.000,50 ; cierre 10 = 1.093,50 − 97,00 = 996,50
final = 993,00 (posición 0; la señal de la barra 15 no se ejecuta)
Diagrama¶
flowchart LR
W(["WARMUP: t < entry"]) --> FLAT["Plano 0"]
FLAT -->|"C > EU · BUY 1"| L["Largo +1"]
FLAT -->|"C < EL · SELL 1"| S["Corto -1"]
L -->|"C < XL · SELL 1"| FLAT
S -->|"C > XU · BUY 1"| FLAT
L -->|"C >= XL"| L
S -->|"C <= XU"| S
FLAT -->|"EL <= C <= EU"| FLAT
No hay flecha entre largo y corto: una ruptura contraria cierra primero (la salida manda) y la nueva entrada se decide en la barra siguiente.
Cómo lanzarla¶
Backtest con ambos motores sobre NVDA 1d, con los defaults:
BASE=http://localhost:8000
SID=$(curl -sS -X POST "$BASE/v1/backtest" -H 'Content-Type: application/json' -d '{
"request_id": "manual-p11-0001",
"correlation_id": null,
"schema_version": "v1",
"payload": {
"strategy": {"template_id": "P11", "params": {"entry": 20, "exit": 10},
"name": "Donchian 20/10 base", "register": true},
"data": {"asset_id": "NVDA", "timeframe": "1d",
"window": {"start": "2015-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": "P11", "params": {"entry": 20, "exit": 10},
"register": True},
"data": {"asset_id": "NVDA", "timeframe": "1d",
"window": {"start": "2015-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"]["n_fills"])
Derivar una variante con un canal de entrada de 55 (el clásico "sistema 2" de las Tortugas):
curl -sS -X POST "$BASE/v1/strategies/$SID/derive" -H 'Content-Type: application/json' -d '{
"request_id": "manual-p11-derive", "correlation_id": null, "schema_version": "v1",
"payload": {"param_overrides": {"entry": 55}, "name": "Donchian 55/10",
"slug": "donchian-55-10", "tags": ["sensibilidad"]}}' | jq '.payload.strategy_id, .payload.cache_forecast'
entry es parámetro de feature (tabla de invalidación, §5.3 de la spec): donchian{55} es fallo de caché si no se calculó antes y donchian{10} se reutiliza. La particularidad de P11 es que la clave de caché es la spec, no el rol: si ya calculaste donchian{20} como canal de entrada, un candidato con exit = 20 lo reutiliza como canal de salida. Ojo: derivar {"exit": 20} desde 20/10 daría exit == entry y la API respondería con PolicyParamError.
Sweep de sus variant_keys (rejilla completa del catálogo):
curl -sS -X POST "$BASE/v1/sweep" -H 'Content-Type: application/json' -d '{
"request_id": "manual-p11-sweep", "correlation_id": null, "schema_version": "v1",
"payload": {"base_strategy_id": "'"$SID"'",
"grid": {"entry": [20, 40, 55, 80, 100], "exit": [5, 10, 20]},
"candidate_policy": {"drop_invalid": true, "drop_duplicates": true,
"register_strategies": true, "name_template": "{template_name} {entry}/{exit}"},
"data": {"asset_id": "NVDA", "timeframe": "1d",
"window": {"start": "2015-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.dropped, .payload.factorization_plan'
El resultado se consulta en GET /v1/jobs/{job_id}.
Cuánto cuesta la rejilla¶
| Concepto | Cuenta (spec §5.4) |
|---|---|
| Candidatos cartesianos | 5 entry × 3 exit = 15 |
Inválidos por exit < entry |
1: (20, 20); se registra, no desaparece del denominador |
| Candidatos válidos | 2 (con entry = 20) + 4 · 3 = 14 |
| Canales distintos | {20, 40, 55, 80, 100} ∪ {5, 10, 20} = 7 (el 20 se comparte entre roles) |
| Peticiones lógicas sin factorizar | 14 × 2 = 28 → 21 aciertos de caché (75 % de ahorro) |
| Memoria de los 7 canales con N = 632.000 | ≈ 79,6 MB (2 salidas f64 con máscara cada uno) |
Qué mirar en los resultados¶
- Número de fills por carril antes que cualquier tolerancia. La spec avisa (§11, punto 3) de que un canal inclusivo produce cero trades; una divergencia relativa entre dos series de ceros es cero, y un comparador que solo mire tolerancias no lo vería.
- Paridad nativa:
NT_ONLINE(native)falla ya en la capafeatures(barra 32 de KO 1d, 49,73 frente a 49,17) porque elDonchianChannelde Nautilus es inclusivo. Es otra definición, no un problema de redondeo. - Barras con entrada contraria y salida simultáneas (como la 8 y la 14 del fixture) y barras con
C == EUoC == ELexacto: en activos de tick grueso explican entradas "que no se produjeron". - Anchura
(EU − EL)/(XU − XL): cuantifica la asimetría del candidato. - Número mínimo de trades antes de ordenar por nada (spec §10.2): con
entry = 100en diario puede haber una decena. fills = Σ|delta|: nunca invierte.DATA_GAP_SESSIONSen KO: el canal cruza el hueco de 271 sesiones y suprime rupturas hastaentrybarras.
Causalidad comprobada¶
El test de "futuros alterados" (spec §8) desplaza en bloque +15 todos los precios posteriores a la barra 400 de NVDA 1d (600 barras, 20/10) y exige canales, objetivos, motivos, fills con fill_index ≤ 400 y equity hasta el cierre 400 idénticos bit a bit. El test específico de P11 modifica solo high[t] y low[t] de una barra interior: EU[t], EL[t], XU[t], XL[t] no deben cambiar y EU[t+1 .. t+entry] sí. Falla de inmediato con una ventana inclusiva.
Para reanudar en modo continue basta guardar las últimas entry barras de high y low (1.600 bytes con entry = 100): el canal de salida es un sufijo de esa misma ventana. Guardar solo EU y EL no basta, porque el canal siguiente necesita saber qué barra sale de la ventana.
Errores típicos¶
| Error | Consecuencia | Cómo evitarlo |
|---|---|---|
exit >= entry |
PolicyParamError (la asimetría que define la receta desaparece) |
Respeta exit < entry; en el sweep cuenta como inválido |
Incluir high[t] en el canal |
Cero trades, que parece "parámetros poco agresivos" | Usa donchian (desplazado), no rolling_max |
Llenar al precio del canal cuando high[t] > EU[t] |
Stop fantasma, sesgo optimista | Señal solo con close[t] > EU[t]; fill en open[t+1] |
| Invertir de largo a corto en la misma barra | No cuadra con el motor | La salida manda; la entrada contraria, en la barra siguiente |
Tratar el toque C == EU como ruptura |
Entradas de más | Desigualdad estricta |
Arrancar un tramo con menos de entry barras previas |
Canal estrecho y rupturas fabricadas | Cargar los antecedentes que pide el warmup |
| Snapshot online sin las cinco claves durante el warmup | KeyError en la política |
Entregar siempre close y los cuatro canales, con NaN si no hay valor |
Variantes y extensiones¶
- Sistema Turtle completo: unidades por volatilidad (N), piramidación y filtro del trade ganador; sería otra entrada de catálogo.
- Canal inclusivo con ruptura sobre
high: otra receta con otra señal. - Canal de la sesión: territorio de P12 ORB.
- Salida por ATR: combinar con el trailing de P07 o el Chandelier de P19.
- Parientes: P10 Ichimoku (puntos medios de rango con ventanas inclusivas) y P04 (bandas como canal).
Resumen¶
- P11 entra al cierre que supera el máximo (o rompe el mínimo) de las
entrybarras anteriores y sale al romper el canal deexitbarras en contra. - Las cuatro desigualdades son estrictas; el toque no cuenta.
- La salida tiene prioridad y nunca hay reversión:
|delta|siempre 1. - En el fixture: largo 104,50 → 99,50, corto 98,50 → 100,50, equity final 993,00 y una señal final sin fill.
- 14 candidatos válidos y 7 canales; la variante nativa de Nautilus diverge por definición (ventana inclusiva).
Para practicar¶
- En el fixture, calcula a mano
EU[15]yXU[14]y comprueba 101,00 y 99,00. ¿Por qué la barra 15 es una entrada y no una salida? - Rehaz el fixture con una ventana inclusiva: calcula
EU[5],EU[15]y comprueba que no hay ninguna entrada larga en toda la serie. - Lanza el sweep 5×3 sobre NVDA 1d y KO 1d y comprueba en la respuesta 15 solicitados, 14 efectivos y 1 inválido. ¿Qué candidato gana y con cuántos trades?