使用图表可视化交易(第一部分):选择分析时段·进阶篇
📘

使用图表可视化交易(第一部分):选择分析时段·进阶篇

第 2/3 篇

◍ 脚本启动前的颜色与变量声明

想把每笔成交在图上标清楚,第一步是把标签颜色做成可输入参数。下面这段代码把开仓价、平仓价、止损、止盈和成交趋势线分别绑到 clr_price_open、clr_price_close、clr_stop、clr_take、clr_main 五个 input color 上,默认除了止损用红、止盈用草绿,其余全是 WhiteSmoke。改一个变量就能整体换色,不用动绘图逻辑。 脚本真正跑起来之前,先用 Print 打一句“Script starts its work.”确认加载,再用 ResetLastError 把上一轮错误码清掉。接着通过 TerminalInfoString(TERMINAL_COMPANY) 取券商名、AccountInfoInteger(ACCOUNT_LOGIN) 取账号数字,这两行在排查不同券商点差或权限差异时很实用。 后面是一组待填的容器变量:ticket 先置 0,entry 用 ENUM_DEAL_ENTRY 且初值 -1 表示尚未分类出入场;position_id、type、magic、reason 也都配了对应的数组(PositionID[]、arr_type[] 等)准备批量接历史成交。time_open 和 arr_time_open[] 专门存开仓时间,后续按时间画线段就靠它。外汇和贵金属波动大、杠杆高,回测脚本前先在模拟盘验证变量抓取是否准确。

MQL5 / C++
class="kw">input class="type">class="kw">color clr_price_open = clrWhiteSmoke;                                                     class=class="str">"cmt">// Color of price open label
class="kw">input class="type">class="kw">color clr_price_close = clrWhiteSmoke;                                                    class=class="str">"cmt">// Color of price close label
class="kw">input class="type">class="kw">color clr_stop = clrRed;                                                                  class=class="str">"cmt">// Color of stop loss label
class="kw">input class="type">class="kw">color clr_take = clrLawnGreen;                                                            class=class="str">"cmt">// Color of take profit label
class="kw">input class="type">class="kw">color clr_main = clrWhiteSmoke;                                                           class=class="str">"cmt">// Color of deals trendline
   Print("Script starts its work.");                                                                       class=class="str">"cmt">// notified
   ResetLastError();                                                                                        class=class="str">"cmt">// reset error
   class="type">class="kw">string brok_name = TerminalInfoString(TERMINAL_COMPANY);                                               class=class="str">"cmt">// get broker name
   class="type">long account_num = AccountInfoInteger(ACCOUNT_LOGIN);                                                  class=class="str">"cmt">// get account number
class=class="str">"cmt">//---
   class="type">ulong    ticket = class="num">0;                                                                                     class=class="str">"cmt">// ticket
   ENUM_DEAL_ENTRY entry = -class="num">1;                                                                              class=class="str">"cmt">// entry or exit
   class="type">long     position_id = class="num">0,  PositionID[];                                                                 class=class="str">"cmt">// main id
   class="type">int      type = -class="num">1,        arr_type[];                                                                   class=class="str">"cmt">// deal type
   class="type">int      magic = -class="num">1,       arr_magic[];                                                                  class=class="str">"cmt">// magic number
   ENUM_DEAL_REASON       reason = -class="num">1,      arr_reason[];                                                  class=class="str">"cmt">// reason
   class="type">class="kw">datetime time_open = class="num">0,   arr_time_open[];                                                              class=class="str">"cmt">// deal open time

成交流水字段先铺好数组

做历史成交回放前,先把每笔 deal 的字段用标量加动态数组成对声明,是 MT5 里最省事的内存布局。下面这段声明覆盖了平仓时间、品种、注释、外部 ID、SL/TP、开平仓价、成交量、佣金、库存费、净利与手续费,基本能把一单外汇或贵金属的全部成本结构拉出来。 外汇和贵金属杠杆高,佣金与 swap 在迷你账户里可能吃掉 0.3~1.2 个点差,回测时漏掉 arr_swap 会虚增收益概率。 逐行看:datetime time_close 配 arr_time_close 存平仓时间;string symbol / comment / externalID 三组分别接品种名、订单备注、外部系统 ID;double 系的 stop_loss、take_profit、open、close、volume、commission、swap、profit、fee 都是先给 0 初值再扩数组;最后 int res = -1 留作自定义指令返回值。

