- PVSM.RU - https://www.pvsm.ru -

Недавно я решил проверить на себе классический сюжет: может ли нейронка зарабатывать на бирже. Не «взял стратегию с ютуба», а как положено инженеру: собрал данные Мосбиржи, построил честный walk‑forward пайплайн, зашил комиссии в каждую цифру и стал измерять.
Эта статья — про самый неочевидный урок, который я получил. Он не про рынок и не про архитектуры. Он про то, что результат бэктеста нейронной сети — случайная величина с разбросом в десять процентных пунктов годовой доходности, и если вы это не учитываете, все ваши выводы — шум.
Подозреваю, что этим страдает заметная часть статей вида «я обучил LSTM на котировках и получил X% годовых». Проверить легко: спросите автора, что будет, если поменять random seed.
Чтобы дальнейшие цифры имели смысл, фиксирую условия эксперимента:
Данные: часовые свечи 20 самых ликвидных акций Мосбиржи (TQBR), 2021–2026, ~370 тысяч баров. Источник — бесплатный ISS API биржи, складываю в SQLite.
Модель: рекуррентная сеть класса GRU, на вход — окно последних баров с несколькими десятками признаков (доходности разных горизонтов, нормированная волатильность, положение бумаги относительно корзины, микроструктура часа). Точный состав признаков — мой единственный секрет, для этой статьи он не важен: всё, о чём пойдёт речь, воспроизводится на любом разумном наборе.
Метка: вырастет ли цена больше чем на пороговую величину за фиксированный горизонт в несколько часов. Бинарная классификация, на выходе — вероятность.
Портфель: капитал поровну на 20 бумаг, вход при вероятности выше порога, выход — по гистерезису (порог выхода ниже порога входа, чтобы не дёргаться на границе). Комиссия 0.05% за сторону — тариф реального брокера.
Железо: одна потребительская GPU; один прогон 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», но и «на скольких эпохах».
Что будет с результатом при смене random seed? (Автор не знает — дальше можно не читать.)
По какой цене исполняются сигналы — не по той ли, по которой посчитаны?
Есть ли зазор между train и test на длину горизонта метки?
Какая комиссия зашита в цифры и есть ли она вообще?
Проверялась ли система на другой рыночной эпохе?
Если добавили признак и стало лучше — показана ли атрибуция: лучше стало ТАМ, где признак есть?
Шесть вопросов отсеивают ~95% красивых эквити.
Система сейчас торгует виртуально в реальном времени, осенью выпускаю её на реальные деньги — с публичным трек‑рекордом, чтобы самому не сжульничать. Все эксперименты (включая провальные — из ~35 гипотез выжило 6) документирую публично: каждый месяц буду показывать PnL как есть, включая минусы.
Если статья зайдёт — следующая будет целиком про новости: как из пяти способов заставить их предсказывать цену не выжил ни один, с цифрами каждого провала.
Не является индивидуальной инвестиционной рекомендацией.
Автор: svtoroi
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/nejronny-e-seti/456452
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/1069594/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1069594
Нажмите здесь для печати.