P01 · Cruce de medias EMA¶
| Campo | Valor |
|---|---|
| Familia | Tendencia · dirección continua (siempre largo o corto tras el warmup) |
| Features requeridas | ema_fast, ema_slow (primitiva ema, entrada close) |
| Parámetros y defaults | fast = 20, slow = 50 |
| Rejilla del catálogo | fast ∈ {5, 10, 20, 40}, slow ∈ {50, 100, 150, 200} → 16 candidatos válidos, 8 curvas EMA distintas |
variant_keys |
fast, slow |
| Restricciones | enteros ≥ 1 y fast < slow (si no, PolicyParamError) |
first_decision_index |
slow − 1 = 49 con defaults |
| Activos y timeframes | NVDA y KO en los 7 TF (1min … 1d); AMZN, INTC, MSFT, TSLA en 1d; BME y FX en 1d con reservas |
| Paridad en la matriz | PASS en NT_FEATURES, NT_INTENT_REPLAY y NT_ONLINE(vectorta) (12/12 cada una). PARITY_FAIL nativa en NT_ONLINE(native) (12/12): la EMA nativa de Nautilus usa semilla "primer valor" (nt_seed_first_value) y la canónica semilla SMA; primera divergencia en la barra 19 de KO 1d (ema_fast 45,3790 frente a 45,3564). Es una variante declarada, no un bug |
Fuentes: docs/estrategias/P01.md (especificación), catalog/strategy_catalog.json, docs/DATOS_POR_ESTRATEGIA.md, runs/parity/summary.json.
Qué vas a aprender¶
- Por qué dos medias exponenciales de distinta velocidad sirven para seguir una tendencia.
- Que P01, pese a su nombre, no reacciona a un "cruce" sino al nivel relativo de las medias en cada barra.
- A seguir un trade completo del fixture de la especificación: entrada, reversión de largo a corto y la cuenta de caja.
- Por qué una rejilla 4×4 cuesta 8 curvas y no 32.
- Qué significa la divergencia "nativa" de la matriz de paridad y por qué no invalida la estrategia.
La idea en una frase¶
Si la media rápida está por encima de la lenta, estar largo; si está por debajo, estar corto.
Intuición de mercado¶
Una EMA es un "promedio con memoria que se desvanece": pesa más lo reciente. La rápida (20) reacciona a las últimas semanas; la lenta (50) resume un periodo más largo. Cuando el precio sube de forma sostenida, la rápida se despega por encima de la lenta; cuando cae, por debajo. Comparar las dos es una forma barata de preguntar "¿el pasado reciente es mejor que el pasado largo?".
Analogía. Dos termómetros en una habitación: uno reacciona en minutos, el otro en horas. Si el rápido marca más que el lento, la habitación se está calentando. No sabes cuánto durará, pero sabes la dirección.
Por qué podría funcionar. Los mercados muestran momentum de medio plazo: las tendencias persisten más de lo que predeciría un paseo aleatorio, por flujos lentos de capital, difusión gradual de información y comportamiento gregario.
Cuándo falla.
- Mercados laterales: las medias se entrecruzan una y otra vez y cada cruce es una reversión con coste (el famoso whipsaw).
- Giros bruscos: la media lenta tarda en reaccionar; se devuelve buena parte de la tendencia antes de invertir.
- Costes: cada reversión mueve 2 unidades (de +1 a −1), así que paga doble comisión.
Sesgos típicos al evaluarla.
- Sobreajuste de la pareja (elegir 17/43 porque "salió mejor"): compara siempre contra la rejilla completa.
- Sesgo de supervivencia: probarla solo en NVDA (una tendencia histórica excepcional) hace que parezca mejor de lo que es.
- Warmup distinto entre candidatos:
slow = 200pierde 199 barras; compara candidatos desde un inicio común.
Las reglas exactas¶
La regla canónica del catálogo: "Con ambas medias válidas, objetivo +1 si EMA(f)>EMA(s), −1 si EMA(f)<EMA(s); igualdad conserva el objetivo anterior. Estado inicial 0. El objetivo solo genera una orden cuando cambia. Ejecución en la apertura siguiente."
Situación en el cierre de t |
Objetivo target[t] |
Motivo (reason) |
|---|---|---|
t < first_decision_index (alguna EMA aún NaN) |
0 | WARMUP |
F[t] > S[t] (estricto) |
+1 | ENTRY / REVERSAL / HOLD |
F[t] < S[t] (estricto) |
−1 | ENTRY / REVERSAL / HOLD |
F[t] == S[t] (igualdad exacta en f64) |
el anterior | HOLD |
F o S es NaN tardío |
el anterior | HOLD |
barra no operable (is_tradable = False) |
se congela el publicado | NOT_TRADABLE |
- Entrada: la primera barra válida en la que las medias están separadas ya da ±1 (no espera a un cruce).
- Salida: no hay salida a plano; la "salida" de un largo es la reversión a corto.
- Reversión: permitida; delta de 2 unidades.
- Filtros: ninguno.
# decisión en el cierre de t, fill en el open de t+1
target_prev = 0
para cada barra t:
si t < first_decision_index: target = 0 # WARMUP
si no, si F[t] > S[t]: target = +1
si no, si F[t] < S[t]: target = -1
si no: target = target_prev # empate o NaN
si target != target_prev:
delta = target - target_prev # ±1 al abrir, ±2 al invertir
orden(delta) -> fill a open[t+1] # nunca a close[t]
target_prev = target
El nombre engaña: no es una regla de cruce
La regla compara niveles de la misma barra t; no consulta t−1 para detectar un cruce. Consecuencia: en la primera barra válida, si ema20 > ema50, P01 ya entra largo sin haber visto ningún cruce. La especificación lo registra en su §11 como desviación de forma respecto a la lectura ingenua del nombre.
Los indicadores que usa¶
| Feature | Fórmula | first_valid |
|---|---|---|
ema{n} |
α = 2/(n+1); semilla ema[n−1] = SMA(close[0..n−1]); después ema[t] = α·close[t] + (1−α)·ema[t−1] |
n − 1 |
first_decision_index = max(fast−1, slow−1) = slow − 1. Con defaults son 49 barras de warmup; con slow = 200, 199.
La EMA es una recursión causal: ema[t] solo depende de close[0..t]. Detalle numérico y comparación entre motores en Indicadores paso a paso y en Indicadores y features.
Ejemplo barra a barra¶
Fixture §6 de docs/estrategias/P01.md: fast = 3, slow = 5 (fuera de rejilla a propósito, para calcular a mano), cash_inicial = 1.000,00, tick 0,01, sin costes. α_3 = 0,5, α_5 = 1/3. first_valid(ema3) = 2, first_valid(ema5) = 4 → first_decision_index = 4.
Las barras 0–1 son idénticas a la 2 (O=100,00, H=100,20, L=99,80, C=100,00) con ambas EMA en NaN.
| t | O | H | L | C | ema3 | ema5 | target | orden | fill | pos. | PnL acum. sin costes | equity con 5 bps* |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2 | 100,00 | 100,20 | 99,80 | 100,00 | 100,000000 | NaN | 0 WARMUP | — | — | 0 | 0,00 | 1.000,00 |
| 3 | 100,00 | 100,20 | 99,80 | 100,00 | 100,000000 | NaN | 0 WARMUP | — | — | 0 | 0,00 | 1.000,00 |
| 4 | 100,00 | 100,20 | 99,80 | 100,00 | 100,000000 | 100,000000 | 0 HOLD (empate) | — | — | 0 | 0,00 | 1.000,00 |
| 5 | 100,00 | 100,20 | 99,80 | 100,00 | 100,000000 | 100,000000 | 0 HOLD | — | — | 0 | 0,00 | 1.000,00 |
| 6 | 100,00 | 100,20 | 99,80 | 100,00 | 100,000000 | 100,000000 | 0 HOLD | — | — | 0 | 0,00 | 1.000,00 |
| 7 | 100,00 | 100,30 | 99,70 | 100,00 | 100,000000 | 100,000000 | 0 HOLD | — | — | 0 | 0,00 | 1.000,00 |
| 8 | 100,50 | 102,30 | 100,30 | 102,00 | 101,000000 | 100,666667 | +1 ENTRY | BUY 1 | — | 0 | 0,00 | 1.000,00 |
| 9 | 102,50 | 104,30 | 102,30 | 104,00 | 102,500000 | 101,777778 | +1 | — | 102,50 | +1 | 1,50 | 1.001,45 |
| 10 | 104,50 | 106,30 | 104,30 | 106,00 | 104,250000 | 103,185185 | +1 | — | — | +1 | 3,50 | 1.003,45 |
| 11 | 106,00 | 106,40 | 104,60 | 105,00 | 104,625000 | 103,790123 | +1 | — | — | +1 | 2,50 | 1.002,45 |
| 12 | 104,50 | 105,10 | 102,60 | 103,00 | 103,812500 | 103,526749 | +1 | — | — | +1 | 0,50 | 1.000,45 |
| 13 | 102,50 | 103,10 | 100,60 | 101,00 | 102,406250 | 102,684499 | −1 REVERSAL | SELL 2 | — | +1 | −1,50 | 998,45 |
| 14 | 100,50 | 101,10 | 98,60 | 99,00 | 100,703125 | 101,456333 | −1 | — | 100,50 | −1 | −0,50 | 999,35 |
| 15 | 98,50 | 99,10 | 96,60 | 97,00 | 98,851562 | 99,970889 | −1 | — | — | −1 | 1,50 | 1.001,35 |
Todo es literal del fixture 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 → 102,50 · 0,0005 = 0,05125 → 0,05; fill 2 → 2 · 100,50 · 0,0005 = 0,1005 → 0,10. El PnL acumulado es equity − 1.000,00 del fixture.
Lo que hay que ver en la tabla:
- Barras 4–7: ambas EMA valen exactamente 100,000000; la igualdad conserva el estado inicial 0 y no se emite orden. Es el caso "empate determinista" de la especificación.
- Barra 8:
101,000000 > 100,666667→ objetivo +1. La orden se decide al cierre de 8 y se llena al open de 9 (102,50), no al cierre de 8 (102,00). - Barra 13:
102,406250 < 102,684499→ reversión. El fill en el open de 14 vende 2 unidades: 1 para cerrar el largo y 1 para abrir el corto. Caja:897,50 + 2·100,50 = 1.098,50. - Barra 15: el corto queda abierto; la equity final es
1.098,50 − 97,00 = 1.001,50. No hay liquidación automática al final.
Las cuentas de las barras clave¶
Semillas (t=2 y t=4): ema3[2] = (100+100+100)/3 = 100,000000
ema5[4] = (100·5)/5 = 100,000000
Entrada (t=8): ema3[8] = 0,5·102,00 + 0,5·100,000000 = 101,000000
ema5[8] = (1/3)·102,00 + (2/3)·100,000000 = 100,666667 -> F > S -> +1
Reversión (t=13): ema3[13] = 0,5·101,00 + 0,5·103,812500 = 102,406250
ema5[13] = (1/3)·101,00 + (2/3)·103,526749 = 102,684499 -> F < S -> -1
Caja: 1.000,00 - 1·102,50 = 897,50 (fill 1, open[9])
897,50 + 2·100,50 = 1.098,50 (fill 2, open[14])
Equity final: 1.098,50 + (-1)·97,00 = 1.001,50
Diagrama¶
flowchart LR
W(["WARMUP: t < slow-1"]) -->|"ambas EMA válidas y F == S"| FLAT["Plano 0"]
W -->|"F > S"| L["Largo +1"]
W -->|"F < S"| S["Corto -1"]
FLAT -->|"F > S · BUY 1"| L
FLAT -->|"F < S · SELL 1"| S
L -->|"F < S · SELL 2"| S
S -->|"F > S · BUY 2"| L
L -->|"F >= S"| L
S -->|"F <= S"| S
El estado plano solo existe al principio (arrastrado por el empate): una vez en mercado, P01 alterna entre largo y corto.
Cómo lanzarla¶
Backtest con ambos motores sobre NVDA 5min en 2024, con los defaults (petición real examples/api/01_backtest_p01_both.request.json; aquí con la estrategia referida por plantilla y parámetros, forma "inline" de la API de backtesting):
BASE=http://localhost:8000
curl -sS -X POST "$BASE/v1/backtest" -H 'Content-Type: application/json' -d '{
"request_id": "manual-p01-0001",
"correlation_id": null,
"schema_version": "v1",
"payload": {
"strategy": {"template_id": "P01", "params": {"fast": 20, "slow": 50},
"name": "EMA cross 20/50 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", "indicator_provider": "native"}}
}}' | jq '.payload.signature.strategy_id, .payload.parity.verdict'
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": "P01", "params": {"fast": 20, "slow": 50}},
"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"] # P01-dc9260 con estos parámetros
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 (20/100) desde la instancia base P01-dc9260 (petición real 06b_derive_p01_20_100.request.json):
curl -sS -X POST "$BASE/v1/strategies/P01-dc9260/derive" -H 'Content-Type: application/json' -d '{
"request_id": "manual-p01-derive", "correlation_id": null, "schema_version": "v1",
"payload": {"param_overrides": {"slow": 100}, "name": "EMA cross 20/100",
"slug": "ema-cross-20-100", "tags": ["sensibilidad"]}}' | jq '.payload.strategy_id, .payload.cache_forecast'
slow es parámetro de feature: la respuesta anuncia ema:100 como fallo de caché esperado y ema:20 como reutilizable.
Sweep de sus variant_keys (rejilla completa del catálogo, 03_sweep_p01_4x4.request.json):
curl -sS -X POST "$BASE/v1/sweep" -H 'Content-Type: application/json' -d '{
"request_id": "manual-p01-sweep", "correlation_id": null, "schema_version": "v1",
"payload": {"base_strategy_id": "P01-dc9260",
"grid": {"fast": [5, 10, 20, 40], "slow": [50, 100, 150, 200]},
"candidate_policy": {"drop_invalid": true, "drop_duplicates": true,
"register_strategies": true, "name_template": "{template_name} {fast}/{slow}"},
"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'
La respuesta real de ese sweep declara 16 puntos solicitados, 16 válidos y distintos (0 inválidos, 0 duplicados) y 8 curvas de feature distintas. El resultado se consulta en GET /v1/jobs/{job_id}.
Cuánto cuesta la rejilla¶
| Concepto | Cuenta |
|---|---|
| Candidatos cartesianos | 4 fast × 4 slow = 16 |
Inválidos por fast < slow |
0 (todo fast ≤ 40 < todo slow ≥ 50) |
| Curvas EMA distintas | {5,10,20,40} ∪ {50,100,150,200} = 8 |
| Llamadas lógicas sin factorizar | 16 × 2 = 32 (ahorro del 75 %) |
| Políticas y ledgers | 16 y 16: el ledger no se comparte, cada pareja es otra serie de objetivos |
Los dos parámetros de P01 son de feature: cambiar cualquiera recalcula una EMA. Es lo contrario de P02, cuyos umbrales solo tocan la política. Un cambio de fee_rate o de initial_cash, en cambio, reutiliza las dos EMA y solo rehace el ledger.
Qué mirar en los resultados¶
parity.verdictantes que cualquier cifra. ConNT_FEATURESla respuesta real del ejemplo 01 daPASSen features (toleranciartol 1e-10), señales (exactas), fills (exactos) y equity (céntimos exactos).- Número de fills frente a
|delta|total: en P01 las reversiones cuentan 2 unidades, así que la suma de|delta|supera al número de fills. En el ejemplo real sobre NVDA 5min 2024 hubo 328 fills. costs_totalfrente a la ganancia bruta: en intradía, con cientos de reversiones, 5 bps pueden comerse todo el margen.first_decision_indexefectivo (49 con defaults) y el avisoDATA_GAP_SESSIONSsi usas KO (271 sesiones ausentes; la EMA sigue a través del hueco).- Divergencia conocida: si pides
NT_ONLINEconindicator_provider: "native", esperasPARITY_FAILen la capafeaturesen la barrafast−1(semilla distinta). Converge con el tiempo, pero cerca del warmup puede cambiar el signo deF > S, que es exactamente lo que decide P01. Por eso no se esconde bajo una tolerancia.
Causalidad comprobada¶
La especificación define el test de "futuros alterados": sobre NVDA 1d (600 barras, 20/50) se multiplican por 1,35 todos los precios posteriores a la barra 400 y se exige que EMA, objetivos, motivos, fills con fill_index ≤ 400 y equity hasta el cierre 400 queden idénticos bit a bit. El fill de la señal de la barra 400 sí puede cambiar: su precio es open[401], que ya pertenece al tramo alterado. Confundir la frontera de la decisión con la del fill es justo el error que el test distingue.
Para reanudar P01 en modo continue basta un checkpoint con los dos escalares ema_fast[t0−1] y ema_slow[t0−1], el objetivo vigente y el estado del ledger; guardar solo la equity no basta.
Errores típicos¶
| Error | Consecuencia | Cómo evitarlo |
|---|---|---|
fast >= slow |
PolicyParamError (con fast == slow el objetivo quedaría en 0 para siempre) |
La API lo rechaza; en un sweep se cuenta como inválido, no desaparece |
| Esperar un "cruce" para entrar | Tu backtest manual no cuadra con el motor | Recuerda: regla de nivel, entra en la primera barra válida |
Llenar al close[t] |
Sesgo optimista (usas un precio que ya conocías) | El ledger siempre llena en open[t+1] |
| Sustituir EMA por SMA "porque da igual" | Otra estrategia: señales distintas | La SMA sería P01-SMA, otra entrada de catálogo |
| Comparar 20/50 con 40/200 desde barras distintas | Muestras distintas | Fija un inicio común ≥ 199 |
| Olvidar el empate | Esperar que F == S ponga plano |
La igualdad conserva el objetivo anterior |
Variantes y extensiones¶
- P01-SMA: misma lógica con medias simples; exige su propia entrada de catálogo (no es una sustitución silenciosa).
- Banda neutra: estar plano si
|F − S| < ε·S; reduce whipsaw, pero es otra receta. - Solo largos:
target_lots = [0, 1]con la regla−1 → 0; habitual en acciones. - Filtro de régimen: combinarla con ADX (P15) para operar solo con tendencia fuerte.
- Parientes: P03 MACD (diferencia de EMA suavizada) y P18 Hull (media menos retardada).
Resumen¶
- P01 compara
EMA(fast)yEMA(slow)en cada cierre: +1 si la rápida está por encima, −1 si está por debajo, empate conserva. - Es un sistema siempre en mercado tras el warmup; las reversiones mueven 2 unidades.
- Decisión en el cierre de
t, fill en el open det+1; el fixture lo muestra con una entrada en 102,50 y una reversión en 100,50. - Una rejilla 4×4 son 16 candidatos pero solo 8 curvas EMA: los periodos se reutilizan.
- Paridad PASS en todos los carriles salvo la EMA nativa de Nautilus, que es una variante declarada por la semilla.
Para practicar¶
- Con el fixture, calcula a mano
ema3[9]yema5[9]a partir de los valores de la barra 8 y comprueba 102,500000 y 101,777778. - Rehaz la columna "equity con 5 bps" con 10 bps. ¿Cuál es la equity final? (Pista: 0,10 y 0,20 de comisión.)
- Lanza el sweep 4×4 sobre KO 1d y NVDA 1d. ¿Qué pareja gana en cada activo? ¿Es la misma? Discute qué dice eso sobre el sobreajuste.