Saltar a contenido

P05 · Bollinger Bandwidth: ruptura tras compresión

Campo Valor
Familia Volatilidad-ruptura · se "arma" cuando las bandas se estrechan y entra cuando el precio rompe una banda mientras sigue armado
Features requeridas bbands (middle, upper, lower), bandwidth ((upper − lower)/abs(middle)) y threshold (primitiva rolling_quantile sobre bandwidth); columna de barra close
Parámetros y defaults period = 20, k = 2.0, q = 0.2, window = 252, armed_ttl = 10
Rejilla del catálogo period ∈ {10, 20, 40}, k ∈ {1.5, 2.0, 2.5}, q ∈ {0.1, 0.2, 0.3} → 27 candidatos válidos; window y armed_ttl fijos
variant_keys period, k, q
Restricciones period entero ≥ 2; k > 0; 0 < q < 1; window ≥ 2 (aviso si < 20); armed_ttl ≥ 1 (si no, PolicyParamError)
first_decision_index (period − 1) + window = 271 con defaults (291 con period = 40)
Activos y timeframes NVDA y KO en los 7 TF; AMZN, INTC, MSFT, TSLA en 1d (≈2.900 barras); BME y FX en 1d con reservas. No aplicable a series de menos de ~300 barras
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 'bandwidth'" (tampoco tiene cuantil rolling, y su Bollinger usa el precio típico)

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

Qué vas a aprender

  • Qué es el bandwidth de Bollinger y cómo un cuantil rodante convierte "bandas estrechas" en una condición sin mirar al futuro.
  • Por qué la ventana del cuantil excluye la barra actual y cuánto cambia el umbral si no lo hace.
  • Cómo funciona un armado con caducidad (TTL): cuándo nace, cuánto vive, cuándo se consume.
  • Por qué P05 tiene el warmup más largo del grupo (271 barras) y el checkpoint más grande.
  • Por qué armed_ttl es un parámetro solo de política y q no.

La idea en una frase

Cuando la anchura de las bandas cae por debajo de su percentil q del último año, preparar el disparo durante armed_ttl barras; si en ese plazo el precio cierra fuera de una banda, entrar en esa dirección; salir cuando el precio vuelva al otro lado de la media.

Intuición de mercado

La volatilidad se agrupa: tras periodos de calma suelen venir periodos de movimiento. El bandwidth mide lo estrechas que están las bandas de Bollinger respecto al precio. Si está en su 20 % más bajo del último año, el mercado está "comprimido". P05 no apuesta por una dirección en ese momento; solo se arma. La dirección la da la ruptura: el primer cierre fuera de las bandas mientras el armado sigue vivo.

Analogía. Un muelle que se va comprimiendo: no sabes hacia dónde saltará, pero sabes que cuanto más comprimido, más probable un salto. Montas la trampa (armado) y esperas un tiempo limitado; si en ese plazo salta, lo sigues; si no, desmontas.

Por qué podría funcionar. Las rupturas tras compresión concentran a menudo el inicio de tendencias: órdenes acumuladas en un rango estrecho se ejecutan todas cuando el rango se rompe.

Cuándo falla.

  • Rupturas falsas: el precio sale de la banda, vuelve a la media y la salida llega enseguida con pérdida.
  • Compresión sin expansión: el armado caduca una y otra vez sin señal; la estrategia está plana casi siempre.
  • Series cortas o con huecos: la ventana de 252 barras del cuantil abarca más de un año natural en KO, con 271 sesiones ausentes; el TTL puede atravesar un hueco.

Sesgos típicos al evaluarla.

  • Percentil de toda la muestra: np.quantile(bw, q) es una línea y es una fuga de futuro de libro (el catálogo lo prohíbe por su nombre).
  • Cuantil que incluye la barra actual: no es futuro, pero es autocontaminación; cambia la regla.
  • Comparar periodos distintos: P05 empieza a decidir en la barra 271 y P03 en la 33; la duración evaluada efectiva es otra.

Las reglas exactas

