MQL5中的自优化智能交易系统(第十一部分):线性代数基础入门
在之前关于矩阵分解的讨论中,我们已经探讨了使用MQL5 API内置线性代数指令的诸多优势。那篇入门内容默认读者具备一定前置知识 —— 即时间序列建模与统计分析的基础。然而,回过头来看,显然并非所有读者都具备这一基础。
如果您是初次接触这类主题,那篇入门文章可能并未达到预期的帮助效果。由于内容推进较快,大量概念接连涌现。因此,本文将把重心转向完全没有相关数学基础的读者。
我会从零起点视角讲解这些技术,确保所有读者都能跟上,并理解矩阵与向量API的价值。MQL5提供的矩阵与向量API固然强大,但它本身并不能独立地解决问题。只有理解线性代数原理,才能智能、高效地使用此套API。
本文将为没有接受过系统数学训练的读者搭建桥梁。我们将采用对比式教学:先展示不具备线性代数背景的 MQL5 开发者可能写出的代码,再与理解底层数学原理的开发者所写代码进行对比。这种并排对比的方式,能清晰直观地凸显线性代数的优势。
文中介绍的所有理论概念,都将直接关联实际交易应用,并在MetaTrader 5终端中演示。我们会使用真实市场数据,动手实践数学理论,展现这些线性代数指令的通用性与实用价值。
今天,我们将构建一个同时预测多个目标的统计模型。通常线性回归模型仅用于预测单一目标,例如未来的价格变动。而在本文案例中,我们将同时预测四个不同目标:
- 收盘价的未来移动平均值
- 最高价的未来移动平均值
- 最低价的未来移动平均值
- 未来价格
我们会将这些预测结果融入交易策略,定义开仓与平仓规则,以及平仓过滤条件。MQL5矩阵与向量API为构建现代机器学习应用提供了强大工具。但要释放该API的潜力,必须理解支撑这些专用方法正确使用的基础线性代数规则。
线性代数往往是一门抽象的数学学科。然而,我希望把这门课讲得生动易懂,让你清晰看到即将学习内容的价值,随后将在文章中段进一步讨论其中的数学内容。最后,在明确学习动机、解释完所有必要数学符号后,我们将演示一个实例:如何运用线性代数构建可同时预测多目标的数值驱动交易算法。
可维护的代码结构
大多数情况下,处理市场价格(如开盘价、最高价、最低价、收盘价的集合)时,将数据以矩阵形式存储会十分便捷。在MQL5中,矩阵索引先行后列。因此我们首先定义一个新矩阵A,为3×5矩阵并初始化为0。这意味着矩阵A包含3行5列,所有元素初始值均为0。随后我们输出当前状态的矩阵A,如图1所示,矩阵内确实全为0。
//--- Let's first create an empty matrix matrix A=matrix::Zeros(3,5); //--- Peek at the matrix Print("Original A matrix"); Print(A);

图例1:可视化空白矩阵A
接下来,我们为每一行赋值:第一行(索引0)全部填充常数1,第二行(索引1)全部为2,第三行(索引2)全部为3。
读者可以看到,MQL5中访问矩阵元素的写法始终是:矩阵名后跟方括号,先行索引,后列索引。我们可以再次查看矩阵A,确认赋值是否正确 —— 图例2可以证实赋值无误。
//--- The notation A[R,C] describes the Row and Column we want to manipulate //--- We will set all the values in Row 1 to be 1 A[0,0] = 1; A[0,1] = 1; A[0,2] = 1; A[0,3] = 1; A[0,4] = 1; //--- We will set all the values in Row 2 to be 2 A[1,0] = 2; A[1,1] = 2; A[1,2] = 2; A[1,3] = 2; A[1,4] = 2; //--- We will set all the values in Row 3 to be 3 A[2,0] = 3; A[2,1] = 3; A[2,2] = 3; A[2,3] = 3; A[2,4] = 3; Print("Current A matrix"); Print(A);

图例2:尝试给矩阵A的行进行标记
现在,让我们来对矩阵内部的数值进行操作。假设我们想把矩阵A中第二行的所有值都乘以5。一种直观的写法可能是用一个for循环遍历第二行的每个元素,分别乘以5,再保存结果。由此可见,这种方法能得到预期结果,也能通过所有功能测试。
不过,读者能想到我们为什么要避免用for循环来做这件事吗?
//--- Let's multiply all the values of Row 2 by 5 and leave all the other rows the same. //--- Bad performing code //--- Copy matrix A matrix example_1; example_1.Assign(A); //--- Loop over matrix A and multiply each element by 5 and then replace the original elements for(int i =0;i<5;i++) { example_1[1,i] = example_1[1,i] * 5; } //--- Done Print("Example 1: "); Print(example_1);

