← К списку статей
July 6, 2026
5 мин. чтения

Ловушка точности GPU: как fp32-бэктест на Apple Metal незаметно возвращает мусор

Ловушка точности GPU: как fp32-бэктест на Apple Metal незаметно возвращает мусор
#алготрейдинг
#бэктест
#GPU
#Apple Silicon
#плавающая точка
#численная стабильность
#MLX
Часть 8 из 10 · Подборка
Быстрые движки бэктеста

Статья из серии "Бэктесты без иллюзий".

Мы перенесли наш бэктест с перебором параметров на GPU, и он ускорился в 2,796× раза. Числа, которые он возвращал, выглядели правдоподобно. А еще, в первой рабочей версии, они были полным мусором — с ошибкой в двести раз — и при этом ничего не падало, ничего не предупреждало, ничего не выглядело подозрительно.

Именно в этом ловушка, которая делает GPU-бэктестинг на Apple Silicon опасным так, как CPU-код опасным не бывает: у Metal нет float64. Каждое число, которого касается ваш бэктест на GPU Apple, — это 32-битный float, просите вы об этом или нет. А единственный самый соблазнительный способ векторизовать скользящее среднее — трюк с префиксными суммами за O(n), к которому тянется рукой любой квант, помешанный на производительности, — это именно та формула, которую fp32 не переживает на масштабе цен. Она не выдает ошибку. Она отрабатывает на полной скорости и вручает вам правдоподобно выглядящую кривую капитала, построенную на скользящем среднем, которое ошибается в 211 раз.

Самое интересное — в решении, потому что это не "используй больше бит" (не выйдет) и не "будь внимательнее" (наивный код и так внимателен). Решение — вычислить ту же самую математическую величину другой суммой — прямой оконной сверткой, — которая держит каждое промежуточное значение достаточно малым, чтобы fp32 представлял его точно. Та же WMA, тот же результат с точностью до семи значащих цифр, и в 55.9 раза быстрее однопоточной numba на CPU. Эта статья — вскрытие: почему Metal навязывает вам fp32, где именно очевидная формула переполняется, почему правильная — нет, и как мы доказали — считая сделки, а не разглядывая кривые, — что быстрая версия и честная fp64-версия совпадают.

Все числа здесь измерены на Apple M2 Max, 150,000 баров × 80 комбинаций параметров, лучший из 3 запусков, по данным scripts/bench_param_sweep.py из репозитория (метод GPU M5, коммит 97eadaf), scripts/engine_multitf_gpu.py (04d71e8) и заметок о дизайне в scripts/GPU_NOTES.md.

У Metal нет float64

Конвейер GPU Apple Metal с зачеркнутым float64 — каждая линия принудительно переведена в 32-битный формат, целые числа точны только до 1.6×10⁷

На CPU бэктест на Python/numpy по умолчанию работает с двойной точностью. float64 дает мантиссу в 52 бита: целые числа представляются точно вплоть до 2⁵³ ≈ 9×10¹⁵, а относительная точность — порядка 1.1×10⁻¹⁶. Об этом почти никогда не задумываешься, потому что на масштабе цен — цена закрытия BTC около 30,000, накопленная сумма, доходящая до 10¹⁴ — у fp64 запас более чем достаточный.

GPU от Apple такого запаса не дает, потому что float64 у него нет вообще. В Metal Shading Language нет типа double; MLX, PyTorch-MPS и любой другой фреймворк, нацеленный на Apple Silicon, наследует это ограничение. Нет флага, который бы его включил, и нет медленного, но корректного запасного варианта. Если ваши вычисления касаются GPU, они происходят в fp32:

  • Мантисса: 23 бита (24 с учетом неявного старшего бита). Целые числа точны только вплоть до 2²⁴ = 16,777,216 ≈ 1.6×10⁷.
  • Относительная точность: ~1.2×10⁻⁷. Примерно семь значащих десятичных цифр, и не больше.

Этот потолок в 1.6×10⁷ — вся суть истории. Звучит щедро — шестнадцать миллионов — пока вы не замечаете, что бэктест регулярно строит промежуточные величины намного больше, а в момент, когда промежуточное значение переваливает за 1.6×10⁷, fp32 уже не может представить даже последовательные целые числа, не говоря уже о нужной дробной структуре. Точность деградирует не плавно — она обрывается, как со скалы, и эта скала стоит ровно на том числе, мимо которого ваши данные проходят не задумываясь.

