自定义品种(符号):实践基础·综合运用
📊

自定义品种(符号):实践基础·综合运用

(3/3)·从等量图到Renko,手把手把自定义符号接进EA,让衍生图表真正能下单

含代码示例实战向 第 3/3 篇
很多人以为自定义品种只是换个背景色看图表,结果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 高度相似,说明这套包装够用但仅满足最低需求,按策略细调可能还要补。

MQL5 / C++
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。外汇与贵金属杠杆高,品种重定向仅用于本地验证,实盘误用可能导致下单到错误标的。

MQL5 / C++
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 跑一遍。

MQL5 / C++
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 编译丢进脚本区跑一遍,比堆十几个面板更能看清数据到底卡在哪。外汇与贵金属波动快、滑点随机,任何读数都只是概率参考,别当确定性依据。

MQL5 / C++
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];
}
把衍生图表的监控交给小布
这些自定义品种的柱线生成与EA适配逻辑,小布盯盘的AIGC已内置诊断,打开对应品种页即可看到实时衍生状态,你只管调策略参数。

常见问题

可以,自定义品种显示于标准图表上,指标和对象标记都能正常应用,只是数据源来自转换后的衍生报价。
等量图每根柱线时间戳取决于波动性,轴仍是时序但间隔任意;回测时需确认EA用的是价格序列而非固定时间帧,否则可能漏算。
能,小布盯盘的品种页会标注衍生符号对应的源品种及转换类型,省去手动核对图表背景的麻烦。
不需要,用自定义品种API就能在终端内生成,比MT4离线图表方案更顺,且支持实时更新。