
开发多币种 EA 交易(第 12 部分):开发自营交易级别风险管理器
概述
在整个系列中,我们多次谈到了风险控制这一主题。引入了规范化交易策略的概念,其参数可确保在测试期间达到 10%的回撤水平。然而,以这种方式对交易策略实例以及交易策略组进行归一化处理,只能提供一个历史时期内的给定回撤率。我们无法确定,在前向时期开始测试规范化策略组或在交易账户上启动测试时,是否能观察到指定的回撤水平。
最近,人工交易的风险管理和算法交易的风险管理两篇文章对风险管理这一主题进行了探讨。在这些文章中,作者提出了一种编程式实现方法,可控制各种交易参数是否符合预设指标。例如,如果超过了一天、一周或一个月的设定损失水平,交易就会暂停。
从自营公司吸取一些教训一文也很有意思。作者研究了自营交易公司对希望获得管理资本的交易者提出的交易要求。尽管在各种专门用于交易的资源上可以找到对此类公司活动的模糊态度,但使用明确的风险管理规则是成功交易的最重要组成部分之一。因此,以自营交易公司使用的风险控制模型为基础,利用已经积累的经验,实施我们自己的风险管理器是合理的。
模式和概念
对于风险管理器来说,以下概念会对我们有所帮助:
- 基础余额 - 可用来计算其余参数值的初始账户余额(或部分账户余额)。在这里,我们将使用 10,000 这个值。
- 每日基础余额 - 当前每日时段开始时的交易账户余额。为简单起见,我们假设日线周期的开始时间与终端中 D1 时间框架出现新柱形的时间相吻合。
- 每日基础资金是指交易账户在当前每日时段开始时的资金量。
- 每日水平是每日基础余额和资金的最大值。该值在每日时段开始时确定,并在下一个每日时段开始时保持不变。
- 最大每日亏损 - 账户资金每日水平的向下偏差量,在该水平下,应在当前每日时段停止交易。 交易将在下一个每日时段恢复。止损可以理解为旨在缩小未平仓头寸规模直至完全平仓的各种行动。首先,我们将使用这个简单的模型:当达到每日最大损失时,所有未平仓的市场仓位都将被关闭。
- 最大总损失 - 账户资金与基础余额值的向下偏差,在此情况下交易完全停止(在接下来的时间内不会恢复)。当达到这一水平时,所有未关闭的仓位都会关闭。
我们将限制自己只在两个级别停止交易:每日和总计。也可以用类似的方法添加每周或每月级别。但由于自营交易公司没有这些功能,因此我们不会将风险管理器的首次实现复杂化。如有必要,可在以后添加。
不同的自营交易公司计算每日最大损失和总损失的方法可能略有不同。因此,我们将在风险管理器中提供三种可能的方法来设定计算最大损失的数值:
- 固定存款货币。在这里,我们直接在参数中传递以交易账户货币单位表示的损失值。我们将其设置为正数。
- 占基础余额的百分比。在这种情况下,该值被视为既定基础余额的一个百分比。由于我们模型中的基础余额是一个常数值(直到账户和 EA 以手动设置的不同基础余额值重新启动),因此用这种方法计算出的最大损失也将是一个常数值。我们可以将这种情况简化为第一种情况,但由于通常情况下是以最大损失的百分比来表示的,所以我们将其作为一个单独的情况。
- 占每日水平的百分比。在此选项中,在每天开始时,我们会重新计算最大损失额,它等于刚刚计算出的每日水平的一个指定百分比。随着余额或资金的增加,最大损失额也会增加。这种方法主要用于计算每天的最大损失。相对于基础余额而言,最大总损失通常是固定的。
让我们一如既往地遵循 "最少行动" 原则,开始实现我们的风险管理类。让我们首先进行最起码的必要实现,并为必要时的进一步复杂化做好准备。
CVirtualRiskManager 类
该类的发展经历了几个阶段。起初,它是一个完全静态的对象,因此可以在所有对象中自由使用。然后我们想到,我们还可以优化风险管理器参数,如果能够将它们保存为初始化字符串,那就太好了。为此,该类成为CFactorable 类的后代。以单例模式实现是为了确保在不同类的对象中使用风险管理器的能力。但后来发现,只有 CVirtualAdvisor EA 类才需要风险管理器。因此,我们从风险管理器类中删除了单例模式的实现。
首先,让我们为可能的风险管理器状态和可能的限额计算方法创建枚举:
// 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_OVERALL_LOSS // Overall limit is exceeded }; // Possible methods for calculating limits enum ENUM_RM_CALC_LIMIT { RM_CALC_LIMIT_FIXED, // Fixed (USD) RM_CALC_LIMIT_FIXED_PERCENT, // Fixed (% from Base Balance) RM_CALC_LIMIT_PERCENT // Relative (% from Daily Level) };
在风险管理器类的描述中,我们将使用几个属性来存储通过初始化字符串传递给构造函数的输入参数。我们还将添加用于存储各种计算特征(当前余额、资金、利润等)的属性。让我们在受保护部分声明一些辅助方法。在公有部分,我们基本上只有一个构造函数和一个处理每个分时报价的方法。我们现在只提及保存/加载方法和字符串转换操作符,稍后再编写实现方法。
然后类描述看起来像这样:
//+------------------------------------------------------------------+ //| 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_LIMIT m_calcDailyLossLimit; // Method of calculating the maximum daily loss double m_maxDailyLossLimit; // Parameter of calculating the maximum daily loss ENUM_RM_CALC_LIMIT m_calcOverallLossLimit;// Method of calculating the total daily loss double m_maxOverallLossLimit; // Parameter of calculating the maximum total loss // Current state ENUM_RM_STATE m_state; // 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; // Total profit double m_baseDailyBalance; // Daily basic balance double m_baseDailyEquity; // Daily base balance double m_baseDailyLevel; // Daily base level double m_virtualProfit; // Profit of open virtual positions // Managing the size of open positions double m_prevDepoPart; // Used part of the total balance // Protected methods double DailyLoss(); // Maximum daily loss double OverallLoss(); // Maximum total loss void UpdateProfit(); // Update current profit values void UpdateBaseLevels(); // Updating daily base levels void CheckLimits(); // Check for excess of permissible losses void CheckDailyLimit(); // Check for excess of the permissible daily loss void CheckOverallLimit(); // Check for excess of the permissible total loss double VirtualProfit(); // Determine the real size of the virtual position public: CVirtualRiskManager(string p_params); // Constructor virtual void Tick(); // Tick processing in risk manager virtual bool Load(const int f); // Load status virtual bool Save(const int f); // Save status virtual string operator~() override; // Convert object to string };
风险管理器对象的构造函数希望初始化字符串包含六个数值,这些数值在转换为适当的数据类型后,将分配给对象的主要属性。此外,在创建时,我们将状态设置为正常(未超出限制)。如果在一天中的某个时间段重新启动 EA 时重新创建了对象,则在加载保存的信息时,应将状态更正为上次保存时的状态。这同样适用于设置账户余额中分配用于交易的份额 - 构造函数中设置的值可以在加载已保存的风险管理信息时预先定义。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualRiskManager::CVirtualRiskManager(string p_params) { // Save the initialization string m_params = p_params; // Read the initialization string and set the property values m_isActive = (bool) ReadLong(p_params); m_baseBalance = ReadDouble(p_params); m_calcDailyLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params); m_maxDailyLossLimit = ReadDouble(p_params); m_calcOverallLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params); m_maxOverallLossLimit = ReadDouble(p_params); // Set the state: Limits are not exceeded m_state = RM_STATE_OK; // Remember the share of the account balance allocated for trading m_prevDepoPart = CMoney::DepoPart(); // Update base daily levels UpdateBaseLevels(); // Adjust the base balance if it is not set if(m_baseBalance == 0) { m_baseBalance = m_balance; } }
风险管理器将在事件处理程序中对每个分时报价执行主要工作。这将涉及检查风险管理器的活动,如果活动,必要时更新当前利润值和基础每日水平,以及检查是否超过损失限额:
//+------------------------------------------------------------------+ //| Tick processing in the risk manager | //+------------------------------------------------------------------+ void CVirtualRiskManager::Tick() { // If the risk manager is inactive, exit if(!m_isActive) { return; } // Update the current profit values UpdateProfit(); // If a new daily period has begun, then we update the base daily levels if(IsNewBar(Symbol(), PERIOD_D1)) { UpdateBaseLevels(); } // Check for exceeding loss limits CheckLimits(); }
我们还想单独指出一点,由于开发了涉及虚拟仓位的结构(交易量的接收者会将虚拟仓位转化为真实市场仓位)和资本管理模块(允许我们在虚拟仓位和真实仓位的规模之间设置所需的比例系数),我们可以非常容易地实现市场仓位的安全平仓,而不会违反工作策略的交易逻辑。为此,只需将资本管理模块中的缩放系数设置为 0:
CMoney::DepoPart(0); // Set the used portion of the total balance to 0
如果在此之前,我们在 m_prevDepoPart 属性中记住了之前的比率,那么在新的一天到来且每日限额更新后,我们只需将该比率恢复为之前的值,即可恢复之前关闭的真实仓位:
CMoney::DepoPart(m_prevDepoPart); // Return the used portion of the total balance
当然,与此同时,我们也无法预先知道仓位会以更差还是更好的价格重新开启。但我们可以肯定的是,增加风险管理器并不会影响所有交易策略的整体表现。
现在,让我们来看看风险管理器类的其余方法。
在 UpdateProfits() 方法中,我们更新余额、资金和利润的当前值,并将当前资金与每日水平之差作为每日利润进行计算。需要注意的是,该值并不总是与当前利润一致。如果某些交易在新的每日时段开始后已经关闭,则会出现差异。我们以当前资金与基础余额之间的差额来计算总损失。
//+------------------------------------------------------------------+ //| Updating current profit values | //+------------------------------------------------------------------+ void CVirtualRiskManager::UpdateProfit() { m_equity = AccountInfoDouble(ACCOUNT_EQUITY); m_balance = AccountInfoDouble(ACCOUNT_BALANCE); m_profit = m_equity - m_balance; m_dailyProfit = m_equity - m_baseDailyLevel; m_overallProfit = m_equity - m_baseBalance; m_virtualProfit = VirtualProfit(); if(IsNewBar(Symbol(), PERIOD_H1) && PositionsTotal() > 0) { PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f", m_virtualProfit, m_profit, m_dailyProfit); } }
在这个方法中,我们还计算所谓的当前虚拟利润。它是根据开启的虚拟仓位计算出来的。如果我们在触发风险管理限制时让虚拟仓位处于未平仓状态,那么即使在没有真实未平仓仓位的情况下,我们也可以随时估算出如果风险管理器关闭的真实仓位保持未平仓状态下的大致利润。遗憾的是,这一计算参数并不能给出完全准确的结果(有几个百分点的误差)。不过,它还是有用的。
VirtualProfit() 方法计算当前的虚拟利润。在这个方法中,我们会得到一个指向虚拟交易量接收器对象的指针,因为我们需要从中找出虚拟仓位的总数,并能够访问每个虚拟仓位。然后,我们循环查看所有虚拟仓位,要求资金管理模块计算每个仓位的虚拟利润,并根据当前的交易资金进行缩放:
//+------------------------------------------------------------------+ //| Determine the profit of open virtual positions | //+------------------------------------------------------------------+ double CVirtualRiskManager::VirtualProfit() { // Access the receiver object CVirtualReceiver *m_receiver = CVirtualReceiver::Instance(); double profit = 0; // Find the profit sum for all virtual positions FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i))); return profit; }
在这个方法中,我们使用了一个新的宏 FORI,下面将对此进行讨论。
当新的每日周期开始时,我们将重新计算每日基础余额、资金和水平。我们还将检查,如果前一天达到了每日损失限额,那么我们就需要恢复交易,并根据未关闭虚拟仓位重开真实仓位。UpdateBaseLevels() 方法会处理这个问题:
//+------------------------------------------------------------------+ //| 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); PrintFormat(__FUNCTION__" | DAILY UPDATE: Balance = %.2f | Equity = %.2f | Level = %.2f", m_baseDailyBalance, m_baseDailyEquity, m_baseDailyLevel); // If the daily loss level was reached earlier, then if(m_state == RM_STATE_DAILY_LOSS) { // Restore the status to normal: CMoney::DepoPart(m_prevDepoPart); // Return the used portion of the total balance m_state = RM_STATE_OK; // Set the risk manager to normal CVirtualReceiver::Instance().Changed(); // Notify the recipient about changes PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f", m_virtualProfit, m_profit, m_dailyProfit); PrintFormat(__FUNCTION__" | RESTORE: depoPart = %.2f", m_prevDepoPart); } }
根据参数中规定的方法计算最大损失,我们有两种方法:DailyLoss() 和 OverallLoss()。它们的实现方法非常相似,唯一的区别在于计算所用的数值参数和方法参数:
//+------------------------------------------------------------------+ //| Maximum daily loss | //+------------------------------------------------------------------+ double CVirtualRiskManager::DailyLoss() { if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED) { // To get a fixed value, just return it return m_maxDailyLossLimit; } else if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) { // To get a given percentage of the base balance, calculate it return m_baseBalance * m_maxDailyLossLimit / 100; } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT) // To get a specified percentage of the daily level, calculate it return m_baseDailyLevel * m_maxDailyLossLimit / 100; } } //+------------------------------------------------------------------+ //| Maximum total loss | //+------------------------------------------------------------------+ double CVirtualRiskManager::OverallLoss() { if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED) { // To get a fixed value, just return it return m_maxOverallLossLimit; } else if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) { // To get a given percentage of the base balance, calculate it return m_baseBalance * m_maxOverallLossLimit / 100; } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT) // To get a specified percentage of the daily level, calculate it return m_baseDailyLevel * m_maxOverallLossLimit / 100; } }
检查限额的 CheckLimits() 方法只需调用两个辅助方法来检查每日亏损和总亏损:
//+------------------------------------------------------------------+ //| Check loss limits | //+------------------------------------------------------------------+ void CVirtualRiskManager::CheckLimits() { CheckDailyLimit(); // Check daily limit CheckOverallLimit(); // Check total limit }
每日亏损检查方法使用 DailyLoss() 方法获取允许的最大每日亏损限额,并将其与当前的每日利润进行比较。当超过限额时,风险管理器会切换到 "超过每日限额" 状态,并通过将已用交易余额的大小设为 0 来关闭未结仓位:
//+------------------------------------------------------------------+ //| Check daily loss limit | //+------------------------------------------------------------------+ void CVirtualRiskManager::CheckDailyLimit() { // If daily loss is reached and positions are still open if(m_dailyProfit < -DailyLoss() && CMoney::DepoPart() > 0) { // Switch the risk manager to the achieved daily loss state: m_prevDepoPart = CMoney::DepoPart(); // Save the previous value of the used part of the total balance CMoney::DepoPart(0); // Set the used portion of the total balance to 0 m_state = RM_STATE_DAILY_LOSS; // Set the risk manager to the achieved daily loss state CVirtualReceiver::Instance().Changed();// Notify the recipient about changes PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f", m_virtualProfit, m_profit, m_dailyProfit); PrintFormat(__FUNCTION__" | RESET: depoPart = %.2f", CMoney::DepoPart()); } }
总损失测试方法的原理与此类似,唯一的区别在于它是将总利润与可接受的总损失进行比较。如果超过了总限额,风险管理器就会切换到 "超过总限额" 状态。
将获得的代码保存到当前文件夹的 VirtualRiskManager.mqh 文件中。
现在,让我们来看看为了使用新的风险管理器类,我们需要对之前创建的项目文件进行哪些更改和添加。
实用的宏
在处理数组的有用的宏列表中,我添加了一个新宏 FORI(N, D)。它用 i 变量安排了一个循环,执行 N 次 D 表达式:
// Useful macros for array operations #ifndef __MACROS_INCLUDE__ #define APPEND(A, V) A[ArrayResize(A, ArraySize(A) + 1) - 1] = V; #define FIND(A, V, I) { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } } #define ADD(A, V) { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } } #define FOREACH(A, D) { for(int i=0, im=ArraySize(A);i<im;i++) {D;} } #define FORI(N, D) { for(int i=0; i<N;i++) {D;} } #define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);} #define REMOVE(A, V) { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) } #define __MACROS_INCLUDE__ #endif
将更改保存到当前文件夹的 Macros.mqh 文件中。
CMoney 资金管理类
在本类中,我们将添加一种计算虚拟仓位利润的方法,并将其交易量的缩放因子考虑在内。实际上,我们在 Volume() 方法中执行了类似的操作,以确定虚拟仓位的计算大小:根据当前可用于交易的余额大小和虚拟仓位交易量对应的余额大小的信息,我们找到一个缩放因子,等于这些余额的比率。然后将该系数乘以虚拟仓位交易量,得出计算交易量,即交易账户中将开立的交易量。
因此,让我们首先从 Volume() 方法中删除查找缩放因子的代码部分,将其转换为单独的 Coeff() 方法:
//+------------------------------------------------------------------+ //| Calculate the virtual position volume scaling factor | //+------------------------------------------------------------------+ double CMoney::Coeff(CVirtualOrder *p_order) { // Request the normalized strategy balance for the virtual position double fittedBalance = p_order.FittedBalance(); // If it is 0, then the scaling factor is 1 if(fittedBalance == 0.0) { return 1; } // Otherwise, find the value of the total balance for trading double totalBalance = s_fixedBalance > 0 ? s_fixedBalance : AccountInfoDouble(ACCOUNT_BALANCE); // Return the volume scaling factor return totalBalance * s_depoPart / fittedBalance; }
在此之后,Volume() 和 Profit() 方法的实现就变得非常相似:我们从虚拟仓位中获取所需的值 (交易量或利润),然后乘以由此产生的缩放因子:
//+------------------------------------------------------------------+ //| Determine the calculated size of the virtual position | //+------------------------------------------------------------------+ double CMoney::Volume(CVirtualOrder *p_order) { return p_order.Volume() * Coeff(p_order); } //+------------------------------------------------------------------+ //| Determining the calculated profit of a virtual position | //+------------------------------------------------------------------+ double CMoney::Profit(CVirtualOrder *p_order) { return p_order.Profit() * Coeff(p_order); }
当然,我们需要在类描述中添加新的方法:
//+------------------------------------------------------------------+ //| Basic money management class | //+------------------------------------------------------------------+ class CMoney { ... // Calculate the scaling factor of the virtual position volume static double Coeff(CVirtualOrder *p_order); public: CMoney() = delete; // Disable the constructor // Determine the calculated size of the virtual position static double Volume(CVirtualOrder *p_order); // Determine the calculated profit of a virtual position static double Profit(CVirtualOrder *p_order); ... };
保存对当前文件夹中 Money.mqh 文件所做的更改。
СVirtualFactory 类
由于我们创建的风险管理器类是 CFactorable 类的后代,因此为了确保创建该类的可能性,有必要扩展由 CVirtualFactory 创建的对象的组成。在 Create() 静态方法中,添加一个代码块,负责创建 CVirtualRiskManager 类对象:
//+------------------------------------------------------------------+ //| Object factory class | //+------------------------------------------------------------------+ class CVirtualFactory { public: // Create an object from the initialization string static CFactorable* Create(string p_params) { // Read the object class name string className = CFactorable::ReadClassName(p_params); // Pointer to the object being created CFactorable* object = NULL; // Call the corresponding constructor depending on the class name if(className == "CVirtualAdvisor") { object = new CVirtualAdvisor(p_params); } else if(className == "CVirtualRiskManager") { object = new CVirtualRiskManager(p_params); } else if(className == "CVirtualStrategyGroup") { object = new CVirtualStrategyGroup(p_params); } else if(className == "CSimpleVolumesStrategy") { object = new CSimpleVolumesStrategy(p_params); } ... return object; } };
将获得的代码保存到当前文件夹的 VirtualFactory.mqh 文件中。
CVirtualAdvisor 类
我们需要对 CVirtualAdvisor EA 类进行更多重大修改。由于我们已决定只在本类中使用风险管理器对象,因此我们将在类描述中添加相应的属性:
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: CVirtualReceiver *m_receiver; // Receiver object that brings positions to the market CVirtualInterface *m_interface; // Interface object to show the status to the user CVirtualRiskManager *m_riskManager; // Risk manager object ... };
我们还同意,风险管理器初始化字符串将嵌入 EA 初始化字符串,紧接在策略组初始化字符串之后。另外,让我们在构造函数中把这个初始化字符串读入 riskManagerParams 变量,然后再从中创建风险管理器:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(string p_params) { // Save the initialization string m_params = p_params; // Read the initialization string of the strategy group object string groupParams = ReadObject(p_params); // Read the initialization string of the risk manager object string riskManagerParams = ReadObject(p_params); // Read the magic number ulong p_magic = ReadLong(p_params); // Read the EA name string p_name = ReadString(p_params); // Read the work flag only at the bar opening m_useOnlyNewBar = (bool) ReadLong(p_params); // If there are no read errors, if(IsValid()) { ... // Create the risk manager object m_riskManager = NEW(riskManagerParams); } }
既然我们在构造函数中创建了一个对象,那么也应该在析构函数中删除它:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { if(!!m_receiver) delete m_receiver; // Remove the recipient if(!!m_interface) delete m_interface; // Remove the interface if(!!m_riskManager) delete m_riskManager; // Remove risk manager DestroyNewBar(); // Remove the new bar tracking objects }
最重要的是从相关的 EA 处理程序中调用风险管理器的 Tick() 处理函数。请注意,风险管理器处理函数是在调整市场交易量之前启动的,因此,如果超出了损失限额,或者相反,限额被更新,那么接收者就可以在处理同一分时报价时调整市场仓位的未平仓交易量:
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Define a new bar for all required symbols and timeframes bool isNewBar = UpdateNewBar(); // If there is no new bar anywhere, and we only work on new bars, then exit if(!isNewBar && m_useOnlyNewBar) { return; } // Receiver handles virtual positions m_receiver.Tick(); // Start handling in strategies CAdvisor::Tick(); // Risk manager handles virtual positions m_riskManager.Tick(); // Adjusting market volumes m_receiver.Correct(); // Save status Save(); // Render the interface m_interface.Redraw(); }
保存对当前文件夹中 VirtualAdvisor.mqh 文件所做的更改。
SimpleVolumesExpertSingle EA
要测试风险管理器,只需在 EA 中添加指定参数的功能,并生成所需的初始化字符串。现在,让我们把风险管理器的所有六个参数都移动到独立的 EA 输入参数中:
input group "=== Risk management" input bool rmIsActive_ = true; input double rmStartBaseBalance_ = 10000; input ENUM_RM_CALC_LIMIT rmCalcDailyLossLimit_ = RM_CALC_LIMIT_FIXED; input double rmMaxDailyLossLimit_ = 200; input ENUM_RM_CALC_LIMIT rmCalcOverallLossLimit_ = RM_CALC_LIMIT_FIXED; input double rmMaxOverallLossLimit_ = 500;
在 OnInit() 函数中,有必要添加风险管理器初始化字符串的创建,并将其嵌入到 EA 初始化字符串中。同时,我们将稍微重写为一个策略和包含这一个策略的组创建初始化字符串的代码,将单个对象的初始化字符串分离到不同的变量中:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { CMoney::FixedBalance(fixedBalance_); CMoney::DepoPart(1.0); // Prepare the initialization string for a single strategy instance string strategyParams = StringFormat( "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)", Symbol(), Period(), signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_ ); // Prepare the initialization string for a group with one strategy instance string groupParams = StringFormat( "class CVirtualStrategyGroup(\n" " [\n" " %s\n" " ],%f\n" " )", strategyParams, scale_ ); // Prepare the initialization string for the risk manager string riskManagerParams = StringFormat( "class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%d,%.2f" " )", rmIsActive_, rmStartBaseBalance_, rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCalcOverallLossLimit_, rmMaxOverallLossLimit_ ); // Prepare the initialization string for an EA with a group of a single strategy and the risk manager string expertParams = StringFormat( "class CVirtualAdvisor(\n" " %s,\n" " %s,\n" " %d,%s,%d\n" ")", groupParams, riskManagerParams, magic_, "SimpleVolumesSingle", true ); PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams); // Create an EA handling virtual positions expert = NEW(expertParams); if(!expert) return INIT_FAILED; return(INIT_SUCCEEDED); }
将获得的代码保存到当前文件夹下的 SimpleVolumesExpertSingle.mq5 文件中。现在,测试风险管理器操作的一切准备就绪。
测试
让我们使用前几个开发阶段优化过程中获得的一个交易策略实例的参数。我们称这种交易策略为模型策略。模型策略参数如图 1 所示。
图 1.模型策略参数
让我们在 2021-2022 年期间,使用这些参数和关闭的风险管理器,运行一个单一的测试通过。我们得到以下结果:
图 2.没有风险管理器的模型策略结果
从图中可以看出,在选定的时间段内,资金出现了几次明显的回撤。其中最大的一次发生在 2021 年 10 月底(约 380 美元)和 2022 年 6 月(约 840 美元)。
现在,让我们打开风险管理器,将每日最大损失限额设为 150 美元,最大总损失设为 450 美元。我们得到以下结果:
图 3。无风险管理器的模型策略结果(最大损失:150 美元和 450 美元)
图表显示,2021 年 10 月,风险管理器关闭了两次亏损的市场仓位,但虚拟仓位仍未关闭。因此,第二天一到,市场仓位又被打开了。不幸的是,重新开放的价格并不优惠,因此余额和基金的总回撤额略高于禁用风险管理器的净值回撤。同样明显的是,在平仓之后,该策略并没有获得微薄的利润(与没有风险管理器的情况一样),反而出现了一些亏损。
2022 年 6 月,风险管理器已被触发七次,在每日损失达到 150 美元时关闭了市场头寸。结果又是以较低的价格重新开启,这一系列交易造成了损失。但是,如果这样一个 EA 在一家自营交易公司的模拟账户上运行,并设定了每日最大损失和总损失这样的参数,那么如果没有风险管理器,该账户就会因违反交易规则而被停止,而如果有了风险管理器,该账户就会继续运行,并因此获得略少的利润。
尽管我将总损失设置为 450 美元,而 6 月份的总余额回撤超过了 1000 美元,但由于总损失是根据基础余额计算的,因此并未达到最大损失总额。换句话说,如果资金低于(10,000 - 450)= 9550 美元,就实现了这一目标。但由于之前积累的利润,这一时期的资金额肯定不会低于 10 000 美元。因此,EA 继续开展工作,同时开启市场仓位。
现在让我们模拟一下触发总亏损的情况。为此,我们将提高仓位大小的缩放系数,以便在 2021 年 10 月尚未超过最大亏损总额,而在 2022 年 6 月超过最大亏损总额。让我们设置 scale_ = 50,看看结果如何:
图 4.无风险管理器的模型策略结果(最大损失:150 美元和 450 美元),scale_ = 50
我们可以看到,交易将于 2022 年 6 月结束。在随后的一段时间里,EA 没有开立一个仓位。这是因为达到了总亏损限额(9550 美元)。我们还可以注意到,现在不仅在 2021 年 10 月,而且在其他几个时期,达到每日亏损额的情况也更频繁地出现。
因此,我们的两个限制工具都能正常工作。
风险管理器即使在自营交易公司之外也能发挥作用。举例说明,让我们尝试优化模型策略的风险管理器参数,努力扩大开仓规模,但不超过 10%的允许回撤率。为此,我们将在风险管理器参数中设置最大总损失等于每日水平的 10%。我们还将了解优化期间的最大日亏损额,也是按日亏损额的百分比计算的。
图 5.利用风险管理器优化模型策略的结果
结果显示,使用风险管理器后,一年的标准化利润几乎增加了一倍半:从 1560 美元增至 2276 美元(结果列)。以下是最佳通过的单独显示效果:
图 6.无风险管理器的模型策略结果(最大损失:7.6% 和 10%, scale_ = 88)
请注意,在整个测试期间,EA 都在继续开启交易。这意味着 10%的总限额从未被触发。显然,将风险管理器应用于单个交易策略实例并没有特别的意义,因为我们并不打算在真实账户中逐一启动这些策略。不过,对一个实例有效的方法对有许多实例的 EA 也应同样有效。因此,即使是这样粗略的结果,我们也可以说,风险管理器肯定是有用的。
结论
因此,我们现在有了一个基本的交易风险管理器,它可以让我们遵守指定的每日最大亏损和总亏损水平。它还不支持在重启 EA 时保存和加载状态,因此我不建议在真实账户上使用它。但这种修改并没有带来任何特别的困难。我稍后再谈。
同时,还可以尝试增加在不同时段限制交易的功能,从禁止在一周中某些天的某些时段进行交易,到禁止在重要经济新闻发布期间开立新仓位。风险管理器的其他可能发展领域包括:更平稳地改变仓位大小(例如,当超过限额的一半时,减少两倍),以及更 "智能" 地恢复交易量(例如,只有当损失超过仓位减少水平时)。
我会推迟到以后再说。现在,我将继续自动优化 EA。第一阶段已在前一篇文章中实现。现在该进入第二阶段了。
感谢您的关注!期待很快与您见面!
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/14764
注意: 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.


感谢您的反馈!
可能错了。DailyLoss() 方法不会评估缩水有多大。它只是在必要时将指定的最大缩水水平从百分比转换为账户货币。比较本身在CheckDailyLimit() 方法中进行:
m_dailyProfit 的值在每个 tick 中更新,并计算为当前资金(净值)与每日水平(余额值与 每日期初资金的最大值)之差:
因此,资金缩水似乎只是考虑在内。还是我误解了问题?
为了压缩代码。宏还允许将代码块作为参数传递,而通过函数执行此类操作时,不能将代码块作为参数传递给函数。
因此,资金缩水似乎只是被考虑在内。还是我误解了问题?
为了代码紧凑。另外,宏允许将代码块作为参数传递,而通过函数执行此类操作时,不能将代码块作为参数传递给函数。
非常感谢您的详细解答 ))我们将等待新的文章!)
亲爱的尤里
我正在尝试编译代码,但在 VirtualRiskManager.mqh 中遇到以下错误:
"Changed - undeclared identifier" on line CVirtualReceiver::Instance().Changed(); // Notify the recipient about changes
我检查了多次代码,但都没有发现问题。你能解释一下我漏掉了什么吗?
我很期待本系列的下一篇文章。
谢谢
您好!
很抱歉,我忘了至少再附上一份做过编辑的文件。从第 16 部分开始,每篇文章都附有完整的项目 文件档案。我将在本文中附上。