交易员之活学活用:
📘

交易员之活学活用: "平静" 优化或绘制交易分布(基础篇)

第 1/3 篇

「用分布图揪出策略的平静区与异常区」

很多交易员只看权益曲线,却忽略了一个更底层的视角:把每笔交易按某种维度排成分布,能直接看出策略在‘平静’和‘躁动’两种状态下的真实表现。MT5 自带的工具就能干这事,不用外接脚本。 在 2016 年 10 月的一篇公开示例中,作者 Vladimir Karputov 用一张交易分布图展示了 4214 次历史 tick 聚合后的结果,社区互动数 11,说明这类可视化虽冷门但有人用。 所谓‘平静优化’,是指不在参数表层调来调去,而是观察分布形态:如果峰值集中在窄区间、尾部极短,策略大概率处于低波动适配态;若分布出现双峰或长尾,就要警惕过拟合或行情结构切换。外汇和贵金属市场杠杆高、跳空频繁,这种分布异动往往先于权益回撤出现。 开 MT5 跑一遍你自己的 EA 回测,把‘按盈利排序的交易分布’调出来,先看峰值宽度,再数长尾笔数——这一步比改止盈参数更有参考价值。

这套分析工具到底能拆出什么

这一节先把后续要展开的技术骨架列出来,方便你对着 MT5 terminal 边看边验。核心目标只有一个:把交易历史里的每一笔仓位,重建成可可视化的分布数据。 重建仓位部分会拆到四个细节点:仓位反转的逻辑怎么判定、单笔盈利如何按点值回算、开仓时间戳怎么取、以及中间结果存在哪类数组里不丢精度。 可视化走谷歌图表接口,三种常用形态都得会接:'bar' 直方、'corechart' 饼图、以及三图横向拼排的组合布局。 跑数据的入口有两个:直接挂当前账户读真实历史,或丢进策略测试器用回测结果跑。最后还会提到一种波动极小的“平静”优化思路,以及分布随参数变化的表现。

◍ 按开仓时间拆盈利分布

写 EA 时通常先码逻辑、再丢进策略测试器跑优化,靠回测反推哪组输入参数能盈利。但参数空间太大,盲跑容易陷进过拟合,且你根本不知道策略“为什么”在某段历史里表现好。 换个切入法:在动优化之前,先按仓位的开仓时刻把盈利和亏损摊开看。注意是“仓位”而非零散成交——很多策略在一天里的某些小时、某些星期几、某些月份开仓,天然就处在概率有利或不利的窗口。 原文用 Google Charts 在 HTML 报告里画分布:报表头两行是账户汇总,其下按品种分别列出按小时、按天、按月的开仓盈利分布。这种图能让你在写信号规则前,就先看见时间维度的偏态。 外汇与贵金属杠杆高、滑点跳空频繁,时间分布只是概率倾向,不是入场保证;拿 MT5 导出的成交历史照这张表自己分桶统计,比直接信优化结果更踏实。

「把盈利分布脚本跑起来」

想直接看账户盈利的分布形态,先把 DistributionOfProfits.mqh 丢进 MT5 数据目录下的 MQL5\Include\ 文件夹,这是头文件,供脚本调用。 另存一份 test_report.mq5 到 MQL5\Scripts\,编译后会在输入参数里看到一个 start 时间框,默认值是 2010.05.11 12:05:00,改它能控制从哪天起拉历史交易做统计。 脚本逻辑很薄:OnStart 里先以 start 为起点跑 AnalysisTradingHistory,再调 ShowDistributionOfProfits 把图绘出来。外汇和贵金属品种波动大、杠杆高,历史分布只是概率画像,不能当成未来收益依据。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 test_report.mq5 |
class=class="str">"cmt">//|                                                                 版权所有 class="num">2016, MetaQuotes 软件公司|
class=class="str">"cmt">//|                                                                      [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2016, MetaQuotes 软件公司"
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property script_show_inputs
class=class="str">"cmt">//---
input class="type">class="kw">datetime start=D&class="macro">#x27;class="num">2010.05.class="num">11 class="num">12:class="num">05:class="num">00&class="macro">#x27;;
class="macro">#include <DistributionOfProfits.mqh>
class=class="str">"cmt">//---
CDistributionOfProfits Analysis;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序开始函数                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   Analysis.AnalysisTradingHistory(start);
   Analysis.ShowDistributionOfProfits();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

用成交ID拼回真实仓位

