MT5自動売買ソフトの作成9 自動売買ソフトサンプルを見てみる、その2

自動売買ソフトウェアを動かす

バックテストの設定

EA(エキスパートアドバイザ=自動売買ソフトウェア)が儲けられるのか(重要!)、売買が正常にできているのか・・・など、いわゆるデバッグをしないと自分の口座で運用なんてできません。ではどうしたら?

バックテストをしましょう!

メニューの「表示」から「ストラテジーテスター」を選択します。

エキスパートには「Moving Average」を選択し、「USDJPY」で「M30」の30分足を選択します。

日付は「期間指定」で適当に「2023.01.01」~「2023.04.28」を選択してみます。長期間にするとバックテストに時間がかかります。

MT5ではフォワードテストも選択できますが、まずはバックテストだけにしましょう。

延滞(レイテンシー)は実際のping遅延を選択しておきます。

モデルは「全ティック」を選択しますが、バックテストの時間がかかりすぎるようであれば「リアルティックに基づいたすべてのティック」を選択しましょう。

入金は「100万円」レバレッジは「500倍」にします。

バックテストを行うときは、オプティマイズ(最適化)を「無効化」にします。

[スタート]ボタンを押すとバックテストを開始します。

バックテスト結果

バックテストでは指定した期間のティックデータでエキスパートアドバイザを実行します。

ティックデータは通貨ペア値が変化した時のデータですので、MT4のように1分足のデータではありません。もっと細かい値変化のデータです。

またバックテスト時に指定期間のデータがキャッシュに無い場合はダウンロードするので、MT5のヒストリー品質は(ダウンロードできない場合を除いて)100%になります。

さてサンプルEAのバックテスト結果を見てみましょう。

まず「総損益」が運用結果の損益です。なんと!!5万円のプラスになっています。

(30分足以外は全部マイナスになっていました・・・)

そして総利益・総損失を見てみると、利益分をほとんど損失していますね。プロフィットファクター(利益率)も「1.04」と微利益率になっています。

あと重要なのはドローダウン、勝率、リカバリーファクター、取引数でしょうか。

ドローダウンは残高(もしくは証拠金)が最も減った時の額、パーセンテージです。いわゆるリスクですね。

リカバリーファクターはドローダウンと総損益の比率です。リスクとリターンの比率ですね。

勝率はショート(売り)とロング(買い)で集計されています。もしショート100%、ロング50%になっていたりしたら、売りだけのEAにしてしまえば、100%勝ちのEAになります!!(そんなことは無いでしょうけどね)

取引数も重要です。取引数が少ないとバックテストとして信頼性が無くなりますし、あまり美味しくないEAになります。

売買のタイミングを確認する

結果だけだとどう動いたのかがわかりません。

プログラム通りに売買されているか確認しましょう。

赤い下矢印が「売り」、青い上矢印が「買い」で、赤い点線が始まるのがオーダー、終わるのがクローズです。

移動平均線ローソク足陰線の次のタイミングで赤矢印「売り」がオーダーされて点線が始まり、陽線で青矢印「買い」でクローズしています。

移動平均線ローソク足陰線・陽線を通った次のタイミングで矢印が出ています。EAではローソク足が確定した後にオーダー・クローズするので、手動決済(裁量)よりも一歩遅れます。

ただ売買チャートを見ると正常に売買は行われているようです。

次に残高の動きを確認します。

バックテストのグラフを見てみると、2月末まではいい感じに推移していますが、3月で一気に減っていますね。

バックテストの結果をもう一度見てみましょう。(下にスクロールしてみてください)

時間・曜日・月での集計結果が見られます。

もし金曜日だけ収益がマイナスならば、金曜日だけ売買しなければ収益が上がります。この集計を見ると、9時から15時までは売買しない方が良い感じですね。

ただし!あまり偏った売買しないフィルタを作ってしまうとカーブフィッティング(ある一定のデータに対して最適化するため、他のデータに対しては全く機能しない)になってしまいます。

売買フィルタを作ったら他の期間でバックテストをしてみましょう。違う年度や違う月、できれば3~4年くらいのバックテストをして同じ結果であれば安心です。

次回

次回は自動売買ソフトウェアを作ってみましょう。

MT5自動売買ソフトの作成8 自動売買ソフトサンプルを見てみる、その1

自動売買ソフトウェアを作る、とは?

MT5ではエキスパートアドバイザと言います。

裁量トレードをするときには、インジケータを参考にしてオーダーします。自動売買ソフトウェアでも同様でインジケータを参考にしてオーダーしますが、ソフトウェアが自動でオーダーすることだけが違います。

こう書くと簡単ですが、インジケータよりもソフトウェアはだいぶ面倒になります。

  • インジケータからのシグナルで売買もしくはクローズします。
  • オーダーがある場合は売買しません。
  • オーダー時にはロット、ストップロス、テイクプロフィットを計算もしくは指定します。

いろいろFX用語が出てきたので簡単に解説します。

用語 意味
裁量トレード 手動売買です
トレード 売り買いすることです
売買 売り/買い、buy/sell、ロング/ショートなどと言います。
クローズ オーダーを決済します。
ストップロス 損失を確定する値です。オーダー時に設定します。
テイクプロフィット 利益を確定する値です。オーダー時に設定します。
ロット 取引量をオーダー時に設定します。(いくら売買するかということです)

この中で特にロットを決めるのが面倒です。

まずは面倒なことは後回しにして固定ロットで考えてしまうのが良いと思いますが、なぜ面倒なのかは知っておきましょう。

売買は口座残高=証拠金が無いとオーダーできません。当然ですね、何を買うにしてもお金が必要です。

国内FXブローカーでは個人の場合、レバレッジが25倍上限です。またゼロカットシステムは法律で禁じられています。

海外FXブローカーではブローカーによりますが、レバレッジが500倍前後です。またゼロカットシステムが適用されるところもあります。

レバレッジとは口座残高を担保として信用取引を行う掛け率という感じでしょうか。

具体的に書いた方がわかりやすいので、例えば1万円口座にあるとします。500倍であれば、500万円の取引ができます。

ただし!500万円分のドルを買ったとします。もし0.001円でも下がったら破産します・・・マージンがありませんからね。

破産することをロスカット(ブローカーによって限界の金額が違います)と言いますが、ロスカットになっても追証(マイナス分の支払い)が無いのがゼロカットシステムです。

ロスカットにならないようにロットを決めるにはレバレッジ、口座残高、ストップロスから計算する必要があります。

複数オーダーを扱いたい場合は、扱うロット数やロットごとのオーバー値、ストップロス値を計算して、適切なロットを決めなければいけません。

