見出し画像

「買うべきか、売るべきか」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.6

STEP3:現在値の取得(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%)

売りシグナル
  ー該当なし

ここから先は

11,617字

¥ 300

Amazon Payで支払うと最大2%還元のチャンス! 9/30まで

この記事は noteマネー にピックアップされました

noteマネーのバナー

よろしければ応援お願いします! いただいたチップはクリエイターとしての活動費に使わせていただきます!