07. 67개의 렌즈, 하나의 시장

67개의 렌즈, 하나의 시장

모델보다 중요한 것

솔직히 고백하면, 나는 꽤 오랫동안 모델 아키텍처에 집착했다. LSTM을 쌓고, Transformer 레이어를 얹고, XGBoost와 LightGBM을 앙상블하면 성능이 올라갈 거라고 믿었다. v2.1부터 v2.5까지, 모델 구조를 바꿔가며 정확도 60%대에서 발버둥 쳤다.

그런데 v2.7에서 피처 엔지니어링을 전면 재설계하자 정확도가 88%로 뛰었다. 모델은 오히려 단순해졌다. LSTM을 빼고 XGBoost + LightGBM만 남겼는데도.

"어떻게 예측하는가"보다 "무엇을 먹이는가"가 더 중요하다.

이건 교과서에서 백 번 읽은 말이지만, 직접 겪기 전까지는 와닿지 않았다. 머신러닝 문헌에서도 반복적으로 강조되는 사실이 있다. 대부분의 모델 성능 개선은 알고리즘 변경이 아니라 피처 설계에서 나온다는 것이다. Andrew Ng의 표현을 빌리면, "Applied ML is basically feature engineering." 나는 이제 이 말에 100% 동의한다.


4개 타임프레임, 67개 피처

v2.7의 핵심 철학은 독립적인 다중 타임프레임이다. 5분봉 하나만 보는 게 아니라, 15분/1시간/4시간봉을 각각 실제로 리샘플링해서 독립적인 지표를 계산한다.

timeframes = {
    '5min':  df,                                    # 원본 (빠른 모멘텀)
    '15min': df.resample('15T').agg(ohlcv_rules),  # 3봉 집계
    '1h':    df.resample('1H').agg(ohlcv_rules),   # 12봉 집계
    '4h':    df.resample('4H').agg(ohlcv_rules),   # 48봉 집계
}

지표 수식 해부: 왜 이 공식인가

피처를 나열하기 전에, 각 기술적 지표가 수학적으로 무엇을 의미하는지 짚고 넘어가고 싶다. 코드에서 ta.RSI(close, 14)를 호출하는 건 쉽지만, 이 숫자가 왜 의미 있는지를 이해하지 못하면 피처 설계에서 올바른 판단을 내릴 수 없다.

RSI (Relative Strength Index)

RSI는 최근 $n$기간 동안 상승 폭과 하락 폭의 비율을 0~100 스케일로 정규화한 모멘텀 오실레이터다.

$$RS = \frac{SMA(U, n)}{SMA(D, n)}$$

$$RSI = 100 - \frac{100}{1 + RS}$$

여기서 $U$는 가격이 상승한 봉의 상승폭(하락 봉에서는 0), $D$는 가격이 하락한 봉의 하락폭(상승 봉에서는 0)이다. $SMA(U, n)$은 최근 $n$봉 동안의 평균 상승폭이다.

이 수식의 핵심은 비율(RS)에 있다. 평균 상승폭이 평균 하락폭보다 크면 RS > 1이 되고, RSI는 50 이상이 된다. 이것을 0~100 범위로 매핑한 이유는 서로 다른 가격대의 자산을 동일 스케일로 비교하기 위함이다.

RSI는 줄다리기 점수판과 비슷하다. 최근 며칠간 "상승 팀"이 얼마나 세게 당겼는지와 "하락 팀"이 얼마나 세게 당겼는지를 비율로 환산한 것이다. RSI 70이면 상승 팀이 압도적으로 이기고 있다는 뜻이고, 30이면 하락 팀이 밀어붙이고 있다는 뜻이다. BTC가 $60,000이든 $30,000이든, RSI 70이면 "최근에 상승이 우세했다"는 동일한 의미를 전달한다.

내가 5분봉에서 RSI-14(70분) 대신 RSI-3(15분), RSI-7(35분)을 사용한 이유가 여기에 있다. $n$이 작을수록 RSI는 최근 변화에 더 민감하게 반응한다. 스캘핑에서 70분 전의 추세는 이미 "오래된 뉴스"다.

# 실제 구현
delta = series.diff()
gain = delta.where(delta > 0, 0.0).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0.0)).rolling(window=period).mean()
rs = gain / (loss + 1e-10)
rsi = 100 - (100 / (1 + rs))

EMA (Exponential Moving Average)

단순이동평균(SMA)은 모든 데이터 포인트에 동일한 가중치를 부여한다. 하지만 시장에서는 최근 데이터가 더 중요하다. 지수이동평균(EMA)은 이 직관을 수학적으로 구현한다.

