时间序列挖掘的数据标签(第1部分):通过EA操作图制作具有趋势标记的数据集

Yuqiang Pan | 8 三月, 2024

汇总

当我们设计人工智能模型时,我们通常需要首先准备数据。良好的数据质量将使我们在模型训练和验证方面事半功倍。但我们的外汇或股票数据是特殊的,其中包含复杂的市场信息和时间信息,数据标注很困难,但我们可以很容易地在图表上分析历史数据的趋势。

 

本节介绍了一种通过EA操作图制作带有趋势标记的数据集的方法,您可以根据自己的想法直观地操作数据,当然您也可以使用相同的方法来扩展和自定义自己的数据集!

目录:

  1. 定义标签数据格式
  2. 初始化图表和文件
  3. 设计和标记操作逻辑
  4. 组织数据并写入文件
  5. 附件:完整的EA代码示例


定义标签数据格式

当我们从客户那里获得外汇或股票数据时(本文不讨论从文件中读取或从其他网站下载的外部数据),一般情况是:

Time(时间) Open(开盘价) High(最高价) Low(最低价) Close(收盘价) Tick_volume(分时交易量)
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个时间序列数据的样子。它们的收盘价(Close)和开盘价(Open)自始至终都是相互联系的,连贯性很强。假设我们认为前两个是上升趋势,其他是下降趋势(以上5个数据为例)。一般标记方法将数据分为两部分:

Time(时间)

Open(开盘价) High(最高价)   Low(最低价)   Close(收盘价) Tick_volume(分时交易量)
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

Time(时间) Open(开盘价) High(最高价)   Low(最低价) Close(收盘价) Tick_volume(分时交易量)
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条数据为例,或遵循上述假设):

Time(时间) Open(开盘价) High(最高价) Low(最低价) Close(收盘价) Tick_volume(分时交易量) Trend_group(趋势组)
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个数据为上涨趋势,中间的其余数据为下降趋势),如下所示:

Time(时间) Open(开盘价) High(最高价) Low(最低价) Close(收盘价) Tick_volume(分时交易量) Trend_group(趋势组) Trend_index(趋势索引)
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 — 将图表向后移动一个窗口的距离;
      • Page Down — 将图表向前移动一个窗口的距离;
      • 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。需要注意的是,我们只需要在数据段的开头增加变量,并指定再次按下按钮将通过先反转来执行标记数据段的“end”部分
      b_press
      2) 按's'标记上升趋势结束,“typ”变量仍然为0,“tp”变量设置为“end”,箭头颜色仍然为“clrBlue”,标签计数“Num”保持不变。需要注意的是,我们只需要在数据段的开头增加变量,而‘first’的反转用于指定再次按下按钮将执行标记数据段的“start”部分。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_group”和“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


      让我们检查不同数据段之间的一致性,发现数据是完美的:

      data_1


      附件:完整的EA代码示例

      1. 全局变量和常量的定义。参数“start_t”可以由1970年1月1日起的每秒数据定义。当然,它也可以由标准的“datetime”定义,也可以由输入变量“input int start_t=1403037112;”定义,以便在以后运行EA时随时更改:
      #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结合的客户端对数据进行注释。

      感谢您的耐心阅读,希望您有所收获,祝您生活愉快,下一章再见!