MQL5中的自优化智能交易系统(第十部分):矩阵分解
在本系列的开篇文章(链接见此处)中,我们曾尝试仅用原生MQL5代码和MetaTrader 5终端的原始数据,共同构建一个线性回归模型。在阅读了开篇文章的评论与反馈后,许多读者指出了我们演示方案中遇到的问题,程序出现大量漏洞与错误,部分读者提到模型只会开立单一方向的持仓。总体而言,多位用户反馈我们初次搭建的线性模型存在稳定性问题。
回顾一下,线性模型是一类预测工具,能让程序直接从市场行为观测中学习,并依据所得规律执行其认为最有可能盈利的交易。因此我们的目标,是不再手动硬编码告诉程序何时买卖,而是让它能从历史数据中自主学习。
本文将解决用户在之前遇到的稳定性问题,并展示如何基于任意交易品种的原始数据,构建同样强大的预测模型。为此,我们将引入一类被称为矩阵分解的算法。
矩阵分解是一种将大矩阵拆解为若干更小、更简单矩阵乘积的数学技术。这类方法具备诸多优势。然而,在介绍这些优势之前,我们先来理解为什么需要这些方法。
在日常生活中,有些体验是跨文化共通的。例如,我相信大多数读者都明白这样一个道理:通过和孩子交谈,倾听孩子对父母的描述,我们就能大致想象出父母是什么样的人。这些描述甚至能帮我们推测出,在孩子没有直接提及的场景下,父母可能会如何行事。同样地,矩阵分解把一个大矩阵拆解为若干小矩阵 —— 可以看作它的“子矩阵”。每个子矩阵分别描述原矩阵的不同侧面,帮助我们理解其内在结构。正如孩子的视角有时能帮助我们把握父母的特征一样,这些更小的矩阵也能揭示出我们分析的市场深层的规律。
矩阵分解的结果,通常能为我们前文介绍的线性模型提供数值上更稳定的解。本文还将介绍一个数值计算库:OpenBLAS(全称基础线性代数子程序集)。OpenBLAS是BLAS库的开源分支,经过重新设计,可在现代计算平台上高效运行。BLAS最初是用Fortran语言和手写汇编编写的。
线性代数中有一个基本思想:任何数据集都可以拆分为更小的组成部分,利用这些部分可以构建原始数据的预测模型。由这些小子集得到的数据表示,还能揭示出原本隐藏在原始数据中难以被发现的特征。
本文将循序渐进地介绍一系列强大的线性代数相关函数,用于从原始数据构建预测模型。而这仅仅是开始!这些矩阵分解技术的价值远不止基础预测能力 —— 它们还能帮助我们压缩数据、发掘隐藏趋势、评估市场的稳定或无序程度。仅通过分解就能从任意数据集中提取出如此丰富的信息,这确实令人惊叹。让我们开始吧。
MQL5入门指南
使用MQL5的第一步,是定义本次演示全程要用到的系统常量。这些常量用于支撑我编写的脚本,让我们可以快速上手矩阵分解。
//+------------------------------------------------------------------+ //| Solve.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" #property script_show_inputs //+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ #define HORIZON 10 #define START 0接下来,我们定义脚本的用户输入参数 —— 具体而言,就是需要获取多少根K线数据。
//+------------------------------------------------------------------+ //| User inputs | //+------------------------------------------------------------------+ input int FETCH = 10;//How many bars should we fetch?
随后,我们声明全局变量,其中包含训练数据与测试数据,以及若干其他变量,用于存储程序从我们提供的数据中学习得到的系数。
//+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ROWS = 5; //Dependent variable matrix y,y_test; //Indenpendent variable matrix X = matrix::Ones(ROWS,FETCH); matrix X_test = matrix::Ones(ROWS,FETCH); //Coefficients matrix b; vector temp; //Row Norms vector row_norms = vector::Zeros(4); vector error_vector = vector::Zeros(4);
首先,我们打印出当前状态下的输入数据矩阵X。如图1所示,该矩阵初始时全部填充为1。这是特意设计的:在线性模型中,输入的第一行代表截距项。真实的市场数据(如开盘价、最高价、最低价、收盘价)将从第二行开始填入矩阵。
有一个关键点需要说明,即数据的排布格式。如果您一直在追更本系列文章(例如《重构经典策略》),其中我们从MetaTrader 5提取数据并在Python中处理,您可能习惯这样的格式:列表示市场特征(开盘、最高、最低、收盘),行表示时间(如日期)。但在本文示例中,布局是转置的:列表示时间,行表示开盘、最高、最低、收盘等市场特征。
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Observe the input matrix in its original form PrintFormat("Input Matrix Gathered From %s",Symbol()); Print(X);

图例1:可视化当前EURUSD市场输入数据
理清好格式后,我们进入脚本中负责获取历史市场数据的部分。获取数据后,我们先计算每个向量的范数,再用每个向量除以其自身范数。归一化步骤可确保每个向量的长度为1,这是在进行任何矩阵分解之前的关键前提。
为什么要做归一化?矩阵分解关注矩阵在不同方向上的主要变化,并比较各行各列的相对变化强度。为了让对比结果公平,我们通过除以范数,将每一行转换为单位向量。
//--- Fetch the data temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_OPEN,START+HORIZON+(FETCH*2),FETCH); row_norms[0] = temp.Norm(VECTOR_NORM_P); X.Row(temp/row_norms[0],1); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_HIGH,START+HORIZON+(FETCH*2),FETCH); row_norms[1] = temp.Norm(VECTOR_NORM_P); X.Row(temp/row_norms[1],2); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_LOW,START+HORIZON+(FETCH*2),FETCH); row_norms[2] = temp.Norm(VECTOR_NORM_P); X.Row(temp/row_norms[2],3); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+HORIZON+(FETCH*2),FETCH); row_norms[3] = temp.Norm(VECTOR_NORM_P); X.Row(temp/row_norms[3],4); //--- Fetch the test data temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_OPEN,START+HORIZON+(FETCH),FETCH); X_test.Row(temp/row_norms[0],1); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_HIGH,START+HORIZON+(FETCH),FETCH); X_test.Row(temp/row_norms[1],2); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_LOW,START+HORIZON+(FETCH),FETCH); X_test.Row(temp/row_norms[2],3); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+HORIZON+(FETCH),FETCH); X_test.Row(temp/row_norms[3],4);
打印输入训练数据时可以看到,第一行全为1,表示截距项;随后依次是开盘价、最高价、最低价、收盘价等行。由于进行了归一化,数据大致分布在0.3左右。
//--- The train data Print("Input"); Print(X);

