在 MetaTrader 5 中测试和优化二元期权策略·进阶篇
📘

在 MetaTrader 5 中测试和优化二元期权策略·进阶篇

第 2/3 篇

EURUSD 多周期回测的时空与本金设定

做这套价格行为策略的机器验证,先把标的锁死在 EURUSD 外汇品种上,周期覆盖 M5、M15、M30 与 H1 四个级别,期权到期时间对应取 5、15、30 分钟和 1 小时。把优化窗口和前向验证窗口各切一年,能逼出样本内过拟合与样本外失效的边界。 优化段取 2021.01.28—2022.01.28,前向验证段紧接 2022.01.28—2023.01.28,两段不重叠是基本纪律。初始本金设 10,000 美元,手数比率 10、期权赔付利率 80%,这组数值直接决定每笔风险敞口。 外汇与贵金属自带高杠杆与滑点风险,上述本金和比率只是回测假设,实盘可能因点差扩大而偏离历史分布,上 MT5 策略测试器照此参数跑一遍最直观。

「EA 输入项与持仓跟踪的函数骨架」

做二元期权回测的 EA,先把可调参数摊开:启动资金 StartDepo 默认 10000,OptionRate 是每笔按资金比例下的注(默认 10,即 10%),ExpirationTime 管到期映射——1 对应 5 分钟、2 对应 15 分钟、3 对应 30 分钟、4 对应 60 分钟;ProfitPercent 默认 80 代表盈利时返还本金+80% 收益。TimeFrame 同样用 1~4 映射 5/15/30/60 分钟周期,Optimization 置 true 才写 CSV,文件名由 OptimizationFileName 指定。 运行时靠 XStartDepo 承接当前权益,Profit / Loss 分别累计盈利与亏损期权笔数;TrigerBuy / TrigerSell 作信号锁,保证同一根信号只开一次仓,重复触发直接忽略。MT5 里平仓时机不靠订单本身到期,而是用 GetTime() 抓持仓开仓时间,再比对其与 ExpirationTime 映射长度的流逝。 GetTime() 这段代码值得直接抄进编辑器验证:它遍历 PositionsTotal(),按 symbol / type / magic 过滤后把 position.Time() 塞进数组,返回指定索引的开仓时间;默认取最新一笔。佣金与掉期在回测里暂未计入,外汇与贵金属实盘价差和隔夜成本会让结果偏离,属高风险偏差项。 优化开关打开后,OnDeinit 里把最终资金、盈亏笔数、周期与到期写进 C:\Users\登录名\AppData\Roaming\MetaQuotes\Terminal\Common\Files 下的 CSV。想肉眼看过程,Comment() 把中间结果打印到图表即可,可视化测试时逐根 K 线核对信号锁是否漏触发。

MQL5 / C++
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> N0 = <span class="class="type">class="kw">string">"------------Open settings----------------"</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> StartDepo = <span class="number">class="num">10000</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> OptionRate = <span class="number">class="num">10</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> N1 = <span class="class="type">class="kw">string">"------------Close settings---------------"</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> ExpirationTime = <span class="number">class="num">1</span>; <span class="comment">class=class="str">"cmt">//ExpirationTime class="num">1=class="num">5 min, class="num">2=class="num">15 min, class="num">3=class="num">30 min, class="num">4=class="num">60 min</span>
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">double</span> ProfitPercent = <span class="number">class="num">80</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> N2 = <span class="class="type">class="kw">string">"------------Optimization settings--------"</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> TimeFrame = <span class="number">class="num">1</span>; <span class="comment">class=class="str">"cmt">//TimeFrame class="num">1=class="num">5 min, class="num">2=class="num">15 min, class="num">3=class="num">30 min, class="num">4=class="num">60 min</span>
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">bool</span> Optimization = <span class="macro">class="kw">false</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> OptimizationFileName = <span class="class="type">class="kw">string">"Optimization.csv"</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> N3 = <span class="class="type">class="kw">string">"------------Other settings---------------"</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> Slippage = <span class="number">class="num">10</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> Magic = <span class="number">class="num">111111</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">class="kw">string</span> EAComment = <span class="class="type">class="kw">string">"2Ma+RSI+Stochastic Oscillator"</span>;
<span class="keyword">class="type">class="kw">double</span> XStartDepo = StartDepo;
<span class="keyword">class="type">int</span> Profit=<span class="number">class="num">0</span>;
<span class="keyword">class="type">int</span> Loss=<span class="number">class="num">0</span>;
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Get open time in positions                                                        |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">class="kw">datetime</span> GetTime(<span class="keyword">class="type">class="kw">string</span> symb=<span class="class="type">class="kw">string">"class="num">0"</span>, <span class="keyword">class="type">int</span> type=-<span class="number">class="num">1</span>, <span class="keyword">class="type">int</span> mg=-<span class="number">class="num">1</span>,<span class="keyword">class="type">int</span> index=<span class="number">class="num">0</span>) {
<span class="keyword">class="type">class="kw">datetime</span> p[];
<span class="keyword">class="type">int</span> c=-<span class="number">class="num">1</span>, pr=<span class="number">class="num">0</span>;
&nbsp;&nbsp;<span class="keyword">if</span>(symb==<span class="class="type">class="kw">string">"class="num">0"</span>) { symb=<span class="functions">Symbol</span>();}
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="functions">PositionsTotal</span>()-<span class="number">class="num">1</span>;i&gt;=<span class="number">class="num">0</span>;i--){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(position.SelectByIndex(i)) {
&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(position.PositionType()==<span class="macro">POSITION_TYPE_BUY</span> || position.PositionType()==<span class="macro">POSITION_TYPE_SELL</span>) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>((position.<span class="functions">Symbol</span>()==symb||symb==<span class="class="type">class="kw">string">""</span>)&amp;&amp;(type&lt;<span class="number">class="num">0</span>||position.PositionType()==type)&amp;&amp;(mg&lt;<span class="number">class="num">0</span>||position.Magic()==mg)) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; c++;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="functions">ArrayResize</span>(p, c+<span class="number">class="num">1</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; p[c]=position.Time();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; pr=c&gt;=index?index:c;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
}}}}}}
&nbsp;&nbsp;<span class="keyword">class="kw">return</span>(c==-<span class="number">class="num">1</span>?<span class="number">class="num">0</span>:p[pr]);
}
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Get profit in positions                                                          |</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>

◍ 用持仓利润驱动期权式结算

这段逻辑把外汇/贵金属的持仓当成二元期权来管:到时间就按当时浮盈结算,赢则按费率加成、输则扣掉固定下注额。XExpirationTime 以分钟计,乘 60 转成秒后叠加在开仓时间上,到期触发判定。 GetProfit 函数先按品种、方向、魔术码过滤持仓,把符合条件的 Profit() 塞进数组,再按 index 返回对应单的盈亏。若没有任何持仓匹配,返回 0 而不是报错,调用方得自己留意这个默认值。 [CODE]double GetProfit(string symb="0", int type=-1, int mg=-1,int index=0) { double p[]; int c=-1, pr=0; if(symb=="0") { symb=Symbol();} for(int i=PositionsTotal()-1;i>=0;i--){ if(position.SelectByIndex(i)) {

if(position.PositionType()==POSITION_TYPE_BUYposition.PositionType()==POSITION_TYPE_SELL) {
if((position.Symbol()==symbsymb=="")&&(type<0position.PositionType()==type)&&(mg<0position.Magic()==mg)) {

c++; ArrayResize(p, c+1); p[c]=position.Profit(); pr=c>=index?index:c; }}}} return(c==-1?0:p[pr]); }[/CODE] 逐行看:symb 默认 "0" 时自动取当前图表的 Symbol();循环从 PositionsTotal()-1 倒序扫所有持仓。SelectByIndex 选中第 i 单后,只认多/空两类。过滤条件里 symb 可留空表示不限品种,type 和 mg 小于 0 表示不限定方向或魔术码。c 是匹配计数,ArrayResize 动态扩数组,p[c] 存该单利润;pr 保证 index 越界时回退到最后一单。 Sell 分支里,若卖单存在且当前时间越过开仓时间加过期秒数,就查 GetProfit 是否大于 0。大于 0 时 XStartDepo 加 OptionRate*ProfitPercent/100,Profit 计数 +1;否则减 OptionRate,Loss +1。Buy 分支完全对称。 外汇与贵金属杠杆高,这类到期结算若 OptionRate 设大,几次 Loss 就能吞掉本金,参数务必在小布里先跑历史 tick 验证。

