Pythonでシャープレシオを使って投資戦略を評価する方法|初心者向け

リスクに見合うリターンだったか 基礎知識・戦略

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

「年利30%の戦略」と「年利12%の戦略」、どちらが優れているでしょうか。この質問には答えられません。どれだけのリスクを取ってその成績を出したのかが分からないからです。年利30%でも途中で資産が半分になっているなら、年利12%で安定している戦略のほうが実用的です。

この「リスク1単位あたり、どれだけのリターンを稼いだか」を1つの数字にしたのがシャープレシオです。ファンドの運用報告書からアルゴリズムのバックテストまで、成績を比較する場面ではほぼ必ず登場します。

この記事では、Pythonでシャープレシオを計算する方法に加えて、この指標が見落とすもの数字を盛るのが意外と簡単だという事実まで踏み込みます。計算式を覚えるだけなら5分で終わりますが、正しく使うにはもう少し知っておくべきことがあります。

📘 外部参考Sharpe Ratio(Investopedia)シャープ・レシオ(Wikipedia)

リターンだけで判断してはいけない理由

まず、なぜリスク調整が必要なのかを具体例で確認します。次の2つの戦略を比べてみてください。

戦略A戦略B
年率リターン30%12%
年率ボラティリティ45%8%
最大ドローダウン−52%−9%
シャープレシオ0.641.38

数字だけ見れば戦略Aが魅力的ですが、途中で資産が半分になる過程に耐えられる人はほとんどいません。実際には多くの人が底値付近で撤退します。理論上のリターンと、実際に手にできるリターンが乖離するわけです。

もう1つ重要なのが、戦略Bはレバレッジをかけて戦略Aに追いつけるという点です。ボラティリティ8%の戦略を3倍で運用すれば、リターン36%・ボラティリティ24%となり、戦略Aより高リターンかつ低リスクになります。逆に戦略Aのリスクを下げる方法はありません。

つまり効率のよい戦略は後からいくらでも大きくできるが、非効率な戦略は改善できない。これがシャープレシオを重視する実務的な理由です。

もう1点、見落とされがちな観点があります。リターンは運の影響を強く受けますが、リスクは比較的安定しているということです。相場が良ければ誰でもリターンは出ますが、ボラティリティの大きさは戦略の性格そのものを反映します。

そのため、短い期間の成績を比べるときほど、リターン単体よりリスク調整後の数値のほうが実力を反映します。3か月だけ観察して「年利換算80%」と言われても意味はありませんが、その間のボラティリティが8%だったのか60%だったのかは、戦略の中身を確実に語っています。

計算式と、日本での無リスク金利

シャープレシオの定義はシンプルです。

シャープレシオ = (ポートフォリオのリターン − 無リスク金利) ÷ 標準偏差

分子は「リスクを取ったからこそ得られた超過分」です。何もせず預けておくだけで得られる金利分は、運用の実力ではないので差し引きます。分母はそのリスクの大きさです。

無リスク金利に何を使うか

日本では、短期国債の利回りや無担保コール翌日物レートが使われます。ただし個人の実務では、そこまで厳密に考える必要はありません

import numpy as np

ret, vol = 0.12, 0.18   # 年率リターン12%、ボラティリティ18%

for rf in (0.0, 0.001, 0.005, 0.01, 0.02):
    sharpe = (ret - rf) / vol
    print(f"無リスク金利 {rf * 100:>4.1f}% → シャープレシオ {sharpe:.3f}")

金利が0%から2%まで動いても、シャープレシオは0.67から0.56程度しか変わりません。比較したい対象の間で同じ値を使っていれば、順位は変わらないということです。低金利環境では0とみなす簡略化も広く行われています。

ただし、他人が公表した数値と自分の計算値を比べるときは注意してください。前提が違えば数字も変わります。可能なら自分で同じ条件で計算し直すのが確実です。

無リスク金利の選択肢をまとめると次のようになります。

  • 0%とする: 低金利下では実務的。自分の戦略同士を比べるだけなら十分
  • 普通預金金利: 「銀行に置いておく代わりに運用する」という発想に近い
  • 短期国債利回り: 教科書的に最も正しい。海外の数値と比べるならこれ
  • 信用取引の金利: レバレッジをかける前提なら、調達コストを引くという考え方もある

