Saltar a contenido

P15 · DMI/ADX: tendencia filtrada

Campo Valor
Familia Tendencia · filtro de fuerza con histéresis de tres zonas (puede estar plana tras el warmup)
Features requeridas dmi (primitiva nueva de fase B, entradas high, low, close; salidas plus_di, minus_di, adx; depende de atr)
Parámetros y defaults period = 14 (feature), entry_adx = 25, exit_adx = 20 (política)
Rejilla del catálogo period ∈ {7, 14, 21, 28}, entry_adx ∈ {20, 25, 30}, exit_adx ∈ {15, 20} → 24 cartesianos, 20 válidos, 4 curvas dmi
variant_keys period, entry_adx, exit_adx
Restricciones period >= 2 y 0 <= exit_adx < entry_adx <= 100 (si no, PolicyParamError)
first_decision_index 2·period − 2 = 26 con defaults (54 con period = 28)
Activos y timeframes NVDA y KO en los 7 TF (1min1d); AMZN, INTC, MSFT, TSLA en 1d; BME y FX en 1d con los calendarios de WP-01b
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 'dmi'". Nautilus tiene DirectionalMovement, pero sin ADX, y sin ADX no hay zonas

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

Qué vas a aprender

  • La diferencia entre fuerza de tendencia (ADX) y dirección (+DI frente a −DI), y por qué la regla nunca las mezcla.
  • Cómo funciona una histéresis de tres zonas: entrar arriba de un umbral, salir abajo de otro y no hacer nada en medio.
  • La cadena completa TR → ATR → DM suavizados → DI → DX → ADX y por qué el ADX tarda 2·period − 2 barras.
  • Por qué un empate "decimal" de movimiento direccional no es un empate en float64.
  • Por qué 20 candidatos solo necesitan 4 curvas.

La idea en una frase

Cuando la tendencia es fuerte (ADX alto), ponte del lado del indicador direccional dominante; cuando se debilita del todo (ADX bajo), sal a plano; en la franja intermedia, no toques nada.

Intuición de mercado

El sistema direccional de Wilder separa dos preguntas. +DI y −DI miden qué parte del rango reciente se ha debido a subidas y qué parte a bajadas: responden "¿hacia dónde?". El ADX promedia lo desequilibrados que están los dos DI: responde "¿con cuánta convicción?", sin decir hacia dónde. Un ADX de 95 no es alcista ni bajista; es "muy direccional".

P15 opera solo cuando hay convicción. La histéresis evita el parpadeo: si entraras y salieras con el mismo umbral, un ADX que oscila alrededor de 25 abriría y cerraría posición cada barra.

Analogía. El termostato de una calefacción: enciende a 19 °C y apaga a 21 °C. Entre medias no cambia de estado. Si encendiera y apagara a 20 °C exactos, la caldera no pararía de arrancar.

Por qué podría funcionar. Los sistemas de tendencia pierden sobre todo en mercados laterales. Un filtro de fuerza intenta quedarse fuera precisamente ahí.

Cuándo falla.

  • El ADX llega tarde: es una media de una media; cuando confirma la tendencia, buena parte ya ha pasado.
  • Tendencias que se agotan con ADX aún alto: la zona media conserva la posición mientras el ADX baja lentamente hacia exit_adx.
  • Activos con ADX estructuralmente bajo: si el ADX de un activo casi nunca supera 30, el candidato con entry_adx = 30 apenas opera y sus métricas no significan nada.

Sesgos típicos al evaluarla.

  • Confundir ADX con dirección: el ADX nunca decide el signo.
  • Umbrales elegidos sobre el mismo periodo que evalúas: mira antes la distribución del ADX del activo.
  • Borrar los candidatos inválidos del denominador: los 4 candidatos con exit_adx = entry_adx = 20 se rechazan, pero cuentan en la rejilla.

Las reglas exactas

La regla canónica del catálogo: "Cuando ADX>=entry_adx, objetivo +1 si +DI>−DI y −1 si −DI>+DI. Cuando ADX<exit_adx, objetivo=0. Entre los umbrales, conservar objetivo. Igualdad DI conserva. No confundir ADX, que mide fuerza, con dirección."

