价格行为分析工具包开发(第 22 部分):相关性仪表盘·进阶篇
📘

价格行为分析工具包开发(第 22 部分):相关性仪表盘·进阶篇

第 2/3 篇

◍ 把相关系数翻译成交易动作

相关性不是拿来当学术指标看的,得直接映射到仓位动作。下面这段逻辑把 r 值切成五档:≥0.8 视为强同向,0.5~0.8 中等同向,-0.5~0.5 弱相关或无相关,-0.8~-0.5 中等反向,≤-0.8 强反向。 InterpretCorrelation 只做文字标注,真正有用的是 RecommendAction:强同向时提示避免反向持仓、可用其一做对冲;中等同向提醒开对冲单要谨慎等确认;弱相关直接标为分散化首选;中等反向建议小仓位跨市场价差;强反向则列为统计套利候选。 输入参数里 LookbackBars=100、TimeFrame=PERIOD_H1 意味着用最近 100 根 H1 棒计算,AlertThreshold=0.8 决定只有强相关才弹提醒,UpdateSeconds=5 让定时器每 5 秒重算一次。外汇与贵金属波动剧烈,这类相关在高波动期可能快速瓦解,实盘前请在 MT5 策略测试器跑一遍历史段验证阈值。

MQL5 / C++
class="type">class="kw">string InterpretCorrelation(class="type">class="kw">double r)
  {
   if(r >=  class="num">0.8)
      class="kw">return("strong positive correlation");
   if(r >=  class="num">0.5)
      class="kw">return("moderate positive correlation");
   if(r >  -class="num">0.5)
      class="kw">return("weak or no correlation");
   if(r >  -class="num">0.8)
      class="kw">return("moderate negative correlation");
   class="kw">return("strong negative correlation");
  }
class="type">class="kw">string RecommendAction(class="type">class="kw">double r, const class="type">class="kw">string s1, const class="type">class="kw">string s2)
  {
   if(r >=  class="num">0.8)
      class="kw">return("They move almost in lock-step. Avoid opposite positions; use one as hedge.");
   if(r >=  class="num">0.5)
      class="kw">return("Tendency to move together. Be cautious opening offset trades—seek confirmation.");
   if(r >  -class="num">0.5)
      class="kw">return("Little to no relation: ideal for diversification.");
   if(r >  -class="num">0.8)
      class="kw">return("Often move in opposition. Consider small inter-market spreads.");
   class="kw">return("Very strong inverse relationship—candidate for statistical arbitrage.");
  }
input class="type">class="kw">string   InstrumentsList     = "EURUSD,GBPUSD,USDJPY,USDCHF,AUDUSD,NZDUSD,XAUUSD";
input ENUM_TIMEFRAMES TimeFrame    = PERIOD_H1;
input class="type">int      LookbackBars        = class="num">100;   class=class="str">"cmt">// bars for correlation
input class="type">class="kw">double   AlertThreshold      = class="num">0.8;   class=class="str">"cmt">// threshold for alerts
input class="type">int      UpdateSeconds       = class="num">5;     class=class="str">"cmt">// timer interval
class=class="str">"cmt">// Notifications
input class="type">bool     UsePushNotifications = true;
input class="type">bool     UseEmailAlerts      = false;

初始化与定时刷新的骨架怎么搭

做多品种相关性监控,第一步是把品种清单拆开并确认不少于两个交易对。下面这段全局声明和初始化逻辑,就是整个 EA 的骨架:先解析逗号分隔的品种字符串,少于 2 个直接 INIT_FAILED,避免后面矩阵计算全崩。 OnInit 里用 StringSplit 拿到 InstrumentCount,随后对每个品种调 SymbolSelect 保证市场报价窗口有数据;ArrayResize 按 InstrumentCount 的平方开好相关系数矩阵存储,再画面板、EventSetTimer 按 UpdateSeconds 秒轮询。外汇与贵金属杠杆高,品种漏选或计时器不关都会让 MT5 后台空转吃资源。 OnDeinit 只做两件打扫的事:EventKillTimer 停表、ClearObjects 清掉画的线。OnTimer 才是干活的主体——UpdateCorrelations 算完矩阵,双层循环只跑 j=i+1 的上三角,省掉重复对;每对调 InterpretCorrelation 拿分类、RecommendAction 拿动作,PrintFormat 以 r=%.2f 两位精度打到日志,最后 RefreshValues 和 CheckAlerts 更新面板与报警。 直接把这段拷进 MT5 的 EA 模板,把 InstrumentsList 外部参数填上 "XAUUSD,EURUSD,USDJPY",编译后看日志是否每 UpdateSeconds 秒吐出一次两两相关系数与建议,就能验证骨架通不通。

