Kelly基準でFXのポジションサイズを最適化する|ドル円161円急騰で学んだリスク管理【Python】

Python実装・コード

ドル円が一気に161円台まで駆け上がった日、僕は普段の2倍近いロットでポジションを持っていました。なんとか損切りラインで耐えましたが、正直かなり冷や汗をかきました。

後になって自分に問いかけました。「なぜそのロット数にしたのか」。答えは「なんとなく行けそうだと思ったから」でした。根拠はゼロです。

そこで勉強したのがKelly基準(ケリー基準)でした。勝率と損益比から「賭けるべき資金の割合」を数学的に導くという考え方です。この記事では、Pythonでの実装と、そのまま使うと破産しかねない理由までまとめます。

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

Kelly基準とは何か

Kelly基準は、もともと通信理論の分野で生まれた式です。「勝率と配当が分かっているとき、資金の何割を賭ければ長期的な成長率が最大になるか」を求めます。

f* = (b × p − q) ÷ b

  f* : 賭けるべき資金の割合
  p  : 勝率
  q  : 負ける確率(1 − p)
  b  : 勝ったときの利益 ÷ 負けたときの損失(損益比)

この式が答えているのは「いくら賭ければ最も効率よく資金が増えるか」です。ここで重要なのは、賭けが大きすぎても小さすぎても良くない、という点です。

  • 小さすぎる: 勝っても資金がほとんど増えない。機会損失
  • ちょうどいい: 長期的な成長率が最大になる
  • 大きすぎる: 期待値がプラスでも破産する

3番目が直感に反するところです。期待値がプラスの賭けでも、賭けすぎると資金がゼロに近づいていきます。後ほど数値で確認します。

もう1つ、この式の前提を確認しておきます。Kelly基準が成り立つのは、次の条件が揃っているときです。

  • 勝率と損益比が既知である(実際には推定値しかない)
  • 同じ条件の賭けを何度も繰り返せる(相場環境は変わっていく)
  • 資金を自由に分割できる(最小取引単位の制約がある)
  • 損失は賭けた分だけ(想定を超える損失が起こりうる)

実際の相場では、4つとも完全には成り立ちません。だからこそ、式が出した数値をそのまま使うのではなく、割り引いて使う必要が出てきます。この点は記事の後半で詳しく扱います。

📘 外部参考Kelly Criterion(Investopedia)ケリー基準(Wikipedia)

Pythonで実装する

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


def kelly(win_rate, payoff_ratio):
    """Kelly基準で最適な賭け割合を求める

    win_rate     : 勝率(0〜1)
    payoff_ratio : 平均利益 ÷ 平均損失
    """
    if payoff_ratio <= 0:
        return 0.0

    p, q = win_rate, 1 - win_rate
    f = (payoff_ratio * p - q) / payoff_ratio
    return max(f, 0.0)      # マイナスなら「賭けない」が正解


# 勝率と損益比の組み合わせで、賭け割合がどう変わるか
print(f"{'勝率':>6s} {'損益比':>7s} {'Kelly':>8s} {'期待値':>8s}")
for wr in (0.40, 0.50, 0.55, 0.60, 0.70):
    for pr in (1.0, 1.5, 2.0):
        f = kelly(wr, pr)
        ev = wr * pr - (1 - wr)
        print(f"{wr:>5.0%} {pr:>7.1f} {f:>7.1%} {ev:>+8.3f}")

この表を見ると、勝率50%・損益比1.0では f が0になることが分かります。当たり前ですが、優位性がなければ賭けるべき額はゼロです。

そして勝率60%・損益比2.0だと f は40%という結果になります。資金の4割を1回のトレードに賭けろ、という計算です。これがKelly基準をそのまま使えない理由でもあります。

式の形からも、いくつか性質が読み取れます。

  • 期待値がマイナスなら f もマイナスになる。これは「賭けるな」という意味
  • 損益比が大きいほど f は小さくてよい。少ない資金でも十分に増やせる
  • 勝率のわずかな違いが f を大きく動かす。推定精度が結果を左右する

