Saltar a contenido

P08 · Squeeze Momentum (familia TTM/LazyBear)

Campo Valor
Familia Volatilidad-ruptura · compresión de canales (Bollinger dentro de Keltner) + momento por regresión lineal; evento de liberación y salida por cambio de signo
Features requeridas squeeze_momentum (salidas squeeze, val, m), compuesta de bbands, keltner_sma, tr, sma_of(tr), rolling_max, rolling_min, sma(close) y linreg_endpoint
Parámetros y defaults period = 20, bb_k = 2.0, kc_k = 1.5
Rejilla del catálogo period ∈ {10, 20, 30}, bb_k ∈ {1.5, 2.0, 2.5}, kc_k ∈ {1.0, 1.5, 2.0} → 27 candidatos válidos; solo 3 regresiones rolling
variant_keys period, bb_k, kc_k
Restricciones period entero ≥ 2; bb_k > 0 y kc_k > 0 (si no, PolicyParamError); sin restricción cruzada entre bb_k y kc_k
first_decision_index 2·period − 2 = 38 con defaults (18 con period = 10, 58 con period = 30)
Activos y timeframes NVDA y KO en los 7 TF; AMZN, INTC, MSFT, TSLA en 1d; BME en 1d; FX solo con OHLC reparado
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 'squeeze_momentum'" (Nautilus no tiene regresión lineal rolling; su Bollinger usa precio típico y su Keltner es EMA ± ATR)

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

Versión de la fórmula fijada

El catálogo avisa: "La referencia de LazyBear documenta una corrección histórica del multiplicador: fijar la versión de la fórmula" y "Esta salida es nuestra variante explícita, no la salida por cambio de color de otras versiones". Esta ficha describe nuestra versión; no se afirma paridad con ningún script publicado.

Qué vas a aprender

  • Qué es un squeeze (Bollinger estrictamente dentro de Keltner) y por qué la entrada es la liberación, una transición de una sola barra.
  • Cómo se calcula el momento m: el extremo final de una regresión lineal rolling sobre val, por sumas suficientes.
  • Por qué el Keltner de P08 usa SMA(TR) y no ATR, y por qué eso exige un nombre de primitiva propio.
  • A leer la barra 13 del fixture, donde salida y liberación coinciden y el objetivo es 0, no la posición contraria.
  • Por qué 27 candidatos solo cuestan 3 regresiones.

La idea en una frase

Cuando la volatilidad lleva un tiempo comprimida y de pronto se expande, entra en la dirección del momento; sal en cuanto el momento cambie de signo.

Intuición de mercado

Las bandas de Bollinger miden la volatilidad con la desviación típica del cierre; las de Keltner, con el rango medio de las barras. Cuando el mercado se queda quieto, la desviación típica se encoge más deprisa que el rango y las Bollinger quedan dentro del Keltner: es el squeeze, una fase de compresión. La idea (TTM de John Carter, popularizada por LazyBear) es que a la compresión le sigue una expansión, y que el primer tramo de esa expansión tiende a continuar.

Analogía. Un muelle comprimido: mientras está sujeto no pasa nada; lo interesante es el momento en que se suelta, y hacia dónde apunta al soltarse. El squeeze es el muelle sujeto; la liberación, el momento en que se suelta; el signo de m, hacia dónde apunta.

Por qué podría funcionar. La volatilidad se agrupa en regímenes: a periodos tranquilos les siguen periodos agitados. Entrar al principio de la expansión, a favor del momento, captura el movimiento inicial.

Cuándo falla.

  • Falsas rupturas: la volatilidad se expande una barra y vuelve a comprimirse; m cambia de signo y la salida llega con pérdida.
  • Liberaciones perdidas: si en la barra de liberación m == 0 o hay una salida simultánea, el evento se pierde (no se guarda ni tiene caducidad).
  • Huecos de sesión: el TR de la barra tras el hueco ensancha el Keltner, favorece un squeeze artificial y produce una liberación espuria al deshacerse; contamina justo el evento de entrada.

