以横盘和趋势行情为例强化策略测试器的指标优化

Carl Schreiber | 6 九月, 2016


问题

有太多的参数可以优化。

带有多个指标的智能交易程序需要花费大量时间来组合参数进行测试。在我们开始优化智能交易程序之前能否降低组合的数量?换言之, 在编写交易 EA 之前, 我们可以编写伪 EA. 只针对行情查询特殊问题。我们将一个大问题细分为一个个小问题, 并分别解决他们。这个伪 EA 不可进行交易!作为例子, 我们选择了 ADX. 并检查此指标是否能够区分横盘行情和趋势行情, 可能的话, 我们也许能够获得额外的信息。

想象一款短线交易 EA, 依据行情是否横盘来进行 '振荡-交易' (回至移动均线) 或应用趋势策略进行顺交易 (沿着移动均线交易)。为了区分, 我们的交易 EA 应在较高的时间帧使用 (唯一) ADX - 此处是 1 小时柱线。除了 ADX, 交易 EA 也许还有 5 个指标 (用于短线交易管理)。每个指标大概有 4 个设置参数, 并且由于它们的步长较小所以它们当中的每一个都有 2000 个不同的数值。这将总计产生 2000*5*4 = 40,000。让我们现在来加入 ADX。对于 ADX 的每个参数组合, 理论上, 我们要进行额外的 40,000 次计算。

在此例中我们设置 ADX 的周期 (PER), 它的价格 (PRC) 和极限 (LIM), 因此我们定义趋势开始为 ADX (MODE_MAIN) 上穿 LIM, 而横盘行情为下穿 LIM。对于周期 PER 我们可以尝试 2,..,90 (步长 1=> 89 不同的数值), 对于价格我们可以选择 0,..,6 (=收盘价,.., 加权, 步长 1=> 7), 而对于 LIM 我们尝试 4,..,90 (步长 1=> 87)。总计我们要进行 89*7*87 = 54,201 次组合测试。这里是策略测试器的设置:

图例. 01 启动测试器-设置参数

图例. 02 启动测试器-设置 EA 选项

图例. 03 启动测试器-设置选项

图例. 04 启动测试器-设置优化

如果您要重复优化, 不要忘记删除在 \tester\cache\ 里的缓存文件。否则您将发现在策略测试器里的优化结果和优化图形并未保存在 csv-文件, 因为在此情况下 OnTester() 根本就未执行。

当然, 一般人会通过策略测试器使用这个参数范围来优化, 但首先我们应查找无意义的结果, 看看我们能否检测并排除它们, 其次是有无教育理由来扩展范围。由于事实上我们的伪 EA 不会交易 (无需使用每笔分时!) 且我们只有一个指标和 54,201 次组合来测试, 我们可以关闭遗传模式并让测试器计算所有组合。

如果我们不做这个预-Adx-优化 或者我们不能降低 Adx 参数组合的数量, 我们将不得不把交易 EA 其它变量的 40,000 次组合乘以 ADX 的 54,201 次组合, 然后我们将得到 2,168,040,000 次组合来优化 - 工作量很大, 以至于我们不得不使用遗传优化。

最后我们可以大幅降低 Adx 的参数范围 - 这很好, 符合预期!我们将会更好地理解 ADX, 因为我们能够看到 ADX 确实有能力在横盘和趋势行情之间进行鉴别 - 即使判断横盘行情略滞后于趋势行情 (改进的余地?)!进而, 我们会有一些想法, 根据所发现的横盘和趋势行情的范围来确定交易 EA 的止损和目标。测试的 ADX 周期 PER 从: 11..20 (步长 1=> 10), PRC: 0,6 (步长 6=>2) 以及 LIM: 17,..,23 (步长 1=> 7) - 总计 140 组合。这意味着, 我们仅用 4,680,000 次组合即可替换 2,168,040,000 词组和来测试交易 EA, 即在非遗传模式, 速度快了 ~460 倍, 或比遗传模式快了 ~460 倍。在遗传模式, 测试仅需 ~10.000 遍, 但现在交易 EA 的其它参数的更多数值也被测试!

备注, 如果您使用遗传算法: 其结果很大程度上要依据可用组合总数和实际执行遍数的关系。在优化期间您遇到的最坏结果, 比较小的是来自下一组选择设置的优良结果数量。


理念

我们建立的伪 EA 不进行交易。它只实现了三个重要功能。OnTick(), 在此我们查验指标并判断行情状态, OnTester() 在此我们输出最终结果到我们的 csv 文件, 还有 calcOptVal() 在此我们计算 OptVal 值, 它将由 OnTester() 返回到策略测试器用于排序和遗传算法。函数 OnTester() 将会在一次优化过程的结尾调用, 返回一个特殊值, 且它在 csv 文件里添加一个新行用来在整个优化完成后进行分析。


伪 EA, 第一种方法


现在我们需要确定准则用来计算返回值: OptVal。我们选择横盘和趋势行情的范围, 即实际行情中最高价的最高值与最低价的最低值之间的差价, 以及 "TrndRange" 除以 "FlatRange" 以便优化可以将其最大化:
double   TrndRangHL,       // 趋势行情的 (最高价最高值 - 最低价的最低值) 之和
         TrndNum,          // 趋势行情的编号
         FlatRangHL,       // 横盘行情的 (最高价最高值 - 最低价的最低值) 之和
         FlatNum,          // 横盘行情的编号
         RangesRaw,        // 趋势行情范围除以横盘行情的范围 (越大越好)
         // ...            参阅以下

