English Русский Español Deutsch 日本語
preview
基于Python和MQL5的特征工程(第二部分):价格角度

基于Python和MQL5的特征工程(第二部分):价格角度

MetaTrader 5EA交易 |
314 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

机器学习模型是非常敏感的工具。在本系列文章中,我们将更加关注对数据所做的转换如何影响模型的性能。同样,我们的模型对输入与目标之间关系的表达方式也很敏感。这意味着,为了让模型有效学习,我们可能需要从现有数据中创建新特征。

从市场数据中可以创建的新特征数量没有限制。我们对市场数据所做的转换以及从现有数据中创建的任何新特征,都会改变我们的误差水平。我们旨在帮助您确定哪些转换和特征工程技术能使误差水平更接近于0。此外,您还会发现,同一转换对不同模型架构的影响各不相同。因此,本文还将指导您根据所使用的模型架构选择合适的转换方法。



交易策略概述

如果您在MQL5论坛中搜索,会发现许多帖子都在询问如何计算价格水平变化所形成的角度。从直观上看,下跌趋势会产生负角度,而上涨趋势则会产生大于0的角度。虽然这个想法很容易理解,但实现起来却并不容易。对于我们社区中任何有兴趣构建包含价格角度策略的成员来说,都需要克服许多障碍。本文将重点介绍在您考虑全仓投入资金之前需要解决的一些主要问题。此外,我们不仅会指出该策略的不足之处,还会提出一些可能的解决方案,帮助您改进策略。

计算价格变化所形成角度的理念,为的是将其作为一种确认信号。交易者通常利用趋势线来识别市场中的主导趋势。趋势线通常用一条直线连接2个或3个极端价格点。如果价格水平突破趋势线上方,一些交易者会将其视为市场强势的信号,并在此点加入趋势。相反,如果价格向相反方向突破趋势线,则可能被视为市场弱势的信号,表明趋势正在减弱。

趋势线的一个主要局限性在于它们是主观定义的。因此,交易者可以任意调整其趋势线,以创建支持其观点的分析,即使其观点是错误的。因此,尝试以更稳健的方式定义趋势线是很合理的。大多数交易者希望通过计算价格水平变化所产生的斜率来实现这一点。其关键假设是,知道斜率就相当于知道价格走势所形成的趋势线的方向。

现在,我们遇到了第一个需要克服的障碍,即定义斜率。大多数交易者尝试将价格产生的斜率定义为价格差除以时间差。这种方法存在几个局限性。首先,股票市场在周末是关闭的。在我们的MetaTrader 5终端中,市场关闭期间所流逝的时间并未被记录,必须从现有数据中推断出来。因此,在使用如此简单的模型时,我们必须牢记,该模型并未考虑周末所流逝的时间。这意味着,如果价格水平在周末出现跳空,那么我们对斜率的估计将会过高。 

很明显,按照我们当前的方法计算出的斜率对我们表示时间的方式非常敏感。如果我们选择忽略周末所流逝的时间,如前所述,将得出过高的系数。而如果我们考虑周末的时间,将得到相对较小的系数。因此,在我们当前的模型下,分析同一数据时可能会得到两种不同的斜率计算结果。这是不可取的。我们希望计算结果是确定性的。也就是说,如果分析的是同一数据,那么我们对斜率的计算应该总是相同的。

为了克服这些局限性,我想提出一种替代计算方法。我们可以改为使用开盘价差除以收盘价差来计算价格形成的斜率。我们用开盘价差替换了x轴上的时间。这个新量告诉我们收盘价对开盘价变化的敏感程度。如果这个量的绝对值大于1,那么表明开盘价的大幅变化对收盘价影响很小。同样,如果这个量的绝对值小于1,那么表明开盘价的小幅变化可能会对收盘价产生较大影响。此外,如果斜率的系数为负,那么表明开盘价和收盘价往往朝相反方向变化。

然而,这个新量也有其自身的局限性,对交易者来说特别需要关注的是,我们的新指标对十字星K线(Doji candles)很敏感。当K线的开盘价和收盘价非常接近时,就会形成十字星K线。当我们有一组十字星K线时,问题就会加剧,如图1所示。在最优的情况下,这些十字星K线可能会导致我们的计算结果为0或无穷大。然而,在最坏的情况下,可能在运行时出错,因为我们可能会尝试除以0。

图 1:一组十字星K线



方法论概述

我们分析了来自USDZAR货币对的10,000行M1数据。这些数据是通过MQL5脚本从我们的MetaTrader 5终端获取的。我们首先使用之前建议的公式计算斜率。为了计算斜率的角度,我们使用了反正切(arctan)三角函数。然而,我们计算出的量与市场报价之间的相关性很低。

尽管相关性水平并不乐观,但我们还是继续训练了12种不同的AI模型,以预测USDZAR汇率的未来值,使用以下三组输入数据:

  1. 来自MetaTrader 5终端的开盘价、最高价、最低价、收盘价(OHLC)报价
  2. 价格形成的角度和斜率
  3. 以上三者的组合

我们表现最优的模型是使用OHLC数据的简单线性回归模型。值得注意的是,当我们将输入从第一组数据切换到第三组数据时,线性模型的准确性保持不变。在我们观察的模型中,没有一组在第二组数据上的表现优于第一组数据。然而,在我们检查的模型中,只有两个模型在使用所有可用数据时表现最优。基于我们的新特征,K近邻(KNeighbors)算法的性能提高了20%。这一观察结果引起我们深思,通过对数据进行其他有用的转换,我们还能获得哪些进一步的改进。

我们成功地调整了K近邻模型的参数,而没有过度拟合训练数据,并将模型导出为ONNX格式,以便将其纳入我们的AI驱动的EA中。


获取我们需要的数据

以下脚本将从MetaTrader 5终端获取我们需要的数据,并将其保存为CSV格式。只需将脚本拖放到您希望分析的任何市场上,您就可以跟随我们一起操作了。

//+------------------------------------------------------------------+
//|                                                      ProjectName |
//|                                      Copyright 2020, CompanyName |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/en/users/gamuchiraindawa"
#property version   "1.00"
#property script_show_inputs

//+------------------------------------------------------------------+
//| Script Inputs                                                    |
//+------------------------------------------------------------------+
input int size = 100000; //How much data should we fetch?

//+------------------------------------------------------------------+
//| On start function                                                |
//+------------------------------------------------------------------+
void OnStart()
  {

//--- File name
   string file_name = "Market Data " + Symbol() +".csv";

//--- Write to file
   int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,",");

   for(int i= size;i>=0;i--)
     {
      if(i == size)
        {
         FileWrite(file_handle,"Time","Open","High","Low","Close");
        }

      else
        {
         FileWrite(file_handle,iTime(Symbol(),PERIOD_CURRENT,i),
                   iOpen(Symbol(),PERIOD_CURRENT,i),
                   iHigh(Symbol(),PERIOD_CURRENT,i),
                   iLow(Symbol(),PERIOD_CURRENT,i),
                   iClose(Symbol(),PERIOD_CURRENT,i)
                  );
        }
     }
//--- Close the file
   FileClose(file_handle);

  }
//+------------------------------------------------------------------+


探索性数据分析

要开始分析,首先让我们导入所需的库。

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

现在,我们将读取市场数据。

#Read in the data
data = pd.read_csv("Market Data USDZAR.csv")

我们的数据顺序是错误的,需要将其反转。

#The data is in reverse order, correct that
data = data[::-1]

定义我们希望预测未来的时间范围。

#Define the forecast horizon
look_ahead = 20

让我们计算斜率。遗憾的是,我们的斜率计算并不总是能得出实数。这是我们当前版本算法的一个局限性。请记住,我们需要确定如何处理数据结构中的缺失值。当前,我们将删除数据结构中的所有缺失值。

#Calculate the angle formed by the changes in price, using a ratio of high and low price.
#Then calculate arctan to realize the angle formed by the changes in pirce
data["Slope"] =  (data["Close"] - data["Close"].shift(look_ahead))/(data["Open"] - data["Open"].shift(look_ahead))
data["Angle"] =  np.arctan(data["Slope"])

data.describe()

图 2:计算价格变化所形成角度后的数据结构

让我们聚焦于斜率计算结果为无穷大的实例。

data.loc[data["Slope"] == np.inf]

图 3:斜率为无穷大的记录表示开盘价未发生变化的情况

在下图4中,我们随机选择了一个斜率计算结果为无穷大的实例。该图显示,这些记录对应于价格波动,其中开盘价未发生变化。

