构建一个用于实现带约束条件的自定义最大值的通用优化公式(GOF)·进阶篇
⚙️

构建一个用于实现带约束条件的自定义最大值的通用优化公式(GOF)·进阶篇

(2/3)· 从概念到EA集成,手把手把通用优化公式写进MetaTrader 5测试环境

实战向进阶 第 2/3 篇
很多交易者把MT5的优化面板当成黑箱,只调侧面约束却忽略业务约束,结果跑出的参数连亏到爆仓。本篇接着把通用优化公式(GOF)真正接进EA,让约束函数参与目标求和,而不是被算法悄悄丢弃。

把优化框架接进你的EA

想先动手再读原理,可以直接在 MT5 里把 GenericOptimizationFormulation 这套机制挂到自己的 EA 上。它本质是个头文件,不需要改交易逻辑,只需要在正确位置插两行代码并编译通过。 原文头文件里的注释给出了明确步骤:在 EA 的 .mq5 文件里,输入变量声明之后、OnInit() 函数之前,插入 magic 变量定义和 include 指令;保存编译后若报错,通常是占位符没替换成真实 magic 值或包含路径写错。 外汇与贵金属自动化测试属高风险,回测通过不代表实盘稳健,接入后建议先用策略测试器跑样本再上模拟盘。

MQL5 / C++
class=class="str">"cmt">/*
要在你的EA中使用此文件,你必须执行以下步骤:
-编辑你的EA .mq5文件
-在输入变量之后和OnInit() 函数之前插入以下两行代码:
class="type">ulong gof_magic= an_ulong_value;
class="macro">#include <YOUR_INCLUDE_LOCATION\GenericOptimizationFormulation.mqh>
-保存并编译你的EA文件
-如果你遇到编译错误,请确保你做到了以下几点:
-用包含magic数值的变量或直接用magic数值替换 an_ulong_value
-用你的包含文件所在的文件夹名替换YOUR_INCLUDE_LOCATION
*/

◍ 把优化框架接进 MT5 头文件

通用优化框架(GOF)以 .mqh 形式落地,先要把统计与代数依赖拉进来。下面三行是硬性前置,缺一个编译都过不了: #include <Math\Stat\Lognormal.mqh> #include <Math\Stat\Uniform.mqh> #include <Math\Alglib\alglib.mqh> 目标函数给了 18 个候选,最多同时选 5 个参与组合。枚举 gof_FunctionDefs 里从 MAX_NONE=0 排到 MAX_StratEfficiency=16,中间有几个名字不直白:LRcrr^3 是线性回归相关系数立方;1/(LR std%) 用标准差倒数逼权益曲线贴紧直线;Seff=Profit/(TotalTrades*AvgLot) 专挑高利润、少交易、小仓位的策略。 硬约束有 14 个、最多挂 10 个,hc_GT 是下界、ht_LT 是上界。把 gof_IncludeHardConstraints 设 false 可全关,方便先跑裸目标看基线。 破产风险(RoRA)用蒙特卡洛而非闭式公式算,因为简单公式给出的数值偏离实际。模拟假设盈亏服从对数正态分布,用 CalcDealStatistics() 取均值和标准差;历史交易通常只有几百笔,不足以建马尔可夫转移矩阵,所以这一步只能近似。 蒙特卡洛返回的 1% 是亏光全部投入资本的概率,不是亏 1% 本金。外汇与贵金属杠杆高,这类破产概率对仓位尺度极度敏感,调参前先固定资本定义方式(初始入金百分比 / 固定金额 / 100% 特例)。 结果列的小数位靠 gof_fr 控制:fr_winRate 把 34% 显成 0.39,fr_MCRoRA 把 11% 风险显成 0.11,fr_ConLoss 超过 99 次统一压到 0.99。正目标乘 1e6 取整再加小数,负目标(约束违例多)乘 1e3 压缩纵轴,这套缩放直接写在 AddDecimalsToCustomMax() 里。

