리스크 인식 포지션 사이징을 위한 컨포멀 예측
모든 포지션 사이징 공식에는 불확실성에 대한 추정이 필요하다. 켈리 기준에는 승리 확률과 페이오프 비율이 필요하다(자세한 내용은 전략을 위한 켈리 기준 참고). 평균-분산 최적화에는 공분산 행렬이 필요하다. VaR에는 수익률 분포가 필요하다. 이 모든 것은 데이터 생성 과정에 대한 가정을 요구하는데, 이는 금융 시장이 일상적으로 위반하는 가정이다.
컨포멀 예측은 다른 것을 제공한다. 바로 모수적 분포 가정 없이 유한 표본 커버리지를 보장하는 예측 구간이다. 90% 커버리지를 요청하면 최소 90% 커버리지를 얻게 된다. 수익률이 정규분포든, 두꺼운 꼬리를 갖든, 비대칭이든, 이분산성을 띠든 상관없다. 유일한 요구 조건은 교환 가능성(exchangeability)이다(혹은 앞으로 보게 될 더 약한 조건들).
이 글에서는 이론, 핵심 변형, 그리고 Python으로 구현한 실용적인 포지션 사이징을 다룬다.
핵심 아이디어: 비적합도 점수(Nonconformity Scores)

컨포멀 예측은 새로운 관측치가 과거 데이터에 비해 얼마나 "이상한지"를 측정하는 방식으로 작동한다. 이 이상함은 **비적합도 점수(nonconformity score)**로 정량화된다. 이는 한 데이터 포인트가 나머지 데이터에서 나타난 패턴에 얼마나 잘 적합하지 않는지를 측정하는 임의의 함수다.
회귀(수익률 같은 연속값 예측)의 경우, 가장 단순한 비적합도 점수는 절대 잔차다.
여기서 는 임의의 점 예측기(선형 회귀, 랜덤 포레스트, 신경망 — 무엇이든 상관없다)이고 는 데이터 포인트다.
핵심 통찰은 다음과 같다. 데이터 포인트 가 교환 가능하다면, 중에서 의 순위는 위에서 균등하게 분포한다. 이것은 순전히 조합론적인 사실이며, 나 의 분포에 대한 어떤 가정도 요구하지 않는다.
이 순위 균등성으로부터, 유한 표본 커버리지를 갖는 예측 구간을 구성할 수 있다.
Split 컨포멀 예측

