解方程

在机器学习方法和优化问题中,通常需要求解线性方程系统。MQL5 包含四个方法可根据矩阵类型求解该等方程。

  • Solve 求解线性矩阵方程或线性代数方程系统
  • LstSq 近似求解线性代码方程系统(对于非方形矩阵或退化矩阵)
  • Inv 使用约当-高斯法计算相对于方形非奇异矩阵的乘法逆元矩阵
  • PInv 使用摩尔-彭罗斯法计算伪逆矩阵

下面是方法原型。

vector<T> matrix<T>::Solve(const vector<T> b)

vector<T> matrix<T>::LstSq(const vector<T> b)

matrix<T> matrix<T>::Inv()

matrix<T> matrix<T>::PInv()

SolveLstSq 方法表示求解 A*X=B 形式的方程系统,其中 A 是矩阵,B 是使用函数的值(“因变量”)通过一个参数传递的向量。

我们尝试应用 LstSq 方法求解方程系统,该系统是一个理想组合交易模型(在本例中,我们将分析主要外汇货币组合)。为此,对于给定数量的“历史”柱线,我们需要找出均线趋向于一条持续增长直线时的每种货币的手数。

我们将第 i 个货币对表示为 Si。其在 k 索引柱线处的报价等于 Si[k]。柱线编号从过去到将来编号,如同由 CopyRates 方法填充的矩阵和向量中那样。因此,收集的用于训练模型的报价开头对应于标记为编号 0 的柱线,但在时间线上,它将是(根据算法设置处理的)最老的历史柱线。它右边的柱线(将来方向)编号为 1、2,依此类推,直至用户要求计算的总柱线数。

在第 0 条柱线和第 N 条柱线之间的交易品种价格的变化决定截至第 N 条柱线时的益/损。

考虑到货币对,我们得到第 1 柱线的以下利润方程:

(S1[1] - S1[0]) * X1 + (S2[1] - S2[0]) * X2 + ... + (Sm[1] - Sm[0]) * Xm = B

其中 m 是总字符数, Xi 是每个交易品种的手数,B 是浮动利润(若锁定利润则为条件性余额)。

为简洁起见,我们缩短表示法。我们从绝对值转到价格增量(Ai[k] = Si[k]-Si[0])。考虑跨柱线的移动,我们将获得若干虚拟余额曲线的表达式:

A1[1] * X1 + A2[1] * X2 + ... + Am[1] * Xm = B[1]
A1[2] * X1 + A2[2] * X2 + ... + Am[2] * Xm = B[2]
...
A1[K] * X1 + A2[K] * X2 + ... + Am[K] * Xm = B[K]

成功交易的特征为每条柱线的利润恒定,即,右手边向量 B 的模型是是一个单增函数,理想情况下是一条直线。

我们实施该模型并基于报价为其选择 X 系数。由于我们尚不知道应用程序 API,因此无法编写一个成熟的交易策略。我们只是使用来自标准头文件 Graphic.mqhGraphPlot 函数构建一个虚拟余额图(我们已经使用它演示过 数学函数)。

新示例的完整源代码在 MatrixForexBasket.mq5 脚本中。

在输入参数中,让用户选择用于数据采样的总柱线数 (BarCount),以及该选择范围内的柱线偏移量 (BarOffset),该偏移量对应“假设过去”结束和“假设未来”开始的分界柱线编号。

将对“假设过去”构建一个模型(将求解上述线性方程系统),并对“假设未来”执行前向测试。

input int BarCount = 20;  // BarCount (known "history" and "future")
input int BarOffset = 10// BarOffset (where "future" starts)
input ENUM_CURVE_TYPE CurveType = CURVE_LINES;

为了以一个理想的余额填充向量,我们编写 ConstantGrow 函数:稍后初始化期间将使用这个函数。

void ConstantGrow(vector &v)
{
   for(ulong i = 0i < v.Size(); ++i)
   {
      v[i] = (double)(i + 1);
   }
}

交易的金融工具(主要外汇对)列表在 OnStart 函数的开头硬设定,需进行编辑以适合你的要求和交易环境。

