教程 · 单套解锁 教程 · EA量化教学

EA03 MQL4迁移MQL5指南

使用手册 + 操作教程 共 19 章全部免费开放,一步一步把软件用起来;核心知识点 7 章,¥69 一次解锁讲透原理。

作者 枢汇安全课堂 更新 2026-10-13 阅读 104 收录 EA量化教学 · 26 章
本课程 · 单套解锁
¥69¥104
单品永久解锁 · 会员可阅 · 一次购买终身使用
✓使用手册 + 操作教程 共 19 章全文免费 ✓核心知识点 7 章,¥69 一次解锁 ✓支持电脑 / 手机 · 永久免费更新
注册后解锁核心知识点
免费学习区 使用手册 + 操作教程 共 19 章全部免费开放 · 核心知识点 7 章为付费内容 ¥69 解锁核心知识点
PART ONE

使用手册

功能在哪 · 是什么 · 怎么调出来

一、下单:CTrade 类入门

1、为什么从全局函数变成类

MQL4 的 OrderSend 是全局函数,参数一长串,所有策略共用一个"大锅"状态。MQL5 底层交易只有 OrderSend() 和 OrderSendAsync() 两个函数(配合一个十几字段的 MqlTradeRequest 结构体,见第十二章),官方标准库提供了 CTrade 类帮你封装,日常写 EA 用它最省事:

#include <Trade/Trade.mqh>
CTrade trade;

int OnInit()
{
trade.SetExpertMagicNumber(12345); // 魔术号
trade.SetDeviationInPoints(30); // 允许滑点(点)
trade.SetTypeFillingBySymbol(_Symbol); // 按品种自动选成交方式
    return(INIT_SUCCEEDED);
}

void OnTick()
{
    // 市价买入 0.1 手(symbol 缺省为当前图表品种)
    if(!trade.Buy(0.1))
        Print("Buy失败: ", trade.ResultRetcode(),
              " ", trade.ResultRetcodeDescription());
}

2、Buy / Sell 的参数

官方文档的标准用法:除 volume 外全部可选,symbol 不填就是当前图表品种,CTrade 会自己取现价:

trade.Buy(volume, symbol, price, sl, tp, comment);
trade.Buy(0.1); // 最简:当前品种市价买
trade.Buy(0.1, "GBPUSD"); // 指定品种
trade.Buy(0.1, _Symbol, 0.0, sl, tp); // 带止损止盈

3、关键警告:返回 true 不等于成交了

官方 BuyLimit 文档原话同样适用于 Buy/Sell:"成功完成方法并不意味着总是成功执行交易操作"。返回值 true 只代表请求被服务器接受检查通过,必须再查:

if(trade.Buy(0.1))
{
    uint code = trade.ResultRetcode();
if(code == TRADE_RETCODE_DONE) // 10009:已执行
        Print("成交,订单号", trade.ResultOrder(),
              " 成交单号", trade.ResultDeal());
    else
        Print("服务器拒绝/未完成: ", code);
}
白话注解:MQL4 的 OrderSend 像"公用电话"——打完只知道通没通。MQL5 的 CTrade 像"你自己的手机"——每个策略一个对象,请求、结果、错误码都挂在自己身上。但注意:电话接通(true)不等于事情办成(TRADE_RETCODE_DONE),要二次确认。

二、净持仓 vs 锁仓:先搞清你的账户模式

1、MT4 没有、MT5 逃不掉的问题

MT4 全是"锁仓式"记账:同品种可以同时挂多条多单和空单,各自独立。MT5 有两种账户模式,由券商在开户时固定,不能自己切换:

模式每品种持仓数同向新单反向新单
净持仓 Netting只有一条合并进现有持仓,均价加权减仓 / 平仓 / 反转现有持仓
锁仓 Hedging多条(含多空并存)新开一条独立持仓新开一条反向独立持仓

2、用代码判断账户模式(官方枚举)

long mode = AccountInfoInteger(ACCOUNT_MARGIN_MODE);

if(mode == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
    Print("锁仓账户:可多空并存、多条独立持仓");
else if(mode == ACCOUNT_MARGIN_MODE_RETAIL_NETTING)
    Print("净持仓账户:每品种只有一条净头寸");
else if(mode == ACCOUNT_MARGIN_MODE_EXCHANGE)
    Print("交易所账户:净持仓 + 交易所保证金规则");

3、对 EA 逻辑的三条硬影响

  1. 反向单的语义变了:净持仓账户下,卖出不是"开空",而是"减仓/平仓/反转现有多单"。MT4 上的网格、马丁、锁仓对冲逻辑直接搬过来,行为会完全错乱;
  2. 平仓按什么定位:trade.PositionClose(symbol) 在锁仓账户下平的是单号最小的那条持仓;要精确平某一条,用 PositionClose(ticket);
  3. 持仓标识符:净持仓反转时 POSITION_IDENTIFIER 保持不变而 POSITION_TICKET 变为新订单号——按 ticket 做历史关联时要留意。
// 锁仓账户:逐条管理本 EA 的持仓(按 ticket 精确操作)
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
    ulong ticket = PositionGetTicket(i);
    if(ticket == 0) continue;
    if(PositionGetInteger(POSITION_MAGIC) == 12345
       && PositionGetDouble(POSITION_PROFIT) < -200)
    {
trade.PositionClose(ticket); // 用 ticket,不用 symbol
    }
}
白话注解:净持仓账户像"合并同类项"——所有同品种单子加减抵消成一条净头寸;锁仓账户像"一本一本分账本"——每笔单子独立记录。你的 EA 是按"多笔独立单"设计的,跑到净持仓账户上就会"怎么单子全合并了"。开户前先问清楚账户模式,另外注意:美国 NFA 监管的外汇账户强制净持仓 + FIFO,不允许锁仓。
PART TWO

操作教程

一步一步 · 跟着做完一件事

三、为什么要迁移到 MQL5

MQL4 你已经会写了,但 MT5 是 MetaQuotes 当前主推的平台,有几个硬优势:

  • 真正的面向对象:类、继承、模板、标准库都有;
  • 多线程回测:策略测试器支持多核并行和参数优化并行,还能用 MQL5 云网络分布式跑优化;
  • 真实 Tick 回测:可以按"每个基于真实 Tick"模式回测,比 MT4 的 OHLC 插值贴近实盘;
  • 支持股票、期货、期权等交易所品种,不只是外汇 CFD;
  • 官方新功能都在 MQL5 侧更新,MT4 已基本停止演进。

但 MQL5 不向后兼容 MQL4:OrderSend 参数结构、OrderSelect 语义、指标调用方式全变了。MQL5 官方甚至专门写过《Migrating from MQL4 to MQL5》迁移指南文章——官方都承认这不是"改改就能跑"的事。

白话注解:MQL5 不是 MQL4 的"升级版",是"重写版"。就像 Windows 98 和 Windows 10——名字像,但底层完全不同。老的 MQL4 代码不能直接跑,核心逻辑要按新架构重写。
风险口径:迁移只是把代码搬到新平台,回测表现好不代表实盘能赚钱。回测≠实盘,业绩不代表未来收益。

四、开发环境:MetaEditor 5

1、打开方式

  1. 安装 MetaTrader 5 客户端并登录(模拟账号即可开发);
  2. 按 F4 或点工具栏的 IDE 图标,打开 MetaEditor 5;
  3. 右键 Expert Advisors(专家) 文件夹 → 新建 → 专家顾问(Expert Advisor)。

2、目录结构(MQL5 数据文件夹)

目录放什么
MQL5/Experts/EA 源码(.mq5)与编译产物(.ex5)
MQL5/Indicators/自定义指标
MQL5/Include/头文件(如 Trade/Trade.mqh)
MQL5/Libraries/DLL 与函数库
MQL5/Files/程序读写的数据文件沙箱

3、编译与排错

  • F7 编译;错误在"错误"窗口双击跳转;
  • 编译产物 .ex5 放进对应目录后,在 MT5 终端的导航器里刷新即可看到;
  • 老 .mq4 文件不能直接改后缀——语法和库都要动。
白话注解:MetaEditor 5 和 MetaEditor 4 长得几乎一样,快捷键也一样,这是迁移最省心的部分——难的从来不是编辑器,是 API。

五、EA 骨架:OnInit / OnTick / OnDeinit

MQL5 的 EA 骨架和 MQL4 时代的事件驱动思路一致,但函数名和返回值更规范:

//+------------------------------------------------------------------+
//| EA骨架.mq5 |
//+------------------------------------------------------------------+
int OnInit()
{
    // 初始化:建指标句柄、配置交易对象、校验参数
    // 成功返回 INIT_SUCCEEDED,失败返回 INIT_FAILED
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
    // 卸载:释放指标句柄、删除画图对象
    // reason 是卸载原因代码(REASON_REMOVE、REASON_CHARTCHANGE 等)
}

void OnTick()
{
    // 每来一个新报价(tick)触发一次
    // 没有 tick 就不触发——周末和盘口安静时它不会替你跑
}

1、与 MQL4 的差异点

  • MQL4 老式的 init() / deinit() / start() 在 MQL5 中不存在,必须用 OnInit / OnDeinit / OnTick;
  • OnInit 的返回值是 INIT_SUCCEEDED / INIT_FAILED / INIT_PARAMETERS_INCORRECT 等常量,不再返回整数随意约定;
  • MQL5 还有 OnTimer、OnTrade、OnTradeTransaction、OnTester 等更多事件(见第十五章)。
白话注解:OnInit 是"开机自检",OnTick 是"听到报价就干活",OnDeinit 是"下班关灯"。把建句柄这类重活放在 OnInit,别放 OnTick 里反复做。

六、最核心的变化:一个订单池变三个集合

1、MQL4:一个大池子

MQL4 只有一个订单池,OrdersTotal() 把持仓和挂单混在一起数,靠 OrderType() 区分:

// MQL4:持仓和挂单在同一个池子里
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
        if(OrderSymbol() == Symbol() && OrderMagicNumber() == 123)
            Print(OrderTicket(), OrderLots(), OrderProfit());
    }
}

MQL4 的 OrderSelect() 是"快照模型":先把订单数据复制进程序环境,之后 OrderTicket()、OrderLots()、OrderProfit() 这些无参属性函数读的都是快照。官方文档明确提示:按池位置遍历的排序不保证,数据可能过期,操作前要重新 Select。

2、MQL5:三个独立集合

集合数量函数选择函数内容
持仓 PositionsPositionsTotal()PositionGetTicket(i) / PositionSelect(symbol) / PositionSelectByTicket(ticket)市价持仓
挂单 OrdersOrdersTotal()OrderGetTicket(i) / OrderSelect(ticket)待成交的挂单
历史 HistoryHistoryDealsTotal() / HistoryOrdersTotal()HistorySelect(from,to) / HistoryDealSelect(ticket) / HistoryOrderSelect(ticket)已成交、已取消的记录
// MQL5:遍历持仓
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i); // 返回持仓号,同时把它选进缓存
    if(ticket == 0) continue;
    if(PositionGetString(POSITION_SYMBOL) == _Symbol
       && PositionGetInteger(POSITION_MAGIC) == 123)
    {
        double profit = PositionGetDouble(POSITION_PROFIT);
long type = PositionGetInteger(POSITION_TYPE);
        Print("持仓", ticket, " 盈亏", profit);
    }
}

