交易员之活学活用: "平静" 优化或绘制交易分布(基础篇)
「用分布图揪出策略的平静区与异常区」
很多交易员只看权益曲线,却忽略了一个更底层的视角:把每笔交易按某种维度排成分布,能直接看出策略在‘平静’和‘躁动’两种状态下的真实表现。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 把图绘出来。外汇和贵金属品种波动大、杠杆高,历史分布只是概率画像,不能当成未来收益依据。
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,后面画各种分布图直接拿它当库用。外汇和贵金属杠杆高,历史重建只是复盘手段,不代表任何未来胜率。
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 里打印即可。 外汇与贵金属属高杠杆品种,这类反手逻辑在滑点扩大时可能让单笔亏损快速叠加,实盘前务必用策略测试器跑通历史数据。
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) {