面倒ですよね。

まずはロット、ストップロス、テイクプロフィットを入力パラメータにしてしまいましょう。

そうすると自動売買ソフトウェアで処理するのは、インジケータのタイミングで売買、決済をするだけになります。

MT5自動売買のサンプルコード

Experts\Examples\Moving Average.mq5を見てみましょう。長いのでロット計算関数を端折って入力にします。

#include <Trade\Trade.mqh>

input double MaximumRisk        = 0.02;    // Maximum Risk in percentage
input double DecreaseFactor     = 3;       // Descrease factor
input int    MovingPeriod       = 12;      // Moving Average period
input int    MovingShift        = 6;       // Moving Average shift

input double Rots = 1.0;        // ロット数(サンプルには無いです)
//---
int    ExtHandle=0;
bool   ExtHedging=false;
CTrade ExtTrade;

#define MA_MAGIC 1234501
//+------------------------------------------------------------------+
//| Calculate optimal lot size                                       |
//+------------------------------------------------------------------+
double TradeSizeOptimized(void)
  {
//    :
//    ロット数を計算しています。
//    :
    return Rots;
  }
//+------------------------------------------------------------------+
//| Check for open position conditions                               |
//+------------------------------------------------------------------+
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);
     }
//---
  }
//+------------------------------------------------------------------+
//| Check for close position conditions                              |
//+------------------------------------------------------------------+
void CheckForClose(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;
     }
//--- positions already selected before
   bool signal=false;
   long type=PositionGetInteger(POSITION_TYPE);

   if(type==(long)POSITION_TYPE_BUY && rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=true;
   if(type==(long)POSITION_TYPE_SELL && rt[0].open<ma[0] && rt[0].close>ma[0])
      signal=true;
//--- additional checking
   if(signal)
     {
      if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) && Bars(_Symbol,_Period)>100)
         ExtTrade.PositionClose(_Symbol,3);
     }
//---
  }
//+------------------------------------------------------------------+
//| Position select depending on netting or hedging                  |
//+------------------------------------------------------------------+
bool SelectPosition()
  {
   bool res=false;
//--- check position in Hedging mode
   if(ExtHedging)
     {
      uint total=PositionsTotal();
      for(uint i=0; i<total; i++)
        {
         string position_symbol=PositionGetSymbol(i);
         if(_Symbol==position_symbol && MA_MAGIC==PositionGetInteger(POSITION_MAGIC))
           {
            res=true;
            break;
           }
        }
     }
//--- check position in Netting mode
   else
     {
      if(!PositionSelect(_Symbol))
         return(false);
      else
         return(PositionGetInteger(POSITION_MAGIC)==MA_MAGIC); //---check Magic number
     }
//--- result for Hedging mode
   return(res);
  }
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- prepare trade class to control positions if hedging mode is active
   ExtHedging=((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
   ExtTrade.SetExpertMagicNumber(MA_MAGIC);
   ExtTrade.SetMarginMode();
   ExtTrade.SetTypeFillingBySymbol(Symbol());
//--- Moving Average indicator
   ExtHandle=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE);
   if(ExtHandle==INVALID_HANDLE)
     {
      printf("Error creating MA indicator");
      return(INIT_FAILED);
     }
//--- ok
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(void)
  {
//---
   if(SelectPosition())
      CheckForClose();
   else
      CheckForOpen();
//---
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+

さてコードを見ても全くわかりませんね。

エキスパートアドバイザで必須な関数は3つです。

int OnInit(void)
void OnTick(void)
void OnDeinit(const int reason)

OnInit関数はエキスパートアドバイザがチャートにドラッグドロップされた時に1回だけ実行される、初期化関数です。

(入力パラメータを変更されても実行されます)

OnTick関数はチャートが動いたとき=通貨ペアの値が変化したときに呼び出されます。

OnDeinit関数はエキスパートアドバイザが終了するとき=チャートから削除された時に呼び出されます。

要はOnTick関数で売買と決済を繰り返せば良いということですね。

サンプルを見ると・・・その通りです。

   if(SelectPosition())
      CheckForClose();
   else
      CheckForOpen();

CheckForOpen関数で売買を、CheckForClose関数で決済をしています。

SelectPosition関数は現在オーダーしているかをチェックしています。(オーダーするとポジションを持つことになるのでPositionという名称を使っています)

CheckForOpen関数を見てみましょう。

移動平均始値終値の間にあると売買するようです。陽線(上昇)では買い、陰線(下降)では売りをしています。

   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
     }

CheckForClose関数では決済をします。

   if(type==(long)POSITION_TYPE_BUY && rt[0].open>ma[0] && rt[0].close<ma[0])
      signal=true;
   if(type==(long)POSITION_TYPE_SELL && rt[0].open<ma[0] && rt[0].close>ma[0])
      signal=true;

買いポジションを持っているときは移動平均始値よりも小さく終値よりも大きい場合(陰線)、売りポジションを持っているときは移動平均始値よりも大きく終値よりも小さい場合(陽線)にクローズします。

買いは移動平均が陽線に突っ込んだらオーダー、陰線に突っ込んだらクローズして、売りは移動平均が陰線に突っ込んだらオーダー、陽線に突っ込んだらクローズするということでしょう。

動かして売買を確認できます。

次回

次回は自動売買ソフトウェアを動かしてみましょう。

MT5自動売買ソフトの作成7 パラメータを追加する

パラメータとは

前回までは移動平均RCIの期間は固定値でDLLに渡していました。

でも固定値だと使い勝手が悪いですよね。毎回数値を変更してビルドする必要があります。

MT5標準のインジケータでは入力パラメータで期間などが変更して表示できます。

今回は期間などをパラメータにしてみます。

dll_testインジケータをチャートにドラッグドロップした時に、「インプット」タブを選択すると期間とバーサイズが入力できるようにしましょう。

インジケータのプログラム

//+------------------------------------------------------------------+
//|                                                         rciChart.mq5 |
//|                                      Copyright 2023, MetaQuotes Ltd. |
//|                                                 https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot rciChart
#property indicator_label1  "rciChart"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRoyalBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

#property indicator_label2  "rciChart"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrOrange
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1

input int   middlePeriod = 24;      // 中期期間
input int   shortPeriod = 9;        // 短期期間

input int   barCount = 10000;       // バーサイズ

//--- indicator buffers
double  rciChartBuffer1[];
double  rciChartBuffer2[];

double  closeBuffer[];
double  middleBuffer[];
double  shortBuffer[];

#import  "D:\\mql5dll\\Dll_test\\x64\\Debug\\Dll_test.dll"
    void dllRciArray(const int size, const int period, const double& close[], double& out[]);
#import  

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
    SetIndexBuffer(0, rciChartBuffer1, INDICATOR_DATA);
    SetIndexBuffer(1, rciChartBuffer2, INDICATOR_DATA);

    ArraySetAsSeries(rciChartBuffer1, true);
    ArraySetAsSeries(rciChartBuffer2, true);

    ArrayResize(closeBuffer, barCount, 0);
    ArrayResize(middleBuffer, barCount, 0);
    ArrayResize(shortBuffer, barCount, 0);
//---
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                             |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                 const int prev_calculated,
                 const datetime &time[],
                 const double &open[],
                 const double &high[],
                 const double &low[],
                 const double &close[],
                 const long &tick_volume[],
                 const long &volume[],
                 const int &spread[])
  {
//---
    ArraySetAsSeries(close, true);

    int size = rates_total - prev_calculated;
    size = (size > barCount) ? barCount : size;
    ArrayCopy(closeBuffer, close, 0, 0, size);

    dllRciArray(size, middlePeriod, closeBuffer, middleBuffer);
    ArrayCopy(rciChartBuffer1, middleBuffer, 0, 0, size);

    dllRciArray(size, shortPeriod, closeBuffer, shortBuffer);
    ArrayCopy(rciChartBuffer2, shortBuffer, 0, 0, size);

//--- return value of prev_calculated for next call
    return(rates_total);
}