Situación en el cierre de t Objetivo target[t] Motivo (reason)
t < 2·period − 2 (ADX aún NaN) 0 WARMUP
Zona alta adx[t] >= entry_adx y +DI > −DI +1 ENTRY / REVERSAL / HOLD
Zona alta y −DI > +DI −1 ENTRY / REVERSAL / HOLD
Zona alta y +DI == −DI el anterior HOLD
Zona baja adx[t] < exit_adx 0, sin mirar los DI EXIT / HOLD
Zona media exit_adx <= adx[t] < entry_adx el anterior HOLD
barra no operable se congela el publicado; el estado interno avanza NOT_TRADABLE
  • Entrada: desde plano, solo en zona alta.
  • Salida: a plano, solo en zona baja. adx == exit_adx cae en zona media y no sale.
  • Reversión: , directa en zona alta si cambia el DI dominante (delta 2). Es la diferencia con P16 y P17.
  • Filtros: el propio ADX es el filtro.
# decisión en el cierre de t, fill en el open de t+1
target_prev = 0
para cada barra t:
    si t < 2*period - 2:                    target = 0                # WARMUP
    si no, si adx[t] >= entry_adx:                                    # zona alta
        si   plus_di[t] > minus_di[t]:      target = +1
        si no, minus_di[t] > plus_di[t]:    target = -1
        si no:                              target = target_prev      # empate DI
    si no, si adx[t] < exit_adx:            target = 0                # zona baja
    si no:                                  target = target_prev      # zona media
    si target != target_prev:
        orden(target - target_prev) -> fill a open[t+1]               # ±1 o ±2
    target_prev = target

Las tres zonas no se solapan porque exit_adx < entry_adx

[entry, ∞), (−∞, exit) y [exit, entry) cubren la recta sin huecos. Con exit_adx == entry_adx la zona media desaparece y deja de haber histéresis; con exit_adx > entry_adx una misma barra pediría dos objetivos. Por eso ambas combinaciones son PolicyParamError.

Los indicadores que usa

Paso Fórmula first_valid
TR max(H−L, abs(H−C[t−1]), abs(L−C[t−1])); TR[0] = H0 − L0 0
+DM, −DM up = H[t]−H[t−1], down = L[t−1]−L[t]; +DM = up si up > down y up > 0; −DM = down si down > up y down > 0; si no 0. DM[0] = 0 0
ATR, S+DM, S−DM RMA de Wilder (α = 1/n, semilla SMA de los n primeros) n − 1
+DI, −DI 100 · S±DM / ATR (0 si ATR == 0), escala 0..100 n − 1 = 13
DX 100 · abs(+DI − −DI) / (+DI + −DI) (0 si el denominador es 0) n − 1
adx semilla mean(DX[n−1 .. 2n−2]), luego ADX[t] = ADX[t−1] + (DX[t] − ADX[t−1]) / n 2n − 2 = 26

first_decision_index = 2·period − 2: los DI están listos en la barra 13, pero la política no decide hasta que las tres salidas lo están. Con la rejilla: 12, 26, 40 y 54.

La especificación elige custom_reference como proveedor porque el adx de VectorTA mide first_valid = 27 frente al 26 canónico: una barra de diferencia en la semilla desplaza toda la curva y el punto en que se cruza entry_adx. Detalle en Las 23 primitivas de fase B y en Indicadores y features.

Ejemplo barra a barra

Fixture §6 de docs/estrategias/P15.md: 16 barras, parámetros fuera de rejilla a propósito para que las tres zonas aparezcan en 16 barras: period = 2, entry_adx = 62, exit_adx = 40 (emiten PolicyParamWarning, no error). cash_inicial = 1.000,00, tick 0,01, sin costes. first_valid(DI) = 1, first_valid(adx) = 2first_decision_index = 2.

Se muestran 14 de las 16 barras: se omiten la barra 0 (warmup, O=10,00 H=10,40 L=9,90 C=10,30) y la 4 (zona alta, +1 mantenido, ADX 100,0000).

