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

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

(2/3)· 面对连贯又难标注的贵金属分时数据,用EA在图上直接打趋势标签比写脚本省心得多

新手友好 第 2/3 篇
直接在CSV里按区间切分涨跌,常把连贯的K线关系打断,模型学到的只是碎片。很多人以为标完正负样本就完事,其实人工在图上圈趋势更贴近真实盘感。

◍ 用 B/S 键在图上手动切出趋势段

手动标段的核心是一套虚拟键值:B 键和 S 键成对出现,先后顺序决定方向。先按 B 再按 S,标记的是上升段;先按 S 再按 B,标记的是下降段。EA 里用 KEY_B=66、KEY_S=83 两个宏捕获按键事件。 按键只设定状态,不落点。按 B 且 first 为真时,typ 置 0(上升)、tp 置 "start"、Num 计数 +1、箭头色 clrBlue;再按 S 时 first 已翻转,tp 置 "end"、typ 仍 0、Num 不变。下降段逻辑对称,颜色换 clrRed、typ 置 1。first 变量保证每组必成 pair,不会出 B-B 或 S-S 的脏标。 落点靠鼠标左键。ChartXYToTimePrice() 把点击坐标转成时间价格,写进全局 dt;再用全局 tp 判断当前是段的头还是尾。标完一对,图表上出现蓝(升)或红(降)箭头,Num 直接显示在标签名里,例如 "0-3-start"。 别把这套当自动识别。它纯手动,外汇和贵金属波动快、滑点大,标错段只能删对象重来;原作者也承认没做撤销上一步,手抖就得 Backspace 删最后一个对象。

