Saltar a contenido

P12 · Opening Range Breakout

Campo Valor
Familia Sesión-estructura · ruptura intradía del rango de apertura, anclada al calendario; alterna plano y posición, nunca overnight
Features requeridas opening_range (salidas range_high, range_low, range_complete, in_range) y session_position (sin parámetros; usa is_penultimate_regular_bar, is_last_regular_bar); la política lee además close y session_id
Parámetros y defaults range_minutes = 30
Rejilla del catálogo range_minutes ∈ {5, 15, 30, 60} → 4 candidatos en 1min y 5min; menos en otros TF (la rejilla depende del timeframe)
variant_keys range_minutes
Restricciones entero ≥ 1; timeframe intradía (1d → PolicyParamError); range_minutes múltiplo de la duración de la barra (si no, PolicyParamError)
first_decision_index 0 en la regla (no hay medias ni recursión); en la práctica, primera barra de la primera sesión completa. Dentro de cada sesión, no se entra hasta completar el rango
Activos y timeframes Solo NVDA y KO (twelvedata, XNYS) en 1min, 2min, 5min, 15min, 30min, 1h (en 1h solo M = 60). UNSUPPORTED en 1d y en todas las series diarias de Yahoo, BME y FX
Paridad en la matriz En NT_FEATURES, NT_INTENT_REPLAY y NT_ONLINE(vectorta): PASS 4/12 (NVDA 5min y 1min, ambos costes) y UNSUPPORTED 8/12: en 1d, PolicyParamError "una barra diaria no tiene rango de apertura"; en 1h, PolicyParamError "range_minutes=30: debe ser múltiplo de la duración de la barra (60 min en 1h)". NT_ONLINE(native): UNSUPPORTED 12/12 ("nautilus_trader 1.231.0 no ofrece un indicador nativo para 'opening_range'")

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

Qué vas a aprender

  • Cómo se define con precisión "los primeros M minutos completos de la sesión" y por qué el ancla es el calendario, no la primera barra del fichero.
  • Por qué el rango se entrega acumulado barra a barra y por qué no se puede operar en la barra que lo completa.
  • Cómo se cierra la posición antes del final de sesión sin "vender al cierre que aún no se conocía".
  • Qué hacen las sesiones cortas (cierre 13:01) y el cambio de horario (EDT/EST) con la estrategia, sobre dos sesiones reales.
  • Por qué es la estrategia menos factorizable de su grupo.

La idea en una frase

Mide el máximo y el mínimo de los primeros M minutos de la sesión; cuando un cierre posterior rompa ese rango, entra en esa dirección (una vez por sesión), sal si el cierre cruza el punto medio en contra y, en cualquier caso, cierra antes del final del día.

Intuición de mercado

La primera media hora concentra la reacción a todo lo ocurrido desde el cierre anterior: noticias, órdenes acumuladas, subastas. El rango que deja es el "campo de batalla" del día. Si un cierre posterior lo rompe, uno de los dos bandos ha ganado y el movimiento tiende a continuar durante la sesión.

Analogía. Un partido de tenis en el que el primer set marca el nivel de cada jugador. Cuando uno rompe el saque por encima de lo visto en ese primer set, apuestas por él para el resto del partido; si el marcador vuelve a la mitad, te retiras; y pase lo que pase, te vas a casa antes de que cierren el estadio.

Por qué podría funcionar. Momentum intradía y flujos institucionales que ejecutan a lo largo del día en la dirección de la primera ruptura.

Cuándo falla.

  • Días sin tendencia: ruptura falsa, retorno al punto medio y salida con pérdida; como no se reentra, el día termina ahí.
  • M mal elegido: M pequeño da rangos estrechos y rupturas casi siempre; M grande deja pocas barras operables.
  • Sesiones cortas en 1h: con M = 60 queda una sola barra de oportunidad.

