开发回放系统(第32部分):订单系统(一)·进阶篇
◍ C_Orders 怎么接交易服务器
做 EA 跟交易服务器打交道,最怕类结构乱改。这里把交互逻辑收进 C_Orders 类,不在类里写死终端访问,而是用宏定义转接 C_Terminal,后续换模拟或真实账户只改宏就行。 类本身不继承任何东西,全局变量全标 private:m_TradeRequest 存请求结构体,m_MagicNumber 标魔法号,Terminal 是指向 C_Terminal 的指针(MQL5 指针语义和 C++ 不同,但用法类似)。私有函数 ToServer 集中发单,便于以后在真实/模拟间切换而不碰业务代码。 ToServer 里先 ResetLastError 清错误态,ZeroMemory 把检查与结果结构体清零,再 OrderCheck 预检。只有 _LastError 为 ERR_SUCCESS 才进 OrderSend,否则打印错误号并返回 0。这套集中呼叫是从早先「手动转自动 EA」那套思路搬来的,能在不用策略测试器的情况下做重放验证。 构造函数接收 EA 创建的 C_Terminal 指针存进私有变量;若指针空就给 _LastError 记值,因为构造器不能 return。最后两个公开函数分别发市价单和类市价执行请求:止损止盈用金融价值而非点数,服务器按最优价成交,且必须指明买或卖,否则单子发不出去。 外汇和证交所市场别混:基于 BID-ASK 的图表和基于 LAST 的图表,挂单类型填法不同。新手跨市场常把挂单写成市价成交——价格对但订单类型错,服务器执行时机就偏了。贵金属、外汇波动大、杠杆高,跨市场跑 EA 前务必在模拟账户验一遍类型映射。
class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#include "..\C_Terminal.mqh" class="macro">#define def_AcessTerminal(*Terminal) class="macro">#define def_InfoTerminal def_AcessTerminal.GetInfoTerminal() class C_Orders { class="kw">private : class="type">MqlTradeRequest m_TradeRequest; class="type">class="kw">ulong m_MagicNumber; C_Terminal *Terminal; class="type">class="kw">ulong ToServer(class="type">void) { MqlTradeCheckResult TradeCheck; class="type">MqlTradeResult TradeResult; class="type">bool bTmp; ResetLastError(); ZeroMemory(TradeCheck); ZeroMemory(TradeResult); bTmp = OrderCheck(m_TradeRequest, TradeCheck); if (_LastError == ERR_SUCCESS) bTmp = OrderSend(m_TradeRequest, TradeResult); if (_LastError != ERR_SUCCESS) PrintFormat("Order System - Error Number: %d", _LastError); class="kw">return (_LastError == ERR_SUCCESS ? TradeResult.order : class="num">0); }
把资金止损换算成实际点位的下单封装
下面这段内联函数把「想亏多少美金」直接转成 MT5 的 m_TradeRequest 结构,交易者只需传订单类型、入场价、资金止损、资金止盈、杠杆倍数和是否日内平仓。 CommonData 先 ZeroMemory 清空请求结构,magic 与 symbol 取自终端对象;volume 用 VolumeMinimal + VolumeMinimal*(Leverage-1) 算出实际手数,再按交易品种小数位 NormalizeDouble。注意 deviation 写死 1000、type_filling 用 ORDER_FILLING_RETURN,在流动性差的贵金属跳空时可能滑点偏大。 SL/TP 不是直接写价格,而是调 FinanceToPoints 把资金额转成点数:买单 SL=Price-Desloc、TP=Price+Desloc,卖单反向。Desloc 为 0 时对应不挂止损/止盈。 FinanceToPoints 内部用 MathAbs((Finance/volume)/AdjustToTrade) 再交 AdjustPrice 修正,AdjustToTrade 来自 SYMBOL_TRADE_TICK_VALUE / SYMBOL_TRADE_TICK_SIZE。以 XAUUSD 为例,若 TickSize=0.01、TickValue=1.0、VolumeMinimal=0.01,杠杆 10 倍时 volume=0.1,亏 10 美金约对应 10 个点。外汇与贵金属杠杆交易风险高,点位换算误差可能放大亏损。 构造函数 C_Orders 检查终端指针有效性,无效就 SetUserError;ToMarket 对外暴露,接收资金止损止盈与杠杆,内部调 CommonData 组包。开 MT5 把这段代码塞进 EA,改 Leverage 参数即可看不同仓位下 SL/TP 跳动。
class="kw">inline class="type">void CommonData(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price, class="kw">const class="type">class="kw">double FinanceStop, class="kw">const class="type">class="kw">double FinanceTake, class="kw">const class="type">uint Leverage, class="kw">const class="type">bool IsDayTrade) { class="type">class="kw">double Desloc; ZeroMemory(m_TradeRequest); m_TradeRequest.magic = m_MagicNumber; m_TradeRequest.symbol = def_InfoTerminal.szSymbol; m_TradeRequest.volume = NormalizeDouble(def_InfoTerminal.VolumeMinimal + (def_InfoTerminal.VolumeMinimal * (Leverage - class="num">1)), def_InfoTerminal.nDigits); m_TradeRequest.price = NormalizeDouble(Price, def_InfoTerminal.nDigits); Desloc = def_AcessTerminal.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)), def_InfoTerminal.nDigits); Desloc = def_AcessTerminal.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)), def_InfoTerminal.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."; } class="kw">inline class="type">class="kw">double FinanceToPoints(class="kw">const class="type">class="kw">double Finance, class="kw">const class="type">uint Leverage) { class="type">class="kw">double volume = m_Infos.VolumeMinimal + (m_Infos.VolumeMinimal * (Leverage - class="num">1)); class="kw">return AdjustPrice(MathAbs(((Finance / volume) / m_Infos.AdjustToTrade))); }; m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); m_Infos.AdjustToTrade = m_Infos.ValuePerPoint / m_Infos.PointPerTick; C_Orders(C_Terminal *arg, class="kw">const class="type">class="kw">ulong magic) :m_MagicNumber(magic) { if (CheckPointer(Terminal = arg) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); } class="type">class="kw">ulong ToMarket(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double FinanceStop, class="kw">const class="type">class="kw">double FinanceTake, class="kw">const class="type">uint Leverage, class="kw">const class="type">bool IsDayTrade) {
「挂单与市价单的路由分叉」
这段逻辑把市价单和挂单拆成了两个出口,核心在 CreateOrder 里先抓 bid/ask,再按价格位置决定挂单类型。 当图表模式是 SYMBOL_CHART_MODE_LAST 时,bid 直接取 SYMBOL_LAST,ask 则等于 bid;否则 bid 取 SYMBOL_BID、ask 取 SYMBOL_ASK。这一步决定了你看到的「报价」到底来自成交价还是买卖盘。 挂单类型的判定很直白:买向挂单里,若 ask >= Price 归为 BUY_LIMIT,否则 BUY_STOP;卖向则 bid < Price 为 SELL_LIMIT,否则 SELL_STOP。也就是说,价格在你挂单位置的哪一侧,直接切换限价与止损单。 两个函数都只对 ORDER_TYPE_BUY / ORDER_TYPE_SELL 调用 ToServer(),其余类型直接返回 0,不会发往服务器。外汇与贵金属杠杆交易高风险,挂单触发受滑点影响,回测里该分支命中率需用真实点差数据验证。
CommonData(type, SymbolInfoDouble(def_InfoTerminal.szSymbol, (type == ORDER_TYPE_BUY ? SYMBOL_ASK : SYMBOL_BID)), FinanceStop, FinanceTake, Leverage, IsDayTrade); m_TradeRequest.action = TRADE_ACTION_DEAL; m_TradeRequest.type = type; class="kw">return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : class="num">0); }; class="type">class="kw">ulong CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price, class="kw">const class="type">class="kw">double FinanceStop, class="kw">const class="type">class="kw">double FinanceTake, class="kw">const class="type">uint Leverage, class="kw">const class="type">bool IsDayTrade) { class="type">class="kw">double bid, ask; bid = SymbolInfoDouble(def_InfoTerminal.szSymbol, (def_InfoTerminal.ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : SYMBOL_BID)); ask = (def_InfoTerminal.ChartMode == SYMBOL_CHART_MODE_LAST ? bid : SymbolInfoDouble(def_InfoTerminal.szSymbol, SYMBOL_ASK)); CommonData(type, def_AcessTerminal.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)); class="kw">return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : class="num">0); };
◍ 下一步让订单系统碰交易服务器
这一节没有附带 MQL5 代码,原因很直接:当前只是在搭一个 BASIC 类来承载订单系统的骨架,而不是真正去实现一套可跑的订单执行模块。对比前面几篇里出现的 C_Orders 类,你会发现这里少了一些函数和方法,那是作者故意把系统拆成了若干块,每块大小不一,用来逐步讲清楚它怎么和回放/模拟服务对接。 把订单系统拆开讲并不是偷懒。回放与模拟的集成本身相当复杂,混着真实账户概念一起写,读者很容易彻底乱掉。所以作者选择先孤立模拟层,等逻辑顺了,再让 EA 在物理层面连上演示或真实账户去碰交易服务器。 下一篇的落点是让这个订单系统开始与交易服务器交互,从演示或真实账户上跑通 EA,再回头理解订单类型怎么在模拟系统里映射。外汇与贵金属杠杆交易高风险,这类架构验证请只在策略测试器或演示账户里做,别直接上真实资金。