La regla canónica del catálogo: "BW=(upper-lower)/abs(media). Umbral=cuantil q de BW de las W barras anteriores, excluida t. Armar si BW[t]upper y corto si C<lower; consumir armado. Salir al cruzar la media contra la posición." Notas de ingeniería: "Receta propia sobre compresión/ruptura: no es idéntica a P08. Fijar interpolación lineal del cuantil. Denominador cero invalida BW. No calcular el percentil de toda la muestra."

El orden dentro de la barra t es parte de la regla:

  1. armed_in = (t <= armed_until): ¿venía armado de una barra anterior?
  2. Salida (prioridad): largo y C[t] < middle[t] → 0; corto y C[t] > middle[t] → 0.
  3. Entrada (solo si no hubo salida, desde plano y con armed_in): C[t] > upper[t] → +1; si no, C[t] < lower[t] → −1. Entrar consume el armado (armed_until = −1).
  4. Armado (siempre, al final): si bw[t] < thr[t], armed_until = t + armed_ttl (arma o renueva).
Situación en el cierre de t Objetivo target[t] Motivo (reason)
t < (period − 1) + window 0 WARMUP
largo y C < middle 0 EXIT
corto y C > middle 0 EXIT
plano, armed_in y C > upper +1 ENTRY
plano, armed_in y C < lower −1 ENTRY
plano sin armado vivo o sin ruptura 0 HOLD
en posición sin salida el anterior HOLD
features NaN tardías el anterior (no arma ni desarma) HOLD
barra no operable (is_tradable = False) se congela el publicado; el TTL sigue corriendo NOT_TRADABLE
  • Entrada: ruptura de nivel, estricta (C == upper no rompe); no se mira t−1.
  • Salida: fijada como nivel y estricta (C == middle conserva). El catálogo dice "cruzar", que es ambiguo; la spec lo resuelve como nivel por coherencia con P04 y porque una salida por cruce podría no dispararse nunca.
  • Reversión: prohibida; la entrada exige estar plano. Nunca hay deltas de 2.
  • Armado: bw < thr estricto; la barra que arma no puede romper (el armado se escribe al final, después de leerlo); vive exactamente las barras t+1 … t+A; rearmar renueva el plazo; salir no rearma.
# decisión en el cierre de t, fill en el open de t+1
state = 0 ; armed_until = -1
para cada barra t:
    si t < (period - 1) + window:  target = 0 ; continuar          # WARMUP
    armed_in = (t <= armed_until)                                   # 1. leer armado previo
    salida = False
    si state == +1 y C[t] < middle[t]:  state = 0 ; salida = True   # 2. salida primero
    si state == -1 y C[t] > middle[t]:  state = 0 ; salida = True
    si no salida y state == 0 y armed_in:                           # 3. ruptura
        si C[t] > upper[t]:   state = +1 ; armed_until = -1         #    consumir
        si no, si C[t] < lower[t]: state = -1 ; armed_until = -1
    si bw[t] < thr[t]:  armed_until = t + armed_ttl                 # 4. armar / renovar
    target = state
    si target != target_anterior:
        orden(target - target_anterior)  -> fill a open[t+1]        # siempre ±1

No es P08

P08 (squeeze momentum) también busca compresión, pero con bandas de Keltner y un oscilador de momento. P05 es una receta propia: bandwidth, cuantil rodante y armado con TTL.

Los indicadores que usa

Feature Fórmula first_valid
bbands{n,k} middle = SMA(n), upper/lower = middle ± k·stddev(n, ddof=0) n − 1 (19)
bandwidth{n,k} (upper − lower)/abs(middle); NaN si middle == 0 exacto; fracción, sin ×100 n − 1 (19)
rolling_quantile{W,q} cuantil q con interpolación lineal (numpy.quantile(..., method="linear")) de la ventana estrictamente anterior bw[t−W .. t−1] (n − 1) + W (271)

Interpolación lineal escrita a mano: con la ventana ordenada y[0] ≤ … ≤ y[W−1], h = (W − 1)·q, j = ⌊h⌋, γ = h − j, quantile = y[j] + γ·(y[j+1] − y[j]). El percentil nearest rank de otras bibliotecas no es el mismo número.

first_decision_index = (period − 1) + window: la cadena suma la ventana del cuantil (regla chained_shifted_window). Con defaults, 271 barras; en diario, más de un año sin decisiones. Detalle en Las 23 primitivas de fase B, Indicadores paso a paso e Indicadores y features.