Sesgos típicos al evaluarla.

  • Rango difundido: calcular el máximo de la ventana con un groupby por sesión y pintarlo en todas las barras, incluidas las del propio rango. El sistema "ve" el rango antes de que exista.
  • Venta al cierre: cerrar a close de la última barra en vez de a su apertura.
  • Reloj en vez de calendario: decidir la última barra por hora UTC falla con DST, sesión corta y la barra parcial de 1h.

Las reglas exactas

La regla canónica del catálogo: "Rango=HH/LL de los primeros M minutos completos de la sesión regular. Tras completarse, tomar la primera ruptura de cierre por arriba/abajo. Como máximo una entrada por sesión. Salir al cierre que atraviese el punto medio contra la posición o emitir flat para ejecutarse en la apertura de la última barra regular."

Definiciones (spec §2.3 y §4.b):

  • session_open_ns = apertura del calendario (09:30 local); cutoff_ns = session_open_ns + M·60 s.
  • in_range[t] = market_close_ns[t] <= cutoff_ns (la barra que cierra justo en el corte pertenece).
  • mid = (range_high + range_low) / 2, constante tras completar el rango.
  • Barra operable para entrar: range_complete == 1 y no in_range y no es penúltima ni última.
Situación en el cierre de t (en este orden) Objetivo target[t] Motivo (reason)
Antes de la primera sesión completa 0 WARMUP
Cambio de session_id reinicia state = 0, entered = False (reinicio, no decisión)
Última barra regular 0 HOLD / EXIT
Penúltima barra regular (cierre programado) 0 EXIT si había posición
Largo y close < mid 0 EXIT
Corto y close > mid 0 EXIT
Plano, operable, sin entrada previa en la sesión, close > range_high +1 ENTRY
Plano, operable, sin entrada previa, close < range_low −1 ENTRY
Resto (rango incompleto, toque exacto, ya hubo entrada) el anterior HOLD
Barra no operable (is_tradable = False) se congela, salvo en penúltima y última NOT_TRADABLE
  • Entrada: ruptura estricta de cierre; como máximo una por sesión (tras salir no se reentra aunque vuelva a romper).
  • Salida: el cierre atraviesa el punto medio (estricta: close == mid conserva) o el cierre programado.
  • Reversión: no; |delta| siempre 1.
  • Nunca overnight: penúltima y última barra emiten 0 antes de mirar precios, así que ni un NaN deja una posición abierta.
# decisión en el cierre de t, fill en el open de t+1
para cada barra t:
    si session_id[t] != session_id[t-1]:  state = 0 ; entered = False     # sesión nueva
    si es_ultima[t] o es_penultima[t]:    target = 0                        # cierre programado
    si no, si state == +1:                target = 0 si close[t] < mid si no +1
    si no, si state == -1:                target = 0 si close[t] > mid si no -1
    si no, si operable[t] y no entered:
        si close[t] > range_high[t]:      target = +1 ; entered = True
        si no, si close[t] < range_low[t]: target = -1 ; entered = True
        si no:                            target = 0
    si no:                                target = 0
    si target != state:
        orden(target - state)  -> fill a open[t+1]    # el 0 de la penúltima se llena en open[última]
    state = target

La barra que completa el rango no es una ruptura

En la última barra del rango range_complete ya vale 1, pero range_high incluye el high de esa misma barra: comparar su cierre con ese máximo sería comparar un precio con un máximo que lo contiene. Por eso la entrada exige además in_range == 0. Es la fuga concreta de P12 y el motivo de que la feature tenga cuatro salidas.

Los indicadores que usa

Feature Fórmula Validez
opening_range(M).range_high / range_low máximo de high / mínimo de low de las barras de la sesión con in_range, acumulado hasta t por sesión (first_valid_rule = "session_local"), desde la primera barra
opening_range(M).range_complete 1 desde la última barra del rango hasta el final de la sesión siempre (0/1)
opening_range(M).in_range market_close_ns <= cutoff_ns siempre (0/1)
session_position.is_penultimate_regular_bar / is_last_regular_bar posición en la rejilla del calendario de la sesión (no la hora del reloj ni la barra siguiente del fichero) siempre (0/1)

