从用户面板"动态"更改"EA 交易"参数·进阶篇
EA 生命周期里的事件钩子与参数读取
MT5 的 Expert Advisor 靠几个系统回调把节奏切分开:报价来了跑 OnTick,定时器触发跑 OnTimer,从图表移除时跑 OnDeinit。把交易逻辑塞进 NewBar 判断之后,只在每根 K 线第一跳执行 TradingBlock,能避免同根 K 线内重复下单。 OnTimer 里只做两件轻量事——SetParameters 刷参数、SetInfoPanel 刷面板,不和报价事件抢计算资源。回测环境里 NotTest 返回假,这部分会被跳过,实盘才走完整定时器。 OnDeinit 遇到 REASON_REMOVE 会清掉 EA 自建对象、杀定时器、释放指标句柄 hdlSI;外汇与贵金属杠杆高,误留对象可能干扰后续手动下单,卸载脚本时建议确认图表已干净。 参数读取有个双模开关:flgRead 为 false 时从文件读入并缓存进 arrParamIP 数组,面板数值一变就清零重读。Period_Ind 若 ≤0 会被强行置为 10,Lot ≤0 则落回 0.1,这些是防御性兜底,不是最优仓位建议。
class=class="str">"cmt">//| CURRENT SYMBOL TICKS | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">// If the bar is not new, exit if(!NewBar()) { class="kw">return; } else { TradingBlock(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TIMER | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTimer() { SetParameters(); SetInfoPanel(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EXPERT ADVISOR DEINITIALIZATION | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">// Get the deinitialization reason code if(NotTest()) { { Print(getUnitReasonText(reason)); } class=class="str">"cmt">//--- class=class="str">"cmt">// When deleting from the chart if(reason==REASON_REMOVE) { class=class="str">"cmt">// Delete all objects created by the Expert Advisor DeleteAllExpertObjects(); class=class="str">"cmt">//--- if(NotTest()) { EventKillTimer(); } class=class="str">"cmt">// Stop the timer IndicatorRelease(hdlSI); class=class="str">"cmt">// Delete the indicator handle } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SETTING PARAMETERS IN TWO MODES | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// If this variable is set to false, the parameters in the file are read from the array class=class="str">"cmt">// where they are saved for quick access after they have been read for the first time. class=class="str">"cmt">// The variable is zeroed out upon the change in the value on the panel. class="type">bool flgRead=false; class="type">class="kw">double arrParamIP[]; class=class="str">"cmt">// Array where the parameters from the file are saved class=class="str">"cmt">//--- class="type">void SetParameters() { class=class="str">"cmt">// If currently in the tester or class=class="str">"cmt">// in real time but with the "On The Fly" Setting mode disabled if(!NotTest() || (NotTest() && !SettingOnTheFly)) { class=class="str">"cmt">// Zero out the variable and parameter array flgRead=false; ArrayResize(arrParamIP,class="num">0); class=class="str">"cmt">//--- class=class="str">"cmt">// Check the Indicator Period for correctness if(Period_Ind<=class="num">0) { lcheck=class="num">10; } else { lcheck=Period_Ind; } gPeriod_Ind=(class="type">int)lcheck; class=class="str">"cmt">//--- gStopLoss=StopLoss; gTakeProfit=TakeProfit; gReverse=Reverse; class=class="str">"cmt">//--- class=class="str">"cmt">// Check the Lot for correctness if(Lot<=class="num">0) { lcheck=class="num">0.1; }
「随盘改参的配置文件读写逻辑」
在「On The Fly」模式下,EA 不靠输入框固化参数,而是把仓位、止损、指标周期等写进一个 ini 文件,运行时再读回来。这样你不用重编译就能在 MT5 文件目录里手动改 ParametersOnTheFly.ini 调仓。
当 CheckCreateGetPath() 返回有效路径,程序调用 WriteReadParameters() 去打开同名 ini。文件句柄若不是 INVALID_HANDLE,说明盘上已有参数记录,直接灌进 arrParamIP 数组;若打开失败,则走 iZeroMemory() 把变量清零,避免脏值进场。
读参时有两道硬校验:指标周期 arrParamIP[0] 小于等于 0 会被强制改成 10,手数 arrParamIP[4] 小于等于 0 则兜底成 0.1。外汇与贵金属杠杆高,这类默认值能防止零周期算指标或裸 0 手下单把账户瞬间打爆。
数组长度必须严格等于 szArrIP 才赋值给全局变量 gPeriod_Ind、gTakeProfit、gStopLoss、gReverse、gLot。长度对不上就静默跳过,意味着你手改 ini 少写一行,EA 可能倾向沿用旧内存值而非报错,排查时得盯紧数组尺寸。
else { lcheck=Lot; } gLot=lcheck; } else class=class="str">"cmt">// If "On The Fly" Setting mode is enabled { class=class="str">"cmt">// Check whether there is a file to write/read parameters to/from the file class="type">class="kw">string lpath=""; class=class="str">"cmt">//--- class=class="str">"cmt">// If the folder exists if((lpath=CheckCreateGetPath())!="") { class=class="str">"cmt">// Write or read the file WriteReadParameters(lpath); } } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| WRITE DATA TO FILE | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void WriteReadParameters(class="type">class="kw">string pth) { class="type">class="kw">string nm_fl=pth+"ParametersOnTheFly.ini"; class=class="str">"cmt">// File name and path class=class="str">"cmt">//--- class=class="str">"cmt">// Get the file handle to read the file class="type">int hFl=FileOpen(nm_fl,FILE_READ|FILE_ANSI); class=class="str">"cmt">//--- if(hFl!=INVALID_HANDLE) class=class="str">"cmt">// If the handle has been obtained, the file exists { class=class="str">"cmt">// Get parameters from the file if(!flgRead) { class=class="str">"cmt">// Set the array size ArrayResize(arrParamIP,szArrIP); class=class="str">"cmt">//--- class=class="str">"cmt">// Fill the array with values from the file flgRead=GetValuesParamsFromFile(hFl,arrParamIP); } class=class="str">"cmt">//--- class=class="str">"cmt">// If the array size is correct,... if(ArraySize(arrParamIP)==szArrIP) { class=class="str">"cmt">// ...set the parameters to the variables class=class="str">"cmt">//--- class=class="str">"cmt">// Check the Indicator Period for correctness if((class="type">int)arrParamIP[class="num">0]<=class="num">0) { lcheck=class="num">10; } else { lcheck=(class="type">int)arrParamIP[class="num">0]; } gPeriod_Ind=(class="type">int)lcheck; class=class="str">"cmt">//--- gTakeProfit=arrParamIP[class="num">1]; gStopLoss=arrParamIP[class="num">2]; gReverse=arrParamIP[class="num">3]; class=class="str">"cmt">//--- class=class="str">"cmt">// Check the Lot for correctness if(arrParamIP[class="num">4]<=class="num">0) { lcheck=class="num">0.1; } else { lcheck=arrParamIP[class="num">4]; } gLot=lcheck; } } else class=class="str">"cmt">// If the file does not exist { iZeroMemory(); class=class="str">"cmt">// Zero out variables class=class="str">"cmt">//--- class=class="str">"cmt">// When creating the file, write current parameters of the Expert Advisor
◍ 把参数落盘成可读的 CSV
EA 初始化时把内部参数导出成文件,核心就是拿到一个写句柄然后逐行写。下面这段代码用 FileOpen 以写模式打开一个 ANSI 编码的 CSV,分隔符设为等号,遍历两个数组把参数名和值成对写进去。 写文件前必须判断句柄不等于 INVALID_HANDLE,否则磁盘权限或路径问题会让你以为写成功了实际啥也没落。循环里 FileWrite 每调一次写一行,arr_nmparams 存名字、arr_vparams 存数值,两者长度由 szArrIP 控制。 文件句柄用完后 FileClose 必须配对出现,外层 hFl 和里层 hFl2 各关各的;漏关在 MT5 里会悄悄占着句柄,多跑几次就可能报句柄数超限。成功后会 Print 一条带 EA 名的提示,开 MT5 跑一下能在数据文件夹里直接翻到这个等号分隔的文本。
class=class="str">"cmt">//--- class=class="str">"cmt">// Get the file handle to write to the file class="type">int hFl2=FileOpen(nm_fl,FILE_WRITE|FILE_CSV|FILE_ANSI,""); class=class="str">"cmt">//--- if(hFl2!=INVALID_HANDLE) class=class="str">"cmt">// If the handle has been obtained { class="type">class="kw">string sep="="; class=class="str">"cmt">//--- class=class="str">"cmt">// Parameter names and values are obtained from arrays in the ARRAYS.mqh file for(class="type">int i=class="num">0; i<szArrIP; i++) { FileWrite(hFl2,arr_nmparams[i],sep,arr_vparams[i]); } class=class="str">"cmt">//--- FileClose(hFl2); class=class="str">"cmt">// Close the file class=class="str">"cmt">//--- Print("File with parameters of the "+NAME_EXPERT+" Expert Advisor created successfully."); } } class=class="str">"cmt">//--- FileClose(hFl); class=class="str">"cmt">// Close the file }
面板点击与输入框的事件路由
在 MT5 自定义面板里,和用户交互主要吃两类事件:CHARTEVENT_OBJECT_CLICK(左键点图形对象)和 CHARTEVENT_OBJECT_EDIT(Edit 对象结束文本编辑)。实时模式下必须先用 NotTest() && SettingOnTheFly 拦一道,否则回测环境里点面板只会空转。 主入口 OnChartEvent 只做分发:命中点击就丢给 ChartEvent_ObjectClick,命中编辑结束就丢给 ChartEvent_ObjectEndEdit,两个函数返回 true 就直接 return,不再往下走。这样把交互逻辑拆干净,后面加按钮也不用动主干。 代码里全局区先声明了对话窗口要用的状态量:currVal 存面板当前值或输入框内容,flgDialogWin 标记窗口是否已存在,number 初始为 -1 表示还没点中列表里的具体参数。颜色常量如 clrFonMsg=C'15,15,15' 是直接写 RGB 的暗灰底,和面板风格统一。 想验证这套路由,开 MT5 挂上 EA,把 SettingOnTheFly 设为 true,点信息面板列表项应弹出对话框;在输入框改完按回车,应触发 OBJECT_EDIT 并写回文件。外汇和贵金属波动剧烈,面板改参数只是辅助,实盘仍需自行承担高风险。
class=class="str">"cmt">// Current value on the panel or class=class="str">"cmt">// entered in the input box class="type">class="kw">string currVal=""; class="type">bool flgDialogWin=false; class=class="str">"cmt">// Flag for panel existence class="type">int szArrList=class="num">0,class=class="str">"cmt">// Size of the option list array number=-class="num">1; class=class="str">"cmt">// Parameter number in the panel list class="type">class="kw">string nmMsgBx="", class=class="str">"cmt">// Name of the dialog window nmValObj=""; class=class="str">"cmt">// Name of the selected object class=class="str">"cmt">//--- class=class="str">"cmt">// Option list arrays in the dialog window class="type">class="kw">string lenum[],lenmObj[]; class=class="str">"cmt">//--- class=class="str">"cmt">// colors of the dialog window elements class="type">color clrBrdBtn=clrWhite, clrBrdFonMsg=clrDimGray,clrFonMsg=C&class="macro">#x27;class="num">15,class="num">15,class="num">15&class="macro">#x27;, clrChoice=clrWhiteSmoke,clrHdrBtn=clrBlack, clrFonHdrBtn=clrGainsboro,clrFonStr=C&class="macro">#x27;class="num">22,class="num">39,class="num">38&class="macro">#x27;; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| USER EVENTS | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { class=class="str">"cmt">// If the event is real time and the "On The Fly" setting mode is enabled if(NotTest() && SettingOnTheFly) { class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| THE CHARTEVENT_OBJECT_CLICK EVENT | class=class="str">"cmt">//+------------------------------------------------------------------+ if(ChartEvent_ObjectClick(id,lparam,dparam,sparam)) { class="kw">return; } class=class="str">"cmt">//--- class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| THE CHARTEVENT_OBJECT_ENDEDIT EVENT | class=class="str">"cmt">//+------------------------------------------------------------------+ if(ChartEvent_ObjectEndEdit(id,lparam,dparam,sparam)) { class="kw">return; } } class=class="str">"cmt">//--- class="kw">return; } class=class="str">"cmt">//+------------------------------------------------------------------+
「点一下图表对象就弹参数对话框」
在 MT5 的 EA 或指标里,想让用户点一下面板上的参数名就弹出编辑框,核心是拦截 CHARTEVENT_OBJECT_CLICK 事件(id 固定为 1)。下面这段处理函数演示了从点击事件到打开信息面板的完整链路,复制进你的工程就能跑。 事件进来先调 Get_STV() 抓当前品种数据,再用 sparam 拿到被点对象的名字。随后用 ChartGetInteger 取图表像素宽高:CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS 两个属性,返回值转 int 后存进 width_chart / height_chart,对话框定位就靠这两个数。 DialogWindowInfoPanel 函数判断 flgDialogWin 标志:没开窗口时,先 GetNumberClickedObjIP 找被点对象在数组里的序号,返回 -1 说明点到了空白区直接 return;过了这关再 InitArraysAndDefault 初始化数组,然后 SetDialogWindow 画窗体,把标志置 true 并 ChartRedraw 强制重绘。 窗口已开的状态下再点击,走 SetEditObjInDialogWindow 把对应编辑框激活;若 sparam 是 btnApply 或 btnCancel,就进按钮分支。btnApply 被触发时会比对面板值和列表值,不一致才写文件更新——避免无谓的 IO。外汇与贵金属波动剧烈,这类交互改的是风控参数,实盘前务必在策略测试器里点一遍验证逻辑。
class="type">bool ChartEvent_ObjectClick(class="type">int id,class="type">long lparam,class="type">class="kw">double dparam,class="type">class="kw">string sparam) { class=class="str">"cmt">// If there was an event of clicking on a graphical object if(id==CHARTEVENT_OBJECT_CLICK) class=class="str">"cmt">// id==class="num">1 { Get_STV(); class=class="str">"cmt">// Get all data on the symbol class=class="str">"cmt">//--- class="type">class="kw">string clickedChartObject=sparam; class=class="str">"cmt">// Name of the clicked object class=class="str">"cmt">//--- class=class="str">"cmt">// Get the chart size width_chart=(class="type">int)ChartGetInteger(class="num">0,CHART_WIDTH_IN_PIXELS,class="num">0); height_chart=(class="type">int)ChartGetInteger(class="num">0,CHART_HEIGHT_IN_PIXELS,class="num">0); class=class="str">"cmt">//--- DialogWindowInfoPanel(clickedChartObject); } class=class="str">"cmt">//--- class="kw">return(false); } class="type">void DialogWindowInfoPanel(class="type">class="kw">string clickObj) { class=class="str">"cmt">// If there is currently no dialog window if(!flgDialogWin) { class=class="str">"cmt">// Get the object number in the array class=class="str">"cmt">// Exit if none of the parameters displayed on the panel has been clicked if((number=GetNumberClickedObjIP(clickObj))==-class="num">1) { class="kw">return; } class=class="str">"cmt">//--- class=class="str">"cmt">// Initialization of class="kw">default values class=class="str">"cmt">//and determination of the list array size if(!InitArraysAndDefault()) { class="kw">return; } class=class="str">"cmt">//--- class=class="str">"cmt">// Set the dialog window SetDialogWindow(); class=class="str">"cmt">//--- flgDialogWin=true; class=class="str">"cmt">// Mark the dialog window as open ChartRedraw(); } else class=class="str">"cmt">// If the dialog window is open { class=class="str">"cmt">// Set the input box for the modification of the value SetEditObjInDialogWindow(clickObj); class=class="str">"cmt">//--- class=class="str">"cmt">// If one of the buttons in the dialog box is clicked if(clickObj=="btnApply" || clickObj=="btnCancel") { class=class="str">"cmt">// If the Apply button is clicked if(clickObj=="btnApply") { class=class="str">"cmt">// Compare values on the panel with the ones on the list class=class="str">"cmt">// If the value on the list is different from the one that is currently displayed on the panel class=class="str">"cmt">// (which means it is different from the one in the file), class=class="str">"cmt">// ...change the value on the panel and update the file