开发多币种 EA 交易系统(第 14 部分):风险管理器的适应性交易量变化
概述
在本系列的前一篇文章中,我谈到了风险控制的主题,并开发了一个实现基本功能的风险管理器类。它允许设置最大每日损失水平和最大总损失水平,达到这两个水平时,交易停止,所有未结仓位被平仓。如果达到每日亏损限额,则第二天恢复交易;如果达到整体亏损限额,则根本不恢复交易。
大家可能还记得,风险管理器的可能发展领域是更平滑地改变仓位大小(例如,超过一半限额时减少两倍),以及更 "智能" 地恢复交易量(例如,只在损失超过仓位减少水平时才恢复交易量)。我们还可以添加最大目标利润参数,达到该参数后,交易也会停止。在个人账户上进行交易时,该参数不太可能有用。不过,在使用自营交易公司的账户进行交易时,对它的需求会很大。一旦达到计划的盈利水平,通常只能在另一个账户上继续交易。
我还提到,引入基于时间的交易限制是开发风险管理器的可能方向之一,但我不会在这里谈这个问题,而是留待将来再谈。现在,让我们尝试在风险管理器中实现自适应交易量变化,看看是否能从中受益。
基本情况
让我们使用上一篇文章中的 EA。我们将为其添加处理风险管理器参数的功能。我们还将对其进行一些小的补充,下面将对此进行讨论。现在,让我们就 EA 参数达成一致,我们将使用这些参数来评估所做修改的结果。
首先,让我们确定测试 EA 中使用的交易策略单个实例的具体组成。在 passes_ 参数中为三个优化交易品种和三个时间段中的每个交易品种设置第二优化阶段后最佳通过的 ID(共 9 个 ID)。每个 ID 将表示一个由 16 个单个交易策略实例组成的归一化组。因此,最后一组将包含总共 144 个交易策略实例,分为 9 组,每组 16 个策略。最后一组将不进行归一化处理,因为我们没有为其选择归一化因子。
其次,我们将使用 10,000 美元的固定交易余额和 10% 的标准预期最大回撤率作为缩放系数 1。我们将尝试在 1 到 10 的范围内改变缩放系数。与此同时,允许的最大回撤也将增加,但现在我们将通过风险经理进行额外控制,防止回撤超过 10%。
为此,我们打开风险管理器,将最大总损失值设定为 10,000 美元基础余额的 10%,即 1000 美元。对于最大每日损失,将数值设为二分之一,即 500 美元。随着余额的增长,这两个参数将保持不变。
让我们按照上述方法设置所有输入参数值:
图 1.使用原风险管理器为测试 EA 测试 EA 输入参数
让我们在 2021 年和 2022 年的区间内运行优化,看看在仓位大小的缩放系数(scale_) 取值不同的情况下,EA 是如何工作的。我们得到以下结果:
图 2.使用原有风险管理器对测试 EA 中的 scale_ 参数进行优化的结果
结果按 scale_ 参数值的升序排序,即结果行越低,EA 所使用的仓位大小就越大。很明显,从某个临界值开始,最终的结果是损失略高于 1000。
在测试期间,我们遇到了几次不同深度的回撤。在净值水平没有下降到 9000 美元以下的通过中,交易一直持续到测试期间结束。在资金回撤期间跌至 9000 美元以下的情况下,交易即刻停止,不再恢复。正是在这些通过中,我们损失了大约 1000 美元。其略高于计算值的原因很可能是我们只在新的分钟柱形图上使用了 EA 的运行模式。因此,从精确达到指定损失的那一刻起,到风险管理器检查并决定关闭所有仓位的那一刻为止,价格在一分钟内有时间多变化一点。
这些差异在大多数情况下都是微不足道的,我们可以通过同意在参数中设置一个稍低的限值,或者按计划改变风险管理器的工作方式来忽略它们。唯一值得关注的是 scale_ = 5.5 时,平仓后的损失比计算的损失超出 20%以上,约为 1234 美元。
为了进一步分析,让我们来看看第一个通过(scale_= 1.0)的余额和净值曲线图。由于其余各次的差异仅在于开启仓位的大小,因此其余额和净值图表的外观与第一次的相同。唯一不同的是,它们在垂直方向上会被拉伸得更长。
图 3. 在使用原有风险管理器的测试 EA 中,当 scale_ = 1.0 时的通过结果
让我们把它们与没有风险管理器的结果进行比较:
图 4. 在无风险管理器的测试 EA 中,当 scale_ = 1.0 时的通过结果
在没有风险管理器的情况下,总体利润要高出几个百分点,而净值的最大回撤率保持不变。这表明,在将这些策略组合在一起时对其进行归一化处理,以及随后的联合工作都能产生良好的效果:两年中,风险管理器仅有三次在超过每日损失时不得不平仓。
现在让我们来看看使用风险管理器和 scale_ = 3 时的计算结果。利润增加了约 50%,但回撤也增加了三倍。
图 5. 在使用原有风险管理器的测试 EA 中,当 scale_ = 3.0 时的通过结果
然而,尽管回撤的绝对值增至近 3000 美元,但风险管理器必须防止每天回撤超过 500 美元。换句话说,有连续几天,风险管理器关闭了所有仓位,并在第二天开始时重新开仓。从之前的最高净值开始,当前金额最终跌至 3000 美元,但相对于当日开始时的最高净值或资金,单日回撤不超过 500 美元。但是,扩大仓位大小是危险的,因为总损失也是有限度的。在这种情况下,我们很幸运,大回撤发生在测试期开始后的一段时间。余额有时间增长,从而增加了最大总损失的价值,开始时为 1000 美元,从初始余额值开始计算。如果测试期是在回撤达到 3000 美元之前开始的,那么就会超过总限额,交易就会停止。
为了考虑到可能的错误开始时间的影响,我们将改变风险管理器的参数,使总体损失水平不是从初始余额开始计算,而是从余额或净值的最后最大值开始计算。但要做到这一点,我们首先需要对风险管理器代码进行补充,因为设置所需参数值的功能尚未实现。
CVirtualRiskManager 升级
我们计划对风险管理器类进行一些调整,其中许多需要对相同的方法进行修改。这使得描述起来有些困难,因为很难将与不同添加特征相关的编辑分开。因此,我们将从最简单的段落开始描述已经完成的代码版本。
更新枚举
在类描述之前,我们声明了几个稍后使用的枚举。我们将稍微扩展这些枚举的组成。例如,包含风险管理器可能状态的 ENUM_RM_STATE 枚举就新增了两种状态:
- RM_STATE_RESTORE - 新的每日周期开始后出现的状态,直至未结仓位的大小完全恢复。以前不会出现这种情况,因为我们会在新的一天开始后立即恢复仓位大小。现在,我们可以选择不立即这样做,而是在价格恢复到对开仓更有利的值后再这样做。我们稍后再详细讨论这个问题。
- RM_STATE_OVERALL_PROFIT - 达到指定利润后出现的状态。此事件发生后,交易停止。
我们将之前的枚举 ENUM_RM_CALC_LIMIT 变成了三个独立的枚举:分别用于每日损失、总体损失和总体利润。这些枚举的值将用于确定两件事:
- 如何使用参数中传递的数字:绝对值或相对值,指定为每日水平或余额或净值最高值的百分比;
- 从(到)阈值水平开始计算的值 - 每日水平或最高余额或资金值。
这些选项已在枚举值的注释中注明。
// Possible risk manager states enum ENUM_RM_STATE { RM_STATE_OK, // Limits are not exceeded RM_STATE_DAILY_LOSS, // Daily limit is exceeded RM_STATE_RESTORE, // Recovery after daily limit RM_STATE_OVERALL_LOSS, // Overall limit exceeded RM_STATE_OVERALL_PROFIT // Overall profit reached }; // Possible methods for calculating limits enum ENUM_RM_CALC_DAILY_LOSS { RM_CALC_DAILY_LOSS_MONEY_BB, // [$] to Daily Level RM_CALC_DAILY_LOSS_PERCENT_BB, // [%] from Base Balance to Daily Level RM_CALC_DAILY_LOSS_PERCENT_DL // [%] from/to Daily Level }; // Possible methods for calculating general limits enum ENUM_RM_CALC_OVERALL_LOSS { RM_CALC_OVERALL_LOSS_MONEY_BB, // [$] to Base Balance RM_CALC_OVERALL_LOSS_MONEY_HW_BAL, // [$] to HW Balance RM_CALC_OVERALL_LOSS_MONEY_HW_EQ_BAL, // [$] to HW Equity or Balance RM_CALC_OVERALL_LOSS_PERCENT_BB, // [%] from/to Base Balance RM_CALC_OVERALL_LOSS_PERCENT_HW_BAL, // [%] from/to HW Balance RM_CALC_OVERALL_LOSS_PERCENT_HW_EQ_BAL // [%] from/to HW Equity or Balance }; // Possible methods for calculating overall profit enum ENUM_RM_CALC_OVERALL_PROFIT { RM_CALC_OVERALL_PROFIT_MONEY_BB, // [$] to Base Balance RM_CALC_OVERALL_PROFIT_PERCENT_BB, // [%] from/to Base Balance };
类说明
我们在 CVirtualRiskManager 类描述的受保护部分添加了新的属性和方法。在公有部分,一切保持不变。添加的代码以绿色高亮显示:
//+------------------------------------------------------------------+ //| Risk management class (risk manager) | //+------------------------------------------------------------------+ class CVirtualRiskManager : public CFactorable { protected: // Main constructor parameters bool m_isActive; // Is the risk manager active? double m_baseBalance; // Base balance ENUM_RM_CALC_DAILY_LOSS m_calcDailyLossLimit; // Method of calculating the maximum daily loss double m_maxDailyLossLimit; // Parameter of calculating the maximum daily loss double m_closeDailyPart; // Threshold part of the daily loss ENUM_RM_CALC_OVERALL_LOSS m_calcOverallLossLimit; // Method of calculating the maximum overall loss double m_maxOverallLossLimit; // Parameter of calculating the maximum overall loss double m_closeOverallPart; // Threshold part of the overall loss ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Method for calculating maximum overall profit double m_maxOverallProfitLimit; // Parameter for calculating the maximum overall profit double m_maxRestoreTime; // Waiting time for the best entry on a drawdown double m_lastVirtualProfitFactor; // Initial best drawdown multiplier // Current state ENUM_RM_STATE m_state; // State double m_lastVirtualProfit; // Profit of open virtual positions at the moment of loss limit datetime m_startRestoreTime; // Start time of restoring the size of open positions // Updated values double m_balance; // Current balance double m_equity; // Current equity double m_profit; // Current profit double m_dailyProfit; // Daily profit double m_overallProfit; // Overall profit double m_baseDailyBalance; // Daily basic balance double m_baseDailyEquity; // Daily base balance double m_baseDailyLevel; // Daily base level double m_baseHWBalance; // balance High Watermark double m_baseHWEquityBalance; // equity or balance High Watermark double m_virtualProfit; // Profit of open virtual positions // Managing the size of open positions double m_baseDepoPart; // Used (original) part of the overall balance double m_dailyDepoPart; // Multiplier of the used part of the overall balance by daily loss double m_overallDepoPart; // Multiplier of the used part of the overall balance by overall loss // Protected methods double DailyLoss(); // Maximum daily loss double OverallLoss(); // Maximum overall loss void UpdateProfit(); // Update current profit values void UpdateBaseLevels(); // Updating daily base levels void CheckLimits(); // Check for excess of permissible losses bool CheckDailyLossLimit(); // Check for excess of the permissible daily loss bool CheckOverallLossLimit(); // Check for excess of the permissible overall loss bool CheckOverallProfitLimit(); // Check if the specified profit has been achieved void CheckRestore(); // Check the need for restoring the size of open positions bool CheckDailyRestore(); // Check if the daily multiplier needs to be restored bool CheckOverallRestore(); // Check if the overall multiplier needs to be restored double VirtualProfit(); // Determine the profit of open virtual positions double RestoreVirtualProfit(); // Determine the profit of open virtual positions to restore void SetDepoPart(); // Set the values of the used part of the overall balance public: ... };
通过 m_closeDailyPart 和 m_closeOverallPart 属性,我们可以更平滑地改变仓位大小。它们的用法相似,唯一的区别在于每个属性指的是哪种限制(每日限制还是一般限制)。例如,如果我们设置 m_closeDailyPart = 0.5,那么当损失达到每日限额的一半时,仓位大小将减半。如果亏损继续扩大,达到每日限额剩余一半的一半,那么仓位大小(之前已经减半)将再次减半。
将通过更改 m_dailyDepoPart 和 m_overallDepoPart 属性来减少仓位大小。它们的值用于确定交易总余额中已使用部分的方法。它们作为乘数被纳入等式,因此将任何一个乘数减半都会导致总交易量减半:
//+------------------------------------------------------------------+ //| Set the value of the used part of the overall balance | //+------------------------------------------------------------------+ void CVirtualRiskManager::SetDepoPart() { CMoney::DepoPart(m_baseDepoPart * m_dailyDepoPart * m_overallDepoPart); }
函数中使用的 m_baseDepoPart 属性包含交易所用余额部分的原始大小值。
m_maxRestoreTime 和 m_lastVirtualProfitFactor 属性用于确定恢复未结仓位大小的可能性。
第一项属性规定了以分钟为单位的时间,即使虚拟利润为非负值,也会在该时间后恢复大小。也就是说,在此之后,EA 将再次打开与虚拟仓位相对应的真实市场仓位,即使在真实仓位关闭期间,价格朝着正确的方向发展,虚拟利润变得比达到限额时更大。在此之前,只有当虚拟仓位的估计利润小于随时间变化的某个估计值时,才会发生交易量恢复。
第二个属性指定了一个乘数,用于确定在新的每日时段开始时虚拟仓位的损失应大于达到限制时虚拟仓位损失的次数,以便立即恢复仓位大小。例如,此参数的值为 1 意味着,只有当虚拟仓位与达到限制的最后一刻相比保持相同的跌幅或进入更深的跌幅时,才会在一天开始时恢复仓位大小。
此外,值得注意的是,现在不仅当回撤量超过既定限额时,而且例如当回撤量达到每日限额的一半(如果 m_closeDailyPart 为 0.5)时,达到限额也被视为触发。
计算虚拟仓位的利润
在应用部分平仓和随后恢复未平仓仓位的大小时,我们需要正确确定,如果按照策略开启的所有仓位仍然未平仓,此刻的浮动盈亏会是多少。因此,风险管理器类中增加了计算未结虚拟仓位当前利润的方法。当达到回撤限制时,它们不会被关闭,因此我们可以随时确定与虚拟仓位相对应的开启市场仓位的大致利润。由于缺乏对佣金和库存费的计算,这一计算值与实际情况并不完全一致,但这一精确度对于我们的目的来说已经足够了。
上一次,我们不需要用这种方法进行任何计算,计算结果将决定进一步的行动,而现在我们需要它。此外,为了正确计算虚拟仓位的利润,我们还应该对其进行一些小的补充。问题在于,用于确定一个虚拟仓位利润的方法 CMoney::Profit() 采用了余额使用乘数 CMoney::DepoPart() 进行计算。如果我们减小了这一乘数,那么虚拟利润的计算将根据缩小后的仓位大小进行,而不是根据缩小前的仓位大小进行。
我们感兴趣的是初始仓位大小的利润,因此在计算之前,我们会暂时返回初始余额利用率乘数。之后,我们计算虚拟仓位的利润,然后通过调用 SetDepoPart() 方法 再次设置当前余额使用乘数:
//+------------------------------------------------------------------+ //| Determine the profit of open virtual positions | //+------------------------------------------------------------------+ double CVirtualRiskManager::VirtualProfit() { // Access the receiver object CVirtualReceiver *m_receiver = CVirtualReceiver::Instance(); // Set the initial balance usage multiplier CMoney::DepoPart(m_baseDepoPart); double profit = 0; // Find the profit sum for all virtual positions FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i))); // Restore the current balance usage multiplier SetDepoPart(); return profit; }
高净值
为了更好地分析,我们希望增加计算最大损失水平的能力,不仅可以根据初始账户余额,还可以根据例如上次达到的余额最大值。因此,我们将 m_baseHWBalance 和 m_baseHWEquityBalance 添加到应不断更新的属性列表中。在 UpdateProfit() 方法中,我们添加了包含它们的计算,并检查了整体利润是相对于余额或净值的最高值计算的,而不是相对于基础余额计算的:
//+------------------------------------------------------------------+ //| Updating current profit values | //+------------------------------------------------------------------+ void CVirtualRiskManager::UpdateProfit() { // Current equity m_equity = AccountInfoDouble(ACCOUNT_EQUITY); // Current balance m_balance = AccountInfoDouble(ACCOUNT_BALANCE); // Maximum balance (High Watermark) m_baseHWBalance = MathMax(m_balance, m_baseHWBalance); // Maximum balance or equity (High Watermark) m_baseHWEquityBalance = MathMax(m_equity, MathMax(m_balance, m_baseHWEquityBalance)); // Current profit m_profit = m_equity - m_balance; // Current daily profit relative to the daily level m_dailyProfit = m_equity - m_baseDailyLevel; // Current overall profit relative to base balance m_overallProfit = m_equity - m_baseBalance; // If we take the overall profit relative to the highest balance, if(m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_MONEY_HW_BAL || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_BAL) { // Recalculate it m_overallProfit = m_equity - m_baseHWBalance; } // If we take the overall profit relative to the highest balance or equity, if(m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_MONEY_HW_EQ_BAL || m_calcOverallLossLimit == RM_CALC_OVERALL_LOSS_PERCENT_HW_EQ_BAL) { // Recalculate it m_overallProfit = m_equity - m_baseHWEquityBalance; } // Current profit of virtual open positions m_virtualProfit = VirtualProfit(); ... }
限额检查方法
这个方法也发生了变化:我们将它的代码拆分为几个辅助方法,所以现在它看起来像这样:
//+------------------------------------------------------------------+ //| Check loss limits | //+------------------------------------------------------------------+ void CVirtualRiskManager::CheckLimits() { if(false || CheckDailyLossLimit() // Check daily limit || CheckOverallLossLimit() // Check overall limit || CheckOverallProfitLimit() // Check overall profit ) { // Remember the current level of virtual profit m_lastVirtualProfit = m_virtualProfit; // Notify the recipient about changes CVirtualReceiver::Instance().Changed(); } }
在检查每日损失限额的方法中,我们首先检查是否已达到每日限额或通过 m_closeDailyPart 参数定义的每日限额的指定部分。如果是这样,那么我们就将总余额中已使用部分的乘数减去每日损失。如果它已经变得太小,那么我们就将其完全重置。在此之后,我们设置了总余额中已使用部分的值,并将风险管理器切换到已实现每日损失的状态。
//+------------------------------------------------------------------+ //| Check daily loss limit | //+------------------------------------------------------------------+ bool CVirtualRiskManager::CheckDailyLossLimit() { // If daily loss is reached and positions are still open if(m_dailyProfit < -DailyLoss() * (1 - m_dailyDepoPart * (1 - m_closeDailyPart)) && CMoney::DepoPart() > 0) { // Reduce the multiplier of the used part of the overall balance by the daily loss m_dailyDepoPart *= (1 - m_closeDailyPart); // If the multiplier is already too small, if(m_dailyDepoPart < 0.05) { // Set it to 0 m_dailyDepoPart = 0; } // Set the value of the used part of the overall balance SetDepoPart(); ... // Set the risk manager to the achieved daily loss state m_state = RM_STATE_DAILY_LOSS; return true; } return false; }
检查总损失限额的方法与此类似,唯一不同的是,只有当总余额中已使用部分的总损失乘数等于零时,风险管理器才会切换到已实现的总损失状态:
//+------------------------------------------------------------------+ //| Check the overall loss limit | //+------------------------------------------------------------------+ bool CVirtualRiskManager::CheckOverallLossLimit() { // If overall loss is reached and positions are still open if(m_overallProfit < -OverallLoss() * (1 - m_overallDepoPart * (1 - m_closeOverallPart)) && CMoney::DepoPart() > 0) { // Reduce the multiplier of the used part of the overall balance by the overall loss m_overallDepoPart *= (1 - m_closeOverallPart); // If the multiplier is already too small, if(m_overallDepoPart < 0.05) { // Set it to 0 m_overallDepoPart = 0; // Set the risk manager to the achieved overall loss state m_state = RM_STATE_OVERALL_LOSS; } // Set the value of the used part of the overall balance SetDepoPart(); ... return true; } return false; }
检查指定利润是否已实现看起来更加简单:当利润达到时,我们重置相应的乘数,并将风险管理器设置为已达到总体利润的状态:
//+------------------------------------------------------------------+ //| Check if the specified profit has been achieved | //+------------------------------------------------------------------+ bool CVirtualRiskManager::CheckOverallProfitLimit() { // If overall loss is reached and positions are still open if(m_overallProfit > m_maxOverallProfitLimit && CMoney::DepoPart() > 0) { // Reduce the multiplier of the used part of the overall balance by the overall loss m_overallDepoPart = 0; // Set the risk manager to the achieved overall profit state m_state = RM_STATE_OVERALL_PROFIT; // Set the value of the used part of the overall balance SetDepoPart(); ... return true; } return false; }
这三种方法都会返回一个布尔值,用来回答以下问题:是否已达到相应的限制?如果是,我们会在 CheckLimits() 方法中保存当前的虚拟利润水平,并通知市场仓位的接收者有关仓位组成的变化。
在每日基线更新方法中,我们增加了对每日利润的重新计算,并在之前达到每日损失上限时切换到恢复状态:
//+------------------------------------------------------------------+ //| Update daily base levels | //+------------------------------------------------------------------+ void CVirtualRiskManager::UpdateBaseLevels() { // Update balance, funds and base daily level m_baseDailyBalance = m_balance; m_baseDailyEquity = m_equity; m_baseDailyLevel = MathMax(m_baseDailyBalance, m_baseDailyEquity); m_dailyProfit = m_equity - m_baseDailyLevel; ... // If the daily loss level was reached earlier, then if(m_state == RM_STATE_DAILY_LOSS) { // Switch to the state of restoring the sizes of open positions m_state = RM_STATE_RESTORE; // Remember restoration start time m_startRestoreTime = TimeCurrent(); } }
恢复仓位大小
我们还将以前执行这项任务的方法分为几个方法。检查是否需要恢复的方法最先调用:
//+------------------------------------------------------------------+ //| Check the need for restoring the size of open positions | //+------------------------------------------------------------------+ void CVirtualRiskManager::CheckRestore() { // If we need to restore the state to normal, then if(m_state == RM_STATE_RESTORE) { // Check the possibility of restoring the daily loss multiplier to normal bool dailyRes = CheckDailyRestore(); // Check the possibility of restoring the overall loss multiplier to normal bool overallRes = CheckOverallRestore(); // If at least one of them has recovered, if(dailyRes || overallRes) { ... // Set the value of the used part of the overall balance SetDepoPart(); // Notify the recipient about changes CVirtualReceiver::Instance().Changed(); // If both multipliers are restored to normal, if(dailyRes && overallRes) { // Set normal state m_state = RM_STATE_OK; } } } }
检查是否应恢复日乘数和总乘数的辅助方法目前的工作方式相同,但将来我们可能会改变它们的行为。目前,它们会检查当前的虚拟利润值是否低于预期水平。如果是这样,那么以当前价格重开真实仓位对我们有利,因此我们需要恢复其大小:
//+------------------------------------------------------------------+ //| Check if the daily multiplier needs to be restored | //+------------------------------------------------------------------+ bool CVirtualRiskManager::CheckDailyRestore() { // If the current virtual profit is less than the one desired for recovery, if(m_virtualProfit <= RestoreVirtualProfit()) { // Restore the daily loss multiplier m_dailyDepoPart = 1.0; return true; } return false; } //+------------------------------------------------------------------+ //| Check if the overall multiplier needs to be restored | //+------------------------------------------------------------------+ bool CVirtualRiskManager::CheckOverallRestore() { // If the current virtual profit is less than the one desired for recovery, if(m_virtualProfit <= RestoreVirtualProfit()) { // Restore the overall loss multiplier m_overallDepoPart = 1.0; return true; } return false; }
RestoreVirtualProfit() 方法计算出所需的虚拟利润水平。我们使用了一个简单的线性插值,有两个参数:时间流逝得越多,我们同意重新开放实际仓位的水平就越低。
//+------------------------------------------------------------------+ //| Determine the profit of virtual positions for recovery | //+------------------------------------------------------------------+ double CVirtualRiskManager::RestoreVirtualProfit() { // If the maximum recovery time is not specified, if(m_maxRestoreTime == 0) { // Return the current value of the virtual profit return m_virtualProfit; } // Find the elapsed time since the start of recovery in minutes double t = (TimeCurrent() - m_startRestoreTime) / 60.0; // Return the calculated value of the desired virtual profit // depending on the time elapsed since the start of recovery return m_lastVirtualProfit * m_lastVirtualProfitFactor * (1 - t / m_maxRestoreTime); }
在当前文件夹的 VirtualRiskManager.mqh 文件中保存更改。
测试
首先,让我们使用参数启动 EA,这些参数指定在达到每日限额和总限额时完全平仓。结果应与图 3 相同。
图 6. scale_ = 1.0 的 EA 结果,设置与原始风险管理器相同
回撤的结果完全一致,而利润和归一化年均利润(OnTester 结果)的结果则略高于预期。令人欣慰的是:我们没有破坏以前实现的内容。
现在让我们看看,如果我们将达到部分日限额和总限额时的自适应平仓值设置为 0.5,会发生什么情况:
图 7. 在 scale_ = 1.0、rmCloseDailyPart_ = 0.5 和 rmCloseOverallPart_ = 0.5 条件下的 EA 结果
回撤幅度略有收窄,导致归一化年平均利润略有增加。现在,让我们尝试以最佳价格关联恢复仓位大小。让我们在参数中将回撤期间最佳入场的等待时间设置为 1440 分钟(1 天),并将初始最佳回撤乘数设置为 1.5。这些只是凭直觉得出的值。
图 8. EA 计算结果,参数为:scale_ = 1.0、rmCloseDailyPart_ = 0.5、rmCloseOverallPart_ = 0.5、rmMaxRestoreTime_ = 1440 以及 rmLastVirtualProfitFactor_ = 1.5
归一化后的年均利润再次提高了几个百分点。看来,实现这一机制的努力是合理的。
现在,让我们尝试在不更改风险管理器参数的情况下,将已开仓仓位的大小扩大三倍。这应导致风险管理器被更频繁地触发。让我们看看它是如何完成任务的。
图 9. EA 计算结果,参数为:scale_ = 3.0、rmCloseDailyPart_ = 0.5、rmCloseOverallPart_ = 0.5、rmMaxRestoreTime_ = 1440 和 rmLastVirtualProfitFactor_ = 1.5
与最初推出的类似风险管理器相比,我们再次看到所有指标的结果都有所改善。与之前的运行结果相比,利润增加了约 2 倍,但回撤增加了 3 倍,从而降低了归一化年均利润值。不过,在每个交易日,回撤率仍未超过风险管理器参数中设定的值。
同样有趣的是,如果开启仓位的大小进一步扩大,比如说扩大到 10 倍,会发生什么情况?
图 10. EA 计算结果,参数为:scale_ = 3.0、rmCloseDailyPart_ = 0.5、rmCloseOverallPart_ = 0.5、rmMaxRestoreTime_ = 1440 和 rmLastVirtualProfitFactor_ = 1.5
我们可以看到,在这样的仓位大小下,风险管理器已无法保持符合规定的最大总损失,因此停止了交易。这意味着风险管理器并不是一个无论交易策略有什么古怪,它都能让你防止出现指定最大回撤的通用工具。它只是对交易策略中应包含的强制性资金管理规则的补充。
最后,让我们尝试利用遗传优化为风险管理器选择最佳参数。遗憾的是,每次间隔两年的通过,大约需要三分钟,因此我们需要等待相当长的时间才能完成优化。作为优化标准,我们将选择一个计算归一化平均年利润的用户标准。一段时间后,显示优化结果的表格顶部看起来像这样:
图 11.与风险管理器一起优化 EA 的结果
我们可以看到,选择好的风险管理器参数不仅能保护交易免受回撤增加的影响,还能改善交易结果:不同通过的差异相当于额外利润的 20%。尽管即使是已经找到的最佳参数集也略低于图 8 中按直觉选择的值所获得的结果。进一步的优化可能会改善这一结果,但并没有特别的需要。
结论
让我们来简单总结一下。我们改进了任何成功交易系统的一个重要组成部分 - 风险管理器,使其更加灵活,并可根据具体需求进行定制。我们现在有了一个机制,可以更准确地遵守可接受回撤的既定限制。
尽管我们努力使其尽可能可靠,但应明智地使用其能力。值得记住的是,这是一种保护资本的极端手段,因此您需要尝试选择交易参数,以便风险管理器根本不必干预交易,或者在极少数情况下这样做。正如我们从测试中看到的那样,当大大超过推荐的仓位大小时,净值变化率可能会非常剧烈,以至于即使是风险管理器也可能没有时间将资金从超过指定的回撤水平中保存下来。
在进行风险管理器更新的过程中,出现了改进它的新想法。然而,使其过于复杂也不是一件好事。因此,我将暂时放下风险管理器的工作,在以下部分中回到更紧迫的 EA 开发问题上。
感谢您的关注!期待很快与您见面!
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/15085