图例2:每行向量按其范数归一化后的训练数据可视化
接下来,我们定义目标变量。在本例中,目标变量为收盘价,我们将其复制到矩阵y中。至此,矩阵X存放输入特征,矩阵y存放我们要预测的目标值。
//--- Fill the target y.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+(FETCH*2),FETCH); y_test.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START,FETCH); Print("Target"); Print(y);
![]()
图例3:基于现有历史市场观测数据,我们试图预测的输出值
那么,我们该如何找到能将X映射到y的系数呢?事实上,能满足这一映射关系的系数组合有无穷多种,因此我们需要一种方法来选出最合适的一组。通常,我们选择使预测值与实际目标值之间误差最小的系数。实现这一目标的一种经典方法是使用伪逆矩阵。
为计算系数,我们将X的伪逆矩阵与y相乘。这一矩阵乘法会以闭式解形式给出最优拟合系数。幸运的是,MQL5内置了PInv()函数,可直接计算伪逆。
请不要被这个解法的简洁性所迷惑。仅凭这一行代码的重要意义,就足以用整篇文章的剩余篇幅来讲解。MQL5的PInv()函数所生成的系数解,能够保证最小化预测值与历史观测值之间的均方根误差(RMSE)。此外,这类解必定存在。该算法在数值上稳定可靠,并且让我们能用简洁、易于维护的代码,直接从原始数据构建预测模型。但这并不是本文建议优先采用的方案。
//--- More Penrose Psuedo Inverse Solution implemented by MQL5 Developers b = y.MatMul(X.PInv()); Print("Pseudo Inverse Solution: "); Print(b);
![]()
图例4:将目标矩阵与输入矩阵的伪逆做矩阵乘法后得到的系数,该组系数能够使预测误差最小化
本文旨在为大家介绍OpenBLAS库以及各类矩阵分解算法。既然MQL5已经提供了通过PInv()函数计算系数、快速搭建预测模型的简便方案,我们为何还要学习OpenBLAS?理由十分充分,首要一点就是运算速度。OpenBLAS的运算速度远超MQL5中内置的伪逆计算函数。掌握OpenBLAS的使用方法,能够大幅提升您的回测效率。
无监督矩阵分解:奇异值分解(SVD)
正如开篇所述,任意数据矩阵都可以拆解为若干小型矩阵的乘积。这些更小的矩阵可以看作原矩阵的“子矩阵”,每一个都能从独特维度刻画原矩阵的特征。
奇异值分解(SVD)是矩阵分解的经典算法之一。SVD能够将任意矩阵拆解为三个基础小型矩阵的乘积。这三个矩阵分别捕捉原矩阵截然不同的特征。在本章节中,我们将逐一讲解SVD分解得到的三个子矩阵,并阐释SVD的设计初衷,以及每个分量能够挖掘出原矩阵的哪些隐藏信息。
正式讲解前,要先厘清术语概念。您可能会见到“无监督矩阵分解”与“矩阵分解”两种说法,但二者不能等同替换。无监督矩阵分解是矩阵分解下的一类细分技术。它和通用矩阵分解的区别在于,仅提取数据中最关键的特征分量。
本质上讲,无监督矩阵分解不会输出全部子矩阵,只保留价值最高的核心部分。算法完成矩阵拆解后,会依靠内置评判标准自动筛选出最有价值的因子(子矩阵)。整个筛选过程属于无监督模式:算法无需标注标签、人工干预,自主判定各分量的重要程度。无需手动挑选有效分量,而全部由算法自主完成。
如图例5 所示,SVD正是这类矩阵分解方法,它能将任意矩阵A拆解为三个子矩阵的乘积。
通过传入不同参数调用SVD接口,OpenBLAS既可以输出全部子矩阵,也能仅返回最重要的子矩阵。
本文中,我们会配置OpenBLAS仅输出关键子矩阵,这也是本节标题命名为无监督矩阵分解的原因。前文提到,SVD会把原始矩阵拆解为三个更简单的基础矩阵,下面我们依次解读这三个组成部分。
![]()
图例5:SVD可视化示意图
矩阵U用于刻画驱动市场行情的各类隐藏市场力量,这类隐藏力量专业上称为因子。因此,U的第一列代表一类市场驱动因子,当该因子主导行情时,开盘、最高、最低、收盘四项价格全部下行;第二个因子的系数整体为正,代表该力量会对市场形成整体性利多推动。由于我们处理的是历史行情数据,这里挖掘出的各类市场驱动因子,实际上对应的是背后的投资者情绪。
//--- Native MQL5 SVD Solution are also possible without relying on OpenBLAS Print("Computing Singular Value Decomposition using MQL5"); matrix U,VT; vector S; X.SVD(U,VT,S); Print("U"); Print(U);

图例6:理解SVD中的"U"分量
矩阵V反映了U中各股市场力量在原始数据集所有时间观测节点上的表现强度。举个例子,观察V的第一行,其中最大的数值为0.4262。该值出现在V第一行的第三列,这就意味着U的第三列所代表的力量,在历史第一个交易日主导了市场。U的第三列刻画的是一种混合力量:它对部分价格因子产生负面影响,对另一些则产生正面影响。这类力量通常表现为间歇性出现,或者强度较弱。
Print("VT"); Print(VT);

图例7:理解各股市场驱动力量在不同时间点的影响强度
Σ(奇异值矩阵)反映了矩阵U中各股市场力量的重要性等级。在历史数据中起主导作用的力量,在Σ中会被赋予较大的数值;在数据中表现较弱的力量,则对应较小的数值。由此清晰可见,3.741是Σ中最大的值,且出现在Σ的第一列。这就意味着U的第一列所代表的,是数据中观测到的最具主导性的市场力量。
Print("S"); Print(S);
![]()
图例8:理解SVD分解中的奇异值因子Σ
本文并不追求面面俱到,关于U、S、V这三个因子还有更多内容可以深入探讨。在图例6、7、8中,我们分析了调用MQL5原生SVD方法所得到的结果。这些结果与使用OpenBLAS库中的SingularValueDecompositionDC()函数所返回的结果高度吻合。
以下图例9展示了使用OpenBLAS计算得到的U因子截图,读者可以对比图例9与图例6,会发现MQL5内置函数与OpenBLAS函数计算出的U因子基本一致。由于底层实现函数存在差异,两张图的结果并不会精确到小数点后每一位都完全相同,但这是正常且可理解的。
//--- OpenBLAS SVD Solution, considerably powerful substitute to the closed solution provided by the MQL5 developers matrix OB_U,OB_VT,OB_SIGMA; vector OB_S; //--- Perform truncated SVD, we will explore what 'truncated' means later. PrintFormat("Computing Singular Value Decomposition of %s Data using OpenBLAS",Symbol()); X.SingularValueDecompositionDC(SVDZ_S,OB_S,OB_U,OB_VT); //--- U is a unitary matrix that is of dimension (m,r) Print("Open BLAS U"); Print(OB_U);