//+------------------------------------------------------------------+

まずは入力パラメータを作ります。

input int middlePeriod = 24;      // 中期期間
input int   shortPeriod = 9;        // 短期期間

input int   barCount = 10000;       // バーサイズ

inputで宣言した行の変数ではなく「//」の後ろ、コメント部分がインプットパラメータ名称になります。

MQL5が特殊な言語ということが良くわかります。まさかコメントが!?と思いますが、本当です。

double    closeBuffer[];
double  middleBuffer[];
double  shortBuffer[];

次にバッファの宣言はサイズを指定しません。

OnInit関数で下記のようにバッファのエリアを確保します。

  ArrayResize(closeBuffer, barCount, 0);
    ArrayResize(middleBuffer, barCount, 0);
    ArrayResize(shortBuffer, barCount, 0);

MQL5のArrayResize関数を利用してバッファのリサイズをします。

そしてDLLのdllRciArray関数を呼び出すときに、inputパラメータの期間を指定します。

  dllRciArray(size, middlePeriod, closeBuffer, middleBuffer);
    ArrayCopy(rciChartBuffer1, middleBuffer, 0, 0, size);

    dllRciArray(size, shortPeriod, closeBuffer, shortBuffer);
    ArrayCopy(rciChartBuffer2, shortBuffer, 0, 0, size);

インプットの期間を変えてみましょう。インジケータのグラフが変化しますね。

次回

次回は、ついに自動売買ソフトウェアです。サンプルを見てみましょう。

MT5自動売買ソフトの作成6 RCIを作る

RCIとは

RCIとは「Rank Correlation Index」(順位相関指数)の略で、計算式はこんな感じです。

 RCI=(1-(6×d)÷(nの3乗-n))×100
 d:「日付の順位」と「価格の順位」の差を2乗し、合計した数値
 n:期間
 日付の順位:当日(最新の日付)=1、として遡りながら番号付け
 価格の順位:期間中の最高値=1、として、高い順に順位付け

MACDストキャスティクスとは違ってチャートの数値を直接計算に使いません。

RCIは何故か日本のトレーダーに人気があるそうです。

将来作ろうと思っている自動売買ソフトにも使いたいと考えているインジケータの一つです。

これで何がわかるかというと、なんと「売られすぎ」や「買われすぎ」といった相場の過熱度が判断できます。

ストキャスティクスと同じです・・・まぁオシレータ系インジケータなのでそうなりますね。

ただ「番号付け」「順位付け」という部分が曲者でプログラムは難しくなります。

計算式が「「日付の順位」と「価格の順位」の差を2乗し、合計した数値」ですから順位付けしないわけにはいきません。

さてこの「順位付け」ですが、前回のストキャスティクスのプログラムを見たらピンとくると思います。

順位付け=ソートが必要、ならばmapを使ったら良いのではないかということに。

DLLのプログラム

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"

#include <memory>
#include <vector>
#include <map>

using namespace std;

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

extern "C" __declspec(dllexport) void dllRciArray(const int size, const int period, double* close, double* out)
{
    for (int i = period; i <= size; i++) {
        // RCI=(1-(6×d)÷(nの3乗-n))×100
        // d:「日付の順位」と「価格の順位」の差を2乗し、合計した数値
        // n:期間
        // 日付の順位:当日(最新の日付)=1、として遡りながら、2,3,4・・・と順位付け
        // 価格の順位:期間中の最高値=1、として、高い順に2,3,4・・・と順位付け
        //
        // ・RCIがプラスのゾーンにある時:上昇局面
        // ・RCIがマイナスのゾーンにある時:下落局面
        // 
        std::map<double, int> rciMap;
        for (int loop = 0; loop < period; loop++) {
            rciMap[close[i+loop-period+1] + static_cast<double>(loop + 1) / 10000000.0] = period - loop;
        }
        auto    n = period;
        double d = 0;
        for (auto it = rciMap.begin(); it != rciMap.end(); ++it) {
            d += (it->second - n) * (it->second - n);
            n--;
        }
        out[i] = (1 - ((6 * d) / (period * (period * period - 1)))) * 100.0;
        out[i] = (out[i] > 100.0) ? 100.0 : (out[i] < -100.0) ? -100.0 : out[i];
    }
}

このmap(std::map<double, int> rciMap)がRCIの順位付けを行います。

キーにclose値を、値に日付の順位を登録しています。

日付の順位はperiod - loopを登録しています。古い順にするために期間からループカウントを引いています。

キーはrciMap[close[i+loop-period+1] + static_cast<double>(loop + 1) / 10000000.0]です。

close[i+loop-period+1]はclose値ですが、static_cast<double>(loop + 1) / 10000000.0は何でしょう?

苦肉の策です・・・

mapは同じキー値を登録すると、キー値は2つにならないで1つのまま値が書き換えられます。

期間中のclose値が同じ場合があると・・まずいですよね。1つ減ってしまいます、日付の順位がなくなるということです。

そのため日付の番号=loop+1(逆順ですが気にしない)をソートに影響がないように、大きな値で割って小さくして足しておきます。これでキー値の重複は起きません。

