跨平台智能交易程序: 重用来自 MQL5 标准库的控件·进阶篇
(2/3)· 当 MQL5 标准库控件遇上 MQL4 编译器不兼容,拷贝改写比推倒重写更省力
直接把 MQL5 标准库类塞进 MQL4 项目,编译器报错是多数人的第一反应。与其从头重写,把类拷出来改到能过编译,往往是跨平台 EA 更现实的折中。本篇接着第 1 篇的基础,把这套搬运结构落到具体控件上。
「初始化时把品种属性一次性抓全」
写 EA 或指标时,很多人习惯在每次下单前现查品种参数,其实更稳的做法是在对象初始化阶段用 SymbolInfo 系列函数把关键属性一次性读进成员变量,后续逻辑直接引用,避免重复调用带来的微小延迟和潜在失败。 下面这段代码在初始化函数里连续调用 SymbolInfoDouble 与 SymbolInfoInteger。只要任意一个返回 false,整个初始化直接 return(false),保证后续用到的 m_swap_long、m_digits、m_trade_mode 等字段都是有效值,不会出现野值。 被注释掉的那几行(如 SYMBOL_VOLUME_LIMIT、SYMBOL_MARGIN_LONG)说明原作者在特定场景下并不需要这些字段,你可以按自己策略取消注释来启用。 注意 SYMBOL_CALC_MODE 有 8 种枚举:从 FOREX、FUTURES 到 CFD 系列及交易所股票/期货。贵金属 XAUUSD 在多数券商下是 SYMBOL_CALC_MODE_FOREX 或 CFDLEVERAGE,这直接决定保证金和库存费的计算路径,初始化读错模式会让仓位计算偏差。 外汇与贵金属杠杆高、波动剧烈,初始化失败若被忽略可能造成无保护下单,实盘前务必在 MT5 用 Print 把各字段打出核对。
if(!SymbolInfoDouble(m_name,SYMBOL_SWAP_LONG,m_swap_long)) class="kw">return(false); if(!SymbolInfoDouble(m_name,SYMBOL_SWAP_SHORT,m_swap_short)) class="kw">return(false); if(!SymbolInfoInteger(m_name,SYMBOL_DIGITS,tmp)) class="kw">return(false); m_digits=(class="type">int)tmp; if(!SymbolInfoInteger(m_name,SYMBOL_TRADE_EXEMODE,tmp)) class="kw">return(false); m_trade_execution=(ENUM_SYMBOL_TRADE_EXECUTION)tmp; if(!SymbolInfoInteger(m_name,SYMBOL_TRADE_CALC_MODE,tmp)) class="kw">return(false); m_trade_calcmode=(ENUM_SYMBOL_CALC_MODE)tmp; if(!SymbolInfoInteger(m_name,SYMBOL_TRADE_MODE,tmp)) class="kw">return(false); m_trade_mode=(ENUM_SYMBOL_TRADE_MODE)tmp; if(!SymbolInfoInteger(m_name,SYMBOL_SWAP_MODE,tmp)) class="kw">return(false); m_swap_mode=(ENUM_SYMBOL_SWAP_MODE)tmp; if(!SymbolInfoInteger(m_name,SYMBOL_SWAP_ROLLOVER3DAYS,tmp)) class="kw">return(false); m_swap3=(ENUM_DAY_OF_WEEK)tmp; if(!SymbolInfoDouble(m_name,SYMBOL_MARGIN_INITIAL,m_margin_initial)) class="kw">return(false); if(!SymbolInfoDouble(m_name,SYMBOL_MARGIN_MAINTENANCE,m_margin_maintenance)) class="kw">return(false); class="kw">return(true); } enum ENUM_SYMBOL_CALC_MODE { SYMBOL_CALC_MODE_FOREX, SYMBOL_CALC_MODE_FUTURES, SYMBOL_CALC_MODE_CFD, SYMBOL_CALC_MODE_CFDINDEX, SYMBOL_CALC_MODE_CFDLEVERAGE, SYMBOL_CALC_MODE_EXCH_STOCKS, SYMBOL_CALC_MODE_EXCH_FUTURES, SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS }; enum ENUM_SYMBOL_SWAP_MODE { SYMBOL_SWAP_MODE_DISABLED, SYMBOL_SWAP_MODE_POINTS, SYMBOL_SWAP_MODE_CURRENCY_SYMBOL, SYMBOL_SWAP_MODE_CURRENCY_MARGIN, SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT, SYMBOL_SWAP_MODE_INTEREST_CURRENT, SYMBOL_SWAP_MODE_INTEREST_OPEN, SYMBOL_SWAP_MODE_REOPEN_CURRENT,
◍ 跨编译器下的库存费枚举与头文件切换
MQL5 与 MQL4 在库存费(swap)计算模型上并不一致,SYMBOL_SWAP_MODE_REOPEN_BID 这种枚举值只在重新开仓式隔夜费模式下出现,用于标识买价重开类的换仓逻辑。 为了同一套品种信息封装能在两套环境编译,代码用 __MQL5__ 宏做条件包含:MQL5 下直接引 Trade\SymbolInfo.mqh,MQL4 则回退到相对路径 ..\..\MQL4\Lib\SymbolInfo.mqh。 你在 MT5 里接手老 MQL4 库时,先确认这些预编译分支是否还指向存在的文件,否则会卡在 include 阶段。
class="macro">#ifdef __MQL5__ class="macro">#include <Trade\SymbolInfo.mqh> class="macro">#else class="macro">#include "..\..\MQL4\Lib\SymbolInfo.mqh" class="macro">#endif
多币种对象的容器怎么搭
写跨品种 EA 时,最怕把十几个品种的 CSymbolInfo 实例散落得到处都是。用 CSymbolManager 继承 CArrayObj,就能把品种信息对象收进同一个集合,顺带多几个专属方法。 它本质上还是个对象数组,但加了一条硬规矩:同一个品种名不允许重复进容器。Add 之前先走自定义 Search,按品种名查重,查不到才塞进基类数组。 主要品种(primary)最好初始化时显式设定。不设定的话,默认拿第一个实例当主品种——这种写法在 EURUSD 之外再挂 XAUUSD 做关联波动时够用,但逻辑上别依赖它的隐式顺序。 刷新汇率时,直接循环调每个实例的 RefreshRates。品种一多(比如 20 个以上),全量刷可能拖慢分时处理;那种情况改调用单个 CSymbolInfo 实例的 RefreshRates 更稳。注意 Refresh 只用于初始化,RefreshRates 才是盘中更新。 下面这段是 CSymbolManagerBase 的关键方法,跨 MQL4/MQL5 通用:
class="type">bool CSymbolManagerBase::Add(CSymbolInfo *node) { if(Search(node.Name())==-class="num">1) class="kw">return CArrayObj::Add(node); class="kw">return false; } class="type">int CSymbolManagerBase::Search(class="type">class="kw">string symbol=NULL) { if(symbol==NULL) symbol= Symbol(); for(class="type">int i=class="num">0;i<Total();i++) { CSymbolInfo *item=At(i); if(StringCompare(item.Name(),symbol)==class="num">0) class="kw">return i; } class="kw">return -class="num">1; } class="type">void CSymbolManagerBase::SetPrimary(class="type">class="kw">string symbol=NULL) { if(symbol==NULL) symbol= Symbol(); m_symbol_primary=Get(symbol); } CSymbolInfo *CSymbolManagerBase::GetPrimary(class="type">void) { if (!CheckPointer(m_symbol_primary) && Total()>class="num">0) SetPrimary(class="num">0); class="kw">return m_symbol_primary; } class="type">bool CSymbolManagerBase::RefreshRates(class="type">void) { for(class="type">int i=class="num">0;i<Total();i++) { CSymbolInfo *symbol=At(i); if(!CheckPointer(symbol)) class="kw">continue; if(!symbol.RefreshRates()) class="kw">return false; } class="kw">return true; } class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include "..\Lib\SymbolInfo.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class CSymbolManagerBase : class="kw">public CArrayObj { class="kw">protected: CSymbolInfo *m_symbol_primary; CObject *m_container; class="kw">public: CSymbolManagerBase(class="type">void); ~CSymbolManagerBase(class="type">void); class="kw">virtual class="type">bool Add(CSymbolInfo*);
「符号管理基类的空壳与容器挂钩」
CSymbolManagerBase 作为品种管理器的抽象底座,只声明了接口和极少数实现。头文件里能看到 Deinit、RefreshRates、Search、GetContainer、SetContainer、SetPrimary 等全是 virtual 虚函数,说明真实逻辑留给派生类去填,基类本身不做事。 GetContainer 和 SetContainer 是唯二有实体的方法:前者直接 return m_container,后者把传入的 CObject 指针赋给 m_container。这等于把管理器挂到一个外部容器对象上,方便后续在对象树里互相引用。 构造和析构函数体都是空的,Deinit 内部只调了 Shutdown(),而 Shutdown 在本节没有给出实现——意味着你若直接实例化 CSymbolManagerBase 并调用 Deinit,实际清理动作取决于派生类是否重写。 开 MT5 自建一个继承 CSymbolManagerBase 的类,重写 Shutdown 打印一行日志,就能验证基类这套容器挂钩机制是否如预期工作。外汇与贵金属品种切换频繁,这种基类设计能降低多品种管理时的耦合,但实盘参数误绑可能引发引用错乱,属于高风险操作。
class="kw">virtual class="type">void Deinit(class="type">void); CSymbolInfo *Get(class="type">class="kw">string); class="kw">virtual class="type">bool RefreshRates(class="type">void); class="kw">virtual class="type">int Search(class="type">class="kw">string); class="kw">virtual CObject *GetContainer(class="type">void); class="kw">virtual class="type">void SetContainer(CObject*); class="kw">virtual class="type">void SetPrimary(class="type">class="kw">string); class="kw">virtual class="type">void SetPrimary(const class="type">int); class="kw">virtual CSymbolInfo *GetPrimary(class="type">void); class="kw">virtual class="type">class="kw">string GetPrimaryName(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ CSymbolManagerBase::CSymbolManagerBase(class="type">void) { } CSymbolManagerBase::~CSymbolManagerBase(class="type">void) { } CObject *CSymbolManagerBase::GetContainer(class="type">void) { class="kw">return m_container; } CSymbolManagerBase::SetContainer(CObject *container) { m_container=container; } CSymbolManagerBase::Deinit(class="type">void) { Shutdown(); } class="type">bool CSymbolManagerBase::Add(CSymbolInfo *node) {
◍ 品种管理类的查重与刷新实现
在 MT5 的 EA 或指标里维护多品种对象池时,CSymbolManagerBase 这类封装最关键的不是存,而是查得快、刷得动。下面这段把添加查重、按名取值、批量刷新报价三件事拆开写,逻辑直接可抄。 添加节点时先走 Search 看名字是否已存在,返回 -1 才调基类 CArrayObj::Add,否则直接返回 false 避免重复实例。注意 Search 里不检查指针有效性,只比名字,所以速度优先但要求池子本身干净。 Get 和 Search 都接收默认 NULL,此时用 Symbol() 补当前图表品种。Get 多了一层 CheckPointer 判空再 continue,防止野指针;匹配用 StringCompare==0,线性遍历,品种数在 50 以内时延迟可忽略。 RefreshRates 遍历全部 CSymbolInfo 逐个刷报价,任意一个失败就整体返回 false。实盘中外汇与贵金属报价中断概率不低,调用方应据此判断是否需要重连逻辑,相关品种波动风险显著。 让小布替你跑这套 把 Search 的线性查找换成哈希映射,在监控 200+ 交叉盘时可能把取值耗时从毫秒级降到常数级,值得在策略初始化时测一把。
if(Search(node.Name())==-class="num">1) class="kw">return CArrayObj::Add(node); class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CSymbolInfo *CSymbolManagerBase::Get(class="type">class="kw">string symbol=NULL) { if(symbol==NULL) symbol= Symbol(); for(class="type">int i=class="num">0;i<Total();i++) { CSymbolInfo *item=At(i); if(!CheckPointer(item)) class="kw">continue; if(StringCompare(item.Name(),symbol)==class="num">0) class="kw">return item; } class="kw">return NULL; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CSymbolManagerBase::RefreshRates(class="type">void) { for(class="type">int i=class="num">0;i<Total();i++) { CSymbolInfo *symbol=At(i); if(!CheckPointer(symbol)) class="kw">continue; if(!symbol.RefreshRates()) class="kw">return false; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSymbolManagerBase::Search(class="type">class="kw">string symbol=NULL) { if(symbol==NULL) symbol= Symbol(); for(class="type">int i=class="num">0;i<Total();i++) { CSymbolInfo *item=At(i); if(StringCompare(item.Name(),symbol)==class="num">0) class="kw">return i; } class="kw">return -class="num">1; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSymbolManagerBase::SetPrimary(class="type">class="kw">string symbol=NULL) { if(symbol==NULL)
主交易品种的动态指针兜底
symbol管理基类里,主品种指针靠 SetPrimary 按索引或当前图表品种写入。GetPrimary 在指针为空且列表总数大于 0 时,会自动把索引 0 的品种设为默认主品种再返回,避免空指针调用崩 EA。 GetPrimaryName 则先 CheckPointer 判空,非空才取 Name,否则返回 NULL 字符串——这套兜底在外接多品种扫描时很实用,省得你每次手动判空。 #ifdef __MQL5__ 会切到 MQL5 路径的 SymbolManager.mqh,否则走 MQL4,同一套 CSymbolManager 派生类在双端都能编译。开 MT5 把这段塞进你的品种管理模块,跑一下 Total()>0 的边界情形验证兜底是否生效。
symbol= Symbol(); m_symbol_primary=Get(symbol); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSymbolManagerBase::SetPrimary(const class="type">int idx) { m_symbol_primary=At(idx); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CSymbolInfo *CSymbolManagerBase::GetPrimary(class="type">void) { if (!CheckPointer(m_symbol_primary) && Total()>class="num">0) SetPrimary(class="num">0); class="kw">return m_symbol_primary; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CSymbolManagerBase::GetPrimaryName(class="type">void) { if(CheckPointer(m_symbol_primary)) class="kw">return m_symbol_primary.Name(); class="kw">return NULL; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#ifdef __MQL5__ class="macro">#include "..\..\MQL5\Symbol\SymbolManager.mqh" class="macro">#else class="macro">#include "..\..\MQL4\Symbol\SymbolManager.mqh" class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ class CSymbolManager : class="kw">public CSymbolManagerBase { class="kw">public: CSymbolManager(class="type">void); ~CSymbolManager(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CSymbolManager::CSymbolManager(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CSymbolManager::~CSymbolManager(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+