先日、トヨタ株を買うタイミングを完全に逃した。。。朝イチで「安そう」と思って指値を入れたら、そのまま上昇していってしまった。「なんで僕が買う前に上がるんだ」という、投資家なら誰でも経験したことがある悔しさ。そんな悩みを解決するヒントとして「VWAP」というものに出会った。今日はこれをPythonで実装してみた記録を書く。
そもそも僕がVWAPを調べたきっかけ
子供が生まれてから、日中にチャートを見る時間がほぼゼロになった。朝にざっと注文を入れたら、あとは仕事に集中するしかない生活だ。RSIやMACDは前日の終値ベースで計算できるので朝に確認できるんだけど、「その日の相場の文脈」がまったくわからない。
そこで調べていて出てきたのがVWAP(Volume Weighted Average Price=出来高加重平均価格)。機関投資家が「今日の平均コスト」として意識している価格帯で、「VWAPより下で買えれば割安」「VWAPより上で売れれば割高」という判断ができるらしい。これ、仕事中でも朝に設定しておけば機能するんじゃないか? → 試してみた。
VWAPとは何か(サクッと説明)
VWAPは一言で言うと「その日の取引量で重みをつけた平均株価」だ。単純平均と違って、出来高が多い時間帯の価格が重く反映される。機関投資家はVWAPを基準にして注文を分割することが多く、個人投資家にとっては「プロが意識している水準」として使える。
計算式はシンプルで:
VWAP = Σ(高値+安値+終値)/3 × 出来高) ÷ Σ(出来高)
分足データが必要になるので、日足だけでは計算できない点に注意が必要。
PythonでVWAPを計算する
yfinanceで1分足データを取得してVWAPを計算してみる。yfinanceは直近7日分の1分足が無料で取れるので十分だ。
import yfinance as yf
import pandas as pd
import numpy as np
def calculate_vwap(df):
"""
1日分のOHLCVデータからVWAPを計算する。
日をまたがないよう、日ごとにリセットして計算すること。
"""
df = df.copy()
# 典型価格(Typical Price)= (高値 + 安値 + 終値) / 3
df['tp'] = (df['High'] + df['Low'] + df['Close']) / 3
# 典型価格 × 出来高
df['tp_vol'] = df['tp'] * df['Volume']
# 累積計算
df['cum_tp_vol'] = df['tp_vol'].cumsum()
df['cum_vol'] = df['Volume'].cumsum()
# VWAP
df['VWAP'] = df['cum_tp_vol'] / df['cum_vol']
return df
# トヨタ(7203.T)の1分足データを取得
ticker = yf.Ticker("7203.T")
df_raw = ticker.history(period="5d", interval="1m")
df_raw.index = pd.to_datetime(df_raw.index)
# 日付ごとにVWAPをリセットして計算(これが超重要)
daily_results = []
for date, group in df_raw.groupby(df_raw.index.date):
group_with_vwap = calculate_vwap(group)
daily_results.append(group_with_vwap)
df = pd.concat(daily_results)
# 直近のデータを確認
print(df[['Close', 'VWAP', 'Volume']].tail(20))
実行してみると、こんな感じのデータが取れる(数値はサンプル):
Close VWAP Volume
2026-06-28 09:00:00+09:00 3210.0 3210.00 8500
2026-06-28 09:01:00+09:00 3215.0 3213.21 12300
2026-06-28 09:02:00+09:00 3208.0 3211.44 9800
...
2026-06-28 15:29:00+09:00 3225.0 3218.73 45200
VWAPクロス戦略をシンプルにバックテストする
VWAPの使い方はいろいろあるけど、最もシンプルな「VWAPクロス戦略」を試してみる。ルールはこれだけ:
・終値がVWAPを上から下に抜けた → 買いシグナル(VWAP割れからの反発を狙う)
・終値がVWAPを下から上に抜けた → 売りシグナル(利確)
def backtest_vwap_strategy(df, initial_capital=1_000_000):
"""
VWAPクロス戦略の簡易バックテスト。
VWAP下抜けで買い、上抜けで売り(当日内のみ)。
"""
df = df.copy()
# シグナル生成
df['above_vwap'] = df['Close'] > df['VWAP']
df['signal'] = 0
# VWAPを下から上に抜けた → 売り(+1 → 決済)
# VWAPを上から下に抜けた → 買い(-1 → 仕掛け)
df['signal'] = df['above_vwap'].astype(int).diff()
# signal: +1=売り場、-1=買い場
position = 0
buy_price = 0
capital = initial_capital
trades = []
for i, row in df.iterrows():
if row['signal'] == -1 and position == 0:
# VWAPを下抜け → 買い
buy_price = row['Close']
shares = int(capital * 0.1 / buy_price / 100) * 100 # 資金の10%、単元株
position = shares
elif row['signal'] == 1 and position > 0:
# VWAP上抜け → 売り
sell_price = row['Close']
pnl = (sell_price - buy_price) * position
capital += pnl
trades.append({
'buy': buy_price, 'sell': sell_price,
'shares': position, 'pnl': pnl
})
position = 0
if trades:
df_trades = pd.DataFrame(trades)
print(f"取引回数: {len(df_trades)}")
print(f"勝率: {(df_trades['pnl'] > 0).mean():.1%}")
print(f"合計損益: {df_trades['pnl'].sum():,.0f}円")
print(f"最終資本: {capital:,.0f}円")
else:
print("シグナルなし")
# バックテスト実行
backtest_vwap_strategy(df)
実際に動かしてみた感想と注意点
トヨタで試してみたところ、直近5日間で4〜6回のシグナルが出た。勝率は6割前後といったところで、「まぁそんなもんか」という感じ。ただし注意点がいくつかある:
①yfinanceの分足データは遅延がある
yfinanceは無料なので、1分足データには15〜20分の遅延がある場合がある。リアルタイムトレードには使えない。バックテスト目的と割り切ろう。
②日をまたいでVWAPをリセットしないと意味がない
VWAPは当日のデータのみで計算する。前日のデータを引き継いで計算すると全然違う数値になるので注意。上のコードではgroupby(df_raw.index.date)で日ごとにリセットしている。
③製造業株は出来高が時間帯によって偏る
トヨタなどの大型株は寄り付き(9:00〜9:30)と大引け前(14:30〜15:30)に出来高が集中する。この時間帯はVWAPが激しく動くので、シグナルの信頼性が落ちやすい。
まとめ+Ken個人の感想
VWAPは「その日の相場の重心」を教えてくれる指標として、テクニカル初心者でも理解しやすいと思う。RSIやMACDと違って「当日のコンテキスト」を反映してくれるのが強みだ。
個人的な感想としては、VWAPを単体で使うよりも「VWAPのどちら側にいるか」をRSIのフィルターと組み合わせると精度が上がりそうな気がしている。次は「VWAP+RSIダブルフィルター」を試してみたい。製造業銘柄は出来高のクセがあるので、業種別にチューニングが必要かもしれない。。。

