VWAP 스캘핑 1편 — 평균회귀 스캘핑 봇을 만들었습니다
실시간 체결 데이터로 VWAP을 계산해 평균회귀 스캘핑 봇을 만들었습니다. 수수료를 아끼려 지정가 주문만 썼을 때, 왜 이론과 달리 체결이 나를 비켜가는지 그 구조를 기록입니다.
유튜브에 올린 "미칠듯한 초단타 스캘핑으로 돈을 벌 수 있을까요?" 영상의 자료실 버전입니다. 이 글은 그 정리본에 영상에는 안 넣은 최소 실행 코드를 붙인 것입니다. 수익률은 올리지 않습니다. 여기에 옮겨 적으면 제가 유리한 구간만 고르게 되니까요. 결론부터 말하면, '수수료를 이기는 전략'과 '체결이 되는 전략'은 전혀 다른 문제였습니다.
원리가 간단한 게 함정이다
VWAP(Volume-Weighted Average Price, 거래량 가중평균가)은 아주 직관적인 지표입니다. 단순히 가격의 평균이 아니라, 거래량이 터진 가격에 더 큰 가중치를 둬서 구한 '진짜 평균 단가'에 가깝거든요. 그래서 스캘핑 전략의 가설은 단순합니다. "현재가는 결국 VWAP으로 돌아온다." 현재가가 VWAP보다 비정상적으로 높으면 거품이니 숏, 너무 낮으면 과매도이니 롱으로 진입해서 VWAP에 다시 닿으면 청산하는 평균 회귀 전략입니다.
이 전략의 핵심은 실시간 VWAP을 정확하게 계산하는 겁니다. 그러려면 호가 데이터가 아니라 실제 '체결' 데이터가 필요합니다. 얼마에 몇 개가 거래됐는지를 알아야 가중치를 매길 수 있으니까요. 그래서 이 봇은 바이낸스의 aggTrade 스트림을 받아서, 최근 N분(영상에선 15분) 동안의 체결 기록을 계속 업데이트하며 롤링 VWAP을 계산합니다.
public class Vwap
{
// 창 안의 체결들을 큐로 들고 있음. 원소 = [시각ms, price*qty, qty]
Queue<double[]> trades = new Queue<double[]>();
double sumPV = 0; // Σ(price*qty)
double sumQty = 0; // Σ(qty)
long windowMs;
public Vwap(int windowSec)
{
windowMs = (long)windowSec * 1000L;
}
// 체결 하나 반영. tMs = 체결 시각(거래소가 준 값)
public void Add(double price, double qty, long tMs)
{
double pv = price * qty;
trades.Enqueue(new double[] { tMs, pv, qty });
sumPV += pv;
sumQty += qty;
// 창 밖(오래된) 체결은 큐에서 빼서 합계에서도 제거한다. 이게 '리셋' 역할.
long cutoff = tMs - windowMs;
while (trades.Count > 0 && trades.Peek()[0] < cutoff)
{
double[] old = trades.Dequeue();
sumPV -= old[1];
sumQty -= old[2];
}
}
// 현재 VWAP. 아직 표본 없으면 0.
public double Value()
{
if (sumQty <= 0) return 0;
return sumPV / sumQty;
}
}
스캘핑의 진짜 적은 손실이 아니라 수수료다
초단타 매매는 한 번에 0.1% ~ 0.3% 수준의 작은 수익을 수백 번 쌓아 올리는 게임입니다. 그런데 바이낸스 선물 시장가(Taker) 수수료는 기본 0.04%입니다. 사고팔면 왕복 0.08%가 비용으로 나갑니다. 0.1%를 먹으려다 0.08%를 수수료로 내면 남는 게 없어요. 그래서 스캘핑은 시장가 주문을 쓰는 순간 필패입니다. 선택지가 없습니다. 무조건 지정가(Maker) 주문을 써서 0.02% 수수료(왕복 0.04%)를 내야만 수수료의 허들을 넘을 가능성이 생깁니다.
그런데 지정가 주문은 체결이 보장되지 않습니다. 내가 사려는 가격에 누군가 팔아야만 거래가 성사되니까요. 가격이 내가 원하는 방향으로 빠르게 움직여버리면, 내 주문은 허공에 남겨진 채 영원히 체결되지 않을 수 있습니다. 그래서 가격을 계속 추적하며 주문을 취소하고 다시 내는 '리프라이싱(re-pricing)' 로직이 필수적으로 들어갑니다. 여기가 바로 첫 번째 균열이 생기는 곳입니다.
// ---------- 진입 지정가 관리 ----------
void HandleEntering(double price, double qty, double dev, long tMs, Config cfg, BinanceFutures ex)
{
// 우선 이번 체결로 내 주문이 채워졌나 본다 (로컬 시뮬레이션)
SimFill(price, qty);
// 아직 안 채워졌고 부분체결도 시작 안 됐으면...
if (ordFilled == 0)
{
// 1. 신호가 사라졌으면(가격이 VWAP 으로 돌아왔으면) 취소
if ((pendingDir > 0 && dev >= 0) || (pendingDir < 0 && dev <= 0))
{
CancelOrder(cfg, ex);
state = FLAT;
return;
}
// 2. 너무 오래 안 붙으면 타임아웃 취소
if ((tMs - ordPlacedMs) >= (long)cfg.EntryTimeoutSec * 1000)
{
CancelOrder(cfg, ex);
state = FLAT;
return;
}
// 3. 현재가와 지정가가 너무 벌어지면 취소-재주문 (리프라이스)
if (Math.Abs(price - ordPrice) >= Tick * cfg.RepriceTicks)
{
double newp = pendingDir > 0 ? RoundTick(price) - Tick : RoundTick(price) + Tick;
CancelOrder(cfg, ex);
PlaceOrder(ordSide, ordQty, newp, false, tMs, cfg, ex);
}
}
}
// 여기서 모든 게 결정된다. 체결이 될 것인가, 놓칠 것인가.체결이 나를 골라야 하는 게임
지정가 주문을 쓴다는 건, 시장의 선택을 기다리는 수동적인 플레이어가 되겠다는 뜻입니다. 내가 시장을 때리는(Taker) 게 아니라, 시장이 나에게 와서 체결시켜주기를(Maker) 기다리는 거죠. 이런 방식은 변동성이 낮고 가격이 한곳에 머무는 횡보장에서는 잘 통합니다. 어차피 가격이 위아래로 계속 오가니까요. 하지만 우리가 진짜 돈을 벌고 싶은 순간, 즉 가격이 한 방향으로 의미 있게 움직이는 '추세'가 시작될 때는 어떨까요?
가격은 내 주문을 그대로 남겨둔 채 멀리 떠나버립니다. 결국 체결되는 순간은 내가 원했던 강한 움직임이 아니라, 그 움직임이 거의 끝나고 다시 돌아올 때, 즉 가장 불리한 순간에만 체결이 됩니다. 이것이 바로 '역선택(Adverse Selection)'입니다. 수수료를 아낀 게 아니라, 좋은 기회를 모두 날리는 기회비용을 낸 겁니다. 스캘핑은 체결 속도와 타이밍이 생명인데, 지정가 주문은 그 생명줄을 시장의 손에 넘기는 행위나 마찬가지였습니다.
왜 이게 실전에서 깨지는가
- 지정가 주문은 항상 불리한 쪽으로만 체결됩니다. 추세가 터져서 내가 꼭 잡고 싶은 자리는 체결이 안 되고, 추세가 꺾이기 직전의 위험한 자리에서만 체결이 나를 '골라서' 물량을 안겨줍니다.
- 수수료를 아낀 게 아니라 기회비용을 지불한 겁니다. 수십 번의 좋은 진입 기회를 놓치고 얻는 한 번의 체결은 이미 통계적 우위를 잃은 상태에서 시작하는 것과 같습니다.
- 평균은 언젠가 회귀하지만, 내 계좌가 청산된 뒤에 회귀할 수도 있습니다. 강한 추세장에서 '버티면 돌아온다'는 믿음은 가장 위험한 생각입니다.
- 로컬에서 시뮬레이션한 지정가 체결과 실제 체결은 완전히 다른 세계입니다. 내가 보는 체결 데이터는 이미 과거의 일일 뿐, 내 주문의 미래를 보장해주지 않습니다.
- 체결률을 높이려 리프라이싱 간격이나 타임아웃 같은 파라미터를 계속 추가하게 됩니다. 이건 전략을 개선하는 게 아니라, 근본적인 실행의 한계를 땜질하는 것에 가깝습니다. 과최적화의 늪이죠.
- 결국 미세한 지연 시간(Latency)이 승패를 가릅니다. 내 코드가 체결을 보고 판단해서 주문을 보내는 사이, 가격은 이미 저만치 가 있습니다. 기관들이 데이터센터 안에 서버를 두는 이유입니다.
이 글과 첨부 코드는 전략 구조를 설명하려고 만든 교육·연구 자료입니다. 특정 종목이나 매매를 권하지 않습니다.
첨부파일
영상에 사용된 소스코드와 설명서입니다. 로그인 없이 받을 수 있습니다.
VWAP 스캘핑 봇 전체 소스