
Недавно я решил проверить на себе классический сюжет: может ли нейронка зарабатывать на бирже. Не «взял стратегию с ютуба», а как положено инженеру: собрал данные Мосбиржи, построил честный walk‑forward пайплайн, зашил комиссии в каждую цифру и стал измерять.
Эта статья — про самый неочевидный урок, который я получил. Он не про рынок и не про архитектуры. Он про то, что результат бэктеста нейронной сети — случайная величина с разбросом в десять процентных пунктов годовой доходности, и если вы это не учитываете, все ваши выводы — шум.
Подозреваю, что этим страдает заметная часть статей вида «я обучил LSTM на котировках и получил X% годовых». Проверить легко: спросите автора, что будет, если поменять random seed.
Стенд
Чтобы дальнейшие цифры имели смысл, фиксирую условия эксперимента:
Данные: часовые свечи 20 самых ликвидных акций Мосбиржи (TQBR), 2021–2026, ~370 тысяч баров. Источник — бесплатный ISS API биржи, складываю в SQLite.
Модель: рекуррентная сеть класса GRU, на вход — окно последних баров с несколькими десятками признаков (доходности разных горизонтов, нормированная волатильность, положение бумаги относительно корзины, микроструктура часа). Точный состав признаков — мой единственный секрет, для этой статьи он не важен: всё, о чём пойдёт речь, воспроизводится на любом разумном наборе.
Метка: вырастет ли цена больше чем на пороговую величину за фиксированный горизонт в несколько часов. Бинарная классификация, на выходе — вероятность.
Портфель: капитал поровну на 20 бумаг, вход при вероятности выше порога, выход — по гистерезису (порог выхода ниже порога входа, чтобы не дёргаться на границе). Комиссия 0.05% за сторону — тариф реального брокера.
Железо: одна потребительская GPU; один прогон walk‑forward — около трёх минут. Это важно: всё, что я опишу, доступно любому энтузиасту.

Что такое честный walk‑forward (и где все ошибаются)
Схема, без которой остальное бессмысленно. Модель НИКОГДА не видит будущего:
# упрощённо: суть схемы разрезов HORIZON = ... # горизонт метки в барах STEP = ... # переобучаемся каждые STEP баров window = [] # скользящее окно train (последние N баров) for cut in range(MIN_TRAIN, len(timeline), STEP): # ЗАЗОР: метка бара t созревает только через HORIZON баров. # Обучаться можно лишь на барах, чьи метки уже известны # к моменту cut — иначе утечка будущего в train. safe_train_end = timeline[cut - 1 - HORIZON] train = bars[(bars.ts >= train_lo) & (bars.ts <= safe_train_end)] predict = bars[(bars.ts > timeline[cut-1]) & (bars.ts <= timeline[cut+STEP-1])] model.fit(train) # тёплый старт от прошлого фолда oos_probs[predict.index] = model.predict(predict)
Три места, где чаще всего врут бэктесты из статей:
Нет зазора между train и test. Метка «вырастет за N часов» у последних N часов обучающей выборки подглядывает в тестовый период. Утечка крошечная на вид — и достаточная, чтобы нарисовать несуществующую кромку на сигнале с AUC 0.55.
Исполнение по цене сигнала. Сигнал посчитан по цене закрытия бара — исполнять его можно только по цене СЛЕДУЮЩЕГО бара. Одна строчка
shift(1), разница в итоге — проценты годовых.Комиссии «потом добавим». На часовом горизонте средняя сделка приносит десятки базисных пунктов, круг комиссий стоит десять. «Потом» превращает плюс в минус.
Я закрыл все три с самого начала. И именно поэтому следующий эффект оказался таким болезненным.
Эксперимент, который всё сломал
Первый прогон: +15% за тестовый период. Неплохо!
Меняю в конфиге одно число — random seed. Данные те же, признаки те же, код тот же.
+6%.
Ещё раз. +14%. Ещё. Другая инициализация весов — другая судьба портфеля.