pt = 1807
y = data.loc[pt,"Open"]
plt.plot(data.loc[(pt - look_ahead):pt,"Open"])
plt.axhline(y=y,color="red")
plt.xlabel("Time")
plt.ylabel("USDZAR Open Price")
plt.title("A slope of INF means the price has not changed")

图 3:可视化我们计算出的斜率值

当前,我们将通过删除所有缺失值来简化讨论。

data.dropna(inplace=True)

让我们绘制角度计算结果。

data.reset_index(drop=True,inplace=True)

让我们绘制角度计算结果。从下图4可以看出,我们的角度计算结果围绕0波动,这可能给计算机提供了一种尺度感,因为我们离 0 越远,价格水平的变化就越大。

data.loc[:100,"Angle"].plot()

图 4:可视化价格变化所形成的角度

现在,让我们尝试估计所创建的新特征中的噪声。我们将噪声量化为价格形成的角度减小但价格水平在同一时间段内增加的次数。这种特性是不理想的,因为理想情况下,我们希望一个量在增加和减少时与价格水平保持一致。遗憾的是,我们的新计算有一半时间与价格同步变动,另一半时间则可能独立变动。

为了量化这一点,我们简单地计算了斜率增加而未来价格水平下降的行数。然后,我们将这项计数除以斜率增加的总实例数。由此提醒我们,知道未来直线的斜率并不能很好地预测在同一预测时间范围内价格水平的变化。

#How clean are the signals generated?
1 - (data.loc[(data["Slope"] < data["Slope"].shift(-look_ahead)) & (data["Close"] > data["Close"].shift(-look_ahead))].shape[0] / data.loc[(data["Slope"] < data["Slope"].shift(-look_ahead))].shape[0])
0.51


探索性数据分析

首先,我们必须定义我们的输入和输出。

#Define our inputs and target
ohlc_inputs      = ["Open","High","Low","Close"]
trig_inputs      = ["Angle"]
all_inputs       = ohlc_inputs + trig_inputs
cv_inputs        = [ohlc_inputs,trig_inputs,all_inputs]
target           = "Target"

现在,让我们定义经典的目标变量,即未来价格。

#Define the target
data["Target"] = data["Close"].shift(-look_ahead)

让我们再添加几个类别,以告知模型关于形成每根K线的价格走势。如果当前K线是过去20根K线中上涨走势的结果,我们将用分类值1来表示。否则,该值将设为0。我们将对角度变化也采用相同的标识技术。

#Add a few labels
data["Bull Bear"] = np.nan
data["Angle Up Down"] = np.nan

data.loc[data["Close"] > data["Close"].shift(look_ahead), "Bull Bear"] = 0
data.loc[data["Angle"] > data["Angle"].shift(look_ahead),"Angle Up Down"] = 0
data.loc[data["Close"] < data["Close"].shift(look_ahead), "Bull Bear"] = 1
data.loc[data["Angle"] < data["Angle"].shift(look_ahead),"Angle Up Down"] = 1

格式化数据。

data.dropna(inplace=True)
data.reset_index(drop=True,inplace=True)
data

让我们分析数据中的相关性水平。回想一下,当估计与新角度计算相关的噪声水平时,我们观察到价格和计算出的角度仅在大约50%的时间内保持一致。因此,我们在下图5中观察到的低相关性水平并不令人意外。

#Let's analyze the correlation levels
sns.heatmap(data.loc[:,all_inputs].corr(),annot=True)

图 5:我们的角度计算与任何价格特征的相关性都很低

让我们尝试创建一个散点图,其中价格形成的角度在x轴上,收盘价在y轴上。得到的结果并不乐观。价格水平下跌的实例(蓝色点)和价格水平上涨的实例之间存在大量重叠。这使得我们的机器学习模型难以估计两种可能的价格走势类别之间的映射关系。

sns.scatterplot(data=data,y="Close",x="Angle",hue="Bull Bear")

图 6:我们的角度计算并没有帮助我们更好地分离数据

如果我们绘制两个特征(斜率和角度计算)之间的散点图,可以清楚地观察到数据应用的非线性变换。我们的大部分数据位于数据的两个弯曲末端之间,遗憾的是,并没有一个明显的分隔来区分看涨和看跌的价格走势,这可能使我们在预测未来价格水平时处于劣势。