本記事では0.5%を例として使いますが、大事なのは選んだ値を記録して一貫させることです。半年後に自分の過去の数値と比べるとき、前提を忘れていると比較になりません。

Pythonで計算する

pip install yfinance pandas numpy matplotlib

再利用しやすい形で関数にしておきます。

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

TRADING_DAYS = 252


def sharpe_ratio(returns, risk_free=0.0, periods=TRADING_DAYS):
    """日次リターン列から年率シャープレシオを計算する

    returns   : 日次リターン(小数。0.01 = 1%)
    risk_free : 年率の無リスク金利
    """
    r = pd.Series(returns).dropna()
    if r.std() == 0 or len(r) < 2:
        return np.nan

    excess = r - risk_free / periods        # 日次ベースの超過リターン
    return float(excess.mean() / r.std() * np.sqrt(periods))


px = yf.download("7203.T", period="3y", auto_adjust=True,
                 progress=False)["Close"]
ret = px.pct_change().dropna()

print(f"年率リターン       : {float(ret.mean()) * 252 * 100:>6.2f} %")
print(f"年率ボラティリティ : {float(ret.std()) * np.sqrt(252) * 100:>6.2f} %")
print(f"シャープレシオ     : {sharpe_ratio(ret, risk_free=0.005):>6.3f}")

ポイントは年率換算の掛け方です。分子(超過リターンの平均)は252倍、分母(標準偏差)は√252倍されるため、シャープレシオ全体としては√252を掛ける形になります。ここを間違えて252倍してしまうと、値が約16倍に膨らみます。

複数銘柄・複数戦略を並べる

TICKERS = {
    "7203.T": "トヨタ自動車", "6758.T": "ソニーG", "6501.T": "日立製作所",
    "9432.T": "NTT", "8306.T": "三菱UFJ", "^N225": "日経平均",
}
RF = 0.005

rows = []
for code, name in TICKERS.items():
    r = yf.download(code, period="3y", auto_adjust=True,
                    progress=False)["Close"].pct_change().dropna()
    cum = float((1 + r).prod()) - 1
    rows.append({
        "銘柄": name,
        "累積リターン": round(cum * 100, 1),
        "年率リターン": round(float(r.mean()) * 252 * 100, 1),
        "年率ボラ": round(float(r.std()) * np.sqrt(252) * 100, 1),
        "シャープ": round(sharpe_ratio(r, RF), 2),
    })

df = pd.DataFrame(rows).sort_values("シャープ", ascending=False)
print(df.to_string(index=False))

累積リターンの順位とシャープレシオの順位が一致しないことがよくあります。「いちばん儲かった銘柄」と「いちばん効率がよかった銘柄」は別物だと分かる瞬間です。

数値の目安と、その受け止め方

一般的に使われる評価の目安は次のとおりです。

シャープレシオ評価実感としての意味
0未満論外預金以下。リスクを取る意味がない
0〜0.5効率が悪い値動きの割に報われていない
0.5〜1.0標準的市場平均と同程度
1.0〜2.0優秀個人の自作戦略ならまず十分
2.0以上非常に優秀まず過学習を疑うべき水準

ここで強調したいのが最後の行です。バックテストでシャープレシオ3.0が出たとき、それは「素晴らしい戦略を見つけた」ではなく「どこかで未来の情報を使っている」と考えるのが健全です。

  • 当日終値でシグナルを出し、当日終値で約定している(ルックアヘッドバイアス)
  • 手数料とスリッページを入れていない
  • パラメータを過去データに合わせ込んでいる(カーブフィッティング)
  • 上場廃止銘柄を除いたリストで検証している(生存者バイアス)

1つめは特に多い落とし穴です。詳しくはルックアヘッドバイアスをPythonで排除する方法で扱っています。コストの入れ方は手数料・スリッページをPythonで組み込む方法を参照してください。

参考までに、実在する運用者の水準感を挙げておきます。世界最高峰とされるクオンツファンドでも、長期にわたって安定的に2.0を超え続けている例はごく限られます。個人が自作した戦略で3.0が出たなら、それは新発見ではなく検証ミスと考えるほうが、圧倒的に確率の高い解釈です。

逆に、0.8〜1.2あたりの数字が出たときこそ真剣に検討する価値があります。地味に見えますが、この水準が実運用でも維持できるなら十分に戦えます。派手な数字ほど疑い、地味な数字ほど検証する——これが健全な向き合い方です。