double calcOptVal() // 第一种方法!!
   {
      FlatRange    = FlatRangHL / FlatNum;
      TrndRange    = TrndRangHL / TrndNum;
      RangesRaw    = FlatRange>0 ? TrndRange/FlatRange : 0.0; 
      return(RangesRaw);
   }
...
double OnTester() 
   {
      OptVal = calcOptVal();
      return( OptVal );
   }

如果采用上述设置运行一次优化, 且 OptVal = RangesRaw, 在优化图形里的结果如下所示:

图例. 05 原始测试图形

如果在优化结果里按照 "OnTester 结果" 从上至下排序, 我们看到的最佳结果:

图例. 06 测试器的原始最佳值

可笑的高关联!如果我们查看 csv 文件, 我们会看到横盘行情的平均长度为 1 根柱线, 而开关数量 (横盘行情编号 + 趋势行情编号) 对于实际应用也太小了。(奇怪的数字 PRC=1994719249 而非 0,..,6 应该不会干扰我们, 因为 Adx 的价格正确数字被写在 csv 文件里!)。

这个不令人满意的结果意味着, 我们必须多补充一些准则, 以排除那些可笑的情况。


伪 EA, 改进

首先, 我们简单地添加一个行情横盘或趋势的最小长度或最小柱线:

      FlatBarsAvg  = FlatBars/FlatNum; // 所有 '横盘柱线' 之和 / 横盘行情编号
      TrndBarsAvg  = TrndBars/TrndNum; // 所有 '趋势柱线' 之和 / 趋势行情编号
      BrRaw        = fmin(FlatBarsAvg,TrndBarsAvg);

其次, 我们指定横盘和趋势之间的指定最小开关值:

      SwitchesRaw  = TrndNum+FlatNum; // 趋势和横盘编号

现在我们面对下一个问题!RangesRaw 范围从 0 至 100,000.0, BrRaw 从 0 至 0.5, 而 SwitchesRaw 从 0 至 ~8000 (=Bars()) - 从理论上讲, 如果我们在每根新柱线上都有一个开关。

我们需要均衡我们的三个准则!对于所有这些, 我们使用相同的所需功能: 反正切 - 或 mq4 里的 - atan(..)!除了诸如 sqrt() 或 log(), 我们使用 0 或负值没有任何问题。atan() 根本不会超出极限, 例如 RangesRaw, atan(100,000) 和 atan(20) 之间的差值几乎变为 0, 且它们的权重及或相等, 以至于其它因素的结果得到更大的影响力。甚至 atan() 提供了直到极限的平滑增长, 当一个硬性极限就好像如果 (x>limit) 加权的所有数值同样大于 limit , 则将再次查找靠近我们的极限的最佳数值, 但它不是我们期望的。您将稍后看到!

让我们来看看 atan() 是如何工作的 (对于 atan()-图形我使用 这个):

图例. 07 Atan 函数

蓝色版本是 (仅是) 限制在 +1 和 -1 之间 (除以 pi/2)。
红线 (及其函数) 展示我们如何移动 x-轴的截距从 x=0 到 x=4。
绿线展示我们如何改变陡峭度。我们控制 atan() 接近极限的速度, 和差值逐渐变小的速度。

此处我们不需要的是改变我们的 atan() 逼近的极限。但您应知道, 假如您改变第一个 1*atan(..) 为 2*atan(..), 则极限随之移动到 +2 和 -2。

我们不需要的是通过设置 1*atan() 为 -1*atan() 来切换上边界和下边界极限。现在我们的函数 x 值越大越逼近 -1。

现在我们的伪 EA 已经有了全部内容。让我们开始把功能集成在一起。


伪 EA, 最终版本

我们的伪 EA 不会进行交易!它只在新柱线 形成时调用 iADX(..)。这意味着我们不需要 "每笔分时" 或 "控制点数"!我们可以使用最快的模型 "仅用开盘价", 通过 ADX 的前两根柱线来为我们计算行情状态:

extern int                 PER   = 22;             // Adx 周期
extern ENUM_APPLIED_PRICE  PRC   = PRICE_TYPICAL;  // Adx 价格
extern double              LIM   = 14.0;           // Adx 的主线极限
extern string            fName   = "";             // 在 \tester\files 的文件名, "" => 无 csv 文件!


