English Русский Deutsch 日本語
preview
MQL5中的自优化智能交易系统(第十一部分):线性代数基础入门

MQL5中的自优化智能交易系统(第十一部分):线性代数基础入门

MetaTrader 5示例 |
26 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

在之前关于矩阵分解的讨论中,我们已经探讨了使用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

交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
神经网络在交易中的应用:基于自适应模态分解(ACEFormer)的时间序列预测 神经网络在交易中的应用:基于自适应模态分解(ACEFormer)的时间序列预测
我们诚邀您探索 ACEFormer 架构 — 一种将概率注意力机制的有效性与自适应时间序列分解相结合的现代解决方案。本文将对那些寻求在金融市场中平衡计算性能和预测准确性的读者有所帮助。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
MQL5 MVC范式下的表格视图与控制器组件:可调整尺寸的元素 MQL5 MVC范式下的表格视图与控制器组件:可调整尺寸的元素
在本文中,我们将实现通过鼠标拖动控件的边缘和角点来调整控件大小的功能。