first_decision_index = 0 a efectos de la regla; IntentArrays.first_decision_index se fija en la primera barra de la primera sesión completa (una sesión truncada al inicio se descarta: su "rango" no sería el de apertura). En la matriz de paridad, NVDA 5min reporta first_decision_index = 77.

Ambas features llevan calendar_version en la clave de caché: corregir un festivo o un cierre anticipado cambia el resultado sin que cambie ningún precio. Más contexto en Las 23 primitivas de fase B y en Indicadores y features.

Ejemplo barra a barra

Fixture §6 de docs/estrategias/P12.md: barras de 30 minutos, range_minutes = 60 (2 barras de rango), dos sesiones cortas reales de XNYS: A = 2024-07-03 (EDT, UTC−4) y B = 2024-11-29 (EST, UTC−5), 8 barras cada una (la última de 1 minuto, [13:00, 13:01)). cash_inicial = 1.000,00, tick 0,01, sin costes. No hay barra WARMUP: el fixture empieza en una sesión completa, y los ceros antes de completar el rango son HOLD (decisión informada), no calentamiento.

Se omiten las barras 12 y 13 de la sesión B (mantienen el corto: cierres 98,10 y 98,40 < mid 98,85).

t sesión · hora local O H L C in_range range_high range_low compl. mid pen/últ target orden fill pos. PnL acum. sin costes equity con 5 bps*
0 A · 09:30–10:00 100,00 100,60 99,80 100,40 1 100,60 99,80 0 0 HOLD 0 0,00 1.000,00
1 A · 10:00–10:30 100,40 100,90 100,10 100,20 1 100,90 99,80 1 100,35 0 HOLD (completa el rango) 0 0,00 1.000,00
2 A · 10:30–11:00 100,20 101,20 100,15 101,10 0 100,90 99,80 1 100,35 +1 ENTRY BUY 1 0 0,00 1.000,00
3 A · 11:00–11:30 101,10 101,40 100,80 101,00 0 100,90 99,80 1 100,35 +1 101,10 +1 −0,10 999,85
4 A · 11:30–12:00 101,00 101,10 100,20 100,30 0 100,90 99,80 1 100,35 0 EXIT (mid) SELL 1 +1 −0,80 999,15
5 A · 12:00–12:30 100,30 100,70 100,10 100,60 0 100,90 99,80 1 100,35 0 (ya entró hoy) 100,30 0 −0,80 999,10
6 A · 12:30–13:00 100,60 100,80 100,30 100,50 0 100,90 99,80 1 100,35 pen 0 cierre prog. 0 −0,80 999,10
7 A · 13:00–13:01 100,50 100,90 100,40 100,70 0 100,90 99,80 1 100,35 últ 0 0 −0,80 999,10
8 B · 09:30–10:00 99,00 99,40 98,60 98,80 1 99,40 98,60 0 0 HOLD (sesión nueva) 0 −0,80 999,10
9 B · 10:00–10:30 98,80 99,10 98,30 98,50 1 99,40 98,30 1 98,85 0 HOLD (completa el rango) 0 −0,80 999,10
10 B · 10:30–11:00 98,50 98,70 98,10 98,20 0 99,40 98,30 1 98,85 −1 ENTRY SELL 1 0 −0,80 999,10
11 B · 11:00–11:30 98,20 98,40 97,90 98,00 0 99,40 98,30 1 98,85 −1 98,20 −1 −0,60 999,25
14 B · 12:30–13:00 98,40 98,60 98,25 98,55 0 99,40 98,30 1 98,85 pen 0 EXIT (cierre prog.) BUY 1 −1 −1,15 998,70
15 B · 13:00–13:01 98,55 98,80 98,45 98,70 0 99,40 98,30 1 98,85 últ 0 98,55 0 −1,15 998,65