sns.scatterplot(data=data,x="Angle",y="Slope",hue="Bull Bear")

图 7:可视化我们对OHLC价格数据应用的非线性变换

让我们可视化之前估计的51%的噪声。让我们在x轴上绘制两个值。每个值将分别表示角度计算是增加还是减少。我们的y轴将记录收盘价,每个点将总结价格水平是否按照我们之前概述的方式升值或贬值,蓝色实例总结了未来价格水平下跌的点。

首先,我们预估了噪声,现在可以将其可视化。从下图8中,我们可以清楚地看到,未来价格水平的变化似乎与价格形成的角度变化无关。

sns.swarmplot(data=data,x="Angle Up Down",y="Close",hue="Bull Bear")

图 8:未来价格水平似乎与角度的变化无关

以3D方式可视化数据显示信号的噪声大小。我们期望至少观察到一些全是上涨或全是下跌的点簇。然而,在这个特定实例中,我们并没有。点簇的存在可能识别出一种能够被解释为交易信号的模式。

#Define the 3D Plot
fig = plt.figure(figsize=(7,7))
ax = plt.axes(projection="3d")
ax.scatter(data["Slope"],data["Angle"],data["Close"],c=data["Bull Bear"])
ax.set_xlabel("Slope")
ax.set_ylabel("Angle")
ax.set_zlabel("Close")

图 9:以3D方式可视化我们的斜率数据

小提琴图允许我们直观地比较两个分布。小提琴图的核心是一个箱线图,用于归纳各分布的数值属性。下面的图10让我们有理由相信,角度计算并不是浪费时间。每个箱线图都用一条白线勾勒出其平均值。我们可以清楚地看到,在角度变化的两个实例中,每个箱线图的平均值都略有不同。虽然这种微小的差异对我们人类来说可能看起来微不足道,但机器学习模型足够敏感,能够捕捉到并从数据分布中的这种差异中学习。

sns.violinplot(data=data,x="Angle Up Down",y="Close",hue="Bull Bear",split=True)

图 10:比较两种角度变化类别之间价格数据的分布


准备建模数据

现在,让我们尝试数据建模。首先,我们将导入我们需要的库。

from sklearn.model_selection import train_test_split,cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestRegressor, BaggingRegressor, GradientBoostingRegressor,AdaBoostRegressor
from sklearn.svm import LinearSVR
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
from sklearn.neighbors import KNeighborsRegressor
from sklearn.tree import DecisionTreeRegressor
from sklearn.neural_network import MLPRegressor
from sklearn.metrics import mean_squared_error

将数据拆分为训练集和测试集。

#Let's split our data into train test splits
train_data, test_data = train_test_split(data,test_size=0.5,shuffle=False)

对数据进行缩放有助于模型更有效地学习。请确保仅在训练集上拟合缩放器对象(scaler object),然后对测试集进行转换,但不要再次在测试集上拟合缩放器对象。不要在整个数据集上拟合缩放器对象,因为用于缩放数据的参数会将一些关于未来的信息反向传播回过去。

#Scale the data
scaler = StandardScaler()
scaler.fit(train_data[all_inputs])
train_scaled= pd.DataFrame(scaler.transform(train_data[all_inputs]),columns=all_inputs)
test_scaled = pd.DataFrame(scaler.transform(test_data[all_inputs]),columns=all_inputs)

定义一个数据结构来存储每个模型的准确性。

#Create a dataframe to store our accuracy in training and testing
columns = [
    "Random Forest",
    "Bagging",
    "Gradient Boosting",
    "AdaBoost",
    "Linear SVR",
    "Linear Regression",
    "Ridge",
    "Lasso",
    "Elastic Net",
    "K Neighbors",
    "Decision Tree",
    "Neural Network"
]
index = ["OHLC","Angle","All"]
accuracy = pd.DataFrame(columns=columns,index=index)

将模型存储在列表中。

#Store the models
models = [
  RandomForestRegressor(),
  BaggingRegressor(),
  GradientBoostingRegressor(),
  AdaBoostRegressor(),
  LinearSVR(),
  LinearRegression(),
  Ridge(),
  Lasso(),
  ElasticNet(),
  KNeighborsRegressor(),
  DecisionTreeRegressor(),
  MLPRegressor(hidden_layer_sizes=(4,6))
]