//+------------------------------------------------------------------+
//| 全局变量定义                                                      |
//+------------------------------------------------------------------+
double   OptVal,           // 此数值由 OnTester() 返回, 且其值可在策略测试器的 OnTerster() 栏里找到
         TrndHi,           // 实际趋势行情的最高价最高值
         TrndLo,           // 实际趋势行情的最低价最低值
         TrndBeg,          // 趋势行情的起始价格
         TrndRangHL,       // 趋势行情的 (最高价最高值 - 最低价的最低值) 之和
         TrndRangCl,       // 最后的收盘价 - 趋势行情的第一个收盘价 (预留但未用)
         TrndNum,          // 趋势行情编号
         TrndBars=0.0,     // 趋势行情柱线数量
         TrndBarsAvg=0.0,  // 趋势行情的平均柱线
         FlatBarsAvg=0.0,  // 横盘行情的平均柱线
         FlatHi,           // 实际横盘行情的最高价最高值
         FlatLo,           // 实际横盘行情的最低价最低值
         FlatBeg,          // 横盘行情的起始价格
         FlatRangHL,       // 横盘行情的 (最高价最高值 - 最低价的最低值) 之和
         FlatRangCl,       // 最后的收盘价 - 横盘行情的第一个收盘价 (预留但未用)
         FlatNum,          // 横盘行情的编号
         FlatBars=0.0,     // 横盘行情柱线数量
         FlatRange,        // tmp FlatRangHL / FlatNum
         TrndRange,        // tmp TrndRangHL / TrndNum
         SwitchesRaw,      // 开关编号
         SwitchesAtan,     // 开关编号的正切
         BrRaw,            // 横盘或趋势行情的最小小时数 (越多越好)
         BrAtan,           // BrRaw 的正切
         RangesRaw,        // 趋势行情范围除以横盘行情的范围 (越大越好)
         RangesAtan;       // (TrndRange/FlatRange) 的正切

enum __Mkt // 行情的 3 种状态
 {
   UNDEF,  
   FLAT,
   TREND
 };
__Mkt MARKET = UNDEF;      // 行情起始状态。
string iName;              // 指标名称
double main1,main2;        // Adx 主线数值


//+------------------------------------------------------------------+
//| 计算指标, 判断行情状态                                             |
//+------------------------------------------------------------------+
void OnTick() 
 {
 //---
   static datetime tNewBar=0;
   if ( tNewBar < Time[0] ) 
    {
      tNewBar = Time[0];
      main1 = iADX(_Symbol,_Period,PER,PRC,  MODE_MAIN, 1); // ADX
      main2 = iADX(_Symbol,_Period,PER,PRC,  MODE_MAIN, 2); // ADX)
      iName = "ADX";

      // 设置变量。已定义行情近似状态
      if ( MARKET == UNDEF ) 
       { 
         if      ( main1 < LIM ) main2 = LIM+10.0*_Point; // 行情变为横盘
         else if ( main1 > LIM ) main2 = LIM-10.0*_Point; // 行情变为趋势
         FlatHi  = High[0];
         FlatLo  = Low[0];
         FlatBeg = Close[2];//
         TrndHi  = High[0];
         TrndLo  = Low[0];
         TrndBeg = Close[2];//
       }
      
      // 我们要在横盘行情入场吗?
      if ( MARKET != FLAT && main2>LIM && main1<LIM)  // ADX
       {
         //终结趋势行情
         TrndRangCl += fabs(Close[2] - TrndBeg)/_Point;
         TrndRangHL += fabs(TrndHi - TrndLo)/_Point;
                  
         // 更新相关值
         OptVal = calcOptVal();

         //设置新的横盘行情
         MARKET  = FLAT;
         FlatHi  = High[0];
         FlatLo  = Low[0];
         FlatBeg = Close[1];//
         ++FlatNum;
         if ( IsVisualMode() )
          {
            if (!drawArrow("横盘 "+TimeToStr(Time[0]), Time[0], Open[0]-(High[1]-Low[1]), 243, clrDarkBlue) ) // 39:蜡烛行情休眠
               Print("出错 drawError ",__LINE__," ",_LastError);
          }
       } 
      else if ( MARKET == TREND )   // 更新当前趋势行情
       {
         TrndHi = fmax(TrndHi,High[0]); 
         TrndLo = fmin(TrndLo,Low[0]); 
         TrndBars++;
       }
      
      // 我们要在趋势行情入场吗?
      if ( MARKET != TREND && main2<LIM && main1>LIM) 
       { 
         // 终结横盘行情
         FlatRangCl += fabs(Close[2] - FlatBeg)/_Point;
         FlatRangHL += fabs(FlatHi - FlatLo)/_Point;
         
         // 更新相关值
         OptVal = calcOptVal();

         // 设置新的趋势行情
         MARKET  = TREND;
         TrndHi  = High[0];
         TrndLo  = Low[0];
         TrndBeg = Close[1];//
         ++TrndNum;
         TrndBars++;
         if ( IsVisualMode() )
          {
            if(!drawArrow("趋势 "+TimeToStr(Time[0]), Time[0], Open[0]-(High[1]-Low[1]), 244, clrRed)) // 119:kl 钻石
               Print("出错 drawError ",__LINE__," ",_LastError);
          }
       } 
      else if ( MARKET == FLAT  ) // 更新当前横盘行情
       {
         FlatHi = fmax(FlatHi,High[0]);
         FlatLo = fmin(FlatLo,Low[0]); 
         FlatBars++; 
       }
      
    }
   if ( IsVisualMode() )  // 在可视模式显示实际情形
    {
      string lne = StringFormat("%s  PER: %i    PRC: %s    LIM: %.2f\n行情  #   BarsAvg  RangeAvg"+
                                "\n横盘:    %03.f    %06.2f         %.1f\n趋势: %03.f    %06.2f         %.1f   =>  %.2f",
                                 iName,PER,EnumToString(PRC),LIM,FlatNum,FlatBarsAvg,FlatRange,
                                 TrndNum,TrndBarsAvg,TrndRange,(FlatRange>Point?TrndRange/FlatRange:0.0)
      );
      Comment(TimeToString(tNewBar),"  ",EnumToString(MARKET),"  Adx: ",DoubleToString(main1,3),
              "  Adx-Lim:",DoubleToString(main1-LIM,3),"\n",lne);
    }
 }

