使用手册
功能在哪 · 是什么 · 怎么调出来一、下单: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 逻辑的三条硬影响
- 反向单的语义变了:净持仓账户下,卖出不是"开空",而是"减仓/平仓/反转现有多单"。MT4 上的网格、马丁、锁仓对冲逻辑直接搬过来,行为会完全错乱;
- 平仓按什么定位:
trade.PositionClose(symbol)在锁仓账户下平的是单号最小的那条持仓;要精确平某一条,用PositionClose(ticket); - 持仓标识符:净持仓反转时
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,不允许锁仓。
操作教程
一步一步 · 跟着做完一件事三、为什么要迁移到 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、打开方式
- 安装 MetaTrader 5 客户端并登录(模拟账号即可开发);
- 按 F4 或点工具栏的 IDE 图标,打开 MetaEditor 5;
- 右键 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:三个独立集合
| 集合 | 数量函数 | 选择函数 | 内容 |
|---|---|---|---|
| 持仓 Positions | PositionsTotal() | PositionGetTicket(i) / PositionSelect(symbol) / PositionSelectByTicket(ticket) | 市价持仓 |
| 挂单 Orders | OrdersTotal() | OrderGetTicket(i) / OrderSelect(ticket) | 待成交的挂单 |
| 历史 History | HistoryDealsTotal() / 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_TYPE | POSITION_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% 的工作就是查这张表。函数名与语义均出自两份官方参考手册:
| MQL4 | MQL5 等价物 | 备注 |
|---|---|---|
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_TYPE | DEAL_TYPE_BUY / DEAL_TYPE_SELL / BALANCE 等 |
DEAL_ENTRY | DEAL_ENTRY_IN(开)/ OUT(平)/ INOUT(反转)/ OUT_BY(互平) |
DEAL_PROFIT / DEAL_SWAP / DEAL_COMMISSION | 盈亏 / 隔夜利息 / 佣金 |
DEAL_VOLUME | 手数 |
DEAL_PRICE | 成交价 |
DEAL_REASON | DEAL_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 | 新报价 |
OnTimer | EventSetTimer 设定的定时器到期 |
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、官方文档明确的三条注意事项
- 到达顺序不保证:别假设某类事务一定在另一类之后到达;
- 队列长度 1024:处理太慢时旧事务会被新事务挤掉,别把重活压在 OnTradeTransaction 里;
- 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 的两个要点
- 品种名要写全:不同券商的黄金可能叫 XAUUSD、GOLD、XAUUSDm——写 EA 时用 SymbolInfo 查实际名称或做成输入参数,别硬编码;
- 别的品种没数据时要先请求: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 / input | input(支持 input group 分组) |
| 字符串格式化 | 拼接为主 | StringFormat() / PrintFormat() 常用 |
| 枚举转文字 | 手写 switch | EnumToString() 直接用 |
| 类与对象 | 支持但不普及 | 标准库庞大(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、改了哪几刀(复盘)
start()→OnTick(),extern→input;- 四次
iMA(...)直取值 → 两个句柄 + CopyBuffer; Ask→SymbolInfoDouble(SYMBOL_ASK);OrdersTotal()池遍历 →PositionsTotal()+PositionGetTicket();OrderSend()→trade.Buy()+ 魔术号前置配置;- 增加新 K 线检测和账户模式判断(MQL4 时代不存在的问题)。
白话注解:迁移不是"翻译单词",是"换水电煤"。逻辑(金叉买死叉平)一行没变,但取数、下单、查仓三条管线全换了新接口。逻辑不变、管线重铺,这就是迁移的全部心法。
十八、工程化建议与 VPS 部署
1、魔术号与状态管理
- 每个策略实例一个独立 magic,账户对账、历史统计、风控都按 magic 切分;
- 策略内部状态(是否已触发、连亏次数)用 EA 全局变量 + 持久化文件(
FileOpen写在 MQL5/Files 沙箱)保存,应对终端重启; - 手动单 magic 为 0,EA 遍历时务必过滤,别替人工单做决定。
2、防重复下单三件套
- 新 K 线检测(第20章的 iTime 方案);
- 下单前按 magic+symbol 检查是否已有持仓/挂单;
- 对提交未确认的请求做超时重查(结合 OnTradeTransaction 或 HistorySelect),而不是无脑重发。
3、VPS 部署(MT5 与 MT4 思路一致)
- 选离券商服务器近的 VPS(外汇常用伦敦/纽约节点,MT5 自带 ping 显示,可在终端右下角看延迟);
- 装 MT5 客户端,登录账号,勾选"算法交易"总开关(工具→选项→算法交易),EA 属性里也允许算法交易;
- EA 挂上图表,确认图表右上角出现算法交易图标(非灰);
- 远程桌面断开不影响 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()的调用
💬 用户评价 (0)
立即登录