계산이 없는 지표를 한 편이나 쓰는 이유

커밋 로그를 뒤지다가 세어봤다. 내 봇 461개 버전에서 넣었다 뺐다를 제일 많이 반복한 게이트가 거래량이었다. 두 번 죽고 한 번 살아났다. 계산이라고는 하나도 없는 지표를 두고 왜 그렇게 헤맸는지 스스로도 궁금해서, 오늘은 그 기록을 전부 꺼내놓기로 했다.

거래량은 지표 중에서 유일하게 가공이 없다. RSI도 가공이고 이동평균도 가공인데 거래량만은 거래소가 넘겨주는 원자료 그대로여서, 손댈 구석이 없으니 오해할 구석도 없을 것 같은데 실제로는 내가 다뤄본 지표 중에 제일 오해를 많이 받는 물건이었다.

개념부터. 차트 아래 막대는 보통 ‘거래량(수량)‘인데, 코인 시장에서 의미 있는 건 대체로 거래대금(수량×가격)이다. 개당 4억 원짜리 BTC의 1개와 개당 200원짜리 코인의 1개가 같은 ‘1’일 리 없으니까. 그리고 거래량 해석에는 최소 세 가지 다른 얼굴이 있다.

volume — 같은 막대의 세 가지 얼굴
① 절대 거래대금   "이 종목은 하루에 얼마나 거래되나"
     용도: 유니버스 필터 (살 수 있는 종목인가) — 종목 간 비교
② 상대 배수       "지금 이 봉의 거래량은 자기 평균의 몇 배인가"
     용도: 신호 게이트 (뭔가 일어나고 있는가) — 자기 자신과 비교
③ 시간대 프로파일  "이 시장은 언제 붐비나"
     용도: 운용 정책 (언제 진입을 허용하나) — 시장 리듬과 비교
흔한 실수: ①의 잣대로 ②의 질문에 답하는 것.
"거래대금 50억 이상만" 은 대형주 필터이지, 신호 감지가 아니다.

이 셋을 섞으면 게이트가 이상해진다. 내 봇이 정확히 그 실수를 했고 대가는 실측으로 치렀다. 아래 연대기에 다 나온다.

코드는 다섯 줄, 함정은 셋

코드는 짧다. 함정은 안 짧다.

python — 상대 배수와 거래대금, 그리고 함정들
# 거래대금 (코인 시장의 기본 단위)
value = df["volume"] * df["close"]          # 또는 거래소가 주는 quote_volume
# 상대 배수: 이 봉의 거래량 / 최근 N봉 평균
vol_mult = df["volume"] / df["volume"].rolling(20).mean()
# 함정 1: 진행 중인 봉 — 15분봉이 3분 지난 시점의 거래량은 당연히 평균 미달
#         완성봉만 쓰거나, 경과 시간으로 정규화해야 한다
# 함정 2: 자기 포함 — rolling 평균에 현재 봉을 넣으면 급증 봉이 분모를 키워
#         자기 자신의 배수를 깎아 먹는다 → shift(1) 로 직전까지의 평균과 비교
# 함정 3: 세탁 거래 — 거래소·종목에 따라 거래량 자체가 오염될 수 있다.
#         거래량 지표는 시장의 정직함을 전제로 한다는 것을 잊지 말 것

함정 1은 RSI 편부터 계속 나오는 완성봉 원칙의 거래량 버전인데, 여기서는 증상이 훨씬 심하다. 가격은 봉이 진행 중이어도 ‘현재가’라는 완결된 값을 갖는다. 거래량은 봉이 닫힐 때까지 계속 누적되는 미완성 값이다. 진행봉 거래량으로 배수를 계산하면 하루 종일 모든 봉이 ‘거래량 부족’으로 보인다.

두 번 죽고 한 번 부활한 게이트의 연대기

여기서부터 실측이다. 아래 수치는 전부 커밋 기록에 남아 있는 값이다.

history — 거래량 게이트 연대기 (커밋 기록 실측값)
── 유니버스 필터 (절대 거래대금) ──
VV131-KO  MIN_TRADE_VALUE_24H  50억 → 10억   평가 종목 ~10 → ~40-50개
VV191     유니버스 하한 400원 / 7.5억         "살 수 없는 종목을 스캔에서 제거"
── 신호 게이트 (상대 배수) : 완화의 내리막 ──
VV131→138  VOL_SMA_5_MULT  1.5 → 1.2 → 1.0 → 0.6   (신호 가뭄 해소 목적)
VV140      0.6 → 0.5  "통과율 31% → ~60% 목표"
── 1차 사망 ──
VV167     구조적 편향 지적 — 게이트가 종목을 거르는 게 아니라 체급을 차별
VV182     정밀분석에서 판별력 부족 판정 → 게이트 제거
── 부활 ──
VV197     10번째 게이트로 재설계 부활: 거량 >= 0.5x (자기 평균 대비 배수)
VV200     0.5x → 0.6x 한 칸 조임
VV201     8.9일 전수 스윕: "거량은 현행(0.6)이 정점" — 생존 확정