OHLC, horas, features, objetivos, los cuatro fills (2→3 a 101,10; 4→5 a 100,30; 10→11 a 98,20; 14→15 a 98,55) y la equity en las barras 3, 5, 11 y 15 son literales del fixture; el resto del PnL es caja + pos · close − 1.000,00 con la caja del fixture. El fixture da mid también en las barras 0 y 8 (100,20 y 99,00, con rango parcial); aquí se muestra "—" porque la regla no lo usa hasta completar el rango. *"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 3): 101,10 · 0,0005 = 0,05055 → 0,05;
  • fill 2 (barra 5): 100,30 · 0,0005 = 0,05015 → 0,05;
  • fill 3 (barra 11): 98,20 · 0,0005 = 0,0491 → 0,05;
  • fill 4 (barra 15): 98,55 · 0,0005 = 0,049275 → 0,05.

Total 0,20; equity final 998,85 − 0,20 = 998,65.

Lo que hay que ver en la tabla:

  1. Barras 0 y 8: range_high ya tiene valor (acumulado parcial), pero range_complete = 0: no se puede operar. No existe un rango completo antes de terminarlo.
  2. Barras 1 y 9: completan el rango (market_close_ns == cutoff_ns, igualdad incluida) y no son operables (in_range = 1).
  3. Barra 2: 101,10 > 100,90 → largo; fill al open de 3.
  4. Barra 4: 100,30 < 100,35 → sale por punto medio. Barra 5: ya hubo entrada hoy; aunque rompiera, no reentraría.
  5. Barra 6: cierre programado sin posición: lo importante es que no se abre nada.
  6. Barra 14: cierre programado con corto abierto; el fill es open[15] = 98,55, la apertura de la última barra (la subasta de cierre), nunca close[15] = 98,70.
  7. DST: la barra 6 (A) es 16:30–17:00 UTC y la 14 (B) es 17:30–18:00 UTC; misma hora local. La feature las marca igual porque usa la posición en la rejilla de la sesión, no la hora UTC.

Las cuentas de las barras clave

Sesión A: session_open_ns = 1_720_013_400e9 (09:30 EDT = 13:30Z)
          cutoff_ns = session_open_ns + 60·60·1e9 = 1_720_017_000e9 (10:30 local = 14:30Z)
Barra 1:  market_close_ns = 1_720_017_000e9 <= cutoff_ns  -> in_range = 1 (igualdad: pertenece)
          range_high = max(100,60; 100,90) = 100,90 ; range_low = min(99,80; 100,10) = 99,80
          mid = (100,90 + 99,80)/2 = 100,35 ; operable = compl. y no in_range = False
Barra 2:  close 101,10 > 100,90 -> +1 ; exit_long = 101,10 < 100,35 = False (imposible en la barra de entrada)
Barra 4:  close 100,30 < 100,35 -> 0 (entered sigue True)
Sesión B: session_open_ns = 1_732_890_600e9 (09:30 EST = 14:30Z) ; cutoff 10:30 local = 15:30Z
Barra 10: range_low = 98,30 ; close 98,20 < 98,30 -> -1
Barra 14: es_penultima = 1 -> 0 sin mirar precios ; fill en open[15] = 98,55
Caja:  1.000,00 − 101,10 = 898,90 ; + 100,30 = 999,20 ; + 98,20 = 1.097,40 ; − 98,55 = 998,85
Equity: cierre 3 = 898,90 + 101,00 = 999,90 ; cierre 11 = 1.097,40 − 98,00 = 999,40 ; final 998,85

session_id salta de 20240703 a 20241129 (426 en el valor, 105 sesiones de calendario): los límites se detectan por cambio de valor, nunca restando ids.

Diagrama

Opening Range Breakout: rango de apertura, ruptura, punto medio y cierre programado

flowchart LR
    NS(["Sesión nueva: state 0, entered = no"]) --> R["Formando rango (in_range)"]
    R -->|"rango completo y barra fuera del rango"| FLAT["Plano, puede entrar"]
    FLAT -->|"C > range_high · BUY 1"| L["Largo +1"]
    FLAT -->|"C < range_low · SELL 1"| S["Corto -1"]
    L -->|"C < mid · SELL 1"| DONE["Plano, ya entró hoy"]
    S -->|"C > mid · BUY 1"| DONE
    L -->|"penúltima barra · SELL 1"| END["Fin de sesión: plano"]
    S -->|"penúltima barra · BUY 1"| END
    FLAT -->|"penúltima barra"| END
    DONE -->|"penúltima barra"| END
    END -->|"cambio de session_id"| NS

