自定义交易历史表述并创建报告图表(基础篇)
把交易历史导出成自定义图表
MT5 自带的历史报表格式固定,想按自己的口径统计胜率、持仓时长或品种分布,得自己写脚本捞数据。平台提供 HistorySelect() 按时间区间抽取挂单与成交,再用 HistoryDealsTotal() 遍历每笔成交的字段。 实际跑过的人知道,默认报表不区分手动单与 EA 单,复盘时容易混淆策略边界。用 MQL5 拉一次 2018 年 9 月全月成交,在 EURUSD 上约能拿到数百条 deal,逐条读 DEAL_SYMBOL / DEAL_PROFIT 就能重画盈亏曲线。 外汇与贵金属杠杆高、滑点随机,自建报表只是看清过去,不代表未来复制同样结果。代码跑通后建议先在小周期样本验证字段映射,再扩到全量。
◍ 把交易算法当合成资产来盯
做交易绕不开一件事:回头看自己到底赚在哪、亏在哪。不管是手动机还是EA,历史成交记录都是检验策略成色的唯一硬证据。 核心思路是把一套交易算法看成一种“合成资产”——它没有Black-Scholes那种现成公式来定价,但可以用编程逻辑生成自己的盈亏曲线,再拿这条曲线跟标的品种的多头表现去比。问题在于,这种曲线在不同阶段的利润分布并不稳定,难以用统一财经模型刻画。 一个务实做法是监控算法交易的回撤,捕捉实际结果与预期的偏离。本文不教你怎么建财经模型,只给一套能把交易历史完整翻出来的方法;拿到数据后,概率特征、复杂模型都你自己搭。 技术部分先讲存在终端里的大数据怎么提取成报告,数据源就是这些成交明细。后面再选样本(全品种或单个符号、全历史或某区间)算回逆,结果写进独立文件并在终端简显。文里示例用的是我实盘真实历史,而代码演示数据是我特意在模拟账户上跑出来的测试区间。
「把 MT5 成交流水重排成可分析仓位」
MT5 终端导出的交易历史按成交时间排序,但一笔 6 月 1 日开、6 月 5 日平的仓位,在表格里会排在一笔 6 月 2 日开平的同品种仓位之前——这种顺序和实际平仓顺序错位,直接拿来做统计会误判持仓周期与盈亏节奏。 真正能用的样本应按「品种 + 仓位 ID」聚合。终端里仓位 ID 对加仓、反手都保持不变,反手只是方向变、ID 不变,所以用一个 DealKeeper 结构挂住同 ID 下的所有成交,就能还原一条完整仓位线。 核心取数靠 HistorySelectByPosition(PositionID),它只拉该 ID 的成交,自动剔掉入金出金和保证金变动行。再用 HistoryDealsTotal 拿总数,循环读每条成交填进数组。下面这段是填充逻辑的主干: 下面是一份真实导出的 Si-12.17 仓位片段:23.11.2017 17:41 以 58736 开 1 手多,之后每天 13:00 和 15:48 出现 variation margin close/open 对,价格从 58682 一路晃到 59181,单笔盈亏在 -468 到 +624 之间摆动。这种高频标记行如果不按 ID 聚合,根本看不出原仓位净敞口。 外汇和贵金属品种同样存在这种流水噪声,直接读终端报告做价格行为统计容易失真,聚合后再算才靠谱。
class="kw">struct DealData { class="type">long ticket; class="type">long order; class="type">class="kw">datetime time; class="type">long time_msc; class="type">long type; class="type">long entry; class="type">long magic; class="type">long reason; class="type">long position_id; class="type">class="kw">double volume; class="type">class="kw">double price; class="type">class="kw">double commission; class="type">class="kw">double swap; class="type">class="kw">double profit; class="type">class="kw">string symbol; class="type">class="kw">string comment; class="type">long external_id; }; class="kw">struct DealKeeper { class="type">class="kw">string symbol; class="type">long position_id; class="type">int total_deals; DealData deals[]; }; class CDealHistoryGetter { class="kw">public: class="type">bool getHistory(DealKeeper &keepers[], class="kw">const class="type">class="kw">string symbol, class="type">class="kw">datetime from, class="type">class="kw">datetime to) { CArrayLong unique_ids; class=class="str">"cmt">// 先扫一遍成交,收集独有 position_id class="type">int total = HistoryDealsTotal(); for(class="type">int i=class="num">0; i<total; i++) { class="type">class="kw">ulong deal_ticket = HistoryDealGetTicket(i); class="type">long pos_id = HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); class="type">class="kw">string sym = HistoryDealGetString(deal_ticket, DEAL_SYMBOL); if(sym == symbol && unique_ids.Search(pos_id) < class="num">0) unique_ids.Add(pos_id); } class=class="str">"cmt">// 按 ID 拉完整成交 for(class="type">int k=class="num">0; k<unique_ids.Total(); k++) { class="type">long pid = unique_ids.At(k); HistorySelectByPosition(pid); class="type">int cnt = HistoryDealsTotal(); DealKeeper keeper; keeper.symbol = symbol; keeper.position_id = pid; keeper.total_deals = cnt; ArrayResize(keeper.deals, cnt); for(class="type">int j=class="num">0; j<cnt; j++) { class="type">class="kw">ulong t = HistoryDealGetTicket(j); keeper.deals[j].ticket = (class="type">long)t; keeper.deals[j].time = (class="type">class="kw">datetime)HistoryDealGetInteger(t, DEAL_TIME); keeper.deals[j].type = HistoryDealGetInteger(t, DEAL_TYPE); keeper.deals[j].volume = HistoryDealGetDouble(t, DEAL_VOLUME); keeper.deals[j].price = HistoryDealGetDouble(t, DEAL_PRICE); keeper.deals[j].profit = HistoryDealGetDouble(t, DEAL_PROFIT); } keepers[ArraySize(keepers)] = keeper; } class="kw">return true; } };
逐日盯穿变动保证金的穿仓轨迹
上面这组成交记录来自 Si-12.17 合约在 2017 年 12 月 6 日至 21 日的账户流变,全部由 DEAL_REASON_VMARGIN 触发,也就是经纪商在保证金不足时强制平补的变动保证金单。每对 [variation margin open] 与 [variation margin close] 都是先 SELL 平出、再 BUY 回补同一仓位,净盈亏栏里的数字直接反映当日重估损益。 把净盈亏拎出来看:12 月 8 日 15:48 那笔是 -78,12 月 11 日 13:00 跳到 -206,12 月 12 日 13:00 更砸到 -720,随后 12 月 14 日 13:00 是 -796。连续负值说明持仓方向在那几天持续吃变动保证金,账户权益被每日重估反复刮走。
- 月 14 日 14:48 出现一条例外:DEAL_REASON_EXPERT 的 SELL 平仓,成交量 0.5、注释写 PartialClose position_2,这是 EA 主动部分平仓,和 VMARGIN 强制单混在同一时间窗里。之后 VMARGIN 单的 magic 从 2 变成 1,说明强平后剩余仓位的标识切换了。
做贵金属或外汇的人该警觉:这种逐日 VMARGIN 刮净值的现象,在高杠杆品种上可能几天就把缓冲吃光。开 MT5 账户历史去看 DEAL_REASON 筛选项,把 VMARGIN 单独拉出来,就能复现自己账户里有没有同类暗亏。
◍ 把零散成交收敛成单笔仓位账本
MT5 默认的成交历史会把一笔仓位拆成多笔 deal 记录,例如 FORTS 模拟户上的 Si-12.17 期货,2017.12.21 13:00 因变化保证金入场两笔相同手数,15:45 到期强制平仓时最后一笔平仓成交的订单 ID 为 0。直接拿这些原始记录做分析会漏掉滑点、手续费和仓位量变,必须重建成「一行一笔仓位」的表。 重建逻辑依赖三个私有方法:isBorderPoint 判断该成交是否算边界点(手工开平或加减仓),MA_price 用成交量加权平均算真实开平仓价,calcContracts 追踪持仓期间的合约数峰值。以动态仓位为例,t1 到 t10 累计买卖各 11 手,但按 +/- 符号滚动计算后,持仓峰值只到 8 手——这个 8 才是策略回测该用的暴露量。 isBorderPoint 用订单 ID、DEAL_ENTRY、DEAL_TYPE、DEAL_REASON 四个维度过滤噪音。像 DEAL_REASON_VMARGIN、DEAL_TYPE_ROLLOVER 这类归到 OtherPoint 直接忽略;到期强平的平仓单允许订单 ID=0 仍记为 UsualOut。getDeals_forSelectedKeeper 从第 404 行声明数组、411-414 行只留边界点,非边界成交仅累加盈亏,避免用「开平价差」粗暴算利而丢掉佣金与部分平仓影响。 跑完后得到逐仓汇总表:RTS-12.17 在 2017.11.17 19:53 开 2 手多单,隔一分钟平掉,每手亏 25.78、总亏 55.56;Si-12.17 一笔 2017.11.23 开、2017.12.21 到期的 2 手多单总亏 253.5。外汇贵金属品种同样适用该结构,但杠杆与隔夜费会让峰值手数和实际盈亏偏离更夸张,复盘前先确认账户是撮合而非做市环境,这类品种高风险,历史最大持仓不代表未来可承受。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 选定成交的数据结构 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="kw">struct</span> DealData {
「把成交记录拆成可分析的结构体」
想用 MT5 复盘自己的黄金或欧美短线单,第一步是把杂乱的成交历史变成能逐字段读的结构。下面这套定义直接对应终端历史里的每条 deal,字段粒度到毫秒和佣金,够了。 [CODE]struct DealData { long ticket; // 成交单号 long order; // 此笔成交的订单编号 datetime DT; // 开仓日期 long DT_msc; // 开仓日期的毫秒值 ENUM_DEAL_TYPE type; // 开仓类型 ENUM_DEAL_ENTRY entry; // 开仓入场类型 long magic; // 仓位的独有编号 ENUM_DEAL_REASON reason; // 如何下订单 long ID; // 仓位 ID double volume; // 仓位交易量 (手数) double price; // 开仓价格 double comission; // 支付佣金 double swap; // 隔夜利息 double profit; // 盈亏 string symbol; // 品名 string comment; // 开仓时指定的注释 string ID_external; // 外部 ID }; //+------------------------------------------------------------------+
| // | 存储特定仓位所有成交的结构, |
|---|---|
| // | 由 ID 选择 |
//+------------------------------------------------------------------+ struct DealKeeper { DealData deals[]; /* 此仓位的所有成交清单(或仓位逆转时的多笔仓位)*/ string symbol; // 品名 long ID; // 仓位的 ID datetime DT_min; // 开仓日期 datetime DT_max; // 平仓日期 }; //+-----------------------------------------------------------------------+
| // | 从终端提取交易历史的类,并 |
|---|---|
| // | 将其转换为易于分析的视图 |
//+-----------------------------------------------------------------------+
class CDealHistoryGetter
{
public:
bool getHistory(DealKeeper &deals[],datetime from,datetime till); // 返回所有历史成交的数据
bool getIDArr(ID_struct &ID_arr[],datetime from,datetime till); // 返回每笔成交独有 ID 的数组[/CODE]
DealData 单行记录一次成交:DT_msc 留到毫秒,是为了同一秒内的加仓或剥头皮能区分先后;comission 和 swap 分开存,回测时算净值曲线才不会把成本漏掉。外汇和贵金属杠杆高,滑点之外这两项是实打实侵蚀盈利的,结构里不记就后面补不回来。
DealKeeper 按仓位 ID 聚合成组,DT_min 到 DT_max 直接框住持仓生命周期。逆势加仓导致一仓多笔成交的情况,用 deals[] 动态数组兜住,不用另写判断。
CDealHistoryGetter 的两个公开方法把时间区间 [from,till] 丢进去就能拉数据:getHistory 填 DealKeeper 数组,getIDArr 单独拿 ID 列表做索引。开 MT5 建个 EA 把这段粘进头文件,跑一次历史提取,验证自己上个月的 XAUUSD 单佣金占比是否超过盈亏的 15%——可能比你直觉高。
class="kw">struct DealData { class="type">long ticket; class=class="str">"cmt">// 成交单号 class="type">long order; class=class="str">"cmt">// 此笔成交的订单编号 class="type">class="kw">datetime DT; class=class="str">"cmt">// 开仓日期 class="type">long DT_msc; class=class="str">"cmt">// 开仓日期的毫秒值 ENUM_DEAL_TYPE type; class=class="str">"cmt">// 开仓类型 ENUM_DEAL_ENTRY entry; class=class="str">"cmt">// 开仓入场类型 class="type">long magic; class=class="str">"cmt">// 仓位的独有编号 ENUM_DEAL_REASON reason; class=class="str">"cmt">// 如何下订单 class="type">long ID; class=class="str">"cmt">// 仓位 ID class="type">class="kw">double volume; class=class="str">"cmt">// 仓位交易量 (手数) class="type">class="kw">double price; class=class="str">"cmt">// 开仓价格 class="type">class="kw">double comission; class=class="str">"cmt">// 支付佣金 class="type">class="kw">double swap; class=class="str">"cmt">// 隔夜利息 class="type">class="kw">double profit; class=class="str">"cmt">// 盈亏 class="type">class="kw">string symbol; class=class="str">"cmt">// 品名 class="type">class="kw">string comment; class=class="str">"cmt">// 开仓时指定的注释 class="type">class="kw">string ID_external; class=class="str">"cmt">// 外部 ID }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 存储特定仓位所有成交的结构, | class=class="str">"cmt">//| 由 ID 选择 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct DealKeeper { DealData deals[]; class=class="str">"cmt">/* 此仓位的所有成交清单(或仓位逆转时的多笔仓位)*/ class="type">class="kw">string symbol; class=class="str">"cmt">// 品名 class="type">long ID; class=class="str">"cmt">// 仓位的 ID class="type">class="kw">datetime DT_min; class=class="str">"cmt">// 开仓日期 class="type">class="kw">datetime DT_max; class=class="str">"cmt">// 平仓日期 }; class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//| 从终端提取交易历史的类,并 | class=class="str">"cmt">//| 将其转换为易于分析的视图 | class=class="str">"cmt">//+-----------------------------------------------------------------------+ class CDealHistoryGetter { class="kw">public: class="type">bool getHistory(DealKeeper &deals[],class="type">class="kw">datetime from,class="type">class="kw">datetime till); class=class="str">"cmt">// 返回所有历史成交的数据 class="type">bool getIDArr(ID_struct &ID_arr[],class="type">class="kw">datetime from,class="type">class="kw">datetime till); class=class="str">"cmt">// 返回每笔成交独有 ID 的数组