English Русский Deutsch 日本語
preview
MQL5 自优化 EA(第12部分):利用矩阵分解构建线性分类器

MQL5 自优化 EA(第12部分):利用矩阵分解构建线性分类器

MetaTrader 5 — 示例 |
37 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

矩阵分解对想要开发数值驱动型程序的算法交易者而言,是一项重要工具。借助这些工具,我们可以搭建各类机器学习算法,以及更多其他模型。到目前为止的讨论中,我们只研究了回归任务。接下来我们把目光转向分类问题。本次我们将尝试构建一个市场分类器。该分类器能够区分市场上涨与下跌行情。我们希望借助它正确地执行交易。分类器的任务是从历史市场行情数据中学习,推断出在特定交易日应当采取的正确操作。

我们的交易策略逻辑如下:策略目标是根据移动平均线指标的预期走势预判市场行情。除此之外,我们要求价格走势与移动平均线方向一致。也就是说:如果分类模型预测移动平均线将下行,我们希望看到价格进一步跌破并运行在该指标下方。若预判移动平均线与价格同步下跌,我们就开空单。移动平均线用于指示价格方向,但在开仓前,我们还希望价格加速突破该指标并进一步远离它。

做多逻辑同理。我们预判均线上行,且价格要大幅向上突破均线,此时才进场强势做多。

从上面的描述能看出,模型会同时输出两个分类结果。但不要把它和多分类模型混淆。模型预测的这两个变量,各自都是二分类结果。换句话说,模型同时处理两组独立的二分类任务。这里介绍的架构并不适用于单次执行多类别分类。

图 1:交易策略运行可视化



用矩阵分解构建线性分类器

和我们大多数交易程序一样,先定义系统参数。在本例中,输入数据矩阵 X 共包含 6 个输入特征。

//+------------------------------------------------------------------+
//|                                            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                                                    |
//+------------------------------------------------------------------+
int bars = 200;//Number of historical bars to fetch
int horizon = 1;//How far into the future should we forecast
int MA_PERIOD = 50; //Moving average period
ENUM_TIMEFRAMES TIME_FRAME = PERIOD_D1;//User Time Frame
input ENUM_TIMEFRAMES RISK_TIME_FRAME = PERIOD_D1;
input double sl_size = 2;

此外,程序需要引入必要的系统库,包括交易库,以及我编写的两个自定义库,分别用于时间处理和获取交易关键信息。

//+------------------------------------------------------------------+
//| Dependencies                                                     |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
#include <VolatilityDoctor\Time\Time.mqh>
#include <VolatilityDoctor\Trade\TradeInfo.mqh>

接下来,程序还需要为本案例创建关键全局变量。本例中,全局变量将作为技术指标的句柄。我们需要全局变量,用来保存从市场获取的输入数据与输出数据。同时还需要指标缓冲区,用来读取技术指标的实时数值。

//+------------------------------------------------------------------+
//| 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[];

当系统首次加载时,会调用一个专门的 initialize 方法,完成所有系统变量的初始化工作。 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   initialize();
//---
   return(INIT_SUCCEEDED);
  }

当系统不再使用时,我们需要释放技术指标所占用的内存。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   IndicatorRelease(atr_handler);
   IndicatorRelease(ma_close_handler);
  }

每当新的 K 线形成时,程序会调用两个专用方法。第一个方法是 setup 方法,第二个是 find_setup 方法。后续章节我们会详细讲解这两个函数。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(Timer.NewCandle())
     {
      setup();
      find_setup();
     }
  }

初始化各类对象与指标句柄,并将它们赋给相应的全局标识符。举个例子:我们会动态创建时间库对象,用来跟踪 K 线生成状态。同时创建交易信息类对象。该类负责记录各类关键信息,例如当前品种允许的最小交易手数、当前卖价和买价。除此之外,initialize 函数还负责为矩阵及相关标识符加载全部默认值。

//+------------------------------------------------------------------+
//| Initialize our system variables                                  |
//+------------------------------------------------------------------+
void initialize(void)
  {
   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);
   atr_handler             = iATR(Symbol(),RISK_TIME_FRAME,14);
   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;
  }

find_setup () 函数,相比前面介绍的其他函数逻辑要复杂很多。首先,findSetup 函数记录当前收盘价,接着复制指标的实时数值。也就是把 ATR 与移动平均线的读数拷贝到对应的缓冲区中。接下来,如果当前没有持仓,就重置系统状态。之后读取线性分类器给出的预测结果。

回顾一下:我们的分类器会同时输出两个预测值。第一个预测值代表移动平均线未来预期运行方向;第二个预测值反映价格相对移动平均线的位置关系。我们要求这两组预测结果保持一致。如果预测均线将会下行,同时希望价格进一步跌到均线下方;如果预测均线将会上行,则希望价格上涨至均线上方。