El estado "ya entró hoy" solo sale por el final de la sesión: es la regla de "como máximo una entrada por sesión".

Cómo lanzarla

Backtest con ambos motores sobre NVDA 5min en 2024, con los defaults (30 minutos = 6 barras de rango):

BASE=http://localhost:8000
SID=$(curl -sS -X POST "$BASE/v1/backtest" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p12-0001",
  "correlation_id": null,
  "schema_version": "v1",
  "payload": {
    "strategy": {"template_id": "P12", "params": {"range_minutes": 30},
                 "name": "ORB 30 min 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": "P12", "params": {"range_minutes": 30}, "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"])

No pidas timeframe: "1d" ni "1h" con M = 30: la API responde con PolicyParamError (son exactamente los 8 casos UNSUPPORTED de la matriz).

Derivar una variante con un rango de 15 minutos:

curl -sS -X POST "$BASE/v1/strategies/$SID/derive" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p12-derive", "correlation_id": null, "schema_version": "v1",
  "payload": {"param_overrides": {"range_minutes": 15}, "name": "ORB 15 min",
              "slug": "orb-15", "tags": ["sensibilidad"]}}' | jq '.payload.strategy_id, .payload.cache_forecast'

range_minutes es el único parámetro y es de feature: según la tabla de invalidación (§5.3 de la spec) hay que rehacer opening_range(15) entera, porque cambia el conjunto de barras del rango y no se comparte ningún acumulado entre M distintos. session_position no tiene parámetros y se reutiliza.

Sweep de sus variant_keys:

curl -sS -X POST "$BASE/v1/sweep" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p12-sweep", "correlation_id": null, "schema_version": "v1",
  "payload": {"base_strategy_id": "'"$SID"'",
    "grid": {"range_minutes": [5, 15, 30, 60]},
    "candidate_policy": {"drop_invalid": true, "drop_duplicates": true,
                         "register_strategies": true, "name_template": "{template_name} {range_minutes}"},
    "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'

Resultado en GET /v1/jobs/{job_id}.

Cuánto cuesta la rejilla

Timeframe Candidatos válidos (spec §5.4) Curvas opening_range session_position
1min 4 (5, 15, 30, 60) 4 1
2min 2 (30, 60) 2 1
5min 4 (5, 15, 30, 60) 4 1
15min 3 (15, 30, 60) 3 1
30min 2 (30, 60) 2 1
1h 1 (60) 1 1
1d 0 (UNSUPPORTED)
Total intradía 16 16 6 (22 entradas de caché)

Sobre NVDA 5min: 4 candidatos, 4 curvas de rango distintas (ninguna se comparte) y una sola session_position, que además comparte P13. Los candidatos rechazados por el múltiplo en otros TF se conservan con su motivo.

Qué mirar en los resultados

  • Exposición overnight = 0 exactamente (posición del ledger en la última barra de cada sesión). Cualquier otro valor es un fallo, no un resultado.
  • Trades por sesión ∈ {0, 1} y fracción de sesiones con trade: 5 % indica M demasiado grande; 95 %, demasiado pequeño.
  • Reparto de salidas: punto medio frente a cierre programado. 100 % de cierres programados = "entrar y aguantar hasta el cierre".
  • Barras operables por sesión (distribución), sesiones sin rango completo, sesiones descartadas por apertura truncada y, aparte, missing_sessions / DATA_GAP_SESSIONS (KO: 271 sesiones sin barras, que no son "sesiones sin rango").
  • calendar_version por adaptador: si difieren, las marcas de sesión difieren y la divergencia se atribuiría al motor por error.
  • Paridad: PASS en NVDA 5min y 1min en los tres carriles soportados; el carril nativo no aplica (Nautilus no tiene calendario de mercado ni indicador de rango de apertura).

Causalidad comprobada

La spec (§8.2) define tres tests: perturbar las barras 3–7 y exigir idénticos bit a bit rango, objetivos y el fill de la entrada de la barra 2; perturbar solo barras posteriores al rango y exigir que range_high/range_low no cambien en ninguna barra de la sesión (caza el groupby difundido); y perturbar una sesión entera sin que cambie nada de la otra (el estado se reinicia de verdad).

Riesgo operativo número uno sobre los datos reales (§8.4): el proveedor rellena algunas sesiones cortas (2021-11-26, 2023-07-03) con 179 barras de volumen cero hasta las 15:59. El calendario las deja fuera de la sesión; si entraran, el cierre programado se emitiría casi tres horas tarde.

Para continue a mitad de sesión hacen falta seis campos (target, entered, session_id, rango acumulado, marcas y ancla); lo robusto es cortar el checkpoint en un límite de sesión, donde el estado es trivial.

Errores típicos

Error Consecuencia Cómo evitarlo
Usar timeframe = "1d" PolicyParamError: "una barra diaria no tiene rango de apertura" Solo intradía de NVDA o KO
range_minutes no múltiplo de la barra (p. ej. 30 en 1h) PolicyParamError Consulta la matriz TF × M de la spec §2.2
Rango difundido a toda la sesión Opera con un máximo que aún no existe Rango acumulado y range_complete
Entrar en la barra que completa el rango Compara el cierre con un máximo que lo contiene Exigir in_range == 0
< en vez de <= en in_range Rango de M − 30 minutos (en 30min) La barra que cierra en el corte pertenece
Cerrar a close de la última barra Venta a un precio que no se conocía Emitir 0 en la penúltima; fill en open[última]
Última barra por hora UTC Falla con DST y en sesión corta Posición en la rejilla del calendario
Reentrar tras salir por punto medio Más trades de los que permite la receta entered no se reinicia dentro de la sesión
Restar session_id Saltos de 73 (viernes-lunes) o 8.870 (diciembre-enero) Detectar cambio de valor

Variantes y extensiones

  • Reentrada permitida: sin el candado entered; es la diferencia principal con P13, que sí reentra.
  • Buffer de ruptura (close > range_high + k·tick) o salida con banda: parámetros nuevos, otra estrategia.
  • Stop en el extremo opuesto del rango en lugar del punto medio.
  • FX: exigiría decidir qué es "la apertura" en un mercado 24/5; decisión de producto, no de implementación.
  • Parientes: P11 Donchian (ruptura de canal sin sesión) y P13 VWAP (misma session_position y mismo cierre programado).

Resumen

  • P12 mide el rango de los primeros M minutos completos (ancla del calendario), y entra con la primera ruptura estricta de cierre posterior, una vez por sesión.
  • Sale al atravesar el punto medio o con el cierre programado: 0 en la penúltima barra, fill en la apertura de la última.
  • El rango se entrega acumulado y la barra que lo completa no es operable: así no se conoce el rango antes de terminarlo.
  • Solo NVDA y KO intradía; 1d y los múltiplos imposibles son PolicyParamError. En la matriz: PASS 4/12, UNSUPPORTED 8/12.
  • Es la estrategia menos factorizable: cada M es un rango nuevo; solo session_position se comparte.

Para practicar

  1. En la sesión B del fixture, calcula cutoff_ns (EST) y comprueba que la barra 9 cierra exactamente en él. ¿Qué pasaría con range_low y con la entrada de la barra 10 si in_range usara <?
  2. Rehaz la sesión A suponiendo que la barra 5 cierra en 101,50. ¿Cambia algo? ¿Por qué? Después, rehaz la barra 6 con un cierre de 102,00.
  3. Lanza el sweep de range_minutes sobre NVDA 5min y sobre KO 5min y compara la fracción de salidas por punto medio frente a cierre programado para M = 5 y M = 60.