价格行为分析工具包开发(第 22 部分):相关性仪表盘·进阶篇
◍ 把相关系数翻译成交易动作
相关性不是拿来当学术指标看的,得直接映射到仓位动作。下面这段逻辑把 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 策略测试器跑一遍历史段验证阈值。
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 秒吐出一次两两相关系数与建议,就能验证骨架通不通。
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。外汇与贵金属波动剧烈,相关性在风险事件下可能骤变,面板数值仅反映历史窗口,实盘决策需结合当下行情。
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 阈值,颜色分层会立刻变密或变疏。
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));