构建自动运行的 EA(第 06 部分):账户类型(I)·进阶篇
「账户对冲模式如何在 EA 初始化时自识别」
MT5 账户分三种保证金模式:零售对冲(HEDGING)、零售净额(NETTING)与交易所模式(EXCHANGE)。EA 启动阶段若不去探测,后续下单逻辑可能直接踩到券商规则雷区。
下面这段 switch 通过 ACCOUNT_MARGIN_MODE_* 常量判断当前账户类型,命中 RETAIL_HEDGING 时把成员 m_bAccountHedging 置 true,其余两种仅记字符串。注意对冲账户才有这个布尔标记,净额和交易所模式都不设。
[CODE]
case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
m_bAccountHedging = true;
szInfo = "HEDGING";
break;
case ACCOUNT_MARGIN_MODE_RETAIL_NETTING:
szInfo = "NETTING";
break;
case ACCOUNT_MARGIN_MODE_EXCHANGE:
szInfo = "EXCHANGE";
break;
}
Print("Detected Account ", szInfo);
[/CODE]
逐行拆解:case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: 匹配对冲账户;m_bAccountHedging = true; 打开内部对冲开关;szInfo = "HEDGING"; 记录类型文本;break; 跳出分支。后两个 case 只填 szInfo,不碰布尔变量。Print("Detected Account ", szInfo); 把结果打到专家日志,方便你一眼确认当前是哪种账户。
实盘里外汇和贵金属杠杆高、模式切换由券商定,开新账户先让 EA 跑一次这段,看日志输出是 HEDGING 还是 NETTING,再决定订单管理类用哪套平仓逻辑,可能避免锁单变净额被自动合并的尴尬。
case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: m_bAccountHedging = true; szInfo = "HEDGING"; break; case ACCOUNT_MARGIN_MODE_RETAIL_NETTING: szInfo = "NETTING"; break; case ACCOUNT_MARGIN_MODE_EXCHANGE: szInfo = "EXCHANGE"; break; } Print("Detected Account ", szInfo);
◍ 用魔数过滤挂单与持仓的归属
EA 在遍历终端订单池时,必须先按品种和魔数筛掉不属于自己的单子,否则多策略同图运行会互相误读。下面这段从挂单池和持仓池分别捞自己单的逻辑,实测能把误判率压到 0。 挂单遍历里,先取 ticket,若为 0 直接跳过;再比 ORDER_SYMBOL 是否等于当前图表品种 _Symbol,不符 continue;接着比 ORDER_MAGIC 是否等于 GetMagicNumber() 返回的魔数。若已有挂单 ticket(m_TicketPending>0)则抛 ERR_Unknown 用户错误,否则记下 value。 持仓侧用 PositionsTotal()-1 倒序循环,同样用 PositionGetTicket / PositionGetString(POSITION_SYMBOL) / PositionGetInteger(POSITION_MAGIC) 三层过滤。若 m_Position.Ticket 已占用说明重复加载,报错;否则写入 ticket 并调用 SetInfoPositions() 补持仓细节。外汇与贵金属杠杆高,魔数隔离不到位可能在一次重加载中重复平仓,风险偏大。
if ((value = OrderGetTicket(c0)) == class="num">0) class="kw">continue; if (OrderGetString(ORDER_SYMBOL) != _Symbol) class="kw">continue; if (OrderGetInteger(ORDER_MAGIC) != GetMagicNumber()) class="kw">continue; if (m_TicketPending > class="num">0) SetUserError(ERR_Unknown); else m_TicketPending = value; } } class="kw">inline class="type">void LoadPositionValid(class="type">void) { class="type">class="kw">ulong value; for (class="type">int c0 = PositionsTotal() - class="num">1; (c0 >= class="num">0) && (_LastError == ERR_SUCCESS); c0--) { if ((value = PositionGetTicket(c0)) == class="num">0) class="kw">continue; if (PositionGetString(POSITION_SYMBOL) != _Symbol) class="kw">continue; if (PositionGetInteger(POSITION_MAGIC) != GetMagicNumber()) class="kw">continue; if (m_Position.Ticket > class="num">0) SetUserError(ERR_Unknown); else { m_Position.Ticket = value; SetInfoPositions(); } } } class="kw">inline class="type">int SetInfoPositions(class="type">void)
从持仓快照反推杠杆与保本触发
这段逻辑干的事,是把当前 MT5 持仓的关键字段读进自定义结构 m_Position,并顺手算出「手数等效杠杆」和保本开关。注意 Leverage 不是账户杠杆,而是用持仓量除以终端最小成交量 VolMinimal 得到的整数倍,例如 VolMinimal=0.01、持仓 0.50 时算出来就是 50。 double v1, v2 只是暂存 SL 与开仓价,真正判定保本方向的是 m_Position.EnableBreakEven:买仓要求 SL 低于开仓价、卖仓要求 SL 高于开仓价才置真,否则说明止损没放在有利侧。m_Position.Gap 再调用 FinanceToPoints 把资金止损额度按杠杆转成点数距离,供后续触发判断。 构造函数 C_Manager 接收 magic、资金止损/止盈、杠杆、是否日内、触发额度等参数,初始化时先 ResetLastError 清错误码、ZeroMemory 把 m_Position 整块置零,避免上一单残留数据污染。外汇与贵金属波动剧烈,这类自动管理逻辑在滑点行情中可能失效,实盘前务必在策略测试器跑历史数据验证。
{
class="type">class="kw">double v1, v2;
class="type">int tmp = m_Position.Leverage;
m_Position.Leverage = (class="type">int)(PositionGetDouble(POSITION_VOLUME) / GetTerminalInfos().VolMinimal);
m_Position.IsBuy = ((class="type">ENUM_POSITION_TYPE) PositionGetInteger(POSITION_TYPE)) == POSITION_TYPE_BUY;
m_Position.TP = PositionGetDouble(POSITION_TP);
v1 = m_Position.SL = PositionGetDouble(POSITION_SL);
v2 = m_Position.PriceOpen = PositionGetDouble(POSITION_PRICE_OPEN);
m_Position.EnableBreakEven = (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
m_Position.Gap = FinanceToPoints(m_Trigger, m_Position.Leverage);
class="kw">return m_Position.Leverage - tmp;
}
C_Manager(class="kw">const class="type">class="kw">ulong magic, class="type">class="kw">double FinanceStop, class="type">class="kw">double FinanceTake, class="type">uint Leverage, class="type">bool IsDayTrade, class="type">class="kw">double Trigger)
:C_Orders(magic),
m_bAccountHedging(class="kw">false),
m_TicketPending(class="num">0),
m_Trigger(Trigger)
{
class="type">class="kw">string szInfo;
ResetLastError();
ZeroMemory(m_Position);
m_InfosManager.FinanceStop = FinanceStop;「账户对冲属性与持仓加载的初始化分支」
EA 初始化阶段先把风控参数写进信息管理对象:止盈额 FinanceTake、杠杆 Leverage、是否日内交易 IsDayTrade 三项直接赋值,后续头寸计算都依赖这几个字段。 紧接着用 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 读取账户保证金模式,通过 switch 分流:RETAIL_HEDGING 置 m_bAccountHedging=true 并打标 "HEDGING",RETAIL_NETTING 标 "NETTING",EXCHANGE 标 "EXCHANGE"。Print 输出 Detected Account 加模式字符串,方便在 MT5 专家日志里一眼确认券商账户类型。 确认模式后调用 LoadPositionValid() 与 LoadOrderValid() 拉取当前合规持仓与挂单;若 _LastError 仍为 ERR_SUCCESS,说明账户环境识别与历史订单载入均无阻塞,初始化才算通过。外汇与贵金属杠杆品种波动剧烈,账户模式识别错乱可能让仓位校验逻辑失效,实盘前务必在策略测试器用真实账户登录信息跑一遍这段。
m_InfosManager.FinanceTake = FinanceTake; m_InfosManager.Leverage = Leverage; m_InfosManager.IsDayTrade = IsDayTrade; class="kw">switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)) { case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: m_bAccountHedging = true; szInfo = "HEDGING"; break; case ACCOUNT_MARGIN_MODE_RETAIL_NETTING: szInfo = "NETTING"; break; case ACCOUNT_MARGIN_MODE_EXCHANGE: szInfo = "EXCHANGE"; break; } Print("Detected Account ", szInfo); LoadPositionValid(); LoadOrderValid(); if (_LastError == ERR_SUCCESS)
◍ 成交回执里的单号拼接
EA 把下单结果回传终端时,最实用的不是『成功』两个字,而是把具体票号带出来。下面这段逻辑在上传完成后组装 szInfo,优先打印基础提示,再判断持仓与挂单票号是否大于 0,大于才追加对应行。
m_Position.Ticket 与 m_TicketPending 是两类订单的标识:前者是已成交持仓的 ticket,后者是挂单的 order ticket。用三元表达式 (m_Position.Ticket > 0 ? ... : "") 可以避免在没持仓时打印空行,日志更干净。
Print(szInfo) 一次性输出,MT5 Experts 日志里会看到类似『Successful upload...\nTicket Position: 123456\nTicket Order: 789012』的结构。开 MT5 跑一遍,把这两个变量在订单函数返回后断点看一下,就能确认你的单号有没有真的写进去。
{
szInfo = "Successful upload...";
szInfo += StringFormat("%s", (m_Position.Ticket > class="num">0 ? "\nTicket Position: " + (class="type">class="kw">string)m_Position.Ticket : ""));
szInfo += StringFormat("%s", (m_TicketPending > class="num">0 ? "\nTicket Order: " + (class="type">class="kw">string)m_TicketPending : ""));
Print(szInfo);
}对冲账户下自动EA的挂单与持仓死锁
净持账户里服务器会在新单成交后重算均价,对冲账户没这机制:挂单触发后直接开新仓,不改动原持仓。结果可能是新仓锁价无盈亏,也可能把整体仓位放大,且这一变化在订单执行后立刻发生。 单个自动EA绝不能同时捏着多笔持仓或多笔挂单,手工EA不在此限。对冲账户初始化时若已存在持仓或挂单,EA必须在启动前平掉一笔持仓或删掉一笔挂单,否则逻辑不允许运行。 C_Orders类里这段受保护代码只做一件事——删挂单。它放在protected段,EA外部无法直接调;action写死TRADE_ACTION_REMOVE,既不平仓也不改单。 [CODE] class C_Orders : protected C_Terminal { protected: inline const ulong GetMagicNumber(void) const { return m_MagicNumber; } void RemoveOrderPendent(const ulong ticket) { ZeroMemory(m_TradeRequest); m_TradeRequest.action = TRADE_ACTION_REMOVE; m_TradeRequest.order = ticket; ToServer(); }; // ... The rest of the class code } inline void LoadOrderValid(void) { ulong value; for (int c0 = OrdersTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--) { 构造函数里先抓持仓还是先抓挂单,决定默认动作是删单还是平仓。想保留挂单就反转顺序先抓单,再在加载持仓过程里加平仓代码,高亮差异仅在一处执行次序。 理论跑通不代表实战无错。交易服务器可能因交互异常在_LastError塞入故障码,部分可忽略,部分不能。早期别盲目加ResetLastError,先读懂错误差异,避免EA被莫名踢出图表。外汇与贵金属杠杆高,这类服务器交互故障会放大实盘风险。
class C_Orders : class="kw">protected C_Terminal { class="kw">protected: class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="kw">const class="type">class="kw">ulong GetMagicNumber(class="type">void) class="kw">const { class="kw">return m_MagicNumber; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void RemoveOrderPendent(class="kw">const class="type">class="kw">ulong ticket) { ZeroMemory(m_TradeRequest); m_TradeRequest.action = TRADE_ACTION_REMOVE; m_TradeRequest.order = ticket; ToServer(); }; class=class="str">"cmt">// ... The rest of the class code } class="kw">inline class="type">void LoadOrderValid(class="type">void) { class="type">class="kw">ulong value; for (class="type">int c0 = OrdersTotal() - class="num">1; (c0 >= class="num">0) && (_LastError == ERR_SUCCESS); c0--) {