ちなみにstatic_cast<double>(loop + 1)は、C言語では(double)(loop + 1)と書く、ただのキャストです。

もう一つ初見があると思います、for (auto it = rciMap.begin(); it != rciMap.end(); ++it)ですね。

このfor文ではmapの最初から最後までループしています。

auto it = rciMap.begin()で「it」に最初の項目を、it != rciMap.end()で最後(項目がなくなるまで)の項目まで回します。「it」をイテレータと言います。

イテレータの変数型は「auto」になっています。autoは左辺を右辺の式から推定される型にしますので、mapのイテレータ型が返されます、

mapのイテレータでは、first=キー、second=値がポインタでアクセスできます。

mapはソートされますので、it->second - nでは「値=日付の順位 ー キーの順:価格の(高い方からの)順位」が計算されます。

      out[i] = (1 - ((6 * d) / (period * (period * period - 1)))) * 100.0;
        out[i] = (out[i] > 100.0) ? 100.0 : (out[i] < -100.0) ? -100.0 : out[i];

あとは計算結果を出力エリアに入れるだけですね。

インジケータのプログラム

//+------------------------------------------------------------------+
//|                                                         rciChart.mq5 |
//|                                      Copyright 2023, MetaQuotes Ltd. |
//|                                                 https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot rciChart
#property indicator_label1  "rciChart"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRoyalBlue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

#property indicator_label2  "rciChart"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrOrange
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1

#define barCount    10000

//--- indicator buffers
double  rciChartBuffer1[];
double  rciChartBuffer2[];

double  closeBuffer[barCount];
double  middleBuffer[barCount];
double  shortBuffer[barCount];

#import  "D:\\Projects\\Dll_test\\x64\\Debug\\Dll_test.dll"
    void dllRciArray(const int size, const int period, const double& close[], double& out[]);
#import  

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
    SetIndexBuffer(0, rciChartBuffer1, INDICATOR_DATA);
    SetIndexBuffer(1, rciChartBuffer2, INDICATOR_DATA);

    ArraySetAsSeries(rciChartBuffer1, true);
    ArraySetAsSeries(rciChartBuffer2, true);
//---
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                             |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                 const int prev_calculated,
                 const datetime &time[],
                 const double &open[],
                 const double &high[],
                 const double &low[],
                 const double &close[],
                 const long &tick_volume[],
                 const long &volume[],
                 const int &spread[])
  {
//---
    ArraySetAsSeries(close, true);

    int size = rates_total - prev_calculated;
    size = (size > barCount) ? barCount : size;
    ArrayCopy(closeBuffer, close, 0, 0, size);

    dllRciArray(size, 24, closeBuffer, middleBuffer);
    ArrayCopy(rciChartBuffer1, middleBuffer, 0, 0, size);

    dllRciArray(size, 9, closeBuffer, shortBuffer);
    ArrayCopy(rciChartBuffer2, shortBuffer, 0, 0, size);

//--- return value of prev_calculated for next call
    return(rates_total);
}

//+------------------------------------------------------------------+

もうMQL5のプログラムの説明はいらないと思います。どれも同じ感じですからね。

ビルドして実行しましょう。

RCIはMT5に最初から入っていませんので、MQL5 communityなどで探してみてください。

比べているRCIは3本線のものですが、期間24と期間9にして一緒に表示しています。

どうでしょうか、合っていますでしょうか?

「売られすぎ」や「買われすぎ」の仲間、ストキャスティクスと比べてみましょう。

期間9(オレンジ)は似ている動きをしていますが、RCIの方が滑らかに動く感じです。

次回

次回は、MQL5中心になりますが、パラメータの追加についてです。

MT5自動売買ソフトの作成5 ストキャスティクスを作る

ストキャスティクスとは

通常のストキャスティクスは下記の線で構成されています。

%K =(当日終値-n日間の最安値)÷(n日間の最高値-n日間の最安値)×100
%D = %Kの単純移動平均
Slow%D = %Dの単純移動平均

当日の終値が過去数日間のどの位置にあるのかがわかります。

ストキャスティクスでは「売られすぎ」や「買われすぎ」といった相場の過熱度が判断できます。オシレーター系の基本ですね。

オシレーター系インジケータの特徴ですが、トレンドに入るとずっと買われすぎ、ずっと売られすぎ状態になってしまいます。なのでレンジ状態(行ったり来たりの状態)でのみ参考になります。

前回のMACDはオシレータ系に入っていますが、指数移動平均がベースなのでトレンド系に近いとインジケータです。そのため、ストキャスティクスMACDと組み合わせて使われることがあります。

今回作るストキャスティクス

%D(Slow%Kとも言います)のみを使います。ただし1本ではなく4期間の4本編みにします。

4本のストキャスティクスが纏まるところがシグナルになる感じですね。

想像つかないと思いますので画像を。

4本の各期間は、9、14、38、62にしています。(通貨ペアで有効な期間が変わると思いますので、いろいろ試してみましょう)

DLLのプログラム

普通の計算式

extern "C" __declspec(dllexport) void DllStochasArray(const int size, const int period, const int slow, const double* close, const double* lowArg, const double* highArg, double* out) {
    auto pStoch = make_unique<double[]>(size);

    for (int i = period; i <= size; i++) {
        double  high = 0.0;
        double  low = 100000.0;
        for (int n = 0; n < period; n++) {
            if (high < highArg[i - period + n]) {
                high = highArg[i - period + n];
            }
            if (low > lowArg[i - period + n]) {
                low = lowArg[i - period + n];
            }
        }
        pStoch[i - 1] = (close[i - 1] - low) * 100.0 / (high - low);
    }
    smaArray(size, slow, pStoch.get(), out);
}

period期間の高値・安値を調べて、%K(pStoch)を計算します。%Kをslow期間で単純移動平均して%Dを算出します。

このストキャスティクス関数を4本分呼び出せば良いのですが、せっかくC++でDLLを作成するのですから、今回はC++要素を入れてみましょう。

4本全部計算してみましょう。