图例9:由OpenBLAS计算得到的U因子
在之前的图例4中我们已经展示过,MQL5的PInv()函数总能给出误差最小的系数,实现从输入数据到目标变量的映射。再次强调,不要被图例4中简洁的解法所迷惑。这是一个在数学上非常强大的解,对任意矩阵A都保证存在,并且能够最小化矩阵误差Ax-b的L2范数。
在图例4的讲解中我们没有提及的是,PInv()函数底层本质上就是替您调用SVD()。从数学角度来说,伪逆矩阵通常就是通过原始数据的奇异值分解来计算的。让我们亲自验证这一点。
在以下代码段中,我使用了对市场数据执行SVD()后保存的三个子矩阵。我们不会逐一讲解所需的线性代数定理,也不会推导这个公式,我只想演示利用SVD返回的子矩阵,就可以轻松求出拟合输入与目标变量的线性系数。
Print("Comparing OLS Solutions"); Print("Native MQL5 Solution"); //--- We will always benchmark the native solution as the truth, the MQL5 developers implemented an extremely performant benchmark for us Print(b); //--- The OpenBLAS solution came closest to the native solution implemented for us Print("OpenBLAS Solution"); matrix ob_solution = y.MatMul(OB_VT.Transpose().MatMul(OB_SIGMA.Inv()).MatMul(OB_U.Transpose())); Print(ob_solution); //--- Our manual solution was not even close! We will therefore rely on the OpenBLAS solution. Print("Manual SVD Solution"); matrix svd_solution = y.MatMul(VT).MatMul(SIGMA.Inv()).MatMul(U.Transpose()); Print(svd_solution);

图例10:通过SVD可得到最优拟合输入与输出数据的系数
细心的读者可能已经发现,图例10中的几组系数彼此并非完全精确一致。这是正常现象,请不要忘记我们是用三种不同函数分别计算出各组系数的。这就好比三名独立完成作业的学生,各自用了不同的解题方法。然而,对我们而言更重要的是,用这些系数在未参与训练的数据上做预测时,会产生多大的误差。
如图例11所示,OpenBLAS的SVD解法在测试集预测中取得了最低误差。但我希望读者不要误以为图例11就是我们引入OpenBLAS的主要原因。
由此可见,三组误差水平其实相差不大。因此,如果我们在不同品种、不同数据长度下多次重复测试,OpenBLAS并不一定总能给出最低误差。我希望读者明白,OpenBLAS库的真正优势,在于它经过精心优化、持续维护,具备极高的运算速度与稳定性。它并不能保证每次误差都是最低的 —— 没有任何一个库可以做出这样绝对的保证。
//--- Measuring the amount of error //--- Information lost by MQL5 PsuedoInverse solution //--- The Frobenius norm squares all PrintFormat("Information Loss in Forcasting %s Market : ",Symbol()); Print("PInv: "); matrix pinv_error = ((b.MatMul(X_test)) - y_test); Print(pinv_error.Norm(MATRIX_NORM_FROBENIUS)); //--- Let the MQL5 implementation be our benchmark double benchmark = pinv_error.Norm(MATRIX_NORM_FROBENIUS); //--- Information lost by Manual SVD solution Print("Manual SVD: "); matrix svd_error = ((svd_solution.MatMul(X_test)) - y_test); Print(svd_error.Norm(MATRIX_NORM_FROBENIUS)); //--- Information lost by OpenBLAS SVD solution Print("OpenBLAS SVD: "); matrix ob_error = ((ob_solution.MatMul(X_test)) - y_test); Print(ob_error.Norm(MATRIX_NORM_FROBENIUS));

图例11:各组系数在训练集以外数据上预测时产生的误差
无监督矩阵分解在预测建模之外的应用
希望到目前为止,我这种简洁的讲解方式已经让你对矩阵分解是什么,以及它为何能在金融市场数据分析中发挥作用有了一些思路。正如我之前所说,利用合适的矩阵分解构建预测模型,仅仅是其众多实用功能中的一小部分。在本节中,我将展示矩阵分解的其他实用场景,以及如何将这些分析思路应用到我们的交易程序和策略中。
用于无监督市场过滤的矩阵分解
我假设读者都有一定的实盘交易经验,并通过自己的实践对我接下来所提出的问题有所认知。在外汇市场和加密货币市场之间,您认为哪类资产的波动性更大?
我相信这个答案对大家来说都是显而易见的。加密货币的波动性远高于传统外汇市场。为了让不太确定的读者有直观地认识,我们在1分钟图表上分别计算BTCETH(比特币兑以太坊)的ATR(平均真实波幅)(图例12)和EURUSD(欧元兑美元)的ATR(图例13)ATR指标用于衡量市场波动程度,ATR数值越大,代表市场波动越剧烈。BTCETH图表上的ATR数值大约比EURUSD高出 6000%。因此,足以帮助所有读者理解,为何加密货币市场通常被认为波动性远高于传统外汇市场。

图例12:BTCETH的波动幅度远高于EURUSD
再强调一下,EURUSD是全球流动性最强、交易最活跃的货币对,但它的波动水平和加密货币市场相比,显得微不足道。

