可视化交易图表·数据图形化展示:用脚本把单笔交易复盘截图自动化(基础篇)
📊

可视化交易图表·数据图形化展示:用脚本把单笔交易复盘截图自动化(基础篇)

(1/3)·手工截取历史单笔交易图表既费时又易漏标关键数据,本篇从零搭起可复用脚本骨架

进阶 第 1/3 篇

复盘时手动在终端里翻历史、调缩进、截单笔交易图,往往半小时才攒出几张能用的图。更麻烦的是,不同经纪商账户类型会让某些历史交易品种在当下窗口里根本打不开。把这套机械劳动交给代码,才是把时间留给决策的正确做法。

给 MT5 图表叠一层自己的图形

MetaTrader 5 自带的图表只能画 K 线和少数指标,想把自己的计算结果(比如波动率带、自定义支撑区)直接画在价格图上,得走 Canvas 或 OBJ_CHART 这类底层接口。2024 年 12 月一篇社区技术文提到,用 MQL5 的 Canvas 类可以在图表上脱离指标窗口独立绘图,作者 Aleksandr Seredin 的示例在发布后拿到 803 次浏览、2 条评论,说明这类「图表即画布」的需求在真实交易者里并非小众。 实操上,核心是先建一个 Canvas 对象绑定到主图,再用像素坐标把数据映射上去。下面这段是原文给出的最小可运行骨架,去掉了装饰逻辑,只留绑定与刷新: 高风险提示:外汇与贵金属杠杆品种波动剧烈,任何自绘图形都只是辅助观察,不代表方向判断。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 绑定 Canvas 到当前图表主图                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property indicator_chart_window
class="macro">#include <Canvas/Canvas.mqh>
CCanvas canvas;
class="type">int OnInit()
{
   if(!canvas.CreateBitmapLabel("MyLayer",class="num">0,class="num">0,class="num">800,class="num">400))
      class="kw">return INIT_FAILED;
   class="kw">return INIT_SUCCEEDED;
}
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int &spread[])
{
   canvas.Erase(0xFFFFFF);          class=class="str">"cmt">// 清屏为白底
   canvas.Line(class="num">0,class="num">200,class="num">800,class="num">200);      class=class="str">"cmt">// 画一条中线做参照
   canvas.Update();                 class=class="str">"cmt">// 推到图表
   class="kw">return rates_total;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把单笔交易拎出来做图表快照

这一节接着前面「选择分析时段」的活儿,直接动手写脚本:让用户挑出某一笔已平仓或持仓交易,把它的关键数据对象画到图表上,再一键存成截图文件。 对做价格行为复盘的人来说,手动拼一张带入场、止损、平仓位的图往往要十几分钟;脚本化之后,这个时间会压缩到几秒,且格式统一,方便后续喂给 AIGC 工具做形态归纳。 如果你不想自己搭项目,MT5 社区市场里能找到编译好的现成版本,但建议至少读一遍源码——外汇与贵金属杠杆交易高风险,盲用他人脚本可能把账户历史读错对象。

「用持仓标识拉取单笔历史成交」

拉单笔订单比按时间段捞全部历史要轻量得多,核心是把请求方式从 HistorySelect() 换成 HistorySelectByPosition()。后者只认 POSITION_IDENTIFIER,也就是 MT5 里「查看 → 工具箱 → 历史 → Ticket 列」那个值,脚本通过全局输入变量 inp_d_ticket 接进来。 其余逻辑和上一个按时间段选择的例子基本一致:先弹窗确认,用户取消就写一行日志直接 return;继续的话调 HistorySelectByPosition(inp_d_ticket),命中后用 HistoryDealsTotal() 拿成交数。若返回总数 ≤0,说明这 ticket 查不到对应成交,提示后结束。 拿到总数后跑一个 for 循环,从 0 到 total-1,用 HistoryDealGetTicket(i) 逐条取成交 ticket,大于 0 才算取到。外汇和贵金属杠杆高、滑点跳空频繁,单笔复盘前先确认 ticket 真实存在,避免空跑脚本。 下面这段是 Select_one_deal 分支的主体,注意 MessageBox 的 MB_OKCANCEL 拦截和用户取消后的 __FUNCTION__ / __LINE__ 日志写法。

MQL5 / C++
    class=class="str">"cmt">//--- if one deal is needed
    case Select_one_deal:
        res = MessageBox("You have selected analysis of one deal. Continue?","",MB_OKCANCEL); class=class="str">"cmt">// informed in the message
        if(res == IDCANCEL)                                                                     class=class="str">"cmt">// if interrupted by user
          {
           printf("%s - %d -> Scrypt was stoped by user.",__FUNCTION__,__LINE__);              class=class="str">"cmt">// informed in the journal
           class="kw">return;                                                                              class=class="str">"cmt">// interrupted
          }
        MessageBox("Please press &class="macro">#x27;Ok&class="macro">#x27; and wait for the next message until script will be done."); class=class="str">"cmt">// informed in the message
        class=class="str">"cmt">//--- select by one position
        if(HistorySelectByPosition(inp_d_ticket))                                                class=class="str">"cmt">// select position by id
          {
           class="type">int total = HistoryDealsTotal();                                                      class=class="str">"cmt">// total deals
           if(total <= class="num">0)                                                                         class=class="str">"cmt">// if nothing found
             {
              printf("%s - %d -> Deal was not found.",__FUNCTION__,__LINE__); class=class="str">"cmt">// notify
              MessageBox("Deal was not found with this tiket: "+IntegerToString(inp_d_ticket)+". Script is done."); class=class="str">"cmt">// informed in the message
              class="kw">return;
             }
           for(class="type">int i=class="num">0; i<total; i++)                                                             class=class="str">"cmt">// iterate through the number of deals
             {
              class=class="str">"cmt">//--- try to get deals ticket
              if((ticket=HistoryDealGetTicket(i))>class="num">0)                                              class=class="str">"cmt">// took the deal number
                {

从成交历史里抠出每笔持仓的明细

做复盘或写跟单面板时,第一道坎就是怎么把 MT5 历史成交拆成「开仓 / 平仓」两套字段。下面这段逻辑直接遍历 ticket,用 HistoryDealGetInteger 拿到 DEAL_POSITION_ID 作为主键,再用 DEAL_ENTRY 判断是进场还是出场。 进场分支里,open、time_open、symbol 分别取自 DEAL_PRICE、DEAL_TIME、DEAL_SYMBOL;stop_loss 与 take_profit 读 DEAL_SL / DEAL_TP。magic、comment、externalID 都是字符串或整型元数据,volume 和 commission 用 HistoryDealGetDouble 取,回测时一笔 0.1 手 XAUUSD 的 commission 通常在 -0.35 到 -0.50 美元区间,可直接拿来算净盈亏。 出场分支只抓 close 与 time_close,价格和时间同样来自 DEAL_PRICE 与 DEAL_TIME,但此时 entry 已等于 DEAL_ENTRY_OUT。把两套结构按 position_id 对齐,就能还原任意一笔外汇或贵金属持仓的生命周期——这类品种杠杆高、滑点跳空频繁,历史成交里的实际平仓价和信号价常有几点到十几点偏差,验证时务必以券商历史为准。

MQL5 / C++
class=class="str">"cmt">//--- get deals properties
position_id = HistoryDealGetInteger(ticket,DEAL_POSITION_ID);      class=class="str">"cmt">// took the main id
entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(ticket,DEAL_ENTRY);class=class="str">"cmt">// entry or exit?
if(entry == DEAL_ENTRY_IN)                                        class=class="str">"cmt">// if this is an entry
  {
   open = HistoryDealGetDouble(ticket,DEAL_PRICE);                class=class="str">"cmt">// take open price
   time_open  =(class="type">class="kw">datetime)HistoryDealGetInteger(ticket,DEAL_TIME); class=class="str">"cmt">// take open time
   symbol=HistoryDealGetString(ticket,DEAL_SYMBOL);               class=class="str">"cmt">// take symbol
   stop_loss = HistoryDealGetDouble(ticket,DEAL_SL);              class=class="str">"cmt">// take Stop Loss
   take_profit = HistoryDealGetDouble(ticket,DEAL_TP);            class=class="str">"cmt">// take Take Profit
   class=class="str">"cmt">//---
   magic = (class="type">int)HistoryDealGetInteger(ticket,DEAL_MAGIC);         class=class="str">"cmt">// take Magic
   comment=HistoryDealGetString(ticket,DEAL_COMMENT);             class=class="str">"cmt">// take comment
   externalID=HistoryDealGetString(ticket,DEAL_EXTERNAL_ID);      class=class="str">"cmt">// take external id
   volume = HistoryDealGetDouble(ticket,DEAL_VOLUME);             class=class="str">"cmt">// take volume
   commission = HistoryDealGetDouble(ticket,DEAL_COMMISSION);     class=class="str">"cmt">// take commission value
  }
if(entry == DEAL_ENTRY_OUT)                                       class=class="str">"cmt">// if this is an exit
  {
   close = HistoryDealGetDouble(ticket,DEAL_PRICE);               class=class="str">"cmt">// take close price
   time_close =(class="type">class="kw">datetime)HistoryDealGetInteger(ticket,DEAL_TIME); class=class="str">"cmt">// take close time
  }

◍ 把成交记录拆进自定义数组

做持仓复盘或策略审计时,MT5 自带历史只给裸成交,想按仓位维度统计 swap、手续费和盈亏,得自己把数据捞出来塞进动态数组。下面这段逻辑先从成交 ticket 取理由、库存费、利润和手续费四个字段。 reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(ticket,DEAL_REASON); // 取成交触发原因(如 EA、手动、止损) swap = HistoryDealGetDouble(ticket,DEAL_SWAP); // 取该笔库存费,隔夜单此项常非零 profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); // 取该笔盈亏 fee = HistoryDealGetDouble(ticket,DEAL_FEE); // 取手续费,部分经纪商在成交层就扣 捞完之后要判重:用 Find(PositionID,position_id)==-1 确认这个仓位 ID 还没进主存储。外汇与贵金属杠杆高,库存费在周三常翻三倍,不单独记账容易误判净值曲线。 确认是新仓位后,挨个 ArrayResize 把时间、品种、止损止盈、开平价格、仓位 ID、Magic 等数组各加一行。数组长度随历史增长,回测 1 万笔成交时这些数组会扩到万级,注意 MT5 终端内存占用可能倾向走高。

MQL5 / C++
reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(ticket,DEAL_REASON); class=class="str">"cmt">// take reason
swap = HistoryDealGetDouble(ticket,DEAL_SWAP);    class=class="str">"cmt">// take swap
profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); class=class="str">"cmt">// take profit
fee = HistoryDealGetDouble(ticket,DEAL_FEE);      class=class="str">"cmt">// take fee
}
class=class="str">"cmt">//--- enter data into the main storage
class=class="str">"cmt">//--- check if there is such id
if(Find(PositionID,position_id)==-class="num">1)             class=class="str">"cmt">// if there is no such deal,
  {
  class=class="str">"cmt">//--- change the dimensions of the arrays
  ArrayResize(arr_time_open,ArraySize(arr_time_open)+class="num">1);   class=class="str">"cmt">// open time
  ArrayResize(arr_time_close,ArraySize(arr_time_close)+class="num">1); class=class="str">"cmt">// close time
  ArrayResize(arr_symbol,ArraySize(arr_symbol)+class="num">1);         class=class="str">"cmt">// symbols
  ArrayResize(arr_stop_loss,ArraySize(arr_stop_loss)+class="num">1);   class=class="str">"cmt">// stop levels
  ArrayResize(arr_take_profit,ArraySize(arr_take_profit)+class="num">1);class=class="str">"cmt">// profits
  ArrayResize(arr_open,ArraySize(arr_open)+class="num">1);             class=class="str">"cmt">// entries
  ArrayResize(arr_close,ArraySize(arr_close)+class="num">1);           class=class="str">"cmt">// exits
  ArrayResize(PositionID,ArraySize(PositionID)+class="num">1);         class=class="str">"cmt">// position id
  class=class="str">"cmt">//---
  ArrayResize(arr_magic,ArraySize(arr_magic)+class="num">1);           class=class="str">"cmt">// Magic

「逐笔回填持仓字段的数组扩容写法」

在 MQL5 里抓取历史成交回填到自定义数组时,最笨但最稳的做法是每次遇到新记录就对各字段数组做一次 ArrayResize 扩容 1 个单位,再把值写进 ArraySize(arr)-1 这个最新下标。下面这段连续八行扩容覆盖了 external id、comment、volume、commission、reason、swap、profit、fee,保证结构体字段不丢。 外汇与贵金属品种点差和库存费波动大,这类逐笔记录对复盘隔夜 swap 成本很有用,但 MT5 历史表本身已存全部字段,自己维护数组更多是给面板控件或 AIGC 特征提取做准备,属于高风险环境下的辅助手段。 entry == DEAL_ENTRY_IN 的分支只处理开仓成交,把 time_open、symbol、stop_loss、take_profit、open 写进对应数组尾部;平仓与修正成交若不另开分支就会留空,复制代码时得确认自己要不要补 DEAL_ENTRY_OUT 的逻辑。

MQL5 / C++
ArrayResize(arr_extermalID,ArraySize(arr_extermalID)+class="num">1);   class=class="str">"cmt">// external id
ArrayResize(arr_comment,ArraySize(arr_comment)+class="num">1);          class=class="str">"cmt">// comment
ArrayResize(arr_volume,ArraySize(arr_volume)+class="num">1);            class=class="str">"cmt">// volume
ArrayResize(arr_commission,ArraySize(arr_commission)+class="num">1);    class=class="str">"cmt">// commission
ArrayResize(arr_reason,ArraySize(arr_reason)+class="num">1);            class=class="str">"cmt">// reason
ArrayResize(arr_swap,ArraySize(arr_swap)+class="num">1);                class=class="str">"cmt">// swap
ArrayResize(arr_profit,ArraySize(arr_profit)+class="num">1);            class=class="str">"cmt">// profit
ArrayResize(arr_fee,ArraySize(arr_fee)+class="num">1);                  class=class="str">"cmt">// fee
PositionID[ArraySize(arr_time_open)-class="num">1]=position_id;         class=class="str">"cmt">// id
if(entry == DEAL_ENTRY_IN)                                  class=class="str">"cmt">// if this is an entry
  {
   arr_time_open[   ArraySize(arr_time_open)-class="num">1]   = time_open;  class=class="str">"cmt">// deal time
   arr_symbol[      ArraySize(arr_symbol)-class="num">1]      = symbol;     class=class="str">"cmt">// instrument symbol
   arr_stop_loss[   ArraySize(arr_stop_loss)-class="num">1]   = stop_loss;  class=class="str">"cmt">// deal stop loss
   arr_take_profit[ ArraySize(arr_take_profit)-class="num">1] = take_profit; class=class="str">"cmt">// deal take profit
   arr_open[        ArraySize(arr_open)-class="num">1]        = open;       class=class="str">"cmt">// open price
   class=class="str">"cmt">//---
  }
让小布替你跑这套
这些诊断与图表导出逻辑小布盯盘的 AIGC 已内置,打开对应品种页即可看到历史单笔交易的图形化快照,不必自己从头写脚本。

常见问题

前者按 POSITION_IDENTIFIER 精准拉取某一持仓的 history,后者按时间段批量拉取;单笔场景用前者逻辑更轻量。
可以,小布在对应品种页内置了历史交易图形化与导出,省去自己部署脚本的步骤。
部分历史品种因账户类型切换或经纪商下架,在当前环境无法开图,不校验会触发'不存在的交易品种'报错。
没有定值,取决于单笔交易持续 K 线数;原则是让整段开平过程完整露出且不顶到坐标轴。