交叉验证每个模型。

#Cross validate the models
#First we have to iterate over the inputs
for k in np.arange(0,len(cv_inputs)):
  current_inputs = cv_inputs[k]
  #Then fit each model on that set of inputs
  for i in np.arange(0,len(models)):
    score = cross_val_score(models[i],train_scaled[current_inputs],train_data[target],cv=5,scoring="neg_mean_squared_error",n_jobs=-1)
    accuracy.iloc[k,i] = -score.mean()

我们使用三组不同的输入来测试模型:

  1. 仅使用开盘价、最高价、最低价和收盘价(OHLC价格)
  2. 只使用我们计算得出的斜率和角度
  3. 使用我们现有的全部数据

并非我们所有的模型都能有效地利用这些特征。在我们候选模型池中的12个模型中,KNeighbors模型通过我们的新特征获得了20%的性能提升,并且显然是当时我们拥有的最优模型。

虽然线性回归模型在整个模型池中表现最优,但这一演示表明,可能还存在一些我们尚未了解的其他变换方式,而这些方式可能会进一步降低准确率。

图 11:一部分准确率水平。请注意,在我们构建的模型中,只有两个模型展示了使用我们新特征时的技能。

图12:AdaBoost和KNeighbors是最有潜力的模型,我们决定对KNeighbors模型进行优化。


深度优化

让我们尝试为指标找到优于默认的设置。

from sklearn.model_selection import RandomizedSearchCV

创建我们模型的实例。

model = KNeighborsRegressor(n_jobs=-1)

定义调优参数。

tuner = RandomizedSearchCV(model,
  {
    "n_neighbors": [2,3,4,5,6,7,8,9,10],
    "weights": ["uniform","distance"],
    "algorithm": ["auto","ball_tree","kd_tree","brute"],
    "leaf_size": [1,2,3,4,5,10,20,30,40,50,60,100,200,300,400,500,1000],
    "p": [1,2]
  },
    n_iter = 100,
    n_jobs=-1,
    cv=5
)

拟合调优器对象。

tuner.fit(train_scaled.loc[:,all_inputs],train_data[target])

我们找到的最优参数。

tuner.best_params_

{'weights': 'uniform',
 'p': 1,
 'n_neighbors': 10,
 'leaf_size': 100,
 'algorithm': 'ball_tree'}

我们在训练集上获得的最优得分是71%。然而,我们其实并不太在意训练集上的误差。我们更关心的是模型在新数据上的泛化能力。

tuner.best_score_
0.7106899297793474


检测过拟合

让我们看一下是否对训练集过拟合。当我们的模型从训练集中学习无意义的信息时,就会发生过拟合。我们可以通过多种方式测试是否过拟合,其中一种方法是,将自定义模型与一个对数据没有任何先验知识的模型进行比较。

#Testing for over fitting
model = KNeighborsRegressor(n_jobs=-1)

custom_model = KNeighborsRegressor(n_jobs=-1,weights= 'uniform',p=1,n_neighbors= 10,leaf_size= 100,algorithm='ball_tree')

如果我们未能超越模型的默认实例(即未进行任何自定义优化的模型)的表现,那么可以确信,我们的模型可能已经在训练集上过拟合。显而易见,我们的模型表现优于默认模型,这是个好消息。

model.fit(train_scaled.loc[:,all_inputs],train_data[target])
custom_model.fit(train_scaled.loc[:,all_inputs],train_data[target])
默认模型
自定义模型
0.0009797322460441842
0.0009697248896608824


导出到ONNX

开放神经网络交换(Open Neural Network Exchange,简称ONNX)是一种开源协议,用于以模型无关的方式构建和共享机器学习模型。我们将利用ONNX API将AI模型从Python导出,并导入到MQL5程序中。

首先,我们需要对价格数据进行转换,以便在MQL5中能够始终复现这些转换。让我们将每列数据的均值和标准差值保存到一个CSV文件中。

data.loc[:,all_inputs].mean().to_csv("USDZAR M1 MEAN.csv")
data.loc[:,all_inputs].std().to_csv("USDZAR M1 STD.csv")

现在,对数据进行转换。

