シストレどうですか

  Algorithmic Trading for Dummies

FXボット(シストレ)作成編 ー 資金管理 ロットサイズを計算する

やはりトレードするにあたっては資金管理が大事と皆言っているようですので、いわゆる”2%ルール”と言われている「一回のトレードでの損失額を証拠金残高の2%以内に抑えましょう!」というやり方についていろいろ考えてみました。
日本では「投資苑2」という本で有名なようですが、原文の"The 2% Rule"で検索してみると不動産投資関連の内容がたくさん表示されます。これは金融商品以外にも広く応用されているという事でしょうか?
"The 2% Solution"という節で紹介されていましたので、こちらの名前でも検索してみましたが、2% solutionだと"2%に希釈"という意味も含むので、化学関連の内容のほうが多く表示されました。 ("The 2% Solution"という政治学的な本が初めにでてきましたが・・・)


計算式を考える

初めに計算方法を考えてみたいと思います。 単純に表現すれば”適正ロットサイズ = 許容リスク額 ÷ 許容リスク値幅”(OANDAの場合1lot=1unit)になりますが、実際は口座保有通貨や取引の通貨の組合せで細かい計算方法が変わってきます。
また、これを実際にプログラミングしようと思えば、どこの値をどのように計算するかいろいろ考えないといけません。

口座の通貨が円の場合

ここでは、まず一番簡単に計算できる円口座をもっていて対円の取引(USD/YEN, EUR/YEN..XXX/YEN)だけを行うという前提で計算式を考えてみます。

許容リスク額 = 証拠金残高 × 許容リスク率(%)
許容リスク値幅 = 損切り幅(Pips) × Pip 桁位置(0.01) ←対円取引であればいつも1pipは0.01

と考えれば、以下のような場合:

証拠金残高:¥500,000
許容リスク率: 2%
損切り幅 : 20 Pips
Pip桁位置: 0.01

適正ロットサイズ = 許容リスク額 ÷ 許容リスク値幅
= (証拠金残高 × 許容リスク率(%)) ÷ (損切り幅 × Pip桁位置)
= (¥500,000 × 2%) ÷ (20 pips × 0.01)
= 50,000 Units

となりますので、対円であればどの通貨でも50,000 Unitsの取引ができます。($50,000 €50,000£50,000...)

次にEUR/USDのように円(口座保有通貨)が絡まない取引の場合は、リスク許容額を口座の通貨から取引銘柄の決済通貨に換算する必要があります。
(通貨ペアの左側の通貨を主軸通貨、右側の通貨を決済通貨と呼びますが、ちなみに英語では主軸通貨の事をBase Currency、決済通貨の事をQuote Currencyと呼びます。)

換算を加えた式は、

{(証拠金残高 × 許容リスク率) ÷決済通貨の換算レート} ÷ (損切り幅 × Pip桁位置)

になりますので、下の例の場合

取引通貨: EUR/USD
証拠金残高:¥500,000
許容リスク率: 2%
損切り幅 : 20 Pips
Pip桁位置: 0.0001
決済通貨の口座保有通貨換算レート(USD/JPY): 105円

を計算すると、
適正ロットサイズ = {(¥500,000 × 2%) ÷ ¥105} ÷ (20 Pips × 0.0001) = €47,619 Units
になります。

その他の通貨の場合

日本円は日本の国民性を反映してか世界でもっとも謙虚な通貨ですので、決済通貨側(右側)にいつもいるので計算方法は簡単です。 その他の通貨の場合は、(EURを除けば)右になったり左になったりするので、円以外で口座を持っている場合は、より複雑になります。
ですからFXで儲けたら将来はどこの国にでも住めるよう準備する為に、口座の通貨がなんであっても計算できる方法をいまのうちから考えておこうと思います。

計算式は先程と同じ、{(証拠金残高 × 許容リスク率) ÷ 決済通貨の換算レート} ÷ (Pips × Pip換算値) で考えると"÷ 決済通貨の邦貨換算レート"の部分が通貨の組み合わせによってうまく計算できません。 許容損失額を決済通貨へ換算する過程で掛けるか割るかを判定しないといけないからです。

例えば、

口座保有通貨:EUR
証拠金残高:€500,000
許容リスク: 2%
取引通貨: EUR/YEN
Pips : 20 Pips
Pip換算値: 0.01
決済通貨の換算レート(EUR/JPY): 128円

の場合を計算すると、

{(€500,000 × 2%) ÷ 128円} ÷ (20 pips × 0.01) = €390.63