extern "C" __declspec(dllexport) void DllStochasBraid(const int size, const int slow,
    const int period1, const int period2, const int period3, const int period4,
    const double* close, const double* lowArg, const double* highArg,
    double* out1, double* out2, double* out3, double* out4) {

    vector<int>   periods = { period1, period2, period3, period4 };
    vector<map<double, int>>    mapHighs = {
            map<double, int>(),
            map<double, int>(),
            map<double, int>(),
            map<double, int>() };
    vector<map<double, int>>    mapLows = {
            map<double, int>(),
            map<double, int>(),
            map<double, int>(),
            map<double, int>() };

    auto ptr1 = make_unique<double[]>(size);
    auto ptr2 = make_unique<double[]>(size);
    auto ptr3 = make_unique<double[]>(size);
    auto ptr4 = make_unique<double[]>(size);
    vector<unique_ptr<double[]>*>   stocks = { &ptr1, &ptr2, &ptr3, &ptr4 };

    for (int i = 0; i < size; i++) {
        for (int n = 0; n < periods.size(); n++) {
            // HIGH. LOW値をmapに入れておく
            mapHighs[n][highArg[i]] = mapLows[n][lowArg[i]] = 1;
            
            if (i >= periods[n]) {
                // 期間が過ぎたら入れ替える
                auto ith = mapHighs[n].find(highArg[i - periods[n]]);
                if (ith != mapHighs[n].end()) mapHighs[n].erase(ith);

                auto itl = mapLows[n].find(lowArg[i - periods[n]]);
                if (itl != mapLows[n].end()) mapLows[n].erase(itl);

                // 期間が来たらストキャスティクスを計算する
                (*stocks[n])[i] = (close[i] - mapLows[n].begin()->first) * 100.0
                    / (mapHighs[n].rbegin()->first - mapLows[n].begin()->first);
                (*stocks[n])[i] = ((*stocks[n])[i] > 100.0) ? 100.0 : ((*stocks[n])[i] < 0.0) ? 0.0 : (*stocks[n])[i];
            }
        }
    }
    smaArray(size, slow, stocks[0]->get(), out1);
    smaArray(size, slow, stocks[1]->get(), out2);
    smaArray(size, slow, stocks[2]->get(), out3);
    smaArray(size, slow, stocks[3]->get(), out4);
}

初見のキーワードがあると思います。(初見でない人は飛ばしてください)

  • vector

    動的な配列と考えてください。[数値]で各要素にアクセスできます。

  • map

    連想配列と考えてください。[文字列・数値]で各要素にアクセスできます。

    例えばmap<string, string> aであれば、a["abc"] = "def";のように"abc"の要素に"def"を登録できます。

("abc"をキー、"def"を値と言います)

また map は登録されたら自動的にキーでソートします。

今回はこのmapはキーでソートするという機能を使って高値の最大値、安値の最小値を取り出します。 (キーをソートするので、値には適当な"1"を固定で入れています・・・あまり良いプログラムとは言えませんので参考程度で)

(*stocks[n])[i] = (close[i] - mapLows[n].begin()->first) * 100.0
                    / (mapHighs[n].rbegin()->first - mapLows[n].begin()->first);

.begin()->firstがmapの最初の要素、.rbegin()->firstがmapの最後の要素です。

mapHighs、mapLowsは、各期間分の高値・安値を登録しています。

periods期間が過ぎたら.erase(ith)で要素から削除します。

stocksにはperiods期間の%Kが計算されます。

最後に smaArray でslow期間で単純移動平均を行いout1~4に出力します。

全体のコード

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"

#include <memory>
#include <vector>
#include <map>

using namespace std;

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

static void smaArray(const int size, const int period, const double* close, double* out) {
    for (int i = period; i <= size; i++) {
        double  total = 0.0;
        for (int n = 0; n < period; n++) {
            total += close[i - period + n];
        }
        out[i - 1] = total / period;
    }
}

extern "C" __declspec(dllexport) void DllStochasBraid(const int size, const int slow,
    const int period1, const int period2, const int period3, const int period4,
    const double* close, const double* lowArg, const double* highArg,
    double* out1, double* out2, double* out3, double* out4) {

    vector<int>   periods = { period1, period2, period3, period4 };
    vector<map<double, int>>    mapHighs = {
            map<double, int>(),
            map<double, int>(),
            map<double, int>(),
            map<double, int>() };
    vector<map<double, int>>    mapLows = {
            map<double, int>(),
            map<double, int>(),
            map<double, int>(),
            map<double, int>() };

    auto ptr1 = make_unique<double[]>(size);
    auto ptr2 = make_unique<double[]>(size);
    auto ptr3 = make_unique<double[]>(size);
    auto ptr4 = make_unique<double[]>(size);
    vector<unique_ptr<double[]>*>   stocks = { &ptr1, &ptr2, &ptr3, &ptr4 };

    for (int i = 0; i < size; i++) {
        for (int n = 0; n < periods.size(); n++) {
            // HIGH. LOW値をmapに入れておく
            mapHighs[n][highArg[i]] = mapLows[n][lowArg[i]] = 1;
            
            if (i >= periods[n]) {
                // 期間が過ぎたら入れ替える
                auto ith = mapHighs[n].find(highArg[i - periods[n]]);
                if (ith != mapHighs[n].end()) mapHighs[n].erase(ith);

                auto itl = mapLows[n].find(lowArg[i - periods[n]]);
                if (itl != mapLows[n].end()) mapLows[n].erase(itl);

                // 期間が来たらストキャスティクスを計算する
                (*stocks[n])[i] = (close[i] - mapLows[n].begin()->first) * 100.0
                    / (mapHighs[n].rbegin()->first - mapLows[n].begin()->first);
                (*stocks[n])[i] = ((*stocks[n])[i] > 100.0) ? 100.0 : ((*stocks[n])[i] < 0.0) ? 0.0 : (*stocks[n])[i];
            }
        }
    }
    smaArray(size, slow, stocks[0]->get(), out1);
    smaArray(size, slow, stocks[1]->get(), out2);
    smaArray(size, slow, stocks[2]->get(), out3);
    smaArray(size, slow, stocks[3]->get(), out4);
}

インジケータのプログラム

//+------------------------------------------------------------------+
//|                                                         dll_test.mq5 |
//|                                      Copyright 2023, MetaQuotes Ltd. |
//|                                                 https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_minimum 0
#property indicator_maximum 100
#property indicator_buffers 4
#property indicator_plots   4
//--- plot memConf
#property indicator_label1  "stoch1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Orange
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

#property indicator_label2  "stoch2"
#property indicator_type2   DRAW_LINE
#property indicator_color2  Chocolate
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1

#property indicator_label3  "stoch3"
#property indicator_type3   DRAW_LINE
#property indicator_color3  Turquoise
#property indicator_style3  STYLE_SOLID
#property indicator_width3  1

#property indicator_label4  "stoch4"
#property indicator_type4   DRAW_LINE
#property indicator_color4  LimeGreen
#property indicator_style4  STYLE_SOLID
#property indicator_width4  1

#define barCount    20000

