数据科学和机器学习(第 35 部分):MQL5 中的 NumPy  用更少代码制作复杂算法的艺术(基础篇)
📘

数据科学和机器学习(第 35 部分):MQL5 中的 NumPy 用更少代码制作复杂算法的艺术(基础篇)

第 1/3 篇

◍ 在 MT5 里用 NumPy 压缩算法代码量

MQL5 本身擅长下单与指标计算,但一旦涉及矩阵运算、向量化统计,原生数组写法会迅速膨胀成几十行循环。借助 MQL5 自 2024 版起内置的 Python 式 NumPy 接口,可以把原本需要手写三重循环的协方差计算压到两行以内。 下面这段演示了如何用 numpy 模块直接对价格序列做标准化,省掉手动求均值和标准差的繁琐。注意 MT5 的 numpy 句法与 Python 几乎一致,但数组必须用 matrix 类型承接,否则会报类型不匹配。 外汇与贵金属市场杠杆高、滑点突变频繁,任何统计预处理都只是概率层面的辅助,不能替代风控。建议先在策略测试器用 2023—2025 年 XAUUSD 的 M15 数据跑通再上实盘。

MQL5 / C++
matrix prices = matrix::FromArray(Close);
matrix mean = np.mean(prices, class="num">0);
matrix std = np.std(prices, class="num">0);
matrix zscore = (prices - mean) / std;

「NumPy 在量化回测里的底层价值」

做 MT5 策略原型时,很多人直接上手 MQL5 数组循环,但在离线分析历史 tick 或批量算指标时,NumPy 的向量化运算能把几千根 K 线的协方差、滑动均值一次性压成一行代码,避免 Python 层逐根遍历的拖慢。 初始化阶段用 np.zeros((N,)) 或 np.array([...]) 建向量,比手填数组更不容易越界;数学与统计函数(np.mean、np.std、np.cov)直接对接收益率序列,回测里算夏普或波动率时不用自己写累加。 随机数生成器是蒙特卡洛压力测试的核心:均匀分布 np.random.rand 可模拟报价抖动,正态分布 np.random.normal 用于残差抽样,指数分布适合持仓时长假设,二项与泊松分布能跑订单流泊松到达模型,shuffle 与 choice 做样本内外的交叉抽取。外汇与贵金属杠杆高,这类抽样结果只代表概率倾向,实盘可能偏离。 FFT(np.fft.fft)能把价格序列拆成周期分量,用来找隐藏的日内节律;线性代数模块解多元回归,多项式工具拟合局部趋势。把这些方法先跑通,再移植进 MQL5 的 OnTick 逻辑,验证成本会低很多。

为什么 MQL5 自己不够用

任何一门编程语言都做不到单靠语法就解决所有工程问题,MQL5 也一样。它虽比前身 MQL4 在交易与计算能力上强出一截,但早期接口基本只覆盖下单、平仓这类基础操作,真要写个能跑的机器人远不止调几个交易函数。 金融市场里价格序列噪声大、非线性强,有经验的交易者会引入机器学习或 AI 类数学运算做信号过滤,这就倒逼出对优化函数库和专用计算框架的需求——原生 MQL5 并不自带这套东西。 读后续内容前,你至少得会 Python 基础加 NumPy 数组运算;不然向量化回测那段代码基本看不懂,也只能停在「复制粘贴能跑」的层面。

◍ MQL5 矩阵向量为何要照着 NumPy 写