图例13:传统资产品类的波动程度远不及加密货币市场
我们前文用到的矩阵分解方法,其实也能得出相同的结论。回顾图例8的讲解:奇异值矩阵Σ能够衡量数据中各类市场驱动力量的重要程度。稳定市场的奇异值矩阵Σ中,只会存在一个较大数值,其余数值都趋近于0;而Σ中偏离0的有效数值越多,从数据层面来看,市场就越无序、波动越剧烈。
我们可以分别运行两次脚本,一次针对EURUSD市场,一次针对BTCETH市场。然而,从图例14、图例15的结果来看,两个市场看似都很稳定:二者的奇异值矩阵Σ都仅存在一个明显非零的大数值。单看这一点会让人误以为BTCETH和EURUSD一样走势平稳、波动温和。但这并非完整的事实。想要获得可靠的市场状态判断,我们还需要了解矩阵分解的另一项用途。
//+------------------------------------------------------------------+ //| What are we demonstrating here? | //| 1) We have shown you that any matrix of market data you have, | //| can be analyzed intelligently, to build a linear regression | //| model, using just the raw data. | //| 2) We have demonstrated that the solution to such Linear | //| regression problems, can be obtained through effecient and | //| dedicated functions available in MQL5 or through matrix | //| factorization. | //|__________________________________________________________________| //| I now ask the reader the following question: | //| "If dedicated functions exist, why bother learning matrix | //| factorization?" | //+------------------------------------------------------------------+ //--- Matrix factorization gives us a description of the data and it properties //--- Questions such as: "How stable/chaotic is the market we are in?" can be answered by the factorization we have just performed //--- Or even questions such as: "How best can I expose the hidden trends in all of this market data?" can still be answered by the factorization we have just performed //--- I'm only trying to give you a few examples of why you should bother learning these factorizations, even though dedicated functions exist. //--- Any given matrix A can be represented as the sum of smaller matrices A = USV, this is theorem behind the Singular Value Decomposition. //--- Each factor is special because each describes different charectersitics of its parent. //--- Let's get to know Sigma, represented as the S in A = USV. //--- Sigma technically tells us how many different modes our market appears to exist in, and how important each mode is. //--- However, reintepreted in terms of market data, these modes may correspond to investor sentiment. PrintFormat("Taking a closer look at The Eigenvalues of %s Market Data: ",Symbol()); Print(OB_S/OB_S.Sum()); Print("If sigma has a only few values that are far from 0, then investor's sentiment in this market appears well established and hardly changes"); //--- If Sigma has a lot values that are all far away from 0, then the market is chaotic and it appears investor's sentiment and expectations constantly change //--- If Sigma has a few, or even just one value that is far away from 0, then investor sentiment in that market appears stable, and hardly changes. //--- Traders explicitly looking for fast-action scalping oppurtunities may use Sigma as a filter of how much energy the market has. //--- Quiet market will have a few dominant values in Sigma, not ideal for scalpers, better suited for long-term trend traders.
图例14:EURUSD市场能量分布可视化
图例15:BTCETH市场能量分布可视化
用于数据压缩与信号提取的矩阵分解
矩阵分解还可以用于数据压缩和提取数据中的主导信号。由于分解得到的子矩阵比原矩阵更小,这类算法能够高效地压缩数据。在网络通信、信号处理、电气工程等相关领域的从业者,对矩阵分解的这一特性应该都十分熟悉。我们可以通过将子矩阵S和V相乘,来压缩原始数据。需要注意的是,相乘前要先对S调用Diag()方法,将其转换为对角矩阵。两者相乘的结果,就是原矩阵的一种全新、紧凑的表征形式。
读者可能已经对这个算法有所了解,它通常被称为主成分分析(PCA)。我们不会深入讲解PCA,只是想展示使用矩阵分解能为我们挖掘出大量有价值的信息。计算市场数据主成分的方法有很多,而基于OpenBLAS的矩阵分解,很可能是MQL5原生环境中运算速度最快的方法之一。
//--- Fetch the data and prepare to perform PCA temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_OPEN,START+HORIZON+(FETCH*2),FETCH); row_norms[0] = temp.Mean(); X.Row(temp-row_norms[0],1); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_HIGH,START+HORIZON+(FETCH*2),FETCH); row_norms[1] = temp.Mean(); X.Row(temp-row_norms[1],2); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_LOW,START+HORIZON+(FETCH*2),FETCH); row_norms[2] = temp.Mean(); X.Row(temp-row_norms[2],3); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+HORIZON+(FETCH*2),FETCH); row_norms[3] = temp.Mean(); X.Row(temp-row_norms[3],4); //--- Fetch the test data temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_OPEN,START+HORIZON+(FETCH),FETCH); X_test.Row(temp-row_norms[0],1); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_HIGH,START+HORIZON+(FETCH),FETCH); X_test.Row(temp-row_norms[1],2); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_LOW,START+HORIZON+(FETCH),FETCH); X_test.Row(temp-row_norms[2],3); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+HORIZON+(FETCH),FETCH); X_test.Row(temp-row_norms[3],4); //--- Perform truncated SVD, we will explore what 'truncated' means later. Print("Computing Singular Value Decomposition using OpenBLAS"); X.SingularValueDecompositionDC(SVDZ_S,OB_S,OB_U,OB_VT); OB_SIGMA.Diag(OB_S); //--- Calculating Principal Components Print("Principal Components"); matrix pc = OB_SIGMA.MatMul(OB_VT); Print(pc);

图例16:通过将奇异值因子S与V相乘,我们得到原始数据集的紧凑表征
关于S和V相乘得到的结果,我们还有很多内容可以探讨。这一乘积生成了一种新的数据表示,其内部相关性显著降低。为证明这一点,我们将原始数据的相关矩阵范数,与S、V乘积结果的相关矩阵范数进行对比。回顾一下,线性代数中的范数可以理解为衡量某个量“大小”(强度)的指标。如图例17所示,对原始市场数据做分解后,相关矩阵的范数显著下降。
这一结果旨在向读者说明:基于SVD的矩阵分解可以用来剔除原始数据中冗余的相关特征。通过这一处理,我们有望更好地凸显数据中占主导地位的趋势与模式。
//--- PCA reduces the amount of correlation in our dataset Print("How correlated is our new representation of the data?"); //--- First we will measure the size of our original correlation matrix Print(X.Norm(MATRIX_NORM_FROBENIUS)); //--- Then, we will measure the size of our new correlation matrix produced by factorizing the data Print(pc.CorrCoef().Norm(MATRIX_NORM_FROBENIUS));

图例17:矩阵分解能够显著降低数据集中的特征相关性
掌握这一原理后,我们就可以构建一个仅用3行数据的市场模型,替代最初的5行原始数据。理想情况下,这3组相关性更低的特征,会比原始数据更好地解释市场与目标变量之间的关系。这一过程称为特征提取,因为我们从原始数据中提取了全新的特征。但和大多数优化方法一样,我们无法保证它一定能提升未来的模型表现,正如图例18所示。
//--- Main principal components matrix mpc; mpc.Row(pc.Row(0),0); mpc.Row(pc.Row(1),1); mpc.Row(pc.Row(2),2); //--- The factor VT describes the correlational structure across the columns of our data Print("Performing PCA"); matrix pca_coefs = y.MatMul(mpc.PInv()); //--- Performing PCA on the test data X_test.SingularValueDecompositionDC(SVDZ_S,OB_S,OB_U,OB_VT); Print("Principal Components of Test Data"); pc = OB_SIGMA.MatMul(OB_VT); Print(pc); PrintFormat("Most Important Principal Components in %s Market Test Data",Symbol()); Print(OB_S / OB_S.Sum()); //--- Main principal components mpc.Row(pc.Row(0),0); mpc.Row(pc.Row(1),1); mpc.Row(pc.Row(2),2); matrix pca_error = pca_coefs.MatMul(mpc) - y_test; Print("PCA Error: "); Print(pca_error.Norm(MATRIX_NORM_FROBENIUS)); Print("OpenBLAS Error: "); Print(ob_error.Norm(MATRIX_NORM_FROBENIUS)); Print("Manual Error: "); Print(svd_error.Norm(MATRIX_NORM_FROBENIUS));

图例18:特征提取是一种强大的数值方法,但并不能保证模型效果一定提升
在图例14和图例15 中,我们曾尝试说明:可以用矩阵分解来区分稳定市场与高波动市场。但在第一次测试中,两个市场的奇异值因子S看起来都只有一个较大的数值。然而,在仔细检查数据后我发现,这一现象仅在训练集上成立。如果我们对市场数据的测试集做矩阵分解,再分析得到的奇异值因子S,就能明显看出,BTCETH确实比EURUSD包含更多“能量”。其原因在于BTCETH的S中有两个显著非零值,而EURUSD只有一个。
//--- Performing PCA on the test data X_test.SingularValueDecompositionDC(SVDZ_S,OB_S,OB_U,OB_VT); Print("Principal Components of Test Data"); pc = OB_SIGMA.MatMul(OB_VT); PrintFormat("Most Important Principal Components in %s Market Test Data",Symbol()); Print(OB_S / OB_S.Sum());
![]()
图例19:分析EURUSD市场蕴含的波动能量
![]()
图例19-1:分析BTCETH市场蕴含的波动能量再次强调,奇异值矩阵中偏离0的有效数值越多,代表市场行情越无序、波动越剧烈。
我为本次无监督矩阵分解专题讲解编写的MQL5脚本如下:
//+------------------------------------------------------------------+ //| Solve.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" #property script_show_inputs //+------------------------------------------------------------------+ //| System definitions | //+------------------------------------------------------------------+ #define HORIZON 10 #define START 0 //+------------------------------------------------------------------+ //| User inputs | //+------------------------------------------------------------------+ input int FETCH = 10;//How many bars should we fetch? //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ int ROWS = 5; //Dependent variable matrix y,y_test; //Indenpendent variable matrix X = matrix::Ones(ROWS,FETCH); matrix X_test = matrix::Ones(ROWS,FETCH); //Coefficients matrix b; vector temp; //Row Norms vector row_norms = vector::Zeros(4); vector error_vector = vector::Zeros(4); //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- Observe the input matrix in its original form PrintFormat("Input Matrix Gathered From %s",Symbol()); Print(X); //--- Fetch the data temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_OPEN,START+HORIZON+(FETCH*2),FETCH); row_norms[0] = temp.Norm(VECTOR_NORM_P); X.Row(temp/row_norms[0],1); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_HIGH,START+HORIZON+(FETCH*2),FETCH); row_norms[1] = temp.Norm(VECTOR_NORM_P); X.Row(temp/row_norms[1],2); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_LOW,START+HORIZON+(FETCH*2),FETCH); row_norms[2] = temp.Norm(VECTOR_NORM_P); X.Row(temp/row_norms[2],3); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+HORIZON+(FETCH*2),FETCH); row_norms[3] = temp.Norm(VECTOR_NORM_P); X.Row(temp/row_norms[3],4); //--- Fetch the test data temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_OPEN,START+HORIZON+(FETCH),FETCH); X_test.Row(temp/row_norms[0],1); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_HIGH,START+HORIZON+(FETCH),FETCH); X_test.Row(temp/row_norms[1],2); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_LOW,START+HORIZON+(FETCH),FETCH); X_test.Row(temp/row_norms[2],3); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+HORIZON+(FETCH),FETCH); X_test.Row(temp/row_norms[3],4); //--- The train data Print("Input"); Print(X); //--- Fill the target y.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+(FETCH*2),FETCH); y_test.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START,FETCH); Print("Target"); Print(y); //--- More Penrose Psuedo Inverse Solution implemented by MQL5 Developers, enterprise level effeciency! b = y.MatMul(X.PInv()); Print("Pseudo Inverse Solution: "); Print(b); //--- Native MQL5 SVD Solution are also possible without relying on OpenBLAS Print("Computing Singular Value Decomposition using MQL5"); matrix U,VT; vector S; X.SVD(U,VT,S); Print("U"); Print(U); Print("VT"); Print(VT); Print("S"); Print(S); matrix SIGMA; SIGMA.Diag(S); //--- OpenBLAS SVD Solution, considerably powerful substitute to the closed solution provided by the MQL5 developers matrix OB_U,OB_VT,OB_SIGMA; vector OB_S; //--- Perform truncated SVD, we will explore what 'truncated' means later. PrintFormat("Computing Singular Value Decomposition of %s Data using OpenBLAS",Symbol()); X.SingularValueDecompositionDC(SVDZ_S,OB_S,OB_U,OB_VT); //--- U is a unitary matrix that is of dimension (m,r) Print("Open BLAS U"); Print(OB_U); //--- VT is a mathematically a symmetrical matrix that is (r,r), for effeciency in software it is represented as a vector that is (1,r) Print("Open BLAS VT"); Print(OB_VT); //--- We need it in its intended form as an (r,r) matrix, we will explore what this means later. Print("Open BLAS S"); Print(OB_S); OB_SIGMA.Diag(OB_S); Print("Comparing OLS Solutions"); Print("Native MQL5 Solution"); //--- We will always benchmark the native solution as the truth, the MQL5 developers implemented an extremely performant benchmark for us Print(b); //--- The OpenBLAS solution came closest to the native solution implemented for us Print("OpenBLAS Solution"); matrix ob_solution = y.MatMul(OB_VT.Transpose().MatMul(OB_SIGMA.Inv()).MatMul(OB_U.Transpose())); Print(ob_solution); //--- Our manual solution was not even close! We will therefore rely on the OpenBLAS solution. Print("Manual SVD Solution"); matrix svd_solution = y.MatMul(VT).MatMul(SIGMA.Inv()).MatMul(U.Transpose()); Print(svd_solution); //--- Measuring the amount of error //--- Information lost by MQL5 PsuedoInverse solution //--- The Frobenius norm squares all PrintFormat("Information Loss in Forcasting %s Market : ",Symbol()); Print("PInv: "); matrix pinv_error = ((b.MatMul(X_test)) - y_test); Print(pinv_error.Norm(MATRIX_NORM_FROBENIUS)); //--- Let the MQL5 implementation be our benchmark double benchmark = pinv_error.Norm(MATRIX_NORM_FROBENIUS); //--- Information lost by Manual SVD solution Print("Manual SVD: "); matrix svd_error = ((svd_solution.MatMul(X_test)) - y_test); Print(svd_error.Norm(MATRIX_NORM_FROBENIUS)); //--- Information lost by OpenBLAS SVD solution Print("OpenBLAS SVD: "); matrix ob_error = ((ob_solution.MatMul(X_test)) - y_test); Print(ob_error.Norm(MATRIX_NORM_FROBENIUS)); //+------------------------------------------------------------------+ //| What are we demonstrating here? | //| 1) We have shown you that any matrix of market data you have, | //| can be analyzed intelligently, to build a linear regression | //| model, using just the raw data. | //| 2) We have demonstrated that the solution to such Linear | //| regression problems, can be obtained through effecient and | //| dedicated functions available in MQL5 or through matrix | //| factorization. | //|__________________________________________________________________| //| I now ask the reader the following question: | //| "If dedicated functions exist, why bother learning matrix | //| factorization?" | //+------------------------------------------------------------------+ //--- Matrix factorization gives us a description of the data and it properties //--- Questions such as: "How stable/chaotic is the market we are in?" can be answered by the factorization we have just performed //--- Or even questions such as: "How best can I expose the hidden trends in all of this market data?" can still be answered by the factorization we have just performed //--- I'm only trying to give you a few examples of why you should bother learning these factorizations, even though dedicated functions exist. //--- Any given matrix A can be represented as the sum of smaller matrices A = USV, this is theorem behind the Singular Value Decomposition. //--- Each factor is special because each describes different charectersitics of its parent. //--- Let's get to know Sigma, represented as the S in A = USV. //--- Sigma technically tells us how many different modes our market appears to exist in, and how important each mode is. //--- However, reintepreted in terms of market data, these modes may correspond to investor sentiment. PrintFormat("Taking a closer look at The Eigenvalues of %s Market Data: ",Symbol()); Print(OB_S/OB_S.Sum()); Print("If sigma has a only few values that are far from 0, then investor's sentiment in this market appears well established and hardly changes"); //--- If Sigma has a lot values that are all far away from 0, then the market is chaotic and it appears investor's sentiment and expectations constantly change //--- If Sigma has a few, or even just one value that is far away from 0, then investor sentiment in that market appears stable, and hardly changes. //--- Traders explicitly looking for fast-action scalping oppurtunities may use Sigma as a filter of how much energy the market has. //--- Quiet market will have a few dominant values in Sigma, not ideal for scalpers, better suited for long-term trend traders. //--- Fetch the data and prepare to perform PCA temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_OPEN,START+HORIZON+(FETCH*2),FETCH); row_norms[0] = temp.Mean(); X.Row(temp-row_norms[0],1); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_HIGH,START+HORIZON+(FETCH*2),FETCH); row_norms[1] = temp.Mean(); X.Row(temp-row_norms[1],2); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_LOW,START+HORIZON+(FETCH*2),FETCH); row_norms[2] = temp.Mean(); X.Row(temp-row_norms[2],3); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+HORIZON+(FETCH*2),FETCH); row_norms[3] = temp.Mean(); X.Row(temp-row_norms[3],4); //--- Fetch the test data temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_OPEN,START+HORIZON+(FETCH),FETCH); X_test.Row(temp-row_norms[0],1); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_HIGH,START+HORIZON+(FETCH),FETCH); X_test.Row(temp-row_norms[1],2); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_LOW,START+HORIZON+(FETCH),FETCH); X_test.Row(temp-row_norms[2],3); temp.CopyRates(Symbol(),PERIOD_CURRENT,COPY_RATES_CLOSE,START+HORIZON+(FETCH),FETCH); X_test.Row(temp-row_norms[3],4); //--- Perform truncated SVD, we will explore what 'truncated' means later. Print("Computing Singular Value Decomposition using OpenBLAS"); X.SingularValueDecompositionDC(SVDZ_S,OB_S,OB_U,OB_VT); //--- Calculating Principal Components Print("Principal Components"); matrix pc = OB_SIGMA.MatMul(OB_VT); Print(pc); PrintFormat("Most Important Principal Components of %s Market Data",Symbol()); Print(OB_S / OB_S.Sum()); //--- Main principal components matrix mpc; mpc.Row(pc.Row(0),0); mpc.Row(pc.Row(1),1); mpc.Row(pc.Row(2),2); //--- The factor VT describes the correlational structure across the columns of our data Print("Performing PCA"); matrix pca_coefs = y.MatMul(mpc.PInv()); //--- Performing PCA on the test data X_test.SingularValueDecompositionDC(SVDZ_S,OB_S,OB_U,OB_VT); Print("Principal Components of Test Data"); pc = OB_SIGMA.MatMul(OB_VT); Print(pc); PrintFormat("Most Important Principal Components in %s Market Test Data",Symbol()); Print(OB_S / OB_S.Sum()); //--- Main principal components mpc.Row(pc.Row(0),0); mpc.Row(pc.Row(1),1); mpc.Row(pc.Row(2),2); matrix pca_error = pca_coefs.MatMul(mpc) - y_test; Print("PCA Error: "); Print(pca_error.Norm(MATRIX_NORM_FROBENIUS)); Print("OpenBLAS Error: "); Print(ob_error.Norm(MATRIX_NORM_FROBENIUS)); Print("Manual Error: "); Print(svd_error.Norm(MATRIX_NORM_FROBENIUS)); } //+------------------------------------------------------------------+
搭建应用程序
我们将整合前文讲解的所有知识点,构建一套完整的交易策略。本策略的核心思路:通过预测移动平均线指标的未来数值,测算市场合理价格。该预测值将作为交易判断依据:当实时价格高于预测值时,判定市场高估,执行做空;反之,价格低于预测值时,执行做多。我们在EURUSD日线图上设置周期为10的简单移动平均线,并将均线向前偏移,把偏移后的均线数值当作预测目标。