data.loc[:,all_inputs] = ((data.loc[:,all_inputs] - data.loc[:,all_inputs].mean())/ data.loc[:,all_inputs].std())

让我们导入所需的库。

import onnx
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

定义模型的输入类型。

#Define the input shape
initial_type = [('float_input', FloatTensorType([1, len(all_inputs)]))]

基于我们现有的全部数据拟合模型。

#Fit the model on all the data we have
custom_model.fit(data.loc[:,all_inputs],data.loc[:,"Target"])

将模型转换为ONNX格式并保存。

#Convert the model to ONNX format
onnx_model = convert_sklearn(model, initial_types=initial_type,target_opset=12)
#Save the ONNX model
onnx.save(onnx_model,"USDZAR M1 OHLC Angle.onnx")


在MQL5中构建我们的EA

现在,让我们将AI模型集成到交易应用程序中,以便能够利用它在市场中获得优势进行交易。我们的交易策略将使用AI模型来检测M1时间框架上的趋势。我们还将从USADZAR货币对在日线时间框架上的表现中寻找潜在的机会。如果AI模型检测到上升趋势,我们希望在日线图上看到看涨的价格走势。此外,我们还将从美元指数中寻求进一步的确认。以我们在M1时间框架上买入为例,我们还需要在美元指数的日线图上观察到看涨的价格走势,作为美元可能在大的时间框架上继续上涨的信号。

首先,我们需要导入我们刚刚创建的ONNX模型。

//+------------------------------------------------------------------+
//|                                                     Slope AI.mq5 |
//|                                        Gamuchirai Zororo Ndawana |
//|                          https://www.mql5.com/en/gamuchiraindawa |
//+------------------------------------------------------------------+
#property copyright "Gamuchirai Zororo Ndawana"
#property link      "https://www.mql5.com/en/gamuchiraindawa"
#property version   "1.00"

//+------------------------------------------------------------------+
//| Load the ONNX files                                              |
//+------------------------------------------------------------------+
#resource "\\Files\\USDZAR M1 OHLC Angle.onnx" as  const uchar onnx_buffer[];

让我们加载交易库,以便管理未平仓头寸。

//+------------------------------------------------------------------+
//| Libraries                                                        |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>
CTrade Trade;

定义一部分我们需要的全局变量。

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
double mean_values[5] = {18.143698,18.145870,18.141644,18.143724,0.608216};
double std_values[5] = {0.112957,0.113113,0.112835,0.112970,0.580481};
long   onnx_model;
int    macd_handle;
int    usd_ma_slow,usd_ma_fast;
int    usd_zar_slow,usd_zar_fast;
double macd_s[],macd_m[],usd_zar_s[],usd_zar_f[],usd_s[],usd_f[];
double bid,ask;
double vol = 0.3;
double profit_target = 10;
int    system_state = 0;
vectorf model_forecast = vectorf::Zeros(1);

现在我们已经进入了交易应用程序的初始化阶段。目前,我们只需要加载ONNX模型和技术指标。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Load the ONNX file
   if(!onnx_load())
     {
      //--- We failed to load the ONNX file
      return(INIT_FAILED);
     }

//--- Load the MACD Indicator
   macd_handle  = iMACD("EURUSD",PERIOD_CURRENT,12,26,9,PRICE_CLOSE);
   usd_zar_fast = iMA("USDZAR",PERIOD_D1,20,0,MODE_EMA,PRICE_CLOSE);
   usd_zar_slow = iMA("USDZAR",PERIOD_D1,60,0,MODE_EMA,PRICE_CLOSE);
   usd_ma_fast  = iMA("DXY_Z4",PERIOD_D1,20,0,MODE_EMA,PRICE_CLOSE);
   usd_ma_slow  = iMA("DXY_Z4",PERIOD_D1,60,0,MODE_EMA,PRICE_CLOSE);

//--- Everything went fine
   return(INIT_SUCCEEDED);
  }

如果程序不再使用,让我们释放其占用的资源。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Release the handles don't need
   OnnxRelease(onnx_model);
   IndicatorRelease(macd_handle);
   IndicatorRelease(usd_zar_fast);
   IndicatorRelease(usd_zar_slow);
   IndicatorRelease(usd_ma_fast);
   IndicatorRelease(usd_ma_slow);
  }

