构建自动EA新函数:基础篇铺垫订单系统必备模块(基础篇)
⚙️

构建自动EA新函数:基础篇铺垫订单系统必备模块(基础篇)

(1/3)· 从净持与对冲账户差异说起,搞懂EA为什么至少缺这三个核心函数

案例拆解 第 1/3 篇

很多交易者在同一经纪商账户上同时跑多个EA,或手工与EA同做一品种,却没意识到净持账户下均摊均价会让两边指令互冲,短时间内就可能亏光本金。对冲账户虽能分仓并存,但锁单不等于平仓,这点也常被误读。先把账户结算机制理清,再谈加函数才安全。

「EA 里的函数该长什么样」

在 MT5 里写 EA,最先要习惯的不是指标计算,而是把动作拆成函数。一个干净的 EA 结构,通常会把下单、平仓、信号判断各自独立成块,而不是全堆在 OnTick 里。 下面这段是 MQL5 里最基础的函数骨架,定义了带返回值和参数的写法。注意 MQL5 强制类型声明,和脚本语言随手写变量完全不同。 [CODE] double CalculateLot(double riskPercent) { double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); double lot = accountBalance * riskPercent / 100.0 / 10000.0; return lot; } [/CODE] 逐行看:第一行声明返回 double 类型的 CalculateLot,入参是风险百分比;函数体内先用 AccountInfoDouble 取账户余额,再按‘余额×风险%/100÷1万点值’估算手数,最后 return 出手数。 实际在 MT5 里按 F4 编译这段,若账户余额 10000、riskPercent 填 1,算出来 lot 是 0.01。外汇和贵金属杠杆高,这种按余额倒推手数的写法能避免重仓,但市场仍可能瞬间反向,需自担高风险。

MQL5 / C++
class="type">class="kw">double CalculateLot(class="type">class="kw">double riskPercent)
{
   class="type">class="kw">double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   class="type">class="kw">double lot = accountBalance * riskPercent / class="num">100.0 / class="num">10000.0;
   class="kw">return lot;
}

◍ 同资产多EA的账户雷区

上一篇文章里我们只给自动化 EA 搭了订单系统的一个基础函数,离全自动驾驶还差几块拼图。不少人习惯挂多个 EA 用不同参数盯同一品种,或者手工单和 EA 单混跑,这里头账户类型直接决定会不会自爆。 净持结算账户(NDD 类常见)由交易服务器维护均摊持仓价,两个 EA 一个发卖讯一个发买讯时,会互相撕扯导致双双失效,极端情况下短时间内亏光本金的概率很高。外汇与贵金属本身高杠杆高风险,这种账户结构下的冲突尤其致命。 对冲账户则不存在该问题:一个 EA 卖、另一个买,订单各自独立不抵消。手工和 EA 同品种也只对冲账户可这么干。 存疑时就别在同一经纪商跑两个 EA 做同一资产,也别在 EA 自动跑时手动掺和同品种。把这条红线记死,我们再往 EA 里补那 90% 场景都要用的剩余函数。

EA 下单机制里真正缺的那几个函数

自动 EA 多数时候只做市价直进直出,很少往订单簿里挂单。但要在簿中挂单、改价、平仓,逻辑比直进直出麻烦得多。上一节已经实现了一类挂单函数,对自动 EA 来说还不够,至少还得补两个:一个发市价买/卖单,一个改挂单价格。 开仓通常最简单——按预设交易量市价进出即可。平仓却分账户类型:净持结算账户里,反手同量市价单就能平掉;对冲账户里反手只会锁仓,不叫平仓,必须发明确的平仓指令,所以这里要第三个函数。 有些模型里 EA 开市价仓后立刻挂一个止损单留在簿中,平时不发平仓请求,只管簿中订单让平仓单始终有效。这在净持账户好用,对冲账户得靠 OCO 机制只设止损,或等价格到位再发平仓单。 平仓最棘手,有几个实打实的风险点:高波动时 OCO 订单可能跳价(订单簿里的普通挂单不跳价,只会在目标位外触发);连接断了 EA 一段时间连不上服务器;流动性不足挂单干等无法成交;最糟的是 EA 开始随机乱发单。外汇和贵金属杠杆高,这些意外都可能放大亏损。 别把定时开关当护身符。给 EA 加交易时间表、用触发器自动启停,是我见过最蠢的操作。你以为设了时段就能撒手,错。EA 跑着的时候,你或信得过的人得在旁边盯着。要离开就先停掉,绝不让它无人监督自己跑,否则结果可能极度难看。

「通用填充函数与订单身份令牌」

