Look-Ahead Bias: как ошибка в один бар фабрикует Sharpe 15 из чистого шума
Статья из серии "Бэктесты без иллюзий".
📄 Эта статья выросла в исследовательскую работу. Три едва заметные look-ahead утечки проверены контролируемым тестом против известной истины (4,000 симулированных историй). Читайте статью онлайн (интерактивная версия + PDF) на lookahead.marketmaker.cc, код и данные — на github.com/suenot/lookahead-inflation.
Несколько недель назад наш бенчмарк поиска параметров нам врал, и мы почти этого не заметили.
Движок выглядел чистым. Логика на закрытых барах, честное rolling walk-forward разбиение, Sobol/QMC-поиск по пространству параметров, отложенное тестовое окно. Поиск находил конфигурации, которые хорошо выглядели in-sample. Единственная проблема: out-of-sample почти все было в минусе. Мы решили, что стратегия просто слабая.
Потом мы нашли одну строку. Сигнал определялся по закрытию бара i, но исполнение записывалось на том же баре i вместо открытия следующего бара. Одна ошибка на единицу в индексе исполнения. Мы перенесли исполнение на open[i+1] — единственную цену, по которой реально можно было бы совершить сделку после того, как вы увидели закрытие бара i, — и результат out-of-sample сменил знак. Sobol-поиск перешел из убытка в прибыль. В стратегии не изменилось ничего. Мы просто перестали торговать в прошлом.
Это и есть look-ahead bias, и тревожит именно то, насколько маленькой была ошибка и насколько большим — ее следствие. Эта статья — контролируемый самоаудит: мы строим симулятор, в котором истина известна по построению, по очереди внедряем едва заметные утечки и точно измеряем, насколько каждая из них раздувает бэктест. Главный результат: при полном отсутствии реального edge исполнение на баре сигнала фабрикует годовой Sharpe +14.8 из чистого шума.
Что такое look-ahead bias на самом деле

Look-ahead bias — это любая точка в пайплайне, где решение или измерение использует информацию, которая в реальном времени, в момент использования, была бы недоступна. Хрестоматийные примеры грубые — использование годовой прибыли акции в январе или еще не опубликованного пересмотра отчетности. Их легко заметить. Те, что переживают код-ревью, тоньше, и прячутся они в трех местах:
- Исполнение — вы принимаете решение на баре
iи исполняете сделку на том же бареi(или используете high/low бараiдля стопов на том самом баре, что породил сигнал). Вы торгуете по цене, коррелирующей с тем, что вас триггернуло. - Нормализация — вы z-score, min-max или иначе масштабируете признак, используя статистики, посчитанные по всему ряду, включая будущее. Скейлер "знает" тестовую выборку.
- Индикаторы / признаки — вы сглаживаете или фильтруете с окном, центрированным (или иначе заглядывающим вперед), так что значение на баре
iуже содержит кусочек бараi+1.
Все три — формы того, что в литературе по машинному обучению называют leakage (утечкой): загрязнение обучения/оценки информацией из будущего целевой переменной (Kaufman et al., 2012; Kapoor & Narayanan, 2023). В финансах канонический источник — Advances in Financial Machine Learning Лопеса де Прадо (2018): purged cross-validation, embargo, опасности бэктестинга. Дисциплина point-in-time восходит как минимум к Fama & French (1992), которые намеренно сдвигают бухгалтерские данные на шесть месяцев назад, чтобы переменная была известна раньше доходности, которую она объясняет.
Вопрос, на который отвечает эта статья, — количественный: не "вредна ли утечка" (с этим все согласны), а "сколько очков Sharpe дает каждая форма и какие из них опасны?" Без числа об этом невозможно рассуждать. Нельзя понять, является ли раздутие +0.3 шумом, а раздутие +14 — уликой.
Симулятор с известной истиной

