市场模拟(第四部分):创建 C_Orders 类(一)·进阶篇
(2/3)· 回放系统卡在订单执行前?本篇先把类结构讲透,避免模块故障拖垮整条通信链
「事件解码与下单通道的衔接点」
图表端把交易意图编码成带问号分隔的字符串,Decode 函数负责把它还原成结构化事件。StringSplit 以 '?' 切割,若返回的数组长度不等于 7 直接返回 false,说明协议字段缺失或版本错配,这一道拦截能避免后续越界访问 Res[5]、Res[6] 导致崩EA。 还原出的 loc 结构体包含事件类型、品种、方向标记(Res[3]=="D" 才视为 true)、ushort 端口号以及两个 double 数值字段。只有当 loc.ev 与传入 ev 一致、且 loc.szSymbol 等于当前图表 _Symbol 时,才把数据写进 Data,等于做了品种与事件类型的双重过滤。 真正把单子丢给服务器的是 SendToPhysicalServer。它先 ZeroMemory 两个结果结构体,再用 OrderCheck 做预检;预检失败直接打印错误码并返回 0,不浪费一次实盘请求。 OrderSend 之后只看 TradeResult.retcode 是否等于 TRADE_RETCODE_DONE,非此即打印 retcode 并返回 0,成功才返回订单号 TradeResult.order。外汇与贵金属杠杆高,这套预检+回码判断能降低误发单概率,但是否成交仍取决于流动性与滑点,实盘前务必在 MT5 策略测试器用历史数据跑通 retcode 分支。
class="type">bool Decode(const EnumEvents ev, const class="type">class="kw">string sparam) { class="type">class="kw">string Res[]; if (StringSplit(sparam, &class="macro">#x27;?&class="macro">#x27;, Res) != class="num">7) class="kw">return false; stEvent loc = {(EnumEvents) StringToInteger(Res[class="num">0]), Res[class="num">1], Res[class="num">2], (class="type">bool)(Res[class="num">3] == "D"), (class="type">class="kw">ushort) StringToInteger(Res[class="num">4]), StringToDouble(Res[class="num">5]), StringToDouble(Res[class="num">6])}; if ((ev == loc.ev) && (loc.szSymbol == _Symbol)) Data = loc; class="kw">return true; } class=class="str">"cmt">//--- }m_ChartTrade; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong SendToPhysicalServer(class="type">void) { MqlTradeCheckResult TradeCheck; class="type">MqlTradeResult TradeResult; ZeroMemory(TradeCheck); ZeroMemory(TradeResult); if (!OrderCheck(m_TradeRequest, TradeCheck)) { PrintFormat("Order System - Check Error: %d", GetLastError()); class="kw">return class="num">0; } m_bTrash = OrderSend(m_TradeRequest, TradeResult); if (TradeResult.retcode != TRADE_RETCODE_DONE) { PrintFormat("Order System - Send Error: %d", TradeResult.retcode); class="kw">return class="num">0; }; class="kw">return TradeResult.order; }
市价单请求是怎么拼出来的
下面这段函数把一笔市价单的 MqlTradeRequest 结构一次性填完,开 MT5 把代码贴进 EA 就能直接发单验证。 价格取 ASK 或 BID 取决于买卖方向,成交量用 SYMBOL_VOLUME_STEP 做最小步长,再用 Leverange 字段放大——比如 Leverange 填 2,volume 就是步长的 2 倍,这不是常规手数逻辑,调参时得盯紧。 SL/TP 按 PointsStop、PointsTake 的点数偏移算,买单止损在价下方、卖单在价上方;若偏移为 0 则不设。day trade 标记会把 type_time 切到 ORDER_TIME_DAY,否则走 GTC。 deviation 写死 1000 点、filling 用 RETURN,这种宽松设置可能在流动性差的贵金属时段导致部分成交,外汇和贵金属均为高风险品种,实盘前务必在策略测试器回测。
class="type">ulong ToMarket(const ENUM_ORDER_TYPE type) { class="type">class="kw">double price = SymbolInfoDouble(m_ChartTrade.Data.szContract, (type == ORDER_TYPE_BUY ? SYMBOL_ASK : SYMBOL_BID)); class="type">class="kw">double vol = SymbolInfoDouble(m_ChartTrade.Data.szContract, SYMBOL_VOLUME_STEP); class="type">uchar nDigit = (class="type">uchar)SymbolInfoInteger(m_ChartTrade.Data.szContract, SYMBOL_DIGITS); ZeroMemory(m_TradeRequest); m_TradeRequest.magic = m_MagicNumber; m_TradeRequest.symbol = m_ChartTrade.Data.szContract; m_TradeRequest.price = NormalizeDouble(price, nDigit); m_TradeRequest.action = TRADE_ACTION_DEAL; m_TradeRequest.sl = NormalizeDouble(m_ChartTrade.Data.PointsStop == class="num">0 ? class="num">0 : price + (m_ChartTrade.Data.PointsStop * (type == ORDER_TYPE_BUY ? -class="num">1 : class="num">1)), nDigit); m_TradeRequest.tp = NormalizeDouble(m_ChartTrade.Data.PointsTake == class="num">0 ? class="num">0 : price + (m_ChartTrade.Data.PointsTake * (type == ORDER_TYPE_BUY ? class="num">1 : -class="num">1)), nDigit); m_TradeRequest.volume = NormalizeDouble(vol + (vol * (m_ChartTrade.Data.Leverange - class="num">1)), nDigit); m_TradeRequest.type = type; m_TradeRequest.type_time = (m_ChartTrade.Data.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."; class="type">MqlTradeRequest TradeRequest[class="num">1]; TradeRequest[class="num">0] = m_TradeRequest; ArrayPrint(TradeRequest);
◍ 图表事件如何驱动下单
这段 C_Orders 类的实现展示了把图表自定义事件翻译成实盘动作的典型写法。构造函数只接收一个 magic 参数并存入 m_MagicNumber,用于后续订单识别与过滤。 DispatchMessage 是核心入口:当收到 CHARTEVENT_CUSTOM 加上 evChartTradeBuy / evChartTradeSell / evChartTradeCloseAll 的事件 id 时,先由 m_ChartTrade.Decode 解析 sparam 字符串,再按 m_ChartTrade.Data.ev 分支处理。 注意第 113 与 116 行,Buy 和 Sell 事件直接调用 ToMarket(ORDER_TYPE_BUY/SELL),而 CloseAll 分支在第 119 行仅是 break,没有平仓逻辑——这是留给你补的空位。 回看第 93 行,ToMarket 内部对市价单类型才走 SendToPhysicalServer(),返回 0 表示非市价事件被忽略。开 MT5 把这段贴进 EA,在自定义事件里发一个 evChartTradeBuy 字符串,就能验证图表按钮是否真的触发了服务器发单。外汇与贵金属杠杆高,这类自动发单逻辑务必先在策略测试器跑通再上真仓。
class="num">093. class="kw">return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? SendToPhysicalServer() : class="num">0); class="num">094. }; class="num">095. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">096. class="kw">public : class="num">097. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">098. C_Orders(const class="type">ulong magic) class="num">099. :m_MagicNumber(magic) class="num">100. { class="num">101. } class="num">102. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">103. class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) class="num">104. { class="num">105. class="kw">switch (id) class="num">106. { class="num">107. case CHARTEVENT_CUSTOM + evChartTradeBuy : class="num">108. case CHARTEVENT_CUSTOM + evChartTradeSell : class="num">109. case CHARTEVENT_CUSTOM + evChartTradeCloseAll: class="num">110. if (m_ChartTrade.Decode((EnumEvents)(id - CHARTEVENT_CUSTOM), sparam)) class="kw">switch (m_ChartTrade.Data.ev) class="num">111. { class="num">112. case evChartTradeBuy: class="num">113. ToMarket(ORDER_TYPE_BUY); class="num">114. break; class="num">115. case evChartTradeSell: class="num">116. ToMarket(ORDER_TYPE_SELL); class="num">117. break; class="num">118. case evChartTradeCloseAll: class="num">119. break; class="num">120. } class="num">121. break; class="num">122. } class="num">123. } class="num">124. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">125. }; class="num">126. class=class="str">"cmt">//+------------------------------------------------------------------+
「用结构替类建模消息的隐患与边界」
在回放系统的翻译模块里,我弃用类、改用结构来承载消息数据。类本质就是更复杂的结构,当需求只是简单建模一组变量时,结构更轻;代码里第 18 到 42 行是整体数据结构,其中第 20 到 29 行才是真正的消息子集,消息本体是一条字符串。 若只按结构长度去切字符串,极易误读含义,所以第 31 到 40 行放了一个主结构内的函数来专门解码,它不属消息结构本身,把逻辑拆出来能降低出错概率。 第 36 行是真正的雷区:这行把全部所需值填进数据结构。若是函数或过程,执行第 35 行、或调用被第 36 行覆盖的内存段,传统 C 下会出严重事故——这也正是旧派 C 强调用类隔离的原因。 调用点在第 110 行,不是直接写函数名,而是传一个指向结构数据的引用,逻辑等同于调类方法。DECODE 刻意不塞进 C_Orders 类,因为它只负责解码消息;若塞进去,日后换目标结构就得硬造新名,时间一长代码语义就烂掉。从逻辑而非方便去分职责,才能少留命名冲突和隐性 bug。