// MQL5:遍历挂单(和持仓完全是两个循环)
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
ulong ticket = OrderGetTicket(i); // 同样是选中进缓存
    if(ticket == 0) continue;
    Print("挂单", ticket, " ", OrderGetDouble(ORDER_PRICE_OPEN));
}

3、字段读取方式的差别

  • MQL4:OrderProfit()——无参数,读快照;
  • MQL5:PositionGetDouble(POSITION_PROFIT)——带枚举参数,读"当前选中"的持仓缓存;
  • MQL5 的历史要另外用 History 系列查(见第十四章),MQL4 是 MODE_HISTORY 池。
白话注解:MQL4 把"手里拿的单子"和"挂出去等成交的单子"混在一起数。MQL5 分开数:持仓一栏、挂单一栏、历史一档。老习惯里 OrdersTotal() 包含挂单这一点,是迁移时最容易踩的思维惯性。

七、查持仓:PositionsTotal 与 PositionGetTicket

1、推荐的遍历姿势

PositionGetTicket(i) 是官方推荐的遍历方式:它返回索引 i 处的持仓单号,同时把这个持仓选进缓存,之后就能直接用 PositionGet\* 系列读字段。失败返回 0:

for(int i = PositionsTotal() - 1; i >= 0; i--)
{
    ulong ticket = PositionGetTicket(i);
    if(ticket == 0) continue;

    // 属性读取三兄弟:String / Double / Integer
    string symbol = PositionGetString(POSITION_SYMBOL);
long magic = PositionGetInteger(POSITION_MAGIC);
long ptype = PositionGetInteger(POSITION_TYPE); // POSITION_TYPE_BUY / SELL
    double volume = PositionGetDouble(POSITION_VOLUME);
double open = PositionGetDouble(POSITION_PRICE_OPEN);
    double profit = PositionGetDouble(POSITION_PROFIT);
double sl = PositionGetDouble(POSITION_SL);
double tp = PositionGetDouble(POSITION_TP);
}

2、常用持仓属性枚举

枚举含义
POSITION_TICKET持仓单号
POSITION_IDENTIFIER持仓标识符(净持仓反转时不变,见第十六章)
POSITION_SYMBOL品种名
POSITION_MAGIC魔术号(区分策略的钥匙)
POSITION_TYPEPOSITION_TYPE_BUY / POSITION_TYPE_SELL
POSITION_VOLUME手数
POSITION_PRICE_OPEN / POSITION_PRICE_CURRENT开仓价 / 现价
POSITION_SL / POSITION_TP止损价 / 止盈价
POSITION_PROFIT浮动盈亏
POSITION_SWAP隔夜利息
POSITION_TIME开仓时间
POSITION_REASON开仓原因(CLIENT/MOBILE/EXPERT 等)

3、警告:PositionSelect(symbol) 在锁仓账户只选一条

PositionSelect(_Symbol) 按品种选持仓,在锁仓(hedging)账户上同品种有多条持仓时,它只会选中其中一条(最早开仓的那条)。想逐条处理必须用 PositionGetTicket(i) 遍历。这是"MT4 习惯直接搬过来"的高发错位点。

白话注解:PositionSelect 像"按名字点名"——净持仓账户下每品种只有一个仓位,点名很安全;锁仓账户下同名仓位一大排,点名只叫得动一个。要挨个处理,就按索引一个个来。

八、挂单:BuyLimit / BuyStop / SellLimit / SellStop

1、四个便捷方法(官方签名)

官方文档 BuyLimit 的完整签名(BuyStop/SellLimit/SellStop 同构,只是方向和价位逻辑不同):

bool BuyLimit(
double volume, // 手数
double price, // 挂单价格(必须低于现价)
const string symbol = NULL, // 品种,缺省当前
double sl = 0.0, // 止损
double tp = 0.0, // 止盈
ENUM_ORDER_TYPE_TIME type_time = ORDER_TIME_GTC, // 有效期类型
datetime expiration = 0, // 过期时间(仅 SPECIFIED 有效)
const string comment = "" // 注释
);
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double entry = NormalizeDouble(ask - 100 * _Point, _Digits);

if(trade.BuyLimit(0.1, entry, _Symbol, 0, 0, ORDER_TIME_GTC, 0, "回踩买单"))
    Print("挂单提交,单号", trade.ResultOrder());

2、有效期类型 ORDER_TIME_GTC / ORDER_TIME_DAY / ORDER_TIME_SPECIFIED

  • ORDER_TIME_GTC:直到手动撤销;
  • ORDER_TIME_DAY:当日有效;
  • ORDER_TIME_SPECIFIED:到 expiration 时间点过期,此时才需要填 expiration 参数。

3、挂单的查询与撤销

// 遍历挂单
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
    ulong ticket = OrderGetTicket(i);
    if(ticket == 0) continue;
    if(OrderGetString(ORDER_SYMBOL) == _Symbol
       && OrderGetInteger(ORDER_MAGIC) == 12345)
    {
trade.OrderDelete(ticket); // 撤单
    }
}

// 修改挂单价格/止损止盈(用单号)
trade.OrderModify(ticket, newPrice, newSL, newTP, ORDER_TIME_GTC, 0);

挂单被触发后,它就从 Orders(挂单)集合消失,变成 Positions(持仓)或 History(历史)里的记录——写状态机时别再用挂单集合判断"仓位是否存在"。

白话注解:MQL4 的挂单和持仓混在一个池子里,靠 OrderType 猜。MQL5 里挂单是独立集合,触发即"搬家"。就像快递:在途(Orders)和签收(Positions)分开查,别到签收架上去找在途件。

九、价格、点值与品种属性

