MetaTrader 中的 Multibot(第二部分):改进的动态模板·综合运用
(3/3)·当虚拟图表、自动幻数与 API 同步叠加,传统静态模板的出错率会指数级放大
把十几张图表上的 EA 挨个手填参数串,是多数多品种策略崩盘前的标准动作。人在复制粘贴里漏一位字符,幻数冲突和错周期下单就埋下了。本篇收尾这套动态模板系列,直接拆开改进版怎么把配置熵压到最低。
临时文件里的参数热加载逻辑
这段片段做的是在 MT5 的 MQL5 程序里,从临时子目录翻出一个命名带 tempsubfolder 前缀、filenametemp 后缀的文本文件,读出来比对上一次内容,若不一样就重新解析参数并重启配置。 搜索句柄 handlestart 在用完之后必须调 FileFindClose 释放,否则在 EA 反复轮询时可能泄漏系统资源,这在跑 24x5 的外汇或贵金属策略时是高概率踩坑点。
| 文件打开用了 FILE_READ | FILE_SHARE_READ | FILE_TXT | FILE_ANSI 组合,意味着允许别的进程同时读、按 ANSI 文本解析;若你的配置文件含中文路径或 UTF-8 签名,这里会读歪,贵金属跨平台拷贝时要留意。 |
|---|
核心去重靠 PrevReaded 字符串比对,只有 str 非空且不等于上次读取才进 ReadFileStrings 并 RestartParams,同时记 LastRead = TimeCurrent()。这一道拦截能避免每 tick 重读文件拖慢执行,回测里文件 size 超过几 MB 时差异会很明显。
filename=tempsubfolder+filenametemp; break; } } } if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);class=class="str">"cmt">// Release resources after search if (filename != "") { Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI); if ( Handle0x != INVALID_HANDLE )class=class="str">"cmt">//if the file exists { FileSeek(Handle0x,class="num">0,SEEK_SET); class="type">ulong size = FileSize(Handle0x); class="type">class="kw">string str = ""; for(class="type">ulong i = class="num">0; i < size; i++) { str += FileReadString(Handle0x); } if (str != "" && str != PrevReaded) { FileSeek(Handle0x,class="num">0,SEEK_SET); class=class="str">"cmt">//read the required parameters ReadFileStrings(Handle0x); class=class="str">"cmt">// FileClose(Handle0x); LastRead = TimeCurrent(); RestartParams(); } else { FileClose(Handle0x); } class="kw">return true; } else { class="kw">return false; } } } else
◍ 在公共目录里按前缀捞文件并读内容
EA 在 MT5 公共目录(FILE_COMMON)里批量找文件时,先用 FileFindFirst 拿句柄,再用 StringSubstr 比对文件名前缀,命中就拼出完整路径。这段逻辑能解决多子目录下同名前缀配置的自动定位,不用写死文件名。 下面这段是定位与读取的核心片段,逐行拆一下:FileFindFirst 在 tempsubfolder 下搜 '*' 拿到首个文件句柄;SearchStart=0 表示从第 0 字符起比前缀;若首文件前缀就等于 FileNameString,直接记 filename。若没直接命中,进 while(FileFindNext) 继续扫,命中就 break 退出循环。
| 扫完无论找没找到,handlestart 不是 INVALID_HANDLE 就必须 FileFindClose 释放,否则句柄泄漏会让后续文件操作在跑几个小时后概率性失败。找到文件后 FileOpen 用 FILE_READ | FILE_SHARE_READ | FILE_TXT | FILE_ANSI | FILE_COMMON 打开,FileSeek 归零后按 FileSize 逐字节 FileReadString 拼进 str,若和上次 PrevReaded 不同才进 ReadFileStrings 解析并关闭。外汇与贵金属行情下这类共享文件读写涉及多程序并发,属高风险操作,实盘前请在策略测试器里先验证句柄释放路径。 |
|---|
{
handlestart=FileFindFirst(tempsubfolder+"*",filenametemp,FILE_COMMON);
class="type">int SearchStart=class="num">0;
if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
{
filename=tempsubfolder+filenametemp;
}
if (filename != filenametemp || filename == "")
{
while ( FileFindNext(handlestart,filenametemp) )
{
if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
{
filename=tempsubfolder+filenametemp;
break;
}
}
}
if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);class=class="str">"cmt">// Release resources after search
if (filename != "")
{
Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI|FILE_COMMON);
if ( Handle0x != INVALID_HANDLE )class=class="str">"cmt">//if the file exists
{
FileSeek(Handle0x,class="num">0,SEEK_SET);
class="type">ulong size = FileSize(Handle0x);
class="type">class="kw">string str = "";
for(class="type">ulong i = class="num">0; i < size; i++)
{
str += FileReadString(Handle0x);
}
if (str != "" && str != PrevReaded)
{
FileSeek(Handle0x,class="num">0,SEEK_SET);
class=class="str">"cmt">//read the required parameters
ReadFileStrings(Handle0x);
class=class="str">"cmt">//
FileClose(Handle0x);「用文件标记驱动平仓的检索逻辑」
EA 和外部信号之间常用临时文件做桥梁。当进入“读文件平仓”分支时,先调用 FileFindFirst 在 tempsubfolder 目录下按 '*' 通配,拿到首个文件名放入 filenametemp,同时把 SearchStart 固定为 8——因为约定文件名以 'CLOSING '(含尾部空格,正好 8 字符)开头。 判定目标文件时,要求 filenametemp 长度不小于 8 + FileNameString 长度,且前 8 字符等于 'CLOSING ',再从偏移 8 起截取与 FileNameString 等长的子串做匹配。首轮若没命中,就靠 while 循环套 FileFindNext 继续翻,直到找到或遍历完。 找到后把完整路径写回 filename 并 break。搜完无论是否命中,只要 handlestart 不是 INVALID_HANDLE 就必须 FileFindClose 释放句柄,否则 MT5 终端跑久了可能句柄泄露。
| 确认 filename 非空才用 FileOpen 以 FILE_READ | FILE_SHARE_READ | FILE_TXT | FILE_ANSI 打开;若返回不是 INVALID_HANDLE,说明平仓指令文件确实存在,后续再读内容执行。外汇和贵金属波动剧烈,这种文件通信机制只是信号通道,实际平仓仍受滑点和点差影响,成交价格可能偏离预期。 |
|---|
handlestart=FileFindFirst(tempsubfolder+"*",filenametemp); class="type">int SearchStart=class="num">8;class=class="str">"cmt">//when the line starts with "CLOSING " if ( StringLen(filenametemp) >= (class="num">8 + StringLen(FileNameString)) && StringSubstr(filenametemp,class="num">0,class="num">8) == "CLOSING " && StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString ) { filename=tempsubfolder+filenametemp; } if (filename != filenametemp || filename == "") { while ( FileFindNext(handlestart,filenametemp) ) { if ( StringLen(filenametemp) >= (class="num">8 + StringLen(FileNameString)) && StringSubstr(filenametemp,class="num">0,class="num">8) == "CLOSING " && StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString ) { filename=tempsubfolder+filenametemp; break; } } } if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);class=class="str">"cmt">// Release resources after search if (filename != "") { Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI); if ( Handle0x != INVALID_HANDLE )class=class="str">"cmt">//if the file exists {
文件变更触发参数热重载
这段逻辑解决一个实盘痛点:EA 跑着的时候,你改了外部配置文件,怎么让实例不重启就吃进新参数。核心做法是用 FileSeek 把句柄拨回头部,拿 FileSize 得到字节数,再逐字节用 FileReadString 拼成整串。 拼完的字符串会跟上次读到的 PrevReaded 比。如果不相等且非空,说明文件真被改过,这时先记下新内容,重新 Seek 到 0,调 ReadFileStrings 去解析所需参数,关句柄,把 LastRead 刷成 TimeCurrent(),最后跑 RestartParams() 让新魔术号与交易对象生效。若内容没变,直接关文件返回,不做任何重载。 注意 ReadFileStrings 在这版里是空壳,只留了接口;真要按行解析得自己填。RestartParams 里 SmartMagic 依据基础名映射的 symbol 与周期生成新魔术号,再喂给 CTrade 的 SetExpertMagicNumber,这就是为什么多实例不会互串单。 外汇与贵金属杠杆高,热重载改了魔术号却没清旧挂单,可能让历史仓位脱离新逻辑管控,验证前先在策略测试器跑一遍。
FileSeek(Handle0x,class="num">0,SEEK_SET); class="type">ulong size = FileSize(Handle0x); class="type">class="kw">string str = ""; for(class="type">ulong i = class="num">0; i < size; i++) { str += FileReadString(Handle0x); } if (str != "" && str != PrevReaded) { PrevReaded=str; FileSeek(Handle0x,class="num">0,SEEK_SET); class=class="str">"cmt">//read the required parameters ReadFileStrings(Handle0x); class=class="str">"cmt">// FileClose(Handle0x); LastRead = TimeCurrent(); RestartParams(); } else { FileClose(Handle0x); } class="kw">return true; } else { class="kw">return false; } } class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| read settings from file line by line | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void BotInstance::ReadFileStrings(class="type">int handle) { class=class="str">"cmt">//FileReadString(Handle,class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| function to prepare new parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void BotInstance::RestartParams() { class=class="str">"cmt">//additional code class=class="str">"cmt">// MagicF=SmartMagic(BasicNameToSymbol(Charts[chartindex].BasicName), Charts[chartindex].Timeframe); CurrentSymbol=Charts[chartindex].CurrentSymbol; m_trade.SetExpertMagicNumber(MagicF); }
◍ 用符号和周期拼出不会撞车的幻数
虚拟 EA 模板要在同一终端里跑多品种多周期,最怕不同实例抢同一个幻数导致平仓逻辑串台。一种做法是给每个品种预留一段 10000 的幻数空间,再叠加周期本身的分钟数做偏移,这样 EURUSD+M1 和 XAUUSD+H1 算出来的整数基本不会重叠。 代码里 SmartMagic 先遍历 III 符号数组,命中后取 MagicHelp+(i+1)*10000 作为品种基址,然后直接加 ENUM_TIMEFRAMES 的枚举值(M1=1、H1=60、H4=240 这类分钟量)当周期后缀,返回拼接好的整数。MagicHelp 宏限定在 0~9999,所以单个品种内部还能再腾出近万格余量。 偏移步长别正好踩中 240 的整数倍。若用 240、480 这种,两个不同模板在 H4 上就可能算出相同幻数;改成 241、482、723 这类稍大的素数式间隔,重合概率会明显降低。外汇和贵金属杠杆高,幻数撞车会让策略互相平掉对方的单,实盘前务必在 MT5 策略测试器里打印几个品种周期的返回值核对。 这种结构最大的副作用是两套独立虚拟 EA 无法手动并网——它们的幻数生成规则一致,合并后必然冲突使逻辑崩坏。对普通多品种布控来说,免设幻数已经够用,真要跨模板联动得另写映射层。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Smart generation of magical numbers | class=class="str">"cmt">//| (each instrument-period has its own fixed magic number) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int BotInstance::SmartMagic(class="type">class="kw">string InstrumentSymbol,ENUM_TIMEFRAMES InstrumentTimeframe) { class=class="str">"cmt">// initialization class="type">int magicbuild=class="num">0; class=class="str">"cmt">// loop through the array for ( class="type">int i=class="num">0;i<ArraySize(III); i++ ) { class=class="str">"cmt">// check the symbol to assign a magic number if ( III[i] == InstrumentSymbol ) { magicbuild=MagicHelp+(i+class="num">1)*class="num">10000; break; class=class="str">"cmt">// stop the loop } } class=class="str">"cmt">// add identifier for time frame magicbuild+=InstrumentTimeframe; class="kw">return magicbuild; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| unique shift for difference of EA | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define MagicHelp class="num">0 class=class="str">"cmt">//bot magic shift [class="num">0...class="num">9999]
「把多EA仓位折算到M1尺度再配手数」
做组合EA的人迟早会撞上同一个墙:H1和M5的柱形幅度差好几倍,按EA数量平分手数只会让大周期偷偷吃掉净值波动。作者给出的解法是——所有工具-周期的仓位贡献,必须按M1柱的平均净值增减来拉平,而不是简单按个数或存款比例切。 核心是两个自己定义的旋钮:DeltaBarPercent 是 DepositDeltaEquity 的百分比,后者指单个虚拟EA在「一根M1柱从顶到底或底到顶」时,净值允许增减的存款份额。也就是说,你先拍一个存款额,再告诉系统一根M1柱最坏波动要动用它百分之几,手数就从这倒推。 折算靠一组量:Mb是工作图平均柱宽(点),Mb1是重算成M1的柱宽,Kb是二者比率,T是周期缩到分钟的因子。代码里的 PeriodK 就是算 Kb 的——比如 H1 返回 sqrt(1/60)≈0.129,M5 返回 sqrt(1/5)≈0.447,M1 就是 1.0。拿这个系数乘原图柱宽,就能不加载M1数据而近似出M1等价幅度。 下一步算 Basisb:用「1手1点净值变动×M1平均柱宽(点)」得出1手仓在M1柱均幅下的净值摆动。再乘上你的 DeltaBarPercent 和 DepositDeltaEquity 得到 BasisI,令 Basisb×Lot = BasisI,反解出来的 Lot 就是该EA应开的手数。效果上类似给每个EA单独定固定手数,但免了手动挨个调。 外汇与贵金属杠杆高,这种跨周期均衡只解决「贡献度对等」,不消除单边市爆仓可能,上实盘前请在MT5策略测试器用虚拟存款跑通再挂真钱。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| timeframe to average movement adjustment coefficient | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double PeriodK(ENUM_TIMEFRAMES tf) { class="type">class="kw">double ktemp; class="kw">switch(tf) { case PERIOD_H1: ktemp = MathSqrt(class="num">1.0/class="num">60.0); break; case PERIOD_H4: ktemp = MathSqrt(class="num">1.0/class="num">240.0); break; case PERIOD_M1: ktemp = class="num">1.0; break; case PERIOD_M5: ktemp = MathSqrt(class="num">1.0/class="num">5.0); break; case PERIOD_M15: ktemp = MathSqrt(class="num">1.0/class="num">15.0); break; case PERIOD_M30: ktemp = MathSqrt(class="num">1.0/class="num">30.0); break; class="kw">default: ktemp = class="num">0; } class="kw">return ktemp; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| average candle size in points for M1 for the current chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CalculateAverageBarPoints(Chart &Ch) { class="type">class="kw">double SummPointsSize=class="num">0.0; class="type">class="kw">double MaxPointSize=class="num">0.0; for (class="type">int j = class="num">0; j < ArraySize(Ch.HighI); j++) { if (Ch.HighI[j]-Ch.LowI[j] > MaxPointSize) MaxPointSize= Ch.HighI[j]-Ch.LowI[j]; } for (class="type">int j = class="num">0; j < ArraySize(Ch.HighI); j++) { if (Ch.HighI[j]-Ch.LowI[j] > class="num">0) SummPointsSize+=(Ch.HighI[j]-Ch.LowI[j]);
用平均波幅反推平衡仓位
这段代码把「图表平均蜡烛点值」和「账户风险额度」接起来,直接算出一张图表该挂多大手数。核心在 OptimalLot():先拿 DeltaBarPercent(默认百分比)乘上 DepositDeltaEquityE 得到单笔可亏的基准金额 BasisDX0,再除以每点价值 DY0,DY0 由 CalculateAverageBarPoints() 乘 SYMBOL_TRADE_TICK_VALUE 得到。 CalculateAverageBarPoints() 返回的已经是「按分钟折算后的平均蜡烛大小」——它用 PeriodK() 把不同周期波幅压缩到同一时间尺度,再除以总 K 线数除 ChartPoint。这样跨周期比波幅才不虚。 实际在 MT5 里跑,把 DeltaBarPercent 设 1.0、DepositDeltaEquityE 填净值,就能看到黄金 M5 和 EURUSD H1 算出的手数差可能差出 3~5 倍。外汇和贵金属杠杆高,这个手数只是数学平衡值,遇滑点和跳空仍可能超预期亏损,须自行加缓冲。
else SummPointsSize+=MaxPointSize; } SummPointsSize=(SummPointsSize/ArraySize(Ch.HighI))/Ch.ChartPoint; class="kw">return PeriodK(Ch.Timeframe)*SummPointsSize;class=class="str">"cmt">//class="kw">return the average size of candles reduced to a minute using the PeriodK() adjustment function } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| calculate the optimal balanced lot for the selected chart | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OptimalLot(Chart &Ch) { class="type">class="kw">double BasisDX0 = (DeltaBarPercent/class="num">100.0) * DepositDeltaEquityE; class="type">class="kw">double DY0=CalculateAverageBarPoints(Ch)*SymbolInfoDouble(Ch.CurrentSymbol,SYMBOL_TRADE_TICK_VALUE); class="kw">return BasisDX0/DY0; }
◍ 按存款比例重算虚拟EA手数
多机器人框架里,每个虚拟 EA 原本按固定 DepositDeltaEquity 跑标准化手数 Lot,完全不理当前真实存款。开启自动手数后,得把真实存款摊到单个 bot 上再反推手数。 核心关系是 AutoLot = Lot * (DepositPerOneBot / DepositDeltaEquity),而 DepositPerOneBot = Deposit / BotQuantity。也就是说,总存款除以虚拟 EA 数量得到单 bot 可控存款份额,再跟原标准化基准做比,缩放出手数,风险才跟真实账户规模对齐。 这套逻辑在 MT5 里直接挂上就能验:初始信号能盈利时,自动手数下的权益曲线倾向平滑且带指数坡度。策略盈利因子越高、测试区间成交越密,曲线越平、指数感越强——这是多样化摊薄方差的结果。 顺带看存款负载的平滑度:曲线均匀说明手数归一化和后续缩放没过载。外汇与贵金属杠杆高,实盘前务必用策略测试器跑一遍不同 BotQuantity 下的回撤分布。
「用定时器拉取远程信号配置」
EA 里这套远程同步是可选的,直接从模板里剪掉也不影响主逻辑,但用它做多信号切换很省事。核心靠定时器触发:每过 5 分钟再带一点随机抖动去服务器拉一次文件,避免所有实例整齐划一地打请求。 同步的第一步是访问 API 拿到指定目录(代码里叫 SignalDirectoryE)下的文件清单,随后逐个下载。目录名即信号名,想加几个信号就建几个文件夹,随时在 MT5 里换目录就行。EA 侧实际只依赖两个 API 函数:取文件列表、取文件内容;上传和清理放在你自己的后台程序里做更合理。 URL 里两个字段最关键:filelist 是取列表的接口名(可改),Password_key 是防外人闯入的访问密钥头。服务器回的是 JSON,没有通用解析代码,得按你自己的结构写;作者偏好手写解析而不是引库。小文件逐字符串写本地完全够用,实测没感觉到延迟。 下面这段是定时器与下载主逻辑的骨架。DownloadTimer 用 TimeCurrent 减上次时间判断是否过了 5*60 秒再加 0~60 秒随机量;DownloadFiles 先 GetFiles 拿数组,返回 200 才遍历,每个文件再 GetFileContent 拿到内容数组后 WriteData 落盘。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| used to read the settings every class="num">5 minutes + | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DownloadTimer() { class=class="str">"cmt">// Check if the passed time from the last download time is more than class="num">5 minutes if ( TimeCurrent() - LastDownloadTime > class="num">5*class="num">60 + class="type">int((class="type">class="kw">double(MathRand())/class="num">32767.0) * class="num">60) ) { class=class="str">"cmt">// Set the last download time to the current time LastDownloadTime=TimeCurrent(); class=class="str">"cmt">// Download files again DownloadFiles(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| used to download control files if they isn&class="macro">#x27;t present | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void DownloadFiles() { class="type">class="kw">string Files[]; class=class="str">"cmt">// Initialize the response code by getting files from the signal directory class="type">int res_code=GetFiles(SignalDirectoryE,Files); class=class="str">"cmt">// Check if the list of files is successfully got if (res_code == class="num">200) { class=class="str">"cmt">// Proceed if there is at least one file in the server if (ArraySize(Files) > class="num">0) { class=class="str">"cmt">// Download each file individually for (class="type">int i = class="num">0; i < ArraySize(Files); i++) { class="type">class="kw">string FileContent[]; class=class="str">"cmt">// Get the content of the file class="type">int resfile = GetFileContent(SignalDirectoryE,Files[i],FileContent); class=class="str">"cmt">// Check if the file content is successfully got if (resfile == class="num">200) { class=class="str">"cmt">// Write the file content in our local file WriteData(FileContent,Files[i]); } } } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| getting the list of files into an array | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int GetFiles(class="type">class="kw">string directory,class="type">class="kw">string &fileArray[]) { class=class="str">"cmt">//class="type">class="kw">string for getting a list of files in the form of JSON via GET to API
用 WebRequest 拉取远端文件清单与内容
在 MT5 里做 AIGC 信号中转,第一步往往是把云端的策略文件或模型参数同步到本地终端。下面这段实现用 WebRequest 向自有 API 发 GET 请求,拿到 JSON 后再拆成文件列表与内容行,timeout 写死 1500 毫秒,意味着弱网环境下请求可能在 1.5 秒后被掐断返回 400。 请求头里塞了 Password_key 做鉴权,urlList 由 ApiDomen 拼出 /filelist/ 或 /file_content/ 路径。extractFiles 与 extractContent 在示例里直接 return false,属于留给你接 JSON 解析的空壳,真要跑通得自己补反序列化逻辑。 写文件时按 SubfolderE 决定落盘子目录,bCommonReadE 为 false 才进本地沙盒目录。外汇与贵金属行情高波动,这类自动拉取若误写路径可能覆盖历史回测文件,实盘前务必在策略测试器里先验证目录权限。
class="type">class="kw">string urlList = ApiDomen+"/filelist/"+directory;class=class="str">"cmt">//URL class="type">char message[];class=class="str">"cmt">//Body of the request class="type">class="kw">string headers = "Password_key: " + key;class=class="str">"cmt">// We form the headers of the request class="type">class="kw">string resultheaders = "";class=class="str">"cmt">//returning headers class="type">class="kw">string cookie = "";class=class="str">"cmt">//cookies class="type">int timeout = class="num">1500;class=class="str">"cmt">//waiting for a response when requesting a file or json class="type">char result[]; class=class="str">"cmt">// We send a GET request to the server to receive JSON with a list of files class="type">int res_code = WebRequest("GET", urlList, headers, timeout, message, result, resultheaders); class="type">bool rez = extractFiles(CharArrayToString(result),fileArray); if (rez) class="kw">return res_code; else class="kw">return class="num">400; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| get file list from JSON class="type">class="kw">string | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool extractFiles(class="type">class="kw">string json, class="type">class="kw">string &Files[]) { class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| getting the file content | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int GetFileContent(class="type">class="kw">string directory,class="type">class="kw">string filename,class="type">class="kw">string &OutContent[]) { class=class="str">"cmt">//class="type">class="kw">string for getting a file content in the form of JSON via GET to API class="type">class="kw">string urlList = ApiDomen+"/file_content/"+directory+"/"+filename;class=class="str">"cmt">// class="type">char message[];class=class="str">"cmt">// Body of the request class="type">class="kw">string headers = "Password_key: " + key;class=class="str">"cmt">// We form the headers of the request class="type">class="kw">string resultheaders = "";class=class="str">"cmt">//returning headers class="type">class="kw">string cookie = "";class=class="str">"cmt">//cookies class="type">int timeout = class="num">1500;class=class="str">"cmt">//waiting for a response when requesting a file or json class="type">char result[]; class=class="str">"cmt">// We send a GET request to the server to receive JSON with a file content class="type">int res_code = WebRequest("GET", urlList, headers, timeout, message, result, resultheaders); class="type">bool rez = extractContent(CharArrayToString(result),OutContent); if (rez) class="kw">return res_code; else class="kw">return class="num">400; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| read the contents of the file from JSON each line separately | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool extractContent(class="type">class="kw">string json, class="type">class="kw">string &FileLines[]) { class="kw">return false; } class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//| fill the file with its lines, which are all contained in data | class=class="str">"cmt">//| with a new line separator, and save in the corresponding directory | class=class="str">"cmt">//+-----------------------------------------------------------------------+ class="type">void WriteData(class="type">class="kw">string &data[],class="type">class="kw">string FileName) { class="type">int fileHandle; class="type">class="kw">string tempsubfolder= SubfolderE == "" ? "" : SubfolderE + "\\"; if (!bCommonReadE) {
◍ 把数组落盘到本地或公共目录的两种开法
| 在 MT5 里写文件,第一步是用 FileOpen 拿到句柄,但本地目录和公共目录(terminal_data\MQL5\Files 之外可被多终端共享的位置)的旗标不一样。上面这段逻辑先判断走哪个分支:普通本地写用 FILE_REWRITE | FILE_WRITE | FILE_TXT | FILE_ANSI,若需要跨终端可见则追加 FILE_COMMON。 |
|---|
拿到句柄后别急着写,先用 FileSeek(fileHandle,0,SEEK_SET) 把指针拨回文件头,否则 REWRITE 模式下也可能出现错位。随后用 for 循环把 data[] 逐行写进去,每行末尾补 "\r ",最后必须 FileClose 释放句柄,否则句柄泄漏会让后续开盘会话打不开同名文件。 验证方法很简单:在 EA 里塞一段字符串数组,跑完去对应的 Files 子目录看文本,本地写默认落在 MQL5\Files\tempsubfolder,加 FILE_COMMON 的会进公共区。外汇与贵金属品种上做这类日志,注意点差跳空时段写盘延迟可能偏高,属正常风险。
fileHandle = FileOpen(tempsubfolder+FileName,FILE_REWRITE|FILE_WRITE|FILE_TXT|FILE_ANSI); } else { fileHandle = FileOpen(tempsubfolder+FileName,FILE_REWRITE|FILE_WRITE|FILE_TXT|FILE_ANSI|FILE_COMMON); } if(fileHandle != INVALID_HANDLE) { FileSeek(fileHandle,class="num">0,SEEK_SET); for (class="type">int i=class="num">0; i < ArraySize(data) ; i++) { FileWriteString(fileHandle,data[i]+"\r\n"); } FileClose(fileHandle); }
「在 BotInstance 里挂自己的开棒逻辑」
MQL5 原生没有 OnBar 这类处理函数,模板里用每根虚拟 EA 新柱触发来模拟,效果接近 OnTick 但只在柱闭合后跑一次,对柱级交易影响可忽略。建议把自定义逻辑和变量直接写在 BotInstance 类的 Trade 方法旁,别动主结构,避免和回测框架其他部分搅在一起。 下面这段是模板里的主交易函数骨架,零号位是当前未闭杆,一号位是已闭上一杆。注意 CloseI[0] 这类字段在实盘里会随报价跳动,但我们的逻辑只取已成型杆,所以用的是下标 1。 代码里的示例逻辑很直白:上一根收高于开,就平多并开空;收低于开,就平空并开多。这属于裸 K 反转套路,在欧元兑美元 15 分钟这类高噪品种上假突破概率偏高,贵金属 XAUUSD 同样受美联储数据窗干扰,高风险,只能当接线样例,不要直接跑实盘。 想验证的话,把这段塞进 MT5 的 EA 模板,把 chartindex 指向你的观察图,先开可视化回测看每根开平信号是否如预期触发。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| the main trading function of individual robot instance | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void BotInstance::Trade() { class=class="str">"cmt">//data access class=class="str">"cmt">//Charts[chartindex].CloseI[class="num">0]//current bar(zero bar is current like in mql4) class=class="str">"cmt">//Charts[chartindex].OpenI[class="num">0] class=class="str">"cmt">//Charts[chartindex].HighI[class="num">0] class=class="str">"cmt">//Charts[chartindex].LowI[class="num">0] class=class="str">"cmt">//Charts[chartindex]. ??? class=class="str">"cmt">//close & open class=class="str">"cmt">//CloseBuyF(); class=class="str">"cmt">//CloseSellF(); class=class="str">"cmt">//BuyF(); class=class="str">"cmt">//SellF(); class=class="str">"cmt">// Here we can include operations such as closing the buying position, closing selling position and opening new positions. class=class="str">"cmt">// Other information from the chart can be used for making our buying/selling decisions. class=class="str">"cmt">// Here is a simple trading logic example if ( Charts[chartindex].CloseI[class="num">1] > Charts[chartindex].OpenI[class="num">1] ) { CloseBuyF(); SellF(); } if ( Charts[chartindex].CloseI[class="num">1] < Charts[chartindex].OpenI[class="num">1] ) { CloseSellF(); BuyF(); } }
界面里那三个隐藏字段才是关键
模板自带的图形界面在 MT4 和 MT5 下共用同一套配色与布局,比早先版本观感更干净。界面里的问号是预留位,删块或加数据直接改对应位置即可,不用动框架。 对外暴露的只有两个轻量方法:CreateSimpleInterface 负责建界面,UpdateStatus 负责刷状态,按函数名在源码里就能定位,不用额外演示。 真正值得盯的是我后加的三个字段。看界面最后三个字符串,第一个是“保留的幻数走廊”,它跟着你当前加载的设置文件走——删掉或新增配置文件,走廊就相应收窄或放宽,用来隔离不同 EA 的幻数冲突。剩下两个字段分别显示最近一次读取设置的时刻、以及最近一次和 API 同步的时刻(若发生过同步)。 开 MT5 把模板跑起来,对照界面末三行就能验证走廊宽度随 set 文件数量变化的逻辑,外汇与贵金属交易本身高风险,参数隔离只是工程层面的防护。
◍ 把模板往跨货币优化器推
这一版模板已经能撑住扩展和修改,但代码本身离干净还差得远,优化和修洞是持续活。即便如此,下一步落点已经清楚:把头寸虚拟化,再据此做一套跨货币优化器。 跨货币优化器的核心动作是同时优化所有虚拟工具-周期,把各路设置合并后,利润曲线可能更平、回撤倾向更低,但外汇与贵金属本身高风险,合并并不等于风险消失。 自动多样化和利润提升应排在最前面。理想状态是在任意策略上套一层基础外骨骼,既榨出更多潜在收益,又给终端用户留足可调空间,而不是把策略锁死。
「作者资源与延伸阅读入口」
这套动态模板的示例包以 DynamicTemplate.zip 形式提供,体积 45.56 KB,解压后可直接在 MT5 加载查看基于模板的免费产品骨架。原文作者 Evgeniy Ilin 在平台累计发布 40596 项内容,其店铺汇集了从 ChatGPT 接入到多机器人调度的多篇 MQL5 技术文,可作为后续深挖的索引。 文末列出的经纪商链接(RoboForex、ByBit、Tickmill 等)属于作者个人推荐,并非策略组成部分。外汇与贵金属杠杆交易高风险,接入任何第三方 broker 前应先在模拟账户验证出入金与滑点表现。 若想复刻文中的模板思路,建议从「暴力方式搜素形态」系列第五、六部分入手,配合 MQL5 向导的 GAN/MLP 学习率文章交叉阅读,比单看示例包更容易摸清参数边界。