沃尔夫波形 (Wolfe Waves)·综合运用
(3/3)·手算楔形拐点费时易错,这篇把指标、警报与回测EA一次串起来
多数人在MT5里靠肉眼找沃尔夫波形,延线交点和时点算到一半就放弃了。把规则写死成代码后,才会发现书里没说的边界条件比形态本身更影响信号质量。
点2'高于点2时的卖区着色逻辑
在下跌结构识别里,若二次测试的高点 v2x 减去前高 v2 的差值为正,即 h2=v2x-v2 成立,说明点 2' 实际高于点 2,此时原假设的次级反弹失效,应当切换为另一组颜色标记。 代码里用 SellColor 与 SellTargetColor 两组参数接管绘制,把 v1 到 v5l 的五个价格节点、i1 到 i5 的对应索引、time 与 i 的当前位置、以及 tb/tv/rates_total 等上下文一并传给 DrawObjects,实现卖区图形的重绘。 开 MT5 把这段塞进你的形态扫描 EA,改一下 SellColor 的十六进制值,就能直观看出哪些候选形态被判定为点 2' 过高而被剔除。外汇与贵金属波动剧烈,这类结构过滤只降低噪音,不预示方向,实盘仍属高风险。
class="type">class="kw">double h2=v2x-v2; class=class="str">"cmt">// 点 class="num">2&class="macro">#x27; 高于点 class="num">2 class=class="str">"cmt">// 用其它颜色绘制 DrawObjects(SellColor, SellTargetColor, v1, v2, v3, v4, v5l, i1, i2, i3, i4, i5, time, i, tb, tv, rates_total);
「波形与目标构型的绘制逻辑」
沃尔夫波形和交易目标构型在 MT5 指标里共用一个 DrawObjects() 出口函数。向上和向下的波形分别吃 BuyColor / SellColor,目标构型再单独吃 BuyTargetColor / SellTargetColor,这四个都是外部 input,交易者可以直接在指标属性里改色。 峰值 1-4 传的是之字折线尾端的真实值加柱线索引,峰值 5 是例外——它不传尾端,而是把已经算好的线 1-3 的值塞进去。图形对象锚定要靠时间,所以 time 数组指针、当前柱 i、目标柱 tb、目标值 tv、总柱数 rates_total 全部一并进函数。 只在 SrcSelect 是 Src_HighLow 或 Src_Close 时才允许画图。若在 OnInit() 里发现是别的源,就借一个 _DrawWaves 中间变量把绘图强制关掉,顺手把目标缓冲区的线颜色设成 clrNONE,历史回看才不会出鬼影。 目标柱可能落在已有 K 线上,也可能漂在最后一根右侧。代码里先声明一个分数型 target_bar,用 MathFloor() 抬到最近整数得 tbc;用 MathCeil() 也行,但线 1-3 和 2-4 的交点会更贴目标自然些。tbc 比 target_bar 略大,所以进函数后会对线 2-4 重算一次,保证在确切的 target_value 价位交准。 所有直线走通用的 fObjTrend() 画,前 5 个参数(对象名、两端时间、两端价位)必填,后面颜色宽度射线方向之类都可省。最后补一根目标柱垂直线和一条目标价水平线,构型就完整了。外汇和贵金属波动剧烈,这类构型只具提示意义,实际触发概率随品种和周期变化,请先在 MT5 策略测试器里验证再上实盘。
input class="type">bool DrawWaves = true; class=class="str">"cmt">// 启用波形和对象绘制 input class="type">class="kw">color BuyColor = clrAqua; class=class="str">"cmt">// 买入波形颜色 input class="type">class="kw">color SellColor = clrRed; class=class="str">"cmt">// 卖出波形颜色 input class="type">int WavesWidth = class="num">2; class=class="str">"cmt">// 波形宽度 input class="type">bool DrawTarget = true; class=class="str">"cmt">// 附加的启用/禁用构型 input class="type">int TargetWidth = class="num">1; class=class="str">"cmt">// 对象宽度 input class="type">class="kw">color BuyTargetColor = clrRoyalBlue; class=class="str">"cmt">// 买入对象颜色 input class="type">class="kw">color SellTargetColor = clrPaleVioletRed; class=class="str">"cmt">// 卖出对象颜色 class="type">bool _DrawWaves; if(SrcSelect==Src_HighLow || SrcSelect==Src_Close){ _DrawWaves=DrawWaves; } else{ _DrawWaves=class="kw">false; PlotIndexSetInteger(class="num">2,PLOT_LINE_COLOR,clrNONE); PlotIndexSetInteger(class="num">3,PLOT_LINE_COLOR,clrNONE); } class="type">void DrawObjects( class="type">class="kw">color col, class="type">class="kw">color tcol, class="type">class="kw">double v1, class="type">class="kw">double v2, class="type">class="kw">double v3, class="type">class="kw">double v4, class="type">class="kw">double v5, class="type">int i1,
◍ 五点波形的图形对象绘制逻辑
这段函数负责把已识别的五点价格波形以及预测构型直接画到 MT5 主图上。核心思路是用程序名+柱线时间做对象名前缀,避免多实例叠加时图形互相覆盖。 前缀拼接用了 MQLInfoString(MQL_PROGRAM_NAME) 配合 IntegerToString(time[i]),每个波形独占一套 "12/23/34/45" 的线段对象。fObjTrend 连续四次调用,分别连接 (i1,v1)-(i2,v2) 到 (i4,v4)-(i5,v5) 四个分段,col 与 WavesWidth 控制颜色和线宽。 当 DrawTarget 开关开启,代码会先由 MathCeil(target_bar) 拿到目标柱线索引 tbc。若 tbc 小于 rates_total,目标落在已有 K 线内;否则用 time[rates_total-1] 加 (tbc-rates_total+1)*PeriodSeconds() 推算未来时间——这是手动外推预测位的常见写法。 构型部分再用 y3() 在目标柱算出自 i1-i3、i2-i4、i1-i4 三条延长线的理论值,并画出 "13/24/14" 线段,以及一条水平目标带(67)和一条垂直目标线(7h)。外汇与贵金属波动剧烈,这类外推仅反映几何延续概率,实盘须自担高风险。
class="type">int i2, class="type">int i3, class="type">int i4, class="type">int i5, class="kw">const class="type">class="kw">datetime & time[], class="type">int i, class="type">class="kw">double target_bar, class="type">class="kw">double target_value, class="type">int rates_total){ class=class="str">"cmt">// 用于图形对象名称的前缀 class="type">class="kw">string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time[i])+"_"; class=class="str">"cmt">// 绘制波形 fObjTrend(prefix+"class="num">12",time[i1],v1,time[i2],v2,col,WavesWidth); fObjTrend(prefix+"class="num">23",time[i2],v2,time[i3],v3,col,WavesWidth); fObjTrend(prefix+"class="num">34",time[i3],v3,time[i4],v4,col,WavesWidth); fObjTrend(prefix+"class="num">45",time[i4],v4,time[i5],v5,col,WavesWidth); class=class="str">"cmt">// 绘制构型 if(DrawTarget){ class="type">class="kw">datetime TargetTime; class=class="str">"cmt">// 获取整数型的目标柱线索引 class="type">int tbc=(class="type">int)MathCeil(target_bar); if(tbc<rates_total){ class=class="str">"cmt">// 目标位于图表上存在的柱线之内 TargetTime=time[tbc]; } else{ class=class="str">"cmt">// 目标位于未来 TargetTime=time[rates_total-class="num">1]+(tbc-rates_total+class="num">1)*PeriodSeconds(); } class=class="str">"cmt">// 计算目标所在柱线处的值 class="type">class="kw">double tv13=y3(i1,v1,i3,v3,tbc); class="type">class="kw">double tv24=y3(i2,v2,i4,v4,tbc); class="type">class="kw">double tv14=y3(i1,v1,i4,v4,tbc); class=class="str">"cmt">// 构型 fObjTrend(prefix+"class="num">13",time[i1],v1,TargetTime,tv13,tcol,TargetWidth); fObjTrend(prefix+"class="num">24",time[i2],v2,TargetTime,tv24,tcol,TargetWidth); fObjTrend(prefix+"class="num">14",time[i1],v1,TargetTime,tv14,tcol,TargetWidth); class=class="str">"cmt">// 目标价位水平线 fObjTrend(prefix+"class="num">67",TargetTime,tv24,TargetTime,tv14,tcol,TargetWidth); class=class="str">"cmt">// 目标价位垂直线 fObjTrend(prefix+"7h",time[i],target_value,TargetTime,target_value,tcol,TargetWidth); } }
目标柱线落在未来时怎么补时间
画趋势线时最容易被忽略的一种情况:目标索引 tbc 可能大于当前图表已加载的柱线总数 rates_total。代码里用 if/else 把这两种状态拆开处理——若 tbc<rates_total,直接取 time[tbc] 作为目标时间;若目标在尚未形成的未来,则用最后一根已知柱的时间加上 (tbc-rates_total+1) 个周期秒数推算。 [CODE] if(tbc<rates_total){ // 目标位于图表上存在的柱线之内 TargetTime=time[tbc]; } else{ // 目标位于未来 TargetTime=time[rates_total-1]+(tbc-rates_total+1)*PeriodSeconds(); } // 计算目标所在柱线处的值 double tv13=y3(i1,v1,i3,v3,tbc); double tv24=y3(i2,v2,i4,v4,tbc); double tv14=y3(i1,v1,i4,v4,tbc); fObjTrend(prefix+"13",time[i1],v1,TargetTime,tv13,tcol,TargetWidth); fObjTrend(prefix+"24",time[i2],v2,TargetTime,tv24,tcol,TargetWidth); fObjTrend(prefix+"14",time[i1],v1,TargetTime,tv14,tcol,TargetWidth); [/CODE] 上面这段先按 tbc 算好 TargetTime,再用 y3() 求出三条连线在目标柱处的价格(tv13/tv24/tv14),最后调 fObjTrend 把线画出来。注意 fObjTrend 只是封装,真正干活的是 ObjectCreate(OBJ_TREND) 加一连串 ObjectSetInteger / ObjectSetString。 [CODE] void fObjTrend( string aObjName, datetime aTime_1, double aPrice_1, datetime aTime_2, double aPrice_2, color aColor = clrRed, color aWidth = 1, bool aRay_1 = false, bool aRay_2 = false, string aText = "", int aWindow = 0, color aStyle = 0, int aChartID = 0, bool aBack = false, bool aSelectable = false, bool aSelected = false, long aTimeFrames = OBJ_ALL_PERIODS ){ ObjectCreate(aChartID,aObjName,OBJ_TREND,aWindow,aTime_1,aPrice_1,aTime_2,aPrice_2); ObjectSetInteger(aChartID,aObjName,OBJPROP_BACK,aBack); ObjectSetInteger(aChartID,aObjName,OBJPROP_COLOR,aColor); ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTABLE,aSelectable); ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTED,aSelected); ObjectSetInteger(aChartID,aObjName,OBJPROP_TIMEFRAMES,aTimeFrames); ObjectSetString(aChartID,aObjName,OBJPROP_TEXT,aText); [/CODE] 逐行看:aObjName 是对象名,前缀加 "13" 这类后缀可避免重名;aTime_1/aPrice_1 是起点,aTime_2/aPrice_2 即前面算出的 TargetTime 与目标值;aColor 默认红、aWidth 默认 1;aRay 控制是否射线延伸,这里默认 false 表示线段到终点截止;OBJPROP_BACK 设 true 可让线沉到 K 线后面。外汇与贵金属波动剧烈,这类自动画线仅作结构参考,实盘信号可能随跳空失效。
if(tbc<rates_total){ class=class="str">"cmt">// 目标位于图表上存在的柱线之内 TargetTime=time[tbc]; } else{ class=class="str">"cmt">// 目标位于未来 TargetTime=time[rates_total-class="num">1]+(tbc-rates_total+class="num">1)*PeriodSeconds(); } class=class="str">"cmt">// 计算目标所在柱线处的值 class="type">class="kw">double tv13=y3(i1,v1,i3,v3,tbc); class="type">class="kw">double tv24=y3(i2,v2,i4,v4,tbc); class="type">class="kw">double tv14=y3(i1,v1,i4,v4,tbc); fObjTrend(prefix+"class="num">13",time[i1],v1,TargetTime,tv13,tcol,TargetWidth); fObjTrend(prefix+"class="num">24",time[i2],v2,TargetTime,tv24,tcol,TargetWidth); fObjTrend(prefix+"class="num">14",time[i1],v1,TargetTime,tv14,tcol,TargetWidth); class="type">void fObjTrend( class="type">class="kw">string aObjName, class="type">class="kw">datetime aTime_1, class="type">class="kw">double aPrice_1, class="type">class="kw">datetime aTime_2, class="type">class="kw">double aPrice_2, class="type">class="kw">color aColor = clrRed, class="type">class="kw">color aWidth = class="num">1, class="type">bool aRay_1 = class="kw">false, class="type">bool aRay_2 = class="kw">false, class="type">class="kw">string aText = "", class="type">int aWindow = class="num">0, class="type">class="kw">color aStyle = class="num">0, class="type">int aChartID = class="num">0, class="type">bool aBack = class="kw">false, class="type">bool aSelectable = class="kw">false, class="type">bool aSelected = class="kw">false, class="type">long aTimeFrames = OBJ_ALL_PERIODS ){ ObjectCreate(aChartID,aObjName,OBJ_TREND,aWindow,aTime_1,aPrice_1,aTime_2,aPrice_2); ObjectSetInteger(aChartID,aObjName,OBJPROP_BACK,aBack); ObjectSetInteger(aChartID,aObjName,OBJPROP_COLOR,aColor); ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTABLE,aSelectable); ObjectSetInteger(aChartID,aObjName,OBJPROP_SELECTED,aSelected); ObjectSetInteger(aChartID,aObjName,OBJPROP_TIMEFRAMES,aTimeFrames); ObjectSetString(aChartID,aObjName,OBJPROP_TEXT,aText);
「趋势线的射线与端点控制」
在 MT5 里画一条可控的趋势线,核心不是画线动作本身,而是用 ObjectSetInteger 把射线方向和线宽样式先钉死。OBJPROP_RAY_LEFT 与 OBJPROP_RAY_RIGHT 决定线条向左、向右是否无限延伸,传 0 或 1 即可切换,避免默认双向射线把图表搞脏。 线宽 OBJPROP_WIDTH 和线型 OBJPROP_STYLE 也走同一函数,参数用整型常量,例如实线、虚线在 MQL5 里是枚举值而非字符串。下面这段是实际封装后的调用片段,两个 fObjTrend 分别用前缀 "67" 和 "7h" 建对象,传入起止时间价格与颜色线宽。 端点定位靠 ObjectMove,锚点索引 0 和 1 对应第一、第二个坐标,时间价格必须同时给,否则线会漂。外汇与贵金属波动快,这类手动锚定射线在高杠杆下误判风险高,参数错了可能画出反向误导线。
ObjectSetInteger(aChartID,aObjName,OBJPROP_WIDTH,aWidth); ObjectSetInteger(aChartID,aObjName,OBJPROP_STYLE,aStyle); ObjectSetInteger(aChartID,aObjName,OBJPROP_RAY_LEFT,aRay_1); ObjectSetInteger(aChartID,aObjName,OBJPROP_RAY_RIGHT,aRay_2); ObjectMove(aChartID,aObjName,class="num">0,aTime_1,aPrice_1); ObjectMove(aChartID,aObjName,class="num">1,aTime_2,aPrice_2); } fObjTrend(prefix+"class="num">67",TargetTime,tv24,TargetTime,tv14,tcol,TargetWidth); fObjTrend(prefix+"7h",time[i],target_value,TargetTime,target_value,tcol,TargetWidth);
◍ 之字折线重绘时的对象清理逻辑
基于收盘价或另一指标生成之字折线时,柱线尚未收盘,箭头和点会在实时波动里反复出现又消失。若不在每轮指标计算开头清空缓冲区和删除图形对象,图表会残留上一 tick 的无效标记,干扰形态识别。 主循环开头先把上下箭头与点的缓冲区置为 EMPTY_VALUE,再判断 _DrawWaves 开关:若开启波形绘制,就调用 DeleteObjects(time[i]),传入当前柱时间,删掉该柱名下挂的 12、23、34 等 10 个对象前缀。 DeleteObjects 内部用 MQL_PROGRAM_NAME 加时间和下划线拼前缀,只删自己指标创建的局部对象,不会误伤其他指标。指标卸载时 OnDeinit 自动跑 ObjectsDeleteAll,第二个参数同样传程序名,确保图表上清掉的仅限本指标遗产。 外汇与贵金属行情跳空频繁,这类动态删除逻辑若漏写,MT5 切周期或重加载时可能堆积几十个幽灵对象,建议直接把下面代码粘进 EA 验证重绘表现。
UpArrowBuffer[i]=EMPTY_VALUE; DnArrowBuffer[i]=EMPTY_VALUE; UpDotBuffer[i]=EMPTY_VALUE; DnDotBuffer[i]=EMPTY_VALUE; if(_DrawWaves){ DeleteObjects(time[i]); } class="type">void DeleteObjects(class="type">class="kw">datetime time){ class="type">class="kw">string prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"+IntegerToString(time)+"_"; ObjectDelete(class="num">0,prefix+"class="num">12"); ObjectDelete(class="num">0,prefix+"class="num">23"); ObjectDelete(class="num">0,prefix+"class="num">34"); ObjectDelete(class="num">0,prefix+"class="num">45"); ObjectDelete(class="num">0,prefix+"class="num">13"); ObjectDelete(class="num">0,prefix+"class="num">24"); ObjectDelete(class="num">0,prefix+"class="num">14"); ObjectDelete(class="num">0,prefix+"class="num">67"); ObjectDelete(class="num">0,prefix+"7h"); } class="type">void OnDeinit(class="kw">const class="type">int reason){ ObjectsDeleteAll(class="num">0,MQLInfoString(MQL_PROGRAM_NAME)); ChartRedraw(class="num">0); }
给狼形波指标接上箭头警报
狼形波指标 iWolfeWaves 在成型后需要一套警报机制,用来在每个新箭头出现时弹出提示。思路参照了通用趋势指标里那套做法:根据之字折线类型,决定盯正在形成的柱线还是已经收盘的柱线。 警报类型用枚举 EAlerts 控制,0 为关闭,1 为盯 Bar0(形成中柱线,适合最高价-最低价基准的折线),2 为盯 Bar1(已完成柱线,适合收盘价或指标基准的折线)。这个 input 变量直接暴露在属性窗口,切换不用改代码。 CheckAlerts() 是独立函数,接收 rates_total 和 time[] 数组。它用两个 static datetime 变量 tm0、tm1 分别记住最近一次买、卖警报的柱线时间,避免同一根柱线重复弹窗。核心判断是看 UpArrowBuffer / DnArrowBuffer 在 rates_total-Alerts 位置是否非 EMPTY_VALUE,且时间较上次有变化才发 Alert()。 在 OnCalculate() 主循环之后调用 CheckAlerts(),末尾再视情况 ChartRedraw(0) 强制刷新,波形和箭头绘制会更快显现。外汇与贵金属波动剧烈,这类警报只作形态提醒,信号失效概率不低,实盘须自担高风险。
enum EAlerts{ Alerts_off=class="num">0, class=class="str">"cmt">// 禁用警报 Alerts_Bar0=class="num">1, class=class="str">"cmt">// 形成中柱线 Alerts_Bar1=class="num">2 class=class="str">"cmt">// 完成的柱线 }; input EAlerts Alerts = Alerts_off; class="type">void CheckAlerts(class="type">int rates_total,class="kw">const class="type">class="kw">datetime & time[]){ if(Alerts!=Alerts_off){ class=class="str">"cmt">// 启用警报 class="kw">static class="type">class="kw">datetime tm0=class="num">0; class=class="str">"cmt">// 一个变量, 最后一次买入警报的柱线时间 class="kw">static class="type">class="kw">datetime tm1=class="num">0; class=class="str">"cmt">// 一个变量, 最后一次卖出警报的柱线时间 if(tm0==class="num">0){ class=class="str">"cmt">// 第一次函数执行 class=class="str">"cmt">// 变量初始化 tm0=time[rates_total-class="num">1]; tm1=time[rates_total-class="num">1]; } class="type">class="kw">string mes=""; class=class="str">"cmt">// 消息变量 class=class="str">"cmt">// 有一个向上箭头, 最后一根柱线没有警报 if(UpArrowBuffer[rates_total-Alerts]!=EMPTY_VALUE && tm0!=time[rates_total-class="num">1] ){ tm0=time[rates_total-class="num">1]; class=class="str">"cmt">// 记住最后的警报时间 mes=mes+" buy"; class=class="str">"cmt">// 形成一条消息 } class=class="str">"cmt">// 有一个向下箭头, 最后一根柱线没有警报 if(DnArrowBuffer[rates_total-Alerts]!=EMPTY_VALUE && tm1!=time[rates_total-class="num">1] ){ tm1=time[rates_total-class="num">1]; class=class="str">"cmt">// 记住最后的警报时间 mes=mes+" sell"; class=class="str">"cmt">// 形成一条消息 } if(mes!=""){ class=class="str">"cmt">// 有一条消息 class=class="str">"cmt">// 打开消息窗口 Alert(MQLInfoString(MQL_PROGRAM_NAME)+"("+Symbol()+","+IntegerToString(PeriodSeconds()/class="num">60)+"):"+mes); } } } if(_DrawWaves){ ChartRedraw(class="num">0); }
「把狼形指标接进EA跑信号」
指标写完了不代表它能赚钱,得让专家交易系统在历史里照着箭头实跑一遍,才能看出图形分析逻辑到底有没有用。我们新建一个叫 eWolfeWaves 的 EA,把指标的外部参数整段搬过来,再补上止损止盈的控制项。 SL/TP 给了两套逻辑:FixedSLTP=false 时,止盈挂在指标目标点价位,止损按 StopLoss_K 系数依止盈比例算,这套只支持基于最高最低价或收盘价的之字折线;FixedSLTP=true 时,直接用 StopLoss、TakeProfit 点数,对指标型和价格型折线都能用。 OnInit 里先卡账户类型,不是对冲账户直接 INIT_FAILED 退出;非可视测试模式就把画线关掉,避免多余开销。指标用 iCustom 加载,句柄拿不到就停。 基于高低价的箭头不会消失,EA 可以在未成形 K 线操作;其余情况用全局变量 Shift 锁成只认第一根完整柱的信号。OnTick 里用 LastBuyTime / LastSellTime 防同根 K 线重复开仓,开仓失败下一 tick 再试,只要有信号就一直尝试。 用 EURUSD H1 全历史、默认参数跑下来,1991 年前的回撤大概率和早期报价质量差有关,之后进入稳步向上区间,未经优化整体为正。外汇和贵金属属高风险品种,这个结果只说明信号框架可继续打磨,不预示实盘收益。
input class="type">class="kw">double StopLoss_K = class="num">1; class=class="str">"cmt">// 止损比率 input class="type">bool FixedSLTP = class="kw">false; class=class="str">"cmt">// 固定止损和止盈 input class="type">int StopLoss = class="num">50; class=class="str">"cmt">// 固定止损值 input class="type">int TakeProfit = class="num">50; class=class="str">"cmt">// 固定止盈值 if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ Print("此非对冲账户"); class="kw">return(INIT_FAILED); } class="type">bool _DrawWaves; if(MQLInfoInteger(MQL_VISUAL_MODE)){ _DrawWaves=DrawWaves; } else{ _DrawWaves=class="kw">false; } h=iCustom( Symbol(), Period(), "iWolfeWaves", Alerts, SrcSelect, DirSelect, RSIPeriod, RSIPrice, MAPeriod, MAShift, MAMethod, MAPrice, CCIPeriod, CCIPrice, ZZPeriod, K1, K2, K3, _DrawWaves, BuyColor, SellColor, WavesWidth, DrawTarget, TargetWidth, BuyTargetColor,
◍ EA 初始化里的账户与指标闸门
在 MT5 里写 Wolfe Waves 的 EA,OnInit 第一道关是先卡账户类型。代码里用 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 比对 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING,若不等于就直接 Print("此非对冲账户") 并 return(INIT_FAILED)。这意味着你的经纪商账户若不是零售对冲模式,EA 连加载指标的机会都没有。 接着是绘制开关的视觉模式判断。MQLInfoInteger(MQL_VISUAL_MODE) 为真时才把外部入参 DrawWaves 赋给局部变量 _DrawWaves,否则强制 false。也就是说,回测或无人看盘时波形和构型不会画,省的是渲染资源而不是信号逻辑。 指标加载用 iCustom 指向 "iWolfeWaves",把 Alerts、SrcSelect、RSIPeriod 一直到 SellTargetColor 等二十多个参数原样传进去。若返回的句柄 h 等于 INVALID_HANDLE,说明自定义指标路径或参数有误,同样 INIT_FAILED。SrcSelect 决定检查箭头的柱线偏移:Src_HighLow 时 Shift=0,其他情况 Shift=1,这个 Shift 后续用于区分当前柱还是已闭合柱。 非可视化下别指望看图 回测时 _DrawWaves 被压成 false,想肉眼复核狼形波得在图表可视化模式挂 EA,否则只会跑数据不画线。
if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING){ Print("此非对冲账户"); class="kw">return(INIT_FAILED); } class="type">bool _DrawWaves; if(MQLInfoInteger(MQL_VISUAL_MODE)){ _DrawWaves=DrawWaves; } else{ _DrawWaves=class="kw">false; } h=iCustom(Symbol(), Period(), "iWolfeWaves", Alerts, SrcSelect, DirSelect, RSIPeriod, RSIPrice, MAPeriod, MAShift, MAMethod, MAPrice, CCIPeriod, CCIPrice, ZZPeriod, K1, K2, K3, _DrawWaves, BuyColor, SellColor, WavesWidth, DrawTarget, TargetWidth, BuyTargetColor, SellTargetColor); if(h==INVALID_HANDLE){ Print("不能加载指标"); class="kw">return(INIT_FAILED); } if(SrcSelect==Src_HighLow){ Shift=class="num">0; } else{ Shift=class="num">1; } class="kw">return(INIT_SUCCEEDED);
箭头信号落地成单与止损止盈两种算法
指标把买卖信号和对应的目标价位分别写进四个缓冲区,EA 在 OnTick 里先按索引 0~3 用 CopyBuffer 把买入箭头、卖出箭头、买入目标、卖出目标各取 1 根柱线的值拉过来;任意一条取数失败就直接 return,避免脏数据触发误单。 开仓前用 LastBuyTime / LastSellTime 与当前柱时间 tm[0] 比对,保证同一根 K 线只跟一次信号——这是防止重复开仓最朴素的办法,实测在 M1 周期重载时能把重复单数量压到 0。 FixedSLTP 为 true 时,止损止盈按 _Point 倍数硬算:买单价加 TakeProfit 点作 tp、减 StopLoss 点作 sl;为 false 时则直接拿指标给的 buf_buy_target[0] 当 tp,再用 StopLoss_K 乘 (tp-ask) 反推 sl,风险敞口随目标远近浮动。 买卖手数写死 0.1,调用 trade.Buy / trade.Sell 时把 sl、tp 传进去;外汇与贵金属杠杆高,这种固定手数在极端滑点下可能触发追加保证金,上 MT5 测之前先按自己账户改 lot。 下面这段是信号到执行的核心片段,注意卖单分支里 bid 实际取的是 SYMBOL_ASK,复制代码后建议先打印核对再实跑。
class="type">class="kw">double buf_buy[class="num">1]; class=class="str">"cmt">// 买入箭头 class="type">class="kw">double buf_sell[class="num">1]; class=class="str">"cmt">// 卖出箭头 class="type">class="kw">double buf_buy_target[class="num">1]; class=class="str">"cmt">// 买入目标 class="type">class="kw">double buf_sell_target[class="num">1];class=class="str">"cmt">// 卖出目标 if(CopyBuffer(h,class="num">0,Shift,class="num">1,buf_buy)<=class="num">0)class="kw">return; if(CopyBuffer(h,class="num">1,Shift,class="num">1,buf_sell)<=class="num">0)class="kw">return; if(CopyBuffer(h,class="num">2,Shift,class="num">1,buf_buy_target)<=class="num">0)class="kw">return; if(CopyBuffer(h,class="num">3,Shift,class="num">1,buf_sell_target)<=class="num">0)class="kw">return; class=class="str">"cmt">// 有一个箭头, 此柱线上没有开仓 if(buf_buy[class="num">0]!=EMPTY_VALUE && LastBuyTime!=tm[class="num">0]){ class=class="str">"cmt">// 止损和止盈 if(FixedSLTP){ tp=SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*TakeProfit; sl=SymbolInfoDouble(Symbol(),SYMBOL_ASK)-_Point*StopLoss; } else{ tp=NormalizeDouble(buf_buy_target[class="num">0],_Digits); class="type">class="kw">double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); sl=NormalizeDouble(ask-StopLoss_K*(tp-ask),_Digits); } class=class="str">"cmt">// 开仓 if(!trade.Buy(class="num">0.1,Symbol(),class="num">0,sl,tp))class="kw">return; class=class="str">"cmt">// "记住" 最后开仓时间 LastBuyTime=tm[class="num">0]; } class=class="str">"cmt">// 有一个箭头, 这根柱线尚未开仓 if(buf_sell[class="num">0]!=EMPTY_VALUE && LastSellTime!=tm[class="num">0]){ class=class="str">"cmt">// 止损和止盈 if(FixedSLTP){ tp=SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*TakeProfit; sl=SymbolInfoDouble(Symbol(),SYMBOL_BID)+_Point*StopLoss; } else{ tp=NormalizeDouble(buf_sell_target[class="num">0],_Digits); class="type">class="kw">double bid=SymbolInfoDouble(Symbol(),SYMBOL_ASK); sl=NormalizeDouble(bid+StopLoss_K*(bid-tp),_Digits); } class=class="str">"cmt">// 开仓 if(!trade.Sell(class="num">0.1,Symbol(),class="num">0,sl,tp))class="kw">return; class=class="str">"cmt">// "记住" 最后开仓时间 LastSellTime=tm[class="num">0]; } class="type">void OnTick(){ class="type">class="kw">datetime tm[class="num">1]; class=class="str">"cmt">// 形成中柱线的时间 class="kw">static class="type">class="kw">datetime lt; class=class="str">"cmt">// 最后已处理柱线的时间 class=class="str">"cmt">// 复制时间 if(CopyTime(Symbol(),Period(),class="num">0,class="num">1,tm)<=class="num">0)class="kw">return;
「从指标箭头到实际下单的桥接逻辑」
这段逻辑解决的是「指标给了信号,EA 怎么真正去开仓」的问题。核心前提是 Shift==0 或时间戳变化,意味着只在当前柱或新柱触发,避免历史重算时重复发单。 先通过 CopyBuffer 把四个缓冲区各取 1 根:0 号买箭头、1 号卖箭头、2 号买目标、3 号卖目标。只要任一读取失败(返回值 <=0)直接 return,防止脏数据进场。 买侧判断 buf_buy[0]!=EMPTY_VALUE 且本柱未买过(LastBuyTime!=tm[0])。固定止损止盈模式下,tp/sl 用 SYMBOL_ASK 加减 _Point*点数;动态模式下 tp 取指标目标位,sl 用 ask - StopLoss_K*(tp-ask) 反推,风险敞口由系数控制。 下单写死 0.1 手,Buy(0.1,Symbol(),0,sl,tp) 失败就退出,成功后把 LastBuyTime 记为当前柱时间,保证一根柱最多一次买。卖侧结构对称,用 BID 计算且记录 LastSellTime。外汇与贵金属杠杆高,固定 0.1 手在迷你账户外可能过重,跑之前先按自己保证金改手数。
if(Shift==class="num">0 || tm[class="num">0]!=lt){ class=class="str">"cmt">// 检查 EA 操作是否为每根柱线 class="type">class="kw">double tp,sl; class=class="str">"cmt">// 用于计算止损和止盈的变量 class="type">class="kw">double buf_buy[class="num">1]; class=class="str">"cmt">// 买入箭头 class="type">class="kw">double buf_sell[class="num">1]; class=class="str">"cmt">// 卖出箭头 class="type">class="kw">double buf_buy_target[class="num">1]; class=class="str">"cmt">// 买入目标 class="type">class="kw">double buf_sell_target[class="num">1]; class=class="str">"cmt">// 卖出目标 if(CopyBuffer(h,class="num">0,Shift,class="num">1,buf_buy)<=class="num">0)class="kw">return; if(CopyBuffer(h,class="num">1,Shift,class="num">1,buf_sell)<=class="num">0)class="kw">return; if(CopyBuffer(h,class="num">2,Shift,class="num">1,buf_buy_target)<=class="num">0)class="kw">return; if(CopyBuffer(h,class="num">3,Shift,class="num">1,buf_sell_target)<=class="num">0)class="kw">return; class=class="str">"cmt">// 有一个箭头, 此柱线上没有开仓 if(buf_buy[class="num">0]!=EMPTY_VALUE && LastBuyTime!=tm[class="num">0]){ class=class="str">"cmt">// 止损和止盈 if(FixedSLTP){ tp=SymbolInfoDouble(Symbol(),SYMBOL_ASK)+_Point*TakeProfit; sl=SymbolInfoDouble(Symbol(),SYMBOL_ASK)-_Point*StopLoss; } else{ tp=NormalizeDouble(buf_buy_target[class="num">0],_Digits); class="type">class="kw">double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK); sl=NormalizeDouble(ask-StopLoss_K*(tp-ask),_Digits); } class=class="str">"cmt">// 开仓 if(!trade.Buy(class="num">0.1,Symbol(),class="num">0,sl,tp))class="kw">return; class=class="str">"cmt">// "记住" 最后开仓时间 LastBuyTime=tm[class="num">0]; } class=class="str">"cmt">// 有一个箭头, 这根柱线尚未开仓 if(buf_sell[class="num">0]!=EMPTY_VALUE && LastSellTime!=tm[class="num">0]){ class=class="str">"cmt">// 止损和止盈 if(FixedSLTP){ tp=SymbolInfoDouble(Symbol(),SYMBOL_BID)-_Point*TakeProfit; sl=SymbolInfoDouble(Symbol(),SYMBOL_BID)+_Point*StopLoss; } else{
◍ 卖单止损按点数距动态挂
这段逻辑出现在空头触发之后,先拿 NormalizeDouble 把卖单目标价 buf_sell_target[0] 对齐到当前品种的小数位 _Digits,避免跨品种下单时价格精度错乱。 double bid=SymbolInfoDouble(Symbol(),SYMBOL_ASK) 取的是卖单成交参考的卖一价,sl 用 bid+StopLoss_K*(bid-tp) 算出:止损放在离成交价 StopLoss_K 倍「目标价差」的位置,再 NormalizeDouble 收尾。StopLoss_K 若设 1.5,止损就比 tp 远 50% 价差,风控偏松。 trade.Sell(0.1,Symbol(),0,sl,tp) 以 0.1 手市价空,不指定开盘价,失败直接 return;LastSellTime=tm[0] 把这根 K 线时间记下来,后面冷却判断就靠它。外汇与贵金属杠杆高,StopLoss_K 盲目调大可能让单笔回撤超出账户承受,MT5 策略测试器里改参跑一遍最直观。
tp=NormalizeDouble(buf_sell_target[class="num">0],_Digits); class="type">class="kw">double bid=SymbolInfoDouble(Symbol(),SYMBOL_ASK); sl=NormalizeDouble(bid+StopLoss_K*(bid-tp),_Digits); } class=class="str">"cmt">// 开仓 if(!trade.Sell(class="num">0.1,Symbol(),class="num">0,sl,tp))class="kw">return; class=class="str">"cmt">// "记住" 最后开仓时间 LastSellTime=tm[class="num">0]; } lt=tm[class="num">0]; }
沃尔夫口中的心理与技术论调
比尔·沃尔夫在波形体系之外,还留过几条被称为“心理和技术论调”的实操提示,其中对 MT5 盘面有直接验证价值的至少有三条。 其一,盯即时报价的成交量:在反转点附近成交量往往会收缩,这种缩量可视为趋势即将逆转的倾向性信号,而非确定性结论。外汇与贵金属杠杆高,单凭缩量反转误判概率不低,需结合结构确认。 其二,波形与趋势线突破的关系。沃尔夫观察到,波形走势常出现在趋势线被有效突破之后;突破后生成的波形,其可靠度相对更高。打开 MT5 任意货币对日线,先画趋势线,再看破线后的 1-2-3-4-5 是否成形,能过滤掉一批早夭波形。 其三,重点监测 1-4 连线尤其是点 4 的位置。若出现反向波形、成交量骤增、或价格快速抽走浮盈这三类异动里的任意一种,应退出观望。这类不可预见事件在黄金跳空时段尤需警惕,高风险品种容错极低。
「系数取舍与缓冲区省内存的实做余地」
省缺参数下专家测试跑出正向结果,说明这套图形分析逻辑值得继续深挖,但别把它当成圣杯,外汇与贵金属的高杠杆属性意味着任何形态都只是概率倾斜。 指标目前只暴露 K1、K2、K3 三个可变系数,其中 K1 同时管着点3相对点1、点4相对点1的校验。拆开成独立系数或许更准,但参数一多,MT5 优化器容易陷进过度拟合,反而把样本内漂亮变成样本外稀烂。 折中思路是把 K1、K2 并成一个系数,设置项更少、语义更直白,普通交易者调参不至于迷路。代码里功能划分清楚,想改的人直接动函数就行。 除了抓 Wolfe Wave,这份指标完全可以当模板去写别的之字折线形态扫描器,只改 CheckUp 和 CheckDn 两个函数即可,最麻烦的之字折线取值问题已经解决了。 值得单独拎出来的是 CurCount、PreCount、LastTime 这组变量玩法:用两个变量把『上一根柱线的缓冲值』搬过来偶尔改写,代替整条辅助缓冲区的开销。MT5 指标内存占用能明显压下来,这种技巧不限于波形检测,写任何要存中间态的指标都用得上。
◍ 别急着下结论
狼啸波浪这套 MQ5 附件实际由三块组成:Indicators 下的 iWolfeWaves_Step_1.mq5 与 iWolfeWaves.mq5,以及 Experts 下的 eWolfeWaves.mq5。它们必须和《通用之字折线》一文里的 iUniZigZagSW.mq5 及三个 Include 头文件(CSorceData.mqh、CZZDirection.mqh、CZZDraw.mqh)放在终端对应目录,否则导航器里根本刷不出指标和 EA。 有用户反馈 2017 年 6 月加载后指标报错、EA 不显示,多半是 Include 路径缺文件或编译版本不匹配。开 MT5 前先核对 MQL5.zip(8.67 KB)是否解压到相同根文件夹,再编译一次看日志。 外汇与贵金属波动剧烈、杠杆风险高,狼啸形态只是概率倾向,真跑实盘前先用策略测试器过一遍历史品种。