在 MQL5 中实现保本机制(第 2 部分):基于 ATR 和风险回报比的保本机制
- 引言
- 基于 ATR 指标开发 CBreakEvenAtr 类
- 基于风险回报比的 CBreakEvenRR 类基础与设计
- CBreakEven 类:保本模式的选择与动态配置
- 不同保本类型下的订单块EA测试
- 结论
引言
欢迎来到保本机制系列文章的第二部分。本文将继续完成第一部分中尚未编写的各类保本模式代码。此外,我们将创建一个辅助类,简化用户对保本模式的选择。我们将首先介绍基于 ATR 的保本类型,然后是基于风险回报比(RRR)的保本类型,最后实现一个用于选择和使用保本机制的类。文章末尾,我们将进行对比分析,确定哪种保本类型最适合最初在风险管理系列上一篇文章中开发的订单块EA。
基于 ATR 开发 CBreakEvenAtr 类
正如上一篇文章所述,基于 ATR 的保本模式,无论是保本触发的价差距离,还是保本价格与持仓开仓价之间的价差,都不使用固定点数。取而代之的是,使用 ATR 值的乘数来计算。这种方法在配置保本触发距离和保本设置水平时提供了更大的灵活性。
较大的 ATR 乘数通常意味着更大的偏移量,因此保本触发频率更低。反之,较小的乘数可能导致保本触发频率高于正常水平,如果没有给持仓留出足够的运行空间,可能会对盈利能力产生负面影响。
为在 MQL5 中实现该方法,我们将创建 CBreakEvenAtr 类,它继承自所有保本类型的基类 CBreakEvenBase。
//--- class CBreakEvenAtr class CBreakEvenAtr : public CBreakEvenBase
要使用 ATR 指标,我们需要一个句柄(handle),通过它获取指标数据,即复制指标值。此外,我们将使用一个名为 atr_buff 的 double 类型数组,用于存储从指标复制的数据。
我们还需要一个整型变量 atr_idx,表示从哪个索引开始复制数据;例如,值为 0 表示从当前 ATR 值开始复制。每次访问时恰好复制一个缓冲区元素。
接下来,我们需要两个乘数:一个用于确定保本水平设置距离,另一个用于确定保本触发距离。
私有变量
private: int atr_handle; double atr_buff[]; int atr_idx; double atr_multiplier_be; double atr_multiplier_extra_be;
构造函数与析构函数
在构造函数中,我们用默认值初始化私有变量。此外,我们准备好用于存储 ATR 值的atr_buff数组。我们还指定该类需要 5 个参数,因此num_params变量将设为对应的值。
构造函数接收三个参数,并将它们传递给基类 CBreakEvenBase 进行初始化。
CBreakEvenAtr(string symbol_, ulong magic_, ENUM_BREAKEVEN_MODE mode_) : CBreakEvenBase(symbol_, magic_, mode_), atr_handle(INVALID_HANDLE), atr_idx(0), atr_multiplier_be(1.0), atr_multiplier_extra_be(1.0) { ArraySetAsSeries(atr_buff, true); this.num_params = 5; }
在析构函数中,我们将释放 ATR 指标句柄,以及存储 ATR 值的数组:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CBreakEvenAtr::~CBreakEvenAtr(void) { ArrayFree(atr_buff); if(this.atr_handle != INVALID_HANDLE) IndicatorRelease(this.atr_handle); }
参数设置函数
继续定义和声明基于 ATR 的保本类,接下来定义用于设置内部变量的方法。首先,我们定义 SetSimple 系列函数。这些函数用于在不使用 MqlParams 的情况下设置类所需的参数,当已知保本模式固定为基于 ATR 时,这种方式非常理想。我们提供了该函数的两个版本:
void SetSimple(int atr_handle_, int atr_idx_, double atr_multiplier_extra_be_, double atr_multiplier_be_); void SetSimple(int atr_period, int atr_idx_, double atr_multiplier_extra_be_, double atr_multiplier_be_, ENUM_TIMEFRAMES timeframe);
在第一个 SetSimple 函数中,我们首先检查乘数等参数是否有效。
if(atr_multiplier_extra_be_ >= atr_multiplier_be_) { printf("%s: Error | The multiplier of the atr to calculate the price be is greater than or equal to the multiplier to set the be", __FUNCSIG__); ExpertRemove(); }
我们检查保本触发乘数必须小于保本设置乘数。这是必要的,以防止出现无效止损位等错误。
然后,我们确保 ATR 索引始终大于或等于零。
if(atr_idx_ < 0) { printf("%s: Critical error | Atr index is less than 0", __FUNCTION__); ExpertRemove(); }
最后,我们确保 ATR 指标句柄不是无效句柄;如果无效,EA将从图表上移除。
if(atr_handle_ == INVALID_HANDLE) { printf("%s: Critical Error | The handle of the indicator atr is invalid, last error: %I32d", __FUNCTION__, GetLastError()); ExpertRemove(); }
完成所有检查后,我们将参数赋值给类的内部变量。
this.atr_idx = atr_idx_; this.atr_multiplier_be = atr_multiplier_be_; this.atr_multiplier_extra_be = atr_multiplier_extra_be_; this.atr_handle = atr_handle_;
完整代码
//+------------------------------------------------------------------------------------+ //| Function to set the values of the CBreakEvenAtr class without using MqlParams | //| Using the handle instead of period and timeframe | //+------------------------------------------------------------------------------------+ void CBreakEvenAtr::SetSimple(int atr_handle_, int atr_idx_, double atr_multiplier_extra_be_, double atr_multiplier_be_) { if(atr_multiplier_extra_be_ >= atr_multiplier_be_) { printf("%s: Error | The multiplier of the atr to calculate the price be is greater than or equal to the multiplier to set the be", __FUNCSIG__); ExpertRemove(); } if(atr_idx_ < 0) { printf("%s: Critical error | Atr index is less than 0", __FUNCTION__); ExpertRemove(); } if(atr_handle_ == INVALID_HANDLE) { printf("%s: Critical Error | The handle of the indicator atr is invalid, last error: %I32d", __FUNCTION__, GetLastError()); ExpertRemove(); } this.atr_idx = atr_idx_; this.atr_multiplier_be = atr_multiplier_be_; this.atr_multiplier_extra_be = atr_multiplier_extra_be_; this.atr_handle = atr_handle_; }
第二个 SetSimple 函数需要一个额外参数。该函数不接收句柄,而是要求传入 ATR 周期,因为我们将在函数内部配置句柄。随后调用第一个 SetSimple 函数,并将创建好的句柄传递给它。
//+------------------------------------------------------------------------------------+ //| Function to set the values of the CBreakEvenAtr class without using MqlParams | //| Configuring the handle with the period and timeframe. | //+------------------------------------------------------------------------------------+ void CBreakEvenAtr::SetSimple(int atr_period, int atr_idx_, double atr_multiplier_extra_be_, double atr_multiplier_be_, ENUM_TIMEFRAMES timeframe) { TesterHideIndicators(true); ResetLastError(); this.atr_handle = iATR(this.symbol, timeframe, atr_period); TesterHideIndicators(false); SetSimple(atr_handle,atr_idx_,atr_multiplier_extra_be_,atr_multiplier_be_); }
现在我们来定义 Set 函数,它继承自保本机制基类。该函数将在 CBreakEven 选择器类中使用。正如我在上一篇文章中提到的,我们无法在基类中重写 SetSimple 函数,因为事先不知道它会接收什么参数。这就是 Set 函数允许我们处理动态参数的原因。
对于 Set 函数,由于我们有两个版本的 SetSimple,所需参数数量不同,我们将使用最大参数集的大小来检查输入数组的长度是否正确。为区分传入已创建句柄的情况和不传入句柄的情况,我们将创建一个名为 HANDLE_INSTEAD_OF_PERIOD 的宏定义,默认值为 0。
#define HANDLE_INSTEAD_OF_PERIOD 0
该宏定义位于 params 数组的最后一个索引位置。如果其值为 0,我们假定使用已创建的句柄进行配置;否则,将使用不带预创建句柄的 SetSimple 函数。
上述逻辑意味着 Set 函数的主体如下:
//+------------------------------------------------------------------+ //| Function for setting CBreakEvenAtr variables with MqlParams | //+------------------------------------------------------------------+ void CBreakEvenAtr::Set(MqlParam ¶ms[]) { // Check that params has the correct size; ATR requires 5 parameters if(params.Size() < 5) { printf("%s: Critical error | The size of the MqlParams array %I32u is less than 5", __FUNCTION__, params.Size()); ExpertRemove(); return; } // If the last parameter is HANDLE_INSTEAD_OF_PERIOD, use SetSimple with a handle if(params[4].integer_value == HANDLE_INSTEAD_OF_PERIOD) { SetSimple( (int)params[3].integer_value, // atr_handle (int)params[0].integer_value, // atr_idx params[2].double_value, // atr_multiplier_extra_be params[1].double_value // atr_multiplier_be ); } else { // Otherwise, use SetSimple with period and timeframe SetSimple( (int)params[4].integer_value, // atr_period (int)params[0].integer_value, // atr_idx params[2].double_value, // atr_multiplier_extra_be params[1].double_value, // atr_multiplier_be (ENUM_TIMEFRAMES)params[3].integer_value // timeframe ); } }
保本机制的实现函数
为实现基于 ATR 的保本模式,我们将重写.Add 方法。该方法在每次新开仓时执行,即在基类的 OnTradeTransactionEvent 函数内部调用。在.Add 方法的主体中,我们需要确定保本价格,以及触发保本机制的触发价格。
为计算所需的价格,如保本价格(breakeven_price)和触发价格(price_to_beat,即触发保本并修改持仓止损的价格),我们将使用与固定点数保本类中类似的公式。
- 买入持仓:
break_even_price = open_price + (atr_buff[0] * atr_multiplier_extra_be)
- 卖出持仓:
break_even_price = open_price - (atr_buff[0]* atr_multiplier_extra_be)
在该公式中,我们使用 atr_multiplier_extra_be 代替 extra_points_be 来计算保本价格。对于触发价格(price_to_beat),公式相同,只是将 atr_multiplier_extra_be 替换为 atr_multiplier_be 变量。
确定要赋值的数值后,我们来定义.Add 函数。首先,从 this.atr_idx 指定的位置开始复制 ATR 数据,且仅复制一个元素。我们检查 CopyBuffer 返回的值不小于 1;否则返回 false,表示该订单号无法添加到保本数组中。
ResetLastError(); if(CopyBuffer(this.atr_handle, 0, this.atr_idx, 1, this.atr_buff) < 1) { printf("%s: Error | When copying the data of the indicator atr, last error %I32d", __FUNCTION__, GetLastError()); return false; }
然后创建 new_pos 结构体,它包含 BreakEven() 函数所需的重要信息。我们为其成员赋值:保本价格、触发价格(使用上述公式)、订单号,以及持仓类型(多单或空单)。
position_be new_pos; new_pos.breakeven_price = position_type == POSITION_TYPE_BUY ? open_price + (atr_buff[0] * atr_multiplier_extra_be) : open_price - (atr_buff[0] * atr_multiplier_extra_be); new_pos.type = position_type; new_pos.price_to_beat = position_type == POSITION_TYPE_BUY ? open_price + (atr_buff[0] * atr_multiplier_be) : open_price - (atr_buff[0] * atr_multiplier_be); new_pos.ticket = post_ticket; ExtraFunctions::AddArrayNoVerification(this.PostionsBe, new_pos);
.Add 函数完整代码
//+------------------------------------------------------------------+ //| Function to add an element to the positions array | //+------------------------------------------------------------------+ bool CBreakEvenAtr::Add(ulong post_ticket, double open_price, double sl_price, ENUM_POSITION_TYPE position_type) { ResetLastError(); if(CopyBuffer(this.atr_handle, 0, this.atr_idx, 1, this.atr_buff) < 1) { printf("%s: Error | When copying the data of the indicator atr, last error %I32d", __FUNCTION__, GetLastError()); return false; } position_be new_pos; new_pos.breakeven_price = position_type == POSITION_TYPE_BUY ? open_price + (atr_buff[0] * atr_multiplier_extra_be) : open_price - (atr_buff[0] * atr_multiplier_extra_be); new_pos.type = position_type; new_pos.price_to_beat = position_type == POSITION_TYPE_BUY ? open_price + (atr_buff[0] * atr_multiplier_be) : open_price - (atr_buff[0] * atr_multiplier_be); new_pos.ticket = post_ticket; ExtraFunctions::AddArrayNoVerification(this.PostionsBe, new_pos); return true; }
基于风险回报比的 CBreakEvenRR 类基础与设计
基于风险回报比(RRR)的保本模式,与基于 ATR 的模式一样,是另一种动态保本类型。简而言之,其原理是:当持仓盈利达到止损距离的指定倍数时,将止损移动到距离开仓价一定距离的位置。例如,比值为 1 表示当当前价格大于等于开仓价加上止损点数距离时,触发保本。
首先,我将做一些改动。在上一篇文章中,止损是从开仓价移动固定点数。在本部分中,我将增加基于 ATR 移动止损的功能,使基于 RRR 的保本更加动态。
现在开始编写代码。
正如我已经指出的,将 ATR 作为保本价格计算的一个选项,需要创建一个枚举,允许我们选择保本价格的计算方式:固定点数方式,还是 ATR 值乘以相应乘数的方式。
enum ENUM_TYPE_EXTRA_BE_BY_RRR { EXTRA_BE_RRR_BY_ATR, //By Atr EXTRA_BE_RRR_BY_FIXED_POINTS //By Fixed Points };
该类公有继承自 CBreakEvenBase 基类。
class CBreakEvenRR : public CBreakEvenBase { private:
我们从定义主要变量开始。我们需要一个变量来存储保本机制触发的系数。
double coefficient_rr; //Coefficient of rr
我们还需要存储保本价格的计算类型。
ENUM_TYPE_EXTRA_BE_BY_RRR type;
接下来是一个变量,根据所选模式,它将存储 ATR 乘数,或者存储已乘以 point_value 的数值:
double extra_value_be; //Extra value that will be added to the opening price of the position to obtain the breakeven price //Note: if the type is atr this will contain the atr multiplier, if not it will contain the value already multiplied by the point value
为集成 ATR 功能,我们需要三个必要变量:句柄、存储值的数组,以及存储数据复制起始索引的整型变量。
int handle_atr; int idx_atr; double atr_buff[];
构造函数
该类的构造函数接收与主类相同的三个参数。
CBreakEvenRR(string symbol_, ulong magic_, ENUM_BREAKEVEN_MODE mode_);
在构造函数主体内,我们初始化类的内部变量,并调用基类构造函数。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ void CBreakEvenRR::CBreakEvenRR(string symbol_, ulong magic_, ENUM_BREAKEVEN_MODE mode_) : CBreakEvenBase(symbol_, magic_, mode_), handle_atr(INVALID_HANDLE), idx_atr(0), coefficient_rr(1.0), extra_value_be(100.0), type(EXTRA_BE_RRR_BY_FIXED_POINTS) { ArraySetAsSeries(atr_buff, true); this.num_params = 6; }
析构函数
在析构函数中,释放存储 ATR 数据的数组和 ATR 指标句柄。
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CBreakEvenRR::~CBreakEvenRR(void) { ArrayFree(this.atr_buff); if(handle_atr != INVALID_HANDLE) IndicatorRelease(this.handle_atr); }
参数设置函数
在类参数设置函数中,我们将创建另外两个函数,逻辑与基于 ATR 的保本类类似。其中一个接收句柄,另一个接收时间周期和 ATR 周期等参数,直接在函数内部创建句柄。
首先,我们定义接收句柄的函数。
在函数开头,我们检查不存在无效的变量值,并且存储在 type_extra 变量中的保本类型是有效的。
//+------------------------------------------------------------------+ //| Function to set break even values by rr without MqlParams | //+------------------------------------------------------------------+ void CBreakEvenRR::SetSimple(double rr_a_put_the_break_even, ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, double atr_multiplier_or_extra_points, int idx_atr_, int atr_handle_) { if(coefficient_rr <= 0.00) { printf("%s: Critical Error | The %+f coefficient of 'reward' is invalid", __FUNCTION__, coefficient_rr); ExpertRemove(); return; } if(atr_multiplier_or_extra_points <= 0.00) { printf("%s: Error | The ATR multiplier or extra points value %f is less than or equal to 0", __FUNCTION__, atr_multiplier_or_extra_points); ExpertRemove(); return; } if(type_extra != EXTRA_BE_RRR_BY_ATR && type_extra != EXTRA_BE_RRR_BY_FIXED_POINTS) { printf("%s: Critical error | The extra value type %s is invalid", __FUNCTION__, EnumToString(type_extra)); ExpertRemove(); return; }
如果任何检查失败,我们将把EA从当前图表移除,并打印错误信息。
然后将相应的值赋值给内部变量:
this.type = type_extra; this.coefficient_rr = rr_a_put_the_break_even;
如果 type_extra 变量的值为 EXTRA_BE_RRR_BY_ATR,我们检查 ATR 索引。
if(type_extra == EXTRA_BE_RRR_BY_ATR) { if(idx_atr_ < 0) { printf("%s: Error | The ATR index %I32d is invalid", __FUNCTION__, idx_atr_); ExpertRemove(); return; } else this.idx_atr = idx_atr_;
检查通过后,将 idx_atr_参数值赋值给内部 idx_atr 变量。
接下来检查句柄是否有效;如果无效,移除EA。如果检查通过,将 atr_handle_参数值赋值给内部 handle_atr 变量。此外,extra_value_be 变量赋值为 atr_multiplier_or_extra_points 的值。
if(atr_handle_ == INVALID_HANDLE) { printf("%s: Critical Error | The handle of the indicator atr is invalid, last error: %I32d", __FUNCTION__, GetLastError()); ExpertRemove(); return; } else this.handle_atr = atr_handle_; this.extra_value_be = atr_multiplier_or_extra_points; } else this.extra_value_be = atr_multiplier_or_extra_points * this.point_value;
现在来看第二个函数,它接收用于创建 ATR 句柄的参数。
void SetSimple(double rr_a_put_the_break_even, ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, double atr_multiplier_or_extra_points, int idx_atr_, ENUM_TIMEFRAMES tf_atr, int atr_period_);
首先,检查 type_extra 和 atr_period_参数值是否有效。
//+------------------------------------------------------------------+ //| Function to set break even values by rr without MqlParams | //+------------------------------------------------------------------+ void CBreakEvenRR::SetSimple(double rr_a_put_the_break_even, ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, double atr_multiplier_or_extra_points, int idx_atr_, ENUM_TIMEFRAMES tf_atr, int atr_period_) { if(type_extra != EXTRA_BE_RRR_BY_ATR && type_extra != EXTRA_BE_RRR_BY_FIXED_POINTS) { printf("%s: Critical error | The extra value type %s is invalid", __FUNCTION__, EnumToString(type_extra)); ExpertRemove(); return; } if(atr_period_ < 0) { printf("%s: Error | The ATR period %I32d is invalid", __FUNCTION__, atr_period_); ExpertRemove(); return; }
如果任何值无效,将EA从当前图表移除。
如果 type_extra 的值为 EXTRA_BE_RRR_BY_ATR,使用 tf_atr 和 atr_period_参数创建 ATR 指标句柄。
if(type_extra == EXTRA_BE_RRR_BY_ATR) { TesterHideIndicators(true); ResetLastError(); this.handle_atr = iATR(this.symbol, tf_atr, atr_period_); TesterHideIndicators(false); }
最后,调用第一个 SetSimple 函数。
SetSimple(rr_a_put_the_break_even, type_extra, atr_multiplier_or_extra_points, idx_atr_, this.handle_atr);
} 现在重写.Set () 函数,该函数继承自保本基类,用于设置类参数。
void Set(MqlParam ¶ms[]) override;
在 Set 函数中,首先检查 params 数组的大小不小于该类所需的参数数量。
//+------------------------------------------------------------------+ //| Function to set break even values by rr with MqlParams | //+------------------------------------------------------------------+ void CBreakEvenRR::Set(MqlParam ¶ms[]) { if((int)params.Size() < num_params) { printf("%s: Error | The size of the MqlParams array %I32u to set the be by rr is less than 2", __FUNCTION__, params.Size()); ExpertRemove(); return; }
如果检查失败,移除EA。
为使用 params 数组设置类内部值,应用 HANDLE_INSTEAD_OF_PERIOD 宏定义。该宏定义指示 params 数组中的参数是否已包含现成的句柄。如果是,使用第一个 SetSimple 函数;否则,调用第二个 SetSimple 函数,由其创建句柄。
if(params[5].integer_value == HANDLE_INSTEAD_OF_PERIOD) { //-> (0)double rr_a_put_the_break_even,(1) ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, (2)double atr_multiplier_or_extra_points, (3)int idx_atr_, (4)int atr_handle_ SetSimple(params[0].double_value, (ENUM_TYPE_EXTRA_BE_BY_RRR)params[1].integer_value, params[2].double_value, (int)params[3].integer_value, (int)params[4].integer_value); } else { //-> (0)double rr_a_put_the_break_even,(1) ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, (2)double atr_multiplier_or_extra_points, (3)int idx_atr_, (4)ENUM_TIMEFRAMES tf_atr, (5)int atr_period_ SetSimple(params[0].double_value, (ENUM_TYPE_EXTRA_BE_BY_RRR)params[1].integer_value, params[2].double_value, (int)params[3].integer_value, (ENUM_TIMEFRAMES)params[4].integer_value, (int)params[5].integer_value); } }
.Add 函数
在 Add 函数内部,我们首先设置 CBreakEvenBase 类所需的值,以确保CBreakEven 函数(负责对持仓应用保本机制)能够正确执行。
首先,检查持仓是否设置了止损:
//+------------------------------------------------------------------+ //| Function to add an element to the positions array | //+------------------------------------------------------------------+ bool CBreakEvenRR::Add(ulong post_ticket, double open_price, double sl_price, ENUM_POSITION_TYPE position_type) { if(sl_price <= 0.00) { printf("%s: Error | Position %I64u with stop loss %+f has sl less than 0", __FUNCTION__, post_ticket, sl_price); return false; }
如果没有止损,返回 false,该持仓不会被添加到 PositionsBe 数组中。
接下来,创建一个名为 val 的 double 变量,用于存储止损位与开仓价之间的距离。如果 type 变量的值为 EXTRA_BE_RRR_BY_ATR,从内部 idx_atr 变量开始复制 ATR 数据,仅复制一个元素。如果复制的元素少于一个,返回 false 并打印错误信息。否则,将 val 乘以 atr_buff 数组索引 0 处的值。
double val = this.extra_value_be; if(type == EXTRA_BE_RRR_BY_ATR) { ResetLastError(); if(CopyBuffer(this.handle_atr, 0, this.idx_atr, 1, this.atr_buff) < 1) { printf("%s: Error | When copying the data of the indicator atr, last error %I32d", __FUNCTION__, GetLastError()); return false; } else val *= this.atr_buff[0]; }
接下来,检查止损设置距离是否有效,即小于保本机制触发距离,以避免无效止损错误。
double diff = MathAbs(open_price - sl_price); if((diff * coefficient_rr) <= val) { printf("%s: Error | The distance from the opening price %f where the stoploss is located is greater than or equal to the price to trigger the breakeven", __FUNCTION__, this.extra_value_be); return false; }
最后,如果所有检查通过,计算新的止损价格(breakeven_price),以及触发保本模式的价格(price_to_beat),使用以下公式:
新止损价(breakeven_price):
- 买入持仓:
break_even_price = open_price + val
- 卖出持仓:
break_even_price = open_price - val
保本触发价(price_to_beat):
- 买入持仓:
price_to_beat = open_price + (coefficient_rr * diff)
- 卖出持仓:
price_to_beat = open_price - (coefficient_rr * diff)
创建 position_be 结构体,为其赋值所需的值:持仓类型、订单号,以及两个价格水平(breakeven_price 和 price_to_beat)。
之后,将该结构体添加到 PositionsBe 数组中。
position_be new_pos; new_pos.breakeven_price = position_type == POSITION_TYPE_BUY ? open_price + val : open_price - val; new_pos.type = position_type; new_pos.price_to_beat = position_type == POSITION_TYPE_BUY ? open_price + (coefficient_rr * diff) : open_price - (coefficient_rr * diff); new_pos.ticket = post_ticket; ExtraFunctions::AddArrayNoVerification(this.PostionsBe, new_pos); return true; }
CBreakEven 类:保本模式的选择与动态配置
为高效选择要使用的保本模式,我们将创建一个选择器类。它负责执行保本逻辑,并根据用户选择的保本类型,将内部指针设置为相应派生类对象的指针。
首先定义 BreakEvenParams 结构体,它包含一个 MqlParam 数组,用于存储特定保本类型的参数。
struct BreakEvenParams { MqlParam params[]; };
现在声明类本身及其私有部分。
//--- class CBreakEven { private: ulong magic; string symbol; ENUM_BREAKEVEN_MODE breakeven_mode; BreakEvenParams parameters[]; CBreakEvenBase* CreateBreakEven(ENUM_BREAKEVEN_TYPE type);
私有部分包含魔术数字、交易品种和保本模式。还包括一个 BreakEvenParams 类型的数组,用于存储所支持的三种保本类型的参数。还声明了一个返回 CBreakEvenBase 指针的函数;它将在后续函数中被调用,用于设置内部指针。
最后,创建一个 CBreakEvenBase * 类型的变量 obj;它将在 SetInternalPointer 函数中根据用户选择的保本模式进行配置,并转换为所需类型。
CBreakEvenBase* obj;
构造函数
构造函数的参数与保本基类构造函数相同。
CBreakEven(ulong magic_, string symbol_, ENUM_BREAKEVEN_MODE mode);
在构造函数内部,初始化内部变量值,并将 parameters 数组的大小设为 3,对应所支持的三种保本类型。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CBreakEven::CBreakEven(ulong magic_, string symbol_, ENUM_BREAKEVEN_MODE mode) { this.magic = magic_; this.symbol = symbol_; this.breakeven_mode = mode; ArrayResize(parameters, 3); }
析构函数
在析构函数中,首先释放 parameters 数组。
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CBreakEven::~CBreakEven() { ArrayFree(this.parameters);
在删除保本指针 obj 之前,检查它是否为动态指针,如果是则删除它。
if(CheckPointer(this.obj) == POINTER_DYNAMIC) { delete obj; obj = NULL; } }
参数设置函数
为在 parameters 数组中设置值,我们将创建五个函数,用于配置三种保本模式的参数:两个用于 ATR,两个用于 RRR,一个用于固定点数保本。
RRR 保本参数设置函数
我们从 RRR 函数开始。如上所述,共有两个。一个需要传入 ATR 句柄:
void SetBeByRR(double rr_a_put_the_break_even, ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, double atr_multiplier_or_extra_points, int idx_atr_, int atr_handle_);
另一个需要通用参数和创建 ATR 句柄的参数:
void SetBeByRR(double rr_a_put_the_break_even, ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, double atr_multiplier_or_extra_points, int idx_atr_, ENUM_TIMEFRAMES tf_atr, int atr_period_);
首先定义需要传入 ATR 句柄的第一个函数。在该函数中,与其他函数一样,首先将 params 数组的大小设为 6。
ArrayResize(parameters[int(BREAKEVEN_TYPE_RR)].params, 6);
然后为 params 数组的每个索引赋值相应的值。
parameters[BREAKEVEN_TYPE_RR].params[0].double_value = rr_a_put_the_break_even; parameters[BREAKEVEN_TYPE_RR].params[1].integer_value = (int)type_extra; parameters[BREAKEVEN_TYPE_RR].params[2].double_value = atr_multiplier_or_extra_points; parameters[BREAKEVEN_TYPE_RR].params[3].integer_value = idx_atr_; parameters[BREAKEVEN_TYPE_RR].params[4].integer_value = atr_handle_;
最后一个索引(索引 5)将赋值为 HANDLE_INSTEAD_OF_PERIOD,以便 CBreakEvenRR 类的参数设置函数能够确定应调用哪个函数;在这种情况下,调用需要传入句柄的函数。
parameters[BREAKEVEN_TYPE_RR].params[5].integer_value = HANDLE_INSTEAD_OF_PERIOD; 第二个函数的步骤类似,但现在索引 4(之前存储 ATR 句柄值)将存储 ATR 时间周期,索引 5 将存储 ATR 周期。
//+------------------------------------------------------------------+ //| Set rr without handle | //+------------------------------------------------------------------+ void CBreakEven::SetBeByRR(double rr_a_put_the_break_even, ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, double atr_multiplier_or_extra_points, int idx_atr_, ENUM_TIMEFRAMES tf_atr, int atr_period_) { ArrayResize(parameters[int(BREAKEVEN_TYPE_RR)].params, 6); parameters[BREAKEVEN_TYPE_RR].params[0].double_value = rr_a_put_the_break_even; parameters[BREAKEVEN_TYPE_RR].params[1].integer_value = (int)type_extra; parameters[BREAKEVEN_TYPE_RR].params[2].double_value = atr_multiplier_or_extra_points; parameters[BREAKEVEN_TYPE_RR].params[3].integer_value = idx_atr_; parameters[BREAKEVEN_TYPE_RR].params[4].integer_value = (int)tf_atr; parameters[BREAKEVEN_TYPE_RR].params[5].integer_value = atr_period_; }
完整代码
//+------------------------------------------------------------------+ //| Set rr without handle | //+------------------------------------------------------------------+ void CBreakEven::SetBeByRR(double rr_a_put_the_break_even, ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, double atr_multiplier_or_extra_points, int idx_atr_, ENUM_TIMEFRAMES tf_atr, int atr_period_) { ArrayResize(parameters[int(BREAKEVEN_TYPE_RR)].params, 6); parameters[BREAKEVEN_TYPE_RR].params[0].double_value = rr_a_put_the_break_even; parameters[BREAKEVEN_TYPE_RR].params[1].integer_value = (int)type_extra; parameters[BREAKEVEN_TYPE_RR].params[2].double_value = atr_multiplier_or_extra_points; parameters[BREAKEVEN_TYPE_RR].params[3].integer_value = idx_atr_; parameters[BREAKEVEN_TYPE_RR].params[4].integer_value = (int)tf_atr; parameters[BREAKEVEN_TYPE_RR].params[5].integer_value = atr_period_; } //+------------------------------------------------------------------+ //| Set rr with handle | //+------------------------------------------------------------------+ void CBreakEven::SetBeByRR(double rr_a_put_the_break_even, ENUM_TYPE_EXTRA_BE_BY_RRR type_extra, double atr_multiplier_or_extra_points, int idx_atr_, int atr_handle_) { ArrayResize(parameters[int(BREAKEVEN_TYPE_RR)].params, 6); parameters[BREAKEVEN_TYPE_RR].params[0].double_value = rr_a_put_the_break_even; parameters[BREAKEVEN_TYPE_RR].params[1].integer_value = (int)type_extra; parameters[BREAKEVEN_TYPE_RR].params[2].double_value = atr_multiplier_or_extra_points; parameters[BREAKEVEN_TYPE_RR].params[3].integer_value = idx_atr_; parameters[BREAKEVEN_TYPE_RR].params[4].integer_value = atr_handle_; parameters[BREAKEVEN_TYPE_RR].params[5].integer_value = HANDLE_INSTEAD_OF_PERIOD; }
ATR 保本参数设置函数
与 RRR 保本的情况类似,为设置 params 数组的值,我们也使用两个函数,但参数更少。
两个参数设置函数都恰好有 5 个参数。第一个需要传入句柄,定义如下:
void CBreakEven::SetBeByAtr(int atr_idx, double atr_multiplier_be, double atr_multiplier_extra_be, int atr_handle)
在编写该函数时,第一步是设置 params 数组内 parameters 数组的大小:
ArrayResize(parameters[int(BREAKEVEN_TYPE_ATR)].params, 5);
然后为 params 数组的全部 5 个元素赋值:
parameters[BREAKEVEN_TYPE_ATR].params[0].integer_value = atr_idx; parameters[BREAKEVEN_TYPE_ATR].params[1].double_value = atr_multiplier_be; parameters[BREAKEVEN_TYPE_ATR].params[2].double_value = atr_multiplier_extra_be; parameters[BREAKEVEN_TYPE_ATR].params[3].integer_value = atr_handle;
记住最后一个元素(索引 4)必须赋值为 HANDLE_INSTEAD_OF_PERIOD,以便选择 CBreakEvenAtr 类中需要传入句柄的成员函数:
parameters[BREAKEVEN_TYPE_ATR].params[4].integer_value = HANDLE_INSTEAD_OF_PERIOD;
对于第二个函数,仅索引 3 和 4 的值发生变化。
索引 3 将包含 ATR 时间周期值:
parameters[BREAKEVEN_TYPE_ATR].params[3].integer_value = int(timeframe);
索引 4 将包含 ATR 周期值:
parameters[BREAKEVEN_TYPE_ATR].params[4].integer_value = atr_period;
完整代码
//+------------------------------------------------------------------+ //| Set atr (without handle) | //+------------------------------------------------------------------+ void CBreakEven::SetBeByAtr(int atr_period, int atr_idx_, double atr_multiplier_extra_be_, double atr_multiplier_be_, ENUM_TIMEFRAMES timeframe) { ArrayResize(parameters[int(BREAKEVEN_TYPE_ATR)].params, 5); parameters[BREAKEVEN_TYPE_ATR].params[0].integer_value = atr_idx_; parameters[BREAKEVEN_TYPE_ATR].params[1].double_value = atr_multiplier_be_; parameters[BREAKEVEN_TYPE_ATR].params[2].double_value = atr_multiplier_extra_be_; parameters[BREAKEVEN_TYPE_ATR].params[3].integer_value = int(timeframe); parameters[BREAKEVEN_TYPE_ATR].params[4].integer_value = atr_period; } //+------------------------------------------------------------------+ //| Set atr (with handle) | //+------------------------------------------------------------------+ void CBreakEven::SetBeByAtr(int atr_idx, double atr_multiplier_be, double atr_multiplier_extra_be, int atr_handle) { ArrayResize(parameters[int(BREAKEVEN_TYPE_ATR)].params, 5); parameters[BREAKEVEN_TYPE_ATR].params[0].integer_value = atr_idx; parameters[BREAKEVEN_TYPE_ATR].params[1].double_value = atr_multiplier_be; parameters[BREAKEVEN_TYPE_ATR].params[2].double_value = atr_multiplier_extra_be; parameters[BREAKEVEN_TYPE_ATR].params[3].integer_value = atr_handle; parameters[BREAKEVEN_TYPE_ATR].params[4].integer_value = HANDLE_INSTEAD_OF_PERIOD; }
设置固定点数保本参数的函数
为完成配置,还剩下基于点数的保本;它仅需两个参数。在 CBreakEven 类中,我们将其声明如下:
void SetBeByFixedPoints(int points_be_, int extra_points_be_);
定义该函数时,首先将对应 params 数组的大小设为 2:
ArrayResize(parameters[int(BREAKEVEN_TYPE_FIXED_POINTS)].params, 2);
然后为索引 0 赋值保本触发点数,为索引 1 赋值额外点数。
parameters[BREAKEVEN_TYPE_FIXED_POINTS].params[0].integer_value = points_be_; parameters[BREAKEVEN_TYPE_FIXED_POINTS].params[1].integer_value = extra_points_be_;
完整代码
//+------------------------------------------------------------------+ //| Set Fixed Point be | //+------------------------------------------------------------------+ void CBreakEven::SetBeByFixedPoints(int points_be_, int extra_points_be_) { ArrayResize(parameters[int(BREAKEVEN_TYPE_FIXED_POINTS)].params, 2); parameters[BREAKEVEN_TYPE_FIXED_POINTS].params[0].integer_value = points_be_; parameters[BREAKEVEN_TYPE_FIXED_POINTS].params[1].integer_value = extra_points_be_; }
根据保本类型获取指针的函数
为根据用户设置的类型获取所需的保本模式,我们将创建一个返回 CBreakEvenBase 指针的函数。
CBreakEvenBase *CBreakEven::CreateBreakEven(ENUM_BREAKEVEN_TYPE type)
首先创建一个 switch 语句,每个 case 对应一种保本类型。如果 type 变量不匹配任何类型,默认返回 NULL。如果匹配成功,函数返回指向对应子类的指针。
switch(type) { case BREAKEVEN_TYPE_ATR: return new CBreakEvenAtr(this.symbol, this.magic, this.breakeven_mode); case BREAKEVEN_TYPE_RR: return new CBreakEvenRR(this.symbol, this.magic, this.breakeven_mode); case BREAKEVEN_TYPE_FIXED_POINTS: return new CBreakEvenSimple(this.symbol, this.magic, this.breakeven_mode); default: return NULL; }
完整代码
//+------------------------------------------------------------------+ //| Dynamically create the correct BreakEven | //+------------------------------------------------------------------+ CBreakEvenBase *CBreakEven::CreateBreakEven(ENUM_BREAKEVEN_TYPE type) { switch(type) { case BREAKEVEN_TYPE_ATR: return new CBreakEvenAtr(this.symbol, this.magic, this.breakeven_mode); case BREAKEVEN_TYPE_RR: return new CBreakEvenRR(this.symbol, this.magic, this.breakeven_mode); case BREAKEVEN_TYPE_FIXED_POINTS: return new CBreakEvenSimple(this.symbol, this.magic, this.breakeven_mode); default: return NULL; } return NULL; }
设置 obj 指针内部值的函数
最后,为给内部指针赋值,我们将创建一个名为 SetInternalPointer 的函数,它接收保本类型作为参数:
void SetInternalPointer(ENUM_BREAKEVEN_TYPE type) 函数定义开始时,先检查 obj 指针的状态。
if(CheckPointer(this.obj) == POINTER_DYNAMIC) { delete this.obj; this.obj = NULL; }
如果指针是动态的,将其删除。
然后用 CreateBreakEven 函数初始化 obj 指针,该函数根据选择的保本类型返回对应指针。赋值后,检查它是否为 NULL。如果为 NULL,打印错误信息并将EA从当前图表移除。
this.obj = CreateBreakEven(type); if(this.obj == NULL) { printf("%s: Critical error | The type %d is invalid.", __FUNCTION__, type); ExpertRemove(); return; }
检查 params 数组的大小是否足以存储所需参数。如果检查失败,删除对象,将指针设为 NULL,并将EA从图表移除。
if((int)parameters[type].params.Size() < obj.GetNumParams()) { printf("%s: Error | The parameter array for %s is too small (%I32u elements)", __FUNCTION__, EnumToString(type), parameters[type].params.Size()); delete obj; obj = NULL; ExpertRemove(); return; }
最后,使用基类的 Set 函数设置所需值:
obj.Set(parameters[type].params);
完整代码
//+------------------------------------------------------------------+ //| Set Pointer | //+------------------------------------------------------------------+ void CBreakEven::SetInternalPointer(ENUM_BREAKEVEN_TYPE type) { if(CheckPointer(this.obj) == POINTER_DYNAMIC) { delete this.obj; this.obj = NULL; } this.obj = CreateBreakEven(type); if(this.obj == NULL) { printf("%s: Critical error | The type %d is invalid.", __FUNCTION__, type); ExpertRemove(); return; } if((int)parameters[type].params.Size() < obj.GetNumParams()) { printf("%s: Error | The parameter array for %s is too small (%I32u elements)", __FUNCTION__, EnumToString(type), parameters[type].params.Size()); delete obj; obj = NULL; ExpertRemove(); return; } obj.Set(parameters[type].params); } //+------------------------------------------------------------------+
不同保本类型下订单块EA的测试
在最后一部分,我们将重点在策略测试器中测试三种保本模式。
首先配置参数。我们将使用基于 ATR 的止损和止盈。止损的 ATR 乘数设为 3.0,止盈设为 6.0,即 1:2 的风险回报比。交易时间设为 GMT-3 时区的 03:00 至 14:00。
设置

图 1:订单块EA回测设置
如图所示,EA将在黄金(XAUUSD)上测试,选择 M5 时间周期(5 分钟)。模型采用真实每笔 tick,杠杆 1:30,初始资金 10,000 美元。
不启用保本的初始回测
为便于清晰对比,第一次测试将不启用保本模式。

图 2:不启用保本的订单块EA回测图
开始回测前需要说明:所得结果仅对特定参数有效,因此给出的结论不能推广到所有策略或任意参数组合。但对于相近参数,可以做出近似评估。
例如,在下面的回测中,我们将测试固定点数保本。参数设置为:保本价格额外 100 点,保本触发 200 点。因此,本次回测得出的结论适用于保本触发约 200 点、保本额外偏移约 100 点的情况。
基于点数的保本回测
对于固定点数回测,我们选择保本触发 200 点,额外 100 点。所得结果如下。

图 3:固定点数保本下订单块EA的交易结果图
如图所示,出现了许多微利平仓的持仓序列。不出所料,这些都受到了保本机制的影响。此外,在这次固定点数保本回测中,2025 年 4 月 16 日至 4 月 29 日期间具有正浮动盈利的持仓都以无亏损平仓。相比之下,第一次回测中该时段录得亏损。
这可能表明基于点数的保本是一种更保守的方法,但它也会限制未来的利润。结果是,本次回测最终余额仅为 12,000 美元,而第一次回测最终余额约为 15,800 美元。这说明固定点数保本减少了亏损,但同时也显著限制了利润。
基于 ATR 的保本回测

图 4:基于 ATR 保本的订单块EA回测图
在第二次基于 ATR 保本的回测中,图表表现有所改善。首先,我们分析这种保本类型对盈利和亏损交易序列的影响。
在盈利序列中,这种保本不像固定点数保本那样激进。在亏损序列中,它适度限制了亏损。这在第一次大幅回撤序列中清晰可见 —— 该回撤从 2024 年初持续到 3 月。本次回测的资金曲线与第一次回测的曲线大体相似。
因此,我们可以得出结论:这种保本模式更为温和,或许不像固定点数保本那样激进,因为根据波动率的不同,其触发价格会根据 ATR 值或高或低。
基于 RRR 的保本回测

图 5:止损止盈比 1:2 时基于 RRR 保本的订单块EA交易结果图
从本次回测可以看出,在亏损序列中,特别是 2024 年 1 月和 2 月,不保本回测与基于 RRR 保本版本之间的差异很小。这些亏损是实质性亏损,即持仓从未出现正浮动盈利:开仓后价格随即触及止损,因此基于 RRR 的保本没有机会触发。
在盈利序列中,利润增长受到适度限制。例如,2024 年 7 月,第一次回测中几乎所有持仓都被止盈平仓,而使用基于 RRR 保本时,这波上涨序列基本停滞。这可能是因为EA在盘整区间交易,价格没有形成明确趋势,导致保本机制在明确方向形成前就触发并平仓。
我们可以得出结论:基于 RRR 的保本更为保守,因为价格需要运行更远的距离它才会触发。因此,它不是激进策略的最佳选择,但可能适用于追求高风险回报比(如 1:3 或 1:5)的波段类策略。为验证这一点,接下来我们进行风险回报比 1:3 的回测。

图 6:止损止盈比 1:3 时基于 RRR 保本的订单块EA交易结果图
新的回测结果比之前 1:2 比例的回测表现更好。主要变化是最低余额约为 9,500 美元,几乎与基于 ATR 保本回测的结果持平。此外,在第一次亏损序列(1 月至 2 月)中,亏损不像初始回测那样严重。
然而,4 月份出现了三种保本类型中最长的亏损序列:近五个月的亏损。这与黄金大约五个月的横盘走势相吻合。与第一次 RRR 保本回测相比,这可能表明持仓没有立即平仓,而是在正负浮动盈亏之间波动,因此止损和止盈的幅度一定很大。
问题在于,在这次新回测中,止损幅度缩小到原来的三分之一,因此持仓亏损平仓的概率增加了。这可能说明在横盘期间,持仓时间更长,平仓前在正负值之间波动。
结论
在本文中,我们完成了 MQL5 中基于 ATR 和 RRR 的保本机制实现。然后我们创建了一个类,允许在单个类中管理不同的保本类型,简化了模式之间的切换,无需为每个新模式重新输入参数。
最后,我们测试并分析了不同的保本类型。我们看到,使用指定参数的固定点数保本更为激进。相比之下,基于 RRR 的保本更为被动,但对利润的限制也更强。基于 ATR 的保本则介于这两者之间。
总结来说,需要指出:无法断言哪种保本类型最适合每个具体策略,因为这取决于诸多需要考虑的因素。这需要单独评估。不过作为建议,可以测试不同的保本类型,选择最适合所用策略的那一种。
因此,综合上述几次回测结果,我们可以认为基于风险回报比(RRR)的保本更适合预设风险回报比大于等于 1:3 的策略。这在第五次回测中得到了证实,其净利润几乎是第四次回测的三倍。
本文使用/更新的文件:
| 文件名 | 类型 | 说明 |
|---|---|---|
| Risk_Management.mqh | .mqh (头文件) | 包含风险管理系列的上一篇文章中开发的风险管理类。 |
| Order_Block_Indicador_New_Part_2.mq5 | .mq5 (指标) | 包含订单块指标的代码。 |
| Order Block EA MT5.mq5 | .mq5 (EA) | 集成保本机制的订单块EA统代码 |
| OB_SET_WITHOUT_BREAKEVEN.set | .set (配置文件) | 第一次回测的设置文件,未启用保本机制。 |
| OB_SET_BREAKEVEN_POINTS.set | .set (配置文件) | 第二次回测的设置文件,固定点数保本。 |
| OB_SET_BREAKEVEN_ATR.set | .set (配置文件) | 第三次回测的设置文件,基于 ATR 的保本。 |
| OB_SET_BREAKEVEN_RR_1_2.set | .set (配置文件) | 第四次回测的设置文件,基于 RRR 的保本,比例 1:2。 |
| OB_SET_BREAKEVEN_RR_1_3.set | .set (配置文件) | 第五次回测的设置文件,基于 RRR 的保本,比例 1:3。 |
| PositionManagement.mqh | .mqh (头文件) | 包含保本机制代码的 MQH 头文件。 |
本文由MetaQuotes Ltd译自西班牙语
原文地址: https://www.mql5.com/es/articles/18111
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
使用机器学习检测与分类分形模式
MQL5中的自优化智能交易系统(第十部分):矩阵分解
新手在交易中的10个基本错误
协方差矩阵自适应演化策略(CMA-ES)