如果 ADX 与 LIM 交叉, 我们终结以前的行情状态, 并为新状态做准备。伪 EA 计算其所有的报价点数差。

现在, 让我们来看看为此我们要达到并确定哪些需要。我们需要一个编号用于 OnTester() 返回。策略测试器的优化器计算越大越好。对于我们的需要, OnTester() (OptVal) 的返回值应该因此增加, 由此在横盘和趋势行情之间进行识别会更好!

我们已经确定三个变量来计算 OptVal。其中两个我们很容易可以设置一个合理的最低值:

  1. RangesRaw = TrndRage/FlatRange 应大于 1!趋势行情应比横盘行情的范围更高。TrndRage 和 FlatRange 定义为实际行情的最高价的最高值 - 最低价的最低值。让我们来设置 x-轴截距为 x=1。
  2. BrRaw 应大于 3 根柱线 (= 3 小时)。BrRaw = fmin(FlatBarsAvg,TrndBarsAvg)。FlatBarsAvg 和 TrndBarsAvg 是每类行情的平均柱线数量。我们需要它在边界处防止 a.m. 值。让我们来设置它的 x-轴截距为 x=3。
  3. SwitchesRaw。我们将要优化 8000 多条。例中结果只有 20 个开关 (10 个横盘行情和 10 个趋势行情) 这没有任何意义。它意味着每类行情平均为 400 个小时或 16 天?

问题在于找到一个不错的 SwitchesRaw 极限, 因为它十分依赖于所处时间帧和柱线总数。除了 1) 和 2), 考虑到合理性, 我们能够设置极限, 我们要看查第一个结果 (附加 csv 文件中的一栏:Opti ADX ALL) 以便得到极限:

图例. 08 Opti ADX ALL 开关图形

替代处理 ~2500 不同的开关, 我们只使用 sqrt(2500) = 50 级别, 这可以更好地进行处理。对于每个级别, 我们计算其平均值和并绘图。我们看到, 在 172 是局部最小值。让我们使用 100 来看看我们的伪 EA 如何在此边界进行处理。我们使用一个较小的系数 0.01 来保证自这个极限缓慢增加, 从 100。然后, 我们正成使用一个较高的极限, 也许是 200 - 但只是出于教育原因 ...

为了获得其它系数, 我们来看看函数的绘图仪。我们调整它以便曲线不那么平坦, 我们假定此处是有趣的结果。蓝色是函数 SwitchesRaw):

图例. 09 开关的正切 (蓝色)

现在让我们来看看我们的其它两个评估函数。
红色是函数 BrRaw: 接受最小持续时间为 3 根柱线的任何行情, 且系数为 0.5, 保证即使 8 根柱线 (小时) 将会产生不同。
绿色是 RangesRaw: 此处接受最小为 1, 并且由于我们不能奢望奇迹, 超过 8 可能不是严谨的结果。

图例. 10 柱线正切 (红) 范围 (绿) 和开关 (蓝)

现在我们可以构建函数来计算 OptVal , 它将由 OnTester() 返回。

  1. 由于这适用于所有的三个变量, 我们可以将它们相乘。
  2. 我们有三个变量, 且它们的 atan(..) 可以为负值, 所以我们要评估: fmax(0.0,atan(..))。否则, 两个 atan() 函数的负值结果将会产生错误的正值结果 OptVal
//+------------------------------------------------------------------+
//| calcOptVal 计算返回到策略测试器的 OptVal                           |
//| 及其评估系数                                                      |
//+------------------------------------------------------------------+
// 系数。用于 SwitchesAtan, 开关数量:
double SwHigh = 1.0, SwCoeff=0.01, SwMin = 100;
// 系数。用于 BrAtan, 柱线的数量:
double BrHigh = 1.0, BrCoeff=0.5,  BrMin = 3.0;
// 系数。用于 RangesAtan, TrendRange/FlatRange:
double RgHigh = 1.0, RgCoeff=0.7,  RgMin = 1.0;

double calcOptVal() {
   if ( FlatNum*TrndNum>0 ) {
      SwitchesRaw  = TrndNum+FlatNum;
      SwitchesAtan = SwHigh*atan( SwCoeff*(SwitchesRaw-SwMin))/M_PI_2;

      FlatBarsAvg  = FlatBars/FlatNum;
      TrndBarsAvg  = TrndBars/TrndNum;
      BrRaw        = fmin(FlatBarsAvg,TrndBarsAvg);
      BrAtan       = BrHigh*atan( BrCoeff*(BrRaw-BrMin))/M_PI_2;

      FlatRange    = FlatRangHL / FlatNum;
      TrndRange    = TrndRangHL / TrndNum;
      RangesRaw    = FlatRange>0 ? TrndRange/FlatRange : 0.0; 
      RangesAtan   = FlatRange>0 ? RgHigh*atan( RgCoeff*(RangesRaw-RgMin))/M_PI_2 : 0.0;
      return(fmax(0.0,SwitchesAtan) * fmax(0.0,BrAtan) * fmax(0.0,RangesAtan));  
   }
   return(0.0);
}



