Дизайн целевой функции: метрика, которую вы оптимизируете, тайно выбирает вашу стратегию
Статья из серии "Бэктесты без иллюзий".
📄 Эта статья выросла в исследовательскую работу. Каждое число ниже получено одним детерминированным скриптом, который строит контролируемую истину — синтетический рынок с известным edge в умеренной полосе сигнала и толстохвостым шумом повсюду, — а затем прогоняет один поиск порога через шесть разных целевых функций и измеряет, out of sample, какую стратегию каждая целевая функция на самом деле выбирает. Читайте статью онлайн (интерактивная версия + PDF) на objective-design.marketmaker.cc, код и данные — на github.com/suenot/objective-design-degeneracy.
Вы хотите лучшую стратегию. Поэтому вы запускаете поиск — перебираете порог, lookback, дистанцию стопа — и оставляете ту настройку, которая набрала больше всего очков. Поиск завершается и вручает вам победителя. Разумно. Стандартно. Именно это делает любой оптимизатор, grid search и hyperparameter tuner на свете.
Но посмотрите на глагол: набрала больше всего очков. Больше всего — в чем? Прежде чем поиск сможет кого-то короновать, вы должны были вручить ему одно-единственное число для максимизации — целевую функцию. PnL. Sharpe. Sharpe на барах, где вы торговали. Return-over-max-drawdown. Вы напечатали одно из этого, скорее всего не особо задумываясь, а затем поиск потратил миллион вычислений, делая ровно то, что вы попросили.
Этот единственный выбор — не формальность. Это все решение целиком. Поиск не находит "хорошую стратегию" — такой вещи не существует в абстракции. Он находит стратегию, которая максимизирует выбранный вами скаляр, а разные скаляры указывают на совершенно разные стратегии на одних и тех же данных. Целевая функция — это тайная рука на руле, и в большинстве случаев на нее никто не смотрит.
Вот вся статья в одной таблице. Один поиск порога, один синтетический рынок с реальным, известным edge, шесть целевых функций — и шесть стратегий, которые они выбирают, измеренных на отложенных данных:
| Целевая функция (что максимизирует поиск) | Средний market exposure | In-sample Sharpe | Out-of-sample Sharpe | Вырожденные победители |
|---|---|---|---|---|
| Raw PnL | 0.859 | 1.76 | 1.61 | 0% |
| Full-timeline Sharpe | 0.740 | 1.82 | 1.71 | 0% |
| Per-trade ("активный") Sharpe | 0.286 | 1.00 | 0.70 | 57% |
| Exposure floor () | 0.740 | 1.82 | 1.71 | 0% |
| Trade-count shrinkage (conf_k) | 0.523 | 1.54 | 1.31 | 20.7% |
| Robust (floor + conf_k) | 0.675 | 1.78 | 1.70 | 0.2% |
600 независимых seeds, по баров на каждый, 80 кандидатов-порогов на поиск, in-sample и out-of-sample взяты независимо. Годовой Sharpe (252 периода в году). "Вырожденный" = выбранный победитель находится в рынке менее 5% времени, либо показывает неположительный out-of-sample Sharpe. Истинный оптимум этого рынка — годовой out-of-sample Sharpe 1.77.
Читайте третью строку, пока не зацепит. Per-trade Sharpe — представитель целого семейства activity-conditional метрик (per-trade Sharpe, expectancy, SQN ван Тарпа, win rate), все они считаются только на барах, где вы торговали, — выбирает стратегию, которая out of sample хуже половины остальных, и делает это вырожденно в 57% случаев. Это не слегка худшая целевая функция. На этих данных это ловушка, и поиск попадает в нее больше чем в половине случаев. А теперь прочитайте строку прямо над ней: обычный full-timeline Sharpe никогда не вырождается и показывает 1.71 out of sample. Это и есть развязка всего исправления, раскрытая заранее — честное исправление состоит просто в том, чтобы измерять на полном таймлайне; более изощренные доработки в нижних строках в лучшем случае лишь сравниваются с этим числом, но никогда его не превосходят. Эта статья — анатомия этой ловушки и ее исправления, с известной на всем протяжении истиной, так что вопрос "выбрала ли целевая функция правильную стратегию?" становится фактом, а не мнением.
Акт 1 — Тайное решение: закон Гудхарта — это и есть поиск

