价格行为分析工具包开发(第二十九部分):暴涨与暴跌拦截EA·进阶篇
📘

价格行为分析工具包开发(第二十九部分):暴涨与暴跌拦截EA·进阶篇

第 2/2 篇

◍ 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。

MQL5 / C++
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,可避开流动性稀薄的时段。

MQL5 / C++
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 止损在跳空时可能不生效,实盘前务必在策略测试器用历史数据验证滑点和点值。

MQL5 / C++
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% 再信信号。

MQL5 / C++
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);

常见问题

在 OnInit 里先校验品种交易权限与最小止损点数,再创建必要的全局变量和箭头对象,失败直接返回 INIT_FAILED,别让后续逻辑空跑。
建议先用近 200 根 K 线的 tick 速度做百分位计算,阈值设在 90~95 分位,模拟盘跑两周看拦截准确率再微调。
可以,把 EA 接到的品种交给小布,它能用 AIGC 跑历史分位并标出假突破高发时段,你只管看结论调阈值。
下单前把 ticket 和箭头对象名绑定到日志字符串,用 Print 落盘含时间、价格、滑点,复盘时按对象名搜就能对齐。
只画触发真实分位阈值的暴涨暴跌箭头,其余用后台变量记录,把视觉噪音压到每屏不超过 3 个信号。