$$EMA_t = \alpha \cdot P_t + (1 - \alpha) \cdot EMA_{t-1}$$

여기서 평활 계수 $\alpha = \frac{2}{n+1}$이고, $n$은 기간(span)이다. 이 재귀적 구조가 핵심인데, 현재 EMA는 현재 가격과 이전 EMA의 가중 평균이다. $\alpha$가 클수록(즉, $n$이 작을수록) 최근 가격에 더 큰 가중치를 준다.

EMA-3의 경우 $\alpha = 0.5$로, 현재 가격과 이전 EMA를 50:50으로 반영한다. EMA-10은 $\alpha \approx 0.18$로, 현재 가격의 영향이 작다. 내가 EMA-3/7/10의 크로스오버를 피처로 사용한 이유는, 서로 다른 감응 속도를 가진 평균선의 교차가 추세 전환 시점을 포착하기 때문이다.

# EMA 크로스 차이 - 정규화된 형태
ema3 = ema(close, 3)
ema7 = ema(close, 7)
result['EMA_3_7_Diff'] = (ema3 - ema7) / (close + 1e-10)
result['EMA_Cross_3_7'] = (ema3 > ema7).astype(float)

EMA_3_7_Diff를 종가로 나누는 것에 주목하자. 이 정규화 덕분에 BTC가 $30,000일 때와 $60,000일 때의 EMA 차이를 동일한 스케일로 비교할 수 있다.

MACD (Moving Average Convergence Divergence)

MACD는 두 EMA의 차이를 이용한 추세 추종 지표다.

$$MACD = EMA_{12} - EMA_{26}$$

$$\text{Signal Line} = EMA_9(MACD)$$

$$\text{Histogram} = MACD - \text{Signal Line}$$

MACD 자체는 단기(12기간)와 장기(26기간) 추세의 괴리를 나타내고, Signal Line은 MACD의 "추세"를 보여준다. Histogram은 MACD와 Signal의 차이, 즉 추세 가속도의 가속도에 해당한다.

자동차로 비유하면 이렇다. MACD는 "최근 속도와 평소 속도의 차이"(가속 중인가?), Signal Line은 "그 가속의 평균 흐름", Histogram은 "가속 페달을 더 밟고 있는가, 발을 떼고 있는가"에 해당한다. 히스토그램이 양수에서 음수로 전환되면 가속에서 감속으로 전환되는 순간이다.

내 시스템에서는 MACD를 그대로 쓰지 않고 ATR로 정규화했다.

$$MACD_{normalized} = \frac{MACD}{ATR}$$

왜냐하면 MACD의 절대값은 가격 수준에 의존하기 때문이다. BTC가 $60,000일 때의 MACD 100과 $30,000일 때의 MACD 100은 전혀 다른 의미다. ATR로 나누면 "현재 변동성 대비 추세의 강도"라는 의미가 되어 스케일 독립적인 비교가 가능해진다.

볼린저 밴드 (Bollinger Bands)

볼린저 밴드는 통계학의 표준편차 개념을 시장에 적용한 것이다.

$$BB_{upper} = SMA_n + k \cdot \sigma_n$$

$$BB_{lower} = SMA_n - k \cdot \sigma_n$$

$$BB_{position} = \frac{P - BB_{lower}}{BB_{upper} - BB_{lower}}$$

$$BB_{width} = \frac{BB_{upper} - BB_{lower}}{SMA_n}$$

여기서 $SMA_n$은 $n$기간 단순이동평균, $\sigma_n$은 $n$기간 표준편차, $k$는 보통 2를 사용한다. 정규분포를 가정하면, 가격이 $\pm 2\sigma$ 밴드 안에 있을 확률은 약 95.4%다. 물론 금융 수익률은 정규분포를 따르지 않고 fat tail을 가지지만, 그럼에도 밴드를 벗어나는 것은 통계적으로 "이례적"이라는 신호가 된다.

학교 시험 성적으로 비유하면, 볼린저 밴드는 "평균 점수 +/- 표준편차 2배" 범위를 그려놓은 것이다. 반 평균이 70점이고 표준편차가 10점이면, 50~90점 밴드 안에 대부분의 학생이 들어간다. 95점을 받은 학생은 밴드를 벗어난 "이례적" 케이스인데, 가격도 마찬가지로 밴드를 뚫으면 "뭔가 평소와 다르다"는 신호다.

내가 피처로 사용한 것은 BB Position(밴드 내 위치)과 BB Width(밴드 폭)이다. Position이 0에 가까우면 하단 밴드 근처, 1에 가까우면 상단 밴드 근처다. Width가 좁아지면 변동성 축소(Squeeze)를 의미하고, 이후 큰 움직임이 나오는 경향이 있다.

