FXオートトレードプログラムを開発する会(4)

前回のリンク
次回のリンク

こんにちは,Oです.第4回です.
最近,この活動が一番楽しいです.

前回に引き続き,MQL5のサンプルプログラムの解読を進めていきます.
今回は,ポジションを立てるための関数であるCheckForOpen関数を見ていきましょう.

CheckForOpen関数

void CheckForOpen(void)
  {
   MqlRates rt[2];
//--- go trading only for first ticks of new bar
   if(CopyRates(_Symbol,_Period,0,2,rt)!=2)
     {
      Print("CopyRates of ",_Symbol," failed, no history");
      return;
     }
   if(rt[1].tick_volume>1)
      return;
//--- get current Moving Average 
   double   ma[1];
   if(CopyBuffer(ExtHandle,0,0,1,ma)!=1)
     {
      Print("CopyBuffer from iMA failed, no data");
      return;
     }
//--- check signals
   ENUM_ORDER_TYPE signal=WRONG_VALUE;

   if(rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=ORDER_TYPE_SELL;    // sell conditions
   else
     {
      if(rt[0].open<ma[0] && rt[0].close>ma[0])
         signal=ORDER_TYPE_BUY;  // buy conditions
     }
//--- additional checking
   if(signal!=WRONG_VALUE)
     {
      if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>100)
         ExtTrade.PositionOpen(_Symbol,signal,TradeSizeOptimized(),
                               SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK),
                               0,0);
     }
//---
  }

まず,内容を理解する上で必要になる用語やデータ構造,関数について解説し,その後にコードの詳細な説明を行います.(解説しきれなかったものは,コードの詳細な説明と共に行っています.文章構成がきっちりしてなくてすみません…)

スポンサーリンク

【用語やデータ構造,関数の説明】

・MqlRates構造体

MqlRatesはプライス,ボリューム,スプレッドに関する情報を保存する構造体です.

struct MqlRates
  {
   datetime time;         // Period start time
   double   open;         // Open price
   double   high;         // The highest price of the period
   double   low;          // The lowest price of the period
   double   close;        // Close price
   long     tick_volume;  // Tick volume
   int      spread;       // Spread
   long     real_volume;  // Trade volume
  };

MqlRates構造体を定義した後は,CopyRates関数によってヒストリーデータを取得しています.

・CopyRate関数

ここから,CopyRates関数の説明に多少紙面を割きます.為替レートのヒストリーデータを読み込むための大切な関数だからです.

CopyRates関数の呼び方には3種類あり,第1, 2, 5引数は共通で,第3, 4引数の型によってヒストリーデータの取り方(データ区間を指定する方法)に違いが出ます.このデータ区間指定に関する3つの様式は他の関数(例えば,CopyBuffer関数)においても共通しているので,MQL独特な感じですが,知っておくと実装可能なプログラムの幅が広がります.以下に3つの様式を列挙します.

#1:開始点の番号と必要データ数による指定

int  CopyRates(
   string           symbol_name,       // symbol name
   ENUM_TIMEFRAMES  timeframe,         // period
   int              start_pos,         // start position
   int              count,             // data count to copy
   MqlRates         rates_array[]      // target array to copy
   );

#2:開始点の日時と必要データ数による指定

int  CopyRates(
   string           symbol_name,       // symbol name
   ENUM_TIMEFRAMES  timeframe,         // period
   datetime         start_time,        // start date and time
   int              count,             // data count to copy
   MqlRates         rates_array[]      // target array to copy
   );

#3:開始点の日時と終了点の日時による指定

int  CopyRates(
   string           symbol_name,       // symbol name
   ENUM_TIMEFRAMES  timeframe,         // period
   datetime         start_time,        // start date and time
   datetime         stop_time,         // end date and time
   MqlRates         rates_array[]      // target array to copy
   );

CopyRates関数の引数

まず,共通する第1, 2, 5引数の説明を行います.

