← К списку статей
June 12, 2026
5 мин. чтения

Конформное прогнозирование для риск-ориентированного определения размера позиции

Конформное прогнозирование для риск-ориентированного определения размера позиции
#uncertainty
#conformal-prediction
#risk
#position-sizing
#statistics
#algorithmic-trading
🧠
Часть 4 из 4 · Подборка
Глубокое обучение для рынков

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

Конформное прогнозирование предлагает нечто иное: предсказательные интервалы с гарантиями покрытия на конечной выборке, без каких-либо параметрических предположений о распределении. Если вы просите 90% покрытия, вы получаете как минимум 90% покрытия — независимо от того, являются ли доходности гауссовскими, с тяжелыми хвостами, асимметричными или гетероскедастичными. Единственное требование — обмениваемость (или более слабые условия, как мы увидим).

В этой статье разбираются теория, ключевые варианты и практическая реализация определения размера позиции на Python.

Основная идея: меры неконформности

Распределение мер неконформности (остатков) с отмеченным квантильным порогом — основа конформной калибровки

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

Для регрессии (предсказания непрерывной величины, например доходности) простейшая мера неконформности — это абсолютный остаток:

Ri=Yiμ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)|

где μ^\hat{\mu} — любой точечный предиктор (линейная регрессия, случайный лес, нейронная сеть — это не имеет значения), а (Xi,Yi)(X_i, Y_i) — точка данных.

Ключевое наблюдение: если точки данных (X1,Y1),,(Xn,Yn),(Xn+1,Yn+1)(X_1, Y_1), \ldots, (X_n, Y_n), (X_{n+1}, Y_{n+1}) обмениваемы, то ранг Rn+1R_{n+1} среди R1,,Rn,Rn+1R_1, \ldots, R_n, R_{n+1} равномерно распределен на {1,,n+1}\{1, \ldots, n+1\}. Это чисто комбинаторный факт — он не требует никаких предположений о распределении XX или YY.

Из этой равномерности рангов мы можем построить предсказательные интервалы с покрытием на конечной выборке.

Split conformal prediction

Split conformal prediction: данные разделены на обучающую и калибровочную части, остатки калибровки порождают предсказательные интервалы

Split conformal prediction (Papadopoulos et al., 2002; Lei et al., 2018) — наиболее практичный вариант. Алгоритм прост:

Шаг 1. Разделите данные на обучающее множество Dtrain\mathcal{D}_{\text{train}} и калибровочное множество Dcal={(X1,Y1),,(Xn,Yn)}\mathcal{D}_{\text{cal}} = \{(X_1, Y_1), \ldots, (X_n, Y_n)\}.

Шаг 2. Обучите любую модель μ^\hat{\mu} на Dtrain\mathcal{D}_{\text{train}}.

Шаг 3. Вычислите меры неконформности на калибровочном множестве:

Ri=Yiμ^(Xi),i=1,,nR_i = |Y_i - \hat{\mu}(X_i)|, \quad i = 1, \ldots, n

Шаг 4. Для желаемого уровня непокрытия α(0,1)\alpha \in (0, 1) возьмите q^\hat{q} как эмпирический квантиль уровня (1α)(n+1)n\frac{\lceil (1 - \alpha)(n + 1) \rceil}{n} величин R1,,RnR_1, \ldots, R_n. Конкретно это (1α)(n+1)\lceil (1 - \alpha)(n + 1) \rceil-й по величине наименьший остаток (и q^=+\hat{q} = +\infty всякий раз, когда (1α)(n+1)>n\lceil (1 - \alpha)(n + 1) \rceil > n, то есть при очень малых nn).

Шаг 5. Предсказательный интервал для новой точки Xn+1X_{n+1}:

C(Xn+1)=[μ^(Xn+1)q^,  μ^(Xn+1)+q^]C(X_{n+1}) = \left[\hat{\mu}(X_{n+1}) - \hat{q}, \; \hat{\mu}(X_{n+1}) + \hat{q}\right]

Гарантия покрытия

При обмениваемости калибровочных данных и новой тестовой точки:

P(Yn+1C(Xn+1))1α\mathbb{P}\left(Y_{n+1} \in C(X_{n+1})\right) \geq 1 - \alpha

Это гарантия на конечной выборке — а не асимптотическое приближение. Она выполняется для любой модели μ^\hat{\mu}, любого распределения данных и любого размера выборки nn. Если μ^\hat{\mu} — ужасный предиктор, интервалы просто будут шире. Гарантия покрытия все равно сохраняется.

Есть также верхняя граница, когда среди мер нет совпадений: P(Yn+1C(Xn+1))1α+1n+1\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \leq 1 - \alpha + \frac{1}{n+1}, так что покрытие не является расточительно консервативным.

Почему это важно для трейдинга