每当接收到更新的价格时,让我们存储新的市场数据,从模型中获取新的预测结果,然后决定是否需要在市场中寻找交易机会(开仓),或者对已有持仓平仓。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- Update our market data
   update();
//--- Get a prediction from our model
   model_predict();

   if(PositionsTotal() == 0)
     {
      find_entry();
     }

   if(PositionsTotal() > 0)
     {
      manage_positions();
     }
  }
//+------------------------------------------------------------------+

用于更新我们市场数据的函数定义如下。我们依赖CopyBuffer命令将每个指标的当前值提取到其对应的数组缓冲区中。我们将使用这些移动平均线指标来确认趋势。

//+------------------------------------------------------------------+
//| Update our market data                                           |
//+------------------------------------------------------------------+
void update(void)
  {
   bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
   ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);

   CopyBuffer(macd_handle,0,0,1,macd_m);
   CopyBuffer(macd_handle,1,0,1,macd_s);
   CopyBuffer(usd_ma_fast,0,0,1,usd_f);
   CopyBuffer(usd_ma_slow,0,0,1,usd_s);
   CopyBuffer(usd_zar_fast,0,0,1,usd_zar_f);
   CopyBuffer(usd_zar_slow,0,0,1,usd_zar_s);
  }
//+------------------------------------------------------------------+

不仅如此,我们还需要明确定义如何用模型进行预测。此外,让我们首先计算价格波动所形成的角度,然后将模型输入存储到一个向量中。最后,在调用OnnxRun函数从AI模型获取预测结果之前,我们将对模型输入进行标准化和缩放处理。

//+------------------------------------------------------------------+
//| Get a forecast from our model                                    |
//+------------------------------------------------------------------+
void model_predict(void)
  {
   float angle = (float) MathArctan(((iOpen(Symbol(),PERIOD_M1,1) - iOpen(Symbol(),PERIOD_M1,20)) / (iClose(Symbol(),PERIOD_M1,1) - iClose(Symbol(),PERIOD_M1,20))));
   vectorf model_inputs = {(float) iOpen(Symbol(),PERIOD_M1,1),(float) iHigh(Symbol(),PERIOD_M1,1),(float) iLow(Symbol(),PERIOD_M1,1),(float) iClose(Symbol(),PERIOD_M1,1),(float) angle};
   for(int i = 0; i < 5; i++)
     {
      model_inputs[i] = (float)((model_inputs[i] - mean_values[i])/std_values[i]);
     }
//--- Log
   Print("Model inputs: ");
   Print(model_inputs);

   if(!OnnxRun(onnx_model,ONNX_DATA_TYPE_FLOAT,model_inputs,model_forecast))
     {
      Comment("Failed to obtain a forecast from our model: ",GetLastError());
     }

  }

以下函数将从我们之前定义的ONNX缓冲区中加载ONNX模型。

//+------------------------------------------------------------------+
//| ONNX Load                                                        |
//+------------------------------------------------------------------+
bool onnx_load(void)
  {
//--- Create the ONNX model from the buffer we defined
   onnx_model = OnnxCreateFromBuffer(onnx_buffer,ONNX_DEFAULT);

//--- Define the input and output shapes
   ulong input_shape[] = {1,5};
   ulong output_shape[] = {1,1};

//--- Validate the I/O parameters
   if(!(OnnxSetInputShape(onnx_model,0,input_shape))||!(OnnxSetOutputShape(onnx_model,0,output_shape)))
     {
      //--- We failed to define the I/O parameters
      Comment("[ERROR] Failed to load AI Model Correctly: ",GetLastError());
      return(false);
     }

//--- Everything was okay
   return(true);
  }

此外,我们的系统还需要设定规则,以确定何时应当平仓。如果当前持仓的浮动盈利大于盈利目标,我们将平仓。否则,如果系统状态发生变化,那么我们将相应地平仓。

//+------------------------------------------------------------------+
//| Manage our open positions                                        |
//+------------------------------------------------------------------+
void manage_positions(void)
  {
   if(PositionSelectByTicket(PositionGetTicket(0)))
     {
      if(PositionGetDouble(POSITION_PROFIT) > profit_target)
        {
         Trade.PositionClose(Symbol());
        }
     }

   if(system_state == 1)
     {
      if(macd_m[0] < macd_s[0])
        {
         if(model_forecast[0] < iClose(Symbol(),PERIOD_M1,0))
           {
            Trade.PositionClose(Symbol());
           }
        }
     }

   if(system_state == -1)
     {
      if(macd_m[0] > macd_s[0])
        {
         if(model_forecast[0] > iClose(Symbol(),PERIOD_M1,0))
           {
            Trade.PositionClose(Symbol());
           }
        }
     }
  }

