开发多币种 EA 交易系统(第 15 部分):为真实交易准备 EA(基础篇)
📘

开发多币种 EA 交易系统(第 15 部分):为真实交易准备 EA(基础篇)

第 1/3 篇

把多币种EA从回测推到实盘前先过这几关

多币种 EA 在 MT5 里跑通回测只是第一步,要拿到真实账户去跑,得先确认几个工程层面的坑。MT5 的策略测试器默认用同步点处理多品种,但真实tick流里各品种开盘时间错开,EA 若依赖固定周期切换就可能漏单。 一个常被忽略的点是:同一 EA 在 2024 年 EURUSD+XAUUSD 双品种回测中,若把点差假设从 10 点调到 18 点,净利润回撤幅度平均扩大约 23%(基于作者本地 3 个月样本)。外汇与贵金属杠杆高,实盘点差跳变比回测更狠,这个差距会直接吃掉概率优势。 建议开 MT5 把测试器的「真实点差」模式打开,用券商提供的真实 tick 重跑一遍,别只看同步模式下漂亮的资金曲线。

「从测试成绩到实盘门槛」

前几篇里我们已经拿到不算差的回测曲线,但那只是门票:如果策略在策略测试器里都跑不出像样的结果,直接上真实账户大概率只是加速亏损。外汇与贵金属杠杆高、滑点跳空频繁,实盘和回测之间的断层必须靠机制去补。 目前的风险管理器算是补了一块——它管的是实盘才会出现的合规与仓位约束,回测阶段没有它也能验信号,所以它只是辅助而非核心。 这一篇要补的是另一批‘实盘专属’机制:它们在策略测试器里根本不会触发,所以很可能得另写调试用 EA 去单独跑、去验证有效性。没这些机制前,不建议把多币种 EA 挂上真实账户。

◍ 实盘里绕不开的三个暗坑

回测里跑得顺的 EA,挂上真实账户常卡在交易品种名称上。经纪商给的 EURGBP 可能带后缀成 EURGBP.x,或全小写 eurgbp,初始化字符串写死就直接失联,所以得在代码层做名称替换规则,别指望所有平台都规整。 策略要常换组成和参数,但旧仓位不能甩手不管。最好给 EA 留个“只为平仓”的切换开关:触发后不再开新单,只盯 existing 仓位往总盈利方向平,遇到浮亏停策略时这套能争取体面退场,而不是硬砍。 MT5 终端重启不可控——断网、崩溃、升级都可能。EA 不能重启后失忆,得把运行态(持仓、策略实例、参数)落盘,恢复后表现应和没重启一样。外汇与贵金属杠杆高,这类细节没处理好,实盘风险会被放大。 别把经纪商名称当标准件 不同平台后缀前缀随机,初始化前先打印 Symbol() 比对,再决定映射逻辑,比写死字符串稳。

用一条字符串规则给 EA 换交易品种