1、警告:MQL5 没有 Ask / Bid 预定义变量

MQL4 里到处能写的 Ask、Bid,在 MQL5 里不存在。官方示例统一用 SymbolInfoDouble() 取价:

double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

// 预定义变量 _Point / _Digits / _Symbol / _Period 在 MQL5 里可用
double sl = NormalizeDouble(ask - 250 * _Point, _Digits);

2、常用品种属性(SymbolInfo\* 体系替代 MQL4 的 MarketInfo)

枚举含义读取函数
SYMBOL_ASK / SYMBOL_BID卖价 / 买价SymbolInfoDouble
SYMBOL_POINT / SYMBOL_DIGITS点值 / 小数位Double / Integer
SYMBOL_VOLUME_MIN最小手数SymbolInfoDouble
SYMBOL_VOLUME_MAX最大手数SymbolInfoDouble
SYMBOL_VOLUME_STEP手数步长SymbolInfoDouble
SYMBOL_VOLUME_LIMIT单向持仓+挂单总量上限SymbolInfoDouble
SYMBOL_TRADE_STOPS_LEVEL止损/挂单距现价的最小距离(点)SymbolInfoInteger
SYMBOL_TRADE_FREEZE_LEVEL交易冻结距离(点)SymbolInfoInteger
SYMBOL_TRADE_TICK_SIZE最小价格变动SymbolInfoDouble
SYMBOL_TRADE_TICK_VALUE每点价值(结算货币)SymbolInfoDouble
SYMBOL_TRADE_CONTRACT_SIZE合约大小SymbolInfoDouble
SYMBOL_SWAP_LONG / SYMBOL_SWAP_SHORT多/空隔夜利息SymbolInfoDouble

3、手数规范化标准函数

double NormalizeVolume(double lots)
{
double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double vmax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
    double vstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

    lots = MathMax(vmin, MathMin(vmax, lots));
lots = MathFloor(lots / vstep) * vstep; // 对齐步长
    return lots;
}
白话注解:MarketInfo 在 MQL5 里整个退休,换成 SymbolInfo 家族。手数不按步长对齐、止损贴着现价,服务器直接拒单——先查属性再下单,比事后看错误码省事。

十、MQL4 MQL5 函数对照总表

迁移 80% 的工作就是查这张表。函数名与语义均出自两份官方参考手册:

MQL4MQL5 等价物备注
OrderSend()(市价)trade.Buy() / trade.Sell() 或 OrderSend(MqlTradeRequest)CTrade 更省事
OrderSend()(挂单)trade.BuyLimit/BuyStop/SellLimit/SellStop()一函数拆四个方法
OrderClose(ticket, ...)trade.PositionClose(ticket或symbol)锁仓账户建议用 ticket
OrderCloseBy()trade.PositionCloseBy(ticket1, ticket2)对冲互平
OrderModify()(持仓 SL/TP)trade.PositionModify(ticket或symbol, sl, tp)挂单改价仍是 trade.OrderModify()
OrderModify()(挂单价格)trade.OrderModify(ticket, price, sl, tp, type_time, expiration)—
OrderDelete()trade.OrderDelete(ticket)—
OrderSelect(i, SELECT_BY_POS, MODE_TRADES)PositionGetTicket(i)(持仓)/ OrderGetTicket(i)(挂单)池子拆成两个
OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)HistorySelect(from,to) + HistoryDealsTotal()/HistoryDealGetTicket(i)见第十四章
OrderTicket()PositionGetInteger(POSITION_TICKET) 等改为属性枚举读取
OrderProfit()PositionGetDouble(POSITION_PROFIT)历史成交用 HistoryDealGetDouble(t, DEAL_PROFIT)
OrderLots() / OrderOpenPrice() / OrderStopLoss() / OrderTakeProfit() / OrderMagicNumber() / OrderSymbol()PositionGetDouble/Integer/String(POSITION_*)—
OrdersTotal()(含持仓+挂单)PositionsTotal() + OrdersTotal()(各管各的)语义变了,别直接替换
OrdersHistoryTotal()HistoryDealsTotal()(先 HistorySelect)—
Ask / Bid(预定义变量)SymbolInfoDouble(sym, SYMBOL_ASK / SYMBOL_BID)MQL5 无此预定义变量
Point / Digits_Point / _Digits(或 SymbolInfo 系列)加下划线
MarketInfo(...)SymbolInfoDouble / SymbolInfoInteger / SymbolInfoString见第九章属性表
AccountBalance() / AccountEquity() / AccountFreeMargin()AccountInfoDouble(ACCOUNT_BALANCE / ACCOUNT_EQUITY / ACCOUNT_MARGIN_FREE)—
AccountInfo()、账户名等AccountInfoInteger / AccountInfoString(ACCOUNT_LOGIN、ACCOUNT_COMPANY 等)—
iMA(..., shift) 返回数值iMA(...) 返回句柄 + CopyBuffer()见第八章
iClose/iHigh/iLow/iOpen/iTime(sym, tf, shift)同名函数存在(官方为方便迁移加入 MQL5)见第十八章
IsTradeAllowed() 等TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)、MQLInfoInteger(MQL_TRADE_ALLOWED)、AccountInfoInteger(ACCOUNT_TRADE_ALLOWED/EXPERT_ALLOWED)权限拆得更细
RefreshRates()无对应物,不需要MQL5 每次查询实时取
白话注解:这张表贴墙上。迁移时从上往下过一遍,过了这张表,你的 EA 至少能编译通过;能不能赚钱,跟语言没关系。

十一、底层下单:MqlTradeRequest 与 OrderSend

1、CTrade 帮你干了什么

