← Volver a los artículos
July 3, 2026
5 min de lectura

El espacio de parámetros de dos ejes: por qué la mayor parte de tu barrido debería ser casi gratis

El espacio de parámetros de dos ejes: por qué la mayor parte de tu barrido debería ser casi gratis
#algotrading
#backtest
#búsqueda de parámetros
#optimización
#caché
#maldición de la dimensionalidad
Part 4 of 10 · Collection
High-Performance Backtest Engines

Parte de la serie "Backtests sin ilusiones".

La maldición de la dimensionalidad suele contarse como una advertencia: cada parámetro que añades multiplica el espacio de búsqueda, así que una estrategia de 18 dimensiones es inviable de barrer. Ese planteamiento asume calladamente que cada dimensión cuesta lo mismo evaluar. No es así. En nuestro motor dual/triple-timeframe, dos tercios de los parámetros son casi gratis de barrer, y un tercio carga con esencialmente toda la factura de cómputo. En cuanto ves esa división, "el espacio de búsqueda es demasiado grande" deja de ser la queja correcta. La pregunta correcta es: ¿por qué dimensiones estás pagando?

La dimensión que no necesitaba recalcularse

Nuestro benchmark de búsqueda de parámetros ejecuta una estrategia de momentum multi-timeframe: una media móvil de Hull y su variante triplemente suavizada (HMA3) en dos o tres marcos temporales, con una compuerta de separación direccional que decide cuándo un cruce es lo bastante "limpio" como para actuar sobre él. El espacio de búsqueda completo para la variante de tres timeframes tiene dieciocho dimensiones: el periodo y la longitud de HMA por cada timeframe, más un conjunto de umbrales de separación de entrada y salida en cada timeframe.

La forma ingenua de barrer esto es un único bucle: elegir un vector de parámetros, construir cada indicador desde cero, ejecutar la simulación, puntuarla y repetir. Así fue como empezamos. Era lento, y era lento por una razón que resultó ser embarazosa en cuanto la identificamos: para la abrumadora mayoría de las pruebas adyacentes, estábamos recalculando indicadores que no habían cambiado.

Desplaza el umbral de separación de entrada de 0,03 a 0,035 y vuelve a ejecutar. La HMA sobre un año de velas de 1 minuto es idéntica bit a bit a la prueba anterior — el umbral no aparece en ningún lugar de su definición. Aun así, el bucle ingenuo la reconstruye de todos modos, sobre más de medio millón de velas, en tres timeframes, cada vez. Estábamos gastando casi todo nuestro presupuesto de cómputo re-derivando cantidades que eran invariantes respecto al parámetro que en realidad estábamos variando.

Esa observación es todo el artículo. En el momento en que notas que algunos parámetros cambian los indicadores y otros solo cambian la regla de decisión aplicada sobre indicadores fijos, el espacio de parámetros deja de ser una nube plana de dimensiones y se convierte en dos capas anidadas con precios radicalmente distintos.

Dos ejes, no uno

Dos ejes de un espacio de parámetros a precios distintos: una rejilla dispersa y costosa de indicadores cuyas celdas desbloquean cada una una hoja densa y casi gratuita de configuraciones de umbrales

Divide los parámetros según lo que su reevaluación te obliga a recalcular:

  • El eje costoso — parámetros de indicadores. El periodo del timeframe y la longitud de la HMA. Cambia cualquiera de los dos y tienes que reconstruir el indicador sobre toda la serie de precios: remuestrear el timeframe superior, ejecutar la ponderación de Hull, recalcular la serie de cruces y la separación en cada cruce. Esto es un barrido O(n) por vela de historia, y lo pagas por completo en cada combinación distinta. En la estrategia de tres timeframes este eje tiene seis parámetros: (period, hma_length) para cada uno de los timeframes alto, medio y bajo.

  • El eje barato — umbrales de decisión. Las compuertas de separación direccional que deciden, dadas las series de cruce y separación ya calculadas, si entrar o salir. Cambia un umbral y ningún indicador se mueve. Solo vuelves a ejecutar un único recorrido O(n) sobre las señales precalculadas, comprobando las compuertas y registrando las ejecuciones. En la estrategia de tres timeframes este eje tiene doce parámetros (cuatro umbrales de separación — entrada-compra, entrada-venta, salida-compra, salida-venta — en cada uno de los tres timeframes). La variante de dos timeframes tiene ocho (cuatro umbrales en cada uno de los dos timeframes).