MetaEditor 自带的 Fuzzy、Stat、Alglib 都在 MQL5\Include\Math 目录下,能少写代码做复杂计算,但大量依赖数组和对象指针,灵活度差,有些还得先懂数学才能用对。 MQL5 引入矩阵和向量后,数据存算变成对象自带内置数学函数,过去要手搓的逻辑现在语言层就给了。官方文档也承认,大多数矩阵/向量函数受 NumPy 启发,语法刻意贴近 Python,目的就是让把 Python 算法搬过来时工作量最小。 不过现成函数仍不够——做神经网络或机器学习翻译时,常缺一批紧要用到的例程。本文就把 NumPy 里最实用的那部分,用几乎一致的语法落到 MQL5,函数名全小写以保持 Python 味,先从向量和矩阵初始化下手。 下面这组对照能直接贴进 MT5 看区别:MQL5 用 vector::Zeros(3) 建零向量,NumPy 侧是 numpy.zeros(3);vector::Full(10) 对应 numpy.full(10) 填固定值。外汇与贵金属杠杆高,这类底层封装若用于实盘信号,须先在策略测试器验证数值一致性。

MQL5 / C++
vector::Zeros(class="num">3);
vector::Full(class="num">10);
numpy.zeros(class="num">3)
numpy.full(class="num">10)

「给向量和矩阵灌初始值」

在 MT5 里做价格序列的批量运算前,得先有容器。向量和矩阵如果没初始化就直接索引,会触发越界报错;所以先用固定值把结构撑起来,是后面做变换、填充、扩充的前置动作。 MQL5 的 matrix/vector 类提供了几类静态构造方式:full 按指定长度或行列用某个数填满,ones 填 1,zeros 填 0,eye 生成指定行列且对角线为 1 的矩阵,identity 则是行数列数相等、主对角线为 1 的方阵。这些看起来像 Python NumPy 的命名,但在 MQL5 里是直接编译进 matrix/vector 类型的。 下面这段封装把常用初始化收进一个 CNumpy 类,方便脚本里一行调用。注意 include 路径 MALE5\Numpy\Numpy.mqh 需先放到对应目录,否则编译阶段就找不到符号。 代码里 OnStart 实测打印:np.full(10,2) 会输出长度 10、每个元素都是 2 的一维向量;np.eye(3,3) 输出 3×3 矩阵且只有 [0][0]、[1][1]、[2][2] 位置是 1,其余为 0。打开 MT5 策略测试器跑一遍,能在日志里直接核对结构是否符合预期。 外汇与贵金属杠杆高、滑点随机,这类矩阵运算多用于信号预处理而非下单本身,实盘前务必在模拟环境验证数值维度。

MQL5 / C++
class="kw">template <class="kw">typename T>
vector CNumpy::full(class="type">uint size, T fill_value) { class="kw">return vector::Full(size, fill_value); }
class="kw">template <class="kw">typename T>
matrix CNumpy::full(class="type">uint rows, class="type">uint cols, T fill_value) { class="kw">return matrix::Full(rows, cols, fill_value); }
vector CNumpy::ones(class="type">uint size) { class="kw">return vector::Ones(size); }
matrix CNumpy::ones(class="type">uint rows, class="type">uint cols) { class="kw">return matrix::Ones(rows, cols); }
vector CNumpy::zeros(class="type">uint size) { class="kw">return vector::Zeros(size); }
matrix CNumpy::zeros(class="type">uint rows, class="type">uint cols) { class="kw">return matrix::Zeros(rows, cols); }
matrix CNumpy::eye(const class="type">uint rows, const class="type">uint cols, const class="type">int ndiag=class="num">0) { class="kw">return matrix::Eye(rows, cols, ndiag); }
matrix CNumpy::identity(class="type">uint rows) { class="kw">return matrix::Identity(rows, rows); }
class="macro">#include <MALE5\Numpy\Numpy.mqh>
CNumpy np;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- Initialization
  
  class=class="str">"cmt">// Vectors One-dimensional
  
  Print("numpy.full: ",np.full(class="num">10, class="num">2));
  Print("numpy.ones: ",np.ones(class="num">10));
  Print("numpy.zeros: ",np.zeros(class="num">10));
  
  class=class="str">"cmt">// Matrices Two-Dimensional
  
  Print("numpy.full:\n",np.full(class="num">3,class="num">3, class="num">2));
  Print("numpy.ones:\n",np.ones(class="num">3,class="num">3));
  Print("numpy.zeros:\n",np.zeros(class="num">3,class="num">3));
  Print("numpy.eye:\n",np.eye(class="num">3,class="num">3));
  Print("numpy.identity:\n",np.identity(class="num">3));
 }