ATR (Average True Range)

ATR은 변동성의 절대적 척도다. True Range는 세 가지 값 중 최대값으로 정의된다.

$$TR = \max(H_t - L_t, \; |H_t - C_{t-1}|, \; |L_t - C_{t-1}|)$$

$$ATR_n = \frac{1}{n} \sum_{i=1}^{n} TR_i$$

$H_t - L_t$는 당일 고저 범위, $|H_t - C_{t-1}|$와 $|L_t - C_{t-1}|$는 갭을 포함한 범위다. 이 세 가지를 비교하는 이유는, 장 마감 후 갭이 발생하면 당일 고저만으로는 실제 변동폭을 과소평가하기 때문이다.

체온계에 비유하면, ATR은 "하루 동안 체온이 얼마나 오르내렸는가"를 측정하는 것이다. 단순히 오늘의 최고-최저 체온만 보면 어젯밤 급격한 열이 빠진 걸 놓칠 수 있으니, 어제 저녁 체온(전일 종가)과의 차이까지 포함해서 "진짜 변동 폭"을 잡아낸다. 크립토는 24시간 거래이므로 갭이 거의 없지만, ATR의 수학적 정의 자체가 이런 상황을 커버한다.

내 시스템에서 ATR은 두 가지 형태로 사용된다. 하나는 ATR_Percent로, ATR을 현재 가격으로 나눈 비율이다.

$$ATR\% = \frac{ATR}{Close}$$

이것이 XGBoost 피처 중요도 1위(10.8%)를 차지한 피처다. 이 수치가 높으면 시장이 활발하게 움직이고 있다는 뜻이고, 낮으면 조용하다는 뜻이다. 모델은 변동성 레짐에 따라 전략을 달리해야 한다는 것을 스스로 학습한 셈이다.

ADX (Average Directional Index) / DI

ADX는 추세의 존재 여부와 강도를 측정한다. 방향은 알려주지 않고 "지금 추세가 있느냐 없느냐"만 답한다.

먼저 방향성 지표를 계산한다.

$$+DM = H_t - H_{t-1} \quad (\text{if } +DM > -DM \text{ and } +DM > 0, \text{ else } 0)$$

$$-DM = L_{t-1} - L_t \quad (\text{if } -DM > +DM \text{ and } -DM > 0, \text{ else } 0)$$

$$+DI = 100 \times \frac{SMA(+DM, n)}{ATR_n}$$

$$-DI = 100 \times \frac{SMA(-DM, n)}{ATR_n}$$

$$DX = 100 \times \frac{|+DI - (-DI)|}{+DI + (-DI)}$$

$$ADX = SMA(DX, n)$$

$+DI$는 상승 압력, $-DI$는 하락 압력을 ATR 대비 비율로 나타낸다. DX는 이 두 힘의 괴리를 0~100으로 정규화하고, ADX는 DX를 평활한 것이다. ADX > 25면 추세가 존재한다고 해석하고, < 20이면 횡보로 본다.

ADX는 풍속계와 비슷하다. 바람이 어느 방향으로 부는지는 알려주지 않지만, "지금 바람이 세게 불고 있는지 잔잔한지"를 측정한다. ADX가 높으면 강한 바람(추세)이 불고 있으니 바람을 타고 가면 되고, 낮으면 무풍(횡보)이니 돛을 접고 기다려야 한다.

나는 1시간봉에서만 ADX를 계산했다. 5분봉에서 ADX를 계산하면 노이즈가 너무 심해서 의미 있는 추세 판단이 어렵다. 1시간봉의 ADX가 높으면 큰 추세가 있다는 뜻이고, 이때 5분봉의 빠른 모멘텀 신호를 신뢰할 수 있다.

OBV (On-Balance Volume)

OBV는 거래량의 누적 합으로 자금 흐름을 추적한다.

$$OBV_t = OBV_{t-1} + \begin{cases} V_t & \text{if } C_t > C_{t-1} \\ -V_t & \text{if } C_t < C_{t-1} \\ 0 & \text{if } C_t = C_{t-1} \end{cases}$$

가격이 상승한 날의 거래량은 더하고, 하락한 날은 빼는 단순한 논리다. 이 누적값이 가격과 같은 방향으로 움직이면 추세의 신뢰도가 높고, 반대로 움직이면 다이버전스(괴리)가 발생한 것이다.

나는 OBV 자체보다 OBV Ratio(OBV를 10봉 이동평균으로 나눈 비율)를 사용했다. OBV의 절대값은 시간이 지나면서 계속 커지므로, 비율로 변환해야 최근의 거래량 흐름 변화를 포착할 수 있다.