3番目が実務上いちばん厄介です。勝率を5ポイント高く見積もっただけで、Kelly値は倍近くになることがあります。そして人は、自分の戦略を過大評価しがちです。

なぜフルKellyが危険なのか

「数学的に最適なら、そのまま使えばいいのでは」と思うかもしれません。実際にシミュレーションしてみると、その危うさがよく分かります。

def simulate(win_rate, payoff_ratio, fraction,
             trades=200, trials=1000, seed=42):
    """指定した賭け割合で運用したときの資金推移をシミュレートする"""
    rng = np.random.default_rng(seed)
    finals, max_dds, ruins = [], [], 0

    for _ in range(trials):
        capital = 1.0
        peak = 1.0
        max_dd = 0.0

        for _ in range(trades):
            bet = capital * fraction
            if rng.random() < win_rate:
                capital += bet * payoff_ratio
            else:
                capital -= bet

            if capital <= 0.01:      # 実質的な破産
                ruins += 1
                capital = 0.0
                break

            peak = max(peak, capital)
            max_dd = min(max_dd, capital / peak - 1)

        finals.append(capital)
        max_dds.append(max_dd)

    finals = np.array(finals)
    return {
        "中央値": round(float(np.median(finals)), 2),
        "平均": round(float(finals.mean()), 2),
        "最悪": round(float(finals.min()), 3),
        "平均最大DD": round(float(np.mean(max_dds)) * 100, 1),
        "破産率%": round(ruins / trials * 100, 1),
    }


WR, PR = 0.55, 1.5
full = kelly(WR, PR)
print(f"勝率{WR:.0%} 損益比{PR} → フルKelly = {full:.1%}\n")

rows = []
for label, frac in [("フル", full), ("1/2", full / 2),
                    ("1/4", full / 4), ("1/8", full / 8)]:
    r = simulate(WR, PR, frac)
    r["設定"] = f"{label} ({frac:.1%})"
    rows.append(r)

print(pd.DataFrame(rows).set_index("設定").to_string())

実行すると、フルKellyでは平均最大ドローダウンが50%を超えることが確認できます。資金が半分以下になる過程を、平均的に経験するということです。

さらに注目すべきは「平均」と「中央値」の差です。フルKellyでは平均が大きく見えても、中央値はずっと小さくなります。少数の大成功が平均を押し上げ、多くの人は平凡か悲惨な結果になるという分布です。

この「平均と中央値がずれる」現象については平均・中央値・最頻値を計算して株価を読む統計学入門で詳しく扱っています。

賭けすぎると期待値がプラスでも減る

もっと極端な例で確かめます。Kelly値を超えて賭けると、資金は減っていきます。

print(f"{'賭け割合':>9s} {'中央値':>9s} {'破産率':>8s}")
for frac in (0.05, 0.10, full, full * 1.5, full * 2, 0.50):
    r = simulate(WR, PR, frac, trades=200, trials=1000)
    mark = "  ← フルKelly" if abs(frac - full) < 1e-9 else ""
    print(f"{frac:>8.1%} {r['中央値']:>9.2f} {r['破産率%']:>7.1f}%{mark}")

Kelly値の2倍を賭けると、期待値がプラスの賭けなのに中央値が1を下回ります。つまり「平均的には資金が減る」ということです。

理由は複利の性質にあります。50%失うと、元に戻すには100%の利益が必要です。大きく賭けるほど、この非対称性が効いてきます。

# 損失からの回復に必要な利益率
print(f"{'損失':>6s} {'必要な回復':>10s}")
for loss in (0.10, 0.20, 0.30, 0.50, 0.70, 0.90):
    need = 1 / (1 - loss) - 1
    print(f"{loss:>5.0%} {need:>10.0%}")

90%失えば、回復には900%の利益が必要です。この非対称性がある限り、大きなドローダウンは避けるべきだと分かります。

実データから勝率と損益比を求める

