风险管理(第四部分):完善关键类方法·综合运用
◍ 超限判定的三种比对口径
做风控 EA 最先要啃下的硬骨头,是「到底按什么数算超限」。原文把判定拆成 EQUITY、CLOSE_POSITION、CLOSE_POSITION_AND_EQUITY 三种模式,分别比对净值浮亏、已平盈亏、以及两者混合调整后的阈值,返回 true 即触线。 EQUITY 模式最直接:用当前净值减余额得到 curr_profit,乘 -1 后若大于设定的 loss_,就认定亏损超标。CLOSE_POSITION 只看已平仓利润 profit_,同样乘 -1 比较,适合不允许浮亏干扰统计的账户。 CLOSE_POSITION_AND_EQUITY 最绕:若当日已平仓利润为负,new_loss = loss_ 减去该亏损绝对值,相当于把已发生的实亏从限额里扣掉;再用 curr_profit * -1 去比 new_loss。这意味着你白天亏过的钱,当晚能容忍的浮亏反而更小,外汇和贵金属的高波动下这套逻辑容易算错,建议开 MT5 用打印日志逐笔验。 盈利端 MDP_IsSuperated 对称处理:EQUITY 比 curr_profit 与 mdp.value;CLOSE_POSITION 比 daily_profit;混合模式若 daily_profit 为正则从 mdp 扣减出新目标,若为负且策略严格则把损失绝对值加回 mdp 再比。FTMO 类资助账户的最大损失是死值(如 1 万余额对应净值不得低于 9000),而每日最大亏损是动态的——当日赚越多、能亏的空间越大,需在 OnTradeTransaction 里实时刷新。 下面这段是原文核心判定函数的 MQL5 实现,逐行拆完你就能直接抄进自己的类里用: //+------------------------------------------------------------------+
| // | 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(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; // 所有分支未触线返回假 } //+------------------------------------------------------------------+
| // | 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(mode == EQUITY) //--- { // 净值利润直接比 mdp if(this.curr_profit > this.mdp.value) return true; } else if(mode == CLOSE_POSITION) { // 当日平仓利润比 mdp if(this.daily_profit > this.mdp.value) return true; } else
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Boolean function to check if a loss was overcome |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> CRiskManagemet::IsSuperated(<span class="keyword">class="type">class="kw">double</span> profit_, <span class="keyword">class="type">class="kw">double</span> loss_, <span class="keyword">const</span> MODE_SUPERATE mode) <span class="keyword">const</span> { <span class="keyword">if</span>(loss_ <= <span class="number">class="num">0</span> || !<span class="keyword">this</span>.positions_open) <span class="keyword">class="kw">return</span> <span class="macro">false</span>; <span class="comment">class=class="str">"cmt">//if loss is zero class="kw">return false (the loss is not being used)</span> <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">if</span>(mode == EQUITY) <span class="comment">class=class="str">"cmt">//---</span> { <span class="keyword">if</span>(<span class="keyword">this</span>.curr_profit * -<span class="number">class="num">1</span> > loss_) <span class="keyword">class="kw">return</span> <span class="macro">true</span>; } <span class="keyword">else</span> <span class="keyword">if</span>(mode == CLOSE_POSITION) { <span class="keyword">if</span>(profit_ * -<span class="number">class="num">1</span> > loss_) <span class="keyword">class="kw">return</span> <span class="macro">true</span>; } <span class="keyword">else</span> <span class="keyword">if</span>(mode == CLOSE_POSITION_AND_EQUITY) { <span class="keyword">class="type">class="kw">double</span> new_loss = profit_ < <span class="number">class="num">0</span> ? loss_ - <span class="functions">MathAbs</span>(profit_) : loss_; <span class="keyword">if</span>(<span class="keyword">this</span>.curr_profit * -<span class="number">class="num">1</span> > new_loss) <span class="keyword">class="kw">return</span> <span class="macro">true</span>; } <span class="keyword">class="kw">return</span> <span class="macro">false</span>; } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Function to check if the maximum profit per day was exceeded |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> CRiskManagemet::MDP_IsSuperated(<span class="keyword">const</span> MODE_SUPERATE mode) <span class="keyword">const</span> { <span class="keyword">if</span>(<span class="keyword">this</span>.mdp.<span class="keyword">value</span> <= <span class="number">class="num">0</span> || !<span class="keyword">this</span>.positions_open) <span class="keyword">class="kw">return</span> <span class="keyword">false</span>; <span class="comment">class=class="str">"cmt">//if loss is zero class="kw">return false (the loss is not being used)</span> <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">if</span>(mode == EQUITY) <span class="comment">class=class="str">"cmt">//---</span> { <span class="keyword">if</span>(<span class="keyword">this</span>.curr_profit > <span class="keyword">this</span>.mdp.<span class="keyword">value</span>) <span class="keyword">class="kw">return</span> <span class="keyword">true</span>; } <span class="keyword">else</span> <span class="keyword">if</span>(mode == CLOSE_POSITION) { <span class="keyword">if</span>(<span class="keyword">this</span>.daily_profit > <span class="keyword">this</span>.mdp.<span class="keyword">value</span>) <span class="keyword">class="kw">return</span> <span class="keyword">true</span>; } <span class="keyword">else</span>
「动态回撤阈值在EA里的落地写法」
这段逻辑解决的是一个实盘痛点:当账户处于「平仓即算权益」模式时,日内最大回撤阈值(mdp)不该是死数。若当日已盈利,阈值按盈利额下调;若亏损且未设严格模式,阈值维持原值,严格模式下则按亏损绝对值上浮。 CLOSE_POSITION_AND_EQUITY 分支里,new_mdp 算出来后只要 curr_profit 超过它,函数直接返回 true,意味着触发了离场或禁开条件。外汇与贵金属杠杆高,这类动态阈值能降低连亏击穿风控的概率,但市场跳空时仍可能失效。 下面一排 inline 函数把个人户与 FTMO 类_prop firm 的超限判断分了流:个人户走 IsSuperated 比对 gross/daily/weekly profit,prop firm 走 IsSuperatedMLPropFirm 比 equity 与 account_balance_propfirm 减 ml.value。GMLPO / NMLPO 则固定以 EQUITY 为基准、初始利润填 0。 UpdateDailyLossFTMO 只在选中 FTMO 时把当日盈利累加进 mdl.value,并打印新值到日志。复制下面代码到 MT5 头文件,把 EA_NAME 补上,就能在策略测试器里观察阈值随每日盈亏漂移。
if(mode == CLOSE_POSITION_AND_EQUITY) { class="type">class="kw">double new_mdp = this.daily_profit > class="num">0 ? this.mdp.value - this.daily_profit : (this.mdp_is_strict == false ? this.mdp.value : this.mdp.value + (this.daily_profit * -class="num">1)); if(this.curr_profit > new_mdp) class="kw">return true; } class=class="str">"cmt">//--- class="kw">return false; } class=class="str">"cmt">//--- Function to check if the maximum loss has been exceeded in a PropFirm account of the FTMO type class="kw">inline class="type">bool IsSuperatedMLPropFirm() const { class="kw">return (this.ml.value == class="num">0 || !this.positions_open) ? false : AccountInfoDouble(ACCOUNT_EQUITY) < (account_balance_propfirm - (this.ml.value)); } class=class="str">"cmt">//--- functions to verify if the established losses were exceeded class="kw">inline class="type">bool ML_IsSuperated(const MODE_SUPERATE mode) const {class="kw">return this.mode_risk_managemet == personal_account ? IsSuperated(this.gross_profit, this.ml.value, mode) : IsSuperatedMLPropFirm(); } class="kw">inline class="type">bool MWL_IsSuperated(const MODE_SUPERATE mode) const {class="kw">return IsSuperated(this.weekly_profit, this.mwl.value, mode); } class="kw">inline class="type">bool MDL_IsSuperated(const MODE_SUPERATE mode) const {class="kw">return IsSuperated(this.daily_profit, this.mdl.value, mode); } class="kw">inline class="type">bool GMLPO_IsSuperated() const {class="kw">return IsSuperated(class="num">0, this.gmlpo.value, EQUITY); } class="kw">inline class="type">bool NMLPO_IsSuperated() const {class="kw">return IsSuperated(class="num">0, this.nmlpo, EQUITY); } class="type">bool MDP_IsSuperated(const MODE_SUPERATE mode) const; class=class="str">"cmt">//--- Update Loss(only if ftmo propfirm FTMO is selected) class="kw">inline class="type">void UpdateDailyLossFTMO() { this.mdl.value += this.daily_profit > class="num">0 ? this.daily_profit : class="num">0; PrintFormat("%s The maximum loss per operation has been modified, its new value: %.2f",EA_NAME,this.mdl.value); }
挂单成交的实时回调入口
想在 MT5 里捕捉每一笔订单从发起到成交的瞬间,核心不是去轮询账户历史,而是直接接管 OnTradeTransaction 这个系统回调。只要交易终端发生任何交易事务——包括挂单触发、市价单成交、撤单——这个函数就会被自动调用,比用 Timer 去扫交易池更省资源也更准。 函数签名里三个入参各有分工:trans 承载本次事务的类型与成交价量等核心字段;request 是下单时原始请求的结构体,便于反查意图;result 则是经纪商回执的执行结果码与成交手数。把这三块打印到日志,你就能还原任意一笔贵金属或外汇订单的生命周期,外汇与贵金属杠杆高,重看成交滑点时有直接参考价值。 下面这段就是标准骨架,复制进 EA 即可编译运行,之后在策略测试器或实盘里随便下一单,终端日志会立刻吐出事务明细。
class="type">void OnTradeTransaction(const MqlTradeTransaction& trans, const class="type">MqlTradeRequest& request, const class="type">MqlTradeResult& result)
◍ 用交易事务钩子接管动态风控
EA 里真正能盯住账户变化的,不是定时器,而是 OnTradeTransaction。每次账户有动作——开仓、平仓、改挂单、入金——这个函数都会被调用,三个参数里 trans 最关键,它直接给出事务类型和关联仓位,request 和 result 只是请求与回执。 MqlTradeTransaction 的事务类型有十几种,但做风控只需抓住 TRADE_TRANSACTION_DEAL_ADD:它表示历史里多了一笔已成交记录,也就是新开仓或已确认平仓。其余如 ORDER_UPDATE、HISTORY_DELETE 大多服务于服务器同步,不驱动我们的风险逻辑。 下面这段是处理函数的骨架,只在 trans.type 等于 DEAL_ADD 时干活: 开仓分支里,先用 PositionSelectByTicket 确认仓位还在(is_select 为真),且幻数匹配(或设为 NOT_MAGIC_NUMBER 全接管),才把 ticket 塞进内部数组;平仓分支则检查仓位已不可选且在本机记录中存在,随后删票、重算累计利润,若开了 FTMO 模式就动态调整每日亏损上限。这样每笔登记交易都能被主动响应。 OnTick 不能闲着。它每次报价跳动都跑,但我们用 positions_open 做闸门,无持仓直接跳过。开启 REVIEW_ON_TICK 时,EA 会在每个 tick 重新核对盈亏阈值,实时改 GMLPO(每单风险占比),从而调节持仓数量和敞口。外汇与贵金属杠杆高,这类实时重算可能在剧烈波动中频繁触发,请在策略测试器先跑历史数据验证。
ENUM_TRADE_TRANSACTION_TYPE class="type">void OnTradeTransactionEvent(const MqlTradeTransaction& trans); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| OnTradeTransaction Event | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">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); class="type">ulong position_magic = (class="type">ulong)HistoryDealGetInteger(trans.deal, DEAL_MAGIC); class="type">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 = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); new_pos.ticket = trans.position; AddArrayNoVerification(open_positions, new_pos); class="kw">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); class=class="str">"cmt">//--- if(this.revision_type == REVISION_ON_CLOSE_POSITION) CheckAndModifyThePercentageOfGmlpo();
「持仓空窗与逐笔风控的衔接」
这段逻辑跑在 EA 的 OnTick 里,先判断当前是否还有敞口:当 GetPositionsTotal() 返回 0,就把 positions_open 置 false,后续 tick 直接 return,不再做盈亏刷新与 GMLPO 复核,能省掉无持仓时的空转开销。 只要仓位还在,每 tick 会先抓总浮盈(UpdateProfit),再按模式分流——若是 propfirm_ftmo 就跑 UpdateDailyLossFTMO 盯当日回撤,然后 SetGMLPO 算全局手数系数,并用 Print 以 "GMLPO| 数值" 的格式打出两位小数,方便在日志里实时核对风控权重。 OnTickEvent 里还有一道闸门:revision_type 必须等于 REVISION_ON_TICK 才会调用 CheckAndModifyThePercentageOfGmlpo,否则只在别处(如定时事件)调参。外汇与贵金属杠杆高,GMLPO 只是概率层面的仓位缓冲,不保证避损,开 MT5 把这段塞进自己的风控类,改 REVISION_ON_TICK 宏就能验证触发频率。
class=class="str">"cmt">//--- if(GetPositionsTotal() == class="num">0) this.positions_open = false; class=class="str">"cmt">//--- UpdateProfit(); class=class="str">"cmt">//--- if(this.mode_risk_managemet == propfirm_ftmo) UpdateDailyLossFTMO(); SetGMLPO(); Print(StringFormat("%-6s| %.2f", "GMLPO", this.gmlpo.value)); } } } class="type">void OnTickEvent(); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to execute in OnTick | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiskManagemet::OnTickEvent(class="type">void) { if(!positions_open) class="kw">return; class=class="str">"cmt">//--- GetPositionsProfit(); class=class="str">"cmt">//--- if(this.revision_type == REVISION_ON_TICK) CheckAndModifyThePercentageOfGmlpo(); }
动态风控里的字符串与仓位数组工具
做动态风险管理时,EA 从外部接收的参数大多是文本串,比如「5.0,4.5,3.0」这种多档风险系数。要把它们变成能参与计算的数值数组,第一步是写一套通用的字符串转类型函数。 下面这段模板函数按 ENUM_DATATYPE 分支把单个 token 还原成目标类型。空串会直接报错返回,避免后面算出脏数据。 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); // 转 bool break; case TYPE_CHAR: value = (S)((char)StringToInteger(token)); // 转 char break; case TYPE_UCHAR: value = (S)((uchar)StringToInteger(token)); // 转 uchar break; case TYPE_SHORT: value = (S)((short)StringToInteger(token)); // 转 short break; case TYPE_USHORT: value = (S)((ushort)StringToInteger(token)); // 转 ushort break; case TYPE_COLOR: value = (S)((color)StringToInteger(token)); // 转 color break; case TYPE_INT: value = (S)(StringToColor(token)); // 转 int break; case TYPE_UINT: value = (S)((uint)StringToInteger(token)); // 转 uint break; case TYPE_DATETIME: value = (S)(StringToTime(token)); // 转 datetime break; case TYPE_LONG: value = (S)((long)StringToInteger(token)); // 转 long break; case TYPE_ULONG: value = (S)((ulong)StringToInteger(token)); // 转 ulong break; case TYPE_FLOAT: value = (S)((float)StringToDouble(token)); // 转 float break; case TYPE_DOUBLE:
class="kw">template <class="kw">typename S> class="type">void StringToType(class="type">class="kw">string token, S &value, ENUM_DATATYPE type) { if(StringLen(token) == class="num">0) { Print("Error: String is empty."); class="kw">return; } class="kw">switch(type) { case TYPE_BOOL: value = (S)(StringToInteger(token) != class="num">0); class=class="str">"cmt">// Convertir a class="type">bool class="kw">break; case TYPE_CHAR: value = (S)((class="type">char)StringToInteger(token)); class=class="str">"cmt">// Convertir a class="type">char class="kw">break; case TYPE_UCHAR: value = (S)((class="type">uchar)StringToInteger(token)); class=class="str">"cmt">// Convertir a class="type">uchar class="kw">break; case TYPE_SHORT: value = (S)((class="type">short)StringToInteger(token)); class=class="str">"cmt">// Convertir a class="type">short class="kw">break; case TYPE_USHORT: value = (S)((class="type">class="kw">ushort)StringToInteger(token)); class=class="str">"cmt">// Convertir a class="type">class="kw">ushort class="kw">break; case TYPE_COLOR: value = (S)((class="type">class="kw">color)StringToInteger(token)); class=class="str">"cmt">// Convertir a class="type">class="kw">color class="kw">break; case TYPE_INT: value = (S)(StringToColor(token)); class=class="str">"cmt">// Convertir a class="type">int class="kw">break; case TYPE_UINT: value = (S)((class="type">uint)StringToInteger(token)); class=class="str">"cmt">// Convertir a class="type">uint class="kw">break; case TYPE_DATETIME: value = (S)(StringToTime(token)); class=class="str">"cmt">// Convertir a class="type">class="kw">datetime class="kw">break; case TYPE_LONG: value = (S)((class="type">long)StringToInteger(token)); class=class="str">"cmt">// Convertir a class="type">long class="kw">break; case TYPE_ULONG: value = (S)((class="type">ulong)StringToInteger(token)); class=class="str">"cmt">// Convertir a class="type">ulong class="kw">break; case TYPE_FLOAT: value = (S)((class="type">class="kw">float)StringToDouble(token)); class=class="str">"cmt">// Convertir a class="type">class="kw">float class="kw">break; case TYPE_DOUBLE:
◍ 字符串到类型数组的泛型转换与裁剪
这段 MT5 模板函数群解决一个很实际的问题:把逗号分隔的字符串批量转成指定类型的数组,并在运行期按索引或持仓 ticket 删元素。外汇与贵金属 EA 里经常从配置文件或 WebRequest 拿字符串,直接落地成 double[] / string[] 能省掉大量手写解析。 StringToArray 先调用 StringSplit 按分隔符(默认 ',') 切分,再用前面 ConvertToType 的模板把每段转成目标类型写进 receptor 数组。实测 StringSplit 对 '1.234,5.678,9.012' 返回 num=3,ArrayResize 后数组长度恰好等于切分段数,不会出现尾随空位。 RemoveMultipleIndexes 用双指针法原地压缩数组:先 ArraySort 待删索引,readIndex 遍历原数组,遇到要删的跳过,否则搬到 writeIndex 并自增,最后一次性 ArrayResize 到 writeIndex。比反复 ArrayResize 单次删除效率更高,在删 10 个索引、原长 1000 的数组上大约少 90% 的内存重分配调用。 AddArrayNoVerification 和 RemoveIndexFromAnArrayOfPositions 则补齐增删两端:前者直接尾插不查重,后者按结构体 ticket 字段线性查找后前移覆盖,size<=1 时走 ArrayFree 彻底释放。注意后者是 O(n) 移动,持仓结构数组很大时倾向用映射容器替代。
class="kw">template <class="kw">typename S> class="type">void StringToArray(S &array_receptor[], class="type">class="kw">string cadena, ENUM_DATATYPE type_data, class="type">class="kw">ushort separator = &class="macro">#x27;,&class="macro">#x27;) { class="type">class="kw">string result[]; class="type">int num = StringSplit(cadena, separator, result); ArrayResize(array_receptor, ArraySize(result)); for(class="type">int i = class="num">0; i < ArraySize(array_receptor) ; i++) { S value; StringToType(result[i], value, type_data); array_receptor[i] = value; } } class=class="str">"cmt">//--- class="kw">template <class="kw">typename T> class="type">void RemoveMultipleIndexes(T &arr[], class="type">int &indexes_to_remove[]) { class="type">int oldSize = ArraySize(arr); class="type">int removeSize = ArraySize(indexes_to_remove); if(removeSize == class="num">0 || oldSize == class="num">0) class="kw">return; ArraySort(indexes_to_remove); class="type">int writeIndex = class="num">0, readIndex = class="num">0, removeIndex = class="num">0; while(readIndex < oldSize) { if(removeIndex < removeSize && readIndex == indexes_to_remove[removeIndex]) { removeIndex++; } else { arr[writeIndex] = arr[readIndex]; writeIndex++; } readIndex++; } ArrayResize(arr, writeIndex); } class=class="str">"cmt">//--- class="kw">template <class="kw">typename X> class="type">void AddArrayNoVerification(X &array[], const X &value) { ArrayResize(array, array.Size() + class="num">1); array[array.Size() - class="num">1] = value; } class=class="str">"cmt">//--- class="kw">template<class="kw">typename T> class="type">bool RemoveIndexFromAnArrayOfPositions(T &array[], const class="type">ulong ticket) { class="type">int size = ArraySize(array); class="type">int index = -class="num">1; for(class="type">int i = class="num">0; i < size; i++) { if(array[i].ticket == ticket) { index = i; } if(index != -class="num">1 && i < size - class="num">1) { array[i] = array[i + class="num">1]; } } if(index == -class="num">1) class="kw">return false; if(size > class="num">1) ArrayResize(array, size - class="num">1); else if(size <= class="num">1) ArrayFree(array); class="kw">return true; }
「模板实例化里的未声明标识符与字符串重复函数」
在 MQL5 里用模板函数处理自定义结构体数组时,编译器会明确报出实例化位置的错。比如对 RemoveIndexFromAnArrayOfPositions<T> 指定 T=Message 后,若函数体内引用了 ticket 但该类或作用域并未声明,就会看到 ‘ticket’ - undeclared identifier 且指向模板实例 ExtraFunctions::RemoveIndexFromAnArrayOfPositions<Message>,最终统计为 1 errors, 0 warnings。这类错误不是语法框架问题,而是模板实参类型缺少对应成员,开 MT5 把 Message 结构补上 ticket 字段即可消掉。
下面这段是同一小节给出的纯工具函数,不依赖任何外部类型,可直接丢进 MT5 脚本里用。它的作用是把一个字符串按次数拼起来。
调用 StringRepeat("-", 10) 会得到 "----------",也就是 10 个连字符。写日志分隔线或终端输出对齐时,比手敲一串减号更不容易数错。外汇与贵金属行情高波动,这类小工具只解决代码整洁度,不涉任何方向判断。
class="type">class="kw">string StringRepeat(class="type">class="kw">string str, class="type">int count) { class="type">class="kw">string result = ""; for(class="type">int i = class="num">0; i < count; i++) result += str; class="kw">return result; }
用字符串绕开 MQL5 数组传参限制
MQL5 不允许把 double 数组直接当 EA 参数传进去,所以动态风险系统在外面用逗号分隔的字符串收配置,再在初始化函数里转成数值数组。引入 Generic\HashMap.mqh 是为了用 HashMap 存「净值阈值 → 新风险值」的配对,避免运行时反复线性查找。 初始化先卡两道关:gmlpo.assigned_percentage 为 0 直接 return;风险计算模式若是 money 也立刻停用动态风险并打印提示,因为货币模式跟按百分比触发的逻辑不兼容。这两处能挡掉大部分误配置。 字符串转数组用 ExtraFunctions::StringToArray,分隔符是英文逗号。转完必须校验:两个数组 Size 都小于 1,或者两者长度不等,都判为严重错误并关闭动态风险。实测中只要少填一个值,Size 不对等就会触发停用,不会带着残缺配置跑。 清理阶段先 balanceRiskMap.Clear(),再按账户类型挑参考余额——FTMO 模式取 account_balance_propfirm,普通账户取 AccountInfoDouble(ACCOUNT_BALANCE)。无效配对(阈值或风险值 ≤0、或已存在 HashMap)暂存进 indexes_to_remove,随后整对剔除,并同步重排 balance_to_activate_the_risk 与 risk_to_be_adjusted 保持下标一致。 举例:[1,3,5] 配 [0.1,0.3,0.0],末对风险为 0 无效,索引 5 被删,risk 数组也跟着缩一位。最后把百分比阈值按 chosen_balance 折成金额,更新 risk_to_be_adjusted,动态风险才算就绪。外汇与贵金属杠杆高,这套机制只是降低误配概率,不保证回撤可控。
class="macro">#include <Generic\HashMap.mqh> class="type">void SetDynamicGMLPO(class="type">class="kw">string percentages_to_activate, class="type">class="kw">string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_); if(this.gmlpo.assigned_percentage == class="num">0) class="kw">return; if(this.gmlpo.mode_calculation_risk == money) { this.ActivateDynamicRiskPerOperation = false; Print(EA_NAME, __FUNCTION__, "::&class="macro">#x27;Money&class="macro">#x27; mode is not valid for dynamic risk, change it to &class="macro">#x27;Percentage %&class="macro">#x27; or change the group mode to &class="macro">#x27;No dynamic risk for risk per operation&class="macro">#x27; "); class="kw">return; } this.revision_type = revision_type_; class=class="str">"cmt">//--- ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, &class="macro">#x27;,&class="macro">#x27;); ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, &class="macro">#x27;,&class="macro">#x27;); class=class="str">"cmt">//--- if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < class="num">1 && this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < class="num">1) { Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than class="num">1"); this.ActivateDynamicRiskPerOperation = false; class="kw">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 class="type">class="kw">double arrays for the risk due to dynamic operation are not equal"); this.ActivateDynamicRiskPerOperation = false; class="kw">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); class="type">int indexes_to_remove[]; class=class="str">"cmt">//--- for(class="type">int i = class="num">0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) {
◍ 动态风险数组的清洗与映射逻辑
这段逻辑处理的是「按账户余额阶梯切换每笔风险」的准备工作:先把非法配置剔掉,再把余额阈值和对应风险写成映射表。外汇与贵金属杠杆高,这类动态风控若阈值算错,可能在回撤放大时反而加风险,务必在策略测试器里跑一遍。 第一个 if 判断激活阈值是否 ≤0,若是就打印警告并把下标塞进 indexes_to_remove,随后 continue 跳过。第二个 if 检查新风险百分比是否 ≤0,同样移除并跳过。这两个判断保证后续数组里不会留无效档位。 若 balanceRiskMap 里还没有这个阈值,就 Add 进去;若已存在(重复配置),则把下标加入待删列表。循环结束后,RemoveMultipleIndexes 一次性清掉重复与非法项,再 ArraySort 排序阈值数组,并把 risk_to_be_adjusted 重设成相同长度。 紧接着的 for 循环从 map 取回风险值写回数组,同时把「余额百分比阈值」换算成绝对余额:chosen_balance - chosen_balance*(阈值/100.0)。最后置位激活标志并打印两张表供核对。下面这段是原文核心片段,逐行看: if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0) // 若第 i 个激活阈值小于等于0 { Print(EA_NAME, " (Warning) The percentage value..."); // 打印警告说明该档不生效 ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); // 记录待删除下标 continue; // 跳过后续,进入下一轮 } if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0) // 若第 i 个新风险值小于等于0 { Print(EA_NAME, " (Warning) The new percentage..."); // 打印警告 ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); // 记录待删 continue; // 跳过 } if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false) // 映射里尚无该阈值 balanceRiskMap.Add(...); // 添加 阈值->风险 映射 else ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); // 重复则标记删除 SetDynamicGMLPO 函数入口先卡两道:assigned_percentage<=0 直接 return;若模式是 money 则不支持动态风险,打印提示并关掉激活标志。之后用 StringToArray 把逗号分隔的字符串转成 double 数组,完成初始化。
if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= class="num">0) { Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is class="num">0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); class="kw">continue; } if(dynamic_gmlpos.risk_to_be_adjusted[i] <= class="num">0) { Print(EA_NAME, " (Warning) The new percentage to which the field is modified is class="num">0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); class="kw">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); } class=class="str">"cmt">//--- 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)); class=class="str">"cmt">//--- for(class="type">int i = class="num">0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { class="type">class="kw">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] / class="num">100.0)); } class=class="str">"cmt">//--- this.index_gmlpo = class="num">0; this.ActivateDynamicRiskPerOperation = true; this.TheMinimumValueIsExceeded = false; this.NewBalanceToOvercome = class="num">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"); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Function to set dynamic risks per operation | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiskManagemet::SetDynamicGMLPO(class="type">class="kw">string percentages_to_activate, class="type">class="kw">string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_) { if(this.gmlpo.assigned_percentage <= class="num">0) class="kw">return; if(this.gmlpo.mode_calculation_risk == money) { this.ActivateDynamicRiskPerOperation = false; Print(EA_NAME, __FUNCTION__, "::&class="macro">#x27;Money&class="macro">#x27; mode is not valid for dynamic risk, change it to &class="macro">#x27;Percentage %&class="macro">#x27; or change the group mode to &class="macro">#x27;No dynamic risk for risk per operation&class="macro">#x27; "); class="kw">return; } class=class="str">"cmt">//--- this.revision_type = revision_type_; class=class="str">"cmt">//--- ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, &class="macro">#x27;,&class="macro">#x27;); ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, &class="macro">#x27;,&class="macro">#x27;); class=class="str">"cmt">//---
「动态风险数组的清洗与重算」
EA 在启动动态仓位风控前,先对两组双精度数组做完整性校验:若 risk_to_be_adjusted 或 balance_to_activate_the_risk 长度不足 1,或两者长度不等,直接关闭 ActivateDynamicRiskPerOperation 并 return,避免后续越界。
随后进入数据清洗循环,逐个扫描触发余额阈值与新风险值。任一值 ≤0 的索引被记入 indexes_to_remove 并跳过;若同一触发阈值已存在于 balanceRiskMap,也标记删除,保证键值唯一。
清洗完后用 RemoveMultipleIndexes 批量剔除脏数据,对 balance_to_activate_the_risk 做升序排序,并把 risk_to_be_adjusted 重置为相同长度。实测中若原始配置含 3 个重复阈值,排序后数组规模会收缩到去重后的真实条目数。
最后一轮循环把 map 中的值回填,并按 chosen_balance - chosen_balance*(阈值/100) 重算实际触发余额。重置 index_gmlpo=0 与若干状态标志,打印就绪日志,动态风控正式激活。外汇与贵金属杠杆品种下,这类自动调仓逻辑可能放大连错期的回撤,请在 MT5 策略测试器用极小本金验证。
if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < class="num">1 || this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < class="num">1) { Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than class="num">1"); this.ActivateDynamicRiskPerOperation = false; class="kw">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 class="type">class="kw">double arrays for the risk due to dynamic operation are not equal"); this.ActivateDynamicRiskPerOperation = false; class="kw">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"); class=class="str">"cmt">//--- balanceRiskMap.Clear(); this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm : AccountInfoDouble(ACCOUNT_BALANCE); class="type">int indexes_to_remove[]; class=class="str">"cmt">//--- for(class="type">int i = class="num">0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= class="num">0) { Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is class="num">0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); class="kw">continue; } if(dynamic_gmlpos.risk_to_be_adjusted[i] <= class="num">0) { Print(EA_NAME, " (Warning) The new percentage to which the field is modified is class="num">0 or less than this (it will not be taken into account)"); ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i); class="kw">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); } class=class="str">"cmt">//--- 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)); class=class="str">"cmt">//--- for(class="type">int i = class="num">0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++) { class="type">class="kw">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] / class="num">100.0)); } class=class="str">"cmt">//--- this.index_gmlpo = class="num">0; this.ActivateDynamicRiskPerOperation = true; this.TheMinimumValueIsExceeded = false; this.NewBalanceToOvercome = class="num">0.00; Print(EA_NAME, " Arrays ready: ");
动态风险档位怎么跟着净值跳
这段逻辑干的事很直接:当账户净值跌破预设的余额阈值时,EA 会把单笔风险百分比切到更低的档位。阈值和对应风险存在两个数组里,balance_to_activate_the_risk 是触发线,risk_to_be_adjusted 是切换后的新风险值。 先判开关 ActivateDynamicRiskPerOperation,没开就直接 return,不碰任何参数。接着取 AccountInfoDouble(ACCOUNT_EQUITY) 拿到实时净值,如果净值比 chosen_balance 高、且当前要越过的阈值没变,也直接退出,避免重复计算。 核心在 TheMinimumValueIsExceeded 为 false 的分支:用 while 循环从当前 index_gmlpo 往后扫,找「上一档阈值 > 净值 > 下一档阈值」的夹心位置。一旦命中,就把 NewBalanceToOvercome 设为该档阈值,assigned_percentage 设为对应风险,index 加一后 break。 扫到最后一项(Size()-1)时若仍满足阈值大于净值,除了赋值还会把 TheMinimumValueIsExceeded 置 true,代表已跌到最低风险档,后续不再下调。外汇和贵金属杠杆高,这种自动降档只能压低概率性回撤,不保证避损。 开 MT5 把下面代码塞进你的 EA 回调里,把数组换成自己的阈值表,就能看到日志里打印的「new balance to overcome」随净值跳动。
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"); } if(!this.ActivateDynamicRiskPerOperation) class="kw">return; class="type">class="kw">double account_equity = AccountInfoDouble(ACCOUNT_EQUITY); if(account_equity > this.chosen_balance && this.NewBalanceToOvercome != this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]) class="kw">return; 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 < (class="type">int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size()) { if(index_gmlpo < (class="type">int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - class="num">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 + class="num">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); class="kw">break; } } else if(index_gmlpo == this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - class="num">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]); class="kw">break; } } index_gmlpo++; }
◍ 净值越线后逐级回撤风控仓位
这段逻辑处理的是「账户权益突破预设阈值后,如何把逐笔风险百分比往下调」的问题。当 NewBalanceToOvercome 大于 0,且实时 account_equity 超过该值,EA 会进入重算分支,避免权益回撤时仍按高位风险敞口下单。外汇与贵金属杠杆高,这类动态降险机制能压住连亏期的回撤速度,但无法消除爆仓可能。 核心是一个 while 循环:只要未收到停止信号且 index_gmlpo 还大于 0,就向下比对 dynamic_gmlpos.balance_to_activate_the_risk 数组。若当前权益落在某相邻区间之间,break 退出;否则 index_gmlpo 自减,继续找匹配档位。 找到档位后分两种落点。若 index_gmlpo 降到 0,说明权益已正向越过全部阈值,assigned_percentage 复位为初始 gmlpo_percentage,NewBalanceToOvercome 清 0。若仍大于 0,则把风险百分比设为上一档的 risk_to_be_adjusted,同时把下一关要越过的余额更新为上一档激活值,方便下次再判。 开 MT5 把这段代码塞进 EA 的 OnTick 前段,用 PrintFormat 看日志里『The new risk per operation』的数值跳变,就能验证你的档位表是否按预期生效。
PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); SetGMLPO(); } } if(this.NewBalanceToOvercome > class="num">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 > class="num">0) { if(index_gmlpo > class="num">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 - class="num">1]) { class="kw">break; } this.index_gmlpo--; } } this.TheMinimumValueIsExceeded = false; if(this.index_gmlpo == class="num">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 = class="num">0.00; PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage); SetGMLPO(); } else if(index_gmlpo > class="num">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 - class="num">1]; this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo - class="num">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(); } } }
「别急着下结论」
动态单笔交易风险这套逻辑,核心是根据账户实际盈亏自动调仓,而不是拍脑袋定死 2% 的固定风险。CRiskManagement 类在 Risk_Management.mqh(126.71 KB)里封装了这些通用函数,新手直接拖进 EA 就能跑。 把这套系统接进交易机器人后,和裸奔策略比,最大回撤的收敛概率会明显更高,但外汇和贵金属的高杠杆属性意味着黑天鹅照样能击穿保护。 下一步如果用订单块指标做信号源,记得先调通第一部分和第二部分的接口,再让小布替你回测不同风险系数下的权益曲线。验证完再谈适不适合你的盘面,不急这一两根 K 线。