時系列マイニングのためのデータラベル(第1回):EA操作チャートでトレンドマーカー付きデータセットを作成する

Yuqiang Pan | 8 1月, 2024

まとめ

人工知能モデルを設計する際には、まずデータを準備する必要があることがよくあります。データの質が高ければ、モデルの訓練と検証に半分の労力をかけ、2倍の結果を得ることができます。しかし、私たちの外国為替や株式のデータは、複雑な市場情報と時間情報を含む特殊なものであり、データのラベル付けは困難ですが、チャート上の過去のデータの傾向を簡単に分析することができます。

 

ここでは、EA操作チャートによるトレンドマーク付きデータセットの作成方法を紹介します。直感的に自分のアイデアでデータを操作することができます。もちろん、同じ方法で自分のデータセットを拡張カスタマイズすることもできます。

目次

  1. ラベルデータ形式の定義
  2. チャートとファイルの初期化
  3. 操作ロジックの設計とマーク
  4. データを整理し、ファイルに書き込む
  5. 添付ファイル:完全なEAコード例


ラベルデータ形式の定義

外国為替や株式のデータをクライアントから取得する場合(この記事では、ファイルから読み込んだり、他のWebサイトからダウンロードした外部データについては触れません)、一般的にはこのような状況になります。

時間 始値 高値 安値 終値 ティックボリューム
2021-12-10 01:15:00
1775.94
1775.96
1775.58
1775.58
173
2021-12-10 01:30:00
1775.58
1776.11
1775.48
1775.88
210
2021-12-10 01:45:00
1775.88
1776.22
1775.68
1776.22
212
2021-12-10 02:00:00
1776.22
1777.57
1775.98
1777.02
392
2021-12-10 02:15:00
1776.99
1777.72
1776.89
1777.72
264

上の画像は5つの時系列データです。終値と始値が最初から最後までつながっていて、一貫性が非常に強くなっています。仮に、最初の2つが上昇トレンドで、他は下降トレンドだと考えたとしましょう(上記5つのデータを例にとっている)。一般的なラベル付け法では、データを2つの部分に分けます。

時間

始値 高値   安値   終値 ティックボリューム
2021-12-10 01:15:00
1775.94
1775.96
1775.58
1775.58
173
2021-12-10 01:30:00
1775.58
1776.11
1775.48
1775.88
210

時間 始値 高値   安値 終値 ティックボリューム
2021-12-10 01:45:00
1775.88
1776.22
1775.68
1776.22
212
2021-12-10 02:00:00
1776.22
1777.57
1775.98
1777.02
392
2021-12-10 02:15:00
1776.99
1777.72
1776.89
1777.72
264

これで、どの部分が上昇トレンドで、どの部分が下降トレンドなのかをモデルに伝えます。しかし、それでは全体的な属性を無視することになり、データの整合性が崩れてしまいます。では、どうやってこの問題を解決するのでしょうか。

実現可能な方法は、以下のように時系列にトレンドのグループ化を追加することです(上記の5つのデータを例とするか、上記の仮定に従う)。

時間 始値 高値 安値 終値 ティックボリューム トレンドグループ
2021-12-10 01:15:00
1775.94
1775.96
1775.58
1775.58
173 0
2021-12-10 01:30:00
1775.58
1776.11
1775.48
1775.88
210 0
2021-12-10 01:45:00
1775.88
1776.22
1775.68
1776.22
212 1
2021-12-10 02:00:00
1776.22
1777.57
1775.98
1777.02
392 1
2021-12-10 02:15:00
1776.99
1777.72
1776.89
1777.72
264 1

しかし、現在のトレンドがどの程度発展しているかなどのトレンド発展分析をモデルに実装したい場合は、(たとえば、波動理論では、一般的なトレンドには一般にトレンド ステージと調整ステージが含まれており、トレンド ステージには 5 つの波動ステージがあり、調整ステージには 3 つの波動調整があるなど)、次の10個のデータのうち、最初の2個が上昇トレンド、最後の5個が上昇トレンドで、真ん中の残りは下降トレンドであると仮定します)、データにさらにラベルを付ける必要があります。これは、データの傾向の推移を表す別のインデックス列を追加することで実現できます(次の10個のデータのうち最初の2個が上昇傾向で、最後の5個が上昇傾向であると仮定すると、 中間の残りは下降傾向です)、次のようになります。

