账户类型隔离层:用 C_Manager 给 EA 自动化铺地基(基础篇)
(1/3)· 手工触发之后,EA 为什么还不能直接托管?缺的不是策略,是一层账户安全的隔离
EA 起步先认账户类型
写 MT5 的 EA 之前,得先搞清楚跑它的账户到底属于哪一类。不同的账户类型在杠杆、点差、执行模式上差异很大,同一套 EA 逻辑在净仓账户和单边账户里行为可能完全相反。 以 MetaTrader 5 终端为例,账户分对冲(hedging)与净仓(netting)两类。前者允许同一品种开多个反方向持仓,后者一个品种只能有一条净头寸。EA 里若用 OrderSend 不判断账户模式,在净仓账户上重复下单会被拒或自动平仓对冲,这类 bug 在回测里发现不了,只会在实盘炸。
- 年 3 月 27 日发布的这组教程原文显示,该节在终端内阅读量 2474,说明账户类型这一坑不少人在踩。打开 MT5 按 F1 搜 AccountInfoInteger(ACCOUNT_TRADE_MODE),先确认自己账户是 ACCOUNT_TRADE_MODE_REAL 还是 DEMO,再决定 EA 的持仓管理分支。外汇与贵金属杠杆高,账户模式误配可能瞬间放大回撤,属高风险操作。
◍ 从手动触发走向通用EA的过渡逻辑
上篇做出来的EA健壮性不错,能跑在外汇、股票等各类品种上,但本质还是纯手动控制,离自动化还差关键几步。 目前它能在任意行情状态下不出错,却没法自己决策进出场。若现在就塞进盈亏平衡或尾随止损,后续重构通用框架时反而要拆掉重做,性价比低。 更稳的路线是先搭一个通用EA骨架,把控制流和交易接口抽象好,再往里填风控模块。外汇与贵金属杠杆高、滑点跳空频繁,这种先框架后细节的顺序能少走弯路。
「用私有继承把下单权限收口到管理员」
在 MT5 写 EA 时,直接让策略逻辑去碰订单发送函数,很容易在某个调用里填错手数或止损参数。引入一个 C_Manager 类作为 EA 与底层订单系统之间的隔离层,能让 EA 只通过有限接口下达意图,具体怎么发单由管理器内部决定。 这个类用 private 方式继承 C_Orders,意味着 EA 完全看不到父类的原始下单方法,只能通过管理器暴露的两个函数操作仓位。私密继承在这里不是语法炫技,而是把「谁能发单」这件事锁死成唯一通信点,安全性和可靠性会明显提升。 对比上一版的 EA 代码就能看出来:原先散落在各处的下单参数被整段删掉,替换成在构造函数里一次性传入 FinanceStop、FinanceTake、Leverage、IsDayTrade。之后 EA 调用 CreateOrder 只需要给订单类型和价格,止损止盈等关键信息早已固化在管理器里,不会再出现单次调用填错值的可能。 别把私有继承当累赘 很多交易者嫌多写一层类麻烦,但外汇和贵金属都是高杠杆高风险品种,参数错一处就可能让账户承受计划外亏损。把订单参数集中在初始化阶段定义,等于给 EA 加了道防火墙,后续调策略逻辑时不用反复担心手数或止损被误改。
class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_Orders.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Manager : class="kw">private C_Orders { class="kw">private : class="kw">struct st00 { class="type">class="kw">double FinanceStop, FinanceTake; class="type">uint Leverage; class="type">bool IsDayTrade; }m_InfosManager; class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ 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) :C_Orders(magic) { m_InfosManager.FinanceStop = FinanceStop; m_InfosManager.FinanceTake = FinanceTake; m_InfosManager.Leverage = Leverage; m_InfosManager.IsDayTrade = IsDayTrade; } class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_Manager() { } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price) { C_Orders::CreateOrder(type, Price, m_InfosManager.FinanceStop, m_InfosManager.FinanceTake, m_InfosManager.Leverage, m_InfosManager.IsDayTrade); } };
用鼠标和快捷键把挂单逻辑搬进图表
这段 EA 片段把下单动作从菜单里解放出来,直接绑到图表事件上。核心是两个类:C_Orders 管订单执行,C_Mouse 负责捕获鼠标按键与修饰键状态,初始化时通过 new 分别挂到 magic number 和一组外部参数 user01~user07 上。 OnChartEvent 里先调 mouse.DispatchMessage 和 GetStatus 拿到价格和按键码。若按住 Control 再按方向键,就走 ToMarket 市价单分支;这里代码出现了两套调用,一套带 user03/user02/user01/user04 显式传止损止盈杠杆,一套只传 ORDER_TYPE_BUY/SELL,后者依赖 manager 内部已初始化的 m_InfosManager 默认值。 左键单击且 Shift/Ctrl 状态互斥时,触发 CreateOrder 挂限价单:mem 为 0 才记录 Price 并下单,否则清 0。这个 mem 静态变量其实就是防重复触发的简易锁,实测在快速连点下若 Tick 间隔小于事件响应,可能漏掉第二次点击。 外汇与贵金属杠杆高,这类一键下单逻辑若参数未隔离,可能误发重仓单,建议先在策略测试器用 0.01 手验证按键映射。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ToMarket(class="kw">const ENUM_ORDER_TYPE type) { C_Orders::ToMarket(type, m_InfosManager.FinanceStop, m_InfosManager.FinanceTake, m_InfosManager.Leverage, m_InfosManager.IsDayTrade); } class=class="str">"cmt">//+------------------------------------------------------------------+ }; class="type">int OnInit() { manager = new C_Orders(def_MAGIC_NUMBER); manager = new C_Manager(def_MAGIC_NUMBER, user03, user02, user01, user04); mouse = new C_Mouse(user05, user06, user07, user03, user02, user01); class="kw">return INIT_SUCCEEDED; } class="type">void OnChartEvent(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { class="type">uint BtnStatus; class="type">class="kw">double Price; class="kw">static class="type">class="kw">double mem = class="num">0; (*mouse).DispatchMessage(id, lparam, dparam, sparam); (*mouse).GetStatus(Price, BtnStatus); if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL)) { if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) (*manager).ToMarket(ORDER_TYPE_BUY, user03, user02, user01, user04); if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))(*manager).ToMarket(ORDER_TYPE_SELL, user03, user02, user01, user04); if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) (*manager).ToMarket(ORDER_TYPE_BUY); if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN)) (*manager).ToMarket(ORDER_TYPE_SELL); } if ((def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus)) && def_BtnLeftClick(BtnStatus)) { if (mem == class="num">0) (*manager).CreateOrder((def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), mem = Price, user03, user02, user01, user04); if (mem == class="num">0) (*manager).CreateOrder((def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), mem = Price); }else mem = class="num">0; }
◍ EA 死活跑不顺,先查账户是净持还是对冲
不少写 EA 的人栽在账户类型上而不自知。MT5 底下其实分净持结算(NETTING)、对冲(HEDGING)和交易所(EXCHANGE)三类,但多数自动化策略只须直面前两种:净持账户下服务器会随加减仓实时摊平均价,对冲账户则每笔持仓独立,多空可并存;同手数反向下单在净持端直接被服务器平掉。 同一段盈亏平衡或尾随停止代码,在两类账户上行为事实不同。若 EA 带任何自动逻辑,就必须明确它按哪类账户设计,否则轻则滑点异常,重则触发非预期平仓。手工下单的半自动脚本倒无所谓。 为抹平差异,做法是将 EA 在对冲账户上的自由度压到和净持一样:只允许一笔持仓加一笔挂单。下面这段结构塞进 C_Manager 类,顺带记下账户类型,后续盈亏平衡触发器要用。 [CODE] class C_Manager : private C_Orders { private : struct st00 { double FinanceStop, FinanceTake; uint Leverage; bool IsDayTrade; }m_InfosManager; //--- struct st01 { ulong Ticket; double SL, TP, OpenPrice; double Lots; bool IsHedgeAccount; bool IsPendingOrder; // 暂未启用 }m_TradeInfo; //--- enum e_Error { ERR_Unknown = 1, ERR_MultiPosition, ERR_MultiPending }; //--- void LoadPending(void); void LoadPosition(void); void SetInfoPositions(void); }; [/CODE] 构造函数里先把结构清零,并探明当前账户属性:AccountInfoInteger(ACCOUNT_MARGIN_MODE) 返回的是否为对冲模式,写进 IsHedgeAccount。若图表重启后发现多笔持仓或多笔挂单,EA 直接走 ERR_MultiPosition / ERR_MultiPending 报错,因为架构只容得下一笔。 加载挂单用 for(int c0 = OrdersTotal()-1; c0 >= 0 && _LastError == ERR_SUCCESS; c0--) 倒序扫单,OrderGetTicket(c0) 取单号,再按品种名与魔术码双重过滤,非本 EA 的单子直接跳过。命中后单号存 m_TradeInfo.Ticket,否则 SetUserError(ERR_Unknown) 写进 _LastError。 持仓扫描逻辑同构,只是换成 PositionSelect 系列。SetInfoPositions 须在 PositionGetTicket 等调用之后才能跑,否则读到的持仓数据是空的。外汇与贵金属杠杆高、滑点跳空频繁,这类账户类型误配在重大数据行情中可能瞬间放大亏损,上 MT5 开个 demo 切换净持/对冲验证最直观。
class C_Manager : class="kw">private C_Orders { class="kw">private : class="kw">struct st00 { class="type">class="kw">double FinanceStop, FinanceTake; class="type">uint Leverage; class="type">bool IsDayTrade; }m_InfosManager; class=class="str">"cmt">//--- class="kw">struct st01 { class="type">class="kw">ulong Ticket; class="type">class="kw">double SL, TP, OpenPrice; class="type">class="kw">double Lots; class="type">bool IsHedgeAccount; class="type">bool IsPendingOrder; }m_TradeInfo; class=class="str">"cmt">//--- enum e_Error { ERR_Unknown = class="num">1, ERR_MultiPosition, ERR_MultiPending }; class=class="str">"cmt">//--- class="type">void LoadPending(class="type">void); class="type">void LoadPosition(class="type">void); class="type">void SetInfoPositions(class="type">void); };
「持仓结构体与管理器构造的落地细节」
这段代码片段定义了一个持仓信息结构体 m_Position 以及 C_Manager 的构造函数入口,核心是把账户层面的风控参数在对象初始化时就固化下来。 结构体里包含了 PriceOpen、Gap、EnableBreakEven、IsBuy、Leverage 等字段,最后用 }m_Position; 直接声明实例;外层还有 m_TicketPending(挂单票号)、m_bAccountHedging(账户是否锁仓模式)、m_Trigger(触发阈值)三个独立成员。 构造函数 C_Manager 接收 magic、FinanceStop、FinanceTake、Leverage、IsDayTrade、Trigger 六个参数,初始化列表里把 m_bAccountHedging 置为 false、m_TicketPending 置 0、m_Trigger 赋入参,函数体内先用 ZeroMemory(m_Position) 清空结构体,再给 m_InfosManager 的各字段赋值。 值得注意的是,构造末尾用 switch 判断 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 的枚举值,说明不同保证金模式(如交易所模式、券商模式)下管理器行为可能分化,开 MT5 后可在初始化日志里打印该枚举确认自己账户属于哪一类。外汇与贵金属杠杆交易高风险,参数误配可能导致保证金计算偏差。
PriceOpen,
Gap;
class="type">bool EnableBreakEven,
IsBuy;
class="type">int Leverage;
}m_Position;
class="type">class="kw">ulong m_TicketPending;
class="type">bool m_bAccountHedging;
class="type">class="kw">double m_Trigger;
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;
ZeroMemory(m_Position);
m_InfosManager.FinanceStop = FinanceStop;
m_InfosManager.FinanceTake = FinanceTake;
m_InfosManager.Leverage = Leverage;
m_InfosManager.IsDayTrade = IsDayTrade;
class="kw">switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE))
{