如何在莫斯科交易所安全地使用您的 EA 进行交易·综合运用
🛡️

如何在莫斯科交易所安全地使用您的 EA 进行交易·综合运用

(3/3)·从价格缺口到休眠模式,把技术风险关进笼子再让 EA 上场

含代码示例 第 3/3 篇
很多人以为 EA 跑通回测就能直接挂莫斯科交易所,结果被一次价格尖刺洗掉半月利润。离散流动和流动性真空不会提前打招呼,用限价单和休眠逻辑先把意外挡在外面,比调参数更重要。

点差采集与柱线初始化的底层写法

这段逻辑把点差拆成了开、高、低三档来记录,而不是只存一个瞬时值。GetSpread() 用 SYMBOL_ASK 减 SYMBOL_BID 再除以 Point(),最后 NormalizeDouble 到整数位,返回的是以「点」为单位的点差点数。 InitNewBar() 在新柱到来时把 spread_open 设为当前点差,spread_high 归零、spread_low 置为 DBL_MAX,相当于给后续逐 tick 刷新高低区间留好坑。avrg_current 和 count 清零,说明均值统计是每根柱独立计算的。 EA 侧通过 iCustom 挂了一个名为「Spread Record」的自定义指标,句柄存进 h_spread_record;MaxSpread 输入参数默认 9,意味着只处理点差不超过 9 点的行情——外汇与贵金属点差受流动性影响大,实盘里这个阈值可能要按品种重调。 开 MT5 把这段贴进 EA,改 MaxSpread 到对应品种常态点差上限,就能直接验证新柱初始化是否如预期清零。

MQL5 / C++
class="type">class="kw">double GetSpread()
  {
   class="type">class="kw">double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
   class="type">class="kw">double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
   class="kw">return NormalizeDouble((ask-bid)/Point(), class="num">0);
  }
class="type">void InitNewBar(class="type">int index)
  {
   spread_open[index] = GetSpread();
   spread_high[index] = class="num">0.0;
   spread_low[index]=DBL_MAX;
   avrg_current=class="num">0.0;
   count=class="num">0;
  }
class="macro">#define OPEN  class="num">0
class="macro">#define HIGH  class="num">1
class="macro">#define LOW   class="num">2
class="macro">#define CLOSE class="num">3
class="macro">#define AVRG  class="num">4
input class="type">int MaxSpread=class="num">9;
class="type">int h_spread_record=INVALID_HANDLE;
class="type">bool print_disable = class="kw">false;
class="type">int OnInit()
  {
   h_spread_record=iCustom(Symbol(),Period(),"Spread Record");
   class="kw">return(INIT_SUCCEEDED);
  }

◍ 用点差阈值卡掉无序行情

EA 在 OnTick 里第一道闸不是信号,而是点差过滤。IsTradeDisable(MaxSpread) 返回 true 就直接 return,后续交易逻辑根本不跑,相当于把流动性差的瞬间挡在门外。 核心判断在 IsTradeDisable:先用 CopyBuffer 取点差指标的最新收盘价,若取不到或句柄失效则返回 false(不拦交易);只有 close[0] 大于 MaxSpread 才置 print_disable 并禁交易。日志里 2015.05.27 16:57:08 这两行,允许和禁止相隔仅 20 毫秒,说明点差在秒级内穿越阈值,自动化拦截比手动快得多。 OnDeinit 里一句 IndicatorRelease(h_spread_record) 别漏,指标句柄不释放会在反复加载脚本时堆残留。外汇和贵金属点差跳变频繁,这类阈值过滤只能降低风险,不保证避开所有滑点。