となりおかしくなります。(50万ユーロも証拠金があるのにたった390ユーロしか売買できないのはどうみても変ですね。
この場合は128円を掛ければ正しく計算されます。

同様にドル建て口座を想定していろいろ計算してみると、

  • 取引通貨がXXX/USDでは、換算がないので掛けても割っても同じ。
  • 取引通貨がEUR/GBPではドルへの換算レートがGBP/USDでたっているので割る。
  • 取引通貨がEUR/YENではドルへの換算レートがUSD/YENでたっているので掛ける。

のようになるので、

口座保有通貨から取引決済通貨への換算レートが、口座保有通貨=基軸通貨の場合

{(証拠金残高 × 許容リスク率(%)) × 決済通貨の換算レート} ÷ (損切り幅 × Pip換算値)

口座保有通貨=決済通貨の場合

{(証拠金残高 × 許容リスク率(%)) ÷ 決済通貨の換算レート} ÷ (損切り幅 × Pip換算値)

でいけそうです。

この事からも 円口座の場合は、円がいつも決済通貨側なので単純に割ればいい事になりますね。


実際にやってみる

それではなんとなく計算の仕方が理解できたという前提で、プログラミングしてみました。

口座保有通貨と取引決済通貨の換算部分について

その前に自分の持っている口座の通貨を換算する部分を実際にプログラミングしようとしてみると結構面倒くさいことに気づきます。
円口座の場合は、どんな通貨組み合わせで取引しようが、その決済通貨と円の換算レートをいつも”XXX(決済通貨名)_JPY"の形で換算レートを取得できるので、円の許容損失額をそのレートで割れば取引の決済通貨に換算できますが、USD口座の場合は、決済通貨がUSDより強い場合(EUR,GBP...)と弱い場合(JPY,AUD...)で”XXX_USD”か”USD_XXX"でレートを取得するので、掛けるか割るかの判定が必要になります。

通貨の右左を判定するような仕組みを追加で作成するとか、決済通貨ごとに一つずつ”if”で掛け算か割り算をセットするのも美しくないので、レート情報を取得する際に含まれている"quoteHomeConversionFactors"キーの値を使って換算してみました。
これで計算すると、余分な判定処理がなくなり、またレート情報を取引する通貨ペアによって2種類(取引通貨用レートと口座保有通貨用)取得する必要もなくすっきりした感じになりました。

サンプルプログラム

許容リスク率を2%、損切り幅を10Pipsとして作成しました。 今回もoandapyv20を使ってデータを取得してみます。
得られるレートがBidとAskに分かれている場合はその平均値で計算しています。 また、計算時には浮動小数点でそのまま計算していますので誤差が発生している可能性があります。 正確に計算したい場合はDecimal関数等を使ってください。

#外部モジュール
#import json
from oandapyV20 import API
import oandapyV20.endpoints.accounts as accounts
import oandapyV20.endpoints.pricing as pricing
  
#口座情報(自分の情報をセット)
API_TOKEN = '~'
API_ID = '~'
  
#通貨ペア
INSTRUMENTS = "USD_JPY"
  
#許容損失率(%)、損切り幅(Pips)
MAX_LOSS_RATIO = 2 #% 
MAX_STOP_LOSS = 10 #pips
  
api = API(API_TOKEN, "practice")
  
try:
        #銘柄(通貨ペア)情報の取得
        params = {"instruments" : INSTRUMENTS}
        request = accounts.AccountInstruments(API_ID, params=params)
        response_body = api.request(request)
        
        #必要情報の取得 (銘柄名、ピップ桁位置、レート桁数、レバレッジ)
        name = response_body['instruments'][0]['name'] 
        pipLocation = float(response_body['instruments'][0]['pipLocation']) 
        displayPrecision = int(response_body['instruments'][0]['displayPrecision']) 
        marginRate = 1 / float(response_body['instruments'][0]['marginRate']) 

        print("<銘柄情報>")
        #print(json.dumps(response_body, indent=2))
        print("取引銘柄: {0}\nPip桁位置: {1}\nレート表示桁数: {2:,.2f}\nレバレッジ: {3:.0f}倍".format(name, pipLocation, displayPrecision, marginRate))
  
        #口座情報サマリーの取得
        request = accounts.AccountSummary(API_ID)
        response_body = api.request(request)
        
        #必要情報の取得 (口座通貨、利用可能証拠金残高)
        homeCurrency = response_body['account']['currency']
        marginAvailable = float(response_body['account']['marginAvailable'])
        
        print("<口座情報>")
        #print(json.dumps(response_body, indent=2))
        print("邦貨: {0}\n取引可能証拠金残高: {0} {1:,.2f}".format(homeCurrency,  marginAvailable))
        
        #最新レートの取得
        params = {"instruments" : INSTRUMENTS}
        request = pricing.PricingInfo(API_ID, params)
        response_body = api.request(request)
        
        #邦貨換算レートの計算(中間値)
        quoteHome_ask = float(response_body['prices'][0]['quoteHomeConversionFactors']['positiveUnits'])
        quoteHome_bid = float(response_body['prices'][0]['quoteHomeConversionFactors']['negativeUnits'])
        quoteHome_mid = (quoteHome_ask + quoteHome_bid) / 2
        
        print("<レート情報>")
        #print(json.dumps(response_body, indent=2))
        print("邦貨換算レート: {0}".format(quoteHome_mid))
        
        #ロットサイズの計算 (=許容損失額÷許容損失値幅)
        max_loss_allowance = marginAvailable * (MAX_LOSS_RATIO / 100) / quoteHome_mid
        stop_loss_allowance = MAX_STOP_LOSS * (10 ** pipLocation)
        new_lot_size = (max_loss_allowance / stop_loss_allowance) // 1       
        print("<計算結果>")
        print(f"新ロットサイズ: {new_lot_size:,.0f}") 
  
#エラー時の処理
except Exception as e:
        print('Error!=>\n%s' %e)  

このプログラムを実行してみると、以下のような結果が得られました。

<銘柄情報>
取引銘柄: USD_JPY
Pip桁位置: -2.0
レート表示桁数: 3.00
レバレッジ: 20倍
<口座情報>
邦貨: USD
取引可能証拠金残高: USD 98,891.66
<レート情報>
邦貨換算レート: 0.009226795
<計算結果>
新ロットサイズ: 2,143,575

違う通貨ペアで実行してみると、

"EUR_USD"の時、

<銘柄情報>
取引銘柄: EUR_USD
Pip桁位置: -4.0
レート表示桁数: 5.00
レバレッジ: 20倍
<口座情報>
邦貨: USD
取引可能証拠金残高: USD 98,891.66
<レート情報>
邦貨換算レート: 1.0
<計算結果>
新ロットサイズ: 1,977,833

"EUR_GBP"の時、

<銘柄情報>
取引銘柄: EUR_GBP
Pip桁位置: -4.0
レート表示桁数: 5.00
レバレッジ: 20倍
<口座情報>
邦貨: USD
取引可能証拠金残高: USD 98,891.66
<レート情報>
邦貨換算レート: 1.3839199999999998
<計算結果>
新ロットサイズ: 1,429,152

になりました。

レバレッジも考慮してみる

これで無事計算できたと思いましたが、よくよく結果を見てみるとおかしな事に気が付きます。この計算式の場合はレバレッジを考慮していないので、与えるパラメーターによっては取引できる上限ロットサイズを超えた結果が出てくる場合があります。

先程のサンプルプログラムの結果例:

<銘柄情報>
取引銘柄: USD_JPY
Pip桁位置: -2.0
レート表示桁数: 3.00
レバレッジ: 20倍
<口座情報>
邦貨: USD
取引可能証拠金残高: USD 98,891.66
<レート情報>
邦貨換算レート: 0.009226795
<計算結果>
新ロットサイズ: 2,143,575

で言えば、証拠金残高の20倍で($98,891.66×20=)$1,977,833が最大取引可能額になりますが、ロットサイズの計算結果は$2,143,575になりオーバーしています。

こういったケースを考えてレバレッジを考慮した上で取引可能な上限の金額を判定したいと思います。
最新のレートを取得(PricingInfo)する際に、そこに含まれている"unitsAvailable"キーの中にある値を使えば、わざわざ自分で計算しなくても対象基軸通貨の最大取引可能ロットサイズが取得できます。

      "unitsAvailable": {
        "default": {
          "long": "1977833",
          "short": "1977833"
        },
        "openOnly": {
          "long": "1977833",
          "short": "1977833"
        },
        "reduceFirst": {
          "long": "1977833",
          "short": "1977833"
        },
        "reduceOnly": {
          "long": "0",
          "short": "0"
        }
      },

注意点としては、Oandaのマニュアルによれば、”将来のAPIのアップデートで廃止予定(deprecated)” となっておりいつ取得できなくなるかわかりませんので、いまから自分で計算しておいたほうが良いかとは思いますが、今回はとりあえずこの値を使った場合で考えてみました。

先程のサンプルプログラムの最後の部分(exceptの前)に処理を追加してみます。

        #💡取引可能ロットサイズの取得
        unitsAvailable = int(response_body['prices'][0]['unitsAvailable']['default']['long'])
        print("取引可能ロットサイズ: {0:,.0f}".format(unitsAvailable))
        if new_lot_size > unitsAvailable:
                new_lot_size =  unitsAvailable        
        print(f"最終新ロットサイズ: {new_lot_size:,.0f}") 

結果としては、

<銘柄情報>
取引銘柄: USD_JPY
Pip桁位置: -2.0
レート表示桁数: 3.00
レバレッジ: 20倍
<口座情報>
邦貨: USD
取引可能証拠金残高: USD 98,891.66
<レート情報>
邦貨換算レート: 0.009226795
<計算結果>
新ロットサイズ: 2,143,575
取引可能ロットサイズ: 1,977,833
最終新ロットサイズ: 1,977,833

証拠金残高とレバレッジを考慮して、最大取引可能ロットサイズを超えた場合は、そのロットサイズにする。という処理が追加できました。

更なる注意点としては、例では保有ポジションがないという前提で['unitsAvailable']['default']['long']を使っていますが、既にポジションがあってそのサイズを変更する、更に新規の取引を行ったり、両建てを行うなどの場合は、それ以外のキーを参照する必要がでてきます。

損切り幅を逆計算してみる

許容リスク率を2%と固定した場合は、損切り幅(Pips)を小さくとりすぎると簡単に取引可能ロットサイズをオーバーしてしまいます。 そこで今度は、取引可能ロットサイズを超えずに損切幅を最小何Pipsにまでする事ができるかというのを試してみました。

計算式は、取引銘柄の決済通貨に換算された許容損失額を主軸通貨の取引可能ロットサイズで割ればよいので、

最小損切り幅(Pips) = {(証拠金残高 × 許容リスク(%)) ÷ 邦貨換算レート} ÷ (取引可能ロットサイズ ÷ Pip桁位置)

と考えて以下の処理をサンプルプログラムに追加します。

        #💡最小損切り幅の計算
        max_loss_allowance = marginAvailable * (MAX_LOSS_RATIO / 100) / quoteHome_mid
        unitsAvailable = int(response_body['prices'][0]['unitsAvailable']['default']['long'])
        min_stop_loss_pips = -((max_loss_allowance / unitsAvailable) / (10 ** pipLocation) // -1)  # //は切り上げ処理
        print("最小損失幅: {0:,.0f}pips".format(min_stop_loss_pips))
        new_lot_size_pip = int(max_loss_allowance / (min_stop_loss_pips * (10 ** pipLocation)))     
        print(f"最小損切り幅を考慮したロットサイズ: {new_lot_size_pip:,.0f}")        

実行した結果は

<銘柄情報>
取引銘柄: USD_JPY
Pip桁位置: -2.0
レート表示桁数: 3.00
レバレッジ: 20倍
<口座情報>
邦貨: USD
取引可能証拠金残高: USD 98,891.66
<レート情報>
邦貨換算レート: 0.00920251
<計算結果>
新ロットサイズ: 2,149,232
取引可能ロットサイズ: 1,977,833
最終新ロットサイズ: 1,977,833
最小損切り幅: 11pips
最小損切り幅を考慮したロットサイズ: 1,953,847

のようになり、損切り幅を11Pipsより大きい値にしておけば、ロットサイズがオーバーしないという事になります。


まとめ

適正なロットサイズの計算方法についていろいろ試してみました。
”一回の取引での許容損失額が2%”と一口にいってもいろいろ細かい設定を考えないといけないですね。
証拠金を超える取引は出来ませんので、どの部分(レバレッジ・ロットサイズ・許容リスク率・損切り幅)を調整するのか?
それには、ボットでやろうとしているトレードスタイル(例えばスイングかスキャルプ)や取引通貨ペアの変動幅の特性等によっても変わります。

トレードの考え方や手法をいろいろな書物やオンライン媒体から学ぶ機会は多いですが、これをプログラミングしようと試みる事によって、オリジナルのコンテンツからは見えてこなかった部分が見えてくるというひとつの事例かなとは思います。
(というわけでこのブログをまとめるにも思った以上に時間がかかってしまいました・・・。)

今回は、熱くなりすぎず大切な資金をきちんと管理しながらトレードしましょうというお話しでしたが、ボットで自動売買を行えば、心理(Mind)と資金管理(Money)を上手にコントロールする事はできそうでしょうか?