時間 始値 高値 安値 終値 ティックボリューム トレンドグループ トレンドインデックス
2021-12-10 03:15:00 1776.38 1777.94 1775.47 1777.71 565 0 0
2021-12-10 03:30:00 1777.75 1778.93 1777.68 1778.61 406 0 1
2021-12-10 03:45:00 1778.58 1778.78 1777.65 1778.16 388 1 0
2021-12-10 04:00:00 1778.14 1779.42 1778.06 1779.14 393 1 1
2021-12-10 04:15:00 1779.16 1779.49 1778.42 1779.31 451 1 2
2021-12-10 04:30:00 1779.22 1779.42 1778.36 1778.37 306 0 0
2021-12-10 04:45:00 1778.42 1778.51 1777.60 1777.78 411 0 1
2021-12-10 05:00:00 1777.81 1778.68 1777.61 1778.57 372 0 2
2021-12-10 05:15:00 1778.54 1779.29 1778.42 1779.02 413 0 3
2021-12-10 05:30:00 1778.97 1779.49 1778.48 1778.50 278 0 4

注:

1.上昇トレンドを定義するTrend_groupは0

2.下降トレンドを定義するTrend_groupは1

 次に、クライアントサイドでチャートを操作し、希望するパターンに従ってデータにラベルを付けます。


チャートとファイルの初期化