Традиционные предсказательные интервалы, скажем, из линейной регрессии предполагают гауссовские ошибки. Гауссовский интервал, откалиброванный по основной массе данных, может сильно ошибаться в хвостах, когда истинные остатки имеют тяжелые хвосты (например, распределение Стьюдента с несколькими степенями свободы): центральная масса тоньше гауссовской, поэтому гауссовский интервал, подогнанный по дисперсии, переоценивает покрытие вблизи центра, но недооценивает в хвостах, а интервал, подогнанный по хвостам, делает наоборот. Дело не в каком-то одном волшебном числе — а в том, что фактическое покрытие параметрического интервала зависит от предположения о распределении, которое вы не проверили.

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

  • Если вы определяете размер позиций обратно пропорционально ширине интервала, вы автоматически снижаете экспозицию, когда модель не уверена.
  • Гарантия покрытия означает, что ваши оценки риска честны — если вы говорите "90% реализованных доходностей попадут в этот интервал", это утверждение статистически корректно (маргинально, при обмениваемости).

Full conformal и jackknife+

Split conformal прост, но расточителен с данными: калибровочное множество нельзя использовать для обучения. Две альтернативы решают эту проблему.

Full conformal prediction

Full conformal prediction (Vovk et al., 2005) использует все данные как для обучения, так и для калибровки. Для каждого кандидатного значения yy величины Yn+1Y_{n+1}:

  1. Дополните набор данных точкой (Xn+1,y)(X_{n+1}, y).
  2. Переобучите модель на дополненном наборе данных.
  3. Вычислите все меры неконформности.
  4. Включите yy в предсказательное множество, если мера для (Xn+1,y)(X_{n+1}, y) не слишком экстремальна.

Предсказательное множество:

C(Xn+1)={y:{i:RiyRn+1y}n+1>α}C(X_{n+1}) = \left\{y : \frac{|\{i : R_i^y \geq R_{n+1}^y\}|}{n+1} > \alpha \right\}

где RiyR_i^y — меры неконформности, вычисленные на дополненном наборе данных.

Full conformal дает самые узкие интервалы, но вычислительно непосильна для большинства моделей — необходимо переобучать модель для каждого кандидата yy на сетке. Для предсказания доходности это могло бы означать тысячи переобучений на одно предсказание.

Jackknife+ (Barber et al., 2021)

Jackknife+ находит баланс. Он использует остатки по принципу исключения одного наблюдения (leave-one-out, LOO), но учитывает изменчивость обученной модели по LOO-разбиениям.

Пусть μ^i\hat{\mu}_{-i} обозначает модель, обученную на всех данных, кроме точки ii. Определим LOO-меру неконформности через единственный абсолютный остаток:

Ri=Yiμ^i(Xi)R_i = |Y_i - \hat{\mu}_{-i}(X_i)|

Предсказательный интервал jackknife+ затем строится из LOO-предсказаний в тестовой точке, расширенных на эти остатки:

C(Xn+1)=[qα ⁣{μ^i(Xn+1)Ri},    q1α+ ⁣{μ^i(Xn+1)+Ri}]C(X_{n+1}) = \left[\, q_{\alpha}^{-}\!\left\{\hat{\mu}_{-i}(X_{n+1}) - R_i\right\}, \;\; q_{1-\alpha}^{+}\!\left\{\hat{\mu}_{-i}(X_{n+1}) + R_i\right\} \right]

Здесь q1α+{vi}q_{1-\alpha}^{+}\{v_i\} обозначает (1α)(n+1)\lceil (1-\alpha)(n+1)\rceil-е по величине наименьшее значение множества {vi}i=1n\{v_i\}_{i=1}^n, а qα{vi}q_{\alpha}^{-}\{v_i\}α(n+1)\lfloor \alpha(n+1)\rfloor-е по величине наименьшее значение. Нижняя граница вычитает остаток из каждого LOO-предсказания; верхняя граница его прибавляет. Эта асимметрия и есть весь смысл — если свести обе границы к μ^i+Ri\hat{\mu}_{-i} + R_i, то нижняя граница окажется выше предсказания, что неверно.

Jackknife+ дает гарантию покрытия не менее 12α1 - 2\alpha (немного слабее, чем 1α1-\alpha у split conformal), но использует все данные как для обучения, так и для калибровки. На практике покрытие обычно близко к 1α1-\alpha.

Для торговых моделей, обученных на ограниченных данных (например, специфичных для режима моделей с лишь несколькими сотнями наблюдений), jackknife+ часто оказывается лучшим выбором — он не жертвует дефицитными данными ради калибровки. Цена этого — nn переобучений модели.

Проблема финансовых временных рядов: необмениваемость

Необмениваемость в финансовых временных рядах: нестационарный ряд со сменой режима, покрытие рушится по мере дрейфа распределения

Стандартная конформная гарантия требует обмениваемости: совместное распределение (Z1,,Zn+1)(Z_1, \ldots, Z_{n+1}) инвариантно относительно перестановок. Для i.i.d.-данных это выполняется тривиально.