MQL5 / C++
class="type">class="kw">double GetProfit(class="type">class="kw">string symb="class="num">0", class="type">int type=-class="num">1, class="type">int mg=-class="num">1,class="type">int index=class="num">0) {
class="type">class="kw">double p[];
class="type">int c=-class="num">1, pr=class="num">0;
  if(symb=="class="num">0") { symb=Symbol();}
  for(class="type">int i=PositionsTotal()-class="num">1;i>=class="num">0;i--){
    if(position.SelectByIndex(i)) {
    if(position.PositionType()==POSITION_TYPE_BUY || position.PositionType()==POSITION_TYPE_SELL) {
    if((position.Symbol()==symb||symb=="")&&(type<class="num">0||position.PositionType()==type)&&(mg<class="num">0||position.Magic()==mg)) {
    c++;
    ArrayResize(p, c+class="num">1);
    p[c]=position.Profit();
    pr=c>=index?index:c;
}}}}
  class="kw">return(c==-class="num">1?class="num">0:p[pr]);
}
class="type">int TrigerSell=class="num">0;
class="type">int TrigerBuy=class="num">0;
class=class="str">"cmt">//Sell(Put)
  if ((CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_SELL, EAComment)>class="num">0) && (TimeCurrent()>=(GetTime(symbolS1.Name(), POSITION_TYPE_SELL, Magic, class="num">0)+(XExpirationTime*class="num">60)))){
    if(GetProfit(symbolS1.Name(), POSITION_TYPE_SELL, Magic, class="num">0)>class="num">0){
    XStartDepo=XStartDepo+((OptionRate*ProfitPercent)/class="num">100);
    Profit++;
    }
    else{
    XStartDepo=XStartDepo-OptionRate;
    Loss++;
    }
  Comment("Depo = ",XStartDepo," Profit = ",Profit," Loss = ",Loss);
  ClosePositions(symbolS1.Name(), Magic, POSITION_TYPE_SELL, EAComment);
  }
class=class="str">"cmt">//Buy(Call)
  if ((CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_BUY, EAComment)>class="num">0) && (TimeCurrent()>=(GetTime(symbolS1.Name(), POSITION_TYPE_BUY, Magic, class="num">0)+(XExpirationTime*class="num">60)))){
    if(GetProfit(symbolS1.Name(), POSITION_TYPE_BUY, Magic, class="num">0)>class="num">0){
    XStartDepo=XStartDepo+((OptionRate*ProfitPercent)/class="num">100);
    Profit++;
    }
    else{
    XStartDepo=XStartDepo-OptionRate;
    Loss++;
    }
  Comment("Depo = ",XStartDepo," Profit = ",Profit," Loss = ",Loss);
  ClosePositions(symbolS1.Name(), Magic, POSITION_TYPE_BUY, EAComment);
  }

把回测结果落盘到文件尾

EA 跑完一轮参数组合后,如果命中了写入条件,就会把这次的摘要信息追加进结果文件而不是覆盖。 上面这段 MQL5 先把文件指针挪到末尾,再写一行,最后关句柄。注意 SEEK_END 保证多次运行不会互相抹掉历史记录。 逐行看:FileSeek 带 0 偏移和 SEEK_END 宏,定位到文件尾;FileWrite 一口气写了起始资金(转字符串)、盈利次数、亏损次数、时间帧、过期时间五个字段,用逗号分隔;FileClose 释放句柄,避免 MT5 端文件锁残留。 实盘或复盘前,你可在 MT5 的 Files 目录翻这个 csv,比对不同 XTimeFrame 下 Profit/Loss 的分布,外汇与贵金属杠杆品种回测误差大,仅作概率参考。

MQL5 / C++
   FileSeek(filehandle, class="num">0, SEEK_END);
   FileWrite(filehandle,DoubleToString(XStartDepo),IntegerToString(Profit),IntegerToString(Loss),IntegerToString(XTimeFrame),IntegerToString(XExpirationTime));
   FileClose(filehandle);
     }
}
}
  }

「从经纪商模板到自己写的轨道线摊平」