//--- indicator buffers
double  stochasChart1[];
double  stochasChart2[];
double  stochasChart3[];
double  stochasChart4[];

double  closeBuffer[barCount];
double  lowBuffer[barCount];
double  highBuffer[barCount];
double  out1Buffer[barCount];
double  out2Buffer[barCount];
double  out3Buffer[barCount];
double  out4Buffer[barCount];


#import  "D:\\Projects\\Dll_test\\x64\\Debug\\Dll_test.dll"
    void DllStochasBraid(const int size, const int slow, 
                const int period1, const int period2, const int period3, const int period4, 
                const double& close[], const double& low[], double& high[], 
                double& out1[], double& out2[], double& out3[], double& out4[]);
#import  

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
    SetIndexBuffer(0, stochasChart1, INDICATOR_DATA);
    SetIndexBuffer(1, stochasChart2, INDICATOR_DATA);
    SetIndexBuffer(2, stochasChart3, INDICATOR_DATA);
    SetIndexBuffer(3, stochasChart4, INDICATOR_DATA);

    ArraySetAsSeries(stochasChart1, true);
    ArraySetAsSeries(stochasChart2, true);
    ArraySetAsSeries(stochasChart3, true);
    ArraySetAsSeries(stochasChart4, true);
//---
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                             |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                 const int prev_calculated,
                 const datetime &time[],
                 const double &open[],
                 const double &high[],
                 const double &low[],
                 const double &close[],
                 const long &tick_volume[],
                 const long &volume[],
                 const int &spread[])
  {
//---
    ArraySetAsSeries(close, true);
    ArraySetAsSeries(high, true);
    ArraySetAsSeries(low, true);

    int size = rates_total - prev_calculated;
    size = (size > barCount) ? barCount : size;
    ArrayCopy(closeBuffer, close, 0, 0, size);
    ArrayCopy(highBuffer, high, 0, 0, size);
    ArrayCopy(lowBuffer, low, 0, 0, size);

    DllStochasBraid(size, 3, 9, 14, 38, 62, 
                    closeBuffer, lowBuffer, highBuffer, 
                    out1Buffer, out2Buffer, out3Buffer, out4Buffer);
    ArrayCopy(stochasChart1, out1Buffer, 0, 0, size);
    ArrayCopy(stochasChart2, out2Buffer, 0, 0, size);
    ArrayCopy(stochasChart3, out3Buffer, 0, 0, size);
    ArrayCopy(stochasChart4, out4Buffer, 0, 0, size);

//--- return value of prev_calculated for next call
    return(rates_total);
}

//+------------------------------------------------------------------+

今回はストキャスティクス4本を表示します。

#property indicator_minimum 0はチャート表示の最小値、#property indicator_maximum 100は最大値を指定します。

また、インジケータバッファ、プロット数を4にして、各インジケータの色や種類を指定しています。

#property indicator_separate_window
#property indicator_minimum 0
#property indicator_maximum 100
#property indicator_buffers 4
#property indicator_plots   4
//--- plot memConf
#property indicator_label1  "stoch1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Orange
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

そしてDLLとの受け渡しバッファはClose、High、Lowと出力用合わせて7つ用意します。

double    closeBuffer[barCount];
double  lowBuffer[barCount];
double  highBuffer[barCount];
double  out1Buffer[barCount];
double  out2Buffer[barCount];
double  out3Buffer[barCount];
double  out4Buffer[barCount];

DLLの呼び出しは、4本分のストキャスティクス用パラメータを指定します。

  void DllStochasBraid(const int size, const int slow, 
                const int period1, const int period2, const int period3, const int period4, 
                const double& close[], const double& low[], double& high[], 
                double& out1[], double& out2[], double& out3[], double& out4[]);

MSCodeにコピーして、ビルドをします。

ストキャスティクス4本表示して比べてみましょう。

次回

次回は、またオシレータ系インジケータのRCIを作ってみましょう。

MT5自動売買ソフトの作成4 オシレータ系インジケータ

まずポインタとは

SMA(単純移動平均)を前々回に作ってみました。

また前回の「データを見る」でもDLL側でポインタを使っています。

C++なのでポインタを普通に書いていましたが、もしかしたらポインタでC、C++をあきらめた人もいるかもしれませんので、少し解説します。

・・・ポインタは何故か嫌われ者で悲しいです。

SMAの回のMQL5側のインポート文では「double& close[]」で呼び出します。

#import  "D:\\Projects\\Dll_test\\x64\\Debug\\Dll_test.dll"
      void dllSmaArray(const int size, const int period, const double& close[], double& out[]);
#import 

DLL側では「double* close」で受けています。

extern "C" __declspec(dllexport) void dllSmaArray(const int size, const int period, double* close, double* out)
{
    for (int i=0; i <= size - period; i++) {
        double  total = 0.0;
        for (int n = 0; n < period; n++) {
            total += close[i+n];
        }
        out[i+period-1] = total / period;
    }
}

DLL受け渡しバッファは下図でしたね。DLL受け渡しバッファは「double& 変数」→「double* 変数」となっていることになります。

「double& 変数」=double変数配列は、「double*」=double変数配列のポインタということになりますね。

ポインタはメモリの位置を指す変数ですが、ここでは簡単にバッファの先頭(位置)を指す変数と考えてください。配列変数と同様の扱いができます。

オシレータ系インジケータ

今回はMACDを作成します。

MACDとは「Moving Average Convergence Divergence」の略ですが、「マックディー」としか呼ばれませんし、正式名称を知っている人はほとんどいないと思います。

MACDは短期(9か12期間)と長期(26期間)の指数移動平均(EMA)の差分をヒストグラム表示して、その差分をSMA(9期間)したシグナルを破線で表示します。(上図下側)

見るべきポイントはゴールデンクロスデッドクロスヒストグラム表示とシグナルの交差です。

ゴールデンクロスデッドクロスでは判断が遅くなることが多いので、ヒストグラム表示とシグナルの交差を見るようです。(ピンクの矢印ですが、騙しが多い気もしますね)

上図を見てもシグナル交差のほうが判断が早いです。

DLLのプログラム

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"

#include <memory>

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

// EMA= 前日EMA+平滑化定数α×(当日終値-前日EMA)  平滑化定数α=2÷(n+1)
static void emaArray(const int size, const int period, const double* close, std::unique_ptr<double[]>& out) {
    double  alpha = 2.0 / ((double)period + 1.0);
    double  total = 0.0;
    for (int n = 0; n < period; n++) {
        total += close[n];
    }
    out[period-1] = total / period;

    for (int i = period; i < size; i++) {
        out[i] = out[i - 1] + (close[i] - out[i - 1]) * alpha;
    }
}