MQL5 / C++
class=class="str">"cmt">//| Globals                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string Instruments[];
class="type">class="kw">double CorrArray[];
class="type">int    InstrumentCount;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">// parse & select instruments
   InstrumentCount = StringSplit(InstrumentsList, &class="macro">#x27;,&class="macro">#x27;, Instruments);
   if(InstrumentCount < class="num">2)
     {
       Print("Error: need at least two symbols.");
       class="kw">return(INIT_FAILED);
     }
   for(class="type">int i = class="num">0; i < InstrumentCount; i++)
      SymbolSelect(Instruments[i], true);
class=class="str">"cmt">// allocate storage
   ArrayResize(CorrArray, InstrumentCount * InstrumentCount);
class=class="str">"cmt">// draw dashboard & start timer
   DrawDashboard();
   EventSetTimer(UpdateSeconds);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
   EventKillTimer();
   ClearObjects();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Timer: update, interpret, display, alert                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTimer()
  {
   UpdateCorrelations();
class=class="str">"cmt">// interpret & advise for each unique pair
   for(class="type">int i = class="num">0; i < InstrumentCount; i++)
      for(class="type">int j = i + class="num">1; j < InstrumentCount; j++)
        {
         class="type">class="kw">double r    = CorrArray[i * InstrumentCount + j];
         class="type">class="kw">string cat  = InterpretCorrelation(r);
         class="type">class="kw">string tip  = RecommendAction(r, Instruments[i], Instruments[j]);
         PrintFormat(
            "%s vs %s \xBB r=%.2f: %s; Advice: %s",
            Instruments[i], Instruments[j],
            r, cat, tip
         );
        }
   RefreshValues();
   CheckAlerts();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Compute pairwise correlations                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void UpdateCorrelations()
  {
   for(class="type">int i = class="num">0; i < InstrumentCount; i++)

「用皮尔逊系数给多品种相关性画板」

相关性面板的核心不是画图,而是先把两两品种的对数收益相关系数算准。下面这段 CalcCorrelation 走的是标准皮尔逊路径:取两个品种最近 LookbackBars 根 K 线的收盘价,转成逐根收益率 r = (a[k]-a[k-1])/a[k-1],再套用协方差除以标准差的公式。 代码里有个容易踩的坑:CopyClose 返回值小于 LookbackBars 时直接 return(0),意味着历史数据不足的周期会被当成零相关处理,而不是报错。在 MT5 上加载小周期或冷门贵金属时,这个分支会悄悄让面板出现假中性格。 DrawDashboard 负责把矩阵铺到图表左上角。x0=20、y0=40 是起点,dx=100 每列宽、dy=25 每行高,列头用 clrYellow 10 号字,标题用 clrWhite 14 号字。你改 dx 到 120 就能塞下 XAUUSD 这类长名字,不至于重叠。 面板是静态的,不会随新 tick 自动刷新,需要手动重跑或挂 OnTimer。外汇与贵金属波动剧烈,相关性在风险事件下可能骤变,面板数值仅反映历史窗口,实盘决策需结合当下行情。

MQL5 / C++
for(class="type">int j = class="num">0; j < InstrumentCount; j++)
      CorrArray[i * InstrumentCount + j] =
         CalcCorrelation(Instruments[i], Instruments[j]);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Pearson correlation                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double CalcCorrelation(const class="type">class="kw">string s1, const class="type">class="kw">string s2)
  {
   class="type">class="kw">double a1[], a2[];
   if(CopyClose(s1, TimeFrame, class="num">0, LookbackBars, a1) < LookbackBars ||
      CopyClose(s2, TimeFrame, class="num">0, LookbackBars, a2) < LookbackBars)
      class="kw">return(class="num">0);
class="type">int n = LookbackBars - class="num">1;
class="type">class="kw">double r1[], r2[];
ArrayResize(r1, n);
ArrayResize(r2, n);
for(class="type">int k = class="num">1; k < LookbackBars; k++)
  {
   r1[k-class="num">1] = (a1[k] - a1[k-class="num">1]) / a1[k-class="num">1];
   r2[k-class="num">1] = (a2[k] - a2[k-class="num">1]) / a2[k-class="num">1];
  }
class="type">class="kw">double m1 = AverageArray(r1), m2 = AverageArray(r2);
class="type">class="kw">double num = class="num">0, d1 = class="num">0, d2 = class="num">0;
for(class="type">int k = class="num">0; k < n; k++)
  {
   class="type">class="kw">double da = r1[k] - m1;
   class="type">class="kw">double db = r2[k] - m2;
   num += da * db;
   d1  += da * da;
   d2  += db * db;
  }
class="kw">return (d1 > class="num">0 && d2 > class="num">0) ? num / MathSqrt(d1 * d2) : class="num">0;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Draw class="kw">static dashboard                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DrawDashboard()
  {
   ClearObjects();
   const class="type">int x0 = class="num">20, y0 = class="num">40, dx = class="num">100, dy = class="num">25;
   CreateLabel("hdr", x0, y0-class="num">30, "Correlation Dashboard", clrWhite, class="num">14);
class=class="str">"cmt">// column headers
   for(class="type">int j = class="num">0; j < InstrumentCount; j++)
      CreateLabel(
         StringFormat("col_%d", j),
         x0 + (j+class="num">1)*dx, y0,
         Instruments[j], clrYellow, class="num">10
      );
class=class="str">"cmt">// row headers & cells
   for(class="type">int i = class="num">0; i < InstrumentCount; i++)
     {
      CreateLabel(
         StringFormat("row_%d", i),
         x0, y0 + (i+class="num">1)*dy,
         Instruments[i], clrYellow, class="num">10
      );
      for(class="type">int j = class="num">0; j < InstrumentCount; j++)
        {
         class="type">class="kw">string rect = StringFormat("r_%d_%d", i, j);

◍ 热力图对象的清理与刷新逻辑

画完关联矩阵之后,图表上会堆出几十个矩形和标签对象。若每次刷新都新建而不清理,MT5 图表对象总数会线性膨胀,拖慢客户端渲染——外汇与贵金属多品种同屏时这种现象更明显,属于高风险环境下的性能隐患。 ClearObjects 用逆序循环从 ObjectsTotal(0)-1 扫到 0,逐个 ObjectDelete 删掉当前图表全部对象。逆序是关键:正序删除会让后续索引错位,漏删或越界报错。 RefreshValues 则按 InstrumentCount 双层循环,从 CorrArray 取相关系数 v。阈值写死在代码里:v>0.8 染绿、>0.5 浅绿、<-0.5 红、其余浅珊瑚;文字色按正负用 Lime 或 Red。 矩形名 r_i_j 与标签名 val_i_j 若已存在(ObjectFind 不等于 -1),才写回颜色与 DoubleToString(v,2) 的两位小数值。这样刷新时不用重建对象,只改属性和文本,开销低一个量级。 把这两段直接塞进 EA 的 OnDeinit 和定时器回调,就能在 MT5 里跑出可验证的矩阵热力图;调一下 0.8 / 0.5 阈值,颜色分层会立刻变密或变疏。

MQL5 / C++
class="type">void ClearObjects()
  {
   for(class="type">int i = ObjectsTotal(class="num">0)-class="num">1; i >= class="num">0; i--)
      ObjectDelete(class="num">0, ObjectName(class="num">0, i));
  }
class="type">void RefreshValues()
  {
   for(class="type">int i = class="num">0; i < InstrumentCount; i++)
    for(class="type">int j = class="num">0; j < InstrumentCount; j++)
     {
      class="type">class="kw">double v = CorrArray[i * InstrumentCount + j];
      class="type">class="kw">color bg = (v >  class="num">0.8 ? clrGreen :
                  v >  class="num">0.5 ? clrLightGreen :
                  v < -class="num">0.5 ? clrRed  : clrLightCoral);
      class="type">class="kw">color fg = (v >= class="num">0  ? clrLime : clrRed);
      class="type">class="kw">string rect = StringFormat("r_%d_%d", i, j);
      if(ObjectFind(class="num">0, rect) != -class="num">1)
         ObjectSetInteger(class="num">0, rect, OBJPROP_COLOR, bg);
      class="type">class="kw">string lbl = StringFormat("val_%d_%d", i, j);
      if(ObjectFind(class="num">0, lbl) != -class="num">1)
       {
         ObjectSetString(class="num">0, lbl, OBJPROP_TEXT, DoubleToString(v, class="num">2));

常见问题

设定阈值如±0.7:高于0.7倾向同向下单或避开设反向单,低于-0.7可考虑配对对冲,中间区间当作噪音不动作。
漏掉定时器销毁与对象句柄数组清空。每次重绘前先 DeleteObjectsByPrefix 再重建,避免旧对象堆积拖垮终端。
可以。小布盯盘的 AIGC 已内置相关性诊断,打开对应品种页即可看到实时热力与突变提醒,不用自己写刷新逻辑。
未必。0.2属弱相关,同涨可能来自共同避险情绪而非稳定线性关联,样本窗口过短也会放大偶然同步。
在 OnDeinit 和每次刷新头部调用按前缀删除对象,并置空索引数组;只改颜色不删对象会造成重叠残影。