第1引数はstring型変数で対象としたい取引通貨ペアの銘柄名(シンボル名)を入れます.例えば,ドル円では”USDJPY”ですし,ユーロドルでは”EURUSD”ですね.サンプルプログラム上では_Symbolが使われています._SymbolはMQL5において事前に定義された変数の一つであり,現在選択中のチャートの通貨ペアの銘柄名を表します.”USDJPY”チャート上でこのEA (Expert Advisor) を実行すれば,_Symbolには”USDJPY”が入っているわけです.

第2引数はENUM_TIMEFRAMESという列挙型変数で,その中から対象とするチャートの時間フレームを選択します.例えば,5分足ならPERIOD_M1,1時間足ならPERIOD_H1になります.サンプルプログラム上では_Periodが使われており,_Symbolと同様に,これは現在選択中のチャートの時間フレームを意味します.

第5引数はヒストリーデータを入れたいMqlRates構造体のターゲット配列です.サンプルプログラム上では,関数冒頭で定義したMqlRates構造体のrtを指定しています.

次に,3つの様式で異なる第3, 4引数の説明を行います.

第1様式(#1)では第3引数に開始点の番号,第4引数に必要なデータ数をそれぞれint型で指定します.「ココからn個のデータをください」という感じです.ここで注意しなくてはいけないのが,MQL5において最新データから過去に向かって番号が振られていることです.つまり0番が最新のデータ,1番が最新から1つ前のデータ,2番が最新から2つ前のデータ…(以下同じ),となっています.最新から10個データが欲しければ,CopyRate(_Symbol, _Period, 0, 10, rt)のようにすればよいわけです.

int  CopyRates(
   string           symbol_name,       // symbol name
   ENUM_TIMEFRAMES  timeframe,         // period
   int              start_pos,         // start position
   int              count,             // data count to copy
   MqlRates         rates_array[]      // target array to copy
   );

第2様式(#2)では,第3引数にdatetime型変数という日時と時間を入れる変数を与えます.datetime型変数には様々な書き方があるようですが,D’2020.09.13 11:54:30’と書くと2020年9月13日11時54分30秒という意味になります.つまり開始時刻を日時で指定します.第4引数には必要なデータ数をint型で与えるということで,これは第1様式と同じです.「いついつからn個のデータをください」という感じです.

int  CopyRates(
   string           symbol_name,       // symbol name
   ENUM_TIMEFRAMES  timeframe,         // period
   datetime         start_time,        // start date and time
   int              count,             // data count to copy
   MqlRates         rates_array[]      // target array to copy
   );

第3様式(#3)では,第3引数と第4引数をdatetime型変数で与えています.つまり,開始日時と終了日時を与えて,その間にあるデータをコピーします.「いついつからいついつまでのデータをください」という感じですね.

int  CopyRates(
   string           symbol_name,       // symbol name
   ENUM_TIMEFRAMES  timeframe,         // period
   datetime         start_time,        // start date and time
   datetime         stop_time,         // end date and time
   MqlRates         rates_array[]      // target array to copy
   );

サンプルプログラム上では,

CopyRates(_Symbol,_Period,0,2,rt)

となっており,第3, 4引数が共にint型であることからCopyRatesの第1様式が用いられていることがわかります.開始点が0で個数が2ですね.MQL5においてヒストリーデータの最新が0番に割り振られているので,ここでは最新のデータと最新から1つ前のデータを計2つ要求しています.

CopyRates関数の返り値

CopyRates関数はコピーしたデータの要素数を返します.第1, 2様式では第4引数で必要なデータ数を指定するので,正常に動いたのであればそこで指定した値が返ってくるわけです.エラーが生じた場合は-1を返すようになっています.

CopyBuffer関数

CopyBuffer関数は特定の指標のバッファデータを取得する関数です.”Buffer(バッファ)”とは一般に緩衝材の意味ですが,情報科学においてはデータ通信のために設ける記憶領域を指します.本サンプルプログラム内では,公式に提供されている移動平均線に関するインディケータ(指標関数)の指標値(これがバッファデータ)をローカル変数にコピーするために使われています.CopyBuffer関数もデータ取得の様式が3つありますが,CopyRates関数と同じなので説明は省略します.今回は開始点と必要な要素数による形式(第1様式)を使います.

int  CopyBuffer(
   int       indicator_handle,     // indicator handle
   int       buffer_num,           // indicator buffer number
   int       start_pos,            // start position
   int       count,                // amount to copy
   double    buffer[]              // target array to copy
   );

CopyBuffer関数の引数

第1, 2引数は最も大切で,それぞれ特定のインディケータのハンドルとバッファー番号を表します.インディケータのハンドルはインディケータ関数(例えば,iMA関数)の返り値として得られます.バッファー番号は通常0でよいですが,複数のバッファーを持つインディケータの場合,これを指定することで単一のインディケータ関数から複数種類のバッファーデータを得ることができると予想されます(確認はしてません.間違っていたらごめんなさい).第3, 4引数はデータ区間を指定するもので,それぞれ開始点とコピーしたい要素数を指します.ちなみに,開始点は0を現在として1,2,3…と値が増えるほど過去に戻ります.第5引数にはバッファーデータをコピーしたい配列を入れます.

CopyBuffer関数の返り値

CopyBuffer関数の戻り値は結果的にコピーされたデータ数で,エラーの場合は-1です.(これもCopyRates関数と同じ)

スポンサーリンク

【コードの内容の解説】

以下,各セクション毎にコードの内容を解説していきます.

コード解説(1, 2つ目のif文: “go trading only for first ticks of new bar”)

2つ目のif文までの内容を見ていきます.

//--- go trading only for first ticks of new bar
if(CopyRates(_Symbol,_Period,0,2,rt)!=2)
     {
      Print("CopyRates of ",_Symbol," failed, no history");
      return;
     }
if(rt[1].tick_volume>1)
      return;

サンプルプログラムの最初のif文では,

if(CopyRates(_Symbol,_Period,0,2,rt)!=2)
      {
       Print("CopyRates of ",_Symbol," failed, no history");
       return;
      }

とありますが,ここではCopyRates(_Symbol,_Period,0,2,rt)の返り値が2でなければ,”CopyRates of “,_Symbol,” failed, no history”といってエラー終了しています.前述の通り,CopyRates関数は第4引数でコピーしたいデータ数を指定し,結果的にコピーできた要素数を返り値として出力します.ここでは2つのデータを要求して,結果的に2つのデータをコピーできたならOKとして続行し,それ以外ならNGとしてエラー終了するようになっています.

2つ目のif文では,

if(rt[1].tick_volume>1)
      return;

とあり,ここでrt[1].tick_volumeは最新のデータのtick数を意味していますが,これが1より大きい場合は無言で終了しています.tick数とは着目している時間フレーム,例えば5分足であれば,その5分の間に何回tickが変動したかという量です.これが2以上はダメというのは,つまり時間フレームの切替わった直後にトレードの成立条件を判定している(“go trading only for first ticks of new bar”)ということです.

コード解説(3つ目のif文: “get current Moving Average )

続いて,3つ目のif文の中身を見ていきましょう.

//--- get current Moving Average 
double   ma[1];
if(CopyBuffer(ExtHandle,0,0,1,ma)!=1)
      {
       Print("CopyBuffer from iMA failed, no data");
       return;
      }

まず,double型の長さ1の配列maを定義し,これにバッファデータをコピーすることを目指します.

変数ExtHandleはOninit関数内で次のように定義されています.

ExtHandle=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE);

iMAは移動平均線に関するインディケータのハンドルを出力するインディケータ関数であり,ここで得られたハンドルを使って移動平均線の指標値に関するバッファデータにアクセスします.

第3, 4引数はそれぞれ0, 1なので,最新のデータを1つコピーしてくることを要求しています.

そして,if文によってこの要求が正常に処理されたかどうかをチェックしています.すなわち,結果的にコピーされたバッファデータの個数が1であれば問題ないとしてプログラムを続行し,それ以外であれば””CopyBuffer from iMA failed, no data”と表示してエラー終了しています.

コード解説(4つ目のif文: “check signals”)

4つ目のif文です.

//--- check signals
ENUM_ORDER_TYPE signal=WRONG_VALUE;

if(rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=ORDER_TYPE_SELL;    // sell conditions
else
  {
   if(rt[0].open<ma[0] && rt[0].close>ma[0])
      signal=ORDER_TYPE_BUY;  // buy conditions
  }」

まず,ENUM_ORDER_TYPEの変数signalにWRONG_VALUEを代入しています.

ENUM_ORDER_TYPEとは注文プロパティに関する列挙型変数です.例えば,成行買い注文ならORDER_TYPE_BUY,買い指値注文ならORDER_TYPE_BUY_LIMITのように指定できます.WRONG_VALUEは「その他の定数」として事前に定義された予約語であり,-1の値を持ちます.ただし,任意の列挙型変数に暗黙的にキャスト(対応する列挙型変数の型に変換すること)されるため,ENUM_ORDER_TYPEといった列挙型変数を使う場合のエラー状態として幅広く用いることができます.

次に,if文の中身の売買条件です.現在のオープン価格が1つ前の移動平均値より大きくて,かつ現在のクローズ価格が1つ前の移動平均値より小さい場合,売るようになっています.対して,現在のオープン価格が1つ前の移動平均値より小さくて,かつ現在のクローズ価格が1つ前の移動平均値より大きい場合,買うようになっています.すなわち,ある時間フレーム内において,現在の為替レートが移動平均線を上から下にクロスするときに売り,下から上にクロスするときに買うようになっているわけです.エラー処理や注文処理の丁寧な作りに対して,肝心のトレードアルゴリズムはとても簡素です.「ぜひ自分たちでよりよいトレードアルゴリズムを考えてみてください」という意図が伝わってくるようです.

コード解説(5つ目のif文: “additional checking”)

最後に,5つ目のif文の中身を見ていきましょう.

//--- additional checking
if(signal!=WRONG_VALUE)
   {
   if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>100)
      ExtTrade.PositionOpen(_Symbol,signal,TradeSizeOptimized(),
                            SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK),
                            0,0);
   }

外側のif文の中でENUM_ORDER_TYPE変数signalの状態が初期値のWRONG_VALUEと異なるかどうかを確認しています.すなわち,売買条件のいずれかが達成された場合,追加の確認を行うようになっています.

TerminalInfoInteger関数(関数名末尾の”Integer”は返り値がint型の意)は次のようにint型あるいはbool型の識別子を引数に取り,対応するプロパティの値をint型で返す関数です.

int  TerminalInfoInteger(
  int  property_id      // プロパティ識別子
  );

ここではTERMINAL_TRADE_ALLOWEDという自動売買システムの約定許可に関するプロパティを指定しています.任意のEAは実行に際して自動売買の可否を問われるのですが,そもそもこれが許可されているかどうかを確認しています.

さらにAND条件として,Bars関数を用いて指定した銘柄と時間区間におけるバーの数の最低数が100を下回らないようにしています.バー(データ)の数が少なすぎると,それによって計算される移動平均線の意味がよくわからなくなりますから,このような条件を付けているのでしょう.

int  Bars(
   string           symbol_name,     // symbol name
   ENUM_TIMEFRAMES  timeframe        // period
   );

最後に,注文処理が次のようになっています.

ExtTrade.PositionOpen(_Symbol,signal,TradeSizeOptimized(),
                            SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK),
                            0,0);

