风险提示:本文为软件开发与技术服务类技术文章,仅用于MQL5程序化开发的技术交流与软件工程实践探讨。文中涉及的矩阵与向量运算、回归系数拟合、相关性监控等内容,均为软件工程原理演示与教学用途,不构成任何投资建议、交易策略推荐或收益预期。外汇、贵金属、CFD等杠杆交易存在较高风险,可能导致重大本金损失。文中代码片段仅供学习参考,需经人工审查、编译测试与充分的模拟盘验证后方可使用,直接投入实盘可能造成资金损失。本网站仅提供软件开发技术服务,不涉及任何交易平台运营、经纪业务或投资咨询业务。
在 MQL5 里做多因子回归、多品种相关性这类计算,过去只有两条路:手写三重循环的数组运算,或者把数据丢给 DLL、Python 再等结果回来。前者代码长、下标容易错,后者要跨进程搬数据。从 build 3180 起,MQL5 有了 matrix 与 vector 类型;build 4570 接入 OpenBLAS 之后,矩阵乘、求逆、协方差、分解都有了原生实现。这篇讲三件事:哪些计算值得搬进原生矩阵、内存怎么复用才不拖累 OnTick、求逆失败和因子共线这类坑怎么在代码里堵住。
一、外部调用的成本,藏在四个环节里
先说清楚外部调用贵在哪里,才知道原生省掉了什么。把一个 5 因子、200 根 K 线的设计矩阵交给 Python,路径是:EA 把 double 数组序列化成字节流,走 socket 或共享内存,外部进程反序列化后计算,结果再原路返回。这条链上有四段固定成本:序列化与反序列化的内存拷贝、用户态与内核态的切换、等待返回的挂起时间、以及外部依赖本身的可用性。
第三段容易被忽略。OnTick 是同步执行的,等外部结果就等于让这个 tick 的处理时间由外部进程的响应时间决定。第四段则经常在换机器、换系统、换终端版本时集中爆发:端口被占、位数不匹配、依赖库缺失、进程被安全软件杀掉,任何一种都会让 EA 在一个没有预期的地方停住。
原生矩阵运算把这条链压成一段:数据在 EA 进程的堆上,MatMul 直接调用本地实现,没有序列化,没有进程边界,也没有等待。省掉的就是上面四段,换来一个必须接受的约束——计算得在本进程内算完,出错也得在本进程内兜住。
知识点:matrix 与 vector 类型自 build 3180 起提供;@ 运算符、Cov/Std/Var 的 ddof 参数自 build 5100 起提供;OpenBLAS 相关方法自 build 4570 起逐步引入。在老终端上编译不过,先看 build 号。
二、matrix 是对象,不是数组
matrix 和 vector 不是语法糖包起来的数组,而是带尺寸和类型的对象:matrix 有 Rows()、Cols(),vector 有 Size(),内存按需动态分配。创建走静态方法 matrix::Zeros()、Ones()、Eye()、Identity()、Full()、Tri(),或者先 Init(rows, cols) 再逐元素填。
MQL5 没有 new/delete,对象离开作用域自动释放,不需要像 C++ 那样手动配对。但”不用管释放”不等于”没有分配成本”:每次构造、每次 Resize 都要在堆上要一块内存。真正决定性能的不是释放,是分配次数。
| 类别 | 方法 | 用途 | 版本前提 |
|---|---|---|---|
| 创建 | Zeros、Ones、Eye、Identity、Full、Tri、Init | 预分配与单位阵,避免循环里反复构造 | 3180+ |
| 乘法 | MatMul、GeMM、@ | 矩阵乘;GeMM 为 C=αAB+βC,写回已有对象 | MatMul 3180+;GeMM 4570+;@ 5100+ |
| 向量积 | Dot、Inner、Outer、Kron | 点积、内积、外积、Kronecker 积 | 3180+ |
| 线性代数 | Inv、PInv、Det、Rank、Cond、Power | 逆、伪逆、行列式、秩、条件数、整数幂 | 3180+ |
| 求解 | Solve、LstSq | 线性方程组、最小二乘解 | 3180+ |
| 统计 | Cov、CorrCoef、Correlate、Convolve、Std、Var | 协方差、相关系数、互相关、卷积、标准差、方差 | 3180+;ddof 参数 5100+ |
| 分解 | LU、LUP、QR、Cholesky、SVD、Eig、EigVals | 矩阵分解与特征值,OpenBLAS 扩展自 4570+ | 3180+ 起逐步扩充 |
重点提示:MatMul 每次返回新的 matrix/vector 对象,也就是每次都新分配;GeMM 把结果写进已有对象,尺寸不变时不重新分配。逐 tick 重算的场景,用 GeMM 配预分配容器。
还有一个容易被忽略的性质:矩阵在内存里是连续缓冲区,官方文档里可以直接按 float 指针传给 DLL,说明它没有额外的间接层。这也意味着 Reshape 只是改形状不改数据,而 Resize 会重新要内存——两者别混用。
三、实战一:多变量线性回归
多因子融合常见的形式是线性加权:score = β₀ + β₁f₁ + … + βₖfₖ。系数不该拍脑袋定,最小二乘给的是解析解 β = (XᵀX)⁻¹Xᵀy,推导见 Greene《Econometric Analysis》第 3 章的最小二乘代数部分。X 是 n×k 的设计矩阵,每行一个样本、每列一个因子,y 是 n 维目标向量。
用原生矩阵写,核心只有四步:转置、两个乘法、一次求逆。三个细节必须处理:样本数要大于因子数,否则 XᵀX 必然奇异;求逆前先看行列式,共线因子会让 Det 趋近 0;Inv() 不可逆时返回的是空矩阵而不是抛异常,得自己判。
// 前提:matrix/vector 需 build 3180+,GeMM 走 OpenBLAS 需 build 4570+
matrix g_XtX; // 复用容器,只在因子数变化时重建
vector g_XtY;
bool FitOLS(const matrix &X, const vector &y, vector &beta)
{
ulong n = X.Rows(), k = X.Cols();
if(n <= k || n < 2) return(false); // 样本数必须大于因子数
matrix Xt = X.Transpose(); // k×n,转置会新建一份数据
if(!g_XtX.GeMM(Xt, X, 1.0, 0.0)) return(false); // k×k,写回已有容器
if(!g_XtY.GeMM(Xt, y, 1.0, 0.0)) return(false); // k×1
if(MathAbs(g_XtX.Det()) < 1e-12) return(false); // 因子共线,先停手
matrix inv = g_XtX.Inv(); // 不可逆时返回空矩阵
if(inv.Rows() == 0 || inv.HasNan() > 0) return(false);
beta = inv.MatMul(g_XtY); // β = (XᵀX)⁻¹Xᵀy
return(beta.Size() == k);
}
风险提示:共线因子下 (XᵀX)⁻¹ 会把数值误差放大,系数正负号可能整段翻转。这不是代码 bug,是模型本身不可辨识,正确处理是剔除因子或改用正则化,不是调大迭代次数。
操作参考:工程上更省事的是 vector beta = X.LstSq(y),它对非方阵与退化矩阵都能给出最小二乘解,不用自己组 XᵀX,也绕开了求逆。先看 Cond() 与 Det():条件数大就走 LstSq 或 PInv,可控再走正规方程。
四、实战二:用相关矩阵盯住品种间的同步性
多品种 EA 容易死在”以为分散了,其实是一个方向”。判定不能靠看图,要算。把每个品种的收益率排成一行,得到 n×m 的矩阵(n 个品种、m 个观测),Cov() 给出 n×n 协方差矩阵,CorrCoef() 给出同尺寸的 Pearson 相关矩阵。两个都要:协方差带量纲,用来算组合方差;相关系数无量纲,用来跨品种比较。
布局上有个默认值要记住:Cov() 与 CorrCoef() 的 rowvar 参数默认为 true,意思是每一行是一个变量、观测在列上。如果数据是每列一个品种,要么先转置,要么传 rowvar=false,否则算出来的是时间点之间的相关,不是品种之间的。ddof 同样要说清楚:Cov() 的 ddof 默认为 1,除数是 N-1,属样本口径;传 0 变成总体口径,除数 N。
// 前提:matrix/vector 需 build 3180+,ddof 参数需 build 5100+
bool MaxCorr(const string &symbols[], const int bars, double &maxRho)
{
int n = ArraySize(symbols);
if(n < 2 || bars < 8) return(false);
matrix R;
if(!R.Init(n, bars - 1)) return(false); // 行=品种,列=观测
double cl[];
for(int s = 0; s < n; s++)
{
if(CopyClose(symbols[s], PERIOD_H1, 0, bars, cl) != bars) return(false);
for(int i = 0; i < bars - 1; i++)
{
if(cl[i + 1] <= 0) return(false);
R[s][i] = (cl[i] - cl[i + 1]) / cl[i + 1]; // 收益率口径全文统一
}
}
matrix corr = R.CorrCoef(); // 默认 rowvar=true,行即变量
maxRho = 0;
for(int a = 0; a < n; a++)
for(int b = a + 1; b < n; b++)
maxRho = MathMax(maxRho, MathAbs(corr[a][b]));
return(true);
}
知识点:Cov 返回的矩阵对角元就是各品种方差,非对角元才是协方差。组合方差等于 wᵀΣw,用两次 MatMul 就能算完,不用展开循环。
重点提示:rowvar 与 ddof 是两个静默参数,写错了不报错、结果偏一截。策略里所有相关性阈值都要跟固定口径绑定,改口径就得重标阈值。
拿到相关矩阵后,取上三角的最大绝对值当同步度。同步度本身不是信号,它的跳变才是:跟滚动基线比,数值突然抬升说明原本分散的品种开始同向,这时候加仓等于放大同一个敞口。判定用比值不用绝对值,避免不同品种波动率差异干扰。
五、OnTick 里的三个致命写法
第一个,每 tick 重新分配。在 OnTick 里声明 matrix X(…),等于每个 tick 都向堆要一块内存再还回去,长期运行后堆碎片会让分配变慢,极端情况下分配失败返回空矩阵,后续计算全废。改法是把容器提到全局,只在因子数变化时 Init。
第二个,参数值传递。void f(matrix X) 会触发一次完整深拷贝,几百乘几百的矩阵拷一次就是几十万次浮点搬运。统一写成 const matrix &X,编译器不会替你优化这件事。第三个,不判奇异。Inv() 失败返回空矩阵,空矩阵参与后续 MatMul 得到的还是空矩阵或 NaN,一路传进信号层,最后表现为”EA 突然不下单了”,而日志里什么都没报。
| 现象 | 常见写法 | 代价 | 改法 |
|---|---|---|---|
| 每 tick 新建 | OnTick 里声明 matrix X(…) | 反复分配释放,长跑后堆碎片 | 容器提到全局,用 GeMM 写回 |
| 参数值传递 | void f(matrix X) | 整个矩阵深拷贝一次 | const matrix &X |
| 求逆不判奇异 | Inv() 之后直接使用 | 空矩阵或 NaN 污染系数 | Det、Rows、HasNan 三重判断 |
| 共线因子硬拟合 | 高度相关因子全塞进 X | 系数数值不稳,正负号乱跳 | 先用 CorrCoef 剔除,或改用 LstSq |
// 只在产生新 K 线时重算,其余 tick 直接返回
datetime g_lastBar = 0;
vector g_beta; // 复用,不在 OnTick 里重新声明
void OnTick()
{
datetime bar = iTime(_Symbol, PERIOD_H1, 0);
if(bar == g_lastBar) return; // 同一根 K 线内不重算
g_lastBar = bar;
if(!BuildDesignMatrix(g_X, g_y)) return; // g_X、g_y 同样预分配
if(!FitOLS(g_X, g_y, g_beta)) // 失败就沿用上一组系数
{
Print("OLS failed: collinear factors, keep previous beta");
return;
}
UseBeta(g_beta); // 系数交给信号层,不在这里下单
}
风险提示:矩阵计算失败必须是显式分支,不能靠默认值兜底。系数算不出来就沿用上一组或停止开新仓,不要让空矩阵悄悄变成全零系数继续跑。
操作参考:上线前用 GetMicrosecondCount() 把单次计算耗时打进日志,在策略测试器里跑一遍长周期,确认单 tick 计算时间远小于平均 tick 间隔,再考虑实盘。
进阶思考:更彻底的做法是把特征工程移出 OnTick:OnTimer 里按固定周期算一次系数,OnTick 只读结果。计算频率与行情频率解耦之后,矩阵运算的开销就不再随 tick 数量增长。
原生矩阵不是把 Python 搬进 MT5,它解决的是中等复杂度、低延迟要求的那一段:因子数几个到几十个,样本几百到几千。再往上——深度模型、大规模优化——外部进程仍然是对的选择。判断标准不是哪个更先进,而是这段计算能不能在本进程内算完、算错能不能在本进程内兜住。
风险提示:本文内容仅供软件开发与技术学习参考,不构成任何投资建议或交易指导。程序化交易存在较高风险,可能导致重大本金损失。文中涉及的回归拟合、相关性监控与矩阵复用代码均为技术原理演示,未经完整工程化封装,直接复制使用可能存在边界条件遗漏、因子数变化时容器未重建、收益率口径与阈值不匹配、多品种历史数据长度不一致导致 CopyClose 返回不足等缺陷,需经人工审查、编译测试与充分的模拟盘验证。文中 API 名称、返回类型与版本前提依据 MQL5 官方文档与 MetaQuotes 版本发布说明整理,具体行为以对应 build 的官方文档与实测结果为准。实盘应用前请自行决策并承担相应风险。本网站仅提供软件开发技术服务,不涉及任何交易平台运营、经纪业务或投资咨询业务。
第三方免责声明:本文仅代表作者个人技术观点,与任何经纪商、交易平台或金融机构无关。文中提及的 MetaTrader 5、MQL5、MetaEditor、matrix、vector、MatMul、GeMM、Transpose、Inv、PInv、Det、Rank、Cond、Power、Solve、LstSq、Cov、CorrCoef、Std、Var、LU、QR、Cholesky、SVD、Eig、CopyClose、GetMicrosecondCount、OnTick、OnTimer、Init、Rows、Cols、Size、HasNan、Reshape、Resize 等名称与产品,其商标与权利归各自所有者所有,本文仅作技术说明与交流引用,不构成任何推荐、背书或商业合作关系。文中关于矩阵内存分配、GeMM 与 MatMul 的差异、rowvar 与 ddof 默认值的描述依据公开文档整理,具体表现以各版本官方说明与实测为准。读者应自行核实相关信息,因使用本文内容而产生的直接或间接损失,作者不承担任何责任。本文不推荐任何具体的经纪商、交易平台或金融产品。
扫码关注,获取更多EA开发干货
微信公众号
视频号
