시장중립10분 분량

월가 필살기 페어트레이딩 돌려봤다 — 승률 60%인데 돈이 녹는 이유

상관계수와 공적분으로 페어를 찾고 칼만 필터로 헷지하는 시장중립 전략을 백테스트로 돌려봤습니다. 높은 승률에도 불구하고 수수료와 손익비 구조 때문에 왜 계좌가 녹는지 그 기록입니다.

유튜브에서 시장 방향과 상관없이 수익을 내는 전략으로 페어 트레이딩을 다뤘습니다. 결론부터 말하면, 백테스트 그래프는 그럴싸했지만 막상 돌려보니 돈이 녹았습니다. 이 글은 그 정리본에 영상에는 안 넣은 최소 실행 코드를 붙인 것입니다. 제 수익률은 여기에 옮겨 적지 않겠습니다. 제가 유리한 구간만 고르게 되니까요. 대신 승률은 높은데 왜 계좌는 우하향했는지 그 구조를 추적해 봤습니다.

시장중립이라는 말은 공짜 점심이 아니다

페어 트레이딩의 아이디어는 간단합니다. ETH와 ETC처럼 원래 같이 움직이던 두 자산의 가격이 일시적으로 벌어졌을 때, 비싸진 쪽을 팔고(숏) 싸진 쪽을 사서(롱) 그 차이가 다시 좁혀지면 청산해 차익을 얻는 전략입니다. 시장 전체가 오르든 내리든 두 자산의 '상대적 가격'에만 베팅하기 때문에 '시장중립'이라고 부릅니다. 방향을 예측할 필요가 없다는 점에서 아주 매력적으로 들리죠.

그런데 이 전략의 모든 것은 "원래 같이 움직이던"이라는 가정이 진짜일 때만 성립합니다. 그냥 우연히 지난 6개월간 같이 우상향한 두 코인을 묶으면, 그 관계는 언제든 깨질 수 있습니다. 관계가 깨진다는 건 벌어진 격차가 영원히 돌아오지 않는다는 뜻이고, 제 계좌는 그대로 박살 나는 겁니다. 그래서 이 전략의 성패는 '진짜 관계'를 어떻게 찾아내느냐에 달려 있습니다.

진짜 '목줄'은 상관계수가 아니라 공적분이다

흔히 쓰는 상관계수는 함정입니다. 두 코인 가격이 그냥 같이 오르기만 해도 0.9 넘게 찍히거든요. 이건 진짜 관계가 아니라 같은 추세를 공유했을 뿐인 '허상 관계'일 가능성이 높습니다. 진짜 관계, 즉 한 놈이 멀리 도망가면 다른 한 놈이 끌어당기는 '목줄'이 있는지를 통계적으로 검증하는 방법이 바로 공적분(Cointegration)입니다.

원리는 이렇습니다. 두 코인 가격(y와 x)을 회귀분석해서(y = α + βx + e) 그 잔차(e, 오차)를 뽑아냅니다. 이 잔차가 바로 두 코인 간의 스프레드, 즉 벌어진 정도입니다. 만약 두 코인에 목줄이 있다면 이 스프레드는 평균값 0 주변을 계속 맴돌아야 합니다. 즉, 스프레드가 안정적 시계열(Stationary Series)이어야 합니다. 이걸 ADF(Augmented Dickey-Fuller) 검정으로 확인하는 겁니다. 진짜 관계는 상관계수가 아니라 공적분입니다.

// ---- 공적분(목줄) 판정용 도우미 ----
// y 를 x 로 회귀 -> 그 잔차(스프레드)에 ADF 걸어서 t값 리턴.
// 이 t값이 임계값(-3.34쯤)보다 더 아래(더 음수)면 목줄 있음 판정.
public static double EngleGranger(double[] x, double[] y)
{
    double a, b;
    double[] resid;
    Ols(x, y, out a, out b, out resid);

    // 잔차(스프레드)가 평균으로 회귀하는 성질이 있는지 검사한다.
    // 이게 페어 트레이딩의 심장이다.
    // 통계적으로 유의미하게 회귀하지 않으면(목줄 없으면), 그 페어는 그냥 남남이다.
    return Adf(resid, 1, false);
}

// ADF 검정 (Augmented Dickey-Fuller)
// 시계열이 평균으로 되돌아오는 성질(정상성)이 있는지 확인.
// t값이 특정 임계치보다 낮으면 정상성이 있다고 본다.
public static double Adf(double[] y, int lags, bool withConst)
{
    // (내부 구현은 회귀분석이라 복잡해서 생략)
    // ...
    // 핵심: 회귀계수가 통계적으로 유의미한 음수인지 확인하는 과정
    double tstat = coef[gIdx] / se[gIdx];
    return tstat;
}

헷지 비율은 칼만 필터로 실시간 보정한다

목줄이 있는 페어를 찾았다고 끝이 아닙니다. ETH 1개를 살 때 ETC를 몇 개 팔아야 정확히 시장중립이 될까요? 이 비율(헷지 비율, 베타)은 고정된 값이 아닙니다. 시장 상황에 따라 계속 변합니다. 과거 6개월 데이터로 구한 베타 값이 다음 달에도 유효하리란 보장이 없습니다.