图例20:演示示例,将移动平均线指标整体向前偏移10根K线
整套交易策略的底层逻辑是,当前价格最终会向预测的合理价格回归。如图例21的行情场景,预测价格偏低,但实时价格处于高位。如果模型预测出这种行情结构,就会触发交易信号。

图例21:交易策略可视化,偏移后的移动平均线代表模型预判的均线位置,我们以此作为交易依据
建立基准参照模型
在开发正式程序前,我们需要搭建一套基准模型,用于评判AI模型的效果。基准模型代表不使用AI算法时,策略能达到的预期收益水平。我们在后续章节会完整地讲解程序的实现流程,此处,我将简要地介绍基准模型的核心构成要素。
//+------------------------------------------------------------------+ //| Obtain a prediction from our model | //+------------------------------------------------------------------+ void setup(void) { y.CopyIndicatorBuffer(ma_close_handler,0,0,bars); Print("Training Target"); Print(y); //--- Get a prediction prediction = y.Mean(); Print("Prediction"); Print(prediction); } //+------------------------------------------------------------------+
基准模型的预测方式是直接复用移动平均线的数值 —— 计算均线均值,并以此为依据进行交易。如果移动平均线的均值高于当前价格,我们就买入;反之则卖出。
if(prediction > c) { Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),proposed_buy_sl,0); state = 1; } if(prediction < c) { Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),proposed_sell_sl,0); state = -1; }现在我们将这套程序应用于USD/UL货币对,如图例22所示,回测区间选用2023年1月至2025年3月共两年的历史行情数据。

图例22:基于历史行情数据测试基准交易程序
图例23展示了本次使用的程序参数配置。为保证对比结果公平,所有测试都将固定这套输入参数不变。

图例23:统一基准策略参数,确保多组实验可公平对比
图例24为该交易策略生成的资金曲线。回测结果证明这套基础思路具备可行性,账户资金整体呈上行趋势。

图例24:基准策略为我们设立了清晰的业绩参照标准,后续我们将利用矩阵分解相关知识,打造表现更优的新策略
图例25列出完整的业绩指标明细。该策略胜率达到51%,盈利表现稳定;夏普比率为0.47,数值健康。虽然进一步优化参数能够提升夏普比率,但当前这套系统已经是合格的基准参照。即便只是简单地预测均线未来数值,我们也能搭建出可盈利的交易策略。现在,让我们来研究,如何通过更有依据的预测提升策略效果。

