「買うべきか、売るべきか」VWAP割安率と分位点分析で客観的に判断する方法 #24
株式アプリや証券会社のサービスでは、保有株の状況は確認できても、「過去に保有していた株」まで含めてまとめて見られる機能は意外と少ない。
「もしこれを一括で俯瞰できれば、売買判断のヒントになるかもしれない」──そんな思いから、Google Colab 上で仕組みを実装してみました。
今回の基礎ロジックは VWAP(数量加重平均価格) と 分布処理(分位点分析) です。
VWAPとは?
Volume Weighted Average Price の略で、株式市場では「その日の平均的な約定価格」を表す指標。これを自分の売買履歴に当てはめれば、「自分がどのくらいの単価で買い集めてきたか」を示す “マイVWAP” が算出できます。分布処理とは?
統計的に「中央値」「25%点(P25)」「75%点(P75)」といった分位点を計算する方法。自分の買付単価の分布を可視化すれば、現在値が安値圏か高値圏かを客観的に把握できます。
この二つを組み合わせることで、「現在値が自分の買付履歴と比べてどれだけ割安/割高か」 を一覧化することができます。
今回の実装は、前回紹介した 売買履歴スクリプト をベースにしています。
保有株と過去株の履歴を分析する
前回の記事では「売買履歴から保有株サマリーを作成」しました。
今回はさらに一歩進めて、過去の買付・売却履歴も含めて銘柄ごとの現在状況を俯瞰できる参照テーブルを作ります。
ゴール
銘柄別に以下の情報を整理し、保有株・過去株を含めて一覧化します。
過去買付の統計(VWAP、中央値、P25/P75、最安/最高、回数、初回/最終日)
直近N日の買付統計(平均単価、回数、最終買付日)
売却統計(VWAP、中央値、初回/最終日)
現在値 vs 買付VWAP からの VWAP割安率(%)
分位帯位置(P25–P75の中で今どこにいるか)
保有フラグ(今持っているかどうか)
これを作ることで、次回の 「売買シグナル判定」 に直接つなげられる土台ができます。
前提
前回までのコードで、以下が準備できていること
df(売買履歴の行明細:日付, ティッカー, 銘柄名, 数量, 実効単価, side)
df_summary(保有サマリー:ティッカー, 銘柄名, 保有数, 現在値, 最終取引日)
to_yf_symbol() 関数
STEP1:売買履歴の読み込み
前回の記事では「Googleスプレッドシートに売買履歴を記録するところ」まで準備しました。
ここからはいよいよ、Colab でそのデータを自動処理していきます。
最初のステップは、スプレッドシートから売買履歴を読み込むところです。
データ準備
Googleスプレッドシートに「売買履歴」というシートを用意しておきます。
カラムの例はこんな感じです 👇
実装コード(Colab)
まずは必要なライブラリをインポートして、スプレッドシートからデータを読み込みます。
# STEP1: Googleスプレッドシートから「売買履歴」を読み込む
import pandas as pd
import gspread
from google.colab import auth
from google.auth import default
from gspread_dataframe import get_as_dataframe
# 🔑 Google認証(初回実行時に認証画面が表示されます)
auth.authenticate_user()
creds, _ = default()
gc = gspread.authorize(creds)
# スプレッドシートのURLを指定
SPREADSHEET_URL = "https://docs.google.com/spreadsheets/d/XXXXX/edit" # ご自身のURLに置き換え
spreadsheet = gc.open_by_url(SPREADSHEET_URL)
# 「売買履歴」シートを開く
sheet_ticker_list = spreadsheet.worksheet("売買履歴")
# pandas.DataFrame に変換
ticker_data = sheet_ticker_list.get_all_values()
df = pd.DataFrame(ticker_data[1:], columns=ticker_data[0]) # 1行目をヘッダーに設定
print("✅ 売買履歴を読み込みました")
display(df.head())
出力イメージ
実行すると、以下のようにスプレッドシートの内容が DataFrame として読み込まれます。
約定日 コード 売買 数量 単価 手数料 税金 証券口座 口座区分 備考 ティッカー 銘柄名
0 2021-01-06 5301 買付 3 1265 0 0 LINE 特定 5301.T Tokai Carbon Co., Ltd.
1 2021-01-20 8306 買付 7 494.6 0 0 SBI 特定 8306.T Mitsubishi
UFJ Financial Group, Inc.
STEP2:データ整形(買付/売却の判別と実効単価計算)
売買履歴をそのまま読み込んだだけでは、損益計算やサマリー化に必要な情報が足りません。
そこで次の整形処理を行います。
「買付/売却」を +1 / -1 の数値に変換
手数料・税金を補正して 取引総額 を計算
そこから 実効単価(費用込みの実際の単価) を算出
こうすることで、後続の集計処理がスムーズになります。
実装コード(Colab)
# STEP2: データ整形(買付/売却の判別と実効単価の計算)
import numpy as np
# ---- 売買ラベルを数値に変換 ----
buy_aliases = {"買付","買い","買","buy","購入","long","buying"}
sell_aliases = {"売付","売り","売","sell","売却","short","selling"}
def side_to_sign(x: object) -> int:
"""売買ラベルを +1 (買い) / -1 (売り) に変換"""
s = str(x).strip().lower()
if s in buy_aliases:
return 1
if s in sell_aliases:
return -1
# 未知ラベルを検知した場合はエラー
raise ValueError(f"未知の売買ラベル: {x} |想定={sorted(buy_aliases | sell_aliases)}")
# 新しい列 "side" を追加
df["side"] = df["売買"].apply(side_to_sign)
# ---- 手数料・税金を0で埋める(欠損値対策)----
for c in ["手数料", "税金"]:
if c not in df.columns:
df[c] = 0.0
df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0.0)
# ---- 取引総額を計算 ----
# 買い: 単価×数量 + 手数料 + 税金
# 売り: 単価×数量 - 手数料 - 税金
df["数量"] = pd.to_numeric(df["数量"], errors="coerce").fillna(0).astype(float)
df["単価"] = pd.to_numeric(df["単価"], errors="coerce").fillna(0).astype(float)
df["取引総額"] = (
df["単価"] * df["数量"]
+ np.where(df["side"]==1, df["手数料"] + df["税金"], -(df["手数料"] + df["税金"]))
)
# ---- 実効単価を計算 ----
# 数量=0 の場合は NaN にする
df["実効単価"] = df["取引総額"] / df["数量"].replace(0, np.nan)
print("✅ STEP2 完了(売買判別・実効単価の計算)")
display(df.head(10))
出力イメージ
新たに以下の列が追加されます:
side:買付=+1 / 売却=-1
取引総額:数量×単価+手数料+税金(売りは逆に控除)
実効単価:手数料・税金込みの実質単価
約定日 コード 売買 数量 単価 手数料 税金 side 取引総額 実効単価
0 2021-01-06 5301 買付 3 1265 0 0 1 3795.0 1265.0
1 2021-01-20 8306 買付 7 494.6 0 0 1 3462.2 494.6STEP3:現在値の取得(yfinance)
売買履歴を整形したら、最新の株価を取り込みます。
ここでは yfinance を使い、info → fast_info → 1分足 → 日足 の順にフォールバックして、欠損を最小化します。さらに 前日終値 も取得し、現在値が欠損・ゼロのときは 前日終値で補完 します。
実装コード(Colab)
# STEP3: yfinance から現在値・前日終値を取得し、df_summary に反映
import time
import pandas as pd
import numpy as np
import yfinance as yf
from datetime import datetime
import pytz
# ▼ 任意:ティッカー整形(.T 付与や例外置換が必要な場合)
TICKER_OVERRIDE = {
# "旧コード.T": "新コード.T",
}
def to_yf_symbol(tkr: str) -> str:
t = str(tkr).strip()
t = t if t.endswith(".T") else f"{t}.T"
return TICKER_OVERRIDE.get(t, t)
# ▼ 安全な現在値取得(優先度: info → fast_info → 1分足 → 日足)
def safe_current_price(sym: str) -> float:
tk = yf.Ticker(sym)
# 1) info(精度高め)
try:
info = tk.info # dict
p = info.get("regularMarketPrice") or info.get("currentPrice")
if p is not None:
return float(p)
except Exception:
pass
# 2) fast_info(高速)
try:
fi = tk.fast_info # SimpleNamespace 風 or dict
lp = getattr(fi, "last_price", None) if not isinstance(fi, dict) else fi.get("last_price")
if lp is not None:
return float(lp)
except Exception:
pass
# 3) 1分足(場中の直近値)
try:
m1 = tk.history(period="1d", interval="1m", auto_adjust=False, raise_errors=False)
if m1 is not None and not m1.empty:
last = m1["Close"].dropna()
if not last.empty:
return float(last.iloc[-1])
except Exception:
pass
# 4) 直近日足(実質 前日終値相当)
try:
d1 = tk.history(period="5d", interval="1d", auto_adjust=False, raise_errors=False)
if d1 is not None and not d1.empty:
close = d1["Close"].dropna()
if not close.empty:
return float(close.iloc[-1])
except Exception:
pass
return float("nan")
# ▼ 前日終値(優先度: fast_info → 日足N-1)
def previous_close(sym: str) -> float:
tk = yf.Ticker(sym)
# 1) fast_info
try:
fi = tk.fast_info
pc = getattr(fi, "previous_close", None) if not isinstance(fi, dict) else fi.get("previous_close")
if pc is not None:
return float(pc)
except Exception:
pass
# 2) 日足(N-1)
try:
d1 = tk.history(period="10d", interval="1d", auto_adjust=False, raise_errors=False)
if d1 is not None and not d1.empty:
close = d1["Close"].dropna()
if len(close) >= 2:
return float(close.iloc[-2])
elif len(close) == 1:
return float(close.iloc[-1])
except Exception:
pass
return float("nan")
# ▼ df_summary への適用
# 数値列の型を保証
for c in ["保有数", "簿価", "実現損益", "平均取得額"]:
if c in df_summary.columns:
df_summary[c] = pd.to_numeric(df_summary[c], errors="coerce")
# yfinance 用シンボル列を用意
if "yf_symbol" not in df_summary.columns:
df_summary["yf_symbol"] = df_summary["ティッカー"].map(to_yf_symbol)
symbols = [s for s in df_summary["yf_symbol"].dropna().unique().tolist() if s]
# 価格取得(レート制限に配慮しつつ個別取得)
cur_map, prev_map, missing_syms = {}, {}, []
for sym in symbols:
time.sleep(0.05) # アクセス間隔
cur = safe_current_price(sym)
prev = previous_close(sym)
cur_map[sym] = cur
prev_map[sym] = prev
if (pd.isna(cur) or cur == 0) and (pd.isna(prev) or prev == 0):
missing_syms.append(sym)
# 反映
df_summary["現在値"] = df_summary["yf_symbol"].map(cur_map)
df_summary["前日終値"] = df_summary["yf_symbol"].map(prev_map)
# 現在値が NaN/0 の場合は前日終値で補完(見栄え改善)
df_summary["現在値"] = df_summary["現在値"].where(
df_summary["現在値"].notna() & (df_summary["現在値"] > 0),
df_summary["前日終値"]
)
# JST タイムスタンプ
jst = pytz.timezone("Asia/Tokyo")
df_summary["データ更新時刻(JST)"] = datetime.now(tz=jst).strftime("%Y-%m-%d %H:%M:%S")
print("✅ STEP3 完了(現在値・前日終値を付与)")
if missing_syms:
print("⚠ 価格未取得シンボル:", missing_syms)
cols_show = ["ティッカー","銘柄名","保有数","平均取得額","現在値","前日終値","データ更新時刻(JST)"]
display(df_summary[[c for c in cols_show if c in df_summary.columns]].head(20))
気にした点
多段フォールバックで欠損を最小化:info → fast_info → 1分足 → 日足
補完戦略:現在値が取れないときは 前日終値 で埋めてダッシュボードの空欄を回避
日本時間で更新時刻:データ更新時刻(JST) を付け、データの鮮度が一目で分かる
レート制限対策:time.sleep(0.05) を挟み、連続アクセスを抑制
STEP4:保有株サマリーの作成
ここまでで売買履歴の整形(STEP2)と現在値の取得(STEP3)が終わりました。
最後に、保有数・平均取得額・現在値・評価額・損益 をまとめた「保有株サマリー」を仕上げます。
JSTの更新時刻や列順の整えまで一気に行い、見やすい表にします。
実装コード(Colab)
# STEP4: 保有株サマリー(評価額・損益・時刻付与・列順調整)
import pandas as pd
import numpy as np
from datetime import datetime
import pytz
# --- 1) 数値列の型を保証(文字カンマ/記号混入にも対応) ---
def _to_numeric_safe(x):
if isinstance(x, str):
x = x.replace(",", "").replace("¥", "").replace("%", "")
return pd.to_numeric(x, errors="coerce")
for c in ["保有数", "平均取得額", "現在値", "前日終値", "簿価", "実現損益"]:
if c not in df_summary.columns:
df_summary[c] = 0.0
df_summary[c] = df_summary[c].map(_to_numeric_safe).fillna(0.0)
# --- 2) 評価系の計算 ---
df_summary["評価額"] = (df_summary["保有数"] * df_summary["現在値"]).round(2)
df_summary["含み損益"] = (df_summary["評価額"] - df_summary["簿価"]).round(2)
df_summary["トータル損益"] = (df_summary["実現損益"] + df_summary["含み損益"]).round(2)
# --- 3) JSTタイムスタンプ ---
jst = pytz.timezone("Asia/Tokyo")
df_summary["データ更新時刻(JST)"] = datetime.now(tz=jst).strftime("%Y-%m-%d %H:%M:%S")
# --- 4) 列順を整える(存在する列だけ採用) ---
preferred_order = [
"ティッカー", "銘柄名",
"証券口座", "口座区分", # あれば表示
"保有数", "平均取得額",
"現在値", "前日終値",
"簿価", "評価額",
"含み損益", "実現損益", "トータル損益",
"最終取引日", # あれば表示
"データ更新時刻(JST)"
]
cols = [c for c in preferred_order if c in df_summary.columns]
df_summary = df_summary.reindex(columns=cols)
# --- 5) 確認表示 ---
print("✅ STEP4 完了(保有株サマリーを作成)")
display(df_summary.head(20))
気にした点
型の安全化:計算前に数値列を一括で数値化(カンマ・通貨記号混在でもOK)。
丸め:表示の見やすさ優先で .round(2)。必要に応じて桁数を調整。
列順:ダッシュボードやスクショに載せやすい並びへ整理。
JSTの更新時刻:データの鮮度がすぐに分かる。
ここまでのまとめ(STEP1〜4)
STEP1:スプレッドシートから売買履歴を読み込み
STEP2:買い/売りの判別(±1)と、手数料・税金込みの実効単価を計算
STEP3:yfinance で現在値・前日終値を取得(欠損は前日終値で補完)
STEP4:評価額・損益を計算し、保有株サマリーを整形して完成
これで、日々の保有状況を自動で一覧化できる基礎が整いました。
ここまでは STEP1〜4 までを解説しました。
さらに一歩進めて、VWAPや分布処理を使った過去売買の分析テーブル(df_price_reference) まで作りたい方のために、完成版スクリプト(STEP1〜4の統合+STEP5付き) を有料部分にまとめています。
「読むよりすぐに動かして、自分で修正したい」「一括コードが欲しい」という方はぜひご活用ください。
なお、実装環境や読み込みデータの構成によっては挙動が異なる場合がありますので、必要に応じて微調整をお願いします。
関連記事
STEP5以降で実施期できる出力イメージ
買いシグナル
ー(8306) Mitsubishi UFJ Financial Group, Inc. (VWAP割安率: -12.3%)
追加買いシグナル
ー(5301) Tokai Carbon Co., Ltd. (VWAP割安率: -21.7%)
売りシグナル
ー該当なしここから先は
¥ 300
この記事は noteマネー にピックアップされました
よろしければ応援お願いします! いただいたチップはクリエイターとしての活動費に使わせていただきます!

