【FX自動売買】EAソースコード解説【MQL5】
こんにちは、ゆみねです。
フリーランスのエンジニアをやっています。
さて、前回はプログラミングに触った事のない方へ向けて「なんとなく」プログラムのイメージがつかめるようになってもらう事を目指して記事を執筆しました。
今回は、実際のFX自動売買プログラム(以下EA)のサンプルプログラム(ソースコード)を見ながら、その動作等を具体的に解説していこうと思います。
対象言語はMQL4の方が難易度が低かったのですが、MQL5の情報を求める声が多かったため、今回はMQL5のソースコードで解説していきたいと思います。
前回の記事には出てこなかった概念も出てくるのですが、極力解説していきますので、頑張ってついてきてください。
(動作の解説が中心ですので、基礎的な部分は省略する事もあります)
※おそらく、初心者の方にとっては今回の記事は前回の記事より遥かに難易度が高いです。極力細かい説明も付与しながら解説しますが、この記事はあくまでも「ソースコードの動作解説」が中心であるため、自身で学習を進める際の「補助資料」としてお使い頂けますと幸いです。公式ドキュメントのリンクを各所に貼っておきますので、都度ご確認頂きながらご覧頂けますと理解が深まりやすいかと思います。(貼りすぎて広告みたいになってしまったので、極力テキストにリンクを張るようにしてます…)
解説対象EAについて
今回は、MT5にサンプルとして最初から付属しているEAを具体例として解説していきます。(MT4にもありますがMQL4なので若干内容が異なります)
具体的には、MA(移動平均線)を用いた「Moving Average」というEAのソースコードの動作を解説していきます。
MT5をインストールされている方であればどなたでもこのソースコードを見る事ができますので、ご自身でお使いのPCでも同じソースコードを開いて確認しながらこちらの記事を読んで頂けると、より理解しやすいかもしれません。
対象のソースコードの開き方は以下の通りです。
1.MT5を起動する
2.メニューの「ツール」→「Meta Quotes 言語エディタ」を選択
※もしかしたら若干名称が違う環境もあるかもしれません。
3.「MetaEditor」というものが起動するので、ナビゲータから「Moving Average.mq5」をダブルクリック
※「experts」→「Examples」→「Moving Average」の順にフォルダアイコンをダブルクリックすれば「Moving Average.mq5」が出てきます。
※ナビゲータが表示されていない場合は、青丸のアイコン、もしくはメニューの「表示」→「ナビゲータ」をクリックすれば表示されます。
4.Moving Average のソースコードを閲覧できます
※背景の色は違うかもしれませんが、ソースコード自体は同じはずです。
実際にソースコードを開けましたでしょうか?
※MT4にも同じ名前のEA(ソースコード)があるので、見比べてみるとMQL4とMQL5の違いが見れて面白いかもしれません。
では早速解説に移ります。
ソースコード解説
1行目から解説していきます。
※一度解説したものと同じようなコードの場合は省略します。
※既に説明したものは知っているものとして書いていくため、後半にいくにつれ省略が増えますが、ご了承ください。
1~5行目
1~5行目までは先頭に "//" が書かれていますので、全てコメント行です。
"//" より後ろは全てコメント(説明・落書き)として扱われ、プログラムの動作には一切影響を及ぼしません。
ここでは、このプログラムの著作権表記等が書かれていますね。
コメントは基本的に人間がメモ用途で使うものですので、開発者が自由に好きな事をメモしておく事ができます。
ちなみに、"//" は単一行コメントで、(その行の中の)"//" より後ろの箇所だけがコメントとして扱われますが、以下のように "/*" と書くと、そこから "*/" までの間の全ての文字列がコメントとして扱われます。
/* コメント
ここもコメント
ここも
全部コメント
*/
int count = 0; // ←これは実行される
※以降、コメント行の説明は省略します。
6~10行目
先頭に "#" が書かれている行はプリプロセッサ命令(識別子)であり、ソースコードをコンパイルする際にその前処理として実行される命令です。
「コンパイル」とはソースコードをコンピュータにもわかる言語に変換する事を指します。
前回の記事で、「プログラム(ソースコード)はコンピュータにもわかる言語だ」という旨の説明を書きましたが、厳密にはこのソースコード自体はコンピュータは理解できません。
コンパイルを行って初めてコンピュータが理解できる状態になり、その理解できる状態になったものが「インジケータ/EA(の本体)」です。
つまり、ソースコードを書くだけではインジケータ/EAは作られず、そのソースコードをコンパイルして出来上がったものこそが、我々が普段利用しているインジケータ/EAになります。
またまた前回の記事で、「MT4向けのインジケータ/EAはMT5では使えないけど、ソースコードは使える場合もある」という旨の説明を書いたのはこれが理由ですが、詳細は割愛します。
ソースコードの説明に戻りますが、ここではインジケータ/EAのプロパティ(属性的なもの)の設定と外部ファイルの読み込み(Include)をしています。
プロパティとはインジケータ/EAの説明文等の事で、プログラムの動作とは直接関係の無い箇所の設定になります。
(以下の画像は私の自作インジケータの例です)
外部ファイルの読み込み(Include)は、他のファイル(プログラム)を読み込んでこのプログラム内でも使えるようにするもの、と思って頂いてOKです。
ここで読み込んだものは後々使う事になります。
12~15行目
前回の記事でも出てきた変数の宣言と初期化を行っています。
宣言する変数の名前は何でもOKです。
(一応規則があって、付けられない名前もあります)
変数の型(種類)として、doubleは倍精度浮動小数点数型(いわゆる実数)、intは整数型を意味します。
変数を宣言しつつ、同時に中身を設定(初期化)したい場合はこのように書く事ができます。
先頭に input と書くと、インジケータ/EAを利用する時にパラメータとして設定できるようになります。
例えば12行目では、double型のMaximumRiskという変数を宣言し、0.02で初期化していますが、パラメータ(input)として宣言されているので、このEAをチャートにセットした時に画面上から自由に数値を設定できる、という事ですね。
また、この変数のように、後述する「関数」の外側で宣言された変数はグローバル変数と呼ばれ、このEAのプログラム内からならどこからでも呼び出せる変数になります。
ここで宣言されている変数が実際にどう使われているのかは現時点では誰にもわかりませんので、「変数?それがどうしたの?何に使うの?わかんない。無理」とならないでください。私もわかりませんし、この時点ではわからないのが当たり前なので…。
17~21行目
17~18行目は変数宣言と初期化です。
19行目も変数と似たようなものですが、厳密には CTradeクラス をインスタンス化(MQL5的には初期化、という方が正しいのかも…?)しており、CTradeクラスの実装を ExtTrade という名前で使う事を宣言しています。(今はわからなくてOKです)
17~19行目では input が書いていないので、これらはEA利用時にパラメータとして値を設定する事はできません。
あくまでもプログラム内部でのみ使う変数の宣言です。
bool は論理型で、true か false の2値を保持できます。
(内部的には1byteの整数ですが、trueかfalseのどちらかしか持たない、という認識でOKです)
21行目ではまたプリプロセッサ命令(#define)です。
#define 名称 定数
これで定数に名前を付ける事ができ、プログラム内ではその名前で利用する事ができます。
今回の例だと、 プログラム内で MA_MAGIC と書くと、 1234501 として認識されるようになる、という事ですね。
25~85行目(TradeSizeOptimized関数)
これ全体で一つの TradeSizeOptimized関数 を定義しています。
前回の記事でも書いた通り、関数は定義するだけでは動作せず、利用される(呼び出される)事で初めて実行されます。
順を追って見ていきましょう。
25~26行目(+85行目)
戻り値がdouble型、引数は無し(void)の TradeSizeOptimized という名前の関数を定義しています。
つまり、この関数を利用する時は引数を付けずに利用し、double型の値を受け取る準備をする必要がある、という事ですね。
double ret = TradeSizeOptimized(); // こんな感じで呼び出せる
Print(TradeSizeOptimized()); // これもOK
波括弧 "{" から85行目の波括弧 "}" までがこの関数の定義部になります。
27~28行目
変数宣言と初期化です。
関数定義の内部で変数を宣言した場合、その変数はその関数の内部でしか利用できません。(ローカル変数)
この関数の外で price と書いても、「そんな変数は無い」とエラーになってしまいますので気を付けましょう。
※以降の変数宣言は省略します。
30~35行目
条件分岐が3つ出てきました。以前の記事でも紹介した if ですね。
以前の記事では「if の括弧()内の条件が正しければその後ろの処理を実行する」と書きましたが、厳密には「if の括弧()内がtrueと評価されれば…」となります。
「とりあえず括弧()内が true とか正しい事書いていれば実行されるんだ」くらいの認識でOKです。
30行目の SymbolInfoDouble(_Symbol, SYMBOL_ASK, price) は、price にこのEAをセットしているチャートの銘柄の価格(ask)を代入する、という命令(関数)です。(_Symbol は銘柄名が格納されている変数です)
この関数の戻り値は 論理型 で、成功すれば true 、失敗すれば false を返します。(重要)
基本的には関数が先に評価されて、その後に if が評価されますので、30行目は先に SymbolInfoDouble(_Symbol, SYMBOL_ASK, price) が評価され、戻り値の true か false が if の条件式として評価されます。
つまり、関数の実行が成功した場合は、以下のようなイメージです。
if ( !SymbolInfoDouble(_Symbol, SYMBOL_ASK, price) ) return;
// 上のコードの関数の戻り値が true であれば、以下と同じ
if ( !true ) return;
「関数を呼び出すとその関数の戻り値に置き換わる」と思って頂いてOKです。
しかしよく見てみると、if 括弧()の中のSymbolInfoDouble関数の前に ! が書かれていますね。
これは論理演算子のうち、論理否定と呼ばれる演算を行う演算子です。
false なら true, true なら false に、というように true と false を反転させる演算子です。
仮に SymbolInfoDouble(_Symbol, SYMBOL_ASK, price) が実行に失敗した場合は false を返しますので、 その場合は論理否定(!)によってこの if は true と評価されます。
つまり、30行目の if は SymbolInfoDouble(_Symbol, SYMBOL_ASK, price) が失敗した時に if の条件が true と評価され、31行目の return(0,0); が実行されます。
return で関数内の処理をそこで終了し、 0.0 を返しています。
(このあたりで「わからん」という声が一気に増えそうな予感ですね…)
32行目の OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,1.0,price,margin) は、このEAをセットしているチャートの通貨ペアでprice価格で1ロットのロングエントリーをする時に必要な証拠金を計算し、その金額を margin に代入する、という命令(関数)です。
現在保有中のポジションは無視されるので注意が必要です。
これも先程と同様に成功すれば true、失敗すれば false を返しますので、失敗した場合に return(0,0); が実行されます。
34行目は先程の margin (1ロットロングするために必要な証拠金)が0.0以下だった場合に return(0.0); が実行されます。
37行目
AccountInfoDouble(ACCOUNT_MARGIN_FREE) は余剰証拠金を返す処理です。
MaximumRisk は冒頭で宣言した変数、margin はすぐ上で宣言した変数ですね。
NormalizeDouble() は、与えられた浮動小数点数の数値の小数点以下を、与えられた桁数で丸めて返す関数です。
NormalizeDouble(数値, 桁数) のように使います。
ここでは、「余剰証拠金×MaximumRisk÷margin」の計算結果を小数点以下2桁で丸める、という処理を行い、それを double型の変数の lot に代入しています。
(プログラム内では、"*"は掛け算、"/"は割り算を表します)
39~71行目
39行目、冒頭で宣言した変数の DecreaseFactor が 0より大きければ、この大きなブロック(40~71行目)が実行されます。
42行目の HistorySelect(0, TimeCurrent()) で注文と取引の履歴を取得します。注文や取引の履歴を扱いたい場合に呼び出す関数です。
44行目の HistoryDealsTotal() は約定履歴の数を取得する関数です。注文履歴と約定履歴は別ものなので注意が必要です。
47(48)~67行目までは繰り返し構文の for で繰り返される処理です。
i という変数に orders(ここでは約定履歴の数)-1 を代入し、i を1ずつ減らしながら i が0以上の間ずっと繰り返し続ける、という for文 です。
for の定義は以下です。
for (初期処理; 繰り返し条件; 繰り返し時に実行するコード) {
// 色々な処理
}
49行目で ulong型 の変数 ticket を宣言し、HistoryDealGetTicket(i) の戻り値を代入しています。
ulong型は負の値を持たない大きなint(整数型)だと思ってください。
HistoryDealGetTicket(i) は、指定したインデックス(約定履歴内での番号、0,1,2...の様に0から順に付与)の約定履歴からその約定チケット(ID)を取得します。
ここでは最新の約定履歴から1つずつ順に約定チケット(ID)を取得し、以降の処理でこの約定履歴の情報を1件ずつ扱っていくための準備をしています。(このチケットを用いる事で約定履歴を一意に指定することができます)
50行目の if は ticket が取得出来なかった場合のエラー処理で、52行目のPrint()関数は文字列をログ(ツールボックス)に出力する関数です。
53行目の break は for のブロックの中で記述可能な命令で、forの繰り返し処理から強制的に抜け出す命令です。
この場合、break が実行されると、強制的に69行目へ処理が移ります。
56行目の HistoryDealGetString(ticket,DEAL_SYMBOL) は、先程取得した ticket の銘柄(通貨ペア)を返す処理で、_Symbol は現在のチャートの銘柄になります。
ここの if では、"!=" によって比較演算が行われていますが、これは「等しくなければtrue」となる演算子です。
つまりここの if では、約定履歴(ticket)と現在開いているチャートの銘柄を比較して異なる場合に57行目の continue が実行されます。
continue は先程の break と同様に for のブロック内で記述可能な命令で、強制的に次の繰り返し処理へ移る命令です。
つまり57行目の continue が実行されると、58行目以降は無視され、強制的に for の次の繰り返し処理(48行目~)に移ります。( i-- は実行されます)
59行目の HistoryDealGetInteger(ticket,DEAL_MAGIC) は、ticket のマジックナンバーを取得できる処理で、マジックナンバーはエントリー時にその取引に紐づける事のできる任意の数値です。
ここの if では、冒頭で #define した MA_ MAGIC と比較し、異なる場合に continue しています。
後ほど出てきますが、このEAではエントリー時に必ず MA_MAGIC をマジックナンバーとして設定しているため、このEAからの約定履歴には必ずこの MA_MAGIC が含まれているはずです。
つまり、56行目と59行目の if 2つで何をやっているかというと、たくさんある約定履歴の中から「今のチャートで開いている銘柄」であり、かつ「このEAでエントリー(約定)した履歴」を探している、という事ですね。(そうでない ticket(約定履歴) は全て continue される)
62行目では double型変数 の profit を宣言し、HistoryDealGetDouble(ticket,DEAL_PROFIT) の戻り値を代入しています。
HistoryDealGetDouble(ticket,DEAL_PROFIT) はその約定履歴の利益を取得できる処理ですので、この約定履歴の利益を取得して profit に代入しています。
63行目で、この profit がプラス(>0.0) であれば break で for を抜け、65行目で profit がマイナス(<0.0)であれば losses をプラス1しています。
losses は45行目で宣言されている整数型の変数で、"++" はインクリメント演算子と呼ばれ、変数に1を足す演算子です。
…for の処理が長かったですが、この繰り返し処理では直近の約定履歴からこのEAでの約定履歴を探し、その履歴から勝ったのか負けたのかを調べ、直近の連敗数を調べているようです。
後で出てきますが、このEAは保有ポジションを1つに限定しており、ポジション保有中はこの関数が実行されることもないようなので、この処理で連敗数を調べる時に保有中の(まだ利益が確定していない)ポジションを対象に調べてしまう事は無さそうです。
そして69行目でその連敗数が1より大きい場合(つまり2連敗以上)、lot に lot-lot×losses÷DecreaseFactor の計算結果を(Normalizeして)代入しています。
DecreaseFactor はデフォルトでは 3 が設定されているので、3連敗以上すると lot が0以下になってしまいますね。
つまり、DecreaseFactor とは最大連敗数的な何かを定義しているのかな?(推測)という事がここからわかります。
73~84行目
73行目では SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP) でお使いの証券会社のロットの最小単位を取得し、74行目で最小単位以下の端数を丸めています。
76行目の SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN) でお使いの証券会社の最小ロット数を取得し、上で算出したロット数が最小ロット数以下だった場合は最小ロットをロット数として設定しています(78行目)。
80行目の SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX) ではお使いの証券会社の最大ロット数を取得し、上で算出したロット数が最大ロット数以上だった場合に最大ロット数をロット数として設定しています(82行目)。
84行目で、この関数からの戻り値として lot (ロット数)を返して関数定義を終了しています。
ちょっと余談
知らない関数が大量に出てきて「あ、無理」となる人も多いかと思いますが、関数を知らないのは皆同じで、私も知らないものたくさんあるのでその都度調べます。一々覚えていません。
だからそういう人は「皆同じ。そういうもの。」と思って頑張りましょう。
でも、計算式が出てきて「あ、無理」となってしまう人は厳しい道のりかもしれません…。
プログラミングはめちゃめちゃ数学を使うので、数学が苦手な方はかなりの努力が必要になるかと思います。
苦手なものにチャレンジするのはストレスも大きいので、EA開発を勉強しようとしてストレスで普段のトレードがボロボロになってしまっては元も子もありません。
少し学習ペースを落としてゆったり進める等、何かしら工夫をして確実に努力を重ねていく必要があるかもしれませんね。
89~126行目(CheckForOpen関数)
さて、また関数の定義です。
戻り値も引数も void なので、戻り値も引数も無い CheckForOpen関数 を定義しています。
関数名的に新規注文を出すための関数っぽいですね。
91行目
MqlRates型 の配列を定義しています。いきなり新しい概念が2つも出てきました。
MqlRates は構造体の名前で、構造体とは1つの変数の中に複数の変数を保存できるもの、と思って頂いてOKです。
構造体も変数と同じで自由に名前を定義できますし、内部にどんな値をどんな名前で保存するかも自由に定義できます。
この MqlRates はMQL5で最初から定義されている構造体で、価格やスプレッド等の情報(≒ロウソク足の情報)を格納するための構造体です。
そして配列とは、その型の変数を同じ名前で複数並べて(連番で)保持できるものです。
例えば以下のように使います。
int count[4]; // int型の配列(サイズは4)を宣言
count[0] = 0; // []の中に数字(index)を書いて各変数にアクセスできる
count[1] = 10; // []の中の数字は必ず0から始まり、1ずつ増える
count[2] = 20;
count[3] = 30;
// 例えば以下のように使える
for (int i = 0; i < 4; i++) { // iを0から4未満まで1ずつ増やしながら繰り返し
Print(count[i]); // count配列の[0]から[3]までを一気にログに出力する
} // iが4になった瞬間に条件を満たさなくなるため、Print(count[4])は実行されない
つまり、ここでは MqlRates構造体 をサイズ2の配列で宣言しているため、 (以下の93行目を実行した後に)rt[0] と rt[1] で MqlRates構造体 にアクセスできる、という事です。
93~99行目
CopyRates(_Symbol, _Period, 0, 2, rt) はこのEAをセットしているチャートの銘柄・時間足で、現在のロウソク足(0本目)から2本分のロウソク足の情報を rt(配列) に格納する命令で、戻り値は実際に格納できたMqlRates構造体の数です。(_Period は現在の時間足が格納されている変数です)
MqlRates構造体の rt は配列でサイズ2と宣言していて、関数への引数に2本分の情報を指定しているので、関数が正しく実行されれば2を返すはずです。
93行目の if では、正しく関数が実行されたかを確認するためにこのような書き方になっており、正しく実行されていなかった場合は96行目で return (終了)しています。
98行目では、実際に取得したMqlRates構造体のデータにアクセスしています。
この時、rt[0]は一つ前のロウソク足のデータ、rt[1]は現在のロウソク足のデータが格納されています。(ここややこしいです…)
構造体は「.(ドット)」を付けて各データへアクセスします。
rt[1].tick_volume には、現在のロウソク足のティックボリュームが格納されています。
※他にも、.open で始値、.low で最安値、等の情報が格納されています。
つまり98行目の if では、現在のロウソク足のティックボリュームが1より大きい場合にreturnさせており、逆に言えば現在のロウソク足のティックボリュームが1以下、つまりロウソク足が切り替わって最初のティック時のみreturn されずに通過できる、という事です。
ロウソク足が切り替わったタイミングで何か処理をしたい場合に使えるテクニックですね。
101~106行目
101行目で double型 の配列を宣言しています。
CopyBuffer(ExtHandle,0,0,1,ma) は指標ハンドル(ExtHandle)から対象のバッファ番号(0)を持つデータを現在のロウソク足から1つ分だけ取得して ma(配列) に格納する処理で、CopyRates関数と同様に実際に格納したデータの数を戻り値として返します。
指標ハンドルやバッファ番号については割愛しますが、ここでやっている処理は ma にMA(移動平均線)の値を格納しているだけです。(指標ハンドルは後述の処理で正体がわかります。バッファ番号は基本0です。)
実行に失敗したら return しているのも CopyRates関数の時と同じですね。
108行目
出ました列挙型。これも変数の型の一つで、特定の決められた値(名前付き定数)しか保持できない変数です。
自分で列挙型の変数を定義する場合は自由に名前を付けて定義できますが、ここではMQL5で最初から用意されている ENUM_ORDER_TYPE という名前の列挙型を使っています。
ENUM_ORDER_TYPE では「注文の種類」を指定するための値を保持できます。
ここでは、WRONG_VALUE という定数を指定して初期化しています。(内部的にはただの-1)
110~116行目
1つ前のロウソク足の始値の方がMAの値より大きく、かつ(&&)、1つ前のロウソク足の終値の方がMAの値より小さい場合に signal に ORDER_TYPE_SELL(定数) を代入しています。
("&&" は、その前後の式がどちらも true だった時に true と評価される論理演算子です)
その条件を満たさない場合(112行目以降)は別の条件で(114行目)、1つ前のロウソク足の始値の方がMAの値より小さく、かつ(&&)、1つ前のロウソク足の終値の方がMAの値より大きければ、signal に ORDER_TYPE_BUY(定数) を代入しています。
どちらの条件も満たしていない場合は signal に何も代入しないので、signal を初期化したときの WRONG_VALUE が入ったままになっています。
118~124行目
signal が WRONG_VALUE でなければ120~123行目の処理を実行します。
TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) は自動売買が許可されているかどうかを取得する処理で、Bars(_Symbol,_Period) はこのEAをセットしているチャートの銘柄と時間足でのMT5上のロウソク足の本数を取得する処理です。
つまり120行目の if では、「自動売買が許可されていて(true)、かつ(&&)、100本より多くのロウソク足が存在していれば」121行目の処理を実行します。
121~123行目は一つの処理で、ついに「新規(成行)注文」の実行です。
(関数に渡す引数が長くなってしまう場合はこのようにカンマの後に改行しても問題ありません)
ここでも.(ドット)が出てますが、これは構造体ではありません。
これはクラスというものですが今回は説明を割愛します。これだけで1つの記事ができてしまうので…。
これは構造体と似たようなものなのでぼんやりと同じイメージで捉えていても大丈夫です。
ここまで読めていれば、「ExtTrade.PositionOpen関数(メソッド) に対して色々と引数として値を渡して注文処理を実行している…のかな?」と思って欲しいところです。(その認識で正しいです)
…が、122行目に新しい演算子「?」がでてきます。
式1 ? 式2 : 式3;
上記の場合、式1を評価した結果が true だった場合に式2が、false だった場合に式3が実行され、その結果が返る、というものになります。
122行目のうち、
signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK
この部分は三項演算子"?"を使った処理ですので、signal が ORDER_TYPE_SELL と等しい場合は SYMBOL_BID が、そうでなかった場合は SYMBOL_ASK がそれぞれ返されます。
つまり、
SymbolInfoDouble(_Symbol, SYMBOL_BID) で現在のBid価格、
SymbolInfoDouble(_Symbol, SYMBOL_ASK) で現在のAsk価格、
をそれぞれ取得できますので、signal に応じてBid価格かAsk価格か、どちらかを取得する、というコードになっています。
結局ここでは、「このEAがセットされてるチャートの銘柄で signal(BUY or SELL) を TradeSizeOptimized() ロット、現在の価格で新規注文する」という処理になっています。(最後の0,0はそれぞれ SL と TP を設定する箇所で、0の場合はただの新規注文になります)
またまた余談
引数部分にごちゃごちゃと式を書かれたり、あえてわかりにくく書かれてたりするコードもけっこうありますが、一応理由があります。(書いてる人がそれを意識しているかは別として…)
実は短いコードの方が処理速度が早かったりメモリを節約できたりする場合があるので、ベテランのプログラマさん程すごく読みにくい(短い)コードを書く人が多い印象です。(必ずしも短いコードが効率が良いわけではないですが、そういうテクニックがある、という話です)
数十年前のゲーム業界なんかはこれが死活問題で、今では考えられない程効率の良いコードを書かないとそもそも仕事にならない、という時代でした。(ちょっと大げさかもしれません…笑)
当時のプログラマは本当に職人芸で、技術力の塊みたいな人が裏でひっそりとすごいプログラムを書いて世の中のゲーム業界を引っ張っていました。
コードの書き方とは少しズレますが、例えばファミリーコンピュータで出た「スーパーマリオブラザーズ」は皆さんご存知かと思います。
あのゲーム、容量(プログラムのサイズ)がどれくらいかご存知でしょうか?
最近だとPS4のゲームでは平気で数十GB(ギガバイト、1GB=1024MB)を超えるものが多いですね。
スマホゲーでも数GBに達するゲームもあります。
当時と一概に比較できるわけではありませんが、当時の「スーパーマリオブラザーズ」はなんと40KB(キロバイト)でした。
みなさんが良く聞く音楽も1曲あたり10MB(メガバイト、1MB=1024KB)以上はありますし、Youtube動画の小さいサムネ画像1枚で1MB程度はあります。
マリオや敵キャラクターの画像、ステージの背景、音楽、全部含んで40KBです。
当然、普通にプログラムを書いていたらこのサイズには収まりません。
当時のゲームカセットの容量の都合でどうしてもこのサイズに収める必要があったため、画像や音楽、プログラムをいかに小さいサイズで実装するかを突き詰め、最適化に最適化を重ねてこのサイズに収める事に成功しました。
つまり、当時は(今もですが)プログラマの腕がそのまま品質に直結する時代だったため、本当に凄腕のプログラマの書くコードは普通のプログラマには読めない、理解できない、という事もありました。
「どうしてこのコードでこんな動作をするプログラムになるの!?」という感じでした。
このように、同じ動作をするプログラムを書こうと思っても、人によってコードが全然違うのです。
100人が同じ動作をするプログラムを書いたら、100通りのコードが出来上がります。おそらく全く同じコードを書く人は1人もいないでしょう。(もちろん規模にもよります)
パッと見の動作が同じでも、それはPCの性能が良いからたまたま同じように見えているだけで、実は内部的には全然処理が違っていて処理速度もメモリ使用量も全然違う、という事は往々にしてあります。
ただ、最近のPCやサーバは処理性能が十分に高く、コンパイラの性能も上がっているので、今は処理速度や使用メモリを意識して実装しなくてもさほど品質に影響しなくなりました。(今でもゴリゴリに突き詰める必要がある業界もあります)
そのため、「実行効率の良いコード」よりも「見やすさや保守性の高いコード」の方が価値が高いと評価される業界が増えてきました。
一概にプログラムといっても色んな考え方や書き方があるのですが、「同じ動作でも人によって全然違うコードになる」というのはプログラミングの面白さの一つだと思っています。
自分の考え方や個性がそのままプログラムのコードとなって現れる、なんだかアーティストのようでもありますね…笑
ある程度経験を積んだプログラマでも、他の人のプログラムを読むだけで「なるほど…こういう書き方もあるのか…」と他者の考えを吸収して成長できるため、思うようにプログラムが書けなくとも様々なプログラムを読んでいるうちにアイディアが降ってきたりするものです。
今プログラミングを学習中の皆さんも、続けていれば少しずつプログラムが読めるようになっていき、いつの間にか経験値が溜まって自分の書きたいプログラムが書けるようになっている、というように着実にレベルアップできますので、挫折しそうになっても「とりあえずプログラム読んでみる」というところだけでも意識して続けてみてはいかがでしょうか。
(すみません、余談長くなりました…)
130~163行目(CheckForClose関数)
さて、後半戦です。
関数名的にポジションをクローズするための関数っぽいですね。
こちらも戻り値、引数共にvoidで定義されています。
132~147行目
ここは上で説明したCheckForOpen関数と全く同じ処理なので省略します。
150行目
long型の type に PositionGetInteger(POSITION_TYPE) の戻り値を代入しています。
この関数は事前にポジションを選択しておく必要があり、その選択されたポジションを対象として、そのポジションの情報(ここではポジションが買いなのか売りなのかという情報)を取得するものです。
ポジションの選択は PositionGetSymbol関数 または PositionSelect関数 にて事前にポジションを選択するのですが、この関数の内部では実施されていないため、この関数の外側で実行される事が必須になります。
思わぬ事故に繋がるので、こういう書き方(設計)は個人的には嫌いです…このEAの作者さんはどうしてこんな設計にしたのでしょうか…。(一応このEA全体で見れば事故らない設計にはなっていますが…)
152~155行目
またややこしいコードになっていますが、これも余談で書いたような「ちょっと実行効率の良い(かもしれない)コード(テクニック)」です。
これは以下のように分解して考えます。(152行目を例に)
if ( 式1 && 式2 && 式3 )
// 式1が"type==(long)POSITION_TYPE_BUY"
// 式2が"rt[0].open>ma[0]"
// 式3が"rt[0].close<ma[0]"
この時、式1、式2、式3が全て && で繋がれており、左の式1から順番に評価されていくため、式1や式2が false として評価された時点で式3の評価をせずに(省略して)if の条件分岐が実行されます。
&& は論理積で、どこかに false が一つでもあれば全体が必ず false として評価されるため、この性質を利用して効率よく条件分岐を行う事ができる…というテクニックです。
ここでは型キャストも行っていますね。
type は150行目で long型 として宣言していましたが、POSITION_TYPE_BUY は列挙型(内部的には4バイト整数型)なので、 型が異なり、== で比較ができないのです。
しかし、列挙型は long型 に変換が可能なので、ここでは型キャスト(型の一時的な変換)を行って比較を可能にしています。
つまり150行目では、「対象のポジションが買いポジションで、かつ1つ前のロウソク足の始値がMAの値より大きく、かつ1つ前のロウソク足の終値がMAの値より小さい場合」という条件になり、これを満たす場合に153行目で signal に true を代入しています。
(154行目も同様なので省略します)
157~161行目
signal が true であれば159行目以降を実行します。
159行目は CheckForOpen関数 でも説明したものと同じ条件ですね。
この条件を満たす場合、160行目でポジションをクローズ(決済)しています。
ExtTrade.PositionClose(_Symbol,3); はEAがセットされているチャートの銘柄の選択中のポジションを許容偏差値3(point)で決済する処理です。
許容偏差値はいわゆるスリッページの事で、どこまでのスリッページを許容するかをpoint単位で指定します。
167~194行目(SelectPosition関数)
オリジナル関数はこれで最後です。頑張りましょう。
戻り値は bool で宣言されていますが、何故かこの関数だけは引数の宣言を省略しています。
省略した場合は void とみなされますが、明示的に void と記述すべきです。
171行目では ExtHedging 変数をみて条件分岐していますが、この変数は後述する OnInit関数 の方で出てくるもので、お使いの口座で同一銘柄に対し複数ポジションを持てるかどうかを定義している変数です。
日本人の使う証券会社のほとんどは ExtHedging が true になる(複数ポジションを持てる)ものと思います。
173~182行目
ExtHedging が true の場合はこの処理を行い、最後の res を return してこの関数を終了します。
PositionsTotal関数 は現在保有中のポジションの数(未決済ポジション数)を取得する関数で、それを uint型 の変数 total へ代入しています(uint型 は正の値しか取らない整数型です)。
次の for で、i を0から total 未満まで1ずつ増やしながら繰り返し処理を行っています。
PositionGetSymbol関数 は、指定したインデックス(未決済ポジションリスト内での番号、0,1,2...の様に0から順に付与)のポジションの銘柄を string で取得する関数で、PositionGetInteger(POSITION_MAGIC) は現在選択中のポジションのマジックナンバーを返す処理です。
つまり177行目の if は、選択中のポジションがEAをセットしているチャートの銘柄と一致し、マジックナンバーも一致する場合に true となり、res に true を代入して break し、 res を return してこの関数を終了しています。
つまるところ、ここでは「保有ポジション一覧からこのEAでエントリーしたポジションを探し、見つかれば true を返している」という事になります。
187~190行目
171行目の ExtHedging が false の場合にはこちらの処理を行います。
この場合、銘柄毎に1つしかポジションを保有できないため、処理も非常にシンプルです。
PositionSelect関数 は与えられた銘柄の保有ポジションを選択する関数で、成功した場合に true 、失敗した場合に false を返します。
この場合、187行目で保有ポジションがある場合は if の中は(論理否定により)false となり190行目に処理が移り、保有ポジションが無い場合は188行目の return(false) が実行されます。(逆に書いた方がわかりやすい気がします…)
190行目では保有ポジションのマジックナンバーを確認し、このEAからエントリーしたポジションであるかどうかの比較結果をそのまま return しています。(true or false)
198~214行目(OnInit 関数)
これ以降の先頭に On が付く関数は特殊な関数で、特定のイベントが発生した際に実行される、(名前だけ)予め定義されている関数になります。
これまで解説してきた関数とは違い、システム側が勝手に呼び出す関数ですので、自由に呼び出す事は推奨しません。
この OnInit関数 は、システム側で Init イベントが発生した際に実行される関数ですが、一言で言えばEAがセットされた時(初期化時)に一度だけ実行される関数です。
なので、ここにはEAの初期化処理を書いていく事になります。
201~204行目
201行目では少し前に出てきた ExtHedging変数 に値を代入しています。
AccountInfoInteger(ACCOUNT_MARGIN_MODE) は証拠金計算モードを返す処理で、銘柄毎に複数ポジションを持てるかどうかを判定した結果を返します。
ACCOUNT_MARGIN_MODE_RETAIL_HEDGING はENUM_ACCOUNT_MARGIN_MODE列挙型の定数 なので、ここでは銘柄毎に複数ポジションを持てるのであれば true 、そうでなければ false が ExtHedging に代入されます。(ちょっと複雑ですが、ここまでに説明している内容で読み解ける…はず…)
202行目では CTradeクラス の SetExpertMagicNumberメソッド(クラスに属する関数をメソッドと呼びます。が、今はわからなくてOKです)を実行し、ExtTrade に対してマジックナンバーを設定しています。
203行目では CTradeクラス の SetMarginModeメソッド を実行し、ExtTrade に対して証拠金モードを設定しています。(口座情報から勝手に情報を取得して設定しています)
204行目では Ctradeクラス の SetTypeFillingBySymbolメソッド を実行し、 ExtTrade に対して対象銘柄の充填ポリシーを設定しています。
206~211行目
ExtHandle に対してiMA関数の戻り値を代入していますが、この関数ではMA(移動平均線)のインジケータに対するハンドルを取得できます。(iMAの詳細はiMAのリンク参照のこと)
ハンドルとはそのインジケータの値を取得するための通り道だと思ってください。(ちなみにハンドルは int型 です)
ここでは、「EAがセットされているチャートの銘柄・時間足の、 MovingPeriod期間 の MovingShift期間シフトさせた SMA を扱うハンドルをExtHandleに代入」しています。
正しくハンドルを取得できていない場合、iMA は INVALID_HANDLE定数 を返しますので、その場合は INIT_FAILED定数 を return して OnInit関数 を終了しています。
OnInit関数 では、処理終了時にどのようなステータスで処理が終了したかを表す定数を return する(返す)必要があります。(厳密には2020年10月現在では返さなくても良い仕様なのですが、今後必須になる可能性もありますので明示的に返す事を推奨します)
OnInit関数 で INIT_FAILED を return した場合、初期化に失敗したとみなされ、処理が中断されますので、初期化に成功した(正常に処理を終えた)場合は必ず INIT_SUCCEEDED を return するようにします。
※実際、213行目では以下のように INIT_SUCCEEDED を return しています。
218~226行目(OnTick関数)
これも OnInit関数 と同様に、システム側で NewTick イベントが発生した際に呼ばれる関数です。
NewTick イベントは「チャートが動いたら発火するイベント」です。
そのため、1秒間に何度も呼ばれる可能性があり、ほとんどのインジケータ/EAにおいて最も触れる機会の多い関数かと思います。
このEAでは凄くシンプルに書かれており、「ポジションを持っていれば CheckForClose関数を、持っていなければCheckForOpen関数をそれぞれ実行する」という処理のみになっています。
つまり、チャートがピクッと動くたびに CheckForClose関数 か CheckForOpen関数 かのどちらかの処理を実行している、という事です。
もしここに無駄な処理を書いてしまっていたら、PCがけっこう重くなりそうな気がしませんか?
余談にも書きましたが、最近のPCの性能が十分に良くなったとしても、極力無駄なコードを避け、わかりやすく、かつ効率的なコードを書く事には大きな意義があり、そこがプログラマの腕の見せ所でもありますね。(そしてここがプログラムの面白いところでもあります)
230~232行目(OnDeinit関数)
これも同様に、システム側で Deinit イベントが発生した際に呼ばれる関数です。
いわゆる、「インジケータ/EAが終了する際に呼ばれる関数」ですね。
※時間足の切り替え等でも Deinit イベントが発生し、OnDeinit関数 が呼ばれます。
引数の reason には終了理由を表すコードが入ってきます。
※詳しくは上記の OnDeinit の公式ドキュメントを参照のこと
このEAでは何も処理を書かず、名前だけ定義している状態ですので、呼ばれても何もしません。
何もしないのであれば定義も書く必要はありませんが、テンプレート的に書くクセを付けておいた方が良いかもしれません。
まとめ
すごくざっくりとこのEAの動作を説明すると、
「MAを上(下)抜けでロング(ショート)エントリー、逆で手仕舞い」という、いわゆるゴールデンクロス、デッドクロスの手法です。
もちろん勝てません。
資金管理手法は「(パラメータによるが)連敗したらロットを減らす」というものです。
もちろん勝てません。
※特に検証してません。
いかがでしたでしょうか?
MQL5でインジケータ/EAを製作中の方の参考になれば幸いです。
雑記
想像以上の大ボリュームになってしまいました…。
おそらく、本当に初心者の方であればここまで一発で理解できる人はほぼいないと思います。
今回は実際のソースコードの解説記事でしたので、お世辞にも初心者向けの学習教材とは言えず、まして正直あまり初心者に見せるべきではないコードもありましたので、余計に混乱を招きかねない内容だったかもしれません。
それでも初心者の方でも「なんとなく」読める程度にはしたいと、できるだけ詳細に解説したり公式ドキュメントのリンクを張ってみたりと、善処したつもりです…。
ある程度経験者の方であれば、わからなかった部分のコードも「あ、そういうことか」くらいには思ってもらえる解説ができているはず…と思いたいです。
もしわからない部分があれば、Twitterの方へご連絡頂ければ出来る範囲でお答え致しますので、お気軽にご連絡ください。
また、「これ解説間違ってね?」という箇所があれば遠慮なくご指摘頂けますと幸いです。
※ソースコードを読んだだけで実際の動作確認は行っていないので、もしかしたら私が壮大に勘違いしている箇所があるかもしれません…。
この記事が皆様にとって何かの参考になれば幸いです…!
普段はこういったプログラミング関係の記事やインジケータ/EAに関する記事の執筆、自作インジケータ/EAの公開等を行っておりますので、興味をもって頂いた方は是非他の記事も下記リンクからお読み頂けますと幸いです。
また、「こういうインジケータ/EAが欲しい!」というお話も承っております。(MT4/MT5どちらでも)
NDA(秘密保持契約)や業務委託契約書等を結ばせて頂く事を前提としておりますので、手法だけ盗んだり手法を勝手に公開するような事は一切ありません。
もちろんこのインジケータに関する質問等も承っておりますので、お気軽にご連絡ください。
以上、最後までお読み頂きありがとうございました!
この記事が気に入ったらサポートをしてみませんか?