旗形形态·综合运用
🚩

旗形形态·综合运用

(3/3)·旗形、三角旗形、楔形与水平三角形的边界混乱,让多数自动识别脚本误报,本文用指标收口

实战向进阶 第 3/3 篇
把三角旗形直接当三角形处理的人不在少数,结果指标在反转点附近疯狂报警却抓不到旗杆后的盘整。旗形和三角形本就分属两类构造逻辑,混用 ZigZag 与柱覆盖判定只会让搜索结果变成噪声。看清类别差异,再谈自动识别。

「双顶点目标价的几何求法」

在形态识别里,若选择用两个顶点推目标位,核心不是量绝对幅度,而是把两组峰谷连成直线、求交叉点。下面这段函数就干了这件事:先取初始线(底部到顶部)坐标,再取同向双底或双顶的延伸线坐标,交给 TwoLinesCrossY 算出交叉 Y 值 t。 根据方向,买入时目标 = t 减去起始谷值,卖出时目标 = 起始峰值减 t。注意 li+4 必须存在,否则数组越界会让 EA 在实时外汇行情里直接崩。贵金属与外汇杠杆高,这种几何目标仅代表潜在测量位,突破失败概率不低。 水平级别那段逻辑也很直接:之字转向在 i 柱更新后,若 dir==1 且最新峰值大于水平 cl,就画向上箭头,并把目标写进 UpDotBuffer。若 Target 模式是 FromVertexToVertex,buffer 直接存 CurLevel.target 价格;否则存 cl+target 的距离值,随后 dir 清零停止跟踪。 [CODE] void SetTwoVerticesTarget(int dir,int li){ // 形态中初始线的坐标 // - 从底部到顶部 double x11=PeackTrough[li].Bar; double y11=PeackTrough[li].Val; double x12=PeackTrough[li+1].Bar; double y12=PeackTrough[li+1].Val; // 通过两个底部画线用于买入的坐标 // 或者通过两个顶部画线用于卖出 double x21=PeackTrough[li+2].Bar; double y21=PeackTrough[li+2].Val; double x22=PeackTrough[li+4].Bar; double y22=PeackTrough[li+4].Val; // 线的交叉点的数值 double t=TwoLinesCrossY(x11,y11,x12,y12,x21,y21,x22,y22); // 根据方向设置目标数值 if(dir==1){ CurLevel.target=t-PeackTrough[li].Val; } else if(dir==-1){ CurLevel.target=PeackTrough[li].Val-t; } } [/CODE] 逐行拆解:函数入口拿方向 dir 与顶点索引 li;x11/y11 是起始柱与价格,x12/y12 是下一转折点,构成第一根测量线。x21 到 x22 取 li+2 与 li+4,即隔一个顶点后的同向两点,作第二根线。TwoLinesCrossY 返回两线延长交叉的纵坐标 t。dir==1 时目标为交叉点相对起涨谷的净高,dir==-1 则为起跌峰相对交叉点的净深。

MQL5 / C++
class="type">void SetTwoVerticesTarget(class="type">int dir,class="type">int li){
  class=class="str">"cmt">// 形态中初始线的坐标
  class=class="str">"cmt">//  - 从底部到顶部
  class="type">class="kw">double x11=PeackTrough[li].Bar;
  class="type">class="kw">double y11=PeackTrough[li].Val;
  class="type">class="kw">double x12=PeackTrough[li+class="num">1].Bar;
  class="type">class="kw">double y12=PeackTrough[li+class="num">1].Val;
  class=class="str">"cmt">// 通过两个底部画线用于买入的坐标
  class=class="str">"cmt">// 或者通过两个顶部画线用于卖出
  class="type">class="kw">double x21=PeackTrough[li+class="num">2].Bar;
  class="type">class="kw">double y21=PeackTrough[li+class="num">2].Val;
  class="type">class="kw">double x22=PeackTrough[li+class="num">4].Bar;
  class="type">class="kw">double y22=PeackTrough[li+class="num">4].Val;
  class=class="str">"cmt">// 线的交叉点的数值
  class="type">class="kw">double t=TwoLinesCrossY(x11,y11,x12,y12,x21,y21,x22,y22);
  class=class="str">"cmt">// 根据方向设置目标数值
  if(dir==class="num">1){
    CurLevel.target=t-PeackTrough[li].Val;
  }
  else if(dir==-class="num">1){
    CurLevel.target=PeackTrough[li].Val-t;
  }
}

◍ 顶点到顶点的目标位画法分支

这段逻辑处理的是 ZigZag 跟踪里,当最新峰谷与当前 Bar 重合时,如何往缓冲写目标点。核心判断在 Target 枚举:若等于 FromVertexToVertex,直接把 CurLevel.target 当作价格写入;否则按距离叠加到收盘价 cl 上。 下行段里,DnDotBuffer[i] 取 CurLevel.target 或 cl-CurLevel.target,写完立刻把 CurLevel.dir 置 0,意味着该水平停止被继续跟踪。上行段对称:PeackTrough 末项 Val 大于 cl 时,UpDotBuffer[i] 写 target 或 cl+target,同样清 dir。 在 MT5 里把这段塞进自定义指标 OnCalculate,用 EURUSD 的 M15 跑一遍,能看到 dir 归零后水平线不再随新 K 线漂移——外汇与贵金属波动剧烈,这类跟踪停止逻辑只是降低重绘干扰,不预示方向,实盘仍属高风险。

MQL5 / C++
if(Target==FromVertexToVertex){
   class=class="str">"cmt">// &class="macro">#x27;target&class="macro">#x27; 栏位中的价格
   DnDotBuffer[i]=CurLevel.target;
}
 else{
   class=class="str">"cmt">// 与 &class="macro">#x27;target&class="macro">#x27; 栏位中水平的距离
   DnDotBuffer[i]=cl-CurLevel.target;
}
 class=class="str">"cmt">// 清零 &class="macro">#x27;dir&class="macro">#x27; 栏位以停止跟踪水平
 CurLevel.dir=class="num">0;
 }
 }
 }
