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

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

(2/3)· 从价格缺口到休眠模式,六节实操拆解让 EA 避开尖刺与流动性黑洞

新手友好 第 2/3 篇
很多人以为 EA 只要逻辑对就能稳跑,却忽略交易所价格离散带来的瞬间缺口。限价单设错一档,滑点就可能吞掉半月利润。先把执行层的防御补齐,再谈策略优化。

◍ 面板对象的创建与事件响应拆解

在 MT5 自建交易面板时,先用 ObjectCreate 以 OBJ_LABEL 类型建出说明标签,再靠 OBJPROP_XDISTANCE / YDISTANCE 把文字推到距图表左上角 12 与 20 像素的位置,XSIZE 给到 80 保证显示宽度,背景刷白、字色黑,最后 ChartRedraw 一次性重绘。 销毁阶段在析构函数里逐个 ObjectDelete,把买/卖按钮、背景框、偏离值编辑框和描述标签全清掉,避免切换周期或重载 EA 时残留图形对象。 事件入口 OnChartEvent 只认两种系统事件:CHARTEVENT_OBJECT_CLICK 走 OnObjClick,CHARTEVENT_OBJECT_ENDEDIT 走 OnEndEdit;其余图表事件直接忽略。 OnEndEdit 里先比对 sparam 是否等于偏离值编辑框名,不等就 return。接着把编辑框文本强转 int,若 ≤0 则回写默认 m_deviation,等于给手动输错做了道硬保险。

MQL5 / C++
class=class="str">"cmt">//--- 描述
   ObjectCreate(class="num">0,m_descr_dev,OBJ_LABEL,class="num">0,class="num">0,class="num">0);
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_XDISTANCE,class="num">12);
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_YDISTANCE,class="num">20);
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_XSIZE,class="num">80);
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_BGCOLOR,clrWhite);
   ObjectSetString(class="num">0,m_descr_dev,OBJPROP_TEXT,"Deviation(pips):");
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_COLOR,clrBlack);
   ChartRedraw();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CDevPanel::~CDevPanel(class="type">void)
  {
   ObjectDelete(class="num">0,m_buy_button_name);
   ObjectDelete(class="num">0,m_sell_button_name);
   ObjectDelete(class="num">0,m_bg_fon);
   ObjectDelete(class="num">0,m_deviation_name);
   ObjectDelete(class="num">0,m_descr_dev);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CDevPanel::OnChartEvent(const class="type">int id,
                            const class="type">long &lparam,
                            const class="type">class="kw">double &dparam,
                            const class="type">class="kw">string &sparam)
  {
   class="kw">switch(id)
     {
      case CHARTEVENT_OBJECT_CLICK:
         OnObjClick(sparam);
         class="kw">break;
      case CHARTEVENT_OBJECT_ENDEDIT:
         OnEndEdit(sparam);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 编辑检测结束                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CDevPanel::OnEndEdit(class="type">class="kw">string sparam)
  {
   if(sparam != m_deviation_name)class="kw">return;
   class="type">int value = (class="type">int)ObjectGetString(class="num">0, m_deviation_name, OBJPROP_TEXT);
   if(value <= class="num">0)
      ObjectSetString(class="num">0,m_deviation_name,OBJPROP_TEXT,(class="type">class="kw">string)m_deviation);
代码逐行:第1行注释标记“描述”区块;ObjectCreate 建标签对象,坐标参数 0,0,0 表示依赖后续距离属性定位;三句 OBJPROP_XDISTANCE/YDISTANCE/XSIZE 分别设水平 12px、垂直 20px、宽 80px;BGCOLOR 设 clrWhite、TEXT 写“Deviation (pips):”、COLOR 设 clrBlack;ChartRedraw 提交绘制。 析构函数中 5 个 ObjectDelete 按按钮、背景、偏离框、描述顺序清理;OnChartEvent 用 switch 分发点击事件与编辑结束事件;OnEndEdit 先过滤对象名,再读文本转 int,≤0 时回退 m_deviation 默认值。外汇与贵金属波动剧烈,面板参数校验虽能挡错输,但实盘仍属高风险,开 MT5 跑一遍最能验证这套逻辑。

MQL5 / C++
class=class="str">"cmt">//--- 描述
   ObjectCreate(class="num">0,m_descr_dev,OBJ_LABEL,class="num">0,class="num">0,class="num">0);
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_XDISTANCE,class="num">12);
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_YDISTANCE,class="num">20);
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_XSIZE,class="num">80);
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_BGCOLOR,clrWhite);
   ObjectSetString(class="num">0,m_descr_dev,OBJPROP_TEXT,"Deviation(pips):");
   ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_COLOR,clrBlack);
   ChartRedraw();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
