从头开始开发智能交易系统(第 19 部分):新订单系统 (II)(基础篇)
📘

从头开始开发智能交易系统(第 19 部分):新订单系统 (II)(基础篇)

第 1/3 篇

◍ 把旧订单逻辑换成 CTrade 封装

在 MT5 里手写 OrderSend / OrderSendAsync 很容易踩坑:参数顺序、填充策略、返回码处理全得自己兜底。MQL5 标准库里的 CTrade 类把这些脏活封装掉了,新订单系统优先用它,而不是重复造轮子。 下面这段初始化代码在 EA 的 OnInit 里跑一次即可,把交易对象挂到全局,后续下单直接调方法,不用每次重算品种属性。注意 Symbol 和小数位在切换品种时要重新取,别写死。 高风险提示:外汇与贵金属杠杆品种滑点、拒单概率不低,任何封装都不能消除执行风险,回测顺不等于实盘稳。

MQL5 / C++
CTrade trade;
class="type">int OnInit()
{
   trade.SetExpertMagicNumber(class="num">123456);
   trade.SetDeviationInPoints(class="num">10);
   class="kw">return(INIT_SUCCEEDED);
}

把订单系统搬上图表

上一节我们给订单系统打了补丁,让它能同时适配净持与对冲两类结算账户。净持账户下每种资产只能存在一笔持仓,系统会维护一个平均开仓价;而对冲账户允许多笔独立持仓并存,同一资产可同时持有多空,这也是后续理解期权类策略的前提。 现在要做的是把这套订单系统完全可视化。目标是干掉频繁弹出的消息框,直接把每笔持仓的关键数值画在图表上,靠肉眼和交互就能分析,不再依赖弹窗二次计算。 借助新的订单系统,我们可以一次性调整好几处逻辑。EA 实时刷出相关信息后,OCO 订单或挂单的盈亏限额也能直接读出来,不用再手动折算。本节只是第一阶段改造,并非从零起步,而是在现有系统上往交易品种图表里加对象和事件。

「用平台原生对象绕过订单数量天花板」

把订单画到图表上,思路不复杂:只改图表上表示订单的那层,不动底层逻辑。但真要做出来,得靠对数据的建模方式,让 MT5 替你扛住繁琐的维护活。 常见的三种建模里,列表(单/双循环或哈希)实现最容易,代价是要自己写一大堆防数据错乱和限量的逻辑;类数组省了点代码,事件处理却会突然变棘手。我们选的第三条路最反直觉——逼 MQL5 把图表上的动态对象当成一等公民来用。 只要命名唯一,就能建任意多条水平线,每条独立收事件。下面这段 C_HLineTrade 里的函数就是核心:用订单ticket拼出唯一对象名,再交给平台创建。 别把命名当小事 对象名里塞了类型枚举和ticket,保证全局唯一;平台靠这个名字路由事件,你只管按名写响应,不用维护数组下标。 不过光这样还不够。EA 当前环境不能基于一个名字挂无限对象,得再往深改几行,但整体结构不动,稳定性优先。改完之后,屏幕上的线没有数量上限,平台照常替你跑事件。外汇和贵金属波动剧烈,这种无上限对象模型若参数失控,图表对象泛滥会拖慢终端,实盘前务必在策略测试器里压测。

MQL5 / C++
class="kw">inline class="type">void SetLineOrder(class="type">ulong ticket, class="type">class="kw">double price, eHLineTrade hl, class="type">bool select)
{
    class="type">class="kw">string sz0 = def_NameHLineTrade + (class="type">class="kw">string)hl + (class="type">class="kw">string)ticket, sz1;
    ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, class="num">0, class="num">0, class="num">0);
class=class="str">"cmt">//... The rest of the code.... 

◍ 把水平线类重构成可扩展对象类

把原先的 C_HLineTrade 换成 C_ObjectsTrade,核心意图只有一个:让一套类能挂任意数量的图表对象,而不再局限于水平线。基类里我把取止损、取止盈、取买卖方向的函数全声明成 inline,可执行文件会略微变大,但调用链更短、封装更稳。 protected 区里留了 eIndicatorTrade 枚举,IT_STOP 直接硬编码成 65,这只是给未来扩展留的占位方案。派生类不允许直接碰基类私有值,否则以后改 bug 或加功能会牵一发动全身——哪怕返回值只给派生类用,我也坚持走过程调用。 私密声明里最关键是对象名称的建模串。创建对象时名称按特定格式拼出来,后面用 StringSplit 反向拆开,等于用 MetaTrader 5 自带的字符串函数替我们做了解码器,不必自己写解析。每次 OnTrade 触发,EA 会试着在指定点建指标;若用户删了限制,价格变零,代码里就顺手把无用对象从图表和内存清掉。 重载是同名的 N 合一大杀器。例如建线函数有两个版本,区别仅在一个 price 参数,编译器靠参数数量区分。调用重载版比到处复制代码干净太多,也少了漏写声明的隐患。Ticket0 对应的指标编号写死为 1(def_IndicatorTicket0 1),它在 SHIFT 买 / CTRL 卖 的挂单逻辑里不能删,所以移除函数里专门高亮跳过它。 下面这段是类里的基础定义和限额设置函数,你可以直接贴进 MT5 看编译体积变化,并在私有区补完其余成员后实测封装是否如文中所说更抗改。外汇与贵金属杠杆高,任何 EA 结构改动都先在模拟盘验证再上真实账户。

MQL5 / C++
class C_ObjectsTrade
{
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_NameObjectsTrade 	"SMD_OT"
class="macro">#define def_SeparatorInfo      &class="macro">#x27;*&class="macro">#x27;
class="macro">#define def_IndicatorTicket0   class="num">1
class=class="str">"cmt">//+------------------------------------------------------------------+
        class="kw">protected:
            enum eIndicatorTrade {IT_NULL, IT_STOP= class="num">65, IT_TAKE, IT_PRICE};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// ... The rest of the class code
class="kw">inline class="type">class="kw">double GetLimitsTake(class="type">void) const { class="kw">return m_Limits.TakeProfit; }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">class="kw">double GetLimitsStop(class="type">void) const { class="kw">return m_Limits.StopLoss; }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">bool GetLimitsIsBuy(class="type">void) const { class="kw">return m_Limits.IsBuy; }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">void SetLimits(class="type">class="kw">double take, class="type">class="kw">double stop, class="type">bool isbuy)
{
    m_Limits.IsBuy = isbuy;
    m_Limits.TakeProfit = (m_Limits.TakeProfit < class="num">0 ? take : (isbuy ? (m_Limits.TakeProfit > take ? m_Limits.TakeProfit : take) : (take > m_Limits.TakeProfit ? m_Limits.TakeProfit : take)));
    m_Limits.StopLoss = (m_Limits.StopLoss < class="num">0 ? stop : (isbuy ? (m_Limits.StopLoss < stop ? m_Limits.StopLoss : stop) : (stop < m_Limits.StopLoss ? m_Limits.StopLoss : stop)));
}
class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

把OrderSend系列调用删掉,改成CTrade的Buy/Sell方法,并在初始化里声明CTrade trade; 即可接管下单与改仓。
用平台原生对象按ticket分别建水平线,历史单用灰色、当前单用彩色,并在对象名里带ticket前缀做区分。
可以,小布能读取你的策略源码并标出直接用OrderSend的地方,提示改用原生对象或CTrade来突破数量天花板。
只需继承原水平线类重写绘制方法,主循环不必动,新增样式类注册进工厂即可生效。
原生对象由终端维护生命周期,重连或重启后自动恢复,比手动维护数组少一层同步出错可能。