Прямое измерение шума
Тогда я сделал то, что стоило сделать с самого начала: зафиксировал всё, кроме сида, и прогнал пять одинаковых обучений. В том первом замере одиночные сиды легли в диапазон от −0.12 до +0.85 по Sharpe — от «стратегия убыточна» до «вполне прилично» на одинаковых данных, признаках и коде.
Эффект никуда не делся и на зрелой версии системы. Вот пять сидов текущей боевой конфигурации (замер на этой неделе, полный OOS‑период, одинаковый порог):
seed |
Sharpe (OOS) |
|---|---|
42 |
0.91 |
43 |
0.92 |
44 |
0.39 |
45 |
1.28 |
46 |
0.91 |

Признаки стали сильнее, минусовых сидов больше нет — но между худшим и лучшим по‑прежнему более трёх раз. Разница между «выбросить проект» и «показывать друзьям» — один seed.
Почему у нейросетей на финансовых данных так плохо с воспроизводимостью? Потому что сигнал исчезающе мал. Хорошая модель на часовом горизонте угадывает направление с AUC 0.54–0.55 — едва лучше монетки. Лосс‑ландшафт почти плоский, и куда именно скатится Adam из случайной инициализации — лотерея. В компьютерном зрении сигнал силён, все сиды сходятся к похожим решениям; в финансах каждая инициализация находит собственный способ переобучиться.
Отдельно замечу: torch.manual_seed фиксирует лишь часть недетерминизма — есть ещё порядок батчей в DataLoader, недетерминированные CUDA‑ядра, инициализация воркеров. Для воспроизводимости фиксировать нужно всё; для оценки шума — наоборот, честно варьировать.
Что с этим делать: протокол из трёх правил
Правило 1. Одиночный прогон не значит ничего. Торгует только ансамбль.
Усреднение вероятностей пяти моделей с разными сидами:
# каждый сид -> свой полный walk-forward -> свои OOS-вероятности ensemble_prob = ( pd.concat([run_walkforward(seed=s) for s in range(42, 47)]) .groupby(["ticker", "ts"])["prob"].mean() )
Результат на тех же пяти сидах из таблицы выше: Sharpe 1.88 — заметно лучше даже самого удачливого одиночного (1.28). В первом историческом замере эффект был ещё драматичнее: ансамбль 1.39 против лучшего сида 0.85 и худшего −0.12. Это не «среднее по больнице»: ошибки переобучения у разных сидов декоррелированы (я мерил попарные корреляции OOS‑вероятностей — они далеки от единицы), и усреднение гасит именно шумовую компоненту, оставляя общий сигнал. Классика ансамблей — но в финансах она не «немного улучшает», а отделяет случайность от результата.
Правило 2. Любая новая идея сравнивается ансамбль против ансамбля.
Хотите проверить новый признак? Пять моделей с ним против пяти без него. Да, впятеро дороже по GPU. Альтернатива — сравнивать одну случайную величину с другой и называть разницу «улучшением». У меня прижилось и второе усиление: если средний AUC пяти сидов не сдвинулся — портфельные метрики можно даже не считать, дальше эта идея не идёт.
Правило 3. Атрибуция: разбирайте, откуда именно пришла прибавка.
Самое коварное — когда правила 1–2 соблюдены, а бэктест всё равно обманывает. Инструмент — примитивный:
per_ticker = trades.groupby("ticker")["pnl"].sum() # Прибавка пришла из бумаг, у которых нового признака НЕТ? Это шум.
Три кейса из моей практики:
Кейс 1. Позиции физлиц по фьючерсам. Признак существует только у 3 бумаг из 20 (у остальных в этой колонке нули). Доходность ансамбля выросла с +14% до +21.5%. Победа? Атрибуция: весь прирост — на 17 бумагах без признака, а три бумаги с признаком стали хуже. Признак не работал — он изменил размерность входа, перетасовал внутренний шум обучения, и новая случайность оказалась удачливее старой. На следующем переобучении «прибавка» испарилась бы.
Кейс 2. Дивидендный календарь. Близость отсечки, дивидендная доходность. Скрининг: Sharpe 2.16 → 2.49. Атрибуция: у дивидендных плательщиков суммарный минус, весь плюс — на одной бумаге, которая в тот период дивиденды не платила. Та же перетасовка, вид сбоку.
Кейс 3. Новости. Архив в 476 тысяч постов финансовых телеграм‑каналов, пять постановок: счётчики упоминаний, тональность (разметил rubert‑tiny2), типы событий (регэкспы по дивидендам/отчётности/санкциям), дневной календарь, реакция цены за 15 минут после поста. Две из пяти показали «прирост» на агрегатной цифре — обе рассыпались на атрибуции по тому же паттерну: прибавка живёт там, где признака нет. Самая злая деталь: у типов событий рухнуло всё сразу — AUC 0.519 против 0.544 у всех пяти сидов и Sharpe 0.38 против 2.16. Фоновые признаки вроде «на рынке санкционная неделя», константные внутри длинных окон, учат модель запоминать эпоху вместо сигнала. А две «удачные» постановки прошли скрининг ровно на перетасовке шума — и рассыпались на атрибуции.

