MT4自定义优化标准实战
⚙️

MT4自定义优化标准实战

用计数器法实现盈利亏损比排序优化

回测与优化 难度 · 进阶 2026-01-30 6 分钟阅读
#MT4#回测优化#MQL4#自定义标准

为什么要自定义优化标准

MetaTrader 4 的策略测试器提供了一些基础的优化目标,例如净利润、胜率、夏普比率等。但在实际交易中,不同用户面对的品种、周期和风控逻辑千差万别,官方内置的标准往往无法精确表达交易者真正关心的性能度量。社区中经常有人提出需要扩展优化标准集,但无论开发者预置多少指标,总会有用户需要完全不同的评估方式,例如本文关注的获利与最大亏损之间的关系。

这个问题能否在 MQL4 和 MetaTrader 内部解决?答案是肯定的。通过巧妙利用 Expert Advisor 的可更换参数与测试器的运行特性,我们可以把任意计算逻辑作为优化目标,并在测试结束后自动排序选出最优参数组合。本文以标准移动平均线 EA 为例,演示如何将“获利/亏损关系”作为自定义优化标准。

✦ 核心思路
自定义标准的核心是把 EA 的真实参数映射到一个特殊的“计数器”外部变量上,让测试器只优化计数器,再由 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 后,在测试器选择该 EA,打开属性框确认:1) MovingPeriodStepsNumber 与 MovingShiftStepsNumber 之积等于 TestsNumber;2) 优化仅对 Counter 开启;3) Counter 范围 1 到 TestsNumber、步长 1;4) 禁用遗传算法。启动优化后,到 [MetaTrader]\tester\files 查看 test.txt。

方法局限与扩展可能

计数器法牺牲了测试器原生多参数界面,且缓存机制需手动规避。但 FilePtr 与独立计数器机制,可用于管理样本外测试或自行实现遗传算法——例如依据历史结果动态筛选下轮 Counter 子集。本文 EA 仅作方法演示,真实交易应结合样本外验证与风控。

✦ 延伸阅读
原文讨论源自 MQL4 论坛,俄文版见 https://www.mql5.com/ru/articles/1498 ,中文译版 https://www.mql5.com/zh/articles/1498 提供附件 Test_Moving_Average.mq4 可供研究。

常见问题

使用计数器外部变量 Counter 让测试器只优化它,EA 内部根据 Counter 反推真实参数,在 deinit 中计算如 (余额-初始)/最大回撤 的标准并写入文件,末次运行排序输出。
缓存命中不会跑 init/deinit,可用独立全局变量 TestsCnt 检测真实运行数;消除办法是重新编译 EA 或关闭再打开测试器窗口强制清缓存。
因为计数器法依赖每次运行都执行 init/deinit 来记录数据,遗传算法会缓存并跳过部分变体,造成数据阵列零值,所以属性里要取消勾选遗传算法。
声明 extern int Counter=1、TestsNumber=总运行数、周期与偏移的步数及下限步长;保证周期步数×偏移步数=TestsNumber,测试器仅优化 Counter 从1到TestsNumber步长1。