价格行为分析工具包开发(第六部分):均值回归信号捕捉器·进阶篇
📉

价格行为分析工具包开发(第六部分):均值回归信号捕捉器·进阶篇

(2/3)·手动盯均值回归常漏掉拐点,这套 EA 把 50 周期 EMA 与 RSI 合成可视信号

实战向 第 2/3 篇
接上篇,我们继续深挖工具包的信号层。均值回归最怕的是人眼在极端波动里犹豫,等反应过来价格已经抽回均值。把判定规则写进 EA,图表上直接标箭头,能少掉很多迟疑。

指标取值与信号箭头落地的底层函数

把 EMA、RSI、ATR 的当前值抓出来,是任何多指标共振策略的第一步。下面三个函数都只取最新一根 K 线的缓冲值:用 CopyBuffer 把手柄指向的指标线第 0 根数据拷进长度为 1 的数组,失败就返回 0,成功直接吐出 emaValues[0] / rsiValues[0] / atrValues[0]。 DrawSignalArrow 负责把买卖信号画到图上。它用 signalType 加当前分钟级时间拼出唯一对象名,先查重再删旧,避免同根 K 线反复重绘。箭头样式靠 OBJPROP_ARROWCODE 区分:买信号用 233(向上箭头),卖信号用 234(向下箭头),颜色与宽度由入参和全局 ArrowSize 控制。 开 MT5 把这段直接塞进 EA,把 EMA_Handle、RSI_Handle、ATR_Handle 在 OnInit 里用 iMA / iRSI / iATR 建好,就能在实时行情里看到箭头随信号刷新。外汇与贵金属杠杆高,信号箭头只是概率提示,不代表方向必然成立。

MQL5 / C++
class="type">class="kw">double GetEMA()
  {
   class="type">class="kw">double emaValues[class="num">1];
   if(CopyBuffer(EMA_Handle, class="num">0, class="num">0, class="num">1, emaValues) <= class="num">0)
      class="kw">return class="num">0;
   class="kw">return emaValues[class="num">0];
  }
class="type">class="kw">double GetRSI()
  {
   class="type">class="kw">double rsiValues[class="num">1];
   if(CopyBuffer(RSI_Handle, class="num">0, class="num">0, class="num">1, rsiValues) <= class="num">0)
      class="kw">return class="num">0;
   class="kw">return rsiValues[class="num">0];
  }
class="type">class="kw">double GetATR()
  {
   class="type">class="kw">double atrValues[class="num">1];
   if(CopyBuffer(ATR_Handle, class="num">0, class="num">0, class="num">1, atrValues) <= class="num">0)
      class="kw">return class="num">0;
   class="kw">return atrValues[class="num">0];
  }
class="type">void DrawSignalArrow(class="type">class="kw">string signalType, class="type">class="kw">double price, class="type">class="kw">color arrowColor)
  {
   class="type">class="kw">string arrowName = signalType + "_" + TimeToString(TimeCurrent(), TIME_MINUTES);
   if(ObjectFind(class="num">0, arrowName) != -class="num">1)
     {
      ObjectDelete(class="num">0, arrowName);
     }
   ObjectCreate(class="num">0, arrowName, OBJ_ARROW, class="num">0, TimeCurrent(), price);
   ObjectSetInteger(class="num">0, arrowName, OBJPROP_ARROWCODE, (signalType == "BuySignal") ? class="num">233 : class="num">234);
   ObjectSetInteger(class="num">0, arrowName, OBJPROP_COLOR, arrowColor);
   ObjectSetInteger(class="num">0, arrowName, OBJPROP_WIDTH, ArrowSize);
  }

「信号标签绘制与冷却间隔控制」