图例25:基准策略完整业绩指标分析
优化我们的策略表现
现在,我们正式开始编写MQL5应用程序。我们首先定义程序所需的核心系统常量,这些常量用于控制系统依赖的技术指标,同时规定程序所需输入特征的总数量。
//+------------------------------------------------------------------+ //| Linear Regression.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define TOTAL_INPUTS 6
接下来,我们将定义系统输入参数,用户可通过调整这些参数来改变程序的运行逻辑。
//+------------------------------------------------------------------+ //| System Inputs | //+------------------------------------------------------------------+ input int bars = 10;//Number of historical bars to fetch input int horizon = 10;//How far into the future should we forecast input int MA_PERIOD = 24; //Moving average period input ENUM_TIMEFRAMES TIME_FRAME = PERIOD_H1;//User Time Frame
同时,我们还将声明一组关键全局变量,用于记录线性回归模型所用到的全部参数。
//+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh>
在EA的初始化阶段,我们会为所有全局变量赋予默认值,并完成相关技术指标的初始化。
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ int ma_close_handler; double ma_close[]; Time *Timer; TradeInfo *TradeInformation; vector bias,temp,Z1,Z2; matrix X,y,prediction,b; int time; CTrade Trade; int state; int atr_handler; double atr[];
在反初始化阶段,我们将释放之前分配给全局变量的内存,包括不再需要的技术指标相关资源。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Timer = new Time(Symbol(),TIME_FRAME); TradeInformation = new TradeInfo(Symbol(),TIME_FRAME); ma_close_handler = iMA(Symbol(),TIME_FRAME,MA_PERIOD,0,MODE_SMA,PRICE_CLOSE); bias = vector::Ones(TOTAL_INPUTS); Z1 = vector::Ones(TOTAL_INPUTS); Z2 = vector::Ones(TOTAL_INPUTS); X = matrix::Ones(TOTAL_INPUTS,bars); y = matrix::Ones(1,bars); time = 0; state = 0; atr_handler = iATR(Symbol(),TIME_FRAME,14); //--- return(INIT_SUCCEEDED); }
当程序接收到更新的价格数据时,我们需要适时调整模型系数的权重,使其紧密贴合当前市场状态。这意味着在回测过程中,我们会频繁执行SVD矩阵分解计算。然而,这正是OpenBLAS团队所提供便捷实现的优势所在:即便多次调用,也几乎不会拖慢历史回测的速度。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if(Timer.NewCandle()) { setup(); double c = iClose(Symbol(),TIME_FRAME,0); CopyBuffer(atr_handler,0,0,1,atr); CopyBuffer(ma_close_handler,0,0,1,ma_close); if(PositionsTotal() == 0) { state = 0; if(prediction[0,0] > c) { Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),(TradeInformation.GetBid() - (2 * atr[0])),0); state = 1; } if(prediction[0,0] < c) { Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),(TradeInformation.GetAsk() + (2 * atr[0])),0); state = -1; } } if(PositionsTotal() > 0) { if(((state == -1) && (prediction[0,0] > c)) || ((state == 1)&&(prediction[0,0] < c))) Trade.PositionClose(Symbol()); if(PositionSelect(Symbol())) { double current_sl = PositionGetDouble(POSITION_SL); if((state == 1) && ((ma_close[0] - (2 * atr[0]))>current_sl)) { Trade.PositionModify(Symbol(),(ma_close[0] - (2 * atr[0])),0); } else if((state == -1) && ((ma_close[0] + (1 * atr[0]))<current_sl)) { Trade.PositionModify(Symbol(),(ma_close[0] + (2 * atr[0])),0); } } } } }
最后,我们定义用于从线性回归模型中获取预测值的函数。该函数使用经过标准化与缩放处理的Z值,这些值由向量Z1(均值)和Z2(标准差)记录。每一行缩放后的向量都存储在矩阵X_inputs中,而我们要预测的对应移动平均值则存储在Y中。随后,我们使用前文介绍的矩阵分解方法对模型进行拟合,并利用学习得到的系数生成预测。
//+------------------------------------------------------------------+ //| Obtain a prediction from our model | //+------------------------------------------------------------------+ void setup(void) { //--- OpenBLAS SVD Solution, considerably powerful substitute to the closed solution provided by the MQL5 developers matrix OB_U,OB_VT,OB_SIGMA; vector OB_S; //--- Reshape the matrix X = matrix::Ones(TOTAL_INPUTS,bars); //--- Store the Z-scores temp.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_OPEN,horizon,bars); Z1[0] = temp.Mean(); Z2[0] = temp.Std(); temp = ((temp - Z1[0]) / Z2[0]); X.Row(temp,1); //--- Store the Z-scores temp.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_HIGH,horizon,bars); Z1[1] = temp.Mean(); Z2[1] = temp.Std(); temp = ((temp - Z1[1]) / Z2[1]); X.Row(temp,2); //--- Store the Z-scores temp.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_LOW,horizon,bars); Z1[2] = temp.Mean(); Z2[2] = temp.Std(); temp = ((temp - Z1[2]) / Z2[2]); X.Row(temp,3); //--- Store the Z-scores temp.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_CLOSE,horizon,bars); Z1[3] = temp.Mean(); Z2[3] = temp.Std(); temp = ((temp - Z1[3]) / Z2[3]); X.Row(temp,4); //--- Store the Z-scores temp.CopyIndicatorBuffer(ma_close_handler,0,horizon,bars); Z1[4] = temp.Mean(); Z2[4] = temp.Std(); temp = ((temp - Z1[4]) / Z2[4]); X.Row(temp,5); temp.CopyIndicatorBuffer(ma_close_handler,0,0,bars); y.Row(temp,0); Print("Training Input Data: "); Print(X); Print("Training Target"); Print(y); //--- Perform truncated SVD, we will explore what 'truncated' means later. PrintFormat("Computing Singular Value Decomposition of %s Data using OpenBLAS",Symbol()); X.SingularValueDecompositionDC(SVDZ_S,OB_S,OB_U,OB_VT); OB_SIGMA.Diag(OB_S); //--- Fit the model //--- More Penrose Psuedo Inverse Solution implemented by MQL5 Developers, enterprise level effeciency! b = y.MatMul(OB_VT.Transpose().MatMul(OB_SIGMA.Inv()).MatMul(OB_U.Transpose())); Print("OLS Solutions: "); Print(b); //--- Prepare to get a prediction //--- Reshape the data X = matrix::Ones(TOTAL_INPUTS,1); //--- Get a prediction temp.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_OPEN,0,1); temp = ((temp - Z1[0]) / Z2[0]); X.Row(temp,1); temp.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_HIGH,0,1); temp = ((temp - Z1[1]) / Z2[1]); X.Row(temp,2); temp.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_LOW,0,1); temp = ((temp - Z1[2]) / Z2[2]); X.Row(temp,3); temp.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_CLOSE,0,1); temp = ((temp - Z1[3]) / Z2[3]); X.Row(temp,4); temp.CopyIndicatorBuffer(ma_close_handler,0,0,1); temp = ((temp - Z1[4]) / Z2[4]); X.Row(temp,5); Print("Prediction Inputs: "); Print(X); //--- Get a prediction prediction = b.MatMul(X); Print("Prediction"); Print(prediction[0,0]); } //+------------------------------------------------------------------+
我们现在可以开始测试优化版交易算法了。本实现旨在对预期价格提供更有依据的预测。我们将沿用初始测试的时间区间,如图例26所示。同时,如图例23所示,程序参数设置保持不变,读者可继续使用相同配置进行复现。

图例26:准备测试新版交易程序实现的性能提升
分析新结果,可清晰观察到显著的性能提升。基础策略总净利润为77美元,而优化版策略达到101美元,增幅可观,净利润整体增长31%。此外,夏普比率从初始版本的0.47提升至0.63,风险调整后收益提升34%,表明系统性能实现了实质性增强。
盈利交易占比也从基础系统的51.4%升至优化版的51.8%。此外,总交易次数从70次增加到83次,说明新系统能捕捉到更多交易信号。
尽管单笔盈利与亏损交易的平均规模均有所下降,但系统整体交易活跃度与有效性更高。所有这些成果均通过原生MQL5代码实现,并对可用数据合理运用矩阵分解技术达成。

图例27:基于价格预测的系统性能详细分析
我们同样展示了优化版交易程序生成的收益曲线。新系统在历史数据上呈现出账户余额持续增长的积极趋势,这激励我们继续探索更多优化方向。

图例28:优化版交易程序生成的收益曲线
结论
本文向读者介绍了MQL5矩阵API的诸多优势。该API提供强大的数学工具,显著提升了我们做出更有依据的交易决策的能力。
矩阵分解技术能挖掘相关数据中隐藏的模式—— 这些模式通过传统市场分析方法可能难以察觉。读者们现在掌握了替代金融领域常用传统时间序列分析的有力方案。例如,典型时间序列分析始于对数据差分以衡量周期性变化,而我们的方法完全避免进行差分,转而依赖数据分解。
这种思路转变为广泛应用场景打开了大门。我们已证明,矩阵分解能实现快速且数值稳定的统计建模,同时降低数据维度,将其简化为更紧凑的形式,从而更好地揭示潜在趋势。
尽管矩阵分解的优势远不止于此,但本文已奠定坚实基础。重要的是,分解技术可减少对显式交易规则的依赖,让系统直接从数据中学习最优策略。
将MQL5矩阵API集成到日常交易工作流中,能带来的收益着实令人惊叹。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18873
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
在 MQL5 中实现保本机制(第 2 部分):基于 ATR 和风险回报比的保本机制
协方差矩阵自适应演化策略(CMA-ES)
使用机器学习检测与分类分形模式