根据特定的价格变化自动侦测极值点·进阶篇
用变化范围剔除噪音极值点
柱体的大小和极值参数随时段剧烈波动,趋势、时段甚至一天内的交易活跃度都会改变顶底的出现概率。分形类工具能在任意周期标出极值,但日内交易里距均线仅 2 点的毛刺底,和长线里小于 20 点的回调,本质都是噪音。 引入“变化范围”作为过滤阈值:它规定相邻顶部与底部之间的最小价格距离。以均线为参考会受周期选择干扰,不如直接要求“价格从顶跌到背后再回来”的净落差不小于该范围。实测同一段图表,变化范围设 60 点值只抓出 4 个极值,降到 30 点值同区间冒出 10 个——范围越小,伪极值越多。 搜索从图表末端已确认的极值(点 1)向前迭代:先找底再找顶,找不到后续顶就回退重定底的位置。这种迭代法计算重,建议只抓最近几个关键极值;要批量找多点直接用 ZigZag 更划算。外汇与贵金属波动随机,该算法仅降低噪音,不预示方向。 下面这段 MQL5 用三层嵌套循环定 4 个极值,核心入参是搜索柱数 bars 与最小落差 delta_points。注意 first_extrem 只是放宽首极值搜索比例,reload_time 控制重算间隔。
input class="type">int bars= class="num">500 ; class=class="str">"cmt">// 搜索极值点的范围 input class="type">class="kw">double delta_points= class="num">160 ; // 定义的最小的顶部和底部距离点数的变化范围 input class="type">class="kw">double first_extrem= class="num">0.9 ; // 额外的用语搜索第一个极值的比例 input class="type">class="kw">double reload_time= class="num">5 ; // 时间间隔,重新计算指标值的秒数 class="type">class="kw">double High [], Low []; class="type">class="kw">datetime Time []; ArraySetAsSeries( Low , true ); class="type">int copied1= CopyLow( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">2 , Low ); ArraySetAsSeries( High , true ); class="type">int copied2= CopyHigh( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">2 , High ); ArraySetAsSeries( Time , true ); class="type">int copied3= CopyTime( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">2 , Time ); class="type">class="kw">double delta=delta_points* Point(); class=class="str">"cmt">// 极值点之间差距的变化范围 class="type">int j,k,l; class="type">int j2,k2,l2; class="type">class="kw">double j1,k1,l1; class="type">int min[ class="num">6 ]; // 定义底部的数组,数值对应着侦测出的极值点的柱的索引 class="type">int max[ class="num">6 ]; // 定义顶部的数组,数值对应着侦测出的极值点的柱的索引 class="type">int mag1=bars; class="type">int mag2=bars; class="type">int mag3=bars; class="type">int mag4=bars; j1= SymbolInfoDouble( Symbol(), SYMBOL_BID )+( class="num">1 -first_extrem)*delta_points* Point(); // 当搜索第一个极值点时,另外的比例变量定义了最小价格,第一个底部就是位于低于它的位置 j2= class="num">0 ; // 在第一个迭代中,搜索是从最新的历史柱上开始进行的 for (j= class="num">0 ;j<= class="num">15 ;j++) // 循环定义第一个底部 - min[class="num">1] { min[ class="num">1 ]=minimum(j2,bars,j1); //在指定的时间段中定义最近的底部 j2=min[ class="num">1 ]+ class="num">1 ; // 在下一次迭代中,搜索是从已经侦测出的底部 min[class="num">1] 开始进行的 j1= Low [min[ class="num">1 ]]+delta; //在随后的迭代中找到的底部的最低价应该低于当前迭代中找到的底部的最低价 k1= Low [min[ class="num">1 ]]; //当搜索下一个极值点时找到的最高价应该低于底部的最低价,这样才应该是顶部。 k2=min[ class="num">1 ]; //搜索位于底部之后的顶部,从侦测的底部 min[class="num">1]开始进行 for (k= class="num">0 ;k<= class="num">12 ;k++) // 循环定义第一个顶部 - max[class="num">1] { max[ class="num">1 ]=maximum(k2,bars,k1); //--- 定义指定时间段内最近的顶部 k1= High [max[ class="num">1 ]]-delta; //下一个迭代的最高价应该超过当前迭代侦测出的顶部的最高价 k2=max[ class="num">1 ]+ class="num">1 ; // 在下一次迭代中,搜索时从已经侦测到的顶部 max[class="num">1] 开始进行的 l1= High [max[ class="num">1 ]]; //当搜索下一个底部最低价时使用的极值点的最高价,底部应该低于这个价格 l2=max[ class="num">1 ]; // 搜索顶部之后的底部,应该从已侦测的顶部 max[class="num">1] 开始进行 for (l= class="num">0 ;l<= class="num">10 ;l++) // 循环定义第二个底部 - min[class="num">2] 以及第二个顶部 max[class="num">2] { min[ class="num">2 ]=minimum(l2,bars,l1); //---在指定的时间段内定义最近的底部 l1= Low [min[ class="num">2 ]]+delta; //用于在随后迭代中侦测的底部的最低价应该低于当前迭代中底部的最低价 l2=min[ class="num">2 ]+ class="num">1 ; // 在下一次迭代中,搜索是从已经侦测到的底部 min[class="num">2] 开始进行的 max[ class="num">2 ]=maximum(min[ class="num">2 ],bars, Low [min[ class="num">2 ]]); //在指定的时间间隔内定义最近的顶部 if (max[ class="num">1 ]>min[ class="num">1 ] && min[ class="num">1 ]> class="num">0 && min[ class="num">2 ]>max[ class="num">1 ] && min[ class="num">2 ]<max[ class="num">2 ] && max[ class="num">2 ]<mag4) //排除掉冲突的极值和特殊状况 { mag1=min[ class="num">1 ]; //在每次迭代中,侦测到的极值位置在条件符合时要保存 mag2=max[ class="num">1 ]; mag3=min[ class="num">2 ]; mag4=max[ class="num">2 ]; } } } } min[ class="num">1 ]=mag1; // 定义了极值点,否则所有变量设为 &class="macro">#x27;bars&class="macro">#x27; 的数值 max[ class="num">1 ]=mag2; min[ class="num">2 ]=mag3; max[ class="num">2 ]=mag4; class="type">int minimum( class="type">int a, class="type">int b, class="type">class="kw">double price0) class=class="str">"cmt">//本函数在指定的时间段内定义最近的底部. 底部的位置低于 price0 一定的距离,大于变化范围 { class="type">class="kw">double High [], Low []; ArraySetAsSeries( Low , true ); class="type">int copied4= CopyLow( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">2 , Low ); class="type">int i,e; e=bars; class="type">class="kw">double pr=price0-delta_points* Point(); // 价格应该低于算上变化范围的底部之下 for (i=a;i<=b;i++) // 在由a和b参数指定的范围内搜索底部 { if ( Low [i]<pr && Low [i]< Low [i+ class="num">1 ]) // 定义最近的底部,之后价格开始上升 { e=i; break ; } } class="kw">return (e); } class="type">int maximum( class="type">int a, class="type">int b, class="type">class="kw">double price1) //--- 本函数在指定的时间段内定义最近的顶部. 顶部要高于 price1 比变化范围大的距离 { class="type">class="kw">double High [], Low []; ArraySetAsSeries( High , true ); class="type">int copied5= CopyHigh( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">2 , High ); class="type">int i,e; e=bars; class="type">class="kw">double pr1=price1+delta_points* Point(); // 价格应该高于顶部加上变化范围的距离 for (i=a;i<=b;i++) // 在由参数a和b指定的范围内搜索顶部 { if ( High [i]>pr1 && High [i]> High [i+ class="num">1 ]) // 定义最近的顶部,之后价格开始下跌 { e=i; break ; } } class="kw">return (e); } min[ class="num">1 ]=check_min(min[ class="num">1 ],max[ class="num">1 ]); class=class="str">"cmt">// 验证和修改在指定时间段中的第一个底部的位置 max[ class="num">1 ]=check_max(max[ class="num">1 ],min[ class="num">2 ]); // 验证和修改在指定时间段中的第一个顶部的位置 min[ class="num">2 ]=check_min(min[ class="num">2 ],max[ class="num">2 ]); // 验证和修改在指定时间段中的第二个底部的位置 class="type">int check_min( class="type">int a, class="type">int b) class=class="str">"cmt">// 用于验证和修改在指定时间段内的底部位置 { class="type">class="kw">double High [], Low []; ArraySetAsSeries( Low , true ); class="type">int copied6= CopyLow( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">1 , Low ); class="type">int i,c; c=a; for (i=a+ class="num">1 ;i<b;i++) // 当从底部开始搜索时,所有由范围指定的柱都要验证 { if ( Low [i]< Low [a] && Low [i]< Low [c]) // 如果找到了更低的底部 c=i; // 重新定义底部的位置 } class="kw">return (c); }
◍ 极值点验证与箭头标注的实现细节
极值点确认后,先用 check_max 一类函数在已识别的 4 个极值点中只复核前三个的位置关系,避免远端噪点干扰。验证函数以当前极值点位置为起点、向后一个极值点为终点扫描,若区间内出现更高 High 则重定义顶部索引,最终返回可信的顶部位置。 第一个极值点的捕获引入了额外搜索比例 first_extrem,它本质是变化区间的分数部分。实测中取值 0.8…0.9 能更准地锁定极值但带轻微延迟;取 0.1…0.25 延迟最小却明显牺牲准确度,该值须按你所用策略的持仓周期来定。 [CODE] int check_max( int a, int b) //--- 本函数在指定的时间间隔内验证和修改顶部的位置 { double High [], Low []; ArraySetAsSeries ( High , true ); // 让 High 数组按时间倒序排列,索引0为最新柱 int copied7= CopyHigh ( Symbol (), 0 , 0 ,bars+ 1 , High ); // 拷贝当前品种当前周期 bars+1 根最高价到 High int i,d; d=a; // 先假定 a 为顶部位置 for (i=(a+ 1 );i<b;i++) // 从 a 的下一根扫到 b 之前 { if ( High [i]> High [a] && High [i]> High [d]) // 若发现更高的高点 d=i; // 把顶部位置重定义为 i } return (d); // 返回最终确认的顶部索引 } [/CODE] 标注阶段用 OBJ_ARROW_UP / OBJ_ARROW_DOWN 画箭头:若底部更近(min[1]<Max[1])则先删旧标签 id_1…id_6,再在 Low[min[1]] 绑箭头标第一个底、High[max[1]] 绑箭头标第一个顶、Low[min[2]] 标第二个底;顶部更近则对称处理。若 min[1]==Max[1] 直接 Alert 提示在 bars 范围内未找到极值点。 图 10 的对照很说明问题:delta_points=120 点值时极值互相贴得很近,范围大小已不太关键;调到 160 点值以上极值间距明显拉开。平盘里建议选小变化范围,自动抓小波动顶底、跳过时间间隔过大的极值。外汇与贵金属杠杆高,参数误设可能放大滑点与错标风险。 [CODE] if (min[ 1 ]<Max[ 1 ]) // 如果底部距离更近,显示它的位置以及相关极值的数值 { ObjectDelete ( 0 , "id_1" ); // 删除前一阶段生成的标签 ObjectDelete ( 0 , "id_2" ); ObjectDelete ( 0 , "id_3" ); ObjectDelete ( 0 , "id_4" ); ObjectDelete ( 0 , "id_5" ); ObjectDelete ( 0 , "id_6" ); ObjectCreate ( 0 , "id_1" , OBJ_ARROW_UP , 0 , Time [min[ 1 ]], Low [min[ 1 ]]); // 突出显示第一个底部 ObjectSetInteger ( 0 , "id_1" , OBJPROP_ANCHOR , ANCHOR_TOP ); // 箭头锚定在底价上方 ObjectCreate ( 0 , "id_2" , OBJ_ARROW_DOWN , 0 , Time [max[ 1 ]], High [max[ 1 ]]); // 突出显示第一个顶部 ObjectSetInteger ( 0 , "id_2" , OBJPROP_ANCHOR , ANCHOR_BOTTOM ); // 箭头锚定在顶价下方 ObjectCreate ( 0 , "id_3" , OBJ_ARROW_UP , 0 , Time [min[ 2 ]], Low [min[ 2 ]]); // 突出显示第二个底部 ObjectSetInteger ( 0 , "id_3" , OBJPROP_ANCHOR , ANCHOR_TOP ); } if (min[ 1 ]>Max[ 1 ]) // 如果顶部位置更近,显示它的位置和相关极值的数值 { ObjectDelete ( 0 , "id_1" ); // 删除前一阶段的标签 ObjectDelete ( 0 , "id_2" ); ObjectDelete ( 0 , "id_3" ); ObjectDelete ( 0 , "id_4" ); ObjectDelete ( 0 , "id_5" ); ObjectDelete ( 0 , "id_6" ); ObjectCreate ( 0 , "id_4" , OBJ_ARROW_DOWN , 0 , Time [Max[ 1 ]], High [Max[ 1 ]]); // 定义第一个顶部 ObjectSetInteger ( 0 , "id_4" , OBJPROP_ANCHOR , ANCHOR_BOTTOM ); ObjectCreate ( 0 , "id_5" , OBJ_ARROW_UP , 0 , Time [Min[ 1 ]], Low [Min[ 1 ]]); // 突出显示第一个底部 ObjectSetInteger ( 0 , "id_5" , OBJPROP_ANCHOR , ANCHOR_TOP ); ObjectCreate ( 0 , "id_6" , OBJ_ARROW_DOWN , 0 , Time [Max[ 2 ]], High [Max[ 2 ]]); // 定义第二个顶部 ObjectSetInteger ( 0 , "id_6" , OBJPROP_ANCHOR , ANCHOR_BOTTOM ); } if (min[ 1 ]==Max[ 1 ]) Alert ( "在指定的范围, " ,bars, " 没有找到极值点" ); // 如果没有找到极值点,显示对应消息 [/CODE] EA 化时把上述算法接进 MACD 背离逻辑:价格新高而 MACD 顶更低倾向卖出,价格新低而 MACD 底更高倾向买入。输入参数里 bars=500 管搜索范围、delta_points=160 管最小波动跨度、first_extrem=0.9 管首极值比例、macd_t=0.00002 管柱图最小背离,guard_points=30 是止损偏移点,show_info=1 才在图上吐极值数据。打开 MT5 把这段 input 块拷进 EA,先跑 show_info=1 看箭头落点是否和肉眼顶底一致。
class="type">int check_max( class="type">int a, class="type">int b) class=class="str">"cmt">//--- 本函数在指定的时间间隔内验证和修改顶部的位置 { class="type">class="kw">double High [], Low []; ArraySetAsSeries( High , true ); class="type">int copied7= CopyHigh( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">1 , High ); class="type">int i,d; d=a; for (i=(a+ class="num">1 );i<b;i++) { if ( High [i]> High [a] && High [i]> High [d]) d=i; } class="kw">return (d); } if (min[ class="num">1 ]<Max[ class="num">1 ]) { ObjectDelete( class="num">0 , "id_1" ); ObjectDelete( class="num">0 , "id_2" ); ObjectDelete( class="num">0 , "id_3" ); ObjectDelete( class="num">0 , "id_4" ); ObjectDelete( class="num">0 , "id_5" ); ObjectDelete( class="num">0 , "id_6" ); ObjectCreate( class="num">0 , "id_1" , OBJ_ARROW_UP , class="num">0 , Time [min[ class="num">1 ]], Low [min[ class="num">1 ]]); ObjectSetInteger( class="num">0 , "id_1" , OBJPROP_ANCHOR , ANCHOR_TOP ); ObjectCreate( class="num">0 , "id_2" , OBJ_ARROW_DOWN , class="num">0 , Time [max[ class="num">1 ]], High [max[ class="num">1 ]]); ObjectSetInteger( class="num">0 , "id_2" , OBJPROP_ANCHOR , ANCHOR_BOTTOM ); ObjectCreate( class="num">0 , "id_3" , OBJ_ARROW_UP , class="num">0 , Time [min[ class="num">2 ]], Low [min[ class="num">2 ]]); ObjectSetInteger( class="num">0 , "id_3" , OBJPROP_ANCHOR , ANCHOR_TOP ); } if (min[ class="num">1 ]>Max[ class="num">1 ]) { ObjectDelete( class="num">0 , "id_1" ); ObjectDelete( class="num">0 , "id_2" ); ObjectDelete( class="num">0 , "id_3" ); ObjectDelete( class="num">0 , "id_4" ); ObjectDelete( class="num">0 , "id_5" ); ObjectDelete( class="num">0 , "id_6" ); ObjectCreate( class="num">0 , "id_4" , OBJ_ARROW_DOWN , class="num">0 , Time [Max[ class="num">1 ]], High [Max[ class="num">1 ]]); ObjectSetInteger( class="num">0 , "id_4" , OBJPROP_ANCHOR , ANCHOR_BOTTOM ); ObjectCreate( class="num">0 , "id_5" , OBJ_ARROW_UP , class="num">0 , Time [Min[ class="num">1 ]], Low [Min[ class="num">1 ]]); ObjectSetInteger( class="num">0 , "id_5" , OBJPROP_ANCHOR , ANCHOR_TOP ); ObjectCreate( class="num">0 , "id_6" , OBJ_ARROW_DOWN , class="num">0 , Time [Max[ class="num">2 ]], High [Max[ class="num">2 ]]); ObjectSetInteger( class="num">0 , "id_6" , OBJPROP_ANCHOR , ANCHOR_BOTTOM ); } if (min[ class="num">1 ]==Max[ class="num">1 ]) Alert( "在指定的范围, " ,bars, " 没有找到极值点" );
「双路径极值扫描与背离开仓判定」
这段逻辑在 OnTick 里用两套独立循环去抓最近的高低点结构:一套假设首现极值偏底部(min/max 数组),另一套假设首现极值偏顶部(Min/Max 数组),各自最多向前扫 15+12+10 层嵌套,覆盖约 37 根候选柱的极值链。 [CODE]void OnTick () { Sleep ( 1000 *time); // 引入时间延迟 double High [], Low []; ArraySetAsSeries ( Low , true ); int copied1= CopyLow ( Symbol (), 0 , 0 ,bars+ 2 , Low ); ArraySetAsSeries ( High , true ); int copied2= CopyHigh ( Symbol (), 0 , 0 ,bars+ 2 , High ); ArraySetAsSeries ( Time , true ); int copied3= CopyTime ( Symbol (), 0 , 0 ,bars+ 2 , Time ); MqlTick last_tick; double Bid =last_tick.bid; double Ask =last_tick.ask; double delta=delta_points* Point (); // 变化范围的绝对值 double trendd=trend* Point (); // 最近的两个顶部/底部偏差的绝对值 double guard=guard_points* Point (); // 止损偏移的绝对值 int j,k,l; int j2,k2,l2; double j1,k1,l1; int min[ 6 ]; // 如果第一个侦测到的极值是底部,定义底部的数组,该数值对应着侦测到的极值点的柱的索引 int max[ 6 ]; // 如果第一个侦测到的极值是底部,定义顶部的数组,该数值对应着侦测到的极值点的柱的索引 int Min[ 6 ]; // 如果第一个侦测到的极值是顶部,定义底部的数组,该数值对应着侦测到的极值点的柱的索引 int Max[ 6 ]; // 如果第一个侦测到的极值是底部,定义顶部的数组,该数值对应着侦测到的极值点的柱的索引 int mag1=bars; int mag2=bars; int mag3=bars; int mag4=bars; j1= SymbolInfoDouble ( Symbol (), SYMBOL_BID )+( 1 -first_extrem)*delta_points* Point (); // 当搜索第一个极值点时,额外比例定义了第一个底部的最小价格位置 j2= 0 ; // 在第一次迭代中,搜索是从最后面的历史柱开始进行的 for (j= 0 ;j<= 15 ;j++) // 循环定义第一个底部 - min[1] { min[ 1 ]=minimum(j2,bars,j1); //在指定的时间间隔中定义最近的底部 j2=min[ 1 ]+ 1 ; //在下一次迭代中,搜索时从已经侦测到的底部 min[1] 开始进行的 j1= Low [min[ 1 ]]+delta; //--- 随后的迭代中所侦测的底部的最低价应该比当前迭代中找到的底部最低价更低 k1= Low [min[ 1 ]]; //搜索到的底部的最低价,下面搜索的顶部的极值点的最高价格应该在此之上 k2=min[ 1 ]; // 搜索底部之后的顶部,从侦测到的底部 min[1] 开始进行 for (k= 0 ;k<= 12 ;k++) // 循环定义第一个顶部 - max[1] { max[ 1 ]=maximum(k2,bars,k1); //--- 在指定的时间间隔内定义最近的顶部 k1= High [max[ 1 ]]-delta; //--- 下次迭代的最高价应该高于当前迭代中侦测到的顶部的最高价 k2=max[ 1 ]+ 1 ; // 在下一次迭代中,搜索是从已经侦测到的顶部 max[1] 开始的 l1= High [max[ 1 ]]; //--- 极值点的最高价,当搜索下个底部的最低价时,位置应该低于这个值 l2=max[ 1 ]; // 搜索底部应该是从已经侦测到的顶部 max[1] 开始的 for (l= 0 ;l<= 10 ;l++) // 循环定义第二个底部 - min[2] 以及第二个顶部 max[2] { min[ 2 ]=minimum(l2,bars,l1); //--- 在指定时间段之内定义最近的底部 l1= Low [min[ 2 ]]+delta; //在随后的迭代中用于搜索底部的最低价应该比当前迭代中找到的底部的最低价要低 l2=min[ 2 ]+ 1 ; //在下一次迭代中,搜索是从已经侦测到的底部 min[2] 开始的 max[ 2 ]=maximum(min[ 2 ],bars, Low [min[ 2 ]]); //在指定的时间间隔内定义最近的顶部 if (max[ 1 ]>min[ 1 ] && min[ 1 ]> 0 && min[ 2 ]>max[ 1 ] && min[ 2 ]<max[ 2 ] && max[ 2 ]<mag4) //--- 排除掉冲突的极值和特殊状况 { mag1=min[ 1 ]; // 在每次迭代中,所侦测到的极值在条件满足时要保存位置 mag2=max[ 1 ]; mag3=min[ 2 ]; mag4=max[ 2 ]; } } } } //--- 定义了极值点,否则对所有变量赋值为 'bars' 的值 min[ 1 ]=mag1; max[ 1 ]=mag2; min[ 2 ]=mag3; max[ 2 ]=mag4; //--- 在指定的时间间隔中验证和修改极值点的位置 min[ 1 ]=check_min(min[ 1 ],max[ 1 ]); max[ 1 ]=check_max(max[ 1 ],min[ 2 ]); min[ 2 ]=check_min(min[ 2 ],max[ 2 ]); //--------------------------------------------------------------------------------------------------------------- mag1=bars; mag2=bars; mag3=bars; mag4=bars; j1= SymbolInfoDouble ( Symbol (), SYMBOL_BID )-( 1 -first_extrem)*delta_points* Point (); // 当搜索第一个极值点时,额外比例定义了最高价格,第一个顶部应该高于它 j2= 0 ; // 在第一次迭代中,搜索是从最后的历史柱开始的 for (j= 0 ;j<= 15 ;j++) // 循环定义第一个顶部 - Max[1] { Max[ 1 ]=maximum(j2,bars,j1); //在指定的时间间隔中定义最近的顶部 j1= High [Max[ 1 ]]-delta; //下次迭代的最高价应该高于当前迭代中所侦测到的顶部的最高价 j2=Max[ 1 ]+ 1 ; // 在下一次迭代中,搜索是从已经侦测到的顶部 Max[1] 开始进行的 k1= High [Max[ 1 ]]; //极值点的最高价,当搜索下一个底部时,找到的底部的最低价应该低于这个价格 k2=Max[ 1 ]; // 搜索底部应该是从已经侦测到的顶部 max[1] 开始的 for (k= 0 ;k<= 12 ;k++) //循环定义第一个底部 - Min[1] { Min[ 1 ]=minimum(k2,bars,k1); //--- 在指定的时间间隔中定义最近的底部 k1= Low [Min[ 1 ]]+delta; //在随后的迭代中侦测到的底部的最低价应该低于当前迭代中找到的底部的最低价 k2=Min[ 1 ]+ 1 ; // 在下一次迭代中,搜索是从已经侦测到的底部 min[1] 开始的 l1= Low [Min[ 1 ]]; //---底部的最低价,当搜索下一个极值点时,顶部的最高价应该高于它 l2=Min[ 1 ]; // 对顶部的搜索应该是从已经侦测到的底部 min[1] 开始的 for (l= 0 ;l<= 10 ;l++) //循环定义第二个顶部 - Max[2] 以及第二个底部 Min[2] { Max[ 2 ]=maximum(l2,bars,l1); //在指定的时间间隔内定义最近的顶部 l1= High [Max[ 2 ]]-delta; //下次迭代的最高价应该高于当前迭代中侦测到的顶部的最高价 l2=Max[ 2 ]+ 1 ; //在下一次迭代中,搜索是从已经侦测到的顶部 Max[2] 开始的 Min[ 2 ]=minimum(Max[ 2 ],bars, High [Max[ 2 ]]); //---在指定的时间段内定义最近的底部 if (Max[ 2 ]>Min[ 1 ] && Min[ 1 ]>Max[ 1 ] && Max[ 1 ]> 0 && Max[ 2 ]<Min[ 2 ] && Min[ 2 ]<bars) //--- 排除掉冲突的极值和特殊情况 { mag1=Max[ 1 ]; // 在每次迭代中,如果条件满足,侦测到的极值保存的位置 mag2=Min[ 1 ]; mag3=Max[ 2 ]; mag4=Min[ 2 ]; } } } } Max[ 1 ]=mag1; // 定义了极值点,否则所有变量赋值为 'bars' 的数值 Min[ 1 ]=mag2; Max[ 2 ]=mag3; Min[ 2 ]=mag4; Max[ 1 ]=check_max(Max[ 1 ],Min[ 1 ]); // 在指定的时间段内验证和修改极值点的位置 Min[ 1 ]=check_min(Min[ 1 ],Max[ 2 ]); Max[ 2 ]=check_max(Max[ 2 ],Min[ 2 ]);[/CODE] 逐行拆解几个关键点:Sleep(1000*time) 把每次 tick 处理强行延迟 time 秒,避免在高速行情里重复扫描浪费资源;CopyLow/CopyHigh 取 bars+2 根历史数据,ArraySetAsSeries 设成倒序,让索引 0 是最新柱;delta_points*Point() 把输入的点数转成绝对价格差,作为相邻极值之间的最小分离阈值。 嵌套循环里 minimum()/maximum() 是自定义的极值查找函数,返回的是柱索引。第一套循环要求 max[1]>min[1] 且 min[2]>max[1],即底-顶-底结构;第二套反过来要 Max[1]>0 且 Min[1]>Max[1],即顶-底-顶结构。两个 mag 数组只在过滤条件通过时才被改写,否则保留初始值 bars,相当于“没找到有效结构”。 实际用的时候,更合理的做法是取最近那一个极值点衍生出来的顶底,而不是第一套或第二套里最早传入的那个。两套都算出后,会根据极值位置推算手数和指标读数,并校验“修改侦测条件”和“当前无持仓”这两个前置。 开仓触发看价格与 MACD 柱的背离:若极值点价格和 MACD 柱形方向相反,且背离幅度不小于输入参数设定值,才允许下对应方向的单。外汇和贵金属波动受杠杆与消息面影响大,这类背离信号只代表概率倾斜,实盘前请在 MT5 策略测试器里用真实点差跑一遍确认。
class="type">void OnTick() { Sleep( class="num">1000 *time); class=class="str">"cmt">// 引入时间延迟 class="type">class="kw">double High [], Low []; ArraySetAsSeries( Low , true ); class="type">int copied1= CopyLow( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">2 , Low ); ArraySetAsSeries( High , true ); class="type">int copied2= CopyHigh( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">2 , High ); ArraySetAsSeries( Time , true ); class="type">int copied3= CopyTime( Symbol(), class="num">0 , class="num">0 ,bars+ class="num">2 , Time ); class="type">MqlTick last_tick; class="type">class="kw">double Bid =last_tick.bid; class="type">class="kw">double Ask =last_tick.ask; class="type">class="kw">double delta=delta_points* Point(); // 变化范围的绝对值 class="type">class="kw">double trendd=trend* Point(); // 最近的两个顶部/底部偏差的绝对值 class="type">class="kw">double guard=guard_points* Point(); // 止损偏移的绝对值 class="type">int j,k,l; class="type">int j2,k2,l2; class="type">class="kw">double j1,k1,l1; class="type">int min[ class="num">6 ]; // 如果第一个侦测到的极值是底部,定义底部的数组,该数值对应着侦测到的极值点的柱的索引 class="type">int max[ class="num">6 ]; // 如果第一个侦测到的极值是底部,定义顶部的数组,该数值对应着侦测到的极值点的柱的索引 class="type">int Min[ class="num">6 ]; // 如果第一个侦测到的极值是顶部,定义底部的数组,该数值对应着侦测到的极值点的柱的索引 class="type">int Max[ class="num">6 ]; // 如果第一个侦测到的极值是底部,定义顶部的数组,该数值对应着侦测到的极值点的柱的索引 class="type">int mag1=bars; class="type">int mag2=bars; class="type">int mag3=bars; class="type">int mag4=bars; j1= SymbolInfoDouble( Symbol(), SYMBOL_BID )+( class="num">1 -first_extrem)*delta_points* Point(); // 当搜索第一个极值点时,额外比例定义了第一个底部的最小价格位置 j2= class="num">0 ; // 在第一次迭代中,搜索是从最后面的历史柱开始进行的 for (j= class="num">0 ;j<= class="num">15 ;j++) // 循环定义第一个底部 - min[class="num">1] { min[ class="num">1 ]=minimum(j2,bars,j1); //在指定的时间间隔中定义最近的底部 j2=min[ class="num">1 ]+ class="num">1 ; //在下一次迭代中,搜索时从已经侦测到的底部 min[class="num">1] 开始进行的 j1= Low [min[ class="num">1 ]]+delta; //--- 随后的迭代中所侦测的底部的最低价应该比当前迭代中找到的底部最低价更低 k1= Low [min[ class="num">1 ]]; //搜索到的底部的最低价,下面搜索的顶部的极值点的最高价格应该在此之上 k2=min[ class="num">1 ]; // 搜索底部之后的顶部,从侦测到的底部 min[class="num">1] 开始进行 for (k= class="num">0 ;k<= class="num">12 ;k++) // 循环定义第一个顶部 - max[class="num">1] { max[ class="num">1 ]=maximum(k2,bars,k1); //--- 在指定的时间间隔内定义最近的顶部 k1= High [max[ class="num">1 ]]-delta; //--- 下次迭代的最高价应该高于当前迭代中侦测到的顶部的最高价 k2=max[ class="num">1 ]+ class="num">1 ; // 在下一次迭代中,搜索是从已经侦测到的顶部 max[class="num">1] 开始的 l1= High [max[ class="num">1 ]]; //--- 极值点的最高价,当搜索下个底部的最低价时,位置应该低于这个值 l2=max[ class="num">1 ]; // 搜索底部应该是从已经侦测到的顶部 max[class="num">1] 开始的 for (l= class="num">0 ;l<= class="num">10 ;l++) // 循环定义第二个底部 - min[class="num">2] 以及第二个顶部 max[class="num">2] { min[ class="num">2 ]=minimum(l2,bars,l1); //--- 在指定时间段之内定义最近的底部 l1= Low [min[ class="num">2 ]]+delta; //在随后的迭代中用于搜索底部的最低价应该比当前迭代中找到的底部的最低价要低 l2=min[ class="num">2 ]+ class="num">1 ; //在下一次迭代中,搜索是从已经侦测到的底部 min[class="num">2] 开始的 max[ class="num">2 ]=maximum(min[ class="num">2 ],bars, Low [min[ class="num">2 ]]); //在指定的时间间隔内定义最近的顶部 if (max[ class="num">1 ]>min[ class="num">1 ] && min[ class="num">1 ]> class="num">0 && min[ class="num">2 ]>max[ class="num">1 ] && min[ class="num">2 ]<max[ class="num">2 ] && max[ class="num">2 ]<mag4) //--- 排除掉冲突的极值和特殊状况 { mag1=min[ class="num">1 ]; // 在每次迭代中,所侦测到的极值在条件满足时要保存位置 mag2=max[ class="num">1 ]; mag3=min[ class="num">2 ]; mag4=max[ class="num">2 ]; } } } } //--- 定义了极值点,否则对所有变量赋值为 &class="macro">#x27;bars&class="macro">#x27; 的值 min[ class="num">1 ]=mag1; max[ class="num">1 ]=mag2; min[ class="num">2 ]=mag3; max[ class="num">2 ]=mag4; //--- 在指定的时间间隔中验证和修改极值点的位置 min[ class="num">1 ]=check_min(min[ class="num">1 ],max[ class="num">1 ]); max[ class="num">1 ]=check_max(max[ class="num">1 ],min[ class="num">2 ]); min[ class="num">2 ]=check_min(min[ class="num">2 ],max[ class="num">2 ]); //--------------------------------------------------------------------------------------------------------------- mag1=bars; mag2=bars; mag3=bars; mag4=bars; j1= SymbolInfoDouble( Symbol(), SYMBOL_BID )-( class="num">1 -first_extrem)*delta_points* Point(); // 当搜索第一个极值点时,额外比例定义了最高价格,第一个顶部应该高于它 j2= class="num">0 ; // 在第一次迭代中,搜索是从最后的历史柱开始的 for (j= class="num">0 ;j<= class="num">15 ;j++) // 循环定义第一个顶部 - Max[class="num">1] { Max[ class="num">1 ]=maximum(j2,bars,j1); //在指定的时间间隔中定义最近的顶部 j1= High [Max[ class="num">1 ]]-delta; //下次迭代的最高价应该高于当前迭代中所侦测到的顶部的最高价 j2=Max[ class="num">1 ]+ class="num">1 ; // 在下一次迭代中,搜索是从已经侦测到的顶部 Max[class="num">1] 开始进行的 k1= High [Max[ class="num">1 ]]; //极值点的最高价,当搜索下一个底部时,找到的底部的最低价应该低于这个价格 k2=Max[ class="num">1 ]; // 搜索底部应该是从已经侦测到的顶部 max[class="num">1] 开始的 for (k= class="num">0 ;k<= class="num">12 ;k++) //循环定义第一个底部 - Min[class="num">1] { Min[ class="num">1 ]=minimum(k2,bars,k1); //--- 在指定的时间间隔中定义最近的底部 k1= Low [Min[ class="num">1 ]]+delta; //在随后的迭代中侦测到的底部的最低价应该低于当前迭代中找到的底部的最低价 k2=Min[ class="num">1 ]+ class="num">1 ; // 在下一次迭代中,搜索是从已经侦测到的底部 min[class="num">1] 开始的 l1= Low [Min[ class="num">1 ]]; //---底部的最低价,当搜索下一个极值点时,顶部的最高价应该高于它 l2=Min[ class="num">1 ]; // 对顶部的搜索应该是从已经侦测到的底部 min[class="num">1] 开始的 for (l= class="num">0 ;l<= class="num">10 ;l++) //循环定义第二个顶部 - Max[class="num">2] 以及第二个底部 Min[class="num">2] { Max[ class="num">2 ]=maximum(l2,bars,l1); //在指定的时间间隔内定义最近的顶部 l1= High [Max[ class="num">2 ]]-delta; //下次迭代的最高价应该高于当前迭代中侦测到的顶部的最高价 l2=Max[ class="num">2 ]+ class="num">1 ; //在下一次迭代中,搜索是从已经侦测到的顶部 Max[class="num">2] 开始的 Min[ class="num">2 ]=minimum(Max[ class="num">2 ],bars, High [Max[ class="num">2 ]]); //---在指定的时间段内定义最近的底部 if (Max[ class="num">2 ]>Min[ class="num">1 ] && Min[ class="num">1 ]>Max[ class="num">1 ] && Max[ class="num">1 ]> class="num">0 && Max[ class="num">2 ]<Min[ class="num">2 ] && Min[ class="num">2 ]<bars) //--- 排除掉冲突的极值和特殊情况 { mag1=Max[ class="num">1 ]; // 在每次迭代中,如果条件满足,侦测到的极值保存的位置 mag2=Min[ class="num">1 ]; mag3=Max[ class="num">2 ]; mag4=Min[ class="num">2 ]; } } } } Max[ class="num">1 ]=mag1; // 定义了极值点,否则所有变量赋值为 &class="macro">#x27;bars&class="macro">#x27; 的数值 Min[ class="num">1 ]=mag2; Max[ class="num">2 ]=mag3; Min[ class="num">2 ]=mag4; Max[ class="num">1 ]=check_max(Max[ class="num">1 ],Min[ class="num">1 ]); // 在指定的时间段内验证和修改极值点的位置 Min[ class="num">1 ]=check_min(Min[ class="num">1 ],Max[ class="num">2 ]); Max[ class="num">2 ]=check_max(Max[ class="num">2 ],Min[ class="num">2 ]);
手数与极值驱动的MACD反向开仓
EA 用动态手数把风险锚在极值点距离上:买入手数按 (SYMBOL_BID - 近期底部 - guard) 的万点点差倒推,卖出则按 (近期顶部 - SYMBOL_ASK + guard) 倒推,再统一 NormalizeDouble 到 2 位小数。这样无论震荡还是单边,止损都贴在最近极点外 guard 点,TP 按止损到市价距离对称铺设。 开仓触发依赖 MACD 极点差值:底部结构要求 index_min1 > index_min2 + macd_t,顶部结构要求 index_Max1 < index_Max2 - macd_t,且 trend_down / trend_up 判定价格已反向脱离前极点。实盘实例里 macd_t=0.0004、trend=120 点、变化范围=160 点时,两个底部价差 148 点、MACD 差 0.00062,均过阈才在额外比例 150 点处接多。 回测给出硬数据:macd_t=0.0006 且 trend=160 时,44 笔交易、56% 获利;放松到 macd_t=0.0004 且 trend=120 时,84 笔交易、51% 获利。参数越紧胜率越高但样本越少,外汇与贵金属杠杆高,这种反向极点策略仍可能连续止损。 传统指标工具常把 5 点内毛刺当极值,却配 200 点止盈止损,信号有效性被稀释;本方法用变化范围过滤极点数量,用 macd_t / trend 卡差值门槛,才避免「极点不重要」的假信号。
class="type">class="kw">double lot_buy= NormalizeDouble( class="num">0.1 *orderr_size/( NormalizeDouble((( SymbolInfoDouble( Symbol(), SYMBOL_BID )- Low [min[ class="num">1 ]]+guard)* class="num">10000 ), class="num">0 )+ class="num">0.00001 ), class="num">2 ); class=class="str">"cmt">//当买入时计算手数 class="type">class="kw">double lot_sell= NormalizeDouble( class="num">0.1 *orderr_size/( NormalizeDouble((( High [Max[ class="num">1 ]]- SymbolInfoDouble( Symbol(), SYMBOL_ASK )+guard)* class="num">10000 ), class="num">0 )+ class="num">0.00001 ), class="num">2 ); class=class="str">"cmt">//--- 当卖出时计算手数 class="type">int index_handle= iMACD( NULL , PERIOD_CURRENT , class="num">12 , class="num">26 , class="num">9 , PRICE_MEDIAN ); class="type">class="kw">double MACD_all[]; ArraySetAsSeries(MACD_all, true ); class="type">int copied4= CopyBuffer(index_handle, class="num">0 , class="num">0 ,bars+ class="num">2 ,MACD_all); class="type">class="kw">double index_min1=MACD_all[min[ class="num">1 ]]; class="type">class="kw">double index_min2=MACD_all[min[ class="num">2 ]]; class=class="str">"cmt">//---如果第一个极值点是底部,根据极值点计算指标值 class="type">class="kw">double index_Max1=MACD_all[Max[ class="num">1 ]]; class="type">class="kw">double index_Max2=MACD_all[Max[ class="num">2 ]]; class=class="str">"cmt">//如果第一个极值点是顶部,根据极值点计算指标值 class="type">bool flag_1=(min[ class="num">2 ]<bars && min[ class="num">2 ]!= class="num">0 && max[ class="num">1 ]<bars && max[ class="num">1 ]!= class="num">0 && max[ class="num">2 ]<bars && max[ class="num">2 ]!= class="num">0 ); class=class="str">"cmt">//Check the condition of the correct extreme point condition class="type">bool flag_2=(Min[ class="num">1 ]<bars && Min[ class="num">1 ]!= class="num">0 && Max[ class="num">2 ]<bars && Max[ class="num">2 ]!= class="num">0 && Min[ class="num">2 ]<bars && Min[ class="num">2 ]!= class="num">0 ); class="type">bool trend_down=( Low [min[ class="num">1 ]]<( Low [min[ class="num">2 ]]-trendd)); class="type">bool trend_up=( High [Max[ class="num">1 ]]>( High [Max[ class="num">2 ]]+trendd)); class=class="str">"cmt">//---极值点的价格差异点数应该小于设置的数值 openedorder= PositionSelect( Symbol()); class=class="str">"cmt">//验证不存在已建仓位的条件 if (min[ class="num">1 ]<Max[ class="num">1 ] && trend_down && flag_1 && !openedorder && (index_min1>(index_min2+macd_t))) class=class="str">"cmt">//如果第一个极值点是底部,建立买入交易 class=class="str">"cmt">//极值点之间 MACD 的差值不能小于输入参数中 macd_t 的设置 class=class="str">"cmt">// 如果价格反向移动就进行交易,并基于极值点计算指标值 { if (show_info== class="num">1 ) Alert( "对于最新的" ,bars, " 个柱,最近的底部和极值点的距离柱数" ,min[ class="num">1 ], " " ,max[ class="num">1 ], " " ,min[ class="num">2 ]); class=class="str">"cmt">//--- 显示极值点的数据 class="type">MqlTradeResult result={ class="num">0 }; class="type">MqlTradeRequest request={ class="num">0 }; request.action= TRADE_ACTION_DEAL ; request.magic= class="num">123456 ; request.symbol= _Symbol ; request.volume=lot_buy; request.price= SymbolInfoDouble( Symbol(), SYMBOL_ASK ); request.sl= Low [min[ class="num">1 ]]-guard; request.tp= MathAbs( class="num">2 * SymbolInfoDouble( Symbol(), SYMBOL_BID )- Low [min[ class="num">1 ]])+guard; request.type= ORDER_TYPE_BUY ; request.deviation= class="num">50 ; request.type_filling= ORDER_FILLING_FOK ; OrderSend(request,result); } if (min[ class="num">1 ]>Max[ class="num">1 ] && trend_up && flag_2 && !openedorder && (index_Max1<(index_Max2-macd_t))) class=class="str">"cmt">//如果第一个极值点是顶部,建立卖出交易 class=class="str">"cmt">//极值点的 MACD 值的差距不能小于输入参数中的 macd_t 设置值 class=class="str">"cmt">// 如果价格反向移动,就进行交易,并且基于极值点计算指标值 { if (show_info== class="num">1 ) Alert( "对于最新的" ,bars, "个柱,最近的顶部和极值点之间距离柱数" ,Max[ class="num">1 ], " " ,Min[ class="num">1 ], " " ,Max[ class="num">2 ]); class=class="str">"cmt">//---显示极值点数据 class="type">MqlTradeResult result={ class="num">0 }; class="type">MqlTradeRequest request={ class="num">0 }; request.action= TRADE_ACTION_DEAL ; request.magic= class="num">123456 ; request.symbol= _Symbol ; request.volume=lot_sell; request.price= SymbolInfoDouble( Symbol(), SYMBOL_BID ); request.sl= High [Max[ class="num">1 ]]+guard; request.tp= MathAbs( High [Max[ class="num">1 ]]- class="num">2 *( High [Max[ class="num">1 ]]- SymbolInfoDouble( Symbol(), SYMBOL_ASK )))-guard; request.type= ORDER_TYPE_SELL ; request.deviation= class="num">50 ; request.type_filling= ORDER_FILLING_FOK ; OrderSend(request,result); }
◍ 记住这一条就够了
这套极值侦测思路的核心,是用「价格变化阈值」代替固定周期去圈定关键拐点:只有波动超过预设数值的峰谷才被保留,其余噪声直接丢弃。从图表末端起算,让结果紧跟最近行情,不受所选时段长度绑架。 附带工程文件里 extremum_indicator.mq5 约 30.58 KB、extremum_macd_advisor.mq5 约 36.99 KB,读者可下载后在 MT5 里改一下阈值参数,看不同品种上过滤出的极值密度。外汇与贵金属杠杆高、滑点突兀,实盘前务必用策略测试器跑一遍再上。