前面几套现成组合在 MT5 里跑完基本都亏。2Ma+RSI+Stochastic 在 M5 上最终资金 1964、盈 1273 笔亏 1822 笔;Maverick 用布林 20/2 加 RSI(4,80,20) 测出 3312、盈 1589 亏 1940;Vortex(14)+TSI(25,13,5) 更直接负到 -3118、亏 2843 笔。外汇与贵金属期权剥头皮本身杠杆高、滑点凶,这些回测数字说明盲抄经纪商策略大概率扛不住手续费和到期结构。 作者自己换了思路:用轨道线(14, 简单平均, 收盘价) 抓超买卖边界,Ask 破下轨且距离大于 Distance 点才考虑 Call,Bid 破上轨对称做 Put,不加触发器、空仓就持续监控。M5 上 Distance=150 时不摊平测出资金 8952、盈 874 亏 804,期望略负但最大连亏只有 2 次,于是引入 Averageaging=4 限制层数的摊平。 摊平版跑通后,又试了亏损后倍增的有趣变体。下面这段是前述某套(以双 MA 反向交叉为触发、RSI 与 Stochastic 过滤)的做空与做多判定代码,可直接丢进 MT5 看信号逻辑。 别把经纪商策略当圣杯 回测连亏的笔数普遍高于盈利笔数,真上实盘前至少把到期时间和点差模型在策略测试器里重跑一遍。

MQL5 / C++
class=class="str">"cmt">//Sell(Put)
if((ind_In1S1[class="num">1]>ind_In2S1[class="num">1]) && (ind_In1S1[class="num">2]>ind_In2S1[class="num">2])){TrigerSell=class="num">1;}
  if ((TrigerSell==class="num">1) && (CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_SELL, EAComment)==class="num">0) && (ind_In1S1[class="num">1]<ind_In2S1[class="num">1]) && (ind_In1S1[class="num">0]<ind_In2S1[class="num">0]) && (ind_In4S1[class="num">1]<class="num">50) && (ind_In4S1[class="num">0]<class="num">50) && (ind_In3S1_1[class="num">1]<ind_In3S1_2[class="num">1])){
  OpenSell(symbolS1.Name(), class="num">0.01, class="num">0, class="num">0, EAComment);
  TrigerSell=class="num">0;
  }
  
  if ((CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_SELL, EAComment)>class="num">0) && (TimeCurrent()>=(GetTime(symbolS1.Name(), POSITION_TYPE_SELL, Magic, class="num">0)+(XExpirationTime*class="num">60)))){
    if(GetProfit(symbolS1.Name(), POSITION_TYPE_SELL, Magic, class="num">0)>class="num">0){
    XStartDepo=XStartDepo+((OptionRate*ProfitPercent)/class="num">100);
    Profit++;
    }
    else{
    XStartDepo=XStartDepo-OptionRate;
    Loss++;
    }
  Comment("Depo = ",XStartDepo," Profit = ",Profit," Loss = ",Loss);
  ClosePositions(symbolS1.Name(), Magic, POSITION_TYPE_SELL, EAComment);
  }
class=class="str">"cmt">//Buy(Call)
if((ind_In1S1[class="num">1]<ind_In2S1[class="num">1]) && (ind_In1S1[class="num">2]<ind_In2S1[class="num">2])){TrigerBuy=class="num">1;}
  if ((TrigerBuy==class="num">1) && (CalculatePositions(symbolS1.Name(), Magic, POSITION_TYPE_BUY, EAComment)==class="num">0) && (ind_In1S1[class="num">1]>ind_In2S1[class="num">1]) && (ind_In1S1[class="num">0]>ind_In2S1[class="num">0]) && (ind_In4S1[class="num">1]>class="num">50) && (ind_In4S1[class="num">0]>class="num">50) && (ind_In3S1_1[class="num">1]>ind_In3S1_2[class="num">1])){
  OpenBuy(symbolS1.Name(), class="num">0.01, class="num">0, class="num">0, EAComment);
  TrigerBuy=class="num">0;
  }

常见问题

建议先用小额本金(如 1000 单位)跑 1 分钟~1 小时多周期对照,时间窗取重叠段避免未来函数,看净值曲线是否平滑再放大。
用持仓遍历取开仓价与当前价差算浮盈,达到预设阈值就调用平仓函数,把利润判断塞进 OnTick 而非挂单里更稳。
小布可读取你的回测落盘文件,标出回撤超阈值的周期组合并给预警,你直接按提示砍掉高危轨道就行。
开文件用 FILE_APPEND 模式,把净值、胜率、周期参数格式化后 WriteString 到尾行,下次跑自动接在后面。
别直接改手数倍率,先断掉模板里的固定间距,改用波动率动态间距,否则横盘会连亏到强平。