Финансовые временные ряды не обмениваемы. Доходности демонстрируют:

  • Кластеризацию волатильности: периоды высокой волатильности следуют за периодами высокой волатильности (GARCH-эффекты).
  • Моментум и возврат к среднему: автокорреляцию доходностей или квадратов доходностей.
  • Смену режимов: распределение смещается со временем (бычьи и медвежьи рынки).

Если вы наивно применяете split conformal к временному ряду — используя случайное калибровочное разбиение — вы нарушаете временную структуру. Меры калибровки из спокойного 2017 года не отразят неопределенность волатильного 2020 года. Ваша гарантия покрытия ломается.

Адаптивный конформный вывод (ACI)

Адаптивный конформный вывод: предсказательный интервал, который расширяется и сужается через цикл обратной связи, ведущий фактическое покрытие к целевому

Gibbs и Candes (2021, NeurIPS) представили адаптивный конформный вывод (Adaptive Conformal Inference, ACI) для работы со сдвигом распределения и необмениваемыми данными. Идея изящна: вместо использования фиксированного уровня покрытия адаптировать целевой уровень непокрытия в онлайн-режиме в зависимости от того, покрывали ли недавние интервалы истинный исход, и заново выводить квантиль из распределения мер на каждом шаге.

Алгоритм ACI

ACI не двигает ширину интервала напрямую. Он поддерживает адаптивный параметр непокрытия αt\alpha_t и пересчитывает из него конформный квантиль. На каждом временном шаге tt:

  1. Вычислите конформный порог как эмпирический квантиль уровня (1αt)(1 - \alpha_t) текущего множества остатков (меры калибровки плюс любые реализованные на данный момент меры): q^t=Quantile^1αt({Rj})\hat{q}_t = \widehat{\text{Quantile}}_{1-\alpha_t}(\{R_j\}).
  2. Наблюдайте признаки XtX_t, постройте интервал Ct(Xt)=[μ^(Xt)q^t,  μ^(Xt)+q^t]C_t(X_t) = [\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t].
  3. Наблюдайте истинное значение YtY_t и вычислите индикатор ошибки errt=1{YtCt(Xt)}\text{err}_t = \mathbf{1}\{Y_t \notin C_t(X_t)\}.
  4. Обновите уровень (а не ширину):

αt+1=clip ⁣(αt+γ(αerrt),  0,  1)\alpha_{t+1} = \text{clip}\!\left(\alpha_t + \gamma\,(\alpha - \text{err}_t),\; 0,\; 1\right)

где γ>0\gamma > 0 — размер шага, а α\alpha — целевое непокрытие. Если интервал промахнулся (errt=1\text{err}_t = 1), αt\alpha_t уменьшается, что поднимает следующий квантиль выше и расширяет интервал; если он покрыл, αt\alpha_t растет и интервалы сужаются. Принципиально важно, что γ\gamma здесь измеряется в единицах вероятности — он сдвигает уровень в [0,1][0,1], а не порог в исходных единицах доходности — поэтому одно и то же γ\gamma ведет себя разумно, будь остатки порядка 10310^{-3} или нет.

Гарантия покрытия для ACI

ACI дает долгосрочную гарантию покрытия, которая не зависит от модели распределения:

1Tt=1TerrtααT+1α1γT\left|\frac{1}{T}\sum_{t=1}^{T} \text{err}_t - \alpha\right| \leq \frac{|\alpha_{T+1} - \alpha_1|}{\gamma T}

Поскольку αt\alpha_t ограничено отрезком [0,1][0,1], числитель ограничен величиной 1/γ1/\gamma, умноженной на константу, так что правая часть имеет порядок O(1/T)O(1/T) и эмпирическая частота непокрытия сходится к α\alpha. Точная формулировка: ACI гарантирует, что долгосрочная эмпирическая частота непокрытия сходится к α\alpha для произвольных (в том числе состязательных) последовательностей, при условии что адаптированные уровни остаются ограниченными — что и обеспечивает усечение (clip). Это гарантия на частоту покрытия, а не на информативность интервалов: при по-настоящему состязательной последовательности интервалы могут стать неинформативно широкими, по-прежнему достигая целевого покрытия.

Динамически настраиваемый ACI (DtACI)

Gibbs и Candes (2024, JMLR) усовершенствовали ACI динамической настройкой размера шага γ\gamma. Вместо фиксации γ\gamma они поддерживают набор кандидатов Γ={γ1,,γK}\Gamma = \{\gamma_1, \ldots, \gamma_K\} и комбинируют их по правилу агрегации экспертов, отдавая предпочтение тому γ\gamma, чье недавнее покрытие ближе всего к целевому.

Это решает практическую проблему: большое γ\gamma быстро адаптируется к смене режимов, но дает волатильную ширину интервалов; малое γ\gamma стабильно, но медленно адаптируется. DtACI находит компромисс между ними автоматически.

Почему это важно для трейдинга

