可视化交易图表·数据图形化展示:用脚本把单笔交易复盘截图自动化(基础篇)
(1/3)·手工截取历史单笔交易图表既费时又易漏标关键数据,本篇从零搭起可复用脚本骨架
复盘时手动在终端里翻历史、调缩进、截单笔交易图,往往半小时才攒出几张能用的图。更麻烦的是,不同经纪商账户类型会让某些历史交易品种在当下窗口里根本打不开。把这套机械劳动交给代码,才是把时间留给决策的正确做法。
给 MT5 图表叠一层自己的图形
MetaTrader 5 自带的图表只能画 K 线和少数指标,想把自己的计算结果(比如波动率带、自定义支撑区)直接画在价格图上,得走 Canvas 或 OBJ_CHART 这类底层接口。2024 年 12 月一篇社区技术文提到,用 MQL5 的 Canvas 类可以在图表上脱离指标窗口独立绘图,作者 Aleksandr Seredin 的示例在发布后拿到 803 次浏览、2 条评论,说明这类「图表即画布」的需求在真实交易者里并非小众。 实操上,核心是先建一个 Canvas 对象绑定到主图,再用像素坐标把数据映射上去。下面这段是原文给出的最小可运行骨架,去掉了装饰逻辑,只留绑定与刷新: 高风险提示:外汇与贵金属杠杆品种波动剧烈,任何自绘图形都只是辅助观察,不代表方向判断。
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__ 日志写法。
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 对齐,就能还原任意一笔外汇或贵金属持仓的生命周期——这类品种杠杆高、滑点跳空频繁,历史成交里的实际平仓价和信号价常有几点到十几点偏差,验证时务必以券商历史为准。
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 终端内存占用可能倾向走高。
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 的逻辑。
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">//--- }