5분봉: 빠른 모멘텀 - 27개

5분봉은 스캘핑의 핵심이다. 여기서 중요한 건 빠른 감응이다. RSI-14(70분)은 5분봉 트레이딩에는 너무 느리다. 그래서 RSI-3(15분), RSI-7(35분)처럼 초단기 지표를 설계했다.

  • 초단기 수익률: Return 1bar(5분), 3bar(15분), 6bar(30분)
  • 빠른 RSI: RSI-3, RSI-7, RSI-14
  • EMA 크로스: EMA-3/7/10의 크로스오버와 차이값
  • 볼륨 스파이크: 3봉/6봉 평균 대비 현재 볼륨 비율
  • 연속 패턴: 연속 양봉/음봉 수
  • 가격 위치: 6봉/12봉 범위 내 현재 가격 위치
  • 변동성: 로그수익률 기반 5봉/10봉 변동성
  • ATR%: 가격 대비 ATR (변동성의 절대 척도)
  • MACD 정규화: MACD를 ATR로 나눠서 스케일 독립적으로

로그수익률 기반 변동성에 대해 한마디 하고 싶다. 단순 수익률 $r_t = \frac{P_t - P_{t-1}}{P_{t-1}}$ 대신 로그수익률 $r_t^{log} = \ln\frac{P_t}{P_{t-1}}$를 사용하는 이유는 정상성(stationarity) 때문이다. 로그수익률은 평균과 분산이 시간에 따라 상대적으로 안정적이며, 복리 수익률을 단순 합산할 수 있다는 장점이 있다. $\sum_{t=1}^{n} r_t^{log} = \ln\frac{P_n}{P_0}$. 단순 수익률은 이 성질이 성립하지 않는다.

15분봉: 중기 확인 - 12개

15분봉은 5분봉의 노이즈를 걸러주는 필터 역할이다. 여기서는 좀 더 전통적인 지표를 사용한다.

  • RSI-14, MACD/Signal/Histogram
  • 볼린저 밴드: Position(밴드 내 위치)과 Width(밴드 폭)
  • 볼륨 비율, ATR%
  • 수익률: 1봉/3봉 변화율
  • 추세: MA20 대비 가격 위치

1시간봉: 추세 판단 - 14개

1시간봉부터는 진짜 "추세"가 보이기 시작한다. 여기에 ADX(추세 강도)와 OBV(거래량 누적)를 추가했다.

  • 15분봉의 기본 지표 12개 + ADX, OBV Ratio
  • ADX가 높으면 추세가 강하다는 뜻이고, OBV가 가격과 같은 방향이면 추세 신뢰도가 높다

4시간봉: 장기 방향 - 14개

4시간봉은 "큰 그림"을 보여준다. 하루에 6개밖에 안 나오는 봉이니까, 여기서 나오는 신호는 무겁다.

  • 기본 지표 12개 + MA50 거리 + 추세 방향(5봉 변화 부호)
  • tf4h_MA50_Dist: 50봉(약 8일) 이동평균과의 거리. 장기 지지/저항 역할
  • tf4h_Trend_Dir: 최근 20시간 동안의 방향. +1이면 상승 추세, -1이면 하락 추세

XGBoost가 말해준 것: 피처 중요도

모델을 학습시킨 후, XGBoost의 피처 중요도를 뽑아봤다. 결과가 꽤 인상적이었다.

ATR_Percent:       10.8%  ████████████████████
tf4h_Return_1:      9.6%  ██████████████████
tf4h_Volume_Ratio:  4.0%  ████████
MACD_Normalized:    3.9%  ███████
tf1h_Return_1:      3.8%  ███████
EMA_3_10_Diff:      3.3%  ██████
tf4h_ATR_Pct:       3.1%  ██████
tf4h_Return_3:      2.9%  █████

Top 20 피처 중 12개(60%)가 다중 타임프레임 피처였다. 특히 놀라운 건 tf4h_Return_1(4시간 수익률)이 2위를 차지한 것이다. 5분봉으로 트레이딩하는데, 4시간 추세가 그만큼 중요하다는 뜻이다.

1위인 ATR_Percent는 "지금 시장이 얼마나 움직이고 있는가"를 나타낸다. 변동성이 높을 때와 낮을 때 전략이 달라야 한다는 걸 모델이 스스로 학습한 셈이다.

피처 중요도의 수학: Gini Impurity 기반 Information Gain

