风险管理(第三部分):构建风险管理主类
- 概述
- 风险管理设计与规划
- 常量、枚举和结构体的定义
- 风险管理关键变量的声明
- 创建构造函数、析构函数和初始化方法
- 损益赋值方法
- 根据单笔交易风险计算手数和止损位
- 用于检索亏损、利润和风险值的函数
- 新一天和新一周的规划事件
- 结论
概述
我们继续我们的系列文章,我们在其中开发了一个风险管理系统。在上一篇文章中,我们运用第一部分学到的概念创建了界面。现在,在本系列的第三部分中,我们将重点完善风险管理类的核心结构。
在本文中,我们将创建一个类,允许我们为亏损和利润赋值,为利润计算和跟踪奠定基础。这是朝着建立一个稳健和功能强大的风险管理系统迈出的关键一步。

以下框架概述了我们系统中设计和规划风险管理的结构化计划:
-
定义、结构和枚举
第一步是定义所需的结构和枚举。这些对于存储关键信息(如累计亏损和利润)以及促进系统内的数据处理至关重要。 -
创建 CRiskManagement 类
接下来,我们将开发负责风险管理的主类 CRiskManagement。这个中心类将整合所有与风险控制相关的计算和流程,确保有组织和高效的实现。 -
基本函数:值的赋值和检索
然后我们将实现赋值和检索函数,使我们能够更新和查询损益。在这个阶段,我们还将定义类构造函数和析构函数,以便正确管理内存和数据初始化。 -
事件驱动函数
最后,我们将开发在关键时刻触发的函数,例如新的一天或一周的开始。这些函数将有助于重新计算利润、调整风险和重新初始化某些时期的累积变量,确保随着时间的推移进行适当的绩效跟踪。
有了这项计划,我们将从头开始建设。
常量、枚举和结构体的定义
在实现代码之前,我们将把所有风险管理逻辑组织到一个名为 Risk_Management.mqh 的单独文件中(我们创建的基础文件)。这使代码更加结构化和模块化。
遵循我们的风险管理框架,第一步是定义所需的结构、枚举和常量。这些元素对于多种函数至关重要,尤其是那些负责计算和控制每日、每周或单笔交易最大亏损等数值的函数。
常量定义(#define)
为了便于风险管理和提高代码可读性,我们将定义几个关键常数:
1.NOT_MAGIC_NUMBER
该常量将用作类构造函数中的初始化值。其目的是表明不会使用特定的幻数;相反,风险管理将适用于所有交易,而不限制于特定的标识符。在真实交易环境中对整个账户实施风险管理时,这非常有用。
#define NOT_MAGIC_NUMBER 0 //Not Magic Number
2.平仓标志
为了使平仓函数更加动态,我们将使用标志而不是创建多个单独的函数来关闭盈利、亏损或两者兼有的交易。这些标志可以简化代码并提高代码的重用性。
#define FLAG_CLOSE_ALL_PROFIT 2 //Flag indicating to close only operations with profit #define FLAG_CLOSE_ALL_LOSS 4 //Flag indicating to close only operations without profit
枚举
1.计算方法:固定金额或百分比我们需要一个枚举来确定该策略中最大损失和利润的计算方法。主要有两种方法:
-
固定的(资金):用户设定一个具体的金额来定义最大亏损或盈利。无论账户余额如何,该值保持不变。
- 示例:系统运行期间,每日最大亏损金额将保持在 1000 美元不变。
-
动态的(百分比):用户可以选择一个百分比,该百分比应用于账户中的参考参数,例如余额或净值,而不是一个固定值。
- 示例:如果限额为余额的 2%,则最大亏损会随账户余额而变化。如果余额增加,亏损限额也相应增加;如果余额减少,限额也相应减少。
这在代码中用以下枚举表示。
enum ENUM_RISK_CALCULATION_MODE //enumeration to define the types of calculation of the value of maximum profits and losses { money, //Money percentage //Percentage % };
2.百分比应用:风险计算的参考参数
如果计算方法是基于百分比的,我们必须定义该百分比将应用于哪个账户值。我们考虑以下四个主要选项:
- 余额 - 应用于账户总余额。
- 净利润 - 基于账户创建以来的累计利润。
- 可用保证金 - 根据可用于开设新交易的资金计算得出。
- 净额 - 代表根据未平仓头寸的盈亏进行调整后的余额。
enum ENUM_APPLIED_PERCENTAGES //enumeration to define the value to which the percentages will be applied { Balance, //Balance ganancianeta,//Net profit free_margin, //Free margin equity //Equity };
3.风险管理类型:个人账户或自营公司(FTMO)
如前几章所述,该系统既可应用于个人账户,也可应用于自营公司,特别是 FTMO。FTMO 采用更动态的方法来计算每日最大亏损。
稍后,在分析结构时,我们将详细解释 FTMO 如何处理此计算。目前,我们将创建一个枚举,允许用户选择风险管理的账户类型。
enum ENUM_MODE_RISK_MANAGEMENT //enumeration to define the type of risk management { propfirm_ftmo, //Prop Firm FTMO personal_account // Personal Account };
4.选择手数类型
并非所有交易者都喜欢使用动态手数进行交易。我们将允许用户选择手数类型。这种选择不会影响风险管理类的核心逻辑,但对于开发 EA 来说很重要,因为用户应该能够使用固定手数或动态手数进行交易。
我们将创建以下枚举:
enum ENUM_LOTE_TYPE //lot type { Dinamico,//Dynamic Fijo//Fixed };
开仓时,手数选择可以按以下方式设置:
trade.Sell( (Lote_Type == Dinamico ? risk.GetLote(ORDER_TYPE_SELL,GET_LOT_BY_ONLY_RISK_PER_OPERATION) : lote), _Symbol,tick.bid,sl,tp,"EA Sell");
在这段代码中,如果用户选择动态手数(Dinamico),GetLote 函数将根据每笔交易的风险计算手数大小。如果用户选择固定手数(Fijo),EA 将使用分配给手数变量(lote)的默认值。
稍后,我们将创建 GetLot 函数来正确获取动态手数。
5.动态手数计算方法
如果用户选择动态手数,我们需要定义其大小的计算方式。我们将创建一个枚举来指定计算是仅基于每次交易的风险,还是根据止损进行调整。
-
仅基于每笔交易风险的计算:
手数大小完全根据单笔交易的风险百分比来确定。在这种情况下,无需指定止损的具体值。 -
基于风险和止损的计算
手数大小是根据每笔交易的风险计算的,并根据止损进行调整。这一点很重要,因为如果您在不考虑止损的情况下设定固定的风险百分比,您可能会收到不合适的交易手数。示例:
假设我们想冒险投入 1000 美元账户中的 1%,这相当于最大风险为 10 美元。我们的策略在 EUR/USD(5 位小数经纪商)上采用 1:2 的风险回报比和 100 点止损,简单的计算可能会得出很多 0.02 的结果。- 如果交易触发止损,实际损失将是 2 美元而不是 10 美元。
- 如果交易触发止盈,实际收益将为 4 美元而不是 20 美元。
为了解决这个问题,我们调整手数,使得在给定止损的情况下,最大亏损恰好为账户的 1%。这样可以确保,如果交易达到 1:2 的止盈比例,则利润为 2%。
GetIdealLot 函数(在前一篇文章中介绍过)会自动调整手数。
void GetIdealLot(double& nlot, double glot, double max_risk_per_operation, double& new_risk_per_operation, long StopLoss)
它会调整交易手数,使每次交易的风险保持在规定的限度内。
为了表示这两种计算方法,我们定义以下枚举:enum ENUM_GET_LOT { GET_LOT_BY_ONLY_RISK_PER_OPERATION, //Obtain the lot for the risk per operation GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION //Obtain and adjust the lot through the risk per operation and stop loss respectively. };
结构:
结构在我们的风险管理体系中发挥着关键作用。如前所述,亏损和利润的计算方式可以是直接计算(固定金额)或动态计算(百分比)的。如果采用百分比法,则以账户中的参考值为准。
我们将创建一个包含所有必要信息的结构,用于定义亏损或盈利。
- value - 表示亏损或盈利金额。
- assigned_percentage - 如果计算是动态的,则表示分配的百分比。
- mode_calculation_risk - 定义每笔交易的风险计算方法(直接或动态)。
- percentage_applied_to - 枚举,指定在使用动态方法时,百分比适用于哪个帐户指标。
这在代码中表示为:
struct Loss_Profit { double value; //value double assigned_percentage; //percentage to apply ENUM_RISK_CALCULATION_MODE mode_calculation_risk; //risk calculation method ENUM_APPLIED_PERCENTAGES percentage_applied_to; //percentage applied to };
使用这种结构,我们可以确定以下几种盈亏方案:
| 名称 | 描述 | 重置 |
|---|---|---|
| 每日最大亏损 (MDL) | 单日允许的最大亏损。 | 每日重置为 0。 |
| 最大亏损(ML) | 账户在其存续期间可能达到的最大总亏损。 | 不重置。 |
| 单笔交易最大亏损 (GMLPO) | 与用户定义的单笔交易风险类似;用于估计合适的交易手数。 | 每次交易平仓或新一天开始时重新计算。 |
| 每周最大亏损 (MWL) | 一周内允许的最大亏损。这是可选的,因为对于自营公司来说并非必需。 | 每周重置。 |
| 单笔交易净最大亏损 (NMLPO) | 仅在调用根据每笔交易风险和止损计算手数的函数时生成。 | 每次调用基于风险和止损的手数检索函数时都会重新计算。 |
| 每日最高利润 | 机器人或账户一天内可获得的最大利润。 | 每日重置。 |
FTMO 中每日最大亏损的特点
当将此风险管理系统应用于普通账户和自营公司(如 FTMO)时,会出现一些关键差异。在 FTMO 中,每日最大亏损额是动态变化的,因为如果在一天内产生盈利,亏损额可能会增加。
例如,在初始账户余额为 20 万美元的 FTMO 挑战赛中,每日最大损失限额为 1 万美元。如果您在已平仓交易中损失了 8000 美元,那么您当天的账户损失不得超过 2000 美元。你的未平仓浮动亏损额也不能超过 -2000 美元。该限额包含佣金和库存费费用。
反之亦然,如果你一天盈利 5000 美元,那么你可以承受损失 15000 美元,但不能损失更多。再次提醒,您的每日最大亏损额也包括您未平仓的交易。例如,如果你在一天内平仓亏损了 6000 美元,然后你又开了一笔新交易,这笔交易一开始亏损了大约 5700 美元,但最终却盈利了,很遗憾,已经太晚了。一瞬间,你的当日权益损失为 -11700 美元,超过了允许的 10000 美元损失。
正如 FTMO 的指导方针中直接指出的那样,每日最大亏损额会根据当天获得的盈利情况而有所不同。
例如,如果一天开始时您允许的最大亏损为 10000 美元,而您当天获得了利润,则该利润将添加到最初的 10000 美元中,从而增加您的允许亏损范围。因此,随着利润的积累,您可以承担的风险金额也会相应调整。
风险管理关键变量的声明
在本节中,我们将定义 CRiskManagement 类,该类负责管理交易风险。
//+------------------------------------------------------------------+ //| Class CRisk Management | //+------------------------------------------------------------------+ class CRiskManagemet final
包含库
在定义类之前,我们将引入 CTrade.mqh 库,以便能够管理交易,例如平仓:
#include <Trade/Trade.mqh> 1.指向 CTrade 的指针
第一个变量是指向 CTrade 类的指针,这将允许我们与未平仓头寸进行交互:
private: //--- CTrade class pointer to be able to work with open positions CTrade *trade;
2.关键变量
我们定义了三个主要变量:
- account_profit(double)- 存储自1971.01.01以来的账户净利润(datetime 类型的最小日期);
- 止损(long)- 维持用于计算手数大小的止损点数水平;
- batch(double)- 存储上次计算手数的内部变量。
代码:
//-- Main variables double account_profit; long StopLoss; double lote;
3.通用变量
- magic_number(ulong)- 存储用于管理与给定交易集相关的风险的幻数。它也可以等于 0,这样就可以用于个人账户。
- mode_risk_management (ENUM_MODE_RISK_MANAGEMENT) - 定义将要应用的风险管理类型:适用于普通账户或自营公司(例如 FTMO)。
代码:
//--- General variables ENUM_MODE_RISK_MANAGEMENT mode_risk_managemet; ulong magic_number;
4.自营公司账户的具体变量(FTMO)
在自营会计公司账户中,风险管理规则适用于期初余额,而不是期末余额。例如,每日最大损失和最大总损失通常按期初余额的百分比计算(通常为 10%)。
为了实现这一功能,我们将添加一个变量来存储初始余额,该变量需要用户手动设置:
//--- Variables to work with anchoring tests double account_balance_propfirm;
5.下一笔交易预期最大亏损变量(NMLPO)
该变量将存储下一次交易的预计最大亏损。虽然不是强制性的,但可以用来预测如果机器人仓位达到止损水平将会亏损多少。
它只会通过 GetLote() 调用进行计算。
//--- Variables to store the values of the maximum losses double nmlpo;
6.损益管理变量
先前定义的 Loss_Profit 结构将用于存储损失金额、计算方法以及百分比的应用方式。
让我们定义五个关键变量:
//--- variables that store percentages and enumeration, which will be used for the subsequent calculation of losses
Loss_Profit mdl, mwl, ml, gmlpo, mdp; 其中:
- mdl - 每日最大亏损;
- mwl - 每周最大亏损;
- ml - 最大总亏损;
- gmlpo - 每笔交易的最大亏损;
- mdp - 每日最大利润。
7.用于利润的变量(日利润、周利润和总利润)
为了监控机器人/ EA 交易的绩效,我们将添加变量来存储盈利数据,以便在达到亏损限额或采取安全措施时发出警报。
我们需要:
- 利润计算所依据的参考时间
- 用于存储利润的变量。
代码:
//--- Variables to store the profit and the time from which they will be obtained double daily_profit, weekly_profit, gross_profit; datetime last_weekly_time, last_day_time, init_time;
其中:
- daily_profit - 当日获得的利润,
- weekly_profit - 一周内获得的利润,
- gross_profit - 自风险管理开始以来的总利润
- last_weekly_time - 用于计算每周利润的最后记录日期
- last_day_time - 用于计算每日利润的最后记录日期
- init_time - 风险管理的开始日期。
创建构造函数、析构函数和初始化方法
现在我们已经定义了关键变量,接下来我们将实现 CRiskManagement 类的主要函数。
构造函数
构造函数将负责初始化关键变量,例如幻数、风险管理模式和自有公司账户余额(如果适用)。
构造函数声明如下:
CRiskManagemet(ulong magic_number_ = NOT_MAGIC_NUMBER,ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, double account_propfirm_balance=0);
在构造函数实现中,我们给变量赋值,初始化 CTrade 对象,并计算从 1972 年 1 月 1 日开始的账户净利润。我们还设置了适当的时间戳来跟踪利润。
CRiskManagemet::CRiskManagemet(ulong magic_number_ = NOT_MAGIC_NUMBER,ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, double account_propfirm_balance =0) { if(magic_number_ == NOT_MAGIC_NUMBER) { Print("| Warning | No magic number has been chosen, taking into account all the magic numbers and the user's trades"); } //--- this.account_balance_propfirm = account_propfirm_balance ; trade = new CTrade(); this.account_profit = GetNetProfitSince(true,this.magic_number,D'1972.01.01 00:00'); this.magic_number = magic_number_; this.mode_risk_managemet = mode_risk_management_; //--- this.last_day_time = iTime(_Symbol,PERIOD_D1,0); this.last_weekly_time = iTime(_Symbol,PERIOD_W1,0); this.init_time =magic_number_ != NOT_MAGIC_NUMBER ? TimeCurrent() : D'1972.01.01 00:00'; }
请注意:如果用户未指定有效交易数量,则会显示警告,提示系统将考虑用户已开立的所有交易。
析构函数
当 CRiskManagement 实例被删除时,类析构函数将负责释放分配给 CTrade 对象的内存。
CRiskManagemet::~CRiskManagemet()
{
delete trade;
}
初始化方法
为了简化风险管理设置和更新,我们将创建一些特殊函数,使您能够:
- 设置止损位,
- 设置盈亏参数。
1.设置亏损结构值的函数
我们定义了一些方法,使我们能够为损益计算的关键变量赋值。
//--- Functions to assign values to variables for subsequent calculation of losses void SetPorcentages(double percentage_or_money_mdl, double percentage_or_money_mwl,double percentage_or_money_gmlpo, double percentage_or_money_ml, double percentage_or_money_mdp_); void SetEnums(ENUM_RISK_CALCULATION_MODE mode_mdl_, ENUM_RISK_CALCULATION_MODE mode_mwl_, ENUM_RISK_CALCULATION_MODE mode_gmlpo_, ENUM_RISK_CALCULATION_MODE mode_ml_, ENUM_RISK_CALCULATION_MODE mode_mdp_); void SetApplieds(ENUM_APPLIED_PERCENTAGES applied_mdl_, ENUM_APPLIED_PERCENTAGES applied_mwl_, ENUM_APPLIED_PERCENTAGES applied_gmlpo_, ENUM_APPLIED_PERCENTAGES applied_ml_, ENUM_APPLIED_PERCENTAGES applied_mdp_);
2.为亏损结构分配百分比
以下方法为相应的结构赋予百分比或金额值。
void CRiskManagemet::SetPorcentages(double percentage_or_money_mdl,double percentage_or_money_mwl,double percentage_or_money_gmlpo,double percentage_or_money_ml,double percentage_or_money_mdp_) { this.gmlpo.assigned_percentage = percentage_or_money_gmlpo; this.mdl.assigned_percentage = percentage_or_money_mdl; this.ml.assigned_percentage = percentage_or_money_ml; this.mdp.assigned_percentage = percentage_or_money_mdp_; this.mwl.assigned_percentage = percentage_or_money_mwl; }
3.风险计算模式初始化
该方法为每个结构设置计算模式。如果用户选择以金额为基础的计算模式,则直接分配相应的值。
void CRiskManagemet::SetEnums(ENUM_RISK_CALCULATION_MODE mode_mdl_,ENUM_RISK_CALCULATION_MODE mode_mwl_,ENUM_RISK_CALCULATION_MODE mode_gmlpo_,ENUM_RISK_CALCULATION_MODE mode_ml_,ENUM_RISK_CALCULATION_MODE mode_mdp_) { this.gmlpo.mode_calculation_risk = mode_gmlpo_; this.mdl.mode_calculation_risk = mode_mdl_; this.mdp.mode_calculation_risk = mode_mdp_; this.ml.mode_calculation_risk = mode_ml_; this.mwl.mode_calculation_risk = mode_mwl_; //-- If the money mode has been chosen, assign the variable that stores the money or percentage to the corresponding variables. this.gmlpo.value = this.gmlpo.mode_calculation_risk == money ? this.gmlpo.value : 0; this.mdp.value = this.mdp.mode_calculation_risk == money ? this.mdp.value : 0; this.mdl.value = this.mdl.mode_calculation_risk == money ? this.mdl.value : 0; this.ml.value = this.ml.mode_calculation_risk == money ? this.ml.value : 0; this.mwl.value = this.mwl.mode_calculation_risk == money ? this.mwl.value : 0; }
4.为帐户分配参数
此方法决定如何将配置的百分比应用于帐户。
void CRiskManagemet::SetApplieds(ENUM_APPLIED_PERCENTAGES applied_mdl_,ENUM_APPLIED_PERCENTAGES applied_mwl_,ENUM_APPLIED_PERCENTAGES applied_gmlpo_,ENUM_APPLIED_PERCENTAGES applied_ml_,ENUM_APPLIED_PERCENTAGES applied_mdp_) { this.gmlpo.percentage_applied_to = applied_gmlpo_; this.mdl.percentage_applied_to = applied_mdl_; this.mdp.percentage_applied_to = applied_mdp_; this.mwl.percentage_applied_to = applied_mwl_; this.ml.percentage_applied_to = applied_ml_; }
5.止损设置函数
为了设置止损,我们将使用两种简单的方法来为止损变量赋值:
- 方法 1:用户直接设置止损点数。
- 方法 2:用户定义止损点与入场点之间的距离,然后将其转换为点数(使用我们在文章第一部分中编写的函数)。
//--- Function to set the "StopLoss" variable, in points or distance inline void SetStopLoss(double dist_open_sl) { this.StopLoss = DistanceToPoint(dist_open_sl); } inline void SetStopLoss(long _sl_point_) { this.StopLoss = _sl_point_; }
损益赋值方法
在本节中,我们将开发在风险管理系统中为损益赋值所需的方法。
1.通用赋值函数
为了确保灵活和适应性的风险管理,我们将创建一个通用函数,根据应用于各种账户指标的百分比计算适当的值。
函数定义:
我们将使用的主要函数如下:
//--- General function to assign values to loss variables double GetValorWithApplied(const ENUM_APPLIED_PERCENTAGES applied,const double percentage_);
该函数将接受两个关键参数:
- applied - 定义百分比将应用于哪个账户指标(余额、可用保证金、净值等)。
- percentage_ - 要应用于所选指标的百分比。
double CRiskManagemet::GetValorWithApplied(const ENUM_APPLIED_PERCENTAGES applied,const double percentage_) { if(this.mode_risk_managemet == propfirm_ftmo && percentage_ != this.mdp.assigned_percentage && percentage_ != this.gmlpo.assigned_percentage) return this.account_balance_propfirm * (percentage_/100.0); switch(applied) { case Balance: return NormalizeDouble((percentage_/100.0) * AccountInfoDouble(ACCOUNT_BALANCE),2); case ganancianeta: { if(this.account_profit <= 0) { PrintFormat("The total profit of the account which is %+.2f is invalid or negative",this.account_profit); return 0; } else return NormalizeDouble((percentage_/100.0) * this.account_profit,2); } case free_margin: { if(AccountInfoDouble(ACCOUNT_MARGIN_FREE) <= 0) { PrintFormat("free margin of %+.2f is invalid",AccountInfoDouble(ACCOUNT_MARGIN_FREE)); return 0; } else return NormalizeDouble((percentage_/100.0) * AccountInfoDouble(ACCOUNT_MARGIN_FREE),2); } case equity: return NormalizeDouble((percentage_/100.0) * AccountInfoDouble(ACCOUNT_EQUITY),2); default: Print("Critical Error | It was not found that: ", EnumToString(applied), " be part of the allowed enumeration"); } return 0; }代码解释
- 自营公司账户:
- 如果账户属于自营公司(例如 FTMO),并且所应用的百分比不是指每日最大损失 (mdp.assigned_percentage) 或每次交易的最大损失 (gmlpo.assigned_percentage),则计算基于自营公司的账户余额。
- 基于多种账户指标的计算:
- Balance - 占账户总余额的百分比。
- ganancianeta - 应用于账户净利润的百分比。如果利润为负数或零,则会显示错误信息。
- free_margin - 应用于可用保证金的百分比。如果可用保证金为负数,则该函数返回 0。
- equity - 应用于账户当前权益的百分比。
- 错误处理:
- 如果向应用的函数传递无效指标,该函数将打印错误消息并返回 0。
2.创建用于配置损益变量的函数
为了有效地管理损益,我们将创建一组函数,使我们能够以结构化的方式分配值。我们将总共定义六个函数:五个专门用于配置主要亏损参数,一个用于根据止损计算理想的手数大小。
1.赋值函数
盈亏值的确定方法基于以下逻辑:
- 如果计算方式为金额,则保留先前赋值。
- 如果计算不是以金额模式进行,则使用 GetValorWithApplied 函数根据指定的百分比和相应的应用程序参数来确定值。
- 如果指定的百分比为 0,则亏损被视为未使用,因此,其值被赋予 0。
2.赋值函数
以下函数针对每种亏损类型实现了上述逻辑:
//--- Functions to assign values to internal variables void SetMDL() {this.mdl.value = this.mdl.mode_calculation_risk == money ? this.mdl.value : (this.mdl.assigned_percentage > 0 ? GetValorWithApplied(this.mdl.percentage_applied_to,mdl.assigned_percentage) : 0); } void SetMWL() {this.mwl.value = this.mwl.mode_calculation_risk == money ? this.mwl.value : (this.mwl.assigned_percentage > 0 ? GetValorWithApplied(this.mwl.percentage_applied_to,mwl.assigned_percentage) : 0); } void SetML() {this.ml.value = this.ml.mode_calculation_risk == money ? this.ml.value : (this.ml.assigned_percentage > 0 ? GetValorWithApplied(this.ml.percentage_applied_to,ml.assigned_percentage): 0); } void SetGMLPO() {this.gmlpo.value = this.gmlpo.mode_calculation_risk == money ? this.gmlpo.value : (this.gmlpo.assigned_percentage > 0 ? GetValorWithApplied(this.gmlpo.percentage_applied_to,gmlpo.assigned_percentage) : 0); } void SetMDP() {this.mdp.value = this.mdp.mode_calculation_risk == money ? this.mdp.value : (this.mdp.assigned_percentage > 0 ? GetValorWithApplied(this.mdp.percentage_applied_to,mdp.assigned_percentage) : 0); } void SetNMPLO(double& TLB_new, double tlb) { GetIdealLot(TLB_new,tlb,this.gmlpo.value,this.nmlpo,this.StopLoss); }
根据单笔交易风险计算手数和止损位
在本节中,我们将实现两个基本函数,以准确计算手数和止损,确保每笔交易都符合我们愿意承担的风险水平。
1.计算手数大小的函数
为了计算手数大小,我们将使用第一篇文章中开发的函数,以确保手数符合我们预定义的风险规则。
用于获取合适手数大小的函数是:
//--- Get the lot double GetLote(const ENUM_ORDER_TYPE order_type, const ENUM_GET_LOT mode_get_lot);其参数:
- order_type - 指定订单类型(买入、卖出、止损、限价、止损限价等)。
- mode_get_lot - 指示手数计算方法(在前面的枚举部分中已解释)。
该函数的目的
该函数使用两种不同的方法帮助计算理想手数大小:
- 根据止损位和每笔交易的最大风险,
- 直接基于每笔交易的最大风险。
现在,让我们看看代码中是如何实现这个计算的。
//+-----------------------------------------------------------------------------------------------+ //| Function to obtain the ideal lot based on the maximum loss per operation and the stop loss | //+-----------------------------------------------------------------------------------------------+ double CRiskManagemet::GetLote(const ENUM_ORDER_TYPE order_type, const ENUM_GET_LOT mode_get_lot) { if(mode_get_lot == GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION) { double MaxLote = GetMaxLote(order_type); SetNMPLO(this.lote,MaxLote); PrintFormat("Maximum loss in case the next operation fails %.2f ", this.nmlpo); } else { this.lote = GetLotByRiskPerOperation(this.gmlpo.value,order_type); } return this.lote; }
模式 1.基于止损和最大单笔交易风险的交易手数计算
当用户选择 GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION 时,系统会根据每笔交易的风险计算手数,然后根据止损进行调整,以确保如果交易导致亏损,实际亏损与定义的风险相符。
- 使用 GetMaxLote(order_type) 函数获取允许的最大手数。
- 使用 SetNMPLO(this.lot, MaxLot) 调整手数大小,以防止超出账户限制。
- 屏幕上会打印一条信息,指出如果交易达到止损位,预计的最大亏损是多少。
模式 2.基于单笔交易最大风险的交易手数计算
当用户选择不同的计算模式(例如,GET_LOT_BY_RISK_PER_OPERATION)时,系统会以更直接的方式调整手数大小:
- GetLotByRiskPerOperation(this.gmlpo.value, order_type) 函数根据我们愿意承担的最大交易风险来确定手数大小。
- 这种方法更简单,对于不依赖固定止损,而是更动态地调整风险的交易者来说可能很有用。
2.计算止损的函数
由于我们在前面的章节中已经开发了辅助函数,因此该函数将相对简单,因为我们将重用这些函数来有效地计算止损。
帮助我们获得理想止损位的函数是:
long GetSL(const ENUM_ORDER_TYPE type, double DEVIATION = 100, double STOP_LIMIT = 50);其参数:
- type - 指定订单类型:买入、卖出、止损、限价、止损限价等。
- DEVIATION(可选 - 表示允许的执行偏差,默认值为 100 个点)。
- STOP_LIMIT(可选)- 表示 STOP_LIMIT 类型订单的点数距离。
这些参数确保止损计算是动态的,能够适应不同的市场状况。
函数实现:
//+----------------------------------------------------------------------------------+ //| Get the ideal stop loss based on a specified lot and the maximum loss per trade | //+----------------------------------------------------------------------------------+ long CRiskManagemet::GetSL(const ENUM_ORDER_TYPE type, double DEVIATION = 100, double STOP_LIMIT = 50) { double lot; return CalculateSL(type,this.gmlpo.value,lot,DEVIATION,STOP_LIMIT); }
- 该函数的工作原理
- 它定义了变量 lot,该变量将存储止损计算得出的交易量。
- 它调用 CalculateSL() 函数,并传递以下参数:
- 订单类型(type),以便正确计算买入或卖出的止损价;
- 最大单笔交易风险(this.gmlpo.value),以便止损与风险管理规则一致;
- lot - 将根据所使用的手数大小进行更新;
- DEVIATION - 允许的偏差,为订单执行提供一定的灵活性;
- STOP_LIMIT - 表示 STOP_LIMIT 类型订单的点数距离。
- 最后,该函数以点数为单位返回止损值。
用于获取损益值的函数
本节介绍交易系统中负责获取关键损益指标的函数。这些函数被声明为内联函数和常量函数,以确保效率并防止对类变量进行意外修改。
1.获取最大亏损和利润的函数
以下函数返回存储在变量中的值,这些变量跟踪不同时期达到的最大利润和亏损:
inline double GetML() const { return this.ml.value; } inline double GetMWL() const { return this.mwl.value; } inline double GetMDL() const { return this.mdl.value; } inline double GetGMLPO() const { return this.gmlpo.value; } inline double GetNMLPO() const { return this.nmlpo; } inline double GetMDP() const { return this.mdp.value; }
每个函数都提供不同分析层次上与交易绩效相关的数据。
2.获取每日、每周和总利润的函数
这些函数可以返回不同时间段的利润,从而可以对系统进行绩效评估。
//--- Obtain only profits: inline double GetGrossProfit() const { return this.gross_profit; } inline double GetWeeklyProfit() const { return this.weekly_profit; } inline double GetDailyProfit() const { return this.daily_profit; }
3.根据幻数和标志位平仓的函数
此函数旨在平掉所有符合特定条件的未平仓位。它基于一个标志系统,允许指定要平仓的交易:
- 只在获利的,
- 只在亏损的,
- 两种类型(所有交易)。
void CloseAllPositions(int flags = FLAG_CLOSE_ALL_LOSS | FLAG_CLOSE_ALL_PROFIT);
- flags - 一个整数,表示平仓的条件。可以使用按位或运算符(|)组合多个标志。
#define FLAG_CLOSE_ALL_PROFIT 2 //Flag indicating to close only operations with profit #define FLAG_CLOSE_ALL_LOSS 4 //Flag indicating to close only operations without profit
这些标志被定义为 2 的幂。
每个标志位代表一个值,该值可以单独激活,也可以组合激活:
| 标志 | 数值(十进制) | 值(二进制) | 用途 |
|---|---|---|---|
| FLAG_CLOSE_ALL_PROFIT | 2 | 00000010 | 只平掉盈利的交易。 |
| FLAG_CLOSE_ALL_LOSS | 4 | 00000100 | 只平掉亏损的交易。 |
2.平仓函数
该函数遍历所有未平仓位,并根据激活标志将其平仓。
3.遍历所有未平仓位
for(int i = PositionsTotal() - 1; i >= 0; i--)
- PositionsTotal() 返回未平仓头寸的总数。
- 该循环按相反的顺序(从最后一个到第一个)遍历仓位,以避免实时平仓时出错。
4.获取位置标识符(单号)
ulong position_ticket = PositionGetTicket(i);
- 每个仓位都有一个唯一的单号标识。
5.仓位选择
if (!PositionSelectByTicket(position_ticket)) continue;
- PositionSelectByTicket(position_ticket) 尝试根据收到的单号选择仓位。
- 如果选择失败,“继续”移动到下一个仓位。
6.检查幻数
ulong magic = PositionGetInteger(POSITION_MAGIC); if (magic != this.magic_number && this.magic_number != NOT_MAGIC_NUMBER) continue;
- 获取仓位的幻数。
- 如果仓位的幻数与预期的幻数不同,并且不允许对任何订单进行操作(NOT_MAGIC_NUMBER),则忽略该仓位。
7.获取持仓利润
double profit = PositionGetDouble(POSITION_PROFIT);
- POSITION_PROFIT 返回当前交易的利润。该值可以是正数(盈利)或负数(亏损)。
8.检查标志并平仓
if ((flags & FLAG_CLOSE_ALL_PROFIT) != 0 && profit > 0) { trade.PositionClose(position_ticket); } else if ((flags & FLAG_CLOSE_ALL_LOSS) != 0 && profit < 0) { trade.PositionClose(position_ticket); }
- & 运算符用于检查某个标志是否已启用。
- 如果 FLAG_CLOSE_ALL_PROFIT 处于激活状态且仓位盈利,则平仓。
- 如果 FLAG_CLOSE_ALL_LOSS 处于激活状态且仓位正在亏损,则平仓。
void CRiskManagemet::CloseAllPositions(int flags = FLAG_CLOSE_ALL_LOSS | FLAG_CLOSE_ALL_PROFIT) { for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong position_ticket = PositionGetTicket(i); if(!PositionSelectByTicket(position_ticket)) continue; // If you don't select the position, continue double profit = PositionGetDouble(POSITION_PROFIT); // Check flags before closing the position if((flags & FLAG_CLOSE_ALL_PROFIT) != 0 && profit > 0) // Close only profit positions { trade.PositionClose(position_ticket); } else if((flags & FLAG_CLOSE_ALL_LOSS) != 0 && profit < 0) // Close only losing positions { trade.PositionClose(position_ticket); } } }
新一天和新一周的规划事件
为了结束本文,我们将实现在特定事件期间执行的最终函数。具体来说,我们将创建两个关键函数:
- 每日事件 - 在每个新一天开始时执行。
- 每周事件 - 在每周开始时执行。
这些事件有助于以结构化的方式管理损失和利润,确保价值得到正确更新。
每日事件
每天,都有必要重新计算和设置所有最大损益的值,并将累计的每日利润重置为 0。我们还将把损益值打印到日志中以进行监控。//+------------------------------------------------------------------+ //| Function that runs every new day | //+------------------------------------------------------------------+ void CRiskManagemet::OnNewDay(void) { SetMWL(); SetMDL(); SetML(); SetMDP(); SetGMLPO(); this.daily_profit = 0; this.last_day_time = iTime(_Symbol,PERIOD_D1,0); Print(" New day "); Print(StringFormat("%-6s| %s", "Losses", "Loss")); Print(StringFormat("%-6s| %.2f", "MDP", this.mdp.value)); Print(StringFormat("%-6s| %.2f", "MWL", this.mwl.value)); Print(StringFormat("%-6s| %.2f", "ML", this.ml.value)); Print(StringFormat("%-6s| %.2f", "MDl", this.mdl.value)); Print(StringFormat("%-6s| %.2f", "GMLPO", this.gmlpo.value)); }
- 调用设置损益值所需的所有函数。
- 每日利润变量将被重置为 0,以开始新的一天,不继承之前的盈亏。
- 使用 iTime(_Symbol, PERIOD_D1, 0) 存储最后记录日期的时间戳。
- 亏损和盈利数值会打印到日志中,以监控系统行为。
每周事件
每周事件负责记录新一周的开始,并将上周累计利润重置为 0。//+------------------------------------------------------------------+ //| Function that runs every new week | //+------------------------------------------------------------------+ void CRiskManagemet::OnNewWeek(void) { this.last_weekly_time = iTime(_Symbol,PERIOD_W1,0); this.weekly_profit = 0; }
- last_weekly_time 变量会更新为新一周的开始时间。
- 为防止累积上周的盈亏,将 weekly_profit 变量重置为 0。
检测新事件(日/周/自定义周期)
为了在 EA 交易中自动执行这些函数,我们可以使用日期时间变量检查时间段是否发生了变化。这样我们就可以检测新的一天、一周或任何其他时间段(H1、H12 等)。
新日期检测示例:
datetime prev_time = 0; void OnTick() { if(prev_time != iTime(_Symbol, PERIOD_D1, 0)) { Print("New day detected"); prev_time = iTime(_Symbol, PERIOD_D1, 0); OnNewDay(); // Call the corresponding function } }
新一周检测示例:
datetime prev_week_time = 0; void OnTick() { if(prev_week_time != iTime(_Symbol, PERIOD_W1, 0)) { Print("New week detected") //Call the corresponding function prev_week_time = iTime(_Symbol, PERIOD_W1, 0); } }
解释:
- 定义了一个日期时间变量来存储之前记录的时间。
- 在每个分时报价(OnTick)上,将当前时间与存储在 prev_time 或 prev_week_time 中的值进行比较。
- 如果该值发生了变化,则表示新的一天或新的一周已经开始,此时将调用 OnNewDay() 或 OnNewWeek() 函数。
- prev_time 或 prev_week_time 变量将更新为新值。
结论
在本文中,我们开发了 CRiskManagement 风险管理类的第一部分。在这个初始阶段,我们的主要重点是为最大的亏损和利润赋值,从而为结构化风险控制奠定基础。
虽然该类尚未准备好在实时交易环境中使用,但我们已经可以看到这些值在系统中是如何分配和管理的。
在下一篇文章中,我们将通过添加关键函数来完成这个类的实现,这些函数将允许我们:
- 检查是否已超过最大亏损限额,并执行相应的措施;
- 增加新的事件以进一步改进风险管理;
- 创建特定函数,例如自动更新自营公司类型账户(如 FTMO)的最大每日损失。
通过这些改进,CRiskManagement 类将能够有效地管理风险限额并适应不同的市场条件。
本文中使用/改进的文件:
| 文件名称 | 类型 | 描述 |
|---|---|---|
| Risk_Management.mqh | .mqh(包含文件) | 包含常用函数和 CRiskManagement 类实现的主文件,该类负责系统中的风险管理。本文件定义、开发和扩展了与损益管理相关的所有函数。 |
本文由MetaQuotes Ltd译自西班牙语
原文地址: https://www.mql5.com/es/articles/17249
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
您应当知道的 MQL5 向导技术(第 59 部分):配以移动平均和随机振荡器形态的强化学习(DDPG)
克服机器学习的局限性(第二部分):缺乏可重复性
市场模拟(第 10 部分):套接字(四)
使用MQL5经济日历进行交易(第十部分):可拖动仪表盘与交互式悬停效果,实现流畅的新闻导航