图例3:当矩阵A规模变大时,使用for循环操作可能会变得过于缓慢
让我们来看一种稍作改进的写法。不再使用循环,而是直接将矩阵的第二行选取为行向量,将其乘以5,再把结果赋值回原位置。这样产生的效果完全相同,且代码更简洁优雅。但我还是要再问一遍:您能想到为什么即便这样做,也未必是最优方案吗?
//--- Slightly better code //--- Copy the row, multiply it and then put it back matrix example_2; vector copy_vector; example_2.Assign(A); copy_vector = example_2.Row(1); example_2.Row(copy_vector*5,1); Print("Example 2"); Print(example_2);

图例4:使用向量方法操作矩阵A优于传统for循环,但仍非最优
最后,我将演示在此场景下更合适的做法:先创建一个缩放向量,再通过矩阵乘法将该向量作用于矩阵A。如图所示,三段代码最终的结果完全一致。然而,读者应当注意到几个关键的区别。
第三种方法代码行数最少,这体现了在日常交易任务中使用线性代数的核心优势:可以写出更简洁、更易于维护的代码。在后续内容中,我还会展示更多的优势。这里只是一个很好的起点。
//--- Reliable code matrix example_3,scaler; vector scale = {1,5,1}; scaler.Diag(scale); example_3 = scaler.MatMul(A); //--- Done Print("Example 3"); Print(example_3);

图例5:优先使用专门的矩阵与向量内置方法永远是最优选择
这里,我们把例子扩展一下。之前我们只把第二行乘以5。现在我们把第一行乘以2,最后一行乘以10,中间一行保持不变。到这里,读者应该开始能看出使用for循环的问题了。随着对矩阵A操作的增多,循环的长度也会增加 —— 我们需要编写的代码行数也随之变多。此外,如果矩阵A足够大,像循环那样逐个遍历每一个数值,会显著降低执行速度,尤其是在回测过程中。
//--- Now, multiply the first and last rows by 2 and 10, but leave the middle row as it is. //--- Loops can slow us down during backtests, especially if they must be repeated often. for(int i =0;i<5;i++) { example_1[0,i] = example_1[0,i] * 2; example_1[2,i] = example_1[2,i] * 10; } //--- Done Print("Example 1"); Print(example_1);

图例6:为了实现同样的效果,我们不得不在for循环中编写更多的代码
同样地,随着操作数量增加,逐行选取再重新赋值的方法也会变得越来越复杂。尽管这种方式优于基础的for循环,但依然会导致代码臃肿,进而增加出错的概率。
//--- The difference between example 2 and 3 starts to show //--- Copy the row, multiply it and then put it back vector copy_vector_2; copy_vector = example_2.Row(0); copy_vector_2 = example_2.Row(2); example_2.Row(copy_vector*2,0); example_2.Row(copy_vector_2*10,2); //--- Done Print("Example 2"); Print(example_2);

图例7:串联通用矩阵与向量方法虽然也能完成任务,但我们还有更优方案
借助矩阵乘法,我们还能做得更好。使用这种方法,唯一需要改变的只有缩放系数。无论涉及多少行,代码的其余部分基本保持不变。三种方法最终输出完全一致 —— 但在并排对比后,我想问问读者:在处理海量历史市场数据时,哪种方案看起来最合适?
//--- Reliable code vector scale_2 = {2,1,10}; scaler.Diag(scale_2); example_3 = scaler.MatMul(example_3); //--- Done Print("Example 3"); Print(example_3);

图例8:通过更精简的编程写法,我们能用更少代码实现完全相同的输出
回测耗时
使用传统for循环时,我们无法高效处理全部数据点,在回测这类对时效敏感的运算中问题尤为突出。由此引出第二个核心优势:合理运用矩阵与向量API,不仅能大幅降低代码维护成本,还能提升交易策略的执行效率。
归根结底,绝大多数读者的目标是搭建实用的量化交易AI模型 —— 能够自主做出可靠交易判断的模型。但AI模型想要输出有效决策,必须依托海量的数据。而在将数据送入模型前,我们需要完成一系列数据预处理与数值变换操作。
如果处理方式低效,尤其是依靠for循环实现时,程序代码量会急剧膨胀。代码体量越大,回测所需耗费的时间也会同步增加。