Así que la forma real del espacio es: 6 costosos + 12 baratos = 18 para tres timeframes, y 4 costosos + 8 baratos = 12 para dos timeframes. Dos tercios de la dimensionalidad viven en el eje barato. Y los dos ejes no solo difieren en cantidad — difieren en coste unitario por más de tres órdenes de magnitud, que es la cifra de la que trata el resto de este artículo.

La estructura está estrictamente anidada. Cada punto del eje costoso — un (period, hma_length) concreto por timeframe — define un conjunto de arrays de señales fijos. Sobre esa base fija se asienta toda una hoja del eje barato: miles de vectores de umbrales, cada uno un recorrido rápido sobre los mismos arrays. Pagas la base una sola vez y la amortizas sobre toda la hoja.

Por qué los indicadores son invariantes a los umbrales

El cacheo solo funciona por un hecho matemático, no por un detalle de implementación afortunado, y merece la pena enunciarlo con precisión porque es el supuesto que sostiene todo lo demás. Los arrays de indicadores son función únicamente de (period, hma_length). Los umbrales de separación no aparecen en ningún lugar de su definición.

En concreto, para cada timeframe el motor precalcula cuatro arrays alineados sobre el índice base: la HMA hma, su versión triplemente suavizada hma3, la serie de cruces cross (+1 compra / −1 venta / 0), y el porcentaje de separation en cada cruce. Cada uno de ellos se deriva de los precios y de los dos parámetros del indicador. El umbral — el número contra el que comparas separation — se aplica más tarde, en el momento de la decisión. Nunca toca los arrays.

Esto no es una reordenación trivialmente segura. Es segura solo porque tuvimos cuidado con cuándo está disponible la información, que es la preocupación del trabajo sobre sesgo de look-ahead en otra parte de esta serie. El indicador del timeframe superior en la vela base i se calcula a partir de velas superiores ya cerradas más una vela en formación cuyo cierre corriente es igual al cierre base actual close[i] — un valor conocido en la vela i. Nada del futuro se filtra. Así que la señal precalculada en la vela i es exactamente lo que un bot en vivo habría visto en la vela i, y sigue siendo válida sin importar contra qué umbral la compares después. Cachear una señal con fugas solo cachearía la fuga; cachear una señal causal cachea algo con lo que realmente puedes operar.

La invariancia nos da una factorización. Escribamos la evaluación de un vector de parámetros completo θ = (indicator_params, threshold_params) como:

signals   = build_indicators(indicator_params)      # EXPENSIVE, depends only on indicator_params
score     = simulate(signals, threshold_params)     # CHEAP, reuses signals across all thresholds

build_indicators no lee threshold_params. Ese único hecho es lo que legitima el caché: mantén indicator_params fijo, varía threshold_params libremente, y signals es una constante que calculas una sola vez.

La arquitectura: calcular una vez, barrer muchas

Un caché de señales indexado por parámetros de indicadores que alimenta un barrido rápido por lotes: una única construcción de indicador se despliega en una amplia fila de evaluaciones de umbrales que comparten los mismos arrays cacheados

El motor (scripts/engine_multitf.py, commit bfc8aaa en nuestro backtester) implementa la factorización con dos piezas cuyos nombres dicen exactamente lo que hacen.

SignalCache — el eje costoso, memoizado. Es un diccionario indexado por (period_bars, hma_length). Le pides las señales de un timeframe y devuelve el TFSignals cacheado si esa combinación de indicador ya se construyó antes; en caso contrario, la construye una vez y la guarda. Como la clave son solo los parámetros del indicador, cada configuración de umbral que comparte una combinación de indicador — y en un barrido denso de umbrales eso son miles de ellas — impacta la misma entrada de caché. Los timeframes superiores se benefician especialmente de esto: una rejilla gruesa de, digamos, cuatro periodos candidatos por un puñado de longitudes de HMA es un número pequeño de construcciones costosas distintas, y cada una se reutiliza en toda la hoja de umbrales apilada encima.

sweep_separations — el eje barato, por lotes. Toma los arrays de señales cacheados y una matriz de vectores de umbrales (sps, de forma [m, 12]) y los pasa todos por un único kernel compilado. Cada fila es un recorrido O(n): recorre las velas, aplica las compuertas, registra ejecuciones sin fugas en open[i+1], y totaliza el PnL y el tiempo en posición. Ningún indicador se reconstruye dentro de este bucle — lee cross y separation directamente del caché. La simulación interna está compilada JIT (Numba), así que una vez calentada, el coste por configuración está dominado por el único barrido lineal sobre las velas, no por la sobrecarga de Python.

