使用手册
功能在哪 · 是什么 · 怎么调出来一、ECN 账户:先下单、后设损
1、问题:单子刚发就被 130 拒绝
部分 ECN / STP 券商不允许在 OrderSend 里同时携带止损止盈——市价单发出时还不知道成交价,带着 SL/TP 会直接报 130(ERR_INVALID_STOPS)。标准解法是两步走:
// 第一步:裸单进场(sl=0, tp=0)
int ticket = OrderSend(Symbol(), OP_BUY, Lots, Ask, 30, 0, 0,
"ecn", 12345, 0, Blue);
if(ticket < 0)
{
Print("ECN下单失败 error=", GetLastError());
return;
}
// 第二步:选中刚开的单,立刻补挂 SL/TP
if(OrderSelect(ticket, SELECT_BY_TICKET))
{
RefreshRates();
double sl = NormalizeDouble(OrderOpenPrice() - StopLossPts * Point, Digits);
double tp = NormalizeDouble(OrderOpenPrice() + TakeProfitPts * Point, Digits);
if(!OrderModify(ticket, OrderOpenPrice(), sl, tp, 0, Blue))
Print("补挂SL/TP失败 error=", GetLastError());
// 这里失败不能撤单了事:单子已经进场,必须记录并在下个tick重试补挂
}
2、两步之间的危险窗口
裸单和补挂之间有时间差,这几秒里这单是"无止损裸奔"。工程上的补救:
- 补挂失败时把 ticket 存进待处理列表,
OnTick里持续重试; - EA 重启时扫描所有"本魔术号但 SL=TP=0"的持仓单,统一补挂——这一步就是第十八章对账逻辑的一部分。
白话注解:先判断你的账户是不是 ECN 模式很简单——普通账户带着 SL/TP 能下单就没问题;如果实盘反复 130 而距离明明够宽,八成是 ECN。这套两步写法在两种账户上都能跑,干脆统一用它。
操作教程
一步一步 · 跟着做完一件事二、本课地图:从"会下单"到"管得住仓"
第 1 课你已经能写一个会自动开平仓的 EA。但真实市场里,下单只占 20% 的工作量,剩下 80% 是订单管理:
- 浮盈越来越多,止损怎么自动往上挪(追踪止损);
- 赚了一半想先落袋,怎么平掉部分仓位(部分平仓);
- 下单被服务器拒绝,怎么知道错在哪、要不要重试(错误码);
- 网线拔了又插上,EA 怎么确认自己没漏单(断线对账)。
本课全部围绕 MQL4 的交易函数族展开:OrderSend、OrderSelect、OrderModify、OrderClose、OrderDelete、OrderCloseBy。这 6 个函数就是你操纵订单池的全部入口,本课会逐个用透。
白话注解:第 1 课像学开车点火,本课学的是变道、倒车、刹车。不会订单管理的 EA,等于只会踩油门不会刹车。
三、订单池与遍历的标准姿势
1、一个账户就是一个订单池
MT4 里账户的所有订单(持仓单 + 挂单)都放在一个池子里。OrdersTotal() 返回池子里的总数,想操作其中任何一单,必须先用 OrderSelect() 把它"选中"。
2、OrderSelect 的两种选法、两个池
| 参数 | 取值 | 含义 |
|---|---|---|
| select | SELECT_BY_POS | 按位置选,index 是 0 起的下标 |
| select | SELECT_BY_TICKET | 按订单号选,index 传订单号;此时 pool 参数被忽略 |
| pool | MODE_TRADES(默认) | 当前持仓 + 挂单("持仓"标签页) |
| pool | MODE_HISTORY | 已平仓 + 已删除订单("账户历史"标签页,受历史深度显示范围限制) |
3、为什么平仓循环必须倒序
池子是个列表。你平掉了下标 2 的单子,后面所有单子的下标立刻前移。正序循环会跳单、越界。标准写法:
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
// ... 过滤 + 操作
}
白话注解:像拆一排积木,从最后一块往前拆,前面积木的位置就不会乱。只读遍历(比如统计数量)倒序正序都行,但养成倒序习惯,写平仓时就不会翻车。
四、订单属性函数全家桶
OrderSelect 选中一单后,下面这些函数返回的就是这一单的属性。这是 MQL4 用的最高频的一组函数:
| 函数 | 返回 | 备注 |
|---|---|---|
| OrderTicket() | int 订单号 | 账户内唯一,永不变 |
| OrderSymbol() | string 品种名 | 过滤别的品种的单 |
| OrderType() | int 订单类型 | OP_BUY / OP_SELL / OP_BUYLIMIT / OP_SELLLIMIT / OP_BUYSTOP / OP_SELLSTOP |
| OrderLots() | double 手数 | |
| OrderOpenPrice() | double 开仓价 | 挂单为委托价 |
| OrderOpenTime() | datetime 开仓时间 | |
| OrderStopLoss() | double 当前止损价 | 没设为 0 |
| OrderTakeProfit() | double 当前止盈价 | 没设为 0 |
| OrderClosePrice() | double 平仓价 | 持仓单返回当前市价 |
| OrderCloseTime() | datetime 平仓时间 | >0 表示已平仓(历史池) |
| OrderProfit() | double 浮动盈亏 | 不含点差手续费 |
| OrderSwap() | double 库存费 | 隔夜利息 |
| OrderCommission() | double 手续费 | |
| OrderMagicNumber() | int 魔术号 | EA 的"签名",见第十九章 |
| OrderComment() | string 注释 | 券商可能改写,不可作为唯一依据 |
| OrderExpiration() | datetime 挂单过期时间 | 仅挂单有效 |
注意:所有 Order 属性函数都作用于"当前被选中的订单"。没选中就调用,行为不可预期(对应错误 4105 ERR_NO_ORDER_SELECTED)。
五、追踪止损(Trailing Stop)
1、原理
每来一个 tick,遍历持仓:
- 多单浮盈超过 N 点后,把止损挪到"当前价 − N 点";
- 只往有利方向挪,绝不往回退。
2、代码(加固版)
//+------------------------------------------------------------------+
//| 追踪止损:每个 tick 调用一次 |
//+------------------------------------------------------------------+
void TrailingStop(int trailingPoints)
{
if(OrdersTotal() == 0) return;
// 最小止损距离(点),见第十四章
double stopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL);
if(trailingPoints < stopLevel)
{
Print("追踪距离 ", trailingPoints, " 点小于 STOPLEVEL ", stopLevel, ",会被拒单");
return;
}
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderSymbol() != Symbol()) continue;
if(OrderMagicNumber() != 12345) continue;
if(OrderType() != OP_BUY && OrderType() != OP_SELL) continue;
double newSL = 0;
if(OrderType() == OP_BUY)
{
double profitPoints = (Bid - OrderOpenPrice()) / Point;
if(profitPoints >= trailingPoints)
{
newSL = NormalizeDouble(Bid - trailingPoints * Point, Digits);
// 只上移不下移;无止损(0)时直接设
if(newSL > OrderStopLoss() || OrderStopLoss() == 0)
{
if(!OrderModify(OrderTicket(), OrderOpenPrice(), newSL,
OrderTakeProfit(), 0, Blue))
Print("修改多单止损失败, error=", GetLastError());
}
}
}
else if(OrderType() == OP_SELL)
{
double profitPoints = (OrderOpenPrice() - Ask) / Point;
if(profitPoints >= trailingPoints)
{
newSL = NormalizeDouble(Ask + trailingPoints * Point, Digits);
if(newSL < OrderStopLoss() || OrderStopLoss() == 0)
{
if(!OrderModify(OrderTicket(), OrderOpenPrice(), newSL,
OrderTakeProfit(), 0, Orange))
Print("修改空单止损失败, error=", GetLastError());
}
}
}
}
}
3、三个不能省的细节
- 方向判断:
newSL > OrderStopLoss()(多单)不能省,否则价格回撤时止损会被改回去,追踪止损变成"追跌止损"; - NormalizeDouble:新止损价必须按
Digits取整,否则可能出现 1.234560000001 这种浮点尾巴,直接报 129/130; - 止损距离:追踪距离必须 ≥ 券商的 STOPLEVEL,否则每次改单都报 130(见第十四章)。
注意:OrderModify里传的价格参数是"挂单新开仓价",对持仓单固定传OrderOpenPrice(),不要自己另算。
六、保本移损(Break Even)
追踪止损之外的另一个标配:浮盈到 N 点后,把止损推到开仓价附近,这笔单最差也是保本出局。
void MoveToBreakEven(int triggerPoints, int bufferPoints)
{
double spreadPts = MarketInfo(Symbol(), MODE_SPREAD); // 点差(点)
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderSymbol() != Symbol()) continue;
if(OrderMagicNumber() != 12345) continue;
if(OrderType() == OP_BUY)
{
if((Bid - OrderOpenPrice()) / Point >= triggerPoints)
{
// 多单保本价 = 开仓价 + 点差缓冲,平掉才真不亏
double be = NormalizeDouble(OrderOpenPrice() + (spreadPts + bufferPoints) * Point, Digits);
if(be > OrderStopLoss() || OrderStopLoss() == 0)
if(!OrderModify(OrderTicket(), OrderOpenPrice(), be,
OrderTakeProfit(), 0, Blue))
Print("保本移损失败, error=", GetLastError());
}
}
else if(OrderType() == OP_SELL)
{
if((OrderOpenPrice() - Ask) / Point >= triggerPoints)
{
double be = NormalizeDouble(OrderOpenPrice() - (spreadPts + bufferPoints) * Point, Digits);
if(be < OrderStopLoss() || OrderStopLoss() == 0)
if(!OrderModify(OrderTicket(), OrderOpenPrice(), be,
OrderTakeProfit(), 0, Orange))
Print("保本移损失败, error=", GetLastError());
}
}
}
}
// OnTick 里:MoveToBreakEven(30, 2); // 浮盈30点后,止损推到开仓价+点差+2点
白话注解:多单开在 1.1000,平仓价是 Bid。就算价格回到 1.1000 你平仓,拿到手也是 Bid≈1.0998(扣点差),其实是小亏。所以保本价要加一点缓冲,把点差盖住。
七、部分平仓:先平一半锁利润
1、基本写法
OrderClose 的手数参数允许小于该单总手数,就是部分平仓:
bool PartialCloseHalf(int ticket)
{
if(!OrderSelect(ticket, SELECT_BY_TICKET, MODE_TRADES)) return false;
double closeLots = NormalizeLots(OrderLots() / 2.0); // 手数规范化,见第十六章
if(closeLots <= 0) return false;
RefreshRates();
double price = (OrderType() == OP_BUY) ? Bid : Ask;
return OrderClose(ticket, closeLots, price, 30, Red);
}
2、关键真相:剩余仓位会换订单号
很多教程(包括本课旧版)都写错了一个事实,这里按实际行为纠正:
- 部分平仓成功后,被平掉的部分按原订单号进历史;
- 剩余仓位会得到一个新订单号继续留在持仓池里;
- 剩余仓位的 Magic Number 不变(靠魔术号管理的 EA 不受影响);
- 剩余仓位的 Comment 通常被改写成 "from #原订单号",但注释可能被券商覆写,不可全信;
- 剩余仓位的开仓价、开仓时间仍保留原值。
这意味着:部分平仓后不能用旧 ticket 继续管理这单,必须重新扫描订单池。官方论坛公认的稳妥做法是:平仓前记录池内所有 ticket,平仓后重扫一遍,找到那个"多出来的新 ticket"。
// 记录平仓前的全部 ticket
int before[]; int n = 0;
ArrayResize(before, OrdersTotal());
for(int i = 0; i < OrdersTotal(); i++)
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
before[n++] = OrderTicket();
OrderClose(ticket, halfLots, price, 30, Red);
// 重新扫描,找出不在 before 里的新 ticket
for(int i = 0; i < OrdersTotal(); i++)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
bool isNew = true;
for(int j = 0; j < n; j++)
if(before[j] == OrderTicket()) { isNew = false; break; }
if(isNew) Print("剩余仓位的新订单号: ", OrderTicket());
}
注意:不要用"原订单号 + 1"去猜新订单号。回测器里单号确实连号,但实盘单号没有可靠的连续规律,多账户多终端同时操作时必然猜错。
3、防零碎仓位
反复"平一半"会平出 0.01、0.02 手的零碎单,可能低于部分平仓的最低手数限制而被拒。触发前要判断:剩余手数和要平的手数都必须 ≥ MODE_MINLOT 且是 MODE_LOTSTEP 的整数倍(见第十六章)。
八、多持仓管理:计数、限仓、加仓间隔
1、统计自己 EA 的持仓
int CountPositions(int type)
{
int count = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderSymbol() != Symbol()) continue;
if(OrderMagicNumber() != 12345) continue;
if(OrderType() == type) count++;
}
return count;
}
double TotalLots()
{
double lots = 0;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderSymbol() != Symbol()) continue;
if(OrderMagicNumber() != 12345) continue;
if(OrderType() == OP_BUY || OrderType() == OP_SELL)
lots += OrderLots();
}
return lots;
}
2、开仓前的三道闸
// 三道闸:单笔数量、总手数、加仓间隔
if(CountPositions(OP_BUY) < 3 // 最多 3 笔多单
&& TotalLots() <= 1.0 // 总手数上限
&& TimeCurrent() - LastOpenTime(OP_BUY) >= 3600) // 距上次开仓至少1小时
{
// OrderSend(...)
}
白话注解:加仓类策略不做数量上限,等于给自己装了个资金绞肉机。行情单边走,EA 会一根接一根地加仓,直到保证金被吃光。三道闸缺一不可。
九、下单前的环境自检
1、三个检查函数,各管一件事(别混用)
| 函数 | 管什么 | 返回 false 的典型场景 |
|---|---|---|
| IsConnected() | 终端与服务器的网络连接 | 断网、服务器失联 |
| IsTradeAllowed() | 交易许可:自动交易按钮 + EA 属性"允许实时交易",且交易上下文空闲 | AutoTrading 按钮没开;EA 设置未勾允许;正有别的 EA 在交易 |
| IsTradeContextBusy() | 交易上下文是否被占用 | 同一时刻全终端只有一个交易线程,别的 EA 正在下单 |
纠正一个常见误解:不能用IsTradeContextBusy()判断断线。它检测的是"终端内部的交易线程忙不忙",和网络连接毫无关系。判断断线只能用IsConnected()。本课旧版在这里犯过错,特此更正。
2、标准自检流程
bool TradingAllowed()
{
if(!IsConnected()) { Print("未连接服务器"); return false; }
if(IsTradeContextBusy()) { Print("交易上下文忙,稍后再试"); return false; }
if(!IsTradeAllowed()) { Print("自动交易未开启或EA无交易权限"); return false; }
// 该品种当前是否允许交易(休市/节假日)
if(!MarketInfo(Symbol(), MODE_TRADEALLOWED))
{ Print("品种休市"); return false; }
return true;
}
另有两个常用的只读检查:IsDemo() 判断是否模拟账户;IsTesting() 判断是否在策略测试器里运行(有些日志输出可以只在实盘打印)。
十、保证金预检:AccountFreeMarginCheck
错误 134(资金不足)最好在发出订单之前就拦下来。MQL4 提供了官方预检函数:
double AccountFreeMarginCheck(
string symbol, // 品种
int cmd, // 只能 OP_BUY 或 OP_SELL
double volume // 手数
);
返回"这笔单开出去之后还剩多少可用保证金"。官方文档给出的用法就是一行:
if(AccountFreeMarginCheck(Symbol(), OP_BUY, Lots) <= 0 || GetLastError() == 134)
return; // 保证金不够,放弃本笔
- 返回值 ≤ 0:开了这笔单保证金就穿底,直接放弃;
- 资金不足时该函数会置错误码 134(ERR_NOT_ENOUGH_MONEY);
- 想算"一手要占多少保证金",用
MarketInfo(Symbol(), MODE_MARGINREQUIRED)。
白话注解:与其让服务器打回 134,不如下单前自己算清楚。尤其加仓网格类 EA,第 5 笔、第 6 笔单往往就是被 134 卡死的,预检可以让它优雅停手而不是疯狂重试。
十一、标准重试模板
把第八、九章的检查和第十章的错误码组合起来,就是一个能上实盘的下单模板:
int SafeOrderSend(string symbol, int cmd, double lots, double price,
int slippage, double sl, double tp,
string comment, int magic, datetime expiration, color clr)
{
// 1) 环境自检
if(!IsConnected() || !IsTradeAllowed()) return -1;
// 2) 交易上下文忙就等待(最多约2.5秒)
int wait = 0;
while(IsTradeContextBusy() && wait < 2500)
{
Sleep(100);
wait += 100;
}
// 3) 最多重试3次,只对"可重试"错误重试
int ticket = -1;
for(int attempt = 0; attempt < 3; attempt++)
{
if(IsTradeContextBusy()) break;
ticket = OrderSend(symbol, cmd, lots, price, slippage, sl, tp,
comment, magic, expiration, clr);
if(ticket > 0) return ticket;
int err = GetLastError();
Print("下单失败 attempt=", attempt, " error=", err);
if(err == 135 || err == 136 || err == 138 || err == 129)
{
// 价格类错误:刷新报价,重取价再试
Sleep(500);
if(!RefreshRates()) break;
price = (cmd == OP_BUY) ? Ask : Bid;
continue;
}
if(err == 146)
{
Sleep(1000);
continue;
}
// 130/131/134/148 等属于参数或资金问题,重试没有意义
break;
}
return ticket;
}
两条铁律:① 收到 128/142/143 这类"结果未知"的码,重试前必须先扫一遍订单池确认没成交,否则可能开出双倍仓位;② 重试循环里每次都检查 IsTradeContextBusy()——MT4 全终端共用一个交易线程,重试时别的 EA 可能已经插队。
白话注解:Sleep()在 EA 里可用(指标里不行)。Sleep 之后Bid/Ask缓存就是旧价了,必须RefreshRates()再取——顺序不能反。
十二、时间控制:时段过滤与收盘全平
1、判断当前服务器时间
MQL4 里 TimeCurrent() 是服务器时间(不是你电脑的本地时间)。两种取法:
// 写法一:经典函数
int h = TimeHour(TimeCurrent());
int m = TimeMinute(TimeCurrent());
// 写法二:结构体(build 600+ 之后与 MQL5 语法一致)
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
// dt.hour / dt.min / dt.day_of_week ...
2、时段过滤:只在该交易的时间交易
bool InTradingSession(int startHour, int endHour)
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(startHour <= endHour)
return (dt.hour >= startHour && dt.hour < endHour);
// 跨零点时段,例如 22 点到次日 5 点
return (dt.hour >= startHour || dt.hour < endHour);
}
// if(!InTradingSession(9, 23)) return; // 非 9-23 点不开新仓
3、收盘全平:只执行一次,不要每 tick 都触发
void CloseAllAtTime(int hour, int minute)
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.hour != hour || dt.min != minute) return;
// 关键:用"天"做标记,同一时刻最多执行一轮,防止这一分钟内几千个tick反复触发
static int lastCloseDay = -1;
if(lastCloseDay == dt.day) return;
lastCloseDay = dt.day;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderSymbol() != Symbol()) continue;
if(OrderMagicNumber() != 12345) continue;
RefreshRates();
if(OrderType() == OP_BUY)
OrderClose(OrderTicket(), OrderLots(), Bid, 30, Red);
else if(OrderType() == OP_SELL)
OrderClose(OrderTicket(), OrderLots(), Ask, 30, Red);
else if(OrderType() >= OP_BUYLIMIT)
OrderDelete(OrderTicket()); // 挂单一起撤掉
}
Print("收盘全平完成");
}
白话注解:静态变量 lastCloseDay 是"一次性开关"。没有它, minute 等于收盘分钟的那整整 60 秒里,每个 tick 都会跑一遍全平循环——好在第二次全是报错,但这既刷日志又有踩中重复下单边角情况的风险。
十三、STOPLEVEL 与 FREEZELEVEL:止损距离的两道闸
1、两道闸是什么
两个都是通过 MarketInfo 查询的品种属性,单位都是"点":
| 常量 | 值 | 管什么 |
|---|---|---|
| MODE_STOPLEVEL | 14 | 新设 SL/TP、挂单价与现价的最小距离 |
| MODE_FREEZELEVEL | 33 | 已有订单进入"冻结区"后,不允许修改/删除/平仓 |
查询方式:
double stopLevel = MarketInfo(Symbol(), MODE_STOPLEVEL); // 点
double freezeLevel = MarketInfo(Symbol(), MODE_FREEZELEVEL); // 点
2、官方文档点名的 STOPLEVEL 陷阱
文档明确说:MODE_STOPLEVEL 为 0 有两种含义——要么真的没有最小距离限制,要么券商在用服务端动态机制控距离,客户端查不到。后者被称为"浮动 stoplevel":你查到 0,实际下单还是可能报 130。所以不要写"查到 0 就随便贴价"的代码,保持一个固定的安全距离缓冲。
3、FREEZELEVEL 对管理逻辑的影响
价格逼近你的止损/止盈时,这单进入冻结区,此时 OrderModify / OrderClose 会被拒绝(可能报 145 或 130)。追踪止损、部分平仓这类"贴着市场改单"的逻辑,动手前应该先确认不在冻结区:
bool NotFrozen()
{
double freeze = MarketInfo(Symbol(), MODE_FREEZELEVEL) * Point;
if(freeze == 0) return true;
// 选中的单:检查其 SL/TP/挂单价是否已进入冻结区
if(OrderType() == OP_BUY || OrderType() == OP_SELL)
{
if(OrderStopLoss() > 0 && MathAbs(Bid - OrderStopLoss()) < freeze) return false;
if(OrderTakeProfit() > 0 && MathAbs(Bid - OrderTakeProfit()) < freeze) return false;
}
return true;
}
白话注解:STOPLEVEL 管"新的距离够不够远",FREEZELEVEL 管"快成交的单别再动"。冻结区里改单必然失败,与其撞墙重试,不如先查再动。
十四、点与 pip:三位 / 五位小数换算
1、Point ≠ pip
老行情(4 位小数报价)里 1 pip = 1 Point;现代 5 位小数报价里 1 pip = 10 Point。同一句"追踪 50 点",在 5 位报价的账户上实际只追了 5 pip,止损贴脸。统一写法:
double PipPoint()
{
// 3/5 位小数:1 pip = 10 * Point;2/4 位小数:1 pip = 1 * Point
if(Digits == 3 || Digits == 5) return Point * 10;
return Point;
}
// 用 pip 思维写参数,内部换算成 Point
double trailing = 50 * PipPoint(); // 追踪 50 pip
2、滑点参数同理
OrderSend / OrderClose 的 slippage 参数单位也是 Point。老代码里的 3 在 5 位报价账户上等于 0.3 pip,几乎必然被拒:
int slippagePoints = 3;
if(Digits == 3 || Digits == 5) slippagePoints *= 10; // 3 pip → 30 Point
注意:Digits和Point是当前图表品种的预定义变量;操作非当前图表品种时,用MarketInfo(sym, MODE_DIGITS)和MarketInfo(sym, MODE_POINT)取对应品种的值,别拿图表品种的值去算别的品种。
十五、手数规范化:MINLOT / LOTSTEP / MAXLOT
1、三个常量
double minLot = MarketInfo(Symbol(), MODE_MINLOT); // 最小手数
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT); // 最大手数
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP); // 手数步长(常见0.01或0.1)
131(无效手数)的全部成因都在这三个数里:低于最小值、不是步长整数倍、超过最大值。
2、通用规范化函数
double NormalizeLots(double lots)
{
double minLot = MarketInfo(Symbol(), MODE_MINLOT);
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
if(lotStep > 0)
lots = MathFloor(lots / lotStep) * lotStep; // 向下取整到合法步长
lots = MathMax(minLot, MathMin(maxLot, lots));
return NormalizeDouble(lots, 2);
}
// 例:算出 0.137 手 → LOTSTEP 0.01 → 规范成 0.13;算出 0.004 → 提到最小 0.01
白话注解:部分平仓最容易踩这里——1.00 手平一半 0.50 没问题;但 0.03 手平一半算出 0.015,直接 131 被拒。所有"算出来的手数"都必须过一遍 NormalizeLots。向下取整是为了宁可少平不多平。
十六、挂单管理:OrderModify / OrderDelete / 过期时间
1、挂单六兄弟
| 常量 | 含义 | 触发方向 |
|---|---|---|
| OP_BUYLIMIT | 限价买 | 价格跌到委托价以下买进 |
| OP_SELLLIMIT | 限价卖 | 价格涨到委托价以上卖出 |
| OP_BUYSTOP | 突破买 | 价格涨到委托价以上买进 |
| OP_SELLSTOP | 突破卖 | 价格跌到委托价以下卖出 |
判断是否挂单:OrderType() > OP_SELL(即值大于 1)。
2、改价与撤单
// 挂单改价/改SL/TP/过期时间,都用 OrderModify
// 注意第二个参数此时是"新委托价",与持仓单含义不同
if(!OrderModify(ticket, newPrice, newSL, newTP, newExpiration, Yellow))
Print("挂单修改失败 error=", GetLastError());
// 撤单用 OrderDelete,不是 OrderClose
if(!OrderDelete(ticket))
Print("撤单失败 error=", GetLastError());
3、过期时间(expiration)
OrderSend/OrderModify都能设过期时间,到点券商自动撤单;- 不是所有券商都支持:不支持时挂单报 147(ERR_TRADE_EXPIRATION_DENIED),此时 expiration 传 0(永不过期),用第十二章的定时逻辑自己撤;
- 挂单到期前的策略逻辑:突破单如果一直没触发,行情逻辑已经变了,与其让它"守株待兔",不如定期清理超过 N 根K线的旧挂单。
十七、断线重连与状态对账
1、检测断线:IsConnected
断线期间 EA 收不到 tick,OnTick 不会被调用——这正是问题所在:恢复后的第一个 tick 才是补救的时机。
void OnTick()
{
static bool wasConnected = true;
bool nowConnected = IsConnected(); // 用 IsConnected,不是 IsTradeContextBusy
if(wasConnected && !nowConnected)
{
// 通常收不到这个打印:断线时没有 tick,OnTick 不进来
Print("连接断开");
}
else if(!wasConnected && nowConnected)
{
Print("重连成功,开始对账");
ResyncPositions();
}
wasConnected = nowConnected;
// ... 正常交易逻辑
}
2、对账要查什么
断线期间的"未知状态"主要有四种,重连后逐一核对:
| 未知状态 | 核对方法 |
|---|---|
| 重发的订单有没有实际成交 | 按 comment/魔术号扫 MODE_TRADES,比对下单前的 ticket 快照 |
| 持仓里有没有"裸单"(SL=TP=0) | 扫出魔术号匹配但 OrderStopLoss()==0 且 OrderTakeProfit()==0 的单,按第十三章补挂 |
| 该撤的挂单还在不在 | 按时间/价格条件重扫挂单,过期的撤掉 |
| 本地缓存的 ticket 表还准不准 | 全部作废,从 OrdersTotal() 重建 |
void ResyncPositions()
{
int bare = 0; // 裸单计数
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
if(OrderMagicNumber() != 12345) continue;
if((OrderType() == OP_BUY || OrderType() == OP_SELL)
&& OrderStopLoss() == 0 && OrderTakeProfit() == 0)
{
bare++;
// TODO: 按第十三章两步法补挂 SL/TP
}
}
Print("对账完成:本EA持仓 ", CountPositions(OP_BUY) + CountPositions(OP_SELL),
" 笔,其中裸单 ", bare, " 笔待补挂");
}
注意:重连后立刻下单可能撞上 128 超时或 142/143 状态码(连接还在恢复中),对账逻辑先跑,交易动作缓一两个 tick 再恢复,并配合第十一章"结果未知先查成交"的铁律。
十八、多 EA 共存:Magic Number 与 Comment 规范
1、魔术号是唯一可靠的"所有权标记"
账户里同时跑多个 EA、还有手工单,怎么分清谁的单?唯一的可靠依据是 Magic Number:
- 每个 EA 用一个专属整数(如 12345、20260102),写进
OrderSend的 magic 参数; - 遍历时用
OrderMagicNumber() != 我的魔术号过滤; - 部分平仓后魔术号不变(第六章),所以所有权标记终身有效;
- 魔术号开单后无法修改——别指望开单后"改名"。
2、Comment 只能当参考
comment 参数看似好用,实际有三个坑:
- 部分平仓时 comment 通常被改写成 "from #原单号",你自己写的内容被覆盖;
- 部分券商在出入金调整、服务器事件时也会改写注释;
- 不同券商对 comment 长度和字符的限制不同。
结论:comment 只用于人类阅读的备注,任何业务判断都不要依赖它。
白话注解:魔术号是身份证,comment 是胸前名牌。身份证跟人一辈子,名牌随时可能被别人摘了换掉。认身份证,别认名牌。
十九、防重复下单:一个信号只开一单
1、问题根源:一根 K 线几千个 tick
信号是"均线上穿"这种状态,它在这根 K 线的每个 tick 都成立。不做防护,一根 K 线能给你开出几十单。
2、方案一:每根 K 线只评估一次
void OnTick()
{
static datetime lastBar = 0;
if(Time[0] == lastBar) return; // 还是同一根K线,直接跳过
lastBar = Time[0];
// 到这里说明是新K线的第一个tick,信号判断只在这里做
// ...
}
3、方案二:信号触发即锁定,直到信号消失
static bool buySignalActive = false;
bool crossUp = (FastMA[1] > SlowMA[1] && FastMA[2] <= SlowMA[2]);
if(crossUp && !buySignalActive)
{
buySignalActive = true; // 锁定
SafeOrderSend(...); // 下单
}
if(!crossUp) buySignalActive = false; // 信号消失才解锁
4、方案三:持仓数量兜底
前两个方案都依赖程序状态,EA 重启后状态清零。最硬的兜底是查池子(第七章):
if(CountPositions(OP_BUY) == 0) // 池子里没有我的多单才开
{
// OrderSend(...)
}
白话注解:三道防线各挡一层——按K线节流、按信号锁定、按持仓兜底。重启不丢状态的只有第三道,因为它查的是服务器里的真实订单池,不是 EA 自己的记忆。
二十、OnTimer:无 tick 时段的自救
1、问题:冷门品种半夜没有 tick
OnTick 只在有新报价时触发。凌晨冷清时段、流动性极差的品种,可能几分钟没有 tick——你的断线检测、到期全平、补挂裸单逻辑全部停摆,因为它们都挂在 OnTick 里。
2、解法:定时器事件
int OnInit()
{
EventSetTimer(1); // 每 1 秒触发一次 OnTimer
return(INIT_SUCCEEDED);
}
void OnDeinit(const int reason)
{
EventKillTimer(); // 必须释放,否则EA卸载后定时器残留
}
void OnTimer()
{
// 放"不依赖tick"的维护任务:
if(!IsConnected()) { Print("定时器发现断线"); return; }
// 补挂裸单、检查收盘全平时间、清理过期挂单……
ResyncPositions();
}
void OnTick()
{
// 放交易信号等对时效敏感的逻辑
}
EventSetTimer(seconds)单位是秒;EventSetMillisecondTimer(ms)可到毫秒级(耗资源,一般用不上);- 定时器事件与 tick 事件不会同时执行(同一 EA 内部串行),不用额外加锁;
- 回测器里定时器事件的模拟支持有限,OnTimer 逻辑要在实盘/模拟盘上单独验证。
白话注解:OnTick 是"来活儿才干",OnTimer 是"闹钟到点必干"。断线检测这种性命攸关的事,交给闹钟比交给"等行情叫你"可靠得多。
二十一、下一课预告
第 3 课《MQL4 迁移 MQL5 指南》已上线:一个订单池变三个集合、CTrade 类、OnTradeTransaction、净持仓与锁仓的区别。如果你要升级 MT5,从那一课开始。
本站固定免责声明:本教程仅讲解软件编程与操作方法,不构成投资建议或策略推荐。回测业绩不代表未来收益,EA自动交易风险大,请谨慎决策。
💬 用户评价 (0)
立即登录