if(PeackTrough[CurCount-class="num">1].Bar==i){
class="type">class="kw">double cl=CurLevel.y3(i);
if(PeackTrough[CurCount-class="num">1].Val>cl){
if(Target==FromVertexToVertex){
  UpDotBuffer[i]=CurLevel.target;
}
else{
  UpDotBuffer[i]=cl+CurLevel.target;
}
CurLevel.dir=class="num">0;

用ATR锁定长柱来起手旗形类形态

旗形、三角旗形、楔形这类持续形态,起手都依赖一根突发的大实体长柱。与其主观画趋势线,不如用大周期 ATR 乘系数来机械判定:当某根 K 线的影线跨度(high-low)大于 ATR(50) 的 3 倍时,就标记为长柱,并把期待方向按收闭口关系写入状态结构。 具体落地时,ATR 周期和外部系数都做成输入参数,默认 ATRPeriod=50、K1=3。长柱出现后,Cur 结构里的 Whait 设 1(看涨)或 -1(看跌),Count 清零,Bar 记下标;后续覆盖柱计数才正式开始。覆盖判定不从第二根算起,而是跳过紧邻长柱的第一根,从第三根起算两根高低价的最小交集。 覆盖阈值不能用固定点数,否则换时段就失真。原文改用两柱最长影线作基准来比覆盖差,只有当连续覆盖柱数超过 MinCount(计算时扣掉长柱和末柱两根)才进入形状筛查。形状分收敛、水平、扩张三类,各自有独立开关和系数;倾斜检查再按期待方向分买/卖两套逻辑,最后末柱方向可设顺向或逆向入场。外汇与贵金属杠杆高,形态失败率不低,上述信号仅描述概率倾向,实盘前请在 MT5 用复盘跑一遍附件 iFlag。 下面这段是核心判定的 MQL5 骨架,逐行看:input 定义 ATR 周期 50 与系数 3;iATR 取句柄失败就报警并返回 INIT_FAILED;CopyBuffer 取 1 根 ATR 值;high-low 超 atr*K1 判长柱,收高于开则 Whait=1 反之 -1,同时重置 Count 与 Bar;SCurPre 结构带 Init() 一键清零三个字段;静态变量 Cur/Pre/LastTime 跨 tick 保持状态;start 为循环起点。

MQL5 / C++
input class="type">int                ATRPeriod            =  class="num">50;
input class="type">class="kw">double             K1                   =  class="num">3;
class="type">int h;
h=iATR(Symbol(),Period(),ATRPeriod);
if(h==INVALID_HANDLE){
   Alert("载入指标出错");
   class="kw">return(INIT_FAILED);
}
class="type">class="kw">double atr[class="num">1];
if(CopyBuffer(h,class="num">0,rates_total-i-class="num">1,class="num">1,atr)==-class="num">1){
   class="kw">return(class="num">0);
}
if(high[i]-low[i]>atr[class="num">0]*K1){    class=class="str">"cmt">// 长柱形
   if(close[i]>open[i]){         class=class="str">"cmt">// 柱为上涨方向
      Cur.Whait=class="num">1;
      Cur.Count=class="num">0;
      Cur.Bar=i;
   }
   else if(close[i]<open[i]){    class=class="str">"cmt">// 柱为下跌方向
      Cur.Whait=-class="num">1;     
      Cur.Count=class="num">0;
      Cur.Bar=i;
   }
}
class="kw">struct SCurPre{
   class="type">int Whait;
   class="type">int Count;
   class="type">int Bar;
   class="type">void Init(){
      Whait=class="num">0;
      Count=class="num">0;
      Bar=class="num">0;
   }
};
class="kw">static class="type">class="kw">datetime LastTime=class="num">0;    
class="kw">static SCurPre Cur;            
class="kw">static SCurPre Pre;
class="type">int start=class="num">0;

「重叠度与平均形态参数的判定骨架」

指标首次加载时 prev_calculated 为 0,这时把起始索引 start 设为 1,并初始化当前柱 Cur 与前一根 Pre 的状态容器,同时把 LastTime 清零。之后每次新 tick 进来走 else 分支,start 取 prev_calculated-1,只对最后一根及正在形成的柱做重算,避免整段历史重复跑。 时间推进靠 LastTime 判断:若 time[i] 大于已记录值,说明是新柱,把 Cur 暂存进 Pre 再更新 LastTime;否则就是同根柱重算,直接把 Pre 还原给 Cur,保证形态累计不被抖动破坏。 重叠度用 MathMin(high[i],high[i-1])-MathMax(low[i],low[i-1]) 算两根柱真实交叠区间。PreSize 取相邻两柱实体较大者,当 Overlapping 不到 PreSize 的 40%(MinOverlapping 输入值 0.4)时,趋势延续的概率倾向更高,但这只是过滤条件之一。 Cur.Count 记录等待状态 Whait 非零后的柱数,满 3 根才进进一步检查;外部另设 MinCount=5,要求 Cur.Count-2 不小于 5 才认定形态样本够用。外汇与贵金属波动跳空频繁,这套阈值在重大数据行情中可能失效,需实盘验证。 PatternParameters 函数对 i-1 往前 Cur.Count-2 根柱求平均大小、平均偏移与大小偏差:AverSize 先装首柱极差再累加后除总数;AverBias 与 AverSizeDif 用相邻中轴差和极差差求和,最后 Bias 除以 CurCnt-1。复制下面代码到 MT5 自定义指标,改 MinOverlapping 从 0.4 调到 0.2 看信号密度变化。

MQL5 / C++
if(prev_calculated==class="num">0){           class=class="str">"cmt">// 第一次计算指标
  start=class="num">1;      
  Cur.Init();
  Pre.Init();              
  LastTime=class="num">0;      
}
else{                             class=class="str">"cmt">// 计算新柱和正在生成的柱
  start=prev_calculated-class="num">1;
}
if(time[i]>LastTime){       class=class="str">"cmt">// 第一次计算一个柱
  LastTime=time[i];
  Pre=Cur;             
}
else{                      class=class="str">"cmt">// 重新计算一个柱
  Cur=Pre;
}  
if(Cur.Whait!=class="num">0){
  Cur.Count++;            class=class="str">"cmt">// 计算柱数
  if(Cur.Count>=class="num">3){
    class=class="str">"cmt">// 进一步检查
  }
}
Overlapping=MathMin(high[i],high[i-class="num">1])-MathMax(low[i],low[i-class="num">1]);
class="type">class="kw">double PreSize=MathMax(high[i-class="num">1]-low[i-class="num">1],high[i]-low[i]);
if(!(Overlapping>=PreSize*MinOverlapping))
if(Cur.Count-class="num">2>=MinCount){
  class=class="str">"cmt">// 进一步检查
}
Cur.Whait=class="num">0;
input class="type">class="kw">double              MinOverlapping       =  class="num">0.4;
input class="type">int                 MinCount             =  class="num">5;
class="type">class="kw">double AverSize,AverBias,AverSizeDif;
PatternParameters(high,low,i-class="num">1,Cur.Count-class="num">2,AverSize,AverBias,AverSizeDif);
class="type">void PatternParameters( const class="type">class="kw">double & high[],
                        const class="type">class="kw">double & low[],
                        class="type">int i,
                        class="type">int CurCnt,
                        class="type">class="kw">double & AverSize,
                        class="type">class="kw">double & AverBias,
                        class="type">class="kw">double & AverSizeDif
){
  class=class="str">"cmt">// 平均柱大小            
  AverSize=high[i-CurCnt]-low[i-CurCnt];
  class=class="str">"cmt">// 平均柱偏移
  AverBias=class="num">0;
  class=class="str">"cmt">// 两个邻近柱偏差的平均值
  AverSizeDif=class="num">0;
  
  for(class="type">int k=i-CurCnt+class="num">1;k<i;k++){      class=class="str">"cmt">// 序列中除了第一个柱的所有柱
    class=class="str">"cmt">// 平均大小
    AverSize+=high[k]-low[k];
    class=class="str">"cmt">// 平均偏移
    class="type">class="kw">double mc=(high[k]+low[k])/class="num">2;
    class="type">class="kw">double mp=(high[k-class="num">1]+low[k-class="num">1])/class="num">2;
    AverBias+=(mc-mp);
    class=class="str">"cmt">// 平均大小的偏差
    class="type">class="kw">double sc=(high[k]-low[k]);
    class="type">class="kw">double sp=(high[k-class="num">1]-low[k-class="num">1]);
    AverSizeDif+=(sc-sp);              
    
  }
  
  class=class="str">"cmt">// 总数除以数量
  AverSize/=CurCnt;
  AverBias/=(CurCnt-class="num">1);

◍ 形态分类与方向判定的代码骨架

这段逻辑在算完平均波幅差(AverSizeDif)和平均偏离(AverBias)之后,用三个布尔函数把局部K线簇归成三类:收敛(Tapered)、横盘(Horizontal)、扩张(Expanding)。判定阈值都做成了 input 参数,比如 FormTaperedK=0.05 表示平均波幅差小于 -0.05 倍平均波幅时才算收敛,FormRectangularK=0.33 给横盘留了三分之一波幅的容差。 方向检查接在形态之后:当 Cur.Whait==1 时调 CheckInclineForBuy,把 AverBias/AverSize 传进去;等于 -1 时调 CheckInclineForSell。CheckInclineForBuy 里 Val>0.1 视为顺斜(InclineAlongK=0.1),绝对值小于 0.1 视为水平,Val<-0.1 视为逆斜,三类都可在外部开关。 外汇与贵金属波动随机性高,这些形态只是概率倾向,实盘前务必在 MT5 用历史数据跑一遍确认参数敏感度和漏报率。

MQL5 / C++
AverSizeDif/=(CurCnt-class="num">1);
}
for(class="type">int k=i-CurCnt+class="num">1;k<i;k++)
AverSize+=high[k]-low[k];
class="type">class="kw">double mc=(high[k]+low[k])/class="num">2;
class="type">class="kw">double mp=(high[k-class="num">1]+low[k-class="num">1])/class="num">2;
AverBias+=(mc-mp);
class="type">class="kw">double sc=(high[k]-low[k]);
class="type">class="kw">double sp=(high[k-class="num">1]-low[k-class="num">1]);
AverSizeDif+=(sc-sp);
AverSize/=CurCnt;
AverBias/=(CurCnt-class="num">1);
AverSizeDif/=(CurCnt-class="num">1);
if(   FormTapered(AverSizeDif,AverSize) ||
      FormHorizontal(AverSizeDif,AverSize) ||
      FormExpanding(AverSizeDif,AverSize)
){
   class=class="str">"cmt">// 检查方向
}
input class="type">bool                 FormTapered            =  true;
input class="type">class="kw">double               FormTaperedK           =  class="num">0.05;
input class="type">bool                 FormRectangular        =  true;
input class="type">class="kw">double               FormRectangularK       =  class="num">0.33;
input class="type">bool                 FormExpanding          =  true;
input class="type">class="kw">double               FormExpandingK         =  class="num">0.05;
class="type">bool FormTapered(class="type">class="kw">double AverDif, class="type">class="kw">double AverSize){
   class="kw">return(FormTapered && AverDif<-FormTaperedK*AverSize);
}
class="type">bool FormHorizontal(class="type">class="kw">double AverDif, class="type">class="kw">double AverSize){
   class="kw">return(FormRectangular && MathAbs(AverDif)<FormRectangularK*AverSize);
}
class="type">bool FormExpanding(class="type">class="kw">double AverDif, class="type">class="kw">double AverSize){
   class="kw">return(FormExpanding && AverDif>FormExpandingK*AverSize);
}
if(Cur.Whait==class="num">1){
   if(CheckInclineForBuy(AverBias/AverSize)){
      class=class="str">"cmt">// 再检查上涨方向
   }
}
else if(Cur.Whait==-class="num">1){
   if(CheckInclineForSell(AverBias/AverSize)){
      class=class="str">"cmt">// 再检查下跌方向
   }
}
input class="type">bool                 InclineAlong           =  true;
input class="type">class="kw">double               InclineAlongK          =  class="num">0.1;
input class="type">bool                 InclineHorizontal      =  true;
input class="type">class="kw">double               InclineHorizontalK     =  class="num">0.1;
input class="type">bool                 InclineAgainst         =  true;
input class="type">class="kw">double               InclineAgainstK        =  class="num">0.1;
class="type">bool CheckInclineForBuy(class="type">class="kw">double Val){
   class="kw">return(  (InclineAlong && Val>InclineAlongK) ||
            (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) ||
            (InclineAgainst && Val<-InclineAgainstK)

顺势与逆势的入场过滤写法

这段逻辑把K线倾斜方向拆成三类:沿趋势、水平、逆趋势,分别用 InclineAlong / InclineHorizontal / InclineAgainst 三个开关控制。CheckInclineForBuy 里,沿趋势要求斜率 Val 大于阈值 InclineAlongK,水平要求绝对值小于 InclineHorizontalK,逆趋势则要求小于负的 InclineAgainstK,卖单判断正好反过来。 入场侧用 EnterAlong 和 EnterAgainst 两个 input 决定要不要接阳线或阴线。比如 EnterAlong=true 且收大于开,就在低点挂 Label1Buffer,并在收盘基础上加当前K线振幅作为 Label3Buffer;反之阴线触发则记录高点与下方对称位置。 外汇和贵金属波动跳变频繁,这种对称挂线方式在实时行情里可能触发滑点,开 MT5 把 InclineAlongK 从默认设成 0.0002 附近跑一下 EURUSD 的 M5,能直接看出过滤后的信号密度变化。

MQL5 / C++
class="type">bool CheckInclineForBuy(class="type">class="kw">double Val){
  class="kw">return(  (InclineAlong && Val>InclineAlongK) ||
           (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) ||
           (InclineAgainst && Val<-InclineAgainstK)
  );
}
class="type">bool CheckInclineForSell(class="type">class="kw">double Val){
  class="kw">return(  (InclineAlong && Val<-InclineAlongK) ||
           (InclineHorizontal && MathAbs(Val)<InclineHorizontalK) ||
           (InclineAgainst && Val>InclineAgainstK)
  );
}
input class="type">bool                EnterAlong        =  true;
input class="type">bool                EnterAgainst      =  true;
if((EnterAlong && close[i]>open[i]) || (EnterAgainst && close[i]<open[i])){
  Label1Buffer[i]=low[i];
  Label3Buffer[i]=close[i]+(high[Cur.Bar]-low[Cur.Bar]);
}
if((EnterAlong && close[i]<open[i]) || (EnterAgainst && close[i]>open[i])){
  Label2Buffer[i]=high[i];
  Label4Buffer[i]=close[i]-(high[Cur.Bar]-low[Cur.Bar]);
}

「用子窗口曲线给指标做实盘前体检」

想在 MT5 里验证一个自定义指标到底有没有用,最省事的办法是把策略测试器里的 EA 改改外部参数和 iCustom() 调用跑一遍。但还有更直接的路:做一个测试指标,在另一个子窗口里模拟按主指标箭头交易,把净值线和余额线画出来,一眼就能看出信号质量。 四个实现思路里,iCustom() 调用最直白,缺点是主指标画箭头在价格窗口、测试指标画曲线在子窗口,改参数得两个都改,麻烦。用 ChartIndicatorAdd() 把测试指标挂到图表上更顺手,参数一变只需删了重挂。还有种更简单的第四法:写一个通用测试指标,稍微改改就能套到 iHorizontalFormation 和 iFlag 上。 iHorizontalFormation 基于 ZigZag,可选高价-低价、收盘价或其它源。用 high-low 算时箭头不消失,能在当前形成中柱分析信号;用收盘价等时箭头会追已成型柱。所以测试指标必须知道该查哪根柱。目标点能否当获利位,也只在 high-low 模式下成立。 参数怎么传给测试指标?全局变量行不通——MT5 改参数会新建指标实例并立即退出旧实例,OnInit() 不重跑,prev_calculated 不重置,全局变量拿不到新值。办法是用一个缓冲区元素传两个数:低位表示查形成中柱(0)还是完成柱(1),高位表示是否用目标点(0/1)。两个数都不大于10,直接一个乘10加另一个即可,量小不必二进制省字节。 iFlag 永远查完成柱、目标点永远可用,OnInit() 里把该值写死成 11 即可。iHorizontalFormation 还要多开一个缓冲区画进场价水平,因为当前柱进场时价格未知。 测试指标 iTester 的外部参数包括:ID(主指标标识)、StopLoss_K(用目标点时按获利算止损的系数)、FixedSLTP(用固定止损止盈还是目标点+系数)。图17显示 iHorizontalFormation 箭头在图、iTester 子窗口里绿线净值红线余额的实况。外汇与贵金属杠杆高,曲线好看不代表实盘能复制,上 MT5 挂同一组参数跑才有意义。

MQL5 / C++
input class="type">int                ID          =  class="num">1;
class="type">class="kw">string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
class="type">int ForTester=class="num">0;       class=class="str">"cmt">// 一个用于此数值的变量
if(!(SrcSelect==Src_HighLow)){
   class=class="str">"cmt">// 在已经完成的柱上工作
   ForTester+=class="num">10;
} 
if(!(SrcSelect==Src_HighLow || SrcSelect==Src_Close)){
   class=class="str">"cmt">// 可以使用目标点
   ForTester+=class="num">1;
}  
UpArrowBuffer[class="num">0]=ForTester;  
class="type">class="kw">string ShortName=MQLInfoString(MQL_PROGRAM_NAME)+"-"+IntegerToString(ID);
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);

◍ 用 ID 串起主从指标句柄

在 MT5 里做多指标协同,常见做法是让一个「测试指标」把参数透传给挂在同图表上的另一个实例。上面这段代码的核心,就是靠指标短名称末尾的 «-ID» 去反向定位目标句柄。 输入参数里 ID 默认设为 1,StopLoss/TakeProfit 各为 50 点,FixedSLTP 默认 false 表示暂不写死止损止盈。IndName 通过 MQLInfoString(MQL_PROGRAM_NAME) 拿到自身程序名,方便后续回写短名称。 定位逻辑从 ChartIndicatorsTotal(0,0) 拿到图表指标总数,再逐个用 ChartIndicatorName 取名字,从右往左找 «-» 分隔符。若子串等于 IntegerToString(ID),就认为找对了源,调用 ChartIndicatorGet 拿句柄。 句柄一旦变化,先判 -1:拿不到就只设短名称并 ChartRedraw 后 return。拿到后必须用 BarsCalculated 确认计算完毕,否则直接退出等下一 tick。随后 CopyBuffer 取最后一柱数值到 sh[1],把 sh[0] 整除 10 得到 shift——这步决定了信号是对齐已收盘 K 还是当下形成中的 K。外汇与贵金属杠杆高,这类跨指标耦合在跳空时可能取不到连续句柄,建议先在策略测试器跑一轮验证。

MQL5 / C++
Label1Buffer[class="num">0]=class="num">11;
input class="type">int                ID       =  class="num">1;
input class="type">class="kw">double            StopLoss_K  =  class="num">1;
input class="type">bool              FixedSLTP    =  false;
input class="type">int               StopLoss    =  class="num">50;
input class="type">int               TakeProfit  =  class="num">50;
class="type">class="kw">string IndName;
IndName=MQLInfoString(MQL_PROGRAM_NAME);
class="kw">struct SPos{
   class="type">int dir;
   class="type">class="kw">double price;
   class="type">class="kw">double sl;
   class="type">class="kw">double tp;
   class="type">class="kw">datetime time;
};
SPos Pos[];
class="type">int PosCnt;
class="type">class="kw">datetime LastPosTime;
class="type">int Closed;
class="type">class="kw">datetime CloseTime;
class="type">class="kw">string name;
class="kw">static class="type">int last_handle=-class="num">1;
class="kw">static class="type">int shift=class="num">0;
class="kw">static class="type">int use_target=class="num">0;
class="type">int handle=-class="num">1;   
class="type">int start=class="num">2;  
class="type">int it=ChartIndicatorsTotal(class="num">0,class="num">0);
for(class="type">int i=class="num">0;i<it;i++){        class=class="str">"cmt">// 图表上的全部指标
   class=class="str">"cmt">// 取得下一个指标的名称
   name=ChartIndicatorName(class="num">0,class="num">0,i);
   class=class="str">"cmt">// 搜索子字符串 "-"
   class="type">int p=StringFindRev(name,"-");
   if(p!=-class="num">1){
      class=class="str">"cmt">// 找到了子字符串,检查标识符的值
      if(StringSubstr(name,p+class="num">1,StringLen(name)-p-class="num">1)==IntegerToString(ID)){
         class=class="str">"cmt">// ID 对应,取得句柄
         handle=ChartIndicatorGet(class="num">0,class="num">0,name);
      }
   }
}
if(handle!=last_handle){
   if(handle==-class="num">1){                    class=class="str">"cmt">// 没有句柄
      class=class="str">"cmt">// 设置源名称
      IndicatorSetString(INDICATOR_SHORTNAME,IndName);
      ChartRedraw(class="num">0);
      class="kw">return(class="num">0);
   }
   class=class="str">"cmt">// 检查测试指标的计算是否已经结束
   class="type">int bc=BarsCalculated(handle);
   if(bc<=class="num">0)class="kw">return(class="num">0);                class=class="str">"cmt">// 如果没有,函数就中断
   class=class="str">"cmt">// 使用测试参数复制数据
   class="type">class="kw">double sh[class="num">1];
   if(CopyBuffer(handle,class="num">0,rates_total-class="num">1,class="num">1,sh)==-class="num">1){
      class=class="str">"cmt">// 如果复制失败,函数就中断直到
      class=class="str">"cmt">// 下一个分时
      class="kw">return(class="num">0);
   }
   class=class="str">"cmt">// 展开独立的参数
   shift=((class="type">int)sh[class="num">0])/class="num">10;              class=class="str">"cmt">// 已完成或是正在出现的柱

逐根 K 线回放持仓与权益

这段逻辑干的是把外部测试指标的信号按时间轴一根根 K 线重放,同时把账户余额和净值两条线填进缓冲区。start 决定了从哪一根开始算:首次加载从 2 开始并清零所有统计变量,后续只从 prev_calculated-1 接着跑,避免重复计算已成型 K 线。 循环里先用 BalanceBuffer[i]=BalanceBuffer[i-1] 继承上一根余额,EquityBuffer 同理。若 CloseTime 不等于当前 time[i],说明进了新柱,把 Closed 利润累加器清零,这是按柱切分平仓统计的关键点。 通过 CopyBuffer 从 last_handle 取 0~3 号缓冲区得到 buy、sell、buy_target、sell_target,任一失败直接 return(0) 中断。shift==0 时从指标第 4 号缓冲区取实时开盘价 enter,否则用已完成 K 线的 open[i],这个分支决定了回测用真实开盘还是信号附带价。 遇到 buy[0]!=EMPTY_VALUE 就 AddPos(1,...) 开多,sell 同理开空,是否带目标位由 use_target 控制。每根末尾 CheckClose 扫一遍高低收判断平仓,Closed 累加进 BalanceBuffer,EquityBuffer 再叠 SolveEquity 算浮动盈亏。外汇与贵金属杠杆品种这样回放波动,滑点和点差会显著扭曲净值,属高风险验证。 把下面这段直接贴进 MT5 自定义指标 OnCalculate 中段,接上你自己的 AddPos / CheckClose 就能跑通逐根重放。

MQL5 / C++
use_target=((class="type">int)sh[class="num">0])%class="num">10;         class=class="str">"cmt">// 是否使用目标点?
last_handle=handle;                 class=class="str">"cmt">// 保存句柄数值
class=class="str">"cmt">// 设置指标名称
IndicatorSetString(INDICATOR_SHORTNAME,name);
ChartRedraw(class="num">0);
}
else if(prev_calculated!=class="num">0){
   class=class="str">"cmt">// 如果没有新的句柄,只计算新柱, 
   class=class="str">"cmt">// 还有当前正在形成的柱
   start=prev_calculated-class="num">1;
}
if(start==class="num">2){
   PosCnt=class="num">0;
   BalanceBuffer[class="num">1]=class="num">0;
   EquityBuffer[class="num">1]=class="num">0;
   LastPosTime=class="num">0;
   Closed=class="num">0;
   CloseTime=class="num">0;       
}
for(class="type">int i=start;i<rates_total;i++){
   class=class="str">"cmt">// 传入之前知道的余额和净值
   BalanceBuffer[i]=BalanceBuffer[i-class="num">1];
   EquityBuffer[i]=EquityBuffer[i-class="num">1];
   if(CloseTime!=time[i]){
      class=class="str">"cmt">// 开始新柱的计算
      Closed=class="num">0; class=class="str">"cmt">// 把利润变量清零
      CloseTime=time[i];          
   }
   class=class="str">"cmt">// 取得测试指标的数据
   class="type">class="kw">double buy[class="num">1],sell[class="num">1],buy_target[class="num">1],sell_target[class="num">1],enter[class="num">1];
   class="type">int ind=rates_total-i-class="num">1+shift;
   if(CopyBuffer(last_handle,class="num">0,ind,class="num">1,buy)==-class="num">1 ||
      CopyBuffer(last_handle,class="num">1,ind,class="num">1,sell)==-class="num">1 ||
      CopyBuffer(last_handle,class="num">2,ind,class="num">1,buy_target)==-class="num">1 ||
      CopyBuffer(last_handle,class="num">3,ind,class="num">1,sell_target)==-class="num">1       
   ){
      class="kw">return(class="num">0);
   }
   
   if(shift==class="num">0){
      class=class="str">"cmt">// 如果测试是在正在出现的柱上进行的,取得开盘价格 
      class=class="str">"cmt">// 要从测试指标的额外缓冲区中取得      
      if(CopyBuffer(last_handle,class="num">4,ind,class="num">1,enter)==-class="num">1){
         class="kw">return(class="num">0);
      }
   }
   else{
      class=class="str">"cmt">// 如果测试是在已经完成的柱上进行的,我们使用柱的
      class=class="str">"cmt">// 开盘价格
      enter[class="num">0]=open[i];
   }
   class=class="str">"cmt">// 买入箭头
   if(buy[class="num">0]!=EMPTY_VALUE){
      AddPos(class="num">1,enter[class="num">0],buy_target[class="num">0],spread[i],time[i],use_target);      
   }
   class=class="str">"cmt">// 卖出箭头
   if(sell[class="num">0]!=EMPTY_VALUE){
      AddPos(-class="num">1,enter[class="num">0],sell_target[class="num">0],spread[i],time[i],use_target);      
   }
   class=class="str">"cmt">// 检查是否需要平仓
   CheckClose(i,high,low,close,spread);
   class=class="str">"cmt">// 余额线
   BalanceBuffer[i]+=Closed;
   
   class=class="str">"cmt">// 净值线
   EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);
}

「回测里怎么把信号接进仓位管理」

这段逻辑干的事很直接:从自定义指标缓冲区把买卖信号和目标价读出来,再喂给 AddPos 建仓。CopyBuffer 一次拉 1 根柱的 4 个缓冲(0~3 分别是买、卖、买目标、卖目标),任意一个返回 -1 就直接 return(0) 放弃本轮,避免脏数据进回测。 正在形成的柱(shift==0)要从指标第 4 号缓冲取 enter 开盘价,已完成柱则直接用 open[i]。这处细节决定了你回测当前 K 线信号时,用的是实时虚拟开盘还是历史实测开盘,差一点就会让曲线好看但实盘亏。 AddPos 里有个防重仓判断:time<=LastPosTime 就跳过,同一根柱不会重复加仓。Pos 数组满了对齐扩 32 个元素块(ArrayResize(Pos,ArraySize(Pos)+32)),属于典型的以空间换循环效率写法。 止损算法值得盯:use_target 且非固定 SLTP 时,多单 sl = 入场价 - StopLoss_K*(目标价-入场价)。StopLoss_K 设 1.0 意味着止损幅度等同盈利目标,设 0.5 则风险减半,调这个参数比调魔法数更影响回测权益曲线形态。外汇与贵金属杠杆高,此类回测仅作方法验证,实盘可能显著偏离。 建仓后立刻跑 CheckClose(i,high,low,close,spread) 扫平仓,再累加 BalanceBuffer 和 EquityBuffer。EquityBuffer 用 SolveEquity 把浮盈按 close 和 spread 算进去,所以你看到的权益线已经扣了点差——这是多数自制回测漏掉的一项。

MQL5 / C++
class="type">class="kw">double buy[class="num">1],sell[class="num">1],buy_target[class="num">1],sell_target[class="num">1],enter[class="num">1];
class="type">int ind=rates_total-i-class="num">1+shift;
if(CopyBuffer(last_handle,class="num">0,ind,class="num">1,buy)==-class="num">1 ||
   CopyBuffer(last_handle,class="num">1,ind,class="num">1,sell)==-class="num">1 ||
   CopyBuffer(last_handle,class="num">2,ind,class="num">1,buy_target)==-class="num">1 ||
   CopyBuffer(last_handle,class="num">3,ind,class="num">1,sell_target)==-class="num">1       
){
   class="kw">return(class="num">0);
}
if(shift==class="num">0){
   class=class="str">"cmt">// 如果测试是在正在出现的柱上进行的,取得开盘价格 
   class=class="str">"cmt">// 要从测试指标的额外缓冲区中取得     
   if(CopyBuffer(last_handle,class="num">4,ind,class="num">1,enter)==-class="num">1){
      class="kw">return(class="num">0);
   }
}
else{
   class=class="str">"cmt">// 如果测试是在已经完成的柱上进行的,我们使用柱的
   class=class="str">"cmt">// 开盘价格
   enter[class="num">0]=open[i];
}
class=class="str">"cmt">// 买入箭头
if(buy[class="num">0]!=EMPTY_VALUE){
   AddPos(class="num">1,enter[class="num">0],buy_target[class="num">0],spread[i],time[i],use_target);      
}
class=class="str">"cmt">// 卖出箭头
if(sell[class="num">0]!=EMPTY_VALUE){
   AddPos(-class="num">1,enter[class="num">0],sell_target[class="num">0],spread[i],time[i],use_target);      
}
class=class="str">"cmt">// 检查,是否需要平仓
CheckClose(i,high,low,close,spread);
class=class="str">"cmt">// 余额线
BalanceBuffer[i]+=Closed;
EquityBuffer[i]=BalanceBuffer[i]+SolveEquity(i,close,spread);
class="type">void AddPos(class="type">int dir, class="type">class="kw">double price,class="type">class="kw">double target,class="type">int spread,class="type">class="kw">datetime time,class="type">bool use_target){
   if(time<=LastPosTime){
      class=class="str">"cmt">// 带有 &class="macro">#x27;time&class="macro">#x27; 时间的仓位已经被加过了
      class="kw">return;
   }
   
   class=class="str">"cmt">// 数组中没有空闲空间
   if(PosCnt>=ArraySize(Pos)){
      class=class="str">"cmt">// 数组大小增加32个元素块大小
      ArrayResize(Pos,ArraySize(Pos)+class="num">32);
   }
   
   class=class="str">"cmt">// 保存仓位的方向
   Pos[PosCnt].dir=dir;
   class=class="str">"cmt">// 仓位建立时间
   Pos[PosCnt].time=time;
   class=class="str">"cmt">// 仓位的开盘价格
   if(dir==class="num">1){
      class=class="str">"cmt">// 买入的价格
      Pos[PosCnt].price=price+Point()*spread;  
   }
   else{
      class=class="str">"cmt">// 卖出的价格
      Pos[PosCnt].price=price;  
   }
   class=class="str">"cmt">// 计算止损和获利
   if(use_target && !FixedSLTP){
      class=class="str">"cmt">// 使用目标点来计算止损
      if(dir==class="num">1){
         Pos[PosCnt].tp=target;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price-StopLoss_K*(Pos[PosCnt].tp-Pos[PosCnt].price),Digits());
      }
      else{
         Pos[PosCnt].tp=target+Point()*spread;
         Pos[PosCnt].sl=NormalizeDouble(Pos[PosCnt].price+StopLoss_K*(Pos[PosCnt].price-Pos[PosCnt].tp),Digits());
      }  
   }
   else{
      class=class="str">"cmt">// 使用 StopLoss 和 TakeProfit 变量来止损
      if(dir==class="num">1){
         Pos[PosCnt].tp=Pos[PosCnt].price+Point()*TakeProfit;

◍ 回测里平仓判定与浮盈点数累加

上面这段逻辑跑在 CheckClose 里,每根 K 线对当前所有持仓做一次扫描。买入仓看 low[i] 是否打到 sl、high[i] 是否摸到 tp;卖出仓则把 spread[i] 的点差折算进触发价,再比 high 和 low,避免回测忽略交易成本导致利润虚高。 平仓后利润以『点数』入账:买入命中止损记 (sl-price)/Point,命中止盈记 (tp-price)/Point;卖出反过来用 price 减 sl/tp。注意这里 Closed 是 int 强转,小数点后的零头直接被截断,实盘 EURUSD 最小跳动 0.00001 时 1 点 = 1 Point,XAUUSD 用 0.01 报价时 1 点也对应 1 Point,但点值不同。 仓位关闭不从数组物理删除,而是用 ArrayCopy 把 j+1 之后的元素前移、PosCnt 减一。回测样本里若同时持有 50 仓、某根 K 线平掉中间第 10 仓,ccnt=39 次拷贝,MT5 策略测试器里能直接观察到 PosCnt 的实时回落。外汇与贵金属杠杆高,点差滑点会让上述理论点数缩水,验证时建议先开 MT5 用点差 20 点以上的 XAUUSD 跑一遍。

MQL5 / C++
class="type">void CheckClose(class="type">int i,const class="type">class="kw">double & high[],const class="type">class="kw">double & low[],const class="type">class="kw">double & close[],const class="type">int & spread[]){
  for(class="type">int j=PosCnt-class="num">1;j>=class="num">0;j--){                                       class=class="str">"cmt">// 所有仓位
    class="type">bool closed=false;                                                class=class="str">"cmt">// &class="macro">#x27;false&class="macro">#x27; 值表示仓位依然开启
    if(Pos[j].dir==class="num">1){                                                class=class="str">"cmt">// 买入
      if(low[i]<=Pos[j].sl){                                          class=class="str">"cmt">// 价格低于或者等于止损
        class=class="str">"cmt">// 利润点数
        Closed+=(class="type">int)((Pos[j].sl-Pos[j].price)/Point());
        closed=true;                                                  class=class="str">"cmt">// 以j为索引的仓位已经平仓
      }
      else if(high[i]>=Pos[j].tp){                                    class=class="str">"cmt">// 已经达到了获利
        class=class="str">"cmt">// 利润点数
        Closed+=(class="type">int)((Pos[j].tp-Pos[j].price)/Point());
        closed=true;                                                  class=class="str">"cmt">// 以j为索引的仓位已经平仓
      }
    }
    else{ class=class="str">"cmt">// Selling
      if(high[i]+Point()*spread[i]>=Pos[j].sl){                       class=class="str">"cmt">// 已经达到了止损
        class=class="str">"cmt">// 利润点数
        Closed+=(class="type">int)((Pos[j].price-Pos[j].sl)/Point());
        closed=true;                                                  class=class="str">"cmt">// 以j为索引的仓位已经平仓
      }
      else if(low[i]+Point()*spread[i]<=Pos[j].tp){                   class=class="str">"cmt">// 价格低于或者等于获利
        class=class="str">"cmt">// 利润点数
        Closed+=(class="type">int)((Pos[j].price-Pos[j].tp)/Point());
        closed=true;                                                  class=class="str">"cmt">// 以j为索引的仓位已经平仓
      }
    }
    class=class="str">"cmt">// 仓位已经关闭,它应当从数组中删除
    if(closed){
      class="type">int ccnt=PosCnt-j-class="num">1;
      if(ccnt>class="num">0){
        ArrayCopy(Pos,Pos,j,j+class="num">1,ccnt);
      }
      PosCnt--;
    }
  }
}

逐仓净值折算要算上点差

回测里很多人直接拿开仓价和收盘价相减算浮盈,实盘一跑就发现权益曲线对不上,根子常在卖单没扣点差。下面这段函数把每个持仓按方向摊进总权益点数,买入只看价差,卖出必须加上当前 spread 数组里的点值。 [CODE] int SolveEquity(int i,const double & close[],const int & spread[]){ int rv=0; // 用于结果的变量 for(int j=PosCnt-1;j>=0;j--){ // 所有仓位 if(Pos[j].dir==1){ // 买入 // 利润 rv+=(int)((close[i]-Pos[j].price)/Point()); } else{ // 卖出 // 利润 rv+=(int)((Pos[j].price+Point()*spread[i]-close[i])/Point()); } } return(rv); } [/CODE] 逐行看:rv 初始化为 0,是累计权益点数;循环从最后一个仓位 PosCnt-1 倒序到 0,覆盖全部持仓。dir==1 判定为买,盈利 = (收盘价 - 开仓价) / 点值;否则为卖,盈利 = (开仓价 + 点差点数×点值 - 收盘价) / 点值,这里 spread[i] 乘 Point() 才是真实成本。 外汇与贵金属杠杆高、点差跳变频繁,用 MT5 策略测试器跑历史时,把这段代码塞进 OnTester 或逐根 K 线调用,对比忽略点差的权益曲线,EURUSD 在新闻秒点差扩到 30 点以上时,两条曲线最大偏离可能超过 200 点。开 MT5 把 spread 数组打出来,先确认你的数据源有没有逐笔点差再信回测。

MQL5 / C++
class="type">int SolveEquity(class="type">int i,const class="type">class="kw">double & close[],const class="type">int & spread[]){
   class="type">int rv=class="num">0;                                class=class="str">"cmt">// 用于结果的变量
   for(class="type">int j=PosCnt-class="num">1;j>=class="num">0;j--){            class=class="str">"cmt">// 所有仓位
       if(Pos[j].dir==class="num">1){                    class=class="str">"cmt">// 买入
                                        class=class="str">"cmt">// 利润
         rv+=(class="type">int)((close[i]-Pos[j].price)/Point());
       }
       else{                                class=class="str">"cmt">// 卖出
         class=class="str">"cmt">// 利润
         rv+=(class="type">int)((Pos[j].price+Point()*spread[i]-close[i])/Point());       
       }
   }
   class="kw">return(rv);
}

「形态识别不止一条路」

本文给出的形态侦测方案已经能稳定把旗形、三角旗形、三角形和楔形在图上标出来,初始任务算落地了。这套基于价格分析的逻辑不只是能跑通,在指标构建里也站得住脚。 换思路也行:用线性回归分别算最高价和最低价的拟合线,再看两条线的斜率和收口/扩张状态,同样能判别形态。单拆子任务时,可用的方法还会更多,难点在于怎么拼回通用侦测框架。 顺带一提,这种价格拆解方式对外汇、贵金属这类高波动品种尤其值得在 MT5 里手调一遍——杠杆市场假突破多,形态确认前务必自己验证斜率阈值。本文的算法骨架,挪到别的技术分析目标上也能直接改参数复用。

◍ 随包附带的三个指标文件

本篇涉及的水平形态、旗形与测试器三类指标,都已打包进随文附件,压缩包体积 9.87 KB,在 MT5 里解压后可直接拖入 Indicators 目录编译使用。 具体包括 iHorizontalFormation、iFlag 与 iTester 三个源文件,前两者负责图形识别的绘制与信号标记,后者用于在策略测试器中批量验证形态命中率。 外汇与贵金属市场杠杆高、滑点随机,图形形态识别仅提供概率参考,实盘前务必用 iTester 在本地历史数据跑一遍自己的品种周期。

把形态扫描交给小布盯盘
这些诊断与小布盯盘的 AIGC 已内置,打开对应品种页即可看到旗形类形态的实时标注,你只需在报警后做人工确认与入场决策。

常见问题

旗形由柱体覆盖区域构成,需用柱线范围判定;三角旗形属反转点序列,可用 ZigZag 拐点搜索,两类不能共用同一套识别函数。
扩张三角形窄端在右、开口向左,楔形窄端在左、向右展开,指标里用首尾波幅方向变量即可区分二者。
可以,小布盯盘的品种页已集成类似形态扫描,不需自行编译 mq5,但建议对照本文的测试节核对误报率。
至少双顶双底,三顶三底更可靠;指标参数应开放顶底数量阈值,避免单数摆动被误抓为水平旗形。
楔形依赖左侧窄右侧宽的柱覆盖,若 ZigZag 深度设太大,会把普通盘整带进结果,调小拐点敏感度可明显降噪。