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

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

第 2/3 篇

「用 NumPy 接口在 MT5 里跑数组运算」

MT5 的 Python 环境里直接调 NumPy,能把 K 线数组当矩阵算,不用自己写循环。下面这段把两个数组 a、b 做了减、乘、除,还对 a 做了平方、开方、对数等变换,输出会直接打印在终端。 外汇和贵金属波动非线性,用 np.log1p 处理收益率能压住极端跳空带来的数值畸变,但高杠杆下仍属高风险,结果只代表历史数组关系。 复制进 MT5 的 Python 脚本,先定义 a、b 为等长序列(例如 close[0:10] 与 close[10:20]),跑完看 Print 输出,验证各函数返回维度是否一致。

MQL5 / C++
Print("np.subtract: ",np.subtract(a, b));
Print("np.multiply: ",np.multiply(a, b));
Print("np.divide: ",np.divide(a, b));
Print("np.power: ",np.power(a, class="num">2));
Print("np.sqrt: ",np.sqrt(a));
Print("np.log: ",np.log(a));
Print("np.log1p: ",np.log1p(a));

◍ 给向量矩阵做统计量的封装思路

在特征工程里,均值、方差、标准差这些量值比加减乘除更能说明一段行情的分布特征。MT5 自带的 vector 和 matrix 类型已经内置了统计方法,直接调就能拿到结果,不需要自己写循环。 为了贴近 NumPy 的使用习惯,可以把这些内置方法再包一层,统一成 sum / mean / var / std / median 等短名函数,并支持按轴(AXIS_VERT 默认)计算。这样在写机器学习类指标时,代码读起来更顺。 下面这段封装覆盖了合计、算术平均、方差、标准差、中位数,以及百分位、分位、累积和、累积乘积。注意 percentile 和 quantile 接收的是 int 型参数,实际调用时按 0–100 或分位整数传入,返回 double 或 vector。 外汇与贵金属波动剧烈,用这些统计量做归一化或阈值判断时,样本窗口过短可能放大噪声,建议先在 MT5 策略测试器里用不同窗口长度跑一遍分布。

MQL5 / C++
class="type">class="kw">double sum(const vector& v) { class="kw">return v.Sum(); }
class="type">class="kw">double sum(const matrix& m) { class="kw">return m.Sum(); };
vector sum(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Sum(axis); };
class="type">class="kw">double mean(const vector& v) { class="kw">return v.Mean(); }
class="type">class="kw">double mean(const matrix& m) { class="kw">return m.Mean(); };
vector mean(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Mean(axis); };
class="type">class="kw">double var(const vector& v) { class="kw">return v.Var(); }
class="type">class="kw">double var(const matrix& m) { class="kw">return m.Var(); };
vector var(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Var(axis); };
class="type">class="kw">double std(const vector& v) { class="kw">return v.Std(); }
class="type">class="kw">double std(const matrix& m) { class="kw">return m.Std(); };
vector std(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Std(axis); };
class="type">class="kw">double median(const vector& v) { class="kw">return v.Median(); }
class="type">class="kw">double median(const matrix& m) { class="kw">return m.Median(); };
vector median(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Median(axis); };
class="type">class="kw">double percentile(const vector &v, class="type">int value) { class="kw">return v.Percentile(value); }
class="type">class="kw">double percentile(const matrix &m, class="type">int value) { class="kw">return m.Percentile(value); }
vector percentile(const matrix &m, class="type">int value, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Percentile(value, axis); }
class="type">class="kw">double quantile(const vector &v, class="type">int quantile_) { class="kw">return v.Quantile(quantile_); }
class="type">class="kw">double quantile(const matrix &m, class="type">int quantile_) { class="kw">return m.Quantile(quantile_); }
vector quantile(const matrix &m, class="type">int quantile_, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Quantile(quantile_, axis); }
vector cumsum(const vector& v) { class="kw">return v.CumSum(); };
vector cumsum(const matrix& m) { class="kw">return m.CumSum(); };
matrix cumsum(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.CumSum(axis); };
vector cumprod(const vector& v) { class="kw">return v.CumProd(); }
vector cumprod(const matrix& m) { class="kw">return m.CumProd(); };
matrix cumprod(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.CumProd(axis); };

用 Numpy 风格封装做向量统计