MQL5 / C++
class=class="str">"cmt">//| 程序逆初函数                                                                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
   IndicatorRelease(h_spread_record);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 程序分时价函数                                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   if(IsTradeDisable(MaxSpread))class="kw">return;
   class=class="str">"cmt">//
   class=class="str">"cmt">// 交易逻辑...
   class=class="str">"cmt">//
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 如果禁止交易返回 true, 否则返回 class="kw">false                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsTradeDisable(class="type">int max_spread)
  {
   if(h_spread_record==INVALID_HANDLE)
      class="kw">return class="kw">false;
   class="type">class="kw">double close[];
   if(CopyBuffer(h_spread_record, CLOSE, class="num">0, class="num">1, close) < class="num">1)class="kw">return class="kw">false;
   if(close[class="num">0]>MaxSpread)
     {
      if(!print_disable)
         printf("交易禁止");
      print_disable=true;
      class="kw">return true;
     }
   if(print_disable)
      printf("交易允许");
   print_disable=class="kw">false;
   class="kw">return class="kw">false;
  }
class="num">2015.05.class="num">27 class="num">16:class="num">57:class="num">08.238 SpreadRecordControl(Si-class="num">6.15,H1)    交易允许
class="num">2015.05.class="num">27 class="num">16:class="num">57:class="num">08.218 SpreadRecordControl(Si-class="num">6.15,H1)    交易禁止
class="num">2015.05.class="num">27 class="num">16:class="num">56:class="num">49.411 SpreadRecordControl(Si-class="num">6.15,H1)    交易允许
class="num">2015.05.class="num">27 class="num">16:class="num">56:class="num">49.401 SpreadRecordControl(Si-class="num">6.15,H1)    交易禁止
class="num">2015.05.class="num">27 class="num">16:class="num">56:class="num">36.478 SpreadRecordControl(Si-class="num">6.15,H1)    交易允许
class="num">2015.05.class="num">27 class="num">16:class="num">56:class="num">36.452 SpreadRecordControl(Si-class="num">6.15,H1)    交易禁止

「用周期轮询躲开价格尖刺的止损坑」

市场报价看似连续,实则常出现离散跳空。以 Si-6.15 RUB/USD 期货在 5 月 28 日 10:03(莫斯科时间)的尖刺为例:多头仓位止损设在 52 740,理论亏损 300 点,但真实触发价落在 52 493,实际亏损 547 卢布/合约,比预设多吞了 247 点。若 EA 每分钟而非每笔分时价检查一次止损,这根尖刺会被直接忽略,仓位反而可能盈利收场。 外汇与贵金属自带高杠杆高风险,1:10 杠杆下若遇期货合约 5% 的价格限制,止损被执行时本金可能直接腰斩 50%。所以把交易逻辑「退敏」成固定周期轮询,不是模糊信号,而是避开低流动性下的假突破刺穿。 均线交叉 EA 是典型落地方案:快 MA 上穿慢 MA 买入、下穿卖出。但当前柱的 MA 随收盘价重绘会反复穿越,导致原地翻转。代码里只在「新柱线来临」后检查前一根已闭合柱线的均线状态,最后一根柱的重绘就不会干扰决策。 别把每笔分时价测试当圣经 MT5 策略测试器的「仅用开盘价」模式常被低估,其实它比「每笔分时价」更贴近实盘。后者用分时价发生器模拟 M1 内走势,无法还原价格缺口,容易养出测试里暴赚、实盘崩盘的「圣杯」。2015 年 5 月 25 日 19:00 SBRF-6.15 期货一分钟从 7473 拉到 7530,挂单在 7485 测试器里漂亮成交,真实分时图却显示缺口剧烈,实盘成交区在 7510–7520。 低流动性市场做回测,认准完整柱线或「1 分钟 OHLC」模式,别用每笔分时价激活停止单。大数法则下,理论入场价与实际的偏差只是噪声,差价常被其他正向因素抹平,结果取决于策略本身而非那几个点。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 MovingAverage.mq5 |
class=class="str">"cmt">//|                         Copyright class="num">2015, Vasiliy Sokolov. |
class=class="str">"cmt">//|                                        [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#include <Trade\Trade.mqh>
input class="type">int FastMAPeriod = class="num">10;      class=class="str">"cmt">// 快速 MA 周期
input class="type">int SlowMAPeriod = class="num">20;      class=class="str">"cmt">// 慢速 MA 周期
input class="type">class="kw">double Volume = class="num">1.0;        class=class="str">"cmt">// 交易量
class="type">int FastMA = INVALID_HANDLE;      class=class="str">"cmt">// 快速 MA 指标的句柄。

