投資家が知っておくべき「標準偏差」の使い方 — ボラティリティを数字で掴む

ボラティリティを数字で掴む 基礎知識・戦略

「ボラティリティが高い銘柄は怖い」——感覚としては分かるのですが、「じゃあどのくらい高いと怖いの?」と聞かれて、僕は何も答えられませんでした。標準偏差という言葉は知っていても、それが自分の売買のどこに効いてくるのかが分かっていなかったからです。

転機になったのは、標準偏差を「何株買うか」と「どこで損切るか」に直結させたときでした。それまで100株単位で何となく買っていたものが、「この銘柄はボラが大きいから50株」「こちらは静かだから200株」と根拠を持って決められるようになった。損切り幅も、キリのいい数字ではなく銘柄ごとの値動きに合わせて設定できるようになりました。

この記事は、標準偏差の計算方法そのものではなく「計算したあと、その数字を何に使うのか」に絞って書きます。計算の理屈や年率換算のやり方は標準偏差を使って投資リスクを数値化する方法にまとめてあるので、そちらとあわせて読んでいただけると流れがつかめるはずです。

※本記事のコードや情報は執筆時点の仕様に基づいています。投資は自己責任であり、必ずデモ環境や少額資金でテストした上で運用してください。

結論:標準偏差は「枚数」と「損切り幅」を決める道具

先に結論を書きます。個人投資家が標準偏差を使う場面は、実質的に次の3つに集約されます。

場面やること効果
ポジションサイズボラが大きい銘柄ほど株数を減らすどの銘柄でも損失額が揃う
損切り幅の設定ノイズの範囲外に逆指値を置く無意味な損切りを減らす
銘柄の取捨選択リターンをボラで割って効率を見る割に合わない銘柄を外せる

この3つを実際に導入して変わったことを、先に挙げておきます。

  • 銘柄を変えても1回あたりの損失額がほぼ一定になり、想定外の大負けが消えた
  • 「刈られてから戻る」パターンが減り、損切り後に後悔する回数が明確に少なくなった
  • 買う前に株数と損切り価格が決まるので、エントリー時に迷わなくなった
  • 荒れた相場では自動的に株数が減るため、相場環境に合わせた調整が不要になった

逆に言えば、これ以外の用途で標準偏差を眺めても、あまり得るものはありません。「この銘柄の年率ボラは28%です」と分かったところで、それを株数や損切り価格に変換しなければ、行動は何も変わらないからです。僕がしばらく足踏みしていたのは、まさにこの変換をしていなかったせいでした。

この3つに共通しているのは、「銘柄ごとに違う値動きの大きさを、共通の物差しに載せ替える」という発想です。トヨタの100円とマザーズ銘柄の100円ではまったく意味が違いますが、「日次ボラの何倍か」に直せば同じ土俵で比べられます。

この記事では、実際に手を動かせるコードを添えながら3つを順に見ていきます。難しい統計の話は出てきません。使うのは標準偏差の計算1つだけで、あとは掛け算と割り算です。

📘 外部参考Volatility(Investopedia)Position Sizing(Investopedia)

準備:日々の値動きを数字にする

まず、使い回せる形で計算部分を用意しておきます。ここは道具なので、一度書いたら中身は忘れて構いません。

pip install yfinance pandas numpy
import yfinance as yf
import numpy as np
import pandas as pd


def get_volatility(ticker, window=20, period="6mo"):
    """直近window日の値動きから、日次・年率のボラティリティを返す"""
    df = yf.download(ticker, period=period, auto_adjust=True, progress=False)
    ret = df["Close"].pct_change().dropna()

    daily = float(ret.tail(window).std())
    return {
        "ticker": ticker,
        "price": float(df["Close"].iloc[-1]),
        "daily_vol": daily,                       # 1日の標準的な変動率
        "annual_vol": daily * np.sqrt(252),       # 年率換算
        "daily_yen": float(df["Close"].iloc[-1]) * daily,  # 1日の変動幅(円)
    }