В 1975 году экономист Чарльз Гудхарт написал фразу, которая пережила все остальное, что он сделал:
"Любая наблюдаемая статистическая закономерность будет иметь тенденцию разрушаться, как только на нее оказывается давление в целях контроля."
Более популярный перефраз, обычно приписываемый Мэрилин Стратерн, звучит короче: когда мера становится целью, она перестает быть хорошей мерой.
Поиск параметров — это самый чистый из возможных примеров закона Гудхарта. Целевая функция — это мера. Поиск — это давление: тысячи, миллионы попыток вытолкнуть эту меру как можно выше. И поиску совершенно все равно, что вы имели в виду под этой мерой. Его волнует только число. Если есть хоть какой-то способ сделать число большим, не имеющий никакого отношения к реальному, торгуемому edge, — торговать редко, большую часть времени сидеть в стороне, поймать пару удачных выбросов, — поиск найдет этот способ, потому что нахождение максимума — единственное, для чего он построен.
Это тот же самый сбой, который в литературе по AI safety называют reward hacking: агент, оптимизирующий прокси того, что вам нужно, использует каждый зазор между прокси и целью. Ваш поиск — это агент. "Sharpe ratio" — это прокси. "Стратегия, которой я могу доверить реальные деньги в следующем квартале" — это цель. Зазор между ними — вот где живет вся дисциплина.
Чтобы увидеть, как этот зазор открывается, нам нужен мир, в котором мы знаем истину. Поэтому мы его построим.
Рынок. В каждом периоде приходит предиктор (стандартный нормальный сигнал), за которым следует доходность , которую он частично предсказывает. Edge реален, но ограничен — он существует только внутри умеренной полосы сигнала и исчезает за ее пределами:
Важны два дизайнерских решения. Во-первых, edge живет в умеренной полосе: экстремальные сигналы не несут предсказательной информации, поэтому стратегия должна торговать середину и пропускать хвосты. Во-вторых, шум толстохвостый (Student- с 4 степенями свободы — тот вид тяжелого хвоста, который реально есть у настоящих доходностей), но этот ингредиент здесь ради реализма, а не механизма. Соблазнительно сказать, что именно толстые хвосты делают ловушку возможной, и мы предполагали именно это, пока не прогнали контрольный эксперимент: гауссовская версия этого рынка (gaussian_control в результатах, 300 seeds) воспроизводит ловушку практически без изменений — per-trade целевая функция все так же вырождается в 55.7% случаев при гауссовском шуме против 57.0% с толстыми хвостами, а ее out-of-sample Sharpe составляет 0.71 против 0.70. Так что ловушка не про толстые хвосты. Это чистый эффект малой выборки плюс отбора: возьмите максимум по ~20 порогам с низким exposure от Sharpe, посчитанного на горстке наблюдений, — и какой-нибудь удачливый угол всегда будет выглядеть впечатляюще. Это работает при любом распределении шума; стратегия, находящаяся в рынке всего несколько баров, может чисто по удаче пересидеть пару благоприятных движений и выдать in-sample число, которое ничего не значит. Мы сохраняем толстые хвосты, потому что они есть у реальных доходностей, а не потому, что они нужны ловушке.
Стратегия. Однопараметрическое семейство. Торговать в направлении сигнала всякий раз, когда его величина ниже порога , иначе оставаться вне рынка:
Крошечный торгует только на самых маленьких, ничем не примечательных сигналах — лотерея редких сделок, в рынке почти никогда. у края полосы захватывает весь реальный edge и обеспечивает хороший exposure. Огромный торгует все подряд, включая бары вне полосы, которые не несут edge и лишь добавляют шум.
def gen_dataset(T, rng, beta=0.30, band=1.0, tail_df=4):
s = rng.standard_normal(T)
edge = beta * np.where(np.abs(s) <= band, s, 0.0) # edge ONLY inside |s| <= 1
t = rng.standard_t(tail_df, T) / np.sqrt(tail_df / (tail_df - 2.0)) # fat tails, unit var
return s, edge + t
def simulate(s, r, theta):
pos = np.where(np.abs(s) <= theta, np.sign(s), 0.0) # trade the band, skip outliers
strat = pos * r
active = pos != 0.0
exposure = active.mean() # fraction of bars in a position
sharpe_full = strat.mean() / strat.std(ddof=1) # on the WHOLE timeline
sharpe_active = strat[active].mean() / strat[active].std(ddof=1) # on ONLY the active bars
return dict(exposure=exposure, n_trades=int(active.sum()),
sharpe_full=sharpe_full, sharpe_active=sharpe_active, pnl=strat.sum())
Поскольку мы сами построили этот рынок, мы можем вычислить истину напрямую — среднюю производительность на каждом пороге по всем 600 seeds, out of sample. Истинный оптимум находится на : прямо у края полосы сигнала, в рынке примерно 70% времени (вывод: стандартный нормальный сигнал попадает внутрь с вероятностью ), показывая годовой out-of-sample Sharpe 1.77. Это то число, которое пытается найти каждая целевая функция. Держите его в уме: θ≈1.04, OOS Sharpe 1.77, около 70% таймлайна в рынке. Все, что выбирает целевая функция и что далеко от этого, — это провал целевой функции, а не сложность рынка.
Акт 2 — Ловушка: восемь удачных сделок, Sharpe 21, мираж