シャープレシオは意外と簡単に「盛れる」

この指標には、知っておくべき弱点があります。計算条件を変えるだけで、実力とは無関係に数値を大きくできてしまうのです。

測定頻度を変える

同じ運用成績でも、日次で測るか月次で測るかでシャープレシオは変わります。月次にすると日々の細かい振れが平滑化され、分母が小さくなるためです。

px = yf.download("^N225", period="5y", auto_adjust=True,
                 progress=False)["Close"]

daily = px.pct_change().dropna()
weekly = px.resample("W").last().pct_change().dropna()
monthly = px.resample("ME").last().pct_change().dropna()

for label, r, p in [("日次", daily, 252), ("週次", weekly, 52),
                    ("月次", monthly, 12)]:
    s = float(r.mean() / r.std() * np.sqrt(p))
    print(f"{label} で計算 → シャープレシオ {s:>6.3f}  (n={len(r)})")

同じ期間の同じ値動きなのに、数値が変わることが確認できます。他人の公表値と比較するときは、測定頻度を揃えなければ意味がありません。

期間を都合よく選ぶ

もっと直接的な方法がこれです。相場が良かった期間だけを切り取れば、シャープレシオはいくらでも高く見せられます。

# 1年ごとに区切って、年ごとのシャープレシオを見る
px = yf.download("^N225", period="10y", auto_adjust=True,
                 progress=False)["Close"]
ret = px.pct_change().dropna()

by_year = ret.groupby(ret.index.year).apply(
    lambda r: float(r.mean() / r.std() * np.sqrt(252)) if r.std().item() else np.nan
)
print(by_year.round(2).to_string())
print(f"\n最良の年: {by_year.max():.2f}  最悪の年: {by_year.min():.2f}")
print(f"全期間  : {float(ret.mean() / ret.std() * np.sqrt(252)):.2f}")

年ごとに見ると、同じ資産でもシャープレシオが年によって大きく振れることが分かります。良かった1年だけを取り出せば「シャープレシオ2.5の戦略」と主張できてしまうわけです。

だからこそ、検証期間は必ず明示し、できれば複数の期間で確認する必要があります。全期間の値と年ごとの値を並べて出すだけで、この手のごまかしは防げます。

シャープレシオが見落とすもの

上昇の大きさもリスクとして罰する

分母が標準偏差なので、大きく上昇した月もリスクとしてカウントされます。「たまに大きく勝つ」タイプの戦略は、実際には望ましいのにシャープレシオでは低く評価されがちです。

これを補正したのがソルティノレシオで、分母に下方偏差だけを使います。

def sortino_ratio(returns, risk_free=0.0, periods=TRADING_DAYS):
    """下落だけをリスクとみなす評価指標"""
    r = pd.Series(returns).dropna()
    excess = r - risk_free / periods
    downside = np.sqrt((np.minimum(excess, 0) ** 2).mean())
    if downside == 0:
        return np.nan
    return float(excess.mean() / downside * np.sqrt(periods))


for code, name in [("7203.T", "トヨタ"), ("6758.T", "ソニー"),
                   ("^N225", "日経平均")]:
    r = yf.download(code, period="3y", auto_adjust=True,
                    progress=False)["Close"].pct_change().dropna()
    sh, so = sharpe_ratio(r, 0.005), sortino_ratio(r, 0.005)
    print(f"{name:<8s} シャープ {sh:>5.2f} / ソルティノ {so:>5.2f} "
          f"→ 比 {so / sh:>4.2f}")

ソルティノがシャープを大きく上回る銘柄は、上昇が荒く下落は穏やかという好ましい性格を持っています。逆に比が1に近いか下回るなら、下げのほうが荒いということです。

「連続して負ける苦しさ」は表現できない

シャープレシオは日々のリターンの順序を無視します。同じ数字の集合であれば、コツコツ勝ち続けた場合も、半年負け続けてから急回復した場合も、まったく同じ値になります。

しかし実際に運用する側からすれば、この2つはまるで違います。連敗の長さや含み損の深さは、最大ドローダウンでしか捉えられません。計算方法は最大ドローダウンを自動計算するコードにまとめています。

実務では、シャープレシオと最大ドローダウンを組み合わせたカルマー比率もよく使われます。

