使用图表可视化交易(第一部分):选择分析时段·进阶篇
◍ 脚本启动前的颜色与变量声明
想把每笔成交在图上标清楚,第一步是把标签颜色做成可输入参数。下面这段代码把开仓价、平仓价、止损、止盈和成交趋势线分别绑到 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[] 专门存开仓时间,后续按时间画线段就靠它。外汇和贵金属波动大、杠杆高,回测脚本前先在模拟盘验证变量抓取是否准确。
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 留作自定义指令返回值。
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 脚本即可验证中断机制:
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。 外汇和贵金属杠杆高,历史成交里的滑点、点差在回测和实盘差异大,这段代码只解决「怎么拿到开仓价和时间」,不证明任何策略盈利概率。
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。外汇和贵金属杠杆高,历史统计只是概率参考,不能直接推下次盈亏。
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 } }
「正文」
<span class="keyword">return</span>(-<span class="number">1</span>); <span class="comment">// No such element, return -1</span> } <span class="comment">//--- enter data into the main storage</span> <span class="comment">//--- check if there is such id</span> <span class="keyword">if</span>(Find(PositionID,position_id)==-<span class="number">1</span>) <span class="comment">// if there is no such deal yet,</span> { <span class="keyword">case</span> Select_period: &