构建一个用于实现带约束条件的自定义最大值的通用优化公式(GOF)·进阶篇
(2/3)· 从概念到EA集成,手把手把通用优化公式写进MetaTrader 5测试环境
把优化框架接进你的EA
想先动手再读原理,可以直接在 MT5 里把 GenericOptimizationFormulation 这套机制挂到自己的 EA 上。它本质是个头文件,不需要改交易逻辑,只需要在正确位置插两行代码并编译通过。 原文头文件里的注释给出了明确步骤:在 EA 的 .mq5 文件里,输入变量声明之后、OnInit() 函数之前,插入 magic 变量定义和 include 指令;保存编译后若报错,通常是占位符没替换成真实 magic 值或包含路径写错。 外汇与贵金属自动化测试属高风险,回测通过不代表实盘稳健,接入后建议先用策略测试器跑样本再上模拟盘。
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() 里。
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_Target1 和 gof_Weight1 去逼近你设的年度回报 200%、权重 1 这类数值。
硬约束枚举 gof_HardConstrains 覆盖账户回撤、连亏笔数、胜率、夏普等 14 项。默认开启 gof_IncludeHardConstraints=true,并把第一条约束设为 hc_minAllowedAnnualReturn_pct 配合 hc_GT 与边界值 50,即年化收益不得低于 50% 才被视为合格解。
外汇与贵金属杠杆高,这类约束只是筛选条件,不代表实盘能达到;开 MT5 把这段代码贴进 EA 输入区,改 gof_HCBound_1 就能立刻看到优化结果集变化。
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 倍左右。
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,就能看到不同止损线下的破产概率倾向怎么变。
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 悄悄掰正。外汇与贵金属杠杆高,参数误算可能放大下单偏差,务必先在模拟盘验证。
-(MathFloor(-obj*gof_ObjNegativeScalefactor)+dec/class="num">100.)