MT5终端只把历史拆成订单和成交两张表,但盯盘要的是「仓位」视角:一笔仓位的总盈利、什么时候开的。能串起它们的只有POSITION_IDENTIFIER,它写进每笔订单的ORDER_POSITION_ID和每笔成交的DEAL_POSITION_ID里。 只要从历史成交里捞出DEAL_POSITION_ID相同的所有记录,就能确定性地重建一个仓位。但反转是个坑:文档写明反转后仓位标识符会变成触发反转的那张订单号,意味着旧仓和新仓是两个ID。 跑一遍position_reversal_v1.mq5就能看清分配逻辑。脚本在2016.09.05 10:32:59动手,先买0.01,POSITION_IDENTIFIER=96633525,首笔成交DEAL_POSITION_ID也是96633525;接着卖0.02触发反转,新仓ID变96633564,但第二笔成交仍带旧ID 96633525、账面-0.06。结论很硬:导致反转的那笔成交归属于反转前的旧仓位。 盈利计算就顺着这个逻辑走:只筛DEAL_TYPE_BUY和DEAL_TYPE_SELL,把同DEAL_POSITION_ID的成交利润累加,即等于对应POSITION_IDENTIFIER的仓位盈利。开仓时间更简单——ORDER_TICKET等于POSITION_IDENTIFIER的那张订单,取它的ORDER_TIME_DONE即可。 重建完的仓位落进struct_positions数组,字段含id、time、loss(写正数号便于图表看)、profit、symbol_name,后面画各种分布图直接拿它当库用。外汇和贵金属杠杆高,历史重建只是复盘手段,不代表任何未来胜率。

MQL5 / C++
class="num">10:class="num">32:class="num">59.487      position_reversal_v1(EURUSD,M3)      Buy class="num">0.01, "EURUSD"
class="num">10:class="num">33:class="num">00.156      position_reversal_v1(EURUSD,M3)      Position EURUSD POSITION_IDENTIFIER #class="num">96633525
class="num">10:class="num">33:class="num">00.156      position_reversal_v1(EURUSD,M3)      Deal EURUSD volume class="num">0.01 DEAL_POSITION_ID #class="num">96633525 profit class="num">0.00
class="num">10:class="num">33:class="num">00.156      position_reversal_v1(EURUSD,M3)      
class="num">10:class="num">33:class="num">06.187      position_reversal_v1(EURUSD,M3)      Sell class="num">0.02, "EURUSD"
class="num">10:class="num">33:class="num">06.871      position_reversal_v1(EURUSD,M3)      Position EURUSD POSITION_IDENTIFIER #class="num">96633564
class="num">10:class="num">33:class="num">06.871      position_reversal_v1(EURUSD,M3)      Deal EURUSD volume class="num">0.01 DEAL_POSITION_ID #class="num">96633525 profit class="num">0.00
class="num">10:class="num">33:class="num">06.871      position_reversal_v1(EURUSD,M3)      Deal EURUSD volume class="num">0.02 DEAL_POSITION_ID #class="num">96633525 profit -class="num">0.06
class="num">10:class="num">33:class="num">06.871      position_reversal_v1(EURUSD,M3)      
class="num">10:class="num">33:class="num">12.924      position_reversal_v1(EURUSD,M3)      PositionClose, "EURUSD"

◍ 从成交日志反推仓位盈亏结构

上面这段日志来自 EURUSD 的 M3 周期策略 position_reversal_v1,同一毫秒 10:33:13.593 内连续打出三笔成交:0.01 手平仓利润 0.00,0.02 手反手亏损 -0.06,另一笔 0.01 手亏损 -0.10,仓位 ID 分别是 96633525 与 96633564。这类日志如果不做聚合,肉眼很难判断某个 position_id 下整体是赚是亏。 MT5 的成交历史是逐笔 Deal 记录的,而非按 Position 汇总。想看清每个仓位的净盈亏,就得用 HistorySelect 拉区间、再按 DEAL_POSITION_ID 归并 DEAL_PROFIT。下方函数把这件事做成了结构体数组,直接在 EA 里打印即可。 外汇与贵金属属高杠杆品种,这类反手逻辑在滑点扩大时可能让单笔亏损快速叠加,实盘前务必用策略测试器跑通历史数据。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 打印仓位盈利                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PrintProfitPositions(class="type">void)
  {
class=class="str">"cmt">//--- 仓位盈利结构;
   class="kw">struct struct_positions
     {
       class="type">long                position_id;
       class="type">class="kw">double              position_profit;
       class=class="str">"cmt">//--- 构造器
                         struct_positions() {position_id=class="num">0; position_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">//--- 得到成交属性 
         deal_id  =HistoryDealGetInteger(ticket,DEAL_POSITION_ID);
         profit   =HistoryDealGetDouble(ticket,DEAL_PROFIT);
         type     =HistoryDealGetInteger(ticket,DEAL_TYPE);
         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].position_id==deal_id)
                 {

常见问题

把开仓时间按小时分组画分布,柱体接近零的时段就是平静区,可据此暂停或减仓该时段运行。
能拆出按开仓时间、品种、持仓时长的盈亏分布,标出异常高/低盈利桶,定位策略脆弱点。
小布可读取你的成交日志自动绘出分布图,直接标注平静区与异常区,省去手动跑脚本。
用成交id关联同一笔仓位的开平记录,按差价算盈亏,再按id聚合即得完整仓位结构。
重点看成交id、买卖方向、手数、开平时间,用id分组求和盈亏即可还原仓位层级结果。