图例9:使用for循环编写的代码,回测运行耗时会过长
更好的替代方案是串联调用向量、矩阵运算接口。该方式运行速度远优于原生循环,但如果需要反复复制、修改、重新赋值矩阵行,代码依旧会变得繁琐。这类操作越多,程序运行耗时就越长,再次拖累回测效率与模型优化流程。

图例10:串联矩阵向量API运算比for循环更快,但仍存在优化空间
另一方面,依托线性代数原生向量矩阵运算,无论数据规模如何增长,程序执行耗时、代码长度、回测耗时都能保持稳定可控。这是应用程序中极具价值的特性:我们可以新增大量运算逻辑,而无需同步成倍增加代码量,也不会拉长运行时间,从而更高效地迭代、优化机器学习模型。我相信大多数读者都会感到惊叹:仅仅掌握少量线性代数基础概念,就能带来如此巨大的程序性能提升。无需钻研高深理论,就能让我们的量化程序获得实质性改善。

图例11:运用线性代数运算可让程序保持恒定运行耗时
计算精度
我还想强调一个在技术讨论中常被忽略的关键问题:计算精度。这个话题值得更多的关注与深入探讨,却往往得不到应有的重视。计算精度是构建稳定可靠交易策略的核心要素之一。回测结果必须精准,程序内部的数值运算与交易决策也需要足够精确和可信。 举个典型场景:一段示例代码中执行简单浮点数减法 —— 0.3 - 0.1。计算机输出结果是0.199999...,而非我们预期的0.2。这是计算机科学中广为人知的浮点数运算缺陷,并非MQL5独有。
我想着重说明一点:试想在循环中反复执行这类减法;再试想遍历拥有百万行数据的矩阵A,对每一行都执行该运算。不难发现,微小的精度误差会不断累积放大,最终导致计算结果出现严重数值失稳。
草率地进行数值运算(例如直接在大型矩阵上做这类减法),不仅运行效率低下,计算结果也无法保证精度与数值稳定性。
除此之外,前文示例中的算法会频繁复制数值、新建对象、重赋值数据,持续申请与释放内存,会进一步放大精度问题。计算机内存资源有限,低效的矩阵操作会反复创建、销毁对象,无端加重内存负载。这种杂乱无章的内存管理方式,会产生很多我们容易忽视的实际负面影响。
好在业界已有成熟优化算法,其中大量算法已内置在MQL5矩阵向量API,以及后续我们会讲解的配套函数库中。这些内置实现经过专门优化,最大限度降低浮点数误差,保障数值计算稳定。与之相反,如果开发者选择低效方案(比如手动写for循环处理数据),会在无形中大幅提升问题出现的概率。
//--- Why should you care? //--- Let's start with an often overlooked need, precision! Print("Our computers have limited memory to store numbers with precision"); Print("What is 0.3 - 0.1"); Print(0.3-0.1); Print("You and I know the correct answer is 0.2");

图例12:合理运用矩阵、向量线性代数运算,能够最大限度减少上述计算误差
交易者该如何运用线性代数?
相信读者已经认识到学习线性代数的诸多优势,接下来我们讲解用于量化决策的核心基础规则。掌握这些核心原理是一项极具实用价值的技能。
想要学好线性代数,首先必须牢固掌握基础代数知识。代数本质上是研究未知量的数学工具。让我们先看一下图13的简单例题:这个方程表达的含义是,某个未知数乘以2结果等于4。想要求出未知数x,只需等式两边同时除以2。验证答案也很简单,把x = 2代回原式,检验2×2是否等于4。虽然该例十分基础,但它是理解更复杂概念的铺垫。

图例13:简易代数方程演示,方程解为x = 2
我们再来看一个变形:如果方程为A・x = 4。按照之前的思路,等式两边同时除以A,解写作x = 4/A。由于题目未给出A的具体数值,则该式即为最终解。

图例14:图13例题的进阶变形版本
现在,如果A不再是单个数字,而是一个矩阵呢?那么我们就从普通初等代数过渡到了线性代数的范畴。这里,A为矩阵,我们很容易下意识像之前那样,对等式两边除以A。但在线性代数体系中,矩阵除法是无定义运算。其替代方案是使用矩阵逆,如果该矩阵存在逆矩阵,则等式两边同时左乘A的逆矩阵来求解方程。