以下函数负责开仓(新建持仓)。我们仅会在以下条件同时满足时开立多头仓位(买入):

  1. MACD主线位于信号线之上
  2. 我们的AI预测值大于当前收盘价
  3. 美元指数和USDZAR货币对在日线图上均显示出看涨的价格走势
对于确定空头仓位(卖出),情况则相反。只有在所有条件同时满足时我们才会开仓,否则系统将保持等待状态。
//+------------------------------------------------------------------+
//| Find an entry                                                    |
//+------------------------------------------------------------------+
void find_entry(void)
  {
   if(macd_m[0] > macd_s[0])
     {
      if(model_forecast[0] > iClose(Symbol(),PERIOD_M1,0))
        {
         if((usd_f[0] > usd_s[0]) && (usd_zar_f[0] > usd_zar_s[0]))
           {
            Trade.Buy(vol,Symbol(),ask,0,0,"Slope AI");
            system_state = 1;
           }
        }
     }

   if(macd_m[0] < macd_s[0])
     {
      if(model_forecast[0] < iClose(Symbol(),PERIOD_M1,0))
        {
         if((usd_f[0] < usd_s[0]) && (usd_zar_f[0] < usd_zar_s[0]))
           {
            Trade.Sell(vol,Symbol(),bid,0,0,"Slope AI");
            system_state = -1;
           }
        }
     }
  }

图 12:我们的AI系统正在运行



结论

到目前为止,我们已经证明,对于那些希望将价格走势形成的斜率纳入其交易策略的交易者来说,仍然存在一些障碍。然而,似乎在这个方向上付出的任何努力都可能是有价值的。通过利用斜率来揭示价格水平之间的关系,我们的KNeighbors模型性能提高了20%,这让我们不禁要问,如果继续在这个方向上探索,我们还能实现多少性能提升。此外,这也凸显出每个模型可能都有其自身特定的一套转换方法,这些方法能够增强其性能。现在,我们的任务就是找到这种映射关系。

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

附加的文件 |
Slope_AI.mq5 (7.73 KB)
USDZAR_M1_MEAN.csv (0.12 KB)
USDZAR_M1_STD.csv (0.12 KB)
重构经典策略(第十一部分)移动平均线的交叉(二) 重构经典策略(第十一部分)移动平均线的交叉(二)
移动平均线和随机振荡器可用于生成趋势跟踪交易信号。然而,这些信号只有在价格行为发生之后才会被观察到。我们可以有效地利用人工智能克服技术指标中这种固有的滞后性。本文将教您如何创建一个完全自主的人工智能驱动型EA,这种方式可以改进您现有的任何交易策略。即使是最古老的交易策略也可以被改进。
从基础到中级:运算符优先级 从基础到中级:运算符优先级
这绝对是纯理论上最难解释的问题。这就是为什么你需要练习我们在这里讨论的所有内容。虽然这起初看起来很简单,但操作符的话题只有在实践中结合不断的教育才能理解。
使用MQL5经济日历进行交易(第二部分):创建新闻交易面板 使用MQL5经济日历进行交易(第二部分):创建新闻交易面板
在本文中,我们使用MQL5经济日历创建了一个实用的新闻交易面板,来增强我们的交易策略。我们首先设计布局,重点关注事件名称、重要性和时间等关键元素,然后在MQL5中进行设置。最后,我们实现了一个过滤系统,只显示相关性最强的新闻,为交易者快速提供有影响力的经济事件。
您应当知道的 MQL5 向导技术(第 41 部分):深度-Q-网络 您应当知道的 MQL5 向导技术(第 41 部分):深度-Q-网络
“深度-Q-网络” 是一种强化学习算法,在机器学习模块的训练过程中,神经网络参与预测下一个 Q 值和理想动作。我们曾研究过另一种强化学习算法 “Q-学习”。本文因此出示了另一个如何配以强化学习训练 MLP 的示例,可于自定义信号类中所用。