MQL5 真正的下单入口只有两个函数:OrderSend()(同步,等服务器回话)和 OrderSendAsync()(异步,发出就走,结果通过 OnTradeTransaction 回来看)。它们接收一个 MqlTradeRequest 结构,字段十几个,按操作类型填不同的组合:

// 市价买入的最小可用示例(官方文章同款写法)
MqlTradeRequest request = {};
MqlTradeResult result = {};

request.action = TRADE_ACTION_DEAL; // 市价操作
request.symbol = _Symbol;
request.volume = 0.1;
request.type = ORDER_TYPE_BUY;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.sl = 0.0;
request.tp = 0.0;
request.deviation = 30; // 允许滑点(点)
request.magic = 12345;
request.comment = "raw OrderSend";

if(!OrderSend(request, result))
    Print("发送失败: ", GetLastError());
else
Print("服务器返回码: ", result.retcode); // TRADE_RETCODE_DONE=已执行

2、action 字段的六个值

枚举用途
TRADE_ACTION_DEAL市价开/平仓
TRADE_ACTION_PENDING下挂单
TRADE_ACTION_SLTP改持仓的 SL/TP
TRADE_ACTION_MODIFY改挂单参数
TRADE_ACTION_REMOVE撤挂单
TRADE_ACTION_CLOSE_BY反向持仓互平

3、什么时候需要裸写 OrderSend

  • 日常写 EA:不需要,CTrade 全覆盖;
  • 需要精修每个字段(如部分成交后的特殊处理)、或用 OrderSendAsync 追求低延迟时才碰;
  • OrderSendAsync 发出后立即返回,结果不写在返回值里,要靠 OnTradeTransaction 里的 TRADE_TRANSACTION_REQUEST 事件拿 result(见第十五章)。
白话注解:MqlTradeRequest 像一张十几个格子的申请表,格子按操作类型填,填错一个就被打回。CTrade 就是"代办窗口"——你递一句话,它替你把表填好。除非你要卡流程细节,否则别自己去填表。

十二、交易历史:HistorySelect 与成交查询

1、MQL4 与 MQL5 的历史模型差异

MQL4 历史也是订单池的一部分(OrdersHistoryTotal() + OrderSelect + OrderCloseTime 判断)。MQL5 把历史拆成订单历史和成交(Deal)历史两条线,而且必须先用 HistorySelect() 把时间段拉进缓存,才能统计:

// 统计今日已实现盈亏(按本 EA 的 magic 过滤)
datetime dayStart = iTime(_Symbol, PERIOD_D1, 0); // 今日零点(服务器时间口径见18章)

if(HistorySelect(dayStart, TimeCurrent())) // 先拉历史进缓存,必须!
{
    double total = 0;
    int deals = HistoryDealsTotal();
    for(int i = 0; i < deals; i++)
    {
        ulong dealTicket = HistoryDealGetTicket(i);
        if(dealTicket == 0) continue;
        if(HistoryDealGetString(dealTicket, DEAL_SYMBOL) != _Symbol) continue;
        if(HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != 12345) continue;
        if(HistoryDealGetInteger(dealTicket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; // 只算平仓单

        total += HistoryDealGetDouble(dealTicket, DEAL_PROFIT)
               + HistoryDealGetDouble(dealTicket, DEAL_SWAP)
               + HistoryDealGetDouble(dealTicket, DEAL_COMMISSION);
    }
    Print("今日已实现盈亏: ", total);
}

2、常用 Deal 属性

枚举含义
DEAL_SYMBOL品种
DEAL_MAGIC魔术号
DEAL_TYPEDEAL_TYPE_BUY / DEAL_TYPE_SELL / BALANCE 等
DEAL_ENTRYDEAL_ENTRY_IN(开)/ OUT(平)/ INOUT(反转)/ OUT_BY(互平)
DEAL_PROFIT / DEAL_SWAP / DEAL_COMMISSION盈亏 / 隔夜利息 / 佣金
DEAL_VOLUME手数
DEAL_PRICE成交价
DEAL_REASONDEAL_REASON_CLIENT / MOBILE / EXPERT / SL / TP(可判断是止损还是止盈触发)
DEAL_POSITION_ID所属持仓标识(与 POSITION_IDENTIFIER 对应)

3、按持仓查历史

HistorySelectByPosition(position_identifier) 可以只拉某一笔持仓(含其加仓、减仓全部成交)的历史,适合做"每笔交易的完整生命周期"统计。

白话注解:MQL5 的历史是"先选定时间窗,再翻抽屉"。忘了 HistorySelect 就去数成交,数出来永远是 0——这是迁移后最经典的"为什么我的历史统计是零"问题。

十三、事件模型:OnTrade / OnTradeTransaction

1、MQL5 的完整事件清单

事件函数触发时机
OnInit / OnDeinit加载 / 卸载
OnTick新报价
OnTimerEventSetTimer 设定的定时器到期
OnTrade账户交易活动发生变化
OnTradeTransaction收到具体交易事务(明细)
OnTester回测结束后(自定义优化评分)
OnChartEvent图表交互事件(按钮、键盘等)

MT4 里靠 OnTick 轮询账户状态才能做到的事,MQL5 有事件推送,写法更干净。

2、OnTradeTransaction 签名与事务类型(官方全表)

void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type == TRADE_TRANSACTION_DEAL_ADD) // 新成交入历史
    {
        if(HistoryDealSelect(trans.deal))
        {
            long magic = HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
            if(magic == 12345)
                Print("本 EA 有新成交: ", trans.deal);
        }
    }
}
事务类型含义
TRADE_TRANSACTION_ORDER_ADD / ORDER_UPDATE / ORDER_DELETE活跃挂单的新增 / 更新 / 移出
TRADE_TRANSACTION_DEAL_ADD / DEAL_UPDATE / DEAL_DELETE成交历史的新增 / 更新 / 删除
TRADE_TRANSACTION_HISTORY_ADD / HISTORY_UPDATE / HISTORY_DELETE订单历史的新增 / 更新 / 删除
TRADE_TRANSACTION_POSITION非成交原因的持仓变化(如服务器端改 SL/TP)
TRADE_TRANSACTION_REQUEST交易请求已被服务器处理(此时才需要看 request/result 两个参数)