Split 컨포멀 예측(Papadopoulos et al., 2002; Lei et al., 2018)은 가장 실용적인 변형이다. 알고리즘은 간단하다.
1단계. 데이터를 학습 집합 과 보정 집합 로 분할한다.
2단계. 에 임의의 모델 를 적합시킨다.
3단계. 보정 집합에서 비적합도 점수를 계산한다.
4단계. 원하는 미커버리지 수준 에 대해, 를 의 경험적 분위수로 취한다. 구체적으로 이것은 번째로 작은 잔차이다(그리고 일 때마다, 즉 매우 작은 에 대해서는 이다).
5단계. 새로운 포인트 에 대한 예측 구간은 다음과 같다.
커버리지 보장
보정 데이터와 새로운 테스트 포인트의 교환 가능성 하에서:
이것은 유한 표본 보장이며, 점근적 근사가 아니다. 임의의 모델 , 임의의 데이터 분포, 임의의 표본 크기 에 대해 성립한다. 만약 가 형편없는 예측기라면, 구간이 단지 더 넓어질 뿐이다. 커버리지 보장은 여전히 성립한다.
점수에 동점(ties)이 없을 때는 상한도 존재한다. 이므로, 커버리지가 낭비스러울 만큼 보수적이지는 않다.
이것이 트레이딩에 중요한 이유
가령 선형 회귀에서 나오는 전통적인 예측 구간은 정규 오차를 가정한다. 데이터의 대부분에 대해 보정된 정규 구간은, 실제 잔차가 두꺼운 꼬리를 가질 때(예: 자유도가 몇 안 되는 스튜던트-) 꼬리를 심각하게 잘못 판단할 수 있다. 중심 질량이 정규분포보다 더 얇기 때문에, 분산을 맞춘 정규 구간은 중심 근처에서는 과대 커버리지를 하면서도 꼬리에서는 과소 커버리지를 하고, 꼬리에 맞춘 구간은 그 반대를 한다. 요점은 어떤 마법 같은 단일 숫자가 아니다. 모수적 구간의 실현 커버리지가, 당신이 검증하지 않은 분포 가정에 의존한다는 것이다.
컨포멀 예측 구간은 이를 우회한다. 모델이 불확실할 때 자동으로 넓어지며, 실제 오차 분포와 무관하게 주변 커버리지(marginal coverage)를 유지한다. 트레이더에게 이것은 다음을 의미한다.
- 구간 폭에 반비례하여 포지션 크기를 산정하면, 모델이 불확실할 때 자동으로 노출을 줄이게 된다.
- 커버리지 보장은 당신의 리스크 추정이 정직하다는 것을 의미한다. "실현 수익률의 90%가 이 구간 안에 들어갈 것"이라고 말한다면, 그 진술은 통계적으로 타당하다(교환 가능성 하에서 주변적으로).
Full 컨포멀과 Jackknife+
Split 컨포멀은 간단하지만 데이터를 낭비한다. 보정 집합을 학습에 사용할 수 없기 때문이다. 두 가지 대안이 이를 해결한다.
Full 컨포멀 예측
Full 컨포멀 예측(Vovk et al., 2005)은 모든 데이터를 학습과 보정 양쪽에 사용한다. 의 각 후보값 에 대해:
- 데이터셋에 를 추가한다.
- 증강된 데이터셋에 모델을 재적합시킨다.
- 모든 비적합도 점수를 계산한다.
- 에 대한 점수가 너무 극단적이지 않으면 를 예측 집합에 포함시킨다.
예측 집합은 다음과 같다.
여기서 는 증강된 데이터셋으로 계산한 비적합도 점수다.
Full 컨포멀은 가장 좁은 구간을 제공하지만 대부분의 모델에서 계산상 감당하기 어렵다. 격자 위의 모든 후보 에 대해 모델을 재적합시켜야 하기 때문이다. 수익률 예측의 경우, 이는 예측 한 번당 수천 번의 재적합을 의미할 수 있다.
Jackknife+ (Barber et al., 2021)
Jackknife+는 균형을 맞춘다. leave-one-out(LOO) 잔차를 사용하되, LOO 폴드 전반에 걸친 적합 모델의 변동성을 고려한다.
를 포인트 를 제외한 모든 데이터로 학습한 모델이라고 하자. 단일 절대 잔차로 LOO 비적합도 점수를 정의한다.
Jackknife+ 예측 구간은 테스트 포인트에서의 LOO 예측을 이 잔차들로 넓혀서 구성한다.
여기서 는 집합 의 번째로 작은 값을, 는 번째로 작은 값을 나타낸다. 하한은 각 LOO 예측에서 잔차를 뺀다. 상한은 그것을 더한다. 그 비대칭성이 핵심이다. 두 경계를 모두 로 붕괴시키면 하한이 예측값보다 위에 놓이게 되는데, 이는 잘못된 것이다.
Jackknife+는 최소 의 커버리지 보장을 제공한다(split 컨포멀의 보다 약간 약하다). 하지만 모든 데이터를 학습과 보정 양쪽에 사용한다. 실무에서 커버리지는 보통 에 가깝다.
제한된 데이터로 학습한 트레이딩 모델(예: 수백 개의 관측치만 있는 레짐별 모델)에서는 jackknife+가 종종 최선의 선택이다. 희소한 데이터를 보정에 희생하지 않기 때문이다. 비용은 번의 모델 재적합이다.
금융 시계열의 문제: 비교환 가능성

