市场模拟(第五部分):创建 C_Orders 类(二)(基础篇)
◍ 给订单类补上持仓与历史查询方法
在 MT5 的 EA 工程里,光有发单函数不够,随时摸清「当前持仓」和「已平历史」才是风控前提。这一节接着把 C_Orders 类补全,重点落在两类查询接口:按魔术码取当前持仓单数、按订单号拉历史成交。 实际跑起来,一个常见现象是:用 OrderSelect 遍历历史池时,若不加时间过滤,回测 2024 全年 EURUSD 的 M15 数据会扫出 3000+ 条记录,单次循环耗时可能从 2ms 涨到 40ms 以上,实盘掉帧明显。 下面这段是类里两个核心查询方法的骨架,直接贴进你的 .mqh 就能用。注意 m_magic 是构造时传入的 EA 标识,避免误选别的程序下的单。
class="type">int C_Orders::HistoryTotalByMagic(class="type">void) { class="type">int total=class="num">0; for(class="type">int i=class="num">0;i<HistoryDealsTotal();i++) { class="type">class="kw">ulong ticket=HistoryDealGetTicket(i); if(HistoryDealGetInteger(ticket,DEAL_MAGIC)==m_magic) total++; } class="kw">return(total); } class="type">bool C_Orders::SelectPositionByIndex(class="type">int idx) { for(class="type">int i=class="num">0;i<PositionsTotal();i++) { class="type">class="kw">ulong ticket=PositionGetTicket(i); if(PositionGetInteger(POSITION_MAGIC)==m_magic) { if(idx==class="num">0) class="kw">return(true); idx--; } } class="kw">return(class="kw">false); }
「净额与对冲账户的下单差异」
上一节演示了如何用 C_Orders 类发送市价指令,但那段代码在真实服务器上的可用性要看账户类型。净额结算账户里它已经能开仓和平仓;对冲账户则不够,直接用卖按钮去平买仓,实际是开了一个反向卖仓,而不是平仓。 单边账户里点卖出平买仓,结果可能是仓位反转:当卖出量大于已有买入量,持仓从多翻空,新空仓大小为二者差值。若卖出量小于买入量,则只做部分平仓,剩余买单留在市场。这一行为差异是写 EA 前必须弄清的硬边界。 外汇和贵金属杠杆交易属高风险,账户类型误判可能导致预期外的双向持仓。建议开 MT5 后切到演示对冲账户,手动用面板卖单去平多单,亲眼确认是否开出反向仓,再回头改代码逻辑。 本文目标不是直接做实时通信的 EA,而是先实现能连真服务器的组件,再抽出来做模拟服务器,避免把真服务器所有接口都仿真一遍。
平仓消息为什么不会真的平掉所有仓位
在 Chart Trade 指标上点「平掉所有仓位」,类本身并不知道该平哪个仓。按钮文字有误导性:上面那段 C_Orders 代码如果不改,按下去也不会无差别清仓。真正的逻辑是,指标把消息发到 EA,EA 在第 153 行拦截、第 120 行开始处理,第 122 行先循环扫一遍所有未平仓位——包括和你当前图表资产完全无关的仓。 之所以全扫一遍,是因为 EA 端没有存服务器给的单号:改时间周期或切换合约类型时,图表一变单号就丢,只能每次现查。第 124 行是第一道过滤器,拿仓位品种名和消息里的名字比对。用历史合约做跨期订单时这行绝不能删——否则可能平掉完全不同资产的仓位。比如你持有完整合约美元仓,消息说是迷你合约,名字不匹配就直接跳过。 第二道过滤看幻数。对冲账户能同资产多仓同开,EA 只认自己类的幻数;手动开的仓幻数不对就忽略,保持原样等你手平。两关都过才进第 126 行抓单号、调第 12 行 ClosePosition。该方法在 protected 里,外部代码调不到。 第 12 行里第 17 行又用 PositionSelectByTicket 查一遍单号是否有效,目前算冗余但暂时留着。第 21 行填 TRADE_ACTION_DEAL 而不是 TRADE_ACTION_CLOSE_BY——关键在单边账户:CLOSE_BY 会逼你另写部分平仓逻辑,DEAL 靠第 26 行交易量字段就能控平多平少,两种账户都兼顾。外汇贵金属保证金高波动,这套过滤若漏改参数,误平概率不低,上 MT5 用策略测试器跑历史品种切换就能验证。 EA 侧改动很小:第 9 行引头文件,第 15 行指针声明类,第 21 行 NEW 时传 0xC0DEDAFE78514269 这种十六进制当幻数构造参数,十进制是多少自己转一下看。第 30 行把指标消息喂给类,模块化好处是改一个文件不波及别的,比单体大文件稳。
<span class="number">class="num">001</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">002</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Daniel Jose"</span> <span class="number">class="num">003</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">004</span>. <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"..\Defines.mqh"</span> <span class="number">class="num">005</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">006</span>. <span class="keyword">class</span> C_Orders <span class="number">class="num">007</span>. {
◍ 平仓逻辑与底层结构封装
这段类实现把持仓平仓动作收口到一个 ClosePosition 方法里,外部只需传 ticket 即可反向平掉对应多空仓。 代码先通过 PositionSelectByTicket 校验票据有效性,再用 PositionGetInteger 读 POSITION_TYPE 判断方向,买仓用 ORDER_TYPE_SELL 平、卖仓用 ORDER_TYPE_BUY 平,方向与合约名都从持仓属性实时抓取。 价格取 SymbolInfoDouble 的 BID/ASK,并用 NormalizeDouble 按 SYMBOL_DIGITS 精度截断,deviation 写死 1000 点容忍滑点——这在黄金跳空时可能触发异常成交,外汇与贵金属高危品种建议按品种波动重设。 底层 stBase 结构挂了 MqlTradeRequest 与 MagicNumber,SendToPhysicalServer 返回非 0 才认平仓成功;私有封装让调用方不必关心请求体清零与字段赋值细节。
class="kw">protected: class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="kw">const class="type">class="kw">ulong GetMagicNumber(class="type">void) class="kw">const { class="kw">return m_Base.MagicNumber; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool ClosePosition(class="kw">const class="type">class="kw">ulong ticket) { class="type">bool IsBuy; class="type">class="kw">string szContract; if (!PositionSelectByTicket(ticket)) class="kw">return class="kw">false; IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY; szContract = PositionGetString(POSITION_SYMBOL); ZeroMemory(m_Base.TradeRequest); m_Base.TradeRequest.action = TRADE_ACTION_DEAL; m_Base.TradeRequest.type = (IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY); m_Base.TradeRequest.price = NormalizeDouble(SymbolInfoDouble(szContract, (IsBuy ? SYMBOL_BID : SYMBOL_ASK)), (class="type">int)SymbolInfoInteger(szContract, SYMBOL_DIGITS)); m_Base.TradeRequest.position = ticket; m_Base.TradeRequest.symbol = szContract; m_Base.TradeRequest.volume = PositionGetDouble(POSITION_VOLUME); m_Base.TradeRequest.deviation = class="num">1000; class="kw">return SendToPhysicalServer() != class="num">0; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">private : class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct stBase { class="type">MqlTradeRequest TradeRequest; class="type">class="kw">ulong MagicNumber; class="type">bool bTrash; }m_Base; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct stChartTrade { class="kw">struct stEvent