利用判别分析开发交易系统·进阶篇
📘

利用判别分析开发交易系统·进阶篇

第 2/3 篇

用 EA 把 12 个指标抽成 CSV 训练集

做价格方向预测的第一步不是调模型,而是把指标和后续柱的涨跌绑成一行数据。思路很直接:取倒数第二根已完成柱的指标值,用最后一根已完成柱的价格行为当标签,这样每一行都是一个「已知指标 → 未知方向」的样本。 具体落地时写个 EA 跑数据采集。OnInit 里把 12 个指标句柄全建好,并在 Files 目录生成 MasterData.csv 的列头;OnTick 里识别新柱,把指标绝对值、与前值的差(冠 d 前缀)、信号线差以及小时数写进文件。外汇和贵金属波动剧烈,这种原始采集最好先在策略 tester 里跑,文件会落在 tester/Agent-127.0.0.1-3000/MQL5/Files 下,避免污染实盘环境。 实测采集 EURUSD H1 从 2011-08-01 到 2011-10-01 的数据,用 37 个衍生变量(12 指标绝对值+差值+时间等)就能搭出判别分析用的底表。分组变量按最后一根柱相较标签柱的变动切四档:跌超 200 点=1,跌 200 内=2,涨 200 内=3,涨超 200 点=4,Statistica 里用 =iif(v3<=-200;1;0)+iif(v3<0 and v3>-200;2;0)+iif(v3>0 and v3<200;3;0)+iif(v3>=200;4;0) 一键转好。 指标句柄建错会直接让 EA 退出,所以初始化必须判 INVALID_HANDLE。下面这段是 OnInit 的前半部分,建完句柄顺手把缓冲区设成时间倒序,方便后面按 [0] 取最新完成柱。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA交易初始化函数                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 初始化指标
   h_AC=iAC(Symbol(),Period());
   h_BearsPower=iBearsPower(Symbol(),Period(),BearsPower_PeriodBears);
   h_BullsPower=iBullsPower(Symbol(),Period(),BullsPower_PeriodBulls);
   h_AO=iAO(Symbol(),Period());
   h_CCI=iCCI(Symbol(),Period(),CCI_PeriodCCI,CCI_Applied);
   h_DeMarker=iDeMarker(Symbol(),Period(),DeM_PeriodDeM);
   h_FrAMA=iFrAMA(Symbol(),Period(),FraMA_PeriodMA,FraMA_Shift,FraMA_Applied);
   h_MACD=iMACD(Symbol(),Period(),MACD_PeriodFast,MACD_PeriodSlow,MACD_PeriodSignal,MACD_Applied);
   h_RSI=iRSI(Symbol(),Period(),RSI_PeriodRSI,RSI_Applied);
   h_RVI=iRVI(Symbol(),Period(),RVI_PeriodRVI);
   h_Stoch=iStochastic(Symbol(),Period(),Stoch_PeriodK,Stoch_PeriodD,Stoch_PeriodSlow,MODE_SMA,Stoch_Applied);
   h_WPR=iWPR(Symbol(),Period(),WPR_PeriodWPR);
   if(h_AC==INVALID_HANDLE || h_BearsPower==INVALID_HANDLE ||
      h_BullsPower==INVALID_HANDLE || h_AO==INVALID_HANDLE ||
      h_CCI==INVALID_HANDLE || h_DeMarker==INVALID_HANDLE ||
      h_FrAMA==INVALID_HANDLE || h_MACD==INVALID_HANDLE ||
      h_RSI==INVALID_HANDLE || h_RVI==INVALID_HANDLE ||
      h_Stoch==INVALID_HANDLE || h_WPR==INVALID_HANDLE)
      {
         Print("创建指标错误");
         class="kw">return(class="num">1);
      }
   ArraySetAsSeries(buf_AC,true);
   ArraySetAsSeries(buf_BearsPower,true);
   ArraySetAsSeries(buf_BullsPower,true);
   ArraySetAsSeries(buf_AO,true);
   ArraySetAsSeries(buf_CCI,true);
   ArraySetAsSeries(buf_DeMarker,true);

◍ 把多指标缓冲倒进 CSV 的初始化写法