표준 컨포멀 보장은 교환 가능성을 요구한다. 즉 의 결합 분포가 순열에 대해 불변이어야 한다. i.i.d. 데이터의 경우 이는 자명하게 성립한다.
금융 시계열은 교환 가능하지 않다. 수익률은 다음을 보인다.
- 변동성 군집화: 고변동성 기간 뒤에 고변동성 기간이 따라온다(GARCH 효과).
- 모멘텀과 평균 회귀: 수익률 또는 제곱 수익률의 자기상관.
- 레짐 변화: 시간에 따라 분포가 이동한다(강세장 대 약세장).
순진하게 split 컨포멀을 시계열에 적용하면 — 무작위 보정 분할을 사용하면 — 시간적 구조를 위반하게 된다. 잔잔했던 2017년의 보정 점수는 변동성이 컸던 2020년의 불확실성을 반영하지 못한다. 커버리지 보장이 깨진다.
Adaptive Conformal Inference (ACI)

Gibbs와 Candes(2021, NeurIPS)는 분포 이동과 비교환 가능 데이터를 다루기 위해 **Adaptive Conformal Inference (ACI)**를 도입했다. 아이디어는 우아하다. 고정된 커버리지 수준을 사용하는 대신, 최근 구간이 실제 결과를 커버했는지에 기반하여 목표 미커버리지 수준을 온라인으로 조정하고, 각 단계에서 점수 분포로부터 분위수를 다시 도출한다.
ACI 알고리즘
ACI는 구간 폭을 직접 조정하지 않는다. 적응적 미커버리지 파라미터 를 유지하고 그것으로부터 컨포멀 분위수를 다시 계산한다. 각 시점 에서:
- 현재 잔차 집합(보정 점수에 지금까지의 실현 점수를 더한 것)의 경험적 -분위수로 컨포멀 임계값을 계산한다: .
- 특징 를 관측하고, 구간 를 생성한다.
- 실제값 를 관측하고 오류 지시자 를 계산한다.
- 수준(폭이 아님)을 갱신한다:
여기서 는 스텝 크기이고 는 목표 미커버리지다. 구간이 빗나가면() 가 줄어들고, 이는 다음 분위수를 더 높게 밀어 올려 구간을 넓힌다. 커버했다면 가 커지고 구간이 좁아진다. 결정적으로, 여기서 는 확률 단위다. 원시 수익률 단위의 임계값이 아니라 의 수준을 조정한다. 따라서 잔차가 규모이든 아니든 동일한 가 합리적으로 동작한다.
ACI의 커버리지 보장
ACI는 분포 모델에 의존하지 않는 장기 커버리지 보장을 제공한다.
가 로 클리핑되기 때문에, 분자는 곱하기 상수로 유계가 되므로 우변은 이고, 경험적 미커버리지 빈도가 로 수렴한다. 정확한 진술은 다음과 같다. ACI는 적응된 수준이 유계로 유지되는 한(클립이 이를 강제한다) 임의의(적대적인 것을 포함한) 수열에 대해 장기 경험적 미커버리지 빈도가 로 수렴함을 보장한다. 이것은 커버리지 빈도에 대한 보장이지, 구간의 정보성에 대한 보장이 아니다. 진정으로 적대적인 수열 하에서는, 커버리지 목표를 여전히 달성하면서도 구간이 정보 없이 넓어질 수 있다.
Dynamically-Tuned ACI (DtACI)
Gibbs와 Candes(2024, JMLR)는 스텝 크기 의 동적 튜닝으로 ACI를 정련했다. 를 고정하는 대신, 후보 집합 를 유지하고 전문가 집계(expert-aggregation) 규칙으로 이들을 결합하여, 최근 커버리지가 목표에 가장 가까운 를 선호한다.
이는 실무적 문제를 해결한다. 큰 는 레짐 변화에 빠르게 적응하지만 변동성 큰 구간 폭을 만든다. 작은 는 안정적이지만 적응이 느리다. DtACI는 이 둘을 자동으로 절충한다.
이것이 트레이딩에 중요한 이유
수익률 예측 모델을 사용하는 마켓 메이킹 전략을 생각해 보자. 잔잔한 시장에서는 컨포멀 구간이 좁다. 모델이 확신하므로 더 큰 포지션을 취할 수 있다. 변동성이 급등하면(실적 시즌, FOMC 발표, 지정학적 충격), ACI 수준이 적응하고 구간이 몇 시점 안에 넓어진다. 명시적인 변동성 모델이나 레짐 탐지 로직 없이도 포지션 크기가 그에 반응하여 줄어든다.
이것은 사후적 고려가 아니라, 일급 신호(first-class signal)로서의 불확실성 정량화다.
컨포멀 구간을 이용한 포지션 사이징

