VWAP 스캘핑 1편 — 초단타가 어려운 진짜 이유
바이낸스 API로 VWAP 기준 초단타 스캘핑 봇을 직접 개발해 돌려본 기록입니다. 전략 설계부터 실전에서 깨지는 지점까지 정리했습니다.
초단타는 매력적으로 보입니다. 하루에 수십 번 진입하고, 한 번에 몇 틱만 먹고 나오면 되니까 큰 방향성 예측이 필요 없습니다. 이번 영상에서는 VWAP을 기준선으로 삼는 스캘핑 전략을 하나 세우고, 바이낸스 API로 자동매매 봇을 직접 만들어 실제로 돌려봤습니다. 이 글은 그 영상의 정리본이자, 영상에서 다 못 보여준 코드 부분을 채워 넣은 문서입니다. 결론부터 말하면 생각처럼 쉽지 않았고, 왜 어려운지가 이 글의 대부분을 차지합니다.
전략 — 왜 하필 VWAP인가 (00:14)
VWAP(Volume Weighted Average Price, 거래량 가중 평균가)은 단순 이동평균과 달리 거래가 실제로 많이 체결된 가격대에 무게를 줍니다. 기관 체결 기준선으로 자주 쓰이는 이유도 그겁니다. 스캘핑에서 VWAP을 쓰는 논리는 단순합니다. 짧은 시간 안에 가격이 VWAP에서 과하게 멀어지면, 그 괴리가 다시 좁혀질 확률이 있다고 보는 겁니다. 즉 평균회귀 가정입니다.
- 기준선: 최근 60분 롤링 윈도우의 거래량 가중 평균가 (당일 시가에 앵커되는 세션 VWAP이 아닙니다)
- 괴리 측정: 전형가격(고가+저가+종가)/3과 VWAP의 차이를 같은 윈도우의 표준편차로 정규화
- 밴드: VWAP ± k표준편차. k는 1.5 근처에서 시작해 조정
- 가정: 밴드 밖으로 벗어난 가격은 VWAP 쪽으로 되돌아온다는 것
진입과 청산 규칙
규칙 자체는 한 문단이면 끝납니다. 가격이 하단 밴드를 이탈하면 롱, 상단 밴드를 이탈하면 숏. 목표가는 VWAP 복귀 지점, 손절은 진입가 대비 고정 비율입니다. 여기서 중요한 건 목표가가 큰 수익이 아니라는 점입니다. VWAP까지의 거리가 곧 기대 수익이고, 그 거리는 대체로 아주 짧습니다. 이 짧음이 나중에 문제가 됩니다.
import ccxt, pandas as pd
ex = ccxt.binance({'options': {'defaultType': 'future'}})
def vwap_bands(symbol='BTC/USDT', tf='1m', limit=240, window=60, k=1.5):
# 1분봉 240개를 받아 최근 60분 롤링 윈도우로 VWAP을 계산한다.
# 당일 시가에 앵커되는 세션 VWAP이 아니라, 매 봉마다 최근 60분만 다시 보는 방식.
ohlcv = ex.fetch_ohlcv(symbol, tf, limit=limit)
df = pd.DataFrame(ohlcv, columns=['ts', 'o', 'h', 'l', 'c', 'v'])
tp = (df['h'] + df['l'] + df['c']) / 3 # 전형가격
pv = tp * df['v']
df['vwap'] = pv.rolling(window).sum() / df['v'].rolling(window).sum()
# VWAP 대비 괴리의 롤링 표준편차로 밴드 폭을 만든다
dev = (tp - df['vwap']).rolling(window).std()
df['upper'] = df['vwap'] + k * dev
df['lower'] = df['vwap'] - k * dev
# 워밍업 구간(window 미만)은 VWAP과 dev가 NaN이라 잘라낸다.
# 안 자르면 NaN 비교가 조용히 False로 처리돼 신호가 영원히 안 나온다.
return df.dropna().reset_index(drop=True)프로그램 개발 — 봇으로 만들면 생기는 일 (02:06)
전략을 코드로 옮기는 순간, 차트에서는 보이지 않던 것들이 전부 결정 사항으로 튀어나옵니다. 봉이 마감되기 전에 진입할 것인가 마감 후에 할 것인가. 지정가로 걸 것인가 시장가로 칠 것인가. 주문이 부분 체결되면 남은 수량은 어떻게 할 것인가. 포지션이 열려 있는 동안 봉이 갱신되면 목표가도 같이 움직여야 하는가. 아래는 판정부의 핵심만 떼어낸 코드입니다. 실제 봇에는 주문 상태 조회, 재시도, 중복 진입 차단이 추가로 붙습니다.
def signal(df, pos, stop=0.0025):
last = df.iloc[-1]
price = last['c']
# 포지션이 없을 때: 밴드 이탈 = 평균회귀 진입 신호
if pos is None:
if price < last['lower']:
return ('long', price, last['vwap']) # 목표는 VWAP 복귀
if price > last['upper']:
return ('short', price, last['vwap'])
return None
# 포지션이 있을 때: VWAP 도달(익절) 또는 역행(손절)
side, entry, target = pos
if side == 'long' and (price >= target or price <= entry * (1 - stop)):
return ('close', price, None)
if side == 'short' and (price <= target or price >= entry * (1 + stop)):
return ('close', price, None)
return None결론 — 돌려본 결과 (08:40)
봇은 의도한 대로 작동했습니다. 신호를 잡고, 주문을 넣고, 목표가에서 청산하고, 손절도 제대로 쳤습니다. 문제는 로직이 아니라 구조였습니다. 이긴 트레이드의 이익 폭이 애초에 너무 작아서 수수료와 슬리피지를 빼고 나면 남는 게 거의 없었습니다. 반대로 손절 한 번은 여러 번의 익절을 지웠습니다. 평균회귀 전략의 전형적인 손익 비대칭입니다. 작게 자주 이기고 가끔 크게 지는 구조에서, 그 가끔의 빈도가 조금만 올라가도 곡선은 아래를 향합니다. 이 회차는 개선의 여지를 남긴 상태로 마무리했고, 파라미터와 체결 방식을 바꿔가며 2편에서 이어갑니다.
왜 이게 실전에서 깨지는가
- 수수료 대비 목표 폭이 너무 작습니다. VWAP 복귀까지의 거리가 왕복 수수료(테이커 기준 0.1%)의 몇 배가 안 되면 그 트레이드는 시작부터 기댓값이 음수입니다. 진입 전에 '목표 거리 > 수수료 × 3' 같은 필터가 필요합니다.
- 백테스트의 체결 가정이 낙관적입니다. 종가에 무조건 체결된다고 가정한 백테스트는 실전과 다른 세계입니다. 특히 밴드를 이탈하는 순간은 호가가 얇아지는 순간이라 슬리피지가 가장 크게 발생합니다.
- 평균회귀 가정이 추세장에서 정면으로 깨집니다. 가격이 하단 밴드를 뚫고 계속 내려가는 국면에서 이 봇은 계속 롱을 잡습니다. 추세 필터 없는 평균회귀는 하락에 물타기하는 기계와 같습니다.
- VWAP 자체가 움직이는 목표입니다. 진입 시점의 VWAP과 3분 뒤의 VWAP은 다릅니다. 특히 롤링 윈도우 방식은 오래된 봉이 빠지면서 기준선이 계속 재계산됩니다. 목표가를 고정할지 갱신할지에 따라 결과가 크게 달라지는데, 이건 전략의 본질이 아니라 구현 선택입니다. 결과가 구현 선택에 민감하다는 건 엣지가 얇다는 신호입니다.
- 거래 빈도가 높을수록 운영 리스크가 쌓입니다. API 레이트 리밋, 네트워크 지연, 주문 거부 같은 것들이 하루 수십 회 진입에서는 확률적으로 반드시 발생합니다.
이 글과 첨부 코드는 전략을 권하기 위한 게 아니라 검증 과정을 공유하기 위한 것입니다. 교육·연구 목적이며, 실제 자금 투입에 대한 판단과 책임은 각자에게 있습니다. 코드를 돌려볼 거라면 테스트넷이나 페이퍼 트레이딩부터 시작하고, 수수료와 슬리피지를 넣은 상태의 손익 곡선을 먼저 확인하세요.
첨부파일
영상에 사용된 소스코드와 설명서입니다. 로그인 없이 받을 수 있습니다.
VWAP 스캘핑 봇 소스코드체결 비용 반영 백테스터파라미터 튜닝 노트