v = get_volatility("7203.T")
print(f"株価          : {v['price']:>10,.1f} 円")
print(f"日次ボラ      : {v['daily_vol'] * 100:>10.2f} %")
print(f"年率ボラ      : {v['annual_vol'] * 100:>10.1f} %")
print(f"1日の変動幅   : {v['daily_yen']:>10,.1f} 円")

ここで一番使うのは、実は最後の「1日の変動幅(円)」です。パーセントのままだと実感が湧きませんが、「この銘柄は普通の日でも±60円くらい動く」と円で出ると、一気に判断材料になります。

窓を20日にしているのは、直近1か月程度の値動きが今の実力に近いと考えているからです。1年分をまとめて平均すると、半年前の穏やかだった時期に引きずられます。逆に5日など短すぎると、たまたま大きく動いた1日に振り回されます。20日前後が扱いやすい落としどころでした。

使い方1:損失額を揃えるように株数を決める

いちばん効果が大きかったのがこれです。それまでの僕は、値がさ株もそうでない株も「とりあえず100株」で買っていました。当然ながら、銘柄によって背負っているリスクがバラバラだったわけです。

考え方はシンプルで、1回のトレードで許容する損失額を先に決め、それを銘柄の変動幅で割るだけです。

def position_size(capital, risk_pct, price, daily_vol, stop_sigma=2.0,
                  unit=100):
    """許容損失額から購入株数を逆算する

    capital    : 総資金
    risk_pct   : 1トレードで許容する損失の割合(0.01 = 1%)
    stop_sigma : 何σ分の逆行で損切るか
    unit       : 売買単位(日本株は通常100株)
    """
    risk_yen = capital * risk_pct              # 許容できる損失額
    stop_width = price * daily_vol * stop_sigma  # 損切りまでの値幅(円)
    if stop_width <= 0:
        return 0, 0.0, 0.0

    raw = risk_yen / stop_width                # 理論上の株数
    shares = int(raw // unit) * unit           # 売買単位に丸める
    return shares, stop_width, shares * stop_width


CAPITAL, RISK = 3_000_000, 0.01   # 資金300万円、1回1%まで

for code, name in [("7203.T", "トヨタ"), ("6758.T", "ソニー"),
                   ("6501.T", "日立"), ("9432.T", "NTT")]:
    v = get_volatility(code)
    shares, width, loss = position_size(
        CAPITAL, RISK, v["price"], v["daily_vol"])
    print(f"{name:<6s} 株価{v['price']:>8,.0f}円 "
          f"日次ボラ{v['daily_vol'] * 100:>4.1f}%  "
          f"→ {shares:>5,d}株  損切り幅{width:>6,.0f}円  "
          f"想定損失{loss:>8,.0f}円")

実行すると、どの銘柄を選んでも想定損失額がほぼ3万円(資金の1%)に揃うのが確認できます。ボラが大きい銘柄は自動的に株数が減り、静かな銘柄は多めに買える。これが「リスクを揃える」ということです。

従来(一律100株)ボラ基準
荒い銘柄損失が大きくなりがち株数を減らして損失を抑える
静かな銘柄資金を眠らせている株数を増やして機会を活かす
ポートフォリオ全体特定銘柄にリスクが偏る各銘柄が同じだけ効く

この考え方は適切な購入株数とリスクを自動計算する方法でも扱っていますが、そこでは損切り幅を自分で決める前提でした。ボラティリティを使うと、その損切り幅すら銘柄ごとに自動で決まるのが違いです。

使い方2:ノイズの外側に損切りを置く

「損切りは−5%」のような固定ルールを使っていた頃、僕は刈られてから戻るという展開を何度も食らいました。原因は単純で、その銘柄にとって5%は「ただの普通の値動き」だったからです。

日次ボラが2.5%の銘柄なら、2σ=5%の逆行は20営業日に1回くらいは普通に起こります。つまり損切りではなく、ノイズに引っかかっていただけでした。

def stop_levels(price, daily_vol, entry_side="long"):
    """σごとの損切り候補と、そこに到達する目安の頻度を表示する"""
    print(f"エントリー価格: {price:,.1f} 円  (日次ボラ {daily_vol * 100:.2f}%)")
    print(f"{'倍率':>6s} {'値幅':>10s} {'損切り価格':>12s} {'逆行率':>8s} {'目安頻度':>14s}")

    freq = {1.0: "3日に1回", 1.5: "約7日に1回",
            2.0: "約20日に1回", 2.5: "約80日に1回", 3.0: "約1年に1回"}
    for sigma in (1.0, 1.5, 2.0, 2.5, 3.0):
        width = price * daily_vol * sigma
        stop = price - width if entry_side == "long" else price + width
        print(f"{sigma:>5.1f}σ {width:>10,.1f} {stop:>12,.1f} "
              f"{width / price * 100:>7.2f}% {freq[sigma]:>14s}")


v = get_volatility("7203.T")
stop_levels(v["price"], v["daily_vol"])

この表を見ると、どこに損切りを置けば「ただの揺れ」で刈られずに済むかが一目で分かります。僕の場合、スイングで持つときは2.5σ、短期なら1.5σを目安にしています。

σを大きくすればいいわけではない

ここで注意が必要です。「刈られたくないから3σにしよう」と考えると、1回あたりの損失が大きくなり、その分だけ株数を減らさざるを得ません。損切り幅と株数はトレードオフの関係にあります。

v = get_volatility("7203.T")
print(f"{'損切り':>7s} {'株数':>8s} {'投資額':>12s} {'想定損失':>10s}")
for sigma in (1.0, 1.5, 2.0, 2.5, 3.0):
    shares, width, loss = position_size(
        CAPITAL, RISK, v["price"], v["daily_vol"], stop_sigma=sigma)
    print(f"{sigma:>6.1f}σ {shares:>8,d} {shares * v['price']:>12,.0f} "
          f"{loss:>10,.0f}")

実行すると、σを広げるほど株数が減り、投資額そのものが小さくなることが分かります。損失額は一定に保たれますが、その代わり値上がりしたときの利益も小さくなる。「刈られにくさ」と「リターンの大きさ」を天秤にかける作業だと理解しておくと、σの決め方に納得がいきます。

使い方3:割に合わない銘柄をふるい落とす

3つめは銘柄選びです。リターンだけを見ていると、大きく上がったが同じくらい荒い銘柄を掴みがちになります。ボラティリティで割って効率を見ると、印象がだいぶ変わります。

WATCHLIST = {
    "7203.T": "トヨタ自動車", "6758.T": "ソニーG", "6501.T": "日立製作所",
    "9432.T": "NTT", "8306.T": "三菱UFJ", "6098.T": "リクルート",
}

rows = []
for code, name in WATCHLIST.items():
    px = yf.download(code, period="1y", auto_adjust=True,
                     progress=False)["Close"]
    ret = px.pct_change().dropna()
    ann_ret = float(ret.mean()) * 252
    ann_vol = float(ret.std()) * np.sqrt(252)
    rows.append({
        "銘柄": name,
        "年率リターン": round(ann_ret * 100, 1),
        "年率ボラ": round(ann_vol * 100, 1),
        "効率": round(ann_ret / ann_vol, 2),
    })

df = pd.DataFrame(rows).sort_values("効率", ascending=False)
print(df.to_string(index=False))

「効率」の列が高い銘柄は、少ない揺れで着実に上がってきた銘柄です。逆にリターンは高いのに効率が低い銘柄は、保有中に大きな含み損を経験する可能性が高いということになります。

ここで無リスク金利を引いたものが、いわゆるシャープレシオです。厳密な評価をしたい場合はシャープレシオを使って投資戦略を評価する方法を参照してください。銘柄選びの一次スクリーニングとしては、この簡易版でも十分に機能します。

3つをまとめて1つのツールにする

毎回コードを書き直すのは面倒なので、銘柄コードを渡せば売買計画が出てくる形にまとめました。実際に僕が使っているものを簡略化したものです。

def trade_plan(ticker, capital=3_000_000, risk_pct=0.01,
               stop_sigma=2.0, window=20):
    """銘柄コードから売買計画(株数・損切り・利確)を作る"""
    v = get_volatility(ticker, window=window)
    price, dv = v["price"], v["daily_vol"]

    shares, width, loss = position_size(capital, risk_pct, price, dv,
                                        stop_sigma=stop_sigma)
    if shares == 0:
        return f"{ticker}: 単元に届かないため見送り"

    stop = price - width
    take1 = price + width * 1.5    # リスクリワード 1:1.5
    take2 = price + width * 2.5    # リスクリワード 1:2.5

    return "\n".join([
        f"【{ticker}】 株価 {price:,.1f} 円  日次ボラ {dv * 100:.2f}%",
        f"  購入株数   : {shares:,d} 株  (投資額 {shares * price:,.0f} 円)",
        f"  損切り     : {stop:,.1f} 円  (-{width:,.1f} 円 / 想定損失 {loss:,.0f} 円)",
        f"  利確1      : {take1:,.1f} 円  (+{width * 1.5:,.1f} 円)",
        f"  利確2      : {take2:,.1f} 円  (+{width * 2.5:,.1f} 円)",
        f"  資金に占める割合: {shares * price / capital * 100:.1f} %",
    ])


for code in ["7203.T", "6758.T", "9432.T"]:
    print(trade_plan(code))
    print("-" * 58)

ポイントは、利確目標も損切り幅を基準に決めているところです。「1,000円上がったら売る」ではなく「損切り幅の2.5倍上がったら売る」とすることで、銘柄が変わってもリスクリワードの比率が一定に保たれます。

最後の「資金に占める割合」も地味に重要です。ボラが極端に小さい銘柄では、計算上の株数が大きくなりすぎて1銘柄に資金の大半を突っ込む形になることがあります。そうなっていないかを毎回目視で確認しています。

一律100株と、どれくらい差が出るのか検証する

「理屈は分かったが、本当に効果があるのか」が気になったので、簡単な検証をしてみました。同じ売買シグナルを使い、株数の決め方だけを変えて1年分を比較します。

import yfinance as yf
import numpy as np
import pandas as pd

TICKERS = ["7203.T", "6758.T", "6501.T", "9432.T", "8306.T"]
CAPITAL, RISK, SIGMA = 3_000_000, 0.01, 2.0

results = []
for code in TICKERS:
    df = yf.download(code, period="2y", auto_adjust=True, progress=False)
    df["ret"] = df["Close"].pct_change()
    df["vol"] = df["ret"].rolling(20).std()

    # シグナル: 25日移動平均を上抜けたら買い、下抜けたら手仕舞い
    df["ma"] = df["Close"].rolling(25).mean()
    df["signal"] = (df["Close"] > df["ma"]).astype(int).shift(1)  # 翌日から反映
    df = df.dropna()

    # A: 常に100株
    pnl_fixed = (df["ret"] * df["Close"].shift(1) * 100 * df["signal"]).sum()

    # B: ボラティリティ基準で株数を決める
    shares = ((CAPITAL * RISK) /
              (df["Close"].shift(1) * df["vol"] * SIGMA)).fillna(0)
    shares = (shares // 100 * 100).clip(upper=2000)   # 単元丸め+上限
    pnl_vol = (df["ret"] * df["Close"].shift(1) * shares * df["signal"]).sum()

    # 日次損益の振れ幅も比べる
    daily_fixed = df["ret"] * df["Close"].shift(1) * 100 * df["signal"]
    daily_vol = df["ret"] * df["Close"].shift(1) * shares * df["signal"]

    results.append({
        "銘柄": code,
        "一律100株_損益": round(float(pnl_fixed)),
        "ボラ基準_損益": round(float(pnl_vol)),
        "一律_日次std": round(float(daily_fixed.std())),
        "ボラ基準_日次std": round(float(daily_vol.std())),
    })

df_res = pd.DataFrame(results)
print(df_res.to_string(index=False))
print("\n=== 合計 ===")
print(f"一律100株   損益 {df_res['一律100株_損益'].sum():>10,} 円  "
      f"日次std平均 {df_res['一律_日次std'].mean():>8,.0f} 円")
print(f"ボラ基準     損益 {df_res['ボラ基準_損益'].sum():>10,} 円  "
      f"日次std平均 {df_res['ボラ基準_日次std'].mean():>8,.0f} 円")

ここで見るべきは損益の額ではなく、銘柄ごとの「日次std(損益の振れ幅)」がどれだけ揃っているかです。一律100株では銘柄によって振れ幅がバラバラになりますが、ボラ基準ではほぼ横並びになります。

観点一律100株ボラティリティ基準
銘柄ごとの損益の振れ幅バラバラほぼ揃う
荒れた時期の損失そのまま拡大する株数が減るため抑制される
1銘柄の失敗の影響大きくなりうるあらかじめ上限が決まる
資金効率静かな銘柄で遊ばせる静かな銘柄でも働かせる

この検証はあくまで簡易なもので、手数料もスリッページも入っていません。実際の成績を語るには不十分ですが、「株数の決め方だけでリスクの分布がここまで変わる」ことを確認するには十分でした。より現実的な検証をしたい場合は、手数料・スリッページをPythonで組み込む方法を参考にコストを入れてみてください。

ちなみに .shift(1) を入れているのは、その日の終値を見て同じ日に売買するという不可能な取引を防ぐためです。ここを忘れると成績が不自然に良く出ます。僕も一度やらかしました。

実際に使ってみて気づいた注意点

決算またぎでは通用しない

これは痛い目に遭って学びました。平常時のボラから計算した2σの損切りは、決算発表の翌日には何の意味もありません。決算をまたぐと、平常時の5σ〜10σ相当のギャップが普通に開きます。

# 保有予定期間に決算をまたぐかを事前に確認する
import datetime as dt

def check_earnings(ticker, holding_days=10):
    t = yf.Ticker(ticker)
    try:
        cal = t.calendar
        date = cal.get("Earnings Date") if isinstance(cal, dict) else None
        if not date:
            return "決算日を取得できませんでした(手動確認を推奨)"
        d = date[0] if isinstance(date, list) else date
        days = (d - dt.date.today()).days
        if 0 <= days <= holding_days:
            return f"⚠ {days}日後に決算。保有期間と重なります"
        return f"決算まで {days} 日。保有期間とは重なりません"
    except Exception as e:
        return f"取得失敗: {e}"


print(check_earnings("7203.T"))

決算をまたぐなら、株数を半分にするか、そもそも見送る。これをルールにしてから、想定外の大負けがなくなりました。

ボラが急に変わったら計算し直す

エントリー時に計算したボラは、あくまでその時点の値です。相場が荒れ始めると、同じ2σでも値幅がまったく変わります。保有中も定期的に再計算し、想定より荒くなっていたら株数を減らすようにしています。

# エントリー時と現在のボラを比べる
entry_vol = 0.0180   # エントリー時に記録しておいた値
now = get_volatility("7203.T")["daily_vol"]

ratio = now / entry_vol
print(f"エントリー時 {entry_vol * 100:.2f}% → 現在 {now * 100:.2f}%  ({ratio:.2f}倍)")
if ratio > 1.5:
    print("⚠ ボラが1.5倍以上に拡大。ポジション縮小を検討してください")

銘柄を増やしすぎると効かなくなる

「1銘柄あたり1%」を守っていても、10銘柄同時に持てば全体では10%のリスクを背負っています。しかも同じ業種ばかりだと、下げるときは全部まとめて下げます。この点は相関係数を計算して銘柄間の関係を数値化する方法で確認できます。同時保有は5銘柄まで、というのが僕の落としどころです。

ストップ高・ストップ安では逆指値が機能しない

計算上の損切り価格を設定していても、売買が成立しなければ損切りは実行されません。ストップ安に張り付いた銘柄は、翌日以降さらに下で寄り付くことがあります。ボラティリティ基準の損切りは「普通の相場での話」であり、想定外の事態には無力だと割り切っておく必要があります。

よく聞かれること

許容損失は資金の何%にすべきか

よく言われるのは1%ですが、これは「20連敗しても資金の8割が残る」水準として理にかなっています。連敗の耐性から逆算すると、次のようになります。

  • 0.5%: かなり保守的。連敗しても精神的に耐えやすいが、資金の増え方は緩やか
  • 1%: 標準的。10連敗で約10%の減少に収まる
  • 2%: 攻めの設定。10連敗で約18%減。勝率に確信がないなら避けたい
  • 3%以上: 連敗時の回復が難しくなる。個人的にはおすすめしない

僕は1%から始めて、しばらく運用して成績が安定してから調整するのがよいと思っています。最初から2%で始めて、最初の10トレードが不調だった場合、そこで心が折れてしまうからです。

窓は20日でよいのか

保有期間に合わせるのが基本です。「持つ期間と同じくらいの窓で測る」と考えると分かりやすくなります。

保有期間窓の目安特徴
数日(短期)10日直近の変化に敏感。ぶれやすい
1〜3週間(スイング)20日バランスがよい。標準的な選択
1か月以上60日安定するが、環境変化への反応が遅い

ATRを使うのと何が違うのか

やっていることはほぼ同じです。違いは測り方で、ATRは高値・安値・前日終値から日中の値幅を直接測るのに対し、標準偏差は終値どうしの変化率のばらつきを測ります。

  • ATRが向く場面: 日中の値幅を重視する短期売買。窓(ギャップ)も拾える
  • 標準偏差が向く場面: 終値ベースで判断するスイング以上。他の統計指標と接続しやすい

どちらでも構いません。大切なのは片方に決めて使い続けることで、日によって使い分けると損切り幅の一貫性が失われます。

単元未満株なら、もっと細かく調整できるのか

できます。100株単位の制約があると、特に値がさ株では計算上の株数と実際に買える株数が大きくずれます。単元未満株(S株・かぶミニなど)を使えば1株単位で調整でき、リスクをより正確に揃えられます。

# 単元制約でどれだけズレるかを確認する
v = get_volatility("9983.T")   # 値がさ株の例
for unit in (100, 1):
    shares, width, loss = position_size(
        CAPITAL, RISK, v["price"], v["daily_vol"], unit=unit)
    target = CAPITAL * RISK
    print(f"売買単位{unit:>4d}株 → {shares:>6,d}株  想定損失 {loss:>9,.0f} 円 "
          f"(目標 {target:,.0f} 円 / 乖離 {abs(loss - target) / target * 100:>5.1f}%)")

ただし単元未満株は手数料率が高めだったり、リアルタイムで約定しなかったりする制約があります。リスク調整の精度と取引コストのトレードオフとして考えてください。

まとめ

標準偏差を「計算して終わり」から「売買判断に使う」に変えるための実践的な使い方をまとめました。

  • 使い道は株数・損切り幅・銘柄選びの3つ。これ以外で眺めても行動は変わらない
  • 許容損失額 ÷(株価 × 日次ボラ × σ)で株数が自動的に決まる
  • 損切りはノイズの外側へ。日次ボラの2σ前後なら、普通の揺れでは刈られにくい
  • σを広げれば刈られにくくなるが、その分株数が減って利益も小さくなる
  • 利確目標も損切り幅の倍数で決めると、リスクリワードが銘柄間で揃う
  • 決算またぎでは通用しない。株数を半分にするか見送る
  • ボラは変化する。保有中も再計算し、1.5倍以上に拡大したら縮小を検討する

個人的にいちばん効果を感じたのは、「損切りされたときの気分」が変わったことでした。以前は損切りのたびに「読みが外れた」と落ち込んでいましたが、いまは「想定の範囲内の1%」として処理できます。金額があらかじめ決まっているというだけで、こんなに違うのかと驚きました。

もう1つ、副次的な効果もありました。株数が自動的に決まるようになったことで、「なんとなく多めに買う」ができなくなったのです。自信があるときほど大きく張りたくなるものですが、その「自信」に根拠がないことのほうが多い。計算式が間に入るだけで、その衝動が止まります。

まずは自分がいま持っている銘柄で、日次ボラを計算してみてください。「この銘柄は普通の日でも±80円動く」と分かるだけで、無駄な損切りは確実に減らせます。そのうえで本記事の trade_plan() を回せば、次のエントリーからは株数と損切り価格が自動で出てくるようになります。

タイトルとURLをコピーしました