Теперь дадим наивной целевой функции волю на одном конкретном тираже этого рынка — seed 6. Полная честность насчет seed: это не первый тираж и не случайный. Мы просканировали seeds в поисках ярко вырожденного per-trade победителя и выбрали именно этот, чтобы механизм было невозможно не заметить. Результат, который он показывает, вполне типичен — как подтвердит Акт 3, per-trade целевая функция выбирает лотерею с exposure ниже 5% в 56% всех seeds, — но масштаб seed 6 находится на крайнем краю этого распределения. Читайте это как особенно яркий пример распространенного сбоя, а не как медианный. Мы оптимизируем per-trade Sharpe: Sharpe ratio, посчитанный только на барах, где стратегия реально находится в позиции. Это чрезвычайно естественная вещь для отчета. "Когда она торгует, насколько хороши ее сделки?" Кажется, что это изолирует мастерство от простоя. На деле все наоборот.
Вот стратегия, которую per-trade Sharpe коронует на seed 6:
- Порог — торгует только на самых крошечных сигналах.
- Market exposure 0.4% — вне рынка 99.6% времени.
- Восемь сделок. Восемь. За 2000 баров.
- In-sample годовой per-trade Sharpe: 21.09.
- In-sample full-timeline Sharpe: 0.82.
- Out-of-sample full-timeline Sharpe: 0.13.
Per-trade метрика показывает 21.09 — число, которое не показывала ни одна реальная стратегия, из тех, под которые запускают фонды. И это полный мираж. Эти восемь сделок случайно поймали несколько благоприятных движений; измеренное всего на этих восьми барах, отношение среднего к стандартному отклонению становится астрономическим. Но на полном таймлайне — где стратегия вне рынка 99.6% времени — этот "edge" практически ничего не дает: full-timeline Sharpe 0.82 in sample, схлопывающийся до 0.13 на свежих данных. Победитель, выбранный целевой функцией, для всех практических торговых целей — это отсутствие позиции.
И это даже не реальный edge на этом пороге. Вспомните рынок: edge живет в полосе , а находится в самом центре, где сигнал слабее всего. Истинная out-of-sample кривая на равна −0.01 — неотличимо от нуля (получено из кривой ground truth). Поиск не нашел маленький реальный edge. Он нашел восемь удачных тиражей шума и доложил о них как о Sharpe 21.
Вот ловушка в миниатюре: per-trade Sharpe вознаграждает стратегию за то, что она торгует как можно реже, потому что чем меньше баров вы стоите, тем легче паре из них оказаться удачными, а метрика ни разу не спрашивает "но были ли вы вообще в рынке?" Масштаб seed 6 подобран специально — мы искали яркий пример, — но его природа нет. По всем 600 seeds per-trade Sharpe выбирает вырожденного победителя (того, что почти не торгует, или проигрывает out of sample) в 57% случаев, а именно лотерею с exposure ниже 5% — в 56%. Типичный вырожденный выбор гораздо скромнее, чем Sharpe 21 у seed 6: усредненно по всем 600 seeds победитель per-trade целевой функции имеет in-sample per-trade Sharpe 4.58 и средний exposure 0.286 — все еще вне рынка большую часть времени, просто не на 99.6%. Seed 6 драматизирует механизм; беспокоить вас должны эти 56%. Больше чем в половине случаев эта повседневная метрика вручает вам лотерейный билет и называет его стратегией.
Акт 3 — Статистическая истина: шесть целевых функций, 600 seeds