이제 컨포멀 예측을 구체적인 포지션 사이징과 연결해 보자. 핵심 변수는 대칭적 절대 잔차 경우에 대한 **예측 구간 반폭(half-width)**이다. 대칭 구간 에서 전체 폭은 이다. 공식과 코드를 일관되게 유지하기 위해, 이후 모든 것을 전체 폭 를 기준으로 측정한다.
역폭 사이징(Inverse-Width Sizing)
가장 단순한 접근: 구간 폭에 반비례하여 크기를 정한다.
여기서 는 당신의 리스크 예산에 맞춰 보정된 스케일링 상수다. 모델이 확신할 때(좁은 구간) 더 큰 포지션을 취한다. 불확실할 때(넓은 구간) 더 작은 포지션을 취한다.
이것은 변동성 타기팅()과 유사하지만 결정적인 차이가 있다. 컨포멀 구간 폭은 모수적 변동성 추정치가 아니라 분포 무관 불확실성 측도다. 단순한 수익률 분산이 아니라, 커버리지 보장 하에서의 예측 불확실성을 포착한다.
엣지 비율 사이징과 무거래 필터
순수한 역폭 사이징은 신호 자체의 강도를 무시한다. 자연스러운 정련은 엣지 비율(edge ratio) — 구간 폭에 대한 점 예측의 비율 — 로 스케일링하는 것이다.
이것은 신호 대 잡음비의 컨포멀 유사물이다. 즉 기대 수익률을 분포 무관 불확실성 측도로 나눈 것이다. 우리는 이것을 사이징과 무거래 필터 양쪽에 사용한다.
이 필터는 원리에 기반한다. 구간이 0을 가로지르면,
그러면 예측 구간은 양과 음의 수익률을 모두 포함한다. 즉 실현 수익률이 당신의 예측과 반대 부호를 가질 가능성이 충분히 있다. 최소 엣지 임계값 를 정의하고 일 때만 거래하라. "가 구간이 더 이상 0을 가로지르지 않을 만큼 충분히 크다"의 기하학적 내용은 정확히 임에 주목하라(일 때 구간이 0을 벗어나기 때문이다). 는 백테스팅을 통해 의 실제 스케일에서 선택하라. 일일 수익률 잔차의 경우 는 보통 보다 훨씬 작으므로, 아주 작은 는 거의 모든 거래를 허용할 수 있고 큰 는 아무것도 허용하지 않을 수 있다. 당신의 데이터에 맞춰 보정하라.
"컨포멀 켈리"에 관하여
컨포멀 구간을 켈리 비율 에 갖다 붙이고 싶은 유혹이 있다. 하지만 는 이미 승리 확률 와 페이오프 비율 로부터 도출된 완결되고 유계인 비율이다. 이것에 같은 무계 비율을 곱하는 것은 의사결정 이론적 정당성이 없다. 1을 초과하거나 와 독립적으로 부호를 뒤집을 수 있고, 가 이미 인코딩한 엣지를 이중으로 계산한다. 그래서 우리는 "컨포멀 켈리" 승수를 제시하지 않는다.
구간으로부터 켈리를 구동하고 싶다면, 실제로 그것으로부터 와 를 도출해야 한다. 이는 구간 내부의 분포에 대한 명시적 가정을 요구한다(컨포멀 구간은 의도적으로 그것에 대해 아무 말도 하지 않는다 — 한계 섹션 참고). 예를 들어 가정된 구간 내부 형태 하에서 과 구간 기하학으로부터의 페이오프 비율을 근사할 수 있다. 하지만 그 가정을 크게 표시하라. 그것은 컨포멀 예측이 피하려던 바로 그 모수적 약속을 다시 도입하기 때문이다.
정직하고 가정이 적은 대안은 엣지 비율 를 **부분 켈리 축소(fractional-Kelly shrinkage)**로 사용하는 것이다. 기대 수익률이 구간에 비해 클 때 키우고, 작을 때 줄이며, 이것을 하드 캡 위에 적용한다. "켈리 비율"로서가 아니라 명시적으로 휴리스틱으로서.
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()
MAPIE를 이용한 Split 컨포멀 (Prefit)
split/prefit 컨포멀의 경우, cv="prefit"은 method="base"(나이브 split 컨포멀 추정기)를 요구한다. 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 분할은 단지 기준선일 뿐이다. MAPIE의 MapieTimeSeriesRegressor를 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)
Adaptive Conformal Inference (온라인)
라이브 트레이딩을 위해 진짜 ACI를 처음부터 구현한다. 미커버리지 수준 를 유지하고, 가산적으로 갱신하며, 각 단계에서 잔차 집합으로부터 분위수를 다시 도출한다. 두 가지 유한 표본 세부 사항이 중요하다.
- 보간된 분위수가 아니라 **순서 통계량(order statistic)**을 사용하라.
np.quantile은 기본적으로 보간하는데, 이는 요구되는 순위보다 약간 아래로 떨어져 과소 커버리지를 일으킬 수 있다.method="higher"(동등하게"inverted_cdf")를 전달하라. - 요구되는 순위가 을 초과할 때(작은 , 높은 목표 커버리지), 올바른 임계값은 (구간 = 직선 전체)이지 가장 큰 잔차로의 클램프가 아니다. 클램핑은 보장을 조용히 깨뜨린다.
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}")
실무적 고려 사항
비적합도 점수 선택
절대 잔차 가 기본값이지만, 이는 예측 구간이 점 예측을 중심으로 대칭이어야 한다고 가정한다. 금융 수익률의 경우 비대칭 구간이 종종 더 합리적이다.
- Conformalized Quantile Regression (CQR): 수준 와 에서 분위 회귀기를 적합시킨 다음 컨포멀화한다(Romano et al., 2019). 구간이 국소 분포에 맞춰 모양을 적응시킨다. 즉 드로다운 동안에는 하방으로 더 넓어지고, 랠리 동안에는 상방으로 더 넓어진다. (CQR에서는 구간이 더 이상 대칭이 아니므로, 는 진짜 상한 빼기 하한 폭이다. 모든 곳에서 분모로 를 계속 사용하라.)
- 정규화 점수: , 여기서 는 국소 변동성 추정치다. 이는 조건부로 적응적인 구간을 만든다. 저변동성 레짐에서는 더 좁고 고변동성 레짐에서는 더 넓으면서, 주변 커버리지를 유지한다.
보정 집합 크기
split 컨포멀의 커버리지 보장은 임의의 보정 집합 크기 에 대해 성립하지만, 구간 폭은 이 커질수록 감소한다. 매우 작은 에 대해서는 요구되는 순서 통계량이 존재하지 않을 수 있는데, 이 경우 정직한 임계값은 (정보는 없지만 타당한 구간)다. 가장 큰 잔차로 클램핑하면 보장을 조용히 무효화한다. 실무적 트레이딩의 경우:
- 개의 보정 포인트는 합리적으로 좁은 구간을 제공한다.
- 안정적인 분위수 추정을 위해서는 이 선호된다.
- ACI에서 보정 집합은 초기화용일 뿐이다. 온라인 수준 갱신이 나머지를 처리한다.
재학습 빈도
기저 모델 는 낡아질 수 있다. 두 가지 접근:
- 주기적으로 재학습(예: 매월)하고 컨포멀 분위수를 재보정한다.
- ACI를 사용하고 적응적 수준이 모델의 노후화를 보상하게 둔다. 모델의 잔차가 커짐에 따라 구간이 자동으로 넓어진다.
옵션 2가 더 간단하고 놀랍도록 효과적이다. 컨포멀 계층은 안전망 역할을 한다. 모델이 열화되더라도, 장기 ACI 커버리지 빈도는 여전히 목표로 수렴한다.
거래 비용
컨포멀 구간은 유용한 방식으로 거래 비용과 상호작용한다. 구간이 넓을 때(높은 불확실성) 포지션이 작으므로 회전율이 낮다. 구간이 좁아질 때(모델이 확신할 때) 포지션이 커지지만, 모델이 옳을 가능성이 더 높으므로 회전율을 감수할 가치가 있다.
거래 비용을 무거래 필터에 직접 통합할 수도 있다.
이는 기대 순 엣지가 컨포멀 폭으로 스케일링된 임계값을 초과할 때만 거래하도록 보장한다. 다른 모든 곳과 동일한 분모를 사용한다.
전통적 방법과의 비교
| 속성 | 정규 CI | 부트스트랩 CI | 컨포멀 PI |
|---|---|---|---|
| 분포 가정 | 정규 오차 | i.i.d. + 점근 | 없음(교환 가능성) |
| 유한 표본 보장 | 아니오 | 아니오 | 예 |
| 임의의 모델과 작동 | 아니오 | 예 | 예 |
| 이분산성에 적응 | 아니오 | 부분적 | CQR / 정규화 점수로 가능 |
| 분포 이동 처리 | 아니오 | 아니오 | ACI / EnbPI 변형 |
| 계산 비용 | 낮음 | 중간 | Split: 낮음; jackknife+: 재적합; full: 감당 불가 |
부트스트랩은 그 보장에서만 "점근적"이다. 여전히 i.i.d./교환 가능 데이터와 매끄러움을 가정하므로, 그 분포 가정 칸은 "가정 무관"이 아니다. 그리고 단일 "컨포멀" 열은 변형들 사이의 매우 다른 비용을 숨기고 있는데, 이제 비용 행이 이를 구체적으로 명시한다.
한계
컨포멀 예측은 마법이 아니다. 트레이딩을 위한 핵심 한계는 다음과 같다.
-
주변 커버리지이지 조건부 커버리지가 아니다. 보장은 가 주변적으로 — 와 양쪽의 무작위성에 대해 평균하여 — 성립한다는 것이다. 특정 조건부 값 에 대해 국소 커버리지는 보다 높거나 낮을 수 있다. Conformalized quantile regression이 이를 부분적으로 해결한다.
-
교환 가능성은 실제 요구 조건이다. split 컨포멀의 경우 보정 데이터와 테스트 포인트가 교환 가능해야 한다. 금융 데이터는 그렇지 않다. ACI와 EnbPI는 이를 장기 보장으로 완화하지만, 단기 커버리지는 벗어날 수 있다.
-
구간 폭은 확률 밀도가 아니다. 컨포멀 구간은 가 확률 로 어디에 들어갈지를 알려주지만, 구간 내부의 분포에 대해서는 아무 말도 하지 않는다. 이것은 완전한 예측 분포의 대체물이 아니다. 바로 이 때문에, 그것으로부터 켈리의 와 를 도출하려면 추가적인 명시적 가정이 필요하다.
-
쓰레기가 들어가면 더 넓게 나온다. 나쁜 기저 모델은 넓은 구간을 만든다. 컨포멀 예측은 커버리지를 보장하지만 유용성을 보장하지는 않는다. 모델에 예측력이 없으면, 구간이 너무 넓어져 포지션 사이저가 결코 거래하지 않는다.
요약
컨포멀 예측은 포지션 사이징에 자연스럽게 들어맞는, 엄밀하고 분포 무관한 불확실성 정량화 프레임워크를 제공한다.
- Split 컨포멀: 유한 표본 커버리지를 갖는 정적, 오프라인 보정용.
- Jackknife+: 보정 데이터가 희소하고 모든 관측치를 사용하고 싶을 때(번의 재적합 비용을 감수하고).
- Adaptive conformal inference / EnbPI: 비정상 시장에서의 온라인 트레이딩용.
- 포지션 사이징: 역구간 폭과 엣지 비율 무거래 필터를 통해. 그리고 켈리로 간다면, 정당화되지 않은 승수를 통해서가 아니라 와 를 정직하게 도출한 후에만.
모수적 대안에 비해 핵심 이점은 다음과 같다. 모수적 분포 가정을 지정하거나 검증할 필요가 결코 없다. 구간은 구성에 의해 정직하다(교환 가능성 하에서 주변적으로). 시스템 트레이더에게 이것은 모델 리스크의 원천이 하나 줄어든다는 것을 의미한다. 그리고 모델 리스크가 존립을 좌우하는 사업에서, 그것은 중요하다.
참고 문헌:
- 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.
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.