MT4自定义优化标准实战
用计数器法实现盈利亏损比排序优化
为什么要自定义优化标准
MetaTrader 4 的策略测试器提供了一些基础的优化目标,例如净利润、胜率、夏普比率等。但在实际交易中,不同用户面对的品种、周期和风控逻辑千差万别,官方内置的标准往往无法精确表达交易者真正关心的性能度量。社区中经常有人提出需要扩展优化标准集,但无论开发者预置多少指标,总会有用户需要完全不同的评估方式,例如本文关注的获利与最大亏损之间的关系。
这个问题能否在 MQL4 和 MetaTrader 内部解决?答案是肯定的。通过巧妙利用 Expert Advisor 的可更换参数与测试器的运行特性,我们可以把任意计算逻辑作为优化目标,并在测试结束后自动排序选出最优参数组合。本文以标准移动平均线 EA 为例,演示如何将“获利/亏损关系”作为自定义优化标准。
自定义优化标准的计算逻辑
我们首先需要定义什么是“获利/亏损关系”。文中采用的度量是在测试过程中追踪账户权益峰值与当前权益的最大落差,即最大回撤(MaxDD),以及相对于初始余额的净收益。优化标准 Criterion 被定义为净收益与最大回撤的比值,该值越大代表单位风险下获利能力越强。
为了不依赖 EA 本身的交易逻辑,我们在 start() 函数开头插入追踪代码,在每次价格变动时更新最大权益和最大回撤。同时在 deinit() 中重复处理最后的价格变动,保证测试结束时数据完整。以下是追踪最大权益与回撤的片段:
if (AccountEquity() > MaxEqu) MaxEqu = AccountEquity(); if (MaxEqu-AccountEquity() > MaxDD) MaxDD = MaxEqu-AccountEquity();
在测试结束时,用以下公式计算标准值:
Criterion = (AccountBalance()-StartBalance)/MaxDD;
注意,MaxDD 为 0 时需做保护,实际代码中可加极小值避免除零。该标准独立于 EA 原本的买卖规则,仅作为外部评估使用。
计数器优化法的工作原理
MQL4 没有内建函数可以感知“优化结束”或“当前是第几次优化运行”。作者采用的唯一可行方案是“计数器优化”:将 EA 的唯一可优化参数设为一个外部整型变量 Counter,测试器在 1 到 TestsNumber 之间以步长 1 遍历它。EA 内部根据 Counter 的值反推真实参数(如移动平均周期和偏移)。
这种方法的代价是:你失去了直接用测试器界面勾选多参数组合的能力,必须自行用公式映射;同时测试器的“遗传算法”缓存机制会变成障碍,因为缓存命中的变体不会调用 init/deinit,导致数据缺失。但实践证明该方法稳定可行。
外部变量声明如下,其中 MovingPeriodStepsNumber 与 MovingShiftStepsNumber 的乘积应等于 TestsNumber,以保证全组合覆盖:
extern int Counter = 1; // Counter of the tester's running extern int TestsNumber = 200; // he check digit - total number of runs extern int MovingPeriodStepsNumber = 20; // Number of optimization steps for MovingPeriod extern int MovingShiftStepsNumber = 10; // Number of optimization steps for MovingShift extern double MovingPeriodLow = 150; // Lower limit of the optimization range for MovingPeriod extern double MovingShiftLow = 1; // Lower limit of the optimization range for MovingShift extern double MovingPeriodStep = 1; // Optimization step for MovingPeriod extern double MovingShiftStep = 1; // Optimization step for MovingShift
初始化与参数映射
在 init() 中,仅当处于测试环境且 TestsNumber>0 时才启用自定义逻辑。我们利用全局变量 FilePtr 记录文件写入位置,并根据 Counter 计算本次运行的 MovingPeriod 与 MovingShift。若 TestsNumber=0,EA 退化为普通移动平均线,不影响正常交易使用。
int init() {
if (IsTesting() && TestsNumber > 0) {
if (GlobalVariableCheck("FilePtr")==false || Counter == 1) {
FilePtr = 0;
GlobalVariableSet("FilePtr",0);
} else {
FilePtr = GlobalVariableGet("FilePtr");
}
MovingPeriod = MovingPeriodLow+((Counter-1)/MovingShiftStepsNumber)*MovingPeriodStep;
MovingShift = MovingShiftLow+((Counter-1)%MovingShiftStepsNumber)*MovingShiftStep;
StartBalance = AccountBalance();
MaxEqu = 0;
MaxDD = 0;
}
return(0);
}
这里用整除和取模将一维计数器展开为二维参数网格,是映射的关键。为加速优化,文件指针通过全局变量贯穿各次运行,避免重复读写定位开销。
结果存储与排序实现
deinit() 中区分三种情形:首次运行建文件、中间运行追加、末次运行排序输出。首次运行创建 test.txt 并写入首行,同时保存文件指针:
if (Counter == 1) {
// First run, create/initialize a datafile.
h=FileOpen("test.txt",FILE_CSV|FILE_WRITE,';');
FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter);
// Remember the position of the file pointer after writing in the global variable
FilePtr = FileTell(h);
GlobalVariableSet("FilePtr",FilePtr);
FileClose(h);
后续运行以读写方式打开,跳至全局变量记录的位置追加数据:
} else {
// After the first start is processed, the data are added into the file
h=FileOpen("test.txt",FILE_CSV|FILE_READ|FILE_WRITE,';');
// It is time to use the file pointer written in the global variable
FilePtr = GlobalVariableGet("FilePtr");
FileSeek(h,FilePtr, SEEK_SET);
FileWrite(h,Criterion,MovingPeriod,MovingShift,Counter);
// Remember the file pointer position once again
FilePtr = FileTell(h);
GlobalVariableSet("FilePtr",FilePtr);
当 Counter == TestsNumber 时,读取全部数据到二维数组,按 Criterion 降序排序并覆写文件:
if (Counter == TestsNumber) {
ArrayResize(Data,TestsNumber);
// Returns the file pointer to the beginning
FileSeek(h,0,SEEK_SET);
// Read from the file the results of all testings
int i = 0;
while (i<TestsNumber && FileIsEnding(h)== false) {
for (int j=0;j<4;j++) {
Data[i][j]=FileReadNumber(h);
}
i++;
}
// Sort the array according to our optimization criterion
ArraySort(Data,WHOLE_ARRAY,0,MODE_DESCEND);
// Now let us arrange the results. Reopen the file
FileClose(h);
h=FileOpen("test.txt",FILE_CSV|FILE_WRITE,' ');
FileWrite(h," Criterion"," MovingPeriod"," MovingShift"," Counter");
for (i=0;i<TestsNumber;i++) {
FileWrite(h,DoubleToStr(Data[i][0],10)," ",Data[i][1]," ",Data[i][2]," ",Data[i][3]);
}
GlobalVariableDel("FilePtr");
}
FileClose(h);
}
}
排序后文件首行即为最优参数组合,交易者可直接取用。作者以 EURUSD_H1 从 2004 年中开盘价测试,得到了明确的参数排序结果。
缓存效应与独立计数器
MT4 测试器会对相同参数组合缓存结果。若从缓存读取,init() 与 deinit() 不会执行,导致该变体未被记录,实际运行数少于 TestsNumber,Data 数组出现零值。检测方法是引入独立全局变量 TestsCnt 统计真实运行次数。
在 init 中初始化、deinit 中递增并末次删除:
// Code of the independent counter
if (GlobalVariableCheck("TestsCnt")==false || Counter == 1) {
TestsCnt = 0;
GlobalVariableSet("TestsCnt",0);
} else {
TestsCnt = GlobalVariableGet("TestsCnt");
}
// Code of the independent counter
TestsCnt++;
GlobalVariableSet("TestsCnt",TestsCnt);
// Code of the independent counter
GlobalVariableDel("TestsCnt");
实操步骤与配置要点
编译 EA 后,在测试器选择该 EA,打开属性框确认:1) MovingPeriodStepsNumber 与 MovingShiftStepsNumber 之积等于 TestsNumber;2) 优化仅对 Counter 开启;3) Counter 范围 1 到 TestsNumber、步长 1;4) 禁用遗传算法。启动优化后,到 [MetaTrader]\tester\files 查看 test.txt。
- 设置 TestsNumber=200,周期步数20,偏移步数10
- 仅勾选 Counter 为优化参数,范围1-200步长1
- 遗传算法务必取消勾选
- 优化后打开 tester/files/test.txt 读取排序结果
方法局限与扩展可能
计数器法牺牲了测试器原生多参数界面,且缓存机制需手动规避。但 FilePtr 与独立计数器机制,可用于管理样本外测试或自行实现遗传算法——例如依据历史结果动态筛选下轮 Counter 子集。本文 EA 仅作方法演示,真实交易应结合样本外验证与风控。