이 연대기에서 건질 건 값이 아니고 죽고 살아난 이유다. 초기 거래량 게이트는 ‘거래량 급증 = 좋은 신호’라는 돌파 전략 문법으로 설계됐다. 신호가 마를 때마다 임계를 깎았고(1.5→0.5), 끝내 판별력이 없다는 판정을 받고 제거됐다. 부활한 게이트는 문법이 다르다. 눌림목 전략에서 거래량이 맡은 일은 급증 감지가 아니라 죽은 종목 배제다. 자기 평균의 0.6배도 안 되는 봉에서 올라오는 건 반등보다 호가 공백일 확률이 높다. 같은 데이터, 같은 이름의 게이트인데 전략이 바뀌니 존재 이유가 통째로 바뀌었다.

제일 값진 실측은 철회 기록이었다

거래량 게이트 역사에서 내가 최고로 치는 데이터는 VV184의 철회 기록이다. 스캔로그 1,116건을 전수 시뮬레이션하다가 거래량 게이트 재도입(≥1.0x)을 검토했다. 단변량으로는 유일하게 성적이 좋았다.

VV184 — 거래량 게이트 재도입 검토와 철회 (커밋 기록)
단변량 분석: 거래량 1.0~2.0x 구간
  휩쏘 9.0% / 평균 +0.175% — 전 구간에서 유일한 플러스
    → 여기까지만 보면 채택이 당연해 보인다
조합 분석: 다른 게이트 통과분에 적용하면
  후보 46건 → 6건 (-87%) 로 급감, 성과도 무너짐
    → 철회
교훈: 단변량으로 좋은 필터가 조합에서도 좋다는 보장은 없다.
      다른 게이트들이 이미 같은 정보를 다른 각도로 거르고 있었다 —
      거래량 조건은 '중복 검문소' 였고, 통행량만 죽였다.

지표 스터디를 통틀어 이 한 장이 제일 중요한 도판일지도 모르겠다. 백테스트 글에서 흔히 보는 “이 필터를 넣었더니 승률이 올랐다"는 대부분 단변량 결과다. 그런데 봇은 필터를 하나만 돌리지 않는다. 게이트 10개가 이미 서 있는 시스템에 11번째를 넣을 때 던져야 할 질문은 “이 필터가 좋은가"에서 끝나지 않는다. “이 필터가 나머지 열 개의 눈에 안 걸리는 것을 잡아내는가"까지 가야 한다. 필터의 가치를 정하는 건 그 필터가 들고 있는 정보가 얼마나 독립적인가다. VV201 스윕이 조합 검증을 필수 절차로 못 박은 것도, VV195의 RSI 50이 거래량 게이트 도입 뒤 45로 옮겨간 것도 전부 같은 원리의 재확인이다.

세 번째 얼굴, 시간대 프로파일

거래량의 세 번째 얼굴은 종목 대신 시장의 리듬을 본다. 코인 시장은 24시간 돌지만 균질하지 않다. 한국 시장은 저녁에 붐비고 낮에 한산하고, 해외 세션이 열리고 닫힐 때 성격이 바뀐다. 이걸 처음 코드로 옮긴 게 VV131-KO의 시간대 동적 설정이다. 시간대별로 평가 종목 수와 허용 스프레드, 진입 점수 문턱을 다르게 두는 구조였다.

time profile — 시간대별 운용 파라미터 (VV131-KO 커밋 기록)
  UTC 00-06 (KST 08-14, 한산)   보수: 평가 15 · 점수 75 · 스프레드 0.1%
  UTC 07-11 (KST 16-20, 저녁)   공격: 평가 40 · 점수 70 · 스프레드 0.2%
  기본                          균형: 평가 30 · 점수 72 · 스프레드 0.15%
이후 실측이 이 설계를 뒤집는다 — VV184, 스캔 1,116건 시간대별 성과
  19시 -0.752% (승19.4%) | 20시 -0.806% (승12.1%) | 21시 -0.716% (승14.6%)
  22시 -0.044%           | 23시 -0.409% (승23.6%)
  → 19~23시 차단 채택. 잔여 평균 -0.240% → -0.182%, 부트스트랩 우세 78.8%
  그러나 VV191 에서 해제 — 매수 가뭄 (하루 8건 → 1.6건). 기회 확보를 택함