// SMA
static void smaArray(const int size, const int period, const double* close, double* out) {
    for (int i = period; i <= size; i++) {
        double  total = 0.0;
        for (int n = 0; n < period; n++) {
            total += close[i - period + n];
        }
        out[i - 1] = total / period;
    }
}

// MACD
extern "C" __declspec(dllexport) void dllMacdArray(const int size, const int periodS, const int periodL, const int periodG, double* close, double* outM, double* outS)
{
    std::unique_ptr<double[]>   maS(new double[size + 1]);
    std::unique_ptr<double[]>   maL(new double[size + 1]);
    // MACDの計算
    emaArray(size, periodS, close, maS);
    emaArray(size, periodL, close, maL);
    for (int i = periodL; i < size; i++) {
        outM[i] = maS[i] - maL[i];
    }
    // シグナルの計算
    smaArray(size, periodG, outM, outS);
}

EMAの計算、SMAの計算、MACDの計算の関数を作成します。

新しいのはEMA、MACDです。EMAはSMAと同じ移動平均の「指数移動平均」です。

SMAは二重ループで計算していますが、EMAはループが一重のため計算が早いのが特徴で、最新終値を2回使うので最新値を重視する移動平均です。

EMAの計算式は、

EMA = 前日EMA+平滑化定数α×(当日終値-前日EMA)  ※ただし、平滑化定数α=2÷(n+1)

MACDは2期間EMAの差分をヒストグラム表示して、その差分のSMAをシグナルとして破線表示します。

上記、dllMacdArray関数は見れば一目瞭然ですね。

2期間のEMA計算をするため、2期間分のバッファが必要になります。

C++では、newで領域確保して deleteで領域開放します。ですが、領域開放って面倒ですよね。他の言語のようにガベージコレクションに任せてしまいましょう。

C++11から実装されている「unique_ptr」で領域確保すると、なんと開放しなくてもスコープから抜けると解放されます。

楽ですね。

    std::unique_ptr<double[]>   maS(new double[size + 1]);
    std::unique_ptr<double[]>   maL(new double[size + 1]);

インジケータのプログラム

//+------------------------------------------------------------------+
//|                                                         dll_test.mq5 |
//|                                      Copyright 2023, MetaQuotes Ltd. |
//|                                                 https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot dll_test
#property indicator_label1  "MACD"
#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  Silver
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

#property indicator_label2  "signal"
#property indicator_type2   DRAW_LINE
#property indicator_color2  Red
#property indicator_style2  STYLE_DOT
#property indicator_width2  1

#define barCount    20000

//--- indicator buffers
double  shortChart[];
double  longChart[];

double  closeBuffer[barCount];
double  longBuffer[barCount];
double  shortBuffer[barCount];

#import  "D:\\Projects\\Dll_test\\x64\\Debug\\Dll_test.dll"
    void dllMacdArray(const int size, const int periodS, const int periodL, const int periodG, const double& close[], double& outM[], double& outS[]);
#import  

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
{
//--- indicator buffers mapping
    SetIndexBuffer(0, shortChart, INDICATOR_DATA);
    SetIndexBuffer(1, longChart, INDICATOR_DATA);

    ArraySetAsSeries(shortChart, true);
    ArraySetAsSeries(longChart, true);
//---
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                             |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                 const int prev_calculated,
                 const datetime &time[],
                 const double &open[],
                 const double &high[],
                 const double &low[],
                 const double &close[],
                 const long &tick_volume[],
                 const long &volume[],
                 const int &spread[])
  {
//---
    ArraySetAsSeries(close, true);

    int size = rates_total - prev_calculated;
    size = (size > barCount) ? barCount : size;
    ArrayCopy(closeBuffer, close, 0, 0, size);

    dllMacdArray(size, 9, 26, 9, closeBuffer, shortBuffer, longBuffer);
    ArrayCopy(shortChart, shortBuffer, 0, 0, size);
    ArrayCopy(longChart, longBuffer, 0, 0, size);
//--- return value of prev_calculated for next call
    return(rates_total);
}

//+------------------------------------------------------------------+

今回はMACDとシグナル、2つのグラフを表示します。

#property indicator_separate_windowはチャート上に表示しないで、Window分割したグラフに表示する指定です。

また、インジケータバッファ、プロット数を2にして、各インジケータの色や種類を指定しています。

#property indicator_separate_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot dll_test
#property indicator_label1  "MACD"
#property indicator_type1   DRAW_HISTOGRAM
#property indicator_color1  Silver
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

#property indicator_label2  "signal"
#property indicator_type2   DRAW_LINE
#property indicator_color2  Red
#property indicator_style2  STYLE_DOT
#property indicator_width2  1

そしてDLLとの受け渡しバッファはCloseと合わせて3つ用意します。

double    closeBuffer[barCount];
double  longBuffer[barCount];
double  shortBuffer[barCount];

DLLの呼び出しは、短い期間と長い期間、シグナルの期間を指定して、バッファを受け渡します。

dllMacdArray(size, 9, 26, 9, closeBuffer, shortBuffer, longBuffer);

MSCodeにコピーして、ビルドをします。

MT5に最初からインストールされているMACDと表示を比べてみましょう。

どうでしょうか?合ってますね?

次回

オシレータといえば「MACD」と「ストキャスティクス」です。

次回は、ちょっと変わったストキャスティクスを作ってみましょう。

MT5自動売買ソフトの作成3 データを見る

今回はMQL5とDLL(C++)間でのデータ受け渡しがどうなっているのか見ていきます。

受け渡しのデータ内容がわからないとプログラムが作れませんので、調べておくのが基本です。

DLLのプログラム

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"

#include <fstream>

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

const char* LOG_FILE = "D:\\dll_test.log";

static void write(int i, double value) {
    std::ofstream oss;
    oss.open(LOG_FILE, std::ios::app);
    oss << " i= " << i << ", value= " << value << std::endl;
    oss.close();
}

static void writeStr(const char* str) {
    std::ofstream oss;
    oss.open(LOG_FILE, std::ios::app);
    oss << str << std::endl;
    oss.close();
}

extern "C" __declspec(dllexport) void dllDumpTest(const int size, double* value)
{
    writeStr("dllDumpTest ------------------------------------");
    int len = (size < 10) ? size : 10;

    for (int i = 0; i < size; i++) {
        if (i < len) {
            write(i, value[i]);
        }
        if (i > size - len) {
            write(i, value[i]);
        }
    }
}