Ловушка в том, что fp32 почти всегда работает нормально. Большая часть бэктеста — цены, доходности, PnL, Sharpe — комфортно живет в диапазоне, где семи цифр с избытком хватает. Поэтому наивный порт работает, проходит дымовой тест, выдает вменяемый на вид результат. Сбой локализован ровно в одной операции — там, где промежуточное значение выносит за 10⁷, — и это как раз та операция, которую векторизуют первой.

Соблазнительная формула: WMA за один проход префиксных сумм

Наша стратегия опирается на скользящие средние Халла (Hull). HMA — это композиция трех взвешенных скользящих средних; вариант HMA3 — уже четырех. Взвешенное скользящее среднее с линейным ядром на окне длиной p выглядит так:

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}.

При переборе тысяч комбинаций параметров на 150k баров именно свертки WMA и есть основная стоимость вычислений. Поэтому инстинкт — на CPU правильный инстинкт — сделать каждую WMA за O(n) вместо O(n·p), используя префиксные суммы. Вы один раз заранее считаете две накопленные суммы,

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,

и тогда линейно взвешенная сумма любого окна сворачивается до пары разностей и сдвигов индексов S1 и S2. Ни цикла по окнам, ни редукции по окнам — два прохода cumsum, и вся матрица WMA получается прямо из арифметики массивов. Это прекрасно векторизуется, идеально ложится на примитив параллельного сканирования GPU, и на fp64 это абсолютно точно.

Это же и самое худшее, что можно сделать в fp32, — и причина прячется в S2.

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

Проблема — в члене j · price. При j, доходящем до 150,000, и price порядка 30,000, один только последний член равен 4.5×10⁹, а S2 — это бегущая сумма из 150,000 таких членов. Она не остается на масштабе цены. Она забирается в область, где fp32 уже разучился считать.

Где происходит переполнение: арифметика ловушки

Две гигантские суммы fp32 порядка ~10¹⁴ вычитаются друг из друга, чтобы восстановить маленькое оконное значение, которое тонет в их ошибке округления — катастрофическое сокращение

Сопоставим порядки величин, потому что именно там и живет весь этот сбой.

S2 = cumsum(j · price) достигает примерно price · n²/2 ≈ 30,000 · (150,000)²/2 ≈ 3×10¹⁴. Назовем это ~10¹⁴. Вспомним теперь потолок точного представления целых чисел в fp32: ~1.6×10⁷. Бегущая сумма перескакивает последнее целое число, которое fp32 может представить точно, на семь порядков.

Что это значит конкретно? В районе 10¹⁴ зазор между двумя соседними представимыми числами fp32 — единица в последнем разряде, ULP — составляет примерно 2²³ ≈ 8×10⁶. Значит, как только S2 оказывается в диапазоне 10¹⁴, оно известно лишь с точностью до ±8 миллионов. Каждое хранимое значение округлено до ближайшего кратного ~8×10⁶.

А теперь посмотрим, что с этим делает восстановление WMA. Чтобы извлечь взвешенную сумму одного окна, вы вычитаете два соседних значения S2 (плюс поправки S1). Оба этих значения S2 — порядка ~10¹⁴, и каждое несет ±8×10⁶ шума округления. Их истинная разность — та самая оконная величина, которая вам на самом деле нужна, — после нормализации соответствует WMA порядка самой цены, ~3×10⁴. Итак, арифметика такова:

(1014±8×106)fp32(1014±8×106)fp32=a quantity you need to a few parts in 104true answer\underbrace{(10^{14} \pm 8\times10^6)}_{\text{fp32}} - \underbrace{(10^{14} \pm 8\times10^6)}_{\text{fp32}} = \underbrace{\text{a quantity you need to a few parts in } 10^4}_{\text{true answer}}

Это катастрофическое сокращение (catastrophic cancellation) в чистом виде: ошибка округления каждого операнда (±8×10⁶) больше, чем ответ, который вы пытаетесь восстановить. Сигнал меньше уровня шума тех самых чисел, из которых он извлекается. Дело не в том, что вы теряете несколько цифр — вы теряете их все, и то, что возвращается, полностью определяется накопленным округлением cumsum.