Ejemplo barra a barra

Fixture §6 de docs/estrategias/P05.md: period = 4, k = 1,0, q = 0,5 (la mediana), window = 4, armed_ttl = 2 (todos fuera de rejilla a propósito), cash_inicial = 1.000,00, tick 0,01, sin costes. first_decision_index = 3 + 4 = 7. Bandas con 4 decimales; bw y thr con 6, como en la spec.

Barras 0–5: cierres que alternan 10,00 / 10,30 (O/H/L/C en la tabla §6 de la spec); bandas válidas desde la 3 (middle = 10,1500, upper = 10,3000, lower = 10,0000, bw = 0,029557), umbral NaN, objetivo 0 WARMUP.

t O H L C middle upper lower bw thr armed_until (fin) armed_in target orden fill pos. PnL acum. sin costes* equity con 5 bps*
6 10,30 10,32 9,95 10,00 10,1500 10,3000 10,0000 0,029557 NaN F 0 WARMUP 0 0,00 1.000,00
7 10,00 10,18 9,99 10,15 10,1125 10,2369 9,9881 0,024598 0,029557 9 (arma) F 0 HOLD 0 0,00 1.000,00
8 10,15 10,17 10,12 10,15 10,1500 10,2561 10,0439 0,020900 0,029557 10 V 0 HOLD 0 0,00 1.000,00
9 10,15 10,20 10,13 10,18 10,1200 10,1904 10,0496 0,013904 0,027077 11 V 0 HOLD 0 0,00 1.000,00
10 10,18 10,24 10,16 10,22 10,1750 10,2037 10,1463 0,005646 0,022749 12 (consume y rearma) V +1 ENTRY BUY 1 0 0,00 1.000,00
11 10,22 10,28 10,20 10,26 10,2025 10,2440 10,1610 0,008127 0,017402 13 V† +1 HOLD 10,22 +1 0,04 1.000,03
12 10,26 10,85 10,24 10,80 10,3650 10,6177 10,1123 0,048767 0,011016 13 V +1 HOLD +1 0,58 1.000,57
13 10,80 11,45 10,78 11,40 10,6700 11,1497 10,1903 0,089913 0,011016 13 V +1 HOLD +1 1,18 1.001,17
14 11,40 11,48 11,30 11,35 10,9525 11,4165 10,4885 0,084724 0,028447 13 F (caduca) +1 HOLD +1 1,13 1.001,12
15 11,35 11,40 11,28 11,34 11,2225 11,4675 10,9775 0,043660 0,066746 17 (arma) F +1 HOLD +1 1,12 1.001,11
16 11,34 11,95 11,32 11,90 11,4975 11,7310 11,2640 0,040616 0,066746 18 V +1 HOLD +1 1,68 1.001,67
17 11,90 11,92 11,25 11,30 11,4725 11,7200 11,2250 0,043151 0,064192 19 V 0 EXIT SELL 1 +1 1,08 1.001,07
18 11,30 11,32 10,85 10,90 11,3600 11,7161 11,0039 0,062692 0,043406 19 V −1 ENTRY SELL 1 11,30 0 1,08 1.001,06
19 10,90 10,92 10,65 10,70 11,2000 11,6583 10,7417 0,081832 0,043406 −1 (consumido) F −1 HOLD 10,90 −1 1,28 1.001,25

OHLC, bandas, bw, thr, armado, objetivos, motivos y fills son literales del fixture. *El fixture da la equity en los cierres 11 (1.000,04) y 18 (1.001,08); la columna "PnL acumulado" completa es cálculo de este manual con la regla del ledger equity = cash + posición · close (caja 989,78 tras el fill 1; 1.001,08 tras el 2; 1.011,98 tras el 3) y coincide con el fixture en esas dos barras. "Equity con 5 bps" también es cálculo propio: hypothetical_5bps (fee_rate = 0,0005, sin spread ni deslizamiento), comisión = |delta| · precio · 0,0005 redondeada al céntimo half-even, restada en la barra del fill:

Fill Barra Precio Cuenta Comisión
1 BUY 1 11 10,22 0,00511 0,01
2 SELL 1 18 11,30 0,00565 0,01
3 SELL 1 19 10,90 0,00545 0,01

†En la barra 11 la tabla §6 de la spec imprime a_in = F, pero con su propia regla (armed_in = t <= armed_until, y armed_until = 12 al cerrar la 10) y con el código de finazbench/policy/p05_bandwidth.py el valor es V. No afecta a nada: en la 11 el estado es +1 y la entrada no se evalúa. Aquí se muestra el valor que da la regla.

Lo que hay que ver en la tabla:

  1. Barra 7: primera decisión. bw = 0,024598 < thr = 0,029557 → arma hasta la 9. Pero armed_in es F: la barra que arma no rompe.
  2. Barra 8: la media de (10,30; 10,00; 10,15; 10,15) es exactamente 10,15 = cierre, pero la SMA canónica por suma acumulada da 10,150000000000004 en f64. Es un empate roto de forma determinista; no afecta al objetivo (estado 0).
  3. Barra 10: armado vivo (fijado en la 9 hasta la 11) y 10,22 > 10,2037 → largo. Entrar consume el armado… y el paso 4 lo vuelve a armar (0,005646 < 0,022749). Fill en el open de 11.
  4. Barra 14: el último armado fue en la 11 (armed_until = 13); 14 > 13caducado.
  5. Barra 17: 11,30 < 11,4725 → salida del largo. Había armado vivo, pero la salida tiene prioridad: no se reabre en la misma decisión.
  6. Barra 18: desde plano, con armado vivo hasta la 19, 10,90 < 11,0039 → corto. No es una reversión: dos decisiones y dos fills de una unidad.
  7. Barra 19: el corto queda abierto y se valora al último cierre; no hay liquidación automática.

Las cuentas de las barras clave

Barra 7 (primera decisión):
  ventana close[4..7] = (10,00; 10,30; 10,00; 10,15)   middle = 40,45/4 = 10,1125
  varianza = 0,061875/4 = 0,01546875                    sd = 0,1243734
  upper = 10,2368734 ; lower = 9,9881266
  bw[7] = 0,2487469/10,1125 = 0,024598
  thr[7]: ventana bw[3..6] = cuatro veces 0,029557 -> mediana 0,029557
  armed_in = (7 <= -1) = F ; 0,024598 < 0,029557 -> armed_until = 7 + 2 = 9
Barra 10 (entrada):
  ventana close[7..10] = (10,15; 10,15; 10,18; 10,22)   middle = 40,70/4 = 10,1750
  varianza = 0,0033/4 = 0,000825                        sd = 0,0287228
  upper = 10,2037228 ; bw[10] = 0,0574456/10,1750 = 0,005646
  thr[10]: ventana bw[6..9] ordenada (0,013904; 0,020900; 0,024598; 0,029557)
           h = 3·0,5 = 1,5 -> 0,020900 + 0,5·(0,024598 − 0,020900) = 0,022749
  armed_in = (10 <= 11) = V ; 10,22 > 10,2037228 -> ENTRY +1, armed_until = −1
  paso 4: 0,005646 < 0,022749 -> armed_until = 12
Si el cuantil INCLUYERA t=10: ventana bw[7..10] ordenada (0,005646; 0,013904; 0,020900; 0,024598)
           -> 0,013904 + 0,5·(0,020900 − 0,013904) = 0,017402   (23,5 % más bajo)
Caja:  1.000,00 − 10,22 = 989,78    (fill 1, open[11]); equity cierre 11 = 989,78 + 10,26 = 1.000,04
       989,78 + 11,30   = 1.001,08  (fill 2, open[18]); equity cierre 18 = 1.001,08
       1.001,08 + 10,90 = 1.011,98  (fill 3, open[19]); equity cierre 19 = 1.011,98 − 10,70 = 1.001,28

En la barra 10 el armado se produce con los dos umbrales, pero basta un bw[t] entre ambos para que la versión inclusiva no arme y el trade desaparezca (la spec lo construye en test_p05_quantile_excludes_current_flips_arming).

Diagrama

