旗形形态·综合运用
(3/3)·旗形、三角旗形、楔形与水平三角形的边界混乱,让多数自动识别脚本误报,本文用指标收口
「双顶点目标价的几何求法」
在形态识别里,若选择用两个顶点推目标位,核心不是量绝对幅度,而是把两组峰谷连成直线、求交叉点。下面这段函数就干了这件事:先取初始线(底部到顶部)坐标,再取同向双底或双顶的延伸线坐标,交给 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 则为起跌峰相对交叉点的净深。
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 线漂移——外汇与贵金属波动剧烈,这类跟踪停止逻辑只是降低重绘干扰,不预示方向,实盘仍属高风险。
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 为循环起点。
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 看信号密度变化。
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 用历史数据跑一遍确认参数敏感度和漏报率。
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,能直接看出过滤后的信号密度变化。
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 挂同一组参数跑才有意义。
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。外汇与贵金属杠杆高,这类跨指标耦合在跳空时可能取不到连续句柄,建议先在策略测试器跑一轮验证。
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 就能跑通逐根重放。
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 算进去,所以你看到的权益线已经扣了点差——这是多数自制回测漏掉的一项。
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 跑一遍。
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 数组打出来,先确认你的数据源有没有逐笔点差再信回测。
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 在本地历史数据跑一遍自己的品种周期。