Рассмотрим маркет-мейкинговую стратегию, использующую модель прогнозирования доходности. На спокойных рынках конформные интервалы узки — модель уверена, и вы можете брать более крупные позиции. Когда волатильность подскакивает (сезон отчетности, объявления FOMC, геополитические шоки), уровень ACI адаптируется, и интервалы расширяются за несколько временных шагов. Размер вашей позиции сжимается в ответ, без какой-либо явной модели волатильности или логики обнаружения режимов.

Это количественная оценка неопределенности как полноценный сигнал, а не как нечто второстепенное.

Определение размера позиции с конформными интервалами

Отображение откалиброванного интервала неопределенности в размер позиции: узкий интервал задает крупную позицию, широкий — небольшую

Теперь свяжем конформное прогнозирование с конкретным определением размера позиции. Ключевая переменная — полуширина предсказательного интервала относительно симметричного случая абсолютного остатка. При симметричном интервале [μ^(Xt)q^t,  μ^(Xt)+q^t][\hat{\mu}(X_t) - \hat{q}_t, \; \hat{\mu}(X_t) + \hat{q}_t] полная ширина равна wt=2q^tw_t = 2\hat{q}_t. Чтобы формулы и код были согласованы, мы всюду измеряем все относительно полной ширины wtw_t.

Определение размера обратно пропорционально ширине

Простейший подход: размер обратно пропорционален ширине интервала.

position_sizet=kwt\text{position\_size}_t = \frac{k}{w_t}

где kk — масштабирующая константа, откалиброванная под ваш бюджет риска. Когда модель уверена (узкий интервал), вы берете более крупную позицию. Когда не уверена (широкий интервал), берете меньшую.

Это аналогично таргетированию волатильности (size1/σ\text{size} \propto 1/\sigma), но с принципиальным отличием: ширина конформного интервала — это свободная от распределения мера неопределенности, а не параметрическая оценка волатильности. Она отражает неопределенность предсказания при гарантии покрытия, а не просто дисперсию доходности.

Определение размера по отношению преимущества и фильтр отказа от сделки

Чистое определение размера обратно пропорционально ширине игнорирует силу самого сигнала. Естественное уточнение — масштабировать по отношению преимущества (edge ratio) — точечному предсказанию относительно ширины интервала:

et=μ^(Xt)wte_t = \frac{|\hat{\mu}(X_t)|}{w_t}

Это конформный аналог отношения сигнал/шум: ожидаемая доходность, деленная на свободную от распределения меру неопределенности. Мы используем его и для определения размера, и для фильтра отказа от сделки.

Фильтр обоснован принципиально. Если интервал перекрывает ноль,

lowert<0<uppert,\text{lower}_t < 0 < \text{upper}_t,

то (1α)(1-\alpha)-предсказательный интервал включает как положительные, так и отрицательные доходности — реализованная доходность вполне может иметь знак, противоположный вашему предсказанию. Определите минимальный порог преимущества θ\theta и торгуйте только при et>θe_t > \theta. Заметим, что геометрический смысл условия "ete_t достаточно велико, чтобы интервал больше не перекрывал ноль" — это в точности et>1/2e_t > 1/2 (поскольку интервал не задевает ноль, когда μ^>q^t=wt/2|\hat{\mu}| > \hat{q}_t = w_t/2). Выбирайте θ\theta на фактической шкале et=μ^/wte_t = |\hat{\mu}|/w_t через бэктест; для остатков дневной доходности ete_t обычно заметно ниже 1/21/2, так что крошечное θ\theta может пропускать почти все сделки, а большое — не пропускать ни одной. Калибруйте его под свои данные.

О "конформном Келли"

Заманчиво прикрутить конформные интервалы к доле Келли f=pb(1p)bf^* = \frac{pb - (1-p)}{b}. Но ff^* уже является полной, ограниченной долей, выведенной из вероятности выигрыша pp и отношения выплат bb; умножение ее на неограниченное отношение вроде μ^/q^\hat{\mu}/\hat{q} не имеет теоретико-решающего обоснования — оно может превысить 1 или сменить знак независимо от ff^*, и оно дважды учитывает преимущество, которое ff^* уже кодирует. Поэтому мы не представляем множитель "конформного Келли".

Если вы хотите выводить Келли из интервала, нужно действительно вывести из него pp и bb, что требует явного предположения о распределении внутри интервала (конформные интервалы намеренно ничего о нем не говорят — см. Ограничения). Например, при предполагаемой форме внутри интервала можно приблизить pP(доходность>0)p \approx \mathbb{P}(\text{доходность} > 0) и отношение выплат из геометрии интервала — но громко обозначайте это предположение, потому что оно вновь вводит ровно тот параметрический выбор, которого конформное прогнозирование должно было избежать.

Честная, малопредположительная альтернатива — использовать отношение преимущества et=μ^/wte_t = |\hat{\mu}|/w_t как сжатие дробного Келли: увеличивать размер, когда ожидаемая доходность велика относительно интервала, уменьшать, когда она мала, и применять это поверх жесткого потолка — явно как эвристику, а не как "долю Келли".

Реализация на Python

