使用图表可视化交易(第一部分):选择分析时段·综合运用
📊

使用图表可视化交易(第一部分):选择分析时段·综合运用

(3/3)·手动翻历史找止损止盈线太耗神?这套脚本把截图、标注、统计一气跑完

含代码示例实战向 第 3/3 篇
手动复盘时反复开图表、手抄成交明细、手绘 SL/TP,往往半天只理出两三笔。把这类机械活交给脚本,你才能真正把时间花在判断策略适应度上。

遍历成交记录抓取开仓明细

做历史成交统计时,第一步是把账户历史里的每笔 deal 都过一遍。MT5 的 HistoryDealGetTicket 按索引取票号,索引从 0 到 total-1,total 是 HistoryDealsTotal() 返回的成交总数。 下面这段代码演示了怎么在 for 循环里拿到每笔成交,并过滤出真正的买卖类成交(含取消的挂单成交),再判断是入场还是出场。外汇和贵金属杠杆高,历史回测和实际滑点可能偏离,结论仅作概率参考。 别把取消成交漏掉 DEAL_TYPE_BUY_CANCELED 和 DEAL_TYPE_SELL_CANCELED 虽然没真正开仓,但在统计挂单命中率时可能是关键样本,过滤条件里要显式包含。 确认是 DEAL_ENTRY_IN 后,用 HistoryDealGetDouble 取 DEAL_PRICE 得开仓价,HistoryDealGetInteger 取 DEAL_TIME 得开仓时间,HistoryDealGetString 取 DEAL_SYMBOL 得交易品种。这三行跑通,你就能在 MT5 策略测试器里复现任意一笔历史开仓的快照。

MQL5 / C++
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 ticket
     {
      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?
      type = (class="type">int)HistoryDealGetInteger(ticket,DEAL_TYPE);               class=class="str">"cmt">// entry or exit?
      class=class="str">"cmt">//--- check the deal type
      if(type == DEAL_TYPE_BUY       ||  class=class="str">"cmt">// if it is buy
          type == DEAL_TYPE_SELL      ||  class=class="str">"cmt">// if it is sell
          type == DEAL_TYPE_BUY_CANCELED || class=class="str">"cmt">// if canceled buy
          type == DEAL_TYPE_SELL_CANCELED  class=class="str">"cmt">// if canceled sell
         )
        {
         class=class="str">"cmt">//--- is it 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
           }
        }
     }
  }

◍ 从成交历史里拆出每笔仓的进出字段

回测或复盘时,只拿到 ticket 不够,得把每笔 deal 的 SL、TP、magic、成交量、手续费这些字段逐个捞出来。下面这段逻辑按 DEAL_ENTRY_IN / DEAL_ENTRY_OUT 分流:开仓分支取止损止盈与注释,平仓分支取平仓价、 swap 与净利润。 HistoryDealGetDouble 负责所有浮点类字段(价格、体积、佣金、库存费),HistoryDealGetInteger 拿时间、magic、出场原因,HistoryDealGetString 取评论和外部 ID。注意 DEAL_FEE 与 DEAL_COMMISSION 是分开的两个字段,很多人漏掉 fee 导致成本算浅。 捞完之后用 Find(PositionID, position_id) 判重,返回 -1 说明主存储里还没有这条持仓,这时候才往里写。外汇和贵金属杠杆高,swap 与 commission 在短线频繁交易里可能吃掉大半利润,建议开 MT5 把这段直接塞进自己的历史扫描脚本跑一遍。

