自定义指标:为净额结算账户绘制部分入场、出场和反转交易·进阶篇
📊

自定义指标:为净额结算账户绘制部分入场、出场和反转交易·进阶篇

(2/3)· 净额账户下多头寸合并与反转逻辑,靠手工盯盘极易漏看均价漂移

实战向进阶 第 2/3 篇
在净额账户里做分批加减仓,图表上的加权平均价会随每笔成交悄悄移动。多数交易者只盯报价线,等发现仓位方向已反转时,浮盈结构早就变了样。

◍ 从一张实盘图看指标怎么画线

下面这张实盘截图是我们正在做的自定义指标的真实用法示例:图上是一笔 3 手的买仓,源头是一笔更大的交易,中间经历过多次部分入场、部分出场,甚至方向反转。 因为存在分批操作,你会看到图表上的平均持仓价和实时报价之间有肉眼可见的偏离,同时手数也和初始建仓量对不上——这不是 bug,是部分平仓后的自然结果。 盯着交易事件流就能搞清楚:算法在什么条件下删掉旧线、又在部分出场发生时如何把屏幕上的手数刷新成当前值。开 MT5 把这批事件日志调出来对照图表,比看说明文档直观得多。外汇与贵金属高杠杆,这类平均价跟踪仅作辅助,实际仓位风险仍需自行把控。

「加载页参数从哪几行定」

写自定义指标时,文件头部的 #property 指令直接决定 MT5 指标加载弹窗里呈现的内容与初始样式。改了这些行,不用动核心逻辑,用户在加载界面看到的缓冲区数量、绘图类型和线条颜色就会变。 下面这段声明把指标挂到主图窗口,缓冲区与绘图数量先置 0,随后又给第 1 个 plot 补了标签和线型。注意 indicator_buffers 0 与 indicator_plots 0 并存时,后面再用 indicator_label1 等定义第一个绘图,是 MT5 编译器允许的写法,但容易让初学者误以为真的没有绘图。 [CODE] #property indicator_chart_window // Indicator displayed in the main window #property indicator_buffers 0 // Zero buffers #property indicator_plots 0 // No plotting //--- plot Label1 #property indicator_label1 "Line properties" #property indicator_type1 DRAW_LINE // Line type for the first plot #property indicator_color1 clrRoyalBlue // Line color for the first plot #property indicator_style1 STYLE_SOLID // Line style for the first plot #property indicator_width1 1 // Line width for the first plot [/CODE] 逐行拆一下:indicator_chart_window 指定指标画在主图而非独立子窗;indicator_buffers 0 表示暂不分配数据缓冲区;indicator_plots 0 先声明无绘图序列;从 indicator_label1 开始补第 1 个绘图的名号“Line properties”;type 用 DRAW_LINE 画连续线;color 取 clrRoyalBlue Royal蓝;style 为 STYLE_SOLID 实线;width 1 即 1 像素细线。 开 MT5 随便建个指标把这几行贴进去编译,加载时就能看到主图弹出 RoyalBlue 细线选项。外汇与贵金属波动剧烈,指标仅作辅助,实际信号失效概率不低,请自行回测验证。

MQL5 / C++
class="macro">#class="kw">property indicator_chart_window                    class=class="str">"cmt">// Indicator displayed in the main window
class="macro">#class="kw">property indicator_buffers class="num">0                       class=class="str">"cmt">// Zero buffers
class="macro">#class="kw">property indicator_plots  class="num">0                        class=class="str">"cmt">// No plotting
class=class="str">"cmt">//--- plot Label1
class="macro">#class="kw">property indicator_label1  "Line properties"
class="macro">#class="kw">property indicator_type1   DRAW_LINE               class=class="str">"cmt">// Line type for the first plot
class="macro">#class="kw">property indicator_color1  clrRoyalBlue            class=class="str">"cmt">// Line class="type">color for the first plot
class="macro">#class="kw">property indicator_style1  STYLE_SOLID             class=class="str">"cmt">// Line style for the first plot
class="macro">#class="kw">property indicator_width1  class="num">1                       class=class="str">"cmt">// Line width for the first plot 

指标缓冲区的初始化与交易事件触发链