Один seed ничего не доказывает; он только иллюстрирует. Чтобы измерить целевую функцию, нужно спросить, что она выбирает в среднем, по множеству независимых рынков, и оценить этот выбор на данных, которые поиск никогда не видел. Итак: 600 seeds, каждый — независимый тираж рынка; на каждом запускаем поиск по 80 порогам под каждой целевой функцией; записываем exposure, in-sample и out-of-sample Sharpe того, что было выбрано, и был ли выбор вырожденным.
| Целевая функция | Средний exposure | In-sample Sharpe | Out-of-sample Sharpe | Падение IS→OOS (абс.) | Вырождено |
|---|---|---|---|---|---|
| Raw PnL | 0.859 | 1.76 | 1.61 | 0.15 | 0.0% |
| Full-timeline Sharpe | 0.740 | 1.82 | 1.71 | 0.11 | 0.0% |
| Per-trade Sharpe | 0.286 | 1.00 | 0.70 | 0.30 | 57% |
| Exposure floor () | 0.740 | 1.82 | 1.71 | 0.11 | 0.0% |
| conf_k shrinkage () | 0.523 | 1.54 | 1.31 | 0.23 | 20.7% |
| Robust (floor + conf_k) | 0.675 | 1.78 | 1.70 | 0.08 | 0.2% |
Столбец "Падение IS→OOS" — это абсолютное падение годового Sharpe от in-sample к out-of-sample (например, — это падение на 0.30), а не процент. И обратите внимание: строка "Exposure floor" побайтово идентична строке "Full-timeline Sharpe" — это не совпадение, и Акт 5 объясняет почему.
Бросаются в глаза три факта, и каждый — урок.
Per-trade Sharpe — единственная наивная целевая функция, которая вырождается. Ее средний exposure — 0.286 — она выбирает стратегии, которые большую часть времени вне рынка, — а ее in-sample Sharpe 1.00 падает на 0.30 до out-of-sample 0.70 — худший результат среди всех. Обратите внимание на признак: ее in-sample число (1.00) даже не впечатляет, но на любом отдельном seed она с готовностью выдаст per-trade цифру 21. Среднее вымывается, потому что удачные окна указывают в случайных направлениях; до out-of-sample доживает только 0.70, а 57% отдельных выборов — откровенный мусор.
Exposure-aware целевые функции по своей природе безопасны. Raw PnL и full-timeline Sharpe никогда не вырождаются (0.0%). Причина структурная: обе измеряются по всему таймлайну, поэтому стратегия, вне рынка 99.6% времени, зарабатывает под ними почти ничего. Full-timeline метрику невозможно обмануть, торгуя редко, — простой напрямую и автоматически штрафуется, потому что бары без позиции входят в знаменатель. Это самая важная идея всей статьи, и мы вернемся к ней в Акте 6.
Raw PnL безопасен, но не оптимален — он переэкспонирует. Присмотритесь: средний exposure raw PnL — 0.859, самый высокий из всех, а его out-of-sample Sharpe (1.61) на ступеньку ниже full-timeline Sharpe (1.71) и истинного оптимума (1.77). PnL вознаграждает нахождение в рынке, поэтому поиск толкает слишком высоко (на seed 6 raw PnL выбирает против оптимальных 1.04), затягивая бары вне полосы, которые не несут edge и лишь добавляют шум. Он не взрывается — но проскальзывает мимо реального оптимума в направлении, противоположном per-trade ловушке. Другая целевая функция, другой bias, тот же урок: метрика выбрала стратегию.
Две строки, которые мы еще не обсудили, — exposure floor и conf_k, — это исправление. Это следующий акт.
Акт 4 — Почему восьми сделкам нельзя доверять никогда

Прежде чем чинить ловушку, стоит точно понять, почему восемь сделок дают Sharpe 21, который ничего не значит, — потому что исправление напрямую следует из причины.
Sharpe ratio — это оценка, а у оценок есть error bars. Результат Эндрю Ло 2002 года дает стандартную ошибку Sharpe ratio, оцененного по наблюдениям, при самом щедром из возможных предположений (IID гауссовские доходности):
Ошибка убывает лишь как . Подставим сюда ловушку. Per-trade Sharpe на seed 6 равен годовых, что составляет на наблюдение, посчитано на барах. Стандартная ошибка равна
(получено из формулы Ло). Точечная оценка равна ; ее error bar в одну сигму имеет порядок — читайте это как иллюстративный порядок величины, а не калиброванный доверительный интервал, поскольку формула предполагает IID гауссовские доходности, которые нарушает наш толстохвостый шум. И все же сообщение однозначно: "Sharpe 21" — это число, взятое из распределения настолько широкого, что оно несет практически нулевую информацию, — и это еще щадящий расчет, потому что расширение Мертенса показывает, что толстые хвосты и skew только дополнительно раздувают стандартную ошибку. Sharpe бэктеста с редкими сделками менее надежен, чем его точечное значение, сразу по всем направлениям: наблюдений слишком мало, и распределение не то.
Именно это формализует Minimum Track Record Length (Bailey & López de Prado, 2012). Он переворачивает вопрос — сколько наблюдений мне нужно, прежде чем мне позволено поверить в Sharpe такого размера с уверенностью ? —
превращая "доверяй бэктестам с редкими сделками меньше" в явное, проверяемое число сделок. Глубокая мысль для дизайна целевой функции такова: хорошая целевая функция должна навязывать минимальный track record изнутри, а не оставлять человеку замечать постфактум, что победитель опирается на восемь наблюдений. Per-trade Sharpe делает противоположное — он максимизируется, толкая число наблюдений к минимуму. Любая целевая функция, чей оптимум лежит в "как можно меньше сделок", по построению является целевой функцией, которая разыскивает собственную наименее надежную оценку.
В ловушке складываются два сбоя, и назвать оба — значит понять, как ее чинить. Первый — шум малой выборки: восемь наблюдений не могут точно зафиксировать никакой коэффициент. Второй — отбор: эти восемь баров не были нам просто вручены — поиск выбрал порог, который на них попал, отчасти потому что они оказались удачными. Поиск — это максимизатор; он всегда найдет тот угол пространства, где шум случайно выглядит как сигнал. Это невозможно перехитрить лучшей точечной оценкой. Нужно поменять само значение слова "лучшее", чтобы удачный угол перестал быть максимумом.
Акт 5 — Исправление: exposure floor и trade-count shrinkage