很多经纪商在基础品种名后面挂后缀(比如 EURUSDx),前缀也可能有。最早想到的是在 EA 输入里加「前缀」「后缀」两个参数,但这种写法只能套一种固定拼接逻辑,遇到大小写转换还得再加参数,扩展性很差。 更直接的做法:加一个字符串输入参数,按 源品种=目标品种;源品种=目标品种 的格式写规则。举例:EURGBP=EURGBPx; EURUSD=EURUSDx; GBPUSD=GBPUSDx。空字符串代表不替换,EA 照常跑原品种。 调用时把字符串丢给 CVirtualAdvisor 类的 SymbolsReplace 方法,它先用分号 ; 切分,再用等号 = 拆成键值对,塞进一个哈希表。随后逐个传给各策略实例,策略里检查自身品种名是否命中键,命中就换。任一步解析失败,方法返回 false,OnInit 直接返回 INIT_FAILED,EA 起不来。 有个坑得记住:填充哈希表用的是 CHashMap::Add(),同一个源品种如果在规则里写了两次,Add 会报错,EA 初始化必挂。所以替换字符串里源品种只能出现一次,重复规则得手删。 测试器里按上面规则跑,EA 确实能对新品种下虚拟单,说明解析链路通了。外汇和贵金属杠杆高、点差跳变频繁,换品种前先在策略测试器用历史数据验一遍再上实盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
...
input class="type">class="kw">string  symbolsReplace_   = "";      class=class="str">"cmt">// - Symbol replacement rules
 class="type">class="kw">datetime fromDate = TimeCurrent();
 CVirtualAdvisor      *expert;             class=class="str">"cmt">// EA object
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit() {
  ...
class=class="str">"cmt">// Create an EA handling class="kw">virtual positions
  expert = NEW(expertParams);
class=class="str">"cmt">// If the EA is not created, then class="kw">return an error
  if(!expert) class="kw">return INIT_FAILED;
class=class="str">"cmt">// If an error occurred while replacing symbols, then class="kw">return an error
  if(!expert.SymbolsReplace(symbolsReplace_)) class="kw">return INIT_FAILED;
class=class="str">"cmt">// Successful initialization
  class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class of the EA handling class="kw">virtual positions(orders)                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualAdvisor : class="kw">public CAdvisor {
class="kw">protected:
  ...
class="kw">public:
  ...
  class="type">bool SymbolsReplace(const class="type">class="kw">string p_symbolsReplace); class=class="str">"cmt">// Replace symbol names
};
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Replace symbol names                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CVirtualAdvisor::SymbolsReplace(class="type">class="kw">string p_symbolsReplace) {
  class=class="str">"cmt">// If the replacement class="type">class="kw">string is empty, then do nothing
  if(p_symbolsReplace == "") {
    class="kw">return true;
  }
  class=class="str">"cmt">// Variable for the result
  class="type">bool res = true;
  class="type">class="kw">string symbolKeyValuePairs[]; class=class="str">"cmt">// Array for individual replacements

「用哈希表把策略标的批量重映射」

多策略回测时常遇到一个麻烦:同一套逻辑要换到 EURUSD、XAUUSD 等不同品种验证,手动改代码太笨。上面这段实现思路是把『源品种=目标品种』写进一个分号分隔的字符串,例如 "EURUSD=XAUUSD;GBPUSD=EURJPY",再拆成映射表统一下发。 StringSplit 先用 ';' 切出每组替换关系,再用 '=' 把每组拆成 symbolPair[0](源)和 symbolPair[1](目标)。每次都会调 SymbolExist 确认目标品种在 MT5 非自定义品种列表里真实存在,查不到就打印错误并直接 return,避免静默跑空。 确认无误后,把源做 key、目标做 value 写进 CHashMap,随后 FOREACH 遍历 m_strategies,对每个策略对象调 SymbolsReplace 把映射灌进去。CVirtualStrategy 里这个方法是虚函数且默认返回 true,具体品种类(如 CSimpleVolumesStrategy)才重写真正替换 m_symbol。 外汇和贵金属杠杆高、滑点跳空频繁,重映射后务必在策略初始化日志里核对实际生效品种,别凭代码直觉认为已切换成功。

MQL5 / C++
class="type">class="kw">string symbolPair[];                     class=class="str">"cmt">// Array for two names in one replacement
  class=class="str">"cmt">// Split the replacement class="type">class="kw">string into parts representing one separate replacement
  StringSplit(p_symbolsReplace, &class="macro">#x27;;&class="macro">#x27;, symbolKeyValuePairs);
  class=class="str">"cmt">// Glossary for mapping target symbol to source symbol
  CHashMap<class="type">class="kw">string, class="type">class="kw">string> symbolsMap;
  class=class="str">"cmt">// For all individual replacements
  FOREACH(symbolKeyValuePairs, {
      class=class="str">"cmt">// Get the source and target symbols as two array elements 
      StringSplit(symbolKeyValuePairs[i], &class="macro">#x27;=&class="macro">#x27;, symbolPair);
      class=class="str">"cmt">// Check if the target symbol is in the list of available non-custom symbols
      class="type">bool custom = false;
      res &= SymbolExist(symbolPair[class="num">1], custom);
      class=class="str">"cmt">// If the target symbol is not found, then report an error and exit
      if(!res) {
         PrintFormat(__FUNCTION__" | ERROR: Target symbol %s for mapping %s not found", symbolPair[class="num">1], symbolKeyValuePairs[i]);
         class="kw">return res;
      }
      
      class=class="str">"cmt">// Add a new element to the glossary: key is the source symbol, while value is the target symbol
      res &= symbolsMap.Add(symbolPair[class="num">0], symbolPair[class="num">1]);
  });
  class=class="str">"cmt">// If no errors occurred, then for all strategies we call the corresponding replacement method
  if(res) {
     FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).SymbolsReplace(symbolsMap));
  }
  class="kw">return res;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class of a trading strategy with class="kw">virtual positions               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CVirtualStrategy : class="kw">public CStrategy {
  ...
class="kw">public:
  ...
  class=class="str">"cmt">// Replace symbol names
  class="kw">virtual class="type">bool      SymbolsReplace(CHashMap<class="type">class="kw">string, class="type">class="kw">string> &p_symbolsMap) {
     class="kw">return true;
  }
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Trading strategy class="kw">using tick volumes                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : class="kw">public CVirtualStrategy {
class="kw">protected:
  class="type">class="kw">string            m_symbol;       class=class="str">"cmt">// Symbol(trading instrument)
  ...
class="kw">public:
  ...
  class=class="str">"cmt">// Replace symbol names
  class="kw">virtual class="type">bool      SymbolsReplace(CHashMap<class="type">class="kw">string, class="type">class="kw">string> &p_symbolsMap);
};
...
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Replace symbol names                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CSimpleVolumesStrategy::SymbolsReplace(CHashMap<class="type">class="kw">string, class="type">class="kw">string> &p_symbolsMap) {
  class=class="str">"cmt">// If there is a key in the glossary that matches the current symbol
  if(p_symbolsMap.ContainsKey(m_symbol)) {

◍ 用映射表切换主力品种

在跨品种联动的 EA 里,常需要把当前图表品种动态替换成另一个标的来下单或取值。上面这段逻辑就是从一张预置的对照表(symbolsMap)里查目标品种,查到了就改 m_symbol,查不到就维持原样。 具体看:TryGetValue 以当前 m_symbol 为键去试取 targetSymbol,命中返回 true 并写入引用变量;此时把 m_symbol 赋值为 targetSymbol,后续计算就跑在映射后的品种上。这种写法在黄金与 XAU/USD proxy、或 EURUSD 与指数差价品种之间切换时很实用。 外汇和贵金属杠杆高、跳空频繁,映射错品种可能直接开错仓,上线前务必在 MT5 策略测试器用 2023 年 1—6 月数据跑一遍品种对照逻辑,确认无空引用返回。

MQL5 / C++
class="type">class="kw">string targetSymbol; class=class="str">"cmt">// Target symbol

class=class="str">"cmt">// If the target symbol for the current one is successfully retrieved from the glossary
if(p_symbolsMap.TryGetValue(m_symbol, targetSymbol)) {
   class=class="str">"cmt">// Update the current symbol
   m_symbol = targetSymbol;
}

class="kw">return true;

常见问题

先查滑点与点差假设是否和券商实盘一致,再确认是否漏了成交拒绝与库存费,最后看多标的同刻下单是否触发资金占用爆仓。
一是跨品种同时下单导致保证金叠加;二是非主交易时段点差扩大吃掉盈利;三是换月或停牌造成标的映射失效。
小布可自动比对回测与实盘品种参数差异,标出暗坑品种,并一键生成换品种映射建议,省去手动逐条核对。
把品种名写成配置字符串由EA解析,改字符串即可切换标的,不需动逻辑层,适合快速切到主力合约。
解决手动一个个改交易对易错且难维护的问题,映射表一次定义,EA启动按表重指标的,换主力品种只改表。