MT5 自定义指标启动时,OnInit() 先给名为 Element 的 double 数组分配空间,这个数组就是实际指标缓冲区。它固定三列:索引 0 存价格、1 存成交量、2 存单号,每一行对应历史里的一笔成交。若账户被识别为对冲模式(ACCOUNT_MARGIN_MODE_RETAIL_HEDGING),初始化直接返回 INIT_FAILED,指标从图表移除;否则调 OnTrade() 继续。 OnTrade() 靠 isNewBar 加 isOldBar 的过滤,保证只在每根 K 线成型后触发一次,而不是每次 tick 乱跑。它被激活的时机只有三种:初始化时、新 K 线出现时、以及发生交易事件时。触发后把事件读出来写进 Element,再以线条和文本对象画到屏幕上。 函数开头用静态 datetime 变量 date 记起始时间;若一开始没持仓,date 取当前 K 线开盘时间。有持仓时 PositionsTotal() 大于 0,循环里筛出与当前图表品种相同的持仓,用 HistorySelectByPosition 拉出对应订单,并把 date 更新成这些订单里最早的时间——也就是仓位建仓时间。 当出现不同 ID 的第二笔持仓,先调 ClearRectangles() 清掉旧图形,再把 Element 大小置 0 清空数据;无持仓时同样清图形并重置数组,date 改为服务器当前时间,最后扔给 ListOrdersPositions()。 ListOrdersPositions() 拿到 dateStart 后分两种走向:HistorySelect 区间内没历史就跳到末尾直接 PlotRectangles() 刷新画面;有历史则 HistoryDealsTotal() 非零,逐笔按 DEAL_ENTRY_IN / OUT / INOUT 分类。入场调 AddValue(),出场调 RemoveValue(),反转且单号不在数组里则 AddVolume() 并补价格和成交量差。最后 Sort() 按价格升序排、删成交量为 0 的行,并清掉价格与成交量双零的脏数据,相当于在缓冲区里重建了历史仓位。 外汇与贵金属杠杆高,这类持仓重建逻辑仅用于辅助看盘,不预示方向,实盘前请在策略测试器用历史数据核对 Element 行列数是否符合预期。

MQL5 / C++
class="type">int OnInit()
  {
   ArrayResize(Element,class="num">0,class="num">0);
   class="type">int res=INIT_SUCCEEDED;
   if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
      res=INIT_FAILED;
   else
      OnTrade();
   class="kw">return(res);
  }
