继续漫步优化(第二部分):为任意机器人创建优化报告的机制·综合运用
逐笔统计里的连盈连亏与回撤锚点
这段逻辑跑在每笔成交闭合之后,核心是把单笔盈亏 pl 按是否标准化为一手(isOneLot)做分流统计。dealsCounter 里同时维护连续盈利计数 Profit 与连续回撤计数 DD,盈利一笔就清零 DD、亏损一笔就清零 Profit,因此 out.dealsCounter.DD 与 Profit 取的是历史最大连败与连胜长度。 R_arr 数组记录的是盈亏序列的「段」:n 为 1 表示盈利、0 表示亏损,仅当上一段结尾值不同时才压入新元素,等价于把 101001 压缩成 10101 的连段标记。回测时直接读这个数组长度分布,就能看出策略是碎步盈利还是偶发大亏。 最大单笔回撤与最大单笔利润分别用 Max_DD_forDeal / Max_Profit_forDeal 承接,并保存对应平仓时间 DT_close。Accumulated_DD 只累加负 pl,Accumulated_Profit 只累加正 pl,二者分离便于后续算 Calmar 类比率。
| 基于累计 PL 的浮盈高点回撤更有意思:maxPL 取历史最高累计收益,当当前 PL 低于该高点时,DD 按 PL-maxPL(盈利回吐)或 -( | PL | +maxPL)(击穿成本线)计算,Max_DD_byPL 存的是全局最差水下偏离。外汇与贵金属杠杆品种中该值可能瞬间超过本金 50%,属高风险信号,需用 MT5 跑历史品种验证阈值。 |
|---|
class="type">bool isOneLot) { class="type">class="kw">double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); class=class="str">"cmt">//PL class="type">int n=class="num">0; class=class="str">"cmt">// Number of profits and losses if(pl>=class="num">0) { out.Total_Profit_numDeals++; n=class="num">1; out.dealsCounter.Counter.DD=class="num">0; out.dealsCounter.Counter.Profit++; } else { out.Total_DD_numDeals++; out.dealsCounter.Counter.DD++; out.dealsCounter.Counter.Profit=class="num">0; } out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD); out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit); class=class="str">"cmt">// Series of profits and losses class="type">int s=ArraySize(out.R_arr); if(!(s>class="num">0 && out.R_arr[s-class="num">1]==n)) { ArrayResize(out.R_arr,s+class="num">1,s+class="num">1); out.R_arr[s]=n; } out.PL+=pl; class=class="str">"cmt">//Total PL class=class="str">"cmt">// Max Profit / DD if(out.Max_DD_forDeal>pl) { out.Max_DD_forDeal=pl; out.DT_Max_DD_forDeal=deal.DT_close; } if(out.Max_Profit_forDeal<pl) { out.Max_Profit_forDeal=pl; out.DT_Max_Profit_forDeal=deal.DT_close; } class=class="str">"cmt">// Accumulated Profit / DD out.Accomulated_DD+=(pl>class="num">0 ? class="num">0 : pl); out.Accomulated_Profit+=(pl>class="num">0 ? pl : class="num">0); class=class="str">"cmt">// Extreme profit values class="type">class="kw">double maxPL=MathMax(out.Max_Profit_byPL,out.PL); if(compareDouble(maxPL,out.Max_Profit_byPL)==class="num">1)class=class="str">"cmt">/* || !isNot_firstDeal*/)class=class="str">"cmt">// another check is needed to save the date { out.DT_Max_Profit_byPL=deal.DT_close; out.Max_Profit_byPL=maxPL; } class="type">class="kw">double maxDD=out.Max_DD_byPL; class="type">class="kw">double DD=class="num">0; if(out.PL>class="num">0) DD=out.PL-maxPL; else DD=-(MathAbs(out.PL)+maxPL); maxDD=MathMin(maxDD,DD); if(compareDouble(maxDD,out.Max_DD_byPL)==-class="num">1)class=class="str">"cmt">/* || !isNot_firstDeal*/)class=class="str">"cmt">// another check is needed to save the date { out.Max_DD_byPL=maxDD; out.DT_Max_DD_byPL=deal.DT_close; }
「回测报告里的逐笔聚合逻辑」
MT5 里做交易报告生成时,单笔成交(Deal)如何被归并进总统计,决定了你看到的 Equity 曲线和因子图是否可信。下面这段 CReportCreator::CalcData 展示了非导出模式下,一笔 deal 被塞进哪些计算通道。 注意 isBH 参数:当它为 false,才走完整图表填充。也就是说,如果是某种“仅基础统计”的调用,PL 图、每日盈亏、夏普、Z 分数全部跳过,num_deals 却依然自增——这可能导致你拿到的报告交易数正确,但曲线为空。 从代码看,单笔会同时喂给 oneLot(标准化 1 手)和 total(实际手数)两套 CalcData_item,再分别灌入 PL_total / PL_oneLot / PL_Indicative 三条权益线。外汇与贵金属波动大,用 oneLot 归一能横向比策略,但实盘高风险,归一不等于实盘资金曲线。 因子图部分覆盖了 Profit Factor、Recovery Factor、WinCoef、Sharpe(代码里拼成 ShartRatio)、Altman Z Score,且 each 都分 OneLot 与 Total 双版本。Z 分数计算直接取 num_deals、R_arr 大小、盈利/亏损笔数,说明它是基于每笔结果序列的游程检验,而非简单均值。
out.DD_percent=(balance>class="num">0 ?(MathAbs(DD)/(maxPL>class="num">0 ? maxPL : balance)) :(maxPL>class="num">0 ?(MathAbs(DD)/maxPL) : class="num">0)); } class="type">void CReportCreator::CalcData(class="kw">const DealDetales &deal,CalculationData &out,class="type">bool isBH) { out.num_deals++; class=class="str">"cmt">// Counting the number of deals CalcData_item(deal,out.oneLot,true); CalcData_item(deal,out.total,false); if(!isBH) { class=class="str">"cmt">// Fill PL graphs CalcPL(deal,out,PL.PL_total,_Total); CalcPL(deal,out,PL.PL_oneLot,_OneLot); CalcPL(deal,out,PL.PL_Indicative,_Indicative); class=class="str">"cmt">// Fill PL Histogram graphs CalcPLHist(deal,out,PL_hist.PL_total,_Total); CalcPLHist(deal,out,PL_hist.PL_oneLot,_OneLot); CalcPLHist(deal,out,PL_hist.PL_Indicative,_Indicative); class=class="str">"cmt">// Fill PL graphs by days CalcDailyPL(DailyPL_data.absolute_close,CALC_FOR_CLOSE,deal); CalcDailyPL(DailyPL_data.absolute_open,CALC_FOR_OPEN,deal); CalcDailyPL(DailyPL_data.avarage_close,CALC_FOR_CLOSE,deal); CalcDailyPL(DailyPL_data.avarage_open,CALC_FOR_OPEN,deal); class=class="str">"cmt">// Fill Profit Factor graphs ProfitFactor_chart_calc(CoefChart_data.OneLot_ProfitFactor_chart,out,deal,true); ProfitFactor_chart_calc(CoefChart_data.Total_ProfitFactor_chart,out,deal,false); class=class="str">"cmt">// Fill Recovery Factor graphs RecoveryFactor_chart_calc(CoefChart_data.OneLot_RecoveryFactor_chart,out,deal,true); RecoveryFactor_chart_calc(CoefChart_data.Total_RecoveryFactor_chart,out,deal,false); class=class="str">"cmt">// Fill winning coefficient graphs WinCoef_chart_calc(CoefChart_data.OneLot_WinCoef_chart,out,deal,true); WinCoef_chart_calc(CoefChart_data.Total_WinCoef_chart,out,deal,false); class=class="str">"cmt">// Fill Sharpe Ration graphs ShartRatio_chart_calc(CoefChart_data.OneLot_ShartRatio_chart,PL.PL_oneLot,dealclass=class="str">"cmt">/*,out.isNot_firstDeal*/); ShartRatio_chart_calc(CoefChart_data.Total_ShartRatio_chart,PL.PL_total,dealclass=class="str">"cmt">/*,out.isNot_firstDeal*/); class=class="str">"cmt">// Fill Z Score graphs AltmanZScore_chart_calc(CoefChart_data.OneLot_AltmanZScore_chart,(class="type">class="kw">double)out.num_deals, (class="type">class="kw">double)ArraySize(out.oneLot.R_arr),(class="type">class="kw">double)out.oneLot.Total_Profit_numDeals, (class="type">class="kw">double)out.oneLot.Total_DD_numDealsclass=class="str">"cmt">/*,out.isNot_firstDeal*/,deal); AltmanZScore_chart_calc(CoefChart_data.Total_AltmanZScore_chart,(class="type">class="kw">double)out.num_deals, (class="type">class="kw">double)ArraySize(out.total.R_arr),(class="type">class="kw">double)out.total.Total_Profit_numDeals, (class="type">class="kw">double)out.total.Total_DD_numDealsclass=class="str">"cmt">/*,out.isNot_firstDeal*/,deal);
◍ 按交易日拆分盈亏与胜率统计
报告生成器里这段逻辑,把每笔成交按星期几归类,分别累加盈利和回撤。 CalcDailyPL 依次调用 cmpDay,对周一到周五各跑一次;cmpDay 内部先判断成交按平仓日还是开仓日算(CALC_FOR_CLOSE 取 day_close,否则取 day_open),命中对应星期才累加。 盈利侧:deal.pl_forDeal>0 时,ans.Profit 加该笔盈亏,numTrades_profit 自增;亏损侧:<0 时 ans.Drawdown 加 MathAbs 绝对值,numTrades_drawdown 自增。这样一天内多笔同星期成交会被合并统计。 avarageDay 做均值化:Profit 除以盈利笔数、Drawdown 除以亏损笔数,若某侧笔数为 0 则不除避免 NaN。 ProfitFactor_chart_calc 取累计盈利与累计回撤绝对值,按 isOneLot 切换单手或总仓口径,写入 CoefChart_item 的时间序列,供后续绘 PF 曲线。外汇与贵金属品种波动剧烈,周统计仅反映历史分布,实盘样本不足时结论倾向失真,需谨慎。
else class=class="str">"cmt">// Fill PL Buy and Hold graphs { CalcPL(deal,out,BH.PL_total,_Total); CalcPL(deal,out,BH.PL_oneLot,_OneLot); CalcPL(deal,out,BH.PL_Indicative,_Indicative); CalcPLHist(deal,out,BH_hist.PL_total,_Total); CalcPLHist(deal,out,BH_hist.PL_oneLot,_OneLot); CalcPLHist(deal,out,BH_hist.PL_Indicative,_Indicative); } if(!out.isNot_firstDeal) out.isNot_firstDeal=true; class=class="str">"cmt">// Flag "It is NOT the first deal" } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create a structure of trading during a day | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CReportCreator::CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,class="kw">const DealDetales &deal) { cmpDay(deal,MONDAY,out.Mn,calcBy); cmpDay(deal,TUESDAY,out.Tu,calcBy); cmpDay(deal,WEDNESDAY,out.We,calcBy); cmpDay(deal,THURSDAY,out.Th,calcBy); cmpDay(deal,FRIDAY,out.Fr,calcBy); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Save resulting PL/DD for the day | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CReportCreator::cmpDay(class="kw">const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy) { ENUM_DAY_OF_WEEK day=(calcBy==CALC_FOR_CLOSE ? deal.day_close : deal.day_open); if(day==etalone) { if(deal.pl_forDeal>class="num">0) { ans.Profit+=deal.pl_forDeal; ans.numTrades_profit++; } else if(deal.pl_forDeal<class="num">0) { ans.Drawdown+=MathAbs(deal.pl_forDeal); ans.numTrades_drawdown++; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Average resulting PL/DD for the day | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CReportCreator::avarageDay(PLDrawdown &day) { if(day.numTrades_profit>class="num">0) day.Profit/=day.numTrades_profit; if(day.numTrades_drawdown > class="num">0) day.Drawdown/=day.numTrades_drawdown; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate Profit Factor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CReportCreator::ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,class="kw">const DealDetales &deal,class="type">bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; class="type">class="kw">double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit); class="type">class="kw">double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD);
回撤恢复因子与夏普的类图计算落点
恢复因子(Recovery Factor)衡量累计盈亏相对最大回撤的覆盖能力。代码里当 dd==0 时直接把 coef 置 0,注释写明理想情况应为正无穷,实盘里这意味着账户从未回撤,概率极低,外汇与贵金属品种的高波动特性下更几乎不可能出现。 盈利/回撤比那行用 pl/dd 计算,pl 按 isOneLot 切换单标准手与总仓口径,dd 取 MathAbs 后的最大回撤绝对值。若你跑 EURUSD 的 M15 回测,某段样本 dd 为 120 点、pl 为 360 点,恢复因子就是 3.0,代表盈利是回撤的 3 倍。 胜率系数另走一条路:用 (profit/n_profit)/(dd/n_dd) 算每笔盈利单均值对每笔回撤单均值的比,n_profit 或 n_dd 为 0 时 coef 赋 0,避免除零。夏普类函数 ShartRatio_calc 要求样本数 total>=2,从 i=1 起循环且跳过上一笔 Profit 为 0 的节点,说明它基于非零盈亏步长估计序列波动。 这几个函数在 MT5 里接在 CReportCreator 的报告流水线中,改 coef 公式前先确认 out[] 的 ArrayResize 步长 s+1 是否和你自己的图表点数匹配,否则容易出现尾部 item 丢失。
if(dd==class="num">0) item.coef=class="num">0; else item.coef=profit/dd; class="type">int s=ArraySize(out); ArrayResize(out,s+class="num">1,s+class="num">1); out[s]=item; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate Recovery Factor | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CReportCreator::RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,class="kw">const DealDetales &deal,class="type">bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; class="type">class="kw">double pl=(isOneLot ? data.oneLot.PL : data.total.PL); class="type">class="kw">double dd=MathAbs(isOneLot ? data.oneLot.Max_DD_byPL : data.total.Max_DD_byPL); if(dd==class="num">0) item.coef=class="num">0;class=class="str">"cmt">//ideally it should be plus infinity else item.coef=pl/dd; class="type">int s=ArraySize(out); ArrayResize(out,s+class="num">1,s+class="num">1); out[s]=item; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate Win Rate | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CReportCreator::WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,class="kw">const DealDetales &deal,class="type">bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; class="type">class="kw">double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit); class="type">class="kw">double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD); class="type">int n_profit=(isOneLot ? data.oneLot.Total_Profit_numDeals : data.total.Total_Profit_numDeals); class="type">int n_dd=(isOneLot ? data.oneLot.Total_DD_numDeals : data.total.Total_DD_numDeals); if(n_dd == class="num">0 || n_profit == class="num">0) item.coef = class="num">0; else item.coef=(profit/n_profit)/(dd/n_dd); class="type">int s=ArraySize(out); ArrayResize(out,s+class="num">1,s+class="num">1); out[s]=item; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate Sharpe Ratio | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CReportCreator::ShartRatio_calc(PLChart_item &data[]) { class="type">int total=ArraySize(data); class="type">class="kw">double ans=class="num">0; if(total>=class="num">2) { class="type">class="kw">double pl_r=class="num">0; class="type">int n=class="num">0; for(class="type">int i=class="num">1; i<total; i++) { if(data[i-class="num">1].Profit!=class="num">0) {
「夏普式收益波动比的逐行实现」
这段逻辑在算一个类夏普比率:用相邻笔权益的差值增长率均值 pl_r 减去无风险收益 r,再除以增长率序列的样本标准差 std,得出风险调整后收益 ans。注意分母用了 n-1 而不是 n,是典型的无偏样本标准差写法,外汇与贵金属回测里样本量小的时候这个细节会明显影响数值。 下面拆一下核心几行:pl_r 累加的是 (data[i].Profit-data[i-1].Profit)/data[i-1].Profit,也就是逐笔权益环比增长率;只有 data[i-1].Profit 不为 0 才计入,避免除零。n>=2 才做平均,否则前面循环白跑。 标准差那段把每个增长率与均值的差平方后求和,最后 MathSqrt(std/(double)(n-1)) 开方。ans 在 std 为 0 时直接返回 0,说明策略收益完全无波动时这套指标失效,实盘里概率极低但样本构造不当会触发。 NormalPDF_chart_calc 里顺手算了绝对收益和增长收益的均值与标准差,并清空 distribution 数组准备画正态拟合。Mx_growth 只从 i>0 且前一笔非空开始累加,因此增长类统计的有效样本数 n 通常比 total 少 1,调参时别拿 total 去当分母。
pl_r+=(data[i].Profit-data[i-class="num">1].Profit)/data[i-class="num">1].Profit; n++; } } if(n>=class="num">2) pl_r/=(class="type">class="kw">double)n; class="type">class="kw">double std=class="num">0; n=class="num">0; for(class="type">int i=class="num">1; i<total; i++) { if(data[i-class="num">1].Profit!=class="num">0) { std+=MathPow((data[i].Profit-data[i-class="num">1].Profit)/data[i-class="num">1].Profit-pl_r,class="num">2); n++; } } if(n>=class="num">2) std=MathSqrt(std/(class="type">class="kw">double)(n-class="num">1)); ans=(std!=class="num">0 ?(pl_r-r)/std : class="num">0); } class="kw">return ans; } class="type">void CReportCreator::NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[]) { class="type">class="kw">double Mx_absolute=class="num">0,Mx_growth=class="num">0,Std_absolute=class="num">0,Std_growth=class="num">0; class="type">int total=ArraySize(data); ZeroMemory(out.absolute); ZeroMemory(out.growth); ZeroMemory(out.absolute.VaR); ZeroMemory(out.growth.VaR); ArrayFree(out.absolute.distribution); ArrayFree(out.growth.distribution); if(total>=class="num">2) { class="type">int n=class="num">0; for(class="type">int i=class="num">0; i<total; i++) { Mx_absolute+=data[i].Profit; if(i>class="num">0 && data[i-class="num">1].Profit!=class="num">0) { Mx_growth+=(data[i].Profit-data[i-class="num">1].Profit)/data[i-class="num">1].Profit; n++; } } Mx_absolute/=(class="type">class="kw">double)total; if(n>=class="num">2) Mx_growth/=(class="type">class="kw">double)n; n=class="num">0; for(class="type">int i=class="num">0; i<total; i++) { Std_absolute+=MathPow(data[i].Profit-Mx_absolute,class="num">2);
◍ 从逐笔盈亏到 VaR 与分布曲线的落地
这段逻辑干两件事:先把逐笔绝对盈亏和环比增长率的标准差算出来,再据此填 VaR 并铺出分布点。外汇与贵金属杠杆高,这类统计只描述历史风险轮廓,不代表未来亏损上限。 增长率方差只在 i>0 且上一笔 Profit 非零时累加,分子是 (当前Profit-上一笔Profit)/上一笔Profit 减去均值 Mx_growth 的平方;n 计数后,若 n>=2 才用 n-1 做无偏估计开根号。绝对标准差则直接拿 total-1 除完开平方。 VaR 调用分三档分位:Q_90、Q_95、Q_99 分别喂给绝对和增长两套均值/标准差,输出 out.absolute.VaR.VAR_90~99 与 out.growth 对应字段。若你导出的历史交易笔数不足 30,95% 置信 VaR 可能明显低估尾部。 分布构造用一个 for 循环把每笔 Profit 及其概率密度 PDF_calc 塞进 item_a,增长项 item_g 在 i>0 且分母非零时才算环比;最后 sorter.Sort 按 x 升序排好,方便小布盯盘直接画 CDF。
if(i>class="num">0 && data[i-class="num">1].Profit!=class="num">0) { Std_growth+=MathPow((data[i].Profit-data[i-class="num">1].Profit)/data[i-class="num">1].Profit-Mx_growth,class="num">2); n++; } } Std_absolute=MathSqrt(Std_absolute/(class="type">class="kw">double)(total-class="num">1)); if(n>=class="num">2) Std_growth=MathSqrt(Std_growth/(class="type">class="kw">double)(n-class="num">1)); class=class="str">"cmt">// Calculate VaR out.absolute.VaR.Mx=Mx_absolute; out.absolute.VaR.Std=Std_absolute; out.absolute.VaR.VAR_90=VaR(Q_90,Mx_absolute,Std_absolute); out.absolute.VaR.VAR_95=VaR(Q_95,Mx_absolute,Std_absolute); out.absolute.VaR.VAR_99=VaR(Q_99,Mx_absolute,Std_absolute); out.growth.VaR.Mx=Mx_growth; out.growth.VaR.Std=Std_growth; out.growth.VaR.VAR_90=VaR(Q_90,Mx_growth,Std_growth); out.growth.VaR.VAR_95=VaR(Q_95,Mx_growth,Std_growth); out.growth.VaR.VAR_99=VaR(Q_99,Mx_growth,Std_growth); class=class="str">"cmt">// Calculate distribution for(class="type">int i=class="num">0; i<total; i++) { Chart_item item_a,item_g; ZeroMemory(item_a); ZeroMemory(item_g); item_a.x=data[i].Profit; item_a.y=PDF_calc(Mx_absolute,Std_absolute,data[i].Profit); if(i>class="num">0) { item_g.x=(data[i-class="num">1].Profit != class="num">0 ?(data[i].Profit-data[i-class="num">1].Profit)/data[i-class="num">1].Profit : class="num">0); item_g.y=PDF_calc(Mx_growth,Std_growth,item_g.x); } class="type">int s=ArraySize(out.absolute.distribution); ArrayResize(out.absolute.distribution,s+class="num">1,s+class="num">1); out.absolute.distribution[s]=item_a; s=ArraySize(out.growth.distribution); ArrayResize(out.growth.distribution,s+class="num">1,s+class="num">1); out.growth.distribution[s]=item_g; } class=class="str">"cmt">// Ascending sorter.Sort<Chart_item>(out.absolute.distribution,&chartComparer); sorter.Sort<Chart_item>(out.growth.distribution,&chartComparer); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Calculate VaR | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CReportCreator::VaR(class="type">class="kw">double quantile,class="type">class="kw">double Mx,class="type">class="kw">double Std) {
用概率密度函数给收益分布建模
在 MT5 报表模块里,收益分布的概率密度可以直接用正态分布公式算。下面这段 CReportCreator::PDF_calc 就是核心:传入均值 Mx、标准差 Std 和观测点 x,返回该点的密度值。 当 Std 不为零时,公式走 MathExp(-0.5*((x-Mx)/Std)^2) / (sqrt(2π)*Std)。若 Std 等于 0,函数直接返回 0,避免除零崩溃。 实际验证时,把历史成交的盈亏序列算好 Mx 和 Std,再喂给 PDF_calc 扫一遍 x 轴,就能画出你账户的概率密度曲线。外汇和贵金属杠杆高,这条曲线只描述历史样本分布,对未来回撤只能给概率倾向,不是保本依据。
class="type">class="kw">double CReportCreator::PDF_calc(class="type">class="kw">double Mx,class="type">class="kw">double Std,class="type">class="kw">double x) { if(Std!=class="num">0) class="kw">return MathExp(-class="num">0.5*MathPow((x-Mx)/Std,class="num">2))/(MathSqrt(class="num">2*M_PI)*Std); else class="kw">return class="num">0; }
「把报告生成器接进你的优化流水线」
前面几篇把 C# 函数库和自动优化器的逻辑都铺完了,这一节只是把交易报告生成器单独拎出来收口。它依赖早先写好的报告机制,但接口和内部实现已经改过一轮,目前在多次优化和测试流程里跑过,没崩。 解压附件里的两个文件夹(CustomGeneric、History manager)到 MQL5/Include 目录即可,里面是 GenericSorter.mqh、DealHistoryGetter.mqh、ReportCreator.mqh 等七个文件,直接 #include 就能调。 外汇和贵金属品种波动大、杠杆高,用这套报告回测历史成交时,别把样本期内的漂亮曲线外推成实盘预期,它只反映过去参数下的概率分布。 真要验证,新建一个 EA 把 ReportCreator 挂上,跑一遍前行优化,看生成的成交明细和回撤数据是否对得上你手动统计的数字——对不上就说明某层封装悄悄改了手数逻辑。