Вот практическая реализация. Мы показываем и путь split/prefit, и временной путь (EnbPI), поскольку весь смысл этой статьи в том, что финансовые данные не обмениваемы.

Подготовка и данные

import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.model_selection import KFold

from mapie.regression import MapieRegressor, MapieTimeSeriesRegressor
from mapie.subsample import BlockBootstrap


def prepare_features(prices: pd.Series, lookback: int = 20) -> pd.DataFrame:
    """Create features from a price series."""
    df = pd.DataFrame()
    returns = prices.pct_change()

    for lag in range(1, lookback + 1):
        df[f"ret_lag_{lag}"] = returns.shift(lag)

    for window in [5, 10, 20]:
        df[f"ret_mean_{window}"] = returns.rolling(window).mean().shift(1)
        df[f"ret_std_{window}"] = returns.rolling(window).std().shift(1)
        df[f"ret_skew_{window}"] = returns.rolling(window).skew().shift(1)

    df["target"] = returns.shift(-1)

    return df.dropna()

Split conformal с MAPIE (prefit)

Для split/prefit conformal cv="prefit" требует method="base" (наивный split-conformal оценщик). Опция method="plus" — это оценщик CV+/jackknife+, и она несовместима с cv="prefit" — ей вместо этого нужен объект кросс-валидации. Здесь мы используем правильную комбинацию и стандартизируем определение размера по полной ширине.

def split_conformal_sizing(
    prices: pd.Series,
    alpha: float = 0.1,
    k: float = 1e-3,          # scaling constant, in width units
    max_position: float = 1.0,
    min_edge: float = 0.05,   # threshold on |pred| / width
) -> pd.DataFrame:
    """
    Position sizing using split (prefit) conformal prediction intervals.

    Sizing rule (consistent with the prose):
        edge_t = |pred_t| / width_t
        size_t = clip(k / width_t, 0, max_position)   # inverse-width
        size_t = 0 if edge_t < min_edge               # no-trade filter
    """
    df = prepare_features(prices)
    X = df.drop(columns=["target"])
    y = df["target"]

    n_train = int(len(X) * 0.6)
    n_cal = int(len(X) * 0.2)

    X_train, y_train = X.iloc[:n_train], y.iloc[:n_train]
    X_cal, y_cal = X.iloc[n_train:n_train + n_cal], y.iloc[n_train:n_train + n_cal]
    X_test, y_test = X.iloc[n_train + n_cal:], y.iloc[n_train + n_cal:]

    base_model = GradientBoostingRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42,
    )
    base_model.fit(X_train, y_train)

    mapie = MapieRegressor(estimator=base_model, cv="prefit", method="base")
    mapie.fit(X_cal, y_cal)

    y_pred, y_intervals = mapie.predict(X_test, alpha=alpha)
    lower = y_intervals[:, 0, 0]
    upper = y_intervals[:, 1, 0]
    width = upper - lower

    raw_size = k / np.where(width > 0, width, np.inf)
    position_size = np.clip(raw_size, 0.0, max_position)

    edge_ratio = np.abs(y_pred) / np.where(width > 0, width, np.inf)
    position_size = np.where(edge_ratio < min_edge, 0.0, position_size)

    position_size = position_size * np.sign(y_pred)

    return pd.DataFrame({
        "prediction": y_pred,
        "lower": lower,
        "upper": upper,
        "width": width,
        "edge_ratio": edge_ratio,
        "position_size": position_size,
        "actual": y_test.values,
    }, index=X_test.index)

Конформное прогнозирование для временных рядов с EnbPI

Поскольку доходности не обмениваемы, случайное/prefit-разбиение выше — лишь базовый ориентир. MapieTimeSeriesRegressor из MAPIE с method="enbpi" (Xu & Xie, 2021) использует блочный бутстрап и обновление остатков, спроектированные под временную зависимость. Это инструмент, который соответствует собственному аргументу статьи.

def enbpi_sizing(
    prices: pd.Series,
    alpha: float = 0.1,
    k: float = 1e-3,
    max_position: float = 1.0,
    min_edge: float = 0.05,
) -> pd.DataFrame:
    """Position sizing with EnbPI (block-bootstrap, time-series conformal)."""
    df = prepare_features(prices)
    X = df.drop(columns=["target"])
    y = df["target"]

    n_train = int(len(X) * 0.7)
    X_train, y_train = X.iloc[:n_train], y.iloc[:n_train]
    X_test, y_test = X.iloc[n_train:], y.iloc[n_train:]

    base_model = GradientBoostingRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42,
    )

    cv = BlockBootstrap(n_resamplings=30, length=20, overlapping=False, random_state=42)
    mapie_ts = MapieTimeSeriesRegressor(base_model, method="enbpi", cv=cv, agg_function="mean")
    mapie_ts.fit(X_train, y_train)

    y_pred, y_intervals = mapie_ts.predict(X_test, alpha=alpha, ensemble=True)
    lower = y_intervals[:, 0, 0]
    upper = y_intervals[:, 1, 0]
    width = upper - lower

    raw_size = k / np.where(width > 0, width, np.inf)
    position_size = np.clip(raw_size, 0.0, max_position)
    edge_ratio = np.abs(y_pred) / np.where(width > 0, width, np.inf)
    position_size = np.where(edge_ratio < min_edge, 0.0, position_size)
    position_size = position_size * np.sign(y_pred)

    return pd.DataFrame({
        "prediction": y_pred, "lower": lower, "upper": upper,
        "width": width, "edge_ratio": edge_ratio,
        "position_size": position_size, "actual": y_test.values,
    }, index=X_test.index)

Адаптивный конформный вывод (онлайн)

Для живой торговли мы реализуем настоящий ACI с нуля: поддерживаем уровень непокрытия αt\alpha_t, обновляем его аддитивно и заново выводим квантиль из множества остатков на каждом шаге. Важны две детали для конечной выборки:

  • Используйте порядковую статистику, а не интерполированный квантиль. np.quantile по умолчанию интерполирует, из-за чего значение может оказаться чуть ниже требуемого ранга и дать недопокрытие; передавайте method="higher" (эквивалентно "inverted_cdf").
  • Когда требуемый ранг превышает nn (малое nn, высокое целевое покрытие), корректный порог равен ++\infty (интервал = вся прямая), а не зажим до наибольшего остатка. Зажим тихо ломает гарантию 1αt\ge 1-\alpha_t.
class AdaptiveConformalSizer:
    """
    Online position sizing with Adaptive Conformal Inference (Gibbs & Candes,
    2021). Updates the miscoverage LEVEL alpha_t and re-derives the quantile
    from the residual set each step -- gamma is in probability units.
    """

    def __init__(self, base_model, alpha=0.1, gamma=0.02,
                 max_position=1.0, min_edge=0.05, k=1e-3):
        self.base_model = base_model
        self.alpha_target = alpha     # target miscoverage
        self.alpha_t = alpha          # adaptive miscoverage level
        self.gamma = gamma            # step size, in [0, 1] units
        self.max_position = max_position
        self.min_edge = min_edge
        self.k = k
        self.residuals = []
        self.q_hat = np.inf
        self.coverage_history = []

    @staticmethod
    def _conformal_quantile(residuals, alpha_t):
        """(1 - alpha_t) conformal quantile via the order statistic."""
        n = len(residuals)
        if n == 0:
            return np.inf
        rank = int(np.ceil((1.0 - alpha_t) * (n + 1)))
        if rank > n:                  # required order statistic does not exist
            return np.inf             # -> interval is the whole line
        level = rank / n
        return float(np.quantile(residuals, min(level, 1.0), method="higher"))

    def calibrate(self, X_cal, y_cal):
        preds = self.base_model.predict(X_cal)
        self.residuals = list(np.abs(np.asarray(y_cal) - preds))
        self.q_hat = self._conformal_quantile(self.residuals, self.alpha_t)

    def predict_and_size(self, X_t) -> dict:
        mu_hat = self.base_model.predict(np.asarray(X_t).reshape(1, -1))[0]
        lower, upper = mu_hat - self.q_hat, mu_hat + self.q_hat
        width = upper - lower                      # = 2 * q_hat

        edge_ratio = abs(mu_hat) / width if np.isfinite(width) and width > 0 else 0.0

        if edge_ratio < self.min_edge:
            size = 0.0
        else:
            size = min(self.k / width, self.max_position) if width > 0 else 0.0

        return {
            "prediction": mu_hat, "lower": lower, "upper": upper,
            "width": width, "edge_ratio": edge_ratio,
            "position_size": size * np.sign(mu_hat),
            "alpha_t": self.alpha_t, "q_hat": self.q_hat,
        }

    def update(self, X_t, y_t: float):
        """ACI update: adapt the LEVEL, then re-derive the quantile."""
        mu_hat = self.base_model.predict(np.asarray(X_t).reshape(1, -1))[0]
        residual = abs(y_t - mu_hat)

        covered = int(residual <= self.q_hat)
        err_t = 1 - covered

        self.alpha_t = float(np.clip(
            self.alpha_t + self.gamma * (self.alpha_target - err_t), 0.0, 1.0
        ))

        self.residuals.append(residual)
        self.q_hat = self._conformal_quantile(self.residuals, self.alpha_t)

        self.coverage_history.append(covered)

    @property
    def running_coverage(self) -> float:
        if not self.coverage_history:
            return float("nan")
        return float(np.mean(self.coverage_history))

Собираем вместе: цикл бэктеста