class="type">void
OnTrade()
  {
class=class="str">"cmt">//---
   class="kw">static class="type">class="kw">datetime date=class="num">0;
   if(date==class="num">0)
      date=lastTime;
   class="type">long positionId=-class="num">1,numberOfPositions=class="num">0;
   for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--)
      if(m_position.SelectByIndex(i))
         if(m_position.Symbol()==ativo000)
           {
            numberOfPositions++;
            positionId=m_position.Identifier();
            oldPositionId=positionId;
           }
   if(numberOfPositions!=class="num">0)
    {
      class=class="str">"cmt">//Print("PositionId: "+positionId);
      HistorySelectByPosition(positionId);
      date=TimeCurrent();
      for(class="type">int j=class="num">0; j<HistoryDealsTotal(); j++)
        {
         class="type">ulong ticket = HistoryDealGetTicket(j);
         if(ticket > class="num">0)
            if(HistoryDealGetInteger(ticket,DEAL_TIME)<date)
               date=(class="type">class="kw">datetime)HistoryDealGetInteger(ticket,DEAL_TIME);
        }
      if(HistoryDealsTotal()==class="num">1 && (ArraySize(Element)/class="num">3)>class="num">1)
         if(ClearRectangles())
            ArrayResize(Element,class="num">0,class="num">0);
    }
   else
    {
      class="type">bool isClean=ClearRectangles();

◍ 从成交历史反推当前持仓结构

清仓后把 Element 数组缩容到 0,若 isClean 为真则把 date 刷成 TimeCurrent(),意思是新仓出来前不再动这个数组。ArrayPrint 在零长度数组上不会真打印,这段代码只是占位,避免误用。 ListOrdersPositions 接一个 datetime 入参,内部用 inicio 到 TimeCurrent() 圈定回看区间;若入参为 0 直接 return,不做任何历史扫描。HistorySelect 拉完区间后,按 HistoryDealsTotal 遍历每笔成交。 只处理 _Symbol 匹配的成交:开仓(DEAL_ENTRY_IN)调 AddValue 写进数组,平仓(DEAL_ENTRY_OUT)调 RemoveValue 扣减,同笔单子的 INOUT 则先按 ticket 在 Element 里扫一遍。扫不到再走循环补逻辑,这里每次循环都要用 ArraySize(Element)/3 重算行数,因为 Add/Remove 会让数组实际长度漂移。 外汇与贵金属杠杆高,历史持仓还原仅作仓位透视参考,不预示后续方向,实盘前请在 MT5 策略测试器用真实品种跑一遍确认数组行为。

MQL5 / C++
ArrayResize(Element,class="num">0,class="num">0);
if(isClean)
   date=TimeCurrent();      class=class="str">"cmt">// Do not use the array until there is new open position
ArrayPrint(Element);         class=class="str">"cmt">// If there are no errors, this function will not be called here: the array with zero size
   }
 ListOrdersPositions(date);
}
class="type">void ListOrdersPositions(class="type">class="kw">datetime dateInicio)
 {
class=class="str">"cmt">//Analyze the history
  class="type">class="kw">datetime inicio=dateInicio,fim=TimeCurrent();
  if(inicio==class="num">0)
     class="kw">return;
  HistorySelect(inicio, fim);
  class="type">class="kw">double deal_price=class="num">0, volume=class="num">0,newVolume;
  class="type">bool encontrouTicket;
  class="type">uint tamanhoElement=class="num">0;
  for(class="type">int j=class="num">0; j<HistoryDealsTotal(); j++)
   {
     class="type">ulong ticket = HistoryDealGetTicket(j);
     if(ticket <= class="num">0)
        class="kw">return;
     if(HistoryDealGetString(ticket, DEAL_SYMBOL)==_Symbol)
       {
        encontrouTicket=false;
        newVolume=class="num">0;                class=class="str">"cmt">// Need to reset each &class="macro">#x27;for&class="macro">#x27; loop
        volume=HistoryDealGetDouble(ticket,DEAL_VOLUME);
        deal_price=HistoryDealGetDouble(ticket,DEAL_PRICE);
        class="type">class="kw">double auxArray[class="num">1][class="num">3] = {deal_price,volume,(class="type">class="kw">double)ticket};
        if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_IN)
           AddValue(deal_price,volume,(class="type">class="kw">double)ticket);
        if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT)
           RemoveValue(deal_price,volume,(class="type">class="kw">double)ticket);
        if(HistoryDealGetInteger(ticket,DEAL_ENTRY)==DEAL_ENTRY_INOUT)
         {
          tamanhoElement = ArraySize(Element)/class="num">3; class=class="str">"cmt">//Always check the array size, it can vary with the Add/RemoveValue() functions
          for(class="type">uint i=class="num">0; i<tamanhoElement; i++)
            if(Element[i][class="num">2]==ticket)
             {
              encontrouTicket=true;
              break;
             }
          if(!encontrouTicket) class=class="str">"cmt">// If after the previous scanning we don&class="macro">#x27;t find mentioning of the ticket in the array
           {
            for(class="type">uint i=class="num">0; i<tamanhoElement; i++)
             {
让小布替你算加权均价
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到净额账户的实时持仓均价与反转标记,把重复劳动交给小布,你专注决策。

常见问题

净额账户同品种只允许一个头寸,同向加仓合并、反向交易按量减仓或反转;对冲账户可并存多个独立头寸,互不合并。
不能。自定义指标禁止调用下单类函数,只能读取交易历史与仓位属性,借助 OnCalculate 与 OnTrade 感知事件并绘图。
部分出场只减少头寸规模,剩余量的加权均价维持原成交权重不变,除非后续反向单量超过剩余量触发反转。
可以,小布盯盘的品种页已集成净额持仓诊断,自动标注部分入场、出场及反转节点,省去自己写指标。
实盘中标的价格不动时仓位也可能变化,OnTrade 能让指标在事件触发时即时刷新对象,比等下一根 K 线更跟手。