开发多币种 EA 交易系统(第 15 部分):为真实交易准备 EA(基础篇)
把多币种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 确实能对新品种下虚拟单,说明解析链路通了。外汇和贵金属杠杆高、点差跳变频繁,换品种前先在策略测试器用历史数据验一遍再上实盘。
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。 外汇和贵金属杠杆高、滑点跳空频繁,重映射后务必在策略初始化日志里核对实际生效品种,别凭代码直觉认为已切换成功。
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 月数据跑一遍品种对照逻辑,确认无空引用返回。
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;