結論
XAUUSD のロットサイズ計算は、pip を入力に取らない設計にすると壊れません。価格幅とオンス数だけで計算し、pip は最後に表示用へ変換します。この記事では 3 つの関数と 6 つのテストで、ブローカーごとに 10 倍違う pip 定義を吸収する実装を書きます。
用語の前提
- ロット(lot): 取引量の単位。XAUUSD の標準ロットは 100 トロイオンス(= 100 oz)。
- pip: 値動きの最小単位を表す慣習的な単位。ただし XAUUSD では業界統一の定義がなく、0.10 ドルと 0.01 ドルの 2 系統が併存します。
- 契約サイズ(contract size): 1 ロットが何オンスかを表す数値。XAUUSD は 100。
- 損切り幅(stop distance): エントリー価格と損切り価格の差。価格(ドル)で表します。
この 4 つのうち、ブローカー間でぶれるのは pip の定義だけです。ロットと契約サイズは共通、損切り幅は自分で決める値です。したがって、pip を計算の入力から外せば、ぶれる要素は消えます。
3 つの関数
CONTRACT_SIZE = 100.0 # XAUUSD: 1 lot = 100 oz
def pip_size_of(definition: str) -> float:
"""'0.10' or '0.01' -> pip の価格幅"""
table = {"0.10": 0.10, "0.01": 0.01}
if definition not in table:
raise ValueError(f"unknown pip definition: {definition}")
return table[definition]
def pip_value(lots: float, pip_size: float) -> float:
"""1 pip 動いたときの損益(USD)"""
if lots <= 0:
raise ValueError("lots must be positive")
return lots * CONTRACT_SIZE * pip_size
def position_size(equity: float, risk_pct: float,
stop_distance: float) -> float:
"""risk_pct を % で受け取り、ロットを返す"""
if equity <= 0 or stop_distance <= 0:
raise ValueError("equity and stop_distance must be positive")
if not 0 < risk_pct <= 5:
raise ValueError("risk_pct out of sane range")
risk_money = equity * risk_pct / 100.0
return risk_money / (stop_distance * CONTRACT_SIZE)
position_size() に pip が登場しない点が設計の核心です。損切り幅は価格で受け取るので、ブローカーの pip 定義が変わっても同じ結果を返します。
def break_even_distance(lots: float, spread: float,
commission_round_trip: float) -> float:
"""往復コストを回収するのに必要な価格移動(USD)"""
ounces = lots * CONTRACT_SIZE
if ounces <= 0:
raise ValueError("lots must be positive")
return (ounces * spread + commission_round_trip) / ounces
break_even_distance() はロットに比例する項と比例しない項が混ざるので、オンスで割って価格に戻します。0.1 ロット・スプレッド 0.20・往復手数料 0.60 なら、(10*0.20 + 0.60)/10 = 0.26 ドル。
テストで仕様を固定する
import pytest
def test_pip_value_differs_by_definition():
assert pip_value(1.0, 0.10) == pytest.approx(10.0)
assert pip_value(1.0, 0.01) == pytest.approx(1.0)
def test_pip_value_scales_with_lots():
assert pip_value(0.1, 0.10) == pytest.approx(1.0)
def test_sizing_is_definition_independent():
"""pip 定義を変えてもロットは同じ —— これが設計の目的"""
size = position_size(1000, 1.0, 5.0)
assert size == pytest.approx(0.02)
def test_sizing_rejects_zero_stop():
with pytest.raises(ValueError):
position_size(1000, 1.0, 0.0)
def test_break_even_matches_hand_calculation():
assert break_even_distance(0.1, 0.20, 0.60) == pytest.approx(0.26)
def test_break_even_is_lot_independent():
"""同じスプレッドと手数料率なら、必要値幅はロットに依存しない"""
assert break_even_distance(0.1, 0.20, 0.60) == pytest.approx(0.26)
assert break_even_distance(1.0, 0.20, 6.00) == pytest.approx(0.26)
test_sizing_is_definition_independent が要点です。このテストが通る限り、pip 定義の違いはサイジングに影響しません。逆にここが落ちる実装を書いてしまったら、どこかで pip を計算に混ぜています。
つまずいたところ
1. break_even_distance() を pip で返そうとした。 最初は「26 pips」と返す実装にしましたが、pip 定義が 0.01 のブローカーでは 2600 pips になり、表示が破綻します。価格(ドル)で返し、表示時にだけ pip へ変換する形に直しました。
2. risk_pct を小数で受けてしまった。 0.01 を 1% のつもりで渡すと 0.01% として計算され、ロットが 100 分の 1 になります。関数の docstring に「% で受け取る」と明記し、0 < risk_pct <= 5 の範囲チェックを入れて弾くようにしました。単位の取り違えは例外で落とす方が、静かに間違うより安全です。
3. 最小ロットの丸めを関数に入れなかった。 0.014 が必要でも 0.01 しか出せないブローカーがあります。丸めは表示層の責務にして、計算層は丸めないまま返す設計にしました。計算層で丸めると、テストが実際のリスクと一致しなくなります。
FAQ
Q. pip 定義が 0.01 のブローカーで「50 pips」と言われたら? 価格に直すと 0.50 ドルです。0.10 定義なら 5.00 ドル。受け渡しは必ず価格で行い、pip は表示だけに使うのが安全です。
Q. レバレッジは計算に入れないのですか? 入りません。レバレッジは必要証拠金を決めるだけで、損切り幅とリスク額から算出されるロットには影響しません。強平距離の計算には必要なので、別関数に分けています。
Q. 契約サイズが 100 でないブローカーは? CONTRACT_SIZE を定数ではなく引数にしてください。ただし XAUUSD はほぼ全ブローカーで 100 oz なので、定数のままでも実害は小さいです。
まとめ
- 計算層に pip を入れない。価格とオンス数だけで完結させる。
- pip は表示層でだけ使う。定義(0.10 / 0.01)を必ず併記する。
- 単位の取り違えは例外で落とす。静かに間違う実装を作らない。
- 丸めは表示層。計算層は生の値を返す。
この 4 点を守ると、ブローカーを跨いだ検証が壊れなくなります。
本記事は教育目的であり、投資助言ではありません。契約サイズ・pip 定義・スプレッド・手数料はブローカーごとに異なるため、必ずご自身の契約仕様をご確認ください。