双均线穿越的句柄与触发骨架

EA 启动时先把快、慢两条 SMA 的句柄拿到手,周期取当前图表 Period(),均线周期分别由 FastMAPeriod 与 SlowMAPeriod 控制,平滑方式 MODE_SMA、应用价 PRICE_CLOSE、偏移 1。若任一句柄返回 POINTER_INVALID,直接 INIT_FAILED 退出,避免在 OnTick 里对着空句柄 CopyBuffer 爆错。 OnDeinit 里用 IndicatorRelease 释放两个句柄,MT5 不会自动回收指标资源,漏掉这一句在反复加载脚本时可能堆积句柄。 OnTick 的逻辑很直白:先 NewBarDetect() 拦掉旧 Bar 的重复触发,再用 CrossOver / CrossUnder 判定穿越方向,然后 Trade.Buy / Trade.Sell 按 GetVolume() 手数下单。注意 CrossOver 里 CopyBuffer 从索引 1 取 2 根(即上一根与当前根),判定条件是 fast_ma[1]>slow_ma[1] 且 fast_ma[0]<slow_ma[0],这是「死叉」而非金叉——原文函数名与注释存在错位,实盘前需把命名和逻辑对齐。 外汇与贵金属杠杆高,均线穿越在震荡市会连续假突破,这段代码只是信号骨架,必须自加滑点、点差与净值校验后再上 MT5 回测。

