从头开始开发智能交易系统(第 18 部分):新订单系统 (I)·进阶篇
🧩

从头开始开发智能交易系统(第 18 部分):新订单系统 (I)·进阶篇

(2/3)· 图表订单系统跑久就成弗兰肯斯坦?本篇拆解净持结算账户下的全新订单架构

含代码示例偏理论 第 2/3 篇
很多交易者以为图表上的挂单成交只是简单叠加,殊不知净持账户里新单填单会触发持仓重组,旧代码若硬补逻辑只会越改越乱。先想清楚订单进入系统那一刻发生了什么,比急着写函数更重要。

「挂单类型的实时判定与路由封装」

在 MT5 的 EA 里下 pending order,最容易被忽略的是限价与止损挂单的自动切换:不是你写死 BUY_LIMIT 就永远是限价,而是要看当前 last 与挂单价的大小关系。上面这段 CreateOrderPendent 用三目运算做了实时归类——买向时 last>=Price 归为 BUY_LIMIT,否则 BUY_STOP;卖向对称处理。 注意 m_bContainsPosition 这个开关:当账户已持有同标的仓位时,代码把 sl/tp 强制置 0(见高亮行),意味着加挂的 pending 单不再带独立止损止盈,避免对冲逻辑混乱。这个细节在外汇与贵金属这种高波动品种上尤其要手测,否则可能平白吃掉风控。 TradeRequest.deviation 设成了 1000 点容忍,type_filling 用 ORDER_FILLING_RETURN,属于对报价滑点极宽松的设定。实盘前建议在策略测试器里把偏差调到 broker 实际允许值,否则在 XAUUSD 这种点差跳变频繁的品种上,Send() 返回失败的概率会偏高。 下面把关键几行拆开看,你可以直接复制到自己的 C_Router 类里改参数验证。

MQL5 / C++
SetLineOrder(ul, OrderGetDouble(ORDER_PRICE_OPEN), HL_PRICE, true);
SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true);
SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true);
};
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Router() : m_bContainsPosition(false) {}
class="type">class="kw">ulong CreateOrderPendent(const class="type">bool IsBuy, const class="type">class="kw">double Volume, const class="type">class="kw">double Price, const class="type">class="kw">double Take, const class="type">class="kw">double Stop, const class="type">bool DayTrade = true)
{
   class="type">class="kw">double last = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_LAST);
   ZeroMemory(TradeRequest);
   ZeroMemory(TradeResult);
   TradeRequest.action       = TRADE_ACTION_PENDING;
   TradeRequest.symbol       = Terminal.GetSymbol();
   TradeRequest.volume       = Volume;
   TradeRequest.type         = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
   TradeRequest.price        = NormalizeDouble(Price, Terminal.GetDigits());
   TradeRequest.sl           = NormalizeDouble((m_bContainsPosition ? class="num">0 : Stop), Terminal.GetDigits());
   TradeRequest.tp           = NormalizeDouble((m_bContainsPosition ? class="num">0 : Take), Terminal.GetDigits());
   TradeRequest.type_time    = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
   TradeRequest.stoplimit    = class="num">0;
   TradeRequest.expiration   = class="num">0;
   TradeRequest.type_filling = ORDER_FILLING_RETURN;
   TradeRequest.deviation    = class="num">1000;
   TradeRequest.comment      = "Order Generated by Experts Advisor.";
   if (!Send()) class="kw">return class="num">0;
   class="kw">return TradeResult.order;
};
class="type">void UpdatePosition(class="type">void)
{
   class="kw">static class="type">int memPositions = class="num">0, memOrder = class="num">0;

持仓与挂单变动时的图形线重绘逻辑

在 EA 的 OnChartEvent 或定时器回调里,先抓当前持仓数 PositionsTotal() 与挂单数 OrdersTotal(),一旦和上次记录的 memPositions / memOrder 不一致,就说明账户状态有变,需要清掉旧线条再重画。 清线前用 ChartSetInteger(图表ID, CHART_EVENT_OBJECT_DELETE, false) 临时屏蔽对象删除事件,调 RemoveAllsLines() 清空,再打开事件开关,避免误触发删除回调导致图表对象状态混乱。 接着倒序遍历持仓:用 PositionGetSymbol(i0) 匹配当前品种,取出 ticket 和开仓价、TP、SL,分别通过 SetLineOrder 画三条线(HL_PRICE 实线、HL_TAKE / HL_STOP 虚线),同时把 m_bContainsPosition 置 true。 挂单遍历里有个细节:若已存在同品种持仓(m_bContainsPosition 为真),对 pending 单调 ModifyOrderPendent 重设开仓价、清零 TP/SL,再 OrderSelect 保一下上下文;随后照样画挂单的价/TP/SL 三条线。外汇与贵金属杠杆高,重绘逻辑若漏了事件开关,可能在快速平仓时残留线条造成误读。

MQL5 / C++
class="type">class="kw">ulong ul;
class="type">int p, o;

p = PositionsTotal();
o = OrdersTotal();
if ((memPositions != p) || (memOrder != o))
{
   ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
   RemoveAllsLines();
   ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
   memOrder = o;
   memPositions = p;
   m_bContainsPosition = false;
};
for(class="type">int i0 = p; i0 >= class="num">0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
{
   ul = PositionGetInteger(POSITION_TICKET);
   m_bContainsPosition = true;
   SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false);
   SetLineOrder(ul, PositionGetDouble(POSITION_TP), HL_TAKE, true);
   SetLineOrder(ul, PositionGetDouble(POSITION_SL), HL_STOP, true);
}
for (class="type">int i0 = o; i0 >= class="num">0; i0--) if ((ul = OrderGetTicket(i0)) > class="num">0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol())
{
   if (m_bContainsPosition)
   {
      ModifyOrderPendent(ul, OrderGetDouble(ORDER_PRICE_OPEN), class="num">0, class="num">0);
      (OrderSelect(ul) ? class="num">0 : class="num">0);
   }
   SetLineOrder(ul, OrderGetDouble(ORDER_PRICE_OPEN), HL_PRICE, true);
   SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true);
   SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true);
}

