自定义交易历史表述并创建报告图表(基础篇)
📘

自定义交易历史表述并创建报告图表(基础篇)

第 1/3 篇

把交易历史导出成自定义图表

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 聚合,根本看不出原仓位净敞口。 外汇和贵金属品种同样存在这种流水噪声,直接读终端报告做价格行为统计容易失真,聚合后再算才靠谱。

MQL5 / C++
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。外汇贵金属品种同样适用该结构,但杠杆与隔夜费会让峰值手数和实际盈亏偏离更夸张,复盘前先确认账户是撮合而非做市环境,这类品种高风险,历史最大持仓不代表未来可承受。

MQL5 / C++
<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 留到毫秒,是为了同一秒内的加仓或剥头皮能区分先后;comissionswap 分开存,回测时算净值曲线才不会把成本漏掉。外汇和贵金属杠杆高,滑点之外这两项是实打实侵蚀盈利的,结构里不记就后面补不回来。 DealKeeper 按仓位 ID 聚合成组,DT_minDT_max 直接框住持仓生命周期。逆势加仓导致一仓多笔成交的情况,用 deals[] 动态数组兜住,不用另写判断。 CDealHistoryGetter 的两个公开方法把时间区间 [from,till] 丢进去就能拉数据:getHistoryDealKeeper 数组,getIDArr 单独拿 ID 列表做索引。开 MT5 建个 EA 把这段粘进头文件,跑一次历史提取,验证自己上个月的 XAUUSD 单佣金占比是否超过盈亏的 15%——可能比你直觉高。

MQL5 / C++
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 的数组

常见问题

先按订单号和开平方向把零散成交聚合成单笔仓位,再补上点差与库存费字段,就能得到可排序的账本表格。
把交易历史按时间轴导出为结构化数据,用脚本批量生成含开平仓标记的价格走势图,十分钟可出一周复盘。
可以,小布能直接读取你的成交流水,自动收敛成单笔仓位账本并标记变动保证金,打开对应品种页即可看。
每日结算后重算占用保证金与净值差,画出差额曲线,差值转负且扩大的日期就是穿仓风险窗口,贵金属杠杆高需警惕。
重点看合成净值回撤、单日盈亏波动和胜率衰减,三者同时恶化时说明算法可能已不适应当前行情。