市场模拟(第四部分):创建 C_Orders 类(一)·进阶篇
🔧

市场模拟(第四部分):创建 C_Orders 类(一)·进阶篇

(2/3)· 回放系统卡在订单执行前?本篇先把类结构讲透,避免模块故障拖垮整条通信链

实战向进阶 第 2/3 篇
很多人急着看模拟器下单,却忽略订单类没理顺会让 EA、指标、服务器三方脱节。先把 C_Orders 的边界想清楚,后面跨期订单和实盘通信才不容易崩。这一篇不堆代码,先把结构讲明白。

「事件解码与下单通道的衔接点」

图表端把交易意图编码成带问号分隔的字符串,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 分支。

MQL5 / C++
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,这种宽松设置可能在流动性差的贵金属时段导致部分成交,外汇和贵金属均为高风险品种,实盘前务必在策略测试器回测。

MQL5 / C++
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 字符串,就能验证图表按钮是否真的触发了服务器发单。外汇与贵金属杠杆高,这类自动发单逻辑务必先在策略测试器跑通再上真仓。

MQL5 / C++
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。

把模块联调交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到订单消息流是否卡在某一模块。把重复劳动交给小布,你专注决策。

常见问题

消息传递系统已抽象了来源,EA 只解读收到的指令。这样模块故障不会直接污染下单逻辑,但要求消息结构在前几篇就定义清楚。
每个模块负责单一任务,任一处失败整体即停摆,没有隐藏旁路。这种写法便于定位性能与逻辑问题,也契合市场模拟对确定性的要求。
可以,小布盯盘的品种页会把订单相关消息流做可视化,哪一段没响应一眼能看出来,省去自己打日志的功夫。
概念没铺垫完就上代码容易误用。本篇先讲清楚类职责与过程结构,后续篇再逐步放出完整实现,外汇贵金属模拟涉及高风险,理清逻辑优先。