交易员之活学活用:
📊

交易员之活学活用: "平静" 优化或绘制交易分布·进阶篇

(2/3)· 多数回测只调参数不察时机,本文用仓位级盈利分布揭开入场优劣时刻

含代码示例实战向 第 2/3 篇
不少交易者把策略测试器的优化当成黑箱,只扫参数不顾入场时刻的分布。直接拿成交历史算盈亏,往往把同一仓位的多笔操作拆散,看到的盈利结构全是歪的。

按持仓归并历史成交盈亏

做仓位级统计时,MT5 的成交历史(deal)和持仓(position)不是一一对应的:一次持仓平仓可能拆成多笔 deal 入账。下面这段逻辑用结构体数组把同一持仓的盈亏累加归并,避免重复计算。 核心思路是先拿 HistorySelect 拉取指定时间区间内的成交,遍历每笔 deal 只认 DEAL_TYPE_BUY / DEAL_TYPE_SELL,取出 deal_id 与 profit。再用一个循环在 arr_struct_positions 里按 id 比对:找到了就累加 profit 并置 seach=true 后 break;没找到就 ArrayResize 扩一位,把新 id 和 profit 写进去。 归并完后用 ArraySize 取数组长度,逐条 Print 出「id 盈利」便于在专家日志里肉眼核对。外汇与贵金属杠杆高,历史盈亏仅作复盘参考,实盘滑点可能让相同策略的归并结果偏离。 别让结构体默认零值坑你 结构体 struct_positions 显式写了构造器把 id 和 profit 初始化为 0,否则 MQL5 局部数组元素可能是内存残值,导致首笔归并显示乱码或巨幅假盈利。开 MT5 跑这段时,先故意留一版不写构造器对比日志就能看出来。

MQL5 / C++
arr_struct_positions[j].position_profit+=profit;
seach=true;
class="kw">break;
            }
          }
        if(!seach)
          {
           ArrayResize(arr_struct_positions,number+class="num">1);
           arr_struct_positions[number].position_id=deal_id;
           arr_struct_positions[number].position_profit+=profit;
          }
        }
      }
class=class="str">"cmt">//---
   class="type">int number=ArraySize(arr_struct_positions);
   for(class="type">int i=class="num">0;i<number;i++)
     {
      Print("id ",arr_struct_positions[i].position_id," 盈利 ",arr_struct_positions[i].position_profit);
     }
  }
class=class="str">"cmt">//--- 仓位盈利结构;
   class="kw">struct struct_positions
     {
      class="type">long                id;
      class="type">class="kw">double              profit;
      class=class="str">"cmt">//--- 构造器
                        struct_positions() {id=class="num">0; profit=class="num">0.0;}
     };
   struct_positions arr_struct_positions[];
class=class="str">"cmt">//--- 请求交易历史 
   HistorySelect(start,TimeCurrent());
   class="type">uint    total    =HistoryDealsTotal();
   class="type">ulong   ticket   =class="num">0;
   class="type">long    deal_id  =class="num">0;
   class="type">class="kw">double  profit   =class="num">0;
   class="type">long    type     =class="num">0;