图例15:线性代数沿用简易代数的核心逻辑,仅调整少量运算规则
放到行情数据场景中,该公式代表什么含义?假设矩阵A代表当前市场行情数据(例如各价位数据),向量y代表我们想要预测的未来价格,您需要求解系数向量x,满足:当前行情矩阵 × 系数向量 = 未来价格向量。
但这里存在一个关键问题:并非所有矩阵都存在逆矩阵。强行对不可逆矩阵求逆,会直接导致交易程序崩溃,或是输出完全不可靠的结果。因此,我们不能对任意矩阵“盲目”求逆。更安全且数值稳定性更强的做法是截取A的子矩阵(选取可稳定求逆的数据片段),或是采用QR分解、奇异值分解(SVD)、伪逆等数值稳定的矩阵分解算法处理数据。

图例16:通用线性方程组求解思路拓展
利用线性代数优化交易策略
现在,我们已经熟悉了用线性代数求解线性方程组的基础规则,接下来实战求解系数向量x。在本次案例中,我将演示这套公式既可以预测单一目标,也能轻松同时预测y中的多个目标值。首先定义模型固定参数,我们需要先确定模型的输入特征数量,本案例模型共设置8个输入特征。
//+------------------------------------------------------------------+ //| 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 8
接下来,我们需要定义关键的系统参数。例如,需要获取的历史K线数量、要预测未来多少个周期、使用的时间周期,以及其他相关设置。所有这些细节都将保存在系统参数中。
//+------------------------------------------------------------------+ //| System parameters | //+------------------------------------------------------------------+ int bars = 90; //Number of historical bars to fetch int horizon = 1; //How far into the future should we forecast int MA_PERIOD = 2; //Moving average period ENUM_TIMEFRAMES TIME_FRAME = PERIOD_D1; //User Time Frame ENUM_TIMEFRAMES RISK_TIME_FRAME = PERIOD_D1; //Time Frame for our ATR stop loss double sl_size = 2; //ATR Stop loss size
任何程序中的依赖项都非常重要,因为它们能减少不同项目之间需要重复编写的代码总量。因此,我们将加载几个关键依赖库,例如Trade交易模块依赖项,它预装在每个MetaTrader 5的版本中。其余两个依赖项则是为我们的交易任务专门编写的自定义模块。
//+------------------------------------------------------------------+ //| Dependencies | //+------------------------------------------------------------------+ #include <Trade\Trade.mqh> #include <VolatilityDoctor\Time\Time.mqh> #include <VolatilityDoctor\Trade\TradeInfo.mqh>
我们的系统还会定义关键的全局变量,供程序内不同的模块共用。例如,我们会用全局变量存储当前指标值;另一部分用于保存依赖库所需的参数;还有一部分用来存放从数据中训练得到的模型系数。这些变量包括ATR指标数值,以及系统中其他各类动态计算值。
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ int ma_close_handler,ma_high_handler,ma_low_handler; double ma_close[],ma_high[],ma_low[]; Time *Timer; TradeInfo *TradeInformation; vector bias,temp,temp_2,temp_3,temp_4,temp_5,Z1,Z2; matrix X,y,prediction,b; int time; CTrade Trade; int state; int atr_handler; double atr[];
初始化时,系统会为我们加载的自定义依赖项创建新对象。定时器用于跟踪新K线的形成。交易信息模块会返回关键信息,如最小交易量、Ask价和Bid价。我们还会创建用于跟踪移动平均线的处理器,并将矩阵和向量初始化为占位值1,以便程序启动运行。
//+------------------------------------------------------------------+ //| 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); ma_high_handler = iMA(Symbol(),TIME_FRAME,MA_PERIOD,0,MODE_SMA,PRICE_HIGH); ma_low_handler = iMA(Symbol(),TIME_FRAME,MA_PERIOD,0,MODE_SMA,PRICE_LOW); 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(),RISK_TIME_FRAME,14); //--- return(INIT_SUCCEEDED); }
当不再使用程序时,我们会释放所有占用内存资源的对象。这是MQL5中的良好编程习惯,我们将始终遵循这一原则。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- IndicatorRelease(atr_handler); IndicatorRelease(ma_close_handler); IndicatorRelease(ma_high_handler); IndicatorRelease(ma_low_handler); }
在OnTick事件处理函数中收到最新价格时,我们首先检查是否有新的K线完全形成。如果已经形成,就将所有指标读数复制到对应的数组中,并让模型准备进行行情预测。完成后,记录当前收盘价,并分别计算空单和多单的止损位。
接下来,我们显示模型预测的各个价格水平。重申一下,我们同时预测四项内容:收盘价均线、最高价均线、最低价均线,以及价格本身。如果当前没有持仓,先重置相关状态变量。然后比较预测收盘价与预测收盘价均线之间的关系。
通常,我们希望算法给出的收盘价均线预期值高于当前收盘价。这就意味着当前价格被低估,即市价低于模型认定的合理价值。
为进一步确认,我们还要确保多单和空单的止损都不太可能被触发。买入条件:确认预测最低价均线不会跌破多单止损;卖出条件:确认预测最高价均线不会涨破空单止损。只要任一条件不满足,我们就不入场。
此外,我们还会将每条均线与其对应基准进行配对比较,买入时,预期未来的最低价均线高于当前值;卖出时,预期未来的最低价均线低于当前值。同样,买入时我们希望未来的最高价均线高于当前值。
平仓时,首先判断价格是否可能朝不利方向运行。如果预判某条均线可能触发止损,就立即离场;否则,就让止损继续追踪,向更有利的价位移动。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if(Timer.NewCandle()) { CopyBuffer(atr_handler,0,0,1,atr); CopyBuffer(ma_close_handler,0,0,1,ma_close); CopyBuffer(ma_low_handler,0,0,1,ma_low); CopyBuffer(ma_high_handler,0,0,1,ma_high); setup(); double c = iClose(Symbol(),TIME_FRAME,0); double buy_sl = (TradeInformation.GetBid() - (sl_size * atr[0])); double sell_sl = (TradeInformation.GetAsk() + (sl_size * atr[0])); Comment("Forecasted MA: ",prediction[0,0],"\nForecasted MA High: ",prediction[1,0],"\nForecasted MA Low: ",prediction[2,0],"\nForecasted Price: ",prediction[3,0]); if(PositionsTotal() == 0) { state = 0; if((prediction[0,0] > c) && (prediction[2,0] > buy_sl) && (prediction[3,0] > c) && (prediction[2,0] > ma_low[0]) && (prediction[1,0] > ma_high[0])) { Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),buy_sl,0); state = 1; } if((prediction[0,0] < c) && (prediction[1,0] < sell_sl) && (prediction[3,0] < c) && (prediction[2,0] < ma_low[0]) && (prediction[1,0] < ma_high[0])) { Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),sell_sl,0); state = -1; } } if(PositionsTotal() > 0) { double current_sl = PositionGetDouble(POSITION_SL); if(((state == -1) && (prediction[0,0] > c) && (prediction[1,0] > current_sl)) || ((state == 1)&&(prediction[0,0] < c)&& (prediction[2,0] < current_sl))) Trade.PositionClose(Symbol()); if(PositionSelect(Symbol())) { 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); } } } } }
接下来,我们讲解为上述任务编写的几个具体函数。第一个函数是prepare_data()。该函数只有一个核心用途:将我们需要的所有价格数据复制到输入数据矩阵X中。它会获取开盘价,计算开盘价的均值与标准差,然后通过减去均值再除以标准差的方式对数据进行标准化处理。所有输入都会重复这一过程。同时,通过各均线句柄读取到的数值也会被复制并存入目标矩阵y中。
//+------------------------------------------------------------------+ //| Prepare the training data for our model | //+------------------------------------------------------------------+ void prepare_data(void) { //--- 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); //--- Store the Z-scores temp.CopyIndicatorBuffer(ma_high_handler,0,horizon,bars); Z1[5] = temp.Mean(); Z2[5] = temp.Std(); temp = ((temp - Z1[5]) / Z2[5]); X.Row(temp,6); //--- Store the Z-scores temp.CopyIndicatorBuffer(ma_low_handler,0,horizon,bars); Z1[6] = temp.Mean(); Z2[6] = temp.Std(); temp = ((temp - Z1[6]) / Z2[6]); X.Row(temp,7); //--- Labelling our targets temp.CopyIndicatorBuffer(ma_close_handler,0,0,bars); temp_2.CopyIndicatorBuffer(ma_high_handler,0,0,bars); temp_3.CopyIndicatorBuffer(ma_low_handler,0,0,bars); temp_4.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_CLOSE,0,bars); //--- Reshape y y.Reshape(4,bars); //--- Store the targets y.Row(temp,0); y.Row(temp_2,1); y.Row(temp_3,2); y.Row(temp_4,3); }
接下来,我们定义一个用于拟合模型的函数。该函数首先创建所需的矩阵与向量。我们对矩阵X进行奇异值分解,并将结果保存到前面声明的变量中。通过闭式解公式,我们可以从X中求解出系数矩阵b,然后打印出从b中学习到的模型系数。
//+------------------------------------------------------------------+ //| Fit our model | //+------------------------------------------------------------------+ void fit(void) { //--- Fit the model matrix OB_U,OB_VT,OB_SIGMA; vector OB_S; 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); b = y.MatMul(OB_VT.Transpose().MatMul(OB_SIGMA.Inv()).MatMul(OB_U.Transpose())); Print("OLS Solutions: "); Print(b); }
要生成预测,我们会像在prepare_data中一样,重新获取所有的输入数据,然后执行最后一次矩阵乘法,利用系数矩阵B得到预测结果。简言之,就是用模型系数乘以输入数据。
//+------------------------------------------------------------------+ //| Get a prediction from our multiple output model | //+------------------------------------------------------------------+ void predict(void) { //--- 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); temp.CopyIndicatorBuffer(ma_high_handler,0,0,1); temp = ((temp - Z1[5]) / Z2[5]); X.Row(temp,6); temp.CopyIndicatorBuffer(ma_low_handler,0,0,1); temp = ((temp - Z1[6]) / Z2[6]); X.Row(temp,7); Print("Prediction Inputs: "); Print(X); //--- Get a prediction prediction.Reshape(1,4); prediction = b.MatMul(X); Print("Prediction"); Print(prediction); }
最后,每当OnTick事件处理函数收到最新价格时,我们都会调用setup函数。该函数会依次调用我们刚才介绍的三个核心函数:准备数据、拟合模型,并生成预测结果。
//+------------------------------------------------------------------+ //| Obtain a prediction from our model | //+------------------------------------------------------------------+ void setup(void) { prepare_data(); fit(); Print("Training Input Data: "); Print(X); Print("Training Target"); Print(y); predict(); } //+------------------------------------------------------------------+ #undef TOTAL_INPUTS //+------------------------------------------------------------------+
完成以上全部代码搭建后,我们就可以基于历史数据对程序开展回测。如下图例17所示,我们将该交易程序应用于2022至2025年的欧元兑美元(EUR/USD)品种,回测周期覆盖两年多的历史行情数据。