把信号摘要直接画在图表左上角,能省去交易者切窗口看日志的成本。下面这段函数用 OBJ_LABEL 在坐标 (10,10) 像素处输出信号类型、入场价、TP 与 SL,字体固定 10 号,颜色走外部变量 TextColor。 SL 与 TP 都以 ATR 倍数为基准:BUY 时 SL = refPrice - ATR*ATR_Multiplier,TP = price + ATR*ATR_Multiplier;SELL 则反向。ATR_Multiplier 若在外部设为 2.0,当前 ATR=0.00120、price=1.08500、refPrice=1.08450,则 BUY 的 SL≈1.08210、TP≈1.08740,这组数字开 MT5 用策略测试器能直接复算。 刷新前先 ObjectFind 查重名对象 SignalSummary,存在就 ObjectDelete,避免旧标签叠在新标签上。每次发信号后调 UpdateSignalTime 把 lastSignalTime 写成当前柱 iTime(0,0);BarsSinceLastSignal 用 (当前柱时间-上次信号时间)/PeriodSeconds() 算间隔柱数,没信号过就返回 INT_MAX,方便上层做冷却判断。 外汇与贵金属波动受杠杆放大,ATR 止损位只代表波动尺度,实际触发后亏损幅度仍可能超预期,仓位需自配。

