MetaTrader 中的 Multibot(第二部分):改进的动态模板·进阶篇
(2/3)· 当同一个 EA 要在十几个品种周期上跑,手动复制带来的幻数冲突只是开始
用文件指纹驱动多图表重载
想让一个 EA 同时管几十个「品种+周期」的虚拟实例,核心不是硬写图表列表,而是让工作目录里的文件集当信号源。当终端目录出现新的「EURUSD 60」类 txt,或旧文件被删,就认为配置有变,触发全部图表和 EA 重载,避免手动增删品种-周期对。 FilesGrab 只做一件事:扫描与 EA 关联的文件名,截出能被后续解析成「品种 周期」的前半段(例如「EURUSD 60」),真正读文件内容留到每个虚拟 EA 内部做。过滤掉允许列表之外的品种,把合法项塞进 S 数组,供后面生成虚拟图表对象使用。 备份逻辑要注意:带 CLOSING 前缀的备份始终写当前终端目录;若基础设置已有未平仓位,就停止周期性的设置预留。重载或重读模板时,只有对应虚拟实例还有未平仓位,才会从备份读数据,否则直接与通用设置同步。外汇与贵金属波动剧烈,这套机制只解决配置同步,不预示任何方向。 下面这段代码把「探测文件变化」和「收集文件名」落到了可跑的函数。QuantityConfiguration 先调 FilesGrab,再用 bNewConfiguration 判断是否有增删;FilesGrab 里用 FileFindFirst/Next 遍历,按第二个空格截断文件名存入 SettingsFileNames 数组。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| used for configuration settings | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool QuantityConfiguration() { FilesGrab(); class=class="str">"cmt">// Determine the names of valid files class=class="str">"cmt">// Check if there are changes in the configuration settings(either add or class="kw">delete) if (bNewConfiguration()) { class="kw">return true; } class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| reads all files and forms a list of instruments and periods | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void FilesGrab() { class="type">class="kw">string file; class="type">class="kw">string tempsubfolder= SubfolderE == "" ? "" : SubfolderE + "\\"; class=class="str">"cmt">// SubfolderE is the path to the specific subfolder class=class="str">"cmt">// Returns the handle of the first found file with the specified characteristics, based on whether CommonReadE is True or False class="type">long total_files = !bCommonReadE? FileFindFirst(tempsubfolder+"*"+BotNick+".txt", file) :FileFindFirst(tempsubfolder+"*"+BotNick+".txt", file,FILE_COMMON); if(total_files > class="num">0) { ArrayResize(SettingsFileNames,class="num">0); class=class="str">"cmt">// Clear the array from previous values if there are files to be read do { class="type">int second_space = StringFind(file, " ", StringFind(file, " ") + class="num">1); class=class="str">"cmt">// Searches for the index of the second space in the file&class="macro">#x27;s name if(second_space > class="num">0) { class="type">class="kw">string filename = StringSubstr(file, class="num">0, second_space); class=class="str">"cmt">// Extracts the class="type">class="kw">string/characters from the filename up to the second space ArrayResize(SettingsFileNames, ArraySize(SettingsFileNames) + class="num">1); class=class="str">"cmt">// Increases the size of the array by one SettingsFileNames[ArraySize(SettingsFileNames) - class="num">1] = filename; class=class="str">"cmt">// Adds the new filename into the existing array } } while(FileFindNext(total_files, file)); class=class="str">"cmt">// Repeat for all the files FileFindClose(total_files); class=class="str">"cmt">// Close the file handle to free resources } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| symbol validator | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool AdaptDynamicArrays() { class="type">bool RR=QuantityConfiguration(); class=class="str">"cmt">// If a new configuration of files is detected(new files, changed order, etc.)
「用文件遍历把主图设置同步到平仓图表」
这段逻辑解决一个实盘常见痛点:多图表盯盘时,主图调好的参数若不能自动落到平仓专用图表,人工复制容易漏项。前半段代码在 RR 为真时,先按预设文件名数组尺寸给 S 扩容,再拿 BasicNameToSymbol 转换后的品种名去 III 数组里逐个比对,命中就写进 S 并 Valid 自增,最后用 ArrayResize(S, Valid) 把冗余槽位砍掉——只保留真实有效的设置项。 后半段 SaveCloseSettings 负责拷贝:先用 FileFindFirst 抓子目录首个文件,若 bCommonReadE 为真走 FILE_COMMON 公共目录,否则走本地。StringSubstr 比对文件名前缀是否等于 BasicName,命中就拼出完整路径并 break,避免无谓遍历。 在 MT5 里直接跑这段,你能验证一个现象:当 SettingsFileNames 有 8 个条目、III 只含其中 5 个有效品种时,S 最终长度会收敛为 5 而非 8。外汇与贵金属杠杆高,参数同步出错可能引發非预期平仓,建议先在模拟环境确认路径权限。
if (RR) { class=class="str">"cmt">// Read the settings(returns the count) class="type">int Readed = ArraySize(SettingsFileNames); class="type">int Valid =class="num">0; class=class="str">"cmt">// Only valid symbol name needs to be populated(filenames are taken from already prepared array) ArrayResize(S, Readed); for ( class="type">int j = class="num">0; j < Readed; j++ ) { for ( class="type">int i = class="num">0; i < ArraySize(III); i++ ) { class=class="str">"cmt">// check the symbol to valid if ( III[i] == BasicNameToSymbol(SettingsFileNames[j]) ) { S[Valid++]=SettingsFileNames[j]; break; class=class="str">"cmt">// stop the loop } } } class=class="str">"cmt">//resize S with the actual valid quantity ArrayResize(S, Valid); class="kw">return true; } class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| сopy settings from the main file to a CLOSING file | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SaveCloseSettings() { class="type">class="kw">string FileNameString=Charts[chartindex].BasicName; class="type">bool bCopied; class="type">class="kw">string filenametemp; class="type">class="kw">string filename=""; class="type">long handlestart; class=class="str">"cmt">//Checking if SubfolderE doesn&class="macro">#x27;t exist, if yes, assign tempsubfolder to be an empty class="type">class="kw">string class="type">class="kw">string tempsubfolder= SubfolderE == "" ? "" : SubfolderE + "\\"; class=class="str">"cmt">//Find the first file in the subfolder according to bCommonReadE and assign the result to handlestart if (bCommonReadE) handlestart=FileFindFirst(tempsubfolder+"*",filenametemp,FILE_COMMON); else handlestart=FileFindFirst(tempsubfolder+"*",filenametemp); class=class="str">"cmt">//Check if the start of our found file name matches FileNameString if ( StringSubstr(filenametemp,class="num">0,StringLen(FileNameString)) == FileNameString ) { class=class="str">"cmt">//if yes, complete the file&class="macro">#x27;s path filename=tempsubfolder+filenametemp; } class=class="str">"cmt">//keep finding the next file while conditions are aligned while ( FileFindNext(handlestart,filenametemp) ) { class=class="str">"cmt">//if found file&class="macro">#x27;s name matches FileNameString then add found file&class="macro">#x27;s name to the path if ( StringSubstr(filenametemp,class="num">0,StringLen(FileNameString)) == FileNameString ) { filename=tempsubfolder+filenametemp; break; } }
◍ 收尾时释放句柄并落盘复制
这段逻辑处在文件遍历的末尾,核心动作只有两件:先把搜索句柄关掉,再把命中文件复制出去。 若 handlestart 不是 INVALID_HANDLE,就调用 FileFindClose 释放资源,否则会一直占着文件系统的搜索上下文,多跑几轮就容易把句柄堆满。 复制时分两条路径:bCommonReadE 为真时走 FILE_COMMON 共享目录,否则用本地 0 标志;目标名统一拼成 "CLOSING "+原文件名+".txt",覆盖写且按 ANSI 文本存。 在 MT5 里把这段直接塞进你的遍历函数末尾,跑完用文件管理器看 tempsubfolder 下是否多出 CLOSING 前缀的 txt,就能验证句柄有没有漏关、复制是否真成功。
class=class="str">"cmt">//if handlestart is not INVALID_HANDLE then close the handle to release the resources after the search if (handlestart != INVALID_HANDLE) FileFindClose(handlestart); class=class="str">"cmt">//Perform file copy operation and notice if it was successful if ( bCommonReadE ) bCopied=FileCopy(filename,FILE_COMMON,tempsubfolder+"CLOSING "+FileNameString+".txt",FILE_REWRITE|FILE_TXT|FILE_ANSI); else bCopied=FileCopy(filename,class="num">0,tempsubfolder+"CLOSING "+FileNameString+".txt",FILE_REWRITE|FILE_TXT|FILE_ANSI); }
去重建图与虚拟EA挂载
多品种多周期回测里,先得把交易品种去重,否则同一个符号反复建图会拖垮内存。上面这段逻辑就是拿一个临时数组扫一遍传入的 S 数组,把不重复的符号挑出来,num 计数后据此给 Charts 数组定长。 每个虚拟图表默认柱形数锁在 1000 根(Chart::TCN=1000),对绝大多数日内与波段策略够用;真碰到长周期均线或跨年形态,改这一行 tempcnum 就能扩。 图表对象建好后,下一步是把虚拟 EA 挂上去,并把当前图表 ID 写进 EA 内部,后续拉数据才知道从哪张图取。两个类的内部结构和前文差别不大,只是做了些不痛不痒的调整。 下面代码逐行拆一下建图过程:先按 S 大小开临时数组并清空,双层循环比对挑出唯一品种;ArrayResize(Symbols,num) 定稿后,给每张图 new 出来并预设 lastcopied=0、CloseI 开到 1002 格(多留两格缓冲)。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| creates chart objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateCharts() { class="type">bool bAlready; class="type">int num=class="num">0; class="type">class="kw">string TempSymbols[]; class="type">class="kw">string Symbols[]; ArrayResize(TempSymbols,ArraySize(S)); class=class="str">"cmt">// Resize TempSymbols array to the size of S array for (class="type">int i = class="num">0; i < ArraySize(S); i++) class=class="str">"cmt">// Populate TempSymbols array with empty strings { TempSymbols[i]=""; } for (class="type">int i = class="num">0; i < ArraySize(S); i++) class=class="str">"cmt">// Count the required number of unique trading instruments { bAlready=false; for (class="type">int j = class="num">0; j < ArraySize(TempSymbols); j++) { if ( S[i] == TempSymbols[j] ) class=class="str">"cmt">// If any symbol is already present in TempSymbols from S, then it&class="macro">#x27;s not unique { bAlready=true; break; } } if ( !bAlready ) class=class="str">"cmt">// If the symbol is not found in TempSymbols i.e., it is unique, add it to TempSymbols { for (class="type">int j = class="num">0; j < ArraySize(TempSymbols); j++) { if ( TempSymbols[j] == "" ) { TempSymbols[j] = S[i]; break; } } num++; class=class="str">"cmt">// Increments num if a unique element is added } } ArrayResize(Symbols,num); class=class="str">"cmt">// Resize the Symbols array to the size of the num for (class="type">int j = class="num">0; j < ArraySize(Symbols); j++) class=class="str">"cmt">// Now that the Symbols array has the appropriate size, populate it { Symbols[j]=TempSymbols[j]; } ArrayResize(Charts,num); class=class="str">"cmt">// Resize Charts array to the size of num class="type">int tempcnum=class="num">0; tempcnum=class="num">1000; class=class="str">"cmt">// Sets all charts to a class="kw">default of class="num">1000 bars Chart::TCN=tempcnum; for (class="type">int j = class="num">0; j < ArraySize(Charts); j++) { Charts[j] = new Chart(); Charts[j].lastcopied=class="num">0; class=class="str">"cmt">// Initializes the array position where the last copy of the chart was stored Charts[j].BasicName=Symbols[j]; ArrayResize(Charts[j].CloseI,tempcnum+class="num">2); class=class="str">"cmt">// Resizes the CloseI array to store closing price of each bar
「给图表容器扩容并挂载虚拟机器人」
多品种监控框架里,Charts 数组每个元素都要先把自己的 OHLC 与时间的动态数组撑开,否则后续写入新柱数据会越界。代码里统一按 tempcnum+2 的长度做 ArrayResize,多留两个槽位,是为了缓冲实时行情推送时可能多出的临时帧。 品种名拼装不是直接拿基础名用,而是先过 BasicNameToSymbol 取出,再按 bToLowerE 开关决定是否 StringToLower,最后前后加 SymbolPrefixE / SymbolPostfixE 得到真实交易品种串。Timeframe 也同期从基础名里解析,保证一张图只绑一个周期。 真正把策略跑起来的是 CreateInstances:它拿 S 数组(策略清单)和 Charts 数组(图表清单)做双重遍历,BasicName 对上了就 new 一个 BotInstance(i,j) 塞进 Bots 数组,并立即 break 防止重复挂。Bots 数组此前已 Resize 到 ArraySize(S),所以下标一一对应。 开 MT5 把这段接进自己的 EA,改一下 SymbolPrefixE 和 bToLowerE,就能验证非标准品种(如带 '.m' 后缀的现货黄金报价)是否被正确识别并实例化。外汇与贵金属杠杆高,实盘前请用策略测试器跑历史数据确认挂载逻辑无漏。
ArrayResize(Charts[j].OpenI,tempcnum+class="num">2); class=class="str">"cmt">// Resizes the OpenI array for opening prices ArrayResize(Charts[j].HighI,tempcnum+class="num">2); class=class="str">"cmt">// HighI array for high price points in each bar ArrayResize(Charts[j].LowI,tempcnum+class="num">2); class=class="str">"cmt">// LowI array for low price points of each bar ArrayResize(Charts[j].TimeI,tempcnum+class="num">2); class=class="str">"cmt">// TimeI array is resized to store time of each bar class="type">class="kw">string vv = BasicNameToSymbol(Charts[j].BasicName); StringToLower(vv); class=class="str">"cmt">// Append prefix and postfix to the basic symbol name to get the specific symbol of the financial instrument Charts[j].CurrentSymbol = SymbolPrefixE + (!bToLowerE ? BasicNameToSymbol(Charts[j].BasicName) : vv) + SymbolPostfixE; Charts[j].Timeframe = BasicNameToTimeframe(Charts[j].BasicName); class=class="str">"cmt">// Extracts the timeframe from the basic name class="type">class="kw">string } ArrayResize(Bots,ArraySize(S)); class=class="str">"cmt">// Resize Bots array to the size of S array } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| attaching all class="kw">virtual robots to charts | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateInstances() { class=class="str">"cmt">// iterating over the S array for (class="type">int i = class="num">0; i < ArraySize(S); i++) { class=class="str">"cmt">// iterating over the Charts array for (class="type">int j = class="num">0; j < ArraySize(Charts); j++) { class=class="str">"cmt">// checking if the BasicName of current Chart matches with the current item in S array if ( Charts[j].BasicName == S[i] ) { class=class="str">"cmt">// creating a new Bot instance with indices i, j and assigning it to respective position in Bots array Bots[i] = new BotInstance(i,j); break; } } } }
◍ 用计时器让虚拟EA热更新配置
虚拟图表和EA建起来只是半套活。真正麻烦的是在不打扰交易终端的前提下,让虚拟EA即时吃到新设置并自己重配置。原文用了一个每5分钟触发的轻量计时器,实测这个间隔对动态化已经够用;若你嫌慢,把周期压到1秒也行,但文件操作别太频繁,MT5里这类IO能避就避。 关键在ReadSettings方法:它按虚拟EA逐个读专属文件,然后重配置。设计上区分两类文件——bClosingFile标记决定读常规设置,还是读‘用于平仓’的设置。逻辑是:若该虚拟EA无持仓就读常规设置;有持仓就暂停常规更新,等按原设置平仓后再动。 每次读取走三步:比对上一次文件内容与当前内容;不一致才读更新后的设置;需要时按新设置重启虚拟EA。资源清理等脏活方法内部已包好,你只需填‘逐行读字符串并写进对应变量’那块。最后三行别动,其中SmartMagic会自动给每个虚拟EA分配幻数。你要做的,是把EA设置重分配逻辑写进上层空块,必要时连指标也一起重建。 外汇与贵金属杠杆高,虚拟EA热更新若读错文件可能导致异常下单,实盘前务必在策略测试器跑通文件比对逻辑。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| we will read the settings every class="num">5 minutes + | class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> bReadTimer() { <span class="keyword">if</span> ( <span class="functions">TimeCurrent</span>() - LastTime > <span class="number">class="num">5</span>*<span class="number">class="num">60</span> + <span class="keyword">class="type">int</span>((<span class="keyword">class="type">class="kw">double</span>(<span class="functions">MathRand</span>())/<span class="number">class="num">32767.0</span>) * <span class="number">class="num">60</span>) ) { LastTime=<span class="functions">TimeCurrent</span>(); <span class="keyword">class="type">int</span> orders=OrdersG(); <span class="keyword">class="type">bool</span> bReaded=<span class="macro">false</span>; <span class="keyword">if</span> (orders == <span class="number">class="num">0</span>) bReaded = ReadSettings(<span class="macro">false</span>,Charts[chartindex].BasicName);<span class="comment">class=class="str">"cmt">//reading a regular file</span> <span class="keyword">else</span> bReaded = ReadSettings(<span class="macro">true</span>,Charts[chartindex].BasicName);<span class="comment">class=class="str">"cmt">//reading file to close position</span> <span class="keyword">if</span> (orders == <span class="number">class="num">0</span> && bReaded) SaveCloseSettings();<span class="comment">class=class="str">"cmt">//save settings for closing position</span> <span class="keyword">class="kw">return</span> bReaded; } <span class="keyword">class="kw">return</span> <span class="macro">false</span>; } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| reading settings | class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">bool</span> BotInstance::ReadSettings(<span class="keyword">class="type">bool</span> bClosingFile,<span class="keyword">class="type">class="kw">string</span> Path) { <span class="keyword">class="type">class="kw">string</span> FileNameString=Path; <span class="keyword">class="type">int</span> Handle0x; <span class="keyword">class="type">class="kw">string</span> filenametemp; <span class="keyword">class="type">class="kw">string</span> filename=<span class="class="type">class="kw">string">""</span>; <span class="keyword">class="type">long</span> handlestart; <span class="keyword">class="type">class="kw">string</span> tempsubfolder= SubfolderE == <span class="class="type">class="kw">string">""</span> ? <span class="class="type">class="kw">string">""</span> : SubfolderE + <span class="class="type">class="kw">string">"\\"</span>; <span class="keyword">if</span> (!bClosingFile)<span class="comment">class=class="str">"cmt">//reading a regular file</span> { <span class="keyword">if</span> (!bCommonReadE) { handlestart=<span class="functions">FileFindFirst</span>(tempsubfolder+<span class="class="type">class="kw">string">"*"</span>,filenametemp); <span class="keyword">class="type">int</span> SearchStart=<span class="number">class="num">0</span>; <span class="keyword">if</span> ( <span class="functions">StringSubstr</span>(filenametemp,SearchStart,<span class="functions">StringLen</span>(FileNameString)) == FileNameString ) { filename=tempsubfolder+filenametemp; } <span class="keyword">if</span> (filename != filenametemp || filename == <span class="class="type">class="kw">string">""</span>) { <span class="keyword">while</span> ( <span class="functions">FileFindNext</span>(handlestart,filenametemp) ) { <span class="keyword">if</span> ( <span class="functions">StringSubstr</span>(filenametemp,SearchStart,<span class="functions">StringLen</span>(FileNameString)) == FileNameString ) {