MQL5 / C++
class="macro">#define KEY_B     class="num">66
class="macro">#define KEY_S     class="num">83
if(id==CHARTEVENT_KEYDOWN)
   {
      class="kw">switch(lparam)
         {
          case KEY_B:
            if(first)
               {
                col=clrBlue ;
                typ =class="num">0;
                Num+=class="num">1;
                tp = "start";
               }
            else
               {
                col=clrRed ;
                typ = class="num">1;
                tp = "end";
               }
            ob =OBJ_ARROW_BUY;
            first = !first;
            Name = StringFormat("%d-%d-%s",typ,Num,tp);
            class="kw">break;
          case KEY_S:
            if(first)
               {
                col=clrRed ;
                typ =class="num">1;
                Num+=class="num">1;
                tp = "start";
               }
            else
               {
                col=clrBlue ;
                typ = class="num">0;
                tp = "end";
               }
            ob =OBJ_ARROW_SELL;
            first = !first;
            Name = StringFormat("%d-%d-%s",typ,Num,tp);
            class="kw">break;
          class="kw">default:
            Print("You pressed:"+lparam+" key, do nothing!");
         }

把鼠标点击换算成K线坐标

在 MT5 的图表事件回调里,CHARTEVENT_CLICK 给的是像素坐标而非时间价格,直接用毫无意义。必须靠 ChartXYToTimePrice 把 x、y 反推回 datetime 与 price,否则你画的对象会漂在屏幕逻辑之外。 下面这段处理点击后建对象并落盘起止时间的逻辑,核心是先取 lparam、dparam 转成 int 型 x、y,再调 ChartXYToTimePrice(0,x,y,window,dt,price)。成功才 ObjectCreate,失败则 Print 出 GetLastError 的代码——外汇与贵金属图表的高频重绘下,坐标转换失败概率不低,别假设它永远返回 true。 删除与创建对象也有独立事件号:CHARTEVENT_OBJECT_DELETE 和 CHARTEVENT_OBJECT_CREATE 仅打日志,不参与坐标换算。真要写区间记录,得在 tp=="start" 时存 Start=dt,否则调 file_write(Start,dt) 把首尾时间写文件。 让小布替你跑这套:开 MT5 新建 EA 把这段塞进 OnChartEvent,点两下图表看对象是否精准落在光标 K线位,再查文件里起止时间差。

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

「分段落盘时头部只写一次」

在 MT5 脚本里用鼠标点取趋势起止点后,需把每段 K 线数据写进文件。定义 datetime Start 与 MqlRates rates[] 即可,结束时间直接用图表最后一根即可,不必另设变量;ArraySetAsSeries(rates, false) 把序列按时间正序排列,避免段落首尾接反。 当 tp 标记为 end 时调用 file_write(),内部用 CopyRates() 按起止时间抓取 rates[],再逐根补上 trend_group(即全局变量 typ)与 trend_index 两列。注意 FileClose() 不在此处调用,要留到 OnDeinit() 统一收尾,否则后续段落写不进同一个文件。 头部写入是最容易踩坑的地方:用 FileTell(file_handle)==2 判断文件指针位置,若已是 2 字节说明头已存在,就跳过写头;只在首次(空文件)写 time/open/high/low/close/tick_volume/trend_group/trend_index 这 9 列。若每次都写头,已落盘的数据会被截断,实测多段拼接后列数对不齐。 下面这段是核心落盘逻辑,绿色为触发写文件、黄色为头部判断:

MQL5 / C++
<span class="keyword">class="type">class="kw">datetime</span> Start;
<span class="predefines">class="type">MqlRates</span> rates[];
<span class="functions">ArraySetAsSeries</span>(rates, <span class="macro">false</span>);
&nbsp;&nbsp; <span class="keyword">if</span>(id==<span class="macro">CHARTEVENT_CLICK</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--- definition</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> x=(<span class="keyword">class="type">int</span>)lparam;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> y=(<span class="keyword">class="type">int</span>)dparam;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">datetime</span> dt&nbsp;&nbsp;&nbsp;&nbsp;=<span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp; price =<span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;window=<span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="functions">ChartXYToTimePrice</span>(<span class="number">class="num">0</span>,x,y,window,dt,price))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ObjectCreate</span>(<span class="number">class="num">0</span>,Name,ob,window,dt,price);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,Name,<span class="macro">OBJPROP_COLOR</span>,col);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//Print("time:",dt,"shift:",iBarShift(Symbol(),PERIOD_CURRENT,dt));</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(tp==<span class="class="type">class="kw">string">"start"</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Start=dt;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(file_handle)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="background-class="type">color:rgb(class="num">119, class="num">183, class="num">83);">&nbsp;&nbsp;&nbsp; file_write(Start,dt);</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ChartRedraw</span>(<span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">Print</span>(<span class="class="type">class="kw">string">"ChartXYToTimePrice class="kw">return error code: "</span>,<span class="functions">GetLastError</span>());
&nbsp;&nbsp;&nbsp;&nbsp; }
<span class="keyword">class="type">void</span> file_write(<span class="keyword">class="type">class="kw">datetime</span> start,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">datetime</span> end)
&nbsp;&nbsp;{
&nbsp;&nbsp; <span class="predefines">class="type">MqlRates</span> rates[];
&nbsp;&nbsp; <span class="functions">ArraySetAsSeries</span>(rates,<span class="macro">false</span>);
&nbsp;&nbsp; <span class="keyword">class="type">int</span> n_cp=<span class="functions">CopyRates</span>(<span class="functions">Symbol</span>(),<span class="macro">PERIOD_CURRENT</span>,start,end,rates);
&nbsp;&nbsp; <span class="keyword">if</span>(n_cp&gt;<span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="background-class="type">color:rgb(class="num">255, class="num">246, class="num">200);"><span class="keyword">if</span>(<span class="functions">FileTell</span>(file_handle)==<span class="number">class="num">2</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">FileWrite</span>(file_handle,<span class="class="type">class="kw">string">"time"</span>,<span class="class="type">class="kw">string">"open"</span>,<span class="class="type">class="kw">string">"high"</span>,<span class="class="type">class="kw">string">"low"</span>,<span class="class="type">class="kw">string">"close"</span>,<span class="class="type">class="kw">string">"tick_volume"</span>,<span class="class="type">class="kw">string">"trend_group"</span>,<span class="class="type">class="kw">string">"trend_index"</span>);</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>; i&lt;n_cp; i++)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">FileWrite</span>(file_handle,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rates[i].time,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rates[i].open,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rates[i].high,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rates[i].low,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;rates[i].close,

◍ 把分组的 K 线写进 CSV 的细节

上面这段逻辑处理的是两种写入分支:当处于特定模式时按前一种循环写 rates,否则走 else 里的 for(int i=0;i<n_cp;i++) 把每根 K 线的 time/open/high/low/close/tick_volume 连同 typ 与 i 一并 FileWrite 到句柄。 注意 n_cp 是前面 CopyRates 实际返回的根数,若返回 0 会掉到最外层 else 执行 Print("No data copied!"),此时文件不会被写任何行情,只留空壳。 每次写完内层循环都调 FileFlush(file_handle) 强制刷盘,typ 随后被置为 3,意味着这一批分组写完后趋势标记切换到下一种状态。 文件若原本为空,FileTell 返回 2(BOM 占位)时会补写表头:time/open/high/low/close/tick_volume/trend_group/trend_index,之后数据行才对齐这 8 列。外汇与贵金属行情受杠杆与跳空影响,回测与实盘 Tick 量可能有明显差异,写盘数据仅作概率参考。

MQL5 / C++
                        rates[i].tick_volume,
                        typ,
                        i);
       }
      }
      else
      {
       for(class="type">int i=class="num">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=class="num">3;
  }
 if(FileTell(file_handle)==class="num">2)
 FileWrite(file_handle,"time","open","high","low","close","tick_volume","trend_group","trend_index");

图表上打标签的 EA 骨架怎么搭

把键盘鼠标的交互收口到 OnChartEvent 里,是这套标注 EA 的核心。用 typ 变量做闸门,能避免误点鼠标写出脏数据:标记趋势时才放行 file_write(),其余点击直接忽略。 起始时间用 1970-01-01 以来的秒数定义,代码里写死成 start_t=1403037112(对应 2014-06-17 附近);更实用的做法是改成 input int start_t,每次加载 EA 都能改,不用动源码。按钮键位同样可以 input 化,KEY_B=66(B 键)、KEY_S=83(S 键)只是默认值。

文件句柄在 OnInit 里用 FileOpen 打开,模式是 FILE_WRITEFILE_CSVFILE_READ;OnDeinit 里必须 FileClose(file_handle) 释放,否则 csv 不会真正落盘。EA 跑着的时候去打开那个 csv(样例 GOLD_micro15-1403037112.csv 约 13.84 KB)会读不到完整内容。

下面这段是全局定义与初始化的浓缩版,直接丢进 MT5 能编译,接上 OnChartEvent 的标记逻辑就能在黄金 15 分钟图上手动画线打标。外汇和贵金属波动剧烈、杠杆高风险大,标注出的样本仅供模型训练参考,不代表任何方向判定。

MQL5 / C++
class="macro">#define KEY_B    class="num">66
class="macro">#define KEY_S    class="num">83
class="type">int Num= class="num">0;
class="type">int typ= class="num">3;
class="type">class="kw">string Name;
class="type">class="kw">string tp;
class="type">color col;
class="type">bool first= true;
ENUM_OBJECT ob;
class="type">int file_handle=class="num">0;
class="type">int start_t=class="num">1403037112;
class="type">class="kw">datetime Start;
class="kw">input class="type">int KEY_B=class="num">66;
class="kw">input class="type">int KEY_S=class="num">83;
class="type">int OnInit()
  {
class=class="str">"cmt">//---initial
   class="type">class="kw">string name;
   class="type">class="kw">string reName="class="num">1";
   class="type">int hd=FileFindFirst("*",name,class="num">0);
   class="type">int shift;
   ChartSetInteger(class="num">0,CHART_AUTOSCROLL,false);
   ChartSetInteger(class="num">0,CHART_SHIFT,false);
   ChartSetInteger(class="num">0,CHART_MOUSE_SCROLL,class="num">1);
   do
     {
      class=class="str">"cmt">//---check File
      if(StringFind(name,Symbol())!=-class="num">1 && StringFind(name,".csv")!=-class="num">1)
        reName=name;
     }
   class="kw">while(FileFindNext(hd,name));
   if(FileIsExist(reName))
     {
      file_handle = FileOpen(reName,FILE_WRITE|FILE_CSV|FILE_READ);
      class="type">class="kw">string a[];
      class="type">int i=class="num">0;
      read_csv(file_handle,a);
      i = ArraySize(a);
      shift = -iBarShift(Symbol(),PERIOD_CURRENT,(class="type">class="kw">datetime)a[i-class="num">8]);
把重复劳动交给小布
这些诊断和小布盯盘的 AIGC 已内置,打开对应品种页即可看到趋势分段与标注建议,你只需复核决策。

常见问题

建议在图表上结合更高周期均线辅助判断,只在明显波峰波谷连线后落笔,震荡区留空不标比硬标更稳妥。
文中EA写入的是带趋势列的CSV,Time Open High Low Close Tick_volume Label 结构,pandas直接read_csv即可接入。
两者均属高杠杆高风险品种,跳空和休市会造成样本断层,标注时需剔除异常session以免污染训练集。
目前小布内置的是趋势分段可视化与疑似标注提示,最终标签仍建议交易者确认,半自动比全自动更可控。
格式定了文件写入和读取才不会错位,关于完整数据格式讨论见《时间序列挖掘的数据标签(第1部分):通过EA操作图制作具有趋势标记的数据集·基础篇》。