◍ 市价单执行里的持仓判定与填充策略

这段函数把市价单的请求结构一次性填好,核心在 m_bContainsPosition 这个标记:若账户该品种已有持仓,SL/TP 直接置 0,避免加仓时重复挂止损止盈;若无持仓才写入外部传入的 Stop 与 Take。 deviation 设成 1000 点(以最小报价单位计)配合 ORDER_FILLING_RETURN,意味着滑点容忍极高且拒单即退,适合流动性骤变的外汇或贵金属时段,但极端行情仍可能部分成交。 type_time 由 DayTrade 参数切换:true 用 ORDER_TIME_DAY 当天有效,false 用 ORDER_TIME_GTC 挂到手动撤销。复制下面代码到 MT5 的 EA 里,把 Terminal.GetSymbol() 换成你的品种,就能直接测不同偏差下的成交回执。

MQL5 / C++
class="type">class="kw">ulong ExecuteOrderInMarket(const class="type">bool IsBuy, const class="type">class="kw">double Volume, const class="type">class="kw">double Price, const class="type">class="kw">double Take, const class="type">class="kw">double Stop, const class="type">bool DayTrade = true)
{
      ZeroMemory(TradeRequest);
      ZeroMemory(TradeResult);
      TradeRequest.action           = TRADE_ACTION_DEAL;
      TradeRequest.symbol           = Terminal.GetSymbol();
      TradeRequest.volume           = Volume;
      TradeRequest.type             = (IsBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
      TradeRequest.price            = NormalizeDouble(Price, Terminal.GetDigits());
      TradeRequest.sl               = NormalizeDouble((m_bContainsPosition ? class="num">0 : Stop), Terminal.GetDigits());
      TradeRequest.tp               = NormalizeDouble((m_bContainsPosition ? class="num">0 : Take), Terminal.GetDigits());
      TradeRequest.type_time        = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
      TradeRequest.stoplimit        = class="num">0;
      TradeRequest.expiration       = class="num">0;
      TradeRequest.type_filling     = ORDER_FILLING_RETURN;
      TradeRequest.deviation        = class="num">1000;
      TradeRequest.comment          = "[ Order Market ] Generated by Experts Advisor.";
      if (!Send()) class="kw">return class="num">0;
      class="kw">return TradeResult.order;
};

「限价区里挂单该怎么留」

EA 处理挂单时,先得划出持仓对应的限价禁区。做多时止损构成下限,做空时止损构成上限,价格进入这个区间的挂单,EA 必须按用户设定删掉限价值;若挂单留在区外,则尊重用户配的普通单或 OCO 单,EA 不插手。 订单系统每次状态变动都会触发 C_Router 的更新函数。它先初始化变量,确认有无持仓;一旦有持仓,禁区边界就定下了,随后逐笔扫描挂单位置。做空要求挂单价 > 持仓止损,做多要求挂单价 < 持仓止损,最后再做一次错位测试看是否仍为 OCO 类型。 对冲账户是个例外。高亮检查里若发现账户类型是对冲,即便变量说要用限价,EA 也会直接忽略,完全按用户配置走。这个判断必须在现阶段完成,否则整条订单路由会乱掉。 下面这段是 C_Router 类里限价判定的核心骨架,macro_MAX / macro_MIN 用来快速比对边界,UpdatePosition 在持仓或挂单总数变动时才重算,避免每 tick 空转。外汇和贵金属波动剧烈,这种边界逻辑写错可能瞬间误删挂单,上线前务必在 MT5 策略测试器用真实点差回放一遍。

MQL5 / C++
class C_Router : class="kw">public C_HLineTrade
{
    class="kw">protected:
        class="type">MqlTradeRequest TradeRequest;
        class="type">MqlTradeResult  TradeResult;
    class="kw">private  :
        class="type">bool            m_bContainsPosition;
        class="kw">struct st00
        {
            class="type">class="kw">double  TakeProfit,
                    StopLoss;
            class="type">bool    IsBuy;
        }m_Limits;
class=class="str">"cmt">// ... Rest of the code
class=class="str">"cmt">// Rest of the code....
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define macro_MAX(A, B) (A > B ? A : B)
class="macro">#define macro_MIN(A, B) (A < B ? A : B)
class="type">void UpdatePosition(class="type">void)
{
    class="kw">static class="type">int      memPositions = class="num">0, memOrder = class="num">0;
    class="type">class="kw">ulong           ul;
    class="type">int             p, o;
    class="type">class="kw">double          price;
    class="type">bool            bTest;
    