CTradeクラスのExtTradeのメソッドであるPositionOpenは,

bool  PositionOpen(
   const string     symbol,         // symbol
   ENUM_ORDER_TYPE  order_type,     // order type to open position
   double           volume,         // position volume
   double           price,          // execution price
   double           sl,             // Stop Loss price
   double           tp,             // Take Profit price
   const string     comment=""      // comment
   )

となっています.第1, 2引数はそれぞれ銘柄名と注文種類になっています.第3引数は注文するポジションの量でTradeSizeOptimized関数によって決まります.第4引数は取引価格でSymbolInfoDouble関数(関数名末尾の”Double”は返り値がdouble型の意)

double  SymbolInfoDouble(
   string                   name,       // symbol
   ENUM_SYMBOL_INFO_DOUBLE  prop_id     // identifier of the property
   );

で決まります.第1引数は銘柄名ですね.注目すべきは第2引数で,列挙型変数のENUM_SYMBOL_INFO_DOUBLEを指定する必要がありますが,このときに次の通り3項演算子”?:”が使われています.

signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK

これは次のifを用いたコードと等価です.

if signal==ORDER_TYPE_SELL
	{
	 SYMBOL_BID
	}
else
	{
	 SYMBOL_ASK
	}

すなわち,売り条件が達成されて変数signalがORDER_TYPE_SELLになっている場合はSYMBOL_BIDを入力することで最適なBid値(売値)を返すように指定し,それ以外の場合,つまり買い条件が達成されている場合はSYMBOL_ASKを入力することで最適なAsk値(買値)を返すように指定しています.3項演算子を用いていることで若干トリッキーな感じのコードになっていますが,条件分岐をワンライナーで書くことに成功しています.