이 대목이 재미있다. 거래가 활발한 시간과 수익이 나는 시간이 겹치지 않았다. 설계할 때의 직관은 “붐빌 때 기회가 많으니 공격적으로"였는데, 실측은 그 시간대(KST 19~23시)의 성과가 제일 나쁘다고 답했다. 거래량이 많다는 건 참여자가 많다는 뜻이지 내 전략이 잘 먹힌다는 뜻은 아니다. 붐비는 시간엔 나 같은 눌림목 사냥꾼도 우글거려서 좋은 자리가 안 남아 있을 수도 있고. 그리고 이 차단마저 결국 풀렸다. 성과 개선(+0.058%p)보다 표본 고갈(하루 8건→1.6건)의 비용이 더 컸다. 통계적으로 옳은 규칙이 운용에서는 틀린 규칙일 수 있다는 걸 비싸게 배웠다.

파생 지표 쪽, OBV와 VWAP과 이름의 기원

거래량 원자료 위에 세운 파생 지표도 잠깐 보자. OBV(On Balance Volume)는 상승 봉의 거래량을 더하고 하락 봉의 거래량을 빼서 누적한다. 매집과 분산의 방향성을 보려는 시도다. VWAP(거래량 가중 평균가)는 거래량으로 가중한 평균 체결가고, 기관의 체결 품질 기준선으로 쓰인다.

derived — 거래량 위에 세운 두 지표
# OBV — 방향성 누적
obv = (np.sign(close.diff()) * volume).fillna(0).cumsum()
  해석: 가격은 횡보하는데 OBV가 오르면 '조용한 매집' 이라는 서사
  주의: 봉 하나의 방향에 거래량 전체를 귀속시키는 거친 가정 위에 서 있다
# VWAP — 거래량 가중 평균가 (앵커 시점부터 누적)
vwap = (price * volume).cumsum() / volume.cumsum()   # price = (H+L+C)/3
  이 봇 이름의 기원이 여기 있다 — 앵커드 VWAP 을 쓰던 시기의 코드가
  vvwap → VV 로 줄어 지금의 VV 모듈 시대까지 이름으로 남았다
  현행 전략은 VWAP 을 진입 판정에 쓰지 않는다. 이름만 유산으로 남은 셈

VWAP은 개인적으로 각별하다. 내 봇 이름 ‘VV’가 여기서 나왔다(앵커드 VWAP을 쓰던 시기의 파일명 vvwap이 줄어 굳었다). 지금 현행 전략은 VWAP을 진입 판정에 안 쓴다. 지표는 떠났는데 이름만 남은 것이다. 461개 버전을 지나온 프로젝트에는 이런 화석이 몇 개씩 박혀 있다.

저유동성이 계좌로 보내는 청구서

거래량이 적은 종목의 비용은 ‘신호가 부정확하다’ 수준에서 안 끝난다. 실계좌에서는 훨씬 물리적인 형태로 청구된다. 본전 청산 27건을 실측했을 때(VV206), 보호선을 +0.2%에 뒀는데 평균 실현이 +0.017%, 합계 -18,144원이었다. 슬리피지 탓을 하고 싶었지만 범인은 호가 관통이었다. 저가·저유동성 코인은 호가 한 칸이 0.2%를 넘어서(실측 예: 443원짜리 코인에서 한 칸 0.226%), 내가 그어놓은 청산선이 호가와 호가 사이 허공에 떠 있었다. 관통 손실 0.183%p에 왕복 수수료 0.10%를 얹으면 손익분기만 0.283%. 이 계산이 본전 바닥을 0.4%로 올렸고(VV206), 이틀 뒤에는 호가 한 칸/가격 비율이 0.15%를 넘는 종목은 아예 진입 금지라는 게이트(VV208, TICK_RATIO_MAX)로 굳었다.

liquidity cost — 거래량 부족이 계좌에 청구되는 경로
거래량 부족 종목의 3단 청구서
 ① 스프레드     사고파는 순간마다 매수-매도 호가 간격만큼 상납
 ② 호가 관통    청산선이 호가 사이 허공에 → 의도한 가격에 못 팔림
                실측: 바닥 +0.2% 설계 → 평균 실현 +0.017% (VV206, 27건)
 ③ 체결 실패    수량이 없어 지정가 미체결 → 유령 포지션 위험
                실측: 저유동성 코인 매도 미체결이 원장 불일치로 (VANA 사건)
방어 계층 (현행):
  유니버스: 거래대금 하한 + 가격 하한   게이트: 거량 0.6x + TICK_RATIO 0.15%
  집행: 체결 폴링 확인                  회계: 원장 대조 (게이트21)