def calmar_ratio(returns, periods=TRADING_DAYS):
    """年率リターン ÷ 最大ドローダウン"""
    r = pd.Series(returns).dropna()
    equity = (1 + r).cumprod()
    dd = float((equity / equity.cummax() - 1).min())
    ann = float(r.mean()) * periods
    return ann / abs(dd) if dd < 0 else np.nan


r = yf.download("^N225", period="5y", auto_adjust=True,
                progress=False)["Close"].pct_change().dropna()
print(f"シャープ  : {sharpe_ratio(r, 0.005):>5.2f}")
print(f"ソルティノ: {sortino_ratio(r, 0.005):>5.2f}")
print(f"カルマー  : {calmar_ratio(r):>5.2f}")

3つを並べると、戦略の性格が立体的に見えてきます。1つの指標に頼らないのが、評価で失敗しないコツです。それぞれが答える問いは次のように違います。

指標分母答える問い
シャープ標準偏差値動きの荒さに見合う稼ぎがあるか
ソルティノ下方偏差下げの痛みに見合う稼ぎがあるか
カルマー最大ドローダウン最悪期に見合う稼ぎがあるか

推移を見ると「今の実力」が分かる

全期間の1つの数値だけでは、戦略が今も機能しているかは分かりません。ローリングで計算すると、優位性が薄れてきていないかを追えます。

import matplotlib.pyplot as plt

px = yf.download("^N225", period="10y", auto_adjust=True,
                 progress=False)["Close"]
ret = px.pct_change().dropna()

# 直近252営業日(約1年)のシャープレシオを転がす
rolling = (ret.rolling(252).mean() / ret.rolling(252).std()) * np.sqrt(252)

fig, ax = plt.subplots(figsize=(11, 4.5))
ax.plot(rolling.index, rolling, linewidth=1.2)
ax.axhline(0, color="gray", linewidth=0.8)
ax.axhline(1.0, color="seagreen", linestyle="--", linewidth=1, label="1.0(優秀)")
ax.set_ylabel("ローリング・シャープレシオ(1年)")
ax.set_title("シャープレシオの推移")
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("rolling_sharpe.png", dpi=120)

print(f"1.0を超えていた期間の割合: {float((rolling > 1.0).mean()) * 100:.1f} %")
print(f"マイナスだった期間の割合  : {float((rolling < 0).mean()) * 100:.1f} %")

自作戦略でこれを回すと、いつ頃から効かなくなったのかが視覚的に分かります。ローリング値が継続的に低下しているなら、再最適化か撤退かの判断をする時期です。

読み取り方の目安をまとめておきます。

  • 1.0前後を上下している: 正常。相場環境による変動の範囲内
  • 右肩下がりが半年以上続く: 優位性が薄れている可能性。検証をやり直す
  • マイナス圏に沈んで戻らない: 前提が崩れている。撤退を検討する
  • 急に跳ね上がった: 特殊な相場で偶然当たっただけの可能性。喜ぶ前に中身を確認する

大切なのは、この判断基準を成績が落ちる前に決めておくことです。数字が悪くなってから「どこまでなら様子見か」を考え始めると、たいてい判断が甘くなります。

ポートフォリオ全体で計算する

ここまでは1銘柄ずつ見てきましたが、実際に知りたいのは保有している資産全体のシャープレシオのはずです。ここには面白い性質があって、個々の銘柄より組み合わせのほうが数値が良くなることがあります。

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

HOLDINGS = {"7203.T": 0.3, "6758.T": 0.2, "9432.T": 0.3, "8306.T": 0.2}
RF = 0.005

prices = yf.download(list(HOLDINGS), period="3y", auto_adjust=True,
                     progress=False)["Close"].dropna()
rets = prices.pct_change().dropna()

# 各銘柄を単独で持った場合
print("--- 単独保有 ---")
for code, w in HOLDINGS.items():
    print(f"  {code}: シャープ {sharpe_ratio(rets[code], RF):>5.2f} "
          f"(比率 {w:.0%})")

# 組み合わせた場合
weights = pd.Series(HOLDINGS)
port = (rets * weights).sum(axis=1)
print("\n--- ポートフォリオ ---")
print(f"  シャープ    : {sharpe_ratio(port, RF):>5.2f}")
print(f"  年率ボラ    : {float(port.std()) * np.sqrt(252) * 100:>5.1f} %")
print(f"  単純平均ボラ: "
      f"{float((rets.std() * weights).sum()) * np.sqrt(252) * 100:>5.1f} %")