MT5 里直接拿 vector / matrix 跑统计太啰嗦,包一层 Numpy 风格接口能让回测脚本清爽很多。下面这组重载把 average、argmax、min、max、prod、ptp 都桥接到了原生方法,vector 和 matrix 各有一份,matrix 还能传 ENUM_MATRIX_AXIS 指定按行或按列算。 实际跑一段验证最直观:构造 vector z = {1,2,3,4,5},调 np.sum 得 15、np.mean 得 3.0、np.median 得 3.0。外汇和贵金属波动大,这类聚合值只反映样本区间特征,用于风控或信号平滑时仍属概率参考,杠杆品种高风险不变。 把统计函数收进 CNumpy 后,OnStart 里只留 Print 调用,后续接小布盯盘的逻辑就只用 np.xxx 这一种写法,改参数不用来回翻底层。

MQL5 / C++
class="type">class="kw">double average(const vector &v, const vector &weights) { class="kw">return v.Average(weights); }
class="type">class="kw">double average(const matrix &m, const matrix &weights) { class="kw">return m.Average(weights); }
vector average(const matrix &m, const matrix &weights, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Average(weights, axis); }
class="type">ulong argmax(const vector& v) { class="kw">return v.ArgMax(); }
class="type">ulong argmax(const matrix& m) { class="kw">return m.ArgMax(); }
vector argmax(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.ArgMax(axis); };
class="type">ulong argmin(const vector& v) { class="kw">return v.ArgMin(); }
class="type">ulong argmin(const matrix& m) { class="kw">return m.ArgMin(); }
vector argmin(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.ArgMin(axis); };
class="type">class="kw">double min(const vector& v) { class="kw">return v.Min(); }
class="type">class="kw">double min(const matrix& m) { class="kw">return m.Min(); }
vector min(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Min(axis); };
class="type">class="kw">double max(const vector& v) { class="kw">return v.Max(); }
class="type">class="kw">double max(const matrix& m) { class="kw">return m.Max(); }
vector max(const matrix& m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Max(axis); };
class="type">class="kw">double prod(const vector &v, class="type">class="kw">double initial=class="num">1.0) { class="kw">return v.Prod(initial); }
class="type">class="kw">double prod(const matrix &m, class="type">class="kw">double initial) { class="kw">return m.Prod(initial); }
vector prod(const matrix &m, class="type">class="kw">double initial, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Prod(axis, initial); }
class="type">class="kw">double ptp(const vector &v) { class="kw">return v.Ptp(); }
class="type">class="kw">double ptp(const matrix &m) { class="kw">return m.Ptp(); }
vector ptp(const matrix &m, ENUM_MATRIX_AXIS axis=AXIS_VERT) { class="kw">return m.Ptp(axis); }
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">//--- Statistical functions
  
    vector z = {class="num">1,class="num">2,class="num">3,class="num">4,class="num">5};
  
    Print("np.sum: ", np.sum(z));
    Print("np.mean: ", np.mean(z));
    Print("np.var: ", np.var(z));
    Print("np.std: ", np.std(z));
    Print("np.median: ", np.median(z));

「用 numpy 式接口扒开向量统计底牌」

在 MT5 的向量环境里,np.percentile(z,75) 与 np.quantile(z,75) 给出的都是 z 序列的第 75 分位值,两者数值等价,只是入参语义一个用百分比一个用概率。 np.argmax 和 np.argmin 返回的是极值所在的下标而非数值,配合 np.max、np.min 才能拿到真实峰谷价,写警报逻辑时别只存了下标忘了取数。 np.cumsum 与 np.cumprod 把逐元素累加、累乘展开成新向量,np.prod 则直接吐出全序列乘积;外汇与贵金属波动具有高杠杆高风险,这类聚合值只反映历史样本特征,对后续走势仅具概率层面的参考。 带权平均用 np.average(z, weights) 显式传向量权重,例如 {0.2,0.1,0.5,0.2,0.01} 会让第 3 个分量主导结果;np.ptp 是极差(max-min),一眼看清该段样本的价格跨度。开 MT5 把下面代码贴进脚本跑一遍,就能核对自己的数据分布。