C++というより、ほとんどC言語ですが、少し解説します。

#include <fstream>

ofstreamでファイルを書き込むため、includeしておきます。

const char* LOG_FILE = "D:\\dll_test.log";

static void write(int i, double value) {
    std::ofstream oss;
    oss.open(LOG_FILE, std::ios::app);
    oss << " i= " << i << ", value= " << value << std::endl;
    oss.close();
}

static void writeStr(const char* str) {
    std::ofstream oss;
    oss.open(LOG_FILE, std::ios::app);
    oss << str << std::endl;
    oss.close();
}

ファイルにデータを書き込みます。今回は受け渡されたデータと、文字列を書き込むプログラムです。

extern "C" __declspec(dllexport) void dllDumpTest(const int size, double* value)
{
    writeStr("dllDumpTest ------------------------------------");
    int len = (size < 10) ? size : 10;

    for (int i = 0; i < size; i++) {
        if (i < len) {
            write(i, value[i]);
        }
        if (i > size - len) {
            write(i, value[i]);
        }
    }
}

いよいよメインのMT5との受け渡し関数です。

渡されたデータの前後10データをファイルに書き込みます。

MetaEditorでインジケータ作成

VSCodeでインジケータの編集とMT5で実行

VSCodeエクスプローラーで「Dll_test.mq5」を開き下記のように変更します。

(同じ名称で説明していますが、前のプログラムを残しておきたい場合は、Gitで履歴管理するか、別の名称にコピーして取っておいてください)

//+------------------------------------------------------------------+
//|                                                     Dll_test.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots    1

#property indicator_label1  "dll_test"
#property indicator_type1    DRAW_LINE
#property indicator_color1  Orange
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

#import  "D:\\Projects\\Dll_test\\x64\\Debug\\Dll_test.dll"
    void dllDumpTest(const int size, const double& value[]);
#import  

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping

//---
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
    ArraySetAsSeries(close, true);

    int    size = rates_total - prev_calculated;
    dllDumpTest(size, close);

//--- return value of prev_calculated for next call
    return(rates_total);
  }
//+------------------------------------------------------------------+

OnCalculateイベントは価格が変動するごとに呼び出されますので、平日はガンガン呼び出されます。開発するのは休日のほうが良いかもしれません。

一度動けばいいので、すぐに右クリックしてインディケータリストを開きます。

「Dll_test」を選択して「削除」し、閉じます。

ファイルの内容

まずはCloseデータとArraySetAsSeriesを確認します。

ArraySetAsSeriesはMQL5のライブラリ関数で、trueにするとインデックス0が最新データで、falseにすると最大インデックスが最新データになるという機能です。

dllDumpTest ------------------------------------
 i= 0, value= 97.327
 i= 1, value= 97.385
 i= 2, value= 97.686
 i= 3, value= 98.162
 i= 4, value= 98.514
 i= 5, value= 98.352
 i= 6, value= 98.719
 i= 7, value= 98.655
 i= 8, value= 98.537
 i= 9, value= 98.645
 i= 40391, value= 131.497
 i= 40392, value= 131.46
 i= 40393, value= 131.551
 i= 40394, value= 131.482
 i= 40395, value= 131.51
 i= 40396, value= 131.43
 i= 40397, value= 131.412
 i= 40398, value= 131.425
 i= 40399, value= 131.411

「ArraySetAsSeries」をコメントにします。

//ArraySetAsSeries(close, true);
dllDumpTest ------------------------------------
 i= 0, value= 97.327
 i= 1, value= 97.385
 i= 2, value= 97.686
 i= 3, value= 98.162
 i= 4, value= 98.514
 i= 5, value= 98.352
 i= 6, value= 98.719
 i= 7, value= 98.655
 i= 8, value= 98.537
 i= 9, value= 98.645
 i= 40391, value= 131.497
 i= 40392, value= 131.46
 i= 40393, value= 131.551
 i= 40394, value= 131.482
 i= 40395, value= 131.51
 i= 40396, value= 131.43
 i= 40397, value= 131.412
 i= 40398, value= 131.425
 i= 40399, value= 131.411

変わりませんね。

DLLに渡される時にはその機能は有効にならないことがわかりました。

//+------------------------------------------------------------------+
//|                                                     Dll_test.mq5 |
//|                                  Copyright 2023, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots    1

#property indicator_label1  "dll_test"
#property indicator_type1    DRAW_LINE
#property indicator_color1  Orange
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

#import  "D:\\Projects\\Dll_test\\x64\\Debug\\Dll_test.dll"
    void dllDumpTest(const int size, const double& value[]);
#import  

#define    barCount    10000

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping

//---
   return(INIT_SUCCEEDED);
  }

double    closeBuffer[barCount+1];

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
    ArraySetAsSeries(close, true);
    ArrayCopy(closeBuffer, close, 0, 0, barCount);

    dllDumpTest(barCount, closeBuffer);

//--- return value of prev_calculated for next call
    return(rates_total);
  }
//+------------------------------------------------------------------+

次はバッファにコピー(ArrayCopy)してからDLLにデータを渡します。

チャートのデータが全部渡されてしまうと処理時間がかかるのでレスポンスが遅れます。自動売買はできるだけ早く判断したほうが良いので100程度のデータ渡しで済ませたいです。CloseをそのままDLLに渡すと全データを受け渡さないと最新データが受け取れませんね。

ただ100程度では、インジケータの描画が直近のチャートにしか反映しなくなるので、数千から1万弱くらいが良いと思います。

今回はcloseデータを1万渡すことにします。

 i= 0, value= 127.659
 i= 1, value= 127.912
 i= 2, value= 127.86
 i= 3, value= 127.669
 i= 4, value= 127.672
 i= 5, value= 127.736
 i= 6, value= 127.694
 i= 7, value= 127.85
 i= 8, value= 127.851
 i= 9, value= 127.947
 i= 9991, value= 131.497
 i= 9992, value= 131.46
 i= 9993, value= 131.551
 i= 9994, value= 131.482
 i= 9995, value= 131.51
 i= 9996, value= 131.43
 i= 9997, value= 131.412
 i= 9998, value= 131.425
 i= 9999, value= 131.411

指定したバッファのインデックス最大が最新データになっています。

ArraySetAsSeriesをfalseにしてみた場合はやってみてください。

インジケータ作成の方針としては、MQL5からArraySetAsSeriesはTrueでバッファにコピーして、DLLにデータを渡す形にしましょう。

前回のSMAのプログラミングコードと下の図を参考にしてください。

次回

次回はオシレータ系のインジケータを作ります。