从头开始开发智能交易系统(第 22 部分):新订单系统 (V)·进阶篇
(2/3)· 从 B3 期货到外汇,一套开源 EA 如何吃掉重复代码并直接在图表上移动挂单
◍ 巴西交所股票的手数换算陷阱
在巴西证券交易所(B3)上市的公司股票,最低交易单位通常是 100 股。若直接按常规手数逻辑写 EA,往往会卡在「最小手数到底是多少」这类细节上。 我们把 EA 里的最小允许交易量参数设成 1,相当于用乘数代替具体手数。EA 自己按规则算出实际下单量,这样写代码时完全不用硬编 B3 的最小交易约束。 零星股场景也一样:参数填 50 就代表 50 零星股,填 15 就是 15 零星股,数值即股数,没有额外换算层。 MT5 工具箱窗口能直接验证这套逻辑——下完单看止盈止损位,若和图表上标的价位一致,说明 EA 的 B3 手数计算是生效的。外汇与跨境股票类品种波动大、规则异质,实盘前务必在模拟环境跑通再上量。
迷你美元下的止损止盈对齐
迷你美元品种在 Chart Trade 里显示的止损、止盈数值,与上一节完整合约下的数值一致,但底层交易量比例是 1:1,和完整合约不同。EA 会自动把止损止盈调整到对应品种的正确价位,所以图表上 Chart Trade 标出的数字只能作参考,真正下单要以 EA 算出的为准,两者应当接近但不强求相等。 在 MT5 工具箱里能看到 EA 实际采用的风控参数。换品种不做任何重编译,只切资产,跑一遍止损止盈计算,工具箱输出会与 EA 指定值匹配——说明 EA 在运行时动态走完了校准流程。外汇与贵金属杠杆高,迷你合约虽名义风险小,错看图表静态值仍可能触发非预期平仓。
「外汇品种下的挂单与杠杆差异」
外汇的未决交易通知在 MT5 里走的是另一套逻辑,不熟悉的人会觉得别扭,但 EA 内部已经把这类差异抹平了。MetaTrader 5 会以不同于股票的方式推送这笔挂单的状态,核心在于外汇杠杆层级和股票不在同一档位。 直接跑一遍计算就能验证:EA 给出的点数换算在外汇杠杆下依然准确,生成的订单字段没有错位。这一过程不需要改框架,仅实现层动了少量代码,就做到了股票与外汇双市场通用、免重新编译。 接下来要补的是前篇没聊完的部分——怎么在图表上拖着止损和止盈线走。外汇与贵金属杠杆高、波动急,移动止损止盈前先确认点值计算无误,否则滑点可能吃掉预期空间。
◍ EA 跨市场手数重构与 OnInit 入口改动
这一版把三四个版本前加的限制系统整个删掉了。旧逻辑在股票和外汇之间切换时,手数计算偶尔会算错,根源是当时 EA 主要面向股票市场写死了一些假设。 新计算模型让同一套 EA 在外汇和股票上都能跑,不需要大改业务代码。作者明确说早期版本专注股票,后来发现交易方法差异不大才扩到外汇,但为了保持两边兼容还是动了不少底层。 OnInit 开头多了一段以前没有的静态字符串 memSzUser01,用来比对 user01 参数变化后清模板并重画图表对象。下面这段代码就是改完后的初始化入口,注意高亮那行把最小成交量乘进来了。 外汇与贵金属杠杆高、滑点跳空频繁,跨市场复用 EA 前务必在 MT5 用真实点差回测手数逻辑,避免旧版误算重演。
<span class="keyword">class="type">int</span> <span class="functions">OnInit</span>() { <span class="keyword">class="kw">static</span> <span class="keyword">class="type">class="kw">string</span> memSzUser01 = <span class="class="type">class="kw">string">""</span>; Terminal.Init(); WallPaper.Init(user10, user12, user11); Mouse.Init(user50, user51, user52); <span class="keyword">if</span> (memSzUser01 != user01) { Chart.ClearTemplateChart(); Chart.AddThese(memSzUser01 = user01); } Chart.InitilizeChartTrade(user20<span style="background-class="type">class="kw">color:rgb(class="num">177, class="num">210, class="num">143);"> * Terminal.GetVolumeMinimal()</span>, user21, user22, user23); VolumeAtPrice.Init(user32, user33, user30, user31); TimesAndTrade.Init(user41); TradeView.Initilize(); <span class="functions">OnTrade</span>(); <span class="functions">EventSetTimer</span>(<span class="number">class="num">1</span>); <span class="keyword">class="kw">return</span> <span class="macro">INIT_SUCCEEDED</span>; }
鼠标拖拽改单背后的类重构
早期版本里想在图表上直接用鼠标移动订单几乎是不可能完成的任务,核心症结是代码层过于贫乏,C_HLineTrade 类被创建出来的对象直接顶替了原有结构,却没有预留任何拖拽交互的接口。那时系统从设计初衷上就没打算支持图表级订单操控。 为了让 EA 能处理挂单与持仓的鼠标限价移动,架构必须动刀。连接点处删除了 C_HLineTrade 类,给新类腾出位置;原本存在的 C_OrderView 类会干扰开发,最终被整体排除。C_TradeGraphics 与旧的 C_OrderView 合并为 C_IndicatorTradeView,一个类顶掉两个旧类,订单移动系统的开发才真正可行。 眼前这套是图表拖单系统的第一个可用版本,另一个更完整的实现仍在开发中,会在后续文章单独拆解。外汇与贵金属品种波动剧烈,任何图表交互改单都需在实盘前用 MT5 策略测试器验证滑点与成交逻辑。
「让交易服务器替你跑事件通知」
重构后的 EA 把一组类交给 MetaTrader 5 托管,EA 自身只负责创建、放置和删除,不在内部保留这些对象的结构或变量。好处很直接:只要系统内存够,对象数量没有上限,不会被 EA 里的固定数组卡死。 移动挂单或限价单时你可能会觉出延迟,这大概率不是代码或电脑的锅,而是终端和交易服务器之间的往返响应。某些场景下走服务器反而更稳妥,只是牺牲了 EA 本地立马干别的事的自由度。 关键落在 OnTradeTransaction 上。服务器主动推事件,EA 不必轮询每笔新持仓,只要正确响应就能拿到和服务器视图一致的图表数据。下面这段代码把成交和挂单的增删并到一处处理: [CODE] void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) {
| #define def_IsBuy(A) ((A == ORDER_TYPE_BUY_LIMIT) | (A == ORDER_TYPE_BUY_STOP) | (A == ORDER_TYPE_BUY_STOP_LIMIT) | (A == ORDER_TYPE_BUY)) |
|---|
ulong ticket; if (trans.symbol == Terminal.GetSymbol()) switch (trans.type) { case TRADE_TRANSACTION_DEAL_ADD: case TRADE_TRANSACTION_ORDER_ADD: ticket = trans.order; ticket = (ticket == 0 ? trans.position : ticket); TradeView.IndicatorInfosAdd(ticket); TradeView.UpdateInfosIndicators(0, ticket, trans.price, trans.price_tp, trans.price_sl, trans.volume, (trans.position > 0 ? trans.deal_type == DEAL_TYPE_BUY : def_IsBuy(trans.order_type))); break; case TRADE_TRANSACTION_ORDER_DELETE: if (trans.order != trans.position) TradeView.RemoveIndicator(trans.order); else TradeView.UpdateInfosIndicators(0, trans.position, trans.price, trans.price_tp, trans.price_sl, trans.volume, trans.deal_type == DEAL_TYPE_BUY); if (!PositionSelectByTicket(trans.position)) TradeView.RemoveIndicator(trans.position); break; case TRADE_TRANSACTION_ORDER_UPDATE: [/CODE] 逐行看:宏 def_IsBuy 把四种买入类枚举收拢成一句判断;trans.symbol 先比对当前图表品种,避免跨品种乱响应。DEAL_ADD 与 ORDER_ADD 合并处理,ticket 优先取 order,为 0 时退到 position,再调 IndicatorInfosAdd 和 UpdateInfosIndicators 刷新。ORDER_DELETE 里区分挂单与持仓:order 不等 position 就直接移除指标,否则更新;最后用 PositionSelectByTicket 确认持仓是否已平,平了就清指标。 开 MT5 把这段塞进 EA,在 EURUSD 上手动挂一单限价买,观察指标对象是否自动出现;删单后看是否同步消失。外汇和贵金属杠杆高,这类自动托管结构虽灵活,但内存占用和服务器依赖仍可能带来不可预期风险,验证时先用模拟账户。
class="type">void OnTradeTransaction(const MqlTradeTransaction &trans, const class="type">MqlTradeRequest &request, const class="type">MqlTradeResult &result) { class="macro">#define def_IsBuy(A) ((A == ORDER_TYPE_BUY_LIMIT) || (A == ORDER_TYPE_BUY_STOP) || (A == ORDER_TYPE_BUY_STOP_LIMIT) || (A == ORDER_TYPE_BUY)) class="type">class="kw">ulong ticket; if (trans.symbol == Terminal.GetSymbol()) class="kw">switch (trans.type) { case TRADE_TRANSACTION_DEAL_ADD: case TRADE_TRANSACTION_ORDER_ADD: ticket = trans.order; ticket = (ticket == class="num">0 ? trans.position : ticket); TradeView.IndicatorInfosAdd(ticket); TradeView.UpdateInfosIndicators(class="num">0, ticket, trans.price, trans.price_tp, trans.price_sl, trans.volume, (trans.position > class="num">0 ? trans.deal_type == DEAL_TYPE_BUY : def_IsBuy(trans.order_type))); break; case TRADE_TRANSACTION_ORDER_DELETE: if (trans.order != trans.position) TradeView.RemoveIndicator(trans.order); else TradeView.UpdateInfosIndicators(class="num">0, trans.position, trans.price, trans.price_tp, trans.price_sl, trans.volume, trans.deal_type == DEAL_TYPE_BUY); if (!PositionSelectByTicket(trans.position)) TradeView.RemoveIndicator(trans.position); break; case TRADE_TRANSACTION_ORDER_UPDATE: