自定义品种(符号):实践基础·综合运用
(3/3)·从等量图到Renko,手把手把自定义符号接进EA,让衍生图表真正能下单
◍ 让 Renko 自定义图表真正去交易 EURUSD
自定义品种只在客户端存在,交易服务器并不认。想在 Renko 砖形图上跑 EA 却又实际交易底层真实品种,最省事的办法是 EA 挂在真实品种图表上收自定义品种信号,但很多人想直接看砖形图全貌,且手改源码容易出错。 平台本身不会把自定义品种名和 origin 品种名做绑定。SYMBOL_BASIS 字段虽语义接近,但无法保证第三方生成器填对,所以得自己造一层替换机制:用 CustomOrder 类包住所有带品种名的交易与历史 API,自动在 _Symbol 与 workSymbol 之间互换。 为最小化改动,把 CustomOrder.mqh 放在所有 include 最前面,再用宏把标准 API 调用重定向过去。设 WorkSymbol="EURUSD" 后,MA2CrossCustom 在 100 点 Renko 图上跑,实际成交的是 EURUSD,回测结果比纯自定义品种更接近现实。 误差来源很具体:Renko 柱按 M1 边界起算,而 EURUSD 真实价在一分钟内任意时刻越界,EA 用柱线模式信号会偏移到分钟开盘,平均偏移约等于每笔交易的分钟柱数学期望。联线交易无此问题,测试器内 tick 逻辑与砖形生成也不同,逆转砖开盘必有缺口。 另一套基于标准库和 MT4Orders 的 ExprBot 改完后在 2019–2020 上半年、7/11 LWMA 收盘价设置下,100 点 Renko 跑真实 EURUSD,结果与 MA2CrossCustom 高度相似,说明这套包装够用但仅满足最低需求,按策略细调可能还要补。
class CustomOrder { class="kw">private: class="kw">static class="type">class="kw">string workSymbol; class="kw">static class="type">void replaceRequest(class="type">MqlTradeRequest &request) { if(request.symbol == _Symbol && workSymbol != NULL) { request.symbol = workSymbol; if(request.type == ORDER_TYPE_BUY || request.type == ORDER_TYPE_SELL) { if(request.price == SymbolInfoDouble(_Symbol, SYMBOL_ASK)) request.price = SymbolInfoDouble(workSymbol, SYMBOL_ASK); if(request.price == SymbolInfoDouble(_Symbol, SYMBOL_BID)) request.price = SymbolInfoDouble(workSymbol, SYMBOL_BID); } } } class="kw">public: class="kw">static class="type">void setReplacementSymbol(class="kw">const class="type">class="kw">string replacementSymbol) { workSymbol = replacementSymbol; } class="kw">static class="type">bool OrderSend(class="type">MqlTradeRequest &request, class="type">MqlTradeResult &result) { replaceRequest(request); class="kw">return ::OrderSend(request, result); } class="kw">static class="type">bool OrderCalcProfit(ENUM_ORDER_TYPE action, class="type">class="kw">string symbol, class="type">class="kw">double volume, class="type">class="kw">double price_open, class="type">class="kw">double price_close, class="type">class="kw">double &profit) { if(symbol == _Symbol && workSymbol != NULL) { symbol = workSymbol; } class="kw">return ::OrderCalcProfit(action, symbol, volume, price_open, price_close, profit); } class="kw">static class="type">class="kw">string PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id) { class="kw">const class="type">class="kw">string result = ::PositionGetString(property_id); if(property_id == POSITION_SYMBOL && result == workSymbol) class="kw">return _Symbol; class="kw">return result; } class="kw">static class="type">class="kw">string OrderGetString(ENUM_ORDER_PROPERTY_STRING property_id) { class="kw">const class="type">class="kw">string result = ::OrderGetString(property_id); if(property_id == ORDER_SYMBOL && result == workSymbol) class="kw">return _Symbol; class="kw">return result; } class="kw">static class="type">class="kw">string HistoryOrderGetString(class="type">class="kw">ulong ticket_number, ENUM_ORDER_PROPERTY_STRING property_id) { class="kw">const class="type">class="kw">string result = ::HistoryOrderGetString(ticket_number, property_id);
「用符号重定向把回测伪装成实盘品种」
想在 MT5 里用一个图表跑另一个品种的 EA,不必改 EA 源码,只要拦掉底层交易函数的品种参数即可。上面这组静态方法把 ORDER_SYMBOL、DEAL_SYMBOL、PositionGetSymbol 的返回值做了替换:当读到的品种等于 workSymbol 时,对外统一报成当前图表的 _Symbol。 具体接入方式是在 include 之前宏替换 OrderSend:#define OrderSend CustomOrderSend 让所有 EA 内部发单走 CustomOrder::OrderSend,而 workSymbol 由 OnInit 里的 input 参数 WorkSymbol 设定。若留空则不重定向。 验证方法很简单:给 WorkSymbol 填 "XAUUSD",把 EA 挂到 EURUSD 图表,用 CopyRates(WorkSymbol, PERIOD_H1, 0, 1, rates) 强制开一张 XAUUSD 的 H1 数据缓存(仅可视化模式有效),随后在回测里观察成交品种字段是否被改写成 EURUSD。外汇与贵金属杠杆高,品种重定向仅用于本地验证,实盘误用可能导致下单到错误标的。
if(property_id == ORDER_SYMBOL && result == workSymbol) class="kw">return _Symbol; class="kw">return result; } class="kw">static class="type">class="kw">string HistoryDealGetString(class="type">class="kw">ulong ticket_number, ENUM_DEAL_PROPERTY_STRING property_id) { class="kw">const class="type">class="kw">string result = ::HistoryDealGetString(ticket_number, property_id); if(property_id == DEAL_SYMBOL && result == workSymbol) class="kw">return _Symbol; class="kw">return result; } class="kw">static class="type">bool PositionSelect(class="type">class="kw">string symbol) { if(symbol == _Symbol && workSymbol != NULL) class="kw">return ::PositionSelect(workSymbol); class="kw">return ::PositionSelect(symbol); } class="kw">static class="type">class="kw">string PositionGetSymbol(class="type">int index) { class="kw">const class="type">class="kw">string result = ::PositionGetSymbol(index); if(result == workSymbol) class="kw">return _Symbol; class="kw">return result; } ... }; class="kw">static class="type">class="kw">string CustomOrder::workSymbol = NULL; class="type">bool CustomOrderSend(class="kw">const class="type">MqlTradeRequest &request, class="type">MqlTradeResult &result) { class="kw">return CustomOrder::OrderSend((class="type">MqlTradeRequest)request, result); } class="macro">#define OrderSend CustomOrderSend class="macro">#include <CustomOrder.mqh> class="macro">#include <Expert\Expert.mqh> ... class="kw">input class="type">class="kw">string WorkSymbol = ""; class="type">int OnInit() { if(WorkSymbol != "") { CustomOrder::setReplacementSymbol(WorkSymbol); class=class="str">"cmt">// force a chart for the work symbol to open(in visual mode only) class="type">MqlRates rates[class="num">1]; CopyRates(WorkSymbol, PERIOD_H1, class="num">0, class="num">1, rates); } ... }
自定义品种落地的两个坑
前面几节把用经纪商有效报价合成自定义品种的思路铺开了,实际在 MT5 里跑起来还有两处容易踩的暗坑。一处是三位或五位小数报价的点数换算,RenkoTicks 这类实用工具在三位数定价下会出现点数到点数的转换偏差,社区里有人用全局变量 _PntsToPips 做了针对性修补。
另一处是按 tick 交易的 EA 信号滞后。底层指标对象自己维护内部缓冲区,直接调 m_maFast.Main(ind) 读出来的是缓存里的旧数据;改成 GetData 封装的 CopyBuffer 直读才稳。更麻烦的是新 tick 触发时时间序列可能还没重算,得动态把 bar 索引减一再去判信号,否则在稀疏报价品种上开仓会直接错过。
外汇和贵金属这类高波动品种,tick 稀疏是常态,上面两套改法建议直接抄进你自己的信号头文件里验证。合成品种、成交量增量、第三方数据源的扩展空间很大,但实盘前务必在策略测试器里用历史 tick 跑一遍。
class=class="str">"cmt">//在全局中,插入 class="type">class="kw">double _PntsToPips; class=class="str">"cmt">//在启动时,插入 if(_Digits == class="num">3 || _Digits == class="num">5) { PntsToPips = class="num">10; } else { _PntsToPips = class="num">1; } class=class="str">"cmt">//在包含 _Point 的 class="num">2 行中(不在 sendSpread... 中),插入 * _PntsToPips class=class="str">"cmt">//class="num">2 新行将是 class="type">class="kw">double Renko::boxPoints = NormalizeDouble(RenkoBoxSize * _Point * _PntsToPips, _Digits); Renko::setBoxPoints(NormalizeDouble(RenkoBoxSize * _Point * _PntsToPips, _Digits)); class Signal2MACross : class="kw">public CExpertSignal { ... class=class="str">"cmt">// 读取指标数据的辅助函数 class="type">class="kw">double FastMA(class="type">int ind) { class="kw">static class="type">class="kw">double buffer[class="num">1]; m_maFast.GetData(ind, class="num">1, class="num">0, buffer); class="kw">return buffer[class="num">0]; } class="type">class="kw">double SlowMA(class="type">int ind) { class="kw">static class="type">class="kw">double buffer[class="num">1]; m_maSlow.GetData(ind, class="num">1, class="num">0, buffer); class="kw">return buffer[class="num">0]; } }; class="type">class="kw">double FastMA(class="type">int ind) { class="type">MqlTick t; SymbolInfoTick(m_symbol.Name(), t); class="type">bool correction = class="kw">false; if(t.time / class="num">60 * class="num">60 != iTime(m_symbol.Name(), PERIOD_CURRENT, class="num">0) && ind > class="num">0) { ind--; correction = true; } class="kw">static class="type">class="kw">double buffer[class="num">1]; m_maFast.GetData(ind, class="num">1, class="num">0, buffer); if(correction)
◍ 画得少,看得清
把毫秒级时间戳塞进 PrintFormat,是为了在日志里直接比对本地 tick 时间与当前周期 K 线时间,差出几毫秒都能抓出来。 上面那段返回 buffer[0] 的函数,本质只做一件事:取当前品种、当前周期第 0 根 K 线的指标缓冲首值,并带上 time_msc 取模 1000 的毫秒尾数以利排查。 实盘里这类轻量探针不用挂图表,开 MT5 按 F4 编译丢进脚本区跑一遍,比堆十几个面板更能看清数据到底卡在哪。外汇与贵金属波动快、滑点随机,任何读数都只是概率参考,别当确定性依据。
PrintFormat("F: %s&class="macro">#x27;%03d %s %.5f", TimeToString(t.time, TIME_SECONDS), t.time_msc % class="num">1000, TimeToString(iTime(m_symbol.Name(), PERIOD_CURRENT, class="num">0)), buffer[class="num">0]); class="kw">return buffer[class="num">0]; }