    p = PositionsTotal();
    o = OrdersTotal();
    if ((memPositions != p) || (memOrder != o))
    {
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        RemoveAllsLines();

挂单与持仓的边界判定逻辑

这段逻辑先开启图表的对象删除事件监听,把当前订单数 o 与持仓数 p 暂存进内存变量,并将 m_Limits 的止损、止盈重置为 -1、方向标记置否,相当于每次刷新前先清掉上轮痕迹。 随后倒序遍历持仓:只要符号匹配当前图表,就取出持仓 ticket,标记 m_bContainsPosition 为真,并把开仓价、TP、SL 分别画成参考线,同时记录该持仓是买还是卖。这里 TP/SL 若返回 -1 代表未设,后面挂单测试会用到这个状态。 挂单循环里,先读 ORDER_PRICE_OPEN 作为挂单价 price。若 m_Limits 的止损和止盈都是 -1,bTest 直接为假,说明没有持仓边界可供比对;否则按「卖单且 SL 大于挂单价」「买单且 SL 小于挂单价」「或挂单价落在 SL 与 TP 之间」三种情况之一置真。 当已存在同符号持仓且 bTest 为真,代码进入下一层处理——这意味着该挂单价格可能侵入了现有持仓的保护区间,触发特殊标注。外汇与贵金属杠杆高,这类边界重叠往往暗示风险敞口叠加,建议在 MT5 里用这段把持仓与挂单同屏画线,肉眼核对侵入情况。

MQL5 / C++
ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
memOrder = o;
memPositions = p;
m_bContainsPosition = false;
m_Limits.StopLoss = -class="num">1;
m_Limits.TakeProfit = -class="num">1;
m_Limits.IsBuy = false;
};
for(class="type">int i0 = p; i0 >= class="num">0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
{
   ul = PositionGetInteger(POSITION_TICKET);
   m_bContainsPosition = true;
   SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false);
   SetLineOrder(ul, m_Limits.TakeProfit = PositionGetDouble(POSITION_TP), HL_TAKE, true);
   SetLineOrder(ul, m_Limits.StopLoss = PositionGetDouble(POSITION_SL), HL_STOP, true);
   m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
}
for (class="type">int i0 = o; i0 >= class="num">0; i0--) if ((ul = OrderGetTicket(i0)) > class="num">0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol())
{
   price = OrderGetDouble(ORDER_PRICE_OPEN);
   if ((m_Limits.StopLoss == -class="num">1) && (m_Limits.TakeProfit == -class="num">1)) bTest = false; else
   {
      bTest = ((!m_Limits.IsBuy) && (m_Limits.StopLoss > price));
      bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price));
      bTest = (bTest ? bTest : ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price)));
   }
   if ((m_bContainsPosition) && (bTest))
   {

◍ 持仓与挂单的线条刷新逻辑

这段收尾代码负责把当前持仓和挂单的开仓价、止损、止盈画到图表上。先清掉 macro_MAX / macro_MIN 两个宏定义,避免后续命名污染,然后初始化 m_Limits 的止损、止盈为 -1,IsBuy 置 false,表示尚未读到有效仓位边界。 读取持仓时,若 POSITION_TP 与 POSITION_SL 都返回 -1,bTest 直接置 false,说明该仓位没挂止损止盈,不进入后续边界测试;否则按多空方向判断止损是否落在市价外侧,或止盈与止损是否跨价(一个大于开仓价、一个小于开仓价)。 UpdatePosition 里用 PositionGetInteger(POSITION_TICKET) 拿到 ticket,逐根刷新 HL_PRICE / HL_TAKE / HL_STOP 三条线。值得注意的是,当账户模式为 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING(零售对冲账户)时,代码会把 m_bContainsPosition 强制复位为 false,意味着对冲账户下不沿用之前的持仓标记,可能需交易者自行在 MT5 里验证该分支是否影响你的 EA 显示。 外汇与贵金属杠杆交易风险较高,这类画线逻辑仅用于辅助观察,不代表任何方向确定性。

MQL5 / C++
m_Limits.StopLoss = -class="num">1;
m_Limits.TakeProfit = -class="num">1;
m_Limits.IsBuy = false;
SetLineOrder(ul, m_Limits.TakeProfit = PositionGetDouble(POSITION_TP), HL_TAKE, true);
SetLineOrder(ul, m_Limits.StopLoss = PositionGetDouble(POSITION_SL), HL_STOP, true);
m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
price = OrderGetDouble(ORDER_PRICE_OPEN);
if ((m_Limits.StopLoss == -class="num">1) && (m_Limits.TakeProfit == -class="num">1)) bTest = false; else
{
      bTest = ((!m_Limits.IsBuy) && (m_Limits.StopLoss > price));
      bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price));
      bTest = (bTest ? bTest : ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price)));
}
class="type">void UpdatePosition(class="type">void)
{
      for(class="type">int i0 = p; i0 >= class="num">0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
      {
            ul = PositionGetInteger(POSITION_TICKET);
            m_bContainsPosition = true;
            SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false);
            SetLineOrder(ul, m_Limits.TakeProfit = PositionGetDouble(POSITION_TP), HL_TAKE, true);
            SetLineOrder(ul, m_Limits.StopLoss = PositionGetDouble(POSITION_SL), HL_STOP, true);
            m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
      }
      if (AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
            m_bContainsPosition = false;
      for (class="type">int i0 = o; i0 >= class="num">0; i0--) if ((ul = OrderGetTicket(i0)) > class="num">0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol())
      {
让小布替你跑这套验证逻辑
净持与对冲账户的差异诊断,小布盯盘已内置,打开对应品种页即可看到当前账户模式及持仓冲突预警,你只需专注策略本身。

常见问题

净持账户每个品种仅允许一笔持仓,新单可能修改既有持仓;对冲账户可并存多笔独立持仓互不影响,本文系统主要针对净持设计。
为兼顾持仓与挂单信息不断打补丁,会让结构臃肿、逻辑耦合,后续改动极易引入漏洞,渐进式重构新系统更稳妥。
可以,小布盯盘的品种页会标识账户保证金模式,并提示净持下持仓唯一性带来的订单系统限制,省去手动查常量。
指挂单价格落在特定限价区间时的成交与改写规则,本篇后续小节会结合代码说明如何安全处理这类边界。
在系统中加入账户模型检查,按 ACCOUNT_MARGIN_MODE_RETAIL_NETTING 或 HEDGING 分支调整,验证模块可保留通用。