여기서 쓰는 게 칼만 필터입니다. 칼만 필터는 새로운 가격 데이터가 들어올 때마다 헷지 비율을 아주 조금씩, 실시간으로 보정해줍니다. 과거 데이터에 박제된 비율을 쓰는 게 아니라, 현재 시장의 반응을 보고 계속 미세조정하는 겁니다. 이렇게 하면 관계가 서서히 변하는 것에 어느 정도 대응할 수 있습니다. 물론 이것도 만능은 아닙니다. 관계가 갑자기 부러지는 것까진 못 막거든요.

아래 코드는 실제 백테스트 로직의 일부입니다. 칼만 필터로 계산된 스프레드가 z스코어(평소 출렁임의 몇 배나 벌어졌는지)를 기준으로 진입(EntryZ)과 청산(ExitZ), 손절(StopZ)을 결정합니다. 여기서 수수료(Fee)를 빼먹으면 백테스트는 무조건 우상향하는 그림만 보여주게 됩니다.

// ---------- 진입 판단 (쉬고 있을 때) ----------
if (zReady && pos == 0)
{
    // z가 +로 튐 = Y가 비쌈 -> Y 숏 / X 롱 = 스프레드 숏(-1)
    // z가 -로 튐 = Y가 쌈 -> Y 롱 / X 숏 = 스프레드 롱(+1)
    int dir = 0;
    if (z >= cfg.EntryZ) dir = -1;
    if (z <= -cfg.EntryZ) dir = +1;

    if (dir != 0)
    {
        // ... 수량 계산 후 ...
        pos = dir;

        // 진입 수수료는 비용이다. 양쪽 다리에서 두 번 나간다.
        // 수익률 계산할 때 이걸 빼먹는 실수를 정말 많이 한다.
        double entryNotional = qtyY * curY + qtyX * curX;
        realized -= cfg.Fee * entryNotional;
    }
}

// ---------- 청산 판단 (포지션 있을 때) ----------
if (zReady && pos != 0)
{
    bool doClose = false;
    if (Math.Abs(z) <= cfg.ExitZ) doClose = true; // 익절: 다시 붙었다
    if (Math.Abs(z) >= cfg.StopZ) doClose = true; // 손절: 목줄 끊어졌다

    if (doClose)
    {
        // ... 손익(pnl) 계산 후 ...

        // 청산 수수료 또 나간다. 이게 진짜 아프다.
        double exitNotional = qtyY * curY + qtyX * curX;
        pnl -= cfg.Fee * exitNotional;
        realized += pnl;

        pos = 0; // 이제 쉬자
    }
}
백테스트에서 가장 중요한 숫자는 수익률이 아니라 수수료와 손익비입니다. 진입 z스코어를 2.0에서 1.5로 낮추면 매매 횟수는 늘고 승률도 올라가는 것처럼 보입니다. 하지만 이건 착시입니다. 잦은 매매는 수수료만 누적시킬 뿐입니다. 오히려 미세한 엣지를 수수료가 다 갉아먹어 총수익은 더 나빠지는 경우가 허다합니다. 비용은 확정값이고 엣지는 추정값입니다. 제가 이 간단한 산수를 깨닫기까지 꽤 많은 돈을 수업료로 냈습니다.

왜 이게 실전에서 깨지는가

  • 수수료가 생각보다 훨씬 큽니다. 진입 시 롱/숏 양쪽에서 한 번, 청산 시 또 양쪽에서 한 번, 총 네 번의 수수료를 냅니다. 우리가 얻으려는 스프레드 차익이 이 수수료보다 커야 하는데, 대부분 그렇지 못합니다.
  • 손익비 구조가 치명적입니다. 스프레드가 다시 붙어서 얻는 익절(Win)은 폭이 제한적인데, 목줄이 끊어져서 맞는 손절(Loss)은 무한대에 가깝습니다. 승률이 60%를 넘어도 한 번의 큰 손절이 아홉 번의 작은 익절을 모두 삼켜버릴 수 있습니다.
  • 관계는 영원하지 않습니다. 과거 1년간 공적분 관계였던 페어가 다음 달에 깨지는 일은 비일비재합니다. 가장 위험한 건 깨진 페어가 아니라 '이 페어는 영원할 거야'라는 믿음입니다.
  • 유동성과 슬리피지가 빠져있습니다. 백테스트는 어제 종가에 정확히 체결됐다고 가정하지만, 실전에서는 매수/매도 호가 갭(스프레드)만큼의 슬리피지가 발생합니다. 이것도 전부 비용입니다.
  • 파라미터 과최적화의 덫에 빠지기 쉽습니다. 진입/청산 z스코어, 롤링 윈도우 같은 파라미터를 과거 데이터에 가장 잘 맞는 값으로 설정하면, 그건 미래에 통하지 않는 '정답지'를 만드는 것과 같습니다.
  • 동시 체결이 불가능합니다. 롱 포지션과 숏 포지션을 원자적으로 동시에 체결할 수 없습니다. 한쪽 다리만 체결되고 다른 쪽이 실패하면, 저는 시장중립 포지션이 아니라 방향성에 노출된 위험한 포지션을 떠안게 됩니다.

이 글과 첨부 코드는 전략 구조를 설명하려고 만든 교육·연구 자료입니다. 특정 종목이나 매매를 권하지 않습니다.

첨부파일

영상에 사용된 소스코드와 설명서입니다. 로그인 없이 받을 수 있습니다.

페어트레이딩 백테스터 전체 소스페어트레이딩.zip · 1.9 MB