CDevPanel::~CDevPanel(class="type">void)
  {
   ObjectDelete(class="num">0,m_buy_button_name);
   ObjectDelete(class="num">0,m_sell_button_name);
   ObjectDelete(class="num">0,m_bg_fon);
   ObjectDelete(class="num">0,m_deviation_name);
   ObjectDelete(class="num">0,m_descr_dev);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 事件函数                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CDevPanel::OnChartEvent(const class="type">int id,
                            const class="type">long &lparam,
                            const class="type">class="kw">double &dparam,
                            const class="type">class="kw">string &sparam)
  {
   class="kw">switch(id)
     {
      case CHARTEVENT_OBJECT_CLICK:
         OnObjClick(sparam);
         class="kw">break;
      case CHARTEVENT_OBJECT_ENDEDIT:
         OnEndEdit(sparam);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 编辑检测结束                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CDevPanel::OnEndEdit(class="type">class="kw">string sparam)
  {
   if(sparam != m_deviation_name)class="kw">return;
   class="type">int value = (class="type">int)ObjectGetString(class="num">0, m_deviation_name, OBJPROP_TEXT);
   if(value <= class="num">0)
      ObjectSetString(class="num">0,m_deviation_name,OBJPROP_TEXT,(class="type">class="kw">string)m_deviation);

「挂单按钮背后的价位与报错陷阱」

面板上的买/卖限价按钮点击后,直接调用 CalcCurrentPrice 算挂单价:买限价在 ask 之上加 m_deviation 个点,卖限价在 bid 之下减 m_deviation 个点。m_deviation 由前面滑块或输入决定,Point() 取当前品种最小变动单位,EURUSD 通常是 1e-5。 早期版本只管发单,不校验返回。2015.04.15 14:08:39 实盘日志显示:buy limit 0.10 EURUSD at 1.05927 被拒,报错 [Invalid price]——限价单价格落在可成交范围外,经纪商直接踢回。外汇和贵金属杠杆高,这种无效价挂单若批量触发,可能连续吃拒单占用执行资源。 修正写法把 Trade.BuyLimit / SellLimit 包进 if,成功才补 SendStopLoss。这样挂单失败就不会误挂止损,逻辑上更稳。

MQL5 / C++
class="type">void CDevPanel::OnObjClick(class="type">class="kw">string sparam)
  {
  if(sparam==m_buy_button_name)
    {
    if(Trade.BuyLimit(class="num">1,CalcCurrentPrice(OP_BUY)))
       SendStopLoss(OP_BUY);
    }
  if(sparam==m_sell_button_name)
    {
    if(Trade.SellLimit(class="num">1,CalcCurrentPrice(OP_SELL)))
       SendStopLoss(OP_SELL);
    }
  ObjectSetInteger(class="num">0,sparam,OBJPROP_STATE,class="kw">false);
  Sleep(class="num">100);
  ChartRedraw();
  }
CalcCurrentPrice 的逐行要点:OP_BUY 分支取 SYMBOL_ASK 加偏差,OP_SELL 取 SYMBOL_BID 减偏差,其余情况返回 0.0 让上层判断。开 MT5 把 m_deviation 调到比点差还小,就能复现 Invalid price 的拒单现象。

MQL5 / C++
class="type">void CDevPanel::OnObjClick(class="type">class="kw">string sparam)
  {
  if(sparam==m_buy_button_name)
    {
    if(Trade.BuyLimit(class="num">1,CalcCurrentPrice(OP_BUY)))
       SendStopLoss(OP_BUY);
    }
  if(sparam==m_sell_button_name)
    {
    if(Trade.SellLimit(class="num">1,CalcCurrentPrice(OP_SELL)))
       SendStopLoss(OP_SELL);
    }
  ObjectSetInteger(class="num">0,sparam,OBJPROP_STATE,class="kw">false);
  Sleep(class="num">100);
  ChartRedraw();
  }

用挂单把止损变成被动触发

在 MT5 里给持仓挂止损,不一定非要用 Modify 去改已开仓的 SL。上面这段逻辑走的是另一条路:当多单持仓需要保护时,直接下一笔 SELL_STOP_LIMIT 挂单,价格落在 m_sl_level,stoplimit 再往下偏 m_deviation*Point(),相当于市价一旦跌破止损线就先限价追单平仓。 空单反过来,ask 必须小于 m_sl_level 才允许挂 BUY_STOP_LIMIT,stoplimit 向上偏同样幅度。两个分支都写死 volume=1.0、type_time=ORDER_TIME_DAY,意味着当天有效、不隔夜挂。 OrderSend 返回 bool,失败就 Print 出 GetLastError 码,调用方拿 res 判断是否真正挂上。外汇和贵金属杠杆高,这类自动挂单逻辑若 deviation 设太小,在跳空行情可能不触发,实盘前务必在策略测试器用历史数据跑一遍。

MQL5 / C++
      {
       class="type">MqlTradeRequest request={class="num">0};
       request.action = TRADE_ACTION_PENDING;
       request.symbol = Symbol();
       request.volume = class="num">1.0;
       request.price=m_sl_level;
       request.stoplimit=m_sl_level -(m_deviation*Point());
       request.type=ORDER_TYPE_SELL_STOP_LIMIT;
       request.type_filling=ORDER_FILLING_RETURN;
       request.type_time=ORDER_TIME_DAY;
       class="type">MqlTradeResult result;
       class="type">bool res=OrderSend(request,result);
       if(!res)
         Print("设置 S/L 错误。原因: "+(class="type">class="kw">string)GetLastError());
       class="kw">return res;
      }
   }
   else if(op_type==OP_SELL)
   {
    class="type">class="kw">double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
    if(m_sl_level>=class="num">0.0 && m_sl_level>ask)
     {
      class="type">MqlTradeRequest request={class="num">0};
      request.action = TRADE_ACTION_PENDING;
      request.symbol = Symbol();
      request.volume = class="num">1.0;
      request.price=m_sl_level;
      request.stoplimit=m_sl_level+(m_deviation*Point());
      request.type=ORDER_TYPE_BUY_STOP_LIMIT;
      request.type_filling=ORDER_FILLING_RETURN;
      request.type_time=ORDER_TIME_DAY;
      class="type">MqlTradeResult result;
      class="type">bool res=OrderSend(request,result);
      if(!res)
        Print("设置 S/L 错误。原因: "+(class="type">class="kw">string)GetLastError());
      class="kw">return res;
     }
    if(CharToStr(StringGetChar(data,strlen-class="num">1))==&class="macro">#x27;.&class="macro">#x27;)
      StringSetChar(data,strlen-class="num">1,&class="macro">#x27;&class="macro">#x27;);
   }
   class="kw">return class="kw">false;
}

◍ 入场前先算清滑点和点差

中央集中式市场(如莫斯科交易所期货)提供可见的市场深度,限价单堆叠出当前价附近的流动性。想吃的量越大,越要穿透更远的价位,滑点随之产生。以 Si-6.15 美元/卢布期货为例:买 2 份合约在最优卖价 51931 成交、零滑点;买 4 份时均价变成 (2*51931+2*51932)/4 = 51931.5,滑点 0.5 点;买 25 份均价 51934.5,滑点扩大到 3.5 点。 手工按深度表自下往上加总可预估滑点,但盘口瞬变,必须自动化。CMarketBook 类的 GetDeviationByVol 方法从最优价遍历深度,累计成交量达到需求即算加权均价,与最优价之差就是预检滑点;若深度不够则返回 -1.0 表示流动性不足。实盘面板测试 RTS-6.15:交易量 1 手按钮显示 0 点;100 手买卖滑点升至 10–20 点;500 手约 60–80 点;1500 手时买侧报 1.0(无法计算),卖侧仍有 100–130 点滑点。外汇和贵金属虽多数为场外报价,但大单冲击逻辑类似,高杠杆下滑点可能吞掉止损空间,属高风险情形。 点差是买卖最优价之差,与流动性负相关。SpreadRecord 指标在副图画出每分钟开盘、最大、最小、收盘及平均点差,并用绿点标平均线。Si-6.15 分钟图上,点差在 1–21 点间波动,均值约 3 点,最窄曾触 1 点。 把点差当过滤器更实用:同一品种多数时间落在 1–9 点“绿色”区可交易,突破 9 点进入“红色”区应禁手。EA 端用 iCustom 调 SpreadRecord,CopyBuffer 取数后由 IsTradeDisable 比 MaxSpread 阈值,超限即锁单并返回“交易禁止”。这样在流动性骤降、点差爆宽时,人工和机器都能自动收手。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 通过交易量获取偏差值。返回 -class="num">1.0 如果偏离是无穷大 (流动性不足)    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CMarketBook::GetDeviationByVol(class="type">long vol,ENUM_MBOOK_SIDE side)
  {
   class="type">int best_ask = InfoGetInteger(MBOOK_BEST_ASK_INDEX);
   class="type">int last_ask = InfoGetInteger(MBOOK_LAST_ASK_INDEX);
   class="type">int best_bid = InfoGetInteger(MBOOK_BEST_BID_INDEX);
   class="type">int last_bid = InfoGetInteger(MBOOK_LAST_BID_INDEX);
   class="type">class="kw">double avrg_price=class="num">0.0;
   class="type">long volume_exe=vol;
   if(side==MBOOK_ASK)
     {
      for(class="type">int i=best_ask; i>=last_ask; i--)
        {
         class="type">long currVol=MarketBook[i].volume<volume_exe ?
                            MarketBook[i].volume : volume_exe;
         avrg_price += currVol * MarketBook[i].price;
         volume_exe -= MarketBook[i].volume;
         if(volume_exe<=class="num">0)class="kw">break;
        }

「吃单均价偏离与独立点差记录器」

上面这段逻辑干的事很直接:在卖单簿里从最优买价向更差报价遍历,按委托量加权累出实际吃单均价,再用它和最优报价比偏差。若 volume_exe 还没耗尽就跳出循环,说明盘口薄到不够吃,直接返回 -1.0 表示失效。 deviation 的计算分两边:买侧用 avrg_price 减 best_ask 价,卖侧用 best_bid 价减 avrg_price,返回的就是你真实冲击成本的距离。外汇与贵金属盘口在新闻秒级常抽空,这种返回 -1.0 的情况概率会陡升,属正常高风险现象。 下面挂的指示器把点差当独立品种画出来:用 5 个 buffer(open/high/low/close/avrg)在副窗画 DRAW_BARS 加 DRAW_ARROW,IndicatorSetInteger 把小数位锁成 1 位。开 MT5 把这段拷进自定义指标,切到 XAUUSD 的 M1,你能直接看到点差柱在财报秒的拉伸幅度常常突破日常 3~5 倍。

MQL5 / C++
   }
  else
   {
     for(class="type">int i=best_bid; i<=last_bid; i++)
      {
       class="type">long currVol=MarketBook[i].volume<volume_exe ?
                            MarketBook[i].volume : volume_exe;
       avrg_price += currVol * MarketBook[i].price;
       volume_exe -= MarketBook[i].volume;
       if(volume_exe<=class="num">0)class="kw">break;
      }
   }
   if(volume_exe>class="num">0)
      class="kw">return -class="num">1.0;
   avrg_price/=(class="type">class="kw">double)vol;
   class="type">class="kw">double deviation=class="num">0.0;
   if(side==MBOOK_ASK)
      deviation=avrg_price-MarketBook[best_ask].price;
   else
      deviation=MarketBook[best_bid].price-avrg_price;
   class="kw">return deviation;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Spread Record.mq4 |
class=class="str">"cmt">//|                                        Copyright class="num">2015, MetaQuotes Software Corp. |
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">#class="kw">property description "记录点差并显示它。"
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_buffers class="num">5
class="macro">#class="kw">property indicator_plots   class="num">5
class="macro">#class="kw">property indicator_type1   DRAW_BARS
class="macro">#class="kw">property indicator_type2   DRAW_ARROW
class="macro">#class="kw">property indicator_color1  clrBlack
class="macro">#class="kw">property indicator_color2  clrBlack
class="type">class="kw">double spread_open[];
class="type">class="kw">double spread_high[];
class="type">class="kw">double spread_low[];
class="type">class="kw">double spread_close[];
class="type">class="kw">double spread_avrg[];
class="type">int elements;
class="type">class="kw">double avrg_current;
class="type">int count;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓存区映射
   SetIndexBuffer(class="num">0,spread_open,INDICATOR_DATA);
   SetIndexBuffer(class="num">1,spread_high,INDICATOR_DATA);
   SetIndexBuffer(class="num">2,spread_low,INDICATOR_DATA);
   SetIndexBuffer(class="num">3,spread_close,INDICATOR_DATA);
   SetIndexBuffer(class="num">4,spread_avrg,INDICATOR_DATA);
   IndicatorSetInteger(INDICATOR_DIGITS,class="num">1);

点差通道的实时刷新与绘图收尾

指标初始化末尾用 PlotIndexSetInteger 把第二画图的箭头字符锁成 0x9f,主线和箭头分别染红、绿,肉眼区分点差高低边界。 回测 EURUSD 跳空时段常见点差瞬间冲到 30 点以上,这时 spread_high 数组会实时接住峰值,而 spread_low 只在 d 更小才更新,所以上下轨不会因单帧毛刺来回抖。 OnCalculate 里 prev_calculated==0 时先 ArrayInitialize 五个缓冲区填 EMPTY_VALUE,避免首根 K 线画出 0 值假线;之后每来一根新柱走 InitNewBar 重置局部累加器。 最后一段只扫 i=rates_total-1 这一根,把当前点差 d 写进 close 与滚动均值 spread_avrg,外汇和贵金属点差受流动性影响大,高风险品种建议先在策略测试器跑一周再上实盘。

MQL5 / C++
PlotIndexSetInteger(class="num">1,PLOT_ARROW,0x9f);
PlotIndexSetInteger(class="num">0,PLOT_LINE_COLOR,clrRed);
PlotIndexSetInteger(class="num">1,PLOT_LINE_COLOR,clrGreen);
class=class="str">"cmt">//---
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)
  {
   printf("DEINIT");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标迭代函数                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int &spread[])
  {
class=class="str">"cmt">//---
   if(prev_calculated==class="num">0)
     {
      printf("INITIALIZE INDICATORS "+TimeToString(TimeCurrent()));
      class="type">class="kw">double init_value=EMPTY_VALUE;
      ArrayInitialize(spread_high,init_value);
      ArrayInitialize(spread_low,init_value);
      ArrayInitialize(spread_open,init_value);
      ArrayInitialize(spread_close,init_value);
      ArrayInitialize(spread_avrg,init_value);
      elements=ArraySize(spread_high);
      InitNewBar(elements-class="num">1);
     }
class=class="str">"cmt">//--- 新柱线初始化
   for(; elements<ArraySize(spread_high); elements++)
      InitNewBar(elements);
   class="type">class="kw">double d=GetSpread();
   for(class="type">int i=rates_total-class="num">1; i<rates_total; i++)
     {
      if(d>spread_high[i])
         spread_high[i]=d;
      if(d<spread_low[i])
         spread_low[i]= d;
      spread_close[i] = d;
      avrg_current+=d;
      count++;
      spread_avrg[i]=avrg_current/count;
把盘口异常交给小布盯盘
这些点差骤扩与价格尖刺的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时预警,你只管判断要不要撤单。

常见问题

交易所定价是离散撮合,流动性断层时会出现跳空而非连续滑点,EA 若用市价单追价容易在缺口处成交在极不利价位。
在触发条件满足时发送 buy limit 于当前阻力上方而非市价止损,成交后反向平仓,可规避尖刺扫损。
建议取该品种近 20 日分位 80% 点差为上界,超过则暂停 EA 开仓,具体因品种波动率而异。
可以,小布盯盘的品种页会标注非主流时段的点差与挂单厚度变化,辅助你决定是否切到休眠模式。
分时价回测会掩盖离散成交的真实滑点,完整柱线模式保留开盘高低收,更接近实盘撮合逻辑。