Kelly基準を使うには、自分の戦略の勝率と損益比が必要です。これは過去のトレード記録から計算します。

def estimate_from_trades(pnl):
    """トレード損益の列から、勝率と損益比を推定する"""
    s = pd.Series(pnl).dropna()
    s = s[s != 0]

    wins, losses = s[s > 0], s[s < 0]
    if len(wins) == 0 or len(losses) == 0:
        return None

    win_rate = len(wins) / len(s)
    payoff = float(wins.mean() / abs(losses.mean()))
    f = kelly(win_rate, payoff)

    return {
        "トレード数": len(s),
        "勝率": round(win_rate, 3),
        "平均利益": round(float(wins.mean()), 2),
        "平均損失": round(float(losses.mean()), 2),
        "損益比": round(payoff, 2),
        "フルKelly": round(f, 4),
        "1/4Kelly": round(f / 4, 4),
    }


# 例: 実際のトレード損益(円)
sample = [12000, -8000, 15000, -7500, -9000, 22000, -8200,
          11000, -7800, 18000, -8500, 9500, -7200, 14000, -8800]

result = estimate_from_trades(sample)
for k, v in result.items():
    print(f"  {k:<12s}: {v}")

ここで致命的な問題があります。勝率と損益比の推定値は、サンプルが少ないと大きくぶれるのです。

# サンプル数によって推定値がどれだけぶれるか
rng = np.random.default_rng(0)
TRUE_WR, TRUE_PR = 0.55, 1.5

print(f"{'試行数':>7s} {'推定勝率':>10s} {'推定Kelly':>11s} {'ばらつき':>10s}")
for n in (20, 50, 100, 300, 1000):
    estimates = []
    for _ in range(500):
        outcomes = rng.random(n) < TRUE_WR
        pnl = np.where(outcomes, TRUE_PR, -1.0)
        wr = outcomes.mean()
        estimates.append(kelly(wr, TRUE_PR))

    estimates = np.array(estimates)
    print(f"{n:>7d} {TRUE_WR:>9.1%} {estimates.mean():>10.1%} "
          f"±{estimates.std():>8.1%}")

20回程度の記録では、推定されるKelly値が大きく振れます。実際の勝率が55%でも、たまたま65%と観測されればKelly値は倍近くになります。

推定を誤った状態でフルKellyを賭けるのが、最も危険なパターンです。何回のトレードがあれば信頼できるかについては、p値を知ってバックテストの見方が変わった話で詳しく扱っています。

現実的な使い方:ハーフ〜クォーターKelly

実務ではKelly値の1/2から1/4を使うのが定石です。理由は3つあります。

フルKellyを避ける理由内容
推定誤差勝率も損益比も推定値。実際より高く見積もりやすい
ドローダウン50%以上の落ち込みに人間は耐えられない
前提の変化相場が変われば勝率も変わる。式は変化を織り込まない
# 1/2、1/4に落としたときの成長率とリスクを比較する
full = kelly(0.55, 1.5)

print(f"{'設定':>10s} {'割合':>7s} {'中央値':>8s} {'最大DD':>8s} {'破産率':>7s}")
for label, ratio in [("フル", 1.0), ("1/2", 0.5),
                     ("1/4", 0.25), ("1/8", 0.125)]:
    frac = full * ratio
    r = simulate(0.55, 1.5, frac, trades=300, trials=1000)
    print(f"{label:>10s} {frac:>6.1%} {r['中央値']:>8.2f} "
          f"{r['平均最大DD']:>7.1f}% {r['破産率%']:>6.1f}%")

結果を見ると、1/2Kellyでも成長率はフルの4分の3程度を維持し、ドローダウンは大幅に小さくなります。効率をわずかに犠牲にして、安全性を大きく買える計算です。

僕は1/4Kellyを採用しました。推定の不確かさを考えると、これでも強気なくらいだと感じています。

上限も併せて設定する