这两组判定条件构成本交易策略的核心。做多条件:两个预测值同时大于 0.5。做空条件:两个预测值同时小于 0.5。其他情况:如果已有持仓,则持续监控持仓状态与线性分类器的预期。若当前持有空单,但模型预判均线即将上行,则平仓。做多持仓的平仓逻辑与之相反。

其余场景,若行情条件没有触发平仓,我们会把止损调整到更有利的位置,继续等待后续行情。

//+------------------------------------------------------------------+
//| Find a trading setup for our linear classifier model             |
//+------------------------------------------------------------------+
void find_setup(void)
  {
   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] > 0.5) && (prediction[1,0] > 0.5))
        {
         Trade.Buy(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetAsk(),(TradeInformation.GetBid() - (sl_size * atr[0])),0);
         state = 1;
        }
      if((prediction[0,0] < 0.5) && (prediction[1,0] < 0.5))
        {
         Trade.Sell(TradeInformation.MinVolume(),Symbol(),TradeInformation.GetBid(),(TradeInformation.GetAsk() + (sl_size * atr[0])),0);
         state = -1;
        }
     }

   if(PositionsTotal() > 0)
     {
      if(((state == -1) && (prediction[0,0] > 0.5)) || ((state == 1)&&(prediction[0,0] < 0.5)))
         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] + (2 * atr[0]))<current_sl))
              {
               Trade.PositionModify(Symbol(),(ma_close[0] + (2 * atr[0])),0);
              }
        }
     }
  }

我们编写了专用方法,用来训练拟合线性分类器。如下所示的 fit 方法,用于拟合数据集,将历史价格输入映射到我们需要跟踪的两个目标输出。请记住,我们调用 OpenBLAS 库中的奇异值分解(SVD)算法,快速对模型做矩阵分解,以此求解最优系数,并将系数保存到变量 b 中。

//+------------------------------------------------------------------+
//| Fir our classification model                                     |
//+------------------------------------------------------------------+
void fit(void)
  {
//--- Fit the model
   matrix OB_U,OB_VT,OB_SIGMA;
   vector OB_S;
   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()));
  }

一如既往,数据读取与存储是所有机器学习模型的重要环节。我们会对每一项输入数据做标准化处理:减去均值,再除以标准差。

举例来说,我们需要提取开盘价、最高价、最低价以及技术指标数值。所有这些数据,在存入数据矩阵 X 之前,都要执行减均值、除以标准差的标准化操作。

在重塑输出矩阵 Y 之后,我们把输出标签设置为 0 或 1:0 代表价格下跌,1 代表价格上涨。通过这种方式,我们构建分类模型。

//+------------------------------------------------------------------+
//| Prepare the data needed for our classifier                       |
//+------------------------------------------------------------------+
void fetch_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);

//--- Reshape the output target
   y.Reshape(2,bars);
   vector temp_2,temp_3,temp_4;

//--- Prepare to label the target accordingly
   temp.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_CLOSE,horizon,bars);
   temp_4.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_CLOSE,0,bars);

   temp_2.CopyRates(Symbol(),TIME_FRAME,COPY_RATES_CLOSE,0,bars);
   temp_3.CopyIndicatorBuffer(ma_close_handler,0,0,bars);

   for(int i=0;i<bars;i++)
     {
      //--- Record if price levels appreciated or depreciated
      if(temp[i] > temp_4[i])
         y[0,i] = 0;
      else
         if(temp[i] < temp_4[i])
            y[0,i] = 1;

      //--- Record if price levels remained above the moving average indicator, or fell beneath it.
      if(temp_2[i] < temp_3[i])
         y[1,i] = 0;
      if(temp_2[i] > temp_3[i])
         y[1,i] = 1;
     }

   Print("Training Input Data: ");
   Print(X);

   Print("Training Target");
   Print(y);
  }

完成数据准备后,我们就可以使用分类模型来生成预测。想要从分类模型获取预测结果,需要输入数据矩阵 X 的最后一行。也可以理解为读取当前市场行情、当前市场状态。我们把当前行情特征,乘以从历史数据训练得到的系数,就可以得到预测值。

//+------------------------------------------------------------------+
//| Obtain a prediction from our classification 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);

   Print("Prediction Inputs: ");
   Print(X);

//--- Get a prediction
   prediction = b.MatMul(X);
   Print("Prediction");
   Print(prediction);
  }

如果逐个调用上述函数会比较繁琐。因此我们设计了专用的 setup 函数,按照预定顺序依次调用这三个方法。第一步读取数据,第二步用数据拟合模型,最后一步生成预测。