Sesgos típicos al evaluarla.

  • Pocos trades: P08 pasa mucho tiempo plano; un Sharpe con 5 operaciones no significa nada. Mira squeeze_fraction y el número de trades antes de ordenar.
  • Comparar con el indicador de una plataforma: el Keltner EMA ± ATR, las desigualdades no estrictas o la salida "por color" son otras estrategias.
  • Regresión centrada: center=True en pandas produce un m más suave y "mejor" usando el futuro.

Las reglas exactas

La regla canónica del catálogo: "Squeeze cuando BB está estrictamente dentro de KC. Liberación=squeeze[t-1] y no squeeze[t]. m[t]=valor final de una regresión lineal rolling de C-(midpoint(HH(n),LL(n))+SMA(C,n))/2. Entrar largo/corto en liberación según signo m; salir al cambiar de signo. Esta salida es nuestra variante explícita, no la salida por cambio de color de otras versiones."

Con sign(v) = +1 si v > 0, −1 si v < 0 y 0 si v == 0 o NaN:

Situación en el cierre de t Objetivo target[t] Motivo (reason)
t < 2·period − 2 0 WARMUP
en posición y sign(m[t]) != sign(m[t−1]) 0 EXIT (prioridad sobre la entrada)
plano, release[t] y sign(m[t]) = +1, sin salida en esta barra +1 ENTRY
plano, release[t] y sign(m[t]) = −1, sin salida en esta barra −1 ENTRY
plano, release[t] y m[t] == 0 0 (la liberación se pierde) HOLD
otro caso el anterior HOLD
barra no operable se congela el publicado NOT_TRADABLE
  • Squeeze: bb.lower > kc.lower y bb.upper < kc.upper, ambas estrictas; False si alguna banda es NaN.
  • Liberación: release[t] = squeeze[t−1] and not squeeze[t]. Es un flanco, no un nivel.
  • Salida: cambio de signo de m; sign(0) = 0 cuenta como signo (ratificado, D-18), así que m que pasa de positivo a 0 exacto cierra. Un NaN tardío en m también cierra.
  • Reversión: no permitida. P08 nunca produce un delta de 2.
  • Filtros: ninguno más.
# decisión en el cierre de t, fill en el open de t+1
state = 0
para cada barra t:
    si t < 2*period - 2:  target = 0                          # WARMUP
    si no:
        release = squeeze[t-1] y no squeeze[t]
        exited = False
        si state != 0 y sign(m[t]) != sign(m[t-1]):           # SALIDA primero
            state = 0; exited = True
        si no exited y state == 0 y release y sign(m[t]) != 0:
            state = sign(m[t])                                # ENTRADA
        target = state
    si target != target_prev: orden(target - target_prev) -> fill a open[t+1]

Liberación = squeeze[t−1] y no squeeze[t], nunca squeeze[t+1]

squeeze[t] and not squeeze[t+1] marca la última barra del squeeze en vez de la primera fuera: entra una barra antes con información que en t no existe. Y usar not squeeze[t] como condición de entrada no es una fuga, sino otra estrategia: en el fixture entraría 7 veces en lugar de 1.

Los indicadores que usa

Feature Fórmula first_valid
bbands{n, bb_k} SMA(C, n) ± bb_k · stddev(C, n) (desviación poblacional) n − 1
tr TR[0] = H[0] − L[0]; después max(H − L, |H − Cprev|, |L − Cprev|) 0
keltner_sma{n, kc_k} SMA(C, n) ± kc_k · SMA(TR, n)no EMA ± ATR (eso es el Keltner de P16) n − 1 (dependencias paralelas)
rolling_max{n} / rolling_min{n} máximo de high / mínimo de low en [t − n + 1, t], inclusivos (no el Donchian desplazado) n − 1
val C − ((HH + LL)/2 + SMA(C, n))/2 (promedio doble, literal) n − 1
m = linreg_endpoint(val, n) con x = 0..n−1: slope = (n·Sxy − Sx·Sy)/(n·Sxx − Sx²), intercept = (Sy − slope·Sx)/n, m = intercept + slope·(n−1) 2n − 2