3、官方文档明确的三条注意事项

  1. 到达顺序不保证:别假设某类事务一定在另一类之后到达;
  2. 队列长度 1024:处理太慢时旧事务会被新事务挤掉,别把重活压在 OnTradeTransaction 里;
  3. OnTrade 与 OnTradeTransaction 无一一对应:一次请求可能触发多个事务,一次 OnTrade 可能对应多次事务调用。
白话注解:OnTick 是"每秒看一眼大盘",OnTrade 是"账户有动静叫你一声",OnTradeTransaction 是"把动静的明细单递给你"。明细单可能乱序、可能挤丢,所以明细只做记录和轻处理,重活回 OnTick 或状态机里做。

十四、手数校验与止损距离

1、下单前三查

bool ValidateOrder(double lots, double sl, double tp)
{
    // 1) 手数:最小/最大/步长
double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double vmax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
    double vstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    if(lots < vmin || lots > vmax) return false;
if(MathMod(lots, vstep) != 0) return false; // 更稳妥是先归一化(第9章)

    // 2) 止损距离:距现价不能小于 STOPS_LEVEL
    long stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
    double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if(sl > 0 && (bid - sl) / _Point < stopsLevel) return false; // 多单止损太近
if(tp > 0 && (tp - ask) / _Point < stopsLevel) return false; // 多单止盈太近

    // 3) 资金预检(官方函数,不实际下单)
    MqlTradeRequest req = {};
    MqlTradeCheckResult chk = {};
    req.action = TRADE_ACTION_DEAL;
    req.symbol = _Symbol;
    req.volume = lots;
req.type = ORDER_TYPE_BUY;
req.price = ask;
    if(!OrderCheck(req, chk))
    {
        Print("预检失败: ", chk.retcode);
        return false;
    }
    return true;
}

2、冻结距离 FREEZE_LEVEL

SYMBOL_TRADE_FREEZE_LEVEL 是"冻结区":现价离挂单触发价或 SL/TP 太近时,改单撤单会被服务器拒绝。修改止损前先确认新价不在冻结区里,否则会遇到看似莫名其妙的拒单。

白话注解:STOPS_LEVEL 是"止损别贴脸",FREEZE_LEVEL 是"快触发的单子别乱动"。服务器规则查一下只要一行代码,撞上去浪费的是真金白银的拒单和延迟。

十五、多品种与多周期

1、好消息:MQL4 的便捷函数在 MQL5 里都有

早期 MQL5 只能用 Copy\* 家族拷数组,很多迁移者自己写了 iClose 的替代品。后来 MetaTrader 5 官方更新日志明确宣布:加入了 iTime、iOpen、iHigh、iLow、iClose、iVolume、iBars、iBarShift、iHighest、iLowest、iRealVolume、iTickVolume、iSpread,"这些函数与 MQL4 中使用的类似,为代码向第五代平台迁移提供便利"。签名同 MQL4:

double prevClose = iClose(_Symbol, PERIOD_H1, 1); // H1 上一根收盘价
datetime barTime = iTime(_Symbol, PERIOD_CURRENT, 0); // 当前 K 线开盘时间
int idx = iBarShift(_Symbol, PERIOD_H4, someTime); // 时间换 K 线序号
int hiIdx = iHighest(_Symbol, PERIOD_M15, MODE_HIGH, 20, 1); // 近20根最高点位置

2、一次性拿整段 K 线:CopyRates 与 MqlRates

官方 MqlRates 结构全字段:

MqlRates rates[];
ArraySetAsSeries(rates, true);
int n = CopyRates(_Symbol, PERIOD_H1, 0, 100, rates);
if(n > 0)
{
    // rates[0].time / open / high / low / close
    // rates[0].tick_volume(跳动量) / real_volume(交易所真实量)
    // rates[0].spread(该 K 线最小点差)
}

3、多品种 EA 的两个要点

  1. 品种名要写全:不同券商的黄金可能叫 XAUUSD、GOLD、XAUUSDm——写 EA 时用 SymbolInfo 查实际名称或做成输入参数,别硬编码;
  2. 别的品种没数据时要先请求:CopyRates 对未加载品种可能返回 -1,属于数据在下载中的正常现象,重试即可;测试器里多品种回测会自动处理数据请求。
// 多周期指标:H4 的均线句柄
int handleMA_H4 = iMA(_Symbol, PERIOD_H4, 20, 0, MODE_EMA, PRICE_CLOSE);
白话注解:MT4 时代"EA 只能看自己图表那个周期"的痛苦,MT5 直接解掉——一个 EA 同时盯 M15 信号和 H4 趋势,甚至同时盯多个品种,都只是多几个句柄的事。

十六、新旧语法差异速览

1、编译严格度

  • MQL4 build 600+ 引入 #property strict,不开就还是老宽松语法;
  • MQL5 天生严格:没有隐式转换警告豁免,bool/整型混用、未初始化变量都会被抓出来。

2、高频差异清单

项目MQL4(build 600+)MQL5
点值 / 小数位Point / Digits(或 Point() 函数)_Point / _Digits
品种 / 周期Symbol() / Period()_Symbol / _Period(Symbol() 函数也存在)
现价Ask / Bid 预定义变量SymbolInfoDouble(SYMBOL_ASK / SYMBOL_BID)
刷新报价RefreshRates()不存在,也不需要
输入参数extern / inputinput(支持 input group 分组)
字符串格式化拼接为主StringFormat() / PrintFormat() 常用
枚举转文字手写 switchEnumToString() 直接用
类与对象支持但不普及标准库庞大(CTrade、CArrayObj、CIndicators……)
事件函数OnTick / start()OnTick / OnTimer / OnTrade / OnTradeTransaction / OnTester