def backtest_aci_sizing(prices: pd.Series, alpha=0.1, gamma=0.02) -> pd.DataFrame:
    """Backtest position sizing with Adaptive Conformal Inference."""
    df = prepare_features(prices)
    X = df.drop(columns=["target"]).values
    y = df["target"].values
    index = df.index

    n_train = int(len(X) * 0.5)
    n_cal = int(len(X) * 0.2)

    X_train, y_train = X[:n_train], y[:n_train]
    X_cal, y_cal = X[n_train:n_train + n_cal], y[n_train:n_train + n_cal]
    X_test, y_test = X[n_train + n_cal:], y[n_train + n_cal:]
    test_index = index[n_train + n_cal:]

    model = GradientBoostingRegressor(
        n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42,
    )
    model.fit(X_train, y_train)

    sizer = AdaptiveConformalSizer(base_model=model, alpha=alpha, gamma=gamma)
    sizer.calibrate(X_cal, y_cal)

    records = []
    for i in range(len(X_test)):
        result = sizer.predict_and_size(X_test[i])
        result["actual"] = y_test[i]
        result["pnl"] = result["position_size"] * y_test[i]
        records.append(result)
        sizer.update(X_test[i], y_test[i])   # online residual + level update

    results = pd.DataFrame(records, index=test_index)
    results["cumulative_pnl"] = results["pnl"].cumsum()
    results["running_coverage"] = (
        ((results["lower"] <= results["actual"]) &
         (results["actual"] <= results["upper"])).expanding().mean()
    )
    return results

Оценка результатов

def evaluate(results: pd.DataFrame, alpha: float = 0.1):
    """Print evaluation metrics for conformal position sizing."""
    covered = ((results["actual"] >= results["lower"]) &
               (results["actual"] <= results["upper"]))

    print(f"Target coverage:      {1 - alpha:.1%}")
    print(f"Empirical coverage:   {covered.mean():.1%}")
    print(f"Mean interval width:  {results['width'].mean():.6f}")
    print(f"Median position size: {results['position_size'].abs().median():.4f}")
    print(f"Fraction no-trade:    {(results['position_size'] == 0).mean():.1%}")
    print(f"Total PnL (bps):      {results['pnl'].sum() * 10000:.1f}")
    sd = results['pnl'].std()
    sharpe = results['pnl'].mean() / sd * np.sqrt(252) if sd > 0 else float('nan')
    print(f"Sharpe (annualized):  {sharpe:.2f}")

Практические соображения

Выбор меры неконформности

Абсолютный остаток Yμ^(X)|Y - \hat{\mu}(X)| — это значение по умолчанию, но он предполагает, что предсказательный интервал должен быть симметричным вокруг точечного предсказания. Для финансовых доходностей часто разумнее асимметричные интервалы:

  • Конформизованная квантильная регрессия (CQR): обучите квантильные регрессоры на уровнях α/2\alpha/2 и 1α/21 - \alpha/2, затем конформизуйте (Romano et al., 2019). Интервалы подстраивают свою форму под локальное распределение — шире снизу во время просадок, шире сверху во время ралли. (При CQR интервал уже не симметричен, так что wtw_t — это подлинная ширина "верх минус низ"; продолжайте использовать wtw_t в качестве знаменателя везде.)
  • Нормализованные меры: Ri=Yiμ^(Xi)/σ^(Xi)R_i = |Y_i - \hat{\mu}(X_i)| / \hat{\sigma}(X_i), где σ^\hat{\sigma} — локальная оценка волатильности. Это дает интервалы, которые условно адаптивны — уже в режимах низкой волатильности, шире в режимах высокой — при сохранении маргинального покрытия.

Размер калибровочного множества

Гарантия покрытия split conformal выполняется для любого размера калибровочного множества nn, но ширина интервала уменьшается с ростом nn. При очень малых nn требуемая порядковая статистика может не существовать, и в этом случае честный порог равен ++\infty (неинформативный, но корректный интервал) — зажим до наибольшего остатка тихо аннулирует гарантию. Для практического трейдинга:

  • n100n \geq 100 калибровочных точек дает разумно узкие интервалы.
  • n500n \geq 500 предпочтительно для устойчивой оценки квантилей.
  • С ACI калибровочное множество нужно лишь для инициализации; обновления уровня в онлайн-режиме делают остальное.

Частота переобучения

Базовая модель μ^\hat{\mu} может устаревать. Два подхода:

  1. Переобучать периодически (например, ежемесячно) и заново калибровать конформный квантиль.
  2. Использовать ACI и позволить адаптивному уровню компенсировать устаревание модели — интервалы автоматически расширяются по мере роста остатков модели.

Вариант 2 проще и на удивление эффективен. Конформный слой действует как страховочная сетка: даже если модель деградирует, долгосрочная частота покрытия ACI все равно сходится к целевой.

Транзакционные издержки

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

Можно также встроить транзакционные издержки прямо в фильтр отказа от сделки:

торговать только если μ^(Xt)cost>θwt\text{торговать только если } |\hat{\mu}(X_t)| - \text{cost} > \theta \cdot w_t

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

Сравнение с традиционными методами

Свойство Гауссовский ДИ Бутстрап-ДИ Конформный ПИ
Предположение о распределении Нормальные ошибки i.i.d. + асимптотика Нет (обмениваемость)
Гарантия на конечной выборке Нет Нет Да
Работает с любой моделью Нет Да Да
Адаптируется к гетероскедастичности Нет Частично С CQR / нормализованными мерами
Справляется со сдвигом распределения Нет Нет Вариант ACI / EnbPI
Вычислительная стоимость Низкая Средняя Split: низкая; jackknife+: O(n)O(n) переобучений; full: непосильная

