
複数の商品を同時に取引する際のリスクバランス
この記事では、複数の商品を同時に日中取引する際のリスクバランスについて触れます。この記事の目的は、ユーザーがゼロから商品のバランスするコードを書けるようにすること、そして経験豊富なユーザーに、古いアイデアの他の、おそらく以前は使用されていなかった実装を紹介することです。そのために、リスクの概念の定義を検討し、最適化の基準を選択し、ソリューションを実装するための技術的側面に焦点を当て、そのような実装のための標準的な端末機能のセットを分析し、このアルゴリズムをソフトウェアインフラストラクチャに統合するための他の可能な方法についても触れます。
リスク別に取引商品をバランスする基準
複数の金融商品を同時に取引する場合、リスクバランスの基準として主に2つの要素を考慮します。
- 銘柄のティック価格
- 銘柄の日次平均ボラティリティ
ティック価格は、標準銘柄ロットの銘柄の最小価格変化の通貨での値です。商品によってティック値が大きく異なる可能性があるため、この基準を考慮に入れています。例えば、EURGBPの1.27042からAUDNZDの0.61374までです。
1日平均ボラティリティは、1日の銘柄価格の特徴的な変化です。この値は、前に選択した基準よりも、異なる銘柄では一定ではなく、市場のステージによって時間の経過とともに変化する可能性があります。例えば、EURGBPは通常平均して約336ポイント動きますが、CHFJPYは同じ日に、ほぼ4倍の1271ポイント動くことがあります。ここに示したデータは、価格がロールバックすることなく一方向に非常に強く動き始める特定の瞬間の銘柄の異常な高ボラティリティを考慮することなく、価格のボラティリティの「通常」と「最も可能性の高い」値を特徴づけるものです。以下はUSDJPYのそのような動きの例です。
図1:D1チャートにおける銘柄ボラティリティの上昇
このような行為は、残高に対して非常に深刻なリスクを引き起こす可能性があります。これについては「トレーダーのリスクを低減するには」稿で十分に詳しく説明しています。本稿では、このようなリスクに対して、金融商品のバランスを取ることは不可能であるというテーゼを提案します。これはまったく別のカテゴリーのリスクです。バランスを取ることで、平均ボラティリティの枠内で、市場がポジションに逆行するリスクから預金を守ることができます。異常な動き、つまり「ブラックスワン」から資金を守りたいのであれば、以下の原則を使用してください。
- 単一の銘柄に大きな割合のリスクを取らない
- 常にポジションを持つように努力しない
- すべての運用資金を単一のブローカーの単一の口座に入れない
- 異なる口座やブローカーで同じエントリを同時に取引しない
これらの原則に従うことで、ポジションがあるときに銘柄価格が不利になった場合でも、損失を最小限に抑えることができます。さて、標準ボラティリティに関連するリスクの検討に戻りましょう。
この2つの要素を同時に考慮することで、各通貨ペアのリスクをバランスさせ、期待利益を正規化しながら、単一商品のリスクを偏重することなく同時に取引することができます。このアプローチにより、取引履歴の更なる分析のために、より均質な取引統計が提供され、戦略オプティマイザーがこのデータを扱う際の誤差が減少し、それに応じて、平均データからのサンプルの標準偏差が減少します。標準偏差の値がセットの分析の質にどのように影響するかをより詳細に理解するには、「取引における数:トレード結果の推定方法」稿をお読みください。次に、データを保存するコンテナの選択に移りましょう。
データ保存用コンテナの選択
プロジェクトでデータを保存するコンテナを選択する際には、以下の要素を考慮します。
- コンテナパフォーマンス
- 初期化に必要なメモリ
- データ分析のための組み込み機能が利用可能
- ユーザーインターフェイスによる初期設定の容易さ
データを保存するコンテナを選ぶ際の最も一般的な基準は、コンテナのパフォーマンスと、コンテナを保存するためのコンピュータメモリの必要性です。異なるタイプのストレージは通常、データ処理時のパフォーマンスを向上させたり、メモリの占有量を増やしたりすることができます。
確認するために、単純な配列とvector型の特別なコンテナを宣言し、あらかじめdoubleデータ型と同じ値で初期化しておきましょう。
double arr[] = {1.5, 2.3}; vector<double> vect = {1.5, 2.3};sizeof演算を使い、コンパイル段階で上記の型に対応するメモリサイズを決定します。
Print(sizeof(arr)); Print(sizeof(vect));
その結果、16バイトと128バイトが得られます。vectorデータ型に必要なメモリのこの違いは、より良いパフォーマンスを確保するための追加の冗長メモリ割り当てを含む、組み込み機能の有無によって決定されます。
これに基づいて、以前に選択されたデータを保存することだけを必要とするコンテナのタスクを考慮して、配列のような単純なストレージタイプを使用することにします。その後アルゴリズムで扱うことになる同種のデータについては、特殊なvectorデータ型を使用することが望ましくなります。この型を使用することで、vectorにすでに実装されている標準的な操作のためのカスタム関数を書くという点で、開発時間を短縮することもできます。
その結果、計算に必要なデータストレージは次のようになります。string symbols[]; // symbols used for balancing double tick_val[], // symbol tick price atr[], // symbol volatility volume[], // calculated position volume for symbols taking into account balancing point[]; // value of one price change point vector<double> risk_contract; // risk amount for a standard contract
次に、バランス銘柄のデータを入力するソリューションを導入するためのオプションの検討に移りましょう。
銘柄入力方法の選択
MetaTrader 5でデータ入力方法を選択する場合、多くのソリューションがあります。世界的には、標準的な端末のダイアログボックスを通してユーザーと直接対話することを目的としたソリューション、ディスクに保存されたファイルを通して他のアプリケーションと対話することを目的としたソリューション、またはリモート対話インターフェイスに分けられます。
端末に保存されている多くの銘柄をstring形式で入力する必要があることを考えると、スクリプトにデータ入力を実装するためのオプションとして、以下のようなものが考えられます。
- .csvテーブルファイルからデータを読み込む
- .binバイナリファイルからデータを読み込む
- .sqliteデータベースファイルからデータを読み込む
- 端末とリモートデータベースとのインタラクションにサードパーティの手法を使用する
- Web APIソリューションを使用する
- 入力タイプのメモリクラス修飾子を持つ適切なタイプの変数(複数可)の終端使用のための基準
プロジェクトでデータ入力方法を選択する前に、このタスクを実行するための各オプションの長所と短所を簡単に検討します。1.の方法を選択する場合、*.csvタイプの表ファイルからのデータの読み取りは、ファイルを処理するための標準的な端末関数で実装することができます。一般的には、コードは次のようになります。
string file_name = "Inputs.csv"; // file name int handle=FileOpen(file_name,FILE_CSV|FILE_READ,";"); // attempt to find and open the file if(handle!=INVALID_HANDLE) // if the file is found, then { while(FileIsEnding(handle)==false) // start reading the file { string str_follow = FileReadString(handle); // reading // here we implement filling the container depending on its type } }
ファイルを操作する機能を実装するための他のオプションについては、端末のドキュメントで十分詳しく説明されています。このオプションでは、ユーザーはMS ExcelやOpenOfficeのようなサードパーティのスプレッドシートアプリケーションを使用して入力パラメータファイルを準備するだけでよいです。Windows標準のメモ帳でもよいです。
2.については、FileLoad()標準端末関数は、*.bin ファイルを使用するのに適しています。これを使用するには、この拡張子のファイルからデータを読み取るために、アプリケーションがこのファイルを保存するときにどのようなデータ構造体を使用したかをあらかじめ知っておく必要があります。このようなアイデアを実現するには、次のような方法があります。
struct InputsData // take the structure, according to which the binary file was created { int symbol_id; // id of a balanced symbol ENUM_POSITION_TYPE type; // position type }; InputsData inputsData[]; // storage of inputs string filename="Inputs.bin"; // file name ArrayFree(inputsData); // array released long count=FileLoad(filename,inputsData,FILE_COMMON); // load file
この方法の主な欠点は、FileLoad()関数がオブジェクトデータ型を含むデータ構造体では動作しないことです。従って、構造体にstringデータ型が含まれている場合は動作しません。この場合、カスタムコンテナ辞書を追加で使用し、int型整数値の文字のidを適切なstring データ型に変換するか、対応するデータベースへの追加リクエストをおこなう必要があります。一般的に、この方法は、かなり単純な操作の実行が複雑になりすぎるため、私たちの実装に最も適しているものではあません。
3.では、特に.sqliteファイルのデータベースを操作するために、端末の組み込み機能を使用することを勧めています。これは、ハードディスクに保存されたファイルとの相互作用に基づいて構築されたリレーショナルデータベースを操作するための組み込み端末オプションです。
string filename="Inputs.sqlite"; // file name with inputs prepared in advance int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON); // open the database if(db!=INVALID_HANDLE) // if opened { // implement queries to the database using the DatabaseExecute() function // the query structure will depend on the structure of the database tables }
このアプローチを実施するにあたっては、まずデータベースのテーブル構造を構築することが重要です。必要なデータを得るためのクエリの構造は、これによって決まります。このアプローチの主な利点は、リレーショナルデータストレージが可能になることで、ツールのIDはstring形式ではなく整数形式でテーブルに保存されます。これにより、コンピュータのディスク領域を最適化する上で非常に大きなメリットが得られます。また、特定の条件下では、このバージョンのデータベースが非常に生産的であることも注目に値します。詳細は「SQLite:MQL5でSQLデータベースをネイティブに扱う」をご覧ください。
4.は、リモートデータベースを適用するオプションですが、これにはサードパーティーの追加ライブラリを使用する必要があります。このオプションは、標準的な端末機能では完全に実装できないため、前の方法よりも手間がかかります。「MQL5 (MQL4)から MySQL データベースにアクセスする方法」稿で紹介されている、端末とMySQLデータベースとのインタラクションを実装するための良いオプションを含め、このテーマに関する多くの出版物があります。
5.の、入力を得るためのWeb APIリクエストの使用は、おそらく私たちのタスクにとって最も普遍的でクロスプラットフォームなソリューションと考えられます。この機能は、WebRequest()定義済み関数を介して端末に組み込まれており、汎用性が高いため、バックエンドとフロントエンドのアプリケーションのインフラをすでに持っている場合に最適です。そうでなければ、多くの最新のプログラミング言語やインタープリターでこれらのソリューションを書くことができるにもかかわらず、ゼロからこれらのアプリケーションを開発するのに非常に多くの時間とリソースを要することになりかねません。
現在の実装では、6からinput型メモリクラス修飾子を持つstringデータ型の変数を使用します。このオプションは、カスタムプログラムを追加開発せずに、必要な機能をすべて提供できるからです。当然ながら、銘柄ごとに多くの変数を宣言することはありません。なぜなら、いくつの銘柄をバランスさせるか事前に知ることができないからです。そして、これにはよりエレガントで柔軟な解決策があります。ここでは、すべての値を含む1行で作業し、その中のデータをさらに分離します。そのために、グローバルレベルで次のような形で変数を宣言します。
input string input_symbols = "EURCHFz USDJPYz";
開発者でないユーザーが、アプリケーションが正しく動作するための銘柄の入力方法を正確に理解できるように、必ずデフォルト値で初期化してください。この場合、ユーザーの利便性を考慮して、通常のスペースを行区切りとして使用します。
この変数からデータを取得してsymbols[]配列に入れるには、次のような形でStringSplit() stringを処理するための定義済み端末関数を使用します。
string symbols[]; // storage of user-entered symbols StringSplit(input_symbols,' ',symbols); // split the string into the symbols we need int size = ArraySize(symbols); // remember the size of the resulting array right away
StringSplit()関数を実行すると、参照渡しされたsymbols[]配列に、スペースで区切られた(' ')stringから抽出されたデータが格納されます。
さて、リスクバランシングのための銘柄名の値で満たされた配列ができたので、さらなる計算のために、選択された端末銘柄に関する必要なデータのリクエストに移りましょう。
あらかじめ定義された端末機能による必要な銘柄データの取得
計算には、各商品の価格変動の最小値と、この変動が預金通貨でいくらになるかを知る必要があります。必要なENUM_SYMBOL_INFO_DOUBLE列挙を関数パラメータの1つにして、定義済みのSymbolInfoDouble()端末関数を使用してこれを実装できます。データ列挙の実装は、よく使用されるforループを使用して以下のようにアレンジします。
for(int i=0; i<size; i++) // loop through previously entered symbols { point[i] = SymbolInfoDouble(symbols[i],SYMBOL_POINT); // requested the minimum price change size (tick) tick_val[i] = SymbolInfoDouble(symbols[i],SYMBOL_TRADE_TICK_VALUE_LOSS); // request tick price in currency }
ENUM_SYMBOL_INFO_DOUBLE列挙には、通貨のティック価格を要求するためのSYMBOL_TRADE_TICK_VALUE_LOSS値だけでなく、SYMBOL_TRADE_TICK_VALUE値とそれに等しいSYMBOL_TRADE_TICK_VALUE_PROFIT値も含まれています。実際には、これらの値の差はそれほど大きくないので、指定された値のどれを要求しても、計算に使用することができます。例えば、AUDNZDクロスの指定引数の値は以下の表の通りです。
関数パラメータ | 関数によって返されるティック価格の値 |
---|---|
SYMBOL_TRADE_TICK_VALUE | 0.6062700000000001 |
SYMBOL_TRADE_TICK_VALUE_LOSS | 0.6066200000000002 |
SYMBOL_TRADE_TICK_VALUE_PROFIT | 0.6062700000000001 |
表1:AUDNZDのSymbolInfoDouble()関数の異なるパラメータで返されるティック価格の値の違い
この場合ではSYMBOL_TRADE_TICK_VALUE_LOSSパラメータを使用するのが最も正しいのですが、ここで提案されているオプションのどれでも使用できると思います。Richard Hammingは1962年に次のように述べています。
「計算の目的は洞察であり、数字ではない」
ボラティリティに関する必要なデータのリクエストに移りましょう。
標準のカスタムCiATRクラスを通じて必要なボラティリティデータを取得します。
前の章では、一定期間(ここでは取引日中)の銘柄の典型的な価格変動を特徴付ける指標としてのボラティリティの概念についてすでに述べました。この指標は自明なものであるにもかかわらず、その算出方法は、主に以下の点から大きく異なることがあります。
- 日足チャートの未閉鎖ギャップを考慮する
- 平均化と適用期間を用いる
- 「異常な」(例外的に稀な)ボラティリティを持つバーを計算から除外する
- 日足の高値安値または始値終値のみで計算する
- または、通常、練行足を取引しており、平均時間は私たちにとって全く重要ではない
外国為替市場とは対照的に、証券取引所の仕事ではボラティリティを計算する際にギャップが考慮されることは非常に多いです。なぜなら、外国為替市場では、日中にクローズされない市場のギャップは非常にまれであり、日次平均ボラティリティの最終的な計算は変わらないからです。ここでの計算は、2つの値から最大値を取るという点だけが異なります。1つ目は各バーの高値と安値の差で、2番目は現在のバーの高値と安値と前のバーの終値の差です。そのためには、組み込みのMathMax()関数を使用します。
得られた数値の平均化を適用する場合、平均化期間が長くなるほど、この指標は市場ボラティリティの変化に対して鈍くなることを考慮する必要があります。原則として、外国為替市場の1日のボラティリティの平均には3~5日間の期間が用いられます。また、ボラティリティ指標を計算する際には、市場の異常な動きを除外することが望ましいです。これはサンプルの中央値を用いて自動的におこなうことができます。そのためには、vector型クラスのインスタンスに対して呼び出される組み込みのMedian()メソッドを使用できます。
多くのトレーダーは、ローソク足の髭を考慮せず、始値と終値のみでボラティリティを考慮する平均化を好みます。この方法は、実際の市場ボラティリティよりもはるかに低い値を生み出す可能性があるため、推奨されません。練行足を使用する場合、ロジックは大きく変わり、ここではボラティリティの集計は取引期間からではなく、取引期間はボラティリティによって決定されます。したがって、このアプローチはこの記事にはそぐいません。
実装では、オープンソース形式の端末ライブラリに格納されている標準カスタムCiATRクラスを使用して、ATR端末指標を通じてボラティリティクエリを使用します。指標の平均化には、高速値3を使用します。一般的に、揮発性リクエストのコードは以下のようになります。グローバルレベルでは、デフォルトのコンストラクタを呼び出してクラス変数名を宣言します。
CiATR indAtr[];
ここでは、主に利便性とコードのさらなる機能拡張の可能性のために、1つの既存の指標をオーバーロードするのではなく、同時にデータを格納する指標の配列を使用しています。次に、銘柄の反復ループに次のコードを追加します。ここではすでに銘柄のデータを要求し、指標値を要求しています。
indAtr[i].Create(symbols[i],PERIOD_D1, atr_period); // create symbol and period indAtr[i].Refresh(); // be sure to update data atr[i] = indAtr[i].Main(1); // request data on closed bars on D1
計算に必要なすべての初期データが得られたので、そのまま計算に進みます。
異なるポジション終了方法に対するリスク計算のロジックには2つのオプションがあります。
複数の商品を同時に取引する際のリスクバランスを取るためのロジックを書く際には、以下の重要な点を考慮する必要があります。取引システムにおいて、バランスされたポジションからのエグジットがどのように提供されているか、また、バランスを取る銘柄の相関性をどのように考慮しているか。一見したところ、外国為替市場におけるクロスの相関性の明確な基準は、検討対象期間における相関性のレベルを必ずしも保証するものではなく、しばしば別々の銘柄として考えることができます。例えば、取引日の最初に、今日取引する銘柄とエントリの方向を決めたとしたら、事前に次のことを知っておく必要があります。取引終了時にすべてのポジションを決済するかどうか。日中のポジションの手仕舞いは、各ポジションごとに個別に検討するのか、それとも単に時間をかけてすべてのポジションを手仕舞いするのか。誰にでも当てはまるような普遍的なレシピは存在しません。トレーダーはそれぞれ、自分自身の知識と経験に基づいて、かなり大きな基準の集合からエントリとエグジットを決定するからです。
この実装では、商品の入力リスクパラメータを変更し、トレーダーが現時点で相関性があると考える商品を含める/除外するだけで、取引している事実に基づいてポジションへのエントリ量を柔軟に決定できる普遍的な計算をおこないます。そのために、1つの商品の入力リスクパラメータを以下のように宣言します。
input double risk_per_instr = 50;
また、汎用性を考慮し、ユーザーが入力したリスクを個別に取り上げた取引銘柄に回帰し、これらの銘柄の相関を考慮したバランス結果を同時に出力します。これにより、トレーダーは様々なポジション量と、最も重要なことに、同時取引におけるこれらの商品の比率を得ることができます。そのためには、まずメインの計算サイクルに以下の項目を追加する必要があります。
risk_contract[i] = tick_val[i]*atr[i]/point[i]; // calculate the risk for a standard contract taking into account volatility and point price
次に、指定されたループの外側で、セット全体の取引量のバランスをとるために、リスク値が最大となる商品をセットから探し出し、その商品から比率を計算します。コンテナの組み込み機能は、まさにこのためにあります。
double max_risk = risk_contract.Max(); // call the built-in container method to find the maximum value
セット内の最大リスクがわかったので、サンプル内の各銘柄バランスさrせたボリュームを計算する別のループを設定します(相関関係がない場合)。その結果をすぐに操作ログに表示します。
for(int i=0; i<size; i++) // loop through the size of our symbol array { volume[i] = NormalizeDouble((max_risk / risk_contract[i]) * (risk_per_instr / max_risk),calc_digits); // calculate the balanced volume } Print("Separate"); // display the header in the journal preliminarily for(int i=0; i<size; i++) // loop through the array again { Print(symbols[i]+"\t"+DoubleToString(volume[i],calc_digits)); // display the resulting volume values }
これは、銘柄に割り当てたリスクを考慮し、1日平均の最も確率の高い予想ボラティリティとバランスをとりながら、ポジションの日中取引の最大取引高となりました。
次に、銘柄が取引日に相関し始める可能性があることを前提に、ポジションの出来高を計算します。この場合、銘柄の予想リスクは、入力パラメータに入力したものに比べて、実際には大幅に増加する可能性があります。これをおこなうには、以下のコードを追加し、結果の値を取引されている商品の数で割るだけです。
Print("Complex"); // display the header in the journal preliminarily for(int i=0; i<size; i++) // loop through the array { Print(symbols[i]+"\t"+DoubleToString(volume[i]/size,calc_digits)); // calculate the minimum volume for entry }
これで、リスクバランスポジションの数量の上限と下限が得られました。トレーダーはそれらに基づいて、与えられたレンジ内のエントリ量を独自に判断することができます。主なことは、計算で示された比率を守ることです。また、結果を記録する方法は非常にシンプルですが、それだけではないことにも注意が必要です。また、他の標準的な端末機能を使用して、ユーザーに情報を表示することもできます。この場合、端末は、MessageBox()、Alert()、SendNotification()、SendMail()などの関数の使用を含む、非常に幅広い機能を提供します。コンパイルされたファイルにあるEAの完全なコードに移りましょう。
スクリプトによるソリューションの最終実装
その結果、実装コードは次のようになります。
#property strict #include <Indicators\Oscilators.mqh> //--- input string input_symbols = "EURCHFz USDJPYz"; input double risk_per_instr = 50; input int atr_period = 3; input int calc_digits = 3; CiATR indAtr[]; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { string symbols[]; StringSplit(input_symbols,' ',symbols); int size = ArraySize(symbols); double tick_val[], atr[], volume[], point[]; vector<double> risk_contract; ArrayResize(tick_val,size); ArrayResize(atr,size); ArrayResize(volume,size); ArrayResize(point,size); ArrayResize(indAtr,size); risk_contract.Resize(size); for(int i=0; i<size; i++) { indAtr[i].Create(symbols[i],PERIOD_D1, atr_period); indAtr[i].Refresh(); point[i] = SymbolInfoDouble(symbols[i],SYMBOL_POINT); tick_val[i] = SymbolInfoDouble(symbols[i],SYMBOL_TRADE_TICK_VALUE); atr[i] = indAtr[i].Main(1); risk_contract[i] = tick_val[i]*atr[i]/point[i]; } double max_risk = risk_contract.Max(); Print("Max risk in set\t"+symbols[risk_contract.ArgMax()]+"\t"+DoubleToString(max_risk)); for(int i=0; i<size; i++) { volume[i] = NormalizeDouble((max_risk / risk_contract[i]) * (risk_per_instr / max_risk),calc_digits); } Print("Separate"); for(int i=0; i<size; i++) { Print(symbols[i]+"\t"+DoubleToString(volume[i],calc_digits)); } Print("Complex"); for(int i=0; i<size; i++) { Print(symbols[i]+"\t"+DoubleToString(volume[i]/size,calc_digits)); } } //+------------------------------------------------------------------+
コンパイル後、入力ウィンドウが表示されます。例えば、最大リスクが500米ドルで、3つの銘柄のバランスを取るとします。
図2:トレーダー入力
スクリプトを実行した結果、リスクバランスを考慮した各銘柄の出来高について、以下のデータが得られます。
図3:出力データ
ここでは、端末が提供する最もシンプルで必要最小限の機能を使用して、複数のリスクバランス銘柄の同時取引量を計算するためのフル機能のコードを示します。もしご希望であれば、記事で紹介されている追加機能や私たち独自の開発を使用して、このアルゴリズムを拡張することも可能です。
結論
アルゴリズム取引で取引しないトレーダーが、日中取引時にポジション量を迅速かつ正確に調整できるようにする、完全に機能するスクリプトができました。アルゴリズム取引をしている人にとっても、ソフトウェアインフラを改善し、現在の取引結果を改善するための新しいアイデアがここにあることを願っています。ご精読ありがとうございました。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/14163





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索