伪 EA 的其它部分是 OnInit(), 它在 csv 文件里写入列标头:

//+------------------------------------------------------------------+
//| 智能程序初始化函数                                                |
//+------------------------------------------------------------------+
int OnInit() 
  {
//---
   // 在 Calc.-Sheet 里写入标头行
   if ( StringLen(fName)>0 ) {
      if ( StringFind(fName,".csv", StringLen(fName)-5) < 0 ) fName = fName+".csv";    //  检查文件名
      if ( !FileIsExist(fName) ) {                                                     // 在新文件里写入列标头
         int fH = FileOpen(fName,FILE_WRITE);
         if ( fH == INVALID_HANDLE ) Print("错误 打开 ",fName,": ",_LastError); 
         string hdr = StringFormat("Name;OptVal;RangesRaw;PER;PRC;LIM;FlatNum;FlatBars;FlatBarsAvg;FlatRgHL;FlatRgCls;FlatRange;"+
                      "TrendNum;TrendBars;TrendBarsAvg;TrendRgHL;TrendRgCl;TrendRange;"+
                      "SwitchesRaw;SwitchesAtan;BrRaw;BrAtan;RangesRaw;RangesAtan;FlatHoursAvg;TrendHoursAvg;Bars;"+
                      "Switches: %.1f %.1f %.f, Hours: %.1f %.1f %.1f, Range: %.1f %.1f %.1f\n",
                      SwHigh,SwCoeff,SwMin,BrHigh,BrCoeff,BrMin,RgHigh,RgCoeff,RgMin);
         FileWriteString(fH, hdr, StringLen(hdr));
         FileClose(fH);
      }   
   }
//---
   return(INIT_SUCCEEDED);
  }

OnTester() 终结打开的行情状态并在 csv 文件末尾写入优化结果:

double OnTester() 
 {
   // 检查极限: 至少一个开关
   if ( FlatNum*TrndNum<=1 ) return(0.0);  // 一方是 0 => 跳过无意义的结果
   
   // 现在终结最后的行情: 横盘
   if ( MARKET == FLAT ) 
    {
      TrndRangCl += fabs(Close[2] - TrndBeg)/_Point;
      TrndRangHL += fabs(TrndHi - TrndLo)/_Point;

      // 更新相关值
      OptVal = calcOptVal();

    } 
   else if ( MARKET == TREND ) // .. 以及趋势
    {
      FlatRangCl += fabs(Close[2] - FlatBeg)/_Point;
      FlatRangHL += fabs(FlatHi - FlatLo)/_Point;

      // 更新 OptVal
      OptVal = calcOptVal();
    }
   
   // 数值写入 csv 文件
   if ( StringLen(fName)>0 ) 
    {
      string row = StringFormat("%s;%.5f;%.3f;%i;%i;%.2f;%.0f;%.0f;%.1f;%.0f;%.0f;%.2f;%.2f;%.0f;%.0f;%.1f;%.0f;%.0f;%.2f;%.2f;%.0f;%.5f;%.6f;%.5f;%.6f;%.5f;%.2f;%.2f;%.0f\n",
                  iName,OptVal,RangesRaw,PER,PRC,LIM,
                  FlatNum,FlatBars,FlatBarsAvg,FlatRangHL,FlatRangCl,FlatRange,
                  TrndNum,TrndBars,TrndBarsAvg,TrndRangHL,TrndRangCl,TrndRange,
                  SwitchesRaw,SwitchesAtan,BrRaw,BrAtan,RangesRaw,RangesAtan,
                  FlatBarsAvg*_Period/60.0,TrndBarsAvg*_Period/60.0,
                  (FlatBars+TrndBars)
             );
             
      int fH = FileOpen(fName,FILE_READ|FILE_WRITE);
      if ( fH == INVALID_HANDLE ) Print("错误 打开 ",fName,": ",_LastError);
      FileSeek(fH,0,SEEK_END); 
      FileWriteString(fH, row, StringLen(row) );
      FileClose(fH);
    }
   // 返回 0.0 替代负数值!在我们的情况里, 它们扰乱了优化图形。
   return( fmax(0.0,OptVal) );
 }


现在我们的伪 EA 已经就绪, 并且我们准备策略测试器来优化:

  1. 我们禁用 "遗传算法" 来测试每个组合。
  2. 设置 "优化参数" 为自定义。这会为我们在优化图形里显示更多有趣的图片。
  3. 确认在 ..\tester\caches 里的缓存文件已被删除
  4. 对于 csv 文件, 确认 fName 参数不为空, 且 \tester\files 里的旧文件已被删除
  5. 如果您留有同名 csv 文件, 则优化器将会逐行添加新结果, 其文件大小膨胀直到令您遇到麻烦!
  6. 我们选择品种 EURUSD。
  7. 周期设为 H1 (此处是从 2015 年08 月13 日至 2015 年 11 月 20 日)。
  8. 模型设为 "仅用开盘价"。
  9. 不要忘记启用 "优化"。

