开发多币种 EA 交易(第 1 部分):多种交易策略的协作
在工作期间,我不得不面对各种交易策略。通常情况下,EA 只执行一种交易思路。由于难以确保一个终端上的多个 EA 稳定协作,我们通常只能选择少数最优秀的 EA。但如果因此而放弃完全可行的策略,那还是很可惜的。如何才能让它们协同工作?
确定问题
我们需要决定我们想要什么,以及我们拥有什么。
我们有(或几乎有):
- 以现成 EA 代码的形式,或仅仅是一套用于执行交易操作的规则,在不同交易品种和时间框架上运行的一些不同的交易策略
- 起始存款
- 最大允许回撤
我们想要:
- 在多个交易品种和时间框架上,在一个账户中协作使用所有选定的策略
- 在每个策略之间平均分配或按照规定的比例分配起始存款
- 自动计算已开立头寸的数量,以符合最大允许回撤量的要求
- 正确处理终端重启
- 能够在 MetaTrader 5 和 4 中运行
我们将使用面向对象的方法、MQL5 和 MetaTrader 5 中的标准测试器。
手头的任务相当繁重,因此我们将逐步解决。
在第一阶段,让我们以一个简单的交易思路为例。让我们用它制作一个简单的 EA,对其进行优化,选择两组最佳参数。再创建一个包含两个原始简单 EA 副本的 EA,并查看其结果。
从交易思路到交易策略
让我们把下面的思路作为一个实验。
假设某个交易品种开始密集交易时,单位时间内的价格变化可能比交易低迷时更大。然后,如果我们看到交易已经加剧,价格已经朝着某个方向变化,那么也许在不久的将来,它也会朝着同样的方向变化。让我们试着从中获利吧。
交易策略 是一套基于交易思路的开仓和平仓规则。它不包含任何未知参数。通过这套规则,我们可以确定在策略运行的任何时刻是否应该开仓,如果应该开仓,应该开哪些仓。
让我们试着把想法变成策略。首先,我们需要以某种方式检测交易强度的增加。没有这一点,我们就无法确定何时开仓。为此,我们将使用交易量,即在当前烛形中终端接收到的新价格的数量。交易量越大,表明交易越活跃。但对于不同的交易品种,其强度会有很大差异。因此,我们不能为分时成交量设定一个单一水平,超过分时成交量我们就认为开始了密集交易。那么,要确定这一水平,我们可以从几根烛形的平均交易量入手。经过一番思考,我们可以做出如下描述:
当烛形的分时成交量超过当前烛形方向的平均成交量时下达挂单。每个订单都有有效期,过期后将被删除。如果挂单已变成头寸,则只有在达到指定的止损和止盈水平后才会平仓。如果交易量比平均值超出更多,那么除了已经打开的挂单外,还可能会发出额外的订单。
这是一个更详细的描述,但并不完整。因此,我们要再读一遍,把所有不清楚的地方都标出来,那里需要更详细的解释。
下面是提出的问题:
- "下挂单......" - 我们应该下什么挂单?
- "平均成交量......" - 如何计算烛形中的平均成交量?
- "......超过平均量...... " - 如何确定超过平均量?
- "...如果分时交易量超出平均值更多......" - 如何确定更多的超出量?
- "......可以下额外订单 " - 总共可以下多少订单?
我们将下哪些挂单?基于这一想法,我们希望价格继续沿着烛形开始时的方向移动。例如,如果当前价格高于烛形期间开始时的价格,那么我们就应该开挂单买入。如果我们开启 BUY_LIMIT,那么要让它起作用,价格应该先返回(下跌)一点,然后要让开仓头寸获利,价格应该再次上涨。如果我们开启的是 BUY_STOP,那么要建立头寸,价格就应该继续再移动(上涨)一些,然后再涨得更高,这样才能获利。
目前还不清楚哪种方案更好 。因此,为了简单起见,让我们始终打开止损单(BUY_STOP 和 SELL_STOP)。今后,可以将其作为一个策略参数,由其值决定哪些订单将被开启。
如何计算烛形的平均交易量?要计算平均交易量,我们需要选择交易量将纳入平均计算的烛形。我们来选取一定数量的最近关闭的连续烛形。然后,如果我们设置了烛形数量,就可以计算出平均交易量。
如何确定平均交易量的超出部分?如果我们选择条件
V > V_avr ,
其中
V 是当前烛形的交易量,
V_avr 是平均交易量,
那么大约一半的烛形都能满足这一条件。根据交易思路,我们只有在成交量明显超过平均值时才下单。否则,与之前的烛形不同,这根烛形还不能被视为交易更加活跃的迹象。例如,我们可以使用下面的公式:
V > V_avr + D * V_avr,
其中 D 是一个数值比率。如果 D = 1,则在当前成交量超过平均值 2 倍时开启,如果 D = 2,则在当前成交量超过平均值3 倍时开启。
不过,该条件只能用于开启一个订单,因为如果用于开启第二个和后续订单,那么它们将紧接着第一个订单开启。只需打开一单更大交易量的订单,就可以替代这部分订单。
如何确定更大的超出量? 为此,让我们在条件等式中再添加一个参数 - 未结订单数 N:
V > V_avr + D * V_avr + N * D * V_avr。
那么,为了让第二个订单在第一个订单之后开启(即 N = 1),必须满足以下条件:
V > V_avr + 2 * D * V_avr。
要开启第一个订单(N = 0),方程的形式已经为我们所知:
V > V_avr + D * V_avr。
最后,对开头的方程进行最后一次修正。让我们用两个独立的参数 D 和 D_add,来代替第一个订单和后续订单中的同一个 D:
V > V_avr + D * V_avr + N * D_add * V_avr、
V > V_avr * (1 + D + N * D_add)
这样看来,我们在选择策略的最佳参数时就有了更大的自由度。
如果我们的条件使用 N 值作为订单和头寸的总数,那么我们的意思是每个挂单都会变成一个单独的头寸,而不会增加已开仓头寸的交易量。因此,目前我们只能将这种策略的适用范围限制在对头寸进行独立核算("对冲")的账户上。
一切都清楚后,让我们列出可以取不同值的变量,而不是只有一个值的变量。这些将是我们策略的输入参数。让我们考虑一下,要打开订单,我们还需要知道成交量、与当前价格的距离、到期时间以及止损和止盈水平。然后我们会得到如下描述:
EA 在对冲账户的特定交易品种和周期(时间框架)上运行
设置输入参数:
- 用于计算平均交易量的烛形数量 (K)
- 打开第一个订单时与平均值的相对偏差 (D)
- 第二个订单和后续订单开仓时与平均值的相对偏差 (D_add)
- 从价格到挂单的距离
- 止损(点数)
- 止盈(点数)
- 挂单到期时间(分钟)
- 同时打开的最大订单数 (N_max)
- 单个订单交易量
找出未结订单和头寸的数量 (N)。
如果小于 N_max,则:
计算最近 K 个关闭烛形的平均交易量,得到 V_avr 值。
如果满足 V > V_avr * (1 + D + N * D_add) 条件,则:
确定当前烛形的价格变化方向:如果价格上涨,则下达 BUY_STOP 挂单,否则下达 SELL_STOP 挂单
在参数中指定的距离、到期时间、止损和止盈水平下挂单。
实现交易策略
让我们开始编写代码吧。首先,让我们列出所有参数,将它们分成几组,使其更加清晰,并为每个参数提供注释。这些注释(如果有)将在启动 EA 时显示在参数对话框中,并显示在策略测试器的参数选项卡中,而不是我们为其选择的变量名称。
现在,我们只需设置一些默认值。我们将在优化过程中寻找最佳设置。
input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magicN_ = 27181; // Magic
由于 EA 将执行交易操作,我们将创建一个 CTrade 类的全局对象。我们将通过调用该对象的方法来下挂单。
CTrade trade; // Object for performing trading operations
请记住,全局变量(或对象),是在 EA 代码中函数的 之外声明的变量(或对象)。因此,我们的所有 EA 函数中都可以使用它们。它们不应与全局终端变量混淆。
为了计算开仓订单的参数,我们需要获取当前价格和 EA 将在其上启动的其他交易品种属性。为此,要创建一个 CSymbolInfo 类的全局对象。
CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties
此外,我们还需要计算未结订单和头寸的数量。为此,让我们创建 COrderInfo 和 CPositionInfo 类的全局对象,用于获取未结订单和仓位的数据。我们将在两个全局变量 - countOrders 和 countPositions 中存储数量本身。
COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions
例如,我们可以使用 iVolumes 技术指标来计算多个烛形的平均交易量。要获取其值,我们需要一个变量来存储该指标的句柄(一个整数,用于存储 EA 中使用的所有其他指标中该指标的序列号)。要找出平均交易量,我们首先要将指标缓冲区中的数值复制到一个预先准备好的数组中。我们还将这个数组设为全局可用。
int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves)
现在,我们可以继续使用 OnInit() EA 初始化函数和 OnTick() 分时处理函数。
在初始化过程中,我们可以执行以下操作:
- 加载指标以获取分时交易量并记住其句柄
- 根据烛形的数量设置接收数组的大小,以计算平均量,并设置其寻址方式为时间序列
- 设置通过交易对象下单的幻数
这就是我们的初始化函数:
int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); }
根据策略说明,我们应该首先在分时处理函数中找到未结订单和仓位的数量。让我们用一个单独的 UpdateCounts() 函数来实现这个功能。在这个函数中,我们将查看所有未结头寸和订单,只计算那些幻数与我们的 EA 幻数相匹配的头寸和订单。
void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } }
然后,确保未结头寸和订单的数量不超过设置中指定的数量。在这种情况下,我们需要检查是否满足开立新订单的条件。让我们用一个单独的 SignalForOpen() 函数来实现这种检查。它将返回三个可能值中的一个:
- +1 - 打开 BUY_STOP 订单的信号
- 0 - 无信号
- -1 - 打开 SELL_STOP 订单的信号
为了下挂单,我们还将编写两个独立的函数: OpenBuyOrder() 和 OpenSellOrder()。
现在,我们可以编写 OnTick() 函数的完整实现了。
void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } }
之后,我们再添加其余函数的实现,这样 EA 代码就完成了。让我们将其保存到当前文件夹中的 SimpleVolumes.mq5 文件中。
#include <Trade\OrderInfo.mqh> #include <Trade\PositionInfo.mqh> #include <Trade\SymbolInfo.mqh> #include <Trade\Trade.mqh> input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magicN_ = 27181; // Magic CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) //+------------------------------------------------------------------+ //| Initialization function of the expert | //+------------------------------------------------------------------+ int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } } //+------------------------------------------------------------------+ //| Calculate the number of open orders and positions | //+------------------------------------------------------------------+ void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } } //+------------------------------------------------------------------+ //| Open the BUY_STOP order | //+------------------------------------------------------------------+ void OpenBuyOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = ask + distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price - stopLevel_ * point, digits); double tp = NormalizeDouble(price + (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.BuyStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Open the SELL_STOP order | //+------------------------------------------------------------------+ void OpenSellOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = bid - distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price + stopLevel_ * point, digits); double tp = NormalizeDouble(price - (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.SellStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int SignalForOpen() { // By default, there is no signal int signal = 0; // Copy volume values from the indicator buffer to the receiving array int res = CopyBuffer(iVolumesHandle, 0, 0, signalPeriod_, volumes); // If the required amount of numbers have been copied if(res == signalPeriod_) { // Calculate their average value double avrVolume = ArrayAverage(volumes); // If the current volume exceeds the specified level, then if(volumes[0] > avrVolume * (1 + signalDeviation_ + (countOrders + countPositions) * signaAddlDeviation_)) { // if the opening price of the candle is less than the current (closing) price, then if(iOpen(Symbol(), PERIOD_CURRENT, 0) < iClose(Symbol(), PERIOD_CURRENT, 0)) { signal = 1; // buy signal } else { signal = -1; // otherwise, sell signal } } } return signal; } //+------------------------------------------------------------------+ //| Number array average value | //+------------------------------------------------------------------+ double ArrayAverage(const double &array[]) { double s = 0; int total = ArraySize(array); for(int i = 0; i < total; i++) { s += array[i]; } return s / MathMax(1, total); } //+------------------------------------------------------------------+
让我们开始优化 EURGBP H1 的 EA 参数,以 MetaQuotes 报价为基础,从 2018-01-01 至 2023-01-01 期间,起始存款为 100 000 美元,最小手数为 0.01 手。请注意,在对不同经纪商的报价进行测试时,同一 EA 显示的结果可能略有不同。有时,这些结果可能会大相径庭。
让我们选择两组不错的参数,结果如下:
图 1.[130,0.9,1.4,231,3750,50,600,3,0.01] 的测试结果
图 2. 159,1.7,0.8,248,3600,495,39000,3,0.01] 的测试结果
在一大笔起始存款上进行测试不是偶然的,原因是,如果 EA 开立的仓位有固定的交易量,那么如果回撤大于可用资金,运行可能会提前结束。在这种情况下,我们将无法知道,在使用相同参数的情况下,是否有可能合理地减少未结头寸的数量(或等同于增加起始存款)以避免损失。
让我们回顾一个例子。假设我们的起始存款为 1,000 美元。在测试器中运行时,我们得到了以下结果:
- 最终存款为 11,000 美元(利润 1,000%,EA 赚取 + 10,000 美元,再加上初始的 1,000 美元)
- 最高绝对回撤额为 2,000 美元
显然,我们只是幸运,在 EA 将存款增加到 2,000 多美元之后,才出现了这样的回撤。因此,测试器运行完成后,我们就能看到这些结果。如果这种回撤发生得更早(例如,我们选择了不同的测试期开始时间),那么我们就会损失全部存款。
如果我们手动运行,那么我们可以更改参数中的交易量或增加起始存款,然后重新开始运行。但如果在优化过程中进行运行,则无法做到这一点。在这种情况下,由于资金管理设置选择错误,一组可能很好的参数就可能会被拒绝。为了降低出现这种结果的可能性,我们可以在运行优化时,初始存款额都非常大,而交易量则设为最低。
回到例子中,如果起始存款是 100,000 美元,那么在重复回撤 2,000 美元的情况下,不会出现损失全部存款的情况,测试者将得到这些结果。我们可以计算出,如果我们的最大允许回撤额为 10%,那么初始存款至少应为 20,000 美元。在这种情况下,利润率将只有 50%(EA 赚取的 + 10,000 美元与最初的 20,000 美元之比)。
让我们对所选的两个参数组合进行类似计算,起始存款为 10,000 美元,允许回撤为起始存款的 10%。
参数 | 手数 | 回撤 | 利润 | 可以接受的 回撤 | 可以接受的 手数 | 可以接受的 收益 |
---|---|---|---|---|---|---|
L | D | P | Da | La = L * (Da / D) | Pa = P * (Da / D) | |
[130, 0.9, 1.4, 231, 3750, 50, 600, 3, 0.01] | 0.01 | 28.70 (0.04%) | 260.41 | 1000 (10%) | 0.34 | 9073 (91%) |
[159, 1.7, 0.8, 248、 3600,495,39000,3,0.01] | 0.01 | 92.72 (0.09%) | 666.23 | 1000 (10%) | 0.10 | 7185 (72%) |
我们可以看到,两种输入参数选项都能产生大致相同的收益(约 80%)。第一种选项的绝对收益较低,但回撤幅度较小。因此,在这种情况下,我们可以增加开仓量,而不是第二种选项,因为第二种选项虽然赚得更多,但会有更大的回撤。
因此,我们找到了几种有前景的输入参数组合。让我们开始将它们合并为一个 EA。
基本策略类
让我们创建 CStrategy 类,在其中收集所有策略固有的属性和方法。例如,任何策略都会有某个交易品种和时间框架,无论其与指标的关系如何。我们还将为每种策略分配自己的开仓幻数和单个仓位大小。为简单起见,我们暂不考虑仓位大小可变的策略操作。我们以后一定会这样做的。
在必要的方法中,只有初始化策略参数的构造函数、初始化方法和 OnTick 事件处理程序可以确定。我们得到以下代码:
class CStrategy : public CObject { protected: ulong m_magic; // Magic string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) double m_fixedLot; // Size of opened positions (fixed) public: // Constructor CStrategy(ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot); virtual int Init() = 0; // Strategy initialization - handling OnInit events virtual void Tick() = 0; // Main method - handling OnTick events };
Init() 和 Tick() 方法被声明为纯虚拟方法(方法头后面的 = 0)。这意味着我们不会在 CStrategy 类中编写这些方法的实现。在该类的基础上,我们将创建子类,其中必须包含 Init() 和 Tick() 方法,并包含具体交易规则的实现。
类的说明已准备就绪。之后,我们将添加必要的构造函数实现。由于这是一个在创建策略对象时自动调用的方法函数,因此我们需要在此方法中确保策略参数已初始化。构造函数将接收四个参数,并通过初始化列表将其赋值给相应的类成员变量。
CStrategy::CStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot) : // Initialization list m_magic(p_magic), m_symbol(p_symbol), m_timeframe(p_timeframe), m_fixedLot(p_fixedLot) {}
将代码保存到当前文件夹的 Strategy.mqh 文件中。
交易策略类
让我们把原始简单 EA 的逻辑移植到一个新的子类 CSimpleVolumesStrategy 中。为此,应将所有输入变量和全局变量设为类的成员。我们将用从 CStrategy 基类继承的 m_fixedLot 和 m_magic 基类成员替换 fixedLot_ 和 magicN_ 变量。
#include "Strategy.mqh" class CSimpleVolumeStrategy : public CStrategy { //--- Open signal parameters int signalPeriod_; // Number of candles for volume averaging double signalDeviation_; // Relative deviation from the average to open the first order double signaAddlDeviation_; // Relative deviation from the average for opening the second and subsequent orders //--- Pending order parameters int openDistance_; // Distance from price to pending order double stopLevel_; // Stop Loss (in points) double takeLevel_; // Take Profit (in points) int ordersExpiration_; // Pending order expiration time (in minutes) //--- Money management parameters int maxCountOfOrders_; // Maximum number of simultaneously open orders CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) };
OnInit() 和 OnTick() 函数成为 Init() 和 Tick() 公共方法,所有其他函数成为 CSimpleVolumesStrategy 类的新私有方法。公有方法可以从外部代码中调用,例如从 EA 对象方法中调用。而私有方法只能从指定类的方法中调用。让我们在类描述中添加方法头。
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code double volumes[]; // Receiver array of indicator values (volumes themselves) //--- Methods void UpdateCounts(); // Calculate the number of open orders and positions int SignalForOpen(); // Signal for opening pending orders void OpenBuyOrder(); // Open the BUY_STOP order void OpenSellOrder(); // Open the SELL_STOP order double ArrayAverage( const double &array[]); // Average value of the number array public: //--- Public methods virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
在这些函数的实现位置,在它们的名称前添加 "CSimpleVolumesStrategy::" 前缀,以便让编译器清楚地知道,这些不再只是函数,而是我们类的函数方法。
class CSimpleVolumeStrategy : public CStrategy { // Class description listing properties and methods... }; int CSimpleVolumeStrategy::Init() { // Function code ... } void CSimpleVolumeStrategy::Tick() { // Function code ... } void CSimpleVolumeStrategy::UpdateCounts() { // Function code ... } int CSimpleVolumeStrategy::SignalForOpen() { // Function code ... } void CSimpleVolumeStrategy::OpenBuyOrder() { // Function code ... } void CSimpleVolumeStrategy::OpenSellOrder() { // Function code ... } double CSimpleVolumeStrategy::ArrayAverage(const double &array[]) { // Function code ... }
在最初的简单 EA 中,输入参数值是在声明时分配的。在启动已编译的 EA 时,输入参数对话框中的数值(而不是代码中设置的数值)会被分配给它们。在类描述中无法做到这一点,因此构造函数在此发挥作用。
让我们创建一个带有必要参数列表的构造函数。构造函数也应该是公有的,否则我们将无法从外部代码中创建策略对象。
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code public: //--- Public methods CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders ); // Constructor virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
类的说明已准备就绪。除了构造函数外,它的所有方法都已经有了实现。就让我们加进去吧。在最简单的情况下,该类的构造函数只会将接收到的参数值分配给该类的相应成员。此外,前四个参数将通过调用基类构造函数来实现这一点。
CSimpleVolumeStrategy::CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders) : // Initialization list CStrategy(p_magic, p_symbol, p_timeframe, p_fixedLot), // Call the base class constructor signalPeriod_(p_signalPeriod), signalDeviation_(p_signalDeviation), signaAddlDeviation_(p_signaAddlDeviation), openDistance_(p_openDistance), stopLevel_(p_stopLevel), takeLevel_(p_takeLevel), ordersExpiration_(p_ordersExpiration), maxCountOfOrders_(p_maxCountOfOrders) {}
要做的事情已经所剩无几。在所有遇到 fixedLot_ 和 magicN_ 的地方,将它们重命名 为 m_fixedLot 和 m_magic。用 m_symbol 基类变量代替获取 Symbol() 当前交易品种的函数,用 m_timeframe 代替 PERIOD_CURRENT 常量。将此代码保存在当前文件夹的 SimpleVolumesStrategy.mqh 文件中。
EA 类
让我们创建 CAdvisor 基类。其目的是存储特定交易策略的对象列表,并启动其事件处理程序。对于这个类来说,CExpert 这个名字更合适,但它已经在标准库中使用,所以我们将使用 CAdvisor 来代替。
#include "Strategy.mqh" class CAdvisor : public CObject { protected: CStrategy *m_strategies[]; // Array of trading strategies int m_strategiesCount;// Number of strategies public: virtual int Init(); // EA initialization method virtual void Tick(); // OnTick event handler virtual void Deinit(); // Deinitialization method void AddStrategy(CStrategy &strategy); // Strategy adding method };
在 Init() 和 Tick() 方法中,会循环使用 m_strategies[] 数组中的所有策略,并调用相应的事件处理方法。
void CAdvisor::Tick(void) { // Call OnTick handling for all strategies for(int i = 0; i < m_strategiesCount; i++) { m_strategies[i].Tick(); } }
在策略添加方法中,情况正是如此。
void CAdvisor::AddStrategy(CStrategy &strategy) { // Increase the strategy number counter by 1 m_strategiesCount = ArraySize(m_strategies) + 1; // Increase the size of the strategies array ArrayResize(m_strategies, m_strategiesCount); // Write a pointer to the strategy object to the last element m_strategies[m_strategiesCount - 1] = GetPointer(strategy); }
让我们把这段代码保存到当前文件夹的 Advisor.mqh 文件中。在该类的基础上,可以创建实现管理多个策略的任何特定方法的子类。但现在,我们将只局限于这个基类,而不会以任何方式干涉单个策略的工作。
使用多种策略进行交易的 EA
要编写交易 EA,我们只需创建一个全局 EA 对象(属于 CAdvisor 类)。
在 OnInit() 初始化事件处理程序中,我们将使用所选参数创建策略对象,并将其添加到 EA 对象中。之后,我们会调用 EA 对象的 Init() 方法,以便初始化其中的所有策略。
OnTick() 和 OnDeinit() 事件处理程序只需调用 EA 对象的相应方法。
#include "Advisor.mqh" #include "SimpleVolumesStartegy.mqh" input double depoPart_ = 0.8; // Part of the deposit for one strategy input ulong magic_ = 27182; // Magic CAdvisor expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert.AddStrategy(...); expert.AddStrategy(...); int res = expert.Init(); // Initialization of all EA strategies return(res); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { expert.Deinit(); } //+------------------------------------------------------------------+
现在,让我们来详细了解创建策略对象。由于每个策略实例都会打开并考虑到自己的订单和仓位,因此它们应该具有不同的幻数。幻数是策略构造函数的第一个参数。因此,为了保证不同的幻数,我们将在 magic_ 参数指定的原始幻数上添加不同的数字。
expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 2, ...));
构造函数的第二和第三个参数是交易品种和周期数。由于我们对 EURGBP H1 进行了优化,因此我们指定了这些特定值。
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, ...));
下一个重要参数是 开仓头寸的大小。我们已经计算出两种策略的适当大小(0.34 和 0.10)。但这是策略单独运行时处理 10%(10,000 美元)回撤的大小。如果两个策略同时起作用,第一个策略的回撤可能会与第二个策略的回撤相加。在最坏的情况下,为了不超过规定的 10%,我们将不得不把建仓规模减半。但是,两种策略的回撤情况可能并不一致,甚至在一定程度上相互抵消。在这种情况下,我们可以稍微减少仓位,但仍不能超过 10%。因此,我们将还原乘数设为 EA 参数 (depoPart_),然后选择最佳值。
策略构造器 的其余参数是我们在优化简单 EA 后选择的数值集。最终结果如下:
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, NormalizeDouble(0.34 * depoPart_, 2), 130, 0.9, 1.4, 231, 3750, 50, 600, 3) ); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, NormalizeDouble(0.10 * depoPart_, 2), 159, 1.7, 0.8, 248, 3600, 495, 39000, 3) );
将生成的代码保存到当前文件夹的 SimpleVolumesExpert.mq5 文件中。
测试结果
在测试组合 EA 之前,让我们记住,使用第一组参数的策略应产生约 91% 的利润,而使用第二组参数的策略应产生 72% 的利润(对于 10,000 美元的起始存款和 10%(1,000 美元)的最大回撤,以及最佳手数)。
让我们根据维持给定回撤的标准来选择depoPart_ 参数的最佳值,结果如下。
图 3.组合 EA 操作结果
测试期结束时的余额约为 22 400 美元,利润率为 124%。这比我们在运行该策略的单个实例时得到的结果要多。我们只需利用现有的交易策略,而无需对其进行任何修改,就能改善交易结果。
结论
在实现目标的道路上,我们只迈出了一小步。这让我们更加相信,这种方法可以提高交易质量。到目前为止,EA 还缺乏许多重要方面。
例如,我们看了一个非常简单的策略,它不以任何方式控制平仓,不需要准确确定柱形的起始点,也不使用任何繁琐的计算。要在重启终端后恢复状态,除了计算未结头寸和订单(EA 可以做到这一点)外,您不需要做任何额外的努力 。但并非每个策略都如此简单。此外,该 EA 无法在净额结算账户上运行,并且可以同时打开相反的仓位。我们还没有考虑在不同的交易品种上下功夫。等等等等......
在真正开始交易之前,一定要考虑到这些方面。敬请期待新文章。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/14026