Бутстрап "асимптотичен" только в своей гарантии; он все равно предполагает i.i.d./обмениваемые данные и гладкость, так что его ячейка о предположении про распределение не является "без предположений". А единственный столбец "конформный" скрывает очень разные стоимости разных вариантов, что теперь и расписывает строка стоимости.

Ограничения

Конформное прогнозирование не магия. Ключевые ограничения для трейдинга:

  1. Маргинальное, а не условное покрытие. Гарантия в том, что P(Yn+1C(Xn+1))1α\mathbb{P}(Y_{n+1} \in C(X_{n+1})) \geq 1 - \alpha маргинально — усреднено по случайности и в XX, и в YY. Для конкретного условного значения X=xX = x локальное покрытие может быть выше или ниже 1α1 - \alpha. Конформизованная квантильная регрессия частично решает это.

  2. Обмениваемость — реальное требование. Для split conformal калибровочные данные и тестовая точка должны быть обмениваемы. Финансовые данные таковыми не являются. ACI и EnbPI ослабляют это до долгосрочной гарантии, но краткосрочное покрытие может отклоняться.

  3. Ширина интервала — не плотность вероятности. Конформный интервал говорит вам, куда попадет YY с вероятностью 1α1-\alpha, но он ничего не говорит о распределении внутри интервала. Он не заменяет полное предсказательное распределение — именно поэтому вывод pp и bb по Келли из него требует дополнительного, явного предположения.

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

Резюме

Конформное прогнозирование дает строгий, свободный от распределения каркас для количественной оценки неопределенности, который естественно встраивается в определение размера позиции:

  • Split conformal для статической, офлайн-калибровки с покрытием на конечной выборке.
  • Jackknife+, когда калибровочных данных мало и вы хотите использовать все наблюдения (ценой nn переобучений).
  • Адаптивный конформный вывод / EnbPI для онлайн-торговли на нестационарных рынках.
  • Определение размера позиции через обратную ширину интервала и фильтр отказа от сделки по отношению преимущества — и, если переходите к Келли, лишь после честного вывода pp и bb, а не через необоснованный множитель.

Ключевое преимущество перед параметрическими альтернативами: вам никогда не нужно задавать или проверять параметрическое предположение о распределении. Интервалы честны по построению (маргинально, при обмениваемости). Для систематического трейдера это означает на один источник модельного риска меньше — а в деле, где модельный риск экзистенциален, это важно.


Литература:

  • Papadopoulos, H., Proedrou, K., Vovk, V., Gammerman, A. (2002). Inductive confidence machines for regression. ECML.
  • Vovk, V., Gammerman, A., Shafer, G. (2005). Algorithmic Learning in a Random World. Springer.
  • Lei, J., G'Sell, M., Rinaldo, A., Tibshirani, R.J., Wasserman, L. (2018). Distribution-free predictive inference for regression. JASA.
  • Xu, C., Xie, Y. (2021). Conformal prediction interval for dynamic time-series (EnbPI). ICML.
  • Barber, R.F., Candes, E.J., Ramdas, A., Tibshirani, R.J. (2021). Predictive inference with the jackknife+. Annals of Statistics.
  • Gibbs, I., Candes, E.J. (2021). Adaptive conformal inference under distribution shift. NeurIPS.
  • Gibbs, I., Candes, E.J. (2024). Conformal inference for online prediction with arbitrary distribution shifts. JMLR.
  • Romano, Y., Patterson, E., Candes, E.J. (2019). Conformalized quantile regression. NeurIPS.
  • Cordier, T. et al. (2022). MAPIE: an open-source library for distribution-free uncertainty quantification. arXiv:2207.12274.
Дисклеймер: Информация в этой статье предоставлена исключительно в образовательных и ознакомительных целях и не является финансовым, инвестиционным или торговым советом. Торговля криптовалютами сопряжена с высоким риском убытков.

Авторы

Eugen Soloviov
Eugen Soloviov

Инженер торговых систем

Разработка торговых ботов с 2017 года: межбиржевой арбитраж (подключал до 30 бирж), парный арбитраж на коинтеграции между спотом и фьючерсами, скальпинг, фронтраннинг, торговля по новостям, сентиментный анализ, трендовые алгоритмы, а также алгоритмы управления и балансировки портфелей. Делает выставление ордеров до 1 мс, warehouse для big data, бэктестинг-движки, AI-агентов и интерфейсы для ботов (в т.ч. open-source profitmaker.cc). Стек: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, архитектура.

Newsletter

Будьте в курсе событий

Подпишитесь на нашу рассылку, чтобы получать эксклюзивную аналитику по AI-трейдингу и обновления платформы.

Мы уважаем вашу конфиденциальность. Отписаться можно в любой момент.