피쳐 정규화 - EWMA 기반 Z-Score 와 분모 안정화
트레이딩 피쳐 정규화에서 rolling z-score가 표준인 이유와, EWMA를 활용해 메모리 O(1)로 구현하는 방법, 분모가 0에 가까워지는 문제의 실무적 해결법을 설명합니다.
이전 글: EWMA & EWMS - 메모리 O(1)과 Irregular Interval 처리
이 글은 금융수학 시리즈의 글입니다.
트레이딩에서 체결 금액, 호가 불균형 같은 피쳐는 절대값만으로는 큰지 작은지 알기 어렵습니다. 정규화를 통해 “평소 대비 얼마나 벗어난 값인가”로 바꾸면 판단에 도움이 됩니다. 트레이딩에서 표준인 Rolling z-score 와 EWMA의 변형을 다룹니다.
이번 글에서는 정규화에 관련된 이야기를 해보겠습니다.
먼저 용어를 정리하면, 데이터 분석 영역에서 보통 영향을 주는 쪽을 feature, 영향을 받는 쪽을 label이라고 합니다. 매매에 대한 판단이 label이고, 그 판단에 영향을 주는 요소들이 feature입니다. 트레이딩에서는 호가창 불균형, 체결, Order Flow 등이 feature가 될 수 있습니다.
예를 들어, 매수 혹은 매도 체결이 강하게 들어왔을 때 진입을 하고 싶다고 해보겠습니다. 일정 시점 동안 매수 체결 금액이 백만 원입니다. 이 숫자가 큰 건지 작은 건지 판단이 어렵습니다. 상품의 유동성에 따라, 가격 수준에 따라, 같은 상품이라도 시간대에 따라 다릅니다. 그래서 기준을 맞춰 주는걸 “정규화”라고 합니다. 기준값 (평균 혹은 중앙값)을 빼준 다음에 스케일 (표준편차 혹은 IQR)로 나눕니다.
\[\text{normalized} = \frac{x - (\text{기준값})}{(\text{스케일})}\]기준값과 스케일을 무엇으로 잡느냐에 따라 이름이 달라 집니다. 대표적으로 다음 세 가지가 있습니다.
| 이름 | 빼는 값 (기준) | 나누는 값 (스케일) |
|---|---|---|
| Z-Score | 평균 ($\mu$) | 표준편차 ($\sigma$) |
| Min-Max | 최솟값 ($x_{\min}$) | 범위 ($x_{\max} - x_{\min}$) |
| Robust | 중앙값 (median) | IQR ($Q_3 - Q_1$) |
Robust normalizer 에서 Q1, Q3는 각각 하위 25% 경계값과 상위 25% 경계값입니다. IQR(Interquartile Range)은 이 둘의 차이입니다. 위 세 가지 정규화 방식은 모두 널리 알려져 있습니다. 트레이딩에서는 어떤 걸 써야 할까요? 본격적으로 설명하기 앞서 결론부터 정리하면 다음과 같습니다.
트레이딩에서 피쳐 정규화의 default consensus는 rolling z-score입니다. 이 정규화의 변형을 사용하시면 됩니다.
쓰면 안 되는 건 min-max normalization입니다. 이상치에 취약합니다.
Robust normalizer는 좋아 보이지만, 저는 검토하지 않았습니다. 메모리 문제를 해결하기 어려워 보입니다.
Z-Score
Z-score는 가장 기본적인 정규화입니다.
\[z = \frac{x - \mu}{\sigma} \tag{1}\]$\mu$는 전체 데이터의 평균, $\sigma$는 표준편차입니다. “평균에서 표준편차 몇 개만큼 떨어져 있는가”를 나타냅니다. $z = 2$이면 평균보다 표준편차 2배만큼 큰 값입니다.
식 $(1)$에서 $\mu$와 $\sigma$는 전체 데이터에 대한 값입니다. 아마 20년치 평균과 표준편차로 트레이딩을 하고 싶은 사람은 없을 겁니다. 그래서 최근 $W$개의 데이터만 사용하는 rolling window 방식이 표준입니다.
Rolling Z-Score
최근 $W$개의 데이터에 대해 평균과 표준편차를 구하고, 그 값으로 정규화합니다.
\[\mu_{t,W} = \frac{1}{W}\sum_{i=t-W+1}^{t} x_i, \qquad \sigma_{t,W} = \sqrt{\frac{1}{W}\sum_{i=t-W+1}^{t}(x_i - \mu_{t,W})^2}\] \[z_t = \frac{x_t - \mu_{t,W}}{\sigma_{t,W}} \tag{2}\]“최근 $W$개 기준으로 현재 값이 얼마나 벗어났는가”를 나타냅니다. 시장 상황은 계속 변하므로, 전체 기간이 아닌 최근 기간 기준으로 정규화하는 것이 합리적입니다.
그런데 rolling z-score에도 메모리 문제가 있습니다. $\mu_{t,W}$와 $\sigma_{t,W}$를 계산하려면 최근 $W$개의 데이터를 저장해야 합니다. 윈도우가 밀릴 때마다 오래된 값을 빼고 새 값을 넣는 과정에서 힙 할당이 발생합니다. 이전 글에서 다룬 EWMA를 활용하면 이 문제를 해결할 수 있습니다.
EWMA 기반 Z-Score
이전 글에서 EWMA 평균을 정의했습니다.
\[\bar{\mu}_t = \alpha \, x_t + (1-\alpha)\,\bar{\mu}_{t-1}\]분산도 같은 방식으로 지수 가중 평균을 취할 수 있습니다. 분산은 “편차의 제곱의 평균”이므로, 편차의 제곱에 EWMA를 적용하면 됩니다.
\[\bar{v}_0 \mathrel{:=} 0\] \[\bar{v}_t \mathrel{:=} \alpha\,(x_t - \bar{\mu}_{t-1})^2 + (1-\alpha)\,\bar{v}_{t-1} \tag{3}\]$(x_t - \bar{\mu}{t-1})^2$은 새 관측값이 직전 평균에서 얼마나 떨어져 있는지를 나타냅니다. 갱신 전 평균 $\bar{\mu}{t-1}$을 사용하는 이유는, $\bar{\mu}_t$는 이미 $x_t$ 방향으로 끌려간 값이기 때문입니다. 끌려간 값을 기준으로 편차를 재면 분산이 실제보다 작게 추정됩니다.
EWMA 표준편차와 z-score는 다음과 같습니다.
\[\bar{\sigma}_t = \sqrt{\bar{v}_t}\] \[\tilde{z}_t = \frac{x_t - \bar{\mu}_t}{\bar{\sigma}_t} \tag{4}\]$\bar{\mu}_t$와 $\bar{v}_t$ 모두 직전 값만 있으면 갱신 가능하므로 메모리 $O(1)$입니다. Rolling z-score의 변형이지만, 윈도우 데이터를 저장할 필요가 없습니다. 이전 글에서 다룬 half-life를 적용하면 불규칙 간격에서도 동일하게 사용할 수 있습니다.
실제 업데이트 순서를 정리하면 다음과 같습니다.
\[\begin{aligned} &1.\quad d_t = x_t - \bar{\mu}_{t-1} \\ &2.\quad \bar{\mu}_t = \bar{\mu}_{t-1} + \alpha \cdot d_t \\ &3.\quad \bar{v}_t = (1-\alpha)\,\bar{v}_{t-1} + \alpha \cdot d_t^{\,2} \\ &4.\quad \bar{\sigma}_t = \sqrt{\bar{v}_t} \\ &5.\quad \tilde{z}_t = \frac{x_t - \bar{\mu}_t}{\bar{\sigma}_t} \end{aligned}\]분모 안정화
식 $(4)$에는 문제가 하나 남아 있습니다. 분모 $\bar{\sigma}_t$가 0이거나 0에 매우 가까울 수 있습니다. 예를 들어 유동성이 매우 낮은 상품의 체결 피쳐를 다룬다고 해봅시다. 오랫동안 거래가 없으면 $x_t = 0$이 계속 들어오고, EWMA 분산 $\bar{v}_t$는 점점 0에 수렴합니다. 이 상태에서 갑자기 체결 하나가 들어오면 분모는 거의 0인데 분자는 큰 값이 되어 $\tilde{z}_t$가 폭발합니다.
가장 단순한 대응은 분모에 하한을 두는 것입니다.
\[\hat{\sigma}_t = \max(\epsilon,\; \bar{\sigma}_t) \tag{5}\]그런데 $\epsilon$을 어떻게 정해야 할까요? $\epsilon$이 너무 작으면 의미가 없습니다. $\bar{\sigma}_t \approx 0$일 때 $\epsilon$도 0에 가까우면 $\tilde{z}_t$는 여전히 폭발합니다. 반대로 $\epsilon$이 너무 크면 실제 변동이 있을 때도 분모가 $\epsilon$으로 고정되어 정규화 값이 억제됩니다. 더 근본적으로, $\epsilon$의 적절한 크기가 피쳐마다 다릅니다. 체결 금액 피쳐와 호가 불균형 비율 피쳐는 스케일 자체가 다릅니다.
그래서 어떻게 해야 할까요…? 정답은 없습니다. 그냥 여러 가지 방법을 조합해서 꼬이지 않게 만드는 게 실무입니다. 제가 추천하는 방법은 다음과 같습니다.
첫째, 워밍업 카운트. 관측값이 $N$개 미만이면 정규화를 수행하지 않습니다. 데이터가 충분히 쌓이기 전에는 $\bar{\sigma}_t$ 자체를 신뢰할 수 없으므로, 정규화된 값도 신뢰할 수 없습니다.
둘째, 출력 클리핑.
\[\hat{z}_t = \operatorname{clamp}(\tilde{z}_t,\; -c,\; c) \tag{6}\]| $c = 3$ 정도면 대부분의 경우 충분합니다. 정규 분포 가정 하에서 $ | z | > 3$인 값은 전체의 0.3\% 미만이므로, 잘라내도 정보 손실이 거의 없습니다. 분모가 작아서 생기는 비정상적인 폭발을 방지하면서, 정상 범위의 값은 그대로 유지됩니다. $\epsilon$ 하한과 달리 피쳐 스케일에 독립적이라는 장점도 있습니다. |
셋째, 장기 변동성과 혼합하기. 유동성이 낮은 상품의 체결 피쳐처럼 EWMA 분산이 0에 수렴하기 쉬운 경우, 장기 변동성과 혼합하면 완화할 수 있습니다.
\[\hat{v}_t = \lambda \, \bar{v}_t + (1-\lambda) \, v_{\text{long-term}}\]$\lambda$가 작을수록 장기 변동성의 비중이 커집니다. 장기 변동성으로는 최근 1년치 표준편차 같은 값을 사용할 수 있습니다.
가중 평균 대신 하한(floor)을 거는 방식도 가능합니다.
\[\tilde{\sigma}_t = \max\!\bigl(\bar{\sigma}_t,\; \beta \, \sigma_{\text{long-term}}\bigr)\]$\beta \in (0,1]$로, 장기 변동성의 몇 퍼센트를 바닥으로 깔지 결정합니다. 가중 평균 방식은 EWMA 분산이 클 때도 장기 변동성 쪽으로 당기지만, floor 방식은 EWMA 분산이 충분히 클 때는 그대로 두고 0에 수렴할 때만 개입합니다.
Min-Max — 쓰면 안 되는 이유
Min-max 정규화는 값을 $[0, 1]$ 범위에 매핑합니다.
\[x' = \frac{x - x_{\min}}{x_{\max} - x_{\min}}\]이미지 처리에서는 널리 사용되지만, 트레이딩 피쳐에는 적합하지 않습니다. 이상치에 취약하기 때문입니다. 예를 들어 체결량을 feature로 보고 있는데, 어떤 상품에서 갑자기 대량 체결이 한 번 일어나면 $x_{\max}$가 크게 뛰고, 이후 모든 정상적인 값들이 0 근처로 압축됩니다. Rolling window를 써도 마찬가지입니다. 윈도우 안에 이상치가 하나만 있으면 나머지 값들의 분별력이 사라집니다. 더 문제는 발생한 대량 체결이 해당 윈도우를 벗어나기 전까지 정규화가 뭉개진 상태가 지속된다는 점입니다. 트레이딩에서는 이상치가 자주 발생하므로, min-max는 피쳐 정규화로 적합하지 않습니다. 이런 상황을 조금 loose하게 표현하면 금융 데이터의 분포가 fat-tail이기 때문이라고 할 수 있고, 통계적으로 좀 더 정확히 표현하면 데이터가 non-stationary하기 때문입니다. Stationary하다는 건 시간이 지나도 분포가 동일하다는 뜻입니다. 따라서 어떠한 타임 프레임에서도 평균과 분산이 동일하겠죠. 금융 데이터에서는 좀처럼 그런 경우가 없습니다. 2020년 1월과 2020년 3월 코로나 사태 당시의 금융 데이터의 분포는 완전히 다르니까요.
Robust Normalizer
Robust normalizer는 이상치에 강건한 정규화입니다.
\[r = \frac{x - \text{median}}{\text{IQR}}\]IQR(Interquartile Range)은 $Q_3 - Q_1$, 즉 상위 25\% 경계값과 하위 25\% 경계값의 차이입니다. 중앙값과 IQR 모두 이상치에 민감하지 않으므로 괜찮아 보입니다. 문제는 메모리입니다. 중앙값과 사분위수를 온라인으로 계산하려면 윈도우 내 데이터를 정렬된 상태로 유지해야 합니다. 평균이나 분산과 달리 중앙값과 사분위수 계산에는 간단한 재귀식이 없습니다. 근사 알고리즘이 존재하지만, 이전 글에서 다룬 EWMA처럼 깔끔한 $O(1)$ 해법은 아닙니다.