Отсюда главное эвристическое правило: если улучшение пришло не оттуда, куда указывает механизм признака — это не улучшение, а шум. Агрегатная цифра бэктеста не является доказательством. Доказательство — сдвиг, который переживает ансамбль, атрибуцию и честный out‑of‑sample.
Бонусный уровень: эпохи

Даже идеальный по протоколу результат остаётся результатом своей эпохи. Я прогнал замороженную боевую систему (Sharpe 2.7 на 2021–2026) по данным 2015–2020 без перенастройки: минус 28%. Модель выучила режим рынка и в чужом режиме уверенно торгует прошлым.
Дальше интереснее: я повторил на 2015–2020 полный walk‑forward — то есть дал системе переобучаться, как она делает в бою еженедельно, но только на данных той эпохи. Результат: +17% вместо −28%. Вывод, который стоил мне пары бессонных ночей: главный риск — не «модель не работает в другую эпоху», а скорость адаптации на переходе между эпохами. Регулярное переобучение — не гигиена, а несущая конструкция. Плюс у системы есть автоматический «выключатель»: если трейлинг‑метрики нейронки уступают простой эталонной стратегии — капитал уходит в неё или в денежный рынок. Но это тема отдельной статьи.
Для читателя бэктестов вывод проще: спрашивайте не только «какой Sharpe», но и «на скольких эпохах».
Чек‑лист для чтения статей про ML‑трейдинг
Что будет с результатом при смене random seed? (Автор не знает — дальше можно не читать.)
По какой цене исполняются сигналы — не по той ли, по которой посчитаны?
Есть ли зазор между train и test на длину горизонта метки?
Какая комиссия зашита в цифры и есть ли она вообще?
Проверялась ли система на другой рыночной эпохе?
Если добавили признак и стало лучше — показана ли атрибуция: лучше стало ТАМ, где признак есть?
Шесть вопросов отсеивают ~95% красивых эквити.
Что дальше
Система сейчас торгует виртуально в реальном времени, осенью выпускаю её на реальные деньги — с публичным трек‑рекордом, чтобы самому не сжульничать. Все эксперименты (включая провальные — из ~35 гипотез выжило 6) документирую публично: каждый месяц буду показывать PnL как есть, включая минусы.
Если статья зайдёт — следующая будет целиком про новости: как из пяти способов заставить их предсказывать цену не выжил ни один, с цифрами каждого провала.
Не является индивидуальной инвестиционной рекомендацией.
Комментарии (7)

Innesiya
12.08.2026 12:48Ансамбль по сидам и атрибуция — сильные приемы, но одну дыру в защите вы, кажется, оставили открытой: шум внутри одной гипотезы вы гасите, а гипотез было ~35, выжило 6. Это классический multiple testing — при переборе трех десятков признаков несколько «выстрелят» просто случайно, и ансамбль тут не помощник: он снижает дисперсию оценки, но не делает поправку на число проверок. Держали ли вы отдельный, ни разу не тронутый кусок данных под финальную проверку этих шести — или считали что-то вроде deflated Sharpe? И еще: Sharpe все равно померян на одной реализованной траектории эпохи, так что разброс по сидам — это интервал оценки, а не интервал будущей доходности.

