开发回放系统(第33部分):订单系统(二)·进阶篇
◍ 给订单系统接上回放练习能力
把手动或半自动的订单逻辑升级成可回放练习的模块,思路直接沿用「自动工作EA」系列里已经验证过的框架,不必从零造轮子。MetaTrader 5 自带的策略测试器能跑历史数据,但本文想解决的是另一种场景:行情休市时,你仍能用模拟环境反复练下单与撤单的节奏。 具体落地从计时器类(timer class)切入,它负责在本地模拟时间轴推进订单状态,不依赖真实报价流。第一阶段我们会快速搬运该系列里现成的功能,重点放在「方便访问交易服务器」这一层,不做深究。 若你想抠实现细节,建议直接翻原系列第15篇关于自动化的章节;本文只做工程拼装,不重复底层解释。外汇与贵金属杠杆高,模拟练手不等于实盘安全,参数未经多周期验证前勿直接上真仓。
「用时间表掐住 EA 的开仓时段」
不少做外汇、贵金属的老手都吃过情绪交易的亏:亚盘早段流动性差、点差跳开,明明计划等欧美重叠时段再动,手却比脑子快。C_ControlOfTime 这个类就是用来把「人管不住手」变成「EA 替你挡枪」的,它继承自 C_Orders,但只扩展不外露,EA 主体并不会直接继承那套下单逻辑。 类里最容易被忽略的一行是 m_InfoCtrl[SATURDAY + 1]。矩阵长度必须 +1,因为枚举从 0 开始、SUNDAY 到 SATURDAY 共 7 个值,若不加一就只能装下 6 天,周六直接越界。构造函数里先调 C_Orders 初始化,再用 for 从 SUNDAY 跑到 SATURDAY 做 ZeroMemory——这里依赖枚举数值 SUNDAY 小于 SATURDAY,否则下标访问会乱序。 SetInfoCtrl 接收形如 "09:00-17:30" 的字符串,用 '-' 切分后各自 StringToTime 再对 86400 取模,只留当日秒数。它顺手校验了 Init<=End 以及时分不超 23/59,否则整段配置直接作废。这套写法把时间控制写得很接近自然语言,接手你代码的人哪怕不懂 MQL5 也能看懂哪天哪段能跑。 外汇与贵金属杠杆高、滑点突发,时间过滤器只是降低情绪干扰的概率手段,不保证避开所有坑。把上面代码塞进 MT5 的 include,用 SATURDAY+1 的矩阵跑一遍周回调试,比看十篇鸡汤有用。
class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_Orders.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_ControlOfTime : class="kw">protected C_Orders { class="kw">private : class="kw">struct st_00 { class="type">class="kw">datetime Init, End; }m_InfoCtrl[SATURDAY + class="num">1]; class=class="str">"cmt">//+------------------------------------------------------------------+ C_ControlOfTime(C_Terminal *arg, class="kw">const class="type">class="kw">ulong magic) :C_Orders(arg, magic) { for (ENUM_DAY_OF_WEEK c0 = SUNDAY; c0 <= SATURDAY; c0++) ZeroMemory(m_InfoCtrl[c0]); } class="kw">virtual class="type">void SetInfoCtrl(class="kw">const ENUM_DAY_OF_WEEK index, class="kw">const class="type">class="kw">string szArg) final { class="type">class="kw">string szRes[], sz1[]; class="type">bool bLocal; if (_LastError != ERR_SUCCESS) class="kw">return; if ((index > SATURDAY) || (index < SUNDAY)) class="kw">return; if (bLocal = (StringSplit(szArg, &class="macro">#x27;-&class="macro">#x27;, szRes) == class="num">2)) { m_InfoCtrl[index].Init = (StringToTime(szRes[class="num">0]) % class="num">86400); m_InfoCtrl[index].End = (StringToTime(szRes[class="num">1]) % class="num">86400); bLocal = (m_InfoCtrl[index].Init <= m_InfoCtrl[index].End); for (class="type">char c0 = class="num">0; (c0 <= class="num">1) && (bLocal); c0++) if (bLocal = (StringSplit(szRes[class="num">0], &class="macro">#x27;:&class="macro">#x27;, sz1) == class="num">2)) bLocal = (StringToInteger(sz1[class="num">0]) <= class="num">23) && (StringToInteger(sz1[class="num">1]) <= class="num">59); if (_LastError == ERR_WRONG_STRING_DATE) ResetLastError();
日内时段校验与退出保护
这段逻辑把“时段声明是否合法”和“当前是否处于可控交易窗口”拆成了两层处理。若声明阶段出现 _LastError 非 ERR_SUCCESS,或本地标识 bLocal 为假,直接 Print 具体枚举并调用 ExpertRemove 终止 EA,避免脏配置继续跑。 CtrlTimeIsPassed 用 TimeCurrent 取本地 MqlDateTime,把小时和分钟折算成当日秒数:dt = hour*3600 + min*60。再比对 m_InfoCtrl[day_of_week] 里存的 Init 与 End 秒数边界,落在 [Init, End] 内才返回 true。 实盘里这套写法意味着:周几配错、边界越界都会让 EA 自爆而非乱下单;验证时把 m_InfoCtrl 某天的 End 设成小于 Init,MT5 会在初始化后立刻在日志看到 'Error in the declaration of time of day' 并卸载。
}
if ((_LastError != ERR_SUCCESS) || (!bLocal))
{
Print("Error in the declaration of the time of day: ", EnumToString(index));
ExpertRemove();
}
}
class="kw">virtual class="kw">const class="type">bool CtrlTimeIsPassed(class="type">void) final
{
class="type">class="kw">datetime dt;
class="type">MqlDateTime mdt;
TimeCurrent(mdt);
dt = (mdt.hour * class="num">3600) + (mdt.min * class="num">60);
class="kw">return ((m_InfoCtrl[mdt.day_of_week].Init <= dt) && (m_InfoCtrl[mdt.day_of_week].End >= dt));
}◍ 把 EA 复杂度关进 C_Manager 笼子里
C_Manager 类干的事很简单也很关键:把 EA 本身的逻辑压到几行,所有下单、账户识别、对象管理都收进类里。这样后期接回放/模拟器时,EA 主体不用反复改,代价是当前阶段订单系统只认 MT5 原生能力,不支持交叉品种下单——也就是 EA 处理的标的之外,发不了别的资产订单。外汇与贵金属杠杆高,这种隔离能降低误触其他品种的风险。 类开头用宏定义了一组前缀和快捷访问符,比如 def_LINE_PRICE 拼出 "Manager_PRICE" 用来挂图线,def_InfoTerminal 直接取终端信息。私有区把全局变量塞进 st00、st01 两个结构,st00 装止损止盈金额、杠杆、是否日内及账户锁仓模式,st01 装画线颜色和创建开关。用结构打包,以后删改模块时不用满屏找变量。 第一个对外函数是 CreateOrder,只接收订单类型和价格,内部先跑 CtrlTimeIsPassed() 做时间闸门——超过计划交易时段直接返回 false,不发单。真正填单用的止损、止盈、杠杆都来自 m_Infos 结构,调用方不用管。另一个私有函数走市价成交,只传买卖方向,其余照全局变量填。 全局变量绝不在 EA 里直接碰,初始化交给构造函数,虽长但只是按 EA 传入参数填一遍,并顺手用 TerminalInfoString 查账户类型留作分析。析构函数基本空转,真出事才清掉类建的对象。下面这段是当前类的骨架代码,复制进 MT5 能直接看结构。
class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "C_ControlOfTime.mqh" class="macro">#include "..\System EA\Auxiliar\Study\C_Study.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_Prefix "Manager" class="macro">#define def_LINE_PRICE def_Prefix + "_PRICE" class="macro">#define def_LINE_TAKE def_Prefix + "_TAKE" class="macro">#define def_LINE_STOP def_Prefix + "_STOP" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_AcessTerminal(*Terminal) class="macro">#define def_InfoTerminal def_AcessTerminal.GetInfoTerminal() class="macro">#define def_AcessMouse(*Study) class="macro">#define def_InfoMouse def_AcessMouse.GetInfoMouse() class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Manager : class="kw">public C_ControlOfTime { class="kw">private : class="kw">struct st00 { class="type">class="kw">double FinanceStop, FinanceTake; class="type">uint Leverage; class="type">bool IsDayTrade, AccountHedging; }m_Infos; class="kw">struct st01 { class="type">color corPrice, corTake, corStop; class="type">bool bCreate; }m_Objects; class=class="str">"cmt">//+------------------------------------------------------------------+ C_Terminal *Terminal; C_Study *Study; class="type">bool CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price) { class="type">class="kw">ulong tmp; if (!CtrlTimeIsPassed()) class="kw">return class="kw">false; tmp = C_Orders::CreateOrder(type, Price, m_Infos.FinanceStop, m_Infos.FinanceTake, m_Infos.Leverage, m_Infos.IsDayTrade); }
「下单前先让时间闸和账户模式说话」
这段 MQL5 片段把「能不能下单」拆成了两道关卡:时间控制与账户对冲属性。ToMarket 里先调 CtrlTimeIsPassed(),不通过直接返回 false,说明策略层把交易时段卡死,避免在非活跃窗口瞎单。 C_Manager 构造函数用 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 读账户保证金模式,分三种:RETAIL_HEDGING 置 AccountHedging=true 并打印 "HEDGING";RETAIL_NETTING 标 "NETTING";EXCHANGE 标 "EXCHANGE"。净仓与对冲账户的平仓逻辑不同,EA 启动时就该知道自己在哪种场子里跑。 外汇与贵金属杠杆高、滑点跳空频繁,HEDGING 模式锁仓不抵占用保证金,NETTING 模式同方向加仓合并,写 EA 时不区分可能让风控参数直接失效。开 MT5 接这段,把 Print 出来的账户类型对一下,再决定 FinanceStop / FinanceTake 怎么传。
class="kw">return tmp > class="num">0; } class="type">bool ToMarket(class="kw">const ENUM_ORDER_TYPE type) { class="type">class="kw">ulong tmp; if (!CtrlTimeIsPassed()) class="kw">return class="kw">false; tmp = C_Orders::ToMarket(type, m_Infos.FinanceStop, m_Infos.FinanceTake, m_Infos.Leverage, m_Infos.IsDayTrade); class="kw">return tmp > class="num">0; } C_Manager(C_Terminal *arg1, C_Study *arg2, class="type">color cPrice, class="type">color cStop, class="type">color cTake, class="kw">const class="type">class="kw">ulong magic, class="kw">const class="type">class="kw">double FinanceStop, class="kw">const class="type">class="kw">double FinanceTake, class="type">uint Leverage, class="type">bool IsDayTrade) :C_ControlOfTime(arg1, magic) { class="type">class="kw">string szInfo = "HEDGING"; Terminal = arg1; Study = arg2; if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (CheckPointer(Study) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (_LastError != ERR_SUCCESS) class="kw">return; m_Infos.FinanceStop = FinanceStop; m_Infos.FinanceTake = FinanceTake; m_Infos.Leverage = Leverage; m_Infos.IsDayTrade = IsDayTrade; m_Infos.AccountHedging = class="kw">false; m_Objects.corPrice = cPrice; m_Objects.corStop = cStop; m_Objects.corTake = cTake; m_Objects.bCreate = class="kw">false; class="kw">switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)) { case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: m_Infos.AccountHedging = true; class="kw">break; case ACCOUNT_MARGIN_MODE_RETAIL_NETTING: szInfo = "NETTING"; class="kw">break; case ACCOUNT_MARGIN_MODE_EXCHANGE : szInfo = "EXCHANGE"; class="kw">break; } Print("Detected Account ", szInfo); } ~C_Manager() { ObjectsDeleteAll(def_InfoTerminal.ID, def_Prefix); }
把工具请下神坛
C_Manager 类还剩一个方法没拆完,作者把它留到了下一篇单独讲 EA 代码的故事,理由是怕这篇读起来太冗长。当前这版 EA 代码仍处在开发阶段,不是能直接上线的成品。 本篇刻意没贴任何代码——不是藏私,是怕你在没吃透机制前拿去跑实盘。外汇和贵金属杠杆高,盲用半成品策略大概率亏多赚少。 知识是能复用的:不凭空造轮子,总是在旧东西上改、随时间长进。下一篇见那个方法和 EA 实码时,你再开 MT5 新建个类照着敲一遍,比现在空看更有用。