在我的笔记本电脑上经过 25 分钟, 策略测试器完成了自 2007 年以来的优化, 我们在 ..\tester\files\ 里找到了结果 csv 文件。

在优化图形里我们可以看到例子 (底部 =LIM, 右侧=PER):

图例. 11 测试器图形 SwLim 100

这看上去比我们的初始优化更好一点。我们看到一片高密集度的清晰区域 34>PER>10 和 25>LIM>13, 它比 2,..,90 和 4,..,90 更好一点!

让我们来看看开关差异极小近似相同 (=稳定结果) 的结果是否有所不同:

SwMin = 50:

图例. 11 测试器图形 SwLim 050

SwMin = 150

图例. 13 测试器图形 SwLim 150

SwMin = 200:

图例. 14 测试器图形 SwLim 200

对于所有的优化都适用这些极限: 34>PER>10 和 25>LIM>13 这对于稳健性来说是个好兆头!

记住:
  • 我们不得不使用 atan(..) 函数, 令 OptVal 与我们的三个变量同样敏感。
  • 函数 atan 及其不同系数的用法或多或少很随意!我会一直尝试直到我得到一些令人满意的结果。这可能是一个更好的解决方案, 您自行尝试一下吧!
  • 您可能会认为在我得到想要的结果之前我会修改 - 像在适应 EA。是的, 这就是为什么我们要仔细检查结果!
  • 这款伪 EA 并不意味着找到了最好的单一解决方案, 但对于每个参数可以找到合理的较小极限!最后的成功还是要由交易 EA 决定!


分析在 Excel 中的结果, 合理性检查

在优化期间, 每通过一轮就在 csv 文件里添加新的一行, 相比策略测试器提供的信息更丰富, 跳过我们不需要的类别, 譬如 Profit, Trades, Profit Factor, ... 我们在 Excel (我是用 LibreOffice) 里加载这个文件。

我们不得不先排序, 首先, 根据我们的 OptVal,其次, 根据 RangesRaw, 之后我们得到这个 (栏: "Optimizing ADX SwLim 100 raw"):

图例. 15 优化 ADX SwLim 100 原始

我们注意根据 OptVal 得到的 50 个 '最佳'。不同参数 PER, PRC LIM 为了便于检测均已着色。

  1. RangesRaw 从 2.9 到 4.5 变化。这意味着趋势行情比横盘行情范围大 3 至 4.5 倍。
  2. 横盘行情持续 6 到 9 根柱线 (小时)。
  3. 横盘行情的范围变化从 357 到 220 点 - 对于交易有足够的范围空间。
  4. 趋势持续在 30 和 53 小时之间。
  5. 趋势行情方位从 1,250 到 882 点。
  6. 如果您不仅关注前 50 名, 而是前 200 名,那么范围几乎相同 RangesRaw: 2.5 到 5.4, 横盘行情 221 到 372, 趋势范围: 1,276 到 783。
  7. 前 200 名的 PER : 14 到 20, 以及 LIM: 14 到 20, 但是我们一定要看细节!
  8. 如果我们查看当 OptVal 变为 0.0 时的这个部分, 我们看到 RangesRaw 的数值十分高, 但其它数值告诉我们它们不太适合交易 (栏: "skipped OptVal=0"):

图例. 16 跳过的 OptVal 0

RangesRaw 高的离谱, 但 FlatBarsAvg 实际上对于交易又太短, 且/或 TrndBarsAvg 也太高, 超过了 1000 小时。

现在我们检查 OptVal>0 时的 RangeRaw 部分, 根据 RangesRaw (栏: "OptVal>0 sort RangesRaw") 排序:

图例. 17 OptVal gt 0 sort RangesRaw

RangesRaw 的 50 个最高值范围从 20 到 11。但只查看 TrendBarsAvg: 平均在 100 附近, 这就超过了 4 天。

总之我们可以说 OptVal 已经很好地将所有难以交易的 ADX 结果贬值。在另一方面, 前 200 (5.4) 个最高的 RangesRaw 或前 500 (7.1) 个看上去十分有前途。



参数检查

因此, 经过必要的合理性检查之后我们看到我们的 ADX 参数 PER PRC 还有它的 LIM 极限。

由于行数太多 (=29,106) 我们仅需要 OptVal 大于 0 的那些行。在原始表格里即是前 4085 行 (如果根据 OptVal 排序!)。我们将之拷贝到新的一栏。我们在 PER 旁边添加三列并根据图片加入这些。所有公式您可以查看附加文件。

在 D,E,F 列的第 5 行输入: AVERAGE(D$2:D5), STDEV(D$2:D5), SKEW(D$2:D5)。第二行的单元仅显示最后一行的数值, 即整个 RangesRaw 列的统计结果。为什么?因为表格是从最佳到最坏排序的, 我们将在 n 行看到平均值, 标准背离和 n 个最佳的非对称。n 个最佳值与所有结果的比较可以告诉我们, 何处我们可能找到我们所要查找的 (栏: "OptVal>0 Check PER, PRC, LIM"):

