
开发多币种 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
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.




在我看来,架构骨架应该非常简洁、易于使用。这就是为什么 TC 的基本类是这样的。
接下来是肌腱的细化。它应该非常简单。
书中 也有类似的简单(在接口方面)但扩展(在实现方面)的内容。
书中 也有类似的简单(就界面而言)但扩展(就实现而言)的内容。
在哪里可以下载源代码?
从哪里下载源代码?
https://www.mql5.com/zh/code/45595