t O H L C +DI −DI ADX zona target orden fill pos. PnL acum. sin costes equity con 5 bps*
1 10,30 10,80 10,20 10,70 36,3636 0,0000 NaN 0 WARMUP 0 0,00 1.000,00
2 10,70 11,20 10,60 11,10 52,1739 0,0000 100,0000 alta +1 ENTRY BUY 1 0 0,00 1.000,00
3 11,10 11,60 11,00 11,50 59,5745 0,0000 100,0000 alta +1 11,10 +1 0,40 1.000,39
5 11,60 11,75 11,35 11,45 34,4371 5,2980 86,6667 alta +1 +1 0,35 1.000,34
6 11,45 11,70 11,30 11,55 18,6380 8,6022 61,7544 media +1 +1 0,45 1.000,44
7 11,55 11,65 11,25 11,40 9,7196 10,4673 32,7290 baja 0 EXIT SELL 1 +1 0,30 1.000,29
8 11,40 11,50 10,95 11,00 4,1969 35,5125 55,7954 media 0 11,40 0 0,30 1.000,28
9 11,00 11,10 10,55 10,60 1,9645 55,3079 74,4676 alta −1 ENTRY SELL 1 0 0,30 1.000,28
10 10,60 10,70 10,15 10,20 0,9519 64,2870 85,7748 alta −1 10,60 −1 0,70 1.000,67
11 10,20 10,30 9,85 9,90 0,5163 65,3758 92,1038 alta −1 −1 1,00 1.000,97
12 9,90 10,00 9,40 9,60 0,2326 70,6651 95,7239 alta −1 −1 1,30 1.001,27
13 9,60 10,05 9,35 10,00 0,1019 30,9616 97,5339 alta −1 (empate DM) −1 0,90 1.000,87
14 10,00 10,35 9,90 10,30 28,0198 17,9760 59,6851 media −1 −1 0,60 1.000,57
15 10,30 10,70 10,25 10,65 50,7180 9,7758 63,6825 alta +1 REVERSAL BUY 2 sin fill −1 0,25 1.000,22

Todo es literal del fixture (§6.1, §6.2, §6.3 y la serie de equity de §6.5) salvo la última columna. *"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 de la caja en la barra del fill: fill 1 → 11,10 · 0,0005 = 0,00555 → 0,01; fill 2 → 11,40 · 0,0005 = 0,0057 → 0,01; fill 3 → 10,60 · 0,0005 = 0,0053 → 0,01. El PnL acumulado es equity − 1.000,00 del fixture.

Lo que hay que ver en la tabla:

  1. Barra 1: los DI ya existen, pero el ADX no; la política no decide.
  2. Barra 6: ADX 61,75, un pelo por debajo de 62. No es zona alta ni baja: se conserva el largo. Es el caso propio de P15.
  3. Barra 7: ADX 32,73 < 40 → a plano, aunque −DI > +DI. La zona baja ignora los DI por completo.
  4. Barra 8: el ADX rebota a 55,8, por encima de 40 pero por debajo de 62. No se reabre: la histéresis también funciona desde plano.
  5. Barra 14: +DI ya domina (28,02 > 17,98), pero el ADX está en zona media: se conserva el corto.
  6. Barra 15: zona alta con +DI dominante → reversión directa de −1 a +1 (delta 2), sin pasar por 0. No hay barra 16, así que queda sin fill; el corto se valora al último cierre y la equity final es 1.010,90 − 10,65 = 1.000,25.

Las cuentas de las barras clave

Primera decisión (t=2), period = 2:
  TR[0..2]   = 0,50 ; 0,60 ; 0,60
  ATR[1]     = (0,50 + 0,60)/2 = 0,5500           ATR[2] = 0,55 + (0,60 - 0,55)/2 = 0,5750
  +DM[0..2]  = 0 (DM0 = 0) ; 0,40 ; 0,40          -DM = 0 ; 0 ; 0
  S+DM[1]    = (0 + 0,40)/2 = 0,2000              S+DM[2] = 0,20 + (0,40 - 0,20)/2 = 0,3000
  +DI[2]     = 100 · 0,3000 / 0,5750 = 52,1739    -DI[2] = 0
  DX[1] = DX[2] = 100                             ADX[2] = mean(DX[1], DX[2]) = 100,0000
  100 >= 62 y 52,17 > 0                           -> +1 ENTRY
Salida (t=7):
  up = 11,65 - 11,70 = -0,05 ; down = 11,30 - 11,25 = +0,05  -> +DM = 0 ; -DM = 0,05
  ATR[7] = 0,4180 ; +DI = 9,7196 ; -DI = 10,4673
  DX[7]  = 100 · 0,7477 / 20,1869 = 3,7037
  ADX[7] = 61,7544 + (3,7037 - 61,7544)/2 = 32,7290 < 40   -> 0 EXIT
Caja:  1.000,00 - 11,10 = 988,90 (open[3]) ; + 11,40 = 1.000,30 (open[8]) ; + 10,60 = 1.010,90 (open[10])
Equity final: 1.010,90 + (-1)·10,65 = 1.000,25