first_decision_index = 2·period − 2 (el máximo sobre las tres salidas; D-12, sin +1). En esa barra m[t−1] es NaN, pero squeeze[t−1] existe desde period − 1, así que la regla es evaluable: "no entro" es una decisión válida.

SMA(TR) no es ATR: en la semilla coinciden, pero en el fixture ya divergen en la segunda barra (ATR[4] = 0,13125 frente a SMA(TR,4)[4] = 0,1450). Más 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/P08.md: period = 4, bb_k = 2.0, kc_k = 1.0 (period y kc_k fuera de rejilla), cash_inicial = 1.000,00, tick 0,01, sin costes. first_valid(squeeze) = first_valid(val) = 3, first_valid(m) = 6first_decision_index = 6. 18 barras (0–17), con open[i] = close[i−1].

Barras 0–4: WARMUP (m NaN; el squeeze ya está activo desde la barra 3). Se muestran la 5 (última de warmup) y de la 6 a la 17.

t O H L C squeeze release val m target orden fill pos. PnL acum. sin costes* equity con 5 bps*
5 10,08 10,18 10,04 10,14 V F 0,0438 NaN 0 WARMUP 0 0,00 1.000,00
6 10,14 10,18 10,06 10,10 V F −0,0025 0,0051 0 HOLD 0 0,00 1.000,00
7 10,10 10,20 10,06 10,16 V F 0,0400 0,0301 0 HOLD 0 0,00 1.000,00
8 10,16 10,54 10,12 10,50 F V 0,2425 0,1768 +1 ENTRY BUY 1 0 0,00 1.000,00
9 10,50 11,04 10,46 11,00 F F 0,5050 0,4550 +1 HOLD 10,50 +1 0,50 1.000,49
10 11,00 11,44 10,96 11,40 F F 0,6425 0,6680 +1 HOLD +1 0,90 1.000,89
11 11,40 11,44 11,26 11,30 F F 0,3850 0,5285 +1 HOLD +1 0,80 1.000,79
12 11,30 11,34 10,96 11,00 V F −0,0625 0,0735 +1 HOLD +1 0,50 1.000,49
13 11,00 11,04 10,56 10,60 F V −0,4375 −0,4213 0 EXIT SELL 1 +1 0,10 1.000,09
14 10,60 10,64 10,26 10,30 F F −0,5250 −0,6258 0 HOLD 10,60 0 0,10 1.000,08
15 10,30 10,34 10,06 10,10 F F −0,5000 −0,5912 0 HOLD 0 0,10 1.000,08
16 10,10 10,24 10,06 10,20 F F −0,2250 −0,3225 0 HOLD 0 0,10 1.000,08
17 10,20 10,44 10,16 10,40 V F 0,1000 0,0350 0 HOLD 0 0,10 1.000,08

OHLC, squeeze, release, val, m (4 decimales, half-even), objetivos, motivos y fills son literales del fixture §6 de docs/estrategias/P08.md. *El fixture solo da la equity en el cierre 9 (1.000,50) y en el 14 (1.000,10); el resto de la columna "PnL acumulado sin costes" es cálculo de este manual con la misma regla (caja tras fills + posición · cierre − 1.000,00). "Pos." es la posición tras el fill en la apertura de esa barra.

*"Equity con 5 bps" es también 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 (t=9): 1 · 10,50 · 0,0005 = 0,00525 → 0,01
  • fill 2 (t=14): 1 · 10,60 · 0,0005 = 0,0053 → 0,01

Equity final con 5 bps: 1.000,10 − 0,02 = 1.000,08.

