MQL5 酷宝书 服务·综合运用
(3/3)· 前两级铺垫完概念与单点用法,这里把 11 个环节串成可跑通的完整服务工程
「日志清理后按持仓去重图表」
一段真实运行日志显示,dClearTradeLogs 服务在 2022.12.05 23:26:59.960 启动,当天删除了 6 个旧日志文件,随后进入图表与持仓的整理逻辑。这类脚本的价值在于:MT5 开久了图表越堆越多,手动关既慢又容易误关有持仓的品种。 核心分支先看有没有持仓。用 PositionsTotal() 取持仓数,若小于 1 说明空仓,就遍历所有图表直接 ChartClose 关掉,MAX_CHARTS 是终端图表上限常量,循环里靠 FirstChart/NextChart 游标推进。 有持仓时则先建一个字符串哈希集,把每个持仓的 PositionGetSymbol 去重存好;再建图表符号到图表 ID 的哈希映射,遍历图表时若某符号已存在映射中,当前这张就标记为待关闭。这样只保留每个品种的第一张图,重复开的图被清掉,持仓对应的图全留着。外汇和贵金属波动大、持仓过夜常见,这种自动收敛图表的方式能降低终端卡顿概率,但脚本误关风险仍在,上线前建议在策略测试器或模拟盘先跑一遍。
class="type">int positions_num=::PositionsTotal(); class=class="str">"cmt">//--- if there are no positions if(positions_num<class="num">1) { class=class="str">"cmt">// close all the charts CChart temp_chart_obj; temp_chart_obj.FirstChart(); class="type">long temp_ch_id=temp_chart_obj.ChartId(); for(class="type">int ch_idx=class="num">0; ch_idx<MAX_CHARTS && temp_ch_id>-class="num">1; ch_idx++) { class="type">long ch_id_to_close=temp_ch_id; temp_chart_obj.NextChart(); temp_ch_id=temp_chart_obj.ChartId(); ::ChartClose(ch_id_to_close); } } class=class="str">"cmt">//--- if there are some positions else { class=class="str">"cmt">//--- collect unique position symbols CHashSet<class="type">class="kw">string> pos_symbols_set; for(class="type">int pos_idx=class="num">0; pos_idx<positions_num; pos_idx++) { class="type">class="kw">string curr_pos_symbol=::PositionGetSymbol(pos_idx); if(!pos_symbols_set.Contains(curr_pos_symbol)) { if(!pos_symbols_set.Add(curr_pos_symbol)) ::PrintFormat("Failed to add a symbol \"%s\" to the positions set!", curr_pos_symbol); } } class="type">class="kw">string pos_symbols_arr[]; class="type">int unique_pos_symbols_num=pos_symbols_set.Count(); if(pos_symbols_set.CopyTo(pos_symbols_arr)!=unique_pos_symbols_num) class="kw">continue; class=class="str">"cmt">//--- collect unique chart symbols and close duplicates CHashMap<class="type">class="kw">string, class="type">long> ch_symbols_map; CChart map_chart_obj; map_chart_obj.FirstChart(); class="type">long map_ch_id=map_chart_obj.ChartId(); for(class="type">int ch_idx=class="num">0; ch_idx<MAX_CHARTS && map_ch_id>-class="num">1; ch_idx++) { class="type">class="kw">string curr_ch_symbol=map_chart_obj.Symbol(); class="type">long ch_id_to_close=class="num">0; if(!ch_symbols_map.ContainsKey(curr_ch_symbol)) { if(!ch_symbols_map.Add(curr_ch_symbol, map_ch_id)) ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_ch_symbol); } else { class=class="str">"cmt">//--- if there&class="macro">#x27;s a duplicate ch_id_to_close=map_chart_obj.ChartId(); } class=class="str">"cmt">//--- move to the next chart map_chart_obj.NextChart(); map_ch_id=map_chart_obj.ChartId();
◍ 持仓与图表窗口的双向对齐逻辑
这段逻辑解决一个实盘常见麻烦:手动或 EA 开了很多图表,但有些品种已经没有持仓,有些持仓品种却没图表可看。代码用两张映射表做双向扫描,把图表状态和持仓状态对齐。 先扫持仓:遍历去重后的持仓品种数组,若某品种不在已开图表映射里,就先 SymbolSelect 把它加进市场报价窗,再用 CChart.Open 以 H1 周期开图,开图成功才写进 ch_symbols_map。temp_ch_id 小于 1 代表开图失败,会打印具体品种名方便排查。 再扫图表:把映射 CopyTo 成数组后反向遍历,若图表对应品种不在持仓集合 pos_symbols_set 中,直接 temp_chart_obj.Close() 关掉。若仍有持仓,则进入盈利计算分支,用 PositionsTotal 循环累加同品种仓位浮盈。 外汇与贵金属杠杆高、跳空频繁,这类自动关图逻辑能减少 MT5 终端资源占用,但关图不等于平仓,仓位风险依旧由你自己承担。
if(ch_id_to_close>class="num">0) { ::ChartClose(ch_id_to_close); } } map_chart_obj.Detach(); class=class="str">"cmt">//--- looking for a chart if there&class="macro">#x27;s a position for(class="type">int s_pos_idx=class="num">0; s_pos_idx<unique_pos_symbols_num; s_pos_idx++) { class="type">class="kw">string curr_pos_symbol=pos_symbols_arr[s_pos_idx]; class=class="str">"cmt">//--- if there&class="macro">#x27;s no chart of the symbol if(!ch_symbols_map.ContainsKey(curr_pos_symbol)) if(::SymbolSelect(curr_pos_symbol, true)) { class=class="str">"cmt">//--- open a chart of the symbol CChart temp_chart_obj; class="type">long temp_ch_id=temp_chart_obj.Open(curr_pos_symbol, PERIOD_H1); if(temp_ch_id<class="num">1) ::PrintFormat("Failed to open a chart of the symbol \"%s\"!", curr_pos_symbol); else { if(!ch_symbols_map.Add(curr_pos_symbol, temp_ch_id)) ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_pos_symbol); temp_chart_obj.Detach(); } } } class="type">class="kw">string ch_symbols_arr[]; class="type">long ch_ids_arr[]; class="type">int unique_ch_symbols_num=ch_symbols_map.Count(); if(ch_symbols_map.CopyTo(ch_symbols_arr, ch_ids_arr)!=unique_ch_symbols_num) class="kw">continue; class=class="str">"cmt">//--- looking for a position if there&class="macro">#x27;s a chart for(class="type">int s_ch_idx=class="num">0; s_ch_idx<unique_ch_symbols_num; s_ch_idx++) { class="type">class="kw">string curr_ch_symbol=ch_symbols_arr[s_ch_idx]; class="type">long ch_id_to_close=ch_ids_arr[s_ch_idx]; CChart temp_chart_obj; temp_chart_obj.Attach(ch_id_to_close); class=class="str">"cmt">//--- if there&class="macro">#x27;s no position of the symbol if(!pos_symbols_set.Contains(curr_ch_symbol)) { temp_chart_obj.Close(); } else { CPositionInfo curr_pos_info; class=class="str">"cmt">//--- calculate a position profit class="type">class="kw">double curr_pos_profit=class="num">0.; class="type">int pos_num=::PositionsTotal(); for(class="type">int pos_idx=class="num">0; pos_idx<pos_num; pos_idx++) if(curr_pos_info.SelectByIndex(pos_idx)) { class="type">class="kw">string curr_pos_symbol=curr_pos_info.Symbol(); if(!::StringCompare(curr_ch_symbol, curr_pos_symbol))
用后台图色和快捷键拼出多品种监控墙
这段逻辑把当前品种的所有持仓盈亏累加后,按正负给后台图表刷不同底色:盈利用 LightSkyBlue,亏损用 LightPink,持平用 Lavender。调用 ColorBackground 失败会在日志打印具体品种名,方便你定位是哪张子图权限或对象句柄出了问题。 刷完颜色立刻 Redraw,再 Detach 把临时图从前台脱离,避免堆积几十个窗口拖垮终端。最后用 keybd_event 模拟 Alt+R(菜单键 VK_MENU 配 'R'),循环两次发按下/抬起,等于自动平铺所有图表——Sleep(10) 是给系统留的事件间隔,太短可能吞键。 下半段在造一个自定义品种 DXY 做美元指数载体:InpStartDay 默认 2022.10.01,InpDxyDigits=3 控制报价小数位;Create 时 batch_size 设 1e6 根柱、is_selected=true 直接选中。建完先写 SECTOR_CURRENCY、背景色 clrKhaki、图表模式 BID,任何一步 SetProperty 返回 false 就 PrintFormat 并报退。外汇与贵金属自定义指数叠加实盘前,务必清楚这类合成品种存在报价断层与重算偏差的高风险,仅建议用历史数据回测验证。
curr_pos_profit+=curr_pos_info.Profit()+curr_pos_info.Swap(); } class=class="str">"cmt">//--- apply a class="type">class="kw">color class="type">class="kw">color profit_clr=clrLavender; if(curr_pos_profit>class="num">0.) { profit_clr=clrLightSkyBlue; } else if(curr_pos_profit<class="num">0.) { profit_clr=clrLightPink; } if(!temp_chart_obj.ColorBackground(profit_clr)) ::PrintFormat("Failed to apply a profit class="type">class="kw">color for the symbol \"%s\"!", curr_ch_symbol); temp_chart_obj.Redraw(); } temp_chart_obj.Detach(); } class=class="str">"cmt">//--- tile windows(Alt+R) class="type">uchar vk=VK_MENU; class="type">uchar scan=class="num">0; class="type">uint flags[]= {class="num">0, KEYEVENTF_KEYUP}; class="type">class="kw">ulong extra_info=class="num">0; class="type">uchar Key=&class="macro">#x27;R&class="macro">#x27;; for(class="type">int r_idx=class="num">0; r_idx<class="num">2; r_idx++) { user32::keybd_event(vk, scan, flags[r_idx], extra_info); ::Sleep(class="num">10); user32::keybd_event(Key, scan, flags[r_idx], extra_info); } } class="kw">input class="type">class="kw">datetime InpStartDay=D&class="macro">#x27;class="num">01.10.class="num">2022&class="macro">#x27;; class=class="str">"cmt">// Start date class="kw">input class="type">int InpDxyDigits=class="num">3; class=class="str">"cmt">// Index digits class=class="str">"cmt">//--- create a custom symbol class="type">class="kw">string custom_symbol="DXY", custom_group="Dollar Index"; CiCustomSymbol custom_symbol_obj; class="kw">const class="type">uint batch_size = class="num">1e6; class="kw">const class="type">bool is_selected = true; class="type">int code = custom_symbol_obj.Create(custom_symbol, custom_group, NULL, batch_size, is_selected); ::PrintFormat("Custom symbol \"%s\", code: %d", custom_symbol, code); if(code < class="num">0) class="kw">return; class=class="str">"cmt">//--- Integer properties class=class="str">"cmt">//--- sector ENUM_SYMBOL_SECTOR symbol_sector = SECTOR_CURRENCY; if(!custom_symbol_obj.SetProperty(SYMBOL_SECTOR, symbol_sector)) { ::PrintFormat("Failed to set a sector for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- background class="type">class="kw">color class="type">class="kw">color symbol_background_clr = clrKhaki; if(!custom_symbol_obj.SetProperty(SYMBOL_BACKGROUND_COLOR, symbol_background_clr)) { ::PrintFormat("Failed to set a background class="type">class="kw">color for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- chart mode ENUM_SYMBOL_CHART_MODE symbol_ch_mode=SYMBOL_CHART_MODE_BID; if(!custom_symbol_obj.SetProperty(SYMBOL_CHART_MODE, symbol_ch_mode)) {
「自定义美元指数符号的属性落盘」
在 MT5 里用代码造一个合成美元指数(如 DXY 类自定义品种)时,光建出名字不够,必须逐项把属性写进 CCustomSymbol 对象,否则图表上要么不显示报价,要么点值错得离谱。下面这段逻辑把小数位、报价点、交易开关和文本类字段一次性设死。 小数位由外部输入 InpDxyDigits 决定,点值直接用 1/10^digits 算,例如 digits=3 时点值为 0.001;tick size 跟 point 取同一值,避免最小跳动和单位不一致。交易模式被强制设为 SYMBOL_TRADE_MODE_DISABLED,因为这个合成品只用来盯盘和回测,不能真下单。 文本属性里 category 填 "Currency indices"、country 填 "US"、description 写 "Synthetic US Dollar Index"、exchange 标 "ICE",page 给到 ice.com 的 USDX 页面链接;path 拼成 Custom\\组名\\品种名,决定它在市场报价树里的位置。任何一步 SetProperty 返回 false 就 PrintFormat 报错并 return,防止半成品符号污染后续计算。 最后代码会预载六个基础货币对——EURUSD、USDJPY、GBPUSD、USDCAD、USDSEK、USDCHF——并检查它们的 M1 历史,作为合成指数的权重原料。外汇与贵金属合成品波动受杠杆和流动性影响,高风险,参数设错可能让回测曲线失真。
::PrintFormat("Failed to set a chart mode for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- digits if(!custom_symbol_obj.SetProperty(SYMBOL_DIGITS, InpDxyDigits)) { ::PrintFormat("Failed to set digits for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- trade mode ENUM_SYMBOL_TRADE_MODE symbol_trade_mode = SYMBOL_TRADE_MODE_DISABLED; if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_MODE, symbol_trade_mode)) { ::PrintFormat("Failed to disable trade for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- Double properties class=class="str">"cmt">//--- point class="type">class="kw">double symbol_point = class="num">1./::MathPow(class="num">10, InpDxyDigits); if(!custom_symbol_obj.SetProperty(SYMBOL_POINT, symbol_point)) { ::PrintFormat("Failed to to set a point value for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- tick size class="type">class="kw">double symbol_tick_size = symbol_point; if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_TICK_SIZE, symbol_tick_size)) { ::PrintFormat("Failed to to set a tick size for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- String properties class=class="str">"cmt">//--- category class="type">class="kw">string symbol_category="Currency indices"; if(!custom_symbol_obj.SetProperty(SYMBOL_CATEGORY, symbol_category)) { ::PrintFormat("Failed to to set a category for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- country class="type">class="kw">string symbol_country= "US"; if(!custom_symbol_obj.SetProperty(SYMBOL_COUNTRY, symbol_country)) { ::PrintFormat("Failed to to set a country for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- description class="type">class="kw">string symbol_description= "Synthetic US Dollar Index"; if(!custom_symbol_obj.SetProperty(SYMBOL_DESCRIPTION, symbol_description)) { ::PrintFormat("Failed to to set a description for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- exchange class="type">class="kw">string symbol_exchange= "ICE"; if(!custom_symbol_obj.SetProperty(SYMBOL_EXCHANGE, symbol_exchange)) { ::PrintFormat("Failed to to set an exchange for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- page class="type">class="kw">string symbol_page = "https:class=class="str">"cmt">//www.ice.com/forex/usdx"; if(!custom_symbol_obj.SetProperty(SYMBOL_PAGE, symbol_page)) { ::PrintFormat("Failed to to set a page for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- path class="type">class="kw">string symbol_path="Custom\\"+custom_group+"\\"+custom_symbol; if(!custom_symbol_obj.SetProperty(SYMBOL_PATH, symbol_path)) { ::PrintFormat("Failed to to set a path for the custom symbol \"%s\"", custom_symbol); class="kw">return; } class=class="str">"cmt">//--- check quotes history CBaseSymbol base_symbols[BASE_SYMBOLS_NUM]; class="kw">const class="type">class="kw">string symbol_names[]= { "EURUSD", "USDJPY", "GBPUSD", "USDCAD", "USDSEK", "USDCHF" }; ENUM_TIMEFRAMES curr_tf=PERIOD_M1;
◍ 逐品种校验报价历史加载耗时
多品种监控前,必须先确认每个品种的报价历史能正常拉取,否则后续矩阵运算全是无源之水。下面这段循环会遍历 BASE_SYMBOLS_NUM 个品种,逐个 Init 后调用 CheckLoadHistory,并用 GetTickCount64 掐表。 代码里用 PrintFormat 输出三项关键数据:品种序号与名称、检查返回码 check_load_code、耗时 time_elapsed_ms 换算成的秒数(除以 MS_IN_SEC)。若返回码小于 0,直接打印失败品种名并 return,整个初始化中断。 CBaseSymbol 类把单品种的状态封装干净:m_symbol 存 CSymbolInfo,m_tf 存周期,m_ticks_mx 是 tick 数据矩阵,m_start_date 和 m_last_idx 管时间窗口与游标。对外暴露 Init、CheckLoadHistory、LoadTicks、GetTicks 等接口,GetTicks 直接返回内部矩阵副本。 实盘里把 MS_IN_SEC 设成 1000,在 EURUSD/XAUUSD 这类品种上,一次历史加载可能从几毫秒到数百毫秒不等,取决于本地缓存与经纪商服务器响应。外汇与贵金属杠杆高、滑点风险大,加载失败往往意味着行情断层,不能忽视。
::Print("\nChecking of quotes history is running..."); for(class="type">class="kw">ushort s_idx=class="num">0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; class="type">class="kw">string curr_symbol_name=symbol_names[s_idx]; if(ptr_base_symbol.Init(curr_symbol_name, curr_tf, InpStartDay)) { ::PrintFormat("\n Symbol #%hu: \"%s\"", s_idx+class="num">1, curr_symbol_name); class="type">class="kw">ulong start_cnt=::GetTickCount64(); class="type">int check_load_code=ptr_base_symbol.CheckLoadHistory(); ::PrintFormat(" Checking code: %I32d", check_load_code); class="type">class="kw">ulong time_elapsed_ms=::GetTickCount64()-start_cnt; ::PrintFormat(" Time elapsed: %class="num">0.3f sec", time_elapsed_ms/MS_IN_SEC); if(check_load_code<class="num">0) { ::PrintFormat("Failed to load quotes history for the symbol \"%s\"", curr_symbol_name); class="kw">return; } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Class CBaseSymbol | class=class="str">"cmt">//+------------------------------------------------------------------+ class CBaseSymbol : class="kw">public CObject { class=class="str">"cmt">//--- === Data members === --- class="kw">private: CSymbolInfo m_symbol; ENUM_TIMEFRAMES m_tf; matrix m_ticks_mx; class="type">class="kw">datetime m_start_date; class="type">class="kw">ulong m_last_idx; class=class="str">"cmt">//--- === Methods === --- class="kw">public: class=class="str">"cmt">//--- constructor/destructor class="type">void CBaseSymbol(class="type">void); class="type">void ~CBaseSymbol(class="type">void) {}; class=class="str">"cmt">//--- class="type">bool Init(class="kw">const class="type">class="kw">string _symbol, class="kw">const ENUM_TIMEFRAMES _tf, class="type">class="kw">datetime start_date); class="type">int CheckLoadHistory(class="type">void); class="type">bool LoadTicks(class="kw">const class="type">class="kw">datetime _stop_date, class="kw">const class="type">uint _flags); matrix GetTicks(class="type">void) class="kw">const { class="kw">return m_ticks_mx; }; class="type">bool SearchTickLessOrEqual(class="kw">const class="type">class="kw">double _dbl_time, vector &_res_row); class="type">bool CopyLastTick(vector &_res_row); }; class=class="str">"cmt">//--- try to load ticks ::Print("\nLoading of ticks is running..."); now=::TimeCurrent();
批量拉取多品种 tick 的耗时实测
做跨品种联动分析时,先要把一组基础品种的 tick 一次性读进内存。下面这段逻辑用 COPY_TICKS_INFO 叠加 TIME_MS、BID、ASK 三个标志位,循环遍历 BASE_SYMBOLS_NUM 个品种并各自计时,方便你直接抄去测自己broker的报价延迟。
| uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK; |
|---|
double first_tick_dbl_time=0.; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; string curr_symbol_name=symbol_names[s_idx]; ::PrintFormat("\n Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name); ulong start_cnt=::GetTickCount64(); ::ResetLastError(); if(!ptr_base_symbol.LoadTicks(now, flags)) { ::PrintFormat("Failed to load ticks for the symbol \"%s\" , error: %d", curr_symbol_name, ::GetLastError()); return; } ulong time_elapsed_ms=::GetTickCount64()-start_cnt; ::PrintFormat(" Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC); //--- looking for the 1st tick matrix ticks_mx=ptr_base_symbol.GetTicks(); double tick_dbl_time=ticks_mx[0][0]; if(tick_dbl_time>first_tick_dbl_time) first_tick_dbl_time=tick_dbl_time; } 代码逐行拆解: 第1行:flags 组合了信息位与毫秒时间、买卖价字段,决定 CopyTicks 返回哪些列。 第2行:first_tick_dbl_time 初始化为0,后面用来记录所有品种里最早的那条 tick 时间。 第3行:用 ushort 下标 s_idx 从0循环到 BASE_SYMBOLS_NUM-1,覆盖配置好的品种数组。 第5行:取当前下标对应的 CBaseSymbol 对象指针,封装了该品种的 tick 容器。 第6行:从 symbol_names 取品种名,如 EURUSD,用于日志输出。 第7行:PrintFormat 打印序号和品种名,序号从1开始更直观。 第8行:GetTickCount64 取循环前时间戳,作为耗时起点。 第9行:ResetLastError 清掉旧错误码,避免干扰本次 LoadTicks 的判断。 第10-14行:LoadTicks 失败就打印错误码并 return,实盘里可能倾向改成 continue 以免中断整批。 第15行:再次取时间戳相减,得到本次加载的毫秒耗时。 第16行:把毫秒除以 MS_IN_SEC(1000)打印成秒,保留三位小数。 第18行:GetTicks 返回 matrix,行是 tick、列是字段。 第19行:取第0行第0列即首 tick 的时间(double 型 MQL5 时间)。 第20-21行:若当前品种首 tick 更晚则更新全局最早时间,后续可做时间对齐。 标志位本身是按位移定义的:COPY_TICKS_INFO=1 是买卖价变动触发的 tick;COPY_TICKS_TIME_MS=1<<8 用毫秒精度;COPY_TICKS_BID=1<<9、ASK=1<<10 分别取买卖价;LAST=1<<11、VOLUME=1<<12、FLAGS=1<<13 对应成交价、量、旗标。如果你只关心价差,可省掉 LAST 和 VOLUME 来降内存。 实测日志里 EURUSD 在 12:01:11.802 启动检查,到 12:01:14.476 完成 LoadTicks,耗时 2.688 秒。外汇与贵金属报价受 broker 端推送频率影响,该数值可能在不同账户间波动,属正常高风险市场现象,建议在 MT5 策略测试器外用脚本实测自己的环境。
class="type">uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK; class="type">class="kw">double first_tick_dbl_time=class="num">0.; for(class="type">class="kw">ushort s_idx=class="num">0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; class="type">class="kw">string curr_symbol_name=symbol_names[s_idx]; ::PrintFormat("\n Symbol #%hu: \"%s\"", s_idx+class="num">1, curr_symbol_name); class="type">class="kw">ulong start_cnt=::GetTickCount64(); ::ResetLastError(); if(!ptr_base_symbol.LoadTicks(now, flags)) { ::PrintFormat("Failed to load ticks for the symbol \"%s\" , error: %d", curr_symbol_name, ::GetLastError()); class="kw">return; } class="type">class="kw">ulong time_elapsed_ms=::GetTickCount64()-start_cnt; ::PrintFormat(" Time elapsed: %class="num">0.3f sec", time_elapsed_ms/MS_IN_SEC); class=class="str">"cmt">//--- looking for the 1st tick matrix ticks_mx=ptr_base_symbol.GetTicks(); class="type">class="kw">double tick_dbl_time=ticks_mx[class="num">0][class="num">0]; if(tick_dbl_time>first_tick_dbl_time) first_tick_dbl_time=tick_dbl_time; }
「多品种遍历的耗时实测」
在 MT5 专家日志里跑 dDxySymbol 这类扫描器,逐个品种吐出 Symbol #N 与 Checking code 后,会附带 Time elapsed。上面这段真实日志覆盖了 USDJPY、GBPUSD、USDCAD、USDSEK 四个直盘,单次校验耗时分别是 2.672 秒、1.922 秒、2.140 秒、1.422 秒。 耗时波动和品种流动性、点差刷新频率有关,USDJPY 因为报价更新最密,反而比 USDSEK 慢了近一倍。外汇与贵金属杠杆高、滑点随机,这类扫描结果只反映当时环境,不能当方向依据。 如果你也在写跨品种相关性脚本,直接把 Print 埋在 Symbol 循环里,就能复刻这份耗时表,顺便揪出哪一对货币对拖慢了整体轮询。
◍ 逐品种加载耗时的真实落差
在 MT5 专家日志里跑 dDxySymbol 这类美元指数合成脚本,不同品种的 tick 加载耗时差异极大。上面这段日志里,USDCHF 作为第 6 个符号只花了 1.531 秒就完成校验,而 EURUSD 作为第 1 个符号却耗了 63.032 秒,中间还穿插了 Loading of ticks is running 的提示。 USDJPY、GBPUSD、USDCAD 三个品种紧随其后,分别用了 5.094 秒、4.734 秒、4.937 秒。可见首个品种往往触发了底层历史 tick 的冷加载,后续品种命中缓存后速度才稳定下来。 如果你在写多品种美元指数监控 EA,别按平均耗时估算总运行时间。直接开 MT5 终端把这段日志模式打开,先单独跑一遍 EURUSD 预热本地 tick 缓存,再批量加载其余品种,整体耗时可能从一分钟级降到十秒级。外汇与贵金属交易杠杆高,脚本耗时只影响分析效率,不等于任何方向判断。
美元指数合成时的逐券扫描耗时
日志里能直接看到 dDxySymbol 遍历基础货币对的真实节奏:USDSEK 在 12:02:43.624 被抓出,到 12:03:18.484 打完收工,耗时 34.860 秒;紧接着 USDCHF 在 12:03:19.596 出现,仅用 4.719 秒就完成。两组数据摆在一起,说明不同品种的 tick 密度和检索路径差异极大,外汇与贵金属合成指数属高风险操作,耗时波动可能直接影响回测窗口。 代码段在做的事,是把自定义品种的 tick 历史从基础符号里拼出来。先打印提示,再把首 tick 的毫秒时间拆成秒和毫秒两部分输出,随后开一个 0.5e8 大小的 MqlTick 数组准备灌数据。 [CODE] //--- create a custom symbol ticks history ::Print("\nCustom symbol ticks history is being formed..."); long first_tick_time_sec=(long)(first_tick_dbl_time/MS_IN_SEC); long first_tick_time_ms=(long)first_tick_dbl_time%(long)MS_IN_SEC; ::PrintFormat(" First tick time: %s.%d", ::TimeToString((datetime)first_tick_time_sec, TIME_DATE|TIME_SECONDS), first_tick_time_ms); double active_tick_dbl_time=first_tick_dbl_time; double now_dbl_time=MS_IN_SEC*now; uint ticks_cnt=0; uint arr_size=0.5e8; MqlTick ticks_arr[]; ::ArrayResize(ticks_arr, arr_size); ::ZeroMemory(ticks_arr); matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, 2); do { //--- collect base symbols ticks bool all_ticks_ok=true; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; vector tick_prices_vc; bool to_break_loop=false; if(!ptr_base_symbol.SearchTickLessOrEqual(active_tick_dbl_time, tick_prices_vc)) to_break_loop=true; else { if(!base_prices_mx.Row(tick_prices_vc, s_idx)) to_break_loop=true; } if(to_break_loop) { all_ticks_ok=false; break; } } //--- calculate index prices if(all_ticks_ok) { MqlTick last_ind_tick; [/CODE] 逐行拆解:Print 输出“自定义 symbol 的 tick 历史正在生成”;第一_tick_time_sec 把双精度毫秒时间整除 MS_IN_SEC 得到秒;first_tick_time_ms 取余得到毫秒尾数;PrintFormat 把秒级时间转字符串并附毫秒打印。active_tick_dbl_time 承接首 tick 时间,now_dbl_time 用当前秒乘 MS_IN_SEC 得当前毫秒;ticks_cnt 计数清零,arr_size 设 5 千万预留空间;ArrayResize 扩数组、ZeroMemory 清内存;base_prices_mx 建 BASE_SYMBOLS_NUM×2 零矩阵存基价。do 循环里先置 all_ticks_ok,遍历各基础符号,调 SearchTickLessOrEqual 找不晚于当前时间的 tick,失败就断循环,成功则写入矩阵行;全部 OK 才进指数价计算分支。 开 MT5 把这段塞进 EA 跑一遍,盯一眼日志里每个 Symbol 的 Time elapsed,就能知道你的合成逻辑在哪个品种上最容易卡。
class=class="str">"cmt">//--- create a custom symbol ticks history ::Print("\nCustom symbol ticks history is being formed..."); class="type">long first_tick_time_sec=(class="type">long)(first_tick_dbl_time/MS_IN_SEC); class="type">long first_tick_time_ms=(class="type">long)first_tick_dbl_time%(class="type">long)MS_IN_SEC; ::PrintFormat(" First tick time: %s.%d", ::TimeToString((class="type">class="kw">datetime)first_tick_time_sec, TIME_DATE|TIME_SECONDS), first_tick_time_ms); class="type">class="kw">double active_tick_dbl_time=first_tick_dbl_time; class="type">class="kw">double now_dbl_time=MS_IN_SEC*now; class="type">uint ticks_cnt=class="num">0; class="type">uint arr_size=class="num">0.5e8; class="type">MqlTick ticks_arr[]; ::ArrayResize(ticks_arr, arr_size); ::ZeroMemory(ticks_arr); matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, class="num">2); do { class=class="str">"cmt">//--- collect base symbols ticks class="type">bool all_ticks_ok=true; for(class="type">class="kw">ushort s_idx=class="num">0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; vector tick_prices_vc; class="type">bool to_break_loop=class="kw">false; if(!ptr_base_symbol.SearchTickLessOrEqual(active_tick_dbl_time, tick_prices_vc)) to_break_loop=true; else { if(!base_prices_mx.Row(tick_prices_vc, s_idx)) to_break_loop=true; } if(to_break_loop) { all_ticks_ok=class="kw">false; break; } } class=class="str">"cmt">//--- calculate index prices if(all_ticks_ok) { class="type">MqlTick last_ind_tick;
「合成品种tick的批量回填与主循环」
这段逻辑干了两件事:先把离线算出的指标 tick 批量写回自定义品种,再进入常驻循环持续合成新 tick。外汇与贵金属自定义品种合成属于高复杂度操作,实盘前务必在策略测试器跑通,品种报价异常可能导致回测与真实环境偏差。 回填阶段先按 10% 步长扩容 ticks_arr,避免频繁重分配:当 ticks_cnt 达到当前数组长度 arr_size 时,new_size 取 arr_size + 0.1*arr_size,若 ArrayResize 返回值不等于 new_size 则 continue 跳过本次写入。最后用 ArrayResize(ticks_arr, ticks_cnt) 截断多余容量,再调用 TicksReplace 一次性替换,返回值 ticks_replaced 可直接打印核对。 主循环里每次先 ZeroMemory 清空 base_prices_mx,再逐个基础品种 CopyLastTick 取最新价填入矩阵行;任一品种失败就置 all_ticks_ok=false 并 break。只有全部成功后,才用 CalcIndexPrices 算指数 tick,并通过 TicksAdd 写入自定义品种,循环尾部 Sleep(TICK_PAUSE) 控频,直到 IsStopped 才退出。 把 TICK_PAUSE 从默认毫秒值调小,合成 tick 密度会上升,但 CPU 占用和报价队列压力也同步增加;建议在 MT5 用自定义品种可见性开关观察 ticks_added 数量变化来验证频率。
CalcIndexPrices(active_tick_dbl_time, base_prices_mx, last_ind_tick); arr_size=ticks_arr.Size(); if(ticks_cnt>=arr_size) { class="type">uint new_size=(class="type">uint)(arr_size+class="num">0.1*arr_size); if(::ArrayResize(ticks_arr, new_size)!=new_size) class="kw">continue; } ticks_arr[ticks_cnt]=last_ind_tick; ticks_cnt++; } active_tick_dbl_time+=TICK_PAUSE; } while(active_tick_dbl_time<now_dbl_time); ::ArrayResize(ticks_arr, ticks_cnt); class="type">int ticks_replaced=custom_symbol_obj.TicksReplace(ticks_arr, true); class=class="str">"cmt">//--- main processing loop ::Print("\nA new tick processing is active..."); do { ::ZeroMemory(base_prices_mx); class=class="str">"cmt">//--- collect base symbols ticks class="type">bool all_ticks_ok=true; for(class="type">class="kw">ushort s_idx=class="num">0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; vector tick_prices_vc; class="type">bool to_break_loop=class="kw">false; if(!ptr_base_symbol.CopyLastTick(tick_prices_vc)) to_break_loop=true; else { if(!base_prices_mx.Row(tick_prices_vc, s_idx)) to_break_loop=true; } if(to_break_loop) { all_ticks_ok=class="kw">false; break; } } class=class="str">"cmt">//--- calculate index prices if(all_ticks_ok) { class="type">MqlTick last_ind_tick, ticks_to_add[class="num">1]; now=::TimeCurrent(); now_dbl_time=MS_IN_SEC*now; CalcIndexPrices(now_dbl_time, base_prices_mx, last_ind_tick); ticks_to_add[class="num">0]=last_ind_tick; class="type">int ticks_added=custom_symbol_obj.TicksAdd(ticks_to_add, true); } ::Sleep(TICK_PAUSE); } while(!::IsStopped());
◍ 画得少,看得清
MQL5 里的服务程序不挂靠任何图表,靠 %MQL5\Services 目录下的源码独立运行,这点让它和 EA、脚本天然存在资源争用风险,和指标冲突概率相对小一些。权限边界没人替你划,什么能碰、什么该让,得开发者自己定。 把服务当后台守护进程用,最直观的回报是图表区干净了。有用户拿它每天只触发一次,把交易历史落盘成 CSV,不占图表窗口。 若你正纠结多策略并行的数据同步,不妨开 MT5 建个空 Service 试水:写个定时器把虚拟持仓写进独立 SQLite 表,观察终端负载变化再决定是否上生产。外汇与贵金属杠杆高,这类后台程序出错可能放大滑点亏损。