У нас есть две названные болезни — торгует слишком редко и опирается на слишком мало наблюдений — поэтому мы пропишем два лекарства, каждое нацелено на одну из них.
Лекарство 1: exposure floor. Простейшее из возможных исправлений. Прямо отвергать любую стратегию, которая не в рынке хотя бы времени, — если вы почти не торгуете, ваш счет равен , и поиск не может вас выбрать. Но здесь есть честная тонкость в том, что именно вы флорите, и это тихий урок всей этой статьи. В качестве самостоятельной целевой функции мы флорили full-timeline Sharpe, и на этом рынке это вообще ничего не меняет: собственный победитель full-timeline Sharpe уже сидит на ~74% exposure, так что floor в 20% ни разу не срабатывает. Именно поэтому строки "exposure floor" и "full-timeline Sharpe" в таблицах выше побайтово идентичны — привинтите floor к уже безопасной метрике, и вы просто заново вывели full Sharpe. Floor делает видимую работу только тогда, когда он охраняет метрику, которая в противном случае рванула бы в угол, — per-trade метрику, как в robust целевой функции ниже. Другими словами, "требовать exposure" и "измерять на полном таймлайне" на этих данных — два названия одного и того же вмешательства.
Лекарство 2: trade-count shrinkage — "conf_k". Для случаев, когда вы застряли с per-trade метрикой и хотите мягкую коррекцию вместо жесткого отсечения: дисконтировать Sharpe непрерывно в зависимости от того, на скольких сделках он держится. Умножьте на , где — число сделок, а — фиксированная "константа доверия" — эквивалентная в сделках сила prior, выбранная до поиска:
При счет обнуляется независимо от того, насколько велик сырой Sharpe; при счет сходится к сырому Sharpe. Это та же исправляющая логика, что MinTRL и стандартная ошибка из Акта 4 — сжимать оценку малой выборки к нулю как убывающую функцию ее размера, — только вшитая напрямую в саму целевую функцию, а не примененная как постфактум-фильтр. Ближайший названный прецедент — System Quality Number ван Тарпа (), который точно так же масштабирует per-trade метрику качества числом сделок — хотя функциональная форма другая ( растет неограниченно, тогда как насыщается на 1). По форме наша конструкция — это shrinkage в стиле Bayesian precision-weighted / empirical-Bayes; это наша собственная конструкция для этой задачи, а не названный оценщик, позаимствованный из литературы.
def obj_active_sharpe(m): # the trap: Sharpe on only the active bars
return m["sharpe_active"]
def _shrink(n, conf_k): # trade-count shrinkage n / (n + k)
return n / (n + conf_k) if (n + conf_k) > 0 else 0.0
def obj_confk(m, conf_k=40.0): # few trades -> little credit
return m["sharpe_active"] * _shrink(m["n_trades"], conf_k)
def obj_robust(m, e_min=0.20, conf_k=40.0): # both cures at once
if m["exposure"] < e_min: # floor: reject strategies that barely trade
return -np.inf
return m["sharpe_active"] * _shrink(m["n_trades"], conf_k)
Теперь честная часть: сколько floor, сколько shrinkage? Пройдитесь по обоим и прочитайте всю поверхность. Каждая ячейка — это средний out-of-sample Sharpe по 200 seeds (треть от 600, чтобы двумерный sweep остался дешевым), рядом — доля вырождения:
| \ conf_k | |||
|---|---|---|---|
| 0.00 | 0.66 (59.5%) | 1.26 (22.5%) | 1.47 (11.5%) |
| 0.05 | 1.43 (10.0%) | 1.53 (6.0%) | 1.60 (4.0%) |
| 0.10 | 1.64 (1.5%) | 1.65 (1.0%) | 1.67 (1.0%) |
| 0.20 | 1.71 (0.0%) | 1.71 (0.0%) | 1.71 (0.0%) |
| 0.35 | 1.73 (0.0%) | 1.73 (0.0%) | 1.73 (0.0%) |
Средний годовой out-of-sample Sharpe, доля вырождения в скобках. Верхняя левая ячейка — это сырой per-trade Sharpe, без floor, без shrinkage: OOS 0.66, вырождение 59.5%. Это та же самая целевая функция, что и per-trade строка из Акта 3, где было 0.70 / 57%; небольшой разрыв — это чисто набор seeds: этот sweep использует 200 seeds, Monte Carlo использовал все 600. Та же метрика, меньшая выборка.
Поверхность рассказывает чистую историю в трех прочтениях.
Каждое лекарство работает само по себе. Двигайтесь вправо по верхней строке (добавляем shrinkage, без floor): OOS растет , а вырождение падает . Двигайтесь вниз по левому столбцу (добавляем floor, без shrinkage): OOS растет , а вырождение падает . Любая ручка, повернутая в одиночку, независимо поднимает out-of-sample производительность и убивает вырождение. Exposure floor — более сильный из двух рычагов здесь, потому что он атакует определяющую черту ловушки — почти нулевой exposure — в лоб.
Вместе они достигают плато — и это плато есть просто full-timeline Sharpe. К строка выравнивается на OOS 1.71 с 0% вырождения на любом уровне shrinkage; подвиньте до — и оно ползет к 1.73. Но всмотритесь, чем на самом деле является это 1.71: это ровно тот счет, который обычный full-timeline Sharpe показывает в Акте 3 вообще без floor и без shrinkage. В лучшем случае доработки не превосходят full-timeline Sharpe — они его реконструируют. И даже полностью исправленная robust целевая функция не совсем до него дотягивает: по всем 600 seeds она садится на OOS 1.70 с остаточным вырождением 0.17%, чуть ниже 1.71 / 0% у full Sharpe — она слабо доминируется более простой метрикой. Скромная средняя настройка, с , достигает OOS 1.65 при 1% вырождения — пригодится, если per-trade метрика вам навязана, но никогда не повод ее предпочесть.
Точные числа зависят от масштаба — результат в самой форме. Конкретные значения , , которые полностью чинят этот рынок, подогнаны под этот конкретный data-generating process; на другом рынке с другой частотой сделок и толщиной хвостов плато находится в другом месте. Обобщается не координаты, а поверхность: монотонный подъем по обоим направлениям, вырождение, доведенное до нуля, плато на истине. Свои координаты вы находите своим sweep, точно как выше.
Соедините оба лекарства вместе — robust целевая функция, floor 0.20 плюс conf_k 40 — и вернитесь к seed 6. Ловушка короновала , восемь сделок, full-timeline OOS Sharpe 0.13. Robust целевая функция вместо этого выбирает : market exposure 0.66, 447 сделок, годовой out-of-sample Sharpe 1.77. Это находится на одну точку сетки ниже истинного оптимума , так что она восстанавливает близкий к оптимальному, хорошо экспонированный порог, а не попадает точно в яблочко — ее out-of-sample Sharpe на одном seed (1.77) просто совпадает с оптимумом всей популяции. Те же данные, тот же поиск, те же 80 порогов-кандидатов. Изменилось только определение "лучшего", и оно передвинуло победителя от плоского восьмисделочного миража к реальному, хорошо экспонированному edge — который, что показательно, оказывается тем же самым порогом, что выбирает обычный full-timeline Sharpe на этом seed.
Одно предостережение, которое sweep делает явным: одного conf_k недостаточно на этом рынке. При без floor вырождение по seeds все еще 22.5% — а конкретно на seed 6 один conf_k выбирает , 35 сделок, out-of-sample Sharpe −0.06. Тридцать пять сделок переживают shrinkage , и оставшегося счета хватает, чтобы победить. Именно exposure floor закрывает этот последний зазор, потому что он нацелен прямо на истинную сигнатуру ловушки — простой, — а не полагается на число сделок как на ее прокси.
Акт 6 — Более глубокий урок: измеряйте на всем таймлайне