图例. 18 OptVal gt 0 Check PER, PRC, LIM

我们从中能领会什么?在第二行 (在 last) 之下我们看到所有 PER 的均值 (Avg) 是 33.55, 标准背离 (StdDev) 21.60。如果 PER 的分布是根据 高斯分布, 我们发现所有 PER 数值的 68% 处于均值 +/- StdDev 以及 95% +/-2*StdDev 范围。此处它在 33.55 - 21.60 之间 = 11.95 和 33.55 + 21.60 = 55,15。现在我们查看最佳结果的那些行。均值在第 5 行从 19 开始, 缓慢增长到 20。StdDev 的变化从 2.0 到 2.6。现在 68% 覆盖 18 到 23。最后, 我们看看  偏斜。 它在第 2 行, 所有 PER 的值为 0.61。这意味着左侧 (较小) 比右侧具有更多数值, 即使它仍然是一个高斯分布。如果非对称超过 +/- 1.96 我们不能推断一个高斯分布, 且我们只得十分小心地使用均值和标准背离。因为一侧 '超重' 而另一侧或多或少 '空置'。非对称大于 0 意味着右侧 (>均值) 比之右边价值更低。所以 PER 是高斯分布, 我们可以使用均值和 StdDev。如果我们比较靠顶部结果的发展 (根据 OptVal) 我们看到均值从 19 缓慢增加到 20 (行 487!)。而 StdDev, 同时从 ~2.0 增长到 5.36 (行 487)。如果我们跳过前 10 个结果, 它们主要是正值, 非对称从不会超过 0.4, 这意味着我们应该在均值的 '左侧' 添加一个 (或两个) 数值。

PRC 的结果需要不同的处理!与 PER LIM 不同, PRC 的数值定义了一个定类尺度, 在它们之间的任何计算都是毫无意义的。所以我们只是算了算它们出现的次数, 以及计算每个 PRC 的 RangesRaw 均值 0,..,6。请记住, 哪怕集合很可笑, 我们也要检查。通常情况下, 我们不会使用 PRC=开盘价 (1), PRC=最高价 (2) 或 PRC=最低价 (3)。但是我们必须认识到, 开盘价是前 50 名中最常见的值。这最有可能, 是由于事实上我们只适用全部柱线, ADX 使用柱线的最高价和最低价, 所以最高价和最低价还有收盘价是 '已知开盘价' - 一种不道德的优势, 就因为 ADX 使用它们。最高价和最低价的成功?难以解释!事实上 EURUSD 价格在 2014 年 8 月从 1.33 下跌, 直到 2015 年 11 月份的 1.08, 这也许可以解释为最低价的成功, 而非最高价。也许这是强行情动态的结果。无论如何, 我们将之淡化。如果我们比较 PER = 收盘价, 典型价, 中间价和权重价, 当查看 Q, R, 和 S 列时它们没有多大不同。前 100 名 PRC=典型价(4) 将是最好的选择, 比 PRC=最高价(2) 甚至更佳。但在前 500 名中 PRC=收盘价 则为最佳。

对于 LIM 我们使用如同 PER 的相同公式。有趣的是注意到 'last' 非对称 (所有) 远高于 +1.96, 但不是前 100 个 (=0.38) 或者前 500 个 (=0.46)。因此, 让我们只使用最好的 500 个。前 500 个的均值是 16.65 且 StdDev 3.03。当然, 这个 LIM 极大依赖于 PER: PER 越小 LIM 越大, 反之亦然。这就是为什么 LIM 的范围与 PER 的范围相对应。

所以我们选择前 500 个最佳结果的三个变量范围 PER, PRC, 和 LIM :

  • PER Avg=20.18 +/- StdDev=5.51 Skew=0.35 (-2) => (20.18-5.41-2=) 14,..,(20.18+5.52=) 26 (步长 1 => 13)。
  • PRC 根据 500 行 我们能够决定只用收盘价 (步长 0 => 1)。
  • LIM Avg=16.64 +/- StdDev=3.03 Skew=0.46 (-2) => (16.64-3.03-2=) 12,..,(16.64+3.03=) 20 (步长 1 => 9)

总计我们现在只有 13*1*9 = 117 个组合用于交易 EA 的优化。

我们可以仔细查看结果 (它的工作表的栏名称: "OPT Top 500 Best PER's Average"):

图例. 19 前 500 最佳 PER 的均值

我们看到 PER=18 频率最高, 而 PER=22 的均值最高。两者都在我们的选择以及它的 LIM 覆盖范围。


可视模式

让我们来最后检查前 500 个最佳均值的 PER : PER=22。取消 PRC=开盘价,最低价,最高价, 我们发现这个设置与 38 行的范围 4.48 相关, 参照前一幅表格图片的黄色背景。

我们使用此设置在可视模式下运行伪 EA, ADX 也应用相同的设置。

(只有) 在可视模式, 我们的伪 EA 在检测到横盘行情时会在下一根柱线位置显示一个蓝色左右箭头, 而在趋势行情时则为一个红色上箭头 (此处从: 2015 年 07 月 30 日 05:00 至 2015 年 08 月 04 日 12:00):

