时间序列挖掘的数据标签(第1部分):通过EA操作图制作具有趋势标记的数据集·进阶篇
(2/3)· 面对连贯又难标注的贵金属分时数据,用EA在图上直接打趋势标签比写脚本省心得多
◍ 用 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 删最后一个对象。
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线位,再查文件里起止时间差。
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 列。若每次都写头,已落盘的数据会被截断,实测多段拼接后列数对不齐。 下面这段是核心落盘逻辑,绿色为触发写文件、黄色为头部判断:
<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>); <span class="keyword">if</span>(id==<span class="macro">CHARTEVENT_CLICK</span>) { <span class="comment">class=class="str">"cmt">//--- definition</span> <span class="keyword">class="type">int</span> x=(<span class="keyword">class="type">int</span>)lparam; <span class="keyword">class="type">int</span> y=(<span class="keyword">class="type">int</span>)dparam; <span class="keyword">class="type">class="kw">datetime</span> dt =<span class="number">class="num">0</span>; <span class="keyword">class="type">class="kw">double</span> price =<span class="number">class="num">0</span>; <span class="keyword">class="type">int</span> window=<span class="number">class="num">0</span>; <span class="keyword">if</span>(<span class="functions">ChartXYToTimePrice</span>(<span class="number">class="num">0</span>,x,y,window,dt,price)) { <span class="functions">ObjectCreate</span>(<span class="number">class="num">0</span>,Name,ob,window,dt,price); <span class="functions">ObjectSetInteger</span>(<span class="number">class="num">0</span>,Name,<span class="macro">OBJPROP_COLOR</span>,col); <span class="comment">class=class="str">"cmt">//Print("time:",dt,"shift:",iBarShift(Symbol(),PERIOD_CURRENT,dt));</span> <span class="keyword">if</span>(tp==<span class="class="type">class="kw">string">"start"</span>) Start=dt; <span class="keyword">else</span> { <span class="keyword">if</span>(file_handle) <span style="background-class="type">color:rgb(class="num">119, class="num">183, class="num">83);"> file_write(Start,dt);</span> } <span class="functions">ChartRedraw</span>(<span class="number">class="num">0</span>); } <span class="keyword">else</span> <span class="functions">Print</span>(<span class="class="type">class="kw">string">"ChartXYToTimePrice class="kw">return error code: "</span>,<span class="functions">GetLastError</span>()); } <span class="keyword">class="type">void</span> file_write(<span class="keyword">class="type">class="kw">datetime</span> start, <span class="keyword">class="type">class="kw">datetime</span> end) { <span class="predefines">class="type">MqlRates</span> rates[]; <span class="functions">ArraySetAsSeries</span>(rates,<span class="macro">false</span>); <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); <span class="keyword">if</span>(n_cp><span class="number">class="num">0</span>) { <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>) { <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> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>; i<n_cp; i++) { <span class="functions">FileWrite</span>(file_handle, rates[i].time, rates[i].open, rates[i].high, rates[i].low, 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 量可能有明显差异,写盘数据仅作概率参考。
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_WRITE | FILE_CSV | FILE_READ;OnDeinit 里必须 FileClose(file_handle) 释放,否则 csv 不会真正落盘。EA 跑着的时候去打开那个 csv(样例 GOLD_micro15-1403037112.csv 约 13.84 KB)会读不到完整内容。 |
|---|
下面这段是全局定义与初始化的浓缩版,直接丢进 MT5 能编译,接上 OnChartEvent 的标记逻辑就能在黄金 15 分钟图上手动画线打标。外汇和贵金属波动剧烈、杠杆高风险大,标注出的样本仅供模型训练参考,不代表任何方向判定。
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]);