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_ttles un parámetro solo de política yqno.
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]
El orden dentro de la barra t es parte de la regla:
armed_in = (t <= armed_until): ¿venía armado de una barra anterior?- Salida (prioridad): largo y
C[t] < middle[t]→ 0; corto yC[t] > middle[t]→ 0. - 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). - 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 == upperno rompe); no se mirat−1. - Salida: fijada como nivel y estricta (
C == middleconserva). 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 < threstricto; la barra que arma no puede romper (el armado se escribe al final, después de leerlo); vive exactamente las barrast+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:
- Barra 7: primera decisión.
bw = 0,024598 < thr = 0,029557→ arma hasta la 9. Peroarmed_ines F: la barra que arma no rompe. - 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 da10,150000000000004en f64. Es un empate roto de forma determinista; no afecta al objetivo (estado 0). - 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. - Barra 14: el último armado fue en la 11 (
armed_until = 13);14 > 13→ caducado. - 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. - 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. - 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 sí 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 enNT_FEATURES,NT_INTENT_REPLAYyNT_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), yDATA_GAP_SESSIONSen 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 = cuantilqcon interpolación lineal de laswindowbarras anteriores.bw < umbralarma durantearmed_ttlbarras; 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 debandwidth. armed_ttles solo de política (acierto de caché total);qywindowrehacen el cuantil.
Para practicar¶
- Con el fixture, calcula
thr[11]a mano (ventanabw[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? - 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? - Lanza la instancia base y su derivada con
armed_ttl = 5sobre NVDA 5min. Comprueba encache_forecasty entimingsque las tres features se reutilizan, y compara número de fills y exposición.