Чтобы измерить раздутие, нужно знать истину. Реальные данные никогда не говорят вам истину — они дают одну реализацию и никакого оракула. Поэтому мы строим синтетический рынок, где edge задаем мы сами.
Процесс генерации данных строго каузален и не взрывоопасен:
Здесь — это экзогенный персистентный латентный дрифт (AR(1) с ), а доходность бара имеет небольшой дрифт , который известен на один бар вперед. Поскольку не зависит от прошлых доходностей, обратной связи нет и ничего не взрывается. Параметр — это ручка, задающая объем реального edge:
- — нулевая гипотеза: edge отсутствует полностью. Любой положительный Sharpe в бэктесте на 100% артефакт.
- — реальный, торгуемый edge: честное моментум-правило действительно зарабатывает деньги.
Стратегия намеренно простая — знаковое моментум-правило. Признак — это скользящая сумма доходностей за баров ( бара), а позиция — это ее знак:
csum = np.concatenate(([0.0], np.cumsum(r))) # csum[k] = sum r[0..k-1]
mom = np.full(n, np.nan)
tt = np.arange(L - 1, n)
mom[tt] = csum[tt + 1] - csum[tt - L + 1]
signal = np.sign(mom) # position for the next bar
Этот моментум-признак — идеальный инструмент для изучения утечки на том же баре, потому что у него есть свойство, общее с реальными индикаторами: он механически содержит текущий бар. mom[t] включает r[t]. Так что если вы записываете r[t] как результат своей сделки, вы отчасти делаете ставку на величину, которая уже находится внутри вашего собственного сигнала. Вот и вся утечка, в конкретном виде.
Настройки: (1% волатильности за бар), односторонняя комиссия 0.00045 (round-trip 0.09%, как в нашем движке), Sharpe аннуализирован через (часовые бары), 4,000 независимых историй по 4,000 баров каждая. Все с фиксированным seed и детерминировано.
Честный пайплайн (единственный торгуемый)
Решение принимается по закрытию бара t, доходность зарабатывается на следующем баре, комиссия платится при изменении позиции:
def sharpe(sig, ret_booked):
dpos = np.abs(np.diff(np.concatenate(([0.0], sig))))
pnl = sig * ret_booked - FEE_ONEWAY * dpos
return pnl.mean() / pnl.std() * np.sqrt(8760)
honest = sharpe(signal[idx], r[idx + 1]) # earn r[t+1]: tradable
Три утечки, каждая — одно хирургическое изменение
same_bar = sharpe(signal[idx], r[idx])
z_full = (mom - mom[valid].mean()) / mom[valid].std()
norm_full = sharpe(np.sign(z_full[idx]), r[idx + 1])
z_sm = (mom[:-2] + mom[1:-1] + mom[2:]) / 3.0 # uses t-1, t, t+1
indicator = sharpe(np.sign(z_sm[idx]), r[idx + 1])
Каждая утечка отстоит от честного пайплайна на одну строку. В этом вся суть: это не экзотические ошибки, а именно то, что проходит ревью.
Результаты: величина каждой утечки

По результатам прогона на 4,000 seed-ах — вот годовой Sharpe, который показывает каждый пайплайн, при нулевой гипотезе (без edge) и при реальном edge (, подобранном так, чтобы честный Sharpe был правдоподобным +1.57):
| Пайплайн | Нулевая гипотеза (без edge) | Реальный edge |
|---|---|---|
| Честный (истина) | −0.74 | +1.57 |
| Исполнение на том же баре | +14.79 | +15.85 |
| Подглядывание индикатора (1 бар) | +4.76 | +6.62 |
| Нормализация по всему ряду | −0.84 | +1.46 |
95% доверительные интервалы по seed-ам не шире ±0.05 в каждой ячейке; парные t-тесты на раздутие астрономически значимы там, где эффект реален (t > 400, p ≈ 0).
Сначала читайте столбец нулевой гипотезы, потому что это самый чистый из возможных экспериментов: edge отсутствует, поэтому честный пайплайн корректно теряет деньги (−0.74 — расход на комиссии за торговлю шумом). Теперь посмотрите, что утечки делают с этим же самым нулем:
- Исполнение на том же баре: −0.74 → +14.79. Стратегия с нулевой предсказательной силой, торгующая случайный шум, показывает годовой Sharpe почти 15. Это не тонкое смещение — это фабрикация. Механизм ровно тот, что мы заложили: моментум-признак содержит
r[t], поэтому записьr[t]— это ставка на собственный сигнал. - Подглядывание индикатора: −0.74 → +4.76. Если позволить сглаживателю заглянуть на один бар в будущее, из шума фабрикуется Sharpe около 5, потому что сглаженное значение на
tтеперь коррелирует сr[t+1], которое вы вот-вот заработаете. - Нормализация по всему ряду: −0.74 → −0.84. Раздутия практически нет. Это честный, неочевидный результат (подробнее — ниже).
Столбец реального edge несет более коварное послание. Когда реальный edge действительно есть (честный +1.57), утечки не просто добавляют константу — они толкают измеренный Sharpe до +15.85 и +6.62, далеко выше тех +1.57, которые реально можно торговать. Значит, измеренное число не может отличить мастерство от утечки. Утекший +6 и честный +6 выглядят на отчете идентично. Узнать, какой из них был на самом деле, можно только после того, как вы уже развернули капитал.
Утечка — это градиент, а не переключатель