Lo que hay que ver en la tabla:

  1. Barras 3–7: squeeze continuado; release es falso en todas, porque la liberación es una transición.
  2. Barra 8: squeeze[7] = V, squeeze[8] = F → liberación con m = 0,1768 > 0 → largo. Fill en open[9] = 10,50.
  3. Barras 9–11: sin squeeze pero sin liberación: una implementación "por nivel" habría entrado otra vez aquí.
  4. Barra 12: el squeeze vuelve (transición inversa): no es liberación.
  5. Barra 13, la clave: es a la vez salida (sign(m) pasa de +1 a −1) y liberación con m < 0. La prioridad de la salida y el "no reabrir en la barra de salida" dan objetivo 0, no −1; el fill es de 1 unidad, no de 2.
  6. Barra 17: nuevo squeeze y cambio de signo de m, pero plano y sin liberación: no pasa nada. Termina plano.

Las cuentas de las barras clave

Squeeze (t=3):   SMA(C,4) = (10,00+10,10+10,05+10,12)/4 = 10,0675
                 varianza = 0,008675/4 = 0,00216875 -> stddev = 0,0465698
                 bb = 10,0675 ± 2·0,0465698 = [9,9743603 ; 10,1606397]
                 TR = 0,080; 0,180; 0,130; 0,150 -> SMA(TR,4) = 0,540/4 = 0,1350
                 kc = 10,0675 ± 1,0·0,1350 = [9,9325 ; 10,2025]
                 9,97436 > 9,9325 y 10,16064 < 10,2025 -> squeeze[3] = V
val (t=3):       HH = 10,16; LL = 9,96; midpoint = 10,06
                 val = 10,12 - (10,06 + 10,0675)/2 = 0,05625
m (t=6):         ventana val[3..6] = (0,05625; 0,00625; 0,04375; -0,00250), x = 0..3
                 Sx = 6; Sxx = 14; den = 4·14 - 36 = 20
                 Sy = 0,10375; Sxy = 0,08625
                 slope = (4·0,08625 - 6·0,10375)/20 = -0,013875
                 intercept = (0,10375 + 0,013875·6)/4 = 0,046750
                 m[6] = 0,046750 - 0,013875·3 = 0,005125
Liberación (t=8): bb.lower[8] = 10,2250 - 2·0,1602342 = 9,9045316 ; kc.lower[8] = 10,2250 - 0,2050 = 10,0200
                 9,9045 > 10,0200 es FALSO -> squeeze[8] = F ; squeeze[7] = V -> release[8] = V
                 ventana val[5..8]: Sy = 0,32375 ; Sxy = 0,80500
                 slope = (3,22000 - 1,94250)/20 = 0,063875 ; intercept = -0,014875
                 m[8] = -0,014875 + 0,063875·3 = 0,176750 > 0 -> +1 (ENTRY)
Salida (t=13):   sign(m[12]) = +1 ; sign(m[13]) = -1 -> EXIT, exited = True
                 release[13] = V pero "no reabrir" -> target 0
Caja:            1.000,00 - 10,50 = 989,50   (fill 1, open[9])
                 989,50 + 10,60   = 1.000,10 (fill 2, open[14])

Nota de la especificación sobre m[8]: su valor exacto es 707/4000 = 0,17675, que a 4 decimales half-even es 0,1768; el kernel f64 devuelve 0.1767499999999993. Los tests comparan con atol = 1e-9, no con cadenas redondeadas.

Diagrama

flowchart LR
    W(["WARMUP: t < 2·period-2"]) -->|"m válido"| FLAT["Plano 0"]
    FLAT -->|"release y m > 0 · BUY 1"| L["Largo +1"]
    FLAT -->|"release y m < 0 · SELL 1"| S["Corto -1"]
    FLAT -->|"sin release, o m == 0"| FLAT
    L -->|"sign(m) cambia · SELL 1"| FLAT
    S -->|"sign(m) cambia · BUY 1"| FLAT
    L -->|"mismo signo"| L
    S -->|"mismo signo"| S

No hay flecha directa entre largo y corto: toda salida pasa por plano, aunque en esa misma barra haya una liberación del signo contrario.