Las dos piezas se combinan en la búsqueda anidada natural: el bucle externo recorre el eje costoso (cada iteración construye y cachea una combinación de indicador), y el bucle interno despliega un amplio lote a través del eje barato sobre esos arrays cacheados. En el código la forma es exactamente esa — un pequeño bucle costoso que envuelve un amplio lote barato:

cache = SignalCache(base_close, base_ts)           # keyed by (period, hma_length)

for htf_p, htf_h, mtf_p, mtf_h, ltf_p, ltf_h in indicator_grid:   # EXPENSIVE axis (coarse)
    htf = cache.get(htf_p, htf_h)                  # built once, then a cache hit forever
    mtf = cache.get(mtf_p, mtf_h)
    ltf = cache.get(ltf_p, ltf_h)

    sps = sample_thresholds(m=4000)                # CHEAP axis: [m, 12] threshold vectors
    pnl, n_trades, bars_in_pos = sweep_separations(  # one compiled batch, no indicator work
        base_close, base_open, htf, ltf, sps, mtf=mtf)

Tocas el constructor costoso tan pocas veces como lo permite la rejilla de indicadores, y dejas que el barrido barato haga el volumen — miles de filas de sps contra arrays que nunca se mueven. Esa es toda la optimización — sin aproximaciones, sin pérdida de fidelidad, solo negarse a recalcular lo que no cambió.

Lo que realmente cuesta lo "gratis": los números

Una comparación de barras del coste por configuración: una columna alta para recalcular por configuración junto a una franja fina para el paso de umbral cacheado, anotada con la brecha de aproximadamente 1.600x

Medimos los dos ejes sobre la carga de trabajo de demostración: un año completo de velas de 1 minuto de ETHUSDT (~527k velas), tres timeframes, con los indicadores precalentados y la compilación JIT fuera de la medición de tiempo.

En el eje barato, sweep_separations sostiene ~5.600 configuraciones de umbral por segundo. Eso es una simulación completa — compuertas, ejecuciones, PnL, exposición — para cada configuración, sobre medio millón de velas, a razón de aproximadamente 180 microsegundos por configuración. La razón por la que puede ser tan rápido es que no hace ningún trabajo de indicadores: cada configuración lee los mismos arrays cacheados de cross y separation.

Ahora calculemos el precio de la alternativa. Construir una vez el conjunto de indicadores de tres timeframes toma del orden de unos pocos cientos de milisegundos (~0,3 s) — remuestrear tres timeframes, ponderación de Hull, extracción de cruce y separación sobre todo el año. Si recalcularas los indicadores dentro del bucle de configuraciones — el diseño ingenuo de un solo bucle — cada una de esas 5.600 configuraciones por segundo pagaría en su lugar la construcción completa del indicador. El coste por configuración se dispara de ~180 microsegundos a ~0,3 segundos:

Coste por configuración Configs/s
Eje barato (señales cacheadas) ~180 µs ~5.600
Recalcular indicadores por configuración ~0,3 s ~3,4

La proporción es ~1.600x. Barrer el eje barato sobre señales cacheadas es aproximadamente tres órdenes de magnitud más barato que el diseño ingenuo que reconstruye los indicadores para cada vector de umbrales. Dicho en concreto: un lote de unos pocos miles de configuraciones de umbral que termina en menos de un segundo por la vía cacheada tardaría la mayor parte de una hora si cada configuración reconstruyera sus indicadores. Mismos resultados, misma fidelidad, sin atajos en las matemáticas — la única diferencia es que uno de los dos recalcula un invariante y el otro no.

Esto no es una microoptimización que se añade al final. Cambia qué búsquedas son viables. A 5.600 cfg/s el eje de umbrales es lo bastante denso como para explorarlo como es debido — puedes permitirte una rejilla fina o una muestra larga aleatoria/QMC por combinación de indicador — mientras el eje costoso se mantiene como un puñado deliberadamente grueso de construcciones. El presupuesto de cómputo fluye hacia donde los parámetros realmente cuestan algo.

La maldición de la dimensionalidad, re-tasada

Una rejilla plana y uniforme de parámetros reetiquetada como un mapa de precios de dos niveles: unas pocas celdas costosas resaltadas con trazo grueso, el resto sombreadas como casi gratuitas