El empate de la barra 13. El fixture elige H12 = 10,00, H13 = 10,05, L12 = 9,40, L13 = 9,35 porque 10.05 − 10.00 y 9.40 − 9.35 dan el mismo float64 (0.05000000000000071). Con la regla estricta, up > down y down > up son ambas falsas y los dos DM valen 0. Si se hubieran elegido L12 = 9,60, L13 = 9,55, la resta binaria daría 0.049999999999998934 y el "empate" desaparecería. Con >= en lugar de >, +DM[13] valdría 0,05 y +DI[13] pasaría de 0,1019 a 4,1099.

Este fixture no sirve para probar el fill en la apertura siguiente

Sus barras están encadenadas (open[t+1] == close[t]), así que un fill a close[t] y uno a open[t+1] dan el mismo precio. La especificación lo advierte y usa otro fixture con gap para ese test.

Diagrama

flowchart LR
    W(["WARMUP: t < 2·period-2"]) --> FLAT["Plano 0"]
    FLAT -->|"ADX >= entry y +DI > -DI · BUY 1"| L["Largo +1"]
    FLAT -->|"ADX >= entry y -DI > +DI · SELL 1"| S["Corto -1"]
    L -->|"ADX < exit · SELL 1"| FLAT
    S -->|"ADX < exit · BUY 1"| FLAT
    L -->|"ADX >= entry y -DI > +DI · SELL 2"| S
    S -->|"ADX >= entry y +DI > -DI · BUY 2"| L
    L -->|"zona media o empate DI"| L
    S -->|"zona media o empate DI"| S
    FLAT -->|"zona media o baja"| FLAT

Cómo lanzarla

Backtest con ambos motores sobre KO diario (2015–2024), con los defaults (forma "inline" de la API de backtesting; el resto del payload como examples/api/01_backtest_p01_both.request.json):

BASE=http://localhost:8000
SID=$(curl -sS -X POST "$BASE/v1/backtest" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p15-0001",
  "correlation_id": null,
  "schema_version": "v1",
  "payload": {
    "strategy": {"template_id": "P15",
                 "params": {"period": 14, "entry_adx": 25, "exit_adx": 20},
                 "name": "DMI/ADX 14 25/20 base", "register": true},
    "data": {"asset_id": "KO", "timeframe": "1d",
             "window": {"start": "2015-01-01T00:00:00Z", "end": "2025-01-01T00:00:00Z",
                        "mode": "reset_flat"}},
    "execution": {"lane": "SIM-S", "contract_version": "1", "initial_cash": 100000,
                  "target_lots": [-1, 0, 1], "cost_scenario": "hypothetical_5bps"},
    "engine": "both",
    "cache": {"cache_mode": "WARM_FEATURES", "write_through": true},
    "output": {"output_mode": "metrics", "include_timings": true,
               "include_parity": true, "tail_rows": 5},
    "engine_options": {"vectorta": {"kernel": "scalar", "use_batch": true},
                       "nautilus": {"adapter": "NT_FEATURES"}}
  }}' | jq -r '.payload.signature.strategy_id')
echo "$SID"
import uuid, requests
BASE = "http://localhost:8000"
env = lambda payload: {"request_id": str(uuid.uuid4()), "correlation_id": None,
                       "schema_version": "v1", "payload": payload}
payload = {
    "strategy": {"template_id": "P15",
                 "params": {"period": 14, "entry_adx": 25, "exit_adx": 20},
                 "register": True},
    "data": {"asset_id": "KO", "timeframe": "1d",
             "window": {"start": "2015-01-01T00:00:00Z",
                        "end": "2025-01-01T00:00:00Z", "mode": "reset_flat"}},
    "execution": {"lane": "SIM-S", "contract_version": "1", "initial_cash": 100000,
                  "target_lots": [-1, 0, 1], "cost_scenario": "hypothetical_5bps"},
    "engine": "both",
    "output": {"output_mode": "metrics", "include_parity": True},
    "engine_options": {"vectorta": {"kernel": "scalar", "use_batch": True},
                       "nautilus": {"adapter": "NT_FEATURES"}},
}
r = requests.post(f"{BASE}/v1/backtest", json=env(payload)).json()["payload"]
SID = r["signature"]["strategy_id"]
print(SID, r["parity"]["verdict"])
for res in r["results"]:
    print(res["adapter"], res["run_metrics"]["final_equity"], res["run_metrics"]["n_fills"],
          res["run_metrics"].get("warnings"))

