06. 질문이 만든 감옥

정답률 60%의 벽은 질문이 만든 감옥이었다

오르냐 내리냐, 그게 진짜 질문인가?

v2.1부터 v2.5까지, 내 모든 모델은 하나의 질문에 답하고 있었다.

"N봉 후에 가격이 오를까, 내릴까?"

이진 분류. 0 아니면 1. 단순하고 직관적인 프레이밍이었다. 미래 수익률을 계산하고, 양수면 상승, 음수면 하락으로 레이블을 붙였다. 모델은 이 레이블을 예측하도록 학습되었고, 정확도는 60%대에서 벽에 부딪혔다.

하지만 v2.5에 리스크 관리를 적용하면서 -62.83%라는 처참한 결과를 보고 나서, 나는 근본적인 의문이 들기 시작했다. "모델이 가격 방향을 맞혀도 왜 돈을 잃는 걸까?"

답은 간단했다. 내가 묻는 질문과 실제로 돈을 버는 조건이 달랐기 때문이다.

실제 선물 거래에서 수익과 손실은 "N봉 후 가격"이 아니라 "TP(익절선)에 먼저 닿느냐, SL(손절선)에 먼저 닿느냐"로 결정된다. 모델이 "1시간 후에 상승"이라고 맞춰도, 그 사이에 SL에 걸리면 손절이다. 학습 타겟과 실전 수익 조건 사이에 구조적 괴리가 있었다.

Triple-Barrier: 질문을 재정의하다

Triple-Barrier 레이블링은 이 괴리를 정면으로 해결한다. "오르냐 내리냐"가 아니라, "어떤 배리어에 먼저 도달하느냐"를 묻는다. 이것을 수학적으로 정의하면 정지 시간(Stopping Time) 개념이 필요하다.

가격 과정 $\{P_t\}$에 대해, 현재 시점 $t_0$에서의 진입가격을 $P_0 = P_{t_0}$로 놓자. 세 가지 배리어에 대한 정지 시간을 다음과 같이 정의한다:

상방 배리어(Take-Profit) 정지 시간:

$$\tau_{TP} = \inf\{t > t_0 : P_t \geq P_0(1 + \delta)\}$$

하방 배리어(Stop-Loss) 정지 시간:

$$\tau_{SL} = \inf\{t > t_0 : P_t \leq P_0(1 - \delta)\}$$

시간 배리어(Maximum Holding Period):

$$\tau_{max} = t_0 + T$$

여기서 $\delta$는 배리어 폭(우리 모델에서 0.01, 즉 1%), $T$는 최대 보유 시간(24봉 = 2시간)이다. $\inf$는 하한(infimum)으로, 조건을 처음 만족하는 시점을 의미한다.

최종 정지 시간 $\tau$는 세 배리어 중 가장 먼저 도달하는 것으로 결정된다:

$$\tau = \min(\tau_{TP}, \tau_{SL}, \tau_{max})$$

마라톤 경기를 떠올려보자. 결승선(TP)에 먼저 도착하면 완주 성공, 도중에 체력이 바닥나서 의무실에 실려가면(SL) 실패, 제한 시간($\tau_{max}$) 안에 아무 일도 안 일어나면 기권 처리. 어떤 "문"을 먼저 통과하느냐가 결과를 결정한다는 개념이다.

레이블은 이 $\tau$가 어떤 배리어에 해당하는지에 따라 결정된다:

$$y = \begin{cases} 1 \text{ (LONG)} & \text{if } \tau = \tau_{TP} \\ 0 \text{ (SHORT)} & \text{if } \tau = \tau_{SL} \\ 2 \text{ (NO\_TRADE)} & \text{if } \tau = \tau_{max} \end{cases}$$

이 정의가 기존 이진 분류와 근본적으로 다른 점은, 가격 경로(path)를 고려한다는 것이다. 기존 방식은 $P_{t_0 + N}$이라는 단일 미래 시점의 가격만 봤다면, Triple-Barrier는 $t_0$부터 $\tau$까지의 전체 가격 궤적에서 High와 Low를 추적한다. 이것이 실전 트레이딩의 수익/손실 결정 메커니즘과 정확히 일치한다.

# v2.5 이전: 미래 수익률 기반 (Binary)
future_return = log(Close[t+N] / Close[t])
if future_return > threshold:
    label = 1  # UP
else:
    label = 0  # DOWN