Volvamos al planteamiento con el que abrió este artículo. La maldición de la dimensionalidad dice que el espacio de búsqueda crece exponencialmente con el número de parámetros, así que más dimensiones es categóricamente peor. Eso es cierto respecto al tamaño de la rejilla. Es engañoso respecto al coste de cubrirla, porque valora cada dimensión de igual manera.

Una vez que separas los ejes, el recuento se lee de otra manera. La estrategia de tres timeframes tiene 18 dimensiones, pero solo 6 de ellas son costosas. Las otras 12 son dimensiones del eje barato que expanden la rejilla sin expandir la factura de cómputo de forma significativa — puedes lanzarles miles de configuraciones por céntimos. La estrategia de dos timeframes tiene 12 dimensiones, con solo 4 dimensiones costosas y 8 baratas. En ambos casos, la mayor parte de la "maldición" se concentra en el eje que cuesta casi nada barrer.

Así que la forma honesta de razonar sobre el coste de la búsqueda no es "cuántos parámetros" sino "cuántos parámetros costosos, y de qué grosor puede ser su rejilla." El eje costoso es donde el exponencial realmente duele, y es donde quieres una rejilla pequeña y bien elegida — unos pocos periodos candidatos, un rango modesto de longitudes de HMA — posiblemente refinada de gruesa a fina, en el espíritu del drill-down de resolución adaptativa que usamos en otra parte. El eje barato es donde puedes ser generoso, porque cada configuración de umbral adicional cuesta 180 microsegundos.

Esta re-tasación se generaliza mucho más allá de nuestra estrategia de HMA. El patrón — algunos parámetros cambian las features, la mayoría de los parámetros cambian la regla aplicada sobre features fijas — reaparece por todas partes en el trading sistemático. Las longitudes de indicadores, las frecuencias de remuestreo y las ventanas de lookback son costosas; los umbrales de entrada/salida, las distancias de stop, los multiplicadores de tamaño de posición y las compuertas de confirmación son baratos. Cada vez que un parámetro solo remodela la frontera de decisión sobre señales ya calculadas, pertenece al eje barato, y ahí es donde debe barrerse. Ganancias de cacheo relacionadas de la misma familia aparecen en el cacheo parquet multi-timeframe y en la escalera de velocidad del motor más amplia.

Donde el almuerzo gratis tiene factura

El eje barato es barato en cómputo. No es barato en estadística, y confundir ambas cosas es como conviertes una ganancia de rendimiento en una máquina de sobreajuste.

Cada configuración de umbral que evalúas es una prueba, y cuando ejecutas miles de pruebas sobre un único dataset, la mejor de ellas se ve bien en parte por suerte. Hacer esas pruebas 1.600 veces más baratas no hace que la suerte desaparezca — hace que sea más fácil acumularla. Un barrido denso de umbrales es precisamente la situación en la que la inflación por pruebas múltiples muerde más fuerte: muchos candidatos, una sola historia, y una regla de selección que reporta el máximo. El motor más rápido del mundo te entregará encantado un vector de umbrales que se ajusta bellamente al ruido de tu ventana de prueba y falla fuera de muestra.

Así que la disciplina tiene que escalar con la velocidad. En el momento en que el coste de cómputo de una prueba cae hacia cero, la contabilidad estadística se convierte en la restricción vinculante, y tienes que pagarla explícitamente:

  • Deflacta según el número de pruebas. Puntúa al ganador en función de cuántas configuraciones probaste, no contra cero. El Sharpe Ratio deflactado y la probabilidad de sobreajuste del backtest existen exactamente para esto — convierten "probamos 4.000 vectores de umbral" en un recorte sobre el edge reportado.
  • Valida fuera de muestra, por fold. Un barrido barato debe seguir ejecutándose dentro de una división walk-forward honesta; un umbral que solo gana dentro de muestra no vale nada por muy rápido que lo hayas encontrado. Nuestro motor mantiene las ejecuciones sin fugas (open[i+1]) precisamente para que el eje barato no pueda comprar rendimiento espiando.
  • Prefiere las mesetas a los picos. Como el eje de umbrales es denso y rápido, puedes mapear toda su superficie de respuesta, no solo su argmax. Una región amplia de umbrales que funcionan todos es un edge real; un único máximo puntiagudo es un artefacto de ajuste — la distinción entre meseta y pico, asequible precisamente gracias a la velocidad que estamos describiendo.