3、一个容易忽略的点:数组方向

MQL5 的 Copy 系列拷出来的数组默认最老的在 [0]。MQL4 习惯最新 K 线是 [0] 的人,必须记得 ArraySetAsSeries(buf, true),否则金叉判断全是反的——而且不报错,只是悄悄亏钱。

白话注解:语法差异多数编译器会教你,唯独"数组方向反了"这种逻辑错误编译器一声不吭。迁移后第一件事:用 Print 验证你的 [0] 到底是哪根 K 线。

十七、迁移实战:双均线 EA 改写全流程

1、MQL4 原版(第 1、2 课的双均线示例)

// MQL4:金叉买,死叉平
extern int FastMA = 10;
extern int SlowMA = 30;
extern double Lots = 0.1;
extern int Magic = 12345;

int start()
{
double fastNow = iMA(Symbol(), 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 0);
    double fastPrev = iMA(Symbol(), 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 1);
double slowNow = iMA(Symbol(), 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 0);
    double slowPrev = iMA(Symbol(), 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 1);

    // 只用已收盘K线:判断上一根是否金叉
    if(fastPrev <= slowPrev && fastNow > slowNow)
    {
        if(CountMyPositions() == 0)
            OrderSend(Symbol(), OP_BUY, Lots, Ask, 30, 0, 0, "GoldenCross", Magic, 0, Blue);
    }
    return(0);
}

int CountMyPositions()
{
    int c = 0;
    for(int i = OrdersTotal() - 1; i >= 0; i--)
        if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
            if(OrderSymbol() == Symbol() && OrderMagicNumber() == Magic)
                c++;
    return(c);
}

2、MQL5 重写版(每一步对应前文章节)

// MQL5 重写:句柄在 OnInit(第8章)、持仓用 PositionsTotal(第6章)、
// 下单用 CTrade(第5章)、新K线检测用 iTime(第18章)
#include <Trade/Trade.mqh>

input int FastMA = 10; // 快线周期
input int SlowMA = 30; // 慢线周期
input double Lots = 0.1; // 手数
input ulong Magic = 12345; // 魔术号

CTrade trade;
int hFast = INVALID_HANDLE;
int hSlow = INVALID_HANDLE;
datetime lastBarTime = 0;

int OnInit()
{
    hFast = iMA(_Symbol, PERIOD_CURRENT, FastMA, 0, MODE_EMA, PRICE_CLOSE);
    hSlow = iMA(_Symbol, PERIOD_CURRENT, SlowMA, 0, MODE_EMA, PRICE_CLOSE);
    if(hFast == INVALID_HANDLE || hSlow == INVALID_HANDLE)
        return(INIT_FAILED);

    trade.SetExpertMagicNumber(Magic);
    trade.SetDeviationInPoints(30);
    trade.SetTypeFillingBySymbol(_Symbol);
    return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
    // 注:回测器中 IndicatorRelease 不执行(官方文档),实盘图表上有意义
    if(hFast != INVALID_HANDLE) IndicatorRelease(hFast);
    if(hSlow != INVALID_HANDLE) IndicatorRelease(hSlow);
}

void OnTick()
{
    // 新K线检测:每根K线只在开盘后判断一次(防同一根K线反复开仓)
    datetime barTime = iTime(_Symbol, PERIOD_CURRENT, 0);
    if(barTime == lastBarTime) return;
    lastBarTime = barTime;

    // 取最近3根K线的指标值
    double fast[], slow[];
    ArraySetAsSeries(fast, true);
    ArraySetAsSeries(slow, true);
    if(CopyBuffer(hFast, 0, 0, 3, fast) < 3) return;
    if(CopyBuffer(hSlow, 0, 0, 3, slow) < 3) return;

    // 用已收盘K线 [1] 和 [2] 判断金叉,杜绝未来函数
    bool goldenCross = fast[2] <= slow[2] && fast[1] > slow[1];
bool deadCross = fast[2] >= slow[2] && fast[1] < slow[1];

    long mode = AccountInfoInteger(ACCOUNT_MARGIN_MODE);
    bool isHedging = (mode == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);

    if(deadCross)
    {
        // 平掉本策略所有持仓(锁仓账户逐条平;净持仓账户直接按 symbol 平)
        for(int i = PositionsTotal() - 1; i >= 0; i--)
        {
            ulong ticket = PositionGetTicket(i);
            if(ticket == 0) continue;
            if(PositionGetInteger(POSITION_MAGIC) == (long)Magic
               && PositionGetString(POSITION_SYMBOL) == _Symbol)
                trade.PositionClose(isHedging ? ticket : (ulong)_Symbol);
        }
    }

    if(goldenCross && CountMyPositions() == 0)
    {
        double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        trade.Buy(Lots, _Symbol, ask, 0, 0, "GoldenCross");
// 返回 true 只是请求受理,生产代码要再查 ResultRetcode(第5/13章)
    }
}

int CountMyPositions()
{
    int c = 0;
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if(ticket == 0) continue;
        if(PositionGetInteger(POSITION_MAGIC) == (long)Magic
           && PositionGetString(POSITION_SYMBOL) == _Symbol)
            c++;
    }
    return c;
}

