在 MetaTrader 5 中测试和优化二元期权策略·进阶篇
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 线核对信号锁是否漏触发。
<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>; <span class="keyword">if</span>(symb==<span class="class="type">class="kw">string">"class="num">0"</span>) { symb=<span class="functions">Symbol</span>();} <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>=<span class="number">class="num">0</span>;i--){ <span class="keyword">if</span>(position.SelectByIndex(i)) { <span class="keyword">if</span>(position.PositionType()==<span class="macro">POSITION_TYPE_BUY</span> || position.PositionType()==<span class="macro">POSITION_TYPE_SELL</span>) { <span class="keyword">if</span>((position.<span class="functions">Symbol</span>()==symb||symb==<span class="class="type">class="kw">string">""</span>)&&(type<<span class="number">class="num">0</span>||position.PositionType()==type)&&(mg<<span class="number">class="num">0</span>||position.Magic()==mg)) { c++; <span class="functions">ArrayResize</span>(p, c+<span class="number">class="num">1</span>); p[c]=position.Time(); pr=c>=index?index:c; }}}}}} <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_BUY | position.PositionType()==POSITION_TYPE_SELL) { | ||
|---|---|---|---|
| if((position.Symbol()==symb | symb=="")&&(type<0 | position.PositionType()==type)&&(mg<0 | position.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 验证。
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 的分布,外汇与贵金属杠杆品种回测误差大,仅作概率参考。
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 看信号逻辑。 别把经纪商策略当圣杯 回测连亏的笔数普遍高于盈利笔数,真上实盘前至少把到期时间和点差模型在策略测试器里重跑一遍。
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; }