Kelly値が高く出ても、絶対的な上限は別途決めておくべきです。推定が壊れているときの保険になります。

def position_fraction(win_rate, payoff_ratio,
                      kelly_ratio=0.25, cap=0.02):
    """実務で使うポジション比率を返す

    kelly_ratio : Kelly値の何割を使うか
    cap         : 資金に対する上限(安全弁)
    """
    f = kelly(win_rate, payoff_ratio) * kelly_ratio
    return min(f, cap)


print(f"{'勝率':>6s} {'損益比':>7s} {'1/4Kelly':>10s} {'上限適用後':>11s}")
for wr, pr in [(0.50, 1.2), (0.55, 1.5), (0.60, 2.0), (0.70, 2.5)]:
    raw = kelly(wr, pr) * 0.25
    capped = position_fraction(wr, pr)
    mark = " ← 上限で制限" if capped < raw else ""
    print(f"{wr:>5.0%} {pr:>7.1f} {raw:>9.2%} {capped:>10.2%}{mark}")

資金の2%を上限にしているのは、20回連敗しても資金の3分の1程度が残る水準だからです。Kelly値がどれだけ高く出ても、ここは超えないようにしています。

FXのロット数に変換する

「資金の何%」が決まっても、それだけでは注文できません。損切り幅と組み合わせて、実際のロット数に変換します。

