2026년 9월 3일 · 데이터 엔지니어링

존재하지 않는 분들: OHLCV 이력의 공백, 거래 중단, 거래량 0 봉

존재하지 않는 분들: OHLCV 이력의 공백, 거래 중단, 거래량 0 봉

1분 봉으로 1년을 채우면 525,600행이어야 합니다. 2025년 BTCUSDT 무기한 선물 데이터를 가져왔더니 525,557행이었습니다. 43분이 부족했고 완전성은 99.992%였습니다. 같은 거래소에서 같은 방식으로 가져온 중형 알트코인 무기한 선물 데이터는 1,247분이 빠져 99.76%였습니다. 같은 해 미국 주식 분봉 데이터는 암호화폐 데이터보다 약 427,000행 적었는데, 이는 결측이 아니라 시장이 문을 닫기 때문입니다.

이 숫자 중 3개는 별로 흥미롭지 않습니다. 천 마디 말을 들일 가치가 있는 숫자는 43입니다.

525,600윤년이 아닌 해의 1분 봉 수
0.008%2025년 BTCUSDT에서 누락된 비율
61%누락된 분 중 변동성 상위 10% 시간대에 속한 비율

데이터프레임에서 모두 공백처럼 보이는 4가지 서로 다른 상황

43분 이야기에 앞서 먼저 유형을 짚겠습니다. 대부분의 결측 처리 코드는 이미 이 단계에서 잘못되어 있기 때문입니다. 로컬 parquet에서 행 하나가 빠졌을 때, 어떤 이유로 누락됐는지는 알 수 없으며 각 상황에 맞는 대응도 다릅니다.

유형실제로 일어난 일데이터에 나타나는 방식올바른 대응
거래 없음시장은 열려 있었지만 그 분에는 아무도 스프레드를 넘어서 거래하지 않음일부 엔드포인트에서는 거래량 0 봉(OHLC가 모두 같고 trades=0), 다른 곳에서는 행 자체가 누락됨그대로 둡니다. 거래하려는 사람이 없었다는 실제 정보입니다.
거래소 장애매칭 엔진이 예정된 또는 예기치 않은 이유로 중단됨행이 없고, 때로는 여러 행이 연달아 누락됨데이터를 오래된 상태로 표시합니다. 그 구간에 걸쳐 거래하지 않습니다.
종목 거래 중단LULD 밴드 위반, 뉴스 대기, 상장폐지 공지행이 없다가 거래 재개 경매 체결가가 나타남데이터를 오래된 상태로 표시하고, 거래 재개를 불연속으로 처리합니다.
우리의 실수요청 제한에 걸린 페이지네이션, 페이지를 빠뜨린 재시도, 반복문에 생긴 시간대 경계 오류앞선 경우와 구분할 수 없는 누락 행감지해서 다시 가져옵니다. 우리가 고칠 수 있는 건 이 경우입니다.

표의 첫 번째 유형은 거래소마다 다르게 처리하는데, 바로 이 차이가 문제를 일으킵니다. Coinbase의 캔들 엔드포인트는 거래가 없는 구간을 통째로 건너뛰므로 유동성이 낮은 페어의 이력에는 실제 공백이 가득합니다. Binance의 klines는 대개 거래량 0인 합성 봉을 반환하며, 시가는 직전 거래가로 고정되어 시가=고가=저가=종가가 됩니다. 바탕에 있는 사실은 같지만 데이터 모양은 다릅니다. 전체 분 그리드에 맞춰 인덱스를 다시 지정하는 로더는 아무 설명 없이 한쪽을 다른 쪽으로 바꿔 놓습니다. 결측 보정 코드를 작성한 다음이 아니라, 작성하기 전에 거래소의 동작을 확인하세요.

알트코인에서 누락된 1,247분은 거의 전부 첫 번째 유형이었습니다. 일요일 04:00 UTC의 얕은 호가창에서 아무도 거래하지 않은 경우였죠. 귀찮긴 해도 이해하기 쉽고, 어차피 전략이 그때 거래하지 않았을 테니 피해도 거의 없습니다. 바로 그 이유로 저는 이 문제를 더 들여다보지 않고 43분으로 돌아왔습니다.

43분은 흩어져 있지 않았습니다

