English Русский Deutsch 日本語
preview
MQL5中的自优化智能交易系统(第十部分):矩阵分解

MQL5中的自优化智能交易系统(第十部分):矩阵分解

MetaTrader 5交易系统 |
37 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

在本系列的开篇文章(链接见此处)中,我们曾尝试仅用原生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

附加的文件 |
Solve.mq5 (11.9 KB)
在 MQL5 中实现保本机制(第 2 部分):基于 ATR 和风险回报比的保本机制 在 MQL5 中实现保本机制(第 2 部分):基于 ATR 和风险回报比的保本机制
本文将完成 MQL5 中基于 ATR 和风险回报比(RRR)的保本机制实现,并从零开始开发一个类,使得在无需重新输入参数的情况下即可轻松切换保本模式。为评估每种保本类型的有效性,我们将进行多次回测,并在算法交易的背景下分析它们各自的优缺点。
协方差矩阵自适应演化策略(CMA-ES) 协方差矩阵自适应演化策略(CMA-ES)
本文探讨了一种最有趣的非梯度优化算法,该算法能够学习理解目标函数的几何特性。我们将重点关注对 CMA-ES 的经典实现,并稍作修改 — 用幂律分布代替正态分布。我们将深入探究算法背后的数学原理以及实际实现方式,并验证 CMA-ES 在哪些方面无懈可击,以及在哪些方面应避免使用。
使用机器学习检测与分类分形模式 使用机器学习检测与分类分形模式
在本文中,我们将探讨一个有趣的话题:利用机器学习进行分形分析和市场预测。这只是迈向探索金融价格图表上形成的各种分形结构的最初几步。我们将利用相关性来发现模式,并使用 CatBoost 算法对这些模式进行分类。
无需真实期权的期权交易(第二部分):实盘中的应用 无需真实期权的期权交易(第二部分):实盘中的应用
本文探讨了简单的期权策略及其在MQL5中的实现。我们将开发一个基础 EA,并逐步对其进行升级,使其变得更加复杂。