Отступите от исправления и заметьте, что на самом деле отделяло безопасные целевые функции от ловушки. Это была не изощренность. Raw PnL и full-timeline Sharpe проще, чем per-trade Sharpe, и они ни разу не вырождались — 0% по 600 seeds — вообще без floor, без shrinkage, без всякой настройки.
Разделительная линия — единственное свойство: какое окно измеряет метрика? Per-trade Sharpe измеряет только бары, которые стратегия сама выбрала занять, — самоотобранное окно, которое поиск может сжимать по своему усмотрению. Full-timeline Sharpe и суммарный PnL измеряют весь таймлайн, включая плоские бары. И невозможно сделать full-timeline метрику большой, торгуя редко, потому что каждый час простоя — это час в знаменателе, зарабатывающий ноль. Exposure floor и conf_k, в конечном счете, — это просто способы доработать per-trade метрику exposure-awareness, которая у full-timeline метрик есть бесплатно, — и sweep уже сказал нам потолок этой доработки: в лучшем случае она лишь сравнивается с full-timeline Sharpe (OOS 1.70 против 1.71), никогда не превосходит его. Если у вас есть свобода выбрать окно, выбирайте весь таймлайн и полностью пропускайте доработку.
Итак, принцип дизайна, сформулированный прямо:
Проектируйте целевую функцию так, чтобы ее нельзя было обмануть редкими удачными сделками. У вас есть три инструмента, примерно в порядке предпочтения:
- Измеряйте на полном таймлайне. Значение по умолчанию, от которого почти никогда не следует отступать. Full-timeline Sharpe и суммарная доходность exposure-aware по построению — простой автоматически штрафуется, потому что плоские бары считаются. Если вы ловите себя на том, что отчитываетесь по метрике, посчитанной "только на барах, где мы были активны", остановитесь и спросите, что сделает поиск с свободой выбирать эти бары.
- Требуйте exposure. Если вам приходится использовать activity-conditional метрику, флорите exposure так, чтобы поиск не мог выбрать стратегию, которая почти не торгует. Это самый сильный из одиночных рычагов против конкретно этой ловушки.
- Сжимайте по числу сделок. Дисконтируйте любой коэффициент на , чтобы Sharpe, опирающийся на горстку наблюдений, зарабатывал лишь долю доверия того, что опирается на тысячи. Это навязывание Minimum Track Record Length на уровне самой целевой функции: число из малого числа наблюдений ненадежно (Акт 4), поэтому честная целевая функция закладывает эту ненадежность в цену заранее, вместо того чтобы полагаться на человека, который заметит это позже.
Ничто из этого не делает поиск умнее. Это делает цель честной, так что когда поиск делает ровно то, что он всегда делает — находит максимум, — этот максимум оказывается стратегией, которую вы действительно хотите.
Заметки честности
Три оговорки, сформулированные прямо, потому что контролируемое исследование зарабатывает свои выводы, только называя собственные границы.
- Рынок синтетический, и намеренно. Стандартный нормальный сигнал, линейный edge, ограниченный , толстохвостый шум Student-() — выбраны ради контролируемой истины, а не ради рыночного реализма. Доказать, что целевая функция выбирает неправильную стратегию, можно только прогнав ее на данных, где мы знаем, какая стратегия правильная. Реальные рынки нестационарны, автокоррелированы и меняют режимы. Толстые хвосты — реалистичный ингредиент, который мы сохранили, но — вопреки естественной первой догадке и вопреки более раннему черновику этой самой статьи — они не то, что питает ловушку: гауссовский контроль (300 seeds) вырождается в 55.7% случаев против 57.0% здесь, с out-of-sample Sharpe 0.71 против 0.70. Ловушка — это артефакт малой выборки плюс отбора, который выживает и с толстыми хвостами, и без них. Результат этой работы — диагноз и паттерн исправления, а не стратегия и не универсальная константа.
- Исправляющие значения зависят от масштаба. Конкретные floor и shrinkage , полностью закрывающие ловушку, подогнаны под этот data-generating process — его частоту сделок, толщину хвостов, размер edge. На других данных плато сдвигается. Переносится форма поверхности sweep (монотонный подъем по обеим ручкам, вырождение к нулю, плато на истине) и метод нахождения собственных координат: пройдитесь по обеим и прочитайте поверхность, не копируйте числа.
- conf_k — наша собственная конструкция, а не названный оценщик. Trade-count shrinkage — это устройство в духе Bayesian precision-weighted / empirical-Bayes, которое мы построили для этой задачи; его обоснование опирается на проверенный результат стандартной ошибки Lo/Mertens и MinTRL Bailey–López de Prado, а его ближайший названный родственник — System Quality Number ван Тарпа (, другая функциональная форма), но мы не утверждаем, что сам фигурирует под каким-то именем в литературе. Его лекарства-компаньоны — exposure floor, измерение на full-timeline — это стандартная практика, сформулированная точно. И обратите внимание, какие целевые функции здесь были уже безопасны: raw PnL и full-timeline Sharpe никогда не нуждались в исправлении, потому что они exposure-aware изначально — настолько, что флорирование full-timeline Sharpe сводится к самому full-timeline Sharpe, его победитель уже проходит любой разумный floor. Ловушка — это конкретно per-trade / active Sharpe — и даже полностью исправленная per-trade целевая функция лишь сравнивается с full-timeline Sharpe (OOS 1.70 против 1.71), никогда не превосходит его. Главный урок — не исправление; это измерять на полном таймлайне с самого начала.
Выводы
- Целевая функция — это решение, а не формальность. Поиск не находит "хорошую стратегию" — он находит максимизатор того скаляра, который вы ему вручили, а разные скаляры выбирают совершенно разные стратегии на идентичных данных. Выбор целевой функции и есть выбор стратегии; все, что после, — бухгалтерия. Это и есть закон Гудхарта: в момент, когда ваша метрика становится целью поиска, поиск использует каждый зазор между ней и тем, что вы имели в виду.
- Per-trade Sharpe — это ловушка. Измеренный только на барах, где стратегия торгует, он максимизируется, торгуя как можно реже — чем меньше наблюдений, тем легче паре удачных движений раздуть коэффициент (гауссовский контроль подтверждает, что толстые хвосты не нужны; это эффект малой выборки плюс отбора). По 600 seeds он выбирает лотерею с exposure ниже 5% в 56% случаев и вырождается в 57%; типичный вырожденный выбор в среднем дает in-sample per-trade Sharpe 4.58. На одном намеренно ярком seed он короновал восьмисделочную стратегию с exposure 0.4% и in-sample per-trade Sharpe 21.09, схлопывающимся до full-timeline out-of-sample Sharpe 0.13. Коэффициент, построенный на восьми наблюдениях, имеет стандартную ошибку порядка ±7.7 годовых (Акт 4) — он никогда не был информацией.
- Exposure-aware целевые функции безопасны по своей природе. Raw PnL и full-timeline Sharpe ни разу не вырождались (0%), потому что они измеряют весь таймлайн, и простой автоматически штрафуется. Full-timeline метрику невозможно обмануть, торгуя редко. Единственный изъян raw PnL — противоположный bias: он переэкспонирует (средний exposure 0.859, OOS 1.61 против истинных 1.77), сдвигая мимо оптимума ради большего времени в рынке.
- Исправления работают — но они лишь возвращают вас к full-timeline Sharpe. Exposure floor и trade-count (conf_k) shrinkage каждый независимо поднимает out-of-sample Sharpe и толкает вырождение к нулю; вместе они достигают плато. Но это плато и есть full-timeline Sharpe: вырождение падает с 59.5% при до 0% к , а OOS Sharpe поднимается с 0.66 до 1.71 — ровно то число, которое обычный full Sharpe показывает без всякой помощи, а полностью исправленная robust целевая функция на деле садится чуть ниже него (OOS 1.70, вырождение 0.17%: слабо доминируется). На seed 6 robust целевая функция восстанавливает близкий к оптимальному, хорошо экспонированный порог (, на одну точку сетки ниже истинных ; 447 сделок; OOS Sharpe 1.77). Относитесь к conf_k как к запасному варианту на случай, если per-trade метрика вам навязана, а не как к апгрейду над измерением полного таймлайна. Точные координаты зависят от масштаба; форма поверхности — переносимый результат.
- Проектируйте целевую функцию так, чтобы ее нельзя было обмануть редкими удачными сделками. В порядке предпочтения: измеряйте на полном таймлайне (значение по умолчанию), требуйте exposure (самый сильный из одиночных рычагов), сжимайте по числу сделок (Minimum Track Record Length на уровне целевой функции). Ничто из этого не делает поиск умнее — это делает цель честной, так что максимум, который поиск неизбежно найдет, окажется стратегией, которую вы реально стали бы торговать.
Поиск параметров — послушный джинн. Он исполняет ровно то желание, которое вы сформулировали, а не то, что вы имели в виду, — и "максимизируй эту метрику" и есть это желание. Сформулируйте его как per-trade Sharpe, и он наколдует восемь удачных сделок и назовет их состоянием. Сформулируйте его так, что простой наказывается, а горстка сделок зарабатывает лишь горстку доверия, — и тот же самый джинн на тех же данных вручит вам реальный edge. Стратегия, которую вы разворачиваете, была выбрана в тот момент, когда вы напечатали целевую функцию. Выбирайте ее осознанно.
Полный эксперимент — синтетический рынок, шесть целевых функций, Monte Carlo на 600 seeds и поверхность repair-sweep, каждое число регенерируемо одним детерминированным скриптом — в статье-компаньоне на objective-design.marketmaker.cc, код и данные — на github.com/suenot/objective-design-degeneracy.
Это один фронт той же войны, которую наши другие исследования ведут с разных углов. Deflated Sharpe Ratio оценивает цену победителя поиска после multiple testing — там, где эта статья спрашивает, выбрала ли целевая функция правильную стратегию вообще, DSR спрашивает, обыгрывает ли выбранная стратегия то, что произвела бы одна лишь удача. Готовящееся исследование probability of backtest overfitting атакует тот же selection bias со стороны ресемплинга, оценивая саму процедуру, а не победителя. А таксономия look-ahead bias каталогизирует другого великого производителя фальшивого Sharpe — утечку из будущего, — который производит идентичный симптом (великолепный бэктест, умирающий вживую) совершенно другим механизмом. Дизайн целевой функции, дефляция, вероятность overfitting, look-ahead: четыре имени одной дисциплины — не дать себя обмануть собственным бэктестом.
Авторы
Инженер торговых систем
Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.