在 EA 的 OnInit 阶段,先把 9 个指标缓冲区全部设成时间序列方向,这样后续用 buf[0] 取到的就是当前柱值,而不是从最早历史开始排。 代码里连续调了 ArraySetAsSeries 处理 FrAMA、MACD 快慢线、RSI、RVI 快慢线、Stoch 快慢线和 WPR,共 9 个数组;若漏掉任意一行,回测时索引错位会导致写入 CSV 的数据整体偏移一根柱。

随后用 FileOpen 以 FILE_ANSIFILE_WRITEFILE_CSVFILE_SHARE_READ 打开 MasterData2.csv,分隔符指定为英文分号。文件句柄有效时,首行写死 41 个列名(Time、Hour、Price 加 19 个指标及其一阶差分 dXXX),方便外部 Python 或 R 直接读表。

句柄若等于 INVALID_HANDLE,则打印错误码并调 ExpertRemove 自卸载,避免空文件反复刷错误日志。外汇与贵金属品种波动剧烈,这种批量落盘方式仅用于本地特征采集,实盘直接跟单属高风险行为。 OnTick 里用 static datetime Prev_time 记住上一根柱时间,配合 ArraySetAsSeries(mrate,true) 把最近 K 线复制到 mrate 数组;SymbolInfoTick 取不到最新报价就 Alert 并 return,防止脏数据写进文件。

