Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Лукап-смещение: убийца бэктестов

Будущие данные, просочившиеся в бэктест. Как решения внутри бара, фильтры выживших и пересмотренные цифры завышают результаты и как это выявить.

Лукап-смещение — это использование в бэктесте информации, которой никто не мог располагать в момент размещения моделируемой сделки. Кривая капитала растёт, статистика выглядит превосходно, и ничто из этого не выдерживает контакта с реальным ордером. Эта страница определяет смещение, разбирает три механизма, которые его порождают, и измеряет каждый из них на реальных рыночных данных.

Что такое лукап-смещение

Бэктест — это утверждение о решении: в этот момент времени, с этой информацией, правило поступило бы так. Утверждение верно только тогда, когда каждый входной параметр, который считывает правило, уже существовал в своём окончательном виде до этого момента. Лукап-смещение — это любое нарушение этого условия, и оно бесшумно. Ничто в таблице или скрипте не жалуется, когда строка возвращает значение, помеченное более поздним временем, чем решение, которое оно питает.

Три нарушения покрывают большую часть того, что встречается на практике. Решение, которое считывает ценовой бар, внутри которого оно также торгует. Вселенная, собранная из имён, которые котируются до сих пор. Поле данных, которое было пересмотрено после даты, которую оно несёт.

Механизм первый: решение на баре, которым вы также торгуете

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

Панель ниже прогоняет обе версии на SPY, инструменте отслеживания S&P 500, за десять календарных лет. Первый столбец — невозможный. Второй сдвигает то же самое условие на одну сессию, что является самым ранним сроком, когда пополненный счёт мог бы действовать по нему.

ЗапросРешение в том же баре против лага в одну сессию: SPY, средний прирост за сессию, 2016–2025
Точный SQL-код для каждого числа
WITH bars AS (
    SELECT date,
           toFloat64(open) AS o,
           toFloat64(close) AS c,
           toFloat64(close) > toFloat64(open) AS up_day
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= toDate('2015-12-01')
      AND date <= toDate('2025-12-31')
),
lagged AS (
    SELECT date, o, c, up_day,
           any(up_day) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prior_up
    FROM bars
)
SELECT toYear(date) AS year,
       countIf(up_day) AS signal_days,
       round(100 * avgIf(c / o - 1, up_day), 3) AS same_bar_avg_pct,
       round(100 * avgIf(c / o - 1, prior_up), 3) AS lagged_avg_pct,
       round(100 * (avgIf(c / o - 1, up_day) - avgIf(c / o - 1, prior_up)), 3) AS gap_pp
FROM lagged
WHERE date >= toDate('2016-01-01')
GROUP BY year
ORDER BY year
Run this yourself

Разница не является незначительной. В 2016 версия с тем же баром в среднем приносила 0.447% за квалификационную сессию, в то время как сдвинутая версия в среднем приносила -0.008%, спред в 0.455 процентных пункта в день. В 2025 эти две версии показывают 0.636% и -0.03%. При примерно 140 квалификационных сессиях в год спред в полпроцентных пункта за сессию складывается в кривую капитала, которую ни один реальный счёт никогда не печатал.

Всё преимущество является определительным. Отбор сессий, которые закрылись выше открытия, с последующим измерением движения от открытия до закрытия на тех же сессиях, измеряет величину, гарантированно положительную. Правило обнаружило свой собственный фильтр.

Варианты этого постоянно скрываются в дневных данных: скользящая средняя, которая включает сегодняшнее закрытие и используется для торговли сегодня, сигнал, вычисленный на ценах, уже скорректированных на сплит, который ещё не произошёл, внутридневной стоп, установленный на максимуме сессии, который ещё не был напечатан. Общая форма — это временная метка решения, находящаяся раньше, чем один из его собственных входных параметров.

Механизм второй: вселенная, взятая из сегодняшних выживших

Второй механизм никогда не касается правила. Он находится в списке имён, которые правилу разрешено видеть.

Возьмите список тикеров у текущего поставщика данных, прогоните его на десять лет назад, и выборка незаметно исключит каждую компанию, которая обанкротилась, была приобретена или делистингована по пути. Правилу никогда не предлагали проигравших. Панель измеряет размер этого упущения: каждое имя в листинге США, которое торговалось не менее 200 сессий выше $1 в данном календарном году, доля этой когорты, всё ещё печатающая цены в июле 2026 года, и медианная календарная годовая доходность всей когорты рядом с медианой только выживших.