Естественное возражение: "записывать весь бар сигнала — это крайняя, нереалистичная ошибка". Поэтому мы прошлись по дозе — доле бара сигнала, захватываемой утечкой, от 0 (честно) до 1 (полная утечка на том же баре):
| Доля захвата | Sharpe (нулевая гипотеза) | Sharpe (edge) |
|---|---|---|
| 0.00 (честно) | −0.74 | +1.57 |
| 0.25 | +3.90 | +6.41 |
| 0.50 | +9.86 | +12.20 |
| 1.00 (полная утечка) | +14.79 | +15.85 |
Захват всего четверти бара сигнала переводит стратегию без edge от −0.74 к +3.90. Чтобы обмануться, не нужна полная ошибка на единицу; исполнение, чуть слишком выгодное — капля оптимистичного слиппеджа на баре сигнала, внутрибарный стоп, проверяемый по тому же бару, что его вызвал, — этого достаточно, чтобы преодолеть большинство порогов "готово к продакшену". Раздутие плавно и монотонно зависит от того, сколько настоящего вы позволяете себе торговать.
Как часто это отправляет убыточную стратегию в продакшен?
Число, которое должно тревожить практика, — это частота ложного деплоя: как часто утечка позволяет по-настоящему убыточной конфигурации преодолеть планку, по которой вы бы дали ей зеленый свет. Используя "годовой Sharpe ≥ 1.0" как критерий деплоя, при нулевой гипотезе:
- Исполнение на том же баре: 68% стратегий без edge выглядят готовыми к деплою и при этом по-настоящему убыточны. Две из трех конфигураций чистого шума прошли бы фильтр Sharpe ≥ 1 и потеряли бы деньги в реальной торговле. (Эта величина здесь корректно определена, потому что утечка чисто в исполнении — честный аналог — тот же сигнал с честным исполнением.)
- Подглядывание индикатора: оно тоже проталкивает через планку деплоя практически каждую конфигурацию без edge (99.9% проходят Sharpe ≥ 1) — шум пропускается прямо в продакшен.
- Нормализация по всему ряду: планку преодолевают 12% — по сути базовая частота для шума, никакой премии от утечки.
Таксономия и как обнаружить каждую утечку
Три утечки не одинаково опасны, и различия между ними поучительны.
1. Утечка исполнения (дорогая)

