继续漫步优化(第二部分):为任意机器人创建优化报告的机制·进阶篇
📘

继续漫步优化(第二部分):为任意机器人创建优化报告的机制·进阶篇

第 2/3 篇

◍ 持仓与成交明细的结构化拆法

做历史回测前,先把每笔仓位和成交的字段钉死,后面统计才不会乱。下面这套结构体把「持仓」和「逐笔成交」分开存:持仓记符号、ID、最早开仓时间与平仓时间;成交记开平日期、星期、成交量、多空方向、进出场价、单手盈亏与含佣真实盈亏。 佣金处理上有两种编译分支。若没定义 ONLY_CUSTOM_COMISSION,仅当 data.comission 为 0 且佣金管理器非空时才补算;若定义了该宏,则无条件用管理器取佣金。这样切换券商环境时不用改主逻辑。 读取余额的函数先以 HistorySelect(0, toDate>0?toDate:TimeCurrent()) 拉取历史,再循环 HistoryDealsTotal() 次,用 HistoryDealGetTicket(i) 拿成交票号。外汇与贵金属杠杆高,含佣真实盈亏和单手盈亏的差值,往往决定策略是否经得起点差摩擦。

MQL5 / C++
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 和浮动盈亏不是一回事,统计结果仅反映历史账面流,不预示后续走势。

MQL5 / C++
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 优化时别把正态当圣经。

MQL5 / C++
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 各持一份,意味着盈利单与亏损单的最大连胜连亏都能单独拉出来看。 外汇与贵金属复盘含高杠杆风险,上述结构仅用于离线统计,不代表任何实盘收益可能。

MQL5 / C++
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 文件头部,编译不报错就说明你的数据骨架立住了,接下来填计算逻辑即可。

MQL5 / C++
  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 接上你自己的成交结构,就能在策略测试器里直接画出指示性盈亏曲线。

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

常见问题

按成交历史逐笔遍历,把开平仓配对后写入结构化数组,就能单独看每笔持仓的浮盈、手续费和真实余额变动,不再被汇总曲线掩盖。
从成交历史里按时间顺序重算每笔出入金与手续费,忽略平台显示的权益跳变,用逐笔累加得到真实余额路径。
可以,小布能直接读取你的回测成交明细,自动跑一遍遍历算出夏普、最大回撤和 VaR,并在品种页给出结构化风险面板。
一次遍历只走一遍成交序列,顺手累加均值、方差和分位数,比多次扫描省数倍时间,机器人品种多时很值得。
把收益分布、VaR 和明细指针全塞进同一个报告结构体,初始化时清零,遍历完再统一写盘,取数前判空即可避免异常。