누락이 균일하게 발생했다면 1년간 43분은 8일 반마다 한 번씩, 외롭게 떨어진 단일 누락 43개로 나타났을 겁니다. 각각은 반올림 오차 정도겠죠. 실제로는 그렇지 않았습니다. 누락은 6개 구간에 몰려 있었습니다. 연속 19분 구간 하나, 11분 구간 하나, 4분 구간 2개, 그리고 2분씩 이어진 구간 몇 개였습니다. 우연한 누락 43개가 아니라 6번의 사건이었습니다.

그리고 이 사건들은 우리가 관심을 두는 요인에 따라 발생합니다. 거래소는 부하가 클 때 장애가 나고, 가격이 움직일 때 부하가 커집니다. 1년의 모든 시간을 실현 변동성 기준으로 구간화해 누락된 분이 어디에 있었는지 확인했더니 61%가 상위 10%에 속하는 시간대에 있었습니다. 특정 분이 누락될 무조건부 확률은 0.008%입니다. 변동성 상위 10% 시간대에 속한다는 조건을 걸면 약 0.05%로, 6배 높아지고 여기에 누락까지 군집해 나타납니다.

그러니 데이터 품질 대시보드의 완전성 지표는 엉뚱한 것을 측정하고 있습니다. 99.992%라는 수치만 보면 더는 신경 쓸 필요 없는 데이터셋처럼 보입니다. 하지만 실제로는 전략이 아무것도 하지 않는 시간대에는 데이터가 온전하고, 모든 것을 하는 시간대에는 구멍이 뚫린 시계열이라는 뜻입니다. 변동성 확장을 포착해 진입하는 모멘텀 시스템은 대표 수치가 시사하는 것보다 공백을 만날 가능성이 훨씬 크고, 거래 도중에 그 공백을 맞닥뜨립니다.

몇 줄 뒤의 결측값 채우기가 하는 일

이 글을 쓰게 된 계기가 된 실패 사례를 보겠습니다. 19분 연속 누락 구간을 가져옵니다. 일반적인 정리 방식은 전체 분 그리드에 맞춰 인덱스를 다시 지정하고, 마지막 종가를 OHLC에 앞방향 채우기로 채운 뒤 거래량은 0으로 설정하는 것입니다. 이제 시계열은 끊김이 없고 지표 계산에서도 NaN이 보이지 않습니다.

이 19개 봉은 고가 == 저가 == 종가입니다. 각 봉의 진폭은 0입니다. 거래소 장애 전 약 240 USDT였던 ATR(14)을 이 구간에서 계산하면, 거래소가 복구될 때쯤 약 34까지 떨어집니다. 구간에서 실제로 남은 봉 5개가 평균 전체를 좌우하기 때문입니다. 이제 이 값을 일반적인 size = risk_budget / ATR 방식의 변동성 조정 포지션 규모 산정기에 넣습니다. 포지션 규모가 7배 커집니다.

다음 실제 봉은 거래 재개 때의 체결가이며, 조용한 봉이 아닙니다. 이 사례에서는 장애 전 마지막 종가보다 1.8% 떨어진 가격에 시가가 형성됐습니다. 백테스트는 1.8% 가격 공백을 향해 7배 규모로 포지션을 잡았습니다. 실제로는 체결될 수 없었던 가격, 아무도 호가를 내지 않은 가격에 말입니다. 그 합성 거래 하나의 가치는 수익 곡선상 한 달치 정상적인 손익보다도 컸고, 방향은 반대였습니다. 데이터프레임을 깔끔하게 정리하려고 작성한 한 줄의 데이터 정리 코드에서 비롯된 결과였습니다.

행을 채우는 대신 버려도 해결책은 아닙니다. 같은 버그가 다른 모습으로 나타날 뿐입니다. 행을 버리면 정수 인덱스 기반의 룩백 계산이 거짓말을 합니다. 이제 "20봉 EMA"는 장애 구간을 가로질러 실제 시간으로 39분을 포함하고, 경계의 봉 간 수익률은 1.8% 전체 도약을 1분간의 움직임으로 취급하며, 봉 단위 변동성 추정치는 이를 60시그마 사건으로 읽습니다. 아무 경고도 뜨지 않습니다. 인덱스는 여전히 단조 증가합니다.