Симптом: цена исполнения коррелирует с сигналом, потому что оба берутся с одного бара. Величина: огромная (+15 из шума при полной дозе, +3.9 при четверти дозы). Почему это худшее: ваш сигнал, почти по определению, строится на недавнем движении цены, так что доходность бара сигнала — это именно то, с чем ваш признак коррелирует сильнее всего. Записать ее — почти то же самое, что подсмотреть ответ.
Обнаружение — тест сдвига на один бар. Это самая ценная диагностика во всей статье. Возьмите свой бэктест и сдвиньте каждое исполнение на один бар позже (решение на i, исполнение на open[i+1]). Если результат почти не меняется — исполнение было честным. Если результат обрушивается или меняет знак — вы торговали в прошлом. Именно это произошло с нашим Sobol-поиском: сдвинули исполнение — и "прибыльный" OOS оказался убытком, точнее, реальная зависимость проявилась, как только утечку убрали.
entry_price = open_[i + 1] # NOT close[i], NOT open[i]
2. Утечка индикатора / признака (тихая)
Симптом: индикатор на баре i зависит от данных из i+1 или позже — центрированная скользящая средняя, фильтр без каузальной задержки, метка пика/впадины, для подтверждения которой нужны будущие бары, Heikin-Ashi-подобное преобразование, получающее на вход будущие свечи. Величина: большая (+4.8 из шума). Почему прячется: утечка зарыта внутри вызова библиотеки. scipy.signal.filtfilt — с нулевой фазой, а нулевая фаза означает некаузальность. Признак "этот бар — локальный максимум" непознаваем, пока не напечатается следующий бар.
Обнаружение: для каждого индикатора спросите — какой максимальный индекс он читает? Если вычисление значения на t хоть раз касается t+1, оно некаузально. Считайте индикаторы на расширяющемся/скользящем каузальном окне и проверьте, что значение на баре t одинаково независимо от того, есть ли в массиве бары после t. (Наши реализации HMA/ADX это проходят: каждый выход на t читает только входы ≤ t.)
3. Утечка нормализации (специфичная для канала)
Симптом: скейлер (StandardScaler, min-max, глобальный z-score) обучается на всем датасете, включая тестовую выборку. Канонические ML-предупреждения об этом прямо говорят — Hastie, Tibshirani & Friedman, Elements of Statistical Learning, §7.10.2 ("правильный и неправильный способ делать cross-validation"), и собственный гайд scikit-learn по типичным ошибкам: "среднее должно быть средним по обучающей подвыборке, а не средним по всем данным."
Величина в нашем тесте: ≈ ноль (−0.74 → −0.84). Это неожиданный, честный результат, и его стоит понять, а не просто запомнить.
Почему она не раздула результат? Потому что наша стратегия использует признак только через его знак (порог на нуле). Масштабирование через стандартное отклонение никогда не меняет знак, а центрирование по глобальному среднему лишь слегка сдвигает точку пересечения нуля. Поэтому стандартизация по всему ряду для чисто знакового правила почти безвредна.
Не обобщайте это чрезмерно. Утечка нормализации специфична для канала. В тот момент, когда ваша стратегия начинает использовать величину признака — размер позиции, пропорциональный z-score, ненулевой порог входа, подобранный по масштабированному распределению, нейросеть, потребляющую стандартизированные входы, — скейлер, знающий будущее, начинает иметь значение, и тем больше, чем сильнее глобальные статистики отличаются от каузальных. Наш результат — не "утечка нормализации безопасна". Это "величина утечки зависит от канала, через который утекшая величина попадает в решение, и ее нужно измерять, а не предполагать". Знаковое правило — это единственный случай, когда именно эта утечка обходится дешево.
Куда это ведет
Look-ahead bias — первое звено цепи, которую документирует эта серия:
- Он портит вход для валидации. Утекший бэктест спокойно пройдет walk-forward разбиение и будет выглядеть как широкое плато, а не переобученный пик — утечка одинакова во всех фолдах, так что кросс-валидация ее не поймает. Утечка — это режим отказа выше по потоку от overfitting, и никакая честная валидация ниже по потоку вас не спасет.
- Он взаимодействует с поиском параметров: поиск по тысячам испытаний на утекших данных найдет конфигурацию, которая эксплуатирует утечку агрессивнее всего. "Победитель" — это худший нарушитель.
- Именно поэтому расходится паритет бэктеста и live-торговли. Утечка — самое чистое объяснение разрыва в 30–50% между бэктестом и ботом, потому что live-торговля — это, механически, единственное место, где подглядеть невозможно.
Дисциплина, которая ловит все это, — та же самая, к которой годами призывает академическая литература: относиться к бэктесту как к статистическому эксперименту со строгой информационной границей. Bailey, Borwein, López de Prado & Zhu показали, как легко overfitting фабрикует фейковую производительность (2014); протокол бэктестинга Arnott, Harvey & Markowitz (2019) кодифицирует эту гигиену. Look-ahead bias — самая базовая из всех границ, граница по времени, и ее проще всего нарушить случайно.
Главные выводы

- Look-ahead bias количественно огромен и качественно невидим. Одна-единственная ошибка исполнения на один бар превратила Sharpe −0.74 (чистый шум, корректно убыточный) в +14.79. Ошибка — одна строка; следствие — сфабрикованный трек-рекорд.
- Это градиент. Захват даже 25% бара сигнала дает +3.90 из ничего. Не нужен явный баг — достаточно чуть более оптимистичного исполнения.
- Измеренное число не может отличить мастерство от утечки. Когда реальный edge существует, утечки раздувают отчет далеко за пределы торгуемой истины. Единственная защита — процесс, а не метрика.
- Тест сдвига на один бар — ваша самая быстрая диагностика. Сдвиньте каждое исполнение на один бар позже. Если результат обрушивается — вы торговали в прошлом.
- Величина утечки специфична для канала. Утечки в исполнении и индикаторе разрушительны; нормализация по всему ряду для знакового правила почти бесплатна. Измеряйте утечку через тот канал, которым она реально входит, — не предполагайте.
Полное контролируемое исследование — все три утечки, развертка дозы, анализ ложного деплоя, формальные методы и каждое число, воспроизводимое из единого детерминированного скрипта, — в сопутствующей научной статье на lookahead.marketmaker.cc, код и данные — на github.com/suenot/lookahead-inflation.
У стратегии в нашем нулевом эксперименте не было вообще никакого edge. Тем не менее она показала Sharpe 15. Если ваш бэктест выглядит слишком хорошо, первое, что стоит заподозрить, — не собственную гениальность, а собственные часы.
Авторы
Инженер торговых систем
Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.