MQL5 / C++
  ArraySetAsSeries(buf_FrAMA,true);
  ArraySetAsSeries(buf_MACD_m,true);
  ArraySetAsSeries(buf_MACD_s,true);
  ArraySetAsSeries(buf_RSI,true);
  ArraySetAsSeries(buf_RVI_m,true);
  ArraySetAsSeries(buf_RVI_s,true);
  ArraySetAsSeries(buf_Stoch_m,true);
  ArraySetAsSeries(buf_Stoch_s,true);
  ArraySetAsSeries(buf_WPR,true);
  FileHandle=FileOpen("MasterData2.csv",FILE_ANSI|FILE_WRITE|FILE_CSV|FILE_SHARE_READ,&class="macro">#x27;;&class="macro">#x27;);
  if(FileHandle!=INVALID_HANDLE)
    {
      Print("FileOpen OK");
      class=class="str">"cmt">//--- 为方便起见保存文件第一行变量的名称
      FileWrite(FileHandle,"Time","Hour","Price","AC","dAC","Bears","dBears","Bulls","dBulls",
                "AO","dAO","CCI","dCCI","DeMarker","dDeMarker","FrAMA","dFrAMA","MACDm","dMACDm",
                "MACDs","dMACDs","MACDms","dMACDms","RSI","dRSI","RVIm","dRVIm","RVIs","dRVIs",
                "RVIms","dRVIms","Stoch_m","dStoch_m","Stoch_s","dStoch_s","Stoch_ms","dStoch_ms",
                "WPR","dWPR");
    }
  else
    {
      Print("FileOpen 操作失败. Error",GetLastError());
      ExpertRemove();
    }
class=class="str">"cmt">//---
  class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| EA交易订单函数                                                            |
class=class="str">"cmt">//| 监控市场情况并在                                                              |
class=class="str">"cmt">//| 每个新柱开端时指标值保存到文件中                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//--- 声明 class="type">class="kw">datetime 类型静态变量
  class="kw">static class="type">class="kw">datetime Prev_time;
class=class="str">"cmt">//--- 它将被用作保存价格, 交易量和每个柱的点差
  class="type">MqlRates mrate[];
  class="type">MqlTick tickdata;
  ArraySetAsSeries(mrate,true);    

class=class="str">"cmt">//--- 取得最近报价
  if(!SymbolInfoTick(_Symbol,tickdata))
    {
      Alert("更新报价错误 - 错误: ",GetLastError(),"!!");
      class="kw">return;
    }
class=class="str">"cmt">///--- 复制最近4个柱的数据

「新K线触发后的多指标快照落盘」

EA 只在出现新柱时才执行一次数据采集,避免每 tick 重复写文件。逻辑上先拿最近 4 根 K 线的报价结构体,若 CopyRates 返回小于 0 直接弹窗报警并退出,这说明当前品种或周期的历史报价可能没同步到终端。 判断新柱的方式很朴素:用静态变量 Prev_time 存上一根柱的 time,若和 mrate[0].time 相等就直接 return。一旦不相等,便把新时间写回 Prev_time,随后开始把十几个指标缓冲区的近 4 值填进数组。 下面这段是核心填充链,任一缓冲区复制失败 copy_result 都会变成 false,最后统一检查并 Print 报错退出。注意 MACD、RVI、Stoch 用了双缓冲版本(主线+信号线),其余指标都是单线。

MQL5 / C++
if(CopyRates(_Symbol,_Period,class="num">0,class="num">4,mrate)<class="num">0)
  {
    Alert("复制历史报价错误 - 错误: ",GetLastError(),"!!");
    class="kw">return;
  }
class=class="str">"cmt">//--- 如果两个时间值相等, 说明有新柱
  if(Prev_time==mrate[class="num">0].time) class="kw">return;
class=class="str">"cmt">//--- 把时间保存到静态变量 
  Prev_time=mrate[class="num">0].time;
class=class="str">"cmt">//--- 使用指标值填充数组
  class="type">bool copy_result=true;
  copy_result=copy_result && FillArrayFromBuffer1(buf_AC,h_AC,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_BearsPower,h_BearsPower,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_BullsPower,h_BullsPower,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_AO,h_AO,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_CCI,h_CCI,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_DeMarker,h_DeMarker,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_FrAMA,h_FrAMA,class="num">4);
  copy_result=copy_result && FillArraysFromBuffers2(buf_MACD_m,buf_MACD_s,h_MACD,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_RSI,h_RSI,class="num">4);
  copy_result=copy_result && FillArraysFromBuffers2(buf_RVI_m,buf_RVI_s,h_RVI,class="num">4);
  copy_result=copy_result && FillArraysFromBuffers2(buf_Stoch_m,buf_Stoch_s,h_Stoch,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_WPR,h_WPR,class="num">4);
class=class="str">"cmt">//--- 检查复制数据的正确性
  if(!copy_result==true)
    {
      Print("复制数据错误");
      class="kw">return;
    }
落盘时只写前一根柱(索引 2)的指标值与变动量(索引2减索引3),以及近两根收盘价差除以 _Point 的跳数。外汇与贵金属波动受杠杆与消息面影响大,这类快照仅作特征记录,据此训练或推理出的信号都可能失效,实盘前务必在 MT5 策略测试器核对缓冲索引是否和你用的指标版本一致。 文件句柄有效时才写,TimeCurrent 取的是服务器时间而非本地钟,小时字段单独存一列方便后续按交易时段切分。打开 MT5 把这段接进你的采集 EA,跑一个晚间 session 就能拿到几百行带标签的特征 csv。

MQL5 / C++
if(CopyRates(_Symbol,_Period,class="num">0,class="num">4,mrate)<class="num">0)
  {
    Alert("复制历史报价错误 - 错误: ",GetLastError(),"!!");
    class="kw">return;
  }
class=class="str">"cmt">//--- 如果两个时间值相等, 说明有新柱
  if(Prev_time==mrate[class="num">0].time) class="kw">return;
class=class="str">"cmt">//--- 把时间保存到静态变量 
  Prev_time=mrate[class="num">0].time;
class=class="str">"cmt">//--- 使用指标值填充数组
  class="type">bool copy_result=true;
  copy_result=copy_result && FillArrayFromBuffer1(buf_AC,h_AC,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_BearsPower,h_BearsPower,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_BullsPower,h_BullsPower,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_AO,h_AO,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_CCI,h_CCI,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_DeMarker,h_DeMarker,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_FrAMA,h_FrAMA,class="num">4);
  copy_result=copy_result && FillArraysFromBuffers2(buf_MACD_m,buf_MACD_s,h_MACD,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_RSI,h_RSI,class="num">4);
  copy_result=copy_result && FillArraysFromBuffers2(buf_RVI_m,buf_RVI_s,h_RVI,class="num">4);
  copy_result=copy_result && FillArraysFromBuffers2(buf_Stoch_m,buf_Stoch_s,h_Stoch,class="num">4);
  copy_result=copy_result && FillArrayFromBuffer1(buf_WPR,h_WPR,class="num">4);
class=class="str">"cmt">//--- 检查复制数据的正确性
  if(!copy_result==true)
    {
      Print("复制数据错误");
      class="kw">return;
    }

把多指标状态一次性落盘

这段代码片段是 EA 在每根 K 线收尾时,把多组指标的差值快照写进 CSV 的关键动作。它一口气塞进了 MACD(快/慢线及其差值的二阶变化)、RSI 及其一阶差分、RVI(快/慢线及二阶交叉差)、Stochastic(快/慢线及二阶交叉差)、WPR 及其差分,总共 21 个浮点字段,索引统一用 [2] 和 [3],也就是倒数第二根与倒数第三根 bar 的对比。 这种写法把‘指标动能’而不是‘指标绝对值’喂给后续分析,能直接看出每个振荡器是在加速还是减速。外汇和贵金属市场的高波动下,这类二阶差分对拐点识别有帮助,但信号迟到 1~2 根 bar 是常态,概率上只是提高过滤噪声的效率。 如果 FileWrite 返回 0,说明磁盘写入失败,代码会打印错误号并调用 ExpertRemove() 自爆退出,避免脏数据持续累积。开 MT5 把这段接在你自己的 buf_ 数组后面,跑一个 EURUSD 的 M15 回测,看 CSV 里 [2]-[3] 列的符号翻转是否比价格拐点平均早一根出现。

MQL5 / C++
 buf_MACD_m[class="num">2],buf_MACD_m[class="num">2]-buf_MACD_m[class="num">3],
 buf_MACD_s[class="num">2],buf_MACD_s[class="num">2]-buf_MACD_s[class="num">3],
 buf_MACD_m[class="num">2]-buf_MACD_s[class="num">2],buf_MACD_m[class="num">2]-buf_MACD_s[class="num">2]-buf_MACD_m[class="num">3]+buf_MACD_s[class="num">3],
 buf_RSI[class="num">2],buf_RSI[class="num">2]-buf_RSI[class="num">3],
 buf_RVI_m[class="num">2],buf_RVI_m[class="num">2]-buf_RVI_m[class="num">3],
 buf_RVI_s[class="num">2],buf_RVI_s[class="num">2]-buf_RVI_s[class="num">3],
 buf_RVI_m[class="num">2]-buf_RVI_s[class="num">2],buf_RVI_m[class="num">2]-buf_RVI_s[class="num">2]-buf_RVI_m[class="num">3]+buf_RVI_s[class="num">3],
 buf_Stoch_m[class="num">2],buf_Stoch_m[class="num">2]-buf_Stoch_m[class="num">3],
 buf_Stoch_s[class="num">2],buf_Stoch_s[class="num">2]-buf_Stoch_s[class="num">3],
 buf_Stoch_m[class="num">2]-buf_Stoch_s[class="num">2],buf_Stoch_m[class="num">2]-buf_Stoch_s[class="num">2]-buf_Stoch_m[class="num">3]+buf_Stoch_s[class="num">3],
 buf_WPR[class="num">2],buf_WPR[class="num">2]-buf_WPR[class="num">3]);
 if(Result==class="num">0)
  {
  Print("FileWrite 操作错误 ",GetLastError());
  ExpertRemove();
  }
 }

常见问题

在EA的OnInit里打开文件句柄,OnTick里判断新K线后把各指标缓冲值按顺序写入CSV,注意用FileWrite把表头和数据分开落盘。
在OnInit中用FileOpen以FILE_CSV|FILE_WRITE模式建文件,先FileWrite写列名,再保留句柄供后续快照调用,避免重复开关文件。
小布盯盘的AIGC可接管重复采样,你设定品种与指标后它能按新K线自动落盘并归并CSV,你只管调参和验证。
用静态变量存上一根K线时间,与当前Time[0]比对,不等时才执行指标读取与FileWrite,防止同根K线多次落盘。
按固定顺序定义数组下标,写盘前用ArrayInitialize清缓冲,FileWrite时统一用逗号分隔且每条记录尾加换行,肉眼核对首行表头即可排错。