风险管理(第四部分):完善关键类方法
概述
接续上一篇关于风险管理的文章,我们将解释我们专用类的主要变量和初始方法。这一次,我们将重点完成检查是否超过既定最大亏损或利润限额所需的方法。此外,我们将介绍两种动态的单笔交易风险管理方法。
文章摘要
我们将首先优化一些函数,以及类的构造函数和析构函数。然后我们将定义新的结构、枚举和核心常量。接下来,我们将实现用于管理 EA 交易开仓的函数,包括检测何时超过设定的最大盈利或亏损值的机制。最后,我们将增加管理动态单笔交易风险的方法。
新的枚举和结构
首先,让我们定义一下本文后面将要用到的新结构、枚举和常量。
1.动态单笔交易风险
如前所述,我增加了一个新概念:动态单笔交易风险。
它是什么?
动态单笔交易风险不是一个固定值,而是一个根据相对于指定初始余额的损益进行调整的变量。这对于保护交易账户特别有用。例如,如果余额减少到初始余额的某个百分比,则可以自动调整风险以尽量减少进一步的损失。这意味着每次达到指定水平时,每笔交易的风险都会进行调整。
让我们来看表格中所示的一个实际例子。 假设初始账户余额为 10000 美元。
我们选择以下阈值来调整风险:3%、5% 和 7%。
| 条件 | 新的风险百分比 |
|---|---|
| 如果余额下降 3%(即 300 美元,也就是低于 9700 美元) | 风险调整至 0.7%。 |
| 如果余额下降 5%(即 500 美元,也就是低于 9500 美元) | 风险调整至 0.5%。 |
| 如果余额下降 7%(即 700 美元,也就是低于 9300 美元) | 风险调整至 0.25%。 |
关于动态风险参数和操作的重要说明:
- 本文中提到的所有值都是完全可定制的参数,可以根据用户的需求和偏好进行调整。这包括触发风险调整的阈值和这些百分比适用的初始余额。
初始余额可以通过两种方式确定:
- 自营交易资助平台账户(例如,FTMO):如果通过风险管理参数(propfirm_ftmo)选择此选项,则必须通过 EA 交易设置中的输入参数手动输入初始余额。该余额在整个交易过程中保持不变,这意味着它不会随着时间的推移而变化,并且是用户在开始时设置的静态值。
- 个人账户:如果选择账户类型 personal_account,则在调用 AccountInfoDouble() 时,EA 交易会自动确定初始余额,该函数会在系统初始化时返回当前账户余额。在这种情况下,余额是动态的,能够准确反映当前账户资金。
- 显著降低账户爆仓或交易资金全部损失的风险。
- 通过减少连续亏损的风险来提高整体交易安全性。
动态单笔交易风险的缺点:
-
由于事先减少了投资敞口,连败或大幅亏损后的恢复速度可能会较慢。虽然恢复速度较慢,但更安全、更可控。
实现动态单笔交易风险的结构
要管理和配置动态的单笔交易风险,我们必须访问和修改 gmlpo 变量中的 assigned_percentage 属性。为了实现这些基于余额的自动调整,我们必须创建一个包含两个主要元素的结构:
- 触发风险调整的具体余额(例如,当余额从初始的 10000 美元下降到 9700 美元时,如表格所示)。
- 当达到指定水平时,对余额应用新的风险百分比(例如,在这种情况下,将风险调整为 0.7%)。
最初,你可能会考虑创建一个包含两个独立 double 型变量的简单结构,但这种方法是不够的。相反,我们在结构中使用两个独立的数组。
为什么使用数组而不是单个变量?
我们使用数组而不是单个变量,因为我们必须高效地对多个相关值进行排序。由于我们使用特定的方法进行高效的动态风险管理,因此需要进行排序。具体来说,我们使用 ArraySort() 函数对触发风险调整的余额进行排序(balance_to_activate_the_risk[])。
为什么需要使用 ArraySort() 进行排序
主要原因是,我们实现了一种无循环方法,旨在持续检查当前余额是否已超过触发风险调整的特定阈值。选择这种无循环的方法来优化系统性能和速度,这在执行频繁检查时(例如,在每个价格变动时或在每个交易结束时)至关重要。
如果数值排序不正确(即未按升序排列),可能会出现严重问题。让我们来看一个实际例子来说明这一点。
假设我们最初定义以下阈值:
- 第一阈值:3%(激活余额:9700 美元)
- 第二阈值:7%(激活余额:9300 美元)
- 第三阈值:5%(激活余额:9500 美元)
如您所见,这些值的排序不正确(第二个阈值大于第三个阈值)。排序错误的问题在于我们采用的无循环方法使用整数变量(计数器)来跟踪当前风险状态。
让我们想象一下将会发生什么:
- 当初始余额下降 3%(至 9700 美元)时,计数器加 1,风险调整为 0.7%。
- 然后,当余额下降到下一个级别(7%)时,账户达到 9300 美元,计数器再次增加,跳过中间值(余额为 9500 美元时为 5%)。
- 中间值(9500 美元)仍未使用,造成混乱和严重的计算问题,因为动态风险没有得到适当调整。
此外,如果账户从最低水平(在本例中错误地从 5%)开始恢复,则必须超过 9300 美元才能恢复到之前的值,这是不正确的。但由于原有方法没有正确考虑顺序,恢复也无法正常工作,从而导致其他错误。
因此,正确的排序对于确保无循环方法的最佳运行至关重要。我们建议的最简单结构如下:
struct Dynamic_gmlpo { double balance_to_activate_the_risk[]; double risk_to_be_adjusted[]; };
然而,尽管这种结构很简单,但仍然存在一个重要的局限性。由于这两个数组表示键值对(余额 - 调整后的风险百分比),因此在排序过程中保持这种映射关系至关重要。这就是 CHashMap 结构发挥作用的地方。
通过 CHashMap 实现,可以直接将每个特定余额与其相关的风险百分比关联起来。对主数组(余额)进行排序时,与相关风险的映射关系会自动保留。这保证了所有操作和计算的绝对准确性。
因此,我们目前实现的是一个简单的初始解决方案,使用两个独立的数组,这使我们能够暂时简化 ArraySort() 方法的应用。但是,为了获得更可靠、更准确的实现,特别是如果您计划高效地处理多个键值对,我们建议使用 CHashMap。这种结构确保了每个余额与其相关调整风险的正确映射,这有助于避免在排序和查询动态值时出现潜在错误。
在接下来的文章中,我们将探讨如何使用 CHashMap 实现此解决方案,并提供实际示例和分步说明。
余额检查
继续讨论正确动态风险实施所需的枚举,我们必须添加两个额外的选项,明确规定何时进行余额检查。此检查至关重要,因为它确定了评估当前余额是否已降至预定义百分比阈值以下的确切时刻,从而激活动态风险的变化。
主要有两种检查类型:
-
每次市场价格变动都要查看:在这种方法中,每次价格变动(分时报价)都会持续进行检查。此选项具有很高的准确性,因为它会持续检查净值是否已低于特定阈值。然而,这样做有一个明显的缺点:不断地与当前的净值进行比较可能会导致不理想或低效的情况。
例如,假设初始余额为 10000 美元,当余额达到 9700 美元时,就会触发第一个风险激活级别。如果净值在 9701 美元和 9699 美元之间波动,动态风险重新定义将多次触发和停用,这不仅不方便,而且由于检查频率高,还会给系统资源造成过大的压力。
-
交易平仓时检查:第二种方法只在平仓时进行检查,而不是在每个价格变动时进行检查。此选项在资源利用方面效率更高,因为余额仅在特定时刻进行检查。然而,它可能不太准确,因为检查仅在交易结束时进行,可能无法考虑可能引发动态风险及时变化的重大中间波动。
为了更容易在这两种方法之间进行选择,我们将使用枚举在代码中清楚地定义它们:
//--- enum ENUM_REVISION_TYPE { REVISION_ON_CLOSE_POSITION, //Check GMLPO only when closing positions REVISION_ON_TICK //Check GMLPO on all ticks };
这为用户提供了如何以及何时执行余额检查的明确选择,使他们能够根据个人需求和对系统性能和准确性的偏好来定制动态风险行为。
动态单笔交易风险(GMLPO)模式
为了管理应用动态每笔交易风险(GMLPO)的模式,我们将使用一个特殊的枚举,提供三个明显不同的选项。接下来,我将解释每个选项的目的,以及为什么我决定使用枚举来实现它们。
最初,用户输入的文本字符串用于设置风险应更改的特定百分比,并指定新的风险值。在这种原始方法中,用户必须在一行(字符串输入)中手动输入负余额百分比值,这将触发风险变化,在另一行中输入要应用的新风险值。虽然这种方法非常可行,但它有一个明显的缺点:无法使用 EA 交易优化功能自动优化文本字符串。这使得设置和测试各种场景的过程变得缓慢、不切实际且相当乏味。
为了克服这些限制,我决定实现一个枚举,允许在三种明确定义的模式之间进行轻松选择,为优化提供灵活性和便利性:
-
DYNAMIC_GMLPO_FULL_CUSTOM:此模式完全可定制,允许用户手动设置多个风险激活级别和相关的新百分比值。虽然此模式保留了文本字符串的使用,但用户可以指定所需的更改次数,以牺牲自动优化功能为代价实现最大的灵活性。
-
DYNAMIC_GMLPO_FIXED_PARAMETERS:该模式通过将允许的最大更改次数限制为四次,大大简化了动态风险配置。在这里,用户可以通过数字参数直接指定负余额百分比及其相关风险百分比,这使得优化变得更加容易。此选项在定制灵活性和自动化测试/优化性能之间提供了平衡。
-
NO_DYNAMIC_GMLPO: 此模式将完全禁用动态风险功能。它非常适合那些喜欢在整个交易过程中保持固定风险而不根据余额波动进行动态变化的用户。
//--- enum ENUM_OF_DYNAMIC_MODES_OF_GMLPO { DYNAMIC_GMLPO_FULL_CUSTOM, //Customisable dynamic risk per operation DYNAMIC_GMLPO_FIXED_PARAMETERS,//Risk per operation with fixed parameters NO_DYNAMIC_GMLPO //No dynamic risk for risk per operation };
这种基于枚举的实现提供了清晰性、便利性和轻松优化各种配置的能力,允许根据特定的用户偏好和策略快速识别最佳解决方案。
交易会计
为了改进我们的风险管理系统,我们将添加一个特殊功能,允许精确跟踪账户中的所有持仓,无论它们是由用户还是 EA 交易手动开立的。
对于由 EA 交易开仓的交易,我们还可以清楚地确定特定交易单是否与该 EA 交易分配的幻数相符。当您需要确切了解此 EA 交易开设了多少仓位,并轻松将其与手动开设的仓位区分开来时,此功能尤其有用。
为了实现这一目标,我们将定义一个简单而有效的结构,以保留每个仓位的基本信息:
struct Positions { ulong ticket; //position ticket ENUM_POSITION_TYPE type; //position type };
超过最大利润或亏损限额
现在我们将添加一个单独的枚举,指示将考虑哪些标准来确定是否超过了先前设定的最大利润或亏损的限额。
负责执行此检查的函数将在满足所选标准规定的条件时返回 true。
枚举将由三个明显不同的选项组成:
//--- Mode to check if a maximum loss or gain has been exceeded enum MODE_SUPERATE { EQUITY, //Only Equity CLOSE_POSITION, //Only for closed positions CLOSE_POSITION_AND_EQUITY//Closed positions and equity };
-
EQUITY:
此选项仅评估当前账户净值(即考虑未平和已平仓位的实时余额)。它不考虑当天已平仓交易的盈亏。只有当实时净值直接超过既定限额时,函数才会指示已超过最大盈亏限额。 -
CLOSED_POSITION:
该方法仅考虑当日已平仓的盈亏。它完全忽略了持仓和当前净值。因此,是否突破限额完全取决于已平仓交易的累积结果。 -
CLOSED_POSITION_AND_EQUITY:
这是最全面、最准确的方法,因为它结合了上述两种方法。函数同时评估当天已平仓位的盈亏情况和当前的实时权益。这意味着要分析每日总体结果,从而能够更准确、更严格地评估是否超过了既定限额。
通过在风险管理系统中使用此枚举,我们为用户提供了选择限额验证方法的灵活性,从而可以轻松适应风险管理中所需的各种策略和准确度级别。
定义
继续使用新变量,重要的是要使用 #define 指令定义一些常量。
首先,我们将设置前缀定义,以便轻松识别由我们的 EA 交易生成的操作和消息。这样的前缀可以包含 EA 交易的名称,使其在系统生成的日志或注释中易于区分。例如,在这种情况下我们将使用:
#define EA_NAME "CRiksManagement | " //Prefix
此外,我们还需要定义几个常量(标志),以便进一步精确控制持仓和挂单。这些标志可以快速识别交易类型(买入、卖出、限价单、止损单等),从而简化有效的风险管理、持仓平仓以及执行有关当前市场状况和我们持仓的特定请求。
以下是各自的定义常数:
//--- positions #define FLAG_POSITION_BUY 2 #define FLAG_POSITION_SELL 4 //--- orders #define FLAG_ORDER_TYPE_BUY 1 #define FLAG_ORDER_TYPE_SELL 2 #define FLAG_ORDER_TYPE_BUY_LIMIT 4 #define FLAG_ORDER_TYPE_SELL_LIMIT 8 #define FLAG_ORDER_TYPE_BUY_STOP 16 #define FLAG_ORDER_TYPE_SELL_STOP 32 #define FLAG_ORDER_TYPE_BUY_STOP_LIMIT 64 #define FLAG_ORDER_TYPE_SELL_STOP_LIMIT 128 #define FLAG_ORDER_TYPE_CLOSE_BY 256
这些常量将在未来的函数中提供更清晰、更高效的实现,从而显著简化 EA 交易代码的可读性、维护性和可扩展性。
构造函数和主函数的改进
我们将从 CRiskManagement 类构造函数开始优化风险管理。现在,动态单笔交易风险已经加入,代码也进行了几项重要的改进:
首先,我们明确定义了所用手数类型(type_get_lot)和与初始余额相关的参数(account_propfirm_balance),这在使用自营交易资助平台账户时非常有用。此外,请注意,EA_NAME 定义将不断显示在主类函数生成的注释中。这将有助于在终端日志中快速识别它们。
改进后的构造函数实现如下所示:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CRiskManagemet::CRiskManagemet(bool mdp_strict_, ENUM_GET_LOT type_get_lot_, 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(EA_NAME, " (Warning) No magic number has been chosen, taking into account all the magic numbers and the user's trades"); } //--- this.mdp_is_strict = mdp_strict_; this.type_get_lot = type_get_lot_; //--- this.account_balance_propfirm = account_propfirm_balance ; trade = new CTrade(); trade.SetExpertMagicNumber(this.magic_number); 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.ActivateDynamicRiskPerOperation = false; //--- 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'; //--- this.positions_open = false; this.curr_profit = 0; UpdateProfit(); //--- for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong position_ticket = PositionGetTicket(i); if(!PositionSelectByTicket(position_ticket)) continue; ulong position_magic = PositionGetInteger(POSITION_MAGIC); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); if(position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER) { this.positions_open = true; Positions new_pos; new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); new_pos.ticket = position_ticket; ExtraFunctions::AddArrayNoVerification(open_positions, new_pos); } } }
新增了一些重要变量:
- curr_profit: 存储当前利润,从而实现对结果的持续监控。
- ActivateDynamicRiskPerOperation: 决定 EA 交易运行期间是否使用动态风险的布尔变量。
- mdp_is_strict: 决定风险管理是否会严格监控 mdp 的变量。
- type_get_lot: 存储手数类型的变量。
析构函数的改进
析构函数已得到多项重大改进,尤其是在动态内存管理方面。现在使用 CheckPointer() 函数验证 CTrade 类指针,确保仅在指针是动态指针时才删除它,从而防止潜在的内存释放错误。
优化后的析构函数实现如下所示:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CRiskManagemet::~CRiskManagemet() { if(CheckPointer(trade) == POINTER_DYNAMIC) delete trade; ArrayFree(this.open_positions); ArrayFree(this.dynamic_gmlpos.balance_to_activate_the_risk); ArrayFree(this.dynamic_gmlpos.risk_to_be_adjusted); }
正确使用 CheckPointer() 可确保交易指针在需要时正确释放。此外,通过使用 ArrayFree() 函数,我们可以有效地释放类数组使用的内存,确保适当的内存管理,使实现此风险管理的 EA 交易更加稳定有效。
总体改进
我做了几项重要的改变,旨在全面加强风险管理系统,防止 EA 交易操作中的潜在错误。下文将对此进行更详细的描述。
1.检索交易量手数和止损(SL)时的检查
已添加关键检查,以确保每笔交易的风险值(gmlpo.assigned_percentage)不无效或为零。这些检查允许及时识别关键错误并向用户提供消息,从而能够快速调整不正确的设置。
主要标准是直接验证 gmlpo.assigned_percentage。如果该值小于或等于零,则会向控制台输出一条重要消息,并且该函数将终止,返回安全值以防止 EA 交易发生故障。
if(gmlpo.assigned_percentage <= 0) { PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value); return 0; }
GetSL() 函数中的示例:
//+----------------------------------------------------------------------------------+ //| 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) { if(gmlpo.assigned_percentage <= 0) { PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value); return 0; } double lot; return CalculateSL(type, this.gmlpo.value, lot, DEVIATION, STOP_LIMIT); }
GetLote() 函数中的示例:
//+-----------------------------------------------------------------------------------------------+ //| 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) { if(gmlpo.assigned_percentage <= 0) { PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value); this.lote = 0.00; return this.lote; } //--- if(this.type_get_lot == GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION) { double MaxLote = GetMaxLote(order_type); SetNMPLO(this.lote, MaxLote); PrintFormat("%s Maximum loss in case the next operation fails %.2f ", EA_NAME, this.nmlpo); } else { this.lote = GetLotByRiskPerOperation(this.gmlpo.value, order_type); } //--- return this.lote; }
2.参数赋值函数(SetEnums())的改进
此函数至关重要,必须在 OnInit() 事件中执行。现在,它包括一个额外的检查,确保根据用户选择正确赋予金额或百分比值。这种验证可以防止赋予错误的或负值,尤其是在使用固定金额(钱数)作为标准时。
改进了 SetEnums() 的实现:
//+----------------------------------------------------------------------------------------+ //| Function to set how losses or gains are calculated, | //| by percentage applied to (balance, equity, free margin or net profit) or simply money. | //+----------------------------------------------------------------------------------------+ //Note: This method is mandatory, it must be executed in the OnInit event. 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_; //-- En caso se haya escojido el modo dinero, asignamos la variable que guarda el dinero o porcentage alas varialbes correspondientes if(this.gmlpo.mode_calculation_risk == money) { this.gmlpo.value = this.gmlpo.percentage_applied_to; this.ActivateDynamicRiskPerOperation = false; } else this.gmlpo.value = 0; this.mdp.value = this.mdp.mode_calculation_risk == money ? (this.mdp.percentage_applied_to > 0 ? this.mdp.percentage_applied_to : 0) : 0; this.mdl.value = this.mdl.mode_calculation_risk == money ? (this.mdl.percentage_applied_to > 0 ? this.mdl.percentage_applied_to : 0) : 0; this.ml.value = this.ml.mode_calculation_risk == money ? (this.ml.percentage_applied_to > 0 ? this.ml.percentage_applied_to : 0) : 0; this.mwl.value = this.mwl.mode_calculation_risk == money ? (this.mwl.percentage_applied_to > 0 ? this.mwl.percentage_applied_to : 0) : 0; }
新的类变量
为了持续改进,我们将添加变量,以便更精确、更有效地控制未平仓交易。
首先,我们定义一个特殊数组来存储账户中所有未平仓位的信息,包括用户手动开仓的仓位和 EA 交易自动开仓的仓位:
//--- Positions open_positions[];
此外,我们还将添加两个关键变量:
- 布尔变量 positions_open,表示市场中是否存在持仓。该变量在优化性能方面发挥着重要作用,并且有助于避免在没有活动仓位时进行冗余检查。
//--- Boolean variable to check if there are any open operations by the EA or user bool positions_open;
-
一个名为 curr_profit 的双精度变量,它将实时存储当前累计的利润或亏损。该变量可用于快速计算当前交易状况:
//--- Variable to store the current profit of the EA or user double curr_profit;
动态单笔交易风险
为了有效管理动态单笔交易风险(动态 GMLPO),我们必须在类中添加并明确定义几个具体的变量。这些变量将提供有效的控制,允许根据用户定义的参数自动调整风险。以下是对每项内容的详细说明:
1.用于存储余额和动态风险的结构
我们将使用一个名为 Dynamic_gmlpo 的自定义结构,其中包含两个动态数组:
Dynamic_gmlpo dynamic_gmlpos;
这个结构允许存储多个特定的余额水平以及相应的风险百分比,从而促成动态风险管理。
2.用于确定动态风险检查类型的变量
我们将定义一个名为 revision_type 的枚举变量,允许用户选择如何执行动态风险检查(每次价格变动时或平仓时):
ENUM_REVISION_TYPE revision_type;
3.用于激活或停用动态风险的布尔变量
此布尔变量将存储是否使用动态每笔交易风险的决定:
bool ActivateDynamicRiskPerOperation; 4.用于存储当前动态风险索引的变量
为了精确控制动态风险等级的变化,我们使用一个索引变量来指示我们当前位于哪个动态数组元素:
int index_gmlpo; 5.用于存储基础(初始)余额的变量
此变量存储用户选择的初始或参考余额,稍后将应用指定的百分比来激活动态风险。
double chosen_balance; 6.表示下一个目标余额的变量,用于调整单笔交易风险(正向调整)。
该变量存储着要动态调整风险百分比,必须超过的下一个余额水平:
double NewBalanceToOvercome; 7.用于防止索引超出允许范围时出错的布尔变量
该布尔变量指示是否已达到用户设定的最低余额(最大允许负百分比)。如果变量的值变为 true,则暂停索引递增,以防止超出允许的最大范围。
bool TheMinimumValueIsExceeded; 8.用于存储每笔交易初始风险百分比的变量
该变量存储每次交易风险的初始百分比设置。它主要用于在余额达到较低水平后恢复时恢复原始值。
double gmlpo_percentage; 这些变量允许有效实施可靠、安全和易于管理的机制,确保在各种操作场景中清晰准确地控制动态单笔交易风险。
用于控制每日最大利润的变量
为了更严格地控制每日利润,我们将引入一个布尔变量 mdp_is_strict。该变量将帮助风险管理系统确定是否应调整最大利润的计算,以考虑日常损失。
- 如果 mdp_is_strict 设置为 true,则只有当所有先前的损失都被弥补,并且达到原始目标水平时,才会认为超过了每日最大利润。例如,如果每日最大利润为 50 美元,而当天亏损了 20 美元,那么必须总共赚取 70 美元(才能弥补 20 美元的损失并实现 50 美元的净利润)才能达到目标水平。
- 如果 mdp_is_strict 为 false,则每日亏损不影响最大利润的计算。在这种情况下,如果目标利润为 50 美元,而亏损了 40 美元,那么只需再赚 10 美元(以弥补 40 美元的亏损加上 10 美元的净利润)即可达到每日最大利润。
bool mdp_is_strict; 手数类型变量
为了简化手数赋值功能,我们将修改 GetBatch() 函数,使其不再需要手动指定手数类型。相反,手数类型将在构造函数中初始化,或者可以通过我们将开发的其他函数进行更改。这种方法可以提供更直接的配置,并降低手动输入参数时出错的风险。
ENUM_GET_LOT type_get_lot;
由于这些改进,我们希望提高交易平台上风险管理和手数分配的效率和准确性。
管理持仓
在本节中,我们将重点关注如何正确管理所有通过幻数或用户开仓的情况。 为此,我们将创建几个实用且清晰的函数,以便随时跟踪未平仓交易。
1.用于检查内部数组 (open_positions) 中是否存在单号的函数
TheTicketExists() 函数旨在快速检查特定单号是否包含在我们的内部 open_positions 数组中。在确认某个仓位是否已在管理之下,或者是否需要采取其他措施时,此操作尤为重要。
逻辑很简单:我们遍历数组,并将每个元素与提供的单号进行比较。如果找到匹配项,则返回 true;否则返回 false。
声明:
bool TheTicketExists(const ulong ticket); //Check if a ticket is in the operations array
实现:
//+------------------------------------------------------------------+ //| Function to check if the ticket is in the array | //+------------------------------------------------------------------+ bool CRiskManagemet::TheTicketExists(const ulong ticket) { for(int i = 0; i < this.GetPositionsTotal() ; i++) if(this.open_positions[i].ticket == ticket) return true; return false; }
2.获取持仓总数的函数
GetPositionsTotal() 函数简单地返回内部 open_positions 数组中存在的元素数量。它可以实时轻松快速地确定管理的仓位数量。
声明和实现:
inline int GetPositionsTotal() const { return (int)this.open_positions.Size(); } //Get the total number of open positions
3.获取带有标志的仓位总数的函数
GetPositions() 函数使用标志系统,可以根据特定标准(例如交易类型(买入或卖出))更灵活地计算持仓。为此,我们将仓位类型 (ENUM_POSITION_TYPE) 转换为与二进制标志兼容的值(通常是 2 的幂,例如 2、4、8 等)。
该逻辑涉及遍历所有持仓,并检查每个仓位是否符合指定的标志,然后对找到的每个匹配项递增计数器。
实现:
//+------------------------------------------------------------------+ //| Function to obtain the number of open positions | //+------------------------------------------------------------------+ int CRiskManagemet::GetPositions(int flags) const { int count = 0; for(int i = 0; i < ArraySize(this.open_positions) ; i++) { if(this.open_positions[i].type == POSITION_TYPE_BUY && (flags & FLAG_POSITION_BUY) != 0 ) { count++; } else if(this.open_positions[i].type == POSITION_TYPE_SELL && (flags & FLAG_POSITION_SELL) != 0 ) { count++; } } return count; }
4.用于检查当前未平仓位的函数
最后,ThereAreOpenOperations() 函数提供了一种快速有效的方法来了解我们的 EA 交易当前是否正在处理未平仓位。它只是返回内部布尔变量(positions_open)的值。
声明和实现:
inline bool ThereAreOpenOperations() const { return this.positions_open; } //Check if there are any open operations
5.附加辅助函数
除了 CRiskManagement 类本身包含的函数外,还将创建外部辅助函数,以方便执行特定任务,例如根据标志关闭订单。
5.1 根据标志关闭挂单的函数
在根据特定标准关闭订单之前,我们必须将订单类型(ENUM_ORDER_TYPE)转换为兼容的二进制标志。这一步骤对于防止执行位运算 (&) 时出错至关重要。
下面这个简单的函数可以将特定的订单类型转换为相应的标志。
如果传递了无效值或通用值(WRONG_VALUE),则该函数返回所有标志的组合:
// Converts an order type to its corresponding flag int OrderTypeToFlag(ENUM_ORDER_TYPE type) { if(type == ORDER_TYPE_BUY) return FLAG_ORDER_TYPE_BUY; else if(type == ORDER_TYPE_SELL) return FLAG_ORDER_TYPE_SELL; else if(type == ORDER_TYPE_BUY_LIMIT) return FLAG_ORDER_TYPE_BUY_LIMIT; else if(type == ORDER_TYPE_SELL_LIMIT) return FLAG_ORDER_TYPE_SELL_LIMIT; else if(type == ORDER_TYPE_BUY_STOP) return FLAG_ORDER_TYPE_BUY_STOP; else if(type == ORDER_TYPE_SELL_STOP) return FLAG_ORDER_TYPE_SELL_STOP; else if(type == ORDER_TYPE_BUY_STOP_LIMIT) return FLAG_ORDER_TYPE_BUY_STOP_LIMIT; else if(type == ORDER_TYPE_SELL_STOP_LIMIT) return FLAG_ORDER_TYPE_SELL_STOP_LIMIT; else if(type == ORDER_TYPE_CLOSE_BY) return FLAG_ORDER_TYPE_CLOSE_BY; return (FLAG_ORDER_TYPE_BUY | FLAG_ORDER_TYPE_SELL | FLAG_ORDER_TYPE_BUY_LIMIT | FLAG_ORDER_TYPE_SELL_LIMIT | FLAG_ORDER_TYPE_BUY_STOP | FLAG_ORDER_TYPE_SELL_STOP | FLAG_ORDER_TYPE_BUY_STOP_LIMIT | FLAG_ORDER_TYPE_SELL_STOP_LIMIT | FLAG_ORDER_TYPE_CLOSE_BY); }
主函数遍历所有现有订单,关闭符合指定标志的订单:
// Close all orders that match the flags in `flags` void CloseAllOrders(int flags, CTrade &obj_trade, ulong magic_number_ = NOT_MAGIC_NUMBER) { ResetLastError(); for(int i = OrdersTotal() - 1; i >= 0; i--) { ulong ticket = OrderGetTicket(i); if(OrderSelect(ticket)) { ENUM_ORDER_TYPE type_order = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE); ulong magic = OrderGetInteger(ORDER_MAGIC); int bandera = OrderTypeToFlag(type_order); if((bandera & flags) != 0 && (magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER)) { if(type_order == ORDER_TYPE_BUY || type_order == ORDER_TYPE_SELL) obj_trade.PositionClose(ticket); else obj_trade.OrderDelete(ticket); } } else { PrintFormat("Error selecting order %d, last error %d", ticket, GetLastError()); } } }
5.2 获取持仓总数的函数
此外,还添加了一个简单的外部函数来计算持仓的总数,而无需完全依赖 CRiskManagement 类。首先,我们必须将仓位类型(ENUM_POSITION_TYPE)转换为兼容的标志。
5.2.1 将 ENUM_POSITION_TYPE 转换为有效标志的函数
int PositionTypeToFlag(ENUM_POSITION_TYPE type) { if(type == POSITION_TYPE_BUY) return FLAG_POSITION_BUY; else if(type == POSITION_TYPE_SELL) return FLAG_POSITION_SELL; return FLAG_POSITION_BUY | FLAG_POSITION_SELL; }
5.2.2 根据标志检索持仓总数的函数
此函数遍历所有现有仓位,并仅统计与指定标志匹配的位置:
//--- int GetPositions(int flags = FLAG_POSITION_BUY | FLAG_POSITION_SELL, ulong magic_number_ = NOT_MAGIC_NUMBER) { int counter = 0; for(int i = PositionsTotal() - 1; i >= 0; i--) { ulong position_ticket = PositionGetTicket(i); if(!PositionSelectByTicket(position_ticket)) continue; // Si la selección falla, pasa a la siguiente posición ulong position_magic = PositionGetInteger(POSITION_MAGIC); ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); // Check if the position type matches the flags if((flags & PositionTypeToFlag(type)) != 0 && (position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER)) { counter++; } } return counter; }
处理超限
现在我们将定义几个函数,以便确定我们是否超过了最大损失或最大日利润。
我们首先开发两个主要函数:一个用于检查损失是否超标,另一个用于确认是否达到最大预期利润。
这些模式中的每一种都封装在一个函数中,根据我们选择的选项,该函数会告诉我们是否已经达到设定的目标(返回 true),或者我们是否还有很长的路要走(返回 false)。例如,在 CLOSED_POSITION 模式下,了解已经获得的利润非常重要,因为我们必须将其与已确定的最大亏损或盈利限制(每日或每周)进行比较。
让我们看看如何在代码中实现这一点:
//+------------------------------------------------------------------+ //| Boolean function to check if a loss was overcome | //+------------------------------------------------------------------+ bool CRiskManagemet::IsSuperated(double profit_, double loss_, const MODE_SUPERATE mode) const { if(loss_ <= 0 || !this.positions_open) return false; //if loss is zero return false (the loss is not being used) //--- if(mode == EQUITY) //--- { if(this.curr_profit * -1 > loss_) return true; } else if(mode == CLOSE_POSITION) { if(profit_ * -1 > loss_) return true; } else if(mode == CLOSE_POSITION_AND_EQUITY) { double new_loss = profit_ < 0 ? loss_ - MathAbs(profit_) : loss_; if(this.curr_profit * -1 > new_loss) return true; } return false; }
模式细分:
-
EQUITY:在这种模式下,我们直接比较当前利润(净值减去账户余额)。如果该负值超过规定的损失水平,则我们已超出设定的限值。
-
CLOSE_POSITION :在这种模式下,我们分析已平仓位的利润,为了进行适当的比较,将其乘以 -1。
-
CLOSE_POSITION_AND_EQUITY :这种模式比较复杂。在这里,我们调整最大损失以反映当前的利润。如果当天亏损且利润为负,则从允许的亏损限额中减去该值。如果负的当前利润超过调整后的值,则也超过了阈值。
旨在最大化利润的专门函数
与亏损一样,我们也需要一种方法来检查我们是否超过了预期的最大利润。为此,我们将创建一个专门的函数,以确保我们的交易不会无意中改变每日最大利润( mdp )的值。
以下是该函数的代码:
//+------------------------------------------------------------------+ //| Function to check if the maximum profit per day was exceeded | //+------------------------------------------------------------------+ bool CRiskManagemet::MDP_IsSuperated(const MODE_SUPERATE mode) const { if(this.mdp.value <= 0 || !this.positions_open) return false; //if loss is zero return false (the loss is not being used) //--- if(mode == EQUITY) //--- { if(this.curr_profit > this.mdp.value) return true; } else if(mode == CLOSE_POSITION) { if(this.daily_profit > this.mdp.value) return true; } else if(mode == CLOSE_POSITION_AND_EQUITY) { double new_mdp = this.daily_profit > 0 ? this.mdp.value - this.daily_profit : (this.mdp_is_strict == false ? this.mdp.value : this.mdp.value + (this.daily_profit * -1)); if(this.curr_profit > new_mdp) return true; } //--- return false; }
函数工作原理:
-
EQUITY:这里,当前利润(curr_profit)与每日最大利润(mdp)直接进行比较。如果当前利润高于目标,则视为已超额完成目标。
-
CLOSE_POSITION :这会检查当日平仓的利润(daily_profit)是否超过 mdp 的值。
-
CLOSE_POSITION_AND_EQUITY :这种情况最为复杂,涉及两种情况:
- 如果每日利润为正,则从 mdp 中减去该值,以设定新的调整后目标。
- 如果每日利润为负,且利润管理政策为严格(mdpisstrict),则在目标指标被认为超过之前,我们将该损失的绝对值添加到 mdp 中以抵消它。如果策略不是严格,我们使用 mdp 的初始值并忽略每日损失。
此功能可精确控制盈利目标,确保即使在波动较大的交易日,我们也能准确评估是否达到或超过盈利目标。
自营交易资助平台的最大损失
继续讨论损失管理的话题,需要注意的是,自营交易资助平台账户,特别是 FTMO 式账户的一个关键特点。这些账户的最大损失限额是固定的。它不会改变,从测试开始就保持不变。例如,如果我们从 10000 美元的账户余额开始,则最大损失限额为 9000 美元。这意味着,如果账户净值低于此阈值,则将自动失去获得资金的资格。
为了简化对该阈值的监控并避免其他复杂情况,我们将实现一种特殊方法,以检查是否已达到或超过最大损失限额:
//--- Function to check if the maximum loss has been exceeded in a PropFirm account of the FTMO type inline bool IsSuperatedMLPropFirm() const { return (this.ml.value == 0 || !this.positions_open) ? false : AccountInfoDouble(ACCOUNT_EQUITY) < (account_balance_propfirm - (this.ml.value)); }
逻辑很简单:如果没有未平仓位,或者控制最大损失 (ml) 的变量等于零,则跳过检查并返回 false。如果存在未平仓位且 ml 有值,则该函数会将当前净值与初始测试余额减去最大允许损失进行比较。
验证函数
除了上述方法外,我们还将创建实用函数,以快速确定是否已达到或超过预设的盈亏水平。这些函数是对通用 IsSuperated 方法的简化包装调用:
//--- functions to verify if the established losses were exceeded inline bool ML_IsSuperated(const MODE_SUPERATE mode) const {return this.mode_risk_managemet == personal_account ? IsSuperated(this.gross_profit, this.ml.value, mode) : IsSuperatedMLPropFirm(); } inline bool MWL_IsSuperated(const MODE_SUPERATE mode) const {return IsSuperated(this.weekly_profit, this.mwl.value, mode); } inline bool MDL_IsSuperated(const MODE_SUPERATE mode) const {return IsSuperated(this.daily_profit, this.mdl.value, mode); } inline bool GMLPO_IsSuperated() const {return IsSuperated(0, this.gmlpo.value, EQUITY); } inline bool NMLPO_IsSuperated() const {return IsSuperated(0, this.nmlpo, EQUITY); } bool MDP_IsSuperated(const MODE_SUPERATE mode) const;
这些功能提供了一种高效、直接的方法来评估每日、每周和其他损失限额的条件。
FTMO 最大动态日损失
FTMO 风险管理的一个关键方面是理解每日最大损失不是固定的,而是动态的。该数值会根据当日累计利润而变化。一天内获得的利润越多,允许的潜在损失空间就越大;因此,这个限制是波动的,而不是保持不变的。
以下函数会根据已实现的利润更新每日最大亏损额:
//--- Update Loss (only if ftmo propfirm FTMO is selected) inline void UpdateDailyLossFTMO() { this.mdl.value += this.daily_profit > 0 ? this.daily_profit : 0; PrintFormat("%s The maximum loss per operation has been modified, its new value: %.2f",EA_NAME,this.mdl.value); }
应该从处理 EA 交易事务的方法(即 OnTradeTransaction)中调用此函数。
//+------------------------------------------------------------------+ //| TradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result)
事件处理
现在让我们来看看 EA 交易事件管理方法中将要运行的新事件。
OnTradeTransaction
OnTradeTransaction 方法对于动态风险管理至关重要,因为它会在与账户交易操作相关的任何事件发生时触发,例如开仓或平仓、修改现有订单或为账户注资。
该函数有三个关键参数:
- trans: 现在让我们来看看 EA 交易事件管理方法中将要运行的新事件。
- request: 有关待处理或近期已完成的交易请求的信息。
- result: 处理这些请求后返回的结果。
对于风险管理而言,我们主要关注 trans 中的信息,因为它提供了有关交易类型和相关操作的准确详细信息。
枚举中定义了几种交易事务类型:
ENUM_TRADE_TRANSACTION_TYPE 交易事务类型:
| ID | 描述 |
|---|---|
| TRADE_TRANSACTION_ORDER_ADD | 添加新订单。 |
| TRADE_TRANSACTION_ORDER_UPDATE | 更新未完成的订单。此类更改包括在客户端终端或交易服务器上进行的显式编辑,以及下单期间订单状态的更改(例如,从 ORDER_STATE_STARTED 转换为 ORDER_STATE_PLACED,或从 ORDER_STATE_PLACED 转换为 ORDER_STATE_PARTIAL 等)。 |
| TRADE_TRANSACTION_ORDER_DELETE | 从未完成订单列表中移除订单。订单可以在相关请求发出后,或者在执行(成交)并添加到历史记录后,从未完成订单列表中删除。 |
| TRADE_TRANSACTION_DEAL_ADD | 向历史中增添一笔交易记录。发生在订单执行或账户余额操作执行之后。 |
| TRADE_TRANSACTION_DEAL_UPDATE | 更新历史记录中的交易。服务器上可以修改之前已完成的交易。例如,如果经纪商将交易对发送到外部交易系统(交易所)并对其进行了调整。 |
| TRADE_TRANSACTION_DEAL_DELETE | 从历史记录中删除一笔交易。服务器上已完成的交易可能会被删除。例如,如果它在经纪商发送到的外部交易系统(交易所)中被删除。 |
| TRADE_TRANSACTION_HISTORY_ADD | 在订单执行或取消后,将其添加到历史记录中。 |
| TRADE_TRANSACTION_HISTORY_UPDATE | 更新订单历史记录中的订单。这种类型也是为了扩展服务器功能而设计的。 |
| TRADE_TRANSACTION_HISTORY_DELETE | 从订单历史记录中删除订单。这种类型也是为了扩展服务器功能而设计的。 |
| TRADE_TRANSACTION_POSITION | 持仓变动与交易执行无关。这种事务类型表示交易服务器上的仓位发生了变化。仓位的交易量、开仓价格以及止损和止盈水平都可能发生变化。这些变更信息通过 OnTradeTransaction 处理函数在 MqlTradeTransaction 结构中传递。交易引起的仓位变化(增加、更新或删除)不会随后触发 TRADE_TRANSACTION_POSITION 交易。 |
| TRADE_TRANSACTION_REQUEST | 服务器已处理交易请求并返回结果的通知。对于这种交易类型,只需要分析 MqlTradeTransaction 中的一个字段:type(事务类型)。如需了解更多信息,请分析 OnTradeTransaction 的第二个和第三个参数(请求和结果)。 |
然而,重点在于以下交易类型:
- TRADE_TRANSACTION_DEAL_ADD 表示历史记录中添加了一笔新交易(已完成的交易或已确认的开仓)。
创建 OnTradeTransactionEvent 函数
接下来,我们将定义一个函数,以便清晰、高效地处理此事件:
void OnTradeTransactionEvent(const MqlTradeTransaction& trans);
该函数只需要 trans 参数,其中包含有关当前事务的所有信息。
我们的函数的基本结构首先检查事务类型是否为 TRADE_TRANSACTION_DEAL_ADD,然后从历史记录中预先选择交易:
//+------------------------------------------------------------------+ //| OnTradeTransaction Event | //+------------------------------------------------------------------+ void CRiskManagemet::OnTradeTransactionEvent(const MqlTradeTransaction &trans) { HistoryDealSelect(trans.deal); if(trans.type == TRADE_TRANSACTION_DEAL_ADD) { ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY); ulong position_magic = (ulong)HistoryDealGetInteger(trans.deal, DEAL_MAGIC); bool is_select = PositionSelectByTicket(trans.position); if(entry == DEAL_ENTRY_IN && is_select && (this.magic_number == position_magic || this.magic_number == NOT_MAGIC_NUMBER)) { Print(EA_NAME, " New position opened with ticket: ", trans.position); this.positions_open = true; Positions new_pos; new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); new_pos.ticket = trans.position; AddArrayNoVerification(open_positions, new_pos); return; } if(entry == DEAL_ENTRY_OUT && TheTicketExists(trans.position) == true && !is_select) { Print(EA_NAME, " Position with ticket ", trans.position, " has been closed"); DeleteTicket(trans.position); //--- if(this.revision_type == REVISION_ON_CLOSE_POSITION) CheckAndModifyThePercentageOfGmlpo(); //--- if(GetPositionsTotal() == 0) this.positions_open = false; //--- UpdateProfit(); //--- if(this.mode_risk_managemet == propfirm_ftmo) UpdateDailyLossFTMO(); SetGMLPO(); Print(StringFormat("%-6s| %.2f", "GMLPO", this.gmlpo.value)); } } }
在这个函数内部,我们处理两种关键场景:
-
开仓:我们确认该仓位处于未平状态(is_select)并且其幻数与指定值匹配后,才会将其添加到我们的内部列表中。
-
平仓:我们会检查该仓位是否已平仓(无法选择),以及它是否在我们管理的仓位列表中。在这种情况下,我们会从记录中删除单号,更新关键变量(例如累计利润和持仓的总体状态),并在启用 FTMO 模式时动态调整每日最大亏损。
这样,该框架可以主动响应每笔已登记的交易,并提供清晰、高效和动态的风险管理。
OnTickEvent
OnTick 事件是 EA 交易运行的关键事件之一,因为它会在每次市场价格变动时触发。
在我们的例子中,它用于更新所有未平仓位的总累计利润,如果指定了幻数,则会按幻数进行筛选。如果未设置幻数,则无论幻数如何,都会对所有未平仓位执行更新。
基本方法定义如下:
void OnTickEvent(); 为避免不必要的计算,该函数仅在存在未平仓位时执行。
该函数结构如下:
//+------------------------------------------------------------------+ //| Function to execute in OnTick | //+------------------------------------------------------------------+ void CRiskManagemet::OnTickEvent(void) { if(!positions_open) return; //--- GetPositionsProfit(); //--- if(this.revision_type == REVISION_ON_TICK) CheckAndModifyThePercentageOfGmlpo(); }流程详细说明:
-
更新总利润:
-
GetPositionsProfit() 方法检索和更新有关所管理持仓的当前盈亏信息。这样可以确保我们始终掌握有关未平仓交易整体表现的最新、准确数据。
-
-
验证并动态更新 GMLPO:
-
如果我们选择 REVIEW_ON_TICK 选项,EA 交易会在每个新的价格变动时持续检查是否超过了先前定义的盈利或亏损阈值,从而动态调整每笔交易允许的风险。这使我们能够实时调节持仓的数量(市场风险敞口水平),并提高风险管理绩效。
-
用于动态风险和处理仓位数组的函数
在本节中,我们将实现有效管理动态风险的关键函数,并处理与仓位数组相关的实际任务。这些工具在我们风险管理策略的各个阶段都将发挥作用。
将字符串转换为数据类型的函数
该函数使用模板可以轻松适应各种简单的数据类型(不包括复杂的类或结构)。它的主要目标是将字符串转换为所需的数据类型,并简化实时动态信息处理。
实现:
template <typename S> void StringToType(string token, S &value, ENUM_DATATYPE type) { if(StringLen(token) == 0) { Print("Error: String is empty."); return; } switch(type) { case TYPE_BOOL: value = (S)(StringToInteger(token) != 0); // Convertir a bool break; case TYPE_CHAR: value = (S)((char)StringToInteger(token)); // Convertir a char break; case TYPE_UCHAR: value = (S)((uchar)StringToInteger(token)); // Convertir a uchar break; case TYPE_SHORT: value = (S)((short)StringToInteger(token)); // Convertir a short break; case TYPE_USHORT: value = (S)((ushort)StringToInteger(token)); // Convertir a ushort break; case TYPE_COLOR: value = (S)((color)StringToInteger(token)); // Convertir a color break; case TYPE_INT: value = (S)(StringToColor(token)); // Convertir a int break; case TYPE_UINT: value = (S)((uint)StringToInteger(token)); // Convertir a uint break; case TYPE_DATETIME: value = (S)(StringToTime(token)); // Convertir a datetime break; case TYPE_LONG: value = (S)((long)StringToInteger(token)); // Convertir a long break; case TYPE_ULONG: value = (S)((ulong)StringToInteger(token)); // Convertir a ulong break; case TYPE_FLOAT: value = (S)((float)StringToDouble(token)); // Convertir a float break; case TYPE_DOUBLE: value = (S)(StringToDouble(token)); // Convertir a double break; case TYPE_STRING: value = (S)(token); // Mantener como string break; default: Print("Error: Unsupported data type in ConvertToType."); break; } }
此函数在处理动态风险时尤为重要,因为参数以文本字符串的形式出现,必须转换为数值变量才能进一步使用。
将文本字符串转换为基本类型数组的函数
同样,该函数使用模板来简化将文本字符串转换为所需类型的数组的过程。函数执行以下操作:
- 使用指定的分隔符(默认为逗号“,”)分割输入字符串。
- 将每个结果元素存储在一个临时数组中。
- 将每个元素转换为指定的数据类型,并将其赋值给目标数组。
//--- template <typename S> void StringToArray(S &array_receptor[], string cadena, ENUM_DATATYPE type_data, ushort separator = ',') { string result[]; int num = StringSplit(cadena, separator, result); ArrayResize(array_receptor, ArraySize(result)); for(int i = 0; i < ArraySize(array_receptor) ; i++) { S value; StringToType(result[i], value, type_data); array_receptor[i] = value; } }
例如,在实际的动态风险场景中,我们可能会收到类似“5.0,4.5,3.0”的数据,这些数据将自动转换为双精度型数组。这大大简化了我们系统中动态参数的管理。
用于处理数组的高级函数
以下是一些有助于有效管理数组的实用函数,尤其是在风险管理中使用动态策略和复杂结构时。
通过索引从数组中移除多个元素的函数
该函数允许您一次性从数组中删除多个元素,从而提高性能和可读性。主要逻辑是对要删除的索引进行排序,然后只将要保留的元素复制到原始数组中。
实现:
//--- template <typename T> void RemoveMultipleIndexes(T &arr[], int &indexes_to_remove[]) { int oldSize = ArraySize(arr); int removeSize = ArraySize(indexes_to_remove); if(removeSize == 0 || oldSize == 0) return; // Ordenamos los índices para garantizar eficiencia al recorrerlos ArraySort(indexes_to_remove); int writeIndex = 0, readIndex = 0, removeIndex = 0; while(readIndex < oldSize) { if(removeIndex < removeSize && readIndex == indexes_to_remove[removeIndex]) { removeIndex++; } else { arr[writeIndex] = arr[readIndex]; writeIndex++; } readIndex++; } ArrayResize(arr, writeIndex); }
向数组中添加元素的函数
以下函数可以轻松地向任何类型的数组添加新元素,并自动适应数据类型。
实现:
//--- template <typename X> void AddArrayNoVerification(X &array[], const X &value) { ArrayResize(array, array.Size() + 1); array[array.Size() - 1] = value; }
很简单:我们将数组大小增加 1,并将新值赋给最后一个元素。
用于通过单号移除单个项目的专用函数。
该函数专为包含 ticket 字段的结构体数组而设计。 它会根据该唯一标识符删除特定元素。
实现:
//--- template<typename T> bool RemoveIndexFromAnArrayOfPositions(T &array[], const ulong ticket) { int size = ArraySize(array); int index = -1; // Search index and move elements in a single loop for(int i = 0; i < size; i++) { if(array[i].ticket == ticket) { index = i; } if(index != -1 && i < size - 1) { array[i] = array[i + 1]; // Move the elements } } if(index == -1) return false; // Reducir el tamaño del array if(size > 1) ArrayResize(array, size - 1); else if(size <= 1) ArrayFree(array); return true; }
需要注意的是,此函数要求结构中必须包含一个名为 ticket 的元素。如果尝试将其用于不包含此元素的结构,则会发生错误:
'ticket' - undeclared identifier
in template 'bool RemoveIndexFromAnArrayOfPositions(T&[],const ulong)' specified with [T=Message]
see template instantiation 'ExtraFunctions::RemoveIndexFromAnArrayOfPositions<Message>'
1 errors, 0 warnings
在这种情况下,“Message” 结构不包含 “ticket” 元素。
用于重复文本字符串的附加函数
最后,我们得到了一个实用的函数,可以根据需要生成重复文本的次数。这对于打印表格或在视觉上分隔信息尤其有用。
实现:
string StringRepeat(string str, int count) { string result = ""; for(int i = 0; i < count; i++) result += str; return result; }
例如,当调用
StringRepeat("-", 10) мы получим "----------".
这些高级函数显著简化了数组操作,提高了代码可读性,为我们的交易框架中的动态和精确风险管理提供了高效且多功能的工具。
构建动态风险
最后,我们来到了最重要的部分:实现动态风险管理。为此,我们将使用两个关键函数,它们可以显著简化处理并提高适应性。
开始之前,您需要引入以下库:
#include <Generic\HashMap.mqh> 这个库将帮助我们正确地组织和处理与动态风险相关的数据。
动态风险初始化函数
根据前几篇文章中提出的初始概念,我们的动态风险系统将基于一个包含两个双精度型数组的结构:一个用于存储要应用的新风险值,另一个用于指定触发这些变化的余额水平或百分比。
由于 MQL5 语言的限制,无法直接将双精度型数组作为参数传递给 EA 交易。为了克服这一限制,我们将使用逗号分隔的字符串,然后使用先前实现的函数将其转换为数值数组。
主要的动态风险初始化函数将这些字符串转换为数值数组,检查重复项,并确保值已正确排序。
函数声明如下:
void SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_);
内部过程如下:
1.验证动态风险使用情况
在执行任何操作之前,我们会验证 GMLPO 百分比是否定义正确:
if(this.gmlpo.assigned_percentage == 0) return; if(this.gmlpo.mode_calculation_risk == money) { this.ActivateDynamicRiskPerOperation = false; Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' "); return; }
2.所选检查类型的用途
this.revision_type = revision_type_;
3.将字符串转换为数值数组
我们将使用之前创建的函数,把字符串转换为双精度型数组:
//--- ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ','); ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ',');
4.验证生成的数组
//--- if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 && this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1) { Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1"); this.ActivateDynamicRiskPerOperation = false; return; } if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal"); this.ActivateDynamicRiskPerOperation = false; return; } Print(EA_NAME, " Arrays before revision"); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");
接下来,我们必须确保两个数组长度相同且不为空。否则,我们将停用动态风险以避免出错。
5.清理和准备最终结构
最后,我们清空 HashMap,根据账户类型(FTMO 或个人)选择合适的参考余额,并准备一个辅助数组来存储可能存在的重复或无效索引:
balanceRiskMap.Clear(); this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm : AccountInfoDouble(ACCOUNT_BALANCE); int indexes_to_remove[];
此函数提供可靠、稳健的动态风险设置,可根据每个账户或交易策略的需求量身定制,确保有效和精确的风险管理。
6.用于向 HashMap 添加有效元素的循环
接下来,我们实现一个关键循环,该循环只向 HashMap 添加有效元素,从而确保有组织、高效的动态风险管理。在下列情况下,元素将被视为无效:
- 如果触发风险的百分比小于或等于零。
- 如果新的风险值小于或等于零。
- 如果该元素已存在于 HashMap 中(重复)。
当我们检测到无效元素时,我们会暂时将其添加到 indexes_to_remove 数组中,以便稍后删除。在评估两个数组(balance_to_activate_the_risk 和 risk_to_be_adjusted)时,只要一对值中的任何一个无效,就足以排除整个数组对,从而保持数据的完整性和一致性。
循环的实现:
//--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0) { Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0) { Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false) balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]); else ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); }
例如,假设我们有以下数组:
- [1, 3, 5]
- [0.1, 0.3, 0.0]
在这种情况下,最后一对(5 - 0.0)无效,因为调整后的风险值为零,所以 5 将从相关数组中删除。
7.删除重复项和无效元素
在识别出无效或重复的项目后,我们继续删除它们。此外,我们重新组织了主 balance_to_activate_the_risk 数组,以确保正确的顺序和数据一致性:
//--- ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove); ArraySort(dynamic_gmlpos.balance_to_activate_the_risk); ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk));
删除后,需要调整 risk_to_be_adjusted 数组的大小,以保持两个数组的一致性。
8.调整和转换风险值
接下来,我们将根据选定的账户余额,调整并将存储在 balance_to_activate_the_risk 中的百分比值转换为货币金额。risk_to_be_adjusted 数组已更新为相关值:
//--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { double value; balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value); dynamic_gmlpos.risk_to_be_adjusted[i] = value; dynamic_gmlpos.balance_to_activate_the_risk[i] = this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0)); }
9.动态风险的最终初始化
最后,我们初始化所需的变量,以确保动态风险已准备好正常工作:
//--- this.index_gmlpo = 0; this.ActivateDynamicRiskPerOperation = true; this.TheMinimumValueIsExceeded = false; this.NewBalanceToOvercome = 0.00; Print(EA_NAME, " Arrays ready: "); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");
通过这种方式,动态风险调整准确、高效,并不断适应我们交易策略的当前指标。
完整函数:
//+------------------------------------------------------------------+ //| Function to set dynamic risks per operation | //+------------------------------------------------------------------+ void CRiskManagemet::SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_) { if(this.gmlpo.assigned_percentage <= 0) return; if(this.gmlpo.mode_calculation_risk == money) { this.ActivateDynamicRiskPerOperation = false; Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' "); return; } //--- this.revision_type = revision_type_; //--- ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ','); ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ','); //--- if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 || this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1) { Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1"); this.ActivateDynamicRiskPerOperation = false; return; } if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal"); this.ActivateDynamicRiskPerOperation = false; return; } Print(EA_NAME, " Arrays before revision"); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk"); //--- balanceRiskMap.Clear(); this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm : AccountInfoDouble(ACCOUNT_BALANCE); int indexes_to_remove[]; //--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0) { Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0) { Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); continue; } if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false) balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]); else ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); } //--- ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove); ArraySort(dynamic_gmlpos.balance_to_activate_the_risk); ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk)); //--- for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { double value; balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value); dynamic_gmlpos.risk_to_be_adjusted[i] = value; dynamic_gmlpos.balance_to_activate_the_risk[i] = this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0)); } //--- this.index_gmlpo = 0; this.ActivateDynamicRiskPerOperation = true; this.TheMinimumValueIsExceeded = false; this.NewBalanceToOvercome = 0.00; Print(EA_NAME, " Arrays ready: "); PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance"); PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk"); }
对动态风险百分比 (GMLPO) 的更改函数进行清晰详细的解释
接下来,我们将逐步解释 CheckAndModifyThePercentageOfGmlpo 函数。它能够对每笔交易进行动态风险管理,并根据交易账户中达到的净值水平自动进行调整。
1.初步验证
该函数首先检查是否启用了动态风险管理。如果禁用该功能,则函数立即返回。
if(!this.ActivateDynamicRiskPerOperation) return;
2.验证当前余额
接下来,该函数检索当前账户净值(实际账户价值,包括浮动利润或亏损)。将此值与先前选择的余额(chosen_balance)进行比较。
如果当前净值超过选定的余额,并且尚未达到下一个目标余额水平,则该函数不做任何更改并返回。
double account_equity = AccountInfoDouble(ACCOUNT_EQUITY); if(account_equity > this.chosen_balance && this.NewBalanceToOvercome != this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]) return;
3.随着净值减少,风险也在变化。
如果当前净值低于指定水平,则该系统会判断下一个更低的余额水平。
-
如果余额低于某个特定水平:
- 确定该级别新的风险百分比。
- 内部变量已更新,以反映新的风险等级。
if(this.TheMinimumValueIsExceeded == false) { if(account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]) { PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); while(IsStopped() == false && index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { if(index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - 1) { if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo] > account_equity && account_equity > this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo + 1]) { this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo]; index_gmlpo ++; PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome); break; } } else if(index_gmlpo == this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - 1) { if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo] > account_equity) { this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo]; this.TheMinimumValueIsExceeded = true; PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome); PrintFormat("%s The minimum value %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); break; } } index_gmlpo++; } PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); SetGMLPO(); } }
4.随着净值再次增加,风险也随之恢复。
如果净值随后再次上涨并超过之前达到的目标水平:
-
我们的框架会再次调整每笔交易的风险,根据数组中定义的水平恢复到之前的水平。
if(this.NewBalanceToOvercome > 0.00) { if(account_equity > this.NewBalanceToOvercome) { PrintFormat("%s Equity %.2f exceeded balance to shift risk to %.2f", EA_NAME, account_equity, NewBalanceToOvercome); while(!IsStopped() && index_gmlpo > 0) { if(index_gmlpo > 0) { if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo] < account_equity && account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1]) { break; } this.index_gmlpo--; } } this.TheMinimumValueIsExceeded = false; if(this.index_gmlpo == 0) { Print(EA_NAME, " Excellent, the balance has been positively exceeded"); PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); this.gmlpo.assigned_percentage = this.gmlpo_percentage; this.NewBalanceToOvercome = 0.00; PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); SetGMLPO(); } else if(index_gmlpo > 0) { Print(EA_NAME, " Excellent, the balance has been positively exceeded"); PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]); this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo - 1]; PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome); SetGMLPO(); } } }
结论
在本系列文章中,我们逐步研究了如何开发一个全面、可靠的风险管理系统。在结论部分,我们详细介绍了动态单笔交易风险这一先进、非常有用的概念。该工具根据实际账户结果自动调整风险水平,这是保护资本和提高交易绩效的关键。
我希望这些材料对你有所帮助,特别是如果你刚刚开始 MQL5 编程的旅程。
在下一节风险管理中,我们将进一步扩展这种方法,并学习如何通过将这种先进的管理系统集成到交易机器人中,将我们在实践中研究的所有内容应用于交易。
为此,我们将使用之前开发的订单块指标:
此外,我们将清楚地看到,与没有任何控制措施相比,有效的风险管理具有哪些具体优势。最后,我们将配置特定的设置,大大简化这些高级工具在任何自动化策略中的日常使用。
本文中使用或更新的文件:
| 文件名称 | 类型 | 描述 |
|---|---|---|
| Risk_Management.mqh | .mqh(包含文件) | 主文件包含通用函数和 CRiskManagement 类的实现,该类负责系统中的风险管理。本文件定义并扩展了所有与损益管理相关的功能。 |
本文由MetaQuotes Ltd译自西班牙语
原文地址: https://www.mql5.com/es/articles/17508
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
价格行为分析工具包开发(第二十五部分):双指数移动平均线(EMA)分形突破策略
精通日志记录(第七部分):如何在图表上显示日志
新手在交易中的10个基本错误
MQL5中的ARIMA预测指标