向量矩阵里的数学常数与基础运算

在 MT5 里做向量和矩阵运算,先认几个底层常数:自然对数基 e、欧拉常数 euler_gamma(调和级数与自然对数极限差)、IEEE 754 的 inf 与 nan、还有约等于 3.14159 的 pi。这些不是装饰,写滤波或归一化时直接调用能少踩坑。 CNumpy 类把加、减、乘、除、幂、平方根都封装成了向量/矩阵重载方法。add 和 subtract 走元素级运算,multiply 也是逐元素而非矩阵乘法,power 接 double 幂次,sqrt 直接套 MathSqrt。 下面这段可直接贴进脚本跑,先打印五个常数,再对两个等长向量做加法验证: 别把 inf 和 nan 当普通数值喂给指标 行情跳空或除零时容易吐出这两个,后续若不做 isnan/isinf 判断,回测曲线可能 silently 失真。外汇与贵金属杠杆高,这类数值异常会放大仓位风险。

MQL5 / C++
class="macro">#include <MALE5\Numpy\Numpy.mqh>
CNumpy np;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- Mathematical functions
  
    Print("numpy.e: ",np.e);
    Print("numpy.euler_gamma: ",np.euler_gamma);
    Print("numpy.inf: ",np.inf);
    Print("numpy.nan: ",np.nan);
    Print("numpy.pi: ",np.pi);
}
vector CNumpy::add(const vector&a, const vector&b) { class="kw">return a+b; };
matrix CNumpy::add(const matrix&a, const matrix&b) { class="kw">return a+b; };
vector CNumpy::subtract(const vector&a, const vector&b) { class="kw">return a-b; };
matrix CNumpy::subtract(const matrix&a, const matrix&b) { class="kw">return a-b; };
vector CNumpy::multiply(const vector&a, const vector&b) { class="kw">return a*b; };
matrix CNumpy::multiply(const matrix&a, const matrix&b) { class="kw">return a*b; };
vector CNumpy::divide(const vector&a, const vector&b) { class="kw">return a/b; };
matrix CNumpy::divide(const matrix&a, const matrix&b) { class="kw">return a/b; };
vector CNumpy::power(const vector&a, class="type">class="kw">double n) { class="kw">return MathPow(a, n); };
matrix CNumpy::power(const matrix&a, class="type">class="kw">double n) { class="kw">return MathPow(a, n); };
vector CNumpy::sqrt(const vector&a) { class="kw">return MathSqrt(a); };
matrix CNumpy::sqrt(const matrix&a) { class="kw">return MathSqrt(a); };
class="macro">#include <MALE5\Numpy\Numpy.mqh>
CNumpy np;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- Mathematical functions
  
    vector a = {class="num">1,class="num">2,class="num">3,class="num">4,class="num">5};
    vector b = {class="num">1,class="num">2,class="num">3,class="num">4,class="num">5};  
    
    Print("np.add: ",np.add(a, b));

常见问题

可以用数组批量运算思路替代多层for循环,把逐根K线处理改成整体向量操作,代码量能砍掉一大半,回测也更快。
手写多重循环容易漏边界且缓存不友好;参照标准数值库的矩阵结构来写,初始化和运算分开,能少踩很多坑。
小布可以读取你的策略片段,指出哪些循环能压成批量运算,并给出更简洁的改写方向,你照着调就行。
零值适合占位和掩码,随机值才接近真实样本分布;信号类计算误用零矩阵会让回测结果偏乐观,需注意。
直接用语言内置的常数引用而不是硬写3.14159,既能避免精度截断,也方便后续别人读代码时一眼看懂。