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

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

第 2/3 篇

◍ 把持仓参数塞进内联读取器

这段类实现把账户级风控与单笔挂单界限拆成了两个私有结构:m_BaseFinance 存杠杆、日内开关、止盈止损点数,m_Limits 存具体双向价格与买卖方向。四个 inline getter 直接返回 m_BaseFinance 的成员,调用成本趋近于零,适合在 EA 的 OnTick 里高频取杠杆与止损参数。 外汇与贵金属杠杆可调到 1:500 以上,高杠杆下 m_BaseFinance.Leverange 若未做上限校验,可能让单笔风险暴露超出预期,属典型高风险操作。

MountName 用 StringFormat 拼出形如「前缀类型ticket」的对象名,分隔符走 def_SeparatorInfo(通常为 '' 字符)。CreateIndicatorTrade 据此名创建水平线:IT_PRICE 给蓝、IT_STOP 给砖红、其余给墨绿,颜色映射写死在三目里,改类别需同步改这一行。

开 MT5 把这段贴进自定义类,用 ObjectCreate 建一条 OBJ_HLINE 验证颜色分支;若你的指示器枚举不止三种,优先把颜色判断抽成 switch 而不是嵌套三目。

MQL5 / C++
class="kw">inline class="type">int GetBaseFinanceLeveRange(class="type">void) const { class="kw">return m_BaseFinance.Leverange; }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">int GetBaseFinanceIsDayTrade(class="type">void) const { class="kw">return m_BaseFinance.IsDayTrade; }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">int GetBaseFinanceTakeProfit(class="type">void) const { class="kw">return m_BaseFinance.FinanceTake; }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">int GetBaseFinanceStopLoss(class="type">void) const { class="kw">return m_BaseFinance.FinanceStop; }
class=class="str">"cmt">//+------------------------------------------------------------------+
			class="kw">private :
					class="type">class="kw">string  m_SelectObj;
					class="kw">struct st00
					{
								class="type">class="kw">double  TakeProfit,
											StopLoss;
								class="type">bool    IsBuy;
						}m_Limits;
					class="kw">struct st01
					{
								class="type">int     FinanceTake,
											FinanceStop,
											Leverange;
								class="type">bool    IsDayTrade;
						}m_BaseFinance;
class=class="str">"cmt">//+------------------------------------------------------------------+
					class="type">class="kw">string MountName(class="type">ulong ticket, eIndicatorTrade it)
					{
								class="kw">return StringFormat("%s%c%c%c%d", def_NameObjectsTrade, def_SeparatorInfo, (class="type">char)it, def_SeparatorInfo, ticket);
					}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">inline class="type">class="kw">string CreateIndicatorTrade(class="type">ulong ticket, eIndicatorTrade it, class="type">bool select)
{
				class="type">class="kw">string sz0 = MountName(ticket, it);
				
				ObjectCreate(Terminal.Get_ID(), sz0, OBJ_HLINE, class="num">0, class="num">0, class="num">0);
				ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_COLOR, (it == IT_PRICE ? clrBlue : (it == IT_STOP ? clrFireBrick : clrForestGreen)));

「持仓线绘制时的对象属性与空价拦截」

在 MT5 里用对象画持仓参考线,属性设置直接决定它会不会干扰你看图。上面那段把线宽锁死为 1、线型用 STYLE_DASHDOT 点划线,OBJPROP_BACK 设 true 把它塞到 K 线后面,避免遮挡实体;同时 OBJPROP_SELECTABLE 由外部 select 参数控制,TP 线传了 true 可被鼠标点选,开仓价线传 false 不可选。 tooltip 拼的是 ticket 号加指标类型枚举截取的子串(从 EnumToString 结果第 3 字符起取 10 位),悬停就能看到是哪单哪类线,调试时不用翻日志。 价格小于等于 0 的分支是硬拦截:直接调 RemoveIndicatorTrade 清掉旧对象并返回 NULL,防止把 0 或负价写进图表对象导致报错。外汇和贵金属杠杆高,这类防御写法能少踩坑,但任何画线都只是辅助,信号失效概率始终存在。 路由类里 UpdatePosition 遍历持仓时,对当前符号的每单都建开仓价线与 TP 线,TP 线显式可选取——你改完这段把 EA 挂上 EURUSD 回测,应能看见点划线标出每单 open 与 take 位置。