Измеренное следствие, по данным GPU_NOTES.md: для WMA, посчитанной так на 150k баров при цене ~30,000, максимальная относительная ошибка против fp64 достигает ~211. Не 211 процентов — 211×. Вычисленное скользящее среднее может отличаться от истинного на два порядка. И вот та часть, которая делает это именно ловушкой, а не багом: вычисление доходит до конца и возвращает конечные, правдоподобные числа. Ни переполнения до бесконечности, ни NaN, ни исключения. Скользящее среднее, ошибающееся в 211 раз, все еще выглядит как скользящее среднее — оно гладкое, конечное, примерно в правильном диапазоне на тех барах, где сокращение случайно оказалось мягким, — так что оно проходит сквозь любую проверку на здравый смысл, кроме прямого сравнения с доверенным эталоном. Вы получаете полноценный бэктест, полноценную кривую капитала, полноценный набор "оптимальных" параметров — и все это построено на индикаторе, который есть чистый вымысел.

Решение — не в большей точности, а в другой сумме

Короткое скользящее окно из ~200 членов одного масштаба суммируется напрямую сверткой — каждая частичная сумма остается около масштаба цены, внутри точного диапазона fp32

Увидев эту ошибку, рефлекторно тянешься за большей точностью — накапливать в fp64 или использовать компенсированное суммирование (Kahan). На Metal первое просто недоступно. Но не нужно ни то ни другое, потому что дело никогда не было в количестве бит. Дело было в формуле. Трюк с префиксными суммами создает промежуточные значения масштаба 10¹⁴, а потом вычитает их обратно вниз; эти масштабы — артефакт алгоритма, а не ответа. Выберите формулу, которая никогда их не создает, — и fp32 справляется без проблем.

Эта формула — само определение: прямая оконная свертка. Вместо двух глобальных накопленных сумм скользите линейным ядром длины p вдоль ряда и суммируйте на месте. Каждый выход — это сумма не более чем p ≈ 200 членов, и каждый член — это weight × price, где веса нормированы так, чтобы в сумме давать 1, — поэтому каждый член порядка price / p, каждая частичная сумма остается около масштаба цены (~3×10⁴), и ни одно промежуточное значение никогда не приближается к потолку fp32 ближе, чем на шесть порядков. Сокращать нечего, потому что ничего никогда не раздувалось.

В MLX это один примитив — mx.conv1d — именно то, для чего GPU и создавались:

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)

Та же самая WMA, математически идентичная и версии на префиксных суммах, и fp64-версии на CPU (vec_wma/nb_wma). Но теперь измеренная максимальная относительная ошибка против fp64 составляет 8.2×10⁻⁷ — ровно на уровне шума fp32 в ~1.2×10⁻⁷, семь значащих цифр совпадения. Формула, которая на бумаге выглядит медленнее (O(n·p) вместо O(n)), — единственная, которая корректна, и — поскольку это плотная свертка, которую GPU распараллеливает сразу и по барам, и по окнам, — она же оказывается ошеломляюще быстрой. Мы прошли путь от относительной ошибки 211 до 8×10⁻⁷, изменив не количество бит, в которых мы суммируем, а то, как мы суммируем.

Из такого подхода вытекают два практических замечания. Во-первых, MLX не распространяет NaN через conv1d так, как это делает numpy, поэтому область прогрева (первые p−1 баров, где оконное среднее не определено) нельзя пометить NaN на GPU. Нам это и не нужно: валидное начало каждого ряда известно аналитически, невалидные префиксы заполняются нулями, которые никогда не читаются, а NaN-паддинг восстанавливается потом на CPU — бит в бит та же маска валидности, что и у векторизованной и numba-версий. Во-вторых, весь перебор использует один общий ряд cand_close и активно переиспользует окна между комбинациями, поэтому один пакетный conv1d со множеством выходных каналов вычисляет все уникальные WMA, нужные перебору, за один вызов GPU, материализованный одним mx.eval().

Доказать, что вы не попали в ловушку: паритет по числу сделок