# v2.7: Triple-Barrier 방식 (3-Class)
barrier_pct = 0.01  # +/-1.0% 대칭 배리어
for i in range(1, max_hold_bars + 1):  # 24봉(2시간) 이내
    if High[t+i] >= Close[t] * (1 + barrier_pct):
        label = 1  # LONG: 상방 배리어 먼저 도달
        break
    if Low[t+i] <= Close[t] * (1 - barrier_pct):
        label = 0  # SHORT: 하방 배리어 먼저 도달
        break
else:
    label = 2  # NO_TRADE: 어디에도 도달 못함

세 가지 배리어가 있다. 상방 배리어(+1.0%), 하방 배리어(-1.0%), 그리고 시간 배리어(24봉 = 2시간). 각 시점에서 미래를 순회하며 어떤 배리어에 먼저 닿는지를 레이블로 기록한다. 이것이 "Triple-Barrier"라는 이름의 유래다.

NO_TRADE: "안 하는 것"을 학습하다

이전 모델들의 가장 큰 문제 중 하나는 모든 봉에서 반드시 방향을 선택해야 했다는 것이다. 횡보장에서도 "오름" 아니면 "내림"을 골라야 했다. 결과는 무의미한 거래의 난사.

3-Class 확률 분포와 정보 엔트로피

3-Class 분류에서 모델은 각 시점마다 세 클래스에 대한 확률 분포를 출력한다:

$$\hat{p} = (\hat{p}_{SHORT}, \hat{p}_{LONG}, \hat{p}_{NO\_TRADE}), \quad \sum_{c} \hat{p}_c = 1$$

NO_TRADE 클래스가 추가됨으로써 모델의 불확실성 표현 능력이 근본적으로 달라진다. 이것을 정보 엔트로피로 설명할 수 있다.

셰넌 엔트로피(Shannon Entropy)는 확률 분포의 불확실성을 측정한다:

$$H = -\sum_{i} p_i \log_2 p_i$$

이진 분류에서 횡보 구간을 생각해 보자. 모델이 확신이 없으면 $(0.52, 0.48)$을 출력한다. 이때 엔트로피는:

$$H_{binary} = -0.52 \log_2 0.52 - 0.48 \log_2 0.48 \approx 0.999 \text{ bits}$$

최대 엔트로피(1.0 bits)에 가깝다. 모델이 "모르겠다"고 말하고 있는 것인데, 이진 분류에서는 0.52쪽이 선택되어 거래가 실행된다. 사실상 동전 던지기에 가까운 결정으로 돈을 걸게 되는 셈이다.

객관식 시험에서 2지선다(O/X)면 모르는 문제도 하나를 찍어야 한다. 하지만 3지선다에 "모르겠음" 선택지가 있으면, 확신 없는 문제를 솔직히 넘길 수 있다. 엔트로피가 높다는 건 "찍기"에 가까운 상태라는 뜻이고, NO_TRADE는 "이건 넘기자"라는 정직한 선택지다.

반면 3-Class 분류에서 같은 횡보 구간에 대해 모델이 $(0.15, 0.10, 0.75)$를 출력하면:

$$H_{3class} = -0.15 \log_2 0.15 - 0.10 \log_2 0.10 - 0.75 \log_2 0.75 \approx 1.18 \text{ bits}$$

최대 엔트로피($\log_2 3 \approx 1.585$ bits)보다 한참 낮다. 모델이 "이건 NO_TRADE"라고 비교적 확신 있게 판단하고 있다. NO_TRADE 클래스가 불확실성의 싱크(sink) 역할을 한다. 횡보장의 불확실성이 NO_TRADE로 흡수되면서, LONG과 SHORT 예측의 순도(purity)가 올라간다.

레이블 분포가 이를 잘 보여준다.

SHORT(0):    182,479 (19.2%)
LONG(1):     169,815 (17.9%)
NO_TRADE(2): 597,251 (62.9%)

전체 데이터의 62.9%가 NO_TRADE다. 시장은 대부분의 시간 동안 의미 있는 방향성 없이 횡보한다는 뜻이다. 이전 모델은 이 62.9%의 구간에서도 강제로 방향을 선택하며 손실을 키웠던 것이다. 백테스트에서 NO_TRADE 클래스가 4,196건의 거래를 거부했다(전체 판단의 59.2%). 모델이 "이건 모르겠다"고 솔직하게 말하는 능력을 갖게 된 것이다.

대칭 배리어의 중요성

처음에는 실전 TP/SL을 그대로 배리어로 썼다. TP 1.5%, SL 0.6%. 그러자 레이블 분포가 기울었다. +1.5%에 도달하기가 -0.6%에 도달하기보다 2.5배 어려우니까, LONG 레이블이 8.1%에 불과했고 SHORT는 35.7%가 됐다. 백테스트하니 SHORT 비율이 87.1%. 한쪽으로 완전히 쏠린 모델이었다.

이 현상은 확률적으로도 자연스럽다. 랜덤워크 가정 하에서, 가격이 $+\delta$ 또는 $-\delta$에 먼저 도달할 확률은 배리어 거리에 반비례한다. 비대칭 배리어($\delta_{TP} \neq \delta_{SL}$)를 사용하면:

$$P(\text{LONG}) \approx \frac{\delta_{SL}}{\delta_{TP} + \delta_{SL}} = \frac{0.6}{1.5 + 0.6} \approx 28.6\%$$

실제 데이터는 순수 랜덤워크가 아니지만, 방향성이 뚜렷하지 않은 구간이 대부분이므로 이 근사가 레이블 불균형의 원인을 잘 설명한다.

농구 골대를 생각해보자. 왼쪽 골대는 1m 거리, 오른쪽은 2.5m 거리에 놓으면, 당연히 가까운 왼쪽에 먼저 공이 들어간다. 비대칭 배리어는 이렇게 골대 거리를 다르게 놓는 것과 같아서, 레이블이 한쪽으로 치우칠 수밖에 없다.

해결책은 학습과 실전을 분리하는 것이었다. 학습에서는 +/-1.0% 대칭 배리어로 균형 잡힌 레이블을 만들고, 실전에서는 비대칭 TP/SL을 리스크 관리 레이어에서 적용한다.

XGBoost 다중 클래스 학습: Softmax Cross-Entropy

3-Class 분류로 전환하면서 모델의 목적함수(objective function)도 달라졌다. XGBoost는 multi:softprob 목적함수를 사용한다. 이것은 소프트맥스 크로스엔트로피(softmax cross-entropy)로, 수학적 정의는 다음과 같다.

먼저 소프트맥스 함수가 raw 출력을 확률로 변환한다:

$$\hat{p}_c = \frac{e^{z_c}}{\sum_{k=1}^{3} e^{z_k}}, \quad c \in \{0, 1, 2\}$$

여기서 $z_c$는 클래스 $c$에 대한 모델의 raw 출력(logit)이다. 그런 다음 크로스엔트로피 손실이 계산된다:

$$\mathcal{L} = -\sum_{c=1}^{3} y_c \log \hat{p}_c$$

여기서 $y_c$는 원-핫 인코딩된 실제 레이블이다. 소프트맥스는 여러 선택지의 점수를 "비율"로 바꿔주는 함수인데, 교실에서 학생 3명이 손을 드는 높이(raw 점수)를 보고 "누가 가장 자신 있어 하는가"를 확률로 환산하는 것과 비슷하다. 크로스엔트로피 손실은 모델의 확률 예측이 정답과 얼마나 먼지를 재는 채점표 역할을 한다. XGBoost는 이 손실의 1차(그래디언트)와 2차(헤시안) 도함수를 이용해 트리를 구축한다:

$$g_i = \frac{\partial \mathcal{L}}{\partial z_c} = \hat{p}_c - y_c, \quad h_i = \frac{\partial^2 \mathcal{L}}{\partial z_c^2} = \hat{p}_c(1 - \hat{p}_c)$$

여기서 주목할 점은, NO_TRADE 클래스의 비율이 62.9%로 매우 높다는 것이다. 이 불균형을 처리하기 위해 역빈도 가중치(inverse frequency weighting)를 적용했다:

def _compute_class_weights(self, y):
    unique, counts = np.unique(y, return_counts=True)
    total = len(y)
    weights = {}
    for cls, cnt in zip(unique, counts):
        w = (total / (len(unique) * cnt)) ** 0.5  # 제곱근으로 완화
        weights[int(cls)] = min(w, 3.0)  # 최대 3배 제한
    return weights

제곱근($\sqrt{\cdot}$)을 취하는 이유는, 순수 역빈도 가중치가 소수 클래스를 과도하게 강조해서 노이즈까지 학습하는 것을 방지하기 위함이다. 이것은 v2.2에서 SMOTE로 배운 교훈 -- "과도한 불균형 보정은 역효과" -- 을 반영한 설계다.

진짜 독립적인 타임프레임

v2.5에서 배운 교훈을 반영해, 같은 5분봉을 세 번 읽는 가짜 멀티타임프레임이 아니라 실제로 리샘플링한 4개 타임프레임(5분/15분/1시간/4시간)의 독립 피처 67개를 구성했다. 이 피처 설계의 상세한 내용은 다음 편에서 다룬다.