MQL5 / C++
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
magic = (class="type">int)HistoryDealGetInteger(ticket,DEAL_MAGIC);  class=class="str">"cmt">// take magic number
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
 reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(ticket,DEAL_REASON); class=class="str">"cmt">// reason
 swap = HistoryDealGetDouble(ticket,DEAL_SWAP);         class=class="str">"cmt">// swap
 profit = HistoryDealGetDouble(ticket,DEAL_PROFIT);     class=class="str">"cmt">// profit
 fee = HistoryDealGetDouble(ticket,DEAL_FEE);           class=class="str">"cmt">// 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 yet,
 {

「逐笔记录前先扩容数组」

做跨品种持仓快照时,每抓到一笔新成交就得先把对应的容器拉长,否则直接写越界会抛数组错误。下面这段在每次录入前对 16 个动态数组统一做 +1 扩容,覆盖开平时间、品种、止损止盈、挂单标识、魔数、外部 ID、注释、成交量、佣金、触发原因和库存费。 具体做法是拿 ArraySize 取当前长度再 +1 传给 ArrayResize,属于 MT5 里最朴素的尾部追加逻辑。实盘跑 EA 时若成交频率高,这种每笔都 Resize 的写法会产生碎片,但逻辑直观、便于在策略测试器里单步看每笔结构。 开 MT5 把这段代码贴进自定义函数,接在订单遍历之前,能立刻验证:未扩容时写第 N+1 笔会报 4002 数组越界,加上后日志里 arr_volume 等长度随成交数同步 +1。外汇与贵金属杠杆高,回测结论仅代表历史数据特征,实盘可能偏离。

MQL5 / C++
class=class="str">"cmt">//--- change the size of containers
ArrayResize(arr_time_open,ArraySize(arr_time_open)+class="num">1);
ArrayResize(arr_time_close,ArraySize(arr_time_close)+class="num">1);
ArrayResize(arr_symbol,ArraySize(arr_symbol)+class="num">1);
ArrayResize(arr_stop_loss,ArraySize(arr_stop_loss)+class="num">1);
ArrayResize(arr_take_profit,ArraySize(arr_take_profit)+class="num">1);
ArrayResize(arr_open,ArraySize(arr_open)+class="num">1);
ArrayResize(arr_close,ArraySize(arr_close)+class="num">1);
ArrayResize(PositionID,ArraySize(PositionID)+class="num">1);
ArrayResize(arr_magic,ArraySize(arr_magic)+class="num">1);
ArrayResize(arr_extermalID,ArraySize(arr_extermalID)+class="num">1);
ArrayResize(arr_comment,ArraySize(arr_comment)+class="num">1);
ArrayResize(arr_volume,ArraySize(arr_volume)+class="num">1);
ArrayResize(arr_commission,ArraySize(arr_commission)+class="num">1);
ArrayResize(arr_reason,ArraySize(arr_reason)+class="num">1);
ArrayResize(arr_swap,ArraySize(arr_swap)+class="num">1);

开仓成交时把字段落进动态数组

抓到一笔 entry 为 DEAL_ENTRY_IN 的成交,意味着新仓位建起来了,这时要把这笔单子的关键字段写进对应的动态数组尾部。 做法是用 ArraySize 取当前长度再减 1 当索引,配合 ArrayResize 在每次新增前把数组撑大 1 格,保证不越界。 下面这段把开仓时间、品种、止损、止盈、开价、magic、注释、外部 ID、成交量、佣金逐一塞进各自数组,复盘或统计时能直接按索引对齐同一条 deal。 外汇和贵金属杠杆高,佣金与滑点会啃掉盈利,把这些字段落库后再算净值才靠谱。

MQL5 / C++
ArrayResize(arr_profit,ArraySize(arr_profit)+class="num">1);
ArrayResize(arr_fee,ArraySize(arr_fee)+class="num">1);
PositionID[ArraySize(arr_time_open)-class="num">1]=position_id;
if(entry == DEAL_ENTRY_IN)
  {
   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">//---
   arr_magic[       ArraySize(arr_magic)-class="num">1]       = magic;      class=class="str">"cmt">// magic number
   arr_comment[     ArraySize(arr_comment)-class="num">1]     = comment;    class=class="str">"cmt">// comment
   arr_extermalID[  ArraySize(arr_extermalID)-class="num">1]  = externalID; class=class="str">"cmt">// external id
   arr_volume[      ArraySize(arr_volume)-class="num">1]      = volume;     class=class="str">"cmt">// volume
   arr_commission[  ArraySize(arr_commission)-class="num">1]  = commission; class=class="str">"cmt">// commission
  }

◍ 平仓与开仓的数组落点差异

在 MT5 的成交回放逻辑里,出场和平仓走的是两套不同的数组写入路径。出场(DEAL_ENTRY_OUT)直接追加到各数组末尾,用 ArraySize(arr)-1 定位最后一个空位,把平仓时间、价格、原因、库存费、利润和手续费一次性填进去。 如果是开仓(DEAL_ENTRY_IN),则先靠 PositionID 在 Find() 里拿到已有索引,再把开仓时间、品种、止损、止盈写回对应位置,而不是新建一行。这样同一笔持仓的开平数据能对齐到同一条记录,后续统计胜率或持仓时长才不会错位。 实盘里黄金 XAUUSD 的库存费可能在 arr_swap 里出现正负跳变,欧美则多数时段接近 0;跑一遍历史成交,重点看 arr_profit 与 arr_fee 的差值,就能验证你的净收益计算是否被手续费吃掉。外汇与贵金属杠杆高,回测结论只代表历史概率,实盘仍可能偏离。

MQL5 / C++
if(entry == DEAL_ENTRY_OUT)             class=class="str">"cmt">// if this is an exit
              {
               arr_time_close[   ArraySize(arr_time_close)-class="num">1]  = time_close;   class=class="str">"cmt">// close time
               arr_close[        ArraySize(arr_close)-class="num">1]       = close;        class=class="str">"cmt">// close price
               class=class="str">"cmt">//---
               arr_reason[       ArraySize(arr_reason)-class="num">1]      = reason;       class=class="str">"cmt">// reason
               arr_swap[         ArraySize(arr_swap)-class="num">1]        = swap;         class=class="str">"cmt">// swap
               arr_profit[       ArraySize(arr_profit)-class="num">1]      = profit;       class=class="str">"cmt">// profit
               arr_fee[          ArraySize(arr_fee)-class="num">1]         = fee;          class=class="str">"cmt">// fee
              }
             }
             else
             {
              class="type">int index = Find(PositionID,position_id);       class=class="str">"cmt">// if found, search for the index
              if(entry == DEAL_ENTRY_IN)                      class=class="str">"cmt">// if this is an entry
               {
                arr_time_open[index]   = time_open;           class=class="str">"cmt">// deal time
                arr_symbol[index]      = symbol;              class=class="str">"cmt">// symbol
                arr_stop_loss[index]   = stop_loss;           class=class="str">"cmt">// deal Stop Loss
                arr_take_profit[index] = take_profit;         class=class="str">"cmt">// deal Take Profit

「平仓分支里该记哪些字段」

上面这段逻辑只在外平仓分支(DEAL_ENTRY_OUT)触发时才写数组,和开仓分支分开存,能避免把开平价格混在同一行导致回测错位。 平仓时要落盘的字段包括:平仓时间 arr_time_close、平仓价 arr_close、平仓原因 arr_reason、库存费 arr_swap、浮动转实盈 arr_profit 以及手续费 arr_fee。外汇与贵金属杠杆高,swap 与 fee 在跨周末持仓时可能吃掉大半利润,这两个数组不可省略。 下面把原文平仓段逐行拆一遍,方便直接在 MT5 里对照改结构体: arr_time_close[index] = time_close; // 成交平仓时间,用于算持仓时长 arr_close[index] = close; // 平仓成交价,和 arr_open 配对算盈亏 arr_reason[index] = reason; // 平仓触发源,比如 SL/TP/手动 arr_swap[index] = swap; // 过夜利息,负值代表扣费 arr_profit[index] = profit; // 该笔平仓实现的净利润 arr_fee[index] = fee; // 经纪商手续费,常与 commission 不同口径

MQL5 / C++
   if(entry == DEAL_ENTRY_OUT)                 class=class="str">"cmt">// if this is an exit
   {
   arr_time_close[index]  = time_close;     class=class="str">"cmt">// deal close time
   arr_close[index]       = close;          class=class="str">"cmt">// deal close price
   class=class="str">"cmt">//---
   arr_reason[index]      = reason;         class=class="str">"cmt">// reason
   arr_swap[index]        = swap;           class=class="str">"cmt">// swap
   arr_profit[index]      = profit;         class=class="str">"cmt">// profit
   arr_fee[index]         = fee;            class=class="str">"cmt">// fee
   }

历史成交查询的报错与空结果处理

在 MT5 脚本里拉取历史成交,若 HistorySelect 返回 false,说明时间段选择失败,必须用 GetLastError 把底层错误码打出来,否则后续逻辑会在空数据上裸奔。 当指定区间确实没有成交时,别让脚本静默退出。用 MessageBox 弹出起止时间,能直接确认是参数写错还是行情时段本身无交易——外汇与贵金属在高波动时段之外可能长时间无成交,属正常高风险市场现象。 下面这段是原文里的收尾处理分支,逐行拆一下: printf("%s - %d -> Error of selecting history deals: %d",__FUNCTION__,__LINE__,GetLastError()); // notify // 打印函数名、行号与错误码,定位 HistorySelect 失败点 printf("%s - %d -> No deals were found for the specified period.",__FUNCTION__,__LINE__); // notify // 打印提示:指定周期未找到任何成交 MessageBox("No deals were found for the specified period: "+TimeToString(start_date)+"-"+TimeToString(finish_date)+". Script is done."); // 弹窗告知用户起止时间及脚本结束 return; // 直接退出,避免空数组继续跑 break; // 跳出外层选择分支 开 MT5 把这段塞进你的历史扫描脚本,改一下 start_date / finish_date 就能验证空区间的提示路径。

MQL5 / C++
printf("%s - %d -> Error of selecting history deals: %d",__FUNCTION__,__LINE__,GetLastError()); class=class="str">"cmt">// notify
printf("%s - %d -> No deals were found for the specified period.",__FUNCTION__,__LINE__); class=class="str">"cmt">// notify
MessageBox("No deals were found for the specified period: "+TimeToString(start_date)+"-"+TimeToString(finish_date)+". Script is done.");
class="kw">return;
class="kw">break;

◍ 把这条线请下神坛

前几节把历史交易分析的底子打好了:输入数据怎么成形、选定时间段内怎么筛出历史成交、可重载函数模板怎么省掉容器处理的重复代码,这些在脚本里已经能跑通。外汇和贵金属本身高杠杆、高波动,靠回看历史找规律只是降低盲目下单的概率,不等于能预测下一根 K 线。 下一篇要补的是单笔交易选取、图表绘制和把数据对象推到图上的代码,那时这套东西才从「能算」变成「能看」。现在你开 MT5 把 DealsPrintScreen.mq5 拖进脚本目录,先验证一下当前版本在自选品种上能不能正常吐出历史成交列表,比空谈长期安全交易实在得多。

交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到历史成交与阶段标注,把重复劳动交给小布,你专注决策。

常见问题

脚本会把每笔成交的 SL/TP 与行情叠在一起,横盘密集止损还是趋势段回撤变大,肉眼即可区分,但是否切换策略仍取决于你对市场阶段的判断。
现成脚本开箱即用但逻辑固定;文中分两部分给完整源码,你可以按品种改佣金、隔夜利息的采集字段,适配自己的账户结构。
周度粗看阶段适应、月度细看单笔决策偏误比较常见,过高频率容易被噪声干扰,过低则可能错过风格切换。
小布盯盘品种页已内置历史成交叠加与阶段提示,不用自己跑脚本也能快速扫一遍账户表现,适合盘中抽空核对。
这类品种杠杆高、隔夜息差波动大,长期盈利可能被手续费和 swap 吃掉,分离统计才能看清策略真实净收益,涉及外汇贵金属属高风险活动。