MQL5 / C++
  class="type">class="kw">datetime time_close = class="num">0,    arr_time_close[];                 class=class="str">"cmt">// close time
  class="type">class="kw">string   symbol,             arr_symbol[];                     class=class="str">"cmt">// symbol
  class="type">class="kw">string   comment,            arr_comment[];                    class=class="str">"cmt">// comment
  class="type">class="kw">string   externalID,         arr_extermalID[];                 class=class="str">"cmt">// external id
  class="type">class="kw">double   stop_loss = class="num">0,      arr_stop_loss[];                  class=class="str">"cmt">// deal Stop Loss
  class="type">class="kw">double   take_profit = class="num">0,    arr_take_profit[];                class=class="str">"cmt">// deal Take Profit
  class="type">class="kw">double   open = class="num">0,           arr_open[];                       class=class="str">"cmt">// open price
  class="type">class="kw">double   close = class="num">0,          arr_close[];                      class=class="str">"cmt">// close price
  class="type">class="kw">double   volume = class="num">0,         arr_volume[];                     class=class="str">"cmt">// position volume
  class="type">class="kw">double   commission = class="num">0,     arr_commission[];                 class=class="str">"cmt">// commission
  class="type">class="kw">double   swap = class="num">0,           arr_swap[];                       class=class="str">"cmt">// swap
  class="type">class="kw">double   profit = class="num">0,         arr_profit[];                     class=class="str">"cmt">// profit
  class="type">class="kw">double   fee = class="num">0,            arr_fee[];                        class=class="str">"cmt">// fee
  class="type">int res = -class="num">1;                                                   class=class="str">"cmt">// user command

「用 switch 选时段并拦掉误触发的脚本」

做时段分析时,先用 switch 读 inp_method 全局变量,从 Select_period 分支进逻辑。这一步先弹窗让用户确认,避免选错后干等脚本跑完。 弹窗用 MessageBox() 配 MB_OKCANCEL,用户点取消就走人。把返回值塞进 res,若等于 IDCANCEL 直接 return 中断,终端不继续耗资源。 确认后调 HistorySelect() 拉指定起止时间的已完结交易。随后用 HistoryDealsTotal() 取数量赋给 total;若 total<=0,说明这段日子账户没动静,printf 写 EA 日志再 MessageBox 提示,然后 return 停脚本。 有交易才进 for 循环,用 HistoryDealGetTicket(i) 按序号换唯一 ticket,务必校验返回值有效。再借 HistoryDealGetInteger() 抓三样:仓位 ID、DEAL_ENTRY(开平标识)、DEAL_TYPE(方向类型),靠仓位 ID 聚合同仓的多笔单。 先剔掉充值、出金、赠金这类非买卖记录,只留 DEAL_ENTRY_IN / OUT 的实单。开仓单取价、时间、symbol、SL/TP、magic 等;平仓单取价、时间、reason、swap、profit、fee。多品种混跑时订单时序乱,同仓可能跨品种穿插开平。 存数据用一维数组,写个 template 的 Find() 按仓位 ID 查索引,命中返序号、没有返 -1。返回 -1 就 Redim 加仓再写;命中就凭索引直写对应槽位,不用重扫。 下面这段是确认与空时段拦截的核心代码,复制进 MT5 脚本即可验证中断机制:

MQL5 / C++
      res = MessageBox("You have selected analysis for period. Continue?","",MB_OKCANCEL); class=class="str">"cmt">// wait for user confirmation 
      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">// notify
        class="kw">return;                                                      class=class="str">"cmt">// do not class="kw">continue
      }
        class="type">int total = HistoryDealsTotal();                             class=class="str">"cmt">// got the total number of deals
        if(total <= class="num">0)                                               class=class="str">"cmt">// if nothing found
        {
          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.");

◍ 从成交历史里抠出开仓记录

做持仓统计或复盘脚本时,直接读持仓池往往拿不到已平仓的历史单。MT5 的 HistoryDealGetTicket 按索引遍历历史成交,索引 i 从 0 到 total-1,total 是 HistoryDealsTotal() 返回的成交总数,实盘账号一天可能几千笔,循环别写死上限。 下面这段是抓取「入场成交」的核心循环。它先取 ticket,再读 DEAL_POSITION_ID 锁定主仓位,用 DEAL_ENTRY 判断是 IN 还是 OUT,最后只处理 DEAL_ENTRY_IN 且类型为买卖或取消买卖的成交。 别把取消单当开仓信号。DEAL_TYPE_BUY_CANCELED / SELL_CANCELED 虽然进了 type 判断,但外层只要 entry==DEAL_ENTRY_IN 才取价,取消单通常没有真实开仓价,跑出来 open 可能是 0,写库前最好加一句 if(open==0) continue。 外汇和贵金属杠杆高,历史成交里的滑点、点差在回测和实盘差异大,这段代码只解决「怎么拿到开仓价和时间」,不证明任何策略盈利概率。

MQL5 / C++
      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 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">// deal type
           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
              )
              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

从成交历史里拆出每笔交易的字段

回测或复盘时,光看账户净值曲线不够,得把每一笔成交的微观字段拉出来。MQL5 的 HistoryDealGetDouble / GetInteger / GetString 系列函数就是干这个的:进场成交能取到止盈价、魔术码、注释、外部 ID、成交量、手续费;出场成交能取到平仓价、平仓时间、了结原因、库存费、盈亏和额外费用。 下面这段片段演示了按 DEAL_ENTRY_OUT 区分出场、按 DEAL_ENTRY_IN 取进场属性的典型写法。注意 DEAL_REASON 是枚举强转,能告诉你这单是被止损、止盈还是手动平掉——贵金属隔夜单的 swap 字段也在这里,实盘里可能吃掉不小一块利润。 后面附的 Find 模板函数很轻量:遍历数组比对值,命中即返回下标。用它可以在本地缓存的 ticket 数组里 O(n) 定位某笔单,避免反复调 HistoryDealSelect。外汇和贵金属杠杆高,历史统计只是概率参考,不能直接推下次盈亏。

MQL5 / C++
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">//
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="kw">template<class="kw">typename A>
class="type">int				 Find(A &aArray[],A aValue)
  {
  for(class="type">int i=class="num">0; i<ArraySize(aArray); i++)
	{
	  if(aArray[i]==aValue)
		{
		 class="kw">return(i);											class=class="str">"cmt">// The element exists, class="kw">return the element index
		}
	 }

「正文」

&nbsp;&nbsp; <span class="keyword">return</span>(-<span class="number">1</span>);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">// No such element, return -1</span> &nbsp;&nbsp;} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">//--- enter data into the main storage</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">//--- check if there is such id</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>(Find(PositionID,position_id)==-<span class="number">1</span>)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">// if there is no such deal yet,</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">case</span>&nbsp;&nbsp;Select_period:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&

常见问题

在 OnInit 之前用 #define 或全局变量把画笔颜色、线宽等常量先声明好,再在脚本开头初始化数组,能减少后期调试成本。
按成交记录结构建好对应的 MqlTradeTransaction 或自定义结构体数组,提前分配大小,后面直接按索引写入就不会乱。
可以。小布能读取你的脚本结构,标出 switch 里没拦住的误触发分支,并给出简化判断的建议,省去你手动逐行排查。
注意用成交类型过滤掉平仓和删单记录,只保留 ORDER_TYPE_BUY 和 ORDER_TYPE_SELL 的成交,否则统计会重复。
在循环里把每笔成交的 commission、swap、profit 单独存到结构体字段,最后按订单号汇总,别混在总价差里算。