Cómo lanzarla

Backtest con ambos motores sobre NVDA 5min en 2024, con los defaults (estrategia por plantilla y parámetros, forma "inline" de la API de backtesting):

BASE=http://localhost:8000
SID=$(curl -sS -X POST "$BASE/v1/backtest" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p08-0001",
  "correlation_id": null,
  "schema_version": "v1",
  "payload": {
    "strategy": {"template_id": "P08", "params": {"period": 20, "bb_k": 2.0, "kc_k": 1.5},
                 "name": "Squeeze 20/2.0/1.5 base", "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": "P08",
                 "params": {"period": 20, "bb_k": 2.0, "kc_k": 1.5}, "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"])

Derivar una variante con un Keltner más ancho (squeeze más frecuente):

curl -sS -X POST "$BASE/v1/strategies/$SID/derive" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p08-derive", "correlation_id": null, "schema_version": "v1",
  "payload": {"param_overrides": {"kc_k": 2.0}, "name": "Squeeze 20/2.0/2.0",
              "slug": "squeeze-20-2-0-2-0", "tags": ["sensibilidad"]}}' | jq '.payload.strategy_id, .payload.cache_forecast'

kc_k es parámetro de feature (forma parte de squeeze_momentum), pero barato: según la tabla de invalidación §5.3 de la especificación, se reutilizan sma{20}, sma_of(tr), bbands íntegro y val y m íntegros; solo se rehacen las bandas Keltner, el booleano squeeze, la política y el ledger. Con bb_k pasa lo simétrico. Cambiar period, en cambio, solo reutiliza tr.

Sweep de sus variant_keys:

curl -sS -X POST "$BASE/v1/sweep" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p08-sweep", "correlation_id": null, "schema_version": "v1",
  "payload": {"base_strategy_id": "'"$SID"'",
    "grid": {"period": [10, 20, 30], "bb_k": [1.5, 2.0, 2.5], "kc_k": [1.0, 1.5, 2.0]},
    "candidate_policy": {"drop_invalid": true, "drop_duplicates": true,
                         "register_strategies": true,
                         "name_template": "{template_name} {period}/{bb_k}/{kc_k}"},
    "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 especificación: 27 candidatos, los 27 válidos (no hay restricción bb_k > kc_k, y no debe añadirse). Resultados en GET /v1/jobs/{job_id}.

Cuánto cuesta la rejilla

Nivel Cuenta
tr 1
sma(C), stddev, sma_of(tr), rolling_max, rolling_min 3 de cada (una por period)
val y m (la regresión, el kernel caro) 3 y 3: no dependen de bb_k ni de kc_k
bbands / keltner_sma 9 / 9
squeeze 27 (comparación de cuatro series, barata)
Políticas / ledgers 27 / 27

La regresión se calcula 3 veces para 27 candidatos: un ahorro del 89 % en el kernel caro. Es el extremo opuesto a P07, donde el multiplicador está dentro de la recursión y el ahorro en el kernel caro es 0 %.

Qué mirar en los resultados

  • squeeze_fraction: cerca de 0 % o de 100 % no hay liberaciones y el candidato no tiene señal (no es inválido; se conserva en el denominador).
  • Liberaciones por 1.000 barras y cuántas se convierten en entrada; las perdidas, separadas por causa (m == 0, salida simultánea como la barra 13, barra no operable).
  • Duración media de los ciclos de squeeze: 50 % de barras en ciclos de 2 barras es ruido; en ciclos de 30 es régimen.
  • Exposición temporal: P08 pasa mucho tiempo plano (12 de 18 barras en el fixture).
  • Ausencia de deltas ±2: propiedad estructural comprobable en cualquier corrida.
  • Número de trades antes que Sharpe.
  • DATA_GAP_SESSIONS en KO: el hueco ensancha el Keltner y puede fabricar una liberación.
  • Paridad: exacta en señales en los carriles soportados; la ruta nativa de Nautilus es UNSUPPORTED por falta de regresión lineal rolling.

Causalidad comprobada

El test de "futuros alterados" de la especificación (300 barras, parámetros 20/2.0/1.5, t0 = 200, high, low y close × 1,5 tras t0) exige igualdad bit a bit hasta t0 en todas las columnas intermedias, no solo en target: M es donde se esconde una regresión centrada (dependería de close[t0+9]) y RELEASE donde se esconde el desplazamiento squeeze[t+1]. Un segundo test perturba solo la barra t0 y exige que m[t0−1] no cambie, que m[t0] y m[t0+n−1] sí, y que m[t0+n] ya no: fija que la ventana es finita y termina en t.

Como todo son ventanas finitas (sin EMA ni RMA), el checkpoint para continue son los últimos period cierres, máximos y mínimos, los últimos period valores de val, el squeeze previo y el objetivo vigente.

Errores típicos

Error Consecuencia Cómo evitarlo
period = 1, bb_k <= 0 o kc_k <= 0 PolicyParamError (con period = 1 la regresión divide por cero) La API lo rechaza; en un sweep cuenta como inválido
Exigir bb_k > kc_k "por sentido común" Eliminar candidatos válidos de la rejilla σ y SMA(TR) tienen escalas distintas; las 27 son válidas
Keltner con ATR o con EMA Otro squeeze, otras liberaciones keltner_sma = SMA(C) ± kc_k · SMA(TR)
Entrar con not squeeze[t] (nivel) 7 entradas en el fixture en vez de 1 La liberación es un flanco
Invertir en la barra 13 Delta de 2 donde debe haber 1 Salida primero y "no reabrir" en la barra de salida
Regresión centrada o tsf (extrapolar a x = n) Fuga de futuro, u otra serie m Endpoint en x = n − 1 con ventana [t − n + 1, t]
m como pendiente, o salida "por color" (val[t] > val[t−1]) Otra estrategia m es el valor ajustado final; la salida es por signo
Reutilizar el Donchian desplazado para HH/LL Otra fórmula y otro first_valid rolling_max/rolling_min inclusivos

Variantes y extensiones

  • Salida por "cambio de color" (LazyBear): excluida explícitamente; sería otra entrada de catálogo.
  • Ruptura armada con caducidad: P05 es la receta de compresión/ruptura con armado; el catálogo insiste en que no es P08.
  • Keltner EMA ± ATR: el de P16; comparte tr con P08 pero no el Keltner.
  • Bandas de Bollinger solas: P04.
  • Solo largos: target_lots = [0, 1] ignorando las liberaciones con m < 0.

Resumen

  • P08 entra en la liberación del squeeze (Bollinger que sale del Keltner tras estar estrictamente dentro), en la dirección de sign(m).
  • m es el extremo final de una regresión lineal rolling sobre val; se sale cuando cambia de signo (incluido el paso a 0).
  • Nunca invierte: la barra 13 del fixture es salida y liberación a la vez, y el objetivo es 0.
  • En el fixture: largo a 10,50, salida a 10,60; equity final 1.000,10 (1.000,08 con 5 bps, cálculo propio).
  • 27 candidatos, 3 regresiones: los multiplicadores quedan fuera del kernel caro.

Para practicar

  1. Con el fixture, calcula a mano m[7] a partir de la ventana val[4..7] = (0,00625; 0,04375; −0,00250; 0,04000) y comprueba 0,0301 (valor exacto 0,030125).
  2. ¿Qué pasaría en la barra 13 con una implementación que olvidase la guarda "no reabrir en la barra de salida" (if not exited)? Escribe el fill resultante (lado, cantidad, precio) y la caja tras él.
  3. Lanza el sweep 3×3×3 sobre NVDA 5min y anota squeeze_fraction y el número de trades por candidato. ¿Qué combinaciones de bb_k/kc_k se quedan sin señal? Pide los timings y comprueba que el tiempo de regresión no crece con los multiplicadores.