void OnStart()
{
   const string symbols[] =
   {
      "EURUSD""GBPUSD""USDJPY""USDCAD"
      "USDCHF""AUDUSD""NZDUSD"
   };
   const int size = ArraySize(symbols);
   ...

我们创建将在其中添加交易品种报价的 rates 矩阵、具有所需余额曲线的 model 向量以及用于逐交易品种请求柱线收盘价的辅助 close 向量(它包含的数据将被复制到 rates 矩阵的列中)。

   matrix rates(BarCountsize);
   vector model(BarCount - BarOffsetConstantGrow);
   vector close;

在一个交易品种循环中,我们将收盘价复制到 close 向量,计算价格增量,并将它们写入到 rates 矩阵的对应列中。

   for(int i = 0i < sizei++)
   {
      if(close.CopyRates(symbols[i], _PeriodCOPY_RATES_CLOSE0BarCount))
      {
         // calculate increments (profit on all and on each bar in one line)
         close -= close[0];
         // adjust the profit to the pip value
         close *= SymbolInfoDouble(symbols[i], SYMBOL_TRADE_TICK_VALUE) /
            SymbolInfoDouble(symbols[i], SYMBOL_TRADE_TICK_SIZE);
         // place the vector in the matrix column
         rates.Col(closei);
      }
      else
      {
         Print("vector.CopyRates(%d, COPY_RATES_CLOSE) failed. Error "
            symbols[i], _LastError);
         return;
      }
   }
   ...

我们将在第五章中探讨一个价格点值(以存款货币计)的计算。

还有一点务必要注意,具有相同索引的柱线可能对不同的金融工具有不同的时间戳,例如,如果某个国家因而节假日而休市(外汇以外的交易品种在理论上可能具有不同的交易时段计划表)。为解决这一问题,我们需要对报价更深度的分析,在将它们插入 rates 矩阵之前,需考虑到柱线时间及其同步。在这里,为了简单起见,同时也因为外汇市场大多数时间根据相同规则运作,因此我们不这么做。

我们将矩阵拆分为两部分:初始部分将用于求解(模拟历史优化),后续部分将用于前向测试(计算后续余额变化)。

   matrix split[];
   if(BarOffset > 0)
   {
      // training on BarCount - BarOffset bars
      // check on BarOffset bars
      ulong parts[] = {BarCount - BarOffsetBarOffset};
      rates.Split(parts0split);
   }
  
   // solve the system of linear equations for the model
   vector x = (BarOffset > 0) ? split[0].LstSq(model) : rates.LstSq(model);
   Print("Solution (lots per symbol): ");
   Print(x);
   ...

获得解后,我们来构建样本的所有柱线的余额曲线(理想的“历史”部分将在开头,然后是“将来”部分,后者不会用于调整模型)。

   vector balance = vector::Zeros(BarCount);
   for(int i = 1i < BarCount; ++i)
   {
      balance[i] = 0;
      for(int j = 0j < size; ++j)
      {
         balance[i] += (float)(rates[i][j] * x[j]);
      }
   }
   ...

我们根据 R2 标准估算解的质量。

   if(BarOffset > 0)
   {
      // make a copy of the balance
      vector backtest = balance;
      // select only "historical" bars for backtesting
      backtest.Resize(BarCount - BarOffset);
      // bars for the forward test have to be copied manually
      vector forward(BarOffset);
      for(int i = 0i < BarOffset; ++i)
      {
         forward[i] = balance[BarCount - BarOffset + i];
      }
      // compute regression metrics independently for both parts
      Print("Backtest R2 = "backtest.RegressionMetric(REGRESSION_R2));
      Print("Forward R2 = "forward.RegressionMetric(REGRESSION_R2));
   }
   else
   {
      Print("R2 = "balance.RegressionMetric(REGRESSION_R2));
   }
   ...

要在图表上显示余额曲线,你需要将数据从向量转移到数组。

   double array[];
   balance.Swap(array);
   
   // print the values of the changing balance with an accuracy of 2 digits
   Print("Balance: ");
   ArrayPrint(array2);
  
   // draw the balance curve in the chart object ("backtest" and "forward")
   GraphPlot(arrayCurveType);
}

下面是通过在 EURUSD,H1 上运行该脚本获得的一个日志的示例。

Solution (lots per symbol): 
[-0.0057809334,-0.0079846876,0.0088985749,-0.0041461736,-0.010710154,-0.0025694175,0.01493552]
Backtest R2 = 0.9896645616246145
Forward R2 = 0.8667852183780984
Balance: 
 0.00  1.68  3.38  3.90  5.04  5.92  7.09  7.86  9.17  9.88 
 9.55 10.77 12.06 13.67 15.35 15.89 16.28 15.91 16.85 16.58

下面是虚拟余额曲线的示例。

根据 SLAU 决定(“将来”开始处)按“手数”在货币篮中交易的虚拟余额

根据决定按“手数”交易货币组合的虚拟余额

左半部分的形状更均匀且 R2 更高,这并不奇怪,因为模型(X 变量)专门针对该部分进行调整。

出于兴趣,我们将训练和验证深度增加 10 倍,即我们将在参数中设置 BarCount = 200BarOffset = 100。我们将得到一个新图片。

根据 SLAU 决定(“将来”开始处)按“手数”在货币篮中交易的虚拟余额

根据决定按“手数”交易货币组合的虚拟余额

“将来”部分看起来更平滑,我们甚至可以说,它能够继续增长只是凭运气,尽管这是一个如此简单的模型。原则上,在前向测试期间,虚拟余额曲线会大大退化,开始下滑。

务必需要注意的是,为了测试该模型,我们采用从系统的“原样”解获得的 X 值,而在实践中,我们需要将它们标准化为最小手数和手梯度,这将对结果造成负面影响,使它们更接近现实。