价格行为分析工具包开发(第二十九部分):暴涨与暴跌拦截EA·进阶篇
◍ EA 初始化时把盘口骨架搭起来
这段初始化逻辑决定了一个盯盘 EA 在 MT5 加载瞬间干了什么:先按输入参数框定 7 点到 17 点的活跃时段,再把崩溃日志写到 BoCrashLog.csv,文件为空时自动补表头 DateTime,Type,Velocity,ATR,Price。 代码里先给速度序列 velHistory 按 VelocityHistoryBars 扩容,随后用 iATR 和 iMA 分别抓 ATR 与趋势均线句柄;任一句柄无效就直接 INIT_FAILED,避免后面空指针崩在 OnTick。 界面部分用 OBJ_RECTANGLE_LABEL 画了 200×140 像素的底板,再循环生成 Delta / VelThr / ATR 等 8 个文本标签,初始都显示「: ?」;同时建了 VelUp、VelDown 两条水平参考线,留给后续速度阈值可视化。 外汇与贵金属波动受时段与杠杆放大影响,这类自动建对象的逻辑若参数冲突可能让图表对象堆积,上 MT5 前先确认 SessionStartHour 小于 SessionEndHour。
input class="type">int SessionStartHour = class="num">7; input class="type">int SessionEndHour = class="num">17; input class="type">class="kw">string LogFilename = "BoCrashLog.csv"; class="type">class="kw">double velHistory[]; class="type">class="kw">string dashBG = "DashBG"; class="type">class="kw">string dashNames[] = {"Delta","VelThr","ATR","ATRm","Trend", "Pivot","Zone","Signal"}; class="type">class="kw">string hVelUp = "VelUp"; class="type">class="kw">string hVelDown = "VelDown"; class="type">int OnInit() { ArrayResize(velHistory,VelocityHistoryBars); atrHandle = iATR(_Symbol, ATRTF, ATRPeriod); maHandle = iMA(_Symbol, TrendTF, TrendMAPeriod, class="num">0, MODE_SMA, PRICE_CLOSE); if(atrHandle==INVALID_HANDLE || maHandle==INVALID_HANDLE) class="kw">return INIT_FAILED; logHandle = FileOpen(LogFilename, FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI); if(logHandle>=class="num">0 && FileSize(logHandle)==class="num">0) FileWrite(logHandle,"DateTime,Type,Velocity,ATR,Price"); class=class="str">"cmt">// dashboard background ObjectCreate(class="num">0,dashBG,OBJ_RECTANGLE_LABEL,class="num">0,class="num">0,class="num">0); ObjectSetInteger(class="num">0,dashBG,OBJPROP_XSIZE,class="num">200); ObjectSetInteger(class="num">0,dashBG,OBJPROP_YSIZE,class="num">140); class=class="str">"cmt">// dashboard labels for(class="type">int i=class="num">0;i<ArraySize(dashNames);i++) { class="type">class="kw">string name="Dash_"+dashNames[i]; ObjectCreate(class="num">0,name,OBJ_LABEL,class="num">0,class="num">0,class="num">0); ObjectSetString(class="num">0,name,OBJPROP_TEXT,dashNames[i]+": ?"); } class=class="str">"cmt">// reference lines ObjectCreate(class="num">0,hVelUp ,OBJ_HLINE,class="num">0,class="num">0,class="num">0); ObjectCreate(class="num">0,hVelDown,OBJ_HLINE,class="num">0,class="num">0,class="num">0); class="kw">return INIT_SUCCEEDED; } class="type">void OnDeinit(const class="type">int reason) { if(atrHandle!=INVALID_HANDLE) IndicatorRelease(atrHandle); if(maHandle !=INVALID_HANDLE) IndicatorRelease(maHandle); if(logHandle>=class="num">0) FileClose(logHandle);
「用速度分位阈值卡住假突破」
这段逻辑把「价格速度」从主观感觉变成可量化的触发器。核心做法是:维护一个长度为 VelocityHistoryBars 的 velHistory 数组,每根 K 线收盘时把当前 ASK 价塞进 [0],旧值整体右移,再拿最新价减去最末位算 delta,和动态分位阈值 velTh 比大小。 ComputeVelocityPercentile 是整套机制的标尺。它先由 hist[0] 依次减 hist[i] 得到 n-1 个差分,排序后按 pct 取分位点,再从数组尾部镜像取回——这样得到的 velTh 带有符号方向,能同时框住上行与下行的极端速度。 实盘里若把 VelocityPctile 设成 90,意味着只有超过近 N 根 K 线中 90% 速度样本的变动才被放行,噪声触发概率会明显下降。外汇与贵金属杠杆高,这种过滤只降频不保赢,信号触发后仍需看 ATR 与枢轴区确认。 会话过滤那段也别忽视:UseSessionFilter 开启时,TimeToStruct 取本地小时,不在 SessionStartHour~SessionEndHour 内直接 return,可避开流动性稀薄的时段。
class="type">class="kw">string objs[]={dashBG,hVelUp,hVelDown}; for(class="type">int i=class="num">0;i<ArraySize(objs);i++) ObjectDelete(class="num">0,objs[i]); for(class="type">int i=class="num">0;i<ArraySize(dashNames);i++) ObjectDelete(class="num">0,"Dash_"+dashNames[i]); } class="type">void OnTick() { class=class="str">"cmt">// process only on completed bar class="type">int cur=iBars(_Symbol,MainTF)-class="num">1; if(cur==lastBar) class="kw">return; lastBar=cur; class=class="str">"cmt">// optional session filter class="type">MqlDateTime now; TimeToStruct(TimeCurrent(),now); if(UseSessionFilter && (now.hour<SessionStartHour || now.hour>SessionEndHour)) class="kw">return; class=class="str">"cmt">// velocity calculation class="type">class="kw">double priceNow = SymbolInfoDouble(_Symbol,SYMBOL_ASK); ArrayMove(velHistory,class="num">1,class="num">0,VelocityHistoryBars-class="num">1); class=class="str">"cmt">// shift right velHistory[class="num">0]=priceNow; class="type">class="kw">double delta = priceNow-velHistory[VelocityHistoryBars-class="num">1]; class="type">class="kw">double velTh = ComputeVelocityPercentile(velHistory,VelocityPctile); class="type">bool okVel = (delta>velTh || delta<-velTh); class=class="str">"cmt">// ATR, MA, pivot-zone checks ... class=class="str">"cmt">/* …remaining signal logic… */ if(fire) GenerateSignal(isBoom,priceNow,delta,atrArr[class="num">0],MainTF); } class="type">void UpdateLabel(class="type">int idx,class="type">class="kw">string txt,class="type">bool pass) { class="type">class="kw">string name="Dash_"+dashNames[idx]; ObjectSetString(class="num">0,name,OBJPROP_TEXT ,txt); ObjectSetInteger(class="num">0,name,OBJPROP_COLOR, pass ? clrLime : clrRed); } class="type">class="kw">double ComputeVelocityPercentile(class="type">class="kw">double &hist[],class="type">class="kw">double pct) { class="type">int n=ArraySize(hist); if(n<class="num">2) class="kw">return class="num">0; class="type">class="kw">double d[]; ArrayResize(d,n-class="num">1); for(class="type">int i=class="num">1;i<n;i++) d[i-class="num">1]=hist[class="num">0]-hist[i]; ArraySort(d); class="type">int idx=(class="type">int)MathRound((pct/class="num">100.0)*(ArraySize(d)-class="num">1)); class="kw">return d[ArraySize(d)-class="num">1-idx]; } class="type">void SetHLine(class="type">class="kw">string name,class="type">class="kw">double price,class="type">color clr) { ObjectSetDouble(class="num">0,name,OBJPROP_PRICE, price); ObjectSetInteger(class="num">0,name,OBJPROP_COLOR, clr); ObjectSetInteger(class="num">0,name,OBJPROP_WIDTH, class="num">1); } class="type">void GenerateSignal(class="type">bool isBoom,class="type">class="kw">double price,class="type">class="kw">double delta,
箭头画出之后的下单与日志落盘
信号箭头只是视觉提示,真正要把策略跑起来,得在绘制箭头之后紧接着把风险参数和交易指令补齐。下面这段逻辑承接前面的绘制函数,把 BOOM/CRASH 两类方向的止损、止盈和自动下单开关一次写清。 delta 与 atr 会先写进日志文件(logHandle 有效时),记录到秒级时间、信号类型、delta 两位小数、atr 与 price 按品种精度。这样事后在 MT5 的 File 目录翻 log,能直接核对每一笔信号当时的波动环境。 仓位写死 0.02 手只是示例风险尺度,止损统一取信号价 ±2 倍 atr,止盈指向 pivot 做均值回归。AutoTrade 作为用户总开关,不打开就只报警不下单;打开后 BOOM 走 buy、CRASH 走 sell,挂单备注带 Boom-auto / Crash-auto 方便回看。 外汇与贵金属杠杆高,2 倍 atr 止损在跳空时可能不生效,实盘前务必在策略测试器用历史数据验证滑点和点值。
class="type">class="kw">double atr,ENUM_TIMEFRAMES tf) { class="type">int code = isBoom ? class="num">233 : class="num">234; class="type">class="kw">double y = isBoom ? iLow(_Symbol,tf,class="num">0)-ArrowOffsetPips*_Point : iHigh(_Symbol,tf,class="num">0)+ArrowOffsetPips*_Point; class="type">color clr = isBoom ? BoomArrowColor : CrashArrowColor; class="type">class="kw">string tag = (isBoom?"BOOM":"CRASH")+"_"+ TimeToString(TimeCurrent(),TIME_SECONDS); class="type">class="kw">datetime t0 = iTime(_Symbol,tf,class="num">0); ObjectCreate(class="num">0,tag,OBJ_ARROW,class="num">0,t0,y); ObjectSetInteger(class="num">0,tag,OBJPROP_ARROWCODE,code); ObjectSetInteger(class="num">0,tag,OBJPROP_COLOR,clr); if(logHandle>=class="num">0) FileWrite(logHandle,TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS), isBoom?"BOOM":"CRASH",DoubleToString(delta,class="num">2), DoubleToString(atr,_Digits),DoubleToString(price,_Digits)); Alert((isBoom?"BOOM":"CRASH")+" signal @"+ DoubleToString(price,_Digits)); } class=class="str">"cmt">// Inside GenerateSignal(), after drawing the arrow: class="type">class="kw">double lot = class="num">0.02; class=class="str">"cmt">// example risk sizing class="type">class="kw">double sl = isBoom ? price - atr*class="num">2 : price + atr*class="num">2; class="type">class="kw">double tp = pivot; class=class="str">"cmt">// mean-reversion target if(AutoTrade) class=class="str">"cmt">// user-controlled class="kw">switch { if(isBoom) trade.Buy(lot,_Symbol,price,sl,tp,"Boom-auto"); else trade.Sell(lot,_Symbol,price,sl,tp,"Crash-auto"); }
◍ 用模拟盘把拦截参数磨到能复现
上实盘前先在模拟账户把 EA 跑一遍回测,目的不是看漂亮曲线,而是把 VelocityHistoryBars、VelocityPctile、ATRMultiplier、TrendMAPeriod、ZoneBufferPoints 这几个参数调到信号频率和误报率能接受。MetaEditor 编译完切到 MT5,点工具栏“策略测试器”,从专家列表里选你的暴涨暴跌拦截 EA。 交易品种和 MainTF 必须跟实盘一致,测试区间要覆盖盘整、趋势、剧烈波动三种市况,执行模式选“每 tick”才能拿到精确结果。每次只动一两个参数——比如把速度百分位收紧一点,或者把枢轴缓冲扩大几拍——重跑后看资金曲线和信号列表里漏掉真实尖峰的位置。 Boom 900 的回测里,EA 在日志记下了全部暴涨暴跌信号:枢轴线蓝色、速度阈值线橙色,绿色向上箭头标暴涨、红色向下箭头标暴跌。反复迭代到模拟账户能稳定复现信号且回撤可控,再考虑受控实盘。外汇与合成品种波动极端,回测顺不代表实盘概率同分布,部署必须小仓位。
「画得少,看得清」
回测里 EA 只在速度、ATR 激增、趋势一致与枢轴区突破同时达标时才画箭头,Boom 900 模拟中绿涨红跌信号与动态 GIF 完全一致,蓝色枢轴线、橙色速度线静态图可复核对齐。 少数信号仍偏噪,实盘前建议切到 H1/H4/日线,用更长周期过滤市场噪音;外汇与贵金属杠杆高,误报可能造成快速亏损,须自行叠加交易时段与形态判断。 代码里的速度分位过滤是关键:先按 delta 正负单向排历史速度差,再取分位阈值,MathAbs(delta) 超阈才放行,枢轴区再用 ZONE_Points 卡边界。 把它当战术雷达而非自动下单机,开 MT5 把 VelocityPctile 从默认调到 80 附近,看同方向 K 线占比是否到 80% 再信信号。
ArraySort(d); if(delta>class="num">0) { for(class="type">int i = class="num">1; i < VelocityHistoryBars; i++) d[i - class="num">1] = velHistory[class="num">0] - velHistory[i]; ArraySort(d); } if(delta<class="num">0) { for(class="type">int i = class="num">1; i < VelocityHistoryBars; i++) d[i - class="num">1] = velHistory[i] - velHistory[class="num">0]; ArraySort(d); } class="type">int idx= (class="type">int)MathRound((VelocityPctile / class="num">100.0) * (ArraySize(d) - class="num">1)); class="type">class="kw">double velTh = d[ArraySize(d) - class="num">1 - idx]; class="type">bool okVel = MathAbs(delta) > velTh; class="type">bool okZone = class="kw">false; if((delta > class="num">0 ) && (priceNow > pivot)) okZone = true; if((delta < class="num">0 ) && (priceNow < pivot)) okZone = true; class="type">bool okZone = class="kw">false; if((delta > class="num">0 ) && (priceNow > pivot) && (priceNow < ( pivot + ZONE_Points * _Point))) okZone = true; if((delta < class="num">0 ) && (priceNow < pivot) && (priceNow > ( pivot - ZONE_Points * _Point))) okZone = true; class="type">class="kw">double goodBars = class="num">1; for(class="type">int i=ArraySize(d) - class="num">1; i>=class="num">0; i--) if(d[i] < class="num">0) goodBars=class="num">1 - (i+class="num">1.0)/ArraySize(d);