flowchart LR
    W(["WARMUP: t < period-1+window"]) --> D["Plano sin armar"]
    D -->|"bw < thr · armed_until = t+A"| A["Plano armado"]
    A -->|"bw < thr · renueva"| A
    A -->|"t > armed_until · caduca"| D
    A -->|"C > upper · BUY 1 · consume"| L["Largo +1"]
    A -->|"C < lower · SELL 1 · consume"| S["Corto -1"]
    L -->|"C < media · SELL 1"| D
    S -->|"C > media · BUY 1"| D
    L -->|"C >= media"| L
    S -->|"C <= media"| S

Simplificación del dibujo: al salir, el autómata vuelve a "plano" conservando el armed_until que hubiera (puede estar armado si la compresión se renovó mientras estaba en posición, como en las barras 15–18 del fixture). Nunca hay flecha directa entre largo y corto.

Cómo lanzarla

Backtest con ambos motores sobre NVDA 5min en 2024 con los defaults (forma "inline" de la API de backtesting; mismo payload que examples/api/01_backtest_p01_both.request.json). En 5min, 271 barras de warmup son unos cuatro días de sesión:

BASE=http://localhost:8000
SID=$(curl -sS -X POST "$BASE/v1/backtest" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p05-0001",
  "correlation_id": null,
  "schema_version": "v1",
  "payload": {
    "strategy": {"template_id": "P05",
                 "params": {"period": 20, "k": 2.0, "q": 0.2, "window": 252, "armed_ttl": 10},
                 "name": "BB bandwidth 20/2 q0.2", "register": true},
    "data": {"asset_id": "NVDA", "timeframe": "5min",
             "window": {"start": "2024-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": "P05",
                 "params": {"period": 20, "k": 2.0, "q": 0.2, "window": 252, "armed_ttl": 10},
                 "register": True},
    "data": {"asset_id": "NVDA", "timeframe": "5min",
             "window": {"start": "2024-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 un armado más corto:

curl -sS -X POST "$BASE/v1/strategies/$SID/derive" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p05-derive", "correlation_id": null, "schema_version": "v1",
  "payload": {"param_overrides": {"armed_ttl": 5}, "name": "BB bandwidth TTL 5",
              "slug": "bb-bandwidth-ttl-5", "tags": ["sensibilidad"]}}' \
  | jq '.payload.strategy_id, .payload.cache_forecast'

armed_ttl es el único parámetro solo de política del grupo (tabla §5.3 de la spec): no entra en ninguna FeatureSpec, así que cache_forecast debe anunciar aciertos en las tres features (bbands, bandwidth, threshold) y solo rehacer política y ledger; un acierto del resultado completo sería un bug. En cambio q o window son de feature: reutilizan bbands y bandwidth íntegros y rehacen el cuantil (la parte cara). k reutiliza sma y stddev; period lo rehace todo.

Sweep de sus variant_keys (rejilla completa del catálogo; window y armed_ttl quedan en sus valores de la instancia base):

curl -sS -X POST "$BASE/v1/sweep" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p05-sweep", "correlation_id": null, "schema_version": "v1",
  "payload": {"base_strategy_id": "'"$SID"'",
    "grid": {"period": [10, 20, 40], "k": [1.5, 2.0, 2.5], "q": [0.1, 0.2, 0.3]},
    "candidate_policy": {"drop_invalid": true, "drop_duplicates": true,
                         "register_strategies": true,
                         "name_template": "{template_name} {period}/{k} q{q}"},
    "data": {"asset_id": "NVDA", "timeframe": "5min",
             "window": {"start": "2024-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 spec: 27 candidatos, 27 válidos. Resultados en GET /v1/jobs/{job_id}.

Cuánto cuesta la rejilla

Nivel Cuenta Detalle
Candidatos 3 × 3 × 3 = 27 todos válidos
sma y stddev 3 + 3 una por period
bbands / bandwidth 9 / 9 una por pareja (period, k); bandwidth no depende de q
rolling_quantile 27 9 bandwidth × 3 q: no se factoriza
Pasadas de ventana ordenada 9 si el planificador extrae los tres q de una sola pasada ahorro de 3× disponible
Políticas / ledgers 27 / 27

El cuantil rodante es ~99 % del coste de features: es lo que hay que medir aparte.

Causalidad y checkpoint

Test de "futuros alterados" de la spec: 600 barras sintéticas, defaults, t0 = 400, cierres posteriores × 1,5. Deben quedar idénticos bit a bit en [0, 400] bandas, bandwidth, threshold (columna crítica: un percentil global la cambiaría entera), armado, objetivos, motivos y fills con fill_index ≤ 400. Un segundo test perturba solo close[t0] y exige que threshold[t0] no cambie pero threshold[t0+1] sí: localiza el error de borde de la ventana.

Checkpoint para continue, el más grande del grupo: los últimos period cierres, los últimos window valores de bandwidth (252 f64), state y armed_until como instante absoluto. Reiniciar un rango sin reiniciar el armado permitiría entrar sin compresión en el tramo nuevo.

Qué mirar en los resultados

  • parity.verdict: PASS esperado en NT_FEATURES, NT_INTENT_REPLAY y NT_ONLINE(vectorta).
  • Armados, armados consumidos y armados caducados (§10.2): la terna que caracteriza la estrategia. Un candidato con 0 armados no es malo: no tiene señal.
  • Ratio consumidos / totales: cerca de 0, el TTL es demasiado corto o la compresión no anticipa nada.
  • Exposición temporal: P05 pasa mucho tiempo plana.
  • Ausencia de deltas ±2 en cualquier corrida.
  • Duración evaluada efectiva (N − 271), y DATA_GAP_SESSIONS en KO: la ventana de 252 barras no es "el último año".
  • Número mínimo de trades antes de ordenar por Sharpe.

Errores típicos

Error Consecuencia Cómo evitarlo
q ≤ 0 o q ≥ 1 PolicyParamError (nunca arma / arma siempre) 0 < q < 1
period < 2, k ≤ 0, window < 2, armed_ttl < 1 PolicyParamError Respeta las restricciones; window < 20 solo avisa
Percentil de toda la muestra Fuga de futuro Cuantil rodante sobre bw[t−W .. t−1]
Ventana del cuantil que incluye t Umbral autocontaminado; armados que desaparecen Ventana estrictamente anterior
Cuantil nearest rank Otro número, otros armados Interpolación lineal
Dejar que la barra de armado rompa Otra estrategia (breakout sin compresión previa) Leer el armado antes de escribirlo
No consumir el armado al entrar Reentradas en cadena con la misma compresión armed_until = −1 al entrar
bw infinito o 0 con middle == 0 Armados falsos middle == 0 invalida bw (NaN)
Serie corta (< 300 barras) Ni una decisión Usa series largas; no aplicable

Variantes y extensiones

  • Permitir que la barra de armado rompa: alternativa defendible que la spec deja anotada; es otra receta y otro id.
  • Solo largos: target_lots = [0, 1].
  • Salida por trailing stop en lugar de la media: ver P07 o P19.
  • Parientes: P04 (mismas bandas, lógica opuesta: reversión), P08 (compresión con Keltner y momento) y P11 (ruptura de canal Donchian, sin condición de compresión).

Resumen

  • bw = (upper − lower)/abs(middle); umbral = cuantil q con interpolación lineal de las window barras anteriores.
  • bw < umbral arma durante armed_ttl barras; la barra que arma no rompe; entrar consume el armado.
  • Ruptura de nivel estricta; salida de nivel estricta en la media; nunca invierte.
  • Warmup de (period − 1) + window = 271 barras y checkpoint con 252 valores de bandwidth.
  • armed_ttl es solo de política (acierto de caché total); q y window rehacen el cuantil.

Para practicar

  1. Con el fixture, calcula thr[11] a mano (ventana bw[7..10], mediana con interpolación lineal) y comprueba 0,017402. ¿Arma la barra 11? ¿Hasta qué barra queda vivo el armado y por qué no sirve para nada mientras dure el largo?
  2. Repite el fixture con armed_ttl = 3: ¿en qué barra caduca ahora el armado y cambia algún objetivo entre las barras 11 y 19?
  3. Lanza la instancia base y su derivada con armed_ttl = 5 sobre NVDA 5min. Comprueba en cache_forecast y en timings que las tres features se reutilizan, y compara número de fills y exposición.