ЗапросВыживаемость в выборке: торгуемые имена по годам, доля сохранивших листинг к июлю 2026 и медианная доходность
Точный SQL-код для каждого числа
WITH recent AS (
    SELECT DISTINCT ticker
    FROM global_markets.stocks_daily_aggs
    WHERE date >= toDate('2026-06-15')
      AND date <= toDate('2026-07-28')
),
per_name AS (
    SELECT toYear(date) AS year,
           ticker,
           argMin(toFloat64(close), date) AS first_close,
           argMax(toFloat64(close), date) AS last_close,
           count() AS sessions
    FROM global_markets.stocks_daily_aggs
    WHERE date >= toDate('2015-01-01')
      AND date <= toDate('2024-12-31')
      AND close > 1
    GROUP BY year, ticker
    HAVING sessions >= 200
)
SELECT year,
       uniqExact(ticker) AS names_trading,
       round(100 * uniqExactIf(ticker, ticker IN (SELECT ticker FROM recent)) / uniqExact(ticker), 1) AS still_listed_pct,
       round(100 * quantileDeterministic(0.5)(last_close / first_close - 1, cityHash64(ticker)), 2) AS median_return_all_pct,
       round(100 * quantileDeterministicIf(0.5)(last_close / first_close - 1, cityHash64(ticker), ticker IN (SELECT ticker FROM recent)), 2) AS median_return_survivors_pct,
       round(100 * (quantileDeterministicIf(0.5)(last_close / first_close - 1, cityHash64(ticker), ticker IN (SELECT ticker FROM recent))
                    - quantileDeterministic(0.5)(last_close / first_close - 1, cityHash64(ticker))), 2) AS survivor_gap_pp
FROM per_name
GROUP BY year
ORDER BY year
Run this yourself

Из 7063 имён, которые преодолели барьер в 2015, 52.5% всё ещё торговались в июле 2026 года. Для когорты 2024 года показатель составляет 87.5%, что в основном является вопросом прошедшего времени: у имени 2024 года было два года, чтобы исчезнуть, а у имени 2015 года — одиннадцать.

Столбцы доходности несут само смещение. В 2017 медианное имя принесло 7.51%, в то время как медианный выживший принёс 11.32%, разрыв в 3.81 процентных пунктов, полученный бесплатно. Разрыв положителен в каждом из 10 отображённых на графике лет. Стратегия, протестированная на выживших, наследует его до принятия какого-либо решения, а правила, которые отбирают по слабости, наследуют больше медианы, поскольку исчезнувшие имена непропорционально находятся в этой корзине.

Коррекция — это вселенная на момент времени: список имён, каким он был на каждую дату ребалансировки, включая делистинги, с закрытием делистингованной позиции по её последней цене, а не удалением из записи.

Механизм третий: данные, которые были пересмотрены после их временной метки

Цены штампуются один раз. Фундаментальные показатели — нет. Квартальная цифра несёт дату окончания квартала, исправляется более поздней подачей, и база данных, которая хранит только текущую версию, с готовностью выдаст исправленное число к исходной дате. Правило, фильтрующее по этому числу, считывает корректировку, на получение которой ушли месяцы.

Та же схема проходит через списки членов индекса, перестроенные под текущий состав, истории оценок аналитиков, перезаписанные окончательным консенсусом, и корректировки корпоративных действий, применённые задним числом ко всему ценовому ряду. В каждом случае хранимая запись — это снимок в настоящем времени, носящий дату в прошедшем.

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

Как выглядит кривая, когда будущее просачивается внутрь

У лукап-смещения есть сигнатура: производительность, которая находится намного выше того, что предлагал базовый рынок. Полезной точкой отсчёта является потолок ретроспективы, рост, который произвело бы совершенное предвидение на один день вперёд на одном инструменте.