MQL5 / C++
class="macro">#include <Math\Stat\Lognormal.mqh>
class="macro">#include <Math\Stat\Uniform.mqh>
class="macro">#include <Math\Alglib\alglib.mqh>
enum gof_FunctionDefs {       class=class="str">"cmt">// functions to build objective
   MAX_NONE=class="num">0,                class=class="str">"cmt">// class="num">0] None
   MAX_AnnRetPct,             class=class="str">"cmt">// class="num">1] Annual Return %
   MAX_Balance,               class=class="str">"cmt">// class="num">2] Balance
   MAX_NetProfit,             class=class="str">"cmt">// class="num">3] Net Profit
   MAX_SharpeRatio,          class=class="str">"cmt">// class="num">4] Sharpe Ratio
   MAX_ExpPayOff,            class=class="str">"cmt">// class="num">5] Expected Payoff
   MAX_RecovFact,            class=class="str">"cmt">// class="num">6] Recovery Factor
   MAX_ProfFact,             class=class="str">"cmt">// class="num">7] Profit Factor
   MAX_LRcrr3,               class=class="str">"cmt">// class="num">8] LRcrr^class="num">3
   MAX_NbrTradesPerWeek,     class=class="str">"cmt">// class="num">9] class="macro">#Trades/week
   MAX_WinRatePct,           class=class="str">"cmt">// class="num">10] Win Rate %
   MAX_Rew2RiskRatio,        class=class="str">"cmt">// class="num">11] Reward/Risk(RRR=AvgWin/AvgLoss)
   MAX_OneOverLRstd,         class=class="str">"cmt">// class="num">12] class="num">1/(LR std%)
   MAX_OneHoverWorstTradePct,class=class="str">"cmt">// class="num">13] class="num">100/(class="num">1+|WorstLoss/Init.Dep*class="num">100|)
   MAX_LR,                   class=class="str">"cmt">// class="num">14] LRslope*LRcorr/LRstd
   MAX_OneHoverEqtyMaxDDpct, class=class="str">"cmt">// class="num">15] class="num">100/(class="num">1+EqtyMaxDD%))
   MAX_StratEfficiency,      class=class="str">"cmt">// class="num">16] Seff=Profit/(TotalTrades*AvgLot)

「把风控与优化目标写进枚举」

这段 MT5 代码把仓位优化的目标函数和硬约束都做成了枚举,方便在 EA 参数面板里直接勾选。 gof_FunctionDefs 里列了 18 种可最大化目标,比如 MAX_KellyCrit(凯利准则)和 MAX_OneOverRoRApct(取 1 与破产风险百分比的较大者倒数)。优化器会按 gof_Target1gof_Weight1 去逼近你设的年度回报 200%、权重 1 这类数值。 硬约束枚举 gof_HardConstrains 覆盖账户回撤、连亏笔数、胜率、夏普等 14 项。默认开启 gof_IncludeHardConstraints=true,并把第一条约束设为 hc_minAllowedAnnualReturn_pct 配合 hc_GT 与边界值 50,即年化收益不得低于 50% 才被视为合格解。 外汇与贵金属杠杆高,这类约束只是筛选条件,不代表实盘能达到;开 MT5 把这段代码贴进 EA 输入区,改 gof_HCBound_1 就能立刻看到优化结果集变化。

MQL5 / C++
enum gof_FunctionDefs {
   MAX_KellyCrit,                    class=class="str">"cmt">// class="num">17] Kelly Criterion
   MAX_OneOverRoRApct               class=class="str">"cmt">// class="num">18] class="num">1/Max(class="num">0.01,RoRA %)
};
enum gof_HardConstrains {
   hc_NONE=class="num">0,                       class=class="str">"cmt">// class="num">0] None
   hc_MaxAccountLoss_pct,           class=class="str">"cmt">// class="num">1] Account Loss % InitDep
   hc_maxAllowed_DDpct,             class=class="str">"cmt">// class="num">2] Equity DrawDown %
   hc_maxAllowednbrConLossTrades,   class=class="str">"cmt">// class="num">3] Consecutive losing trades
   hc_minAllowedWin_pct,            class=class="str">"cmt">// class="num">4] Win Rate %
   hc_minAllowedNbrTradesPerWeek,   class=class="str">"cmt">// class="num">5] # trades/week
   hc_minAllowedRecovFactor,        class=class="str">"cmt">// class="num">6] Recov Factor
   hc_minAllowedRRRFactor,          class=class="str">"cmt">// class="num">7] Reward/Risk ratio
   hc_minAllowedAnnualReturn_pct,   class=class="str">"cmt">// class="num">8] Annual Return in %
   hc_minAllowedProfFactor,         class=class="str">"cmt">// class="num">9] Profit Factor
   hc_minAllowedSharpeFactor,       class=class="str">"cmt">// class="num">10] Sharpe Factor
   hc_minAllowedExpPayOff,          class=class="str">"cmt">// class="num">11] Expected PayOff
   hc_minAllowedMarginLevel,        class=class="str">"cmt">// class="num">12] Smallest Margin Level
   hc_maxAllowedTradeLoss,          class=class="str">"cmt">// class="num">13] Max Loss trade
   hc_maxAllowedRoRApct             class=class="str">"cmt">// class="num">14] Risk of Ruin(%)
};
enum gof_HardConstType {
   hc_GT=class="num">0, class=class="str">"cmt">// >=     Greater or equal to
   hc_LT    class=class="str">"cmt">// <=     Less or equal to
};
enum gof_RoRaCapital {
   roraCustomPct=class="num">0,   class=class="str">"cmt">// Custom % of Ini.Dep.
   roraCustomAmount,  class=class="str">"cmt">// Custom Capital amount
   roraIniDep         class=class="str">"cmt">// Initial deposit
};
enum gof_objFuncDecimals {
   fr_winRate=class="num">0, class=class="str">"cmt">// WinRate %
   fr_MCRoRA,    class=class="str">"cmt">// MonteCarlo Sim Risk of Ruin Account %
   fr_LRcorr,    class=class="str">"cmt">// LR correlation
   fr_ConLoss,   class=class="str">"cmt">// Max # Consecutive losing Trades
   fr_NONE       class=class="str">"cmt">// None
};
input group   "- Build Custom Objective to Maximize:"
sinput gof_FunctionDefs gof_Func1   = MAX_AnnRetPct;             class=class="str">"cmt">// Select Objective Function to Maximize class="num">1:
sinput class="type">class="kw">double gof_Target1           = class="num">200;                       class=class="str">"cmt">// Target class="num">1
sinput class="type">class="kw">double gof_Weight1           = class="num">1;                         class=class="str">"cmt">// Weight class="num">1
input group   "- Hard Constraints:"
sinput class="type">bool   gof_IncludeHardConstraints = true;class=class="str">"cmt">//if false, all constraints are ignored
sinput gof_HardConstrains gof_HC_1=hc_minAllowedAnnualReturn_pct; class=class="str">"cmt">// Select Constraint Function class="num">1:
sinput gof_HardConstType gof_HCType_1=hc_GT; class=class="str">"cmt">// Type class="num">1
sinput class="type">class="kw">double gof_HCBound_1=class="num">50; class=class="str">"cmt">// Bound Value class="num">1
input group   "------ Misc Optimization Params -----"
sinput gof_objFuncDecimals gof_fr                = fr_winRate;   class=class="str">"cmt">// Choose Result-column&class="macro">#x27;s decimals
sinput gof_RoRaCapital      gof_roraMaxCap       = roraCustomPct;class=class="str">"cmt">// Choose capital method for Risk of Ruin

破产风险模拟的输入开关与判分逻辑

这段参数声明决定了蒙特卡洛回测怎么跑、结果往哪输出。gof_RoraCustomValue 默认 10,仅在自定义破产风险算法时介入;gof_drawSummary 关、gof_printSummary 开,意味着结果只进 MT5 日志不画图上。 sinput double gof_RoraCustomValue = 10; // 自定义破产风险值(按需使用) sinput bool gof_drawSummary = false; // 图表上绘制摘要 sinput bool gof_printSummary = true; // 日志中打印摘要 sinput bool gof_discardLargestProfit = false; // 从净利润扣除最大盈利 sinput bool gof_discardLargestLoss = false; // 向净利润补回最大亏损 sinput double gof_PenaltyMultiplier = 100; // 约束惩罚乘数(k_p) sinput double gof_ObjMultiplier = 100; // 目标乘数(k_o) //------------ GOF ---------------------- // 打印并显示模拟结果 GOFsummaryReport(); // 计算单目标函数 double SingleObjective = calcObjFunc(); // 若启用硬约束则算总惩罚 if(gof_IncludeHardConstraints) gof_constraintTotalPenalty=calcContraintTotalPenalty(gof_displayContraintFlag); // 计算 customMaxCriterion // gof_PenaltyMultiplier 把不可行解压向低目标值 // gof_PenaltyMultiplier 放大 Y 轴正向 double customMaxCriterion=gof_constraintTotalPenalty>0? SingleObjective-gof_PenaltyMultiplier*gof_constraintTotalPenalty: gof_ObjMultiplier*SingleObjective; // 给结果列补两位小数 customMaxCriterion=AddDecimalsToCustomMax(customMaxCriterion); // 收尾打印 FinishGOFsummaryReport(customMaxCriterion); return (NormalizeDouble(customMaxCriterion,2)); sinput 段逐行拆:第1行 double 型自定义值默认 10,仅特殊风险模型用;第2、3行 bool 控制输出位置,默认日志开、图表关;第4、5行决定是否剔除极端盈亏对净值的影响,默认都保留;第6、7行两个乘数默认都是 100,惩罚项会按 100 倍从目标里扣,可行解则按 100 倍放大。 判分核心在 customMaxCriterion:约束被破时,用 SingleObjective 减去 100 倍惩罚,不可行解分数会被狠狠压低;没破约束时乘 100 放大。最后 NormalizeDouble 保留两位小数返回。 MonteCarlo_RiskOfRuinAccount 函数里写死 10000 次模拟、每轮至少 100 笔。它先用 CalcDealStatistics 取盈亏均值与标准差,再用 TimeLocal()+1 播随机种子,AvgLoss 取绝对值、胜率截断在 0–100。外汇与贵金属杠杆高,这类破产概率只是历史统计外推,实盘可能偏离。 double MonteCarlo_RiskOfRuinAccount(double WinRatePct, double AvgWin, double AvgLoss, double limitLoss_money, int nTrades) { // 10000次蒙特卡洛,每轮至少100笔 // 若有充足历史可用马尔可夫转移矩阵,此处仅用均值标准差 double posDealsMean,posDealsStd,negDealsMean,negDealsStd; CalcDealStatistics(gof_dealsEquity, posDealsMean,posDealsStd,negDealsMean,negDealsStd); MathSrand((int)TimeLocal()+1); AvgLoss=MathAbs(AvgLoss); WinRatePct=MathMin(100,MathMax(0,WinRatePct)); // 胜率100%情形: 验证动作很直接:把 gof_printSummary 保持 true,在 MT5 策略测试器跑完看日志里的 customMaxCriterion,若约束没碰红线,数值应是单目标的 100 倍左右。

MQL5 / C++
sinput class="type">class="kw">double      gof_RoraCustomValue      = class="num">10;        class=class="str">"cmt">// Custom Value for Risk of Ruin(if needed)
sinput class="type">bool        gof_drawSummary          = false;    class=class="str">"cmt">// Draw summary on chart
sinput class="type">bool        gof_printSummary         = true;     class=class="str">"cmt">// Print summary on journal
sinput class="type">bool        gof_discardLargestProfit = false;    class=class="str">"cmt">// Subtract Largest Profit from Netprofit
sinput class="type">bool        gof_discardLargestLoss   = false;    class=class="str">"cmt">// Add Largest Loss to Net profit
sinput class="type">class="kw">double      gof_PenaltyMultiplier    = class="num">100;      class=class="str">"cmt">// Multiplier for Penalties(k_p)
sinput class="type">class="kw">double      gof_ObjMultiplier        = class="num">100;      class=class="str">"cmt">// Multiplier for Objectives(k_o)
class=class="str">"cmt">//------------ GOF ----------------------
class=class="str">"cmt">// Printing and displaying results from the simulation
  GOFsummaryReport();
class=class="str">"cmt">// calculate the single objective function
  class="type">class="kw">double SingleObjective = calcObjFunc();
class=class="str">"cmt">// calculate the total penalty from constraint violations
  if(gof_IncludeHardConstraints) gof_constraintTotalPenalty=calcContraintTotalPenalty(gof_displayContraintFlag);
class=class="str">"cmt">// Compute customMaxCriterion
class=class="str">"cmt">// gof_PenaltyMultiplier pushes infeasible designs to have low objective values
class=class="str">"cmt">// gof_PenaltyMultiplier expand the positive side of the Y axis
  class="type">class="kw">double customMaxCriterion=gof_constraintTotalPenalty>class="num">0?
                            SingleObjective-gof_PenaltyMultiplier*gof_constraintTotalPenalty:
                            gof_ObjMultiplier*SingleObjective;
class=class="str">"cmt">// add additional simulation result as two decimal digits in the result column
  customMaxCriterion=AddDecimalsToCustomMax(customMaxCriterion);
class=class="str">"cmt">// Printing and displaying more results from GOF
  FinishGOFsummaryReport(customMaxCriterion);
  class="kw">return (NormalizeDouble(customMaxCriterion,class="num">2));
class="type">class="kw">double MonteCarlo_RiskOfRuinAccount(class="type">class="kw">double WinRatePct, class="type">class="kw">double AvgWin, class="type">class="kw">double AvgLoss, class="type">class="kw">double limitLoss_money, class="type">int nTrades) {
class=class="str">"cmt">// class="num">10000 Montecarlo simulations, each with at least class="num">100 trades.
class=class="str">"cmt">// Ideally, if we had lots of trades in the history, we could use a Markov Chain transfer probability matrix
class=class="str">"cmt">//  we are limiting the statistics to mean & stdev, without knowledge of a transfer probability information
  class="type">class="kw">double posDealsMean,posDealsStd,negDealsMean,negDealsStd;
  CalcDealStatistics(gof_dealsEquity, posDealsMean,posDealsStd,negDealsMean,negDealsStd);
class=class="str">"cmt">// seeding the random number generator
  MathSrand((class="type">int)TimeLocal()+class="num">1);
class=class="str">"cmt">// ignore posDealsMean and negDealsMean. Use AvgWin and AvgLoss instead
  AvgLoss=MathAbs(AvgLoss);
  WinRatePct=MathMin(class="num">100,MathMax(class="num">0,WinRatePct));
class=class="str">"cmt">// case when win rate is class="num">100%:

◍ 用蒙特卡洛给破产风险定个数

这段逻辑干的事很直接:先对 100% 胜率做保守修正,再把每笔盈亏当成对数正态分布来抽样,最后跑一万次模拟数破产次数。外汇和贵金属杠杆高,这种基于概率的破产测算比拍脑袋靠谱,但结论只代表历史参数下的可能风险,不是 deterministic 保证。 当计算出的胜率交易数大于等于总交易数,代码把胜率压到 99%,平均亏损设为平均盈利的一半,负向标准差直接复用正向标准差——这是明显的工程近似,开 MT5 跑之前你得知道自己改了哪些假设。 核心采样部分用 MathQuantileLognormal 按均匀随机序列 r 生成 win/loss 数组,单次模拟至少 100 笔(nTradesPerSim = MathMax(100, nTrades)),总模拟数 nMCsims 固定为 10000。每笔按 WinRatePct 用 MathRand()/32767 抽大小决定胜负,权益跌破 -limitLoss_money 就记一次 ruin 并 break。 最终 RiskOfRuinPct = ruinCount / successfulMCcounter * 100,也就是万次模拟里破产占比。把这段代码塞进 EA 的回测后处理,调一下 limitLoss_money 和 WinRatePct,就能看到不同止损线下的破产概率倾向怎么变。

MQL5 / C++
  if((class="type">int)(WinRatePct*nTrades/class="num">100)>=nTrades) {
        WinRatePct=class="num">99;            class=class="str">"cmt">// just to be a bit conservative if winrate=class="num">100%
        AvgLoss=AvgWin/class="num">2;         class=class="str">"cmt">// a guessengineering value
        negDealsStd=posDealsStd;  class=class="str">"cmt">// a guessengineering value
  }
class=class="str">"cmt">// Use log-normal distribution function. Mean and Std are estimated as:
  class="type">class="kw">double win_lnMean =log(AvgWin*AvgWin/sqrt(AvgWin*AvgWin+posDealsStd*posDealsStd));
  class="type">class="kw">double loss_lnMean=log(AvgLoss*AvgLoss/sqrt(AvgLoss*AvgLoss+negDealsStd*negDealsStd));
  class="type">class="kw">double win_lnstd  =sqrt(log(class="num">1+(posDealsStd*posDealsStd)/(AvgWin*AvgWin)));
  class="type">class="kw">double loss_lnstd =sqrt(log(class="num">1+(negDealsStd*negDealsStd)/(AvgLoss*AvgLoss)));
  class="type">class="kw">double rand_Win[],rand_Loss[];
  class="type">class="kw">double r[];
class=class="str">"cmt">// limit amount of money that defines Ruin
  limitLoss_money=MathAbs(limitLoss_money);
  class="type">bool success;
  class="type">int ruinCount=class="num">0; class=class="str">"cmt">// counter of ruins
  class="type">int successfulMCcounter=class="num">0;
  class="type">int nTradesPerSim=MathMax(class="num">100,nTrades);class=class="str">"cmt">// at least class="num">100 trades per sim
  class="type">int nMCsims=class="num">10000; class=class="str">"cmt">// MC sims, each one with nTradesPerSim
  for(class="type">int iMC=class="num">0; iMC<nMCsims; iMC++) {
      success=MathRandomUniform(class="num">0,class="num">1,nTradesPerSim,r);
      class=class="str">"cmt">// generate nTradesPerSim wins and losses for each simulation
      class=class="str">"cmt">// use LogNormal distribution
      success&=MathQuantileLognormal(r,win_lnMean,win_lnstd,true,false,rand_Win);
      success&=MathQuantileLognormal(r,loss_lnMean,loss_lnstd,true,false,rand_Loss);
      if(!success)class="kw">continue;
      successfulMCcounter++;
      class=class="str">"cmt">//simulate nTradesPerSim
      class="type">class="kw">double eqty=class="num">0; class=class="str">"cmt">// start each simulation with zero equity
      for(class="type">int i=class="num">0; i<nTradesPerSim; i++) {
          class=class="str">"cmt">// draw a random number in [class="num">0,class="num">1]
          class="type">class="kw">double randNumber=(class="type">class="kw">double)MathRand()/class="num">32767.;
          class=class="str">"cmt">// select a win or a loss depending on the win rate and the random number
          class=class="str">"cmt">// and add to the equity
          eqty+=randNumber*class="num">100 < WinRatePct?
                rand_Win[i]:
                -rand_Loss[i];
          class=class="str">"cmt">// check if equity is below the limit(ruin)
          class=class="str">"cmt">// count the number of times there is a ruin
          if(eqty<= -limitLoss_money) {
              ruinCount++;
              break;
          }
      }
  }
class=class="str">"cmt">// compute risk of ruin as percentage
  class="type">class="kw">double RiskOfRuinPct=(class="type">class="kw">double)(ruinCount)/successfulMCcounter*class="num">100.;
  class="kw">return(RiskOfRuinPct);
}
  obj=obj>class="num">0?
       MathFloor(obj*gof_ObjPositiveScalefactor)+dec/class="num">100.:

「负向缩放的取整陷阱」

MQL5 里处理负向对象缩放时,直接用 Floor 取整会踩坑。上面这行把负值先取反、乘缩放系数、再 Floor,最后加小数偏移,本质是在绕开负数取整向零靠拢的默认行为。 若 obj 为 -3.27、gof_ObjNegativeScalefactor 为 1.5、dec 为 50,则 -obj*gof_ObjNegativeScalefactor = 4.905,MathFloor 得 4,加 0.5 后输出 4.5,而非直觉上的 -4.5 附近。 开 MT5 把这组参数塞进脚本跑一遍,确认你的负向标度逻辑没被 Floor 悄悄掰正。外汇与贵金属杠杆高,参数误算可能放大下单偏差,务必先在模拟盘验证。

MQL5 / C++
-(MathFloor(-obj*gof_ObjNegativeScalefactor)+dec/class="num">100.)
把重复劳动交给小布
这些诊断与小布盯盘的AIGC已内置,打开对应品种页即可看到约束命中与参数敏感度的自动标注,你专注决策而非手写校验逻辑。

常见问题

测试器框架层仅对x_i的上下界做硬性裁剪,其他如最大连亏次数等约束不会自动生效,必须折进目标函数里用惩罚项处理。
求和更稳,权重可调且梯度友好;乘法在目标量级差异大时容易数值塌陷,进阶场景建议优先加权求和。
可以,把测试报告里的约束命中与回撤分布贴进小布对应品种页,AIGC会标出哪组参数只是侥幸过约束。
不是,外汇贵金属高风险,约束若不连续或量级失控,遗传搜索仍可能陷在无效区,建议先做边界采样。