Q07 · HMM causal de regímenes: tendencia / reversión / efectivo¶
| Campo | Valor |
|---|---|
| Familia | Cambio de régimen: un modelo oculto de Markov decide cuánto peso dar a una regla de reversión, a una de tendencia o a quedarse fuera |
| Universo / panel | Una serie (un activo); al menos 1500 barras diarias |
| Datos | OHLC diario |
| Features | returns, volatility (vol20), hmm_parameters, filtered_probabilities, trend_and_reversion_targets |
| Parámetros y defaults | formation = 1260, refit_bars = 126, states = 3, max_iter = 200; trend_lookback = 63 (default añadido por DEV-03) |
| Rejilla | formation ∈ {756, 1260}, refit_bars ∈ {63, 126}, trend_lookback ∈ {21, 63, 126} → 12 candidatos |
| Fijado en la receta | covariance_type = diag, random_state = 1729, n_init = 1, tolerancia 1e-4 |
| Estado | Especificada, no implementada (specified_not_implemented, hito H4) |
| Motores implicados | Ninguno ejecuta hoy un HMM. Relacionados: finaz_models_statistics (GARCH con arch, volatilidad condicional), finaz_training/validation (splits walk-forward sin fuga), la política P04 como objetivo de reversión |
Estado
Q07 no está implementada y no tiene especificación detallada con fixture en docs/estrategias/ (solo la receta del catálogo). La API de backtesting la declara UNSUPPORTED con NOT_IMPLEMENTED_YET. El ejemplo numérico de esta ficha es didáctico, calculado a mano con parámetros inventados para ilustrar la recursión forward; no procede de ningún modelo ajustado ni de datos reales.
Fuentes: catalog/strategy_catalog.json, docs/API.md (DEV-03), docs/DESVIACIONES_INFORME.md (DEV-03), packages/finaz_models_statistics, docs/v2/MOTORES.md (AG-018, AG-021).
Qué vas a aprender¶
- Qué es un modelo oculto de Markov (HMM) y cómo se interpreta un "régimen".
- La diferencia crucial entre probabilidad filtrada (causal) y probabilidad suavizada (usa el futuro).
- Cómo se ordenan los estados para que "bajo, medio, alto" signifique siempre lo mismo tras cada refit.
- Cómo se mezclan dos reglas (reversión y tendencia) con las probabilidades del régimen.
- Por qué
predict_probao Viterbi sobre todo el periodo no sirven en un backtest.
La idea en una frase¶
Estimar en cada barra la probabilidad de estar en un régimen tranquilo, normal o turbulento, y operar reversión cuando es tranquilo, tendencia cuando es normal y nada cuando es turbulento.
Intuición y evidencia académica¶
Intuición. Los mercados alternan periodos de calma (volatilidad baja, movimientos que se deshacen), periodos normales (tendencias moderadas) y periodos de estrés (volatilidad alta, saltos). La literatura de modelos de cambio de régimen en finanzas (modelos de Markov con cambio de estado aplicados a retornos y volatilidad) muestra que estos estados son identificables a posteriori. La dificultad está en identificarlos en tiempo real.
El catálogo lo advierte: "HMM por sí solo no genera una ventaja". La ventaja, si existe, está en la combinación con reglas que funcionan de forma distinta en cada régimen.
Cuándo falla.
- Etiquetas que rotan: el ajuste numera los estados arbitrariamente; sin reordenar, "estado 0" puede ser tranquilo en un refit y turbulento en el siguiente.
- Retraso: la probabilidad filtrada tarda unas barras en detectar un cambio de régimen; el daño del primer día de crisis no se evita.
- Sobreajuste: tres estados con medias, varianzas y matriz de transición son muchos parámetros para un solo activo.
El modelo matemático¶
Observaciones (normalizadas con media y desviación de la formación):
Modelo: K = 3 estados ocultos, matriz de transición A (A[j,k] = P(estado k en t | estado j en t−1)), emisiones gaussianas con covarianza diagonal.
Recursión forward (filtro causal, en espacio logarítmico en la implementación):
alpha_t(k) ∝ p(x_t | k) · sum_j A[j,k] · alpha_{t−1}(j)
alpha_t = alpha_t / sum_k alpha_t(k) probabilidades filtradas
alpha_t solo usa x_1 … x_t. La probabilidad suavizada P(k | x_1 … x_T) usa todo el periodo y está prohibida en la evaluación.
Ordenación de estados: por la varianza de r en la formación → baja, media, alta.
Peso objetivo:
Pseudocódigo causal¶
para cada bloque de refit_bars barras:
formación = [b − formation, b) (+ historial previo para vol20)
normalizar features con la formación; ajustar HMM (semilla 1729, diag, tol 1e-4)
ordenar estados por varianza de r; guardar convergencia, semilla y orden
inicializar alpha filtrando el pasado con el modelo nuevo
para cada t del bloque:
alpha_t = forward(alpha_{t−1}, x_t) # nunca backward
w_t = p_baja · rev_t + p_media · trend_t
q = trunc(w_t · frozen_equity / close[t]); fill en open[t+1]
Cómo lo haría la plataforma¶
| Pieza | Quién la cubriría | Estado |
|---|---|---|
Features r, vol20 |
Primitivas del banco (log_return, rolling_vol) |
Existen |
| Objetivo de reversión | La política P04 (Bollinger) del banco | Existe como estrategia popular |
| Objetivo de tendencia | momentum{trend_lookback} |
Existe (Q01) |
| HMM y filtro forward | Código propio o hmmlearn con filtro propio |
No existe; hmmlearn no está en ninguna imagen |
| Volatilidad condicional alternativa | finaz_models_statistics.ajustar_predecir_garch(retornos, horizonte, niveles, p=1, q=1) |
Existe (G4, arch) |
| Splits sin fuga para validar | finaz_training/validation (folds, splits cronológicos con embargo) |
Existe (AG-018) |
| Registro de semilla, convergencia y orden | finaz_training/registry (artefacto + ModelRef) |
Existe (AG-019) |
| Ejecución | Ledger de cartera (una sola pata) | Existe |
flowchart LR
D["Serie diaria<br/>≥ 1500 barras"] --> F["x = [r, abs r, vol20]<br/>normalizado en formación"]
F --> H["HMM 3 estados<br/>(no implementado)"]
H --> O["Ordenar por varianza<br/>baja · media · alta"]
O --> A["Filtro forward causal<br/>p_baja, p_media, p_alta"]
P["Objetivo P04<br/>reversión"] --> M["Mezcla<br/>p_baja·rev + p_media·trend"]
T["sign(momentum 63)"] --> M
A --> M
M --> L["Ledger<br/>fill open[t+1]"]
G["finaz_models_statistics<br/>GARCH (alternativa de vol)"] -.-> F
Ejemplo numérico¶
Ejemplo didáctico
Parámetros inventados para ilustrar el cálculo. Una sola observación (r) en lugar de tres para que quepa a mano.
Modelo supuesto (tres estados ya ordenados):
baja media alta
media r 0.0005 0.0000 −0.0010
desv. r 0.005 0.012 0.030
A = [[0.95, 0.04, 0.01], (desde baja)
[0.05, 0.90, 0.05], (desde media)
[0.02, 0.08, 0.90]] (desde alta)
alpha_{t−1} = [0.60, 0.30, 0.10]
Barra 1: r = +0,004 (un día tranquilo).
predicción A' · alpha = [0.587, 0.302, 0.111]
verosimilitud p(r|k) = [62.45, 31.45, 13.12] densidad normal en r
producto = [36.66, 9.50, 1.46]
alpha_1 = normalizado = [0.770, 0.200, 0.031]
Si la regla de reversión (P04) está neutral (0) y el momentum es positivo (+1):
Barra 2: r = −0,025 (un golpe fuerte).
predicción A' · alpha_1 = [0.742, 0.213, 0.045]
verosimilitud = [≈ 0, 3.80, 9.66] en baja, −0.025 está a 5 desviaciones
alpha_2 = [0.0001, 0.649, 0.351]
Con reversión en compra (+1, el precio tocó la banda inferior) y momentum ya negativo (−1):
Un solo día violento ha movido la probabilidad del régimen tranquilo de 0,77 a casi cero y ha llevado un 35 % de probabilidad al régimen turbulento, que pesa 0. La regla de reversión, que en calma habría comprado el golpe, queda anulada. Esa es la intuición de Q07.
Lo que no se debe hacer. Si se calculasen las probabilidades suavizadas con toda la serie, la barra 1 "sabría" que viene un golpe y ya bajaría p_baja: una fuga de información que haría el backtest irrealmente bueno.
Cómo lanzarla¶
Hoy no se puede lanzar. GET /v1/capabilities devuelve Q07 UNSUPPORTED en todos los carriles (NOT_IMPLEMENTED_YET) y POST /v1/backtest responde 501 STRATEGY_UNSUPPORTED_BY_ADAPTER. El catálogo publica su param_schema con los cinco parámetros, incluido trend_lookback = 63 (DEV-03).
Qué se puede ejecutar hoy de la familia estadística. GARCH(1,1) con arch está en la imagen quant como adaptador (finaz_models_statistics.adaptador_arch); el job S7 por defecto del rol ejecuta ARIMA/ETS:
echo '{"job_id": "demo-quant-stats", "rol": "quant", "operacion": "ridge_catboost_stats"}' \
> /spool/jobs/demo-quant-stats.json
python -m finaz_runtime.jobs.runner --rol quant --once
Como biblioteca, dentro de la imagen quant (ilustrativo):
from finaz_models_statistics.adaptador_arch import ajustar_predecir_garch
dist = ajustar_predecir_garch(retornos, horizonte=1, niveles=(0.1, 0.5, 0.9), p=1, q=1)
Una volatilidad GARCH filtrada no es un HMM, pero cubre una parte de la misma pregunta ("¿estamos en un periodo turbulento?") con un motor que ya existe y pasa G4.
Riesgos, límites y qué no hace¶
- No implementada y sin especificación con fixture: es la receta menos madura del catálogo después de Q09/Q10.
- Muchos parámetros para una serie: 3 medias × 3 features, 3 × 3 varianzas, 6 transiciones libres.
- Dependencia de la semilla y del orden: por eso la receta fija
random_state = 1729,n_init = 1y guarda el orden de etiquetas. - Retraso de detección inherente al filtro causal.
- Historial de vol20: la ventana de 20 barras debe incluirse antes de la formación, o las primeras filas tendrán NaN.
- No es una estrategia por sí sola: sin P04 y el momentum, el HMM no produce pesos.
Resumen¶
- HMM gaussiano de 3 estados sobre
[r, |r|, vol20], estados ordenados por varianza. - Probabilidades filtradas (forward), nunca suavizadas ni Viterbi sobre la evaluación.
- Peso = mezcla de reversión (régimen bajo) y tendencia (régimen medio); régimen alto = fuera.
- Especificada solo en el catálogo; no implementada; la plataforma aporta features, P04, GARCH y la disciplina de validación sin fuga.
Para practicar¶
- Repite la barra 2 del ejemplo con
r = −0,010. ¿Cuánto bajap_baja? - ¿Por qué ordenar los estados por varianza y no por media?
- Explica con tus palabras por qué Viterbi sobre toda la evaluación es una fuga aunque el modelo se haya ajustado solo con la formación.
- Propón cómo usar la banda de cuantiles de
ajustar_predecir_garchcomo sustituto simple dep_alta.