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

La trampa de precisión de la GPU: cómo un backtest en fp32 sobre Apple Metal devuelve basura en silencio

La trampa de precisión de la GPU: cómo un backtest en fp32 sobre Apple Metal devuelve basura en silencio
#trading algorítmico
#backtest
#gpu
#apple-silicon
#coma flotante
#estabilidad numérica
#mlx
Part 8 of 10 · Collection
High-Performance Backtest Engines

Parte de la serie "Backtests sin ilusiones".

Portamos nuestro backtest de barrido de parámetros a la GPU y se volvió 2796× más rápido. Los números que devolvía eran plausibles. También eran, en la primera versión que funcionaba, pura basura —erraban por un factor de doscientos— y nada se cayó, nada avisó, nada parecía estar mal.

Esta es la trampa que hace que el backtesting sobre GPU en Apple Silicon sea peligroso de un modo que el código de CPU no lo es: Metal no tiene float64. Cada número que tu backtest toca en una GPU de Apple es un float de 32 bits, lo hayas pedido o no. Y la forma más tentadora de vectorizar una media móvil —el truco de la suma de prefijos O(n) que todo quant obsesionado con el rendimiento busca— es precisamente la formulación que fp32 no puede sobrevivir a escala de precios. No lanza un error. Se ejecuta a toda velocidad y te entrega una curva de capital de aspecto plausible construida sobre una media móvil que está equivocada por un factor de 211.

La solución es la parte interesante, porque no es "usa más bits" (no puedes) ni es "sé más cuidadoso" (el código ingenuo ya es cuidadoso). La solución es calcular la misma cantidad matemática mediante una suma distinta —una convolución directa por ventanas— que mantiene cada valor intermedio lo suficientemente pequeño como para que fp32 lo represente con exactitud. La misma WMA, el mismo resultado hasta siete cifras significativas, y 55.9× más rápido que numba de un solo hilo en la CPU. Este artículo es la autopsia: por qué Metal te obliga a usar fp32, exactamente dónde desborda la formulación obvia, por qué la correcta no lo hace, y cómo demostramos —contando operaciones, no mirando curvas de reojo— que la versión rápida y la versión honesta en fp64 concuerdan.

Todos los números aquí están medidos en un Apple M2 Max, 150 000 barras × 80 combinaciones de parámetros, mejor de 3, a partir de scripts/bench_param_sweep.py del repositorio (el método GPU M5, commit 97eadaf), scripts/engine_multitf_gpu.py (04d71e8), y las notas de diseño en scripts/GPU_NOTES.md.

Metal no tiene float64

Pipeline de la GPU Metal de Apple con float64 tachado — cada lane forzado a 32 bits, enteros exactos solo hasta 1.6×10⁷

En una CPU, un backtest de Python/numpy corre en doble precisión por defecto. float64 te da una mantisa de 52 bits: los enteros se representan con exactitud hasta 2⁵³ ≈ 9×10¹⁵, y la precisión relativa es de aproximadamente 1.1×10⁻¹⁶. Casi nunca piensas en ello, porque a escala de precios —un cierre de BTC en torno a 30 000, una suma acumulada que llega a 10¹⁴— fp64 tiene margen de sobra.

La GPU de Apple no ofrece ese margen, porque no ofrece float64 en absoluto. El Metal Shading Language no tiene tipo double; MLX, PyTorch-MPS y cualquier otro framework que apunta a Apple Silicon hereda esto. No hay bandera para activarlo ni un respaldo lento-pero-correcto. Si tu cálculo toca la GPU, ocurre en fp32:

  • Mantisa: 23 bits (24 con el bit líder implícito). Los enteros son exactos solo hasta 2²⁴ = 16 777 216 ≈ 1.6×10⁷.
  • Precisión relativa: ~1.2×10⁻⁷. Aproximadamente siete cifras decimales significativas, y no más.

Ese techo de 1.6×10⁷ es toda la historia. Suena generoso —dieciséis millones— hasta que notas que un backtest habitualmente construye cantidades intermedias mucho mayores que eso, y en el momento en que una intermedia cruza 1.6×10⁷, fp32 ya no puede ni siquiera representar enteros consecutivos, y menos aún la estructura fraccionaria que necesitas. La precisión no se degrada con elegancia; se cae por un precipicio, y el precipicio está en un número que tus datos rebasan sin pensarlo dos veces.