チャートの初期化

    データをマークするためにチャートを見る必要があるため、チャートは気軽にスクロールさせることはできず、手動操作に従ってスクロールさせる必要があります。よって、CHART_AUTOSCROLLとCHART_SHIFTを無効にする必要があります。

     ChartSetInteger (0, CHART_AUTOSCROLL, false);
    
      ChartSetInteger (0, CHART_SHIFT, true);
    
      ChartSetInteger (0, CHART_MOUSE_SCROLL ,1);
    注:コードの緑色の部分は、マウスホイールでチャートをコントロールできるようにするためのものです。 ファイルの初期化

      ファイルの初期化では、まず既存のラベルファイルがあるかどうかを確認し、履歴ファイルがあれば、そのファイル名を変数reNameに保存します。

       do
           {
             //---Find if there are files that match the chart
             if (StringFind(name, Symbol())!=-1 && StringFind(name,".csv")!=-1)
               reName=name;
           }
      
         while (FileFindNext(hd,name));
      注:ここで注意しなければならないのは、「do - while」ループを使っていることです。これは「while」ループとは異なり、最初に演算子を実行してから式を評価します。 しかし、名前の初期化が問題なので、これを行うことができます。
      int hd= FileFindFirst("*",name,0);


      オリジナルのマークされたファイルがあれば、そのファイルを開き、read_csv()関数で最後にマークされた時刻を取得する:

      read_csv(file_handle,a);
      次に、最後にマークした時間までチャートをスクロールします。
      shift = - iBarShift(Symbol(),PERIOD_CURRENT,(datetime)a[i-8]);
      ChartNavigate(0, CHART_END ,shift);


      履歴ファイルがない場合はファイルを作成します。

      file_handle = FileOpen(StringFormat("%s%d-%d.csv",Symbol(),Period(),start_t), FILE_WRITE | FILE_CSV | FILE_READ);
      次に、グローバル変数start_tで指定された位置までチャートをスクロールします。
       shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)start_t);
        ChartNavigate(0,CHART_END,shift);
      開始列を示す縦の赤線を加えます。
       ObjectCreate (0,"Start",OBJ_VLINE,0,(datetime)start_t,0)
      この部分のロジックは次のように構成されています。
       if (FileIsExist(reName))
           {
            file_handle = FileOpen(reName, FILE_WRITE | FILE_CSV | FILE_READ );
             string a[];
             int i= 0 ;
            read_csv(file_handle,a);
            i = ArraySize (a);
            shift = -iBarShift(Symbol(), PERIOD_CURRENT,(datetime)a[i-8]);
             ChartNavigate(0,CHART_END,shift);
           }
         else
           {
            file_handle = FileOpen (StringFormat ("%s%d-%d.csv", Symbol(), Period(),start_t), FILE_WRITE | FILE_CSV | FILE_READ );
             Print ("There is no history file,create file:" , StringFormat ( "%s%d-%d",Symbol(), Period(),start_t));
             shift = - iBarShift (Symbol(), PERIOD_CURRENT ,(datetime)start_t);
             ChartNavigate (0, CHART_END ,shift);
             ObjectCreate (0,"Start", OBJ_VLINE,0,(datetime)start_t,0);
           }
      ご注目ください。チャートを左に移動させたいので、iBarShift()関数の前に「-」を追加しなければなりません。
      shift = -iBarShift(Symbol(), PERIOD_CURRENT ,(datetime)start_t);
      もちろん、次のようなChartNavigate()関数で実装することもできます。
      ChartNavigate(0,CHART_END,-shift);
      この記事のコードは、まだ最初の方法に従って実装されています。
      これらの初期化行動は、OnInit()の中で実装され、必要な変数の定義も含まれます。最も重要なことは、チャートのどこにシフトさせたいかを明確にし、ラベル付けを開始することです。これは主に変数shiftとstart_tによって制御されます。最終的なコードに反映されます。
      int OnInit()
        {
      //---initial
         string name;
         string reName="1";
         int hd=FileFindFirst("*",name,0);
         int shift;
      
         ChartSetInteger(0,CHART_AUTOSCROLL,false);
         ChartSetInteger(0,CHART_SHIFT,false);
         ChartSetInteger(0,CHART_MOUSE_SCROLL,1);
      
      
         do
           {
            //---check File
            if(StringFind(name,Symbol())!=-1 && StringFind(name,".csv")!=-1)
               reName=name;
           }
         while(FileFindNext(hd,name));
      
         if(FileIsExist(reName))
           {
            file_handle = FileOpen(reName,FILE_WRITE|FILE_CSV|FILE_READ);
            string a[];
            int i=0;
            read_csv(file_handle,a);
            i = ArraySize(a);
            shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)a[i-8]);
            ChartNavigate(0,CHART_END,shift);
           }
         else
           {
            file_handle = FileOpen(StringFormat("%s%d-%d.csv",Symbol(),Period(),start_t),FILE_WRITE|FILE_CSV|FILE_READ);
            Print(FileTell(file_handle));
            Print("No history file,create file:",StringFormat("%s%d-%d",Symbol(),Period(),start_t));
            shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)start_t);
            ChartNavigate(0,CHART_END,shift);
            ObjectCreate(0,"Start",OBJ_VLINE,0,(datetime)start_t,0);
           }
         return(INIT_SUCCEEDED);
        }

      注:

      1. start_t変数:開始する時間帯を指定する

      2.shift変数:シフトされる列数を指定し、コード例では、指定された時間を変換してシフトされる列数を示す

      3. read_csv()関数は後で定義します。

      read_csv() 関数の定義:
       void read_csv(int hd,
                     string &arry[])
        {
         int i= 0;
         while(!FileIsEnding(hd))
           {
            ArrayResize(arry,i+1);
            arry[i]= FileReadString(hd);
            i++;
           }
        }

      注:whileループを使って、過去のアノテーションファイルの最終行を見つけ、ファイル内のデータの最終行を取得し、最後のアノテーションの終了時刻を見つけます。このアノテーションは、チャートをこの列列ラフまでスクロールさせるので、ここからアノテーションを続けることができます。


      操作ロジックの設計とマーク

      グラフを操作する:このセクションは、クライアントのヘルプトピックから簡単に照会できます。
      • Home:チャートの最後のバーに移動する
      • End:チャートの最初のバーに移動する
      • Page Up:チャートをウィンドウ1つ分後方に移動する
      • Page Down:チャートをウィンドウ1つ分進める
      • Ctrl+I:指標のリストを表示するウィンドウを開く
      • Ctrl+B:オブジェクトのリストを含むウィンドウを開く
      • Alt+1:チャートは一連のバーとして表示される
      • Alt+2:チャートは日本のローソク足の並びで表示される
      • Alt+3:チャートは終値を結んだ線で表示される
      • Ctrl+G:チャートウィンドウのグリッドの表示/非表示を切り替える
      • +:チャートを拡大する
      • -:チャートをズームアウトする
      • F12:チャートをステップごと(バーごと)にスクロールする
      • F8:プロパティウィンドウを開く
      • Backspace:最後に追加したオブジェクトをチャートから削除する
      • Delete:選択されたオブジェクトをすべて削除する
      • Ctrl+Z:最後のオブジェクトを削除する
      制御ロジック 1. キーを押して、次にマークされるデータがどのようなトレンドになるかをEAに知らせます。 bキーとsキーを定義します。仮想キーコードで定義:
       #define KEY_B     66
       #define KEY_S     83
      bを押してからsを押すと上昇トレンドになり、sを押してからbを押すと下降トレンドになります。 1)この時点でbを押し、上昇トレンドを表します。変数typを0に、変数tpをstartに、矢印の色を clrBlueにそれぞれ設定し、ラベル数Numを1ずつ増やすように設定します。注意すべき点は、データセグメントの先頭の変数をインクリメントするだけで、もう一度ボタンを押すと、マークされたデータセグメントの「終わり」の部分が最初に反転して実行されるように指定することです。
      b_press
      2) sを押して上昇トレンドの終了を示します。変数typは0のままです。変数tpはendに設定されます。矢印の色はclrBlueのままで、ラベルカウントNumは変更されません。注意すべき点は、データセグメントの先頭の変数をインクリメントするだけでよいということです。また、firstの反転は、もう一度ボタンを押すと、マークされたデータセグメントの「開始」部分が実行されることを指定するために使われます。 s_press
      3) switch文を実行した後、ChartRedraw()関数を呼び出してチャートを再描画します。
      if(id==CHARTEVENT_KEYDOWN)
           {
            switch(lparam)
              {
               case KEY_B:
                  if(first)
                    {
                     col=clrBlue ;
                     typ =0;
                     Num+=1;
                     tp = "start";
                    }
                  else
                    {
                     col=clrRed ;
                     typ = 1;
                     tp = "end";
                    }
                  ob =OBJ_ARROW_BUY;
                  first = !first;
                  Name = StringFormat("%d-%d-%s",typ,Num,tp);
                  break;
               case KEY_S:
                  if(first)
                    {
                     col=clrRed ;
                     typ =1;
                     Num+=1;
                     tp = "start";
                    }
                  else
                    {
                     col=clrBlue ;
                     typ = 0;
                     tp = "end";
                    }
                  ob =OBJ_ARROW_SELL;
                  first = !first;
                  Name = StringFormat("%d-%d-%s",typ,Num,tp);
                  break;
      
               default:
                  Print("You pressed:"+lparam+" key, do nothing!");
              }
            ChartRedraw(0);
           }

      注:

      1. typ変数:0は上昇トレンド、1は下降トレンドです

      2. Num変数:マーク数がチャート上に直感的に表示されます

      3. first変数:ラベルが常にペアになるようにコントロールし、各グループが混乱することなくbとs、またはsとbになるようにします

      4. tp変数:データセグメントの開始または終了を決定するために使用されます

      2.チャート上でマウスの左ボタンをクリックし、マークの位置を決定します。

      if(id==CHARTEVENT_CLICK)
           {
            //--- definition
            int x=(int)lparam;
            int y=(int)dparam;
            datetime dt    =0;
            double   price =0;
            int      window=0;
            if(ChartXYToTimePrice(0,x,y,window,dt,price))
              {
               ObjectCreate(0,Name,ob,window,dt,price);
               ObjectSetInteger(0,Name,OBJPROP_COLOR,col);
               //Print("time:",dt,"shift:",iBarShift(Symbol(),PERIOD_CURRENT,dt));
               if(tp=="start")
                  Start=dt;
               else
                 {
                  if(file_handle)
                     file_write(Start,dt);
                 }
               ChartRedraw(0);
              }
            else
               Print("ChartXYToTimePrice return error code: ",GetLastError());
           }
      //--- object delete
         if(id==CHARTEVENT_OBJECT_DELETE)
           {
            Print("The object with name ",sparam," has been deleted");
           }
      //--- object create
         if(id==CHARTEVENT_OBJECT_CREATE)
           {
            Print("The object with name ",sparam," has been created!");
           }

      注:

      1.ChartXYToTimePrice()関数は、主にマウスクリック位置のカラムチャートプロパティ(現在時刻と価格を含む)を取得するために使用されます。グローバル変数dtを使って現在時刻を受け取ります。

      2.マウスをクリックするとき、現在の行動がデータセグメントの始まりなのか終わりなのかを判断する必要もあります。グローバル変数tpを使って判断します。

      3.具体的な作業工程


      上昇トレンドをマークしたい場合は、まずbキーを押し、チャート上でマークを付け始める列でマウスの左ボタンをクリックし、次にsキーを押し、アイコンの列の端でマウスの左ボタンをクリックしてラベル付けを完了します。下の画像のように、チャート上に青い矢印のペアが表示されます。

      上へ


      下降トレンドをマークしたい場合は、まずsキーを押し、チャート上のマークを付け始める列でマウスの左ボタンをクリックし、次にbキーを押し、チャート上の列の端でマウスの左ボタンをクリックします。マーキングが完了すると、下の画像のように赤い矢印のペアが表示されます。

      下へ


      ラベル付け出力欄には、いつでもラベル付け動作が表示されます。図に示すように、ラベル付けプロセスをモニターするのは非常に直感的です。

      アウト注:この部分は本当はもっと最適化できるはずです。例えば、最後の操作を取り消す機能を追加すれば、いつでもマークの位置を調整できるし、間違った操作を避けることもできるのですが、私は怠け者なので... (^o^)


      データを整理し、ファイルに書き込む

      トレンドの開始時間とデータ系列を保存するために、変数Startと「MqlRates rates[]」を定義します。
      datetime Start;
      MqlRates rates[];
      ArraySetAsSeries(rates, false);
      注:1. ここでは、終了時刻を定義する必要はありません。なぜなら、チャートから得られる最後の時刻が終了時刻だからです。 2. ArraySetAsSeries(rates,false)」関数のフラグにfalseを指定することで、時間帯が順次接続されるようにします。 tp = "end "のとき、データセグメントをファイルに書き込みます(コードの緑の部分)。
         if(id==CHARTEVENT_CLICK)
           {
            //--- definition
            int x=(int)lparam;
            int y=(int)dparam;
            datetime dt    =0;
            double   price =0;
            int      window=0;
            if(ChartXYToTimePrice(0,x,y,window,dt,price))
              {
               ObjectCreate(0,Name,ob,window,dt,price);
               ObjectSetInteger(0,Name,OBJPROP_COLOR,col);
               //Print("time:",dt,"shift:",iBarShift(Symbol(),PERIOD_CURRENT,dt));
               if(tp=="start")
                  Start=dt;
               else
                 {
                  if(file_handle)
                     file_write(Start,dt);
                 }
               ChartRedraw(0);
              }
            else
               Print("ChartXYToTimePrice return error code: ",GetLastError());
           }

      CopyRates()関数でセグメントデータを取得し、rates[]に含まれる各データを走査して、trend_grou 列とtrend_index列を追加するには、file_write()関数にこれらの関数を実装する必要があります。
      void file_write(datetime start,
                      datetime end)
        {
         MqlRates rates[];
         ArraySetAsSeries(rates,false);
         int n_cp=CopyRates(Symbol(),PERIOD_CURRENT,start,end,rates);
         if(n_cp>0)
           {
            if(FileTell(file_handle)==2)
              {
               FileWrite(file_handle,"time","open","high","low","close","tick_volume","trend_group","trend_index");
               for(int i=0; i<n_cp; i++)
                 {
                  FileWrite(file_handle,
                            rates[i].time,
                            rates[i].open,
                            rates[i].high,
                            rates[i].low,
                            rates[i].close,
                            rates[i].tick_volume,
                            typ,
                            i);
                 }
              }
            else
              {
               for(int i=0; i<n_cp; i++)
                 {
                  FileWrite(file_handle,
                            rates[i].time,
                            rates[i].open,
                            rates[i].high,
                            rates[i].low,
                            rates[i].close,
                            rates[i].tick_volume,
                            typ,
                            i);
                 }
              }
           }
         else
            Print("No data copied!");
         FileFlush(file_handle);
         typ=3;
        }



      注:

      1.初めてファイルを書き込むときに、インデックスヘッダを書く必要があります。

      2.Trend_groupは実際にはグローバル変数typです。

      3.この関数ではFileClose()関数を呼び出していません。ラベル付けが完了していないためです。OnDeinit()関数の中でこの関数を呼び出し、最終結果をファイルに書き込みます。

      4.コードの黄色い部分に特別な注意を払う必要があります。

      if(FileTell(file_handle)==2)
      ファイルにデータがあるかどうかを判断するには(もちろん、初期化時に変数を追加して値を代入するなど、他の方法を使うこともできます)、ファイルにデータがない場合に次のようなヘッダーを追加する必要があります。
      FileWrite(file_handle,"time","open","high","low","close","tick_volume","trend_group","trend_index");
      ファイル内にデータがあれば、ヘッダーを付ける必要はありません。ヘッダーを付けると、データが切断されます。これは非常に重要です! 書き込まれたファイルの例: data_0


      異なるデータセグメント間の一貫性を確認し、データが完璧であることを確認しましょう。

      データ_1


      添付ファイル:完全なEAコード例

      1. グローバル変数と定数の定義。  パラメータstart_tは、1970年01月01日からの1秒あたりのデータで定義できます。もちろん、標準のdatetimeで定義することもできますし、入力変数「input int start_t=1403037112;」で定義することもできます。
      #define KEY_B     66
      #define KEY_S     83
      
      
      int Num= 0;
      int typ= 3;
      string Name;
      string tp;
      color col;
      bool first= true;
      ENUM_OBJECT ob;
      int file_handle=0;
      int start_t=1403037112;
      datetime Start;

      注:もちろん、個人的な好みに応じて、ボタンを入力変数として定義することもできます。

      input int KEY_B=66;
      input int KEY_S=83;

      これの利点は、ボタンが使いにくいと感じたら、EAを実行するたびに納得がいくまでボタンを自由に変更でき、私たちのコードが一時的に変更されることはないことです。


      2. OnInit()関数で準備を初期化します。

      int OnInit()
        {
      //---initial
         string name;
         string reName="1";
         int hd=FileFindFirst("*",name,0);
         int shift;
      
         ChartSetInteger(0,CHART_AUTOSCROLL,false);
         ChartSetInteger(0,CHART_SHIFT,false);
         ChartSetInteger(0,CHART_MOUSE_SCROLL,1);
      
      
         do
           {
            //---check File
            if(StringFind(name,Symbol())!=-1 && StringFind(name,".csv")!=-1)
               reName=name;
           }
         while(FileFindNext(hd,name));
      
         if(FileIsExist(reName))
           {
            file_handle = FileOpen(reName,FILE_WRITE|FILE_CSV|FILE_READ);
            string a[];
            int i=0;
            read_csv(file_handle,a);
            i = ArraySize(a);
            shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)a[i-8]);
            ChartNavigate(0,CHART_END,shift);
           }
         else
           {
            file_handle = FileOpen(StringFormat("%s%d-%d.csv",Symbol(),Period(),start_t),FILE_WRITE|FILE_CSV|FILE_READ);
            Print(FileTell(file_handle));
            Print("No history file,create file:",StringFormat("%s%d-%d",Symbol(),Period(),start_t));
            shift = -iBarShift(Symbol(),PERIOD_CURRENT,(datetime)start_t);
            ChartNavigate(0,CHART_END,shift);
            ObjectCreate(0,"Start",OBJ_VLINE,0,(datetime)start_t,0);
           }
      //---
         Print("EA:",MQL5InfoString(MQL5_PROGRAM_NAME),"Working!");
      //---
         ChartSetInteger(ChartID(),CHART_EVENT_OBJECT_CREATE,true);
      //---
         ChartSetInteger(ChartID(),CHART_EVENT_OBJECT_DELETE,true);
      //---
         ChartRedraw(0);
      //---
         return(INIT_SUCCEEDED);
        }


      3. キーボードとマウスの操作はすべてチャート上で終了しているので、主要なロジック関数をOnChartEvent()関数に入れて実装します。

      void OnChartEvent(const int id,
                        const long &lparam,
                        const double &dparam,
                        const string &sparam)
        {
      //Comment(__FUNCTION__,": id=",id," lparam=",lparam," dparam=",dparam," sparam=",sparam);
         if(id==CHARTEVENT_KEYDOWN)
           {
            switch(lparam)
              {
               case KEY_B:
                  if(first)
                    {
                     col=clrBlue ;
                     typ =0;
                     Num+=1;
                     tp = "start";
                    }
                  else
                    {
                     col=clrRed ;
                     typ = 1;
                     tp = "end";
                    }
                  ob =OBJ_ARROW_BUY;
                  first = !first;
                  Name = StringFormat("%d-%d-%s",typ,Num,tp);
                  break;
               case KEY_S:
                  if(first)
                    {
                     col=clrRed ;
                     typ =1;
                     Num+=1;
                     tp = "start";
                    }
                  else
                    {
                     col=clrBlue ;
                     typ = 0;
                     tp = "end";
                    }
                  ob =OBJ_ARROW_SELL;
                  first = !first;
                  Name = StringFormat("%d-%d-%s",typ,Num,tp);
                  break;
      
               default:
                  Print("You pressed:"+lparam+" key, do nothing!");
              }
            ChartRedraw(0);
           }
      //---
         if(id==CHARTEVENT_CLICK&&(typ!=3))
           {
            //--- definition
            int x=(int)lparam;
            int y=(int)dparam;
            datetime dt    =0;
            double   price =0;
            int      window=0;
            if(ChartXYToTimePrice(0,x,y,window,dt,price))
              {
               ObjectCreate(0,Name,ob,window,dt,price);
               ObjectSetInteger(0,Name,OBJPROP_COLOR,col);
               //Print("time:",dt,"shift:",iBarShift(Symbol(),PERIOD_CURRENT,dt));
               if(tp=="start")
                  Start=dt;
               else
                 {
                  if(file_handle)
                     file_write(Start,dt);
                 }
               ChartRedraw(0);
              }
            else
               Print("ChartXYToTimePrice return error code: ",GetLastError());
           }
      //--- object delete
         if(id==CHARTEVENT_OBJECT_DELETE)
           {
            Print("The object with name ",sparam," has been deleted");
           }
      //--- object create
         if(id==CHARTEVENT_OBJECT_CREATE)
           {
            Print("The object with name ",sparam," has been created!");
           }
      
        }

      注:この関数の実装では、上記のコードを変更しました。

       if (id==CHARTEVENT_CLICK&&(typ!=3))

      偶発的なマウスクリックによる誤操作を避け、typ変数を使ってマウス操作が有効かどうかをコントロールするためです。トレンドをマークしたら、file_write()関数を実行します。この関数の最後に次の行を追加します。

      typ=3;

      そして、適切なポジションを見つけ、次のトレンドのラベルを貼る準備ができるまで、何もせずに次の段落の印を始める前に、マウスを使ってチャート上で何気なく操作することができます。


      4. データ書き込み関数file_write()を実装します。

      void file_write(datetime start,
                      datetime end)
        {
         MqlRates rates[];
         ArraySetAsSeries(rates,false);
         int n_cp=CopyRates(Symbol(),PERIOD_CURRENT,start,end,rates);
         if(n_cp>0)
           {
            if(FileTell(file_handle)==2)
              {
               FileWrite(file_handle,"time","open","high","low","close","tick_volume","trend_group","trend_index");
               for(int i=0; i<n_cp; i++)
                 {
                  FileWrite(file_handle,
                            rates[i].time,
                            rates[i].open,
                            rates[i].high,
                            rates[i].low,
                            rates[i].close,
                            rates[i].tick_volume,
                            typ,
                            i);
                 }
              }
            else
              {
               for(int i=0; i<n_cp; i++)
                 {
                  FileWrite(file_handle,
                            rates[i].time,
                            rates[i].open,
                            rates[i].high,
                            rates[i].low,
                            rates[i].close,
                            rates[i].tick_volume,
                            typ,
                            i);
                 }
              }
           }
         else
            Print("No data copied!");
         FileFlush(file_handle);
         typ=3;
        }

      5.ファイル読み込み関数read_csv()を実装します。
      void read_csv(int hd,
                    string &arry[])
        {
         int i=0;
         while(!FileIsEnding(hd))
           {
            ArrayResize(arry,i+1);
            arry[i]=FileReadString(hd);
            i++;
           }
        }

      6.ここで対処されていない重要な問題がまだあります。EAの初期化時に開かれたファイルのハンドル file_handleが解放されていません。最後のOnDeinit()関数でハンドルを解放します。FileClose(file_handle)関数を呼び出すと、すべてのデータが実際にcsvファイルに書き込まれます。したがって、EAの実行中にcsvファイルを開こうとしないことが特に重要です。
      void OnDeinit(const int reason)
        {
         FileClose(file_handle);
         Print("Write data!");
        }


      注:この記事で示したコードは、あくまでもデモンストレーションのためのものです。実際に使用する場合は、さらにコードを改良することをお勧めします。記事の最後には、CSVファイルと最終的なMQL5ファイルが提供されています。次回は、クライアントとpythonを組み合わせてデータに注釈を付ける方法を紹介します。

      辛抱強く読んでいただきありがとうございました。何か得るものがあれば幸いです。