svtoroi Автор
12.08.2026 12:48формальной поправки на множественность в протоколе нет, и вечно нетронутого холдаута тоже “честная вторая половина” за десятки экспериментов неизбежно изнашивается, защищаюсь от этого тремя вещами (ни одна из которых не является формальной поправкой, но вместе они работают иначе, чем одиночный порог):
принятие гипотезы - это не “Sharpe выше порога”, а конъюнкция независимых проверок: сдвиг AUC на каждом из пяти сидов, сравнение на равной рабочей точке, атрибуция по механизму (прибавка обязана прийти из бумаг и баров, где признак существует) и для крупных изменений - стресс на другой рыночной эпохе случайный “выстрел” из тридцати обычно проходит одну проверку и валится на атрибуции, два таких кейса разобраны прямо в статье вероятность случайно пройти все ворота на порядки ниже, чем случайно пробить один Sharpe-порог, хотя численно я ее, честно, не оценивал
из выживших шести большинство - не независимые джекпоты перебора признаков, а модификации самой процедуры обучения (ансамблирование, смешение двух схем разметки), подтверждённые на двух эпохах - у них множитель перебора существенно меньше тридцати пяти
финальный нетронутый холдаут у меня - время система с июля торгует виртуально в реальном времени, осенью выходит на реальные деньги с публичным треком будущее - единственный “не тронутый кусок данных” который физически нельзя переиспользовать, все остальное, включая собственну “честную половину” я и сам считаю частично изношенным
deflated Sharpe - не считал, посчитаю и опубликую частично его роль у меня выполняет стресс-тест (замороженная система на 2015–2020 теряет 28%), я сам делаю ставку не на цифру Sharpe, а на скорость адаптации (еженедельное переобучение) и автоматический выключатель в эталонную стратегию
разброс по сидам - интервал шума процедуры, а не интервал будущей доходности, и режимный риск в одной реализованной траектории больше сидового мое ожидание от live скромнее бэктеста насколько - покажет время публичного трека

svtoroi Автор
12.08.2026 12:48посчитал, как обещал
взял дневные доходности боевой конфигурации за весь OOS 2021–2026 (~1500 наблюдений, Sharpe 2.47 годовых - на дневной агрегации он ниже часового) и 31 честно измеренный вариант из журнала экспериментов как пул проб
лучший результат, который показал бы чистый шум перебора при 35 пробах - Sharpe ~1.7, при сотне проб (если считать каждую вариацию параметров отдельной попыткой) - ~2.0 вероятность, что мои 2.47 выше этой планки не случайно - 0.99 при 35 пробах и 0.92 даже при сотне, с поправкой на жирные хвосты ряда (skew +4.4, kurtosis 66)
честные оговорки: пробы у меня коррелированы (одни данные, один пайплайн) - это одновременно занижает дисперсию пула, завышая DSR, и снижает эффективное число независимых попыток, занижая его - куда сместился баланс, строго не скажу и главное: DSR отвечает только на вопрос “не артефакт ли перебора это превосходство”, а не “повторится ли 2.47” - на второй по-прежнему отвечают стресс-тест с его −28% и живой трек
спасибо за вопрос - расчет останется в проекте постоянным инструментом
maxnoosphere
сит 42-46 vs лучший сит - вопрос а сколько ансамблей надо? допустим 5 хватит или там разброс всёравно большой? просто если 20 сидов то это 20x3мин = 60мин на один цикл, как быстро на боевых данных переобучатся получается
svtoroi Автор
Гонял 10 сидов против 5 (те же данные, честный протокол из статьи): на полном периоде десятка чуть глаже, но на свежей половине, которая у меня держит решения, прибавки нет - разница в пределах того же сидового шума, а ретрейн дорожает вдвое
так что вывод осторожный - не 5 лучше 10, а прибавка от удвоения не доказана, и я остался на 5
механика простая: усреднение убирает только декоррелированную часть ошибки (первые 3–5 моделей забирают основной выигрыш, дальше отдача падает быстро), А та часть ошибки, что общая у всех сидов (само переобучение на эпоху), не лечится никаким количеством сидов
по времени: у меня 5 сидов ~ 13 минут на 3050 TI, переобучение раз в неделю по расписанию, так что даже 20 сидов не было бы проблемой. тут узкое место не вычисления, а честность оценки