MQL5 / C++
ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_WIDTH, class="num">1);
ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_STYLE, STYLE_DASHDOT);
ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTABLE, select);
ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_SELECTED, false);
ObjectSetInteger(Terminal.Get_ID(), sz0, OBJPROP_BACK, true);
ObjectSetString(Terminal.Get_ID(), sz0, OBJPROP_TOOLTIP, (class="type">class="kw">string)ticket + " "+StringSubstr(EnumToString(it), class="num">3, class="num">10));

class="kw">return sz0;
}
class="kw">inline class="type">class="kw">string CreateIndicatorTrade(class="type">ulong ticket, class="type">class="kw">double price, eIndicatorTrade it, class="type">bool select)
{
   if (price <= class="num">0)
   {
      RemoveIndicatorTrade(ticket, it);
      class="kw">return NULL;
   }
   class="type">class="kw">string sz0 = CreateIndicatorTrade(ticket, it, select);
   ObjectMove(Terminal.Get_ID(), sz0, class="num">0, class="num">0, price);

   class="kw">return sz0;
}
class C_Router : class="kw">public C_ObjectsTrade
{
class=class="str">"cmt">// ... Internal class code ....
         class="type">void UpdatePosition(class="type">int iAdjust = -class="num">1)
         {
class=class="str">"cmt">// ... Internal function code ...
            for(class="type">int i0 = p; i0 >= class="num">0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
            {
               ul = PositionGetInteger(POSITION_TICKET);
               m_bContainsPosition = true;
               CreateIndicatorTrade(ul, PositionGetDouble(POSITION_PRICE_OPEN), IT_PRICE, false);
               CreateIndicatorTrade(ul, take = PositionGetDouble(POSITION_TP), IT_TAKE, true);

持仓与指标线的绑定写法

在 MT5 的 EA 架构里,把当前持仓和图表上的指标线关联起来,常见做法是调用 CreateIndicatorTrade 并传入持仓 ticket 与对应的价格字段。下面这段截取自一个订单路由类的实现,能直接看到三种绑定场景。 开仓时若已有止损,会用 PositionGetDouble(POSITION_SL) 取回止损价并绑定为 IT_STOP 类型:CreateIndicatorTrade(ul, stop = PositionGetDouble(POSITION_SL), IT_STOP, true); 最后一个 true 表示强制刷新显示。 在 C_OrderView 的 InitBaseFinance 中,对默认指标票 def_IndicatorTicket0 分别建立了价格、止盈、止损三条线,全部用 false 标记为非强制:CreateIndicatorTrade(def_IndicatorTicket0, IT_PRICE, false) 等。这样初始化后图表上就有基础财务参考线。 UpdatePosition 里遍历持仓,若符号匹配本终端品种,就取 ticket 并绑定开盘价线:CreateIndicatorTrade(ul, PositionGetDouble(POSITION_PRICE_OPEN), IT_PRICE, false)。外汇与贵金属波动大、杠杆高,这类绑定仅作可视化辅助,不改变订单本身风险。 复制下面代码到 MT5 策略测试器里的自定义类,能验证指标线是否随持仓更新;若 def_IndicatorTicket0 未预先创建,运行会返回无效句柄。

MQL5 / C++
CreateIndicatorTrade(ul, stop = PositionGetDouble(POSITION_SL), IT_STOP, true);

class C_OrderView : class="kw">public C_Router
{
   class="kw">private:
   class="kw">public:
      class="type">void InitBaseFinance(class="type">int nContracts, class="type">int FinanceTake, class="type">int FinanceStop, class="type">bool b1)
         {
            SetBaseFinance(nContracts, FinanceTake, FinanceStop, b1);
            CreateIndicatorTrade(def_IndicatorTicket0, IT_PRICE, false);
            CreateIndicatorTrade(def_IndicatorTicket0, IT_TAKE, false);
            CreateIndicatorTrade(def_IndicatorTicket0, IT_STOP, false);
         }
};

class C_Router : class="kw">public C_ObjectsTrade
{
   class="type">void UpdatePosition(class="type">int iAdjust = -class="num">1)
   {
      for(class="type">int i0 = p; i0 >= class="num">0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol())
      {
         ul = PositionGetInteger(POSITION_TICKET);
         m_bContainsPosition = true;
         CreateIndicatorTrade(ul, PositionGetDouble(POSITION_PRICE_OPEN), IT_PRICE, false);
      }
   }
};

class="type">bool GetInfosOrder(const class="type">class="kw">string &sparam, class="type">ulong &ticket, class="type">class="kw">double &price, eIndicatorTrade &it)
{
   class="type">class="kw">string szRet[];
   class="type">char szInfo[];

◍ 对象清理时别误删基准挂单

上面这段函数负责把图表上的交易指示对象批量移除,但留了一个口子:当 bFull 为 false 时,会跳过 ticket 等于 def_IndicatorTicket0 的那一个。这个 0 号 ticket 通常是策略用来锚定基准水平的对象,误删会导致后续价格行为判读失去参照。 循环从 ObjectsTotal 拿总数倒序遍历,用 StringSubstr 比对前缀 def_NameObjectsTrade 来识别自绘对象。注意先关 CHART_EVENT_OBJECT_DELETE 再删,否则每删一个都会触发一次事件回调,在外汇和贵金属这种高频波动品种上可能拖慢 MT5 主线程。 实际验证时,可在 RemoveAllsIndicatorTrade(false) 调用前手动画一个 0 号 ticket 对象,跑完函数后用 ObjectFind 查它是否还在。高风险提示:自动化对象管理若逻辑写错,可能在直播盘面瞬间清掉你的预警线。

MQL5 / C++
if (StringSplit(sparam, def_SeparatorInfo, szRet) < class="num">2) class="kw">return false;
if (szRet[class="num">0] != def_NameObjectsTrade) class="kw">return false;
StringToCharArray(szRet[class="num">1], szInfo);
it = (eIndicatorTrade)szInfo[class="num">0];
ticket = (class="type">ulong) StringToInteger(szRet[class="num">2]);
price = ObjectGetDouble(Terminal.Get_ID(), sparam, OBJPROP_PRICE);

class="kw">return true;
}
class="kw">inline class="type">void RemoveAllsIndicatorTrade(class="type">bool bFull)
{
   class="type">class="kw">string sz0, szRet[];
   class="type">int i0 = StringLen(def_NameObjectsTrade);

   ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
   for (class="type">int c0 = ObjectsTotal(Terminal.Get_ID(), -class="num">1, -class="num">1); c0 >= class="num">0; c0--)
   {
      sz0 = ObjectName(Terminal.Get_ID(), c0, -class="num">1, -class="num">1);
      if (StringSubstr(sz0, class="num">0, i0) == def_NameObjectsTrade)
      {
         if (!bFull)
         {
            StringSplit(sz0, def_SeparatorInfo, szRet);
            if (StringToInteger(szRet[class="num">2]) == def_IndicatorTicket0) class="kw">continue;
         }
      }else class="kw">continue;
         ObjectDelete(Terminal.Get_ID(), sz0);
   }
   ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
}
class="kw">inline class="type">void RemoveIndicatorTrade(class="type">ulong ticket, eIndicatorTrade it = IT_NULL)
{

常见问题

多在空价或持仓未完全初始化时触发,读取器应先判断持仓是否存在再取 volume,避免对空对象取属性。
在绘制前加空价拦截:若开仓价小于等于 0 直接跳过该次绘制,不创建对象也不写属性。
可以,小布能对照你的绑定写法指出持仓对象与指标线句柄是否漏连,并提示清理时误删基准挂单的风险。
可能绑定写在了当前图表局部变量里,应把绑定关系存到对象属性或全局映射,周期切换时重新挂载。
清理时用了前缀模糊匹配,把挂单对象名也算进去了;应只删持仓线前缀,保留基准挂单对象不遍历。