그래서 내가 내린 결론. 거래량 게이트가 존재하는 진짜 이유는 알파 쪽에 있지 않고 비용 방어 쪽에 있다. 좋은 종목을 찾아주는 장치라기보다, 계좌를 갉아먹을 종목을 문 앞에서 돌려세우는 장치다. 수익 기여도로만 심사하면 이 게이트는 늘 애매하게 나온다. 그래서 두 번 죽었다. 비용 방어까지 장부에 올려야 손익계산서가 맞아떨어진다.

거래량을 다시 심사대에 올린다면

현행 거량 게이트(0.6x)는 VV201 스윕에서 ‘현행이 정점’이라는 확인을 받았다. 다만 8.9일치 기록 위에서의 판정이었다. 표본이 더 쌓인 지금 다시 심사한다면 설계는 이렇게 짠다.

validation design — 거래량 게이트 재심사 설계
  축                    후보값                     비고
  거량 배수 하한        0.4 / 0.5 / 0.6 / 0.8      현행 0.6
  평균 기간             10 / 20 / 60봉             자기 평균의 정의
  거래대금 하한         5억 / 7.5억 / 15억         현행 7.5억 (유니버스)
  TICK_RATIO 상한       0.10% / 0.15% / 0.20%      현행 0.15%
  목적함수: 평균수익 · 승률 · 손절권 진입률 — 파레토 우세만 채택
  필수 절차: 단변량 → 조합 검증 → 기간 반분 안정성 (VV184의 교훈)
  추가 관측: 게이트 통과분의 '체결 품질' 도 같이 측정한다
            (지정가 체결률 · 실체결가와 의도가의 괴리 · 본전 청산 실현율)
  결과 칸이 비어 있는 이유: 아직 측정 전이다.

마지막 항목이 이 편의 결론과 이어진다. 거래량 게이트를 수익률만으로 심사하면 또 애매하게 나올 것이다. 이미 두 번 그렇게 죽었다. 체결 품질까지 같이 재야 한다. 지정가가 의도한 가격에 실제로 체결되는지, 본전 청산이 설계대로 실현되는지. 그걸 안 재면 이 게이트의 기여는 장부에 안 잡힌다. 측정 대상을 잘못 고르면 멀쩡한 부품도 해고당한다.

소회

계산이 없는 지표 하나에 한 편을 다 썼다. 그럴 만했다고 본다. 거래량은 시장의 의견 말고 행동을 기록한 유일한 데이터다. 가격은 단 한 주의 체결로도 움직이지만, 거래량은 누군가 실제로 지불한 총량이다. 조작만 없다면 거래량은 거짓말을 못 한다. 뒤집으면, 세탁 거래가 있는 시장에서는 거래량부터 의심해야 한다는 얘기이기도 하다.

내 봇에서 거래량 게이트가 걸어온 길(완화 → 사망 → 재설계 부활 → 스윕 생존)은 이 코너가 하려는 얘기의 축소판이다. 지표는 전략의 문법 안에서만 의미를 갖는다. 필터의 가치는 단독 성적보다 조합 기여로 잡힌다. 어떤 게이트는 수익 장부 말고 비용 장부에서 밥값을 한다. 세 문장 다 실측이 가르쳐준 것들이다.

조셉 그랜빌은 거래량이 가격에 선행한다고 가르쳤다. OBV라는 지표까지 만들어가면서. 그 명제가 보편 법칙인지는 지금도 논쟁거리다. 다만 가격은 의견이고 거래량은 지불이라는 문장 하나는 내 계좌에서 여러 번 확인했다. 의견은 공짜인데 지불은 공짜가 아니어서, 나는 지불 쪽 데이터를 조금 더 믿는다.


약어 풀이

* 이 글에 나온 영어 약어를 풀어 둔다.

  • RSI (Relative Strength Index): 상대강도지수. 일정 기간 평균 상승폭과 평균 하락폭의 비율을 0~100 으로 나타내 과매수·과매도를 재는 모멘텀 오실레이터.
  • SMA (Simple Moving Average): 단순이동평균. 기간 안의 가격을 같은 가중치로 평균한 값.
  • VWAP (Volume Weighted Average Price): 거래량가중평균가격. 체결가를 거래량으로 가중해 평균한 값. 앵커드 VWAP 은 특정 시점을 기준으로 누적한다.
  • OBV (On Balance Volume): 누적거래량. 상승 마감일의 거래량은 더하고 하락 마감일의 거래량은 빼서 누적한 값.
  • BTC (Bitcoin): 비트코인. 이 블로그에서는 코인 시장 전체의 방향(레짐)을 재는 기준 자산.
  • KST (Korea Standard Time): 한국 표준시(UTC+9).
  • UTC (Coordinated Universal Time): 협정 세계시. 시간대의 기준. 한국은 여기에 9시간을 더한다.