最後の2行を比べてみてください。ポートフォリオのボラティリティは、各銘柄のボラティリティの加重平均より小さくなります。値動きのタイミングがずれている銘柄同士が、互いの振れを打ち消し合うためです。

リターンは加重平均どおりなのにリスクだけが下がる。これが分散投資が「唯一のただ飯」と呼ばれる理由であり、シャープレシオという指標が最もはっきり効果を示してくれる場面でもあります。

効果の大きさは銘柄間の相関で決まります。相関が低いほど打ち消し合いが強く働くため、同じ業種ばかり並べても効果は限定的です。確認方法は相関係数を計算して銘柄間の関係を数値化する方法にまとめています。

この考え方を推し進めて、シャープレシオが最大になる配分を数学的に求めるのがポートフォリオ最適化です。興味があればマーコウィッツ理論で効率的フロンティアを計算する方法もご覧ください。

よくある疑問

マイナスのときはどう解釈すればよいか

シャープレシオがマイナスということは、リスクを取った結果、無リスク金利を下回ったということです。ただし、マイナス同士の大小比較には意味がありません。

  • リターンが同じマイナスなら、ボラティリティが大きいほうが数値は0に近づく
  • つまり「−0.3のほうが−0.8よりマシ」とは言い切れない
  • マイナス圏では素直にリターンとドローダウンを直接見るほうが確実

最低どれくらいの期間で計算すべきか

日次データなら最低1年(約250営業日)、できれば3年欲しいところです。期間が短いと、たまたま相場が良かっただけの数値が出ます。

期間信頼度使いどころ
3か月低い参考程度。断定はできない
1年そこそこ直近の実力を見る
3年実用的戦略の評価に使える
10年高いただし前半は相場環境が違う

投資信託の交付目論見書に載っている数値と合わない

前提が違うためです。運用会社の数値は月次リターンで計算していることが多く、無リスク金利の取り方も各社で異なります。本記事の日次ベースの計算値と直接比較しても、そもそも土俵が違います。

比べたいなら、自分の側を月次に揃えて計算し直すのが早道です。前述の resample("ME") を使えばすぐに試せます。

レバレッジをかけると数値は上がるのか

理論上は変わりません。リターンもリスクも同じ倍率で増えるため、比率としては一定に保たれます。

r = yf.download("^N225", period="3y", auto_adjust=True,
                progress=False)["Close"].pct_change().dropna()

for lev in (1, 2, 3):
    print(f"レバレッジ {lev}倍 → シャープ {sharpe_ratio(r * lev, 0.0):.3f}")

ただし現実には金利コストがかかり、その分だけ数値は下がります。「レバレッジで効率は上がらない。上がるのはリターンとリスクの絶対値だけ」という理解が正確です。だからこそ、レバレッジをかける前の戦略そのものの効率が重要になります。

まとめ

シャープレシオの計算方法から、その限界と注意点までを見てきました。要点を整理します。

  • 計算式は(リターン − 無リスク金利)÷ 標準偏差。年率換算では√252を掛ける
  • 効率のよい戦略はレバレッジで大きくできるが、非効率な戦略は改善できない
  • 目安は1.0以上で優秀。ただし2.0を超えたらまず過学習を疑う
  • 測定頻度や期間の選び方で数値は簡単に盛れる。条件を揃えずに比較しない
  • 上昇もリスクとして罰するため、下落だけを見たいならソルティノレシオ
  • 連敗の苦しさは表現できない。最大ドローダウンと併用する
  • ローリングで推移を追うと、優位性が薄れてきた時期が分かる

シャープレシオは万能ではありませんが、「リターンだけを見て判断する」状態から抜け出すには最も手軽な道具です。まずは自分の保有銘柄や自作戦略で計算し、日経平均の値と比べてみてください。市場平均に勝てていないと分かれば、それも1つの重要な発見です。

個別株を選んで売買している人にとって、この比較はかなり厳しい現実を突きつけてきます。手間をかけて銘柄を選び、売買を繰り返した結果が、指数を買って放置した場合より効率が悪い——珍しいことではありません。その事実を数字で知ってから戦略を練り直すのと、知らないまま続けるのとでは、数年後の差は決定的です。

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