XGBoost의 피처 중요도가 어떻게 계산되는지 이해하면, 이 숫자를 더 깊이 해석할 수 있다. XGBoost는 트리를 분할할 때 Gini Impurity 감소량(또는 information gain)을 기준으로 최적의 피처와 분할점을 선택한다.

Gini Impurity는 클래스 불순도를 측정한다.

$$G = 1 - \sum_{k=1}^{K} p_k^2$$

여기서 $p_k$는 노드 내에서 클래스 $k$의 비율이고, $K$는 클래스 수(우리의 경우 LONG, SHORT, NO_TRADE 3개)다. $G = 0$이면 노드가 완전히 순수하고(한 클래스만 포함), $G$가 클수록 혼합되어 있다.

과일 바구니를 떠올려보자. 사과만 담긴 바구니는 Gini = 0(완전 순수), 사과-배-귤이 3분의 1씩 섞여 있으면 Gini가 높다(잡다한 바구니). 트리가 분할할 때마다 "이 기준으로 나누면 바구니가 더 깨끗해지는가?"를 묻는 것이고, 피처 중요도가 높다는 건 "이 기준이 바구니를 가장 깔끔하게 정리해준다"는 뜻이다.

피처 $f$로 노드를 분할했을 때의 정보 이득(Information Gain)은:

$$\Delta G = G_{parent} - \sum_{j=1}^{J} \frac{n_j}{n} G_j$$

여기서 $n_j$는 자식 노드 $j$의 샘플 수, $n$은 부모 노드의 샘플 수, $G_j$는 자식 노드의 Gini Impurity다. 피처 중요도는 해당 피처가 관여한 모든 분할에서의 $\Delta G$를 합산한 것이다.

ATR_Percent가 10.8%의 중요도를 가진다는 것은, 트리 전체에서 ATR_Percent를 기준으로 분할했을 때 얻은 정보 이득 총합이 전체의 10.8%라는 뜻이다. 모델이 결정을 내리는 데 있어 "현재 변동성 레벨"이라는 정보가 가장 많은 불확실성을 해소해준다는 의미다.


v2.5의 실수를 되짚다

5편에서 분석했듯이, v2.5의 "다중 타임프레임"은 같은 5분봉 피처 42개를 세 모델이 공유하는 가짜 독립성이었다. 신호 일치율 94%가 그 증거였다. v2.7에서 진짜 독립적인 리샘플링 피처를 구성하자, 피처 중요도 Top 20 중 12개(60%)가 다중 타임프레임 피처로 채워졌다. 같은 시야를 정교하게 파는 것보다, 다른 시야를 조합하는 것이 효과적이었다.


merge의 고통

다중 타임프레임 피처를 만드는 건 개념적으로는 간단하다. 하지만 실제 구현에서는 인덱스 정렬 문제가 지옥이었다. 1시간봉 피처를 5분봉에 붙일 때, 미래 데이터가 새어들어가면 안 된다.

pd.merge_asof를 사용해서 "가장 최근의 상위 타임프레임 값"을 5분봉에 할당했다. 예를 들어 10:15의 5분봉에는 10:00의 1시간봉 값이 들어간다. 11:00의 값이 아니라. direction='backward' 파라미터가 이것을 보장한다.

result = pd.merge_asof(
    result.sort_values('Timestamp'),
    tf_features.sort_values('Timestamp'),
    on='Timestamp',
    direction='backward'  # 항상 과거의 가장 최근 값만 사용
)

이 부분에서 한 번 버그가 있었는데, forward-fill로 미래 값이 들어갔다가 백테스트 결과가 비현실적으로 좋게 나온 적이 있다. look-ahead bias의 무서움을 체감한 순간이었다.


배운 것

67개 피처를 설계하면서 가장 크게 배운 건, 피처 엔지니어링은 도메인 지식의 결정체라는 것이다. RSI-3을 쓸지 RSI-14를 쓸지는 "5분봉 스캘핑에서 얼마나 빠른 반응이 필요한가"라는 도메인 질문에 대한 답이다.

5분봉 피처를 100개로 늘리는 것보다, 4시간봉 피처 14개를 추가하는 게 더 나았다. 이것은 정보 이론에서 말하는 "상호 보완적 정보(complementary information)"의 개념과 맞닿아 있다. 동일한 시간 프레임에서 피처를 늘리면 중복 정보(redundancy)가 증가하지만, 다른 시간 프레임에서 가져온 피처는 진정으로 새로운 정보를 추가한다.

모든 수식에는 이유가 있고, 모든 파라미터에는 실험적 근거가 있다. 모델은 도구일 뿐이다. 좋은 재료를 넣어야 좋은 결과가 나온다. 그리고 재료의 품질은 수학적 이해에서 시작된다.