3、改了哪几刀(复盘)

  1. start() → OnTick(),extern → input;
  2. 四次 iMA(...) 直取值 → 两个句柄 + CopyBuffer;
  3. Ask → SymbolInfoDouble(SYMBOL_ASK);
  4. OrdersTotal() 池遍历 → PositionsTotal() + PositionGetTicket();
  5. OrderSend() → trade.Buy() + 魔术号前置配置;
  6. 增加新 K 线检测和账户模式判断(MQL4 时代不存在的问题)。
白话注解:迁移不是"翻译单词",是"换水电煤"。逻辑(金叉买死叉平)一行没变,但取数、下单、查仓三条管线全换了新接口。逻辑不变、管线重铺,这就是迁移的全部心法。

十八、工程化建议与 VPS 部署

1、魔术号与状态管理

  • 每个策略实例一个独立 magic,账户对账、历史统计、风控都按 magic 切分;
  • 策略内部状态(是否已触发、连亏次数)用 EA 全局变量 + 持久化文件(FileOpen 写在 MQL5/Files 沙箱)保存,应对终端重启;
  • 手动单 magic 为 0,EA 遍历时务必过滤,别替人工单做决定。

2、防重复下单三件套

  1. 新 K 线检测(第20章的 iTime 方案);
  2. 下单前按 magic+symbol 检查是否已有持仓/挂单;
  3. 对提交未确认的请求做超时重查(结合 OnTradeTransaction 或 HistorySelect),而不是无脑重发。

3、VPS 部署(MT5 与 MT4 思路一致)

  1. 选离券商服务器近的 VPS(外汇常用伦敦/纽约节点,MT5 自带 ping 显示,可在终端右下角看延迟);
  2. 装 MT5 客户端,登录账号,勾选"算法交易"总开关(工具→选项→算法交易),EA 属性里也允许算法交易;
  3. EA 挂上图表,确认图表右上角出现算法交易图标(非灰);
  4. 远程桌面断开不影响 EA 运行(VPS 是不关机的远程电脑);终端重启后 EA 是否自动恢复,取决于终端设置里的"启动后恢复 EA"选项,要实测确认。
白话注解:本地电脑跑 EA,Windows 更新一次重启就可能漏一晚行情。VPS 是 7×24 不关机的远程电脑,是自动交易的标配基建,不是可选项。
风险口径:回测≠实盘,历史业绩不代表未来收益。自动交易放大执行速度的同时也放大错误速度,上实盘前必须在模拟盘完整跑通异常分支(断线、拒单、休市)。

十九、迁移检查清单

逐项打勾,全过再上模拟盘:

  • [ ] init/start/deinit → OnInit/OnTick/OnDeinit
  • [ ] OrderSend() → trade.Buy()/Sell()/BuyLimit()/BuyStop()/SellLimit()/SellStop()
  • [ ] OrderClose() → trade.PositionClose(ticket)(锁仓账户按 ticket)
  • [ ] OrderModify() → trade.PositionModify()(持仓)/ trade.OrderModify()(挂单)
  • [ ] OrdersTotal()+OrderSelect → PositionsTotal()+PositionGetTicket(持仓)、OrderGetTicket(挂单)
  • [ ] MODE_HISTORY 遍历 → HistorySelect()+HistoryDealsTotal()
  • [ ] iMA(...,shift) 直取值 → 句柄(OnInit 建)+ CopyBuffer()
  • [ ] Ask/Bid → SymbolInfoDouble(SYMBOL_ASK/SYMBOL_BID)
  • [ ] Point/Digits → _Point/_Digits
  • [ ] MarketInfo() → SymbolInfo 系列
  • [ ] AccountBalance() 等 → AccountInfoDouble(ACCOUNT_*)
  • [ ] 所有数组按需 ArraySetAsSeries(arr, true)
  • [ ] 每次下单后检查 ResultRetcode()
  • [ ] 已判断账户模式(ACCOUNT_MARGIN_MODE)并适配逻辑
  • [ ] 手数规范化(VOLUME_MIN/MAX/STEP)+ 止损距离校验(STOPS_LEVEL)
  • [ ] #include <Trade/Trade.mqh> 已引入,magic 已设置
  • [ ] 删除了对 RefreshRates() 的调用
付费专享 · 核心知识点 7 章

继续阅读:核心知识点,搞懂原理

EA03 MQL4迁移MQL5指南 — 共 26 章,已免费阅读 19 章,剩余 7 章核心知识点讲透「为什么这样、点了会怎样」。

二十、指标取值:句柄 + CopyBuffer
二十一、回测:MT5 策略测试器
二十二、结果码与错误处理
二十三、常见迁移坑 Top 10
二十四、FAQ 与速查表
二十五、学习路线
…等共 7 章核心内容
✍️ 注册后扫码购买 🔐 已有账号,去登录 单套 ¥69 永久 · 会员可阅
SERVICE & PRICING
服务收费说明 · 明码标价,无隐藏收费
全部课程统一 ¥39-69,买一套永久使用。以下为购买与使用的完整流程。
01先免费试读

每套教程的「使用手册」部分均可免费阅读,先确认内容是否适合自己再决定,不必盲买。

02扫码付款

选定课程后点「立即解锁」,页面展示收款二维码与客服微信,扫码转账即可,无需注册海外支付。

03获取卡密

付款完成后客服第一时间发送卡密号与卡密密码,站内消息与微信同步通知。

04兑换解锁

在用户中心「兑换卡密」输入卡密号与密码,系统即时解锁全部章节,无需二次付款。

一次付费,终身可用 · 解锁后课程永久留在账号里,不限次数、不限时间阅读 持续免费更新 · 平台改版、规则变化时课程同步修订
RECOMMEND
相关教程推荐 · 同栏目更多上手机会

💬 用户评价 (0)

🔒 登录并成为会员后可发表评价
还没有评价 · 成为第一个评价的人吧 🎯