第5, 6引数にはそれぞれ損失額と利益額をコントロールするためのストップロス値とテイクプロフィット値を入れることができますが,今回のサンプルプログラムでは0が入れられており設定されていません.

また,第7引数にコメントを入れることができますが,本サンプルプログラムでは省略しています.入力を省略するとデフォルト値である””が入力されます(つまり,何も入力されない).

以上になります.
だんだん理解が深まってきました.
このサンプルプログラムを改造していくことを考えると,インディケータ関数を適宜差し替えて(デフォルトで提供されていないものは自作する必要がありますが…),トレード条件のif文(上では”4つ目のif文”)を変更することが必要になります.また,アルゴリズムによってはポジション管理の方法(TradeSizeOptimized関数の部分)を変更する必要もあるでしょう.
早く自分の考えるトレードアルゴリズムを実装してみたいですね.
あと,コードが見にくくなってしまっているのが申し訳なかったです.謝罪.
次回も引き続き,サンプルプログラムの残りを片付けていきます.

次回のリンクはこちら

コメント

  1. kota より:

    ”Moving Average.mq5″を参考にして、自作の初EA作成しているところでサイト見つけました。
    MQL5の解説は、公式リファレンス以外になかなかないのでめっちゃ参考になりました!

    • O より:

      kotaさん,コメントいただきありがとうございます.
      お役に立ったようで何よりです!

  2. Takuma より:

    とても丁寧な解説ありがとうございます.

    CoppyRatesに関して質問なのですが,このMAのモデルだと
    rtにコピーされたrateの情報は,「rt[1]が最新rateで,rt[0]がひとつ前のrate」となり,
    rateをコピーする際は,最も新しいrateは0で過去にさかのぼるほど+1される.
    という認識であってますでしょうか.

    • O より:

      Takumaさん,コメントいただきありがとうございます.
      返信が遅くなり申し訳ありません.
      そのような認識で合っています.
      CopyRates関数の公式リファレンス(https://www.mql5.com/ja/docs/series/copyrates)のはじめにある図がわかりやすいです.
      コピー元となる配列”rate”(リファレンス先の”History data array”)においては,現在から過去に向かって0, 1, 2, …と番号が振られています.
      対して,コピー宛先となる配列”rt”(リファレンス先の”Array rates_array”)においては,過去から現在に向かって0, 1, 2, …と番号が振られます.

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