La trampa es que fp32 casi siempre está bien. La mayor parte de un backtest —precios, retornos, PnL, Sharpe— vive cómodamente en el rango donde siete dígitos son de sobra. Así que el porte ingenuo funciona, pasa una prueba rápida, produce una salida de aspecto sensato. El fallo está localizado en exactamente una operación, el único lugar donde una intermedia se dispara más allá de 10⁷, y esa operación es la que todo el mundo vectoriza primero.

La formulación tentadora: WMA en una sola pasada de suma de prefijos

Nuestra estrategia se apoya en medias móviles de Hull. Una HMA son tres medias móviles ponderadas compuestas entre sí; la variante HMA3 son cuatro. Una media móvil ponderada con un kernel lineal sobre una ventana de longitud p es

WMAt=k=1pkxtp+kp(p+1)/2.\mathrm{WMA}_t = \frac{\sum_{k=1}^{p} k \cdot x_{t-p+k}}{p(p+1)/2}.

Barriendo miles de combinaciones de parámetros sobre 150k barras, las convoluciones WMA son el costo. Así que el instinto —el instinto correcto, en una CPU— es hacer que cada WMA sea O(n) en lugar de O(n·p) usando sumas de prefijos. Precalculas dos sumas acumuladas una sola vez,

S1[t]=jtxj,S2[t]=jtjxj,S_1[t] = \sum_{j \le t} x_j, \qquad S_2[t] = \sum_{j \le t} j \cdot x_j,

y entonces la suma linealmente ponderada de cualquier ventana se reduce a un puñado de diferencias y desplazamientos de índice de S1 y S2. Sin bucle por ventana, sin reducción por ventana —dos pasadas de cumsum y toda la matriz de WMAs sale de aritmética de arrays. Vectoriza de maravilla, se mapea perfectamente sobre la primitiva de scan paralelo de una GPU, y en fp64 es exactamente correcto.

Es también lo peor que puedes hacer en fp32, y la razón se esconde en S2.

S2 = mx.cumsum(j * price)     # j is the global bar index: 0, 1, 2, ... , n-1

El término j · price es el problema. Con j llegando hasta 150 000 y price en torno a 30 000, el último término por sí solo es 4.5×10⁹, y S2 es la suma acumulada de 150 000 términos de ese tipo. No se queda a escala de precios. Trepa hasta el territorio donde fp32 ya ha dejado de poder contar.

Dónde desborda: la aritmética de la trampa

Dos gigantescas sumas fp32 de ~10¹⁴ restadas para recuperar un pequeño valor por ventanas que se desvanece bajo su error de redondeo — cancelación catastrófica

Pongamos los órdenes de magnitud uno junto al otro, porque ahí es donde vive todo el fallo.

S2 = cumsum(j · price) alcanza aproximadamente price · n²/2 ≈ 30 000 · (150 000)²/2 ≈ 3×10¹⁴. Llamémoslo ~10¹⁴. Ahora recuerda el techo de entero exacto de fp32: ~1.6×10⁷. La suma acumulada rebasa el último entero que fp32 puede representar con exactitud por siete órdenes de magnitud.

¿Qué significa eso en concreto? Cerca de 10¹⁴, el hueco entre dos números fp32 representables —una unidad en el último lugar, el ULP— es de aproximadamente 2²³ ≈ 8×10⁶. Así que una vez que S2 está allá arriba en el rango de 10¹⁴, solo se conoce hasta ±8 millones. Cada valor que almacena ha sido redondeado al múltiplo más cercano de ~8×10⁶.

Ahora observa lo que la recuperación de la WMA hace con eso. Para extraer la suma ponderada de una sola ventana, restas dos valores vecinos de S2 (más correcciones de S1). Esos dos valores de S2 son cada uno ~10¹⁴, cada uno cargando ±8×10⁶ de ruido de redondeo. Su diferencia verdadera —la cantidad por ventanas que en realidad quieres— corresponde, tras la normalización, a una WMA del orden del propio precio, ~3×10⁴. Así que la aritmética es:

(1014±8×106)fp32(1014±8×106)fp32=una cantidad que necesitas con precisioˊn de unas partes en 104respuesta verdadera\underbrace{(10^{14} \pm 8\times10^6)}_{\text{fp32}} - \underbrace{(10^{14} \pm 8\times10^6)}_{\text{fp32}} = \underbrace{\text{una cantidad que necesitas con precisión de unas partes en } 10^4}_{\text{respuesta verdadera}}

Esto es cancelación catastrófica en su forma más pura: el error de redondeo de cada operando (±8×10⁶) es mayor que la respuesta que intentas recuperar. La señal es más pequeña que el piso de ruido de los números de los que se extrae. No es que pierdas unos pocos dígitos —los pierdes todos, y lo que regresa está dominado por el redondeo acumulado de la cumsum.

La consecuencia medida, de GPU_NOTES.md: para una WMA calculada de este modo sobre 150k barras a precio ~30 000, el error relativo máximo frente a fp64 alcanza ~211. No 211 por ciento —211×. La media móvil calculada puede estar a dos órdenes de magnitud de la verdadera. Y aquí está la parte que lo convierte en una trampa en lugar de un bug: se ejecuta hasta el final y devuelve números finitos y plausibles. Sin desbordamiento a infinito, sin NaN, sin excepción. Una media móvil equivocada por 211× sigue pareciendo una media móvil —es suave, es finita, está más o menos en el rango correcto en las barras donde la cancelación resulta ser leve— así que atraviesa toda comprobación de cordura que no sea una comparación directa contra una referencia de confianza. Obtienes un backtest completo, una curva de capital completa, un conjunto completo de parámetros "óptimos", todo construido sobre un indicador que es ficción.

La solución no es más precisión — es una suma distinta

Una ventana deslizante corta de ~200 términos de la misma escala sumados directamente por convolución — cada suma parcial permanece cerca de la escala de precios, dentro del rango exacto de fp32

El reflejo, una vez que ves el error, es buscar más precisión —acumular en fp64, o usar suma compensada (Kahan). En Metal, lo primero simplemente no está disponible. Pero no necesitas ninguna de las dos, porque el problema nunca fue el número de bits. El problema era la formulación. El truco de la suma de prefijos fabrica intermedias a escala de 10¹⁴ y luego las resta hasta bajarlas; las magnitudes que crea son un artefacto del algoritmo, no de la respuesta. Elige una formulación que nunca las cree y fp32 está bien.

Esa formulación es la definición misma: una convolución directa por ventanas. En lugar de dos sumas acumuladas globales, desliza el kernel lineal de longitud p a lo largo de la serie y súmalo en el sitio. Cada salida es una suma de a lo sumo p ≈ 200 términos, y cada término es weight × price donde los pesos están normalizados para sumar 1 —así que cada término es del orden de price / p, cada suma parcial permanece en torno a la escala de precios (~3×10⁴), y ninguna intermedia se acerca jamás a menos de seis órdenes de magnitud del techo de fp32. No hay nada que cancelar porque nada se infló jamás.

En MLX esto es una sola primitiva —mx.conv1d— que es exactamente lo que las GPU están construidas para hacer rápido:

def _mx_wma_valid(x, period):
    w = mx.arange(1, period + 1, dtype=mx.float32) / (period * (period + 1) / 2.0)
    return mx.conv1d(x.reshape(1, -1, 1), w.reshape(1, period, 1), padding=0).reshape(-1)

La misma WMA, matemáticamente idéntica a la versión por suma de prefijos y a las vec_wma/nb_wma en fp64 de la CPU. Pero ahora el error relativo máximo medido frente a fp64 es 8.2×10⁻⁷ —justo en el piso de ruido de fp32 de ~1.2×10⁻⁷, siete cifras significativas de concordancia. La formulación que parece más lenta sobre el papel (O(n·p) en lugar de O(n)) es la única que es correcta, y —porque es una convolución densa que la GPU paraleliza sobre barras y ventanas a la vez— también es velocísima. Pasamos de un error relativo de 211 a 8×10⁻⁷ cambiando cómo sumamos, no en cuántos bits sumamos.