MQL5 / C++
class="type">int SlowMA = INVALID_HANDLE;    class=class="str">"cmt">// 慢速 MA 指标的句柄。
class="type">class="kw">datetime TimeLastBar;
CTrade Trade;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 程序初始化函数                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   FastMA = iMA(Symbol(), Period(), FastMAPeriod, MODE_SMA, class="num">1, PRICE_CLOSE);
   SlowMA = iMA(Symbol(), Period(), SlowMAPeriod, MODE_SMA, class="num">1, PRICE_CLOSE);
   if(FastMA==POINTER_INVALID || SlowMA==POINTER_INVALID)
     {
      printf("指标句柄未能创建");
      class="kw">return(INIT_FAILED);
     }
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 程序逆初函数                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
   IndicatorRelease(FastMA);
   IndicatorRelease(SlowMA);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 程序分时价函数                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
   if(!NewBarDetect())class="kw">return;
   if(CrossOver())
      Trade.Buy(GetVolume());
   else if(CrossUnder())
      Trade.Sell(GetVolume());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回 true 如果快速 ma 穿越慢速 ma 向上。否则返回 class="kw">false。            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CrossOver()
  {
   class="type">class="kw">double fast_ma[];
   class="type">class="kw">double slow_ma[];
   if(CopyBuffer(FastMA, class="num">0, class="num">1, class="num">2, fast_ma) < class="num">1)class="kw">return class="kw">false;
   if(CopyBuffer(SlowMA, class="num">0, class="num">1, class="num">2, slow_ma) < class="num">1)class="kw">return class="kw">false;
   class="type">bool is_over=fast_ma[class="num">1]>slow_ma[class="num">1] && fast_ma[class="num">0]<slow_ma[class="num">0];
   class="kw">return is_over;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回 true 如果快速 ma 穿越慢速 ma 向下。否则返回 class="kw">false。            |
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 死叉判定与仓位自适应的底层函数

这一段给出三个可直接抄进 EA 的底层函数,解决「怎么识别快线向下穿越慢线」「怎么按持仓状态翻倍手数」「怎么只在新 K 线跑逻辑」三个具体问题。外汇与贵金属波动剧烈,这类自动判定只是信号触发器,实际成交仍受滑点与点差影响,存在较高风险。 CrossUnder 取最近两根 K 线的快慢均线值:fast_ma[0] 在慢线上方、fast_ma[1] 在慢线下方,即判定为向下穿越。CopyBuffer 若取不够 2 个值直接返回 false,避免数组越界。 GetVolume 用 PositionSelect 检查当前品种是否已有持仓,有则 Volume*2.0,无则原量输出。这是典型的「加仓倍投」写法,回测中易放大回撤,实盘需谨慎。 NewBarDetect 靠 CopyTime 拿最新柱时间,与模块级变量 TimeLastBar 比对,不同才置新并返 true。OnTick 里首行就拦掉非新柱的 tick,把运算量压到每根 K 线一次而不是每次报价。

MQL5 / C++
class="type">bool CrossUnder()
  {
   class="type">class="kw">double fast_ma[];
   class="type">class="kw">double slow_ma[];
   if(CopyBuffer(FastMA, class="num">0, class="num">1, class="num">2, fast_ma) < class="num">1)class="kw">return class="kw">false;
   if(CopyBuffer(SlowMA, class="num">0, class="num">1, class="num">2, slow_ma) < class="num">1)class="kw">return class="kw">false;
   class="type">bool is_under=fast_ma[class="num">0]>slow_ma[class="num">0] && fast_ma[class="num">1]<slow_ma[class="num">1];
   class="kw">return is_under;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回交易量计数。                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double GetVolume()
  {
   if(PositionSelect(Symbol()))class="kw">return Volume*class="num">2.0;
   class="kw">return Volume;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 返回 true 如果检测到新柱线, 负责返回 class="kw">false。                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool NewBarDetect()
  {
   class="type">class="kw">datetime times[];
   if(CopyTime(Symbol(),Period(),class="num">0,class="num">1,times)<class="num">1)
      class="kw">return class="kw">false;
   if(times[class="num">0] == TimeLastBar)class="kw">return class="kw">false;
   TimeLastBar = times[class="num">0];
   class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
   if(!NewBarDetect())class="kw">return;
   ...
}

「别急着下结论」

把前面几节拆开看,核心其实就一句:价格图表是离散成交的压缩包,柱线没走完之前你看到的只是近似。低流动性品种里这种离散会被放大,限价单和停止限价单是绕开滑点最实在的手段,高位限价买、低位限价卖还能在终端离线时替你管住最大点差。 市场深度和实时点差是两个能摸流动性的窗口,点差一扩就该等,而不是硬吃。EA 用「休眠」模式只在新柱抵达时判一次逻辑,既抗尖刺又兼容完整柱线回测;而完整柱线模式因为吃的是离散且真实的历史数据,才是低流动性市场唯一靠谱的测试选项,速度也不慢。 这些结论都不是圣旨,外汇和贵金属杠杆高、跳空频繁,离散和流动性随时变脸。开 MT5 把 DeviationPanel.mq5 拖上图表,先盯一周自己常做品种的点差分布,再决定挂单类型,比背任何总结都管用。

把盘口异常交给小布盯盘
小布盯盘的 AIGC 已内置点差突变与价格尖刺的预警视图,打开 MOEX 对应品种页就能看到实时流动性诊断,你只管决定 EA 要不要切休眠。

常见问题

交易所定价是离散过程,流动不足时会出现价格缺口和尖刺,市价止损单按跳空后成交,滑点可能远超策略假设,用限价类订单替代可约束最大成交价。
可以,小布盯盘的品种页内置了点差与流动性诊断,开仓前先看预警再决定是否让 EA 发单,相当于加了道人工可干预的过滤器。
在 OnTick 里判断持仓方向后挂对应限价单并设有效期,配合交易执行模式的最大滑点参数,避免用止损单直接接跳空,示例见原文 1.8 节代码。
分时价测试会放大微观噪声让历史表现失真,完整柱线模式只在收盘后评估信号,更贴近真实交易所延迟与离散成交环境,回测结论更可靠。
两者可并行:SpreadRecord 指标做入场过滤,EA 内读取点差阈值暂停发单,极端时人工切休眠模式,避免流动性断层期的被动成交。