class=class="str">"cmt">//--- 对于所有成交 
   for(class="type">uint i=class="num">0;i<total;i++)
     {
      class=class="str">"cmt">//--- 尝试得到成交单号 
      if((ticket=HistoryDealGetTicket(i))>class="num">0)
        {
...
        class=class="str">"cmt">//--- 只有买入或卖出
        if(type==DEAL_TYPE_BUY || type==DEAL_TYPE_SELL)
          {
...
          }
        }
     }
                        class="type">bool seach=false;
                        class="type">int number=ArraySize(arr_struct_positions);
                        for(class="type">int j=class="num">0;j<number;j++)
                          {
                           if(arr_struct_positions[j].id==deal_id)
                             {
                              arr_struct_positions[j].profit+=profit;
                              seach=true;
                              class="kw">break;

「按持仓标识归并成交记录」

这段逻辑处理的是:当一笔成交(deal)找不到已有持仓标识(seach 为 false)时,就把该标识对应的结构体塞进数组,并累加 profit。具体看,ArrayResize 把数组扩 1,用 deal_id 写入 id 字段,profit 字段做累加——这意味着同一 POSITION_IDENTIFIER 下的多笔 deal 会被折叠成一条汇总。 下方日志是 2016.09.06 15:05 在 USDJPY_M1 图表跑 position_reversal_v3 时的真实输出:EURUSD 先 Buy 0.01(POSITION_IDENTIFIER #96803513),6 秒后 Sell 0.02,后者拆成 volume 0.01(profit 0.00)与 volume 0.02(profit -0.08)两笔 deal,都挂在同一 #96803513 下。外汇与贵金属杠杆交易风险高,这种归并若漏掉反向平仓的 deal,净值统计会偏误。 最后一段 for 循环把数组打印出来:ArraySize 取长度,逐条 Print 'id + 盈利'。你开 MT5 把这段接到历史成交遍历里,能直接看到每个持仓标识的总浮动盈亏,验证归并是否对齐日志中的 #96803513。

MQL5 / C++
   if(!seach)
      {
       ArrayResize(arr_struct_positions,number+class="num">1);
       arr_struct_positions[number].id=deal_id;
       arr_struct_positions[number].profit+=profit;
      }
class=class="str">"cmt">//---
   class="type">int number=ArraySize(arr_struct_positions);
   for(class="type">int i=class="num">0;i<number;i++)
      {
       Print("id ",arr_struct_positions[i].id," 盈利 ",arr_struct_positions[i].profit);
      }

◍ 从日志看反手策略的成交与盈亏切片

一段真实的 EA 日志能暴露反手逻辑的执行细节。上面这组记录发生在 2016.09.06 15:05:41 到 15:05:48 的 7 秒内,EA 名为 position_reversal_v3,挂在 USDJPY 的 M1 周期上,却对 EURUSD 下了单——跨品种反手在 MT5 里完全可行,但外汇与贵金属本身杠杆高、滑点跳变频繁,这类操作风险偏大。 具体看成交:15:05:41.767 开了一笔 EURUSD 初始仓 0.02 手,持仓 ID 96803543;约 6 秒后触发 PositionClose,随后三笔 Deal 回写盈亏——0.01 手那笔利润 0.00,0.02 手亏 0.08,另一笔 0.01 手亏 0.05。总亏损 0.13 美元虽小,但 7 秒内连平带开说明反手频率偏高,点差和佣金会悄悄吃掉边际收益。 为了把这类日志结构化,可以用下面的 C 风格结构体把每笔持仓的 ID、时间、亏损、利润和品种名存下来,方便后续统计胜率和最大回撤。 struct struct_positions { long id; datetime time; double loss; double profit; string symbol_name; //--- 构造器 struct_positions() {id=0; time=0; loss=0.0; profit=0.0; symbol_name=NULL;} }; 逐行拆解:long id 存持仓唯一编号,对应日志里的 ORDER_POSITION_ID;datetime time 记录开仓或平仓时间,精度到毫秒;double loss 与 double profit 分别归集该笔的负收益和正收益;string symbol_name 标记品种如 EURUSD;构造器把字段清零,避免未初始化导致的历史回测脏数据。打开 MT5 的「专家日志」面板,对照这个结构体字段,你就能自己写个脚本把成交流导成 CSV 验证。

MQL5 / C++
class="kw">struct struct_positions
  {
   class="type">long                id;
   class="type">class="kw">datetime            time;
   class="type">class="kw">double              loss;
   class="type">class="kw">double              profit;
   class="type">class="kw">string              symbol_name;
   class=class="str">"cmt">//--- 构造器 
                      struct_positions() {id=class="num">0; time=class="num">0; loss=class="num">0.0; profit=class="num">0.0; symbol_name=NULL;}
  };

把品种盈亏丢进浏览器图表

想在不依赖 MT5 报表界面的情况下看品种表现,可以用谷歌图表服务把分析画到 HTML 页面里,再用系统默认浏览器打开。MT5 侧通过 WinAPI 的 ShellExecute 拉起页面,分析逻辑只管把数据吐成网页。 直方图用 'bar' 类型,饼状图走 'corechart' 类型。两者都要求在 <head> 里先引 loader.js,再用 setOnLoadCallback 绑定绘图函数,数据块、选项块、容器指向三者缺一不可。 写数据行时有个省事细节:arrayToDataTable 的每行末尾逗号写不写都不影响页面性能,算法拼数据时可不必严判末行。下面这段是 'bar' 类型最小可运行样例,已经带了三个品种的真实盈亏数字——Si-6.16 盈利 82.00 亏损 944.00,Si-9.16 盈利 56.00 亏损 11.00,SBRF-9.16 盈利 546.00 亏损 189.00。 把代码存成 *.html 双击打开,就能看到垂直柱状图。若要同页混排直方图和饼图,只需把各 drawChart***() 函数并列,并在 <body> 里放对应 id 的 div 容器,浏览器会按回调顺序渲染。

MQL5 / C++
<html>
  <head>
    <!--加载 AJAX API-->
    <script type="text/javascript" src="https:class=class="str">"cmt">//www.gstatic.com/charts/loader.js"></script>
    <script type="text/javascript">
      google.charts.load(&class="macro">#x27;current&class="macro">#x27;, {&class="macro">#x27;packages&class="macro">#x27;:[&class="macro">#x27;bar&class="macro">#x27;]});
      google.charts.setOnLoadCallback(drawChart1);
      function drawChart1() {
        var data1 = google.visualization.arrayToDataTable([
          [&class="macro">#x27;品种&class="macro">#x27;, &class="macro">#x27;盈利&class="macro">#x27;, &class="macro">#x27;亏损&class="macro">#x27;],
          [&class="macro">#x27;Si-class="num">6.16&class="macro">#x27;, class="num">82.00, class="num">944.00],
          [&class="macro">#x27;Si-class="num">9.16&class="macro">#x27;, class="num">56.00, class="num">11.00],
          [&class="macro">#x27;SBRF-class="num">9.16&class="macro">#x27;, class="num">546.00, class="num">189.00],
        ]);
        var options1 = {
          chart: {
            title: &class="macro">#x27;品种的盈利/亏损&class="macro">#x27;,
            subtitle: &class="macro">#x27;合计&class="macro">#x27;,
          },
          bars: &class="macro">#x27;vertical&class="macro">#x27;,
          vAxis: {format: &class="macro">#x27;decimal&class="macro">#x27;},
          width: class="num">440,
          height: class="num">400,
          colors: [&class="macro">#x27;class="macro">#5b9bd5&class="macro">#x27;, &class="macro">#x27;class="macro">#ed7d31&class="macro">#x27;, &class="macro">#x27;class="macro">#7570b3&class="macro">#x27;]
        };
        var chart = new google.charts.Bar(document.getElementById(&class="macro">#x27;chart_div1&class="macro">#x27;));
        chart.draw(data1, options1);
      }
    </script>
  </head>
  <body>
    <!--Div 将容纳饼状图表-->
      <div id="chart_div1"></div>
    <br/>
  </body>
</html>

「用谷歌图表把品种盈亏拆开看」

把交易报告的盈亏做可视化,最省事的办法是直接套 Google Charts 的 Bar 和 Pie 两套组件。下面这段脚本里,data1 用数组塞了三个品种:'Si-6.16' 盈利 82.00、'Si-9.16' 盈利 56.00、'SBRF-9.16' 盈利 546.00 且亏损 189.00,柱子垂直排布、宽 440 高 400,蓝橙紫三色区分序列。 第二个 html 块另起一个饼图,只取盈利百分比:Si-6.16 占 82.00、Si-9.16 占 56.00、SBRF-9.16 占 546.00,pieHole 设 0.4 做成环形。你在 MT5 里导出的成交明细,只要换成这种 arrayToDataTable 的二维数组,丢到本地 html 用浏览器打开就能直接出图。 外汇与贵金属品种波动大、杠杆高,这类盈亏图仅反映历史样本,后续分布可能明显偏移,别拿单段回测当仓位依据。

MQL5 / C++
var data1 = google.visualization.arrayToDataTable([
  [&class="macro">#x27;品种&class="macro">#x27;, &class="macro">#x27;盈利&class="macro">#x27;, &class="macro">#x27;亏损&class="macro">#x27;],
  [&class="macro">#x27;Si-class="num">6.16&class="macro">#x27;, class="num">82.00, class="num">0.00],
  [&class="macro">#x27;Si-class="num">9.16&class="macro">#x27;, class="num">56.00, class="num">0.00],
  [&class="macro">#x27;SBRF-class="num">9.16&class="macro">#x27;, class="num">546.00, class="num">189.00],
]);
var options1 = {
  chart: {
    title: &class="macro">#x27;品种的盈利/亏损&class="macro">#x27;,
    subtitle: &class="macro">#x27;合计&class="macro">#x27;,
  },
  bars: &class="macro">#x27;vertical&class="macro">#x27;,
  vAxis: {format: &class="macro">#x27;decimal&class="macro">#x27;},
  width: class="num">440,
  height: class="num">400,
  colors: [&class="macro">#x27;class="macro">#5b9bd5&class="macro">#x27;, &class="macro">#x27;class="macro">#ed7d31&class="macro">#x27;, &class="macro">#x27;class="macro">#7570b3&class="macro">#x27;]
};
var chart = new google.charts.Bar(document.getElementById(&class="macro">#x27;chart_div1&class="macro">#x27;));
chart.draw(data1, options1);

var data2 = google.visualization.arrayToDataTable([
  [&class="macro">#x27;品种&class="macro">#x27;, &class="macro">#x27;盈利&class="macro">#x27;],
  [&class="macro">#x27;Si-class="num">6.16&class="macro">#x27;, class="num">82.00],
  [&class="macro">#x27;Si-class="num">9.16&class="macro">#x27;, class="num">56.00],
  [&class="macro">#x27;SBRF-class="num">9.16&class="macro">#x27;, class="num">546.00],
]);
var options2 = {
  title: &class="macro">#x27;品种的盈利, %&class="macro">#x27;,
  pieHole: class="num">0.4,
  width: class="num">440,
  height: class="num">400,
};
var chart = new google.visualization.PieChart(document.getElementById(&class="macro">#x27;chart_div2&class="macro">#x27;));
chart.draw(data2, options2);

◍ 把回测盈利直接喂给网页图表

在 MT5 里跑完多品种回测后,最费时间的不是算收益,而是把结果整理成能一眼看懂的图。上面这段脚本把三个合约的盈利数据直接塞进 Google Charts:Si-6.16 盈利 82.00、Si-9.16 盈利 56.00、SBRF-9.16 盈利 546.00,单位均为百分比。 drawChart2 里用 visualization.PieChart 画甜甜圈图(pieHole: 0.4),宽度 440、高度 400,DOM 挂在 chart_div2 上;drawChart1 和 drawChart4 则走 charts.Bar 柱状图,分别绑 chart_div1、chart_div4。三个回调都在 google.charts.setOnLoadCallback 里注册,页面 load 完后自动出图。 你只需把 data2 的数组换成自己 EA 的回测输出,本地起个 html 双击就能看分布。外汇和贵金属品种波动大、杠杆高,回测盈利不代表实盘概率,图表只是帮你看清样本偏差。

MQL5 / C++
  [&class="macro">#x27;品种&class="macro">#x27;, &class="macro">#x27;盈利&class="macro">#x27;],
  [&class="macro">#x27;Si-class="num">6.16&class="macro">#x27;, class="num">82.00],
  [&class="macro">#x27;Si-class="num">9.16&class="macro">#x27;, class="num">56.00],
  [&class="macro">#x27;SBRF-class="num">9.16&class="macro">#x27;, class="num">546.00],
]);
  var options2 = {
    title: &class="macro">#x27;品种的盈利, %&class="macro">#x27;,
    pieHole: class="num">0.4,
    width: class="num">440,
    height: class="num">400,
  };
  var chart = new google.visualization.PieChart(document.getElementById(&class="macro">#x27;chart_div2&class="macro">#x27;));
  chart.draw(data2, options2);
    google.charts.load(&class="macro">#x27;current&class="macro">#x27;, {&class="macro">#x27;packages&class="macro">#x27;:[&class="macro">#x27;bar&class="macro">#x27;, &class="macro">#x27;corechart&class="macro">#x27;]});
<html>
  <head>
    <!--加载 AJAX API-->
    <script type="text/javascript" src="https:class=class="str">"cmt">//www.gstatic.com/charts/loader.js"></script>
    <script type="text/javascript">
      google.charts.load(&class="macro">#x27;current&class="macro">#x27;, {&class="macro">#x27;packages&class="macro">#x27;:[&class="macro">#x27;bar&class="macro">#x27;, &class="macro">#x27;corechart&class="macro">#x27;]});
      google.charts.setOnLoadCallback(drawChart1);
      google.charts.setOnLoadCallback(drawChart2);
      google.charts.setOnLoadCallback(drawChart4);
      function drawChart1() {
        var data1 = ...
        var options1 = ...
        var chart = new google.charts.Bar(document.getElementById(&class="macro">#x27;chart_div1&class="macro">#x27;));
        chart.draw(data1, options1);
      }
      function drawChart2() {
        var data2 = ...
        var options2 = ...
        var chart = new google.visualization.PieChart(document.getElementById(&class="macro">#x27;chart_div2&class="macro">#x27;));
        chart.draw(data2, options2);
      }
      function drawChart4() {
        var data4 = ...
        var options4 = ...
        var chart = new google.charts.Bar(document.getElementById(&class="macro">#x27;chart_div4&class="macro">#x27;));
        chart.draw(data4, options4);
      }
    </script>
  </head>
  <body>
    <!--Div 将容纳饼状图表-->
      <table>
        <tr>
          <td><div id="chart_div1"></div></td>
          <td><div id="chart_div2"></div></td>
          <td><div id="chart_div4"></div></td>
      </table>
    <br/>
  </body>
</html>
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到按小时和月份的仓位盈利分布,不必自己搭 HTML 报告。

常见问题

仓位是同一标的从开到平的整体单元,单笔成交会把盈亏拆碎,只有按 POSITION_IDENTIFIER 重建仓位才能拿到真实持仓利润与首单开仓时间。
可以,小布盯盘的品种页已内置仓位级盈利分布视图,按小时、天、月切分,省去手写 MQL5 与谷歌图表的对接。
文中用到直方图与饼状图,可单行排列也可上下混排,比如直方图加饼图再加直方图,用来分别看小时、占比与月份分布。
终端读取当前账户真实历史,测试器读取回测成交流,两者都需先重建仓位,但数据来源不同,前者验证实盘倾向后者验证样本外概率。