Dos notas prácticas que se desprenden de hacerlo así. Primero, MLX no propaga NaNs a través de un conv1d como lo hace numpy, así que la región de calentamiento (las primeras p−1 barras, donde una media por ventanas no está definida) no puede marcarse con NaN en la GPU. No lo necesitamos: el inicio válido de cada serie se conoce analíticamente, los prefijos inválidos se rellenan con ceros que nunca se leen, y el relleno de NaN se restaura en la CPU después —bit a bit la misma máscara de validez que las versiones vectorizada y numba. Segundo, todo el barrido comparte una única serie cand_close y reutiliza las ventanas intensamente entre combinaciones, así que un único conv1d por lotes con muchos canales de salida calcula cada WMA única que el barrido necesita en una sola llamada a la GPU, materializada con un solo mx.eval().

Demostrar que no caíste: paridad por conteo de operaciones

Aquí está la pregunta incómoda que debería plantear la sección anterior: si una WMA equivocada por 211× todavía parece una WMA, ¿cómo sabes que la versión de 8×10⁻⁷ es realmente correcta y no simplemente equivocada de forma más sutil? No puedes evaluarla a ojo. Necesitas un invariante que una parte discreta y aguas abajo del pipeline exponga —y un backtest te entrega uno perfecto: las operaciones.

Los otros métodos GPU de la escalera (M0–M4) corren enteramente en fp64, así que los sometemos a un assert de equivalencia estricta —conteos de operaciones idénticos, PnL coincidiendo hasta atol=1e-6. El método GPU fp32 (M5) no puede pasar eso por construcción, y aflojar en silencio el assert para todos con tal de acomodarlo sería exactamente el tipo de deshonestidad que esta serie existe para combatir. Así que M5 obtiene su propio informe cuantitativo de paridad, report_equiv_fp32, comparando sus operaciones extraídas contra la referencia fp64.

Vale la pena nombrar con precisión el mecanismo de cualquier desacuerdo residual, porque no es la catástrofe de cancelación —es el redondeo fp32 ordinario y minúsculo que cabría esperar. La estrategia dispara con el cruce de dos medias de Hull, h frente a h3. Un error relativo de ~1×10⁻⁶ sobre un indicador a precio ~30 000 es una oscilación absoluta de ~0.03. En la inmensa mayoría de las barras las dos curvas están más separadas que eso y el cruce es inequívoco. Pero en una barra limítrofe —donde h − h3 está a su vez a menos de 0.03 de cero— esa oscilación puede invertir el signo de la comparación, moviendo un cruce una sola barra, agregando o quitando una operación.

Por eso la "fracción de combinaciones que difieren" es una métrica de salud inútil, y nuestra primera comprobación de paridad se avergonzó a sí misma al usarla. Con 150k barras cada combinación tiene miles de cruces, así que al menos una barra limítrofe aparece en casi todas las combinaciones —37 de 80 combinaciones "diferían", lo que suena alarmante y no significa nada. La métrica que importa es por cuánto:

  • Delta de PnL en las 80 combinaciones: máx |Δ| = 1.843 puntos porcentuales, máx relativo = 1.25×10⁻²; umbral de fallo 5 p.p.
  • Deriva del conteo de operaciones por combinación: máx |Δn| = 4 operaciones de miles, máx relativo = 2.5×10⁻³; umbral de fallo 1%.
  • En agregado: 90 operaciones desplazadas de 479 016 — 0.019%.

Noventa operaciones de casi medio millón, cada una de ellas un cruce limítrofe empujado por una oscilación de redondeo más pequeña que un tick de precio, nada de ello ni cerca de los umbrales de fallo. Esa es la firma de un método fp32 que es correcto —desacuerdo pequeño, acotado y explicable— y es un animal completamente distinto de un error relativo de 211. Los umbrales existen para atrapar una formulación rota disfrazada de "bueno, es solo fp32"; los deltas reales llegan un orden de magnitud por debajo de ellos. El conteo de operaciones es el oráculo que la curva de capital se negó a ser.

La recompensa, y dónde la GPU deja de ayudar

Con la corrección establecida, la velocidad merece enunciarse —y luego matizarse con honestidad, porque la ventaja de la GPU no es uniforme a lo largo del pipeline.