图例17:回测采用跨度两年的欧元兑美元历史行情数据
我们还有基于真实tick的随机延迟设置,尽可能还原真实的市场环境。如果您想要真实模拟程序的实际运行表现,建议采用完全相同的参数配置。

图例18:设置随机延迟参数,在贴近实盘的市场环境下测试交易程序
由图例18可见,程序能够同时生成四项独立预测值,分别对应我们关注的四类价格指标。程序会调用主体代码中编写的筛选逻辑,结合四项预测结果判断是否开仓。

图例19:回测交易算法,验证其同时预测四类目标值的能力
我附上了终端日志截图,可以证明程序已经成功训练出系数矩阵。由此可见,解矩阵包含四行,代表程序为四个预测目标分别训练了一套专属系数,各目标独立完成拟合运算。

图例20:交易程序为指定的四个预测目标分别生成一套独立系数
此外,账户净值曲线整体呈现向上增长的正向趋势。虽然净值曲线存在波动起伏,后续我们会持续优化系统,让收益增长更加平稳。

图例21:两年回测周期内账户净值增长可视化曲线
细化分析账户整体表现可以看到,盈利交易占比达到51%。这个结果是不错的起点,后续我们目标将胜率提升至55%甚至60%。现阶段整体表现开局良好 —— 平均盈利金额高于平均亏损金额,最大盈利接近最大亏损的两倍。说明该交易系统的底层逻辑可行,当然我们仍有优化提升的空间。

图例22:交易程序回测绩效详细分析报表
结论
总体而言,本文向读者阐明了扎实掌握线性代数知识的重要性,以及线性代数如何直接影响我们在MetaTrader 5软件中处理行情数据的效率。如果没有掌握相关知识,那么海量市场数据的分析工作将会变得极其棘手。只需掌握少量线性代数核心原理,并了解其在MQL5中的实现方式,我们就能更高效且更稳定地从市场中提炼出更有价值的信息。后续内容中,我们会讲解各类线性代数工具,并演示如何在MQL5中改写运用,搭建数值稳定和运行高速的交易算法。至此,读者已经具备了接近恒定的运行时间算法的能力,即便需要同时预测多个目标变量,也能快速完成回测与策略迭代优化。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18974
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
神经网络在交易中的应用:基于自适应模态分解(ACEFormer)的时间序列预测
新手在交易中的10个基本错误