Lee la estructura de dos ejes de la manera correcta: no te compra más confianza, te compra más pruebas al mismo coste de confianza por prueba. Eso es genuinamente valioso — la cobertura densa del eje barato es lo que te permite encontrar mesetas y caracterizar la superficie — pero solo si mantienes honesto el libro de cuentas estadístico. La velocidad elimina la excusa del cómputo para no buscar a fondo; no elimina la obligación de descontar lo que esa búsqueda a fondo encuentra.

Cómo estructurar tu propia búsqueda

Para aplicar esto a tu propia estrategia, el trabajo es sobre todo de clasificación — ordenar tus parámetros en el eje correcto — seguido de un bucle anidado:

  1. Etiqueta cada parámetro según lo que te obliga a recalcular. Si cambiarlo cambia un array de indicador/feature, es costoso. Si solo cambia una comparación, un umbral, o una regla de dimensionamiento aplicada sobre arrays fijos, es barato. Ante la duda, pregunta: ¿aparece este parámetro en algún lugar dentro de la definición del indicador? Si no, es barato.
  2. Indexa un caché solo sobre el eje costoso. Memoiza la construcción de features por sus parámetros de indicador (como hace SignalCache con (period, hma_length)). Las pruebas adyacentes que comparten una combinación de indicador reutilizan entonces los mismos arrays gratis.
  3. Agrupa por lotes el eje barato sobre las features cacheadas. Ejecuta las configuraciones de umbral como un bucle compilado y ajustado sobre las señales precalculadas, no como reevaluaciones completas. De aquí viene tu throughput — y en nuestro caso los ~5.600 cfg/s.
  4. Anida los bucles: el costoso por fuera, el barato por dentro. Mantén la rejilla costosa pequeña y deliberada (gruesa, o de gruesa a fina); deja que el barrido barato sea denso. Gasta el presupuesto donde los parámetros realmente cuestan algo.
  5. Presupuesta las pruebas contra el sobreajuste, no contra el reloj. Ahora que el reloj ya no es el límite, deja que el Sharpe deflactado / PBO lo sea. Decide cuántas pruebas del eje barato puedes permitirte estadísticamente, y valida al ganador fuera de muestra.

El beneficio de ingeniería y la salvaguarda estadística son dos caras de la misma idea: separar los ejes te permite buscar exhaustivamente las dimensiones baratas, que es exactamente la razón por la que después debes descontar cuán exhaustivamente buscaste.

Conclusiones clave

  1. No todas las dimensiones cuestan lo mismo. Los parámetros de una estrategia se dividen en un eje costoso (indicadores — recalculados sobre toda la serie) y un eje barato (umbrales — un recorrido O(n) sobre señales precalculadas). En nuestro motor eso es 6 costosos + 12 baratos para tres timeframes, 4 + 8 para dos timeframes.
  2. Los indicadores son invariantes a los umbrales, y esa invariancia es toda la optimización. La construcción de features depende solo de los parámetros del indicador, así que construyes una vez, cacheas por (period, hma_length), y reutilizas en cada configuración de umbral que comparte la combinación.
  3. El eje barato corre ~1.600 veces más barato. ~5.600 configuraciones de umbral por segundo (~180 µs cada una) sobre señales cacheadas, frente a ~0,3 s por configuración si reconstruyes los indicadores cada vez. Misma fidelidad — la única diferencia es negarse a recalcular un invariante.
  4. La maldición de la dimensionalidad en realidad es una maldición de dimensionalidad costosa. La mayor parte del recuento de parámetros vive en el eje que es casi gratuito de barrer. Mantén gruesa la rejilla costosa; sé generoso con la barata.
  5. La velocidad desplaza la restricción vinculante del cómputo a la estadística. Un barrido denso y rápido es un motor de pruebas múltiples. Deflacta según el número de pruebas, valida por fold, y prefiere las mesetas a los picos — el almuerzo gratis es real, pero la factura estadística no es opcional.

El motor completo — SignalCache, sweep_separations, la simulación multi-TF sin fugas, y la prueba de paridad que la ancla a la semántica en vivo de la vela en formación — vive en scripts/engine_multitf.py (commit bfc8aaa) en nuestro backtester. La próxima vez que alguien te diga que una estrategia de 18 parámetros es demasiado grande para buscar, pregúntale cuántos de esos parámetros realmente mueven los indicadores. Normalmente es un tercio de ellos, y el resto es casi gratis.

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Mantente a la vanguardia

Suscríbete a nuestro boletín para recibir información exclusiva sobre trading con IA, análisis de mercado y actualizaciones de la plataforma.

Respetamos tu privacidad. Puedes darte de baja en cualquier momento.