Вот неудобный вопрос, который должен возникнуть после предыдущего раздела: если WMA, ошибающаяся в 211 раз, все еще выглядит как WMA, откуда вы знаете, что версия с 8×10⁻⁷ на самом деле верна, а не просто ошибается более тонко? Разглядыванием глазами это не проверить. Нужен инвариант, который раскрывает дискретная часть пайплайна, расположенная ниже по потоку, — и бэктест дает вам идеальный такой инвариант: сделки.

Остальные методы в лесенке (M0–M4) целиком работают на fp64, поэтому к ним мы предъявляем строгий assert на эквивалентность — идентичное число сделок, совпадение PnL с точностью atol=1e-6. Метод M5 на fp32 не может пройти эту проверку по своей конструкции, и тихо ослабить assert для всех, лишь бы подстроиться под него, было бы ровно той нечестностью, против которой существует эта серия статей. Поэтому у M5 есть свой собственный количественный отчет о паритете, report_equiv_fp32, сравнивающий извлеченные им сделки с fp64-эталоном.

Стоит точно назвать механизм любого остаточного расхождения, потому что это не катастрофа сокращения — это обычное, крошечное округление fp32, которого и следовало ожидать. Стратегия срабатывает на пересечении двух средних Халла, h и h3. Относительная ошибка ~1×10⁻⁶ на индикаторе при цене ~30,000 дает абсолютное дрожание порядка ~0.03. На подавляющем большинстве баров эти две кривые расходятся сильнее, и пересечение однозначно. Но на пограничном баре — где сама разность h − h3 находится в пределах 0.03 от нуля — это дрожание может перевернуть знак сравнения, сдвинув одно пересечение на один бар, добавив или убрав одну сделку.

Вот почему "доля комбинаций, которые отличаются" — бесполезная метрика здоровья, и наша первая проверка паритета опозорилась, используя именно ее. На 150k баров у каждой комбинации тысячи пересечений, поэтому хотя бы один пограничный бар обнаруживается почти в каждой комбинации — 37 из 80 комбинаций "отличались", что звучит тревожно и не значит ничего. Метрика, которая на самом деле важна, — это насколько сильно:

  • Дельта PnL по всем 80 комбинациям: max |Δ| = 1.843 процентных пункта, max relative = 1.25×10⁻²; порог срабатывания тревоги — 5 п.п.
  • Дрейф числа сделок на комбинацию: max |Δn| = 4 сделки из тысяч, max relative = 2.5×10⁻³; порог срабатывания тревоги — 1%.
  • В сумме: 90 сдвинутых сделок из 479,016 — 0.019%.

Девяносто сделок из почти полумиллиона, каждая — пограничное пересечение, сдвинутое дрожанием округления меньше одного ценового тика, и ничто из этого даже близко не подходит к порогам тревоги. Это подпись fp32-метода, который корректен, — маленькое, ограниченное, объяснимое расхождение, — и это совершенно другой зверь по сравнению с относительной ошибкой 211. Пороги существуют, чтобы ловить сломанную формулу, маскирующуюся под "ну, это просто fp32"; настоящие дельты приходят на порядок ниже них. Число сделок — тот самый оракул, которым отказалась быть кривая капитала.

Выигрыш — и где GPU перестает помогать

Раз корректность установлена, стоит назвать и скорость — а затем честно ее оговорить, потому что преимущество GPU неравномерно распределено по пайплайну.

На чистых свертках WMA в изоляции — на операции, ради ускорения которой и существует весь метод, — пакетная fp32-свертка conv1d работает в 55.9 раза быстрее однопоточной numba, с той же относительной ошибкой 8.2×10⁻⁷. Это честное, корректное для сравнения число GPU против CPU: одна и та же математика, один поток скомпилированного CPU-кода против GPU Metal.

Но перебор — это не только свертки. Как только матрицы HMA/HMA3 посчитаны на GPU, сделки все равно нужно извлечь — проход за O(n) по пересечениям каждой комбинации, — и мы делаем это на CPU в fp64, переиспользуя ровно ту же семантику сделок, что и у остальных методов, а не переписывая ее заново на GPU. Сквозная цифра timed() включает все: разогрев ядра исключен (симметрично исключению компиляции numba), но перенос GPU→CPU и извлечение сделок на CPU включены. На 150k баров × 80 комбинаций, лучший из 3 запусков, M2 Max:

Method Wall Speedup vs baseline combos/s
M0 pandas + цикл на Python* 287.08s 1.0× 0.3
M1 векторизованный numpy 3.14s 91.5× 25.5
M2 numba (последовательно) 2.02s 142.3× 39.7
M3 многопроцессность + векторизация 0.50s 570.2× 158.9
M4 многопроцессность + numba (12 ядер) 0.33s 882.5× 245.9
M5 MLX GPU (fp32) 0.10s 2796.0× 779.2

*M0 экстраполирован по равномерной выборке из 5 комбинаций.

Полный движок на M5 выдает 779 комбинаций/с — 2,796× относительно базовой pandas-реализации, в 19.6 раза больше, чем последовательная numba, и в 3.2 раза больше, чем весь пул из 12 ядер CPU, работающий на numba (M4). Один GPU обгоняет все ядра CPU этой машины, причем втрое.

А теперь честная оговорка: заметьте, что сквозное преимущество GPU (19.6× над M2) меньше, чем преимущество только на свертках (55.9× над numba). Этот разрыв — закон Амдала, явившийся точно по расписанию. GPU настолько полно уничтожает свертки, что они перестают быть узким местом; то, что остается — извлечение сделок на CPU за O(n), которое GPU не ускорил вообще никак, — теперь и определяет wall time для M5. Это тот же урок, к которому раз за разом приходят статьи этой серии про лесенку скорости и IPC-налог: начиная с некоторого момента выигрыш дает не "ускорить быструю часть еще сильнее", а оркестрация — где живут данные, какая стадия теперь стала последовательной, за что вы платите при переносе между устройством и хостом. Погоня за гипотетическим M6, который переносит извлечение сделок в кастомное Metal-ядро, отвоевала бы обратно лишь сжимающуюся долю CPU, поэтому мы его и не стали строить.

Общий урок: тихий числовой мусор — это норма по умолчанию

Отступим от средних Халла и MLX, потому что эта ловушка обобщается далеко за пределы одного этого индикатора.

Соблазнительная идея GPU-бэктестинга — "одна большая матрица": сложить каждую комбинацию параметров в тензор, прогнать весь перебор как горстку операций над плотными массивами, отдать это железу на съедение. Идея реальна — ускорения, о которых шла речь выше, реальны. Но она незаметно меняет числовой режим у вас под ногами, и эта перемена невидима в коде. На CPU вас защищали настройки по умолчанию: fp64, распространение NaN, cumsum, способный дойти до 10¹⁴ и не заметить этого. Перенесите то же самое выражение над массивом на Metal — и вы оказываетесь в fp32 с жестким потолком целых чисел в 1.6×10⁷, и та же самая строка кода — cumsum(j * price) — переходит из точной в мусорную. Ничто в синтаксисе вас не предупреждает. Компилятор доволен. Результат конечен и правдоподобен. fp32 не отказывает громко — он отказывает вежливо, числами.

Три привычки, которые реально вас защищают, стоят дешево:

  1. Знайте, где живут ваши промежуточные значения, а не только входы и выходы. Входы (цены ~10⁴) и выходы (WMA ~10⁴) оба комфортно помещались внутри точного диапазона fp32. Катастрофа целиком была в скрытом промежуточном значении (S2 ~10¹⁴), которое не делали видимым ни API, ни типы. Прежде чем доверять любой редукции в fp32, спросите, какого значения достигает наибольшая частичная сумма, — и если она переваливает за ~10⁷, меняйте формулу.
  2. Предпочитайте формулы, которые держат величины ограниченными. Прямая свертка вместо префиксных сумм; локальные окна вместо глобальных проходов; центрирование/взятие разности до суммирования, а не после. "Сначала раздуть, потом сократить" — это антипаттерн. Правильный алгоритм часто оказывается именно тем, который на бумаге выглядит асимптотически хуже, но никогда не создает величину, которую потом придется сокращать.
  3. Проверяйте по fp64-эталону через дискретный инвариант. Не сравнивайте кривые — сравнивайте что-то квантованное и расположенное ниже по потоку: число сделок, число пересечений, события смены позиции. Дискретный инвариант превращает тихую ошибку в 211× в громко падающий assert, а приемлемую ошибку 8×10⁻⁷ — в маленькую, ограниченную, объяснимую дельту. Это та же дисциплина, что и тест сдвига на один бар для look-ahead bias: дешевая диагностика, превращающая невидимый сбой в видимый.