ЗапросПотолок ретроспективы: SPY «купи и держи», тот же год без лучших дней роста и идеальное предвидение на один день
Точный SQL-код для каждого числа
WITH bars AS (
    SELECT date,
           toFloat64(close) AS c,
           any(toFloat64(close)) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= toDate('2015-12-01')
      AND date <= toDate('2025-12-31')
),
rets AS (
    SELECT toYear(date) AS year,
           c / prev_c - 1 AS r
    FROM bars
    WHERE date >= toDate('2016-01-01')
      AND prev_c > 0
)
SELECT year,
       count() AS sessions,
       round(100 * (exp(sum(log(1 + r))) - 1), 1) AS buy_and_hold_pct,
       round(100 * (exp(sumIf(log(1 + r), r < 0.02)) - 1), 1) AS without_big_up_days_pct,
       round(exp(sum(log(1 + abs(r)))), 1) AS hindsight_ceiling_multiple
FROM rets
GROUP BY year
ORDER BY year
Run this yourself

Потолок огромен. В 2020 совершенное однодневное предвидение по SPY умножило капитал в 27.7 раз за 253 сессий, против 16.2% за простое удержание фонда. В самый спокойный год из десяти, 2017, тот же совершенный прогноз вернул капитал, умноженный на 2.2.

Средний столбец показывает, насколько сконцентрирован этот потолок. Уберите сессии, которые выросли более чем на 2%, и 2020 превращается из 16.2% в -44.9%. Утечка, стоящая всего лишь горстки таких сессий в год, продвигает бэктест очень далеко. Кривая с мелкими просадками, высокой частотой попаданий и лучшими днями, сгруппированными на самых бурных сессиях рынка, заслужила аудит.

Калибровка значит больше, чем любой отдельный порог. Правило, заявляющее о гладких 40% в год на протяжении десятилетия, заявляет о большой доле потолка ретроспективы, и честный вопрос заключается в том, какой входной параметр сказал ему, что должно было произойти.

Как протестировать бэктест на лукап-смещение

  • Сдвиньте всё на один бар. Сместите каждый сигнал на один полный период позже и перезапустите. Подлинные преимущества немного ослабевают. Утечки рушатся, часто до нуля.
  • Возмутите будущее. Возьмите данные после каждой временной метки решения, замените их шумом или перетасовкой и перезапустите логику принятия решений. Каждое решение должно остаться идентичным. Любое решение, которое изменилось, считало что-то, чего оно не могло знать.
  • Обращайтесь с временными метками как с данными. Для каждого входного параметра храните момент, когда значение стало доступным, рядом с моментом, который оно описывает. Соединение, которое никогда не сравнивает эти два столбца, не может обеспечить соблюдение правила.
  • Перестройте вселенную на дату. Включите делистингованные и приобретённые имена, закройте их по последней цене и прогоните то же правило на обеих вселенных, чтобы оценить вклад выживаемости.
  • Измерьте относительно потолка. Поместите доходность стратегии рядом с показателем «купи и держи» и показателем совершенного предвидения для того же окна. Расстояние от потолка — это контекст, который один только коэффициент Шарпа не даёт.

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

Часто задаваемые вопросы

Что такое лукап-смещение в бэктестинге?

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

Как можно обнаружить лукап-смещение?

Сдвиньте каждый сигнал на один полный период и перезапустите: утечка обычно рушится, в то время как реальное преимущество мягко ослабевает. Более строгий тест заменяет все данные после каждой временной метки решения шумом и подтверждает, что каждое решение остаётся неизменным.

Является ли смещение выжившего тем же самым, что и лукап-смещение?

Они различны, но идут вместе. Смещение выжившего — это выборка, построенная из имён, которые сохранились, а знание того, какие имена сохранились, само по себе является информацией из будущего, поэтому вселенная только из выживших — это лукап-утечка, находящаяся в данных, а не в правиле.

Почему бэктесты с лукап-смещением выглядят так хорошо?

Утечка отбирает исходы, а не предсказывает их. На панели выше фильтрация сессий по их собственной цене закрытия произвела средний прирост за сессию примерно на 0.455 процентных пункта выше сдвинутой версии в 2016, преимущество, которое является определительным, а не заработанным.

Что такое данные на момент времени?

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


Каждая цифра выше — это сохранённый, версионированный запрос к реальным дневным барам. Разверните SQL любой панели или запустите тест с задержкой на своём собственном правиле на терминале Strasmore.

#backtesting#look-ahead bias#survivorship bias#quant#research process