VWAP이란? 기관 투자자의 핵심 벤치마크
VWAP(Volume Weighted Average Price, 거래량 가중 평균가)는 특정 기간 동안 거래된 모든 가격을 거래량으로 가중 평균한 값입니다. 기관 투자자들이 체결 품질을 평가하는 핵심 벤치마크로 사용하며, 개인 퀀트 트레이더에게는 강력한 지지·저항 라인이자 추세 판단 도구로 활용됩니다.
VWAP이 단순 이동평균(SMA)보다 우수한 이유는 명확합니다. 거래량을 반영하기 때문에 실제로 많은 거래가 이루어진 가격대에 더 큰 가중치를 부여합니다. 대량 거래가 발생한 가격은 시장 참여자들의 합의된 적정가에 가깝기 때문에, VWAP은 단순 평균보다 더 의미 있는 기준선을 제공합니다.
VWAP 계산 공식과 파생 지표
VWAP의 수학적 정의와 함께 활용도 높은 파생 지표를 정리합니다.
VWAP = Σ(가격 × 거래량) / Σ(거래량)
일반적으로 대표가격(Typical Price)을 사용:
TP = (고가 + 저가 + 종가) / 3
VWAP = Σ(TP × Volume) / Σ(Volume)
- VWAP 상단 밴드 = VWAP + n × σ (표준편차)
- VWAP 하단 밴드 = VWAP − n × σ
- MVWAP(Moving VWAP): 롤링 윈도우 VWAP으로 여러 날에 걸친 추세 확인
- Anchored VWAP: 특정 이벤트(실적 발표, 저점 등) 시점부터 계산하는 VWAP
파이썬 환경 설정과 데이터 수집
pip install yfinance pandas numpy matplotlib
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 일봉 데이터 수집
ticker = "AAPL"
df = yf.download(ticker, start="2022-01-01", end="2025-12-31")
df.columns = ['open', 'high', 'low', 'close', 'adj_close', 'volume']
# 대표가격(Typical Price) 계산
df['tp'] = (df['high'] + df['low'] + df['close']) / 3
print(f"데이터 기간: {df.index[0].date()} ~ {df.index[-1].date()}")
print(f"총 거래일: {len(df)}일")
print(df[['close', 'volume', 'tp']].tail())
VWAP 핵심 계산 구현
일별 VWAP과 롤링 VWAP(MVWAP), 표준편차 밴드를 계산하는 코드입니다.
def calculate_vwap(df, period=20):
"""
롤링 VWAP 및 표준편차 밴드 계산
- period: 롤링 윈도우 (거래일 수)
"""
df = df.copy()
# 누적 VWAP (당일 기준 — 인트라데이용)
df['cum_tp_vol'] = (df['tp'] * df['volume']).cumsum()
df['cum_vol'] = df['volume'].cumsum()
df['vwap_cum'] = df['cum_tp_vol'] / df['cum_vol']
# 롤링 VWAP (N일 윈도우)
df['tp_vol'] = df['tp'] * df['volume']
df['rolling_tp_vol'] = df['tp_vol'].rolling(period).sum()
df['rolling_vol'] = df['volume'].rolling(period).sum()
df['vwap'] = df['rolling_tp_vol'] / df['rolling_vol']
# VWAP 표준편차 밴드
# 가격과 VWAP의 편차를 거래량 가중으로 계산
df['sq_diff'] = ((df['tp'] - df['vwap']) ** 2) * df['volume']
df['vwap_std'] = np.sqrt(
df['sq_diff'].rolling(period).sum() / df['rolling_vol']
)
df['vwap_upper1'] = df['vwap'] + 1 * df['vwap_std']
df['vwap_lower1'] = df['vwap'] - 1 * df['vwap_std']
df['vwap_upper2'] = df['vwap'] + 2 * df['vwap_std']
df['vwap_lower2'] = df['vwap'] - 2 * df['vwap_std']
return df
df = calculate_vwap(df, period=20)
print(df[['close', 'vwap', 'vwap_upper1', 'vwap_lower1']].tail(10))
VWAP 매매 전략 1: 추세 추종 (Trend Following)
가격이 VWAP 위에 있으면 상승 추세, 아래에 있으면 하락 추세로 판단하는 기본 전략입니다.
def vwap_trend_strategy(df, period=20):
"""
VWAP 추세 추종 전략
- 종가 > VWAP → 매수 (롱)
- 종가 < VWAP → 현금 (또는 숏)
"""
df = calculate_vwap(df, period)
df = df.dropna()
# 시그널: 가격이 VWAP 위이면 1, 아래이면 0
df['signal'] = np.where(df['close'] > df['vwap'], 1, 0)
# 일별 수익률
df['market_return'] = df['close'].pct_change()
df['strategy_return'] = df['signal'].shift(1) * df['market_return']
# 누적 수익률
df['cum_market'] = (1 + df['market_return']).cumprod()
df['cum_strategy'] = (1 + df['strategy_return']).cumprod()
return df
result = vwap_trend_strategy(df.copy(), period=20)
# 성과 요약
total_days = len(result)
in_market = result['signal'].mean() * 100
strategy_ret = (result['cum_strategy'].iloc[-1] - 1) * 100
market_ret = (result['cum_market'].iloc[-1] - 1) * 100
print(f"시장 투자 비율: {in_market:.1f}%")
print(f"전략 수익률: {strategy_ret:.1f}%")
print(f"시장 수익률: {market_ret:.1f}%")
VWAP 매매 전략 2: 평균 회귀 (Mean Reversion)
가격이 VWAP 밴드의 극단에 도달하면 평균으로 회귀할 것을 기대하고 역방향으로 진입하는 전략입니다.
def vwap_mean_reversion(df, period=20, entry_std=2.0, exit_std=0.5):
"""
VWAP 밴드 평균 회귀 전략
- 가격 < VWAP - entry_std × σ → 매수 (과매도)
- 가격 > VWAP + entry_std × σ → 매도 (과매수)
- VWAP ± exit_std × σ 도달 시 청산
"""
df = calculate_vwap(df, period)
df = df.dropna()
position = 0 # 0: 없음, 1: 롱, -1: 숏
positions = []
trades = []
entry_price = 0
for i in range(len(df)):
row = df.iloc[i]
price = row['close']
vwap = row['vwap']
std = row['vwap_std']
if position == 0:
# 하단 밴드 이탈 → 매수
if price < vwap - entry_std * std:
position = 1
entry_price = price
# 상단 밴드 이탈 → 매도 (숏)
elif price > vwap + entry_std * std:
position = -1
entry_price = price
elif position == 1:
# 롱 청산: VWAP 근처 복귀
if price > vwap - exit_std * std:
pnl = (price - entry_price) / entry_price
trades.append({'pnl': pnl, 'type': 'long',
'date': df.index[i]})
position = 0
elif position == -1:
# 숏 청산: VWAP 근처 복귀
if price < vwap + exit_std * std:
pnl = (entry_price - price) / entry_price
trades.append({'pnl': pnl, 'type': 'short',
'date': df.index[i]})
position = 0
positions.append(position)
df['position'] = positions
trades_df = pd.DataFrame(trades)
if len(trades_df) > 0:
wins = (trades_df['pnl'] > 0).sum()
print(f"총 거래: {len(trades_df)}회")
print(f"승률: {wins/len(trades_df)*100:.1f}%")
print(f"평균 수익: {trades_df['pnl'].mean()*100:.2f}%")
print(f"총 수익: {trades_df['pnl'].sum()*100:.1f}%")
return df, trades_df
result_mr, trades = vwap_mean_reversion(df.copy(), period=20)
VWAP 매매 전략 3: 거래량 확인 돌파
VWAP 돌파에 거래량 급증 조건을 추가하여 가짜 돌파(False Breakout)를 필터링하는 고급 전략입니다.
def vwap_volume_breakout(df, period=20, vol_mult=1.5):
"""
VWAP + 거래량 확인 돌파 전략
- 종가 > VWAP 돌파 + 거래량이 평균의 vol_mult배 이상 → 매수
- 종가 < VWAP 이탈 → 청산
"""
df = calculate_vwap(df, period)
df = df.dropna()
# 거래량 이동 평균
df['vol_ma'] = df['volume'].rolling(period).mean()
df['vol_ratio'] = df['volume'] / df['vol_ma']
# VWAP 돌파 감지
df['above_vwap'] = df['close'] > df['vwap']
df['prev_above'] = df['above_vwap'].shift(1)
# 돌파 시그널: 이전에 VWAP 아래 → 현재 위로 돌파 + 거래량 조건
df['breakout'] = (
(df['above_vwap'] == True) &
(df['prev_above'] == False) &
(df['vol_ratio'] >= vol_mult)
)
# 포지션: 돌파 시 매수, VWAP 아래로 이탈 시 청산
position = 0
signals = []
returns = []
for i in range(len(df)):
row = df.iloc[i]
daily_ret = df['close'].pct_change().iloc[i]
if position == 0 and row['breakout']:
position = 1
signals.append(1)
elif position == 1 and not row['above_vwap']:
position = 0
signals.append(0)
else:
signals.append(position)
returns.append(position * daily_ret if position == 1 else 0)
df['signal'] = signals
df['strategy_return'] = returns
df['cum_strategy'] = (1 + pd.Series(returns, index=df.index)).cumprod()
# 결과
breakouts = df['breakout'].sum()
total_ret = (df['cum_strategy'].iloc[-1] - 1) * 100
print(f"돌파 시그널 발생: {breakouts}회")
print(f"거래량 확인 돌파 전략 수익률: {total_ret:.1f}%")
return df
result_vb = vwap_volume_breakout(df.copy(), period=20, vol_mult=1.5)
Anchored VWAP: 이벤트 기반 분석
Anchored VWAP(앵커드 VWAP)은 특정 시점(실적 발표일, 저점, 고점 등)부터 VWAP을 계산하여 해당 이벤트 이후 시장 참여자들의 평균 진입가를 파악하는 강력한 도구입니다.
def anchored_vwap(df, anchor_date):
"""
특정 날짜부터 시작하는 Anchored VWAP 계산
- anchor_date: 시작 날짜 (str, 'YYYY-MM-DD')
"""
df = df.copy()
df['tp'] = (df['high'] + df['low'] + df['close']) / 3
# 앵커 날짜 이후 필터링
mask = df.index >= anchor_date
df.loc[mask, 'anch_tp_vol'] = (df.loc[mask, 'tp'] * df.loc[mask, 'volume']).cumsum()
df.loc[mask, 'anch_vol'] = df.loc[mask, 'volume'].cumsum()
df['anchored_vwap'] = df['anch_tp_vol'] / df['anch_vol']
return df
def find_swing_lows(df, window=20):
"""스윙 저점 자동 감지"""
lows = []
for i in range(window, len(df) - window):
if df['low'].iloc[i] == df['low'].iloc[i-window:i+window+1].min():
lows.append(df.index[i])
return lows
# 스윙 저점에서 앵커드 VWAP 계산
swing_lows = find_swing_lows(df, window=30)
print(f"감지된 스윙 저점: {len(swing_lows)}개")
if len(swing_lows) > 0:
# 가장 최근 저점 기준 앵커드 VWAP
latest_low = swing_lows[-1]
df_anchored = anchored_vwap(df, str(latest_low.date()))
print(f"앵커 날짜: {latest_low.date()}")
print(f"현재 Anchored VWAP: ${df_anchored['anchored_vwap'].iloc[-1]:.2f}")
print(f"현재 종가: ${df_anchored['close'].iloc[-1]:.2f}")
멀티 타임프레임 VWAP 전략
단일 기간 VWAP보다 여러 기간의 VWAP을 동시에 관찰하면 더 신뢰도 높은 시그널을 얻을 수 있습니다.
def multi_timeframe_vwap(df, periods=[5, 10, 20, 60]):
"""
멀티 타임프레임 VWAP 전략
- 모든 기간의 VWAP 위에 있으면 강한 상승 추세
- 모든 기간의 VWAP 아래에 있으면 강한 하락 추세
"""
df = df.copy()
df['tp'] = (df['high'] + df['low'] + df['close']) / 3
df['tp_vol'] = df['tp'] * df['volume']
vwap_cols = []
for p in periods:
col = f'vwap_{p}'
df[col] = df['tp_vol'].rolling(p).sum() / df['volume'].rolling(p).sum()
vwap_cols.append(col)
# 신호 강도: VWAP 위에 있는 기간 수 / 전체 기간 수
df['bullish_count'] = sum(
(df['close'] > df[col]).astype(int) for col in vwap_cols
)
df['signal_strength'] = df['bullish_count'] / len(periods)
# 강한 추세만 거래: 모든 VWAP 위(매수) 또는 모두 아래(현금)
df['signal'] = np.where(df['signal_strength'] >= 0.75, 1, 0)
df['market_return'] = df['close'].pct_change()
df['strategy_return'] = df['signal'].shift(1) * df['market_return']
df['cum_strategy'] = (1 + df['strategy_return']).cumprod()
df['cum_market'] = (1 + df['market_return']).cumprod()
return df
result_mtf = multi_timeframe_vwap(df.copy())
result_mtf = result_mtf.dropna()
strat_ret = (result_mtf['cum_strategy'].iloc[-1] - 1) * 100
mkt_ret = (result_mtf['cum_market'].iloc[-1] - 1) * 100
in_market = result_mtf['signal'].mean() * 100
print(f"전략 수익률: {strat_ret:.1f}%")
print(f"시장 수익률: {mkt_ret:.1f}%")
print(f"시장 참여 비율: {in_market:.1f}%")
VWAP 전략 성과 비교 분석
def compare_vwap_strategies(df):
"""세 가지 VWAP 전략의 성과를 비교"""
strategies = {}
# 1. 추세 추종
r1 = vwap_trend_strategy(df.copy(), period=20)
strategies['VWAP 추세추종'] = r1['cum_strategy']
# 2. 거래량 돌파
r2 = vwap_volume_breakout(df.copy(), period=20, vol_mult=1.5)
strategies['거래량 확인 돌파'] = r2['cum_strategy']
# 3. 멀티 타임프레임
r3 = multi_timeframe_vwap(df.copy())
strategies['멀티 VWAP'] = r3['cum_strategy'].reindex(r1.index)
# Buy & Hold
strategies['Buy & Hold'] = r1['cum_market']
# 성과 지표 비교
print(f"n{'전략':<20} {'총수익률':>10} {'연수익률':>10} {'MDD':>10}")
print("-" * 55)
for name, cum in strategies.items():
cum = cum.dropna()
total = (cum.iloc[-1] - 1) * 100
years = len(cum) / 252
annual = ((cum.iloc[-1]) ** (1/years) - 1) * 100
peak = cum.cummax()
mdd = ((cum - peak) / peak).min() * 100
print(f"{name:<20} {total:>9.1f}% {annual:>9.1f}% {mdd:>9.1f}%")
return strategies
strategies = compare_vwap_strategies(df)
시각화: VWAP 밴드와 매매 시그널
# VWAP 밴드와 가격 차트
df_plot = calculate_vwap(df.copy(), period=20).dropna().tail(200)
fig, axes = plt.subplots(2, 1, figsize=(16, 10), height_ratios=[3, 1])
# 가격 + VWAP + 밴드
axes[0].plot(df_plot.index, df_plot['close'], label='Close', color='black', linewidth=1.5)
axes[0].plot(df_plot.index, df_plot['vwap'], label='VWAP(20)', color='blue', linewidth=2)
axes[0].fill_between(df_plot.index, df_plot['vwap_upper1'], df_plot['vwap_lower1'],
alpha=0.1, color='blue', label='±1σ')
axes[0].fill_between(df_plot.index, df_plot['vwap_upper2'], df_plot['vwap_lower2'],
alpha=0.05, color='blue', label='±2σ')
axes[0].set_title(f'VWAP 밴드 분석', fontsize=14)
axes[0].legend(fontsize=10)
axes[0].grid(True, alpha=0.3)
# 거래량
axes[1].bar(df_plot.index, df_plot['volume'], color='steelblue', alpha=0.7)
axes[1].axhline(y=df_plot['volume'].mean(), color='red', linestyle='--', label='평균 거래량')
axes[1].set_title('거래량')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('vwap_analysis.png', dpi=150)
plt.show()
실전 적용 시 핵심 고려사항
- VWAP 리셋 주기: 인트라데이 트레이딩에서 VWAP은 매일 리셋됩니다. 일봉 기반에서는 롤링 VWAP을 사용하되, 기간 선택이 중요합니다. 20일(약 1개월)이 가장 보편적입니다.
- 거래량 데이터 품질: VWAP의 정확도는 거래량 데이터에 의존합니다. 장외 거래(dark pool)가 포함되지 않는 데이터는 왜곡될 수 있습니다.
- 갭 처리: 장 시작 시 갭이 크면 VWAP이 급격히 변동합니다. 갭 발생 시 진입을 지연시키는 필터를 추가하는 것이 좋습니다.
- 유동성 낮은 종목 주의: 거래량이 적은 종목에서는 VWAP의 의미가 약해집니다. 일평균 거래대금이 충분한 종목에만 적용하세요.
- 다른 지표와 조합: VWAP 단독보다 RSI, MACD 등과 결합하면 시그널 정확도가 높아집니다.
자주 묻는 질문 (FAQ)
VWAP과 이동평균선의 차이점은 무엇인가요?
이동평균선(SMA, EMA)은 가격만 평균하지만, VWAP은 거래량을 가중치로 반영합니다. 대량 거래가 발생한 가격대에 더 큰 비중을 주기 때문에, 기관 투자자의 평균 진입가에 가까운 의미 있는 기준선을 제공합니다. 실제 시장에서 VWAP이 더 강한 지지·저항으로 작용하는 경우가 많습니다.
VWAP 전략은 어떤 시장에서 효과적인가요?
거래량이 풍부한 대형주와 ETF에서 가장 효과적입니다. 특히 SPY, QQQ, AAPL 같은 고유동성 종목이 적합합니다. 암호화폐 시장에서도 BTC, ETH 등 메이저 코인에 활용 가능하며, 24시간 시장 특성상 VWAP 리셋 기준을 UTC 00:00으로 설정하는 것이 일반적입니다.
Anchored VWAP은 언제 사용하나요?
중요 이벤트(실적 발표, IPO, 52주 저점 등) 이후 시장 참여자들의 평균 진입가를 파악할 때 사용합니다. 예를 들어 실적 발표일부터의 Anchored VWAP이 현재가보다 낮으면, 그 이후 매수한 투자자 대부분이 수익 중이라는 의미로 강한 지지 역할을 합니다.
결론: VWAP 자동매매 실전 로드맵
VWAP은 거래량이라는 시장의 진실된 정보를 가격 분석에 통합하는 강력한 도구입니다. 기관 투자자의 벤치마크이자 개인 퀀트 트레이더의 핵심 지표로, 다음 단계로 적용해 보세요.
- 기본 VWAP 계산 → 롤링 VWAP과 표준편차 밴드 구현
- 추세 추종 → VWAP 위/아래로 포지션 방향 결정
- 평균 회귀 → 밴드 극단에서 역방향 진입
- 거래량 확인 → 거래량 급증 조건으로 가짜 돌파 필터링
- 멀티 타임프레임 → 여러 기간 VWAP 합의로 시그널 강화
VWAP 전략을 마스터했다면, 켈리 기준 자금 관리를 적용하여 최적 포지션 크기를 결정하고, RSI 자동매매 전략과 결합하여 VWAP 추세 + RSI 타이밍의 복합 시그널 시스템을 구축해 보세요.