Ничего из этого не экзотический численный анализ. Это обычная гигиена — не доверять быстрому бэктесту, пока его не подтвердит медленный и доверенный, — распространенная на то единственное место, где язык перестает предупреждать вас, что точность тихо просела на двенадцать порядков.

Выводы

Разворот в одну строку: cumsum(j·price) зачеркнут с ошибкой 211×, рядом mx.conv1d с 8×10⁻⁷ — та же WMA, другая сумма

  1. GPU от Apple не имеет float64 — каждое число на GPU в вашем бэктесте это fp32. Целые числа точны лишь до ~1.6×10⁷, а точность — ~1.2×10⁻⁷. Нет флага, нет запасного варианта. Большая часть бэктеста это переживает; ровно одна операция — обычно нет.
  2. WMA на префиксных суммах — вот ловушка. cumsum(j · price) забирается до ~10¹⁴, на семь порядков за точный потолок fp32, и восстановление окна заставляет вычитать два таких числа, чья ошибка округления (±8×10⁶) уже затмевает ответ. Измеренная максимальная относительная ошибка: 211×. Ничего не падает — возвращается правдоподобный мусор.
  3. Решение — другая сумма, а не больше бит. Прямая оконная свертка (mx.conv1d) держит каждую частичную сумму около масштаба цены, поэтому fp32 удерживает семь честных цифр: относительная ошибка 8.2×10⁻⁷, и в 55.9 раза быстрее однопоточной numba. fp64 на Metal не купить, да и не нужно.
  4. Проверяйте дискретным инвариантом, никогда — кривой. Паритет по числу сделок это поймал: fp32-conv1d разошелся с fp64 на 90 из 479,016 сделок (0.019%), все — пограничные пересечения, все далеко ниже порогов тревоги — подпись корректного метода, безошибочно отличимая от ошибки в 211×. "Доля отличающихся комбинаций" — это метрика-приманка; измеряйте насколько сильно.
  5. Весь перебор целиком выдает 779 комбинаций/с — 2,796× над базовой pandas-реализацией, 3.2× над всем пулом из 12 ядер CPU — но сквозной выигрыш (19.6× над последовательной numba) меньше, чем выигрыш только на свертках (55.9×), потому что извлечение сделок на CPU теперь узкое место. Начиная с некоторого момента скорость определяется оркестрацией, а не арифметикой.

GPU-порт оказался в 2,796 раза быстрее и, в своей первой рабочей версии, полностью неправильным — и эти два факта никак друг с другом не связаны. Скорость была настоящей. Мусор был скрытым промежуточным значением масштаба 10¹⁴, которое fp32 не мог удержать и о котором не упомянуло бы ни одно сообщение об ошибке. Если бэктест внезапно становится намного быстрее, а числа все еще выглядят нормально, — это не подтверждение. На Metal "выглядит нормально" — это ровно то, как выглядит относительная ошибка в 211.

Это GPU-ступень лесенки, по которой поднимается эта серия статей: лесенка скорости бэктест-движка, IPC-налог многопроцессности, таксономия look-ahead утечек и дизайн целевой функции, который решает, что вообще значит "хорошо". Скорость ничего не стоит, если она быстро вычисляет неправильное число.

Дисклеймер: Информация в этой статье предоставлена исключительно в образовательных и ознакомительных целях и не является финансовым, инвестиционным или торговым советом. Торговля криптовалютами сопряжена с высоким риском убытков.

Авторы

Eugen Soloviov
Eugen Soloviov

Инженер торговых систем

Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.

Newsletter

Будьте в курсе событий

Подпишитесь на нашу рассылку, чтобы получать эксклюзивную аналитику по AI-трейдингу и обновления платформы.

Мы уважаем вашу конфиденциальность. Отписаться можно в любой момент.