图例. 20 可视模式 Per 22

我们可以清楚地看到 ADX 的两个问题, 这也许能鼓励您改进这个思路!

  1. ADX 有些滞后, 尤其在横盘行情中巨幅走势穿越 ADX 的判断上。它需要相当长时间才会再次 "冷静下来"。如果是在 2015 年 08 月 03 日 00:00 左右而非 2015 年 08 月 03 日 09:00 检测到横盘行情就更好了。
  2. 如果 ADX 接近 LIM 我们认为是一种洗盘。例如, 如果我们未能在 2015 年 08 月 03 日 14:00 检测到趋势行情就更好了。
  3. 如果柱线的高-低-范围越变越小, 甚至有一对 '小' 柱线在相同方向上, 则被识别为新的趋势。若是将 2015 年 08 月 03 日 20.00 的信号推迟到 2015 年 08 月 04 日 07:00 再判断为新趋势就更好了。
  4. 伪 EA 不区分升势或跌势。它在于您可以使用 ADX 的 DI+ 和 DI-, 或是其它指标。
  5. 也许趋势行情的平均长度 (46.76) 达到几乎 4 天(!) 有些太久了。在此情况下, SwMin 越高 (而非 100) 或 SwCoeff 越小 (而非 0.01), 或是两者都有, 将带给您更符合您理想的结果。
有五个清晰的出发点, 可令您发现或编写您自己的指标, 或设置指标更好地检测。您可以使用 ADX 作为参考。如果您清楚 您的 横盘和趋势行情的定义, 可以轻易地修改附带的伪 EA !



结论

使用 ADX 的交易 EA 即使只是但指标也将要测试 54,201 个组合进行优化 - 希望 ADX 能做到我们要求它做的!如果交易 EA 不能如我们期望的那样成功, 那么定位问题加以改进将是艰难的。测试 ADX 的所有 54,201 个组合需要几分钟, 优化之后我们发现:

  1. ADX 有能力在横盘和趋势行情之间进行区分
  2. 我们能够把 54,201 降低到 117 (= 13 (PER) * 1 (PRC) * 9 (LIM))。
  3. 横盘行情的范围在 372 和 220 点之间 (前 100)。
  4. 趋势行情范围在 1,277 和 782 点之间。

所以我们可以把交易 EA 的初始 2,168,040,000 个组合降低到 (117*40,000=) 4,680,000。仅有 0.21%, 且比遗传优化的情况速度加快 99.7%, 结果也更出色, 因为更多的其它非 ADX 参数可以参与测试检查。这些都为我们的伪 EA 减少了设置:

图例. 21 开始测试-降低设置 EA 选项

图例. 22 开始测试-降低设置优化

此外, 我们得到 (当然这依赖交易思路) 一些交易入场、离场准则, 以及设置或移动止损和盈利目标的有用信息。

请在附件里查找伪 EA 和 Excel 文件。我们已经解释了采取的一些步骤, 为什么我们这样做, 以及可能出现的陷阱。所有这一切, 可令您开始尝试在将指标加入到您的交易 EA 之前优化它。如果您开发您自己的方式来判断横盘和趋势行情, 您可以使用它的结果来与 ADX 比较, 看看您的指标组合是否更好!

如果您打算用它来找到更好的指标, 或针对横盘及趋势行情的指标设置, 您更应该调整 calcOptVal() 的系数。譬如您打算使用更长时间的周期, 您至少应增加 SwMin。请牢记, 良好的 OptVal 将令遗传模式为您发现更佳的多重组合设置。但是您也可以使用这个思路只为指标进行全然不同的优化。在这种情况下, 您可能会被迫完全重写 calcOptVal() 函数。

如果您打算使用此 EA 就不要忘记:

  1. 确认 在 ..\tester\caches 里的缓存文件被删除
  2. 如果您需要在 ..\tester\files\ 里的 csv 文件, 在 fName 字段里输入文件名, 并 删除存在的同名 csv 文件
  3. 如果您不需要 csv 文件, 在伪 EA 的设置 fName 字段里留空即可。
  4. 如果您保留同名 csv 文件, 优化器将会追加新行, 其文件大小膨胀直到令您遇到麻烦!
  5. "测试器" 栏设置 "优化参数"  为 "自定义"
  6. 影响 OptVal 的以及遗传算法结果的最简单方式是改变三个系数的极小: SwMin, BrMin, RgMin
  7. 设置 "模型""仅用开盘价" , 它最快。
  8. 如果您使用不同的日期 ("使用日期" : 从..至) 您要正确调整伪 EA 内部的 calcOptVal() 函数系数。
  9. 在优化完成之后, 从 "优化结果"  栏选择设置并在 "可视模式"  里再次开始来看看优化是否满足您的理想。
  10. 蓝色的左右箭头代表横盘行情开始, 红色上下箭头代表趋势行情。
  11. 如果您打算开发更好的替代 ADX 的方案, 您也许无需 csv 文件: 只是优化, 在 "可视模式" 里观察最佳结果, 修改一些, 优化,...
  12. 对于不同于横盘或趋势的行情, 您也许需要使用 csv 文件, 且可能有不同方式计算 OptVal。

但请记住, 根本无法保证快速成功或任何形式的成功。