def calc_lot(capital, risk_fraction, entry_price, stop_price,
             lot_unit=1000):
    """許容リスクと損切り幅から取引数量を求める

    capital       : 総資金(円)
    risk_fraction : 1トレードで許容する損失の割合
    lot_unit      : 最小取引単位(1000通貨など)
    """
    risk_yen = capital * risk_fraction
    stop_pips = abs(entry_price - stop_price)

    if stop_pips <= 0:
        return 0, 0.0

    # 1通貨あたりの損失 = 損切り幅(円)
    units = risk_yen / stop_pips
    units = int(units // lot_unit) * lot_unit

    return units, units * stop_pips


CAPITAL = 1_000_000
wr, pr = 0.55, 1.5
frac = position_fraction(wr, pr)

entry, stop = 158.50, 157.80      # 70pips の損切り

units, loss = calc_lot(CAPITAL, frac, entry, stop)
print(f"資金        : {CAPITAL:,} 円")
print(f"許容リスク  : {frac:.2%} = {CAPITAL * frac:,.0f} 円")
print(f"損切り幅    : {abs(entry - stop) * 100:.0f} pips")
print(f"→ 取引数量  : {units:,} 通貨")
print(f"→ 想定損失  : {loss:,.0f} 円")

この計算をするようになって、「なんとなく2倍のロット」という判断がなくなりました。損切り幅が広ければ自動的にロットが減り、狭ければ増える——リスクが一定に保たれます。

損切り幅の決め方は標準偏差を使ってポジションサイズを決める方法で詳しく扱っています。ボラティリティから決めると、相場環境に自動で追随します。

ここで順番が重要です。多くの人は「ロット数を決めてから、損切りをどこに置くか考える」という順序で判断しています。しかし正しくは逆です。

よくある順序あるべき順序
1番目ロット数を決める許容損失額を決める
2番目エントリーする損切り位置を決める
3番目損切りを置くロット数が自動的に決まる
結果損失額がばらつく損失額が一定に保たれる

ロット数は決めるものではなく、計算の結果として出てくるもの——この認識に変わってから、僕は無茶なポジションを持たなくなりました。

FX特有の注意点

レバレッジと混同しない

Kelly基準が示すのは「1回のトレードでリスクにさらす資金の割合」であって、レバレッジ倍率ではありません。ここを混同すると危険です。

# 同じ「リスク2%」でも、損切り幅によって建玉は大きく変わる
CAPITAL, RISK = 1_000_000, 0.02

print(f"{'損切り幅':>10s} {'取引数量':>12s} {'建玉金額':>14s} {'実質レバ':>9s}")
for pips in (20, 50, 100, 200):
    stop = pips / 100
    units, _ = calc_lot(CAPITAL, RISK, 158.50, 158.50 - stop)
    notional = units * 158.50
    print(f"{pips:>7d}pips {units:>12,} {notional:>14,.0f} "
          f"{notional / CAPITAL:>8.1f}倍")

損切りを狭くするほど、実質レバレッジは高くなります。リスク額は同じでも、建玉は膨らむ。証拠金維持率にも影響するので、この点は必ず確認してください。

損切りが約定しないリスク

Kelly基準の計算は「損切りが想定価格で必ず約定する」ことを前提にしています。しかし冒頭の161円急騰のような場面では、この前提が崩れます。

指標発表時や通貨当局の介入時には、価格が飛んで想定より不利な位置で約定することがあります。計算上のリスク2%が、実際には5%になることもあり得ます。

これも、フルKellyを避けるべき理由の1つです。「計算どおりにいかない可能性」を割引として織り込んでおく——1/4Kellyという控えめな設定には、そういう意味もあります。

同時に複数ポジションを持つ場合

Kelly基準は1回の賭けを前提にした式です。複数の通貨ペアを同時に持つ場合、単純に足し合わせるとリスクが過大になります。

# 相関が高いペアを同時に持つと、実質的なリスクは合計以上になる
def adjusted_fraction(base_fraction, n_positions, correlation=0.7):
    """相関を考慮して1ポジションあたりの比率を下げる"""
    # 相関が高いほど分散効果が働かない
    effective = np.sqrt(n_positions * (1 + (n_positions - 1) * correlation))
    return base_fraction * np.sqrt(n_positions) / effective


base = 0.02
print(f"{'同時保有':>8s} {'相関0.0':>9s} {'相関0.7':>9s} {'相関1.0':>9s}")
for n in (1, 2, 3, 5):
    row = [adjusted_fraction(base, n, c) for c in (0.0, 0.7, 1.0)]
    print(f"{n:>7d}件 " + " ".join(f"{v:>8.3%}" for v in row))

ドル円・ユーロ円・ポンド円はいずれも「円」が絡むため相関が高くなります。3つ持てば、実質的に同じ方向へ3倍賭けているのと変わらないこともあります。

相関の確認方法は相関係数を計算して銘柄間の関係を数値化する方法で扱っています。

まとめ

Kelly基準でFXのポジションサイズを決める方法と、その限界をまとめました。

  • Kelly基準は勝率と損益比から最適な賭け割合を導く式
  • 賭けすぎると期待値がプラスでも資金が減る。複利の非対称性のため
  • フルKellyは平均最大DDが50%超。人間が耐えられる水準ではない
  • 勝率の推定はサンプルが少ないと大きくぶれる。20回程度では当てにならない
  • 実務では1/2〜1/4Kelly。成長率をわずかに犠牲にして安全性を大きく買う
  • 資金の2%程度の絶対上限も併せて設定する
  • 相関の高い通貨ペアを同時に持つと、実質的なリスクは合計以上になる

Kelly基準を学んで一番良かったのは、最適な数値を知れたことではなく「賭けすぎると期待値がプラスでも負ける」と理解できたことでした。

あの日、普段の2倍のロットを持っていた自分は、「勝てる見込みがあるなら大きく張るべき」と考えていました。その発想自体が間違っていたわけです。見込みの大きさと、賭けるべき額は比例しません。

もう1点、この式には根本的な限界があります。Kelly基準は「同じ条件の賭けを無限に繰り返す」ことを前提にしているということです。相場では条件が刻々と変わるため、この前提は成り立ちません。

だからこの式を、「最適解を教えてくれるもの」ではなく「上限を教えてくれるもの」として使うのが実務的です。Kelly値は「これ以上賭けたら明確に不利」というラインであり、そこに近づく必要はまったくありません。

まずは自分の過去のトレード記録から、勝率と損益比を計算してみてください。そこから出るKelly値の1/4が、いまの自分にとって現実的な水準になるはずです。

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