리샘플링을 거치면 문제가 보이지 않게 됩니다

대부분의 연구는 1분 봉이 아니라 집계된 데이터를 사용하고, 집계 과정에서 문제가 감춰집니다. 5분 단위로 리샘플링하면 19분 공백은 4개 봉이 됩니다. 그중 첫 번째와 마지막은 일부 분만 포함합니다. Pandas는 남아 있는 2분의 데이터로 그럴듯한 OHLC를 계산하고, 5분 전체 데이터로 만든 봉과 똑같이 표시합니다. 결과물에서는 둘을 구분할 수 없습니다.

제가 아는 가장 저렴한 해결책은 모든 리샘플링 과정에서 bars_in_window 열을 유지하고 절대 버리지 않는 것입니다. 행마다 정수 하나만 있으면 이후 단계에서 봉을 신뢰할 수 있는지 묻는 모든 질문에 답할 수 있습니다. 실행 계층에서 활용할 수 있는 같은 정보를 담은 seconds_since_last_real_print도 함께 유지합니다.

현재 적용 중인 정책

에이전트가 지금 적용하는 순서는 다음과 같습니다.

  1. 인덱스를 조용히 다시 지정하지 않습니다. 로더는 공백 명세를 출력합니다. 시작과 끝, 길이, 그리고 4가지 유형 중 어느 것에 해당하는지 기록합니다. 누락 구간이 3개 봉 미만이고 주변 봉의 거래량이 적으면 거래가 없었던 분으로 처리합니다. 거래가 활발한 시간대에 그보다 길게 누락되면 장애로 간주하고, 확인될 때까지 유지합니다.
  2. 해석하기 전에 다시 가져옵니다. 초기 공백의 절반은 페이지네이션 오류였습니다. 다른 엔드포인트나 다른 공급업체에서 데이터를 한 번 더 가져오면 네 번째 유형을 확인하고, 판단이 필요하기 전에 문제를 줄일 수 있습니다.
  3. 공백 채우기가 아니라 오래된 데이터 차단 규칙을 둡니다. 전략에 data_age 입력을 전달하고 엄격한 규칙을 적용합니다. 마지막 실제 체결이 N개 봉보다 오래됐으면 신규 진입을 금지하고, 열린 포지션은 거래 재개 시 명시적인 공백 위험 할인율을 적용해 산정한 시장가 주문으로만 정리합니다. 실제로 체결될 수 없었던 체결은 거래를 하지 않은 것보다 나쁩니다.
  4. 지표에는 허구의 값 대신 NaN을 전달합니다. 앞방향 채우기로 만든 가격은 특성 계층에 전달하지 않습니다. ATR을 계산할 수 없으면 정의되지 않은 값으로 두고, 정의되지 않았다는 것은 포지션을 취하지 않는다는 뜻입니다. 조용히 7배를 키우는 것보다 오류를 명확히 드러내는 편이 낫습니다.
  5. 공백 발생 여부에 따른 성과를 보고합니다. 배포하는 모든 백테스트에 대표 손익과 함께 장애 전후 거래를 제외한 손익을 표시합니다. 그 거래들이 성과를 떠받친다면, 그 성과는 데이터가 만들어 낸 허상입니다.

오늘 바로 할 수 있는 간단한 점검입니다. 누락된 분을 연속 구간별로 묶고 백테스트 거래 중 각 구간의 시작이나 끝에서 30분 이내에 열리거나 닫힌 거래가 몇 퍼센트인지 확인하세요. 1% 미만이면 공백이 결과에 영향을 주지 않을 가능성이 큽니다. 5% 이상이면 수익 곡선의 일부는 거래소 중단에 관한 이야기입니다.

제가 신뢰하게 된 단서는 누락 규모보다 누락의 형태입니다. 거래가 뜸한 시간대에 공백 수천 개가 흩어진 데이터셋은 대체로 괜찮습니다. 공백이 몇 군데에 빽빽하게 뭉친 데이터셋은 부하가 걸릴 때 무언가가 고장 난다는 신호입니다. 그리고 부하가 걸릴 때 고장 나는 바로 그곳이 전략이 활동하는 구간입니다.

OHLCV 공백데이터 엔지니어링백테스팅리샘플링암호화폐 선물
← 전체 글