继续漫步优化(第二部分):为任意机器人创建优化报告的机制·进阶篇
◍ 持仓与成交明细的结构化拆法
做历史回测前,先把每笔仓位和成交的字段钉死,后面统计才不会乱。下面这套结构体把「持仓」和「逐笔成交」分开存:持仓记符号、ID、最早开仓时间与平仓时间;成交记开平日期、星期、成交量、多空方向、进出场价、单手盈亏与含佣真实盈亏。 佣金处理上有两种编译分支。若没定义 ONLY_CUSTOM_COMISSION,仅当 data.comission 为 0 且佣金管理器非空时才补算;若定义了该宏,则无条件用管理器取佣金。这样切换券商环境时不用改主逻辑。 读取余额的函数先以 HistorySelect(0, toDate>0?toDate:TimeCurrent()) 拉取历史,再循环 HistoryDealsTotal() 次,用 HistoryDealGetTicket(i) 拿成交票号。外汇与贵金属杠杆高,含佣真实盈亏和单手盈亏的差值,往往决定策略是否经得起点差摩擦。
class="type">class="kw">string symbol; class=class="str">"cmt">// Symbol class="type">long ID; class=class="str">"cmt">// ID of the position(s) class="type">class="kw">datetime DT_min; class=class="str">"cmt">// Open date(or the date of the very first position) class="type">class="kw">datetime DT_max; class=class="str">"cmt">// Close date }; class="macro">#ifndef ONLY_CUSTOM_COMISSION if(data.comission==class="num">0 && comission_manager != NULL) { data.comission=comission_manager.get(data.symbol,data.price,data.volume); } class="macro">#else data.comission=comission_manager.get(data.symbol,data.price,data.volume); class="macro">#endif class="kw">struct DealDetales { class="type">class="kw">string symbol; class=class="str">"cmt">// Symbol class="type">class="kw">datetime DT_open; class=class="str">"cmt">// Open date ENUM_DAY_OF_WEEK day_open; class=class="str">"cmt">// Open day class="type">class="kw">datetime DT_close; class=class="str">"cmt">// Cloe date ENUM_DAY_OF_WEEK day_close; class=class="str">"cmt">// Close day class="type">class="kw">double volume; class=class="str">"cmt">// Volume(lots) class="type">bool isLong; class=class="str">"cmt">// Long/Short class="type">class="kw">double price_in; class=class="str">"cmt">// Position entry price class="type">class="kw">double price_out; class=class="str">"cmt">// Position exit price class="type">class="kw">double pl_oneLot; class=class="str">"cmt">// Profit / loss is trading one lot class="type">class="kw">double pl_forDeal; class=class="str">"cmt">// Real profit/loss taking into account commission class="type">class="kw">string open_comment; class=class="str">"cmt">// Comment at the time of opening class="type">class="kw">string close_comment; class=class="str">"cmt">// Comment at the time of closing }; class="type">class="kw">double CDealHistoryGetter::getBalance(class="type">class="kw">datetime toDate) { if(HistorySelect(class="num">0,(toDate>class="num">0 ? toDate : TimeCurrent()))) { class="type">int total=HistoryDealsTotal(); class=class="str">"cmt">// Get the total number of positions class="type">class="kw">double balance=class="num">0; for(class="type">int i=class="num">0; i<total; i++) { class="type">long ticket=(class="type">long)HistoryDealGetTicket(i);
「从成交历史里抠出真实余额变动」
想在 MT5 里复盘某段时间的账户净值曲线,不能直接读账户余额字段,因为入金、手续费冲正这些非交易动作也会改余额。下面这段逻辑只挑出三类特殊成交类型,把它们的 DEAL_PROFIT 累加进自定义变量 balance,相当于剥离了纯市场盈亏以外的账面扰动。 代码先通过 HistoryDealGetInteger 取每笔成交的 ENUM_DEAL_TYPE,判断是否属于 DEAL_TYPE_BALANCE(出入金)、DEAL_TYPE_CORRECTION( correction 修正)、DEAL_TYPE_COMMISSION(手续费)。命中任一项就把该笔 profit 加进 balance;若 toDate 没设(<=0)则直接 break 跳出,避免无谓遍历。 实盘验证时建议把 toDate 传具体时间戳,否则在成交超 5000 笔的账户上可能多耗 10~30 ms。外汇与贵金属杠杆高,账面 balance 和浮动盈亏不是一回事,统计结果仅反映历史账面流,不预示后续走势。
ENUM_DEAL_TYPE dealType=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE); if(dealType==DEAL_TYPE_BALANCE || dealType == DEAL_TYPE_CORRECTION || dealType == DEAL_TYPE_COMMISSION) { balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT); if(toDate<=class="num">0) class="kw">break; } } class="kw">return balance; } else class="kw">return class="num">0; }
一次遍历算完所有系数
早先的报表类每来一组参数请求就重下交易历史并整段重算,数据量大时单次可能卡好几秒。后来把计算收进 CReportCreator,核心思路是:下载一次历史,在单个循环里把最大盈亏、累计盈亏等共用数据全算出来,存进类字段,后续取数直接读缓存,不再重跑循环。 这个类在私有作用域里塞了一批嵌套结构,外层的其他模块调不到,避免和对外计算用的结构混在一起。PL_keeper 针对盈亏图存了 4 种表现:PL 前缀来自终端蓝线,BH 前缀来自买入持有策略,hist 后缀是直方图口径。DailyPL_keeper 则按平均/绝对、开仓日/平仓日排了 4 类每日图。 RatioTable_keeper 里 Total 前缀按实际手数算,OneLot 前缀假装全程一手,用来拆出‘是手数管理贡献的还是系统逻辑贡献的’——这套单手思路对评估 EA 很有用。后缀 max 取时段内峰值,absolute 取全历史汇总,percent 相对盈亏曲线峰值算百分率。 连续盈亏的计算靠 S_dealsInARow 继承 S_dealsCounter,中间结果放 Counter 字段,后面 Z 分数用的 R_arr(盈/亏记 1/0)就在 CalculationData_item 里。回测里若历史成交过万,旧法重算可能拖到秒级,新结构一轮扫完,二次取参基本是内存拷贝。 排序侧定义了 СHistoryComparer,只比平仓日期,配合通用排序器给系数图和历史排序。VaR 按历史法估算,分布假定正态——外汇和贵金属杠杆高,这类分布外推在极端行情里可能严重低估尾部风险,跑 MT5 优化时别把正态当圣经。
class="kw">struct PL_keeper { PLChart_item PL_total[]; PLChart_item PL_oneLot[]; PLChart_item PL_Indicative[]; }; PL_keeper PL,PL_hist,BH,BH_hist; class=class="str">"cmt">// The structure of Daily PL graphs class="kw">struct DailyPL_keeper { DailyPL avarage_open,avarage_close,absolute_open,absolute_close; }; class=class="str">"cmt">// Table structure of extreme points class="kw">struct RatioTable_keeper { ProfitDrawdown Total_max,Total_absolute,Total_percent; ProfitDrawdown OneLot_max,OneLot_absolute,OneLot_percent; }; class="kw">struct ProfitDrawdown { class="type">class="kw">double Profit; class=class="str">"cmt">// In some cases Profit, in other Profit / Loss class="type">class="kw">double Drawdown; class=class="str">"cmt">// Drawdown }; class=class="str">"cmt">// Structures for calculating consecutive profits and losses class="kw">struct S_dealsCounter { class="type">int Profit,DD; }; class="kw">struct S_dealsInARow : class="kw">public S_dealsCounter { S_dealsCounter Counter; }; class=class="str">"cmt">// Structures for calculating auxiliary data class="kw">struct CalculationData_item { S_dealsInARow dealsCounter; class="type">int R_arr[]; class="type">class="kw">double DD_percent; class="type">class="kw">double Accomulated_DD,Accomulated_Profit; class="type">class="kw">double PL;
◍ 回测报告里的结构体怎么摆
做 MT5 历史回测报告时,先把盈亏和回撤拆成两套结构,后面算夏普、恢复系数才不容易乱。下面这段声明把单笔与标准手的数据分开了:total 存实际手数汇总,oneLot 存折算成 1 手后的可比数据。 CalculationData 里挂了 total 和 oneLot 两个 CalculationData_item,外加 num_deals 成交笔数和 isNot_firstDeal 首单标记。这样在遍历成交记录时,能区分“已经有过至少一单”和“刚启动”的状态,避免首单前算回撤除零。 CHistoryComparer 只干一件事:按平仓时间 DT_close 排序。返回 0 / 1 / -1 三态,保证多单同毫秒平仓也不会丢顺序。 PL_detales_PLDD 把每类结果记成订单数、占比%、连续笔数、总金额、均值五项。profit 和 drawdown 各持一份,意味着盈利单与亏损单的最大连胜连亏都能单独拉出来看。 外汇与贵金属复盘含高杠杆风险,上述结构仅用于离线统计,不代表任何实盘收益可能。
class="type">class="kw">double Max_DD_forDeal,Max_Profit_forDeal; class="type">class="kw">double Max_DD_byPL,Max_Profit_byPL; class="type">class="kw">datetime DT_Max_DD_byPL,DT_Max_Profit_byPL; class="type">class="kw">datetime DT_Max_DD_forDeal,DT_Max_Profit_forDeal; class="type">int Total_DD_numDeals,Total_Profit_numDeals; }; class="kw">struct CalculationData { CalculationData_item total,oneLot; class="type">int num_deals; class="type">bool isNot_firstDeal; }; class="kw">struct CoefChart_keeper { CoefChart_item OneLot_ShartRatio_chart[],Total_ShartRatio_chart[]; CoefChart_item OneLot_WinCoef_chart[],Total_WinCoef_chart[]; CoefChart_item OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[]; CoefChart_item OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[]; CoefChart_item OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[]; }; class CHistoryComparer : class="kw">public ICustomComparer<DealDetales> { class="kw">public: class="type">int Compare(DealDetales &x,DealDetales &y); }; class="type">int CReportCreator::CHistoryComparer::Compare(DealDetales &x,DealDetales &y) { class="kw">return(x.DT_close == y.DT_close ? class="num">0 : (x.DT_close > y.DT_close ? class="num">1 : -class="num">1)); } PL_detales PL_detales_data; DistributionChart OneLot_PDF_chart,Total_PDF_chart; class="kw">struct PL_detales_PLDD { class="type">int orders; class="type">class="kw">double orders_in_Percent; class="type">int dealsInARow; class="type">class="kw">double totalResult; class="type">class="kw">double averageResult; }; class="kw">struct PL_detales_item { PL_detales_PLDD profit; PL_detales_PLDD drawdown; }; class="kw">struct PL_detales { PL_detales_item total,oneLot; }; class="kw">struct Chart_item {
「把收益分布和 VaR 装进结构体」
在 MT5 里做风险分析,先得把数据形态定清楚。下面这组结构体把「分布图坐标」「VaR 数值」「绝对/增长双区块」分层装好,后面算完直接往里塞,不用每次重定义。 Chart_item 只存两个 double:x 轴与 y 轴,对应直方图的区间中点与频数。VAR 结构体一口气放了三档置信度的 VaR——90%、95%、99%,外加均值 Mx 与标准差 Std,一次计算全保留。 Distribution_item 用动态数组 distribution[] 承接整张图,再挂一个 VaR 子结构;最外层的 DistributionChart 分成 absolute 与 growth 两块,分别存价格绝对值分布和环比变动分布。外汇与贵金属波动大,这类分层能让回测时同时看「原值形态」和「变化形态」,风险敞口可能更直观。 复制下面代码到 MQ5 文件头部,编译不报错就说明你的数据骨架立住了,接下来填计算逻辑即可。
class="type">class="kw">double y; class=class="str">"cmt">// y axis class="type">class="kw">double x; class=class="str">"cmt">// x axis }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Structure contains the VaR value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct VAR { class="type">class="kw">double VAR_90,VAR_95,VAR_99; class="type">class="kw">double Mx,Std; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Structure - it is used to store distribution charts and | class=class="str">"cmt">//| the VaR values | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct Distribution_item { Chart_item distribution[]; class=class="str">"cmt">// Distribution chart VAR VaR; class=class="str">"cmt">// VaR }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Structure - Stores distribution data. Divided into class="num">2 blocks | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct DistributionChart { Distribution_item absolute,growth; };
从成交明细里榨出风险系数
CReportCreator 里的 CalcPL 只做一件事:把每笔平仓后的权益变化写成盈亏图的一个点。非指示性图直接搬已知 PL 和 DD;指示性图才进分支逻辑——用当前累计盈利除以最大回撤,得出“要几次连续最大回撤才能抹平盈利”的系数,外汇与贵金属品种因点差滑点放大,这个系数在重大数据行情后常瞬间恶化,属高风险特征。 CalcData_item 在单笔迭代里维护连胜/连亏计数器,并把胜负交替写进一个 0、1 交替数组,供后续 Z 得分判断交易序列是否随机。它同时刷新最大利润、最大回撤和累计值,这些字段是盈利因子、恢复因子、锋锐率的共同输入。 盈利因子 = 累计利润 / 累计亏损,除数为零时强行置零避免运行时崩。恢复因子按“第 i 次盈利 / 第 i 次回撤”算,盈利为负时系数可为负。胜率用(盈利笔均利 / 亏损笔均损)表达,同样可能为负数。 锋锐率稍重:先由盈亏图算平均盈利能力(相邻盈亏增长比),再做序列常规化算波动率,最后套(均值 - 无风险率)/ 波动率。VaR 走历史模型,正态分布图借统计包思路,x 轴分别取增长基准的盈利性与绝对利润值。 所有方法由 Calculate 统一唤起,公开接口只当取值器。复制下面代码到 MT5 的报表类里,把 deal 和 data 接上你自己的成交结构,就能在策略测试器里直接画出指示性盈亏曲线。
class="type">void CReportCreator::CalcPL(class="kw">const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type) { PLChart_item item; ZeroMemory(item); item.DT=deal.DT_close; class=class="str">"cmt">// 保存平仓时间 if(type!=_Indicative) { item.Profit=(type==_Total ? data.total.PL : data.oneLot.PL); class=class="str">"cmt">// 保存总或单手盈利 item.Drawdown=(type==_Total ? data.total.DD_percent : data.oneLot.DD_percent); class=class="str">"cmt">// 保存对应回撤百分比 } else class=class="str">"cmt">// 计算指示性图表 { if(data.isNot_firstDeal) { if(data.total.PL!=class="num">0) { if(data.total.PL > class="num">0 && data.total.Max_DD_forDeal < class="num">0) item.Profit=data.total.PL/MathAbs(data.total.Max_DD_forDeal); class=class="str">"cmt">// 盈利为正且回撤为负,系数=盈利/回撤绝对值 else if(data.total.PL<class="num">0 && data.total.Max_Profit_forDeal>class="num">0) item.Profit=data.total.PL/data.total.Max_Profit_forDeal; class=class="str">"cmt">// 亏损且曾有盈利,系数=亏损/最大利润 } } } class=class="str">"cmt">// 追加到数组 class="type">int s=ArraySize(pl_out); ArrayResize(pl_out,s+class="num">1,s+class="num">1); pl_out[s]=item; }