Con KO espera el aviso DATA_GAP_SESSIONS (271 sesiones ausentes): los RMA continúan a través del hueco.

Derivar una variante más exigente para entrar:

curl -sS -X POST "$BASE/v1/strategies/$SID/derive" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p15-derive", "correlation_id": null, "schema_version": "v1",
  "payload": {"param_overrides": {"entry_adx": 30}, "name": "DMI/ADX 14 30/20",
              "slug": "dmi-adx-14-30-20", "tags": ["sensibilidad"]}}' | jq '.payload.strategy_id, .payload.cache_forecast'

entry_adx (igual que exit_adx) es parámetro de política: según §5.3 de la especificación, la curva dmi se reutiliza entera y solo se rehacen política y ledger. El cache_forecast debe anunciar dmi como acierto esperado y ningún fallo. Si cambiaras period, en cambio, habría que recalcular ATR, los dos RMA de DM, los DI, el DX y el ADX; solo se reutilizaría tr.

Sweep de sus variant_keys (rejilla completa del catálogo, con la forma de 03_sweep_p01_4x4.request.json):

curl -sS -X POST "$BASE/v1/sweep" -H 'Content-Type: application/json' -d '{
  "request_id": "manual-p15-sweep", "correlation_id": null, "schema_version": "v1",
  "payload": {"base_strategy_id": "'"$SID"'",
    "grid": {"period": [7, 14, 21, 28], "entry_adx": [20, 25, 30], "exit_adx": [15, 20]},
    "candidate_policy": {"drop_invalid": true, "drop_duplicates": true,
                         "register_strategies": true,
                         "name_template": "{template_name} {period} {entry_adx}/{exit_adx}"},
    "data": {"asset_id": "KO", "timeframe": "1d",
             "window": {"start": "2015-01-01T00:00:00Z", "end": "2025-01-01T00:00:00Z", "mode": "reset_flat"}},
    "execution": {"lane": "SIM-S", "contract_version": "1", "initial_cash": 100000,
                  "cost_scenario": "hypothetical_5bps"},
    "engine": "vectorta", "cache": {"cache_mode": "OPT_FULL"},
    "output": {"output_mode": "metrics"},
    "engine_options": {"vectorta": {"kernel": "scalar", "use_batch": true}}}}' | jq '.payload.job_id, .payload.dropped, .payload.factorization_plan'

Según §5.4 de la especificación: 24 candidatos solicitados, 4 inválidos (la pareja entry_adx = 20, exit_adx = 20, uno por cada period), 20 efectivos y 4 curvas dmi distintas. El resultado se consulta en GET /v1/jobs/{job_id}.

Cuánto cuesta la rejilla

Concepto Cuenta
Candidatos cartesianos 4 × 3 × 2 = 24
Parejas de umbrales válidas 5 de 6: (20,15) (25,15) (25,20) (30,15) (30,20); (20,20) viola exit < entry
Candidatos válidos 4 × 5 = 20 (los 4 rechazados siguen contando en el denominador)
Curvas dmi distintas 4, una por period; factor de reutilización 5×
Dependencia compartida una sola tr; atr(14) es la misma clave de caché que piden P16 y P19

Qué mirar en los resultados

  • parity.verdict en NT_FEATURES: PASS en la matriz (features con tolerancia, señales y fills exactos, equity al céntimo).
  • Fracción de barras en cada zona (alta, media, baja) por candidato: explica casi todo. Un candidato con el 90 % de barras en zona baja apenas opera.
  • Fracción de barras con objetivo 0 tras el warmup: debe ser grande. Si es 0, algo falla en la zona baja.
  • Reversiones directas frente a ciclos completos (±1 → ∓1 frente a ±1 → 0 → ∓1): mide si la histéresis actúa.
  • Distribución del ADX (percentiles 10/25/50/75/90) por activo y frecuencia, para decidir si la rejilla 20/25/30 tiene sentido en ese activo.
  • Contador de aciertos de caché del sweep: 20 candidatos, 4 cálculos de dmi.
  • Divergencia conocida: si comparas con vector_ta.adx, espera un desfase de una barra en la semilla (27 frente a 26). Es una variante, no un bug, y no se esconde bajo una tolerancia.

Causalidad comprobada