모델 단순화의 역설

흥미로운 결정도 있었다. LSTM을 제거한 것이다.

v2.1부터 쭉 유지해온 LSTM + XGBoost + LightGBM 3중 앙상블에서, v2.7은 XGBoost + LightGBM 2개로 줄었다. 모델 수로 치면 v2.5의 9개(3 horizon x 3 모델)에서 2개로 축소. 직관적으로는 모델이 줄면 성능도 줄어야 할 것 같지만, 결과는 정반대였다.

tabular 데이터에서 LSTM은 XGBoost보다 항상 낮은 정확도를 보였다(57% vs 60%). 무거운 모델을 유지하면서 얻는 건 미미했고, 오히려 앙상블의 평균 품질을 끌어내리고 있었다. 올바른 타겟과 올바른 피처가 있으면, 단순한 모델로도 충분하다.

결과: 모든 지표가 뒤집혔다

초기 자본:     $10,000
최종 자본:     $631,412
총 수익률:     +6,214.12%
총 거래:       740
승률:          73.5% (544W / 196L)
Profit Factor: 5.01
MDD:           3.62%
평균 보유 시간: 104분

v2.5(+v2.6 리스크)와 비교하면 숫자가 믿기지 않을 정도다.

메트릭 v2.5 (+v2.6) v2.7 변화
수익률 -62.83% +6,214% 완전 반전
승률 32.2% 73.5% +41.3%
Profit Factor 0.38 5.01 13배
MDD N/A 3.62% --
SL 비율 67.6% 26.5% -41.1%

Profit Factor 5.01은 이길 때 번 금액의 합이 질 때 잃은 금액의 합의 5배라는 뜻이다. v2.5(+v2.6)의 PF 0.38과 비교하면, 같은 70%대 승률이 극단적으로 다른 결과를 만들어냈다. 승률만 보면 안 되는 이유가 여기 있다.

13개월 연속 양수 수익. LONG 313건(승률 77%), SHORT 427건(승률 71%). 양방향 모두 수익. 이전 버전에서 SHORT에만 쏠리던 편향도 해결됐다. MDD 3.62%는 이전 버전들의 40~50%와는 차원이 다른 안정성이다.

NO_TRADE 클래스가 4,196건의 거래를 거부했다(전체 판단의 59.2%). 모델이 "이건 모르겠다"고 솔직하게 말하는 능력을 갖게 된 것이다.

핵심 교훈

v2.7에서 바뀐 건 모델 아키텍처가 아니다. XGBoost와 LightGBM은 이전과 같은 알고리즘이다. 피처 엔지니어링도 물론 개선했지만, 가장 결정적인 변화는 "무엇을 예측하는가"를 바꾼 것이다.

  • v2.1~v2.5: "N봉 후 가격 방향은?" -> 정확도 60%대에서 정체
  • v2.7: "어떤 배리어에 먼저 도달하는가?" -> 정확도 88%, 승률 73.5%

같은 데이터, 비슷한 모델이라도 학습 타겟이 달라지면 성능이 극적으로 변한다. 나는 반년 가까이 모델 구조를 이리저리 바꾸고, SMOTE를 넣었다 빼고, 피처를 추가하고 삭제하면서 60%의 벽을 넘지 못했다. 그런데 질문 자체를 바꾸니 벽이 사라졌다.

머신러닝의 관점에서, 이것은 레이블 설계(label design)가 모델 선택(model selection)이나 하이퍼파라미터 튜닝보다 훨씬 중요할 수 있다는 교훈이다. $\hat{f}: X \rightarrow Y$에서 $f$를 아무리 정교하게 만들어도, $Y$의 정의가 잘못되면 소용없다. $Y$를 바로잡는 것이 최우선이다.

What을 바꾸는 것이 How를 바꾸는 것보다 중요하다.

이건 머신러닝에만 국한되는 교훈이 아닌 것 같다. 문제를 열심히 푸는 것보다, 올바른 문제를 정의하는 것이 더 중요하다. v2.7은 내게 그걸 피부로 느끼게 해준 버전이다.

물론 백테스트 6,214%가 실전에서 그대로 나올 거라는 환상은 없다. 슬리피지, 유동성, 시장 미시구조의 변화 -- 실전에서는 수많은 감가 요인이 있다. 하지만 적어도 방향은 맞았다. 다음 편에서는 이 결과를 만든 67개 피처의 설계 철학을 하나씩 해부한다.