使用图表可视化交易(第一部分):选择分析时段·综合运用
(3/3)·手动翻历史找止损止盈线太耗神?这套脚本把截图、标注、统计一气跑完
遍历成交记录抓取开仓明细
做历史成交统计时,第一步是把账户历史里的每笔 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 策略测试器里复现任意一笔历史开仓的快照。
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 把这段直接塞进自己的历史扫描脚本跑一遍。
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。外汇与贵金属杠杆高,回测结论仅代表历史数据特征,实盘可能偏离。
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。 外汇和贵金属杠杆高,佣金与滑点会啃掉盈利,把这些字段落库后再算净值才靠谱。
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 的差值,就能验证你的净收益计算是否被手续费吃掉。外汇与贵金属杠杆高,回测结论只代表历史概率,实盘仍可能偏离。
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 不同口径
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 就能验证空区间的提示路径。
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 拖进脚本目录,先验证一下当前版本在自选品种上能不能正常吐出历史成交列表,比空谈长期安全交易实在得多。