这个简洁的 setup 函数可以按正确顺序一次性完成这三步操作。

//+------------------------------------------------------------------+
//| Obtain a prediction from our model                               |
//+------------------------------------------------------------------+
void setup(void)
  {
   fetch_data();
   fit();
   predict();
  }
//+------------------------------------------------------------------+

最后,我们需要取消程序开头定义的系统常量。

#undef TOTAL_INPUTS

回测从 2020 年 1 月开始,在日线周期上运行 5 年,直到 2025 年。回测品种选用欧元兑美元(EURUSD)。

图 2:选择回测与优化时间区间

除此之外,为了获取最优参数,我们启用随机延迟设置,保证回测环境尽可能贴近未来真实市场环境。

图 3:回测条件的选择同样很重要

我截取了 EA 回测运行时的性能截图。可以看到程序输出了传入模型的预测输入特征。前面提到,第一个输入固定为 1,代表截距项。但我们重点关注模型输出的实际预测结果。

图 4:线性分类器算法运行输出结果

如前文所述,模型会输出两组预测值。第一组预测均线未来位置的变化;第二组预测价格相对均线的变化。我们要求这两组变化方向保持一致。

我们还可以查看本交易策略生成的资金曲线。可以看到资金曲线整体向上,但稳定性不足。

读者需要留意,图中曲线包含两条线:蓝色线是账户余额曲线,绿色线是账户净值曲线。可以观察到多处净值(绿色线)大幅高于余额(蓝色线)。

以作者本人的经验来看,这种现象代表策略错失了部分利润、漏掉了交易信号。行情信号出现,但策略没有捕捉到,因此没能兑现这部分盈利。这说明策略仍有改进空间。

图 5:资金曲线表明还有部分行情信号未能被模型学习并捕捉

最后,我们来看完整的绩效统计数据。可以看到,平均盈利交易的收益大于平均亏损交易的损失。策略产生的最大盈利也高于最大亏损,这一点很鼓舞人心。

但观察盈利交易占比,胜率仅 43%,不足一半。这印证了前面看资金曲线时发现的问题:存在大量净值冲高但未能落袋的浮动盈利行情。

这进一步说明系统仍有优化空间,还有很多信息特征没有被模型提取。虽然目前成果是不错的起点,但仍有更多行情信号等待挖掘学习。

图 6:详细绩效统计佐证了前面的结论:系统尚有优化空间



结论 

总而言之,本文向读者展示了矩阵分解在算法交易、MQL5 程序中的多种应用方式。在系列文章开篇,我们把矩阵分解当作回归建模工具。而本文,我们将其用于分类任务。

在这里我只向读者做出一个承诺:还有大量内容有待讲解。以上仅仅是基础内容。在作者看来,这些都是简单应用,可以快速展示矩阵分解的价值。如果你一直在跟进本系列文章,读完本篇,你已经掌握如何用矩阵分解搭建回归模型、分类模型,或是二者组合模型;这类模型不再局限于单次单目标预测,可以同时预测多个目标。

但我向读者保证,矩阵分解还有更多强大的特性没有介绍。在探索更多高级用法之前,我们必须先夯实基础。同时你也能看到,依托 MQL5 API 内置函数,把这套强大的矩阵算法集成到交易程序中并不复杂。

本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/18987

附加的文件 |
决斗者算法 决斗者算法
如果您的交易策略能像真实的决斗者一样互相较量、彼此学习,会怎样?决斗者算法(Duelist Algorithm)是一种新的优化方法:在该算法中,不同的参数组合会像决斗者一样相互较量,争夺最优解的位置。
从基础到进阶:对象事件(II) 从基础到进阶:对象事件(II)
本文将介绍由对象触发的最后三类事件的工作原理。理解这些内容会非常有意思,因为文末我们要实现的功能在很多人看来有些疯狂,但它完全可行,并且效果十分惊艳。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
市场模拟:MQL5 中的 SQL 入门(一) 市场模拟:MQL5 中的 SQL 入门(一)
在今天的文章中,我们将开始学习如何在 MQL5 代码中使用 SQL。我们还将探讨如何创建数据库。或者更准确地说,如何使用 MQL5 内置的功能来创建 SQLite 数据库文件。我们还将了解如何创建表,以及如何通过使用主键和外键来建立表之间的关系。所有这些都将再次使用 MQL5 来完成。我们将看到,通过使用一个有助于封装底层实现细节的类,创建代码是多么容易,这些代码稍后可以迁移到其他 SQL 实现中。而且,最重要的是,我们将看到,在使用 SQL 时,我们可能会在不同阶段面临出错的风险。这是因为,在 MQL5 代码中,SQL 代码总是被放置在字符串中。