En las convoluciones WMA puras de forma aislada —la operación para acelerar la cual existe todo el método— el lote conv1d en fp32 corre 55.9× más rápido que numba de un solo hilo, con ese error relativo de 8.2×10⁻⁷. Ese es el número limpio y comparable manzana con manzana GPU-frente-a-CPU: la misma matemática, un hilo de código de CPU compilado contra la GPU Metal.

Pero un barrido no es solo convoluciones. Una vez que las matrices HMA/HMA3 se calculan en la GPU, las operaciones todavía tienen que extraerse —un recorrido O(n) sobre los cruces de cada combinación— y eso lo hacemos en la CPU en fp64, reutilizando la semántica exacta de operaciones de los otros métodos en lugar de reimplementarla en la GPU. La cifra timed() de extremo a extremo lo incluye todo: calentamiento del kernel excluido (simétrico con excluir la compilación de numba), pero la transferencia GPU→CPU y la extracción de operaciones en la CPU incluidas. Sobre 150k barras × 80 combinaciones, mejor de 3, M2 Max:

Método Reloj Aceleración vs baseline combos/s
M0 pandas + Python loop* 287.08s 1.0× 0.3
M1 vectorized numpy 3.14s 91.5× 25.5
M2 numba (serial) 2.02s 142.3× 39.7
M3 multiprocess + vectorized 0.50s 570.2× 158.9
M4 multiprocess + numba (12 cores) 0.33s 882.5× 245.9
M5 MLX GPU (fp32) 0.10s 2796.0× 779.2

*M0 extrapolado a partir de una muestra uniforme de 5 combinaciones.

El M5 de motor completo hace 779 combos/s —2796× sobre el baseline de pandas, 19.6× sobre numba serial, y 3.2× sobre todo el pool de CPU de 12 núcleos ejecutando numba (M4). Una sola GPU vence a todos los núcleos de CPU que la máquina tiene, por triple.

Ahora el matiz honesto: nota que la ventaja de la GPU de extremo a extremo (19.6× sobre M2) es menor que la ventaja solo de convolución (55.9× sobre numba). Ese hueco es la ley de Amdahl llegando puntual. La GPU aniquila las convoluciones tan por completo que dejan de ser el cuello de botella; lo que queda —la extracción de operaciones O(n) en la CPU que la GPU no aceleró en absoluto— ahora domina el tiempo de reloj de M5. Esta es la misma lección sobre la que las piezas de la escalera de velocidad y del impuesto IPC de esta serie insisten una y otra vez: pasado cierto punto, la victoria no es "hacer la parte rápida más rápida", es orquestación —dónde vive el dato, qué etapa es ahora serial, qué estás pagando por mover entre dispositivo y host. Perseguir un hipotético M6 que empuje la extracción de operaciones a un kernel Metal a medida solo recuperaría la menguante porción de CPU, y por eso no lo construimos.

La lección general: la basura numérica silenciosa es lo predeterminado

Da un paso atrás de las medias de Hull y de MLX, porque la trampa se generaliza mucho más allá de este único indicador.

El argumento seductor del backtesting sobre GPU es "una gran matriz": apila cada combinación de parámetros en un tensor, ejecuta todo el barrido como un puñado de operaciones densas de arrays, deja que el hardware se lo coma. Ese argumento es real —las aceleraciones de arriba son reales. Pero cambia calladamente el régimen numérico bajo tus pies, y el cambio es invisible en el código. En la CPU tus valores por defecto te protegían: fp64, propagación de NaN, una cumsum que podía llegar a 10¹⁴ sin inmutarse. Mueve la misma expresión de arrays a Metal y estás en fp32 con un techo duro de entero de 1.6×10⁷, y la línea de código idénticacumsum(j * price)— pasa de exacta a basura. Nada en la sintaxis te avisa. El compilador está contento. La salida es finita y plausible. fp32 no falla ruidosamente; falla con educación, con números.

