风险管理(第四部分):完善关键类方法·综合运用
📘

风险管理(第四部分):完善关键类方法·综合运用

第 3/3 篇

◍ 超限判定的三种比对口径

做风控 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

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//| Boolean function to check if a loss was overcome&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; |</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>
{
&nbsp;&nbsp;<span class="keyword">if</span>(loss_ &lt;= <span class="number">class="num">0</span> || !<span class="keyword">this</span>.positions_open)
&nbsp;&nbsp;&nbsp;&nbsp;<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>
&nbsp;&nbsp;<span class="keyword">if</span>(mode == EQUITY) <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="keyword">this</span>.curr_profit * -<span class="number">class="num">1</span> &gt; loss_)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="macro">true</span>;
&nbsp;&nbsp; }
&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(mode == CLOSE_POSITION)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(profit_ * -<span class="number">class="num">1</span> &gt; loss_)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(mode == CLOSE_POSITION_AND_EQUITY)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> new_loss = profit_ &lt; <span class="number">class="num">0</span> ? loss_ - <span class="functions">MathAbs</span>(profit_) : loss_;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="keyword">this</span>.curr_profit * -<span class="number">class="num">1</span> &gt; new_loss)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;<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&nbsp;&nbsp;&nbsp;&nbsp; |</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>
{
&nbsp;&nbsp;<span class="keyword">if</span>(<span class="keyword">this</span>.mdp.<span class="keyword">value</span>&nbsp;&nbsp;&lt;= <span class="number">class="num">0</span> || !<span class="keyword">this</span>.positions_open)
&nbsp;&nbsp;&nbsp;&nbsp;<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>
&nbsp;&nbsp;<span class="keyword">if</span>(mode == EQUITY) <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="keyword">this</span>.curr_profit &gt; <span class="keyword">this</span>.mdp.<span class="keyword">value</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="keyword">true</span>;
&nbsp;&nbsp; }
&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(mode == CLOSE_POSITION)
&nbsp;&nbsp;&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(<span class="keyword">this</span>.daily_profit &gt; <span class="keyword">this</span>.mdp.<span class="keyword">value</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> <span class="keyword">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp;&nbsp;<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 补上,就能在策略测试器里观察阈值随每日盈亏漂移。

MQL5 / C++
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 即可编译运行,之后在策略测试器或实盘里随便下一单,终端日志会立刻吐出事务明细。

MQL5 / C++
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(每单风险占比),从而调节持仓数量和敞口。外汇与贵金属杠杆高,这类实时重算可能在剧烈波动中频繁触发,请在策略测试器先跑历史数据验证。

MQL5 / C++
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 宏就能验证触发频率。

MQL5 / C++
  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:

MQL5 / C++
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) 移动,持仓结构数组很大时倾向用映射容器替代。

MQL5 / C++
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 个连字符。写日志分隔线或终端输出对齐时,比手敲一串减号更不容易数错。外汇与贵金属行情高波动,这类小工具只解决代码整洁度,不涉任何方向判断。

MQL5 / C++
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,动态风险才算就绪。外汇与贵金属杠杆高,这套机制只是降低误配概率,不保证回撤可控。

MQL5 / C++
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 数组,完成初始化。

MQL5 / C++
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_adjustedbalance_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 策略测试器用极小本金验证。

MQL5 / C++
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」随净值跳动。

MQL5 / C++
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』的数值跳变,就能验证你的档位表是否按预期生效。

MQL5 / C++
   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 线。

常见问题

在超限判定里同时保留固定比例与动态阈值两个变量,每笔成交后取两者较小值做比对,避免只盯一个口径漏掉极端行情风险。
在订单成交的实时回调入口里直接调用风控函数,不依赖OnTick轮询,这样成交瞬间就能拦截超限加仓。
小布可接入品种页的AIGC诊断,把动态回撤阈值和实时浮亏做比对,超标时直接推送提醒,你不用自己写EA钩子。
要。空窗期虽无持仓,仍需用交易事务钩子保持风控模块待命,避免下一笔开仓绕过动态风控规则。
不麻烦。用字符串数组记品种名、整数数组记对应持仓数,循环一次就能查清每个品种的敞口,比全局变量清爽。