MQL5 / C++
class="type">void DisplayTextSummary(class="type">class="kw">string signalType, class="type">class="kw">double price, class="type">class="kw">double refPrice, class="type">class="kw">double ATR)
  {
   class="type">class="kw">string objectName = "SignalSummary"; class=class="str">"cmt">// Unique object name for the summary
class=class="str">"cmt">// Delete the existing summary if it exists
   if(ObjectFind(class="num">0, objectName) != -class="num">1)  class=class="str">"cmt">// If the object exists
     {
       ObjectDelete(class="num">0, objectName); class=class="str">"cmt">// Delete the existing object
     }
   class="type">class="kw">double SL = (signalType == "BUY") ? refPrice - (ATR * ATR_Multiplier) : refPrice + (ATR * ATR_Multiplier);
   class="type">class="kw">double TP = (signalType == "BUY") ? price + (ATR * ATR_Multiplier) : price - (ATR * ATR_Multiplier);
   class="type">class="kw">string summary = signalType + " Signal\n" +
                   "Price: " + DoubleToString(price, class="num">5) + "\n" +
                   "TP: " + DoubleToString(TP, class="num">5) + "\n" +
                   "SL: " + DoubleToString(SL, class="num">5);
   ObjectCreate(class="num">0, objectName, OBJ_LABEL, class="num">0, class="num">0, class="num">0);
   ObjectSetString(class="num">0, objectName, OBJPROP_TEXT, summary);
   ObjectSetInteger(class="num">0, objectName, OBJPROP_COLOR, TextColor);
   ObjectSetInteger(class="num">0, objectName, OBJPROP_FONTSIZE, class="num">10); class=class="str">"cmt">// Adjust font size if needed
class=class="str">"cmt">// Position the label at the left upper corner
   ObjectSetInteger(class="num">0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(class="num">0, objectName, OBJPROP_XDISTANCE, class="num">10); class=class="str">"cmt">// class="num">10 pixels from the left
   ObjectSetInteger(class="num">0, objectName, OBJPROP_YDISTANCE, class="num">10); class=class="str">"cmt">// class="num">10 pixels from the top
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update signal time to prevent frequent signals                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void UpdateSignalTime()
  {
   lastSignalTime = iTime(NULL, class="num">0, class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate bars since the last signal                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int BarsSinceLastSignal()
  {
   class="type">class="kw">datetime currentBarTime = iTime(NULL, class="num">0, class="num">0);
   if(lastSignalTime == class="num">0)
      class="kw">return INT_MAX; class=class="str">"cmt">// If no signal has been generated class="kw">return a large number.
   class="kw">return (class="type">int)((currentBarTime - lastSignalTime) / PeriodSeconds());
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把均值回归EA拆成可改的零件

这套EA的头部用 #property 指令挂了版权、版本和 indicator_chart_window,意味着它直接画在主图而非副图。默认参数里 EMA 周期 50、RSI 周期 14、超买 75 / 超卖 25,和常见 70/30 不同,过滤更松,信号可能更频繁。 输入参数把 EMA、RSI、ATR 周期以及 RSI 阈值全开放,还给了 CooldownBars=3 这根冷却阀——相邻信号至少隔 3 根 K 线,避免剧烈行情里刷屏。箭头颜色、文字色也暴露给用户,属于“不改代码就能调风格”的设计。 全局变量只存三件事:三个指标句柄 + lastSignalTime 时间戳。OnInit 里用 iMA / iRSI / iATR 拿句柄并做失败检查,任一句柄建不出就写日志并停止;OnDeinit 只干一件实事:删掉 SignalSummary 标签,图表不留垃圾。 真正干活的是 OnTick:先判冷却,再取 EMA/RSI/ATR 最新值。价格低于 EMA 且 RSI 落进超卖区才画买箭,反之为卖箭;止盈止损按 ATR×2.0 动态算。GetEMA/GetRSI/GetATR 用 CopyBuffer 取数,失败返 0 让 EA 跳过这一 tick。 DrawSignalArrow 用“信号类型+时间”拼唯一名,重名先删后建;DisplayTextSummary 把价、SL、TP 贴左上角;UpdateSignalTime 和 BarsSinceLastSignal 管冷却,首次运行返回大数保证不出错。外汇与贵金属波动大,这类 EA 信号仅作参考,实盘前请在 MT5 策略测试器跑通再上。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Mean Reversion Reaper.mq5                                        |
class=class="str">"cmt">//| Author: Christian Benjamin                                       |
class=class="str">"cmt">//| Website: [MQL5官方文档]                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Christian Benjamin"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">// Indicator operates in the main chart window
class="macro">#class="kw">property indicator_chart_window
input class="type">int EMA_Period = class="num">50;             class=class="str">"cmt">// EMA Period
input class="type">int RSI_Period = class="num">14;             class=class="str">"cmt">// RSI Period
input class="type">class="kw">double RSI_Overbought = class="num">75.0;    class=class="str">"cmt">// RSI Overbought level
input class="type">class="kw">double RSI_Oversold = class="num">25.0;      class=class="str">"cmt">// RSI Oversold level
input class="type">int CooldownBars = class="num">3;            class=class="str">"cmt">// Cooldown bars between signals
input class="type">class="kw">double ATR_Multiplier = class="num">2.0;     class=class="str">"cmt">// ATR Multiplier for TP and SL
input class="type">int ATR_Period = class="num">14;             class=class="str">"cmt">// ATR Period
input class="type">class="kw">color BuySignalColor = clrGreen; class=class="str">"cmt">// Buy signal arrow class="type">class="kw">color
input class="type">class="kw">color SellSignalColor = clrRed;  class=class="str">"cmt">// Sell signal arrow class="type">class="kw">color

均值回归EA的句柄与信号触发骨架

这段 MT5 代码把均值回归策略的初始化、反初始化和主循环搭清楚了。EA 在 OnInit 里一次性申请 EMA、RSI、ATR 三个指标句柄,任意句柄返回 INVALID_HANDLE 就直接 INIT_FAILED 退出,避免后续空指针乱跑。 input int ArrowSize = 2; // 箭头尺寸 input color TextColor = clrDodgerBlue; // TP/SL 文字颜色 int EMA_Handle, RSI_Handle, ATR_Handle; // 三个指标句柄 int OnInit() { EMA_Handle = iMA(NULL,0,EMA_Period,0,MODE_EMA,PRICE_CLOSE); RSI_Handle = iRSI(NULL,0,RSI_Period,PRICE_CLOSE); ATR_Handle = iATR(NULL,0,ATR_Period);

if(EMA_Handle==INVALID_HANDLERSI_Handle==INVALID_HANDLEATR_Handle==INVALID_HANDLE)

return INIT_FAILED; return INIT_SUCCEEDED; } 上面这段逐行看:前两句声明外部可调参数,第三行定义全局句柄;iMA 用收盘价算 EMA,iRSI 同理,iATR 取平均真实波幅。只要有一个创建失败,Print 报错并终止加载。 OnTick 里先判断 BarsSinceLastSignal() < CooldownBars 就 return,这是冷却机制,防止同一波行情连发信号。随后取 EMA/RSI/ATR 与当前收盘价比对:收盘价低于 EMA 且 RSI 进入超卖区,画 BuySignal 箭头并标注前一根低点作参考;反之为 SellSignal。外汇与贵金属波动剧烈,这类信号仅代表概率倾向,实盘前务必在策略测试器跑历史数据验证。 GetEMA 用 CopyBuffer 取最新一根缓冲值,失败返回 0;GetRSI 结构相同。把 EMA_Period、RSI_Period 这类参数调到 50/14 附近,开 MT5 附图就能直观比对信号位置。

MQL5 / C++
input class="type">int ArrowSize = class="num">2;                      class=class="str">"cmt">// Arrow size
input class="type">class="kw">color TextColor = clrDodgerBlue;        class=class="str">"cmt">// Color for TP/SL text summary
class="type">int EMA_Handle, RSI_Handle, ATR_Handle;       class=class="str">"cmt">// Handles for EMA, RSI, and ATR
class="type">class="kw">datetime lastSignalTime = class="num">0;                  class=class="str">"cmt">// Tracks last signal time
class="type">int OnInit()
{
   EMA_Handle = iMA(NULL, class="num">0, EMA_Period, class="num">0, MODE_EMA, PRICE_CLOSE);
   RSI_Handle = iRSI(NULL, class="num">0, RSI_Period, PRICE_CLOSE);
   ATR_Handle = iATR(NULL, class="num">0, ATR_Period);
   if (EMA_Handle == INVALID_HANDLE || RSI_Handle == INVALID_HANDLE || ATR_Handle == INVALID_HANDLE) {
      Print("Failed to create indicator handles. Error: ", GetLastError());
      class="kw">return INIT_FAILED; class=class="str">"cmt">// Stops EA if initialization fails
   }
   Print("Mean Reversion EA initialized.");
   class="kw">return INIT_SUCCEEDED;
}
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
   ObjectDelete(class="num">0, "SignalSummary"); class=class="str">"cmt">// Remove any signal summary text
   Print("Mean Reversion EA deinitialized.");
}
class="type">void OnTick()
{
   if (BarsSinceLastSignal() < CooldownBars) class="kw">return; class=class="str">"cmt">// Skip if cooldown is active
   class="type">class="kw">double EMA_Value = GetEMA();   class=class="str">"cmt">// Fetch EMA value
   class="type">class="kw">double RSI_Value = GetRSI();   class=class="str">"cmt">// Fetch RSI value
   class="type">class="kw">double ATR_Value = GetATR();   class=class="str">"cmt">// Fetch ATR value
   class="type">class="kw">double closePrice = iClose(NULL, class="num">0, class="num">0); class=class="str">"cmt">// Current bar&class="macro">#x27;s close price
   if (closePrice < EMA_Value && RSI_Value <= RSI_Oversold) {
      class=class="str">"cmt">// Buy Signal
      DrawSignalArrow("BuySignal", closePrice, BuySignalColor);
      DisplayTextSummary("BUY", closePrice, iLow(NULL, class="num">0, class="num">1), ATR_Value);
      UpdateSignalTime(); class=class="str">"cmt">// Update last signal time
   } else if (closePrice > EMA_Value && RSI_Value >= RSI_Overbought) {
      class=class="str">"cmt">// Sell Signal
      DrawSignalArrow("SellSignal", closePrice, SellSignalColor);
      DisplayTextSummary("SELL", closePrice, iHigh(NULL, class="num">0, class="num">1), ATR_Value);
      UpdateSignalTime(); class=class="str">"cmt">// Update last signal time
   }
}
class="type">class="kw">double GetEMA()
{
   class="type">class="kw">double emaValues[class="num">1];
   if (CopyBuffer(EMA_Handle, class="num">0, class="num">0, class="num">1, emaValues) <= class="num">0)
      class="kw">return class="num">0;
   class="kw">return emaValues[class="num">0];
}
class="type">class="kw">double GetRSI()
{

「把信号画到图上的底层函数」

指标数值取回来之后,真正要在 MT5 图表上留下痕迹,靠的是几个不显眼但必须跑通的工具函数。GetRSI 和 GetATR 都用 CopyBuffer 拉最新一根的缓冲值,数组长度写死为 1,若返回值 ≤0 直接 return 0,这说明句柄失效时宁可给不出数也不能乱画。 DrawSignalArrow 按信号类型拼出带分钟级时间戳的对象名,先查重再删旧,用 OBJ_ARROW 在 TimeCurrent() 位置落箭头;买信号 arrowcode 填 233、卖信号填 234,线宽走外部变量 ArrowSize。DisplayTextSummary 则在左上角(距边 10 像素)用 OBJ_LABEL 打印信号类型、入场价、TP、SL,价格统一 DoubleToString 保留 5 位小数,SL 与 TP 均按 refPrice±ATR*ATR_Multiplier 推算,外汇与贵金属波动剧烈,这套距离仅作参考,实盘滑点可能吞掉缓冲。 UpdateSignalTime 把当前棒时间存进 lastSignalTime,BarsSinceLastSignal 接着数距离上次信号过了几根棒——这是防重复触发的关键计数点,你在 EA 里改冷却周期直接动这里就行。

MQL5 / C++
class="type">class="kw">double rsiValues[class="num">1];
if (CopyBuffer(RSI_Handle, class="num">0, class="num">0, class="num">1, rsiValues) <= class="num">0)
   class="kw">return class="num">0;
class="kw">return rsiValues[class="num">0];
}
class="type">class="kw">double GetATR()
{
   class="type">class="kw">double atrValues[class="num">1];
   if (CopyBuffer(ATR_Handle, class="num">0, class="num">0, class="num">1, atrValues) <= class="num">0)
      class="kw">return class="num">0;
   class="kw">return atrValues[class="num">0];
}
class="type">void DrawSignalArrow(class="type">class="kw">string signalType, class="type">class="kw">double price, class="type">class="kw">color arrowColor)
{
   class="type">class="kw">string arrowName = signalType + "_" + TimeToString(TimeCurrent(), TIME_MINUTES);
   if (ObjectFind(class="num">0, arrowName) != -class="num">1) ObjectDelete(class="num">0, arrowName);
   ObjectCreate(class="num">0, arrowName, OBJ_ARROW, class="num">0, TimeCurrent(), price);
   ObjectSetInteger(class="num">0, arrowName, OBJPROP_ARROWCODE, (signalType == "BuySignal") ? class="num">233 : class="num">234);
   ObjectSetInteger(class="num">0, arrowName, OBJPROP_COLOR, arrowColor);
   ObjectSetInteger(class="num">0, arrowName, OBJPROP_WIDTH, ArrowSize);
}
class="type">void DisplayTextSummary(class="type">class="kw">string signalType, class="type">class="kw">double price, class="type">class="kw">double refPrice, class="type">class="kw">double ATR)
{
   class="type">class="kw">string objectName = "SignalSummary";
   if (ObjectFind(class="num">0, objectName) != -class="num">1) ObjectDelete(class="num">0, objectName);
   class="type">class="kw">double SL = (signalType == "BUY") ? refPrice - (ATR * ATR_Multiplier) : refPrice + (ATR * ATR_Multiplier);
   class="type">class="kw">double TP = (signalType == "BUY") ? price + (ATR * ATR_Multiplier) : price - (ATR * ATR_Multiplier);
   class="type">class="kw">string summary = signalType + " Signal\n" +
                  "Price: " + DoubleToString(price, class="num">5) + "\n" +
                  "TP: " + DoubleToString(TP, class="num">5) + "\n" +
                  "SL: " + DoubleToString(SL, class="num">5);
   ObjectCreate(class="num">0, objectName, OBJ_LABEL, class="num">0, class="num">0, class="num">0);
   ObjectSetString(class="num">0, objectName, OBJPROP_TEXT, summary);
   ObjectSetInteger(class="num">0, objectName, OBJPROP_COLOR, TextColor);
   ObjectSetInteger(class="num">0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(class="num">0, objectName, OBJPROP_XDISTANCE, class="num">10);
   ObjectSetInteger(class="num">0, objectName, OBJPROP_YDISTANCE, class="num">10);
}
class="type">void UpdateSignalTime()
{
   lastSignalTime = iTime(NULL, class="num">0, class="num">0); class=class="str">"cmt">// Store time of the current bar
}
class="type">int BarsSinceLastSignal()
{
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EMA 偏离与 RSI 区位的实时标注,把重复劳动交给小布,你专注决策。

常见问题

货币对受央行政策锚定,回归概率倾向更高;黄金受地缘事件驱动,极端偏离可能持续更久。两者均属高风险,信号只作概率参考。
50 周期在日内与 swing 级别间取得平衡,太长会钝化、太短噪声多。具体周期可按品种波动率微调。
小布盯盘内置了类似的偏离诊断视图,不需自行编译 EA 也能看到买入卖出区位提示,适合先验证逻辑再决定是否写代码。
原文用 RSI 与 50 EMA 配合,参数未强调改动;若品种震荡频繁,可测试 7~14 区间,信号可能更灵敏但假突破也增多。