La especificación enumera las fugas y errores que su test de futuros alterados y su fixture detectan:

  • up calculado como H[t+1] − H[t] (un np.diff mal alineado): adelanta la señal una barra y mejora el resultado de forma sistemática.
  • Semilla del ADX con los DX de n … 2n−1 en lugar de n−1 … 2n−2: no mira el futuro, pero desplaza la curva; es exactamente la variante que se sospecha en VectorTA.
  • >= en las comparaciones de DM: otra fórmula; la barra 13 del fixture lo caza.
  • Umbrales dentro de la FeatureSpec: no es una fuga, pero destruye la factorización 5× y rompe el test de caché.

El test de futuros alterados desplaza +5,00 high, low y close desde la barra 230 de una serie de 300 y exige igualdad bit a bit de plus_di, minus_di, adx, objetivos y fills hasta la barra 200. Un complemento perturba solo high[230]: plus_di[229] y adx[229] no cambian, plus_di[230] sí.

Para reanudar en modo continue bastan siete escalares: los cuatro RMA (ATR, S+DM, S−DM, ADX en T) y la barra anterior (H[T], L[T], C[T], para el siguiente TR y los siguientes DM), más el objetivo vigente y el ledger. Guardar solo adx, plus_di y minus_di no basta: los DI son un cociente y no permiten reconstruir numerador y denominador. Y la histéresis es estado: sin target[T] la zona media no sabe qué conservar.

Errores típicos

Error Consecuencia Cómo evitarlo
exit_adx >= entry_adx PolicyParamError (las zonas se solapan o desaparece la histéresis) Respeta exit_adx < entry_adx; en un sweep cuenta como inválido, no desaparece
period = 1 o umbrales fuera de [0, 100] PolicyParamError period >= 2; el ADX es un porcentaje
Usar el ADX para decidir el signo Otra estrategia El signo sale solo de +DI frente a −DI
Decidir desde la barra 13 porque los DI ya existen Señales sin ADX válido La primera decisión es 2·period − 2
Tratar adx == exit_adx como salida Salidas de más en la frontera < estricto: la frontera cae en zona media
Escala 0..1 en los DI o el ADX Umbrales 20/25/30 sin sentido La escala canónica es 0..100
Confundir adxr con adx Curva más suave y retrasada adxr es (ADX[t] + ADX[t−n]) / 2, otro indicador
Esperar que P15 nunca invierta Sorpresa con fills de 2 unidades En zona alta un cambio de DI dominante invierte directamente

Variantes y extensiones

  • Sin histéresis (entry_adx == exit_adx): no es un candidato válido de P15; sería otra receta.
  • Solo largos: target_lots = [0, 1]; la zona alta con −DI dominante pone plano.
  • ADX como filtro de otra estrategia: combinarlo con el cruce de P01 o con el SAR de P14 para operar solo en tendencia.
  • Reinicio por sesión de los RMA: no es la convención canónica; cambiaría el ADX intradía.
  • Parientes: P02 comparte la estructura de umbrales con histéresis sobre un oscilador; P16 comparte el atr.

Resumen

  • El ADX decide la zona (alta, media, baja); los DI deciden el signo solo en zona alta.
  • Zona baja pone plano sin mirar los DI; zona media conserva lo que haya, tanto largo como plano.
  • Puede invertir directamente en zona alta (delta 2), a diferencia de P16 y P17.
  • first_decision_index = 2·period − 2 (26 con defaults); el fixture lo muestra con period = 2 y umbrales 62/40 fuera de rejilla.
  • 24 candidatos, 20 válidos, 4 curvas: los umbrales son de política y no recalculan nada.

Para practicar

  1. Con el fixture, calcula a mano la barra 8: parte de ATR[7] = 0,4180, S+DM[7] = 0,0406, S−DM[7] = 0,0438, ADX[7] = 32,7290 y de la barra 8 (H 11,50, L 10,95; H7 11,65, L7 11,25, C7 11,40). Comprueba −DM[8] = 0,30, ADX[8] = 55,7954 y que el objetivo se queda en 0.
  2. Rehaz la columna "equity con 5 bps" con 20 bps. ¿Qué comisión paga cada uno de los tres fills y cuál es la equity final?
  3. Lanza el sweep de 24 candidatos sobre KO 1d y comprueba en la respuesta que dropped.invalid vale 4 y que el plan de factorización tiene 4 curvas dmi. Después, para period = 14, compara la fracción de barras en zona alta con entry_adx = 20 y con entry_adx = 30.