Los tres hábitos que de verdad te protegen son baratos:

  1. Sabe dónde viven tus intermedias, no solo tus entradas y salidas. Las entradas (precios ~10⁴) y las salidas (una WMA ~10⁴) estaban ambas cómodamente dentro del rango exacto de fp32. El desastre estuvo enteramente en una intermedia oculta (S2 ~10¹⁴) que ni la API ni los tipos hacían visible. Antes de confiar en cualquier reducción en fp32, pregunta hasta dónde llega la mayor suma parcial —y si cruza ~10⁷, cambia la formulación.
  2. Prefiere formulaciones que mantengan las magnitudes acotadas. Convolución directa sobre sumas de prefijos; ventanas locales sobre scans globales; centrar/diferenciar antes de sumar en lugar de después. Grande-luego-cancelar es el antipatrón. El algoritmo correcto es a menudo el que parece asintóticamente peor sobre el papel pero nunca fabrica una cantidad que tenga que cancelar.
  3. Valida contra un oráculo fp64 mediante un invariante discreto. No compares curvas; compara algo cuantizado y aguas abajo —conteo de operaciones, número de cruces, eventos de cambio de posición. Un invariante discreto convierte un error silencioso de 211× en un fallo de aserción a gritos, y convierte un error aceptable de 8×10⁻⁷ en un delta pequeño, acotado y explicable. Esta es la misma disciplina que la prueba de desplazamiento de una barra para el sesgo de anticipación: un diagnóstico barato que convierte un fallo invisible en uno visible.

Nada de esto es análisis numérico exótico. Es la higiene ordinaria de no confiar en un backtest rápido hasta que uno lento y de confianza lo haya ratificado —extendida al único lugar donde el lenguaje deja de avisarte de que la precisión ha caído en silencio doce órdenes de magnitud.

Conclusiones

El giro de una sola línea: cumsum(j·price) tachado con un error de 211×, mx.conv1d al lado con 8×10⁻⁷ — la misma WMA, una suma distinta

  1. La GPU de Apple no tiene float64 —cada número de GPU en tu backtest es fp32. Los enteros son exactos solo hasta ~1.6×10⁷ y la precisión es ~1.2×10⁻⁷. No hay bandera, no hay respaldo. La mayor parte de un backtest sobrevive a esto; exactamente una operación normalmente no.
  2. La WMA por suma de prefijos es la trampa. cumsum(j · price) trepa hasta ~10¹⁴, siete órdenes más allá del techo exacto de fp32, y recuperar una ventana te obliga a restar dos números de ese tipo cuyo error de redondeo (±8×10⁶) ya empequeñece la respuesta. Error relativo máximo medido: 211×. Nunca se cae —devuelve basura plausible.
  3. La solución es una suma distinta, no más bits. Una convolución directa por ventanas (mx.conv1d) mantiene cada suma parcial cerca de la escala de precios, así que fp32 conserva siete dígitos honestos: error relativo de 8.2×10⁻⁷, y 55.9× más rápido que numba de un solo hilo. No puedes comprar fp64 en Metal, y no lo necesitas.
  4. Verifica con un invariante discreto, nunca con la curva. La paridad de conteo de operaciones lo atrapó: la conv1d en fp32 discrepó con fp64 en 90 de 479 016 operaciones (0.019%), todas cruces limítrofes, todas muy por debajo de los umbrales de fallo —la firma de un método correcto, inconfundiblemente distinta de un error de 211×. "Fracción de combinaciones que difieren" es una métrica señuelo; mide por cuánto.
  5. El barrido completo hace 779 combos/s —2796× sobre el baseline de pandas, 3.2× sobre todo el pool de CPU de 12 núcleos— pero la victoria de extremo a extremo (19.6× sobre numba serial) es menor que la victoria solo de convolución (55.9×) porque la extracción de operaciones en la CPU es ahora el cuello de botella. Pasado cierto punto, la velocidad es orquestación, no aritmética.

El porte a GPU fue 2796× más rápido y, en su primera versión que funcionaba, completamente equivocado —y los dos hechos no tenían nada que ver el uno con el otro. La velocidad era real. La basura era una intermedia oculta de 10¹⁴ que fp32 no podía retener y que ningún mensaje de error mencionaría. Si un backtest se vuelve dramáticamente más rápido y los números aún parecen bien, eso no es confirmación. En Metal, "parece bien" es exactamente lo que parece un error relativo de 211.

Este es el peldaño de GPU de la escalera que esta serie ha ido subiendo: la escalera de velocidad del motor de backtesting, el impuesto IPC del multiproceso, la taxonomía de anticipación de las fugas, y el diseño de la función objetivo que decide qué significa siquiera "bueno". La velocidad no vale nada si es rápida calculando el número equivocado.

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.