MQL5 / C++
  Print("np.percentile: ", np.percentile(z, class="num">75));
  Print("np.quantile: ", np.quantile(z, class="num">75));
  Print("np.argmax: ", np.argmax(z));
  Print("np.argmin: ", np.argmin(z));
  Print("np.max: ", np.max(z));
  Print("np.min: ", np.min(z));
  Print("np.cumsum: ", np.cumsum(z));
  Print("np.cumprod: ", np.cumprod(z));
  Print("np.prod: ", np.prod(z));
  
  vector weights = {class="num">0.2,class="num">0.1,class="num">0.5,class="num">0.2,class="num">0.01};
  Print("np.average: ", np.average(z, weights));
  Print("np.ptp: ", np.ptp(z));

◍ 用随机种子锁住可复现性

NumPy 的 random 子模块从 1.17 版起底层换成 PCG64 生成器,相比 MQL5 自带的 MathRand() 一类函数,它能按指定统计分布抽数,而后者只能给均匀整数,做神经网络初值或样本特征时往往不够用。 训练迭代型模型最怕每次跑结果都不一样,所以先钉死种子是关键一步。下面这行就是把全局随机流固定到 42 这个状态。 在 MT5 里若想对照验证,可先记 MathRand() 的序列,再比同样种子下 NumPy 抽出的正态样本差异;外汇与贵金属数据回测用随机特征时波动放大,属高风险操作,分布不对结论可能完全偏掉。

MQL5 / C++
np.random.seed(class="num">42);

在 MT5 里造一段均匀分布随机数

均匀分布意味着落在区间 [low, high] 内任意点的概率相等,生成时只需把 [0,1] 随机数按线性映射到目标区间。Numpy.mqh 里用 CRandom 结构封装了这件事,再挂到 CNumpy 类下,调用写法和 Python 的 np.random.uniform 几乎一致。 template<typename T> vector uniform(T low, T high, uint size=1) 是核心:先建一个长度为 size 的零向量,再逐位填入 low + (high-low)*rand()/RAND_MAX。rand() 被归一化到 [0,1] 浮点,避免整数截断导致分布偏移。 实际跑一次 np.random.uniform(1,10,10),日志里吐出十个值,最小 1.939、最大 9.355,散布在 1 到 10 之间无明显聚集——肉眼就能确认均匀性。想验证可自己改 low/high 重跑,看边界是否被触及。 外汇与贵金属行情本身不服从均匀假设,这类随机数多用于蒙特卡洛压力测试或参数扰动,请意识到实盘杠杆下的高风险,分布错觉不等于价格会按此游走。

MQL5 / C++
class="kw">template <class="kw">typename T>
 vector uniform(T low, T high, class="type">uint size=class="num">1)
  { 
    vector res = vector::Zeros(size);
    for (class="type">uint i=class="num">0; i<size; i++)
      res[i] = low + (high - low) * (rand() / class="type">class="kw">double(RAND_MAX));  class=class="str">"cmt">// Normalize rand()
    
    class="kw">return res;
  }
class CNumpy
  {
class="kw">protected:

class="kw">public:
                CNumpy(class="type">void);
                ~CNumpy(class="type">void);
                
                CRandom random;
  }
Print("np.random.uniform: ",np.random.uniform(class="num">1,class="num">10,class="num">10));
class="num">2025.03.class="num">16 class="num">15:class="num">03:class="num">15.102 Numpy  np.random.uniform: [class="num">8.906552323984496,class="num">9.274605548265022,class="num">7.828760643330179,class="num">9.355082857753228,class="num">2.218420972319712,class="num">5.772331919309061,class="num">3.76067384868923,class="num">6.096438489944151,class="num">1.93908505508591,class="num">8.107272560808131]

常见问题

可用 NumPy 风格接口直接对向量整体调用统计函数,一行算出均值、方差,避免逐元素循环,复制文章里的封装函数即可验证。
在生成随机数组前设置固定随机种子,就能让均匀分布随机数每次都一致,便于回测复现和问题排查。
小布盯盘的 AIGC 可内置这类诊断,打开对应品种页就能直接看向量统计结果与可复现性提示,不用自己跑代码。
会,数组运算把统计压成整体操作,外汇和贵金属波动快、风险高,向量化能省下主线程耗时,但结论仍带概率性。
均匀分布适合先验证接口和边界,正态更贴近收益扰动;两者都要设种子,否则回测结果可能不可复现。