市价单与挂单的提交逻辑高度重合,所以把公共字段抽进一个通用填充函数最省事:局部只补差异字段,其余交给通用过程。原文给出的通用入口长这样: [CODE] inline void CommonData(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade) [/CODE] 逐行拆解:inline 提示编译器尽量内联展开,减少调用开销;void 表明只做数据装配不返回状态;ENUM_ORDER_TYPE type 区分市价或挂单类型;Price 是触发价(挂单必填,市价由类内部填);FinanceStop / FinanceTake 是以金融金额表达的止损止盈,不是图表面值;Leverage 传杠杆倍数;IsDayTrade 标记日内与否。这套签名把上一篇挂单函数里的重复代码收拢了。 多出来的魔幻数字(magic number)在对冲账户或纯观察型 EA 里很实用:它让服务器返回的 ticket 能反查到是你的哪套策略下的单。构造函数里接收它,声明默认值时类可无参构造;删掉默认值就强制程序员在实例化时传入,否则编译期报错——前提是类没有别的构造函数,这是很多人踩的坑。 任何提交返回的 ticket 非空即代表单号或仓位号,是 EA 与服务器博弈的“通行证”。每笔订单/仓位单号唯一,不能随机编;丢了虽能枚举找回但耗 EA 时间,所以务必存住。订单(order)在订单簿是未来仓位,仓位(position)是已成交;改订单的 TP/SL 用图表面值而非金额。买入订单 SL 必须低于开仓价、TP 高于,否则服务器报错;但仓位 SL 可任意放,甚至高于开仓价变相锁利。 移动开仓价时,挂单的 TP/SL 不会自动跟——服务器只当它是独立价格点。要同步就得在改价函数里同时重算二者距离。仓位不能移开仓价,但同一函数能移 TP/SL 实现盈亏平衡或尾随:触发阈值后取开仓价填 SL 即盈亏平衡;新价距旧 SL 超阈值则更新 SL 即尾随。 防呆逻辑也得写:自动 EA 偶发算出零价还硬发给服务器,会被拒且可能死循环。通用函数里加一条“开单价为零则直接撤单”,既避卡死也能当手动删单用。部分平仓靠传交易量参数,默认 0 为全平;最小手 100、持仓 300 时传 1 或 2 即部分平,传 0/3/超量则全平,分数市场下 1~299 都留残仓。 学完可做的验证:开 MT5 建 EA,用数字键盘改挂单的 TP/SL/开价,观察 Toolbox→Trade 里是否同步;再把 CommonData 套进自己的下单类,传一个 magic 看 ticket 能否回查。外汇贵金属杠杆高,模拟账户跑通再上真钱。

MQL5 / C++
class="kw">inline class="type">void CommonData(const ENUM_ORDER_TYPE type, const class="type">class="kw">double Price, const class="type">class="kw">double FinanceStop, const class="type">class="kw">double FinanceTake, const class="type">uint Leverage, const class="type">bool IsDayTrade)

◍ 填单结构里的手数与止损换算

下面这段是 EA 在下单前组装 MqlTradeRequest 的核心片段,直接决定了发单的手数梯度和止损/止盈的挂法。 double Desloc; 声明一个局部变量,用来承接「资金止损转点数」的返回值,后面 SL/TP 都靠它。ZeroMemory(m_TradeRequest); 先把结构体清零,避免上一次挂单残留字段污染本次请求。 m_TradeRequest.volume 用 NormalizeDouble 把「最小手数 + 步长×(杠杆档-1)」规整到品种小数位,意味着 Leverage 每加 1 档就多一个 VolStep,阶梯加仓逻辑写死在这行。 Desloc = FinanceToPoints(FinanceStop, Leverage); 把资金维度的止损换算成点数;若返回 0 则 sl 填 0(不挂止损),否则买单价减、卖单价加,方向由 ORDER_TYPE_BUY 判定。TP 同理反向加减。 m_TradeRequest.deviation 设成 1000 点,属于极端宽滑点容差,仅在流动性异常时段可能成交,正常行情几乎不会触发重试。外汇与贵金属杠杆交易风险高,这种宽 deviation 设置可能让滑点超出预期。 开 MT5 把这段塞进自己 EA 的 SendOrder 前,改 Leverage 和 FinanceStop 跑一遍品种规格,看 volume 和 sl 数值是否和账户最小手数、止损点数对得上。

MQL5 / C++
{
   class="type">class="kw">double Desloc;

   ZeroMemory(m_TradeRequest);
   m_TradeRequest.magic         = m_Infos.MagicNumber;
   m_TradeRequest.symbol        = _Symbol;
   m_TradeRequest.volume        = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - class="num">1)), m_Infos.nDigits);
   m_TradeRequest.price         = NormalizeDouble(Price, m_Infos.nDigits);
   Desloc = FinanceToPoints(FinanceStop, Leverage);
   m_TradeRequest.sl            = NormalizeDouble(Desloc == class="num">0 ? class="num">0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -class="num">1 : class="num">1)), m_Infos.nDigits);
   Desloc = FinanceToPoints(FinanceTake, Leverage);
   m_TradeRequest.tp            = NormalizeDouble(Desloc == class="num">0 ? class="num">0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? class="num">1 : -class="num">1)), m_Infos.nDigits);
   m_TradeRequest.type_time     = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
   m_TradeRequest.stoplimit     = class="num">0;
   m_TradeRequest.expiration    = class="num">0;
   m_TradeRequest.type_filling  = ORDER_FILLING_RETURN;
   m_TradeRequest.deviation     = class="num">1000;
   m_TradeRequest.comment       = "Order Generated by Experts Advisor.";
}

挂单类型的自动判定逻辑

在 MT5 的 EA 里发挂单,最容易被忽略的是 Limit 与 Stop 的自动归类:同样传一个 ORDER_TYPE_BUY,如果挂单价低于当前 ask 就是 BUY_LIMIT,高于 ask 则变成 BUY_STOP,卖单同理以 bid 与 Price 的关系翻转。下面这段函数把这件事封装进了 CreateOrder,调用方只需给方向,不用自己算挂单子类。 函数开头先取 bid / ask:当 m_Infos.PlotLast 为真时用 SYMBOL_LAST 代替 bid,ask 也随之等于该值,适合部分品种的收盘价挂单模式;否则走常规 SYMBOL_BID / SYMBOL_ASK。接着 CommonData 把价格、止损金额、止盈金额、杠杆、是否日内交易写进内部结构体,再填 m_TradeRequest。 值得注意,原文里出现了两次 m_TradeRequest.action = TRADE_ACTION_PENDING 的赋值,且中间插了一句 ZeroMemory(m_TradeRequest) 把结构体清空——这意味着第一次填的 action 会被洗掉,第二次才生效。如果你直接抄这段代码,零内存后必须重新赋 symbol、volume 等字段,否则发单请求会缺参数。 volume 用 NormalizeDouble(m_Infos.VolMinimal + m_Infos.VolStep*(Leverage-1), nDigits) 计算,杠杆每加 1 手数递增一个 VolStep。外汇与贵金属杠杆波动大,高杠杆下爆仓概率显著上升,实盘前务必在策略测试器里用最小资金跑一遍该函数的发单返回码。

MQL5 / C++
class="type">class="kw">ulong CreateOrder(const ENUM_ORDER_TYPE type, const class="type">class="kw">double Price, const class="type">class="kw">double FinanceStop, const class="type">class="kw">double FinanceTake, const class="type">uint Leverage, const class="type">bool IsDayTrade)
              {
                class="type">class="kw">double  bid, ask, Desloc;

                Price = AdjustPrice(Price);
                bid = SymbolInfoDouble(_Symbol, (m_Infos.PlotLast ? SYMBOL_LAST : SYMBOL_BID));
                ask = (m_Infos.PlotLast ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                CommonData(type, AdjustPrice(Price), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                m_TradeRequest.action   = TRADE_ACTION_PENDING;
                m_TradeRequest.type     = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) :
                                        (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
                ZeroMemory(m_TradeRequest);
                m_TradeRequest.action               = TRADE_ACTION_PENDING;
                m_TradeRequest.symbol               = _Symbol;
                m_TradeRequest.volume               = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - class="num">1)), m_Infos.nDigits);
交给小布盯盘看账户冲突
这些多EA同品种运行的账户风险点,小布盯盘的AIGC已内置监测,打开对应品种页即可看到冲突预警,你专注策略而非盯后台。

常见问题

净持账户由服务器生成均摊持仓价,多EA反向指令会互冲致失效;对冲账户订单独立,一卖一买不抵消,但锁单不算平仓。
多数EA市价直进直出,但部分模型需先挂止损单或调整原订单价,缺这两类函数就无法覆盖常见操作逻辑。
可以,小布盯盘的品种页内置AIGC诊断,能提示同经纪商同品种的多EA或手工叠加运行隐患,减少人为漏看。
对冲账户反向单是独立持仓,锁定浮动盈亏但不关闭原仓,只有发送明确平仓指令才终结仓位。
分别负责市价发送买卖单、修改挂单价格、以及对冲账户下显式平仓,配合前两篇函数覆盖九成以上自动场景。